第五十章 摄像头实验
STM32H7R7具有DCMIPP接口,并板载了一个摄像头接口(P7),该接口可以用来连接正点原子OV5640/OV2640/OV7725等摄像头模块。本章,我们将使用STM32驱动正点原子 OV5640摄像头模块,实现摄像头功能。
本章分为如下几个小节:
50.1 OV5640和DCMIPP简介
50.2 硬件设计
50.3 程序设计
50.4 下载验证
50.1 OV5640和DCMIPP简介
本节将分为两个部分,分别介绍OV5640和STM32H7R7的DCMIPP接口简介。另外,所有OV5640的相关资料,都在光盘:A盘→7,硬件资料→OV5640资料 文件夹里面。
50.1.1 OV5640简介
OV5640是OV(OmniVision)公司生产的一颗1/4寸的CMOS QSXGA(2592*1944)图像传感器,提供了一个完整的500W像素摄像头解决方案,并且集成了自动对焦(AF)功能,具有非常高的性价比。
该传感器体积小、工作电压低,提供单片QSXGA摄像头和影像处理器的所有功能。通过SCCB 总线控制,可以输出整帧、子采样、缩放和取窗口等方式的各种分辨率8/10位影像数据。该产品QSXGA图像最高达到15帧/秒(1080P图像可达30帧,720P图像可达60帧,QVGA分辨率时可达120帧)。用户可以完全控制图像质量、数据格式和传输方式。所有图像处理功能过程包括伽玛曲线、白平衡、对比度、色度等都可以通过SCCB接口编程。OmmiVision 图像传感器应用独有的传感器技术,通过减少或消除光学或电子缺陷如固定图案噪声、拖尾、浮散等,提高图像质量,得到清晰稳定的彩色图像。
OV5640的特点有:
l 采用1.4μm*1.4μm像素大小,并且使用OmniBSI技术以达到更高性能(高灵敏度、低串扰和低噪声)
l 自动图像控制功能:自动曝光(AEC)、自动白平衡(AWB)、自动消除灯光条纹、自动黑电平校准(ABLC)和自动带通滤波器(ABF)等。
l 支持图像质量控制:色饱和度调节、色调调节、gamma校准、锐度和镜头校准等
l 标准的SCCB接口,兼容IIC接口
l 支持RawRGB、RGB(RGB565/RGB555/RGB444)、CCIR656、YUV(422/420)、YCbCr(422)和压缩图像(JPEG)输出格式
l 支持QSXGA(500W)图像尺寸输出,以及按比例缩小到其他任何尺寸
l 支持闪光灯
l 支持图像缩放、平移和窗口设置
l 支持图像压缩,即可输出JPEG图像数据
l 支持数字视频接口(DVP)和MIPI接口
l 支持自动对焦
l 自带嵌入式微处理器
OV5640的功能框图图如图50.1.1.1所示:

图50.1.1.1 OV5640功能框图
其中image array部分的尺寸,OV5640的官方数据并没有给出具体的数字,其最大的有效输出尺寸为:2592*1944,即500W像素,我们根据官方提供的一些应用文档,发现其设置的image array最大为:2632*1951,所以,在接下来的介绍,我们设定其image array最大为2632*1951。
1、DVP接口说明
OV5640支持数字视频接口(DVP)和MIPI接口,因为我们的STM32H7R7使用的DCMIPP接口,仅支持DVP接口,所以,OV5640必须使用DVP输出接口,才可以连接我们的STM32开发板。
OV5640提供一个10位DVP接口(支持8位接法),其MSB和LSB可以程序设置先后顺序,正点原子OV5640模块采用默认的8位连接方式,如图50.1.1.2所示:

图50.1.1.2 OV5640默认8位连接方式
OV5640的寄存器通过SCCB时序访问并设置,SCCB时序和IIC时序十分类似。在本章我们不做介绍,请大家参考光盘《OmniVision Technologies Seril Camera Control Bus(SCCB) Specification》这个文档。
2、窗口设置说明
接下来,我们介绍一下OV5640的:ISP(Image Signal Processor)输入窗口设置、预缩放窗口设置和输出大小窗口设置,这几个设置与我们的正常使用密切相关,有必要了解一下。他们的设置关系,如图50.1.1.3所示:

图50.1.1.3 OV5640各窗口设置关系
ISP输入窗口设置(ISP input size)
该设置允许用户设置整个传感器区域(physical pixel size ,2632*1951)的感兴趣部分,也就是在传感器里面开窗(X_ADDR_ST、Y_ADDR_ST、X_ADDR_END和Y_ADDR_END),开窗范围从0*0~2632*1951都可以设置,该窗口所设置的范围,将输入ISP进行处理。
ISP输入窗口,通过:0X3800~0X3807等8个寄存器进行设置,这些寄存器的定义请看:OV5640_CSP3_DS_2.01_Ruisipusheng.pdf 这个文档(下同)。
预缩放窗口设置(pre-scaling size)
该设置允许用户在ISP输入窗口的基础上,再次设置将要用于缩放的窗口大小。该设置仅在ISP输入窗口内进行x/y方向的偏移(X_OFFSET/Y_OFFSET)。通过:0X3810~0X3813等4个寄存器进行设置。
输出大小窗口设置(data output size)
该窗口是以预缩放窗口为原始大小,经过内部DSP进行缩放处理后,输出给外部的图像窗口大小。它控制最终的图像输出尺寸(X_OUTPUT_SIZE/Y_OUTPUT_SIZE)。通过:0X3808~0X380B等4个寄存器进行设置。注意:当输出大小窗口与预缩放窗口比例不一致时,图像将进行缩放处理(会变形),仅当两者比例一致时,输出比例才是1:1(正常)。
图50.1.1.3中,右侧data output size区域,才是OV5640输出给外部的图像尺寸,也就是显示在LCD上面的图像大小。输出大小窗口与预缩放窗口比例不一致时,会进行缩放处理,在LCD上面看到的图像将会变形。
3、输出时序说明
接下来,我们介绍一下OV5640的图像数据输出时序。首先我们简单介绍一些定义:
QSXGA,这里指:分辨率为2592*1944的输出格式,类似的还有:QXGA(2048*1536)、UXGA(1600*1200)、SXGA(1280*1024)、WXGA+(1440*900)、WXGA(1280*800)、XGA(1024*768)、SVGA(800*600)、VGA(640*480)、QVGA(320*240)和QQVGA(160*120)等。
PCLK,即像素时钟,一个PCLK时钟,输出一个像素(或半个像素)。
VSYNC,即帧同步信号。
HREF /HSYNC,即行同步信号。
OV5640的图像数据输出(通过Y[9:0])就是在PCLK,VSYNC和HREF/ HSYNC的控制下进行的。首先看看行输出时序,如图50.1.1.4所示:

图50.1.1.4 OV5640行输出时序
从上图可以看出,图像数据在HREF为高的时候输出,当HREF变高后,每一个PCLK时钟,输出一个8位/10位数据。我们采用8位接口,所以每个PCLK输出1个字节,且在RGB/YUV输出格式下,每个tp=2个Tpclk,如果是Raw格式,则一个tp=1个Tpclk。比如我们采用QSXGA时序,RGB565格式输出,每2个字节组成一个像素的颜色(低字节在前,高字节在后),这样每行输出总共有2592*2个PCLK周期,输出2592*2个字节。
再来看看帧时序(QSXGA模式),如图50.1.1.5所示:

图50.1.1.5 OV5640帧时序
上图清楚的表示了OV5640在QSXGA模式下的数据输出。我们按照这个时序去读取OV5640的数据,就可以得到图像数据。
4、自动对焦(Auto Focus)说明
OV5640由内置微型控制器完成自动对焦,并且VCM(Voice Coil Motor,即音圈马达)驱动器也已集成在传感器内部。微型控制器的控制固件(firmware)从主机下载。当固件运行后,内置微型控制器从OV5640传感器读得自动对焦所需的信息,计算并驱动VCM马达带动镜头到达正确的对焦位置。主机可以通过IIC命令控制微型控制器的各种功能。
OV5640的自动对焦命令(通过SCCB总线发送),如表50.1.1.1所示:

