OpenEdv-开源电子网

 找回密码
 立即注册
正点原子全套STM32/Linux/FPGA开发资料,上千讲STM32视频教程免费下载...
查看: 2471|回复: 0

《STM32F407 探索者开发指南》第四十七章 摄像头实验

[复制链接]

1140

主题

1152

帖子

2

精华

超级版主

Rank: 8Rank: 8

积分
4895
金钱
4895
注册时间
2019-5-8
在线时间
1248 小时
发表于 2023-8-28 14:29:29 | 显示全部楼层 |阅读模式
本帖最后由 正点原子运营 于 2023-8-26 16:05 编辑

第四十七章 摄像头实验

1)实验平台:正点原子探索者STM32F407开发板

2) 章节摘自【正点原子】STM32F407开发指南 V1.1


4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/stm32/zdyz_stm32f407_explorerV3.html

5)正点原子官方B站:https://space.bilibili.com/394620890

6)STM32技术交流QQ群:151941872

155537c2odj87vz1z9vj6l.jpg

155537nfqovl2gg9faaol9.png

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

47.1 OV2640和DCMI简介
本节将分为两个部分,分别介绍OV2640简介和STM32F407 DCMI接口简介。另外,所有OV2640的相关资料,都在光盘:A盘à7,硬件资料àOV2640资料 文件夹里面。

47.1.1 OV2640简介
OV2640是OV(OmniVision)公司生产的一颗1/4寸的CMOS UXGA(1632*1232)图像传感器。该传感器体积小、工作电压低,提供单片UXGA摄像头和影像处理器的所有功能。通过SCCB 总线控制,可以输出整帧、子采样、缩放和取窗口等方式的各种分辨率8/10位影像数据。该产品UXGA图像最高达到15帧/秒(SVGA可达30帧,CIF可达60帧)。用户可以完全控制图像质量、数据格式和传输方式。所有图像处理功能过程包括伽玛曲线、白平衡、对比度、色度等都可以通过SCCB接口编程。OmmiVision 图像传感器应用独有的传感器技术,通过减少或消除光学或电子缺陷如固定图案噪声、拖尾、浮散等,提高图像质量,得到清晰的稳定的彩色图像。

OV2640的特点有:
l 高灵敏度、低电压适合嵌入式应用
l 标准的SCCB接口,兼容IIC接口
l 支持RawRGB、RGB(RGB565/RGB555)、GRB422、YUV(422/420)和YCbCr(422)输出格式
l 支持UXGA、SXGA、SVGA以及按比例缩小到从SXGA到40*30的任何尺寸
l 支持自动曝光控制、自动增益控制、自动白平衡、自动消除灯光条纹、自动黑电平校准等自动控制功能。同时支持色饱和度、色相、伽马、锐度等设置。
l 支持闪光灯
l 支持图像缩放、平移和窗口设置
l 支持图像压缩,即可输出JPEG图像数据
l 自带嵌入式微处理器

OV2640的功能框图如图47.1.1.1所示:                                
image001.png
图47.1.1.1 OV2640功能框图

OV2640传感器包括如下一些功能模块。

