44. OV7725摄像头驱动¶
本章参考资料:《STM32F10x参考手册》、《STM32F10x数据手册》。
关于开发板配套的OV7725摄像头参数可查阅《ov7725datasheet》配套资料获知。
STM32的处理速度比传统的8、16位机快得多,所以使用它驱动摄像头采集图像信息并进行基本的加工处理非常适合,本章讲解使用STM32驱动OV7725型号的摄像头。
44.1. 摄像头简介¶
在各类信息中,图像含有最丰富的信息,作为机器视觉领域的核心部件,摄像头被广泛地应用在安防、探险以及车牌检测等场合。摄像头按输出信号的类型来看可以分为数字摄像头和模拟摄像头,按照摄像头图像传感器材料构成来看可以分为CCD和CMOS。现在智能手机的摄像头绝大部分都是CMOS类型的数字摄像头。
44.1.1. 数字摄像头跟模拟摄像头区别¶
输出信号类型:数字摄像头输出信号为数字信号,模拟摄像头输出信号为标准的模拟信号。
接口类型:数字摄像头有usb接口(比如常见的pc端免驱摄像头)、IEE1394火线接口(由苹果公司领导的开发联盟 开发的一种高速度传送接口,数据传输率高达800Mbps)、千兆网接口(网络摄像头)。模拟摄像头多采用AV视频端子(信号线+地线)或S-VIDEO(即莲花头–SUPER VIDEO,是一种五芯的接口,由两路视频亮度信号、两路视频色度信号和一路公共屏蔽地线共五条芯线组成)。
分辨率:模拟摄像头的感光器件,其像素指标一般维持在752(H)*582(V)左右的水平,像素数一般情况下维持在41W左右。 数字摄像头分辨率一般从数十万到数百万甚至数千万。但这并不能说明数字摄像头的成像分辨率就比模拟摄像头的高,原因在于模拟摄像头输出的是模拟视频信号,一般直接输入至电视或监视器,其感光器件的分辨率与电视信号的扫描数呈一定的换算关系,图像的显示介质已经确定,因此模拟摄像头的感光器件分辨率不是不能做高,而是依据于实际情况没必要做这么高。
44.1.2. CCD与CMOS的区别¶
摄像头的图像传感器CCD与CMOS传感器主要区别如下:
成像材料
CCD与CMOS的名称跟它们成像使用的材料有关,CCD是“电荷耦合器件”(Charge Coupled Device)的简称,而CMOS是“互补金属氧化物半导体”(Complementary Metal Oxide Semiconductor)的简称。
功耗
由于CCD的像素由MOS电容构成,读取电荷信号时需使用电压相当大(至少12V)的二相或三相或四相时序脉冲信号,才能有效地传输电荷。因此CCD的取像系统除了要有多个电源外,其外设电路也会消耗相当大的功率。有的CCD取像系统需消耗2~5W的功率。而CMOS光电传感器件只需使用一个单电源5V或3V,耗电量非常小,仅为CCD的1/8~1/10,有的CMOS取像系统只消耗20~50mW的功率。
成像质量
CCD传感器件制作技术起步早,技术成熟,采用PN结或二氧化硅(sio2)隔离层隔离噪声,所以噪声低,成像质量好。与CCD相比,CMOS的主要缺点是噪声高及灵敏度低,不过现在随着CMOS电路消噪技术的不断发展,为生产高密度优质的CMOS传感器件提供了良好的条件,现在的CMOS传感器已经占领了大部分的市场,主流的单反相机、智能手机都已普遍采用CMOS传感器。
44.2. OV7725摄像头¶
本章主要讲解实验板配套的摄像头,它的实物见 图47_1,该摄像头主要由镜头、图像传感器、板载电路、FIFO缓存及下方的信号引脚组成。
图 47‑1 实验板配套的OV7725摄像头
镜头部件包含一个镜头座和一个可旋转调节距离的凸透镜,通过旋转可以调节焦距,正常使用时,镜头座覆盖在电路板上遮光,光线只能经过镜头传输到正中央的图像传感器,它采集光线信号,采集得的数据被缓存到摄像头背面的FIFO缓存中,然后外部器件通过下方的信号引脚获取拍摄得到的图像数据。
44.2.1. OV7725传感器简介¶
若拆开摄像头座,在摄像头的正下方可看到PCB板上的一个方形器件,它是摄像头的核心部件,型号为OV7725的CMOS类型数字图像传感器。该传感器支持输出最大为30万像素的图像 (640x480分辨率),它的体积小,工作电压低,支持使用VGA时序输出图像数据,输出图像的数据格式支持YUV(422/420)、YCbCr422以及RGB565格式。它还可以对采集得的图像进行补偿,支持伽玛曲线、白平衡、饱和度、色度等基础处理。
44.2.2. OV7725引脚及功能框架图¶
OV7725传感器采用BGA封装,它的前端是采光窗口,引脚都在背面引出,引脚的分布见 图47_2。
图 47‑2 OV7725管脚图
图中的非彩色部分是电源相关的引脚,彩色部分是主要的信号引脚,其介绍如表 47‑1。
表 47‑1 OV7725管脚
管脚名称 |
管脚类型 |
管脚描述 |
RSTB |
输入 |
系统复位管脚,低电平有效 |
PWDN |
输入 |
掉电/省电模式(高电平有效) |
HREF |
输出 |
行同步信号 |
VSYNC |
输出 |
场同步信号 |
PCLK |
输出 |
像素时钟 |
XCLK |
输入 |
系统时钟输入端口 |
SCL |
输入 |
SCCB总线的时钟线 |
SDA |
I/O |
SCCB总线的数据线 |
D0…D9 |
输出 |
像素数据端口 |
下面我们配合图 47‑3中的OV7725功能框图讲解这些信号引脚。
图 47‑3 OV7725功能框图
控制寄存器
标号处的是OV7725的控制寄存器,它根据这些寄存器配置的参数来运行,而这些参数是由外部控制器通过SCL和SDA引脚写入的,SCL与SDA使用的通讯协议SCCB跟I2C十分类似,在STM32中我们完全可以直接用I2C硬件外设来控制。
通信、控制信号及时钟
标号处包含了OV7725的通信、控制信号及外部时钟,其中PCLK、HREF及VSYNC分别是像素同步时钟、行同步信号以及帧同步信号,这与液晶屏控制中的VGA信号是很类似的。RSTB引脚为低电平时,用于复位整个传感器芯片,PWDN用于控制芯片进入低功耗模式。注意最后的一个XCLK引脚,它跟PCLK是完全不同的,XCLK是用于驱动整个传感器芯片的时钟信号,是外部输入到OV7725的信号;而PCLK是OV7725输出数据时的同步信号,它是由OV7725输出的信号。XCLK可以外接晶振或由外部控制器提供,若要类比XCLK之于OV7725就相当于HSE时钟输入引脚与STM32芯片的关系,PCLK引脚可类比STM32的I2C外设的SCL引脚。
感光矩阵
标号处的是感光矩阵,光信号在这里转化成电信号,经过各种处理,这些信号存储成由一个个像素点表示的数字图像。
数据输出信号
标号处包含了DSP处理单元,它会根据控制寄存器的配置做一些基本的图像处理运算。这部分还包含了图像格式转换单元及压缩单元,转换出的数据最终通过D0-D9引脚输出,一般来说我们使用8根据数据线来传输,这时仅使用D2-D9引脚。
44.2.3. SCCB时序¶
外部控制器对OV7725寄存器的配置参数是通过SCCB总线传输过去的,而SCCB总线跟I2C十分类似,所以在STM32驱动中可以直接使用片上I2C外设与它通讯。关于SCCB协议的完整内容可查看配套资料里的《SCCB协议》文档,下面进行简单介绍。
44.2.3.1. SCCB的起始、停止信号及数据有效性¶
SCCB的起始信号、停止信号及数据有效性与I2C完全一样,见 图47_4 及 图47_5。
起始信号:在SCL(图中为SIO_C)为高电平时,SDA(图中为SIO_D)出现一个下降沿,则SCCB开始传输。
停止信号:在SCL为高电平时,SDA出现一个上升沿,则SCCB停止传输。
数据有效性:除了开始和停止状态,在数据传输过程中,当SCL为高电平时,必须保证SDA上的数据稳定,也就是说,SDA上的电平变换 只能发生在SCL为低电平的时候,SDA的信号在SCL为高电平时被采集。
图 47‑4 SCCB停止信号
图 47‑5 SCCB的数据有效性
44.2.3.2. SCCB数据读写过程¶
在SCCB协议中定义的读写操作与I2C也是一样的,只是换了一种说法。它定义了两种写操作,即三步写操作和两步写操作。三步写操作可向从设备的一个目的寄存器 中写入数据,见 图47_6。在三步写操作中,第一阶段发送从设备的ID地址+W标志(等于I2C的设备地址:7位设备地址+读写方向标志),第二阶段发送从设备目标寄存器的8位地址,第三阶段发送要写入寄存器的8位数据。图中的“X”数据位可写入1或0,对通讯无影响。
图 47‑6 SCCB的三步写操作
而两步写操作没有第三阶段,即只向从器件传输了设备ID+W标志和目的寄存器的地址,见 图47_7。两步写操作是用来配合后面的读寄存器数据操作的,它与读操作一起使用,实现I2C的复合过程。
图 47‑7 SCCB的两步写操作
两步读操作,它用于读取从设备目的寄存器中的数据,见 图47_8。在第一阶段中发送从设备的 设备ID+R标志(设备地址+读方向标志)和自由位,在第二阶段中读取寄存器中的8位数据和写NA 位(非应答信号)。由于两步读操作没有确定目的寄存器的地址,所以在读操作前,必需有一个两步写操作,以提供读操作中的寄存器地址。
图 47‑8 SCCB的两步读操作
可以看到,以上介绍的SCCB特性都与I2C无区别,完全可以使用STM32的I2C外设来与OV7725进行SCCB通讯。
44.2.4. OV7725的寄存器¶
控制OV7725涉及到它很多的寄存器,可直接查询《OV7725datasheet》了解,通过这些寄存器的配置,可以控制它输出图像的分辨率大小、 图像格式、图像处理及图像方向等。见 图47_9。
图 47‑9 0xFF=0时的DSP相关寄存器说明(部分)
官方还提供了一个《OV7725 Software Application Note》的文档,它针对不同的配置需求,提供了配置范例,见 图47_10。其中write_SCCB是一个 利用SCCB向寄存器写入数据的函数,第一个参数为要写入的寄存器的地址,第二个参数为要写入的内容。
图 47‑10 调节帧率的寄存器配置范例
44.2.5. 像素数据输出时序¶
主控器控制OV7725时采用SCCB协议读写其寄存器,而它输出图像时则使用VGA或QVGA时序,其中VGA在输出图像分辨率为480*640时采用,QVGA是Quarter VGA,其输出分辨率为240*320,这些时序跟控制液晶屏输出图像数据时十分类似。
OV7725传感器输出图像时,一帧帧地输出,在帧内的数据一般从左到右,从上到下,一个像素一个像素地输出(也可通过寄存器修改方向),见 图47_11。
图 47‑11 摄像头数据输出
例如,见 图47_12 和 图47_13,若我们使用D2-D9数据线,图像格式设置为RGB565,进行数据输出时, D2-D9数据线在PCLK在上升沿阶段维持稳定,并且会在1个像素同步时钟PCLK的驱动下发送1字节的数据信号,所以2个PCLK时钟可发送1个RGB565格式的像素数据。当HREF为高电平时,像素数据依次传输,每传输完一行数据时,行同步信号HREF会输出一个电平跳变信号间隔开当前行和下一行的数据;一帧的图像由N行数据组成,当VSYNC为低电平时,各行的像素数据依次传输,每传输完一帧图像时,VSYNC会输出一个电平跳变信号。
图 47‑12像素同步时序
图 47‑13 QVGA帧图像同步时序
44.2.6. FIFO读写时序¶
STM32F1系列的控制器主频高、一般会扩展外部SRAM、SDRAM等存储器,且具有DCMI外设,可以直接根据VGA时序接收并存储摄像头输出的图像数据;而STM32F1系列的控制器一般主频较低、为节省成本可能不扩展SRAM存储器,而且不具DCMI外设,难以直接接收和存储OV7725图像传感器输出的数据。
为了解决上述问题,针对类似STM32F1或更低级的控制器,野火的OV7725摄像头在图像传感器之外还添加了一个型号为AL422B的FIFO, 用于缓冲数据。AL422B的本质是一种RAM存储器,见 图47_14,它的容量大小为393216字节,支持同时写入和读出数据, 这正是专门用于FIFO缓冲功能而设计的,关于它的详细说明可查阅《AL422_datasheet》文档。
图 47‑14 AL422B FIFO引脚图
AL422B的各引脚功能介绍见表 47‑2。
表 47‑2 AL422B引脚功能说明
管脚名称 |
管脚类型 |
管脚描述 |
DI[0:7] |
输入 |
数据输入引脚 |
WCK |
输入 |
数据输入同步时钟 |
/WE |
输入 |
写使能信号,低电平有效 |
/WRST |
输入 |
写指针复位信号,低电平有效 |
DO[0:7] |
输出 |
数据输出引脚 |
RCK |
输入 |
数据输出同步时钟 |
/RE |
输入 |
读使能信号,低电平有效 |
/RRST |
输入 |
读指针复位信号,低电平有效 |
/OE |
输入 |
数据输出使能,低电平有效 |
TST |
输入 |
测试引脚,实际使用时设置为低电平 |
由于AL422B支持同时写入和读出数据,所以它的输入和输出的控制信号线都是互相独立的。写入和读出数据的时序类似,跟VGA的像素输出时序一致,读写时序介绍如下:
写时序
写FIFO(AL422B,下面统称为FIFO)时序见 图47_15。
图 47‑15写 FIFO时序
在写时序中,当WE管脚为低电平时,FIFO写入处于使能状态,随着读时钟WCK的运转, DI[0:7]表示的数据将会就会按地址递增的方式存入FIFO;当WE管脚为高电平时,关闭输入,DI[0:7]的数据不会被写入FIFO。
在控制写入数据时,一般会先控制写指针作一个复位操作:把WRST设置为低电平,写指针会复位到FIFO的0地址,然后FIFO接收到的数据会从该地址开始按自增的方式写入。
读时序
读FIFO时序见 图47_16。
图 47‑16 读FIFO时序
FIFO的读时序类似,不过读使能由两个引脚共同控制,即OE和RE引脚均为低电平时,输出处于使能状态,随着读时钟RCK的运转,在数据输出管脚DO[0:7]就会按地址递增的方式输出数据。
类似地,在控制读出数据时,一般会先控制读指针作一个复位操作:把RRST设置为低电平,读指针会复位到FIFO的0地址,然后FIFO数据从该地址开始按自增的方式输出。
44.2.7. 摄像头的驱动原理¶
野火OV7725摄像头中包含有FIFO,所以外部控制器驱动摄像头时,需要协调好FIFO与OV7725传感器的关系,下面配合摄像头的原理图讲解其驱动原理。
原理图主要分为外部引出接口、OV7725及FIFO部分,见 图47_17。
图 47‑17野火摄像头引出的排母接口
摄像头引出的接口包含了OV7725传感器及FIFO的混合引脚,外部的控制器使用这些引脚即可驱动摄像头,其说明见表 47‑3。
表 47‑3 摄像头引脚列表
管脚名称 |
管脚关系 |
管脚描述 |
OE |
FIFO的OE引脚 |
数据输出使能,低电平有效 |
RRST |
FIFO的RRST引脚 |
读指针复位信号,低电平有效 |
RCLK |
FIFO的RCK引脚 |
数据输出同步时钟 |
VSYNC |
OV7725的VSYNC引脚 |
场同步信号 |
WRST |
FIFO的WRST引脚 |
写指针复位信号,低电平有效 |
WEN |
与下面的HREF共同组成与非门的输入 |
与HREF共同控制FIFO的WE引脚,WEN与HREF同时为高电平时,WE为低电平,OV7725可以向FIFO写入数据 |
HREF |
OV7725的HREF引脚 |
行同步信号 |
DO[0:7] |
FIFO的DO[0:7]引脚 |
数据输出引脚 |
SIO_C |
OV7725的SCL引脚 |
SCCB总线的时钟线 |
SIO_D |
OV7725的SDA引脚 |
SCCB总线的数据线 |
从上述列表与下面的 图47_18 和 图47_19,可了解到,与OV7725传感器像素输出相关的PCLK和D[0:7]并没有引出, 因为这些引脚被连接到了FIFO的输入部分,OV7725的像素输出时序与FIFO的写入数据时序是一致的,所以在OV7725时钟PCLK的驱动下,它输出的数据会一个字节一个字节地被FIFO接收并存储起来。
其中最为特殊的是WEN引脚,它与OV7725的HREF连接到一个与非门的输入,与非门的输出连接到FIFO的WE引脚,因此,当WEN与HREF均为高电平时,FIFO的WE为低电平,此时允许OV7725向FIFO写入数据。
外部控制器通过控制WEN引脚,可防止OV7725覆盖了还未被控制器读出的旧FIFO数据。另外,在OV7725输出时序中,只有当HREF为高电平时,PCLK驱动下 D[0:7]线表示的才是有效像素数据,因此,利用HREF控制FIFO的WE可以确保只有有效数据才被写入到FIFO中。
图 47‑18 摄像头的OV7725部分硬件连接图
图 47‑19 摄像头的FIFO部分硬件连接图
配合摄像头的原理图、OV7725、FIFO的时序图,可总结出摄像头采集数据的过程如下:
利用SIO_C、SIO_D引脚通过SCCB协议向OV7725的寄存器写入初始化配置;
初始化完成后,OV7725传感器会使用VGA时序输出图像数据,它的VSYNC会首先输出帧有效信号(低电平跳变), 当外部的控制器(如STM32)检测到该信号时,把WEN引脚设置为高电平,并且使用WRST引脚复位FIFO的写指针到0地址;
随着OV7725继续按VGA时序输出图像数据,它在传输每行有效数据时, HREF引脚都会持续输出高电平,由于WEN和HREF同时为高电平输入至与非门,使得其连接到FIFO WE引脚的输出为低电平,允许向FIFO写入数据,所以在这期间,OV7725通过它的PCLK和D[0:7]信号线把图像数据存储到FIFO中,由于前面复位了写指针,所以图像数据是从FIFO的0地址开始记录的;
各行图像数据持续传输至FIFO,受HREF控制的WE引脚确保了写入到FIFO中的都是有效的图像数据,OV7725输出完一帧数据时, VSYNC会再次输出帧有效信号,表示一帧图像已输出完成;
控制器检测到上述VSYNC信号后,可知FIFO中已存储好一帧图像数据,这时控制WEN引脚为低电平,使得FIFO禁止写入, 防止OV7725持续输出的下一帧数据覆盖当前FIFO数据;
控制器使用RRST复位读指针到FIFO的0地址,然后通过FIFO的RCLK和DO[0:7]引脚,从0地址开始把FIFO缓存的整帧图像数据读取出来。 在这期间,OV7725是持续输出它采集到的图像数据的,但由于禁止写入FIFO,这些数据被丢弃了;
控制器使用WRST复位写指针到FIFO的0地址,然后等待新的VSYNC有效信号到来,检测到后把WEN引脚设置为高电平, 恢复OV7725向FIFO的写入权限,OV7725输出的新一帧图像数据会被写入到FIFO的0地址中,重复上述过程。
摄像头的整个控制过程见 图47_20。
图 47‑20 摄像头控制过程
在使用本摄像头时,我们一般配套开发板的液晶屏,把OV7725配置为240*320分辨率(QVGA),RGB565格式,那么OV7725输出一帧的图像大小为240*320*2=153600字节,而本摄像头采用的FIFO型号AL422B容量为393216字节,最多可以缓存2帧这样的图像,通过这样的方式,STM32无需直接处理OV7725高速输出的数据。但是,如果配置OV7725为480*640分辨率时,其一帧图像大小为480*640*2=614400字节,FIFO的容量不足以直接存储一帧这样的图像,因此,当OV7725往FIFO写数据的时候,STM32端要同时读取数据,确保在OV7725覆盖旧数据的之前,STM32端已经把这部分数据读取出来了。
44.3. 摄像头驱动实验¶
本小节讲解如何使用如何利用OV7725摄像头采集RGB565格式的图像数据,并把这些数据实时显示到液晶屏上。
学习本小节内容时,请打开配套的“摄像头-OV7725-液晶实时显示”工程配合阅读。
44.3.1. 硬件设计¶
关于摄像头的原理图此处不再分析。在我们的实验板上有引出一个摄像头专用的排母,可直接与摄像头引出的引脚连,接入后它与STM32引脚的连接关系见 图47_21。
图 47‑21 STM32实验板引出的摄像头接口
摄像头与STM32连接关系中主要分为SCCB控制、VGA时序控制、FIFO数据读取部分,介绍如下:
SCCB控制相关
摄像头中的SIO_C和SIO_D引脚直接连接到STM32普通的GPIO,它们不具有硬件I2C的功能,所以在后面的代码中采用模拟I2C时序,实际上直接使用硬件I2C是完全可以实现SCCB协议的,本设计采用模拟I2C是芯片资源分配妥协的结果。
VGA时序相关
检测VGA时序的HREF、VSYNC引脚,它们与STM32连接的GPIO均设置为输入模式,其中HREF在本实验中并没有使用,它已经通过摄像头内部的与非门控制了FIFO的写使能;VSYNC与STM32连接的GPIO引脚会在程序中配置成中断模式,STM32利用该中断信号获知新的图像是否采集完成,从而控制FIFO是否写使能。
FIFO相关
与FIFO控制相关的RCLK、RRST、WRST、WEN及OE与STM32连接的引脚均直接配置成推挽输出,STM32根据图像的采集情况利用这些引脚控制FIFO;读取FIFO数据内容使用的数据引脚DO[0:7]均连接到STM32同一个GPIO端口连续的高8位引脚PB[8:15],这些引脚使用时均配置成输入,程序设计中直接读取GPIO端口的高8位状态直接获取一个字节的FIFO内容,建议在连接这部分数据信号时,参考本设计采用同一个GPIO端口连续的8位(高8位或低8位均可),否则会导致读取数据的程序非常复杂。
XCLK信号
本设计中STM32的摄像头接口还预留了PA8引脚用于与摄像头的XCLK连接,STM32的PA8可以对外输出时钟信号,所以在使用不带晶振的摄像头时,可以通过该引脚给摄像头提供时钟,野火摄像头内部已自带晶振,在程序中没有使用PA8引脚。
以上原理图可查阅《指南者开发板—原理图》文档获知,若您使用的摄像头或实验板不一样,请根据实际连接的引脚修改程序。
44.3.2. 软件设计¶
本实验的工程名称为“液晶实时显示”,学习时请打开该工程配合阅读。为了方便展示及移植,我们把模拟SCCB时序相关的代码写到bsp_sccb.c及bsp_sccb.h文件中,而摄像头模式控制相关的代码都编写到“bsp_ov7725.c”、“bsp_ov7725.h”文件中,这些文件是我们自己编写的,不属于HAL库的内容,可根据您的喜好命名文件。
44.3.2.1. 编程要点¶
初始化SCCB通讯使用的目标引脚及端口时钟;
初始化OV7725的VGA和FIFO控制相关的引脚和时钟;
使用SCCB协议向OV7725写入初始化配置;
配置筛选器的工作方式;
编写测试程序,收发报文并校验。
44.3.2.2. 代码分析¶
44.3.2.2.1. 摄像头硬件相关宏定义¶
我们把摄像头控制硬件相关的配置以宏的形式定义到 “bsp_ov7725.h”及“bsp_sccb.h”文件中,其中包括VGA部分接口、FIFO控制及SCCB(即模拟I2C)相关的引脚,见 代码清单47_1。
// FIFO 输出使能,即模块中的OE
#define OV7725_OE_GPIO_PORT GPIOA
#define OV7725_OE_GPIO_PIN GPIO_PIN_3
// FIFO 写复位
#define OV7725_WRST_GPIO_PORT GPIOC
#define OV7725_WRST_GPIO_PIN GPIO_PIN_4
// FIFO 读复位
#define OV7725_RRST_GPIO_PORT GPIOA
#define OV7725_RRST_GPIO_PIN GPIO_PIN_2
// FIFO 读时钟
#define OV7725_RCLK_GPIO_PORT GPIOC
#define OV7725_RCLK_GPIO_PIN GPIO_PIN_5
// FIFO 写使能
#define OV7725_WE_GPIO_PORT GPIOD
#define OV7725_WE_GPIO_PIN GPIO_PIN_3
// 8位数据口
#define OV7725_DATA_GPIO_PORT GPIOB
#define OV7725_DATA_0_GPIO_PIN GPIO_PIN_8
#define OV7725_DATA_1_GPIO_PIN GPIO_PIN_9
#define OV7725_DATA_2_GPIO_PIN GPIO_PIN_10
#define OV7725_DATA_3_GPIO_PIN GPIO_PIN_11
#define OV7725_DATA_4_GPIO_PIN GPIO_PIN_12
#define OV7725_DATA_5_GPIO_PIN GPIO_PIN_13
#define OV7725_DATA_6_GPIO_PIN GPIO_PIN_14
#define OV7725_DATA_7_GPIO_PIN GPIO_PIN_15
// OV7725场中断
#define OV7725_VSYNC_GPIO_PORT GPIOC
#define OV7725_VSYNC_GPIO_PIN GPIO_PIN_3
#define OV7725_VSYNC_EXTI_IRQ EXTI3_IRQn
#define OV7725_VSYNC_EXTI_INT_FUNCTION EXTI3_IRQHandler
/* 直接操作寄存器的方法控制IO */
#define digitalHi(p,i) {p->BSRR=i;} //设置为高电平
#define digitalLo(p,i) {p->BSRR=(uint32_t)i << 16;}
//输出低电平
#define SCL_H digitalHi(GPIOC , GPIO_PIN_6)
#define SCL_L digitalLo(GPIOC , GPIO_PIN_6)
#define SDA_H digitalHi(GPIOC , GPIO_PIN_7)
#define SDA_L digitalLo(GPIOC , GPIO_PIN_7)
#define SCL_read HAL_GPIO_ReadPin(GPIOC , GPIO_PIN_6)
#define SDA_read HAL_GPIO_ReadPin(GPIOC , GPIO_PIN_7)
#define ADDR_OV7725 0x42
以上代码根据硬件的连接,使用宏封装了各种控制信号,包括控制输出电平或读取输入时使用的库函数操作。若使用STM32与摄像头的引脚连接与我们的设计的不同,修改这两个文件的引脚连接关系即可。
44.3.2.2.2. SCCB总线的软件实现¶
本设计中使用普通GPIO来模拟SCCB时序,需要根据SCCB时序,编写读、写字节的模拟函数,在后面的OV7725_Init会利用这些函数向OV7725相应的寄存器写入配置参数,初始化摄像头。本小节介绍的与SCCB时序相关函数都位于bsp_sccb.c文件中,这些函数跟模拟I2C的基本一致。
初始化SCCB用到的GPIO
在本实验中,使用SCCB_GPIO_Config函数初始化SCCB使用的两个通讯引脚,把它们初始化成普通开漏输出模式,其代码见 代码清单47_3。
void SCCB_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/* SCL(PC6)、SDA(PC7)管脚配置 */
__HAL_RCC_GPIOC_CLK_ENABLE();
GPIO_InitStructure.Pin = GPIO_PIN_7;
GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_MEDIUM;//
GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_OD;
HAL_GPIO_Init(GPIOC, &GPIO_InitStructure);
GPIO_InitStructure.Pin = GPIO_PIN_6;
HAL_GPIO_Init(GPIOC, &GPIO_InitStructure);
}
SCCB起始与结束时序
SCCB通讯需要有起始与结束信号,这分别由SCCB_Start和SCCB_Stop函数实现。
SCCB_Start代码见 代码清单47_4。
/********************************************************************
* 函数名:SCCB_Start
* 描述 :SCCB起始信号
* 输入 :无
* 输出 :无
* 注意 :内部调用
********************************************************************/
static int SCCB_Start(void)
{
SDA_H;
SCL_H;
SCCB_delay();
if (!SDA_read)
return DISABLE; /* SDA线为低电平则总线忙,退出 */
SDA_L;
SCCB_delay();
if (SDA_read)
return DISABLE; /* SDA线为高电平则总线出错,退出 */
SDA_L;
SCCB_delay();
return ENABLE;
}
参照前面介绍的SCCB时序,当SCL线为高电平时,SDA线出现下降沿,表示SCCB时序的起始信号,SCCB_Start函数就是实现了这样功能,其中的SDA_H和SCL_H是用于控制SDA和SCL引脚电平的宏。为了提高程序的健壮性,还使用SDA_read宏检测SDA线是否忙碌或是否正常。
SCCB结束信号的函数实现类似,其SCCB_Stop代码见 代码清单47_5。
/********************************************************************
* 函数名:SCCB_Stop
* 描述 :SCCB停止信号
* 输入 :无
* 输出 :无
* 注意 :内部调用
********************************************************************/
static void SCCB_Stop(void)
{
SCL_L;
SCCB_delay();
SDA_L;
SCCB_delay();
SCL_H;
SCCB_delay();
SDA_H;
SCCB_delay();
}
参照前面SCCB时序的介绍,当SCL线为高电平的时候,使SDA线出现一个上升沿,表示SCCB时序的结束信号。
写寄器与读寄存器
与I2C时序类似,在SCCB时序也使用自由位(Don’t care bit )和非应答(NA)信号来保证正常通讯。自由位和非应答信号位于SCCB每个传输阶段中的第九位。
在写数据的第一个传输阶段中,第9位为自由位,在一般的正常通讯中,第9位时,主机的SDA线输出高电平,而从机把SDA线拉低作为响应,第二、三阶段类似, 只是传输的内容分别为目的寄存器地址和要写入的数据。见 图47_22。
图 47‑22 写操作第一阶段(传输器件地址)
因此,在数据传输的第9位,主机使用SCCB_WaitAck函数来等待从机的应答,代码见 代码清单47_6。
/********************************************************************
* 函数名:SCCB_WaitAck
* 描述 :SCCB 等待应答
* 输入 :无
* 输出 :返回为:=1有ACK,=0无ACK
* 注意 :内部调用
********************************************************************/
static int SCCB_WaitAck(void)
{
SCL_L;
SCCB_delay();
SDA_H;
SCCB_delay();
SCL_H;
SCCB_delay();
if (SDA_read) {
SCL_L;
return DISABLE;
}
SCL_L;
return ENABLE;
}
该函数让主机把SDA线设为高电平,延时一段时间后再检测SDA线的电平,若为低则返回ENABLE表示接收到从机的应答,反之返回DISABLE。
最后,整个三相写过程由函数SCCB_WriteByte实现,它的具体代码见 代码清单47_7。
/*以下宏位于bsp_ov7725.h文件*/
#define ADDR_OV7725 0x42
/*以下宏位于bsp_sccb.h文件*/
#define DEV_ADR ADDR_OV7725
/**************************************************************************
* 函数名:SCCB_WriteByte
* 描述 :写一字节数据
* 输入:-WriteAddress:待写入地址- SendByte:待写入数据 - DeviceAddress: 器件类型
* 输出 :返回为:=1成功写入,=0失败
* 注意 :无
*************************************************************/
int SCCB_WriteByte( uint16_t WriteAddress , uint8_t SendByte )
{
if (!SCCB_Start()) {
return DISABLE;
}
SCCB_SendByte( DEV_ADR ); /* 器件地址 */
if ( !SCCB_WaitAck() ) {
SCCB_Stop();
return DISABLE;
}
SCCB_SendByte((uint8_t)(WriteAddress & 0x00FF)); /* 设置低起始地址 */
SCCB_WaitAck();
SCCB_SendByte(SendByte);
SCCB_WaitAck();
SCCB_Stop();
return ENABLE;
}
SCCB_WriteByte函数调用SCCB_Start产生起始信号,接着调用SCCB_SendByte把器件地址DEV_ADR(这是一个宏,数值是0x42)一位一位地发送出去,在第9位时,调用SCCB_WaitAck函数检测从机的应答,若接收到应答则进入第二阶段——发送目的寄存器地址,再进入第三阶段——发送要写入的内容。在第二、三阶段没有加条件判断语句判断是否接收到从机的应答,这是因为SCCB规定在数据传输阶段允许从机不应答(实际上,OV7725芯片在这两个阶段都会有应答讯号 。在最后,三阶段都传输结束时,要调用SCCB_Stop函数结束本次SCCB传输。
与自由位相对应的非应答信号用在两相读操作的第二阶段的第9位,见 图47_23。 在这第9位中,从机把SDA线置为高电平,而主机把SDA线拉低表示非应答,接着本次读数据的操作就结束了。
图 47‑23 两相读操作第二阶段(读寄存器内容)
主机的非应答信号,由SCCB_NoAck函数实现,其代码见 代码清单47_8。
/********************************************************************
* 函数名:SCCB_NoAck
* 描述 :SCCB 无应答方式
* 输入 :无
* 输出 :无
* 注意 :内部调用
********************************************************************/
static void SCCB_NoAck(void)
{
SCL_L;
SCCB_delay();
SDA_H;
SCCB_delay();
SCL_H;
SCCB_delay();
SCL_L;
SCCB_delay();
}
最后,整个读寄存器的过程由SCCB_ReadByte函数完成,其代码见 代码清单47_9。
/************************************************************************
* 函数名:SCCB_ReadByte
* 描述 :读取一串数据
* 输入:- pBuffer:存放读出数据- length:待读出长度- ReadAddress:待读出地址 -
DeviceAddress: 器件类型
* 输出 :返回为:=1成功读入,=0失败
* 注意 :无
***********************************************************************/
int SCCB_ReadByte(uint8_t* pBuffer, uint16_t length, uint8_t ReadAddress)
{
if (!SCCB_Start()) {
return DISABLE;
}
SCCB_SendByte( DEV_ADR ); /* 器件地址 */
if ( !SCCB_WaitAck() ) {
SCCB_Stop();
return DISABLE;
}
SCCB_SendByte( ReadAddress ); /* 设置低起始地址 */
SCCB_WaitAck();
SCCB_Stop();
if (!SCCB_Start()) {
return DISABLE;
}
SCCB_SendByte( DEV_ADR + 1 ); /* 器件地址 */
if (!SCCB_WaitAck()) {
SCCB_Stop();
return DISABLE;
}
while (length) {
*pBuffer = SCCB_ReceiveByte();
if (length == 1) {
SCCB_NoAck();
} else {
SCCB_Ack();
}
pBuffer++;
length--;
}
SCCB_Stop();
return ENABLE;
}
本函数在两相读操作前,加入了一个两相写操作,用于向从机发送要读取的寄存器地址。在两相写操作的第一个阶段(第26行),使用SCCB_SendByte函数发送的数据是DEV_ADR+1 (即0x43),这与写操作中发送的DEV_ADR有区别,这是因为在第一阶段发送的这个器件地址的最低位是用于表示数据传送方向的,最低位为0时表示主机写数据,最低位为1时表示主机读数据,所以DEV_ADR+1就表示读数据了。读操作的第二阶段使用SCCB_ReceiveByte函数,一位一位地接收数据,然后存放到PBuffer指向的单元中的。接收完8位数据后,主机调用SCCB_NoAck发送非应答位,最后调用SCCB_Stop结束本次读操作。
44.3.2.2.3. 初始化OV7725¶
在上一小节编写了模拟SCCB的读写寄存器的时序后,就可以向OV7725的寄存器发送配置参数,对OV7725进行初始化了。该过程由bsp_ov7725.c文件中的OV7725_Init函数完成, 代码见 代码清单47_10。
/************************************************
* 函数名:Sensor_Init
* 描述 :Sensor初始化
* 输入 :无
* 输出 :返回1成功,返回0失败
* 注意 :无
************************************************/
ErrorStatus OV7725_Init(void)
{
uint16_t i = 0;
uint8_t Sensor_IDCode = 0;
//开始配置ov7725
if ( 0 == SCCB_WriteByte ( 0x12, 0x80 ) ) { /*复位ov7725 */
//sccb 写数据错误
return ERROR ;
}
/* 读取ov7725 ID号*/
if ( 0 == SCCB_ReadByte( &Sensor_IDCode, 1, 0x0b ) ) {
//读取ov7725 ID失败
return ERROR;
}
if (Sensor_IDCode == OV7725_ID) {
for ( i = 0 ; i < OV7725_REG_NUM ; i++ ) {
if ( 0 == SCCB_WriteByte(Sensor_Config[i].Address,Sensor_Config[i].Value) )
{ //DEBUG("write reg faild", Sensor_Config[i].Address);
return ERROR;
}
}
} else {
return ERROR;
}
//ov7725 寄存器配置成功
return SUCCESS;
}
这个函数的执行流程如下:
调用SCCB_WriteByte向地址为0x12的寄存器写入数据0x80,进行复位操作。根据OV7725的数据手册, 把该寄存器的位7置1,可控制它对寄存器进行复位。
调用SCCB_ReadByte函数从地址为0x0b的寄存器读取出OV7725芯片的ID号,并与默认值进行对比, 这个操作可以用来确保OV7725是否正常工作。
利用for语句循环调用SCCB_WriteByte函数,向各个寄存器写入配置参数,其中SCCB_WriteByte的输入参数 为Sensor_Config[i].Address和Sensor_Config[i].Value,这是自定义的结构体数组,分别对应于要配置的寄存器地址和寄存器配置参数。Sensor_Config数组是在bsp_ov7725.c文件定义的,文件中首先定义了Reg_Info结构体类型,它包含地址和寄存器两个结构体成员,见 代码清单47_11。
typedef struct Reg {
uint8_t Address; /*寄存器地址*/
uint8_t Value; /*寄存器值*/
} Reg_Info;
再利用这个自定义的结构体,定义结构体数组,每组的内容就表示了寄存器地址及相应的配置参数。 若要修改对OV7725的配置,可参考OV7725数据手册的说明,修改相应地址的内容即可,SCCB_WriteByte函数会在for循环中把这些参数写入OV7725芯片, 下面是结构体数组的部分代码,它省略了部分寄存器,完整部分请参考源代码,见 代码清单47_12。
/* (bsp_ov7725.h文件)寄存器地址宏定义 */
#define REG_GAIN 0x00
#define REG_BLUE 0x01
#define REG_RED 0x02
#define REG_GREEN 0x03
#define REG_BAVG 0x05
#define REG_GAVG 0x06
#define REG_RAVG 0x07
#define REG_AECH 0x08
#define REG_COM2 0x09
/*...以下省略大部分寄存器地址*/
/* (bsp_ov7725.c文件)寄存器参数配置,左侧为地址,右侧为要写入的值 */
Reg_Info Sensor_Config[] = {
{REG_CLKRC, 0x00}, /*时钟配置*/
{REG_COM7, 0x46}, /*QVGA RGB565 */
{REG_HSTART, 0x3f}, /*水平图像开始*/
{REG_HSIZE, 0x50}, /*水平图像宽度*/
{REG_VSTRT, 0x03}, /*垂直开始*/
{REG_VSIZE, 0x78}, /*垂直高度*/
{REG_HREF, 0x00}, /*杂项*/
{REG_HOutSize, 0x50}, /*水平输出宽度*/
{REG_VOutSize, 0x78}, /*垂直输出高度*/
{REG_EXHCH, 0x00}, /*杂项*/
/*...以下省略大部分内容.*/
};
44.3.2.2.4. 采集并显示图像¶
OV7725初始始化完成后,该芯片开始正常工作,由于OV7725采集得的图像保存到FIFO,我们使用STM32只需要检测摄像头模块的VSYNC输出的帧结束信号,然后从FIFO中读取图像数据即可。
初始化VSYNC引脚
由于使用中断的方式来检测VSYNC的信号,所以要把相应的引脚初始化并为它配置EXTI中断,本实验使用VSYNC_GPIO_Config函数完成该工作, 见 代码清单47_13。。
static void VSYNC_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/*初始化时钟,注意中断要开AFIO*/
__HAL_RCC_AFIO_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
/*初始化引脚*/
GPIO_InitStructure.Pin = OV7725_VSYNC_GPIO_PIN;
GPIO_InitStructure.Mode = GPIO_MODE_IT_RISING;
GPIO_InitStructure.Pull = GPIO_NOPULL;
GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(OV7725_VSYNC_GPIO_PORT, &GPIO_InitStructure);
HAL_NVIC_SetPriority(OV7725_VSYNC_EXTI_IRQ, 0, 3);
HAL_NVIC_EnableIRQ(OV7725_VSYNC_EXTI_IRQ);
}
代码中把VSYNC引脚配置为浮空模式,并使用下降沿中断(配置成上升沿中断也是可以的),正好对应VGA时序中VSYNC输出信号时电平跳变产生的下降沿。
编写检测VSYNC的中断服务函数
由于VSYNC出现两次下降沿,才表示FIFO保存了一幅图像,所以在检测VSYNC下降沿的中断服务函数中,使用一个变量Ov7725_vsync作为标志。Ov7725_vsync标志的初始值为0,当检测到第一次上升沿时,控制FIFO的相应GPIO引脚,允许OV7725向FIFO写入图像数据,并把标志值设置为1;检测到第二次上升沿时,禁止OV7725写FIFO,把标志设置为2,而我们将会在main函数的循环中对该标志进行判断,当Ov7725_vsync=2时,STM32开始从FIFO读取数据并显示,读取完毕后把Ov7725_vsync标置复位为0,重新开始下一幅图像的采集。
中断服务函数位于stm32f1xx_it.c文件,见 代码清单47_14。
/* ov7725 场中断 服务程序 */
void OV7725_VSYNC_EXTI_INT_FUNCTION ( void )
{
if ( __HAL_GPIO_EXTI_GET_IT(OV7725_VSYNC_GPIO_PIN) != RESET ) {
if ( Ov7725_vsync == 0 ) {
FIFO_WRST_L();//拉低使FIFO写(数据from摄像头)指针复位
FIFO_WE_H(); //拉高使FIFO写允许
Ov7725_vsync = 1;
FIFO_WE_H(); //使FIFO写允许
FIFO_WRST_H();//允许使FIFO写(数据from摄像头)指针运动
} else if ( Ov7725_vsync == 1 ) {
FIFO_WE_L(); //拉低使FIFO写暂停
Ov7725_vsync = 2;
}
__HAL_GPIO_EXTI_CLEAR_IT(OV7725_VSYNC_GPIO_PIN);
}
}
读FIFO并显示图像
采集得的图像数据都保存到摄像头模块的FIFO中,在Ov7725_vsync标志变为2的时候,STM32即可读取它并显示到LCD上。与FIFO相关的函数有FIFO_GPIO_Config、FIFO_PREPARE和ImagDisp 。
FIFO_GPIO_Config类似SCCB_GPIO_Config函数,完成了基本的GPIO初始化,见 代码清单47_15。
static void FIFO_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/*开启时钟*/
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOD_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
/*(FIFO_OE--FIFO输出使能)*/
GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStructure.Pin = OV7725_OE_GPIO_PIN;
HAL_GPIO_Init(OV7725_OE_GPIO_PORT, &GPIO_InitStructure);
/*(FIFO_WRST--FIFO写复位)*/
GPIO_InitStructure.Pin = OV7725_WRST_GPIO_PIN;
HAL_GPIO_Init(OV7725_WRST_GPIO_PORT, &GPIO_InitStructure);
/*(FIFO_RRST--FIFO读复位) */
GPIO_InitStructure.Pin = OV7725_RRST_GPIO_PIN;
HAL_GPIO_Init(OV7725_RRST_GPIO_PORT, &GPIO_InitStructure);
/*(FIFO_RCLK-FIFO读时钟)*/
GPIO_InitStructure.Pin = OV7725_RCLK_GPIO_PIN;
HAL_GPIO_Init(OV7725_RCLK_GPIO_PORT, &GPIO_InitStructure);
/*(FIFO_WE--FIFO写使能)*/
GPIO_InitStructure.Pin = OV7725_WE_GPIO_PIN;
HAL_GPIO_Init(OV7725_WE_GPIO_PORT, &GPIO_InitStructure);
/*(FIFO_DATA--FIFO输出数据)*/
GPIO_InitStructure.Pin = OV7725_DATA_0_GPIO_PIN |
OV7725_DATA_1_GPIO_PIN |
OV7725_DATA_2_GPIO_PIN |
OV7725_DATA_3_GPIO_PIN |
OV7725_DATA_4_GPIO_PIN |
OV7725_DATA_5_GPIO_PIN |
OV7725_DATA_6_GPIO_PIN |
OV7725_DATA_7_GPIO_PIN;
GPIO_InitStructure.Mode = GPIO_MODE_INPUT;
GPIO_InitStructure.Pull = GPIO_NOPULL;
GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(OV7725_DATA_GPIO_PORT, &GPIO_InitStructure);
FIFO_OE_L(); /*拉低使FIFO输出使能*/
FIFO_WE_H(); /*拉高使FIFO写允许*/
}
FIFO_PREPAGE实际是一个宏,它是在main函数中,判断到接收完成一幅图像后被调用的,它的作用是把FIFO读指针复位, 使后面的数据读取从FIFO的0地址开始,其代码见 代码清单47_16 (在工程中,要把宏定义写在同一行或用续行符)。
#define FIFO_PREPARE do{\
FIFO_RRST_L();\
FIFO_RCLK_L();\
FIFO_RCLK_H();\
FIFO_RRST_H();\
FIFO_RCLK_L();\
FIFO_RCLK_H();\
}while(0)
ImagDisp函数完成了从FIFO读取图像及显示到LCD的工作,每当OV7725输出完一幅图像,它被调用一次,在调用前, 要用上面的FIFO_PREAGE宏复位FIFO读指针。它的代码见 代码清单47_17。
/**
* @brief 设置显示位置
* @param sx:x起始显示位置
* @param sy:y起始显示位置
* @param width:显示窗口宽度,要求跟OV7725_Window_Set函数中的width一致
* @param height:显示窗口高度,要求跟OV7725_Window_Set函数中的height一致
* @retval 无
*/
void ImagDisp(uint16_t sx,uint16_t sy,uint16_t width,uint16_t height)
{
uint16_t i, j;
uint16_t Camera_Data;
ILI9341_OpenWindow(sx,sy,width,height);
ILI9341_Write_Cmd ( CMD_SetPixel );
for (i = 0; i < width; i++) {
for (j = 0; j < height; j++) {
/* 从FIFO读出一个rgb565像素到Camera_Data变量 */
READ_FIFO_PIXEL(Camera_Data);
ILI9341_Write_Data(Camera_Data);
}
}
}
在代码中,先根据输入参数在液晶屏设置了显示窗口,然后循环调用宏READ_FIFO_PIXEL读取FIFO数据,循环的次数就是摄像头输出的像素个数, 代码中使用width和height控制,最后使用LCD_WR_Data函数把该图像数据显示到LCD上。宏READ_FIFO_PIXEL 代码见 代码清单47_18 (在工程中,要把宏定义写在同一行或用续行符)。
#define READ_FIFO_PIXEL(RGB565) do{\
RGB565=0;\
FIFO_RCLK_L();\
RGB565 = (OV7725_DATA_GPIO_PORT->IDR) & 0xff00;\
FIFO_RCLK_H();\
FIFO_RCLK_L();\
RGB565 |= (OV7725_DATA_GPIO_PORT->IDR >>8) & 0x00ff;\
FIFO_RCLK_H();\
}while(0)
这个宏把FIFO读取到的数据按RGB565的处理,保存到一个16位的变量中,LCD_WR_Data函数可以直接利用这个数据,显示一个像素点到LCD上。
44.3.2.2.5. main文件¶
利用前面介绍的函数,就可以驱动摄像头采集并显示图像了,关于摄像头模式或分辨率的配置本工程还提供了其它的函数进行修改, 首先来看了解实现了采集流程的最基本的main函数,见 代码清单47_19。
int main(void)
{
float frame_count = 0;
uint8_t retry = 0;
/* 系统时钟初始化成72MHz */
SystemClock_Config();
ILI9341_Init (); //LCD 初始化
ILI9341_GramScan( 3 );
LCD_SetFont(&Font8x16);
LCD_SetColors(RED,BLACK);
ILI9341_Clear(0,0,LCD_X_LENGTH,LCD_Y_LENGTH); /* 清屏,显示全黑 */
/********显示字符串示例*******/
ILI9341_DispStringLine_EN(LINE(0),"BH OV7725 Test Demo");
/* LED 端口初始化 */
LED_GPIO_Config();
/* 初始化串口 */
DEBUG_USART_Config();
Key_GPIO_Config();
SysTick_Init();
printf("\r\n ** OV7725摄像头实时液晶显示例程** \r\n");
/* ov7725 gpio 初始化 */
OV7725_GPIO_Config();
LED_BLUE;
/* ov7725 寄存器默认配置初始化 */
while (OV7725_Init() != SUCCESS) {
retry++;
if (retry>5) {
printf("\r\n没有检测到OV7725摄像头\r\n");
ILI9341_DispStringLine_EN(LINE(2),"No OV7725 module detected!");
while (1);
}
}
/*根据摄像头参数组配置模式*/
OV7725_Special_Effect(cam_mode.effect);
/*光照模式*/
OV7725_Light_Mode(cam_mode.light_mode);
/*饱和度*/
OV7725_Color_Saturation(cam_mode.saturation);
/*光照度*/
OV7725_Brightness(cam_mode.brightness);
/*对比度*/
OV7725_Contrast(cam_mode.contrast);
/*特殊效果*/
OV7725_Special_Effect(cam_mode.effect);
/*设置图像采样及模式大小*/
OV7725_Window_Set(cam_mode.cam_sx,
cam_mode.cam_sy,
cam_mode.cam_width,
cam_mode.cam_height,
cam_mode.QVGA_VGA);
/* 设置液晶扫描模式 */
ILI9341_GramScan( cam_mode.lcd_scan );
ILI9341_DispStringLine_EN(LINE(2),"OV7725 initialize success!");
printf("\r\nOV7725摄像头初始化完成\r\n");
Ov7725_vsync = 0;
while (1) {
/*接收到新图像进行显示*/
if ( Ov7725_vsync == 2 ) {
frame_count++;
FIFO_PREPARE; /*FIFO准备*/
ImagDisp(cam_mode.lcd_sx,
cam_mode.lcd_sy,
cam_mode.cam_width,
cam_mode.cam_height); /*采集并显示*/
Ov7725_vsync = 0;
LED1_TOGGLE;
}
/*每隔一段时间计算一次帧率*/
if (Task_Delay[0] == 0) {
printf("\r\nframe_ate = %.2f fps\r\n",frame_count/10);
frame_count = 0;
Task_Delay[0] = 10000;
}
}
}
main函数的执行流程说明如下:
main函数首先调用了ILI9341_Init、USART_Config 、SysTick_Init和LED_GPIO_Config等函数初始化液晶、串口、Systick定时器和LED外设,其中Systick每ms中断一次,为下面计算帧率的代码提供时间。
接下来OV7725_GPIO_Config函数,该函数内部封装了前面介绍的SCCB_GPIO_Config、FIFO_GPIO_Config及VSYNC_GPIO_Config函数, 对控制摄像头使用的相关引脚都进行了初始化。
初始化好SCCB相关的引脚,就可通过OV7725_Init向OV7725芯片写入配置参数,代码中使用while循环在初始化失败时进行多次尝试。
调用ILI9341_GramScan设置液晶屏的扫描方向,使得液晶屏与摄像头的分辨率一致,做好显示的准备。
在while循环中,根据Ov7725_vsync标志,判断FIFO是否接收完了一幅图像。在中断服务程序中, 若检测到两次VSYNC的下降沿(表示接收完一幅图像),会把Ov7725_vsync变量设置为2。
判断接收完成一幅图像后,调用宏FIFO_PREPARE使读FIFO指针复位,使ImagDisp读取FIFO时,能读取得正确的数据并显示到液晶屏。
记录帧数目的变量frame_count加1,这个变量用来统计帧率,每过一段时间后计算帧率通过串口输出到上位机。 最后把Ov7725_vsync置0,使重新开始计数。
44.3.2.2.6. OV7725的其它模式配置¶
以上是最基本的摄像头采集过程,而提供的工程在以上基础还增加了一些摄像头的配置,包括分辨率、光照度、饱和度、对比度及特殊模式等, 如OV7725_Window_Set、OV7725_Brightness、OV7725_Color_Saturation、OV7725_Contrast和OV7725_Special_Effect等函数, 这些函数的本质都是根据函数的输入参数,转化成对应的配置写入到OV7725摄像头的寄存器中,完成相应的配置,下面仅以OV7725_Special_Effect函数 为例进行介绍,见 代码清单47_20。
/**
* @brief 设置特殊效果
* @param eff:特殊效果,参数范围[0~6]:
@arg 0:正常
@arg 1:黑白
@arg 2:偏蓝
@arg 3:复古
@arg 4:偏红
@arg 5:偏绿
@arg 6:反相
* @retval 无
*/
void OV7725_Special_Effect(uint8_t eff)
{
switch (eff) {
case 0://正常
SCCB_WriteByte(0xa6, 0x06);
SCCB_WriteByte(0x60, 0x80);
SCCB_WriteByte(0x61, 0x80);
break;
case 1://黑白
SCCB_WriteByte(0xa6, 0x26);
SCCB_WriteByte(0x60, 0x80);
SCCB_WriteByte(0x61, 0x80);
break;
case 2://偏蓝
SCCB_WriteByte(0xa6, 0x1e);
SCCB_WriteByte(0x60, 0xa0);
SCCB_WriteByte(0x61, 0x40);
break;
case 3://复古
SCCB_WriteByte(0xa6, 0x1e);
SCCB_WriteByte(0x60, 0x40);
SCCB_WriteByte(0x61, 0xa0);
break;
case 4://偏红
SCCB_WriteByte(0xa6, 0x1e);
SCCB_WriteByte(0x60, 0x80);
SCCB_WriteByte(0x61, 0xc0);
break;
case 5://偏绿
SCCB_WriteByte(0xa6, 0x1e);
SCCB_WriteByte(0x60, 0x60);
SCCB_WriteByte(0x61, 0x60);
break;
case 6://反相
SCCB_WriteByte(0xa6, 0x46);
break;
default:
OV7725_DEBUG("Special Effect error!");
break;
}
}
从代码中可了解到,函数支持0~6作为输入参数,分别对应不同的模式,在函数内部根据不同的输入对寄存器写入相应配置。
44.3.2.2.7. 摄像头配置结构体¶
由于分辨率、光照度、饱和度、对比度及特殊模式等摄像头配置涉及众多内容,特别是关于分辨率的配置,需要与液晶扫描方向匹配, 否则容易出现显示错误的现象,为了方便使用,工程中定义了一个结构体类型专门用于设置摄像头的这些配置,在初始化摄像头或想修改配置的时候, 修改该变量的内容,然后把它作为参数输入到各种配置函数调用即可。该摄像头配置结构体类型见 代码清单47_21。
/*摄像头配置结构体*/
typedef struct {
uint8_t QVGA_VGA; //0:QVGA模式,1:VGA模式
/*VGA:sx + width <= 320 或 240 ,sy+height <= 320 或 240*/
/*QVGA:sx + width <= 320 ,sy+height <= 240*/
uint16_t cam_sx; //摄像头窗口X起始位置
uint16_t cam_sy; //摄像头窗口Y起始位置
uint16_t cam_width;//图像分辨率,宽
uint16_t cam_height;//图像分辨率,高
uint16_t lcd_sx;//图像显示在液晶屏的X起始位置
uint16_t lcd_sy;//图像显示在液晶屏的Y起始位置
uint8_t lcd_scan;//液晶屏的扫描模式(0-7)
uint8_t light_mode;//光照模式,参数范围[0~5]
int8_t saturation;//饱和度,参数范围[-4 ~ +4]
int8_t brightness;//光照度,参数范围[-4~+4]
int8_t contrast;//对比度,参数范围[-4~+4]
uint8_t effect; //特殊效果,参数范围[0~6]:
} OV7725_MODE_PARAM;
结构体类型定义中的代码注释有注明各种配置参数的范围,其中QVGA_VGA可以配置采样图像的模式,cam_sx/y可以设置摄像头采样坐标的原点,cam_width/ height可设置图像分辨率,分辨率调小时,可以提高图像的采集帧率,lcd_sx/y可以配置图像显示在液晶屏的起始位置,而lcd_scan可以设置液晶屏的扫描模式,其余的配置如光照度、饱和度等可以按需求设置。
在配置分辨率时,必须注意调节范围,如QVGA模式下最大为320*240(宽*高),若设置分辨率为240*320(宽*高)时,由于设置分辨率的高度超出QVGA的240限制,会导致出错,此时把对应的模式改成VGA模式即可,因为VGA模式的最大分辨率为640*480(宽*高),除了不超过QVGA、VGA模式的极限外,还要注意它们在液晶屏的扫描模式和起始位置的配置不会超出液晶显示范围。
在工程中,提供了三组摄像头及显示配置范例,实验时可以亲自尝试一下来了解,见 代码清单47_22。
//摄像头初始化配置
//注意:使用这种方式初始化结构体,要在c/c++选项中选择 C99 mode
OV7725_MODE_PARAM cam_mode = {
/*以下包含几组摄像头配置,可自行测试,保留一组,把其余配置注释掉即可*/
/************配置1*********横屏显示*****************************/
.QVGA_VGA = 0, //QVGA模式
.cam_sx = 0,
.cam_sy = 0,
.cam_width = 320,
.cam_height = 240,
.lcd_sx = 0,
.lcd_sy = 0,
.lcd_scan = 3, //LCD扫描模式,本横屏配置可用1、3、5、7模式
//以下可根据自己的需要调整,参数范围见结构体类型定义
.light_mode = 0,//自动光照模式
.saturation = 0,
.brightness = 0,
.contrast = 0,
.effect = 0, //正常模式
/**********配置2*********竖屏显示****************************/
/*竖屏显示需要VGA模式,同分辨率情况下,比QVGA帧率稍低*/
/*VGA模式分辨率为640*480,从中取出240*320的图像进行竖屏显示*/
/*本工程不支持超过320*240或 240*320的分辨率配置*/
// .QVGA_VGA = 1, //VGA模式
// //取VGA模式居中的窗口,可根据实际需要调整
// .cam_sx = (640-240)/2,
// .cam_sy = (480-320)/2,
//
// .cam_width = 240,
// .cam_height = 320, //在VGA模式下,此值才可以大于240
//
// .lcd_sx = 0,
// .lcd_sy = 0,
// .lcd_scan = 0, //LCD扫描模式,本竖屏配置可用0、2、4、6模式
//
// //以下可根据自己的需要调整,参数范围见结构体类型定义
// .light_mode = 0,//自动光照模式
// .saturation = 0,
// .brightness = 0,
// .contrast = 0,
// .effect = 0, //正常模式
/*******配置3************小分辨率****************************/
/*小于320*240分辨率的,可使用QVGA模式,设置的时候注意液晶屏边界*/
// .QVGA_VGA = 0, //QVGA模式
// //取QVGA模式居中的窗口,可根据实际需要调整
// .cam_sx = (320-100)/2,
// .cam_sy = (240-150)/2,
//
// .cam_width = 100,
// .cam_height = 150,
//
// /*液晶屏的显示位置也可以根据需要调整,注意不要超过边界即可*/
// .lcd_sx = 50,
// .lcd_sy = 50,
// .lcd_scan = 3, //LCD扫描模式,0-7模式都支持,注意不要超过边界即可
// //以下可根据自己的需要调整,参数范围见结构体类型定义
// .light_mode = 0,//自动光照模式
// .saturation = 0,
// .brightness = 0,
// .contrast = 0,
// .effect = 0, //正常模式
};
代码中使用OV7725_MODE_PARAM类型定义了一个cam_mode变量并对其结构体成员赋予了初始值。本工程默认使用以上第一组配置,采集320*240的QVGA图像在液晶屏上横屏显示;而第二组配置是240*320的VGA图像在液晶屏上竖屏显示,注意两组配置中QVGA_VGA和lcd_mode变量值的区别;第三组配置是50*50的分辨率,由于分辨率比较小,其宽和高都没有超出QVGA及液晶屏显示范围,所以液晶0-7的扫描方式都支持。可亲自尝试以上各组配置,使用时注释掉其余两组即可,也可以在范例的基础上,自己进行修改测试。
使用摄像头配置结构体时,在初始化摄像头时要调用相应的函数对寄存器进行赋值,所以,main函数需要作出相应的修改,见 代码清单47_23。
int main(void)
{
float frame_count = 0;
uint8_t retry = 0;
/* 系统时钟初始化成72MHz */
SystemClock_Config();
ILI9341_Init (); //LCD 初始化
ILI9341_GramScan( 3 );
LCD_SetFont(&Font8x16);
LCD_SetColors(RED,BLACK);
ILI9341_Clear(0,0,LCD_X_LENGTH,LCD_Y_LENGTH); /* 清屏,显示全黑 */
/********显示字符串示例*******/
ILI9341_DispStringLine_EN(LINE(0),"BH OV7725 Test Demo");
/* LED 端口初始化 */
LED_GPIO_Config();
/* 初始化串口 */
DEBUG_USART_Config();
Key_GPIO_Config();
SysTick_Init();
printf("\r\n ** OV7725摄像头实时液晶显示例程** \r\n");
/* ov7725 gpio 初始化 */
OV7725_GPIO_Config();
LED_BLUE;
/* ov7725 寄存器默认配置初始化 */
while(OV7725_Init() != SUCCESS)
{
retry++;
if(retry>5)
{
printf("\r\n没有检测到OV7725摄像头\r\n");
ILI9341_DispStringLine_EN(LINE(2),"No OV7725 module detected!");
while(1);
}
}
/*根据摄像头参数组配置模式*/
OV7725_Special_Effect(cam_mode.effect);
/*光照模式*/
OV7725_Light_Mode(cam_mode.light_mode);
/*饱和度*/
OV7725_Color_Saturation(cam_mode.saturation);
/*光照度*/
OV7725_Brightness(cam_mode.brightness);
/*对比度*/
OV7725_Contrast(cam_mode.contrast);
/*特殊效果*/
OV7725_Special_Effect(cam_mode.effect);
/*设置图像采样及模式大小*/
OV7725_Window_Set(cam_mode.cam_sx,
cam_mode.cam_sy,
cam_mode.cam_width,
cam_mode.cam_height,
cam_mode.QVGA_VGA);
/* 设置液晶扫描模式 */
ILI9341_GramScan( cam_mode.lcd_scan );
ILI9341_DispStringLine_EN(LINE(2),"OV7725 initialize success!");
printf("\r\nOV7725摄像头初始化完成\r\n");
Ov7725_vsync = 0;
while(1)
{
/*接收到新图像进行显示*/
if( Ov7725_vsync == 2 )
{
frame_count++;
FIFO_PREPARE; /*FIFO准备*/
ImagDisp(cam_mode.lcd_sx,
cam_mode.lcd_sy,
cam_mode.cam_width,
cam_mode.cam_height); /*采集并显示*/
Ov7725_vsync = 0;
LED1_TOGGLE;
}
/*检测按键*/
if( Key_Scan(KEY1_GPIO_PORT,KEY1_PIN) == KEY_ON )
{
/*LED反转*/
LED2_TOGGLE;
}
/*检测按键*/
if( Key_Scan(KEY2_GPIO_PORT,KEY2_PIN) == KEY_ON )
{
/*LED反转*/
LED3_TOGGLE;
/*动态配置摄像头的模式,
有需要可以添加使用串口、用户界面下拉选择框
方式修改这些变量,
达到程序运行时更改摄像头模式的目的*/
cam_mode.QVGA_VGA = 0, //QVGA模式
cam_mode.cam_sx = 0,
cam_mode.cam_sy = 0,
cam_mode.cam_width = 320,
cam_mode.cam_height = 240,
cam_mode.lcd_sx = 0,
cam_mode.lcd_sy = 0,
cam_mode.lcd_scan = 3,
//LCD扫描模式,本横屏配置可用1、3、5、7模式
//以下可根据自己的需要调整,参数范围见结构体类型定义
cam_mode.light_mode = 0,//自动光照模式
cam_mode.saturation = 0,
cam_mode.brightness = 0,
cam_mode.contrast = 0,
cam_mode.effect = 1, //黑白模式
/*根据摄像头参数写入配置*/
OV7725_Special_Effect(cam_mode.effect);
/*光照模式*/
OV7725_Light_Mode(cam_mode.light_mode);
/*饱和度*/
OV7725_Color_Saturation(cam_mode.saturation);
/*光照度*/
OV7725_Brightness(cam_mode.brightness);
/*对比度*/
OV7725_Contrast(cam_mode.contrast);
/*特殊效果*/
OV7725_Special_Effect(cam_mode.effect);
/*设置图像采样及模式大小*/
OV7725_Window_Set(cam_mode.cam_sx,
cam_mode.cam_sy,
cam_mode.cam_width,
cam_mode.cam_height,
cam_mode.QVGA_VGA);
/* 设置液晶扫描模式 */
ILI9341_GramScan( cam_mode.lcd_scan );
}
/*每隔一段时间计算一次帧率*/
if(Task_Delay[0] == 0)
{
printf("\r\nframe_ate = %.2f fps\r\n",frame_count/10);
frame_count = 0;
Task_Delay[0] = 10000;
}
}
}
相对于前面介绍的摄像头基本初始化过程,本main函数主要增加了对OV7725_Window_Set、OV7725_Brightness、OV7725_Color_Saturation、OV7725_Contrast和OV7725_Special_Effect等函数的调用,根据摄像头配置结构体cam_mode向OV7725写入寄存器内容,初始化好后,摄像头图像的采集和显示与普通方式无异。
代码中还增加了按键检测,按下了开发板的KEY2按键后,会向摄像头配置结构体cam_mode赋予新的配置并写入到OV7725寄存器中,以上代码把采集的图像设置成了黑白模式。
44.3.2.3. 下载验证¶
把摄像头模块接入到开发板的摄像头接口上,用USB线连接开发板,编译程序下载到实验板并上电复位。即可看到LCD上输出摄像头拍到的图像,若图片显示不够清晰,可调整镜头进行调焦,使得到清晰的图像。