表50.1.1.1 OV5640自动对焦命令
OV5640内部的微控制器收到自动对焦命令后会自动将CMD_MAIN(0X3022)寄存器数据清零,当命令完成后会将CMD_ACK(0X3023)寄存器数据清零。
自动对焦(AF)过程
① 在第一次进入图像预览的时候(图像可以正常输出时),下载固件(firmware)。
② 拍照前,自动对焦,对焦完成后,拍照。
③ 拍照完毕,释放马达到初始状态。
接下来,我们分别说明。
① 下载固件
OV5640初始化完成后,就可以下载AF自动对焦固件了,其操作和下载初始化参数类似,AF固件下载地址为:0X8000,初始化数组由厂家提供(本例程该数组保存在ov5640af.h里面),下载固件完成后,通过检查0X3029寄存器的值,来判断固件状态(等于0X70,说明正常)。
② 自动对焦
OV5640支持单次自动对焦和持续自动对焦,通过0X3022寄存器控制。单次自动对焦过程如下:
1,将0X3022寄存器写为0X03,开始单点对焦过程。
2,读取寄存器0X3029,如果返回值为0X10,代表对焦已完成。
3,写寄存器0X3022为0X06,暂停对焦过程,使镜头将保持在此对焦位置。
其中,前两步是必须的,第三步,可以不要,因为单次自动对焦完成以后,就不会继续自动对焦了,镜头也就不会动了。
持续自动对焦过程如下:
1, 将0X22寄存器写为0X08,释放马达到初始位置(对焦无穷远)。
2, 将0X3022寄存器写为0X04,启动持续自动对焦过程。
3, 读取寄存器0X3023,等待命令完成。
4, 当OV5640每次检测到失焦时,就会自动进行对焦(一直检测)。
③ 释放马达,结束自动对焦
最后,在拍照完成,或者需要结束自动对焦的时候,我们对在寄存器0X3022写入0X08,即可释放马达,结束自动对焦。
最后说一下OV5640的图像数据格式,我们一般用2种输出方式:RGB565和JPEG。当输出RGB565格式数据的时候,时序完全就是上面两幅图介绍的关系。以满足不同需要。而当输出数据是JPEG数据的时候,同样也是这种方式输出(所以数据读取方法一模一样),不过PCLK数目大大减少了,且不连续,输出的数据是压缩后的JPEG数据,输出的JPEG数据以:0XFF,0XD8开头,以0XFF,0XD9结尾,且在0XFF,0XD8之前,或者0XFF,0XD9之后,会有不定数量的其他数据存在(一般是0),这些数据我们直接忽略即可,将得到的0XFF,0XD8~0XFF,0XD9之间的数据,保存为.jpg/.jpeg文件,就可以直接在电脑上打开看到图像了。
OV5640自带的JPEG输出功能,大大减少了图像的数据量,使得其在网络摄像头、无线视频传输等方面具有很大的优势。OV5640我们就介绍到这,关于OV5640更详细的介绍,请大家参考:A盘→7,硬件资料→OV5640资料→OV5640_CSP3_DS_2.01_Ruisipusheng.pdf。
正点原子OV5640摄像头模块
本实验,我们将使用STM32H7R7开发板的DCMIPP接口连接正点原子OV5640摄像头模块,该模块采用8位数据输出接口,自带24M有源晶振,无需外部提供时钟,模组支持自动对焦功能,且支持闪光灯,整个模块只需提供3.3V 供电即可正常使用。
正点原子OV5640摄像头模块外观如图50.1.1.6所示:

图50.1.1.6 正点原子OV5640摄像头模块外观图
模块原理图如图50.1.1.7所示:

图50.1.1.7 正点原子OV5640摄像头模块原理图
从上图可以看出,正点原子OV5640摄像头模块自带了有源晶振,用于产生24M时钟作为OV5640的XCLK输入,模块的闪光灯(LED1&LED2)由OV5640的STROBE脚控制(可编程控制)。同时自带了稳压芯片,用于提供OV5640稳定的2.8V和1.5V工作电压,模块通过一个2*9的双排排针(P7)与外部通信,与外部的通信信号如表50.1.1.2所示:

表50.1.1.2 OV5640模块信号及其作用描述
50.1.2 STM32H7R7 DCMIPP接口简介
STM32H7R7自带了一个数字摄像头(DCMIPP)接口,该接口是一个同步并行接口,能够将获取的像素经过处理后(如抽取、裁剪)存入到内存中。DCMIPP主要特性如下:
l 集成输入并行接口(支持16bit、120MHz,)
l 像素格式:RGB565、RGB888、YUV422、原始拜尔(Bayer)格式/Mono 8/10/12/14 和比特流(JPEG)
l Pipe转存最大支持分辨率为4096*4096
l 典型转存帧率为30fps,通过降低图像分辨率可达到120fps
l 通过pipe管进行传输和管理
DCMIPP接口包括如下一些信号:
1, 数据输入(D[0:13]),用于接摄像头的数据输出,接OV5640我们只用了8位数据。
2, 水平同步(行同步)输入(HSYNC),用于接摄像头的HSYNC/HREF信号。
3, 垂直同步(场同步)输入(VSYNC),用于接摄像头的VSYNC信号。
4, 像素时钟输入(dcmipp_pxclk),用于接摄像头的PCLK信号。
DCMIPP接口是一个同步并行接口,可接收高速(可达120MB/s)数据流。该接口包含多达14条数据线(D13-D0)和一条像素时钟线(dcmipp_pxclk)。像素时钟的极性可以编程,因此可以在像素时钟的上升沿或下降沿捕获数据。
从摄像头接收的数据可以按行/帧来组织(原始YUV/RGB/拜尔模式),也可以是一系列 JPEG图像。
数据流可由可选的 HSYNC(水平同步)信号和 VSYNC(垂直同步)信号硬件同步,或者通 过数据流中嵌入的同步码同步。
50.1.2.1 DCMIPP接口功能概述
STM32H7R7 DCMIPP接口的框图如图50.1.2.1.1所示:

图50.1.2.1.1 DCMIPP接口框图
上图左边可以看到的是DCMIPP的内部信号,如下表所示:

表50.1.2.1.1 DCMIPP内部信号
dcmipp_it_global,DCMIPP 中断信号。DCMIPP每输出一帧完整的图像数据时,就会产生一个DCMIPP中断,我们接收到中断后,就可以处理pipe0传输到指定内存的图像数据了。
dcmipp_pclk和dcmipp_pclk,DCMIPP 接口时钟,时钟源自来于APB时钟或者AXI时钟。
上图左边的DCMIPP物理接口,具体如下表所示:

表50.1.2.1.3 DCMIPP物理接口
该接口由1/13/15/17个输入信号组成。仅支持从模式。根据 DCMIPP_PRCR 寄存器中 EDM[1:0] 位的设置,摄像头接口可以捕获16位数据。如果使用的位数少于16,则必须将未使用的输入引脚接地。
DCMIPP数据与dcmipp_pxclk(即PCLK)保持同步,并根据像素时钟的极性在像素时钟上升沿/下降沿发生变化。
DCMIPP_HSYNC(HREF)信号指示行的开始/结束。
DCMIPP_VSYNC 信号指示帧的开始/结束。
本实验我们用到DCMIPP的8位数据宽度,通过设置DCMIPP_PRCR中的EDM[2:0]=00设置。此时DCMIPP_D0~D7有效,DCMIPP_D8~D15上的数据则忽略,这个时候,每次需要1个像素时钟来捕获一个8位数据。
DCMIPP接口支持两种同步方式:内嵌码同步和硬件(HSYNC和VSYNC)同步。我们使用硬件同步,硬件同步模式下将使用两个同步信号 (HSYNC/VSYNC)。根据摄像头模块/模式的不同,可能在水平/垂直同步期间内发送数据。由于系统会忽略HSYNC/VSYNC信号有效电平期间内接收的所有数据,HSYNC/VSYNC 信号相当于消隐信号。
为了正确地将图像传输到指定内存缓冲区,数据传输将与VSYNC信号同步。选择硬件同步模式并启用捕获(DCMIPP_PRCR中的ESS位置0)时,数据传输将与VSYNC信号的 无效电平同步(开始下一帧时)。之后传输便可以连续执行,由Pipe0将连续帧传输到多个连续的缓冲区或一个具有循环特性的缓冲区。为了Pipe管理连续帧,每一帧结束时都由事件管理器输出事件,并产生DCMIPP中断,我们可以利用这个帧中断来判断是否有一帧数据采集完成,方便处理数据。
DCMIPP接口的捕获模式支持:快照模式和连续采集模式。一般我们使用连续采集模式,通过DCMIPP_P0FCTCR中的CPTMODE位设置。
关于DCMIPP接口的其他特性,我们这里就不再介绍了,请大家参考《STM32H7Rx参考手册_V6(英文版).pdf》第31章相关内容。
50.1.2.2 DCMIPP寄存器
本实验,我们将OV5640默认配置为QVGA输出,也就是320*240的分辨率,输出信号设置为:VSYNC高电平有效,HREF高电平有效,输出数据在PCLK的下降沿输出(即上升沿的时候,MCU才可以采集)。这样,STM32H7R7的DCMIPP接口就必须设置为:VSYNC低电平有效、HSYNC低电平有效和PIXCLK上升沿有效,这些设置都是通过DCMIPP_PRCR寄存器控制。
l DCMIPP 并行接口控制寄存器(DCMIPP_PRCR)
DCMIPP并行接口控制寄存器描述如图50.1.2.2.1所示:

图50.1.2.2.1 DCMIPP_PRCR寄存器
ENABLE,该位用于设置是否使能DCMIPP,不过,在使能之前,必须将其他配置设置好。
FORMAT[7:0],该位用于设置格式,我们用设置为RGB565格式。
EDM[2:0],设置扩展数据模式,选择000:接口每个像素时钟捕获8位数据。
VSPOL,该位用于设置垂直同步极性,也就是VSYNC引脚上面,数据无效时的电平状态,根据前面说所,我们应该设置为0。
HSPOL,该位用于设置水平同步极性,也就是HSYNC引脚上面,数据无效时的电平状态,同样应该设置为0。
PCKPOL,该位用于设置像素时钟极性,我们用上升沿捕获,所以设置为1。
ESS,内嵌码同步选择,我们选择硬件同步,默认置0。
DCMIPP_PRCR寄存器的其他位,我们就不介绍了,另外DCMIPP的其他寄存器这里也不再介绍,请大家参考《STM32H7Rx参考手册_V6(英文版).pdf》第31.10.8小节。
50.2 硬件设计
1. 例程功能
1、本实验开机后,初始化摄像头模块(OV5640),如果初始化成功,则提示选择模式:RGB565模式或者JPEG模式。KEY0用于选择RGB565模式,KEY1用于选择JPEG模式。
2、当使用RGB565时,输出图像(固定为:WXGA)将经过缩放处理(完全由OV5640的DSP控制),显示在LCD上面(默认开启连续自动对焦)。我们可以通过KEY_UP按键选择:1:1显示,即不缩放,图片不变形,但是显示区域小(液晶分辨率大小),或者缩放显示,即将1280*800的图像压缩到液晶分辨率尺寸显示,图片变形,但是显示了整个图片内容。通过KEY0按键,可以执行一次自动对焦;KEY1按键,可以设置特效;KEY_UP按键,可以设置尺寸缩放。
3、当使用JPEG模式时,图像可以设置默认是QVGA 320*240尺寸,采集到的JPEG数据将先存放到STM32的RAM内存里面,每当采集到一帧数据,就会关闭DMA传输,然后将采集到的数据发送到串口2(此时可以通过上位机软件(ATK-CAM.exe)接收,并显示图片),之后再重新启动DMA传输。通过按键KEY_UP设置输出图片的尺寸、按键KEY0设置对比度、按键KEY1启动单次自动对焦、按键KEY2设置特效。
4、LED0闪烁,提示程序运行。LED1用于指示帧中断。
2. 硬件资源
1)LED灯
LED0 :LED0 – PD14
LED1: LED1 – PC0
2)串口1(PB14/PB15连接在板载USB转串口芯片CH340上面)
3)正点原子2.8/3.5/4.3/7/10寸TFTLCD模块(包括MCU屏和RGB屏,都支持)
4)独立按键 :KEY0 – PE9、KEY1 – PE8、KEY2 - PE7、WK_UP - PC13
5)串口2 (USART2_TX – PD5,外接串口线/用杜邦线连接到CH340上面)
6)DCMIPP接口(用于驱动OV5640摄像头模块)
7)定时器6(用于打印摄像头帧率等信息)
8)正点原子OV5640摄像头模块,连接关系为:
OV5640模块 ----------- STM32开发板
OV_D0~D7 ------------ PC6/PC7/PE0/PE1/PE4/PB6/PB8/PB9
OV_SCL ------------ PM3
OV_SDA ------------ PM2
OV_VSYNC ------------ PB7
OV_HSYNC ------------ PG3
OV_RST ------------ PE2
OV_PCLK ------------ PA6
OV_PWDN ------------ PF5
3. 原理图
开发板板载的摄像头模块接口与MCU的连接关系,如下图所示:

图50.2.1 摄像头接口与开发板连接示意图
这些GPIO口的线都在开发板上连接到P7端口,所以我们只需要将OV5640摄像头模块插上开发板的连接座子就好了(摄像头模块正面往外插)。

