49. DCMI—OV5640摄像头

本章参考资料:《STM32F4xx参考手册》、《STM32F4xx规格书》、库帮助文档《stm32f4xx_dsp_stdperiph_lib_um.chm》。

关于开发板配套的OV5640摄像头参数可查阅《ov5640datasheet》配套资料获知。

STM32F4芯片具有浮点运算单元,适合对图像信息使用DSP进行基本的图像处理,其处理速度比传统的8、16位机快得多, 而且它还具有与摄像头通讯的专用DCMI接口,所以使用它驱动摄像头采集图像信息并进行基本的加工处理非常适合。 本章讲解如何使用STM32驱动OV5640型号的摄像头。

49.1. 摄像头简介

在各类信息中,图像含有最丰富的信息,作为机器视觉领域的核心部件,摄像头被广泛地应用在安防、探险以及车牌检测等场合。 摄像头按输出信号的类型来看可以分为数字摄像头和模拟摄像头,按照摄像头图像传感器材料构成来看可以分为CCD和CMOS。 现在智能手机的摄像头绝大部分都是CMOS类型的数字摄像头。

49.1.1. 数字摄像头跟模拟摄像头区别

  • 输出信号类型

数字摄像头输出信号为数字信号,模拟摄像头输出信号为标准的模拟信号。

  • 接口类型

数字摄像头有USB接口(比如常见的PC端免驱摄像头)、IEE1394火线接口(由苹果公司领导的开发联盟开发的一种高速度传送接口, 数据传输率高达800Mbps)、千兆网接口(网络摄像头)。模拟摄像头多采用AV视频端子(信号线+地线)或S-VIDEO(即莲花头–SUPER VIDEO, 是一种五芯的接口,由两路视频亮度信号、两路视频色度信号和一路公共屏蔽地线共五条芯线组成)。

  • 分辨率

模拟摄像头的感光器件,其像素指标一般维持在752(H)*582(V)左右的水平,像素数一般情况下维持在41万左右。 现在的数字摄像头分辨率一般从数十万到数千万。但这并不能说明数字摄像头的成像分辨率就比模拟摄像头的高, 原因在于模拟摄像头输出的是模拟视频信号,一般直接输入至电视或监视器,其感光器件的分辨率与电视信号的扫描数呈一定的换算关系, 图像的显示介质已经确定,因此模拟摄像头的感光器件分辨率不是不能做高,而是依据于实际情况没必要做这么高。

49.1.2. CCD与CMOS的区别

摄像头的图像传感器CCD与CMOS传感器主要区别如下:

  • 成像材料

CCD与CMOS的名称跟它们成像使用的材料有关,CCD是“电荷耦合器件”(Charge Coupled Device)的简称, 而CMOS是“互补金属氧化物半导体”(Complementary Metal Oxide Semiconductor)的简称。

  • 功耗

由于CCD的像素由MOS电容构成,读取电荷信号时需使用电压相当大(至少12V)的二相或三相或四相时序脉冲信号,才能有效地传输电荷。 因此CCD的取像系统除了要有多个电源外,其外设电路也会消耗相当大的功率。有的CCD取像系统需消耗2~5W的功率。 而CMOS光电传感器件只需使用一个单电源5V或3V,耗电量非常小,仅为CCD的1/8~1/10,有的CMOS取像系统只消耗20~50mW的功率。

  • 成像质量

CCD传感器件制作技术起步早,技术成熟,采用PN结或二氧化硅(sio2)隔离层隔离噪声,所以噪声低,成像质量好。与CCD相比, CMOS的主要缺点是噪声高及灵敏度低,不过现在随着CMOS电路消噪技术的不断发展,为生产高密度优质的CMOS传感器件提供了良好的条件, 现在的CMOS传感器已经占领了大部分的市场,主流的单反相机、智能手机都已普遍采用CMOS传感器。

49.2. OV5640摄像头

本章主要讲解实验板配套的摄像头,它的实物见图 实验板配套的OV5640摄像头 ,该摄像头主要由镜头、图像传感器、板载电路及下方的信号引脚组成。

实验板配套的OV5640摄像头 ../_images/DCMIOV0031.png

镜头部件包含一个镜头座和一个可旋转调节距离的凸透镜,通过旋转可以调节焦距,正常使用时,镜头座覆盖在电路板上遮光, 光线只能经过镜头传输到正中央的图像传感器,它采集光线信号,然后把采集得的数据通过下方的信号引脚输出数据到外部器件。

49.2.1. OV5640传感器简介

图像传感器是摄像头的核心部件,上述摄像头中的图像传感器是一款型号为OV5640的CMOS类型数字图像传感器。该传感器支持输出最大为500万像素的图像 (2592x1944分辨率), 支持使用VGA时序输出图像数据,输出图像的数据格式支持YUV(422/420)、YCbCr422、RGB565以及JPEG格式,若直接输出JPEG格式的图像时可大大减少数据量, 方便网络传输。它还可以对采集得的图像进行补偿,支持伽玛曲线、白平衡、饱和度、色度等基础处理。根据不同的分辨率配置, 传感器输出图像数据的帧率从15-60帧可调,工作时功率在150mW-200mW之间。

49.2.2. OV5640引脚及功能框图

OV5640模组带有自动对焦功能,引脚的定义见图 OV5640传感器引脚分布图

OV5640传感器引脚分布图

信号引脚介绍如下表 OV5640管脚

OV5640管脚

下面我们配合图 OV5640功能框图 中的OV5640功能框图讲解这些信号引脚。

OV5640功能框图
  1. 控制寄存器

标号处的是OV5640的控制寄存器,它根据这些寄存器配置的参数来运行,而这些参数是由外部控制器通过SIO_C和SIO_D引脚写入的, SIO_C与SIO_D使用的通讯协议跟I2C十分类似,在STM32中我们完全可以直接用I2C硬件外设来控制。

  1. 通信、控制信号及时钟

标号处包含了OV5640的通信、控制信号及外部时钟,其中PCLK、HREF及VSYNC分别是像素同步时钟、行同步信号以及帧同步信号, 这与液晶屏控制中的信号是很类似的。RESETB引脚为低电平时,用于复位整个传感器芯片,PWDN用于控制芯片进入低功耗模式。 注意最后的一个XCLK引脚,它跟PCLK是完全不同的,XCLK是用于驱动整个传感器芯片的时钟信号,是外部输入到OV5640的信号; 而PCLK是OV5640输出数据时的同步信号,它是由OV5640输出的信号。XCLK可以外接晶振或由外部控制器提供, 若要类比XCLK之于OV5640就相当于HSE时钟输入引脚与STM32芯片的关系,PCLK引脚可类比STM32的I2C外设的SCL引脚。

  1. 感光矩阵

标号处的是感光矩阵,光信号在这里转化成电信号,经过各种处理,这些信号存储成由一个个像素点表示的数字图像。

  1. 数据输出信号

标号处包含了DSP处理单元,它会根据控制寄存器的配置做一些基本的图像处理运算。这部分还包含了图像格式转换单元及压缩单元, 转换出的数据最终通过Y0-Y9引脚输出,一般来说我们使用8根据数据线来传输,这时仅使用Y2-Y9引脚, OV5640与外部器件的连接方式见图 8位数据线接法

8位数据线接法
  1. 数据输出信号

标号⑤处为VCM处理单元,他会通过图像分析来实现图像的自动对焦功能。要实现自动对焦还需要下载自动对焦固件到模组,后面摄像头实验详细介绍这个功能。

49.2.3. SCCB时序