1.感光整列(Image Array
OV2640总共有1632*1232个像素,最大输出尺寸为UXGA(1600*1200),即200W像素。

2.模拟信号处理(Analog Processing
模拟信号处理所有模拟功能,并包括:模拟放大(AMP)、增益控制、通道平衡和平衡控制等。

3.10位A/D 转换(A/D
原始的信号经过模拟放大后,分G和BR两路进入一个10 位的A/D 转换器,A/D 转换器工作频率高达20M,与像素频率完全同步(转换的频率和帧率有关)。除A/D转换器外,该模块还有黑电平校正(BLC)功能。

4.数字信号处理器(DSP
这个部分控制由原始信号插值到RGB信号的过程,并控制一些图像质量:
l  边缘锐化(二维高通滤波器)
l  颜色空间转换(原始信号到RGB或者YUV/YCbYCr)
l  RGB色彩矩阵以消除串扰
l  色相和饱和度的控制
l  黑/白点补偿
l  降噪
l  镜头补偿
l  可编程的伽玛
l  十位到八位数据转换

5.输出格式模块(Output Formatter
该模块按设定优先级控制图像的所有输出数据及其格式。

6.压缩引擎(Compression Engine
压缩引擎框图如图47.1.1.2所示:
image003.png
图47.1.1.2 压缩引擎框图

从图可以看出,压缩引擎主要包括三部分:DCT、QZ和entropy encoder(熵编码器),将原始的数据流,压缩成jpeg数据输出。

7.微处理器(Microcontroller
OV2640自带了一个8位微处理器,该处理器有512字节SRAM,4KB的ROM,它提供一个灵活的主机到控制系统的指令接口,同时也具有细调图像质量的功能。

8.SCCB接口(SCCB Interface
SCCB接口控制图像传感器芯片的运行,详细使用方法参照光盘的《OmniVision Technologies Seril Camera Control Bus(SCCB) Specification》这个文档

9.数字视频接口(Digital Video Port
OV2640拥有一个10位数字视频接口(支持8位接法),其MSB和LSB可以程序设置先后顺序,ALIENTEK OV2640模块采用默认的8位连接方式,如图47.1.1.3所示:
image005.png
图47.1.1.3 OV2640默认8位连接方式

OV2640的寄存器通过SCCB时序访问并设置,SCCB时序和IIC时序十分类似,在本章我们不做介绍,请大家参考光盘《OmniVision Technologies Seril Camera Control Bus(SCCB) Specification》这个文档。

接下来,我们介绍一下OV2640的传感器窗口设置、图像尺寸设置、图像窗口设置和图像输出大小设置,这几个设置与我们的正常使用密切相关,有必要了解一下。其中,除了传感器窗口设置是直接针对传感器阵列的设置,其他都是DSP部分的设置了,接下来我们一个个介绍。

传感器窗口设置,该功能允许用户设置整个传感器区域(1632*1220)的感兴趣部分,也就是在传感器里面开窗,开窗范围从2*2~1632*1220都可以设置,不过要求这个窗口必须大于等于随后设置的图像尺寸。传感器窗口设置,通过:0X03/0X19/0X1A/0X07/0X17/0X18等寄存器设置,寄存器定义请看OV2640_DS(1.6).pdf这个文档(下同)。

图像尺寸设置,也就是DSP输出(最终输出到LCD的)图像的最大尺寸,该尺寸要小于等于前面我们传感器窗口设置所设定的窗口尺寸。图像尺寸通过:0XC0/0XC1/0X8C等寄存器设置。

图像窗口设置,这里起始和前面的传感器窗口设置类似,只是这个窗口是在我们前面设置的图像尺寸里面,再一次设置窗口大小,该窗口必须小于等于前面设置的图像尺寸。该窗口设置后的图像范围,将用于输出到外部。图像窗口设置通过:0X51/0X52/0X53/0X54/0X55/0X57等寄存器设置。

图像输出大小设置,这是最终输出到外部的图像尺寸。该设置将图像窗口设置所决定的窗口大小,通过内部DSP处理,缩放成我们输出到外部的图像大小。该设置将会对图像进行缩放处理,如果设置的图像输出大小不等于图像窗口设置图像大小,那么图像就会被缩放处理,只有这两者设置一样大的时候,输出比例才是1 : 1的。

因为OmniVision 公司公开的文档,对这些设置实在是没有详细介绍。只能从他们提供的初始化代码(还得去linux源码里面移植过来)里面去分析规律,所以,这几个设置,都是作者根据OV2640的调试经验,以及相关文档总结出来的,不保证百分比正确,如有错误,还请大家指正。

以上几个设置,光看文字可能不太清楚,这里我们画一个简图有助于大家理解,如图47.1.1.4所示:     
image007.png
图47.1.1.4 OV2640图像窗口设置简图

上图,最终红色框所示的图像输出大小,才是OV2640输出给外部的图像尺寸,也就是显示在LCD上面的图像大小。当图像输出大小与图像窗口不等时,会进行缩放处理,在LCD上面看到的图像将会变形。

最后,我们介绍一下OV2640的图像数据输出格式。首先我们简单介绍一些定义:
UXGA,即分辨率位1600*1200的输出格式,类似的还有:SXGA(1280*1024)、WXGA+(1440*900)、XVGA(1280*960)、WXGA(1280*800)、XGA(1024*768)、SVGA(800*600)、VGA(640*480)、CIF(352*288)、WQVGA(400*240)、QCIF(176*144)和QQVGA(160*120)等。

PCLK,即像素时钟,一个PCLK时钟,输出一个像素(或半个像素)。

VSYNC,即帧同步信号。

HREF /HSYNC,即行同步信号。

OV2640的图像数据输出(通过Y[9:0])就是在PCLK,VSYNC和HREF/ HSYNC的控制下进行的。首先看看行输出时序,如图47.1.1.5所示:   
image009.png
图47.1.1.5 OV2640行输出时序

从上图可以看出,图像数据在HREF为高的时候输出,当HREF变高后,每一个PCLK时钟,输出一个8位/10位数据。我们采用8位接口,所以每个PCLK输出1个字节,且在RGB/YUV输出格式下,每个tp=2个Tpclk,如果是Raw格式,则一个tp=1个Tpclk。比如我们采用UXGA时序,RGB565格式输出,每2个字节组成一个像素的颜色(高低字节顺序可通过0XDA寄存器设置),这样每行输出总共有1600*2个PCLK周期,输出1600*2个字节。

再来看看帧时序(UXGA模式),如图47.1.1.6所示:     
image011.png
图47.1.1.6 OV2640帧时序

上图清楚的表示了OV2640在UXGA模式下的数据输出。我们按照这个时序去读取OV2640的数据,就可以得到图像数据。

最后说一下OV2640的图像数据格式,我们一般用2种输出方式:RGB565和JPEG。当输出RGB565格式数据的时候,时序完全就是上面两幅图介绍的关系。以满足不同需要。而当输出数据是JPEG数据的时候,同样也是这种方式输出(所以数据读取方法一模一样),不过PCLK数目大大减少了,且不连续,输出的数据是压缩后的JPEG数据,输出的JPEG数据以:0XFF,0XD8开头,以0XFF,0XD9结尾,且在0XFF,0XD8之前,或者0XFF,0XD9之后,会有不定数量的其他数据存在(一般是0),这些数据我们直接忽略即可,将得到的0XFF,0XD8~0XFF,0XD9之间的数据,保存为.jpg/.jpeg文件,就可以直接在电脑上打开看到图像了。

OV2640自带的JPEG输出功能,大大减少了图像的数据量,使得其在网络摄像头、无线视频传输等方面具有很大的优势。OV2640我们就介绍到这,关于OV2640更详细的介绍,请参考:A盘à7,硬件资料à4,OV2640资料à OV2640_DS(1.6).pdf。

ALIENTEKOV2640摄像头模块
本实验,我们将使用探索者STM32F407开发板的DCMI接口连接ALIENTEK OV2640摄像头模块,该模块采用8位数据输出接口,自带24M有源晶振,无需外部提供时钟,百万高清镜头,且支持闪光灯,整个模块只需提供3.3V 供电即可正常使用。

ALIENTEK OV2640摄像头模块外观如图47.1.1.6所示:     
image013.jpg
图47.1.1.6 ALIENTEK OV2640摄像头模块外观图

模块原理图如图47.1.1.7所示:     
image015.png
图47.1.1.7 ALIENTEK OV2640摄像头模块原理图

从上图可以看出,ATK-OV2640摄像头模块自带了有源晶振,用于产生24M时钟作为OV2640的XVCLK输入,模块的闪光灯(LED1&LED2)可由OV2640的STROBE脚控制(可编程控制)或外部引脚控制,只需焊接R2或R3的电阻进行切换控制。同时自带了稳压芯片,用于提供OV2640稳定的2.8V和1.3V两个电压。模块通过一个2*9的双排排针(P1)与外部通信,与外部的通信信号如表43.1.1.2所示:
QQ截图20230826155959.png
表43.1.1.2 OV2640模块信号及其作用描述

注1:当OV2640硬件R2电阻焊接时,OV_FLASH引脚控制才有效。默认R2电阻没有焊接,R3电阻焊接,闪光灯默认通过STROBE脚编程控制。

47.1.2 STM32F407 DCMI接口简介
STM32F407自带了一个数字摄像头(DCMI)接口,该接口是一个同步并行接口,能够接收外部8位、10位、12位或 14位 CMOS 摄像头模块发出的高速数据流。可支持不同的数据格式:YCbCr4:2:2/RGB565逐行视频和压缩数据 (JPEG)。

STM32F407 DCM接口特点:
l 8 位、10 位、12 位或 14 位并行接口
l 内嵌码/外部行同步和帧同步
l 连续模式或快照模式
l 裁剪功能
l 支持以下数据格式:
1,8/10/12/14 位逐行视频:单色或原始拜尔(Bayer)格式
2,YCbCr 4:2:2逐行视频
3,RGB 565 逐行视频
4,压缩数据:JPEG

DCMI接口包括如下一些信号:
1,  数据输入(D[0:13]),用于接摄像头的数据输出,接OV2640我们只用了8位数据。
2,  水平同步(行同步)输入(HSYNC),用于接摄像头的HSYNC/HREF信号。
3,  垂直同步(场同步)输入(VSYNC),用于接摄像头的VSYNC信号。
4,  像素时钟输入(PIXCLK),用于接摄像头的PCLK信号。

DCMI接口是一个同步并行接口,可接收高速(可达54 MB/s)数据流。该接口包含多达14条数据线(D13-D0)和一条像素时钟线(PIXCLK)。像素时钟的极性可以编程,因此可以在像素时钟的上升沿或下降沿捕获数据。

DCMI接收到的摄像头数据被放到一个32位数据寄存器(DCMI_DR)中,然后通过通用 DMA进行传输。图像缓冲区由 DMA 管理,而不是由摄像头接口管理。

从摄像头接收的数据可以按行/帧来组织(原始YUV/RGB/拜尔模式),也可以是一系列 JPEG图像。要使能 JPEG 图像接收,必须将 JPEG 位(DCMI_CR 寄存器的位 3)置 1。

数据流可由可选的 HSYNC(水平同步)信号和 VSYNC(垂直同步)信号硬件同步,或者通过数据流中嵌入的同步码同步。

47.1.2.1 DCMI接口功能概述
STM32F407 DCMI接口的框图如图47.1.2.1.1所示:
image017.png
图47.1.2.1.1 DCMI接口框图

DCMI接口的数据与PIXCLK(即PCLK)保持同步,并根据像素时钟的极性在像素时钟上升沿/下降沿发生变化。HSYNC(HREF)信号指示行的开始/结束,VSYNC信号指示帧的开始/结束。DCMI信号波形如图47.1.2.1.2所示:   
image019.png
图47.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位字中的排布如表47.1.2.1.4所示:
image021.png
表47.1.2.1.4 8位捕获数据在32位字中的排布

从表47.1.2.1.4可以看出,STM32F407的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接口的其他特性,我们这里就不再介绍了,请大家参考《STM32F4xx参考手册_V4(中文版).pdf》第13章相关内容。

47.1.2.2 DCMI寄存器
本实验,我们将OV2640默认配置为UXGA输出,也就是1600*1200的分辨率,输出信号设置为:VSYNC高电平有效,HREF高电平有效,输出数据在PCLK的下降沿输出(即上升沿的时候,MCU才可以采集)。这样,STM32F407的DCMI接口就必须设置为:VSYNC低电平有效、HSYNC低电平有效和PIXCLK上升沿有效,这些设置都是通过DCMI_CR寄存器控制。

l  DCMI控制寄存器(DCMI_CR
DCMI控制寄存器描述如图47.1.2.2.1所示:   
image023.png
图47.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的其他寄存器这里也不再介绍,请大家参考《STM32F4xx参考手册_V4(中文版).pdf》第13.8小节。关于DMA的寄存器,这里就不再赘述,请回顾前面的实验。

47.2 硬件设计
1. 例程功能
1、本实验开机后,初始化摄像头模块(OV2640),如果初始化成功,则提示选择模式:RGB565模式或者JPEG模式。KEY0用于选择RGB565模式,KEY1用于选择JPEG模式。
2、当使用RGB565时,输出图像(固定为:UXGA)将经过缩放处理(完全由OV5640的DSP控制),显示在LCD上面(默认开启连续自动对焦)。我们可以通过KEY_UP按键选择:1:1显示,即不缩放,图片不变形,但是显示区域小(液晶分辨率大小),或者缩放显示,即将1280*800的图像压缩到液晶分辨率尺寸显示,图片变形,但是显示了整个图片内容。通过按键KEY_UP设置输出图片的尺寸、按键KEY0设置对比度、按键KEY1设置饱和度,按键KEY2设置特效。
3、当使用JPEG模式时,图像可以设置默认是QVGA 320*240尺寸,采集到的JPEG数据将先存放到STM32的RAM内存里面,每当采集到一帧数据,就会关闭DMA传输,然后将采集到的数据发送到串口2(此时可以通过上位机软件(ATK-CAM.exe)接收,并显示图片),之后再重新启动DMA传输。通过按键KEY_UP设置输出图片的尺寸、按键KEY0设置对比度、按键KEY1设置饱和度,按键KEY2设置特效。
4、同时可以通过串口1,借助USMART设置/读取OV2640的寄存器,方便大家调试。
5、LED0闪烁,提示程序运行。LED1用于指示帧中断。。

2. 硬件资源
1)RGB灯
    LED0 – PF9
2)串口1(PA9/PA10连接在板载USB转串口芯片CH340上面)
3)正点原子2.8/3.5/4.3/7寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
4)独立按键
    KEY0 – PE4
    KEY1 – PE3
    KEY2 – PE2
    KEY_UP – PA0
5)串口2 (PA2/PA3,通过USB转232线连接在板载232上)
6)DCMI接口(用于驱动OV5640摄像头模块)
7)定时器6(用于打印摄像头帧率等信息)
8)ALIENTEK OV2640摄像头模块,连接关系为:
     OV2640模块-----------   STM32开发板
     OV_D0~D7  ------------  PC6/PC7/PC8/PC9/PC11/PB6/PE5/PE6
     OV_SCL    ------------   PD6
     OV_SDA    ------------   PD7
     OV_VSYNC  ------------ PB7
     OV_HREF   ------------   PA4
     OV_PCLK   ------------   PA6
     OV_PWDN   ------------ PG9
     OV_RESET  ------------  PG15
     OV_XCLK   ------------   PA8

3. 原理图
开发板板载的摄像头模块接口与MCU的连接关系,如下图所示:     
image025.png
图47.2.1 OV2640摄像头与STM32F407连接示意图

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

47.3 程序设计
47.3.1 DCMI的HAL库驱动
DCMI在HAL库中的驱动代码在stm32f4xx_hal_dcmi.c文件(及其头文件)中。

1.HAL_DCMI_Init函数
DCMI初始化函数,其声明如下:
  1. HAL_StatusTypeDefHAL_DCMI_Init(DCMI_HandleTypeDef *hdcmi);
复制代码
l  函数描述:
用于初始化DCMI。

l  函数形参:
形参1是DCMI_HandleTypeDef结构体类型指针变量,其定义如下:
  1. typedef struct
  2. {
  3. DCMI_TypeDef                    *Instance;            /* DCMI 外设寄存器基地址 */
  4. DCMI_InitTypeDef                Init;                  /* DCMI 初始化结构体 */
  5. HAL_LockTypeDef                 Lock;                  /* 锁对象 */
  6.   __IOHAL_DCMI_StateTypeDef    State;                 /* DCMI 工作状态 */
  7.   __IO uint32_t                   XferCount;            /* DMA 传输计数器 */
  8.   __IO uint32_t                   XferSize;             /* DMA 传输数据的大小 */
  9.   uint32_t                         XferTransferNumber; /* DMA 数据的个数 */
  10.   uint32_t                         pBuffPtr;             /* DMA输出缓冲区地址 */
  11. DMA_HandleTypeDef               *DMA_Handle;         /* DMA配置结构体指针 */
  12.   __IO uint32_t                   ErrorCode;           /* DCMI 错误代码 */
  13. }DCMI_HandleTypeDef;
复制代码
下面重点介绍DCMI_InitTypeDef结构体,其定义如下:
  1. typedef struct {
  2. uint32_t SynchroMode;                /* 同步方式选择硬件同步模式还是内嵌码模式 */
  3. uint32_t PCKPolarity;                /* 设置像素时钟的有效边沿 */
  4. uint32_t VSPolarity;                 /* 设置垂直同步的有效电平 */
  5. uint32_t HSPolarity;                 /* 设置水平同步的有效边沿 */
  6. uint32_t CaptureRate;                /* 设置图像的帧捕获率 */
  7. uint32_t ExtendedDataMode;          /* 设置数据线的宽度(扩展数据模式) */
  8. DCMI_CodesInitTypeDef SyncroCode; /* 分隔符设置 */
  9. uint32_t JPEGMode;                   /* JPEG 模式选择 */
  10. }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格式使能或禁止。

l  函数返回值:
HAL_StatusTypeDef枚举类型的值。

DMA方式传输DCMI数据配置步骤
1)配置OV2640控制引脚,并配置OV2640工作模式。
在启动DCMI之前,我们先设置好OV2640。OV2640通过OV_SCL和OV_SDA进行寄存器配置,同时还有OV_PWDN/OV_RESET等信号,我们也需要配置对应IO状态,先设置OV_PWDN为0,退出掉电模式,然后拉低OV_RESET复位OV2640,之后再设置OV_RESET为1,结束复位,然后就是对OV2640的寄存器进行配置了。最后,可以根据我们的需要,设置成RGB565输出模式,还是JPEG输出模式。

2)配置相关引脚的模式和复用功能(AF13),使能时钟。
OV2640配置好之后,再设置DCMI接口与摄像头模块连接的IO口,使能IO和DCMI时钟,然后设置相关IO口为复用功能模式,复用功能选择AF13(DCMI复用)。
DCMI时钟使能方法:
  1. __HAL_RCC_DCMI_CLK_ENABLE();        /* 使能DCMI时钟 */