图50.2.2 OV5640摄像头模块与开发板连接实物图
50.3 程序设计
50.3.1 DCMIPP的HAL库驱动
DCMIPP在HAL库中的驱动代码在stm32h7rsxx_hal_dcmipp.c文件(及其头文件)中。
1. HAL_DCMIPP_Init函数
DCMIPP初始化函数,其声明如下:
HAL_StatusTypeDef HAL_DCMIPP_Init(DCMIPP_HandleTypeDef *hdcmipp);
l 函数描述:
用于初始化DCMIPP。
l 函数形参:
形参1是DCMIPP_HandleTypeDef结构体类型指针变量,其定义如下:
typedef struct
{
DCMIPP_TypeDef *Instance; /* DCMIPP 外设寄存器基地址 */
__IO HAL_DCMIPP_StateTypeDef State; /* DCMIPP 工作状态 */
__IO HAL_DCMIPP_PipeStateTypeDef PipeState[DCMIPP_NUM_OF_PIPES];
/* DCMIPP 管道状态 */
__IO uint32_t ErrorCode; /* DCMIPP 错误代码 */
} DCMIPP_HandleTypeDef;
下面重点介绍DCMIPP_ParallelConfTypeDef结构体,其定义如下:
typedef struct
{
uint32_t Format; /* 格式 */
uint32_t VSPolarity; /* 设置垂直同步的有效电平 */
uint32_t HSPolarity; /* 设置水平同步的有效边沿 */
uint32_t PCKPolarity; /* 设置像素时钟的有效边沿 */
uint32_t ExtendedDataMode ; /* 设置数据线的宽度(扩展数据模式) */
uint32_t SynchroMode; /* 同步方式选择硬件同步模式还是内嵌码模式 */
DCMIPP_EmbeddedSyncCodesTypeDef SynchroCodes; /* 分隔符设置 */
uint32_t SwapBits; /* 使能/失能Swap位*/
uint32_t SwapCycles; /* 使能/失能Swap循环 */
} DCMIPP_ParallelConfTypeDef;
1) Format::用于设置格式,我们选择的是RGB565模式。
2) VSPolarity:用于设置VSYNC的有效电平,当VSYNC信号线表示为有效电平时,表示新的一帧数据传输完成,它可以被设置为高电平有效或低电平有效。
3) HSPolarity:用于设置HSYNC的有效电平,当 HSYNC 信号线表示为有效电平时,表示新的一行数据传输完成,它可以被设置为高电平有效或低电平有效。
4) PCKPolarity:用来设置像素时钟极性为上升沿有效还是下降沿有效。
5) ExtendedDataMode:用于设置扩展数据模式,可设置为8/10/12或者14位数据宽度。
6) SynchroMode:用于设置 DCMI数据的同步模式,可以选择为硬件同步方式或内嵌码方式。
如果选择硬件同步值DCMI_SYNCHRO_HARDWARE,那么数据捕获由HSYNC/VSYNC信号同步,如果选择内嵌码同步方式值DCMI_SYNCHRO_EMBEDDED,那么数据捕获由数据流中嵌入的同步码同步。
7) SyncroCodes:用于设置分隔码,包括:帧结束分隔码,行结束分隔码,行开始分隔码以及帧开始分隔码。
8) SwapBits:用于使能或失能Swap位。
9) SwapCycles:用于使能或失能Swap循环。
l 函数返回值:
HAL_StatusTypeDef枚举类型的值。
DCMIPP数据配置步骤
1)配置OV5640控制引脚,并配置OV5640工作模式。
在启动DCMIPP之前,我们先设置好OV5640。OV5640通过OV_SCL和OV_SDA进行寄存器配置,同时还有OV_PWDN/OV_RESET等信号,我们也需要配置对应IO状态,先设置OV_PWDN为0,退出掉电模式,然后拉低OV_RESET复位OV5640,之后再设置OV_RESET为1,结束复位,然后就是对OV5640的寄存器进行配置了。然后,可以根据我们的需要,设置成RGB565输出模式,还是JPEG输出模式。
2)配置相关引脚的模式和复用功能(AF13),使能时钟。
OV5640配置好之后,再设置DCMIPP接口与摄像头模块连接的IO口,使能IO和DCMIPP时钟,然后设置相关IO口为复用功能模式,复用功能选择AF13(DCMIPP复用)。
DCMIPP时钟使能方法:
__HAL_RCC_DCMIPP_CLK_ENABLE();
引脚模式配置就是通过HAL_GPIO_Init函数来配置。
3)配置DCMIPP相关设置,初始化DCMIPP接口。
这一步,主要通过DCMIPP_PRCR寄存器设置,包括VSPOL/HSPOL/PCKPOL/数据宽度等重要参数,都在这一步设置。HAL库提供了DCMIPP初始化函数HAL_DCMIPP_Init用于注册DCMIPP,用HAL_DCMIPP_PIPE_SetConfig函数配置DCMIPP管道,初始化函数声明如下:
HAL_StatusTypeDef HAL_DCMIPP_Init(DCMIPP_HandleTypeDef *hdcmipp);
该结构体第一个成员变量Instance用来指向寄存器基地址,设置为DCMIPP即可。
其他三个成员变量主要是DCMIPP的一些状态信息,重点需要配置的是DCMIPP管通道配置,函数声明如下:
HAL_StatusTypeDef HAL_DCMIPP_PARALLEL_SetConfig(DCMIPP_HandleTypeDef *hdcmipp, const DCMIPP_ParallelConfTypeDef *pParallelConfig);
第一个结构体参数是使用对应的DCMIPP,第二个结构体参数主要是DCMIPP的配置,我们重点看第二个DCMIPP_ParallelConfTypeDef就好,上面各个成员变量的作用都有介绍到,如Format是配置图像格式,我们这里就不一一介绍了。
同样,HAL库也提供了DCMIPP接口的MSP初始化回调函数:
void HAL_DCMI_MspInit(DCMI_HandleTypeDef* hdcmi);
一般情况下,该函数内部编写时钟使能,IO初始化以及NVIC相关程序。
4)配置DCMIPP管道,开启中断。
本实验我们采用连续模式采集,并将采集到的数据输出到LCD(RGB565模式)或内存(JPEG模式),我们通过PIPE通道进行转存,我们配置好PIPE通道后,通过DCMIPP_P0PPM0AR1寄存器设置图像数据的存放地址,也就是目的地址,目的地址是我们开辟的一片转存地址,大小为1024 * 1024 * 2 / 4。DCMIPP的PIPE管通过有专用的事件管理和DCMIPP中断,我们通过这些标志就可以获取摄像头图像采集的信息了。
5)设置OV5640的图像输出大小,使能DCMIPP捕获。
图像输出大小设置,分两种情况:在RGB565模式下,我们根据LCD的尺寸,设置输出图像大小,以实现全屏显示(图像可能因缩放而变形);在JPEG模式下,我们可以自由设置输出图像大小(可不缩放);最后,开启DCMIPP捕获,即可正常工作了。
50.3.2 程序解析
1. DCMIPP驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。DCMIPP驱动源码包括两个文件:dcmipp.c和dcmipp.h。
dcmipp.h头文件只是一些声明,下面直接开始介绍dcmipp.c文件,首先是DCMIPP初始化函数,其定义如下:
/**
* @brief DCMIPP 初始化
* @note IO对应关系如下:
* 摄像头模块 ------------ STM32开发板
* OV_D0~D7 ------------ PC6/PC7/PE0/PE1/PE4/PB6/PB8/PB9
* OV_SCL ------------ PM3
* OV_SDA ------------ PM2
* OV_VSYNC ------------ PB7
* OV_HREF ------------ PG3
* OV_RESET ------------ PE2
* OV_PCLK ------------ PA6
* OV_PWDN ------------ PF5
* 本函数仅初始化OV_D0~D7/OV_VSYNC/OV_HREF/OV_PCLK等信号(11个).
* @param 无
* @retval 无
*/
void dcmipp_init(void)
{
/* 初始化DCMIPP */
g_dcmipp_handle.Instance = DCMIPP;
HAL_DCMIPP_Init(&g_dcmipp_handle);
g_dcmipp_parallelcfg.Format = DCMIPP_FORMAT_RGB565;
/* RGB565帧格式 */
g_dcmipp_parallelcfg.SwapCycles = DCMIPP_SWAPCYCLES_ENABLE;
g_dcmipp_parallelcfg.SwapBits = DCMIPP_SWAPBITS_DISABLE;
g_dcmipp_parallelcfg.VSPolarity = DCMIPP_VSPOLARITY_LOW;
/* VSYNC 低电平有效 */
g_dcmipp_parallelcfg.HSPolarity = DCMIPP_HSPOLARITY_LOW;
/* HSYNC 低电平有效 */
g_dcmipp_parallelcfg.PCKPolarity = DCMIPP_PCKPOLARITY_RISING;
/* PCLK上升沿有效 */
g_dcmipp_parallelcfg.ExtendedDataMode = DCMIPP_INTERFACE_8BITS;
/* 8位数据格式 */
g_dcmipp_parallelcfg.SynchroMode = DCMIPP_SYNCHRO_HARDWARE;
/* 硬件同步HSYNC,VSYNC */
g_dcmipp_parallelcfg.SynchroCodes.FrameEndCode = 0;
g_dcmipp_parallelcfg.SynchroCodes.FrameStartCode = 0;
g_dcmipp_parallelcfg.SynchroCodes.LineEndCode = 0;
g_dcmipp_parallelcfg.SynchroCodes.LineStartCode = 0;
HAL_DCMIPP_PARALLEL_SetConfig(&g_dcmipp_handle, &g_dcmipp_parallelcfg);
g_dcmipp_pipecfg.FrameRate = DCMIPP_FRAME_RATE_ALL; /* 全帧捕获 */
HAL_DCMIPP_PIPE_SetConfig(&g_dcmipp_handle, DCMIPP_PIPE0,
&g_dcmipp_pipecfg);
/* 使能DCMIPP及其帧捕获完成中断 */
__HAL_DCMIPP_DISABLE_IT(&g_dcmipp_handle, DCMIPP_IT_PIPE0_FRAME |
DCMIPP_IT_PIPE0_OVR | DCMIPP_IT_AXI_TRANSFER_ERROR |
DCMIPP_IT_PARALLEL_SYNC_ERROR | DCMIPP_IT_PIPE0_VSYNC |
DCMIPP_IT_PIPE0_LINE);
__HAL_DCMIPP_ENABLE_IT(&g_dcmipp_handle, DCMIPP_IT_PIPE0_FRAME);
}
该函数主要对DCMIPP_HandleTypeDef结构体成员赋值并初始化,最后关闭所有中断,只开启帧中断,使能DCMIPP。而DCMIPP接口的GPIO口的初始化是在HAL_DCMIPP_MspInit回调函数中完成,其定义如下:
/**
* @brief HAL库DCMIPP初始化MSP回调函数
* @param phdcmipp: DCMIPP句柄指针
* @retval 无
*/
void HAL_DCMIPP_MspInit(DCMIPP_HandleTypeDef *phdcmipp)
{
GPIO_InitTypeDef gpio_init_struct = {0};
if (phdcmipp->Instance == DCMIPP)
{
/* 使能时钟 */
__HAL_RCC_DCMIPP_CLK_ENABLE();
__HAL_RCC_DCMIPP_FORCE_RESET();
__HAL_RCC_DCMIPP_RELEASE_RESET();
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOE_CLK_ENABLE();
__HAL_RCC_GPIOG_CLK_ENABLE();
/* 初始化引脚 */
gpio_init_struct.Pin = GPIO_PIN_6;
gpio_init_struct.Mode = GPIO_MODE_AF_PP;
gpio_init_struct.Pull = GPIO_PULLUP;
gpio_init_struct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
gpio_init_struct.Alternate = GPIO_AF13_DCMIPP;
HAL_GPIO_Init(GPIOA, &gpio_init_struct);
gpio_init_struct.Pin = GPIO_PIN_6 | GPIO_PIN_7 | GPIO_PIN_8 |
GPIO_PIN_9;
HAL_GPIO_Init(GPIOB, &gpio_init_struct);
gpio_init_struct.Pin = GPIO_PIN_6 | GPIO_PIN_7;
HAL_GPIO_Init(GPIOC, &gpio_init_struct);
gpio_init_struct.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_4;
HAL_GPIO_Init(GPIOE, &gpio_init_struct);
gpio_init_struct.Pin = GPIO_PIN_3;
gpio_init_struct.Alternate = GPIO_AF5_DCMIPP;
HAL_GPIO_Init(GPIOG, &gpio_init_struct);
/* 配置中断 */
HAL_NVIC_SetPriority(DCMIPP_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DCMIPP_IRQn);
}
}
下面介绍的是DCMIPP启动传输和关闭传输函数,它们的定义分别如下:
/**
* @brief 启动DCMIPP传输
* @param 无
* @retval 无
*/
void dcmipp_start(void)
{
if (lcddev.wramcmd != 0)
{
lcd_set_cursor(0, 0); /* 设置坐标到原点 */
lcd_write_ram_prepare(); /* 开始写入GRAM */
}
SCB_CleanInvalidateDCache();
HAL_DCMIPP_PIPE_Start(&g_dcmipp_handle, DCMIPP_PIPE0, (uint32_t)camera_date_buf, DCMIPP_MODE_CONTINUOUS);
}
/**
* @brief 停止DCMIPP传输
* @param 无
* @retval 无
*/
void dcmipp_stop(void)
{
HAL_DCMIPP_PIPE_Stop(&g_dcmipp_handle, DCMIPP_PIPE0);
}
下面介绍的是DCMIPP中断服务函数(及其回调函数)、DCMIPP帧捕获完成回调函数,它们的定义分别如下:
/**
* @brief DCMIPP中断服务函数
* @param 无
* @retval 无
*/
void DCMIPP_IRQHandler(void)
{
HAL_DCMIPP_IRQHandler(&g_dcmipp_handle);
}
/**
* @brief HAL库DCMIPP帧捕获完成回调函数
* @param pHdcmipp: DCMIPP句柄指针
* @param Pipe : 配置的管道
* @retval 无
*/
void HAL_DCMIPP_PIPE_FrameEventCallback(DCMIPP_HandleTypeDef *pHdcmipp, uint32_t Pipe)
{
if (pHdcmipp->Instance == DCMIPP)
{
jpeg_data_process(); /* jpeg数据处理 */
LED1_TOGGLE(); /* LED1闪烁 */
g_ov_frame++;
}
}
其中:HAL_DCMIPP_PIPE_FrameEventCallback函数,用于处理帧中断,可以实现帧率统计(需要定时器支持)和JPEG数据处理等
DCMIPP驱动代码就介绍到这里。
2. OV5640驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。OV5640驱动源码包括六个文件:ov5640.c、ov5640.h、ov5640af.h、ov5640cfg.h、sccb.c和sccb.h。
其中sccb.c和sccb.h是SCCB通信接口的驱动代码。OV5640的寄存器通过SCCB时序访问并设置,SCCB时序和IIC时序十分类似,这里我们也是用软件模拟SCCB时序。
下面,首先介绍sccb.h头文件中SCCB的IO口宏定义,其定义情况如下:
/* 引脚 定义 */
#define SCCB_SCL_GPIO_PORT GPIOM
#define SCCB_SCL_GPIO_PIN GPIO_PIN_3
#define SCCB_SCL_GPIO_CLK_ENABLE()
do{ __HAL_RCC_GPIOM_CLK_ENABLE(); }while(0) /* PM口时钟使能 */
#define SCCB_SDA_GPIO_PORT GPIOM
#define SCCB_SDA_GPIO_PIN GPIO_PIN_2
#define SCCB_SDA_GPIO_CLK_ENABLE()
do{ __HAL_RCC_GPIOM_CLK_ENABLE(); }while(0) /* PM口时钟使能 */
/* IO操作函数 */
#define SCCB_SCL(x) do{ x ? \
HAL_GPIO_WritePin(SCCB_SCL_GPIO_PORT, SCCB_SCL_GPIO_PIN, GPIO_PIN_SET) : \
HAL_GPIO_WritePin(SCCB_SCL_GPIO_PORT, SCCB_SCL_GPIO_PIN, GPIO_PIN_RESET); \
}while(0) /* SCL */
#define SCCB_SDA(x) do{ x ? \
HAL_GPIO_WritePin(SCCB_SDA_GPIO_PORT, SCCB_SDA_GPIO_PIN, GPIO_PIN_SET) : \
HAL_GPIO_WritePin(SCCB_SDA_GPIO_PORT, SCCB_SDA_GPIO_PIN, GPIO_PIN_RESET); \
}while(0) /* SDA */
#define SCCB_SDA_READ HAL_GPIO_ReadPin(SCCB_SDA_GPIO_PORT,
SCCB_SDA_GPIO_PIN) /* 读取SDA */
SCCB时序有两根信号线(SCCB_SCL和SCCB_SDA),所以这里定义了两个IO口(PM3和PM2)来控制。IO操作函数有三个,包括SCCB_SCL用于控制时钟,SCCB_SDA是写IO口输出的值为逻辑1或者逻辑0,SCCB_SDA_READ是读取IO口的值,逻辑1或者逻辑0。
接下来介绍sccb.c文件的代码,首先是SCCB接口初始化函数,该函数主要就是初始化PM3和PM2两个IO口,其定义如下:
/**
* @brief 初始化SCCB
* @param 无
* @retval 无
*/
void sccb_init(void)
{
GPIO_InitTypeDef gpio_init_struct = {0};
/* 时钟使能 */
SCCB_SCL_GPIO_CLK_ENABLE();
SCCB_SDA_GPIO_CLK_ENABLE();
HAL_PWREx_EnableUSBVoltageDetector(); /* 使用GPIOM需要开启该电源 */
/* 配置SCL引脚 */
gpio_init_struct.Pin = SCCB_SCL_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;
gpio_init_struct.Pull = GPIO_PULLUP;
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(SCCB_SCL_GPIO_PORT, &gpio_init_struct);
/* 配置SDA引脚 */
gpio_init_struct.Pin = SCCB_SDA_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_OD;
gpio_init_struct.Pull = GPIO_PULLUP;
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(SCCB_SDA_GPIO_PORT, &gpio_init_struct);
sccb_stop();
}
PM3和PM2都设置为带上拉的开漏输出,这样设置的好处是:SDA引脚不用再设置IO口方向了,因为开漏输出模式下,STM32的IO口可以读取外部信号的高低电平,这个在前面介绍GPIO外设的时候已经讲解过。初始化IO口后,调用sccb_stop函数停止总线上的所有设备,防止误操作。
sccb.c文件的其他函数,这里就不再介绍了,主要都是IO口模拟SCCB时序的相关函数,详细请看源码。
ov5640.c、ov5640.h、ov5640af.h和ov5640cfg.h这四个文件主要就是用于OV5640摄像头的初始化,当然也要使用到SCCB的驱动代码。
ov5640cfg.h文件存放的是OV5640初始化数组,总共有3个数组。我们大概了解下数组结构,每个数组条目的第一个字节为寄存器号(也就是寄存器地址),第二个字节为要设置的值,比如{0x4300, 0x30},就表示在0x4300地址,写入0X30这个值。
三个数组中,ov5640_init_reg_tbl数组,用于初始化OV5640,该数组必须最 先进行配置;ov5640_rgb565_reg_tbl数组,用于设置OV5640的输出格式为RGB565,分辨率为1280*800,帧率为15帧,在RGB模式下使用;OV5640_jpeg_reg_tbl数组用于设置OV5640的输出格式为JPEG,分辨率为2592*1944,帧率为7.5帧,在JPEG模式下使用。由于数组的篇幅过长,就不列出来了,请对照源码理解。
ov5640af.h文件存放的是OV5640自动对焦配置数组,前面介绍过。
下面开始介绍ov5640.h文件,主要是OV5640的PWDN和RESET引脚定义和控制函数,以及OV5640的ID、访问地址和相关寄存器定义,其定义如下:
/* 引脚定义 */
#define OV_RESET_GPIO_PORT GPIOE
#define OV_RESET_GPIO_PIN GPIO_PIN_2
#define OV_RESET_GPIO_CLK_ENABLE() do { __HAL_RCC_GPIOE_CLK_ENABLE(); } while(0)
#define OV_PWDN_GPIO_PORT GPIOF
#define OV_PWDN_GPIO_PIN GPIO_PIN_5
#define OV_PWDN_GPIO_CLK_ENABLE() do { __HAL_RCC_GPIOF_CLK_ENABLE(); } while(0)
/* IO操作 */
#define OV5640_PWDN(x) do { (x) ? \
HAL_GPIO_WritePin(OV_PWDN_GPIO_PORT, OV_PWDN_GPIO_PIN, GPIO_PIN_SET): \
HAL_GPIO_WritePin(OV_PWDN_GPIO_PORT, OV_PWDN_GPIO_PIN, GPIO_PIN_RESET); \
} while (0)
#define OV5640_RST(x) do { (x) ? \
HAL_GPIO_WritePin(OV_RESET_GPIO_PORT, OV_RESET_GPIO_PIN, GPIO_PIN_SET): \
HAL_GPIO_WritePin(OV_RESET_GPIO_PORT, OV_RESET_GPIO_PIN, GPIO_PIN_RESET);\
} while (0)
/* OV5640 ID和SCCB访问地址定义 */
#define OV5640_ID 0x5640
#define OV5640_ADDR 0x3C
下面开始介绍ov5640.c文件,首先是OV5640初始化函数,其定义如下:
/**
* @brief 初始化OV5640
* @param 无
* @retval 初始化结果
* @arg 0: 成功
* @arg 1: 失败
*/
uint8_t ov5640_init(void)
{
GPIO_InitTypeDef gpio_init_struct = {0};
uint16_t id;
uint16_t i;
/* 使能时钟 */
OV_RESET_GPIO_CLK_ENABLE();
OV_PWDN_GPIO_CLK_ENABLE();
/* 初始化RESET引脚 */
gpio_init_struct.Pin = OV_RESET_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;
gpio_init_struct.Pull = GPIO_PULLUP;
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(OV_RESET_GPIO_PORT, &gpio_init_struct);
gpio_init_struct.Pin = OV_PWDN_GPIO_PIN;
HAL_GPIO_Init(OV_PWDN_GPIO_PORT, &gpio_init_struct);
/* 硬件复位 */
OV5640_RST(0);
delay_ms(20);
OV5640_PWDN(0);
delay_ms(5);
OV5640_RST(1);
delay_ms(20);
/* 初始化SCCB */
sccb_init();
/* 校验ID */
id = ov5640_read_reg(0x300A) << 8;
id |= ov5640_read_reg(0x300B);
if (id != OV5640_ID)
{
return 1;
}
/* 软件复位 */
ov5640_write_reg(0x3103, 0x11);
ov5640_write_reg(0x3008, 0x82);
delay_ms(10);
/* 初始化OV5640寄存器 */
for (i=0; i<(sizeof(ov5640_init_reg_tbl)/sizeof(ov5640_init_reg_tbl[0]));
i++)
{
ov5640_write_reg(ov5640_init_reg_tbl[i][0], ov5640_init_reg_tbl[i][1]);
}
/* 检查闪光灯 */
ov5640_flash_ctrl(1);
delay_ms(50);
ov5640_flash_ctrl(0);
return 0;
}
该函数除了初始化OV5640相关的IO口,最主要的是完成OV5640的寄存器序列初始化。OV5640的寄存器特多(百几十个),配置特麻烦,幸好厂家有提供参考配置序列,就存放在上面介绍的ov5640_ init_reg_tbl这个数组里面。
另外,在ov5640.c里面,还有几个函数比较重要,这里不贴代码了,只介绍功能:
ov5640_outsize_set函数,用于设置图像输出大小;
ov5640_image_window_set函数,用于设置图像开窗大小(ISP大小);
ov5640_focus_init函数,用于初始化自动对焦功能;
ov5640_focus_single函数,用于实现一次自动对焦;
ov5640_focus_constant函数,用于开启持续自动对焦功能;
其中,ov5640_outsize_set和ov5640_image_window_set函数就是前面在50.1.1节所介绍的3个窗口的设置,他们共同决定了图像的输出。
其他的函数就不列出来了,请大家查看源码。
3. TIMER驱动代码
TIMER驱动代码我们主要就是用到定时器6,在定时器6超时中断回调函数中打印帧率,超时中断回调函数定义如下:
/**
* @brief 定时器超时中断回调函数
* @param htim:定时器句柄指针
* @retval 无
*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim ->Instance == BTIM_TIMX_INT)
{
printf("Frame:%d\r\n", g_ov_frame);
g_ov_frame = 0;
}
}
g_ov_frame变量在main.c中被定义,用于存放帧率。TIMER的其他驱动代码在介绍定时器的时候已经介绍过,请看源码。
4. USART2驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。USART2驱动源码包括两个文件:usart2.c和usart2.h。
usart2.h头文件对usart2的IO口做了宏定义,具体如下:
/* USART2相关定义 */
#define USART2_UX USART2
#define USART2_UX_CLK_ENABLE()
do { __HAL_RCC_USART2_CLK_ENABLE(); } while (0)
#define USART2_UX_TX_GPIO_PORT GPIOD
#define USART2_UX_TX_GPIO_PIN GPIO_PIN_5
#define USART2_UX_TX_GPIO_AF GPIO_AF7_USART2
#define USART2_UX_TX_GPIO_CLK_ENABLE()
do { __HAL_RCC_GPIOD_CLK_ENABLE(); } while (0)
PD5复用为串口2的发送引脚。
下面介绍usart2.c文件的串口2初始化函数,其定义如下:
/**
* @brief 初始化USART2
* @param baudrate: 通信波特率
* @retval 无
*/
void usart2_init(uint32_t baudrate)
{
GPIO_InitTypeDef gpio_init_struct = {0};
/* 使能时钟 */
USART2_UX_CLK_ENABLE();
USART2_UX_TX_GPIO_CLK_ENABLE();
/* 初始化TX引脚 */
gpio_init_struct.Pin = USART2_UX_TX_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_AF_PP;
gpio_init_struct.Pull = GPIO_PULLUP;
gpio_init_struct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
gpio_init_struct.Alternate = USART2_UX_TX_GPIO_AF;
HAL_GPIO_Init(USART2_UX_TX_GPIO_PORT, &gpio_init_struct);
/* 初始化USART2 */
g_usart2_handle.Instance = USART2_UX;
g_usart2_handle.Init.BaudRate = baudrate;
g_usart2_handle.Init.WordLength = UART_WORDLENGTH_8B;
g_usart2_handle.Init.StopBits = UART_STOPBITS_1;
g_usart2_handle.Init.Parity = UART_PARITY_NONE;
g_usart2_handle.Init.Mode = UART_MODE_TX;
g_usart2_handle.Init.HwFlowCtl = UART_HWCONTROL_NONE;
g_usart2_handle.Init.OverSampling = UART_OVERSAMPLING_16;
g_usart2_handle.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE;
g_usart2_handle.Init.ClockPrescaler = UART_PRESCALER_DIV1;
HAL_UART_Init(&g_usart2_handle);
}
这里需要注意的是,串口2的时钟源频率在sys_stm32_clock_init函数中已经设置了。其他的代码基本usart_init函数是一样的。
5. main.c代码
main.c前面定义了一些变量和数组,具体如下:
static uint8_t g_ov_mode; /* OV5640输出数据格式
* 0: RGB565格式
* 1: JPEG格式
*/
uint8_t g_ov_frame = 0; /* 帧数 */
uint16_t g_curline = 0; /* 摄像头输出数据,当前行编号 */
uint16_t g_yoffset = 0; /* y方向的偏移量 */
/**
* 0,数据没有采集完;
* 1,数据采集完了,但是还没处理;
* 2,数据已经处理完成了,可以开始下一帧接收
*/
volatile uint8_t g_jpeg_data_ok = 0; /* JPEG数据采集完成标志 */
volatile uint32_t g_jpeg_data_len = 0; /* buf中的JPEG有效数据长度 */
/* OV5640特效名称定义 */
static const char *effects_tbl[] = {"Normal", "Cool", "Warm", "B&W", "Yellowish ", "Inverse", "Greenish"};
/* JPEG图片尺寸名称定义 */
static const char *jpeg_size_tbl[] = {"QQVGA", "QVGA", "VGA", "SVGA", "XGA", "WXGA", "WXGA+", "SXGA", "UXGA", "1080P", "QXGA", "500W"};
/* JPEG图片尺寸参数定义 */
static const uint16_t jpeg_size_param_tbl[][2] =
{
{160, 120}, /* QQVGA */
{320, 240}, /* QVGA */
{640, 480}, /* VGA */
{800, 600}, /* SVGA */
{1024, 768}, /* XGA */
{1280, 800}, /* WXGA */
{1440, 900}, /* WXGA+ */
{1280, 1024}, /* SXGA */
{1600, 1200}, /* UXGA */
{1920, 1080}, /* 1080P */
{2048, 1536}, /* QXGA */
{2592, 1944} /* 500W */
};
在main.c里面,总共有4个函数,我们接下来分别介绍。首先是处理JPEG数据函数,其定义如下:
/**
* @brief 处理JPEG数据
* @ntoe 在DCMIPP_IRQHandler中断服务函数里面被调用
* 当采集完一帧JPEG数据后,调用此函数,切换JPEG BUF.开始下一帧采集.
*
* @param 无
* @retval 无
*/
void jpeg_data_process(void)
{
if (g_ov_mode & 0X01) /* 只有在JPEG格式下,才需要做处理. */
{
if (g_jpeg_data_ok == 0) /* jpeg数据还未采集完? */
{
dcmipp_stop(); /* 关闭数据传输 */
HAL_DCMIPP_PIPE_GetDataCounter(&g_dcmipp_handle, DCMIPP_PIPE0,
(uint32_t*)&g_jpeg_data_len);
g_jpeg_data_ok = 1; /* 标记JPEG数据采集完成,等待其他函数处理 */
}
/* 采集的JPEG数据已被处理 */
if (g_jpeg_data_ok == 2)
{
/* 重新开始采集JPEG数据 */
g_jpeg_data_ok = 0;
g_jpeg_data_len = 0;
dcmipp_start();
}
}
else
{
// if (lcdltdc.pwidth == 0)
// {
lcd_set_cursor(0, 0);
lcd_write_ram_prepare(); /* 开始写入GRAM */
// }
}
}
该函数用于处理JPEG数据的接收,在HAL_DCMIPP_PIPE_FrameEventCallback函数(在dcmipp.c里面)里面被调用。
接下来介绍的是JPEG测试函数,其定义如下:
/**
* @brief JPEG测试
* @param 无
* @retval 无
*/
void jpeg_test(void)
{
uint8_t key;
uint8_t *p, headok;
uint32_t i, jpgstart, jpglen;
uint8_t effect = 0, contrast = 2;
uint8_t msgbuf[15]; /* 消息缓存区 */
uint8_t size = 1; /* 默认是QVGA 320*240尺寸 */
lcd_clear(WHITE);
lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
lcd_show_string(30, 70, 200, 16, 16, "OV5640 JPEG Mode", RED);
lcd_show_string(30, 100, 200, 16, 16, "KEY0:Contrast", RED); /* 对比度 */
lcd_show_string(30, 120, 200, 16, 16, "KEY1:AUTO Focus", RED); /* 自动对焦 */
lcd_show_string(30, 140, 200, 16, 16, "KEY2:Effect", RED); /* 特效 */
lcd_show_string(30, 160, 200, 16, 16, "KEY_UP:Size", RED); /* 分辨率设置 */
sprintf((char *)msgbuf, "JPEG Size:%s", jpeg_size_tbl[size]);
lcd_show_string(30, 180, 200, 16, 16, (char *)msgbuf, RED);
/* 显示当前JPEG分辨率 */
ov5640_rgb565_mode(); /* RGB565模式 */
ov5640_focus_init();
ov5640_jpeg_mode(); /* JPEG模式 */
ov5640_light_mode(0); /* 自动模式 */
ov5640_color_saturation(3); /* 色彩饱和度0 */
ov5640_brightness(4); /* 亮度0 */
ov5640_contrast(3); /* 对比度0 */
ov5640_sharpness(33); /* 自动锐度 */
ov5640_focus_constant(); /* 启动持续对焦 */
dcmipp_init(); /* DCMIPP配置 */
ov5640_outsize_set(4, 0, jpeg_size_param_tbl[size][0],
jpeg_size_param_tbl[size][1]); /* 设置输出尺寸 */
dcmipp_start(); /* 启动传输 */
while (1)
{
if (g_jpeg_data_ok == 1) /* 已经采集完一帧图像了 */
{
p = (uint8_t *)camera_date_buf;
printf("g_jpeg_data_len:%d\r\n", g_jpeg_data_len * 4);
/* 打印jpeg图片大小 */
lcd_show_string(30, 210, 210, 16, 16, "Sending JPEG data...", RED);
/* 提示正在传输数据 */
jpglen = 0; /* 设置jpg文件大小为0 */
headok = 0; /* 清除jpg头标记 */
for (i = 0; i < g_jpeg_data_len * 4; i++)
/* 查找0XFF,0XD8和0XFF,0XD9,获取jpg文件大小 */
{
if ((p[i] == 0XFF) && (p[i + 1] == 0XD8))
/* 找到FF D8 */
{
jpgstart = i;
headok = 1; /* 标记找到jpg头(FF D8) */
}
if ((p[i] == 0XFF) && (p[i + 1] == 0XD9) && headok)
/* 找到头以后,再找FF D9 */
{
jpglen = i - jpgstart + 2;
break;
}
}
if (jpglen) /* 正常的jpeg数据 */
{
p += jpgstart; /* 偏移到0XFF,0XD8处 */
for (i = 0; i < jpglen; i++) /* 发送整个jpg文件 */
{
USART2->TDR = p[i];
while ((USART2->ISR & 0X40) == 0);
/* 循环发送,直到发送完毕 */
key = key_scan(0);
if (key)break;
}
}
if (key) /* 有按键按下,需要处理 */
{
lcd_show_string(30, 210, 210, 16, 16, "Quit Sending data ",
RED); /* 提示退出数据传输 */
switch (key)
{
case KEY0_PRES: /* 对比度设置 */
contrast++;
if (contrast > 6)contrast = 0;
ov5640_contrast(contrast);
sprintf((char *)msgbuf, "Contrast:%d", (signed
char)contrast - 3);
break;
case KEY1_PRES: /* 执行一次自动对焦 */
ov5640_focus_single();
break;
case KEY2_PRES: /* 特效设置 */
effect++;
if (effect > 6)effect = 0;
ov5640_special_effects(effect); /* 设置特效 */
sprintf((char *)msgbuf, "%s", effects_tbl[effect]);
break;
case WKUP_PRES: /* JPEG输出尺寸设置 */
size++;
if (size > 11)size = 0;
ov5640_outsize_set(16, 4, jpeg_size_param_tbl[size][0],
jpeg_size_param_tbl[size][1]); /* 设置输出尺寸 */
sprintf((char *)msgbuf, "JPEG Size:%s",
jpeg_size_tbl[size]);
break;
}
key = 0; /* 避免重复进入 */
lcd_fill(30, 180, 239, 190 + 16, WHITE);
lcd_show_string(30, 180, 210, 16, 16, (char *)msgbuf, RED);
/* 显示提示内容 */
delay_ms(800);
}
else
{
lcd_show_string(30, 210, 210, 16, 16, "Send data complete!!",
RED); /* 提示传输结束设置 */
}
g_jpeg_data_ok = 2; /* 标记jpeg数据处理完了,可以让DMA去采集下一帧了. */
dcmipp_start();
}
}
}
该函数将OV5640设置为JPEG模式,并开启持续自动对焦、实现OV5640的JPEG数据接收,通过串口2发送给上位机软件。
接下来介绍的是RGB565测试函数,其定义如下:
/**
* @brief RGB565测试
* @param 无
* @retval 无
*/
void rgb565_test(void)
{
uint8_t key;
float fac = 0;
uint8_t effect = 0, contrast = 2;
uint8_t scale = 1;
uint8_t msgbuf[15];
uint16_t outputheight = 0;
uint32_t memaddr;
uint8_t i;
lcd_clear(WHITE);
lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
lcd_show_string(30, 70, 200, 16, 16, "OV5640 RGB565 Mode", RED);
lcd_show_string(30, 90, 200, 16, 16, "KEY0: Auto Focus", RED);
lcd_show_string(30, 110, 200, 16, 16, "KEY1: Effects", RED);
lcd_show_string(30, 130, 200, 16, 16, "KEY_UP: Fullsize/Scale", RED);
/* 配置OV5640 */
ov5640_rgb565_mode();
ov5640_focus_init();
ov5640_light_mode(0);
ov5640_color_saturation(3);
ov5640_brightness(4);
ov5640_contrast(3);
ov5640_sharpness(33);
ov5640_focus_constant();
/* 配置DCMIPP */
dcmipp_init();
if (lcddev.height == 1024)
{
g_yoffset = (lcddev.height - 800) / 2;
outputheight = 800;
ov5640_write_reg(0x3035, 0X51); /* 降低输出帧率,否则可能抖动 */
}
else if (lcddev.height == 1280)
{
g_yoffset = (lcddev.height - 600) / 2;
outputheight = 600;
ov5640_write_reg(0x3035, 0X51); /* 降低输出帧率,否则可能抖动 */
}
else
{
g_yoffset = 0;
outputheight = lcddev.height;
}
g_curline = g_yoffset; /* 行数复位 */
ov5640_outsize_set(4, 0, lcddev.width, outputheight); /* 满屏缩放显示 */
dcmipp_start(); /* 启动传输 */
lcd_clear(0xFFFF);
while (1)
{
lcd_color_fill(0, 0, lcddev.width - 1, lcddev.height - 1,
(uint16_t*)camera_date_buf);
key = key_scan(0);
if (key)
{
if (key != KEY0_PRES)
{
dcmipp_stop(); /* 非KEY1按下,停止显示 */
}
switch (key)
{
case KEY0_PRES: /* 执行一次自动对焦 */
ov5640_focus_single();
break;
case KEY1_PRES: /* 特效设置 */
effect++;
if (effect > 6)effect = 0;
ov5640_special_effects(effect); /* 设置特效 */
sprintf((char *)msgbuf, "%s", effects_tbl[effect]);
break;
case WKUP_PRES: /* 1:1尺寸(显示真实尺寸)/缩放 */
scale = !scale;
if (scale == 0)
{
fac = (float)800 / outputheight; /* 得到比例因子 */
ov5640_outsize_set((1280 - fac * lcddev.width) / 2,
(800 - fac * outputheight) / 2, lcddev.width,
outputheight);
sprintf((char *)msgbuf, "Full Size 1:1");
}
else
{
ov5640_outsize_set(4, 0, lcddev.width, outputheight);
sprintf((char *)msgbuf, "Scale");
}
break;
}
if (key != KEY0_PRES) /* 非KEY0按下 */
{
lcd_show_string(30, 50, 210, 16, 16, (char*)msgbuf, RED);
/* 显示提示内容 */
delay_ms(800);
lcd_set_cursor(0, 0);
lcd_write_ram_prepare();
dcmipp_start(); /* 重新开始传输 */
}
}
delay_ms(10);
}
}
该函数将OV5640设置为RGB565模式,并将接收到的数据,传送给LCD。
接下来介绍的是main函数,其定义如下:
int main(void)
{
uint8_t t = 0;
uint8_t key;
sys_mpu_config(); /* 配置MPU */
sys_cache_enable(); /* 使能Cache */
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(300, 6, 2); /* 配置时钟,600MHz */
delay_init(600); /* 初始化延时 */
usart_init(115200); /* 初始化串口 */
usart2_init(921600); /* 初始化USART2 */
led_init(); /* 初始化LED */
key_init(); /* 按键初始化 */
hyperram_init(); /* 初始化HyperRAM */
lcd_init(); /* 初始化LCD */
btim_timx_int_init(10000 - 1, 30000 - 1);
/* 初始化基本定时器中断,溢出频率为1Hz */
lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
lcd_show_string(30, 70, 200, 16, 16, "OV5640 TEST", RED);
lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
while (ov5640_init() != 0) /* 初始化OV5640 */
{
lcd_show_string(30, 110, 200, 16, 16, "OV5640 Error!", RED);
delay_ms(500);
lcd_show_string(30, 110, 200, 16, 16, "Please Check! ", RED);
delay_ms(500);
LED0_TOGGLE();
}
lcd_show_string(30, 110, 200, 16, 16, "OV5640 OK! ", RED);
/* 选择测试模式 */
lcd_show_string(30, 130, 200, 16, 16, "KEY0: RGB565 Mode", BLUE);
lcd_show_string(30, 150, 200, 16, 16, "KEY1: JPEG Mode", BLUE);
while (1)
{
key = key_scan(0);
if (key == KEY0_PRES)
{
g_ov_mode = 0;
break;
}
else if (key == KEY1_PRES)
{
g_ov_mode = 1;
break;
}
if (++t == 20)
{
t = 0;
LED0_TOGGLE();
}
delay_ms(10);
}
/* 进入测试 */
if (g_ov_mode == 0)
{
rgb565_test();
}
else
{
jpeg_test();
}
}
该函数完成对各相关硬件的初始化,然后检测OV5640,最后通过按键选择来调用jpeg_test还是rgb565_test,实现JPEG测试和RGB565测试。
50.4 下载验证
将程序下载到开发板后,可以看到LED0不停的闪烁,提示程序已经在运行了。LCD显示的内容如图50.4.1所示:

图50.4.1 摄像头实验程序运行效果图
上图是摄像头已经正确的插上开发板上的运行结果,如果没有插上摄像头,会有错误的报错信息。在OV5640初始化成功后,屏幕提示选择测试模式,此时我们可以按KEY0,进入RGB565模式测试,也可以按KEY1,进入JPEG模式测试。
当按KEY0后,选择RGB565模式,LCD满屏显示压缩放后的图像(有变形),如图50.4.2所示:

图50.4.2 RGB565模式测试图片
此时,可以通过KEY0按键,执行一次自动对焦;KEY1按键,设置特效; KEY_UP按键,显示1:1尺寸或者缩放。
当按KEY1后,选择JPEG模式,此时屏幕显示JPEG数据传输进程,如图50.4.3所示:

图50.4.3 JPEG模式测试图
默认条件下,图像分辨率是QVGA (320*240)的,硬件上:我们需要用一根杜邦线连接开发板的PD5排针到RXD排针端。
打开上位机软件:ATK-XCAM.exe(路径:光盘→6,软件资料 →软件 →串口网络摄像头软件 →ATK-XCAM .exe),选择正确的串口,然后波特率设置为921600,打开即可收到下位机传过来的图片了,如图50.4.4所示:

图50.4.4 ATK-XCAM.exe软件接收并显示JPEG图片
我们可以通过KEY_UP设置输出图像的尺寸(QQVGA~ WXGA)。KEY0按键,设置对比度;KEY1按键,执行一次自动对焦;KEY2按键,设置特效。