搜索
bottom↓
回复: 0

《MiniPro STM32H750 开发指南》 第四十三章 摄像头实验

[复制链接]

出0入234汤圆

发表于 2022-9-21 14:40:52 | 显示全部楼层 |阅读模式
本帖最后由 正点原子 于 2022-9-21 14:40 编辑

1)实验平台:正点原子MiniPro STM32H750开发板
2)平台购买链接:https://detail.tmall.com/item.htm?id=677017430560
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boar
4)MiniPro STM32H750技术交流QQ群:756580169 lQLPJxaFi2zaB4UWWrDAMgIsFEW2pwLb3abnwDMA_90_22.png
lQDPJxaFi2nfFizMjM0CbLCPlxn_FVheIQLb3aGrwFQA_620_140.jpg

lQLPJxaFi2nfFhLMkM0BXrDNvOUyeU_FPgLb3aGvQNIA_350_144.png


第四十三章 摄像头实验


STM32H750具有DCMI接口,所以我们的MiniPRO STM32H750开发板板载了一个摄像头接口(P7),该接口可以用来连接ALIENTEK OV5640/OV2640/OV7725等摄像头模块。本章,我们将使用STM32驱动ALIENTEK OV5640摄像头模块,实现摄像头功能。
本章分为如下几个小节:
43.1 OV5640和DCMI简介
43.2 硬件设计
43.3 程序设计
43.4 下载验证


43.1 OV5640和DCMI简介

本节将分为两个部分,分别介绍OV5640简介和STM32H750 DCMI接口简介。另外,所有OV5640的相关资料,都在光盘:A盘7,硬件资料OV5640资料 文件夹里面。
43.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的特点有:
采用1.4μm*1.4μm像素大小,并且使用OmniBSI技术以达到更高性能(高灵敏度、低串扰和低噪声)
自动图像控制功能:自动曝光(AEC)、自动白平衡(AWB)、自动消除灯光条纹、自动黑电平校准(ABLC)和自动带通滤波器(ABF)等。
支持图像质量控制:色饱和度调节、色调调节、gamma校准、锐度和镜头校准等
标准的SCCB接口,兼容IIC接口
支持RawRGB、RGB(RGB565/RGB555/RGB444)、CCIR656、YUV(422/420)、YCbCr(422)和压缩图像(JPEG)输出格式
支持QSXGA(500W)图像尺寸输出,以及按比例缩小到其他任何尺寸
支持闪光灯
支持图像缩放、平移和窗口设置
支持图像压缩,即可输出JPEG图像数据
支持数字视频接口(DVP)和MIPI接口
支持自动对焦
自带嵌入式微处理器
OV5640的功能框图图如图43.1.1.1所示:
第四十三章 摄像头实验1160.png
图43.1.1.1 OV5640功能框图

其中image array部分的尺寸,OV5640的官方数据并没有给出具体的数字,其最大的有效输出尺寸为:2592*1944,即500W像素,我们根据官方提供的一些应用文档,发现其设置的image array最大为:2632*1951,所以,在接下来的介绍,我们设定其image array最大为2632*1951。
1、DVP接口说明
OV5640支持数字视频接口(DVP)和MIPI接口,因为我们的STM32H743使用的DCMI接口,仅支持DVP接口,所以,OV5640必须使用DVP输出接口,才可以连接我们的阿波罗STM32开发板。
OV5640提供一个10位DVP接口(支持8位接法),其MSB和LSB可以程序设置先后顺序,ALIENTEK OV5640模块采用默认的8位连接方式,如图43.1.1.2所示:
第四十三章 摄像头实验1543.png
图43.1.1.2 OV5640默认8位连接方式

OV5640的寄存器通过SCCB时序访问并设置,SCCB时序和IIC时序十分类似。SCCB 与标准的 I2C 协议的区别是它每次传输只能写入或读取一个字节的数据,而 I2C协议是支持突发读写的,即在一次传输中可以写入多个字节的数据(EEPROM中的页写入时序即突发写)。在本章我们不做介绍,请大家参考光盘《OmniVision Technologies Seril Camera Control Bus(SCCB) Specification》这个文档。
2、窗口设置说明
接下来,我们介绍一下OV5640的:ISP(Image Signal Processor)输入窗口设置、预缩放窗口设置和输出大小窗口设置,这几个设置与我们的正常使用密切相关,有必要了解一下。他们的设置关系,如图43.1.1.3所示:
第四十三章 摄像头实验1924.png
图43.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(正常)。  
        图43.1.1.4中,右侧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的控制下进行的。首先看看行输出时序,如图43.1.1.4所示:
第四十三章 摄像头实验3062.png
图43.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模式),如图43.1.1.5所示:
第四十三章 摄像头实验3338.png
图43.1.1.5 OV5640帧时序

上图清楚的表示了OV5640在QSXGA模式下的数据输出。我们按照这个时序去读取OV5640的数据,就可以得到图像数据。
4、自动对焦(Auto Focus)说明
OV5640由内置微型控制器完成自动对焦,并且VCM(Voice Coil Motor,即音圈马达)驱动器也已集成在传感器内部。微型控制器的控制固件(firmware)从主机下载。当固件运行后,内置微型控制器从OV5640传感器读得自动对焦所需的信息,计算并驱动VCM马达带动镜头到达正确的对焦位置。主机可以通过IIC命令控制微型控制器的各种功能。
OV5640的自动对焦命令(通过SCCB总线发送),如表43.1.1.1所示:
lQLPJxa0v1e0IjfNARzNAdqwqFY6_IhajnMDKTPw6gCJAA_474_284.png
表43.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。
ALIENTEK OV5640摄像头模块
本实验,我们将使用MiniPRO STM32H750开发板的DCMI接口连接ALIENTEK OV5640摄像头模块,该模块采用8位数据输出接口,自带24M有源晶振,无需外部提供时钟,模组支持自动对焦功能,且支持闪光灯,整个模块只需提供3.3V 供电即可正常使用。
ALIENTEK OV5640摄像头模块外观如图43.1.1.6所示:  
第四十三章 摄像头实验5353.png
图43.1.1.6 ALIENTEK OV5640摄像头模块外观图

模块原理图如图43.1.1.7所示:
第四十三章 摄像头实验5408.png
图43.1.1.7 ALIENTEK OV5640摄像头模块原理图

        从上图可以看出,ALIENTEK OV5640摄像头模块自带了有源晶振,用于产生24M时钟作为OV5640的XCLK输入,模块的闪光灯(LED1&LED2)由OV5640的STROBE脚控制(可编程控制)。同时自带了稳压芯片,用于提供OV5640稳定的2.8V和1.5V工作电压,模块通过一个2*9的双排排针(P1)与外部通信,与外部的通信信号如表43.1.1.2所示:
7EDF1D00-27B1-4070-8CC0-18958B2B3B4B.png
表43.1.1.2 OV5640模块信号及其作用描述

43.1.2 STM32H750 DCMI接口简介
STM32H750自带了一个数字摄像头(DCMI)接口,该接口是一个同步并行接口,能够接收外部8位、10位、12位或 14位 CMOS 摄像头模块发出的高速数据流。可支持不同的数据格式:YCbCr4:2:2/RGB565逐行视频和压缩数据 (JPEG)。
STM32H750 DCM接口特点:
8 位、10 位、12 位或 14 位并行接口
内嵌码/外部行同步和帧同步
连续模式或快照模式
裁剪功能
支持以下数据格式:
1,8/10/12/14 位逐行视频:单色或原始拜尔(Bayer)格式
2,YCbCr 4:2:2逐行视频
3,RGB 565 逐行视频
4,压缩数据:JPEG
DCMI接口包括如下一些信号:
1,数据输入(D[0:13]),用于接摄像头的数据输出,接OV5640我们只用了8位数据。
2,水平同步(行同步)输入(HSYNC),用于接摄像头的HSYNC/HREF信号。
3,垂直同步(场同步)输入(VSYNC),用于接摄像头的VSYNC信号。
4,像素时钟输入(PIXCLK),用于接摄像头的PCLK信号。
DCMI接口是一个同步并行接口,可接收高速(可达140 MB/s)数据流。该接口包含多达14条数据线(D13-D0)和一条像素时钟线(PIXCLK)。像素时钟的极性可以编程,因此可以在像素时钟的上升沿或下降沿捕获数据。
DCMI接收到的摄像头数据被放到一个32位数据寄存器(DCMI_DR)中,然后通过通用 DMA进行传输。图像缓冲区由 DMA 管理,而不是由摄像头接口管理。
从摄像头接收的数据可以按行/帧来组织(原始YUV/RGB/拜尔模式),也可以是一系列 JPEG图像。要使能 JPEG 图像接收,必须将 JPEG 位(DCMI_CR 寄存器的位 3)置 1。
数据流可由可选的 HSYNC(水平同步)信号和 VSYNC(垂直同步)信号硬件同步,或者通 过数据流中嵌入的同步码同步。
43.1.2.1 DCMI接口功能概述
STM32H750 DCMI接口的框图如图43.1.2.1.1所示:
第四十三章 摄像头实验6732.png
图43.1.2.1.1 DCMI接口框图