复制代码
引脚模式配置就是通过HAL_GPIO_Init函数来配置。

3)配置DCMI相关设置,初始化DCMI接口。
这一步,主要通过DCMI_CR寄存器设置,包括VSPOL/HSPOL/PCKPOL/数据宽度等重要参数,都在这一步设置。HAL库提供了DCMI初始化函数HAL_DCMI_Init,函数声明如下:
  1. 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初始化回调函数:
  1. voidHAL_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)设置OV2640的图像输出大小,使能DCMI捕获。
图像输出大小设置,分两种情况:在RGB565模式下,我们根据LCD的尺寸,设置输出图像大小,以实现全屏显示(图像可能因缩放而变形);在JPEG模式下,我们可以自由设置输出图像大小(可不缩放);最后,开启DCMI捕获,即可正常工作了。

47.3.2 程序流程图
QQ截图20230826160140.png
图47.3.2.1 摄像头实验程序流程图

47.3.3 程序解析
1. DCMI驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。DCMI驱动源码包括两个文件:dcmi.c和dcmi.h。

dcmi.h头文件只是一些声明,下面直接开始介绍dcmi.c文件,首先是DCMI初始化函数,其定义如下:
  1. /**
  2. *@brief        DCMI 初始化
  3. *  @note       IO对应关系如下:
  4. *               摄像头模块 ------------ STM32开发板
  5. *               OV_D0~D7  ------------ PC6/PC7/PC8/PC9/PC11/PB6/PE5/PE6
  6. *               OV_SCL    ------------  PD6
  7. *               OV_SDA    ------------  PD7
  8. *               OV_VSYNC  ------------ PB7
  9. *               OV_HREF   ------------ PA4
  10. *               OV_PCLK   ------------ PA6
  11. *               OV_PWDN   ------------ PG9
  12. *               OV_RESET  ------------ PG15
  13. *               OV_XCLK   ------------ PA8
  14. *               本函数仅初始化OV_D0~D7/OV_VSYNC/OV_HREF/OV_PCLK等信号(11个).
  15. * @param        无
  16. *@retval       无
  17. */
  18. void dcmi_init(void)
  19. {
  20. g_dcmi_handle.Instance = DCMI;
  21. g_dcmi_handle.Init.SynchroMode =DCMI_SYNCHRO_HARDWARE;/*硬件同步HSYNC,VSYNC*/
  22.    g_dcmi_handle.Init.PCKPolarity =DCMI_PCKPOLARITY_RISING;/* PCLK 上升沿有效 */
  23.    g_dcmi_handle.Init.VSPolarity =DCMI_VSPOLARITY_LOW;     /* VSYNC 低电平有效 */
  24.    g_dcmi_handle.Init.HSPolarity =DCMI_HSPOLARITY_LOW;     /* HSYNC 低电平有效 */
  25.    g_dcmi_handle.Init.CaptureRate = DCMI_CR_ALL_FRAME;      /* 全帧捕获 */
  26.    g_dcmi_handle.Init.ExtendedDataMode=DCMI_EXTEND_DATA_8B;/* 8位数据格式 */
  27.    HAL_DCMI_Init(&g_dcmi_handle);    /* 初始化DCMI,此函数会开启帧中断 */

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

  31.     /* 关闭所有中断,函数HAL_DCMI_Init()会默认打开很多中断,开启这些中断
  32.       以后我们就需要对这些中断做相应的处理,否则的话就会导致各种各样的问题,
  33.       但是这些中断很多都不需要,所以这里将其全部关闭掉,也就是将IER寄存器清零。
  34.       关闭完所有中断以后再根据自己的实际需求来使能相应的中断 */
  35.    DCMI->IER=0x0;
  36.    __HAL_DCMI_ENABLE_IT(&g_dcmi_handle,DCMI_IT_FRAME);    /* 使能帧中断 */
  37.    __HAL_DCMI_ENABLE(&g_dcmi_handle);                        /* 使能DCMI */
  38. }
复制代码
该函数主要对DCMI_HandleTypeDef结构体成员赋值并初始化,最后关闭所有中断,只开启帧中断,使能DCMI。而DCMI接口的GPIO口的初始化是在HAL_DCMI_MspInit回调函数中完成,其定义如下:
  1. /**
  2. *@brief        DCMI底层驱动,引脚配置,时钟使能,中断配置
  3. *@param        hdcmi:DCMI句柄
  4. *@note         此函数会被HAL_DCMI_Init()调用
  5. *@retval       无
  6. */
  7. voidHAL_DCMI_MspInit(DCMI_HandleTypeDef* hdcmi)
  8. {
  9.    GPIO_InitTypeDef gpio_init_struct;
  10.    __HAL_RCC_DCMI_CLK_ENABLE();        /* 使能DCMI时钟 */
  11.    __HAL_RCC_GPIOA_CLK_ENABLE();       /* 使能GPIOA时钟 */
  12.    __HAL_RCC_GPIOB_CLK_ENABLE();       /* 使能GPIOB时钟 */
  13.    __HAL_RCC_GPIOC_CLK_ENABLE();       /* 使能GPIOC时钟 */
  14.    __HAL_RCC_GPIOE_CLK_ENABLE();        /* 使能GPIOE时钟 */
  15.    gpio_init_struct.Pin = GPIO_PIN_4 | GPIO_PIN_6;
  16.    gpio_init_struct.Mode = GPIO_MODE_AF_PP;                 /* 推挽复用 */
  17.    gpio_init_struct.Pull = GPIO_PULLUP;                      /* 上拉 */
  18.    gpio_init_struct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;    /* 高速 */
  19.    gpio_init_struct.Alternate = GPIO_AF13_DCMI;             /* 复用为DCMI */
  20.    HAL_GPIO_Init(GPIOA, &gpio_init_struct);                 /* 初始化PA4,6引脚 */

  21.    gpio_init_struct.Pin = GPIO_PIN_6 | GPIO_PIN_7;
  22.    HAL_GPIO_Init(GPIOB, &gpio_init_struct);                /* 初始化PB6,7引脚 */

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

  26.    gpio_init_struct.Pin = GPIO_PIN_5 | GPIO_PIN_6;
  27.    HAL_GPIO_Init(GPIOE, &gpio_init_struct);        /* 初始化PE5,6引脚 */

  28.    HAL_NVIC_SetPriority(DCMI_IRQn, 2, 2);          /* 抢占优先级2,子优先级2 */
  29.    HAL_NVIC_EnableIRQ(DCMI_IRQn);                    /* 使能DCMI中断 */
  30. }
复制代码
DCMI接口的GPIO口前面都介绍过了,该函数最后设置了DCMI中断抢占优先级为2,子优先级为2,并且使能DCMI中断。

