STM32 的处理速度比传统的8、16 位机快得多,所以使用它驱动摄像头采集图像信息并进行基本的加工处理非常适合,本章讲解使用STM32 驱动OV7725 型号的摄像头。
在各类信息中,图像含有最丰富的信息,作为机器视觉领域的核心部件,摄像头被广泛地应用在安防、探险以及车牌检测等场合。摄像头按输出信号的类型来看可以分为数字摄像头和模拟摄像头,按照摄像头图像传感器材料构成来看可以分为CCD 和CMOS。现在智能手机的摄像头绝大部分都是CMOS 类型的数字摄像头。
本节主要讲解实验板配套的摄像头,是一款分辨率为30w的CMOS摄像头它的实物见图,该摄像头主要由镜头、图像传感器、板载电路、FIFO 缓存及下方的信号引脚组成。
镜头部件包含一个镜头座和一个可旋转调节距离的凸透镜,通过旋转可以调节焦距,正常使用时,镜头座覆盖在电路板上遮光,光线只能经过镜头传输到正中央的图像传感器,它采集光线信号,采集得的数据被缓存到摄像头背面的FIFO缓存中,然后外部器件通过下方的信号引脚获取拍摄得到的图像数据。
若拆开摄像头座,在摄像头的正下方可看到PCB 板上的一个方形器件,它是摄像头的核心部件,型号为OV7725 的CMOS 类型数字图像传感器。该传感器支持输出最大为30 万像素的图像 (640x480 分辨率),它的体积小,工作电压低,支持使用VGA 时序输出图像数据,输出图像的数据格式支持YUV(422/420)、YCbCr422 以及RGB565 格式。它还可以对采集得的图像进行补偿,支持伽玛曲线、白平衡、饱和度、色度等基础处理。
OV7725 传感器采用BGA 封装,它的前端是采光窗口,引脚都在背面引出,引脚的分布见图
图中的非彩色部分是电源相关的引脚,彩色部分是主要的信号引脚,其介绍如表
(1)控制寄存器
标号①处的是OV7725 的控制寄存器,它根据这些寄存器配置的参数来运行,而这些参数是由外部控制器通过SCL 和SDA 引脚写入的,SCL 与SDA 使用的通讯协议SCCB 跟I2C 十分类似,在STM32 中我们完全可以直接用I2C 硬件外设来控制。
(2)通信、控制信号及时钟
标号②处包含了OV7725的通信、控制信号及外部时钟,其中PCLK、HREF及VSYNC分别是像素同步时钟、行同步信号以及帧同步信号,这与液晶屏控制中的VGA 信号是很类似的。RSTB 引脚为低电平时,用于复位整个传感器芯片,PWDN 用于控制芯片进入低功耗模式。注意最后的一个XCLK 引脚,它跟PCLK是完全不同的,XCLK 是用于驱动整个传感器芯片的时钟信号,是外部输入到OV7725 的信号;而PCLK 是OV7725 输出数据时的同步信号,它是由OV7725 输出的信号。XCLK 可以外接晶振或由外部控制器提供,若要类比XCLK 之于OV7725 就相当于HSE 时钟输入引脚与STM32 芯片的关系,PCLK 引脚可类比STM32 的I2C 外设的SCL 引脚。
(3) 感光矩阵
标号③处的是感光矩阵,光信号在这里转化成电信号,经过各种处理,这些信号存储成由一个个像素点表示的数字图像。
(4) 数据输出信号
标号④处包含了DSP 处理单元,它会根据控制寄存器的配置做一些基本的图像处理运算。这部分还包含了图像格式转换单元及压缩单元,转换出的数据最终通过D0-D9 引脚输出,一般来说我们使用8 根据数据线来传输,这时仅使用D2-D9 引脚。
外部控制器对OV7725 寄存器的配置参数是通过SCCB 总线传输过去的,而SCCB 总线跟I2C十分类似,所以在STM32驱动中可以直接使用片上I2C外设与它通讯,SCCB可以完全当成I2C使用。关于SCCB协议的完整内容可查看配套资料里的《SCCB 协议》文档,下面进行简单介绍。
SCCB的起始信号、停止信号及数据有效性与I2C完全一样,见下图
在SCCB协议中定义的读写操作与I2C也是一样的,只是换了一种说法。它定义了两种写操作,即三步写操作和两步写操作。三步写操作可向设备的一个目的寄存器写入数据,见下图。
在三步写操作中,第一阶段发送从设备的ID地址+W标志(等于I2C的设备地址:7位设备地址+读写方向标志),第二阶段发送从设备目标寄存器的8位地址,第三阶段发送要写入寄存器的8位数据。图中“X”数据可写入1或者0,对通讯无影响。
而两步写操作没有第三阶段,即只向从器件传输了设备ID+W标志和目的寄存器的地址,见下图,两步写操作是用来配合后面读寄存器数据操作的,它与读操作一起使用,实现I2C的复合过程。
两步读操作,它用于读取从设备目的寄存器中的数据,见图 51-8。在第一阶段中发送从设备的设备ID+R标志(设备地址+读方向标志)和自由位,在第二阶段中读取寄存器中的8位数据和写NA 位(非应答信号)。由于两步读操作没有确定目的寄存器的地址,所以在读操作前,必需有一个两步写操作,以提供读操作中的寄存器地址。
可以看到,以上介绍的SCCB 特性都与I2C 无区别,完全可以使用STM32 的I2C 外设来与OV7725 进行SCCB 通讯。
控制OV7725 涉及到它很多的寄存器,可直接查询《OV7725datasheet》了解,通过这些寄存器的配置,可以控制它输出图像的分辨率大小、图像格式、图像处理及图像方向等。见下图
官方还提供了一个《OV7725 Software Application Note》的文档,它针对不同的配置需求,提供了配置范例,见下图。其中write_SCCB 是一个利用SCCB 向寄存器写入数据的函数,第一个参数为要写入的寄存器的地址,第二个参数为要写入的内容。
调节帧率的寄存器配置范例
主控制器OV7725采样SCCB协议读写其寄存器,而它输出图像时则使用VGA或QVGA时序,其中VGA在输出图像分辨率为480*640时采样,QVGA 是Quarter VGA,其输出分辨率为240*320,这些时序跟控制液晶屏输出图像数据时十分类似。
OV7725传感器输出图像时,一帧帧地输出,在帧内的数据一般从左到右,从上到下,一个像素一个像素地输出(也可以通过寄存器修改方向)
例如见下图,若我们使用D2-D9数据线,图像格式设置为RGB565,进行数据输出时,D2-D9数据线在PCLK在上升沿维持稳定,并且会在1个像素同步时钟PCLK的驱动下发送1字节的数据信号,所以2个PCLK时钟可发送1个RGB565格式的像素数据。当HERF为高电平时,像素数据依次传输,每传输完一行数据时,行同步信号HERF会输出一个电平跳变信号间隔开当前行和下一行的数据;一帧的图像由N行数据组成,当VSYNC为低电平时,各行的像素数据依次传输,每传输完一帧图像时,VSYNC会输出一个电平跳变信号。
STM32F4 系列的控制器主频高、一般会扩展外部SRAM、SDRAM 等存储器,且具有DCMI 外设,可以直接根据VGA 时序接收并存储摄像头输出的图像数据;而STM32F1 系列的控制器一般主频较低、为节省成本可能不扩展SRAM 存储器,而且不具DCMI 外设,难以直接接收和存储OV7725 图像传感器输出的数据。
为了解决上述问题,针对类似STM32F1 或更低级的控制器秉火的OV7725 摄像头在图像传感器之外还添加了一个型号为AL422B 的FIFO,用于缓冲数据。AL422B 的本质是一种RAM存储器,见图 ,它的容量大小为393216 字节,支持同时写入和读出数据,这正是专门用于FIFO缓冲功能而设计的,关于它的详细说明可查阅《AL422_datasheet》文档。
AL422B 的各引脚功能介绍见表
由于AL422B支持同时写入和读出数据,所以它的输入和输出的控制信号线都是互相独立的。写入和读出数据的时序类似,跟VGA的像素输出时序一致,读写时序介绍如下:
写时序:写FIFO(AL422B,下面统称为FIFO)时序见图
在写时序中,当WE管脚为低电平的时候,FIFO写入处于使能状态,随着读时钟WCK的运转,DI[0:7]标识的数据将会按地址递增的方式存入FIFO;当WE管脚为高电平时,关闭输入,DI[0:7]的数据不会被写入FIFO。
在控制写入数据时,一般会先控制写指针作一个复位操作,把WRST设置为低电平,写指针会复位到FIFO的0地址,然后FIFO接收到的数据会从该地址开始按照自增的方式写入。
读时序,读FIFO时序见图
FIFO的读时序类似,不过读使能由两个引脚共同控制,即OE和RE引脚均为低电平时,输出处于使能状态,随着读时钟RCK的运转,在数据输出管脚DO[0:7]就会按地址递增的方式输出数据。
类似地,在控制读出数据时,一般会先控制读指针作为一个复位操作,把RRST设置为低电平,读指针会复位到FIFO的0地址,然后FIFO数据从该地址开始按自增的方式输出。
OV7725 摄像头中包含有FIFO,所以外部控制器驱动摄像头时,需要协调FIFO与OV7725传感器的院系,下面配合摄像头的原理讲解其驱动原理
原理图主要分为外部引出接口、OV7725及FIFO部分
摄像头引出的接口包含了OV7725 传感器及FIFO 的混合引脚,外部的控制器使用这些引脚即可驱动摄像头,其说明见表
与OV7725传感器像素输出相关的PCLK和D[0:7]并没有引出,因为这些引脚被连接到了FIFO的输入部分,OV7725的像素输出时序与FIFO的写入数据时序是一致的,所以在OV7725时钟PCLK的驱动下,它输出的数据会一个字节一个字节地被FIFO接收并存储起来。
其中最为特殊的是WEN引脚,它与OV7725的HREF连接到一个与非门的输入,与非门的输出连接到FIFO的WE引脚,因此,当WEN与HREF均为高电平时,FIFO的WE为低电平,此时允许OV7725向FIFO写入数据。
外部控制器通过控制WEN引脚,可防止OV7725覆盖了还未被控制器读出得旧FIFO数据。另外,在OV7725输出时序中,只有当HERF为高电平时,PCLK驱动下D[0:7]线表示的才是有效像素数据,因此,利用HERF控制FIFO的WE可以确保只有有效数据才被写入到FIFO中
配合摄像头的原理图、OV7725、FIFO时序图,可总结出摄像头采集数据的过程如下:
将OV7725配置为240*320的分辨率(QVGA),RGB565格式,那么OV7725输出一帧的图像大小为240*320*2=153600字节,而本摄像头采用FIFO型号AL422B容量为393216字节,最多可以缓存2帧这样的图像,通过这样的方式,STM32无需直接处理OV7725高速输出的数据,但是配置OV7725为480*640分辨率时,其一帧图像的大小为480*640*2=614400字节,FIFO的容量不足以直接存储这样的图像,因此当OV7725往FIFO写入数据的时候,STM32端要同时读取数据,确保在OV7725覆盖旧数据之前,STM32端已经把这部分数据读取出来了。
因为本身STM32内部的SRAM是存储不了一帧的数据的,所以我们可以一行一行的读,(读一行是480字节),读一行之后,可以把这一行数据通过STM32的FSMC把数据传输到液晶屏的IIL9341,然后液晶屏就可以一行一行的写入了。
摄像头的整个控制过程见图
使用本摄像头时,我们一般配套开发板的液晶屏,把OV7725 配置为240*320 分辨率(QVGA),RGB565 格式,那么OV7725 输出一帧的图像大小为240*320*2=153600 字节,而本摄像头采用的FIFO 型号AL422B 容量为393216 字节,最多可以缓存2 帧这样的图像,通过这样的方式,STM32 无需直接处理OV7725 高速输出的数据。但是,如果配置OV7725 为480*640 分辨率时,其一帧图像大小为480*640*2=614400 字节,FIFO 的容量不足以直接存储一帧这样的图像,因此,当OV7725 往FIFO写数据的时候,STM32端要同时读取数据,确保在OV7725 覆盖旧数据的之前,STM32 端已经把这部分数据读取出来了。
关于摄像头的原理图此处不再分析。在我们的实验板上有引出一个摄像头专用的排母,可直接与摄像头引出的引脚连,接入后它与STM32 引脚的连接关系见图
摄像头与STM32 连接关系中主要分为SCCB 控制、VGA 时序控制、FIFO 数据读取部分,介绍如下:
(1) SCCB 控制相关
摄像头中的SIO_C 和SIO_D 引脚直接连接到STM32 普通的GPIO,它们不具有硬件I2C 的功能,所以在后面的代码中采用模拟I2C 时序,实际上直接使用硬件I2C 是完全可以实现SCCB 协议的,本设计采用模拟I2C 是芯片资源分配妥协的结果。
(2) VGA 时序相关
检测VGA 时序的HREF、VSYNC 引脚,它们与STM32 连接的GPIO 均设置为输入模
式,其中HREF 在本实验中并没有使用,它已经通过摄像头内部的与非门控制了FIFO 的写
使能;VSYNC 与STM32 连接的GPIO 引脚会在程序中配置成中断模式,STM32 利用该中
断信号获知新的图像是否采集完成,从而控制FIFO 是否写使能。
(3) FIFO 相关
与FIFO 控制相关的RCLK、RRST、WRST、WEN 及OE 与STM32 连接的引脚均直接配置成推挽输出,STM32 根据图像的采集情况利用这些引脚控制FIFO;读取FIFO 数据内容使用的数据引脚DO[0:7]均连接到STM32 同一个GPIO端口连续的高8 位引脚PB[8:15],这些引脚使用时均配置成输入,程序设计中直接读取GPIO端口的高8 位状态直接获取一个字节的FIFO 内容,建议在连接这部分数据信号时,参考本设计采用同一个GPIO 端口连续的8 位(高8 位或低8 位均可),否则会导致读取数据的程序非常复杂。
(4) XCLK 信号
本设计中STM32 的摄像头接口还预留了PA8 引脚用于与摄像头的XCLK连接,STM32的PA8 可以对外输出时钟信号,所以在使用不带晶振的摄像头时,可以通过该引脚给摄像头提供时钟,秉火摄像头内部已自带晶振,在程序中没有使用PA8 引脚。以上原理图可查阅《霸道开发板—原理图》文档获知,请根据实际连接的引脚修改程序。
摄像头硬件相关宏定义
我们把摄像头控制硬件相关的配置以宏的形式定义到 “bsp_ov7725.h”及“bsp_sccb.h”文件中,其中包括VGA 部分接口、FIFO 控制及SCCB(即模拟I2C)相关的引脚,见代码
bsp_ov7725.h
/************************** OV7725 连接引脚定义********************************/
// FIFO 输出使能,即模块中的OE
#define OV7725_OE_GPIO_CLK RCC_APB2Periph_GPIOA
#define OV7725_OE_GPIO_PORT GPIOA
#define OV7725_OE_GPIO_PIN GPIO_Pin_3
// FIFO 写复位
#define OV7725_WRST_GPIO_CLK RCC_APB2Periph_GPIOC
#define OV7725_WRST_GPIO_PORT GPIOC
#define OV7725_WRST_GPIO_PIN GPIO_Pin_4
// FIFO 读复位
#define OV7725_RRST_GPIO_CLK RCC_APB2Periph_GPIOA
#define OV7725_RRST_GPIO_PORT GPIOA
#define OV7725_RRST_GPIO_PIN GPIO_Pin_2
// FIFO 读时钟
#define OV7725_RCLK_GPIO_CLK RCC_APB2Periph_GPIOC
#define OV7725_RCLK_GPIO_PORT GPIOC
#define OV7725_RCLK_GPIO_PIN GPIO_Pin_5
// FIFO 写使能
#define OV7725_WE_GPIO_CLK RCC_APB2Periph_GPIOD
#define OV7725_WE_GPIO_PORT GPIOD
#define OV7725_WE_GPIO_PIN GPIO_Pin_3
// 8位数据口
#define OV7725_DATA_GPIO_CLK RCC_APB2Periph_GPIOB
#define OV7725_DATA_GPIO_PORT GPIOB
#define OV7725_DATA_0_GPIO_PIN GPIO_Pin_8
#define OV7725_DATA_1_GPIO_PIN GPIO_Pin_9
#define OV7725_DATA_2_GPIO_PIN GPIO_Pin_10
#define OV7725_DATA_3_GPIO_PIN GPIO_Pin_11
#define OV7725_DATA_4_GPIO_PIN GPIO_Pin_12
#define OV7725_DATA_5_GPIO_PIN GPIO_Pin_13
#define OV7725_DATA_6_GPIO_PIN GPIO_Pin_14
#define OV7725_DATA_7_GPIO_PIN GPIO_Pin_15
// OV7725场中断
#define OV7725_VSYNC_GPIO_CLK RCC_APB2Periph_GPIOC
#define OV7725_VSYNC_GPIO_PORT GPIOC
#define OV7725_VSYNC_GPIO_PIN GPIO_Pin_3
#define OV7725_VSYNC_EXTI_SOURCE_PORT GPIO_PortSourceGPIOC
#define OV7725_VSYNC_EXTI_SOURCE_PIN GPIO_PinSource3
#define OV7725_VSYNC_EXTI_LINE EXTI_Line3
#define OV7725_VSYNC_EXTI_IRQ EXTI3_IRQn
#define OV7725_VSYNC_EXTI_INT_FUNCTION EXTI3_IRQHandler
#define FIFO_OE_H() OV7725_OE_GPIO_PORT->BSRR =OV7725_OE_GPIO_PIN
#define FIFO_OE_L() OV7725_OE_GPIO_PORT->BRR =OV7725_OE_GPIO_PIN /*拉低使FIFO输出使能*/
#define FIFO_WRST_H() OV7725_WRST_GPIO_PORT->BSRR =OV7725_WRST_GPIO_PIN /*拉高允许FIFO写(数据from摄像头)指针运动 */
#define FIFO_WRST_L() OV7725_WRST_GPIO_PORT->BRR =OV7725_WRST_GPIO_PIN /*拉低使FIFO写(数据from摄像头)指针复位*/
#define FIFO_RRST_H() OV7725_RRST_GPIO_PORT->BSRR =OV7725_RRST_GPIO_PIN /*拉高允许FIFO读(数据从FIFO输出)指针运动 */
#define FIFO_RRST_L() OV7725_RRST_GPIO_PORT->BRR =OV7725_RRST_GPIO_PIN /*拉低使FIFO读(数据从FIFO输出)指针复位 */
#define FIFO_RCLK_H() OV7725_RCLK_GPIO_PORT->BSRR =OV7725_RCLK_GPIO_PIN
#define FIFO_RCLK_L() OV7725_RCLK_GPIO_PORT->BRR =OV7725_RCLK_GPIO_PIN /*FIFO输出数据时钟*/
#define FIFO_WE_H() OV7725_WE_GPIO_PORT->BSRR =OV7725_WE_GPIO_PIN /*拉高使FIFO写允许*/
#define FIFO_WE_L() OV7725_WE_GPIO_PORT->BRR =OV7725_WE_GPIO_PIN
bsp_sccb.h
#ifndef __SCCB_H
#define __SCCB_H
#include "stm32f10x.h"
/************************** OV7725 连接引脚定义********************************/
#define OV7725_SIO_C_SCK_APBxClock_FUN RCC_APB2PeriphClockCmd
#define OV7725_SIO_C_GPIO_CLK RCC_APB2Periph_GPIOC
#define OV7725_SIO_C_GPIO_PORT GPIOC
#define OV7725_SIO_C_GPIO_PIN GPIO_Pin_6
#define OV7725_SIO_D_SCK_APBxClock_FUN RCC_APB2PeriphClockCmd
#define OV7725_SIO_D_GPIO_CLK RCC_APB2Periph_GPIOC
#define OV7725_SIO_D_GPIO_PORT GPIOC
#define OV7725_SIO_D_GPIO_PIN GPIO_Pin_7
#define SCL_H GPIO_SetBits(OV7725_SIO_C_GPIO_PORT , OV7725_SIO_C_GPIO_PIN)
#define SCL_L GPIO_ResetBits(OV7725_SIO_C_GPIO_PORT , OV7725_SIO_C_GPIO_PIN)
#define SDA_H GPIO_SetBits(OV7725_SIO_D_GPIO_PORT , OV7725_SIO_D_GPIO_PIN)
#define SDA_L GPIO_ResetBits(OV7725_SIO_D_GPIO_PORT , OV7725_SIO_D_GPIO_PIN)
#define SCL_read GPIO_ReadInputDataBit(OV7725_SIO_C_GPIO_PORT , OV7725_SIO_C_GPIO_PIN)
#define SDA_read GPIO_ReadInputDataBit(OV7725_SIO_D_GPIO_PORT , OV7725_SIO_D_GPIO_PIN)
#define ADDR_OV7725 0x42
void SCCB_GPIO_Config(void);
int SCCB_WriteByte( u16 WriteAddress , u8 SendByte);
int SCCB_ReadByte(u8* pBuffer, u16 length, u8 ReadAddress);
#endif
以上的代码根据硬件的连接,使用宏封装了各种控制信号,包括控制输出电平或读取输入时使用的库函数操作。若使用STM32与摄像头的引脚连接与我们设计的不同,修改这两个文件的引脚连接关系即可。
SCCB总线的软件实现
本设计中使用普通的GPIO来模拟SCCB时序,需要根据SCCB时序,编写读、写字节的模拟函数,在后面的OV7725_Init会利用这些函数向0V7725相应的寄存器写入配置参数,初始化摄像头。本小结介绍的与SCCB时序相关函数都位于bsp_sccb.c文件中,这些函数跟模拟I2C的基本一致。
①初始化SCCB用到的GPIO
在本实验中,使用SCCB_GPIO_Config函数初始化SCCB使用的两个通讯引脚,把他们设置为普通的开漏输出模式,见代码
/********************************************************************
* 函数名:SCCB_Configuration
* 描述 :SCCB管脚配置
* 输入 :无
* 输出 :无
* 注意 :无
********************************************************************/
void SCCB_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/* SCL(PC6)、SDA(PC7)管脚配置 */
OV7725_SIO_C_SCK_APBxClock_FUN ( OV7725_SIO_C_GPIO_CLK, ENABLE );
GPIO_InitStructure.GPIO_Pin = OV7725_SIO_C_GPIO_PIN ;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_Init(OV7725_SIO_C_GPIO_PORT, &GPIO_InitStructure);
OV7725_SIO_D_SCK_APBxClock_FUN ( OV7725_SIO_D_GPIO_CLK, ENABLE );
GPIO_InitStructure.GPIO_Pin = OV7725_SIO_D_GPIO_PIN ;
GPIO_Init(OV7725_SIO_D_GPIO_PORT, &GPIO_InitStructure);
}
②SCCB起始与结束时序
SCCB通讯需要有起始信号与结束信号,这分别由SCCB_Start 和SCCB_Stop 函数实现。
SCCB_Start 函数
/********************************************************************
* 函数名:SCCB_Start
* 描述 :SCCB起始信号
* 输入 :无
* 输出 :无
* 注意 :内部调用
********************************************************************/
static int SCCB_Start(void)
{
SDA_H;
SCL_H;
SCCB_delay();
if(!SDA_read)
return DISABLE; /* SDA线为低电平则总线忙,退出 */
SDA_L;
SCCB_delay();
if(SDA_read)
return DISABLE; /* SDA线为高电平则总线出错,退出 */
SDA_L;
SCCB_delay();
return ENABLE;
}
参照前面介绍的SCCB时序,当SCL为高电平时,SDA出现下降沿,表示SCCB时序的起始信号,SCCB_Start函数就是实现了这样的功能,其中SDA_H和SDA_H是用于控制SDA和SCL引脚电平的宏。为了提高程序的健壮性,还使用SDA_read 宏检测SDA线是否忙碌或是否正常。
SCCB 结束信号的函数实现类似,其SCCB_Stop 代码见代码清单
/********************************************************************
* 函数名:SCCB_Stop
* 描述 :SCCB停止信号
* 输入 :无
* 输出 :无
* 注意 :内部调用
********************************************************************/
static void SCCB_Stop(void)
{
SCL_L;
SCCB_delay();
SDA_L;
SCCB_delay();
SCL_H;
SCCB_delay();
SDA_H;
SCCB_delay();
}
参照前面SCCB时序的介绍,当SCL线为高电平的时候,使SDA线出现一个上升沿,表示SCCB 时序的结束信号。
③写寄存器与读寄存器
与I2C时序类似,在SCCB时序也使用自由位位(Don’t care bit )和非应答(NA)信号来保证正常通讯。自由位和非应答信号位于SCCB每个传输阶段的第九位。
在写数据据的第一个传输阶段中,第9位为自由位,在一般的正常通讯中,第9位时,主机的SDA线输出高电平,而从机把SDA线拉低作为响应,第二三阶段类似,只是传输的内容分别位目的寄存器地址和写入的数据。
写操作第一阶段(传输器件地址)
因此,在数据传输的第9 位,主机使用SCCB_WaitAck 函数来等待从机的应答,见代码
SCCB_WaitAck 函数
/********************************************************************
* 函数名:SCCB_WaitAck
* 描述 :SCCB 等待应答
* 输入 :无
* 输出 :返回为:=1有ACK,=0无ACK
* 注意 :内部调用
********************************************************************/
static int SCCB_WaitAck(void)
{
SCL_L;
SCCB_delay();
SDA_H;
SCCB_delay();
SCL_H;
SCCB_delay();
if(SDA_read)
{
SCL_L;
return DISABLE;
}
SCL_L;
return ENABLE;
}
该函数让主机把SDA线设为高电平,延时一段时间后再检测SDA线的电平,若为低则返回ENABLE表示接收到从机的应答,反之返回DISABLE。
最后,整个三相写过程由函数SCCB_WriteByte实现,它的具体见代码
SCCB_WriteByte 函数
/*****************************************************************************************
* 函数名:SCCB_WriteByte
* 描述 :写一字节数据
* 输入 :- WriteAddress: 待写入地址 - SendByte: 待写入数据 - DeviceAddress: 器件类型
* 输出 :返回为:=1成功写入,=0失败
* 注意 :无
*****************************************************************************************/
int SCCB_WriteByte( uint16_t WriteAddress , uint8_t SendByte )
{
if(!SCCB_Start())
{
return DISABLE;
}
SCCB_SendByte( DEV_ADR ); /* 器件地址 */
if( !SCCB_WaitAck() )
{
SCCB_Stop();
return DISABLE;
}
SCCB_SendByte((uint8_t)(WriteAddress & 0x00FF)); /* 设置低起始地址 */
SCCB_WaitAck();
SCCB_SendByte(SendByte);
SCCB_WaitAck();
SCCB_Stop();
return ENABLE;
}
SCCB_WriteByte 函数调用SCCB_Start 产生起始信号,接着调用SCCB_SendByte 把器件地址DEV_ADR(这是一个宏,数值是0x42)一位一位地发送出去,在第9 位时,调用SCCB_WaitAck 函数检测从机的应答,若接收到应答则进入第二阶段——发送目的寄存器地址,再进入第三阶段——发送要写入的内容。在第二、三阶段没有加条件判断语句判断是否接收到从机的应答,这是因为SCCB 规定在数据传输阶段允许从机不应答(实际上,OV7725 芯片在这两个阶段都会有应答讯号 。在最后,三阶段都传输结束时,要调用SCCB_Stop 函数结束本次SCCB 传输。
与自由位相对应的非应答信号用在两相读操作的第二阶段的第9 位,见图 51-23。在这第9 位中,从机把SDA 线置为高电平,而主机把SDA 线拉低表示非应答,接着本次读数据的操作就结束了。
主机的非应答信号,由SCCB_NoAck 函数实现,
/********************************************************************
* 函数名:SCCB_NoAck
* 描述 :SCCB 无应答方式
* 输入 :无
* 输出 :无
* 注意 :内部调用
********************************************************************/
static void SCCB_NoAck(void)
{
SCL_L;
SCCB_delay();
SDA_H;
SCCB_delay();
SCL_H;
SCCB_delay();
SCL_L;
SCCB_delay();
}
最后,整个读寄存器的过程由SCCB_ReadByte 函数完成,其代码见代码
SCCB_ReadByte 函数
/******************************************************************************************************************
* 函数名:SCCB_ReadByte
* 描述 :读取一串数据
* 输入 :- pBuffer: 存放读出数据 - length: 待读出长度 - ReadAddress: 待读出地址 - DeviceAddress: 器件类型
* 输出 :返回为:=1成功读入,=0失败
* 注意 :无
**********************************************************************************************************************/
int SCCB_ReadByte(uint8_t* pBuffer, uint16_t length, uint8_t ReadAddress)
{
if(!SCCB_Start())
{
return DISABLE;
}
SCCB_SendByte( DEV_ADR ); /* 器件地址 */
if( !SCCB_WaitAck() )
{
SCCB_Stop();
return DISABLE;
}
SCCB_SendByte( ReadAddress ); /* 设置低起始地址 */
SCCB_WaitAck();
SCCB_Stop();
if(!SCCB_Start())
{
return DISABLE;
}
SCCB_SendByte( DEV_ADR + 1 ); /* 器件地址 */
if(!SCCB_WaitAck())
{
SCCB_Stop();
return DISABLE;
}
while(length)
{
*pBuffer = SCCB_ReceiveByte();
if(length == 1)
{
SCCB_NoAck();
}
else
{
SCCB_Ack();
}
pBuffer++;
length--;
}
SCCB_Stop();
return ENABLE;
}
本函数在两相读操作前,加入了一个两相写操作,用于向从机发送要读取的寄存器地址。在两相写操作的第一个阶段(第26 行),使用SCCB_SendByte 函数发送的数据是DEV_ADR+1 (即0x43),这与写操作中发送的DEV_ADR 有区别,这是因为在第一阶段发送的这个器件地址的最低位是用于表示数据传送方向的,最低位为0 时表示主机写数据,最低位为1 时表示主机读数据,所以DEV_ADR+1 就表示读数据了。读操作的第二阶段使用SCCB_ReceiveByte 函数,一位一位地接收数据,然后存放到PBuffer 指向的单元中的。接收完8 位数据后,主机调用SCCB_NoAck 发送非应答位,最后调用SCCB_Stop 结束本次读操作。
初始化OV7725
在上一小节编写了模拟SCCB 的读写寄存器的时序后,就可以向OV7725 的寄存器发送配置参数,对OV7725 进行初始化了。该过程由bsp_ov7725.c 文件中的OV7725_Init 函数完成
/************************************************
* 函数名:Sensor_Init
* 描述 :Sensor初始化
* 输入 :无
* 输出 :返回1成功,返回0失败
* 注意 :无
************************************************/
ErrorStatus OV7725_Init(void)
{
uint16_t i = 0;
uint8_t Sensor_IDCode = 0;
//DEBUG("ov7725 Register Config Start......");
if( 0 == SCCB_WriteByte ( 0x12, 0x80 ) ) /*复位sensor */
{
//DEBUG("sccb write data error");
return ERROR ;
}
if( 0 == SCCB_ReadByte( &Sensor_IDCode, 1, 0x0b ) ) /* 读取sensor ID号*/
{
//DEBUG("read id faild");
return ERROR;
}
//DEBUG("Sensor ID is 0x%x", Sensor_IDCode);
if(Sensor_IDCode == OV7725_ID)
{
for( i = 0 ; i < OV7725_REG_NUM ; i++ )
{
if( 0 == SCCB_WriteByte(Sensor_Config[i].Address, Sensor_Config[i].Value) )
{
//DEBUG("write reg faild", Sensor_Config[i].Address);
return ERROR;
}
}
}
else
{
return ERROR;
}
//DEBUG("ov7725 Register Config Success");
return SUCCESS;
}
这个函数的执行流程如下:
(1)调用SCCB_WriteByte向地址为0x12的寄存器写入数据0x80,进行复位操作,根据OV7725的数据手册,把该寄存器的位置1,可控制它对寄存器进行复位。
(2)调用SCCB_ReadByte函数从地址为0x0b的寄存器读取出OV7725芯片的ID号,并与默认值进行对比,这个操作可以确保OV7725是否正常工作。
(3) 利用for 语句循环调用SCCB_WriteByte 函数,向各个寄存器写入配置参数,其中
SCCB_WriteByte 的输入参数为Sensor_Config[i].Address 和Sensor_Config[i].Value,这是自定义的结构体数组,分别对应于要配置的寄存器地址和寄存器配置参数。Sensor_Config 数组是在bsp_ov7725.c 文件定义的,文件中首先定义了Reg_Info 结构体类型,它包含地址和寄存器两个结构体成员
Register_Info 结构体
再利用这个自定义的结构体,定义结构体数组,每组的内容就表示了寄存器地址及相应的配置参数。若要修改对OV7725的配置,可参考OV7725数据手册的说明,修改相应地址的内容即可, ,SCCB_WriteByte 函数会在for 循环中把这些参数写入OV7725 芯片,下面是结构体数组的部分代码
/* 寄存器宏定义 */
#define REG_GAIN 0x00
#define REG_BLUE 0x01
#define REG_RED 0x02
#define REG_GREEN 0x03
#define REG_BAVG 0x05
#define REG_GAVG 0x06
#define REG_RAVG 0x07
#define REG_AECH 0x08
#define REG_COM2 0x09
#define REG_PID 0x0A
#define REG_VER 0x0B
#define REG_COM3 0x0C
#define REG_COM4 0x0D
#define REG_COM5 0x0E
#define REG_COM6 0x0F
#define REG_AEC 0x10
#define REG_CLKRC 0x11
#define REG_COM7 0x12
#define REG_COM8 0x13
#define REG_COM9 0x14
#define REG_COM10 0x15
#define REG_REG16 0x16
#define REG_HSTART 0x17
#define REG_HSIZE 0x18
#define REG_VSTRT 0x19
#define REG_VSIZE 0x1A
#define REG_PSHFT 0x1B
#define REG_MIDH 0x1C
#define REG_MIDL 0x1D
#define REG_LAEC 0x1F
#define REG_COM11 0x20
#define REG_BDBase 0x22
#define REG_BDMStep 0x23
#define REG_AEW 0x24
#define REG_AEB 0x25
#define REG_VPT 0x26
#define REG_REG28 0x28
#define REG_HOutSize 0x29
#define REG_EXHCH 0x2A
#define REG_EXHCL 0x2B
#define REG_VOutSize 0x2C
#define REG_ADVFL 0x2D
#define REG_ADVFH 0x2E
#define REG_YAVE 0x2F
#define REG_LumHTh 0x30
#define REG_LumLTh 0x31
#define REG_HREF 0x32
#define REG_DM_LNL 0x33
#define REG_DM_LNH 0x34
#define REG_ADoff_B 0x35
#define REG_ADoff_R 0x36
#define REG_ADoff_Gb 0x37
#define REG_ADoff_Gr 0x38
#define REG_Off_B 0x39
#define REG_Off_R 0x3A
#define REG_Off_Gb 0x3B
#define REG_Off_Gr 0x3C
#define REG_COM12 0x3D
#define REG_COM13 0x3E
#define REG_COM14 0x3F
#define REG_COM16 0x41
#define REG_TGT_B 0x42
#define REG_TGT_R 0x43
#define REG_TGT_Gb 0x44
#define REG_TGT_Gr 0x45
#define REG_LC_CTR 0x46
#define REG_LC_XC 0x47
#define REG_LC_YC 0x48
#define REG_LC_COEF 0x49
#define REG_LC_RADI 0x4A
#define REG_LC_COEFB 0x4B
#define REG_LC_COEFR 0x4C
#define REG_FixGain 0x4D
#define REG_AREF1 0x4F
#define REG_AREF6 0x54
#define REG_UFix 0x60
#define REG_VFix 0x61
#define REG_AWBb_blk 0x62
#define REG_AWB_Ctrl0 0x63
#define REG_DSP_Ctrl1 0x64
#define REG_DSP_Ctrl2 0x65
#define REG_DSP_Ctrl3 0x66
#define REG_DSP_Ctrl4 0x67
#define REG_AWB_bias 0x68
#define REG_AWBCtrl1 0x69
#define REG_AWBCtrl2 0x6A
#define REG_AWBCtrl3 0x6B
#define REG_AWBCtrl4 0x6C
#define REG_AWBCtrl5 0x6D
#define REG_AWBCtrl6 0x6E
#define REG_AWBCtrl7 0x6F
#define REG_AWBCtrl8 0x70
#define REG_AWBCtrl9 0x71
#define REG_AWBCtrl10 0x72
#define REG_AWBCtrl11 0x73
#define REG_AWBCtrl12 0x74
#define REG_AWBCtrl13 0x75
#define REG_AWBCtrl14 0x76
#define REG_AWBCtrl15 0x77
#define REG_AWBCtrl16 0x78
#define REG_AWBCtrl17 0x79
#define REG_AWBCtrl18 0x7A
#define REG_AWBCtrl19 0x7B
#define REG_AWBCtrl20 0x7C
#define REG_AWBCtrl21 0x7D
#define REG_GAM1 0x7E
#define REG_GAM2 0x7F
#define REG_GAM3 0x80
#define REG_GAM4 0x81
#define REG_GAM5 0x82
#define REG_GAM6 0x83
#define REG_GAM7 0x84
#define REG_GAM8 0x85
#define REG_GAM9 0x86
#define REG_GAM10 0x87
#define REG_GAM11 0x88
#define REG_GAM12 0x89
#define REG_GAM13 0x8A
#define REG_GAM14 0x8B
#define REG_GAM15 0x8C
#define REG_SLOP 0x8D
#define REG_DNSTh 0x8E
#define REG_EDGE0 0x8F
#define REG_EDGE1 0x90
#define REG_DNSOff 0x91
#define REG_EDGE2 0x92
#define REG_EDGE3 0x93
#define REG_MTX1 0x94
#define REG_MTX2 0x95
#define REG_MTX3 0x96
#define REG_MTX4 0x97
#define REG_MTX5 0x98
#define REG_MTX6 0x99
#define REG_MTX_Ctrl 0x9A
#define REG_BRIGHT 0x9B
#define REG_CNST 0x9C
#define REG_UVADJ0 0x9E
#define REG_UVADJ1 0x9F
#define REG_SCAL0 0xA0
#define REG_SCAL1 0xA1
#define REG_SCAL2 0xA2
#define REG_SDE 0xA6
#define REG_USAT 0xA7
#define REG_VSAT 0xA8
#define REG_HUECOS 0xA9
#define REG_HUESIN 0xAA
#define REG_SIGN 0xAB
#define REG_DSPAuto 0xAC
typedef struct Reg
{
uint8_t Address; /*寄存器地址*/
uint8_t Value; /*寄存器值*/
}Reg_Info;
/* 寄存器参数配置 */
Reg_Info Sensor_Config[] =
{
{REG_CLKRC, 0x00}, /*clock config*/
{REG_COM7, 0x46}, /*QVGA RGB565 */
{REG_HSTART, 0x3f},
{REG_HSIZE, 0x50},
{REG_VSTRT, 0x03},
{REG_VSIZE, 0x78},
{REG_HREF, 0x00},
{REG_HOutSize, 0x50},
{REG_VOutSize, 0x78},
{REG_EXHCH, 0x00},
/*DSP control*/
{REG_TGT_B, 0x7f},
{REG_FixGain, 0x09},
{REG_AWB_Ctrl0, 0xe0},
{REG_DSP_Ctrl1, 0xff},
{REG_DSP_Ctrl2, 0x20},
{REG_DSP_Ctrl3, 0x00},
{REG_DSP_Ctrl4, 0x00},
/*AGC AEC AWB*/
{REG_COM8, 0xf0},
{REG_COM4, 0x81}, /*Pll AEC CONFIG*/
{REG_COM6, 0xc5},
{REG_COM9, 0x21},
{REG_BDBase, 0xFF},
{REG_BDMStep, 0x01},
{REG_AEW, 0x34},
{REG_AEB, 0x3c},
{REG_VPT, 0xa1},
{REG_EXHCL, 0x00},
{REG_AWBCtrl3, 0xaa},
{REG_COM8, 0xff},
{REG_AWBCtrl1, 0x5d},
{REG_EDGE1, 0x0a},
{REG_DNSOff, 0x01},
{REG_EDGE2, 0x01},
{REG_EDGE3, 0x01},
{REG_MTX1, 0x5f},
{REG_MTX2, 0x53},
{REG_MTX3, 0x11},
{REG_MTX4, 0x1a},
{REG_MTX5, 0x3d},
{REG_MTX6, 0x5a},
{REG_MTX_Ctrl, 0x1e},
{REG_BRIGHT, 0x00},
{REG_CNST, 0x25},
{REG_USAT, 0x65},
{REG_VSAT, 0x65},
{REG_UVADJ0, 0x81},
//{REG_SDE, 0x20}, //黑白
{REG_SDE, 0x06}, //彩色 调节SDE这个寄存器还可以实现其他效果
/*GAMMA config*/
{REG_GAM1, 0x0c},
{REG_GAM2, 0x16},
{REG_GAM3, 0x2a},
{REG_GAM4, 0x4e},
{REG_GAM5, 0x61},
{REG_GAM6, 0x6f},
{REG_GAM7, 0x7b},
{REG_GAM8, 0x86},
{REG_GAM9, 0x8e},
{REG_GAM10, 0x97},
{REG_GAM11, 0xa4},
{REG_GAM12, 0xaf},
{REG_GAM13, 0xc5},
{REG_GAM14, 0xd7},
{REG_GAM15, 0xe8},
{REG_SLOP, 0x20},
{REG_HUECOS, 0x80},
{REG_HUESIN, 0x80},
{REG_DSPAuto, 0xff},
{REG_DM_LNL, 0x00},
{REG_BDBase, 0x99},
{REG_BDMStep, 0x03},
{REG_LC_RADI, 0x00},
{REG_LC_COEF, 0x13},
{REG_LC_XC, 0x08},
{REG_LC_COEFB, 0x14},
{REG_LC_COEFR, 0x17},
{REG_LC_CTR, 0x05},
{REG_COM3, 0xd0},/*Horizontal mirror image*/
/*night mode auto frame rate control*/
{REG_COM5, 0xf5}, /*在夜视环境下,自动降低帧率,保证低照度画面质量*/
//{REG_COM5, 0x31}, /*夜视环境帧率不变*/
};
采集并显示图像
OV7725初始化完成后,该芯片开始正常工作,由于OV7725采集得到的图像保存到FIFO,STM32 只需要检测摄像头模块的VSYNC输出的帧结束信号,然后从FIFO中读取图像数据即可
①初始化VSYNC 引脚
由于使用中断的方式来检测VSYNC 的信号,所以要把相应的引脚初始化并为它配置EXTI 中断,本实验使用VSYNC_GPIO_Config 函数完成该工作,见代码
/************************************************
* 函数名:VSYNC_GPIO_Config
* 描述 :OV7725 VSYNC中断相关配置
* 输入 :无
* 输出 :无
* 注意 :无
************************************************/
static void VSYNC_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
/*初始化时钟,注意中断要开AFIO*/
RCC_APB2PeriphClockCmd ( RCC_APB2Periph_AFIO|OV7725_VSYNC_GPIO_CLK, ENABLE );
/*初始化引脚*/
GPIO_InitStructure.GPIO_Pin = OV7725_VSYNC_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(OV7725_VSYNC_GPIO_PORT, &GPIO_InitStructure);
/*配置中断*/
GPIO_EXTILineConfig(OV7725_VSYNC_EXTI_SOURCE_PORT, OV7725_VSYNC_EXTI_SOURCE_PIN);
EXTI_InitStructure.EXTI_Line = OV7725_VSYNC_EXTI_LINE;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling ;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
EXTI_GenerateSWInterrupt(OV7725_VSYNC_EXTI_LINE);
/*配置优先级*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
NVIC_InitStructure.NVIC_IRQChannel = OV7725_VSYNC_EXTI_IRQ;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
代码中把VSYNC引脚配置为浮空模式,并使用下降沿中断(配置成上升沿中断也是可以的),正好对应VGA时序中VSYNC输出信号时电平跳变产生的下降沿。
(2)编写检测VSYNC的中断服务函数
由于VSYNC出现两次下降沿,才表示FIFO保存了一幅图像,所以在检测VSYNC下降沿的中断服务函数中,使用一个变量Ov7725_vsync 作为标志。Ov7725_vsync标志的初始值为0,当检测到第一次上升沿时,控制FIFO相应的GPIO引脚,允许OV7725 向FIFO 写入图像数据,并把标志值设置为1;检测到第二次上升沿时,禁止OV7725 写FIFO,把标志设置为2,而我们将会在main 函数的循环中对该标志进行判断,当Ov7725_vsync=2 时,STM32 开始从FIFO 读取数据并显示,读取完毕后把Ov7725_vsync 标置复位为0,重新开始下一幅图像的采集。中断服务函数位于stm32f10x_it.c 文件,见代码
/* ov7725 场中断 服务程序 */
void OV7725_VSYNC_EXTI_INT_FUNCTION ( void )
{
if ( EXTI_GetITStatus(OV7725_VSYNC_EXTI_LINE) != RESET ) //检查EXTI_Line0线路上的中断请求是否发送到了NVIC
{
if( Ov7725_vsync == 0 )
{
FIFO_WRST_L(); //拉低使FIFO写(数据from摄像头)指针复位
FIFO_WE_H(); //拉高使FIFO写允许
Ov7725_vsync = 1;
FIFO_WE_H(); //使FIFO写允许
FIFO_WRST_H(); //允许使FIFO写(数据from摄像头)指针运动
}
else if( Ov7725_vsync == 1 )
{
FIFO_WE_L(); //拉低使FIFO写暂停
Ov7725_vsync = 2;
}
EXTI_ClearITPendingBit(OV7725_VSYNC_EXTI_LINE); //清除EXTI_Line0线路挂起标志位
}
}
(3)读FIFO并显示图像
采集得到的图像数据都保存到摄像头模块的FIFO中,在Ov7725_vsync标志变为2的时候,STM32即可读取并显示到LCD上,与FIFO相关的函数有FIFO_GPIO_Config、FIFO_PREPARE 和ImagDisp 。
FIFO_GPIO_Config 类似SCCB_GPIO_Config 函数,完成了基本的GPIO 初始化,见代码
/************************************************
* 函数名:FIFO_GPIO_Config
* 描述 :FIFO GPIO配置
* 输入 :无
* 输出 :无
* 注意 :无
************************************************/
static void FIFO_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/*开启时钟*/
RCC_APB2PeriphClockCmd (OV7725_OE_GPIO_CLK|OV7725_WRST_GPIO_CLK|
OV7725_RRST_GPIO_CLK|OV7725_RCLK_GPIO_CLK|
OV7725_WE_GPIO_CLK|OV7725_DATA_GPIO_CLK, ENABLE );
/*(FIFO_OE--FIFO输出使能)*/
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = OV7725_OE_GPIO_PIN;
GPIO_Init(OV7725_OE_GPIO_PORT, &GPIO_InitStructure);
/*(FIFO_WRST--FIFO写复位)*/
GPIO_InitStructure.GPIO_Pin = OV7725_WRST_GPIO_PIN;
GPIO_Init(OV7725_WRST_GPIO_PORT, &GPIO_InitStructure);
/*(FIFO_RRST--FIFO读复位) */
GPIO_InitStructure.GPIO_Pin = OV7725_RRST_GPIO_PIN;
GPIO_Init(OV7725_RRST_GPIO_PORT, &GPIO_InitStructure);
/*(FIFO_RCLK-FIFO读时钟)*/
GPIO_InitStructure.GPIO_Pin = OV7725_RCLK_GPIO_PIN;
GPIO_Init(OV7725_RCLK_GPIO_PORT, &GPIO_InitStructure);
/*(FIFO_WE--FIFO写使能)*/
GPIO_InitStructure.GPIO_Pin = OV7725_WE_GPIO_PIN;
GPIO_Init(OV7725_WE_GPIO_PORT, &GPIO_InitStructure);
/*(FIFO_DATA--FIFO输出数据)*/
GPIO_InitStructure.GPIO_Pin = OV7725_DATA_0_GPIO_PIN | OV7725_DATA_1_GPIO_PIN |
OV7725_DATA_2_GPIO_PIN | OV7725_DATA_3_GPIO_PIN |
OV7725_DATA_4_GPIO_PIN | OV7725_DATA_5_GPIO_PIN |
OV7725_DATA_6_GPIO_PIN | OV7725_DATA_7_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(OV7725_DATA_GPIO_PORT, &GPIO_InitStructure);
FIFO_OE_L(); /*拉低使FIFO输出使能*/
FIFO_WE_H(); /*拉高使FIFO写允许*/
}
FIFO_PREPAGE实际是一个宏,它是在main 函数中,判断到接收完成一幅图像后被调用的,它的作用是把FIFO 读指针复位,使后面的数据读取从FIFO 的0 地址开始
FIFO_PREPAGE 宏
#define FIFO_PREPARE do{\
FIFO_RRST_L();\
FIFO_RCLK_L();\
FIFO_RCLK_H();\
FIFO_RRST_H();\
FIFO_RCLK_L();\
FIFO_RCLK_H();\
}while(0)
ImagDisp函数完成了从FIFO读取图像并显示到LCD的工作,每当OV7725输出完一副图像,它就被调用一次,在调用前,要加上FIFO_PREAGE 宏复位FIFO 读指针。
ImagDisp 函数
/**
* @brief 设置显示位置
* @param sx:x起始显示位置
* @param sy:y起始显示位置
* @param width:显示窗口宽度,要求跟OV7725_Window_Set函数中的width一致
* @param height:显示窗口高度,要求跟OV7725_Window_Set函数中的height一致
* @retval 无
*/
void ImagDisp(uint16_t sx,uint16_t sy,uint16_t width,uint16_t height)
{
uint16_t i, j;
uint16_t Camera_Data;
ILI9341_OpenWindow(sx,sy,width,height);
ILI9341_Write_Cmd ( CMD_SetPixel );
for(i = 0; i < width; i++)
{
for(j = 0; j < height; j++)
{
READ_FIFO_PIXEL(Camera_Data); /* 从FIFO读出一个rgb565像素到Camera_Data变量 */
ILI9341_Write_Data(Camera_Data);
}
}
}
在代码中,先根据输入参数在液晶屏设置了显示窗口,然后循环调用宏READ_FIFO_PIXEL读取FIFO数据,循环的次数就是摄像头输出的像素个数,代码中使用宏width 和height 控制,最后使用LCD_WR_Data函数把该图像数据显示到LCD上。宏READ_FIFO_PIXEL见代码
#define READ_FIFO_PIXEL(RGB565) do{\
RGB565=0;\
FIFO_RCLK_L();\
RGB565 = (OV7725_DATA_GPIO_PORT->IDR) & 0xff00;\
FIFO_RCLK_H();\
FIFO_RCLK_L();\
RGB565 |= (OV7725_DATA_GPIO_PORT->IDR >>8) & 0x00ff;\
FIFO_RCLK_H();\
}while(0)
这个宏把FIFO 读取到的数据按RGB565 的处理,保存到一个16 位的变量中,LCD_WR_Data 函数可以直接利用这个数据,显示一个像素点到LCD 上。
main文件
利用前面介绍的函数,就可以驱动摄像头采集并显示图像了,关于摄像头模式或者分辨率的配置本工程还提供了其它的函数进行修改,首先来了解采集流程最及基本的main函数
#include "stm32f10x.h"
#include "./ov7725/bsp_ov7725.h"
#include "./lcd/bsp_ili9341_lcd.h"
#include "./led/bsp_led.h"
#include "./usart/bsp_usart.h"
#include "./key/bsp_key.h"
#include "./systick/bsp_SysTick.h"
extern uint8_t Ov7725_vsync;
unsigned int Task_Delay[NumOfTask];
extern OV7725_MODE_PARAM cam_mode;
/**
* @brief 主函数
* @param 无
* @retval 无
*/
int main(void)
{
float frame_count = 0;
uint8_t retry = 0;
/* 液晶初始化 */
ILI9341_Init();
ILI9341_GramScan ( 3 );
LCD_SetFont(&Font8x16);
LCD_SetColors(RED,BLACK);
ILI9341_Clear(0,0,LCD_X_LENGTH,LCD_Y_LENGTH); /* 清屏,显示全黑 */
/********显示字符串示例*******/
ILI9341_DispStringLine_EN(LINE(0),"BH OV7725 Test Demo");
USART_Config();
LED_GPIO_Config();
Key_GPIO_Config();
SysTick_Init();
printf("\r\n ** OV7725摄像头实时液晶显示例程** \r\n");
/* ov7725 gpio 初始化 */
OV7725_GPIO_Config();
LED_BLUE;
/* ov7725 寄存器默认配置初始化 */
while(OV7725_Init() != SUCCESS)
{
retry++;
if(retry>5)
{
printf("\r\n没有检测到OV7725摄像头\r\n");
ILI9341_DispStringLine_EN(LINE(2),"No OV7725 module detected!");
while(1);
}
}
while(1)
{
/*接收到新图像进行显示*/
if( Ov7725_vsync == 2 )
{
frame_count++;
FIFO_PREPARE; /*FIFO准备*/
ImagDisp(0,0,320,240); /*采集并显示*/
Ov7725_vsync = 0;
LED1_TOGGLE;
}
/*每隔一段时间计算一次帧率*/
if(Task_Delay[0] == 0)
{
printf("\r\nframe_ate = %.2f fps\r\n",frame_count/10);
frame_count = 0;
Task_Delay[0] = 10000;
}
}
}
main函数的执行流程说明如下:
(1)main函数首先调用了ILI9341_Init 、USART_Config 、SysTick_Init 和LED_GPIO_Config 等函数初始化液晶、串口、Systick 定时器和LED外设,其中Systick每ms 中断一次,为下面计算帧率的代码提供时间
(2)接下来OV7725_GPIO_Config函数,该函数内部封装了前面介绍的SCCB_GPIO_Config、FIFO_GPIO_Config 及VSYNC_GPIO_Config 函数,对控制摄像头使用的相关引脚都进行了初始化。
(3)初始化好SCCB相关的引脚,就可通过OV7725_Init向OV7725芯片写入配置参数,代码中使用while循环在初始化失败时进行多次尝试。
(4)调用ILI9341_GramScan设置液晶屏的扫描方向,使得液晶屏与摄像头的分辨率一致,做好显示准备。
(5)在while循环中,根据Ov7725_vsync标志,判断FIFO是否接收完了一幅图像,在中断服务程序中, 若检测到两次VSYNC 的下降沿( 表示接收完一幅图像) , 会把Ov7725_vsync 变量设置为2。
(6)判断接收完一幅图像后,调用宏FIFO_PREPARE 使读FIFO 指针复位,使ImagDisp读取FIFO 时,能读取得正确的数据并显示到液晶屏。
(7)记录帧数目的变量frame_count 加1,这个变量用来统计帧率,每过一段时间后计算帧
率通过串口输出到上位机。最后把Ov7725_vsync 置0,使重新开始计数。
OV7725 的其它模式配置
以上是最基本的摄像头采集过程,但是还需要一些摄像头的配置,包括分辨率、光照率、饱和度、对比度以及特殊模式,如OV7725_Window_Set、OV7725_Brightness、OV7725_Color_Saturation、OV7725_Contrast 和OV7725_Special_Effect等函数,这些函数的本质都是根据函数的输入参数,转化成对应的配置写入到OV7725 摄像头的寄存器中,完成相应的配置。