上图左边可以看到的是DCMI的内部信号,如下表所示:
lQLPJxa0v3FVa1FPzQHcsNty2c5m5kPnAyk0G1KA7AA_476_79.png
表43.1.2.1.1 DCMI内部信号

dcmi_dma,DCMI DMA请求信号。当DCMI_CR寄存器中的 CAPTURE位置1时,激活DMA接口。摄像头接口每次在其寄存器中收到一个完整的32位数据块时,都将触发一个DMA请求。
dcmi_it,DCMI中断请求信号。DCMI有五种中断源,所有中断都可通过软件屏蔽。全局中断 (dcmi_it) 是所有单个中断的逻辑或运算所得的结果。
DCMI的五种中断源,如下表所示:
lQLPJxa0v3pGPYHMiM0BmbDpLKGVhT4jugMpNCowAGEA_409_136.png
表43.1.2.1.2 DCMI中断源

dcmi_hclk,DCMI 接口时钟,时钟源自来于HCLK3(我们设置为240MHZ)。
上图中间部分就是DCMI内核。
上图右边的DCMI物理接口,具体如下表所示:
lQLPJxa0v4Yx_J3Mmc0B7LAnyHfeSn4UywMpNDzvwIkA_492_153.png
表43.1.2.1.3 DCMI物理接口

该接口由11/13/15/17个输入信号组成。仅支持从模式。根据 DCMI_CR 寄存器中 EDM[1:0] 位的设置,摄像头接口可以捕获8位、10位、12位或14位数据。如果使用的位数少于14,则必须将未使用的输入引脚接地。
DCMI数据与DCMI_PIXCLK(即PCLK)保持同步,并根据像素时钟的极性在像素时钟上升沿/下降沿发生变化。
DCMI_HSYNC(HREF)信号指示行的开始/结束。
DCMI_VSYNC 信号指示帧的开始/结束。
DCMI信号波形,如图43.1.2.1.2所示:
第四十三章 摄像头实验7765.png
图43.1.2.1.2 DCMI信号波形

        上图中,对应设置为:DCMI_PIXCLK的捕获沿为下降沿,DCMI_HSYNC和DCMI_VSYNC的有效状态为1,注意,这里的有效状态实际上对应的是指示数据在并行接口上无效时,HSYNC/VSYNC引脚上面的引脚电平。
        本实验我们用到DCMI的8位数据宽度,通过设置DCMI_CR中的EDM[1:0]=00设置。此时DCMI_D0~D7有效,DCMI_D8~D13上的数据则忽略,这个时候,每次需要4个像素时钟来捕获一个32位数据。捕获的第一个数据存放在32位字的LSB位置,第四个数据存放在32位字的MSB位置 ,捕获数据字节在32位字中的排布如表43.1.2.1.4所示:
第四十三章 摄像头实验8081.png
表43.1.2.1.4 8位捕获数据在32位字中的排布

从表43.1.2.1.4可以看出,STM32H750的DCMI接口,接收的数据是低字节在前,高字节在后的,所以,要求摄像头输出数据也是低字节在前,高字节在后才可以,否则就还得程序上处理字节顺序,会比较麻烦。
DCMI接口支持两种同步方式:内嵌码同步和硬件(HSYNC和VSYNC)同步。我们使用硬件同步,硬件同步模式下将使用两个同步信号 (HSYNC/VSYNC)。根据摄像头模块/模式的不同,可能在水平/垂直同步期间内发送数据。由于系统会忽略HSYNC/VSYNC信号有效电平期间内接收的所有数据,HSYNC/VSYNC 信号相当于消隐信号。
为了正确地将图像传输到 DMA/RAM 缓冲区,数据传输将与VSYNC信号同步。选择硬件同步模式并启用捕获(DCMI_CR中的CAPTURE位置1)时,数据传输将与VSYNC信号的 无效电平同步(开始下一帧时)。之后传输便可以连续执行,由DMA将连续帧传输到多个连续的缓冲区或一个具有循环特性的缓冲区。为了允许DMA管理连续帧,每一帧结束时都将激活VSIF(垂直同步中断标志,即帧中断),我们可以利用这个帧中断来判断是否有一帧数据采集完成,方便处理数据。
DCMI接口的捕获模式支持:快照模式和连续采集模式。一般我们使用连续采集模式,通过DCMI_CR中的CM位设置。另外,DCMI接口还支持实现了4个字深度的 FIFO,配有一个简单的FIFO控制器,每次摄像头接口从AHB读取数据时读指针递增,每次摄像头接口向FIFO写入数据时写指针递增。因为没有溢出保护,如果数据传输率超过AHB接口能够承受的速率,FIFO中的数据就会被覆盖。如果同步信号出错,或者 FIFO 发生溢出,FIFO 将复位,DCMI 接口将等待新的数据帧开始。
关于DCMI接口的其他特性,我们这里就不再介绍了,请大家参考《STM32H7xx参考手册_V7(英文版).pdf》第31章相关内容。
43.1.2.2 DCMI寄存器
本实验,我们将OV5640默认配置为WXGA输出,也就是1280*800的分辨率,输出信号设置为:VSYNC高电平有效,HREF高电平有效,输出数据在PCLK的下降沿输出(即上升沿的时候,MCU才可以采集)。这样,STM32H750的DCMI接口就必须设置为:VSYNC低电平有效、HSYNC低电平有效和PIXCLK上升沿有效,这些设置都是通过DCMI_CR寄存器控制。
DCMI控制寄存器(DCMI_CR)
DCMI控制寄存器描述如图43.1.2.2.1所示:
8B3C04A5-64AB-4776-802C-3B14A686D787.png
图43.1.2.2.1 DCMI_CR寄存器

ENABLE,该位用于设置是否使能DCMI,不过,在使能之前,必须将其他配置设置好。
EDM[1:0],设置扩展数据模式,选择00:接口每个像素时钟捕获8位数据。
        FCRC[1:0],这两个位用于帧率控制,我们捕获所有帧,所以设置为00即可。
        VSPOL,该位用于设置垂直同步极性,也就是VSYNC引脚上面,数据无效时的电平状态,根据前面说所,我们应该设置为0。
        HSPOL,该位用于设置水平同步极性,也就是HSYNC引脚上面,数据无效时的电平状态,同样应该设置为0。
        PCKPOL,该位用于设置像素时钟极性,我们用上升沿捕获,所以设置为1。
ESS,内嵌码同步选择,我们选择硬件同步,默认置0。
        CM,该位用于设置捕获模式,我们用连续采集模式,所以设置为0即可。
CAPTURE,该位用于使能捕获,我们设置为1。该位使能后,将激活DMA,DCMI等待第一帧开始,然后生成DMA请求将收到的数据传输到目标存储器中。注意:该位必须在DCMI的其他配置(包括DMA)都设置好了之后,才设置!!
        DCMI_CR寄存器的其他位,我们就不介绍了,另外DCMI的其他寄存器这里也不再介绍,请大家参考《STM32H7xx参考手册_V7(英文版).pdf》第31.5小节。关于DMA的寄存器,这里就不再赘述,请回顾前面的实验。
43.2 硬件设计
1. 例程功能
1、本实验开机后,初始化摄像头模块(OV5640),如果初始化成功,则提示选择模式:RGB565模式或者JPEG模式。KEY0用于选择RGB565模式,KEY1用于选择JPEG模式。
2、当使用RGB565时,输出图像(固定为:WXGA)将经过缩放处理(完全由OV5640的DSP控制),显示在LCD上面(默认开启连续自动对焦)。我们可以通过KEY_UP按键选择:1:1显示,即不缩放,图片不变形,但是显示区域小(液晶分辨率大小),或者缩放显示,即将1280*800的图像压缩到液晶分辨率尺寸显示,图片变形,但是显示了整个图片内容。通过KEY0按键,可以启动单次自动对焦;KEY1按键,可以设置特效。
3、当使用JPEG模式时,图像可以设置默认是QVGA 320*240尺寸,采集到的JPEG数据将先存放到STM32的RAM内存里面,每当采集到一帧数据,就会关闭DMA传输,然后将采集到的数据发送到串口2(此时可以通过上位机软件(ATK-CAM.exe)接收,并显示图片),之后再重新启动DMA传输。通过按键KEY_UP设置输出图片的尺寸、按键KEY0启动单次自动对焦、按键KEY1设置特效。
4、同时可以通过串口1,借助USMART设置/读取OV5640的寄存器,方便大家调试。
5、LED0闪烁,提示程序运行。LED1用于指示帧中断。。
2. 硬件资源
1)RGB灯
   RED :LED0 - PB4