接下来介绍DCMI DMA配置初始化函数,其定义如下:
  1. /**
  2. *@brief        DCMI DMA配置
  3. *@param        mem0addr: 存储器地址0     将要存储摄像头数据的内存地址(也可以是外设地址)
  4. *@param        mem1addr: 存储器地址1     当只使用mem0addr的时候,该值必须为0
  5. *@param        memsize : 存储器长度      0~65535
  6. *@param        memblen : 存储器位宽     DMA_MDATAALIGN_BYTE,8位;
  7.            DMA_MDATAALIGN_HALFWORD,16位;DMA_MDATAALIGN_WORD,32位
  8. *@param        meminc : 存储器增长方式 DMA_MINC_DISABLE,不增长;DMA_MINC_ENABLE,增长
  9. *@retval       无
  10. */
  11. voiddcmi_dma_init(uint32_t mem0addr,uint32_t mem1addr,
  12. uint16_t memsize,uint32_t memblen,uint32_t meminc)
  13. {
  14. __HAL_RCC_DMA1_CLK_ENABLE();                                    /* 使能DMA1时钟 */
  15. /* 将DMA与DCMI联系起来 */
  16. __HAL_LINKDMA(&g_dcmi_handle, DMA_Handle, g_dma_dcmi_handle);
  17. /* 先关闭DMA传输完成中断(否则在使用MCU屏的时候会出现花屏的情况) */
  18.    __HAL_DMA_DISABLE_IT(&g_dma_dcmi_handle, DMA_IT_TC);

  19.    g_dma_dcmi_handle.Instance = DMA1_Stream1;                    /* DMA1数据流1 */
  20.    g_dma_dcmi_handle.Init. Channel = DMA_CHANNEL_1;           /* DCMI的DMA请求 */
  21.    g_dma_dcmi_handle.Init.Direction = DMA_PERIPH_TO_MEMORY;   /* 外设到存储器 */
  22.    g_dma_dcmi_handle.Init.PeriphInc = DMA_PINC_DISABLE;        /* 外设非增量模式 */
  23. g_dma_dcmi_handle.Init.MemInc = meminc;                      /* 存储器增量模式 */
  24. /* 外设数据长度:32位 */
  25.    g_dma_dcmi_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;   
  26.     g_dma_dcmi_handle.Init.MemDataAlignment = memblen;/*存储器数据长度:8/16/32位*/
  27.    g_dma_dcmi_handle.Init.Mode = DMA_CIRCULAR;                   /* 使用循环模式 */
  28.    g_dma_dcmi_handle.Init.Priority = DMA_PRIORITY_HIGH;        /* 高优先级 */
  29. g_dma_dcmi_handle.Init.FIFOMode = DMA_FIFOMODE_ENABLE;     /* 使能FIFO */
  30. /* 使用1/2的FIFO */
  31.    g_dma_dcmi_handle.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_HALFFULL;
  32.    g_dma_dcmi_handle.Init.MemBurst = DMA_MBURST_SINGLE;        /* 存储器突发传输 */
  33.    g_dma_dcmi_handle.Init.PeriphBurst = DMA_PBURST_SINGLE;  /* 外设突发单次传输 */
  34.    HAL_DMA_DeInit(&g_dma_dcmi_handle);                         /* 先清除以前的设置 */
  35.    HAL_DMA_Init(&g_dma_dcmi_handle);                            /* 初始化DMA */
  36.    
  37. /*  在开启DMA之前先使用__HAL_UNLOCK()解锁一次DMA,因为HAL_DMA_Statrt()
  38. HAL_DMAEx_MultiBufferStart()这两个函数一开始要先使用__HAL_LOCK()锁定DMA,
  39. 而函数__HAL_LOCK()会判断当前的DMA状态是否为锁定状态,如果是锁定状态的话就直接
  40. 返回HAL_BUSY,这样会导致函数HAL_DMA_Statrt()和HAL_DMAEx_MultiBufferStart()
  41. 后续的DMA配置程序直接被跳过!DMA也就不能正常工作,为了避免这种现象,所以在启动
  42. DMA之前先调用__HAL_UNLOC()先解锁一次DMA。 */
  43.    
  44.    __HAL_UNLOCK(&g_dma_dcmi_handle);
  45.     if (mem1addr == 0)  /* 开启DMA,不使用双缓冲 */
  46.     {
  47.        HAL_DMA_Start(&g_dma_dcmi_handle, (uint32_t)&DCMI->DR,
  48. mem0addr, memsize);
  49.     }
  50.     else                /* 使用双缓冲 */
  51.     {
  52.        HAL_DMAEx_MultiBufferStart(&g_dma_dcmi_handle, (uint32_t)&DCMI->DR,
  53. mem0addr, mem1addr, memsize);    /* 开启双缓冲 */
  54.        __HAL_DMA_ENABLE_IT(&g_dma_dcmi_handle, DMA_IT_TC);  /* 开启传输完成中断 */
  55.        HAL_NVIC_SetPriority(DMA2_Stream1_IRQn, 2, 3);        /* DMA中断优先级 */
  56.        HAL_NVIC_EnableIRQ(DMA2_Stream1_IRQn);
  57.     }
  58. }
复制代码
该函数用于配置DCMI的DMA传输,其外设地址固定为:DCMI->DR,而存储器地址可变(LCD或者SRAM)。DMA被配置为循环模式,一旦开启,DMA将不停的循环传输数据。

下面介绍的是DCMI启动传输和关闭传输函数,它们的定义分别如下:
  1. /**
  2. *@brief        DCMI,启动传输
  3. *@param        无
  4. *@retval       无
  5. */
  6. void dcmi_start(void)
  7. {  
  8.    lcd_set_cursor(0,0);                       /* 设置坐标到原点 */
  9.    lcd_write_ram_prepare();                  /* 开始写入GRAM */
  10.    __HAL_DMA_ENABLE(&g_dma_dcmi_handle);   /* 使能DMA */
  11.    DCMI->CR |= DCMI_CR_CAPTURE;             /* DCMI捕获使能 */
  12. }

  13. /**
  14. *@brief        DCMI,关闭传输
  15. *@param        无
  16. *@retval       无
  17. */
  18. void dcmi_stop(void)
  19. {
  20.    DCMI->CR &= ~(DCMI_CR_CAPTURE);          /* DCMI捕获关闭 */
  21.     while (DCMI->CR & 0X01);                  /* 等待传输结束 */
  22.    __HAL_DMA_DISABLE(&g_dma_dcmi_handle); /* 关闭DMA */
  23. }
复制代码
下面介绍的是DCMI中断服务函数(及其回调函数)、DCMI DMA接收回调函数和DMA2数据流1中断服务函数,它们的定义分别如下:
  1. /**
  2. *@brief        DCMI中断服务函数
  3. *@param        无
  4. *@retval       无
  5. */
  6. voidDCMI_IRQHandler(void)
  7. {
  8.    HAL_DCMI_IRQHandler(&g_dcmi_handle);
  9. }

  10. /**
  11. *@brief        DCMI中断回调服务函数
  12. *@param        hdcmi:DCMI句柄
  13. *@note         捕获到一帧图像处理
  14. *@retval       无
  15. */
  16. voidHAL_DCMI_FrameEventCallback(DCMI_HandleTypeDef *hdcmi)
  17. {
  18.    jpeg_data_process();     /* jpeg数据处理 */
  19.    LED1_TOGGLE();            /* LED1闪烁 */
  20.    g_ov_frame++;

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

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

  25. /**
  26. *@brief        DMA2数据流1中断服务函数(仅双缓冲模式会用到)
  27. *@param        无
  28. *@retval       无
  29. */
  30. voidDMA2_Stream1_IRQHandler(void)
  31. {
  32. /* DMA传输完成 */
  33.     if (__HAL_DMA_GET_FLAG(&g_dma_dcmi_handle, DMA_FLAG_TCIF1_5) != RESET)  
  34. {
  35. /* 清除DMA传输完成中断标志位 */
  36.        __HAL_DMA_CLEAR_FLAG(&g_dma_dcmi_handle, DMA_FLAG_TCIF1_5);
  37.        dcmi_rx_callback();     /* 执行摄像头接收回调函数,读取数据等操作在这里面处理 */
  38.     }
  39. }
复制代码
其中:DCMI_IRQHandler函数,用于处理帧中断,可以实现帧率统计(需要定时器支持)和JPEG数据处理等,实际上当捕获到一帧数据后,调用的是HAL库回调函数HAL_DCMI_FrameEventCallback进行处理。DMA2_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. OV2640驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。OV2640驱动源码包括六个文件:ov2640.c、ov2640.h、ov2640af.h、ov2640cfg.h、sccb.c和sccb.h。

其中sccb.c和sccb.h是SCCB通信接口的驱动代码。OV2640的寄存器通过SCCB时序访问并设置,SCCB时序和IIC时序十分类似,这里我们也是用软件模拟SCCB时序。

下面,首先介绍sccb.h头文件中SCCB的IO口宏定义,其定义情况如下:
  1. /*****************************************************************************/
  2. /* 引脚 定义 */
  3. #define SCCB_SCL_GPIO_PORT               GPIOD
  4. #define SCCB_SCL_GPIO_PIN                GPIO_PIN_6
  5. #define SCCB_SCL_GPIO_CLK_ENABLE()      
  6. do{ __HAL_RCC_GPIOD_CLK_ENABLE(); }while(0)      /* PD口时钟使能 */

  7. #define SCCB_SDA_GPIO_PORT               GPIOD
  8. #define SCCB_SDA_GPIO_PIN                GPIO_PIN_7
  9. #define SCCB_SDA_GPIO_CLK_ENABLE()      
  10. do{ __HAL_RCC_GPIOD_CLK_ENABLE(); }while(0)      /* PD口时钟使能 */

  11. /* 如果不用开漏模式或SCCB上无上拉电阻,我们需要推挽和输入切换的方式 */
  12. #define OV_SCCB_TYPE_NOD     1
  13. #if OV_SCCB_TYPE_NOD
  14. #define SCCB_SDA_IN()  { GPIOD->MODER &= ~(3 << (7 *2)); GPIOD->MODER |= \
  15.                                                    0 << (7 * 2); }    /* PD7 输入 */
  16. #define SCCB_SDA_OUT() { GPIOD->MODER&= ~(3 << (7 * 2)); GPIOD->MODER |= \
  17.                                                    1 << (7 * 2); }    /* PD7 输出 */
  18. #endif

  19. /*****************************************************************************/
  20. /* IO操作函数 */
  21. #define SCCB_SCL(x)    do{ x ? \
  22.      HAL_GPIO_WritePin(SCCB_SCL_GPIO_PORT,SCCB_SCL_GPIO_PIN, GPIO_PIN_SET) : \
  23.     HAL_GPIO_WritePin(SCCB_SCL_GPIO_PORT, SCCB_SCL_GPIO_PIN,GPIO_PIN_RESET); \
  24.                           }while(0)       /* SCL */

  25. #define SCCB_SDA(x)   do{ x ? \
  26.      HAL_GPIO_WritePin(SCCB_SDA_GPIO_PORT,SCCB_SDA_GPIO_PIN, GPIO_PIN_SET) : \
  27.     HAL_GPIO_WritePin(SCCB_SDA_GPIO_PORT, SCCB_SDA_GPIO_PIN,GPIO_PIN_RESET); \
  28.                           }while(0)       /* SDA */

  29. #define SCCB_READ_SDA     HAL_GPIO_ReadPin(SCCB_SDA_GPIO_PORT, \
  30. SCCB_SDA_GPIO_PIN) /* 读取SDA */
