《STM32H7R7开发指南 V1.1 》第五十章 摄像头实验

第五十章 摄像头实验


       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按键,设置特效。


请使用浏览器的分享功能分享到微信等