GREEN :LED1 - PE6
2)串口1(PA9/PA10连接在板载USB转串口芯片CH340上面)
3)正点原子2.8/3.5/4.3/7/10寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
4)独立按键 :KEY0 - PA1、KEY1 - PA15
5)串口2 (PA2/PA3,外接串口线/用杜邦线连接到CH340上面)
6)DCMI接口(用于驱动OV5640摄像头模块)
7)定时器6(用于打印摄像头帧率等信息)
8)ALIENTEK OV5640摄像头模块,连接关系为:
     OV5640模块 -----------   STM32开发板
     OV_D0~D7  ------------   PC6/PC7/PC8/PC9/PC11/PD3/PB8/PB9
     OV_SCL    ------------   PB10
     OV_SDA    ------------   PB11
     OV_VSYNC  ------------  PB7
     OV_HREF   ------------   PA4
     OV_RESET  ------------   PA7
     OV_PCLK   ------------   PA6
     OV_PWDN  ------------   PC4
3. 原理图
开发板板载的摄像头模块接口与MCU的连接关系,如下图所示:
第四十三章 摄像头实验11004.png
图43.2.1 QSPI FLASH芯片与STM32H750连接示意图

这些GPIO口的线都在开发板上连接到P2端口,所以我们只需要将OV5640摄像头模块插上开发板的连接座子就好了(摄像头模块正面往外插)。
第四十三章 摄像头实验11111.png
图43.2.2 OV5640摄像头模块与开发板的连接座子

43.3 程序设计
43.3.1 DCMI的HAL库驱动

DCMI在HAL库中的驱动代码在stm32h7xx_hal_dcmi.c文件(及其头文件)中。
1. HAL_DCMI_Init函数
DCMI初始化函数,其声明如下:
HAL_StatusTypeDef HAL_DCMI_Init(DCMI_HandleTypeDef *hdcmi);
函数描述:
用于初始化DCMI。
函数形参:
形参1是DCMI_HandleTypeDef结构体类型指针变量,其定义如下:
typedef struct
{
  DCMI_TypeDef                   *Instance;                   /* DCMI 外设寄存器基地址 */
  DCMI_InitTypeDef              Init;                         /* DCMI 初始化结构体 */
  HAL_LockTypeDef               Lock;                         /* 锁对象 */
  __IO HAL_DCMI_StateTypeDef  State;                        /* DCMI 工作状态 */
  __IO uint32_t                  XferCount;                   /* DMA 传输计数器 */
  __IO uint32_t                  XferSize;                    /* DMA 传输数据的大小 */
  uint32_t                        XferTransferNumber;        /* DMA 数据的个数 */
  uint32_t                        pBuffPtr;                    /* DMA输出缓冲区地址 */
  DMA_HandleTypeDef             *DMA_Handle;                /* DMA配置结构体指针 */
  __IO uint32_t                  ErrorCode;                  /* DCMI 错误代码 */
}DCMI_HandleTypeDef;
下面重点介绍DCMI_InitTypeDef结构体,其定义如下:
typedef struct {
uint32_t SynchroMode;                      /* 同步方式选择硬件同步模式还是内嵌码模式 */
uint32_t PCKPolarity;                      /* 设置像素时钟的有效边沿 */
uint32_t VSPolarity;                       /* 设置垂直同步的有效电平 */
uint32_t HSPolarity;                       /* 设置水平同步的有效边沿 */
uint32_t CaptureRate;                      /* 设置图像的帧捕获率 */
uint32_t ExtendedDataMode;                /* 设置数据线的宽度(扩展数据模式) */
DCMI_CodesInitTypeDef SyncroCode;        /* 分隔符设置 */
uint32_t JPEGMode;                          /* JPEG 模式选择 */
uint32_t ByteSelectMode;                   /* 配置字节选项模式 */
uint32_t ByteSelectStart;                  /* 字节选择开始 */
uint32_t LineSelectMode;                   /* 行选择模式 */
uint32_t LineSelectStart;                  /* 指定数据行是奇数还是偶数 */
} DCMI_InitTypeDef;
1) SynchroMode:用于设置 DCMI数据的同步模式,可以选择为硬件同步方式或内嵌码方式。
如果选择硬件同步值DCMI_SYNCHRO_HARDWARE,那么数据捕获由HSYNC/VSYNC信号同步,如果选择内嵌码同步方式值DCMI_SYNCHRO_EMBEDDED,那么数据捕获由数据流中嵌入的同步码同步。
2) PCKPolarity:用来设置像素时钟极性为上升沿有效还是下降沿有效。
3) VSPolarity:用于设置VSYNC的有效电平,当VSYNC信号线表示为有效电平时,表示新
的一帧数据传输完成,它可以被设置为高电平有效或低电平有效。
4) HSPolarity:用于设置HSYNC的有效电平,当 HSYNC 信号线表示为有效电平时,表示新的一行数据传输完成,它可以被设置为高电平有效或低电平有效。
5) CaptureRate:用于设置帧捕获率,可以设置为全采集、半采集或 1/4 采集。设置的值为DCMI_CR_ALL_FRAME(表示全帧捕获),设置为DCMI_CR_ALTERNATE_2_FRAME(表示2帧捕获一帧),设置为DCMI_CR_ALTERNATE_4_FRAME(表示4帧捕获一帧)。
6) ExtendedDataMode:用于设置扩展数据模式,可设置为8/10/12或者14位数据宽度。
7) SyncroCode:用于设置分隔码,包括:帧结束分隔码,行结束分隔码,行开始分隔码以及帧开始分隔码。
8) JPEGMode:用于设置JPEG格式使能或禁止。
9)ByteSelectMode用于设置字节选项模式,也就是接口对接收到的数据每隔多少个字节捕获一个字节,取值为:DCMI_BSM_ALL(捕获所有字节),DCMI_BSM_OTHER(每隔一个字节进行捕获),DCMI_BSM_ALTERNATE_4(每四个字节捕获一个字节)和DCMI_BSM_ALTERNATE_2(每四个字节捕获两个字节)。
10)ByteSelectStart用于设置奇数偶数字节选择开始,也就是接口从帧/行开始捕获第一个数据同时丢弃第二个字节(DCMI_OEBS_ODD)或者捕获第二个数据同时丢弃第一个字节(DCMI_OEBS_EVEN)。
11)LineSelectMode用于配置行选择模式,也就是选择接口捕获所有接受到的行(DCMI_LSM_ALL)还是每两行捕获一行(DCMI_LSM_ALTERNATE_2)。
12)LineSelectStart用于配置奇数偶数行选择开始。也就是接口在帧开始后捕获第一行丢弃第二行(DCMI_OELS_ODD)或者在帧开始后捕获第二行同时丢弃第一行(DCMI_OELS_EVEN)。
函数返回值:
HAL_StatusTypeDef枚举类型的值。
以DMA方式传输DCMI数据配置步骤
1)配置OV5640控制引脚,并配置OV5640工作模式。
在启动DCMI之前,我们先设置好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配置好之后,再设置DCMI接口与摄像头模块连接的IO口,使能IO和DCMI时钟,然后设置相关IO口为复用功能模式,复用功能选择AF13(DCMI复用)。
DCMI时钟使能方法:
__HAL_RCC_DCMI_CLK_ENABLE();                /* 使能DCMI时钟 */
引脚模式配置就是通过HAL_GPIO_Init函数来配置。
3)配置DCMI相关设置,初始化DCMI接口。
这一步,主要通过DCMI_CR寄存器设置,包括VSPOL/HSPOL/PCKPOL/数据宽度等重要参数,都在这一步设置。HAL库提供了DCMI初始化函数HAL_DCMI_Init,函数声明如下:
HAL_StatusTypeDef HAL_DCMI_Init(DCMI_HandleTypeDef *hdcmi);
该结构体第一个成员变量Instance用来指向寄存器基地址,设置为DCMI即可。
成员变量XferCount,XferSize,XferTransferNumber,pBuffPtr和DMA_Handle是与HAL库中DMA处理相关中间变量,由于使用HAL库配置的DCMI DMA会非常复杂,而且灵活性不高,所以本实验我们是独立配置的DMA。
同样,HAL库也提供了DCMI接口的MSP初始化回调函数:
void HAL_DCMI_MspInit(DCMI_HandleTypeDef* hdcmi);
一般情况下,该函数内部编写时钟使能,IO初始化以及NVIC相关程序。
4)配置DMA。
本实验我们采用连续模式采集,并将采集到的数据输出到LCD(RGB565模式)或内存(JPEG模式),所以源地址都是DCMI_DR,而目的地址可能是LCD->RAM或者SDRAM的地址。DCMI的DMA传输采用的是DMA1数据流1的通道1来实现的,关于DMA的介绍,请大家参考前面的DMA实验章节。
5)设置OV5640的图像输出大小,使能DCMI捕获。
图像输出大小设置,分两种情况:在RGB565模式下,我们根据LCD的尺寸,设置输出图像大小,以实现全屏显示(图像可能因缩放而变形);在JPEG模式下,我们可以自由设置输出图像大小(可不缩放);最后,开启DCMI捕获,即可正常工作了。
43.3.2 程序流程图
F329BA8E-5F0C-474b-AC03-BD98D2746FCC.png
图43.3.2.1 摄像头实验程序流程图