复制代码
SCCB时序有两根信号线(SCCB_SCL和SCCB_SDA),所以这里定义了两个IO口(PD6和PD7)来控制。IO操作函数有三个,包括SCCB_SCL用于控制时钟,SCCB_SDA是写IO口输出的值为逻辑1或者逻辑0,SCCB_READ_SDA是读取IO口的值,逻辑1或者逻辑0。

接下来介绍sccb.c文件的代码,首先是SCCB接口初始化函数,该函数主要就是初始化PD6和PD7两个IO口,其定义如下:
  1. /**
  2. *@brief        初始化SCCB
  3. *@param        无
  4. *@retval       无
  5. */
  6. void sccb_init(void)
  7. {
  8.    GPIO_InitTypeDef gpio_init_struct;

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

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

  16. /* SDA引脚模式设置,开漏输出,上拉, 这样就不用再设置IO方向了,
  17. 开漏输出的时候(=1), 也可以读取外部信号的高低电平 */
  18.    gpio_init_struct.Pin = SCCB_SDA_GPIO_PIN;
  19.     gpio_initure.Mode = GPIO_MODE_OUTPUT_OD;
  20.    HAL_GPIO_Init(SCCB_SDA_GPIO_PORT, &gpio_init_struct);  /* 初始化SDA引脚 */

  21.    sccb_stop();     /* 停止总线上所有设备 */
  22. }
复制代码
PD6和PD7都设置为带上拉的开漏输出,这样设置的好处是:SDA引脚不用再设置IO口方向了,因为开漏输出模式下,STM32的IO口可以读取外部信号的高低电平,这个在前面介绍GPIO外设的时候已经讲解过。初始化IO口后,调用sccb_stop函数停止总线上的所有设备,防止误操作。

sccb.c文件的其他函数,这里就不再介绍了,主要都是IO口模拟SCCB时序的相关函数,详细请看源码。

ov2640.c、ov2640.h和ov2640cfg.h这三个个文件主要就是用于OV2640摄像头的初始化,当然也要使用到SCCB的驱动代码。

ov2640cfg.h文件存放的是OV2640初始化数组,总共有五个数组。我们大概了解下数组结构,每个数组条目的第一个字节为寄存器号(也就是寄存器地址),第二个字节为要设置的值,比如{0x04, 0xA8},就表示在0x04地址,写入0xA8这个值。

五个数组中,ov2640_uxga_init_reg_tbl和ov2640_svga_init_reg_tb1数组,分别用于配置OV2640输出UXGA和SVGA分辨率的图像,我们只用了ov2640_uxga_init_reg_tb1,完成对OV2640的初始化(设置为UXGA)。最后OV2640要输出数据是RGB565还是JPEG,就得通过其他数组设置,输出RGB565时,通过数组:ov2640_rgb565_reg_tbl设置即可;输出JPEG时,则要通过ov2640_yuv422_reg_tbl和ov2640_jpeg_reg_tbl两个数组设置。

下面开始介绍ov2640.h文件,主要是OV2640的PWDN和RESET引脚定义和控制函数,以及OV2640的ID、访问地址,其定义如下:
  1. /*****************************************************************************/
  2. /* PWDN 引脚定义 */
  3. #define OV_PWDN_GPIO_PORT               GPIOG
  4. #define OV_PWDN_GPIO_PIN                GPIO_PIN_9
  5. /* PG口时钟使能 */
  6. #define OV_PWDN_GPIO_CLK_ENABLE()     do{__HAL_RCC_GPIOG_CLK_ENABLE(); }while(0)

  7. /* RESET 引脚定义 */
  8. #define OV_RESET_GPIO_PORT              GPIOG
  9. #define OV_RESET_GPIO_PIN               GPIO_PIN_15、
  10. /* PG口时钟使能 */
  11. #define OV_RESET_GPIO_CLK_ENABLE()    do{__HAL_RCC_GPIOG_CLK_ENABLE(); }while(0)

  12. /* FLASH 引脚定义 */
  13. #define OV_FLASH_GPIO_PORT              GPIOA
  14. #define OV_FLASH_GPIO_PIN               GPIO_PIN_8
  15. /* PA口时钟使能 */
  16. #define OV_FLASH_GPIO_CLK_ENABLE()    do{__HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)

  17. /*****************************************************************************/
  18. /* IO控制函数 */
  19. #define OV2640_PWDN(x)    do{ x ? \
  20.         HAL_GPIO_WritePin(OV_PWDN_GPIO_PORT,OV_PWDN_GPIO_PIN, PIO_PIN_SET) : \
  21.         HAL_GPIO_WritePin(OV_PWDN_GPIO_PORT,OV_PWDN_GPIO_PIN, GPIO_PIN_RESET); \
  22.                               }while(0)          /* POWER DOWN控制信号 */

  23. #define OV2640_RST(x)     do{ x ? \
  24.     HAL_GPIO_WritePin(OV_RESET_GPIO_PORT,OV_RESET_GPIO_PIN, GPIO_PIN_SET) : \
  25.     HAL_GPIO_WritePin(OV_RESET_GPIO_PORT,  OV_RESET_GPIO_PIN, GPIO_PIN_RESET); \
  26.                               }while(0)          /* 复位控制信号 */

  27. #define OV2640_FLASH(x)     do{ x ? \
  28.      HAL_GPIO_WritePin(OV_FLASH_GPIO_PORT,OV_FLASH_GPIO_PIN, GPIO_PIN_SET) : \
  29.     HAL_GPIO_WritePin(OV_FLASH_GPIO_PORT, OV_FLASH_GPIO_PIN,GPIO_PIN_RESET); \
  30.                                 }while(0)        /* 闪光灯控制信号 */

  31. /* 图像垂直翻转使能 1;使能 0:失能(使用ATK-OV2640模组版本必须使能) */
  32. #define Image_FlipVer       1

  33. /* OV2640的ID和访问地址 */
  34. #define OV2640_MID          0x7FA2
  35. #define OV2640_PID          0x2642
  36. #define OV2640_ADDR         0x60              /* OV2640的IIC地址 */
复制代码
OV2640其他相关寄存器定义较多,这里不过多介绍,请参考例程源码。下面开始介绍ov2640.c文件,首先是OV2640初始化函数,其定义如下:
  1. /**
  2. *@brief        OV2640 初始化
  3. *@param        无
  4. *@retval       0, 成功; 1, 失败;
  5. */
  6. uint8_t ov2640_init(void)
  7. {
  8.     uint16_t i = 0;
  9.     uint16_t reg;

  10.    GPIO_InitTypeDef gpio_init_struct;

  11.    OV_PWDN_GPIO_CLK_ENABLE();      /* 使能OV_PWDN脚时钟 */
  12.    OV_RESET_GPIO_CLK_ENABLE();     /* 使能OV_RESET脚时钟 */

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

  18.    gpio_init_struct.Pin = OV_RESET_GPIO_PIN;
  19.    HAL_GPIO_Init(OV_RESET_GPIO_PORT, &gpio_init_struct);/* 初始化OV_RESET引脚 */

  20.    OV2640_PWDN(0);     /* POWER ON */
  21.    delay_ms(10);
  22.    OV2640_RST(0);       /* 必须先拉低OV2640的RST脚,再上电 */
  23.    delay_ms(20);
  24.    OV2640_RST(1);       /* 结束复位 */
  25.    delay_ms(20);
  26.    
  27.    sccb_init();         /* 初始化SCCB 的IO口 */
  28.    delay_ms(5);
  29.    ov2640_write_reg(OV2640_DSP_RA_DLMT, 0x01);     /* 操作sensor寄存器 */
  30.    ov2640_write_reg(OV2640_SENSOR_COM7, 0x80);     /* 软复位OV2640 */
  31.    delay_ms(50);
  32.     reg=ov2640_read_reg(OV2640_SENSOR_MIDH);      /* 读取厂家ID 高八位 */
  33.     reg<<= 8;
  34.     reg|=ov2640_read_reg(OV2640_SENSOR_MIDL);     /* 读取厂家ID 低八位 */

  35.     if (reg != OV2640_MID)       /* ID 是否正常 */
  36.     {
  37.        printf("MID:%d\r\n", reg);
  38.        return 1;    /* 失败 */
  39.     }
  40.    
  41.     reg=ov2640_read_reg(OV2640_SENSOR_PIDH);      /* 读取厂家ID 高八位 */
  42.     reg<<= 8;
  43.     reg|=ov2640_read_reg(OV2640_SENSOR_PIDL);    /* 读取厂家ID 低八位 */

  44.     if (reg != OV2640_PID)      /* ID是否正常 */
  45.     {
  46.        printf("HID:%d\r\n", reg);
  47.        return 1;    /* 失败 */
  48.     }

  49.     /* 初始化 OV2640 */
  50.     for (i = 0; i < sizeof(ov2640_uxga_init_reg_tbl) / 2; i++)
  51.     {
  52.        ov2640_write_reg(ov2640_uxga_init_reg_tbl[0],
  53.                             ov2640_uxga_init_reg_tbl[1]);
  54.     }
  55.     return 0;       /* OV2640初始化完成 */
  56. }