外部控制器对OV5640寄存器的配置参数是通过SCCB总线传输过去的,而SCCB总线跟I2C十分类似,所以在STM32驱动中我们直接使用片上I2C外设与它通讯。 SCCB与标准的I2C协议的区别是它每次传输只能写入或读取一个字节的数据,而I2C协议是支持突发读写的, 即在一次传输中可以写入多个字节的数据(EEPROM中的页写入时序即突发写)。关于SCCB协议的完整内容可查看配套资料里的《SCCB协议》文档,下面我们简单介绍下。

SCCB的起始、停止信号及数据有效性

SCCB的起始信号、停止信号及数据有效性与I2C完全一样,见图 SCCB停止信号 及图 SCCB的数据有效性

  • 起始信号:在SIO_C为高电平时,SIO_D出现一个下降沿,则SCCB开始传输。

  • 停止信号:在SIO_C为高电平时,SIO_D出现一个上升沿,则SCCB停止传输。

  • 数据有效性:除了开始和停止状态,在数据传输过程中,当SIO_C为高电平时,必须保证SIO_D上的数据稳定, 也就是说,SIO_D上的电平变换只能发生在SIO_C为低电平的时候,SIO_D的信号在SIO_C为高电平时被采集。

SCCB停止信号_ SCCB的数据有效性

SCCB数据读写过程

在SCCB协议中定义的读写操作与I2C也是一样的,只是换了一种说法。它定义了两种写操作,即三步写操作和两步写操作。 三步写操作可向从设备的一个目的寄存器中写入数据,见图 SCCB的三步写操作 。在三步写操作中, 第一阶段发送从设备的ID地址+W标志(等于I2C的设备地址:7位设备地址+读写方向标志),第二阶段发送从设备目标寄存器的8位地址, 第三阶段发送要写入寄存器的8位数据。图中的“X”数据位可写入1或0,对通讯无影响。

SCCB的三步写操作

而两步写操作没有第三阶段,即只向从器件传输了设备ID+W标志和目的寄存器的地址,见图 SCCB的两步写操作 。 两步写操作是用来配合后面的读寄存器数据操作的,它与读操作一起使用,实现I2C的复合过程。

SCCB的两步写操作

两步读操作,它用于读取从设备目的寄存器中的数据,见图 SCCB的两步读操作 。在第一阶段中发送从设备的设备ID+R标志(设备地址+读方向标志)和自由位, 在第二阶段中读取寄存器中的8位数据和写NA位(非应答信号)。由于两步读操作没有确定目的寄存器的地址, 所以在读操作前,必需有一个两步写操作,以提供读操作中的寄存器地址。

SCCB的两步读操作

可以看到,以上介绍的SCCB特性都与I2C无区别,而I2C比SCCB还多出了突发读写的功能, 所以SCCB可以看作是I2C的子集,我们完全可以使用STM32的I2C外设来与OV5640进行SCCB通讯。

49.2.4. OV5640的寄存器

控制OV5640涉及到它很多的寄存器,可直接查询《ov5640datasheet》了解,通过这些寄存器的配置,可以控制它输出图像的分辨率大小、 图像格式及图像方向等。要注意的是OV5640寄存器地址为16位。

官方还提供了一个《OV5640_自动对焦照相模组应用指南(DVP_接口)__R2.13C.pdf》的文档,它针对不同的配置需求,提供了配置范例,见图 调节帧率的寄存器配置范例 。 其中write_SCCB是一个利用SCCB向寄存器写入数据的函数,第一个参数为要写入的寄存器的地址,第二个参数为要写入的内容。

调节帧率的寄存器配置范例

49.2.5. 像素数据输出时序

对OV5640时采用SCCB协议进行控制,而它输出图像时则使用VGA时序(还可用SVGA、UXGA,这些时序都差不多), 这跟控制液晶屏输入图像时很类似。OV5640输出图像时,一帧帧地输出,在帧内的数据一般从左到右,从上到下, 一个像素一个像素地输出(也可通过寄存器修改方向),见图 摄像头数据输出

摄像头数据输出

例如,见图 DVP接口时序 ,若我们使用Y2-Y9数据线,图像格式设置为RGB565, 进行数据输出时,Y2-Y9数据线会在1个像素同步时钟PCLK的驱动下发送1字节的数据信号,所以2个PCLK时钟可发送1个RGB565格式的像素数据。 像素数据依次传输,每传输完一行数据时,行同步信号HREF会输出一个电平跳变信号,每传输完一帧图像时,VSYNC会输出一个电平跳变信号。

DVP接口时序

49.3. STM32的DCMI接口简介

STM32F4系列的控制器包含了DCMI数字摄像头接口(Digital cameraInterface), 它支持使用上述类似VGA的时序获取图像数据流,支持原始的按行、帧格式来组织的图像数据,如YUV、RGB, 也支持接收JPEG格式压缩的数据流。接收数据时,主要使用HSYNC及VSYNC信号来同步。

49.3.1. DCMI整体框图

STM32的DCMI接口整体框图见图 DCMI接口整体框图

DCMI接口整体框图

外部接口及时序

上图标号处的是DCMI向外部引出的信号线。DCMI提供的外部接口的方向都是输入的,接口的各个信号线说明见表 DCMI的信号线说明

DCMI的信号线说明

其中DCMI_D数据线的数量可选8、10、12或14位,各个同步信号的有效极性都可编程控制。 它使用的通讯时序与OV5640的图像数据输出接口时序一致,见图 DCMI时序图

DCMI时序图

内部信号及PIXCLK的时钟频率

DCMI接口整体框图 的标号处表示DCMI与内部的信号线。在STM32的内部,使用HCLK作为时钟源提供给DCMI外设。 从DCMI引出有DCMI_IT信号至中断控制器,并可通过DMA_REQ信号发送DMA请求。

DCMI从外部接收数据时,在HCLK的上升沿时对PIXCLK同步的信号进行采样, 它限制了PIXCLK的最小时钟周期要大于2.5个HCLK时钟周期,即最高频率为HCLK的1/4。

49.3.2. DCMI接口内部结构

DCMI接口的内部结构见图 DCMI接口内部结构

DCMI接口内部结构

(1) 同步器

同步器主要用于管理DCMI接收数据的时序,它根据外部的信号提取输入的数据。

(2) FIFO/数据格式化器

为了对数据传输加以管理,STM32在DCMI接口上实现了 4 个字(32bit x4)深度的 FIFO,用以缓冲接收到的数据。

(3) AHB接口

DCMI接口挂载在AHB总线上,在AHB总线中有一个DCMI接口的数据寄存器,当我们读取该寄存器时,它会从FIFO中获取数据, 并且FIFO中的数据指针会自动进行偏移,使得我们每次读取该寄存器都可获得一个新的数据。

(4) 控制/状态寄存器

DCMI的控制寄存器协调图中的各个结构运行,程序中可通过检测状态寄存器来获DCMI的当前运行状态。

(5) DMA接口

由于DCMI采集的数据量很大,我们一般使用DMA来把采集得的数据搬运至内存。

49.3.3. 同步方式

DCMI接口支持硬件同步或内嵌码同步方式,硬件同步方式即使用HSYNC和VSYNC作为同步信号的方式,OV5640就是使用这种同步时序。

而内嵌码同步的方式是使用数据信号线传输中的特定编码来表示同步信息,由于需要用0x00和0xFF来表示编码, 所以表示图像的数据中不能包含有这两个值。利用这两个值,它扩展到4个字节,定义出了2种模式的同步码,每种模式包含4个编码, 编码格式为0xFF0000XY,其中XY的值可通过寄存器设置。当DCMI接收到这样的编码时,它不会把这些当成图像数据, 而是按照表 两种模式的内嵌码 中的编码来解释,作为同步信号。

两种模式的内嵌码

49.3.4. 捕获模式及捕获率