43.3.3 程序解析
1. DCMI驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。DCMI驱动源码包括两个文件:dcmi.c和dcmi.h。
dcmi.h头文件只是一些声明,下面直接开始介绍dcmi.c文件,首先是DCMI初始化函数,其定义如下:
/**
* @brief       DCMI 初始化
*   @note      IO对应关系如下:
*              摄像头模块 ------------ STM32开发板
*               OV_D0~D7  ------------  PC6/PC7/PC8/PC9/PC11/PD3/PB8/PB9
*               OV_SCL    ------------  PB10
*               OV_SDA    ------------  PB11
*               OV_VSYNC  ------------  PB7
*               OV_HREF   ------------  PA4
*               OV_RESET  ------------  PA7
*               OV_PCLK   ------------  PA6
*               OV_PWDN   ------------  PC4
*              本函数仅初始化OV_D0~D7/OV_VSYNC/OV_HREF/OV_PCLK等信号(11个).
* @param       无
* @retval      无
*/
void dcmi_init(void)
{
g_dcmi_handle.Instance = DCMI;
g_dcmi_handle.Init.SynchroMode =DCMI_SYNCHRO_HARDWARE;/*硬件同步HSYNC,VSYNC*/
    g_dcmi_handle.Init.PCKPolarity = DCMI_PCKPOLARITY_RISING;/* PCLK 上升沿有效 */
    g_dcmi_handle.Init.VSPolarity = DCMI_VSPOLARITY_LOW;     /* VSYNC 低电平有效 */
    g_dcmi_handle.Init.HSPolarity = DCMI_HSPOLARITY_LOW;     /* HSYNC 低电平有效 */
    g_dcmi_handle.Init.CaptureRate = DCMI_CR_ALL_FRAME;      /* 全帧捕获 */
    g_dcmi_handle.Init.ExtendedDataMode = DCMI_EXTEND_DATA_8B;/* 8位数据格式 */
    HAL_DCMI_Init(&g_dcmi_handle);    /* 初始化DCMI,此函数会开启帧中断 */

    /* 关闭行中断、VSYNC中断、同步错误中断和溢出中断 */
    //__HAL_DCMI_DISABLE_IT(&g_dcmi_handle,DCMI_IT_LINE|DCMI_IT_VSYNC
|DCMI_IT_ERR|DCMI_IT_OVR);

    /* 关闭所有中断,函数HAL_DCMI_Init()会默认打开很多中断,开启这些中断
      以后我们就需要对这些中断做相应的处理,否则的话就会导致各种各样的问题,
      但是这些中断很多都不需要,所以这里将其全部关闭掉,也就是将IER寄存器清零。
      关闭完所有中断以后再根据自己的实际需求来使能相应的中断 */
    DCMI->IER=0x0;
    __HAL_DCMI_ENABLE_IT(&g_dcmi_handle,DCMI_IT_FRAME);          /* 使能帧中断 */
    __HAL_DCMI_ENABLE(&g_dcmi_handle);                               /* 使能DCMI */
}
该函数主要对DCMI_HandleTypeDef结构体成员赋值并初始化,最后关闭所有中断,只开启帧中断,使能DCMI。而DCMI接口的GPIO口的初始化是在HAL_DCMI_MspInit回调函数中完成,其定义如下:
/**
* @brief       DCMI底层驱动,引脚配置,时钟使能,中断配置
* @param       hdcmi:DCMI句柄
* @note        此函数会被HAL_DCMI_Init()调用
* @retval      无
*/
void HAL_DCMI_MspInit(DCMI_HandleTypeDef* hdcmi)
{
    GPIO_InitTypeDef gpio_init_struct;
    __HAL_RCC_DCMI_CLK_ENABLE();                /* 使能DCMI时钟 */
    __HAL_RCC_GPIOA_CLK_ENABLE();               /* 使能GPIOA时钟 */
    __HAL_RCC_GPIOB_CLK_ENABLE();               /* 使能GPIOB时钟 */
    __HAL_RCC_GPIOC_CLK_ENABLE();               /* 使能GPIOC时钟 */
    __HAL_RCC_GPIOD_CLK_ENABLE();               /* 使能GPIOD时钟 */
    gpio_init_struct.Pin = GPIO_PIN_4 | 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_DCMI;                 /* 复用为DCMI */
    HAL_GPIO_Init(GPIOA, &gpio_init_struct);                      /* 初始化PA4,6引脚 */

    gpio_init_struct.Pin = GPIO_PIN_7 | GPIO_PIN_8 | GPIO_PIN_9;
    HAL_GPIO_Init(GPIOB, &gpio_init_struct);               /* 初始化PB7,8,9引脚 */

gpio_init_struct.Pin = GPIO_PIN_6 | GPIO_PIN_7 | GPIO_PIN_8
| GPIO_PIN_9 | GPIO_PIN_11;
    HAL_GPIO_Init(GPIOC, &gpio_init_struct);         /* 初始化PC6,7,8,9,11引脚 */

    gpio_init_struct.Pin = GPIO_PIN_3;
    HAL_GPIO_Init(GPIOD, &gpio_init_struct);               /* 初始化PD3引脚 */

    HAL_NVIC_SetPriority(DCMI_IRQn, 2, 2);                     /* 抢占优先级2,子优先级2 */
    HAL_NVIC_EnableIRQ(DCMI_IRQn);                              /* 使能DCMI中断 */
}
DCMI接口的GPIO口前面都介绍过了,该函数最后设置了DCMI中断抢占优先级为2,子优先级为2,并且使能DCMI中断。
接下来介绍DCMI DMA配置初始化函数,其定义如下:
/**
* @brief       DCMI DMA配置
* @param       mem0addr: 存储器地址0     将要存储摄像头数据的内存地址(也可以是外设地址)
* @param       mem1addr: 存储器地址1     当只使用mem0addr的时候,该值必须为0
* @param       memsize : 存储器长度      0~65535
* @param       memblen : 存储器位宽      DMA_MDATAALIGN_BYTE,8位;
DMA_MDATAALIGN_HALFWORD,16位;DMA_MDATAALIGN_WORD,32位
* @param       meminc : 存储器增长方式 DMA_MINC_DISABLE,不增长; DMA_MINC_ENABLE,增长
* @retval      无
*/
void dcmi_dma_init(uint32_t mem0addr,uint32_t mem1addr,
uint16_t memsize,uint32_t memblen,uint32_t meminc)
{
__HAL_RCC_DMA1_CLK_ENABLE();                             /* 使能DMA1时钟 */
/* 将DMA与DCMI联系起来 */
__HAL_LINKDMA(&g_dcmi_handle, DMA_Handle, g_dma_dcmi_handle);
/* 先关闭DMA传输完成中断(否则在使用MCU屏的时候会出现花屏的情况) */
    __HAL_DMA_DISABLE_IT(&g_dma_dcmi_handle, DMA_IT_TC);               

    g_dma_dcmi_handle.Instance = DMA1_Stream1;                 /* DMA1数据流1 */
    g_dma_dcmi_handle.Init.Request = DMA_REQUEST_DCMI;       /* DCMI的DMA请求 */
    g_dma_dcmi_handle.Init.Direction = DMA_PERIPH_TO_MEMORY;/* 外设到存储器 */
    g_dma_dcmi_handle.Init.PeriphInc = DMA_PINC_DISABLE;     /* 外设非增量模式 */
g_dma_dcmi_handle.Init.MemInc = meminc;                           /* 存储器增量模式 */
/* 外设数据长度:32位 */
    g_dma_dcmi_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;   
    g_dma_dcmi_handle.Init.MemDataAlignment = memblen;/*存储器数据长度:8/16/32位*/
    g_dma_dcmi_handle.Init.Mode = DMA_CIRCULAR;                      /* 使用循环模式 */
    g_dma_dcmi_handle.Init.Priority = DMA_PRIORITY_HIGH;           /* 高优先级 */
g_dma_dcmi_handle.Init.FIFOMode = DMA_FIFOMODE_ENABLE; /* 使能FIFO */
/* 使用1/2的FIFO */
    g_dma_dcmi_handle.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_HALFFULL;
    g_dma_dcmi_handle.Init.MemBurst = DMA_MBURST_SINGLE;    /* 存储器突发传输 */
    g_dma_dcmi_handle.Init.PeriphBurst = DMA_PBURST_SINGLE;/* 外设突发单次传输 */
    HAL_DMA_DeInit(&g_dma_dcmi_handle);                        /* 先清除以前的设置 */
    HAL_DMA_Init(&g_dma_dcmi_handle);                                  /* 初始化DMA */
   
/* 在开启DMA之前先使用__HAL_UNLOCK()解锁一次DMA,因为HAL_DMA_Statrt()
HAL_DMAEx_MultiBufferStart()这两个函数一开始要先使用__HAL_LOCK()锁定DMA,
而函数__HAL_LOCK()会判断当前的DMA状态是否为锁定状态,如果是锁定状态的话就直接
返回HAL_BUSY,这样会导致函数HAL_DMA_Statrt()和HAL_DMAEx_MultiBufferStart()
后续的DMA配置程序直接被跳过!DMA也就不能正常工作,为了避免这种现象,所以在启动
DMA之前先调用__HAL_UNLOC()先解锁一次DMA。 */
   
    __HAL_UNLOCK(&g_dma_dcmi_handle);
    if (mem1addr == 0)  /* 开启DMA,不使用双缓冲 */
    {
        HAL_DMA_Start(&g_dma_dcmi_handle, (uint32_t)&DCMI->DR,
mem0addr, memsize);
    }
    else                /* 使用双缓冲 */
    {
        HAL_DMAEx_MultiBufferStart(&g_dma_dcmi_handle, (uint32_t)&DCMI->DR,
mem0addr, mem1addr, memsize); /* 开启双缓冲 */
        __HAL_DMA_ENABLE_IT(&g_dma_dcmi_handle, DMA_IT_TC);  /* 开启传输完成中断 */
        HAL_NVIC_SetPriority(DMA1_Stream1_IRQn, 2, 3);        /* DMA中断优先级 */
        HAL_NVIC_EnableIRQ(DMA1_Stream1_IRQn);
    }
}
该函数用于配置DCMI的DMA传输,其外设地址固定为:DCMI->DR,而存储器地址可变(LCD或者SRAM)。DMA被配置为循环模式,一旦开启,DMA将不停的循环传输数据。
下面介绍的是DCMI启动传输和关闭传输函数,它们的定义分别如下:
/**
* @brief       DCMI,启动传输
* @param       无
* @retval      无
*/
void dcmi_start(void)
{  
    lcd_set_cursor(0,0);                      /* 设置坐标到原点 */
    lcd_write_ram_prepare();                 /* 开始写入GRAM */
    __HAL_DMA_ENABLE(&g_dma_dcmi_handle); /* 使能DMA */
    DCMI->CR |= DCMI_CR_CAPTURE;            /* DCMI捕获使能 */
}