复制代码
该函数除了初始化OV2640相关的IO口,最主要的是完成OV2640的寄存器序列初始化。OV2640的寄存器很多,配置繁琐,通过厂家提供的参考配置序列可以更方便的完成OV2640寄存器初始化,配置序列存放在上面介绍的ov2640_uxga_init_reg_tbl和ov2640_svag_init_reg_tb1这个两个数组里面。

另外,在ov2640.c里面,还有几个函数比较重要,这里不贴代码了,只介绍功能:
ov2640_outsize_set函数,用于设置图像输出大小;
ov2640_window_set函数,用于设置传感器输出窗口;
ov2640_image_win_set函数,用于设置图像开窗大小;
ov2640_imagesize_set函数,用于设置图像尺寸大小;

其中,ov2640_outsize_set和ov2640_image_win_set函数就是前面在47.1.1节所介绍的3个窗口的设置,他们共同决定了图像的输出。

其他的函数就不列出来了,请大家查看源码。

3. TIMER驱动代码
TIMER驱动代码我们主要就是用到定时器6,在定时器6更新中断回调函数中打印帧率,更新中断回调函数定义如下:
  1. /**
  2. *@brief        定时器更新中断回调函数
  3. *@param        htim:定时器句柄指针
  4. *@retval       无
  5. */
  6. voidHAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
  7. {
  8.     if (htim == (&g_timx_handle))
  9.     {
  10.        printf("frame:%d\r\n", g_ov_frame);
  11.        g_ov_frame = 0;
  12.     }
  13. }
复制代码
g_ov_frame变量在dcmi.c中被定义,用于存放帧率。TIMER的其他驱动代码在介绍定时器的时候已经介绍过,请看例程源码。

4. USART2驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。USART2驱动源码包括两个文件:usart2.c和usart2.h。

usart2.h头文件对usart2的IO口做了宏定义,具体如下:
  1. /*****************************************************************************/
  2. /* 串口2 引脚 定义 */
  3. #define USART2_TX_GPIO_PORT                  GPIOA
  4. #define USART2_TX_GPIO_PIN                   GPIO_PIN_2
  5. #define USART2_TX_GPIO_AF                    GPIO_AF7_USART2      /* AF功能选择 */
  6. /* PA口时钟使能 */
  7. #define USART2_TX_GPIO_CLK_ENABLE()  do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)

  8. #define USART2_RX_GPIO_PORT                  GPIOA
  9. #define USART2_RX_GPIO_PIN                   GPIO_PIN_3
  10. #define USART2_RX_GPIO_AF                    GPIO_AF7_USART2      /* AF功能选择 */
  11. /* PA口时钟使能 */
  12. #define USART2_RX_GPIO_CLK_ENABLE()  do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)
  13. /*****************************************************************************/
复制代码
PA2和PA3复用为串口2的发送和接收引脚。

下面介绍usart2.c文件的串口2初始化函数,其定义如下:
  1. /**
  2. *@brief        串口2初始化函数
  3. *@note         注意:串口2的时钟源频率在sys已经设置过了
  4. *@param        baudrate : 波特率, 根据自己需要设置波特率值
  5. */
  6. void usart2_init(uint32_t baudrate)
  7. {
  8.    GPIO_InitTypeDef gpio_init_struct;

  9.    _USART2_UX_CLK_ENABLE();        /* USART2时钟使能 */
  10.    USART2_TX_GPIO_CLK_ENABLE();     /* 串口TX脚时钟使能 */
  11.    USART2_RX_GPIO_CLK_ENABLE();     /* 串口RX脚时钟使能 */

  12.    gpio_init_struct.Pin = USART2_TX_GPIO_PIN;
  13.    gpio_init_struct.Mode = GPIO_MODE_AF_PP;                  /* 复用推挽输出 */
  14.    gpio_init_struct.Pull = GPIO_PULLUP;                      /* 上拉 */
  15.    gpio_init_struct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;     /* 高速 */
  16.    gpio_init_struct.Alternate = USART2_TX_GPIO_AF;        /* 复用为USART2 */
  17.    HAL_GPIO_Init(USART2_TX_GPIO_PORT, &gpio_init_struct); /* 初始化串口TX引脚 */

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

  21.    g_uart2_handle.Instance = USART2;                          /* USART2 */
  22.    g_uart2_handle.Init.BaudRate = baudrate;                  /* 波特率 */
  23.    g_uart2_handle.Init.WordLength = UART_WORDLENGTH_8B;   /* 字长为8位数据格式 */
  24.    g_uart2_handle.Init.StopBits = UART_STOPBITS_1;         /* 一个停止位 */
  25.    g_uart2_handle.Init.Parity = UART_PARITY_NONE;           /* 无奇偶校验位 */
  26.    g_uart2_handle.Init.HwFlowCtl =UART_HWCONTROL_NONE;   /* 无硬件流控 */
  27.    g_uart2_handle.Init.Mode = UART_MODE_TX;                  /* 发模式 */
  28.    HAL_UART_Init(&g_uart2_handle);                             /* 使能USART2 */
  29. }
复制代码
这里需要注意的是,串口2的时钟源频率在sys_stm32_clock_init函数中已经设置了。其他的代码基本usart_init函数是一样的。

5. main.c代码
main.c前面定义了一些变量和数组,具体如下:
  1. uint8_t g_ov_mode = 0;               /* bit0: 0, RGB565模式;  1, JPEG模式 */

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

  4. __ALIGNED(4) uint32_t g_jpeg_data_buf[jpeg_buf_size];   /* JPEG数据缓存buf */

  5. /* JPEG数据 DMA双缓存buf */
  6. __ALIGNED(4) uint32_t g_dcmi_line_buf[2][jpeg_line_size];

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

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

  14. /* JPEG尺寸支持列表 */
  15. const uint16_t jpeg_img_size_tbl[][2] =
  16. {
  17.     160, 120,        /* QQVGA */
  18.     176, 144,        /* QCIF */
  19.     320, 240,        /* QVGA */
  20.     400,240,         /* WGVGA */
  21.     352,288,         /* CIF */
  22.     640, 480,        /* VGA */
  23.     800, 600,        /* SVGA */
  24.     1024, 768,       /* XGA */
  25.     1280, 800,       /* WXGA */
  26.     1280, 960,       /* XVGA */
  27.     1440, 900,       /* WXGA+ */
  28.     1280, 1024,      /* SXGA */
  29.     1600, 1200,      /* UXGA */
  30. };

  31. const char *EFFECTS_TBL[7] = {"Normal", "Negative", "B&W", "Redish",
  32.                                     "Greenish", "Bluish", "Antique"};     /* 7种特效 */
  33. const char *JPEG_SIZE_TBL[13] = {"QQVGA", "QCIF", "QVGA", "WGVGA", "CIF",
  34.                                         "VGA", "SVGA", "XGA", "WXGA", "SVGA", "WXGA+",
  35.                                         "SXGA", "UXGA"};          /* JPEG图片 13种尺寸 */
复制代码
其中,定义的g_jpeg_data_buf数组大小为116KB字节,用来存储JPEG数据。

在main.c里面,总共有5个函数,我们接下来分别介绍。首先是处理JPEG数据函数,其定义如下:
  1. /**
  2. *@brief        处理JPEG数据
  3. *  @ntoe       在DCMI_IRQHandler中断服务函数里面被调用
  4. *                当采集完一帧JPEG数据后,调用此函数,切换JPEG BUF.开始下一帧采集.
  5. *
  6. *@param        无
  7. *@retval       无
  8. */
  9. voidjpeg_data_process(void)
  10. {
  11.     uint16_t i;
  12.     uint16_t rlen;            /* 剩余数据长度 */
  13.     uint32_t *pbuf;

  14.     if (g_ov_mode)            /* 只有在JPEG格式下,才需要做处理. */
  15.     {
  16.        if (g_jpeg_data_ok == 0)    /* jpeg数据还未采集完? */
  17.        {
  18.            __HAL_DMA_DISABLE(&g_dma_dcmi_handle);    /* 关闭DMA */
  19.            while(DMA2_Stream1->CR & 0x01);            /* 等待DMA2_Stream1可配置 */

  20. /* 得到剩余数据长度 */
  21.            rlen =jpeg_line_size -__HAL_DMA_GET_COUNTER(&g_dma_dcmi_handle);
  22.            pbuf =g_jpeg_data_buf + g_jpeg_data_len;/* 偏移到有效数据末尾,继续添加 */
  23.            if (DMA2_Stream1->CR & (1 << 19))
  24.            {
  25.                 for (i = 0; i < rlen; i++)
  26.                 {
  27.                     pbuf = g_dcmi_line_buf[1];   /* 读取buf1里面的剩余数据 */
  28.                 }
  29.            }
  30.            else
  31.            {
  32.                 for (i = 0; i < rlen; i++)
  33.                 {
  34.                     pbuf = g_dcmi_line_buf[0];  /* 读取buf0里面的剩余数据 */
  35.                 }
  36.            }
  37.            g_jpeg_data_len += rlen;     /* 加上剩余长度 */
  38.            g_jpeg_data_ok = 1;           /* 标记JPEG数据采集完成,等待其他函数处理 */
  39.        }
  40.        if (g_jpeg_data_ok == 2)          /* 上一次的jpeg数据已经被处理了 */
  41.        {
  42. /* 传输长度为jpeg_buf_size*4字节 */
  43.            __HAL_DMA_SET_COUNTER(&g_dma_dcmi_handle, jpeg_line_size);
  44.            __HAL_DMA_ENABLE(&g_dma_dcmi_handle);    /* 重新传输 */
  45.            g_jpeg_data_ok = 0;       /* 标记数据未采集 */
  46.            g_jpeg_data_len = 0;      /* 数据重新开始 */
  47.        }
  48. }
  49.     else
  50.     {
  51.        lcd_set_cursor(0, 0);
  52.        lcd_write_ram_prepare();     /* 开始写入GRAM */
  53.     }
  54. }