DCMI还支持两种数据捕获模式,分别为快照模式和连续采集模式。快照模式时只采集一帧的图像数据,连续采集模式会一直采集多个帧的数据, 并且可以通过配置捕获率来控制采集多少数据,如可配置为采集所有数据或隔1帧采集一次数据或隔3帧采集一次数据。

49.4. DCMI初始化结构体

与其它外设一样,STM32的DCMI外设也可以使用库函数来控制,其中最主要的配置项都封装到了DCMI_InitTypeDef结构体, 来这些内容都定义在库文件“stm32f4xx_dcmi.h”及“stm32f4xx_ dcmi.c”中,编程时我们可以结合这两个文件内的注释使用或参考库帮助文档。

DCMI_InitTypeDef初始化结构体的内容见 代码清单:OV5640-1

代码清单:OV5640-1 DCMI初始化结构体
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/**
* @brief   DCMI 初始化结构体
*/
typedef struct
{
    uint16_t DCMI_CaptureMode;      /*选择连续模式或拍照模式 */
    uint16_t DCMI_SynchroMode;      /*选择硬件同步模式还是内嵌码模式 */
    uint16_t DCMI_PCKPolarity;      /*设置像素时钟的有效边沿*/
    uint16_t DCMI_VSPolarity;       /*设置VSYNC的有效电平*/
    uint16_t DCMI_HSPolarity;       /*设置HSYNC的有效边沿*/
    uint16_t DCMI_CaptureRate;      /*设置图像的采集间隔 */
    uint16_t DCMI_ExtendedDataMode; /*设置数据线的宽度 */
} DCMI_InitTypeDef;

这些结构体成员说明如下,其中括号内的文字是对应参数在STM32标准库中定义的宏:

(1) DCMI_CaptureMode

本成员设置DCMI的捕获模式, 可以选择为连续摄像(DCMI_CaptureMode_Continuous)或单张拍照DCMI_CaptureMode_SnapShot。

(2) DCMI_SynchroMode

本成员设置DCMI数据的同步模式, 可以选择为硬件同步方式(DCMI_SynchroMode_Hardware)或内嵌码方式(DCMI_SynchroMode_Embedded)。

(3) DCMI_PCKPolarity

本成员用于配置DCMI接口像素时钟的有效边沿,即在该时钟边沿时,DCMI会对数据线上的信号进行采样, 它可以被设置为上升沿有效(DCMI_PCKPolarity_Rising)或下降沿有效(DCMI_PCKPolarity_Falling)。

(4) DCMI_VSPolarity

本成员用于设置VSYNC的有效电平,当VSYNC信号线表示为有效电平时,表示新的一帧数据传输完成, 它可以被设置为高电平有效(DCMI_VSPolarity_High)或低电平有效(DCMI_VSPolarity_Low)。

(5) DCMI_HSPolarity

类似地,本成员用于设置HSYNC的有效电平,当HSYNC信号线表示为有效电平时,表示新的一行数据传输完成, 它可以被设置为高电平有效(DCMI_HSPolarity_High)或低电平有效(DCMI_HSPolarity_Low)。

(6) DCMI_CaptureRate

本成员可以用于设置DCMI捕获数据的频率,可以设置为全采集、半采集或1/4采集(DCMI_CaptureRate_All_Frame/ 1of2_Frame/ 1of4_Frame), 在间隔采集的情况下,STM32的DCMI外设会直接按间隔丢弃数据。

(7) DCMI_ExtendedDataMode

本成员用于设置DCMI的数据线宽度,可配置为8/10/12及14位数据线宽(DCMI_ExtendedDataMode_8b/10b/12b/14b)。

配置完这些结构体成员后,我们调用库函数DCMI_Init即可把这些参数写入到DCMI的控制寄存器中,实现DCMI的初始化。

49.5. DCMI—OV5640摄像头实验

本小节讲解如何使用DCMI接口从OV5640摄像头输出的RGB565格式的图像数据,并把这些数据实时显示到液晶屏上。

学习本小节内容时,请打开配套的“DCMI—OV5640摄像头”工程配合阅读。

49.5.1. 硬件设计

摄像头原理图

本实验采用的OV5640摄像头实物见图 实验板配套的OV5640摄像头 ,其原理图见图 OV5640摄像头原理图

OV5640摄像头原理图

OV5640摄像头原理图 标号①处的是OV5640模组接口电路,在这部分中已对SCCB使用的信号线接了上拉电阻,外部电路可以省略上拉; 标号②处的是一个24MHz的有源晶振,它为OV5640提供系统时钟,如果不想使用外部晶振提供时钟源,可以参考图中的R6处贴上0欧电阻, XCLK引脚引出至外部,由外部控制器提供时钟;标号③处的是电源转换模块,可以从5V转2.8V和1.5V供给模组使用;标号④处的是摄像头引脚集中引出的排针接口, 使用它可以方便地与STM32实验板中的排母连接。标号⑤处的是电源指示灯。

摄像头与实验板的连接

通过排母,OV5640与STM32引脚的连接关系见图 STM32实验板引出的DCMI接口 。控制摄像头的部分引脚与实验板上的RGB彩灯共用,使用时会互相影响。

STM32实验板引出的DCMI接口

以上原理图可查阅《ov5640—黑白原理图》及《F429开发板黑白原理图》文档获知,若您使用的摄像头或实验板不一样,请根据实际连接的引脚修改程序。

49.5.2. 软件设计

为了使工程更加有条理,我们把摄像头控制相关的代码独立分开存储,方便以后移植。在“LTDC—液晶显示”工程的基础上新建“bsp_ov5640.c”及“bsp_ov5640.h”文件, 这些文件也可根据您的喜好命名,它们不属于STM32标准库的内容,是由我们自己根据应用需要编写的。

49.5.2.1. 编程要点

  1. 初始化DCMI时钟,I2C时钟;

  2. 使用I2C接口向OV5640写入寄存器配置;

  3. 初始化DCMI工作模式;

  4. 初始化DMA,用于搬运DCMI的数据到显存空间进行显示;

  5. 编写测试程序,控制采集图像数据并显示到液晶屏。

49.5.2.2. 代码分析

摄像头硬件相关宏定义

我们把摄像头控制硬件相关的配置都以宏的形式定义到 “bsp_ov5640.h”文件中,其中包括I2C及DCMI接口的,见 代码清单:OV5640-2

代码清单:OV5640-2 摄像头硬件配置相关的宏(省略了部分数据线)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/*摄像头接口 */
//IIC SCCB
#define CAMERA_I2C                          I2C1
#define CAMERA_I2C_CLK                      RCC_APB1Periph_I2C1

#define CAMERA_I2C_SCL_PIN                  GPIO_Pin_6
#define CAMERA_I2C_SCL_GPIO_PORT            GPIOB
#define CAMERA_I2C_SCL_GPIO_CLK             RCC_AHB1Periph_GPIOB
#define CAMERA_I2C_SCL_SOURCE               GPIO_PinSource6
#define CAMERA_I2C_SCL_AF                   GPIO_AF_I2C1

#define CAMERA_I2C_SDA_PIN                  GPIO_Pin_7
#define CAMERA_I2C_SDA_GPIO_PORT            GPIOB
#define CAMERA_I2C_SDA_GPIO_CLK             RCC_AHB1Periph_GPIOB
#define CAMERA_I2C_SDA_SOURCE               GPIO_PinSource7
#define CAMERA_I2C_SDA_AF                   GPIO_AF_I2C1