/**
* @brief       DCMI,关闭传输
* @param       无
* @retval      无
*/
void dcmi_stop(void)
{
    DCMI->CR &= ~(DCMI_CR_CAPTURE);                 /* DCMI捕获关闭 */
    while (DCMI->CR & 0X01);                         /* 等待传输结束 */
    __HAL_DMA_DISABLE(&g_dma_dcmi_handle);        /* 关闭DMA */
}
下面介绍的是DCMI中断服务函数(及其回调函数)、DCMI DMA接收回调函数和DMA1数据流1中断服务函数,它们的定义分别如下:
/**
* @brief       DCMI中断服务函数
* @param       无
* @retval      无
*/
void DCMI_IRQHandler(void)
{
    HAL_DCMI_IRQHandler(&g_dcmi_handle);
}

/**
* @brief       DCMI中断回调服务函数
* @param       hdcmi:DCMI句柄
* @note        捕获到一帧图像处理
* @retval      无
*/
void HAL_DCMI_FrameEventCallback(DCMI_HandleTypeDef *hdcmi)
{
    jpeg_data_process();    /* jpeg数据处理 */
    LED1_TOGGLE();           /* LED1闪烁 */
    g_ov_frame++;

    /* 重新使能帧中断,因为HAL_DCMI_IRQHandler()函数会关闭帧中断 */
    __HAL_DCMI_ENABLE_IT(&g_dcmi_handle, DCMI_IT_FRAME);
}

void (*dcmi_rx_callback)(void); /* DCMI DMA接收回调函数 */

/**
* @brief       DMA1数据流1中断服务函数(仅双缓冲模式会用到)
* @param       无
* @retval      无
*/
void DMA1_Stream1_IRQHandler(void)
{
/* DMA传输完成 */
    if (__HAL_DMA_GET_FLAG(&g_dma_dcmi_handle, DMA_FLAG_TCIF1_5) != RESET)  
{
/* 清除DMA传输完成中断标志位 */
        __HAL_DMA_CLEAR_FLAG(&g_dma_dcmi_handle, DMA_FLAG_TCIF1_5);
        dcmi_rx_callback();     /* 执行摄像头接收回调函数,读取数据等操作在这里面处理 */
    }
}
其中:DCMI_IRQHandler函数,用于处理帧中断,可以实现帧率统计(需要定时器支持)和JPEG数据处理等,实际上当捕获到一帧数据后,调用的是HAL库回调函数HAL_DCMI_FrameEventCallback进行处理。DMA1_Stream1_IRQHandler 函数,仅用于在使用RGB屏的时候,双缓冲存储时,数据的搬运处理(通过dcmi_rx_callback函数实现)。
最后还定义两个可以通过usmart调试、辅助测试使用的函数dcmi_set_window和dcmi_cr_set函数。dcmi_set_window函数用于调节屏幕显示的范围,本实验LCD的起始坐标要设置为(0,0),LCD显示范围要设置为屏幕最大像素点范围内。dcmi_cr_set函数用于设置pclk/hsync/vsync 这三个信号的有效电平。
DCMI驱动代码就介绍到这里。
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               GPIOB
#define SCCB_SCL_GPIO_PIN                GPIO_PIN_10
#define SCCB_SCL_GPIO_CLK_ENABLE()      
do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0)   /* PB口时钟使能 */

#define SCCB_SDA_GPIO_PORT               GPIOB
#define SCCB_SDA_GPIO_PIN                GPIO_PIN_11
#define SCCB_SDA_GPIO_CLK_ENABLE()      
do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0)   /* PB口时钟使能 */
/*****************************************************************************/
/* 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_READ_SDA     HAL_GPIO_ReadPin(SCCB_SDA_GPIO_PORT,
SCCB_SDA_GPIO_PIN) /* 读取SDA */
SCCB时序有两根信号线(SCCB_SCL和SCCB_SDA),所以这里定义了两个IO口(PB10和PB11)来控制。IO操作函数有三个,包括SCCB_SCL用于控制时钟,SCCB_SDA是写IO口输出的值为逻辑1或者逻辑0,SCCB_READ_SDA是读取IO口的值,逻辑1或者逻辑0。
接下来介绍sccb.c文件的代码,首先是SCCB接口初始化函数,该函数主要就是初始化PB10和PB11两个IO口,其定义如下:
/**
* @brief       初始化SCCB
* @param       无
* @retval      无
*/
void sccb_init(void)
{
    GPIO_InitTypeDef gpio_init_struct;

    SCCB_SCL_GPIO_CLK_ENABLE();     /* SCL引脚时钟使能 */
    SCCB_SDA_GPIO_CLK_ENABLE();     /* SDA引脚时钟使能 */

    gpio_init_struct.Pin = SCCB_SCL_GPIO_PIN;
    gpio_init_struct.Mode = GPIO_MODE_OUTPUT_OD;                    /* 开漏输出 */
    gpio_init_struct.Pull = GPIO_PULLUP;                             /* 上拉 */
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;           /* 高速 */
    HAL_GPIO_Init(SCCB_SCL_GPIO_PORT, &gpio_init_struct);        /* 初始化SCL引脚 */

    gpio_init_struct.Pin = SCCB_SDA_GPIO_PIN;
    HAL_GPIO_Init(SCCB_SDA_GPIO_PORT, &gpio_init_struct);   /* 初始化SDA引脚 */
/* SDA引脚模式设置,开漏输出,上拉, 这样就不用再设置IO方向了,
开漏输出的时候(=1), 也可以读取外部信号的高低电平 */
    sccb_stop();     /* 停止总线上所有设备 */
}
PB10和PB11都设置为带上拉的开漏输出,这样设置的好处是: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、访问地址和相关寄存器定义,其定义如下:
/*****************************************************************************/
/* PWDN和RESET 引脚 定义 */
#define OV_PWDN_GPIO_PORT               GPIOC
#define OV_PWDN_GPIO_PIN                GPIO_PIN_4
#define OV_PWDN_GPIO_CLK_ENABLE()       do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0)   /* PC口时钟使能 */

#define OV_RESET_GPIO_PORT              GPIOA
#define OV_RESET_GPIO_PIN               GPIO_PIN_7
#define OV_RESET_GPIO_CLK_ENABLE()      do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)   /* PA口时钟使能 */
/*****************************************************************************/
/* 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)                     /* POWER DOWN控制信号 */

