一、前言
本次在硬禾的最后一个项目是使用RP2040实现摄像头采集图像并显示在LCD屏幕上,首先简单介绍一下我的设计方案:
图1 系统设计流程图
根据图1,本次项目大致分为三个步骤来决定系统设计:1.硬件连接,2.软件设置,3.图像采集及显示。
1.硬件连接:
- 连接 OV2640 摄像头到 RP2040 上的合适引脚。摄像头需要使用SCCB协议与微控制器通信。确保正确连接摄像头的电源和地线;
- 连接 ST7735 LCD 到 RP2040 的 SPI 总线,包括 MOSI、MISO、SCK 和 CS 引脚。同时连接 LCD 的电源和地线。
2.软件设置:
- 导入所需的库文件,并且完善摄像头与屏幕的相关驱动问题;
- 设置 I2C 总线和 SPI 总线对象,以便与摄像头和 LCD 进行通信;
- 创建相关函数对象,用于管理和控制 LCD 的显示。
3.图像采集和显示:
- 初始化摄像头,并根据需要设置图像的分辨率、图像格式等参数;
- 将捕获的图像数据转换为适合在 LCD 上显示的格式,并且使用相关函数来存取图像文件。
二、模块介绍
- 树莓派PICO
本次项目用的是树莓派PICO,它功能强大,能够满足我们所做的一切。
图2 树莓派PICO
树莓派PICO具体性能指标:
- 21mm×51 mm尺寸
- RP2040微控制器芯片,由英国的树莓派自己设计
- 双核Arm Cortex-M0+处理器, 灵活的时钟,最高可运行到133 MHz
- 264KB片上SRAM
- 2MB板上的QSPI Flash
- 26多功能GPIO管脚, 包括了3个模拟输入
- 2 × UART, 2 × SPI控制器, 2 × I2C控制器, 16 × PWM通道
- 1 × USB 1.1控制器以及PHY, 支持host和device模式
- 8 × 可编程I/O (PIO)状态机用于定制化外设支持
图3 树莓派PICO引脚图
- OV2640
OV2640 是 OV(OmniVision)公司生产的一颗 1/4 寸的 CMOS UXGA(1632*1232)图像传感器。该传感器体积小、工作电压低,提供单片 UXGA 摄像头和影像处理器的所有功能。通过SCCB总线控制,可以输出整帧、子采样、缩放和取窗口等方式的各种分辨率 8/10 位影像数据。该产品 UXGA 图像最高达到 15 帧/秒(SVGA 可达 30 帧,CIF 可达 60 帧)。用户可以完全控制图像质量、数据格式和传输方式。所有图像处理功能过程包括伽玛曲线、白平衡、对比度、色度等都可以通过SCCB接口编程。
图4 OV2640摄像头
UXGA ,即 分 辨 率 位 1600*1200 的输出格式,类似的还有:SXGA(1280*1024) 、WXGA+(1440*900)、XVGA(1280*960)、WXGA(1280*800)、XGA(1024*768)、SVGA(800*600)、 VGA(640*480)、CIF(352*288)、WQVGA(400*240)、QCIF(176*144)和 QQVGA(160*120)等。PCLK,即像素时钟,一个 PCLK 时钟,输出一个像素(或半个像素)。VSYNC,即帧同步信号。HREF /HSYNC,即行同步信号。通过下面时序图,我们将了解VSYNC与HREF的使用,并且能够分析出数据输出传输原理:
图5 HREF时序图
图像数据在 HREF 为高的时候输出,当 HREF 变高后,每一个 PCLK 时钟,输出一个 8 位/10 位数据。我们采用 8 位接口,所以每个 PCLK 输出 1 个字节,且在 RGB/YUV 输出格式下,每个 tp=2 个 Tpclk,如果是 Raw 格式,则一个 tp=1 个 Tpclk。比如我们采用 UXGA 时序,RGB565 格式输出,每 2 个字节组成一个像素的颜色(高低字节顺序可通过 0XDA 寄存器设置),这样每行输出总共有 1600*2 个 PCLK 周期,输出 1600*2 个字节。
图6 VSYNC时序图
从图中我们可以看出,首先VSYNC发送一个高电平数据表示起始信号,然后在HREF为高电平期间采集有效数据,一共1600条,然后重复1200次,就达到了输出的效果。
根据上面时序图,我们大致了解了OV2640的一个数据传输过程,首先应该初始化摄像头,首先初始化好IO口,然后上电复位,读取ID,执行初始化序列,初始化流程只需要会调用相关库函数就可以完成,大家可以去Github等其他网站查找相关库即可。
接下来就是读取图像数据:
图7 数据读取流程图
OV2640摄像头模块对应管脚:
图8 外部管脚
根据以上管脚图,我们将SCL(SCCB时钟线)与树莓派PICO的GP15相连,SDA(SCCB数据线)与树莓派PICO的GP14相连,并且对这两个管脚加两个上拉电阻。VSYNC与HREF分别与GP18与GP17相连,复位RES端与GP21相连,PCLK与GP19相连,数据线D0-D7与GP2-GP9相连。这样我们就完成了对摄像头管脚的分配。
SCCB协议: SCCB(Serial Camera Control Bus,串行摄像头控制总线)相当于一个简易的I2C协议,是由OV(OmniVision)公司定义和发展的双向三线式同步串行总线,该总线控制着摄像头大部分的功能,包括图像数据格式、分辨率以及图像处理参数等。SCCB协议可以用两线也可以用三线,两线为串行时钟信号总线SIO_C与串行数据信号总线SIO_D,三线增加了一条使能线SCCB_E。当SCCB_E默认拉低时,该协议相当于两线操作,无法通过控制使能端控制选中的从机,因此两线的SCCB总线只能是一个主器件对一个从器件控制,但三线SCCB接口可以对多个从器件控制,因此当只有一个从机时用两线,有多个从机时用三线。
图9 SCCB框图
SCCB_E默认拉低时,相当于:
图10 SCCB传输框图
OV公司为了减少传感器引脚的封装,现在SCCB总线也大多采用两线式接口总线。由此可见,SCCB就是改编版的I2C,完全可以按照I2C来理解,所以此次模块也是按照I2C协议来进行仿写,只是有些小细节会不同,后面会仔细讲解。
- ST7735
ST7735 是 262K 彩色图形型 TFT-LCD 的单芯片控制器/驱动程序。它由396条源线和162门线驱动电路组成。该芯片能够直接连接到外部微处理器,并接受串行外围接口 (SPI)、8 位/9 位/16 位/18 位并行接口。显示数据可以存储在 132 x 162 x 18 位的片上显示数据 RAM 中。它可以在没有外部操作时钟时执行显示数据 RAM 读写操作,以最大限度地降低功耗。此外,由于驱动液晶所需的集成电源电路,因此可以制造出组件较少的显示系统。
图11 ST7735实物图
我使用的这款是为1.44寸,128*128分辨率的TFT屏幕,产商在屏幕设计上添加了3.3V稳压芯片以及电平转换芯片,使得这款原本3.3V供电的裸屏可以兼容5V和3.3V的单片机,这也意味着arduino和51单片机的用户也可以驱动这款屏幕了。
图12 ST7735外部管脚图
根据上图我们可以将屏幕与我们的树莓派进行一个管脚的分配,SCL与SDA分别与GP10与GP11相连接,DC与CS与GP0与GP1相连接,BLK背光直接拉高,会使屏幕显现效果更好,复位端拉低。
图13 ST7735工作流程图
驱动ST7735是最重要的一步,无论是micropython还是circuitpython这都是很重要的一步,找到其中的驱动库并且对要实现相关功能非常重要,可以根据相关数据手册与函数中功能进行修改。
三、主要代码实现及其说明
在这项目中,我使用的是circuitpython的编程方法进行实现,CircuitPython基于Python。Python是增长最快的编程语言。它在学校和大学里教授。它是一种高级编程语言,这意味着它的设计更易于阅读,编写和维护。它支持模块和包,这意味着很容易将代码重用于其他项目。它有一个内置的解释器,这意味着没有额外的步骤,如编译,让你的代码工作。当然,Python是开源软件,这意味着任何人都可以免费使用,修改或改进。circuitpython还有一个非常好的就是有内置库的支持:CircuitPython 在其核心库中提供了广泛的硬件支持和驱动程序,使用户能够轻松地与各种外围设备进行交互,如传感器、显示屏、LED 等。这意味着您可以更快速地开始开发项目,而无需编写大量的底层代码。类似与一个工具箱一样,你需要啥就把工具箱打开,你所需要的拿出来即可。
具体使用方法与使用指南我会在视频中讲到,并且也有一篇文章供大家进行参考(使用 CircuitPython 开发RP2040)
1.导入相关库函数:
import time
from displayio import (
Bitmap,
Group,
TileGrid,
FourWire,
release_displays,
ColorConverter,
Colorspace,
)
from adafruit_st7735r import ST7735R
import board
import busio
import digitalio
import adafruit_ov2640
在这里我们一共导入7个库,分别导入了time,displayio,adafruit_st7735r,board,busio,digitalio,adafruit_ov2640。其中time,displayio,board,busio,digitalio是芯片内部自带的库,而adafruit_st7735r与adafruit_ov2640则是在circuitpython官网上的模块库,在这个模块库中,包含了几百个市面上常见的模块,例如ssd1305,wifi模块等,即调即用非常方便。
displayio是一个用于在嵌入式系统上管理和控制显示设备的 Python 库。它是 CircuitPython 和 MicroPython 中常用的库之一,用于创建和管理图形界面以及在连接的显示屏上显示图像、文本和图形等内容。在displayio中我们调用了Bitmap,Group等一系列函数,具体使用方法大家可以进circuitpython官网进行手册阅读(displayio使用方法)
board是用于连接和交互各种外围设备(如传感器、显示屏、存储器等)的函数。它是一个预定义的模块,提供了与特定硬件开发板相关的信息和功能。通过引入 "board" 模块,您可以轻松地访问与硬件有关的引脚定义、外设配置和其他功能。
busio用于在嵌入式系统中管理通信总线接口,如 I2C、SPI 和 UART,用于配置和控制硬件上的通信总线。
digitalio用于在嵌入式系统中配置和控制数字 I/O 引脚(也称为数字引脚),用于与外部设备进行数字信号的输入和输出。
2.初始化屏幕与摄像头:
release_displays()
spi = busio.SPI(clock=board.GP10, MOSI=board.GP11)
display_bus = FourWire(spi, command=board.GP0, chip_select=board.GP1, reset=board.GP16)
display = ST7735R(display_bus, width=128, height=128, bgr = True)
display.auto_refresh = True
此代码为配置屏幕管脚与初始化屏幕的功能,通过前面的管脚介绍与board函数作用可以轻松将相关管脚配置好,ST7735R为adafruit_st7735r库里面的函数,作用为初始化屏幕,大家可以根据自己需要将库函数里有其他功能的函数调出来供自己使用。
cam = adafruit_ov2640.OV2640(
bus,
data_pins=[
board.GP2,
board.GP3,
board.GP4,
board.GP5,
board.GP6,
board.GP7,
board.GP8,
board.GP9,
],
clock=board.GP19,
vsync=board.GP18,
href=board.GP17,
mclk=board.GP20,
shutdown=None,
reset=board.GP21,
)
此代码为分配摄像头管脚功能;
with digitalio.DigitalInOut(board.GP21) as reset:
reset.switch_to_output(False)
time.sleep(0.001)
bus = busio.I2C(board.GP15, board.GP14)
width = display.width
height = display.height
cam.size = adafruit_ov2640.OV2640_SIZE_QQVGA
bitmap = Bitmap(cam.width, cam.height, 65535)
初始化摄像头,为摄像头创建了一个 I2C 总线对象,连接到开发板上的 GP15 (SDA) 和 GP14 (SCL) 引脚。并对GP21复位端配置为输出模式,并将其设置为低电平。并且获取显示屏的宽度和高度,分别存储在 width 和 height 变量中,设置摄像头的大小为 QQVGA(160x120)尺寸,使用了 adafruit_ov2640.OV2640_SIZE_QQVGA 常量,创建了一个和摄像头尺寸相同的位图对象,宽度为 cam.width,高度为 cam.height,位深度为 65535。
3.功能实现:
print(width, height, cam.width, cam.height)
if bitmap is None:
raise SystemExit("Could not allocate a bitmap")
g = Group(scale=1, x=(width - cam.width) // 2, y=(height - cam.height) // 2)
tg = TileGrid(
bitmap, pixel_shader=ColorConverter(input_colorspace=Colorspace.RGB565_SWAPPED)
)
g.append(tg)
display.show(g)
display.auto_refresh = False
while True:
cam.capture(bitmap)
bitmap.dirty()
display.refresh(minimum_frames_per_second=0)
使用条件判断语句 if bitmap is None 检查位图对象是否为空。如果为空,则会引发系统退出,并输出一条错误消息。创建一个 Group 对象 g,用于组织显示对象。它使用了缩放因子为 1,并计算出位图在显示屏上的位置(x 和 y 坐标)。创建一个 TileGrid 对象 tg,将位图作为参数传入,并配置了像素渲染器为 ColorConverter,指定输入颜色空间为 Colorspace.RGB565_SWAPPED。(Colorspace用法)。调用 display.show(g) 将 g 显示在显示屏上
进入循环时,在每次循环中执行以下操作:使用摄像头的 capture 方法捕获图像并保存到位图对象 bitmap 中。调用 bitmap.dirty() 标记位图为一个状态,表示需要进行更新。调用 display.refresh(minimum_frames_per_second=0) 刷新显示屏,将位图内容显示出来。最后图像可以成功显现出来。
4.完整代码:
import time
from displayio import (
Bitmap,
Group,
TileGrid,
FourWire,
release_displays,
ColorConverter,
Colorspace,
)
from adafruit_st7735r import ST7735R
import board
import busio
import digitalio
import adafruit_ov2640
release_displays()
spi = busio.SPI(clock=board.GP10, MOSI=board.GP11)
display_bus = FourWire(spi, command=board.GP0, chip_select=board.GP1, reset=board.GP16)
display = ST7735R(display_bus, width=128, height=128, bgr = True)
display.auto_refresh = True
with digitalio.DigitalInOut(board.GP21) as reset:
reset.switch_to_output(False)
time.sleep(0.001)
bus = busio.I2C(board.GP15, board.GP14)
cam = adafruit_ov2640.OV2640(
bus,
data_pins=[
board.GP2,
board.GP3,
board.GP4,
board.GP5,
board.GP6,
board.GP7,
board.GP8,
board.GP9,
],
clock=board.GP19,
vsync=board.GP18,
href=board.GP17,
mclk=board.GP20,
shutdown=None,
reset=board.GP21,
)
width = display.width
height = display.height
cam.size = adafruit_ov2640.OV2640_SIZE_QQVGA
bitmap = Bitmap(cam.width, cam.height, 65535)
print(width, height, cam.width, cam.height)
if bitmap is None:
raise SystemExit("Could not allocate a bitmap")
g = Group(scale=1, x=(width - cam.width) // 2, y=(height - cam.height) // 2)
tg = TileGrid(
bitmap, pixel_shader=ColorConverter(input_colorspace=Colorspace.RGB565_SWAPPED)
)
g.append(tg)
display.show(g)
display.auto_refresh = False
while True:
cam.capture(bitmap)
bitmap.dirty()
display.refresh(minimum_frames_per_second=0)
库函数需要自己阅读相关手册并且动手去调,这样做出来效果才是最好的,也能够让自己学到很多东西。
四、实物展示
五、结语
此项目最开始是用SPI摄像头做的,但是由于没有此模块,并且网上也没有购买信息,唯一只有从国外购买回来,但所需成本太高,随之放弃这个方案,最后采用OV系列摄像头进行调试。在网上找到了许多相关代码,眼花缭乱什么类型的都有,但是代码的出错率很高,不能拿来即用,需要自己一一去排除相关问题,这也花费了我大概一周时间,不停地学习相关语法与查阅数据手册,并且进行调试,将出错的地方及时删减掉,才得以调试出来。在这个过程中,无论是自己的耐心还是逻辑能力都得到了相应地提高,这也是作为一名工科人需要具备的能力。目前此系统能够很好地工作,但是屏幕刷新率低,帧数不是很高,同时也可以继续加一些相关功能例如移动目标检测等。但是由于本人才疏学浅,需要太多时间学习相关知识,导致没有能够完成相关功能。但这也为以后的学习工作打下了牢牢的基础。
这是来到硬禾学堂暑期实践营的最后一个项目,三周时间从最开始一无所知到最后调试成功过程中,经历过许多坎坷与曲折,要一直抱着我觉得此次实践给我带来最大的一个感受就是——要不断地试错,在试错中加强自己的技能与沟通相关的素养,如果你自己不亲自的走过这一流程,你永远不知道这个流程是什么样子,不知道中途会出现什么错误,这也是一个工程师应该具备的。非常感谢硬禾学堂能给我这个机会。也希望在接下来的学习生涯中,自己要更努力,也会一如既往支持硬禾学堂后面的活动。