//VSYNC
#define DCMI_VSYNC_GPIO_PORT            GPIOI
#define DCMI_VSYNC_GPIO_CLK             RCC_AHB1Periph_GPIOI
#define DCMI_VSYNC_GPIO_PIN             GPIO_Pin_5
#define DCMI_VSYNC_PINSOURCE            GPIO_PinSource5
#define DCMI_VSYNC_AF                     GPIO_AF_DCMI
// HSYNC
#define DCMI_HSYNC_GPIO_PORT            GPIOA
#define DCMI_HSYNC_GPIO_CLK             RCC_AHB1Periph_GPIOA
#define DCMI_HSYNC_GPIO_PIN             GPIO_Pin_4
#define DCMI_HSYNC_PINSOURCE            GPIO_PinSource4
#define DCMI_HSYNC_AF                     GPIO_AF_DCMI
//PIXCLK
#define DCMI_PIXCLK_GPIO_PORT           GPIOA
#define DCMI_PIXCLK_GPIO_CLK            RCC_AHB1Periph_GPIOA
#define DCMI_PIXCLK_GPIO_PIN            GPIO_Pin_6
#define DCMI_PIXCLK_PINSOURCE           GPIO_PinSource6
#define DCMI_PIXCLK_AF                    GPIO_AF_DCMI
//PWDN
#define DCMI_PWDN_GPIO_PORT         GPIOG
#define DCMI_PWDN_GPIO_CLK          RCC_AHB1Periph_GPIOG
#define DCMI_PWDN_GPIO_PIN          GPIO_Pin_3

//数据信号线
#define DCMI_D0_GPIO_PORT           GPIOH
#define DCMI_D0_GPIO_CLK            RCC_AHB1Periph_GPIOH
#define DCMI_D0_GPIO_PIN            GPIO_Pin_9
#define DCMI_D0_PINSOURCE           GPIO_PinSource9
#define DCMI_D0_AF                    GPIO_AF_DCMI
/*....省略部分数据线*/

以上代码根据硬件的连接,把与DCMI、I2C接口与摄像头通讯使用的引脚号、引脚源以及复用功能映射都以宏封装起来。

初始化DCMI的 GPIO及I2C

利用上面的宏,初始化DCMI的GPIO引脚及I2C,见 代码清单:OV5640-3

代码清单:OV5640-3 初始化DCMI的GPIO及I2C(省略了部分数据线)
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
/**
* @brief  初始化控制摄像头使用的GPIO(I2C/DCMI)
* @param  None
* @retval None
*/
void OV5640_HW_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    I2C_InitTypeDef  I2C_InitStruct;

    /***DCMI引脚配置***/
    /* 使能DCMI时钟 */
    RCC_AHB1PeriphClockCmd(DCMI_PWDN_GPIO_CLK|DCMI_RST_GPIO_CLK|DCMI_VS
                        YNC_GPIO_CLK | DCMI_HSYNC_GPIO_CLK |
                        DCMI_PIXCLK_GPIO_CLK|
                        DCMI_D0_GPIO_CLK| DCMI_D1_GPIO_CLK|
                        DCMI_D2_GPIO_CLK| DCMI_D3_GPIO_CLK|
                        DCMI_D4_GPIO_CLK| DCMI_D5_GPIO_CLK|
                        DCMI_D6_GPIO_CLK| DCMI_D7_GPIO_CLK, ENABLE);

    /*控制/同步信号线*/
    GPIO_InitStructure.GPIO_Pin = DCMI_VSYNC_GPIO_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP ;
    GPIO_Init(DCMI_VSYNC_GPIO_PORT, &GPIO_InitStructure);
    GPIO_PinAFConfig(DCMI_VSYNC_GPIO_PORT, DCMI_VSYNC_PINSOURCE,
                    DCMI_VSYNC_AF);

    GPIO_InitStructure.GPIO_Pin = DCMI_HSYNC_GPIO_PIN ;
    GPIO_Init(DCMI_HSYNC_GPIO_PORT, &GPIO_InitStructure);
    GPIO_PinAFConfig(DCMI_HSYNC_GPIO_PORT, DCMI_HSYNC_PINSOURCE,
                    DCMI_HSYNC_AF);

    GPIO_InitStructure.GPIO_Pin = DCMI_PIXCLK_GPIO_PIN ;
    GPIO_Init(DCMI_PIXCLK_GPIO_PORT, &GPIO_InitStructure);
    GPIO_PinAFConfig(DCMI_PIXCLK_GPIO_PORT, DCMI_PIXCLK_PINSOURCE,
                    DCMI_PIXCLK_AF);

    /*数据信号*/
    GPIO_InitStructure.GPIO_Pin = DCMI_D0_GPIO_PIN ;
    GPIO_Init(DCMI_D0_GPIO_PORT, &GPIO_InitStructure);
    GPIO_PinAFConfig(DCMI_D0_GPIO_PORT, DCMI_D0_PINSOURCE, DCMI_D0_AF);
    /*...省略部分数据信号线*/

    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
    GPIO_InitStructure.GPIO_Pin = DCMI_PWDN_GPIO_PIN ;
    GPIO_Init(DCMI_PWDN_GPIO_PORT, &GPIO_InitStructure);
    GPIO_InitStructure.GPIO_Pin = DCMI_RST_GPIO_PIN ;
    GPIO_Init(DCMI_RST_GPIO_PORT, &GPIO_InitStructure);
    /*PWDN引脚,高电平关闭电源,低电平供电*/

    GPIO_ResetBits(DCMI_RST_GPIO_PORT,DCMI_RST_GPIO_PIN);
    GPIO_SetBits(DCMI_PWDN_GPIO_PORT,DCMI_PWDN_GPIO_PIN);

    Delay(10);//延时10ms

    GPIO_ResetBits(DCMI_PWDN_GPIO_PORT,DCMI_PWDN_GPIO_PIN);

    Delay(10);//延时10ms

    GPIO_SetBits(DCMI_RST_GPIO_PORT,DCMI_RST_GPIO_PIN);

    /****** 配置I2C,使用I2C与摄像头的SCCB接口通讯*****/
    /* 使能I2C时钟 */
    RCC_APB1PeriphClockCmd(CAMERA_I2C_CLK, ENABLE);
    /* 使能I2C使用的GPIO时钟 */
    RCC_AHB1PeriphClockCmd(CAMERA_I2C_SCL_GPIO_CLK|CAMERA_I2C_SDA_GPIO_
                        CLK, ENABLE);
    /* 配置引脚源 */
    GPIO_PinAFConfig(CAMERA_I2C_SCL_GPIO_PORT, CAMERA_I2C_SCL_SOURCE,
                    CAMERA_I2C_SCL_AF);
    GPIO_PinAFConfig(CAMERA_I2C_SDA_GPIO_PORT, CAMERA_I2C_SDA_SOURCE,
                    CAMERA_I2C_SDA_AF);

    /* 初始化GPIO */
    GPIO_InitStructure.GPIO_Pin = CAMERA_I2C_SCL_PIN ;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
    GPIO_InitStructure.GPIO_OType = GPIO_OType_OD;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
    GPIO_Init(CAMERA_I2C_SCL_GPIO_PORT, &GPIO_InitStructure);
    GPIO_PinAFConfig(CAMERA_I2C_SCL_GPIO_PORT, CAMERA_I2C_SCL_SOURCE,
                    CAMERA_I2C_SCL_AF);

    GPIO_InitStructure.GPIO_Pin = CAMERA_I2C_SDA_PIN ;
    GPIO_Init(CAMERA_I2C_SDA_GPIO_PORT, &GPIO_InitStructure);

    /*初始化I2C模式 */
    I2C_DeInit(CAMERA_I2C);

    I2C_InitStruct.I2C_Mode = I2C_Mode_I2C;
    I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2;
    I2C_InitStruct.I2C_OwnAddress1 = 0xFE;
    I2C_InitStruct.I2C_Ack = I2C_Ack_Enable;
    I2C_InitStruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7b
                                            it;
    I2C_InitStruct.I2C_ClockSpeed = 400000;

    /* 写入配置 */
    I2C_Init(CAMERA_I2C, &I2C_InitStruct);

    /* 使能I2C */
    I2C_Cmd(CAMERA_I2C, ENABLE);

    Delay(50);//延时50ms
}