#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和访问地址 */
#define OV5640_ID           0X5640              /* OV5640的芯片ID */
#define OV5640_ADDR         0X78                /* OV5640的IIC地址 */
/* OV5640相关寄存器定义 */
#define OV5640_CHIPIDH      0X300A              /* OV5640芯片ID高字节 */
#define OV5640_CHIPIDL      0X300B              /* OV5640芯片ID低字节 */
下面开始介绍ov5640.c文件,首先是OV5640初始化函数,其定义如下:
/**
* @brief       OV5640 初始化
* @param       无
* @retval      0, 成功; 1, 失败;
*/
uint8_t ov5640_init(void)
{
    GPIO_InitTypeDef gpio_init_struct;

    uint16_t i = 0;
    uint16_t reg;
   
    OV_PWDN_GPIO_CLK_ENABLE();      /* 使能OV_PWDN脚时钟 */
    OV_RESET_GPIO_CLK_ENABLE();     /* 使能OV_RESET脚时钟 */

    gpio_init_struct.Pin = OV_PWDN_GPIO_PIN;
    gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;                   /* 推挽输出 */
    gpio_init_struct.Pull = GPIO_PULLUP;                             /* 上拉 */
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;          /* 高速 */
    HAL_GPIO_Init(OV_PWDN_GPIO_PORT, &gpio_init_struct); /* 初始化OV_PWDN引脚 */

    gpio_init_struct.Pin = OV_RESET_GPIO_PIN;
    HAL_GPIO_Init(OV_RESET_GPIO_PORT, &gpio_init_struct);/* 初始化OV_RESET引脚 */
    OV5640_RST(0);      /* 必须先拉低OV5640的RST脚,再上电 */
    delay_ms(20);
    OV5640_PWDN(0);     /* POWER ON */
    delay_ms(5);
    OV5640_RST(1);      /* 结束复位 */
    delay_ms(20);
    sccb_init();        /* 初始化SCCB 的IO口 */
    delay_ms(5);
    reg = ov5640_read_reg(OV5640_CHIPIDH);  /* 读取ID 高八位 */
    reg <<= 8;
    reg |= ov5640_read_reg(OV5640_CHIPIDL); /* 读取ID 低八位 */
    if (reg != OV5640_ID)   /* ID是否正常? */
    {
        printf("ID:%d\r\n", reg);
        return 1;           /* 失败 */
    }
    ov5640_write_reg(0x3103, 0X11); /* system clock from pad, bit[1] */
    ov5640_write_reg(0X3008, 0X82); /* 软复位 */
    delay_ms(10);

    /* 初始化 OV5640 */
    for (i = 0; i < sizeof(ov5640_init_reg_tbl) / 4; i++)
    {
        ov5640_write_reg(ov5640_init_reg_tbl[0], ov5640_init_reg_tbl[1]);
    }
    /* 检查闪光灯是否正常 */
    ov5640_flash_ctrl(1);           /* 打开闪光灯 */
    delay_ms(50);
    ov5640_flash_ctrl(0);           /* 关闭闪光灯 */
    return 0;                         /* ok */
}
该函数除了初始化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函数就是前面在43.1.1节所介绍的3个窗口的设置,他们共同决定了图像的输出。
其他的函数就不列出来了,请大家查看源码。
3. TIMER驱动代码
TIMER驱动代码我们主要就是用到定时器6,在定时器6更新中断回调函数中打印帧率,更新中断回调函数定义如下:
/**
* @brief       定时器更新中断回调函数
* @param       htim:定时器句柄指针
* @retval      无
*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim == (&g_timx_handle))
    {
        printf("frame:%d\r\n", g_ov_frame);
        g_ov_frame = 0;
    }
}
g_ov_frame变量在dcmi.c中被定义,用于存放帧率。TIMER的其他驱动代码在介绍定时器的时候已经介绍过,请看源码。
4. USART2驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。USART2驱动源码包括两个文件:usart2.c和usart2.h。
usart2.h头文件对usart2的IO口做了宏定义,具体如下:
/*****************************************************************************/
/* 串口2 引脚 定义 */
#define USART2_TX_GPIO_PORT                  GPIOA
#define USART2_TX_GPIO_PIN                   GPIO_PIN_2
#define USART2_TX_GPIO_AF                    GPIO_AF7_USART2             /* AF功能选择 */
#define USART2_TX_GPIO_CLK_ENABLE()         
do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)           /* PA口时钟使能 */

#define USART2_RX_GPIO_PORT                  GPIOA
#define USART2_RX_GPIO_PIN                   GPIO_PIN_3
#define USART2_RX_GPIO_AF                    GPIO_AF7_USART2             /* AF功能选择 */
#define USART2_RX_GPIO_CLK_ENABLE()         
do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)           /* PA口时钟使能 */
/*****************************************************************************/
PA2和PA3复用为串口2的发送和接收引脚。
下面介绍usart2.c文件的串口2初始化函数,其定义如下:
/**
* @brief       串口2初始化函数
* @note        注意:串口2的时钟源频率在sys已经设置过了
* @param       baudrate: 波特率, 根据自己需要设置波特率值
*/
void usart2_init(uint32_t baudrate)
{
    GPIO_InitTypeDef gpio_init_struct;

    __HAL_RCC_USART2_CLK_ENABLE();  /* 使能USART2时钟 */
    USART2_TX_GPIO_CLK_ENABLE();            /* 使能串口TX脚时钟 */
    USART2_RX_GPIO_CLK_ENABLE();            /* 使能串口RX脚时钟 */

    gpio_init_struct.Pin = USART2_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_TX_GPIO_AF;                /* 复用为USART2 */
    HAL_GPIO_Init(USART2_TX_GPIO_PORT, &gpio_init_struct);        /* 初始化串口TX引脚 */

    gpio_init_struct.Pin = USART2_RX_GPIO_PIN;
    gpio_init_struct.Alternate = USART2_RX_GPIO_AF;                 /* 复用为USART2 */
    HAL_GPIO_Init(USART2_RX_GPIO_PORT, &gpio_init_struct); /* 初始化串口RX引脚 */

    g_uart2_handle.Instance = USART2;                                 /* USART2 */
    g_uart2_handle.Init.BaudRate = baudrate;                         /* 波特率 */
    g_uart2_handle.Init.WordLength = UART_WORDLENGTH_8B;          /* 字长为8位数据格式 */
    g_uart2_handle.Init.StopBits = UART_STOPBITS_1;                /* 一个停止位 */
    g_uart2_handle.Init.Parity = UART_PARITY_NONE;                 /* 无奇偶校验位 */
    g_uart2_handle.Init.HwFlowCtl = UART_HWCONTROL_NONE;          /* 无硬件流控 */
    g_uart2_handle.Init.Mode = UART_MODE_TX;                        /* 发模式 */
    HAL_UART_Init(&g_uart2_handle);                                   /* 使能USART2 */
}
这里需要注意的是,串口2的时钟源频率在sys_stm32_clock_init函数中已经设置了。其他的代码基本usart_init函数是一样的。
5. main.c代码
main.c前面定义了一些变量和数组,具体如下:
uint8_t g_ov_mode = 0;            /* bit0: 0,RGB565模式;  1,JPEG模式 */
uint16_t g_curline = 0;           /* 摄像头输出数据,当前行编号 */
uint16_t g_yoffset = 0;           /* y方向的偏移量 */

#define jpeg_buf_size   120*1024 /* 定义JPEG数据缓存jpeg_buf的大小(480KB字节) */
#define jpeg_line_size  4*1024   /* 定义DMA接收数据时,一行数据的最大值 */

/* JPEG数据采集,摄像头采用一行一行读取,定义行缓存 */
uint32_t g_dcmi_line_buf[2][jpeg_line_size] __attribute__((at(0x38000000)));   
uint32_t g_jpeg_data_buf[jpeg_buf_size];/* JPEG数据缓存buf,定义在内部SRAM */

/* buf中的JPEG有效数据长度 */
volatile uint32_t g_jpeg_data_len = 0;

/* JPEG数据采集完成标志
* 0,数据没有采集完;
* 1,数据采集完了,但是还没处理;
* 2,数据已经处理完成了,可以开始下一帧接收
*/
volatile uint8_t g_jpeg_data_ok = 0;

/* JPEG尺寸支持列表 */
const uint16_t jpeg_img_size_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 */
};