复制代码
该函数用于处理JPEG数据的接收,在DCMI_IRQHandler函数(在dcmi.c里面)里面被调用,它与jpeg_dcmi_rx_callback函数和jpeg_test函数共同控制JPEG的数据传送。JPEG数据的接收,采用DMA双缓冲机制,缓冲数组为:dcmi_line_buf(u32类型,RGB屏接收RGB565数据时,也是用这个数组);数组大小为:jpeg_line_size,我们定义的是1*1024,即数组大小为4K字节(数组大小不能小于存储摄像头一行输出数据的大小);JPEG数据接收处理流程如图47.3.3.1所示:
image032.png
图47.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数据接收回调函数,其定义如下:
  1. /**
  2. *@brief        JPEG数据接收回调函数
  3. *  @ntoe       在DMA1_Stream1_IRQHandler中断服务函数里面被调用
  4. *
  5. *@param        无
  6. *@retval       无
  7. */
  8. voidjpeg_dcmi_rx_callback(void)
  9. {
  10.     uint16_t i;
  11.     volatile uint32_t *pbuf;
  12.    pbuf =g_jpeg_data_buf + g_jpeg_data_len;   /* 偏移到有效数据末尾 */
  13.     if (DMA2_Stream1->CR & (1 << 19))            /* buf0已满,正常处理buf1 */
  14.     {
  15.        for (i = 0; i < jpeg_line_size; i++)
  16.        {
  17.            pbuf = g_dcmi_line_buf[0];     /* 读取buf0里面的数据 */
  18.        }
  19.       
  20.        g_jpeg_data_len += jpeg_line_size;       /* 偏移 */
  21.     }
  22.     else    /* buf1已满,正常处理buf0 */
  23.     {
  24.        for (i = 0; i < jpeg_line_size; i++)
  25.        {
  26.            pbuf = g_dcmi_line_buf[1];     /* 读取buf1里面的数据 */
  27.        }
  28.       
  29.        g_jpeg_data_len += jpeg_line_size;       /* 偏移 */
  30.     }
  31. }
复制代码
该函数是jpeg数据接收的主要函数,通过判断DMA2_Stream1->CR寄存器,读取不同buf里面的数据,存储到g_jpeg_data_buf里面。该函数由DMA的传输完成中断服务函数:DMA2_Stream1_IRQHandler调用。

接下来介绍的是JPEG测试函数,其定义如下:
  1. /**
  2. *@brief        JPEG测试
  3. *  @ntoe       JPEG数据,通过串口2发送给电脑.
  4. *
  5. *@param        无
  6. *@retval       无
  7. */
  8. void jpeg_test(void)
  9. {
  10.     uint32_t i, jpgstart, jpglen;
  11.     uint8_t *p;
  12.     uint8_t key, headok = 0;
  13.     uint8_t effect = 0, saturation = 2, contrast = 2;
  14.     uint8_t size = 2;    /* 默认是QVGA 320*240尺寸 */
  15.     uint8_t msgbuf[15]; /* 消息缓存区 */
  16.    lcd_clear(WHITE);
  17.    lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
  18.    lcd_show_string(30, 70, 200, 16, 16, "OV2640 JPEGMode", RED);
  19.    lcd_show_string(30, 100, 200, 16, 16, "KEY0:Contrast", RED);  /* 对比度 */
  20.    lcd_show_string(30, 120, 200, 16, 16, "KEY1:Saturation", RED);/* 色彩饱和度 */
  21.    lcd_show_string(30, 140, 200, 16, 16, "KEY2:Effect", RED);     /* 特效 */
  22.    lcd_show_string(30, 160, 200, 16, 16, "KEY_UP:Size", RED);     /* 分辨率设置 */
  23.    sprintf((char *)msgbuf, "JPEGSize:%s",JPEG_SIZE_TBL[size]);
  24.     /*显示当前JPEG分辨率 */
  25.    lcd_show_string(30, 180, 200, 16, 16, (char*)msgbuf, RED);
  26.    ov2640_jpeg_mode(); /* JPEG模式 */
  27.    dcmi_init();         /* DCMI配置 */
  28.    dcmi_rx_callback = jpeg_dcmi_rx_callback;   /* JPEG接收数据回调函数 */
  29.    dcmi_dma_init((uint32_t)&g_dcmi_line_buf[0], (uint32_t)&g_dcmi_line_buf[1],
  30.    jpeg_line_size, DMA_MDATAALIGN_WORD, DMA_MINC_ENABLE);    /* DCMI DMA配置 */
  31.     /* 设置输出尺寸 */
  32.    ov2640_outsize_set(jpeg_img_size_tbl[size][0], jpeg_img_size_tbl[size][1]);
  33.    dcmi_start();        /* 启动传输 */
  34.     while (1)
  35.     {
  36.        if (g_jpeg_data_ok == 1)    /* 已经采集完一帧图像了 */
  37.        {
  38.            p = (uint8_t *)g_jpeg_data_buf;
  39.            /* 打印数据长度 */
  40.            printf("g_jpeg_data_len:%d\r\n", g_jpeg_data_len * 4);
  41.            /* 提示正在传输数据 */
  42.            lcd_show_string(30, 210, 210, 16, 16, "SendingJPEG data...", RED);
  43.            jpglen = 0;    /* 设置jpg文件大小为0 */
  44.            headok = 0;    /* 清除jpg头标记 */
  45.            /* 查找0XFF,0XD8和0XFF,0XD9,获取jpg文件大小 */
  46.            for (i = 0; i < g_jpeg_data_len * 4; i++)
  47.            {
  48.                 if ((p == 0XFF) && (p[i + 1] == 0XD8))    /* 找到FF D8 */
  49.                 {
  50.                     jpgstart = i;
  51.                     headok = 1;    /* 标记找到jpg头(FF D8) */
  52.                 }
  53.                 /* 找到头以后,再找FF D9 */
  54.                 if ((p == 0XFF) && (p[i + 1] == 0XD9) && headok)
  55.                 {
  56.                     jpglen = i - jpgstart + 2;
  57.                     break;
  58.                 }
  59.            }
  60.            if (jpglen)            /* 正常的jpeg数据 */
  61.            {
  62.                 p += jpgstart;    /* 偏移到0XFF,0XD8处 */
  63.                 for (i = 0; i < jpglen; i++)          /* 发送整个jpg文件 */
  64.                 {
  65.                     USART2->DR = p;
  66.                     while ((USART2->SR & 0X40) == 0);  /* 循环发送,直到发送完毕 */
  67.                     key = key_scan(0);
  68.                     if (key)break;
  69.                 }
  70.            }
  71.            if (key != 0)    /* 有按键按下,需要处理 */
  72.            {
  73.                 /* 提示退出数据传输 */
  74.                 lcd_show_string(30, 210, 210, 16, 16, "QuitSending data  ", RED);
  75.                 switch (key)
  76.                 {
  77.                     case KEY0_PRES: /* 对比度设置 */
  78.                         contrast++;
  79.                         if( contrast > 4)contrast = 0;
  80.                         ov2640_contrast(contrast);
  81.                         sprintf((char*)msgbuf, "Constrast:%d", (signed char)
  82.                                   contrast - 2);
  83.                         break;
  84.                     case KEY1_PRES: /* 饱和度设置 */
  85.                         saturation++;
  86.                         if (saturation > 4)saturation = 0;
  87.                         ov2640_color_saturation(saturation);
  88.                         sprintf((char *)msgbuf, "Saturation:%d", saturation);
  89.                         break;
  90.                     
  91.                     case KEY2_PRES: /* 特效设置 */
  92.                         effect++;
  93.                         if (effect > 6) effect = 0;
  94.                         ov2640_special_effects(effect);
  95.                         sprintf((char *)msgbuf, "Effect:%s", EFFECTS_TBL[effect]);
  96.                         break;
  97.                     
  98.                     case WKUP_PRES: /* 特效设置 */
  99.                         size++;
  100.                         /* 最大只支持WXGA的jpeg数据保存,再大分辨率就不够内存用了 */
  101.                         if (size > 12)size = 0;
  102.                         ov2640_outsize_set(jpeg_img_size_tbl[size][0],
  103.                         jpeg_img_size_tbl[size][1]);     /* 设置输出尺寸 */
  104.                         sprintf((char *)msgbuf, "JPEGSize:%s",
  105.                         JPEG_SIZE_TBL[size]);
  106.                         break;
  107.                     default : break;
  108.                 }
  109.                 lcd_fill(30, 180, 239, 190 + 16, WHITE);
  110.                 /* 显示提示内容 */
  111.                 lcd_show_string(30, 180, 210, 16, 16, (char*)msgbuf, RED);
  112.                 delay_ms(800);
  113.            }
  114.            else
  115.            {
  116.                 /* 提示传输结束设置 */
  117.                 lcd_show_string(30, 210, 210, 16, 16,"Send datacomplete!!", RED);
  118.            }
  119.            
  120.            g_jpeg_data_ok = 2;     /* 标记jpeg数据处理完了,可以让DMA去采集下一帧了. */
  121.        }
  122.     }
  123. }
复制代码
该函数将OV2640设置为JPEG模式,并开启持续自动对焦、实现OV2640的JPEG数据接收,通过串口2发送给上位机软件。