与所有使用到GPIO的外设一样,都要先把使用到的GPIO引脚模式初始化,以上代码把DCMI接口的信号线全都初始化为DCMI复用功能。

注意

OV5640的上电时序比较特殊,我们初始化PWDN则被和RST应该特别小心,先初始化成普通的推挽输出模式, 并且在初始化完毕后直接控制它RST为低电平,PWDN为高电平,使能给摄像头供电处于待机模式,延时10ms后控制PWDN为低电平, 再延时10ms后控制RST为高电平,OV5640模组启动。

函数中还包含了I2C的初始化配置,使用I2C与OV2640OV5640的SCCB接口通讯,这里的I2C模式配置与标准的I2C无异。

警告

I2C初始化完必须延时50ms,再进行对OV5640寄存器的读写操作。

配置DCMI的模式

接下来需要配置DCMI的工作模式,我们通过编写OV5640_Init函数完成该功能,见 代码清单:OV5640-4

代码清单:OV5640-4 配置DCMI的模式(bsp_ov5640.c文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#define FSMC_LCD_ADDRESS      FSMC_Addr_ILI9806G_DATA


/**
* @brief  配置 DCMI/DMA 以捕获摄像头数据
* @param  None
* @retval None
*/
void OV5640_Init(void)
{
    DCMI_InitTypeDef DCMI_InitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    /*** 配置DCMI接口 ***/
    /* 使能DCMI时钟 */
    RCC_AHB2PeriphClockCmd(RCC_AHB2Periph_DCMI, ENABLE);

    /* DCMI 配置*/
    DCMI_InitStructure.DCMI_CaptureMode = DCMI_CaptureMode_Continuous;
    DCMI_InitStructure.DCMI_SynchroMode = DCMI_SynchroMode_Hardware;
    DCMI_InitStructure.DCMI_PCKPolarity = DCMI_PCKPolarity_Rising;
    DCMI_InitStructure.DCMI_VSPolarity = DCMI_VSPolarity_Low;
    DCMI_InitStructure.DCMI_HSPolarity = DCMI_HSPolarity_Low;
    DCMI_InitStructure.DCMI_CaptureRate = DCMI_CaptureRate_All_Frame;
    DCMI_InitStructure.DCMI_ExtendedDataMode = DCMI_ExtendedDataMode_8b;
    DCMI_Init(&DCMI_InitStructure);

    //配置DMA传输,直接配置循环传输即可
    OV5640_DMA_Config(FSMC_LCD_ADDRESS,1);

    /* 配置中断 */
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);

    /* 配置帧中断,接收到帧同步信号就进入中断 */
    NVIC_InitStructure.NVIC_IRQChannel = DCMI_IRQn ;  //帧中断
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority =0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
    DCMI_ITConfig (DCMI_IT_FRAME,ENABLE);
}

该函数的执行流程如下:

(1) 使能DCMI外设的时钟, 它是挂载在AHB2总线上的;

(2) 根据摄像头的时序和硬件连接的要求,配置DCMI工作模式为:使用硬件同步, 连续采集所有帧数据,采集时使用8根数据线,PIXCLK被设置为上升沿有效,VSYNC被设置成高电平有效, HSYNC被设置成低电平有效;

(3) 调用OV5640_DMA_Config函数开始DMA数据传输,每传输完一行数据需要调用一次, 它包含本次传输的目的首地址及传输的数据量,后面我们再详细解释 ;

(4) 配置DMA中断,DMA每次传输完毕会引起中断, 以便我们在中断服务函数配置DMA传输下一行数据;

(5) 配置DCMI的帧传输中断,为了防止有时DMA出现传输错误或传输速度跟不上导致数据错位、 偏移等问题,每次DCMI接收到摄像头的一帧数据,得到新的帧同步信号后(VSYNC),就进入中断,复位DMA,使它重新开始一帧的数据传输。

配置DMA数据传输

上面的DCMI配置函数中调用了OV5640_DMA_Config函数开始了DMA传输,该函数的定义见 代码清单:OV5640-5

代码清单:OV5640-5 配置DMA数据传输(bsp_ov5640.c文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* @brief  配置 DCMI/DMA 以捕获摄像头数据
    * @param  DMA_Memory0BaseAddr:本次传输的目的首地址
* @param DMA_BufferSize:本次传输的数据量(单位为字,
                                                    即4字节)
*/
void OV5640_DMA_Config(uint32_t DMA_Memory0BaseAddr,uint16_t
                    DMA_BufferSize)
{

    DMA_InitTypeDef  DMA_InitStructure;

    /* 配置DMA从DCMI中获取数据*/
    /* 使能DMA*/
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);
    DMA_Cmd(DMA2_Stream1,DISABLE);
    while (DMA_GetCmdStatus(DMA2_Stream1) != DISABLE) {}

    DMA_InitStructure.DMA_Channel = DMA_Channel_1;
    DMA_InitStructure.DMA_PeripheralBaseAddr = DCMI_DR_ADDRESS;
                                            //DCMI数据寄存器地址
    DMA_InitStructure.DMA_Memory0BaseAddr = DMA_Memory0BaseAddr;
                                            //DMA传输的目的地址(传入的参数)
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;
    DMA_InitStructure.DMA_BufferSize =DMA_BufferSize;
                                    //传输的数据大小(传入的参数)
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
                                    //寄存器地址自增
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_W
                                            ord;
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
    DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
                                //循环模式
    DMA_InitStructure.DMA_Priority = DMA_Priority_High;
    DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Enable;
    DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;
    DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_INC8;
    DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;

    /*DMA中断配置 */
    DMA_Init(DMA2_Stream1, &DMA_InitStructure);

    DMA_Cmd(DMA2_Stream1,ENABLE);
    while (DMA_GetCmdStatus(DMA2_Stream1) != ENABLE) {}
}

该函数跟普通的DMA配置无异,它把DCMI接收到的数据从它的数据寄存器搬运到SDRAM显存中,从而直接使用液晶屏显示摄像头采集得的图像。 它包含2个输入参数DMA_Memory0BaseAddr和DMA_BufferSize,其中DMA_Memory0BaseAddr用于设置本次DMA传输的目的首地址, 该参数会被赋值到结构体成员DMA_InitStructure.DMA_Memory0BaseAddr中。DMA_BufferSize则用于指示本次DMA传输的数据量, 它会被赋值到结构体成员DMA_InitStructure.DMA_BufferSize中,要注意它的单位是一个字,即4字节,如我们要传输60字节的数据时, 它应配置为15。在前面的OV5640_Init函数中,对这个函数有如下调用:

#define FSMC_LCD_ADDRESS     ((uint32_t)0xD0000000)

/*液晶屏的分辨率,用来计算地址偏移*/
uint16_t lcd_width=800, lcd_height=480;

/*摄像头采集图像的大小,改变这两个值可以改变数据量,
但不会加快采集速度,要加快采集速度需要改成SVGA*/
uint16_t img_width=800, img_height=480;



//dma_memory 以16位数据为单位, dma_bufsize以32位数据为单位(即像素个数/2)
OV5640_DMA_Config(FSMC_LCD_ADDRESS,img_width*2/4);

其中的lcd_width和lcd_height是液晶屏的分辨率,img_width和img_heigh表示摄像头输出的图像的分辨率,FSMC_LCD_ADDRESS是液晶层的首个显存地址。 另外,本工程中显示摄像头数据的这个液晶层采用RGB565的像素格式,每个像素点占据2个字节。

所以在上面的函数调用中,第一个输入参数:

【FSMC_LCD_ADDRESS】

它表示的是液晶屏第一行的第一个像素的地址。