const char *EFFECTS_TBL[7] = {"Normal", "Cool", "Warm", "B&W", "Yellowish ",
"Inverse", "Greenish"};     /* 7种特效 */
const char *JPEG_SIZE_TBL[12] = {"QQVGA", "QVGA", "VGA", "SVGA", "XGA", "WXGA",
"WXGA+", "SXGA", "UXGA", "1080P", "QXGA", "500W"}; /* JPEG图片 12种尺寸 */
其中,定义的g_jpeg_data_buf数组大小为480KB字节,用来存储JPEG数据。g_dcmi_line_buf数组,我们定义数组的起始地址为0x38000000,由__attribute__关键字,指定数组地址。
在main.c里面,总共有5个函数,我们接下来分别介绍。首先是处理JPEG数据函数,其定义如下:
/**
* @brief       处理JPEG数据
*   @ntoe      在DCMI_IRQHandler中断服务函数里面被调用
*               当采集完一帧JPEG数据后,调用此函数,切换JPEG BUF.开始下一帧采集.
*
* @param       无
* @retval      无
*/
void jpeg_data_process(void)
{
    uint16_t i;
    uint16_t rlen;            /* 剩余数据长度 */
    uint32_t *pbuf;
    g_curline = g_yoffset;  /* 行数复位 */
    if (g_ov_mode & 0X01)   /* 只有在JPEG格式下,才需要做处理. */
    {
        if (g_jpeg_data_ok == 0)    /* jpeg数据还未采集完? */
        {
            __HAL_DMA_DISABLE(&g_dma_dcmi_handle);      /* 关闭DMA */
/* 得到剩余数据长度 */
            rlen = jpeg_line_size - __HAL_DMA_GET_COUNTER(&g_dma_dcmi_handle);
            pbuf = g_jpeg_data_buf + g_jpeg_data_len;/* 偏移到有效数据末尾,继续添加 */
            if (DMA1_Stream1->CR & (1 << 19))
            {
                for (i = 0; i < rlen; i++)
                {
                    pbuf
= g_dcmi_line_buf[1];    /* 读取buf1里面的剩余数据 */
                }
            }
            else
            {
                for (i = 0; i < rlen; i++)
                {
                    pbuf
= g_dcmi_line_buf[0];    /* 读取buf0里面的剩余数据 */
                }
            }
            g_jpeg_data_len += rlen;    /* 加上剩余长度 */
            g_jpeg_data_ok = 1;          /* 标记JPEG数据采集完成,等待其他函数处理 */
        }
        if (g_jpeg_data_ok == 2)        /* 上一次的jpeg数据已经被处理了 */
        {
/* 传输长度为jpeg_buf_size*4字节 */
            __HAL_DMA_SET_COUNTER(&g_dma_dcmi_handle, jpeg_line_size);
            __HAL_DMA_ENABLE(&g_dma_dcmi_handle);           /* 重新传输 */
            g_jpeg_data_ok = 0;                                /* 标记数据未采集 */
            g_jpeg_data_len = 0;                               /* 数据重新开始 */
        }
    }
    else
    {
        lcd_set_cursor(0, 0);
        lcd_write_ram_prepare();        /* 开始写入GRAM */
    }
}
该函数用于处理JPEG数据的接收,在DCMI_IRQHandler函数(在dcmi.c里面)里面被调用,它与jpeg_dcmi_rx_callback函数和jpeg_test函数共同控制JPEG的数据传送。JPEG数据的接收,采用DMA双缓冲机制,缓冲数组为:dcmi_line_buf(u32类型,RGB屏接收RGB565数据时,也是用这个数组);数组大小为:jpeg_line_size,我们定义的是4*1024,即数组大小为16K字节(数组大小不能小于存储摄像头一行输出数据的大小);JPEG数据接收处理流程如图43.3.3.1所示:
第四十三章 摄像头实验37295.png
图43.3.3.1 JPEG数据流DMA双缓冲接收流程

        JPEG数据采集流程:当JPEG数据流传输给MCU的时候,首先由M0AR存储,此时如果M1AR有数据,则可以读取M1AR里面的数据,当M0AR数据满时,由M1AR存储,此时程序可以读取M0AR里面所存储的数据,当M1AR数据满时,由M0AR存储……。这个存储数据的操作,绝大部分是由DMA传输完成中断服务函数,调用jpeg_dcmi_rx_callback函数实现的,当一帧数据传输完成时,会进入DCMI帧中断服务函数,调用jpeg_data_process函数,对最后的剩余数据进行存储,完成一帧JPEG数据的采集。
接下来介绍的是JPEG数据接收回调函数,其定义如下:
/**
* @brief       JPEG数据接收回调函数
*   @ntoe      在DMA1_Stream1_IRQHandler中断服务函数里面被调用
*
* @param       无
* @retval      无
*/
void jpeg_dcmi_rx_callback(void)
{
    uint16_t i;
    volatile uint32_t *pbuf;
    pbuf = g_jpeg_data_buf + g_jpeg_data_len;   /* 偏移到有效数据末尾 */

    if (DMA1_Stream1->CR & (1 << 19))                    /* buf0已满,正常处理buf1 */
    {
        for (i = 0; i < jpeg_line_size; i++)
        {
            pbuf
= g_dcmi_line_buf[0];            /* 读取buf0里面的数据 */
        }
        
        g_jpeg_data_len += jpeg_line_size;              /* 偏移 */
    }
    else    /* buf1已满,正常处理buf0 */
    {
        for (i = 0; i < jpeg_line_size; i++)
        {
            pbuf
= g_dcmi_line_buf[1];            /* 读取buf1里面的数据 */
        }
        
        g_jpeg_data_len += jpeg_line_size;             /* 偏移 */
    }
    SCB_CleanInvalidateDCache();                                /* 清除无效化DCache */
}
该函数是jpeg数据接收的主要函数,通过判断DMA1_Stream1->CR寄存器,读取不同buf里面的数据,存储到g_jpeg_data_buf里面。该函数由DMA的传输完成中断服务函数:DMA1_Stream1_IRQHandler调用。
接下来介绍的是JPEG测试函数,其定义如下:
/**
* @brief       JPEG测试
*   @ntoe      JPEG数据,通过串口2发送给电脑.
*
* @param       无
* @retval      无
*/
void jpeg_test(void)
{
    uint32_t i, jpgstart, jpglen;
    uint8_t *p;
    uint8_t key, headok = 0;
    uint8_t effect = 0;
    uint8_t size = 2;           /* 默认是QVGA 320*240尺寸 */
    uint8_t msgbuf[15]; /* 消息缓存区 */
    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, 120, 200, 16, 16, "KEY0:Auto Focus", RED);/*执行自动对焦*/
    lcd_show_string(30, 140, 200, 16, 16, "KEY1:Effects", RED);   /* 特效 */
    lcd_show_string(30, 160, 200, 16, 16, "KEY_UP:Size", RED);    /* 分辨率设置 */
sprintf((char *)msgbuf, "JPEG Size:%s", JPEG_SIZE_TBL[size]);
/* 显示当前JPEG分辨率 */
    lcd_show_string(30, 180, 200, 16, 16, (char*)msgbuf, RED);
   
    /* 自动对焦初始化 */
    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();          /* 启动持续对焦 */
   
    dcmi_init();                        /* DCMI配置 */
    dcmi_rx_callback = jpeg_dcmi_rx_callback; /* JPEG接收数据回调函数 */