接下来介绍的是RGB565测试函数,其定义如下:
  1. /**
  2. *@brief        RGB565测试
  3. *  @ntoe       RGB数据直接显示在LCD上面
  4. *
  5. *@param        无
  6. *@retval       无
  7. */
  8. void rgb565_test(void)
  9. {
  10.     uint8_t key;
  11.     uint8_t effect = 0, saturation = 3, contrast = 2;
  12.     uint8_t scale = 1;       /* 默认是全尺寸缩放 */
  13.     uint8_t msgbuf[15];     /* 消息缓存区 */
  14.    lcd_clear(WHITE);
  15.    lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
  16.    lcd_show_string(30, 70, 200, 16, 16, "OV2640RGB565 Mode", RED);
  17.    lcd_show_string(30, 100, 200, 16, 16, "KEY0:Contrast", RED);    /* 对比度 */
  18.     /*执行自动对焦*/
  19.    lcd_show_string(30, 120, 200, 16, 16, "KEY1:Saturation", RED);
  20.    lcd_show_string(30, 140, 200, 16, 16, "KEY2:Effects", RED);     /* 特效 */
  21.     /* 1:1尺寸(显示真实尺寸)/全尺寸缩放 */
  22.    lcd_show_string(30, 160, 200, 16, 16, "KEY_UP:FullSize/Scale", RED);
  23.    ov2640_rgb565_mode();   /* RGB565模式 */
  24.    dcmi_init();              /* DCMI配置 */
  25.    dcmi_dma_init((uint32_t)&LCD->LCD_RAM, 0, 1,DMA_MDATAALIGN_HALFWORD,
  26.                       DMA_MINC_DISABLE);    /* DCMI DMA配置,MCU屏,竖屏 */
  27.    ov2640_outsize_set(lcddev.width, lcddev.height);    /* 满屏缩放显示 */
  28.    dcmi_start();             /* 启动传输 */
  29.    
  30.     while (1)
  31.     {
  32.        key = key_scan(0);
  33.        if (key)
  34.        {
  35.            dcmi_stop();           /* 停止显示 */
  36.            switch (key)
  37.            {
  38.                 case KEY0_PRES:   /* 对比度设置 */
  39.                     contrast++;
  40.                     if (contrast > 4)contrast = 0;
  41.                     ov2640_contrast(contrast);
  42.                     sprintf((char*)msgbuf, "Contrast:%d", (signed char)contrast-2);
  43.                     break;
  44.                 case KEY1_PRES:   /* 饱和度设置 */
  45.                     saturation++;
  46.                     if (saturation > 4)saturation = 0;
  47.                     ov2640_color_saturation(saturation);    /* 饱和度设置 */
  48.                     sprintf((char *)msgbuf, "Saturation:%d", (signed char)
  49.                                       saturation - 2);
  50.                     break;
  51.                
  52.                 case KEY2_PRES:   /* 特效设置 */
  53.                     effect++;
  54.                     if (effect > 6)effect = 0;
  55.                     ov2640_special_effects(effect);         /* 设置特效 */
  56.                     sprintf((char *)msgbuf, "%s", EFFECTS_TBL[effect]);
  57.                     break;
  58.                 case WKUP_PRES:   /* 1:1尺寸(显示真实尺寸)/缩放 */
  59.                     scale = !scale;
  60.                     if (scale == 0)
  61.                     {
  62.                         ov2640_image_win_set((1600 - lcddev.width) / 2, (1200 –
  63.                                     lcddev.height) / 2, lcddev.width, lcddev.height);
  64.                         ov2640_outsize_set(lcddev.width, lcddev.height);
  65.                         sprintf((char *)msgbuf, "Full Size1:1");
  66.                     }
  67.                     else
  68.                     {
  69.                         ov2640_image_win_set(0, 0, 1600, 1200);
  70.                         ov2640_outsize_set(lcddev.width, lcddev.height);
  71.                         sprintf((char *)msgbuf, "Scale");
  72.                    }
  73.                     
  74.                     break;
  75.                 default : break;
  76.            }
  77.            /* 显示提示内容 */
  78.            lcd_show_string(30, 50, 210, 16, 16, (char*)msgbuf, RED);
  79.            delay_ms(800);
  80.            dcmi_start();     /* 重新开始传输 */
  81.        }
  82.        delay_ms(10);
  83.     }
  84. }
复制代码
该函数将OV2640设置为RGB565模式,并将接收到的数据,传送给LCD。当使用MCU屏的时候,完全由硬件DMA传输给LCD,CPU不用处理。

接下来介绍的是main函数,其定义如下:
  1. int main(void)
  2. {
  3.     uint8_t key, t;
  4.    HAL_Init();                                  /* 初始化HAL库 */
  5.    sys_stm32_clock_init(336, 8, 2, 7);     /* 设置时钟, 168Mhz */
  6.    delay_init(168);                            /* 延时初始化 */
  7.    usart_init(115200);                        /* 串口初始化为115200 */
  8.    usmart_dev.init(84);                      /* 初始化USMART */
  9.    usart2_init(912600);                       /* 初始化串口2波特率为912600 */
  10.    led_init();                                 /* 初始化LED */
  11.    lcd_init();                                 /* 初始化LCD */
  12.    key_init();                                 /* 初始化按键 */
  13.    /*10Khz计数,1秒钟中断一次,用于统计帧率*/
  14.    btim_timx_int_init(10000 - 1, 8400 - 1);
  15.    lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
  16.    lcd_show_string(30, 70, 200, 16, 16, "OV2640TEST", RED);
  17.    lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
  18.     while (ov2640_init())        /* 初始化OV2640 */
  19.     {
  20.        lcd_show_string(30, 130, 240, 16, 16, "OV2640ERROR", RED);
  21.        delay_ms(200);
  22.        lcd_fill(30, 130, 239, 170, WHITE);
  23.        delay_ms(200);
  24.        LED0_TOGGLE();
  25.     }
  26.    lcd_show_string(30, 130, 200, 16, 16, "OV2640OK", RED);
  27.    ov2640_flash_intctrl();     /* 闪光灯控制 */
  28.     while (1)
  29.     {
  30.        key = key_scan(0);
  31.        if (key == KEY0_PRES)
  32.        {
  33.            g_ov_mode = 0;        /* RGB565模式 */
  34.            break;
  35.        }
  36.        else if (key == KEY1_PRES)
  37.        {
  38.            g_ov_mode = 1;        /* JPEG模式 */
  39.            break;
  40.        }
  41.        t++;
  42.        if (t == 100)lcd_show_string(30, 150, 230, 16, 16,
  43. "KEY0:RGB565 KEY1:JPEG", RED);   /* 闪烁显示提示信息 */
  44.        if (t == 200)
  45.        {
  46.            lcd_fill(30, 150, 210, 150 + 16, WHITE);
  47.            t = 0;
  48.            LED0_TOGGLE();
  49.        }
  50.        delay_ms(5);
  51.     }
  52.     if (g_ov_mode == 1)
  53.     {
  54.        jpeg_test();
  55.     }
  56.     else
  57.     {
  58.        rgb565_test();
  59.     }
  60. }
复制代码
该函数完成对各相关硬件的初始化,然后检测OV2640,最后通过按键选择来调用jpeg_test还是rgb565_test,实现JPEG测试和RGB565测试。

前面提到,我们要用USMART来设置摄像头的参数,我们只需要在usmart_nametab里面添加ov2640_write_reg和ov2640_read_reg等相关函数,就可以轻松调试摄像头了。

47.4 下载验证
将程序下载到开发板后,可以看到LED0不停的闪烁,提示程序已经在运行了。LCD显示的内容如图47.4.1所示:   
image033.png
图47.4.1 摄像头实验程序运行效果图

上图是摄像头已经正确的插上开发板上的运行结果,如果没有插上摄像头,会有错误的报错信息。在OV2640初始化成功后,屏幕提示选择测试模式,此时我们可以按KEY0,进入RGB565模式测试,也可以按KEY1,进入JPEG模式测试。

当按KEY0后,选择RGB565模式,LCD满屏显示压缩放后的图像(有变形),如图47.4.1所示:
image035.jpg
图47.4.1 RGB565模式测试图片

此时,可以按KEY_UP切换为1:1显示(不变形)。同时还可以通过KEY0按键,可以设置对比度;通过KEY1按键,可以设置饱和度;通过KEY2按键,可以设置特效。

当按KEY1后,选择JPEG模式,此时屏幕显示JPEG数据传输进程,如图47.4.2所示:     
image037.png
图47.4.2 JPEG模式测试图

默认条件下,图像分辨率是QVGA (320*240)的,硬件上:我们需要用一根杜邦线连接开发板的PA2排针到RXD排针端。

打开上位机软件:XCAM V1.3.exe(路径:光盘à6,软件资料à软件à串口&网络摄像头软件à XCAM V1.3.exe),选择正确的串口,然后波特率设置为921600,打开即可收到下位机传过来的图片了,如图47.4.3所示:     
image039.png
图47.4.3 XCAM V1.3.exe软件接收并显示JPEG图片

我们可以通过KEY_UP设置输出图像的尺寸(QQVGA~ WXGA)。同时还可以通过KEY0按键,可以设置对比度;通过KEY1按键,可以设置饱和度;通过KEY2按键,可以设置特效。

你还可以在串口(开发板的串口1),通过USMART调用ov2640_write_reg等函数,来设置OV2640的各寄存器,达到调试测试OV2640的目的,如图47.4.4所示:     
image041.png
图47.4.4 USMART调试OV2640

从图47.4.3可以看出,在QVGA模式下,帧率为5帧,传输速度31.1KB/s。从图47.4.4可以看出在UXGA模式,帧率为15帧,并且可以通过USMART调试OV2640。
正点原子逻辑分析仪DL16劲爆上市
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则



关闭

原子哥极力推荐上一条 /2 下一条

正点原子公众号

QQ|手机版|OpenEdv-开源电子网 ( 粤ICP备12000418号-1 )

GMT+8, 2025-2-24 06:35

Powered by OpenEdv-开源电子网

© 2001-2030 OpenEdv-开源电子网

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