而第二个输入参数:

【img_width*2/4】

它表示表示摄像头一行图像的数据量,单位为字,即用一行图像数据的像素个数除以2即可。注意这里使用的变量是“img_width”而不是的“lcd_width”。

由于这里配置的是第一次DMA传输,它把DCMI接收到的第一行摄像头数据传输至液晶屏的第一行,见图 DMA传输过程 , 再配合在后面分析的中断函数里的多次DMA配置,摄像头输出的数据会一行一行地“由上至下”显示到液晶屏上。

DMA传输过程

DMA传输完成中断及帧中断

OV5640_Init函数初始化了DCMI,使能了帧中断、DMA传输完成中断,并使能了第一次DMA传输,当这一行数据传输完成时,会进入DMA中断服务函数, 见 代码清单:OV5640-6 中的DMA2_Stream1_IRQHandler。

代码清单:OV5640-6 DMA传输完成中断与帧中断(stm32f4xx_it.c文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
extern uint16_t lcd_width, lcd_height;
extern uint16_t img_width, img_height;

//记录传输了多少行
static uint16_t line_num =0;
//DMA传输完成中断服务函数
void DMA2_Stream1_IRQHandler(void)
{
    if (  DMA_GetITStatus(DMA2_Stream1,DMA_IT_TCIF1) == SET )
    {
        /*行计数*/
        line_num++;
        if (line_num==img_height)
        {
            /*传输完一帧,计数复位*/
            line_num=0;
        }
        /*DMA 一行一行传输*/
        OV5640_DMA_Config(FSMC_LCD_ADDRESS+(lcd_width*2*line_num)),img_width*2/4);
        DMA_ClearITPendingBit(DMA2_Stream1,DMA_IT_TCIF1);
    }
}


//帧中断服务函数,使用帧中断重置line_num,可防止有时掉数据的时候DMA传送行数出现偏移
void DCMI_IRQHandler(void)
{
    if (  DCMI_GetITStatus (DCMI_IT_FRAME) == SET )
    {
        /*传输完一帧,计数复位*/
        line_num=0;
        DCMI_ClearITPendingBit(DCMI_IT_FRAME);
    }
}

DMA中断服务函数中主要是使用了一个静态变量line_num来记录已传输了多少行数据,每进一次DMA中断时自加1,由于进入一次中断就代表传输完一行数据, 所以line_num的值等于lcd_height时(摄像头输出的数据行数),表示传输完一帧图像,line_num复位为0,开始另一帧数据的传输。 line_num计数完毕后利用前面定义的OV5640_DMA_Config函数配置新的一行DMA数据传输,它利用line_num变量计算显存地址的行偏移, 控制DCMI数据被传送到正确的位置,每次传输的都是一行像素的数据量。

当DCMI接口检测到摄像头传输的帧同步信号时,会进入DCMI_IRQHandler中断服务函数,在这个函数中不管line_num原来的值是什么, 它都把line_num直接复位为0,这样下次再进入DMA中断服务函数的时候,它会开始新一帧数据的传输。这样可以利用DCMI的硬件同步信号, 而不只是依靠DMA自己的传输计数,这样可以避免有时STM32内部DMA传输受到阻塞而跟不上外部摄像头信号导致的数据错误。

使能DCMI采集

以上是我们使用DCMI的传输配置,但它还没有使能DCMI采集,在实际使用中还需要调用下面两个库函数开始采集数据。

//使能DCMI采集数据
DCMI_Cmd(ENABLE);
DCMI_CaptureCmd(ENABLE);

读取OV5640芯片ID

配置完了STM32的DCMI,还需要控制摄像头,它有很多寄存器用于配置工作模式。利用STM32的I2C接口,可向OV5640的寄存器写入控制参数, 我们先写个读取芯片ID的函数测试一下,见 代码清单:OV5640-7

代码清单:OV5640-7 读取OV5640的芯片ID(bsp_ov5640.c文件)
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
//存储摄像头ID的结构体
typedef struct {
    uint8_t PIDH;
    uint8_t PIDL;
} OV5640_IDTypeDef;
#define OV5640_SENSOR_PIDH       0x300A
#define OV5640_SENSOR_PIDL       0x300B
/**
* @brief  读取摄像头的ID.
* @param  OV5640ID: 存储ID的结构体
* @retval None
*/
void OV5640_ReadID(OV5640_IDTypeDef *OV5640ID)
{

    /*读取寄存芯片ID*/
    OV5640ID->PIDH = OV5640_ReadReg(OV5640_SENSOR_PIDH);
    OV5640ID->PIDL = OV5640_ReadReg(OV5640_SENSOR_PIDL);
}
/**
* @brief  从OV5640寄存器中读取一个字节的数据
* @param  Addr: 寄存器地址
* @retval 返回读取得的数据
*/
uint8_t OV5640_ReadReg(uint16_t Addr)
{
    uint32_t timeout = DCMI_TIMEOUT_MAX;
    uint8_t Data = 0;

    /* Generate the Start Condition */
    I2C_GenerateSTART(CAMERA_I2C, ENABLE);

    /* Test on CAMERA_I2C EV5 and clear it */
    timeout = DCMI_TIMEOUT_MAX; /* Initialize timeout value */
    while (!I2C_CheckEvent(CAMERA_I2C, I2C_EVENT_MASTER_MODE_SELECT)) {
        /* If the timeout delay is exeeded, exit with error code */
        if ((timeout--) == 0) return 0xFF;
    }

    /* Send DCMI selcted device slave Address for write */
    I2C_Send7bitAddress(CAMERA_I2C, OV5640_DEVICE_ADDRESS,
                        I2C_Direction_Transmitter);

    /* Test on I2C1 EV6 and clear it */
    timeout = DCMI_TIMEOUT_MAX; /* Initialize timeout value */
    while (!I2C_CheckEvent(CAMERA_I2C,
        I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)) {
        /* If the timeout delay is exeeded, exit with error code */
        if ((timeout--) == 0) return 0xFF;
    }

    /* Send I2C1 location address MSB */
    I2C_SendData( CAMERA_I2C, (uint8_t)((Addr>>8) & 0xFF) );

    /* Test on I2C1 EV8 and clear it */
    timeout = DCMI_TIMEOUT_MAX; /* Initialize timeout value */
    while (!I2C_CheckEvent(CAMERA_I2C,
        I2C_EVENT_MASTER_BYTE_TRANSMITTED)) {
        /* If the timeout delay is exeeded, exit with error code */
        if ((timeout--) == 0) return 0xFF;
    }

    /* Clear AF flag if arised */
    CAMERA_I2C->SR1 |= (uint16_t)0x0400;

//--------------------------------------------------------
    /* Send I2C1 location address LSB */
    I2C_SendData( CAMERA_I2C, (uint8_t)(Addr & 0xFF) );
    /* Test on I2C1 EV8 and clear it */
    timeout = DCMI_TIMEOUT_MAX; /* Initialize timeout value */
    while (!I2C_CheckEvent(CAMERA_I2C,
        I2C_EVENT_MASTER_BYTE_TRANSMITTED)) {
        /* If the timeout delay is exeeded, exit with error code */
        if ((timeout--) == 0) return 0xFF;
    }

    /* Clear AF flag if arised */
    CAMERA_I2C->SR1 |= (uint16_t)0x0400;
    //--------------------------------------------------------

    /* Generate the Start Condition */
    I2C_GenerateSTART(CAMERA_I2C, ENABLE);

    /* Test on I2C1 EV6 and clear it */
    timeout = DCMI_TIMEOUT_MAX; /* Initialize timeout value */
    while (!I2C_CheckEvent(CAMERA_I2C, I2C_EVENT_MASTER_MODE_SELECT)) {
        /* If the timeout delay is exeeded, exit with error code */
        if ((timeout--) == 0) return 0xFF;
    }

    /* Send DCMI selcted device slave Address for write */
    I2C_Send7bitAddress(CAMERA_I2C, OV5640_DEVICE_ADDRESS,
                        I2C_Direction_Receiver);

    /* Test on I2C1 EV6 and clear it */
    timeout = DCMI_TIMEOUT_MAX; /* Initialize timeout value */
    while (!I2C_CheckEvent(CAMERA_I2C,
        I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)) {
        /* If the timeout delay is exeeded, exit with error code */
        if ((timeout--) == 0) return 0xFF;
    }

    /* Prepare an NACK for the next data received */
    I2C_AcknowledgeConfig(CAMERA_I2C, DISABLE);

    /* Test on I2C1 EV7 and clear it */
    timeout = DCMI_TIMEOUT_MAX; /* Initialize timeout value */
    while (!I2C_CheckEvent(CAMERA_I2C, I2C_EVENT_MASTER_BYTE_RECEIVED))
            {
        /* If the timeout delay is exeeded, exit with error code */
        if ((timeout--) == 0) return 0xFF;
    }

    /* Prepare Stop after receiving data */
    I2C_GenerateSTOP(CAMERA_I2C, ENABLE);

    /* Receive the Data */
    Data = I2C_ReceiveData(CAMERA_I2C);

    /* return the read data */
    return Data;
}