dcmi_dma_init((uint32_t)&g_dcmi_line_buf[0], (uint32_t)&g_dcmi_line_buf[1],
jpeg_line_size, DMA_MDATAALIGN_WORD, DMA_MINC_ENABLE); /* DCMI DMA配置 */
ov5640_outsize_set(4, 0, jpeg_img_size_tbl[size][0],
jpeg_img_size_tbl[size][1]); /* 设置输出尺寸 */
    dcmi_start();               /* 启动传输 */
    while (1)
    {
        if (g_jpeg_data_ok == 1)    /* 已经采集完一帧图像了 */
        {
            p = (uint8_t *)g_jpeg_data_buf;
            printf("g_jpeg_data_len:%d\r\n", g_jpeg_data_len * 4);  /* 打印帧率 */
/* 提示正在传输数据 */
            lcd_show_string(30, 210, 210, 16, 16, "Sending JPEG data...", RED);
            jpglen = 0;         /* 设置jpg文件大小为0 */
            headok = 0;         /* 清除jpg头标记 */
/* 查找0XFF,0XD8和0XFF,0XD9,获取jpg文件大小 */
            for (i = 0; i < g_jpeg_data_len * 4; i++)   
            {
                if ((p
== 0XFF) && (p[i + 1] == 0XD8))   /* 找到FF D8 */
                {
                    jpgstart = i;
                    headok = 1; /* 标记找到jpg头(FF D8) */
                }
/* 找到头以后,再找FF D9 */
                if ((p
== 0XFF) && (p[i + 1] == 0XD9) && headok)
                {
                    jpglen = i - jpgstart + 2;
                    break;
                }
            }
            if (jpglen)          /* 正常的jpeg数据 */
            {
                p += jpgstart;  /* 偏移到0XFF,0XD8处 */
                for (i = 0; i < jpglen; i++)    /* 发送整个jpg文件 */
                {
                    USART2->TDR = p
;
                    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: /* 执行一次自动对焦 */
                        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: /* JPEG输出尺寸设置 */
                        size++;
/* 最大只支持WXGA的jpeg数据保存,再大分辨率就不够内存用了 */
                        if (size > 5)size = 0;  

                        ov5640_outsize_set(16, 4, jpeg_img_size_tbl[size][0],
jpeg_img_size_tbl[size][1]);  /* 设置输出尺寸 */
                        sprintf((char *)msgbuf, "JPEG Size:%s",
JPEG_SIZE_TBL[size]);
                        break;
                }
                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去采集下一帧了. */
        }
    }
}
该函数将OV5640设置为JPEG模式,并开启持续自动对焦、实现OV5640的JPEG数据接收,通过串口2发送给上位机软件。
接下来介绍的是RGB565测试函数,其定义如下:
/**
* @brief       RGB565测试
*   @ntoe      RGB数据直接显示在LCD上面
*
* @param       无
* @retval      无
*/
void rgb565_test(void)
{
    uint8_t key;
    float fac = 0;
    uint8_t effect = 0;
    uint8_t scale = 1;                  /* 默认是全尺寸缩放 */
    uint8_t msgbuf[15];                 /* 消息缓存区 */
    uint16_t outputheight = 0;

    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, 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:Effects", RED);   /* 特效 */
/* 1:1尺寸(显示真实尺寸)/全尺寸缩放 */
    lcd_show_string(30, 160, 200, 16, 16, "KEY_UP:FullSize/Scale", RED);   
    /* 自动对焦初始化 */
    ov5640_rgb565_mode();               /* RGB565模式 */
    ov5640_focus_init();
    ov5640_brightness(0);               /* 自动模式 */
    ov5640_color_saturation(3);        /* 色彩饱和度0 */
    ov5640_brightness(4);               /* 亮度0 */
    ov5640_contrast(3);                 /* 对比度0 */
    ov5640_sharpness(33);               /* 自动锐度 */
    ov5640_focus_constant();           /* 启动持续对焦 */
    dcmi_init();                         /* DCMI配置 */
dcmi_dma_init((uint32_t)&LCD->LCD_RAM, 0, 1, DMA_MDATAALIGN_HALFWORD,
DMA_MINC_DISABLE); /* DCMI DMA配置,MCU屏,竖屏 */
    if (lcddev.height >= 800)
    {
        g_yoffset = (lcddev.height - 800) / 2;
        outputheight = 800;
        ov5640_write_reg(0x3035, 0X51);     /* 降低输出帧率,否则可能抖动 */
    }
    else
    {
        g_yoffset = 0;
        outputheight = lcddev.height;
    }
    g_curline = g_yoffset;              /* 行数复位 */
    ov5640_outsize_set(4, 0, lcddev.width, outputheight);   /* 满屏缩放显示 */
    dcmi_start();                        /* 启动传输 */
    lcd_clear(BLACK);

    while (1)
    {
        key = key_scan(0);
        if (key)
        {
            if (key != KEY0_PRES)
            {
                dcmi_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);
                dcmi_start();   /* 重新开始传输 */
            }
        }
        delay_ms(10);
    }
}
该函数将OV5640设置为RGB565模式,并将接收到的数据,传送给LCD。当使用MCU屏的时候,完全由硬件DMA传输给LCD,CPU不用处理。
接下来介绍的是main函数,其定义如下:
int main(void)
{
    uint8_t key = 0;
    uint16_t t = 0;

    sys_cache_enable();                            /* 打开L1-Cache */
    HAL_Init();                                      /* 初始化HAL库 */
    sys_stm32_clock_init(240, 2, 2, 4);         /* 设置时钟, 480Mhz */
    delay_init(480);                                /* 延时初始化 */
    usart_init(115200);                            /* 串口初始化为115200 */
    usmart_dev.init(240);                                 /* 初始化USMART */
    usart2_init(1500000);                          /* 初始化串口2波特率为1500000 */
    mpu_memory_protection();                      /* 保护相关存储区域 */
    led_init();                                             /* 初始化LED */
    lcd_init();                                             /* 初始化LCD */
    key_init();                                             /* 初始化按键 */
    /* 10Khz计数, 1秒钟中断一次, 用于统计帧率 */
    btim_timx_int_init(10000 - 1, 24000 - 1);

    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())   /* 初始化OV5640 */
    {
        lcd_show_string(30, 130, 240, 16, 16, "OV5640 ERROR", RED);
        delay_ms(200);
        lcd_fill(30, 130, 239, 170, WHITE);
        delay_ms(200);
        LED0_TOGGLE();
    }
    lcd_show_string(30, 130, 200, 16, 16, "OV5640 OK", RED);
    while (1)
    {
        key = key_scan(0);
        if (key == KEY0_PRES)
        {
            g_ov_mode = 0;    /* RGB565模式 */
            break;
        }
        else if (key == KEY1_PRES)
        {
            g_ov_mode = 1;    /* JPEG模式 */
            break;
        }
        t++;
        if (t == 100)lcd_show_string(30, 150, 230, 16, 16,
"KEY0:RGB565  KEY1:JPEG", RED);  /* 闪烁显示提示信息 */
        if (t == 200)
        {
            lcd_fill(30, 150, 210, 150 + 16, WHITE);
            t = 0;
            LED0_TOGGLE();
        }
        delay_ms(5);
    }
    if (g_ov_mode == 1)
    {
        jpeg_test();
    }
    else
    {
        rgb565_test();
    }
}
该函数完成对各相关硬件的初始化,然后检测OV5640,最后通过按键选择来调用jpeg_test还是rgb565_test,实现JPEG测试和RGB565测试。
前面提到,我们要用USMART来设置摄像头的参数,我们只需要在usmart_nametab里面添加OV5640_WR_Reg和OV5640_RD_Reg等相关函数,就可以轻松调试摄像头了。
43.4 下载验证
将程序下载到开发板后,可以看到LED0不停的闪烁,提示程序已经在运行了。LCD显示的内容如图43.4.1所示:
第四十三章 摄像头实验48865.png
图43.4.1 摄像头实验程序运行效果图

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

此时,可以按KEY_UP切换为1:1显示(不变形)。同时还可以通过KEY0按键,执行一次自动对焦;KEY1按键,设置特效。
当按KEY1后,选择JPEG模式,此时屏幕显示JPEG数据传输进程,如图43.4.2所示:
第四十三章 摄像头实验49185.png
图43.4.2 JPEG模式测试图

默认条件下,图像分辨率是QVGA (320*240)的,硬件上:我们需要用一根杜邦线连接开发板的PA2排针到RXD排针端。
打开上位机软件:XCAM V1.0.exe(路径:光盘6,软件资料软件串口&网络摄像头软件 XCAM V1.0.exe),选择正确的串口,然后波特率设置为1500000,打开即可收到下位机传过来的图片了,如图43.4.3所示:
第四十三章 摄像头实验49385.png
图43.4.3 XCAM V1.0.exe软件接收并显示JPEG图片

我们可以通过KEY_UP设置输出图像的尺寸(QQVGA~ WXGA)。KEY0按键,执行一次自动对焦;KEY1按键,设置特效。  
同时,你还可以在串口(开发板的串口1),通过USMART调用ov5640_write_reg等函数,来设置OV5640的各寄存器,达到调试测试OV5640的目的,如图43.4.4所示:
第四十三章 摄像头实验49581.png
图43.4.4 USMART调试OV5640

从上图还可以看出,帧率为7/8帧(实际上是7.5帧),每张JPEG图片的大小是33KB左右(分辨率为:640*480的时候)。

阿莫论坛20周年了!感谢大家的支持与爱护!!

一只鸟敢站在脆弱的枝条上歇脚,它依仗的不是枝条不会断,而是自己有翅膀,会飞。
回帖提示: 反政府言论将被立即封锁ID 在按“提交”前,请自问一下:我这样表达会给举报吗,会给自己惹麻烦吗? 另外:尽量不要使用Mark、顶等没有意义的回复。不得大量使用大字体和彩色字。【本论坛不允许直接上传手机拍摄图片,浪费大家下载带宽和论坛服务器空间,请压缩后(图片小于1兆)才上传。压缩方法可以在微信里面发给自己(不要勾选“原图),然后下载,就能得到压缩后的图片】。另外,手机版只能上传图片,要上传附件需要切换到电脑版(不需要使用电脑,手机上切换到电脑版就行,页面底部)。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

手机版|Archiver|amobbs.com 阿莫电子技术论坛 ( 粤ICP备2022115958号, 版权所有:东莞阿莫电子贸易商行 创办于2004年 (公安交互式论坛备案:44190002001997 ) )

GMT+8, 2024-3-29 20:10

© Since 2004 www.amobbs.com, 原www.ourdev.cn, 原www.ouravr.com

快速回复 返回顶部 返回列表