在OV5640的PIDH及PIDL寄存器存储了产品ID,PIDH的默认值为0x56,PIDL的默认值为0x40。在代码中我们定义了一个结构体OV5640_IDTypeDef专门存储这些读取得的ID信息。

OV5640_ReadID函数中使用的OV5640_ReadReg函数是使用STM32的I2C外设向某寄存器读写单个字节数据的底层函数, 它与我们前面章节中用到的I2C函数差异是OV5640的寄存器地址是16位的。程序中是先发高8位地址接着发低8位地址,再读取寄存器的值。

向OV5640写入寄存器配置

检测到OV5640的存在后,向它写入配置参数,见 代码清单:OV5640-8

代码清单:OV5640-8向OV5640写入寄存器配置
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
/**
* @brief  Configures the OV5640 camera in BMP mode.
* @param  BMP ImageSize: BMP image size
* @retval None
*/
void OV5640_RGB565Config(void)
{
    uint32_t i;

    /*摄像头复位*/
    OV5640_Reset();
    /* 写入寄存器配置 */
    /* Initialize OV5640   Set to output RGB565 */
    for (i=0; i<(sizeof(RGB565_Init)/4); i++) {
        OV5640_WriteReg(RGB565_Init[i][0], RGB565_Init[i][1]);
    }

    Delay(500);

    if (img_width == 320)

        ImageFormat=BMP_320x240;

    else if (img_width == 640)

        ImageFormat=BMP_640x480;

    else if (img_width == 800)

        ImageFormat=BMP_800x480;

    switch (ImageFormat) {
    case BMP_320x240: {
        for (i=0; i<(sizeof(RGB565_QVGA)/4); i++) {
            OV5640_WriteReg(RGB565_QVGA[i][0], RGB565_QVGA[i][1]);
        }
        break;
    }
    case BMP_640x480: {
        for (i=0; i<(sizeof(RGB565_VGA)/4); i++) {
            OV5640_WriteReg(RGB565_VGA[i][0], RGB565_VGA[i][1]);
        }
        break;
    }
    case BMP_800x480: {
        for (i=0; i<(sizeof(RGB565_WVGA)/4); i++) {
            OV5640_WriteReg(RGB565_WVGA[i][0], RGB565_WVGA[i][1]);
        }
        break;
    }
    default: {
        for (i=0; i<(sizeof(RGB565_WVGA)/4); i++) {
            OV5640_WriteReg(RGB565_WVGA[i][0], RGB565_WVGA[i][1]);
        }
        break;
    }
    }
}
/**
* @brief  写一字节数据到OV5640寄存器
* @param  Addr: OV5640 的寄存器地址
* @param  Data: 要写入的数据
* @retval 返回0表示写入正常,0xFF表示错误
*/
uint8_t OV5640_WriteReg(uint16_t Addr, uint8_t Data)
{
    uint32_t timeout = DCMI_TIMEOUT_MAX;

    /* Generate the Start Condition */
    I2C_GenerateSTART(CAMERA_I2C, ENABLE);

    /* Test on CAMERA_I2C EV5 and clear it */
    timeout = DCMI_TIMEOUT_MAX; /* Initialize timeout value */
    while (!I2C_CheckEvent(CAMERA_I2C, I2C_EVENT_MASTER_MODE_SELECT)) {
        /* If the timeout delay is exeeded, exit with error code */
        if ((timeout--) == 0) return 0xFF;
    }

    /* Send DCMI selcted device slave Address for write */
    I2C_Send7bitAddress(CAMERA_I2C, OV5640_DEVICE_ADDRESS,
                        I2C_Direction_Transmitter);

    /* Test on CAMERA_I2C EV6 and clear it */
    timeout = DCMI_TIMEOUT_MAX; /* Initialize timeout value */
    while (!I2C_CheckEvent(CAMERA_I2C,
        I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)) {
        /* If the timeout delay is exeeded, exit with error code */
        if ((timeout--) == 0) return 0xFF;
    }

    /* Send CAMERA_I2C location address MSB */
    I2C_SendData(CAMERA_I2C, (uint8_t)( (Addr >> 8) & 0xFF) );

    /* Test on CAMERA_I2C EV8 and clear it */
    timeout = DCMI_TIMEOUT_MAX; /* Initialize timeout value */
    while (!I2C_CheckEvent(CAMERA_I2C,
        I2C_EVENT_MASTER_BYTE_TRANSMITTED)) {
        /* If the timeout delay is exeeded, exit with error code */
        if ((timeout--) == 0) return 0xFF;
    }
    //--------------------------------------------------------
    /* Send I2C1 location address LSB */
    I2C_SendData( CAMERA_I2C, (uint8_t)(Addr & 0xFF) );
    /* Test on I2C1 EV8 and clear it */
    timeout = DCMI_TIMEOUT_MAX; /* Initialize timeout value */
    while (!I2C_CheckEvent(CAMERA_I2C,
            I2C_EVENT_MASTER_BYTE_TRANSMITTED)) {
        /* If the timeout delay is exeeded, exit with error code */
        if ((timeout--) == 0) return 0xFF;
    }


    //--------------------------------------------------------

    /* Send Data */
    I2C_SendData(CAMERA_I2C, Data);

    /* Test on CAMERA_I2C EV8 and clear it */
    timeout = DCMI_TIMEOUT_MAX; /* Initialize timeout value */
    while (!I2C_CheckEvent(CAMERA_I2C,
            I2C_EVENT_MASTER_BYTE_TRANSMITTED)) {
        /* If the timeout delay is exeeded, exit with error code */
        if ((timeout--) == 0) return 0xFF;
    }

    /* Send I2C1 STOP Condition */
    I2C_GenerateSTOP(CAMERA_I2C, ENABLE);

    /* If operation is OK, return 0 */
    return 0;
}

这个OV5640_RGB565Config函数直接把一个初始化的二维数组RGB565_Init和一个分辨率设置的二维数组RGB565_WVGA(分辨率决定)使用I2C传输到OV5640中, 二维数组的第一维存储的是寄存器地址,第二维存储的是对应寄存器要写入的控制参数。OV5640_WriteReg 函数中,因为OV5640的寄存器地址为16位, 所以写寄存器的时候会先写入高8位的地址接着写入低8位的地址,然后再写入寄存器的值,这个是有别于普通的I2C设备的写入方式,需要特别注意。

如果您对这些寄存器配置感兴趣,可以一个个对着OV5640的寄存器说明来阅读,这些配置主要是把OV5640配置成了WVGA时序模式, 并使用8根数据线输出格式为RGB565的图像数据。我们参考《OV5640_自动对焦照相模组应用指南(DVP_接口)__R2.13C.pdf》文档中第20页4.1.3节的800x480预览的寄存器参数进行配置。 使摄像头输出为WVGA模式。

../_images/DCMIOV0201.png

初始化OV5640自动对焦功能

写入OV5640的配置参数后,需要向它写入自动对焦固件,初始化自动对焦功能,才能使用自动对焦功能,见 代码清单:OV5640-9

代码清单:OV5640-9 初始化OV5640自动对焦功能
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
void OV5640_AUTO_FOCUS(void)
{
    OV5640_FOCUS_AD5820_Init();
    OV5640_FOCUS_AD5820_Constant_Focus();
}
static void OV5640_FOCUS_AD5820_Init(void)
{
    u8  state=0x8F;
    u32 iteration = 100;
    u16 totalCnt = 0;

    CAMERA_DEBUG("OV5640_FOCUS_AD5820_Init\n");

    OV5640_WriteReg(0x3000, 0x20);
    totalCnt = sizeof(OV5640_AF_FW);
    CAMERA_DEBUG("Total Count = %d\n", totalCnt);

    //  写入自动对焦固件 Brust mode
    OV5640_WriteFW(OV5640_AF_FW,totalCnt);

    OV5640_WriteReg(0x3022, 0x00);
    OV5640_WriteReg(0x3023, 0x00);
    OV5640_WriteReg(0x3024, 0x00);
    OV5640_WriteReg(0x3025, 0x00);
    OV5640_WriteReg(0x3026, 0x00);
    OV5640_WriteReg(0x3027, 0x00);
    OV5640_WriteReg(0x3028, 0x00);
    OV5640_WriteReg(0x3029, 0xFF);
    OV5640_WriteReg(0x3000, 0x00);
    OV5640_WriteReg(0x3004, 0xFF);
    OV5640_WriteReg(0x0000, 0x00);
    OV5640_WriteReg(0x0000, 0x00);
    OV5640_WriteReg(0x0000, 0x00);
    OV5640_WriteReg(0x0000, 0x00);

    do {
        state = (u8)OV5640_ReadReg(0x3029);
        CAMERA_DEBUG("when init af, state=0x%x\n",state);

        Delay(10);
        if (iteration-- == 0) {
            CAMERA_DEBUG("[OV5640]STA_FOCUS state check ERROR!!,state=0x%x\n",state);
            break;
        }
    } while (state!=0x70);

    OV5640_FOCUS_AD5820_Check_MCU();
    return;
}   /*  OV5640_FOCUS_AD5820_Init  */

//set constant focus
void OV5640_FOCUS_AD5820_Constant_Focus(void)
{
    u8 state = 0x8F;
    u32 iteration = 300;
    //send constant focus mode command to firmware
    OV5640_WriteReg(0x3023,0x01);
    OV5640_WriteReg(0x3022,0x04);

    iteration = 5000;
    do {
        state = (u8)OV5640_ReadReg(0x3023);
        if (iteration-- == 0) {
            CAMERA_DEBUG("[OV5640]AD5820_Single_Focus time out !!%x\n",state);
            return ;
        }
        Delay(10);
    } while (state!=0x00); //0x0 : focused 0x01: is focusing
    return;
}

OV5640_AUTO_FOCUS函数调用了OV5640_FOCUS_AD5820_Init函数和OV5640_FOCUS_AD5820_Constant_Focus函数,我们先来介绍OV5640_FOCUS_AD5820_Init函数, 首先复位OV5640内部的MCU,然后通过I2C的突发模式写入自动对焦固件,突发模式就是只需要写入首地址,接着就一直写数据,这个过程地址会自增, 直接写完数据位置,对于连续地址写入相当方便。写入固件之后OV5640内部MCU开始初始化,最后检查初始化完成的状态是否为0x70,如果是就代表固件已经写入成功, 并初始化成功。接着,我们需要OV5640_FOCUS_AD5820_Constant_Focus函数来调用自动对焦固件中的持续对焦指令,完成以上步骤后,摄像头就已经初始化完毕。

main函数

最后我们来编写main函数,利用前面讲解的函数,控制采集图像,见 代码清单:OV5640-10

代码清单:OV5640-10 main函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/**
* @brief  主函数
* @param  无
* @retval 无
*/
int main(void)
{

    /*摄像头与RGB LED灯共用引脚,不要同时使用LED和摄像头*/
    Debug_USART_Config();

    /*初始化液晶屏*/
    LCD_Init();
    LCD_LayerInit();
    LTDC_Cmd(ENABLE);

    /*把背景层刷黑色*/
    LCD_SetLayer(LCD_BACKGROUND_LAYER);
    LCD_SetTransparency(0xFF);
    LCD_Clear(LCD_COLOR_BLACK);

    /*初始化后默认使用前景层*/
    LCD_SetLayer(LCD_FOREGROUND_LAYER);
    /*默认设置不透明 ,该函数参数为不透明度,范围 0-0xff ,
    0为全透明,0xff为不透明*/
    LCD_SetTransparency(0xFF);
    LCD_Clear(TRANSPARENCY);

    LCD_SetColors(LCD_COLOR_RED,TRANSPARENCY);

    LCD_ClearLine(LINE(18));
    LCD_DisplayStringLine_EN_CH(LINE(18),(uint8_t* )" 模式:UXGA 800x480");

    CAMERA_DEBUG("STM32F429 DCMI 驱动OV5640例程");

    /* 初始化摄像头GPIO及IIC */
    OV5640_HW_Init();

    /* 读取摄像头芯片ID,确定摄像头正常连接 */
    OV5640_ReadID(&OV5640_Camera_ID);

    if (OV5640_Camera_ID.PIDH  == 0x56)
    {
        CAMERA_DEBUG("%x %x",OV5640_Camera_ID.PIDH,OV5640_Camera_ID.PIDL);

    }
    else
    {
        LCD_SetTextColor(LCD_COLOR_RED);
    LCD_DisplayStringLine_EN_CH(LINE(0),(uint8_t*) "没有检测到OV5640,请重新检查连接。");
        CAMERA_DEBUG("没有检测到OV5640摄像头,请重新检查连接。");
        while (1);
    }

    OV5640_Init();
    OV5640_RGB565Config();
    OV5640_AUTO_FOCUS();
    //使能DCMI采集数据
    DCMI_Cmd(ENABLE);
    DCMI_CaptureCmd(ENABLE);

    /*DMA直接传输摄像头数据到LCD屏幕显示*/
    while (1)
    {
    }
}

在main函数中,首先初始化了液晶屏,注意它是把摄像头使用的液晶层初始化成RGB565格式了,可直接在工程的液晶底层驱动解这方面的内容。

摄像头控制部分,首先调用了OV5640_HW_Init函数初始化DCMI及I2C,然后调用OV5640_ReadID函数检测摄像头与实验板是否正常连接, 若连接正常则调用OV5640_Init函数初始化DCMI的工作模式及DMA,再调用OV5640_RGB565Config函数向OV5640写入寄存器配置, 再调用OV5640_AUTO_FOCUS函数初始化OV5640自动对焦功能,最后,一定要记住调用库函数DCMI_Cmd及DCMI_CaptureCmd函数使能DCMI开始捕获数据,这样才能正常开始工作。

49.5.2.3. 下载验证

把OV5640接到实验板的摄像头接口中,用USB线连接开发板,编译程序下载到实验板,并上电复位,液晶屏会显示摄像头采集得的图像,摄像头会自动调焦。