正点原子STM32连载 第46章 摄像头实验(摘自正点原子STM32F103战舰开发指南V1.2)

1)实验平台:正点原子stm32f103战舰开发板V4
2)平台购买地址:https://detail.tmall.com/item.htm?id=609294757420
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/thread-340252-1-1.html#

第四十六章 摄像头实验

正点原子战舰STM32开发板板载了一个摄像头接口(P6),该接口可以用来连接正点原子 OV7725摄像头模块。本章,我们将使用STM32驱动正点原子 OV7725摄像头模块,实现摄像头功能。
本章分为如下几个部分:
46.1 OV7725简介
46.2 硬件设计
46.3 软件设计
46.4 下载验证

46.1 OV7725模块简介

本部分将从正点原子的OV7725模块的原理和驱动方法讲述摄像头模块的使用方法。
46.1.1 正点原子OV7725模块
正点原子OV7725模块是正点原子推出的一款高性能30W像素高清摄像头模块。该模块采用OmniVision公司生产的一颗1/4英寸CMOS VGA(640*480)图像传感器:OV7725。正点原子的OV7725模块采用该OV7725传感器作为核心部件,集成有源晶振和和FIFO(AL422B),可以调整缓存摄像头的图像数据,任意一款MCU都可控制该模块和读取图像。
它的外形如图46.1.1.1所示:

图46.1.1.1 正点原子OV7725模块
正点原子OV7725模块的特点如下:
● 高灵敏度、低电压适合嵌入式应用
● 标准的SCCB接口,兼容IIC接口
● 支持RawRGB、RGB(GBR4:2:2,RGB565/RGB555/RGB444),YUV(4:2:2)和YCbCr(4:2:2)输出格式
● 支持VGA、QVGA,和从CIF到40*30的各种尺寸输出
● 自动图像控制功能:自动曝光(AEC)、自动白平衡(AWB)、自动消除灯光条纹、自动黑电平校准(ABLC)和自动带通滤波器(ABF)
● 支持图像质量控制:色饱和度调节、色调调节、gamma校准、锐度和镜头校准等
● 支持图像缩放、平移和窗口设置
● 集成有源晶振,无需外部提供时钟
● 集成FIFO芯片(AL422B),方便MCU读取图像
● 自带嵌入式微处理器
OV7725的功能框图如图46.1.1.2所示:

图46.1.1.2 OV7725功能框图
由上图可知,感光阵列(image array)在XCLK时钟的驱动下进行图像采样,输出640*480阵列的模拟数据;接着模拟信号处理器在时序发生器(video timing generator)的控制下对模拟数据进行算法处理(analog processing);模拟数据处理完成后分成G(绿色)和R/B(红色/蓝色)两路通道经过AD转换器后转换成数字信号,并且通过DSP进行相关图像处理,最终输出所配置格式的10位视频数据流。模拟信号处理以及DSP等都可以通过寄存器(register)来配置,配置寄存器的接口就是SCCB接口。
正点原子OV7725模块原理图如下图46.1.1.3所示:

图46.1.1.3 正点原子OV7725摄像头模块原理图
从上图可以看出,正点原子OV7725摄像头模块自带了有源晶振,用于产生12M时钟作为OV7725传感器的XCLK输入;带有一个FIFO芯片(AL422B),该FIFO芯片的容量是384K字节,足够存储2帧QVGA的图像数据。当驱动好OV7725模块,图像数据就被存放到FIFO中,获取图像数据就是对FIFO进行读取,这个过程需要用到的引脚就是上图中的29双排座。模块就是通过一个29的双排排针(P1)与外部通信,与外部通信信号如表46.1.1.1所示:

46.1.2 串行摄像头控制总线(SCCB)简介
正点原子OV7725摄像头模块的所有配置,包括图像数据格式、分辨率以及图像处理参数等,都是通过SCCB总线来实现的。
SCCB全称是:Serial Camera Control Bus即串行摄像头控制总线,是由OV(OmniVision的简称)公司定义和发展的三线式串行总线。不过,OV公司为了减少传感器引脚的封装,现在SCCB总线大多采用两线式接口总线。
OV7725使用的两线式接口总线,由两条数据线组成:一条是用于传输时钟信号的SIO_C(即OV_SCL),另一条是用于传输数据信号的SIO_D(即OV_SDA),这两条数据线类型IIC协议中的SCL和SDA信号线。在前面提及到SCCB协议兼容IIC协议,是因为SCCB协议和IIC协议非常相似,有关IIC协议的详细介绍请大家参考前面的“IIC实验”章节。
SCCB包括三种传输周期(也就是协议),即3相写传输周期,2相写传输周期和2相读传输周期。3相写传输周期相当于写操作,而读操作是符合的需要2相写传输周期和2相读传输周期进行结合。这里的相指的是传输的单位,一字节称为一个相。
SCCB的写传输协议如下图46.1.2.1所示:

图46.1.2.1 SCCB写传输协议
上图中就是三相写传输周期,第一个相就是ID Address,由7位器件地址和1位读写控制位构成(0:写 1:读),而OV7725器件地址为0x21,所以在写传输时序中,ID Address(W)为0x42(器件地址左移1位,低位补0);第二个相就是Sub-address,即8位寄存器地址,在OV7725的数据手册中定义了0x00~0xAC共173个寄存器,有些寄存器是可写的,有些是只读的,只有可写的寄存器才能正确写入;第三个相就是Write Data,即要写入寄存器的8位配置数据。而上图中的第9位X表示Don’t Care(不必关心位),该位是由从机(此处指OV7725)发出应答信号来响应主机表示当前ID Adress、Sub-address和Write Data是否传输完成,但是从机有可能不发出应答信号,因此主机(此处指STM32)可不用判断此处是否有应答,直接默认当前传输完成即可。
SCCB和IIC写传输协议是极为相似,只是在SCCB写传输时序中,第9位为不必关心位,而IIC写传输协议中为应答位。SCCB的读传输时序和IIC有些差异,在IIC读传输协议中,写完寄存器地址后,会有一个restart即重复开始的操作;而SCCB读传输协议中没有重复开始的概念,在写完寄存器地址后,发起总线停止信号。
SCCB读传输协议如下图46.1.2.2所示。

图46.1.2.2 SCCB读传输协议
SCCB读传输协议由两个部分组成:二相写传输周期和二相读传输周期。跟IIC的读操作是相似的,都是复合的过程。第一部分是写器件地址和寄存器地址,即先进行一次虚写操作,通过这种虚写操作使地址指针向虚写操作中寄存器地址的位置。第二部分就是读器件地址和读数据,此时读取到的数据才是寄存器地址对应的数据,这里的读器件地址为0x43(器件地址左移1位,低位补1)。上图中的NA位是由主机(这里指STM32)产生,由于SCCB总线不支持连续读写,因此NA位必须为高电平。
关于SCCB的详细介绍,请大家参考正点原子OV7725摄像头模块资料里《OmniVision Technologies Seril Camera Control Bus(SCCB) Specification.pdf》这个文档。
OV7725的初始化,需要配置大量的寄存器,这里我们就不给大家多做介绍了,请大家参考正点原子OV7725摄像头模块资料里《OV7725 Software Application Note.pdf》。
46.1.3 输出时序说明
当使用SCCB总线对OV7725进行寄存器配置后,就会输出图像数据。通过查看输出时序图就可以知道如何进行图像数据的获取。由于OV7725支持多种尺寸(分辨率)输出,所以在这里简单介绍一下这些尺寸定义:
VGA,即分辨率为640 * 480的输出模式;
QVGA,即分辨率为320 * 240的输出模式;
QQVGA,即分辨率为160 * 120的输出模式;
这里,我们就以VGA模式为例子,即OV7725输出的图像分辨率为640*480,下面分析一下图46.1.3.1所示的帧时序图。

图46.1.3.1 VGA模式帧时序图
在分析时序图前,我们先了解几个基本的概念。
VSYNC:场同步信号,也叫帧信号,由摄像头输出,用于标志一帧图像数据的开始与结束。上图中VSYNC的高电平作为一帧的同步信号,在低电平时输出的数据有效。场同步的极性可以通过寄存器0x15去设置的,本实验使用的是和上图一致的默认设置。
HREF/HSYNC:行同步信号,由摄像头输出,用来标志一行数据的开始与结束。上图中的HREF和HSYNC是由同一引脚输出的,只是数据的同步方式不一样。HREF上升沿就马上输出图像数据,而HSYNC会等待一段时间再输出图像数据,如果行中断里需要处理事情再开始采集,显然用HREF上升沿是不容易采集到第一个像素数据的。而我们本实验使用的是HREF格式输出,当HREF为高电平时,图像数据马上输出并有效。该引脚的极性也是可以通过寄存器0x15进行设置。
D[9:0]:数据信号,由摄像头输出,在RGB格式输出中,只用到8个数据引脚,即高8位D[9:2]是有效的。
tPCLK:一个像素时钟周期。
tp:单个数据周期,这里我们需要注意上图中左下角的Note1和2描述,在RGB模式中,tp代表两个tPCLK(像素时钟)。以RGB565数据格式为例,RGB565采用16bit数据表示一个像素点,而OV7725在一个像素周期(tPCLK)内只能传输8bit数据,因此需要两个时钟周期才能输出一个RGB565数据。
tLine:摄像头输出一行数据的时间,共784个tp,包含640tp个高电平和144tp个低电平,其中,640tp为有效像素数据输出的时间。以RGB565数据格式为例,640tp实际上就是6402=1280个tPCLK。
由图46.1.3.1可知,VSYNC的上升沿作为一帧的开始,高电平同步脉冲时间为4
tLine,紧接着等待18tLine时间后,HREF开始拉高,此时OV7725输出一行有效图像数据,这里是一行数据即640个像素点(VGA模式);HREF由640tp个高电平和144tp个低电平构成;输出480行数据之后等待8tLine时间会产生一个VSYNC上升沿标志一帧数据传输结束。所以输出一帧图像的时间实际上是tFrame = (4 + 18 + 480 + 8) * tLine = 510 tLine。
利用以上的公式,结合摄像头的输出时钟fPCLK得到tPCLK,便可算出摄像头输出帧率:
摄像头输出帧率:1s / tFrame = 1s / (510 * 784 * 2 tPCLK)
OV7725模块的输入时钟为12MHz,通过OV7725初始化的配置(主要查看0x0D和0x11寄存器),输出时钟为24MHz(周期为42ns),所以代入以上公式,帧率达到30Hz。这里要跟LCD的刷新率进行区分,由于我们本实验用到的是带FIFO的OV7725模块,MCU不是直接去接收传感器输出的图像数据,而是通过从FIFO里进行获得,所以说这个刷新率是比摄像头的输出帧率要低很多。
从帧时序图中,可以清楚知道OV7725是如何输出图像数据,但是不清楚像素数据的情况,所以接下来,我们来看一下图46.1.3.2所示的输出RGB565时序图。

图46.1.3.2 OV7725 RGB565输出时序
从上图可看出,OV7725的图像数据通过D[9:2]输出一个字节,first byte和second byte组成一个16位RGB565数据。在时序上,HREF为高时开始传输一行数据,1个PCLK传输1个字节,传输完一行数据最后一个字节(last byte)后HREF则变为低。
46.1.4 图像数据存储和读取说明
由于OV7725的像素时钟(PCLK)最高可达24Mhz,我们用STM32F103的IO口直接抓取,会十分消耗CPU(可以通过降低PCLK输出频率,来实现IO口抓取,但是不推荐)。所以,我们并不是采取直接抓取OV7725输出的图像数据,而是通过FIFO读取。正点原子 OV7725摄像头模块自带了一个FIFO芯片,用于暂存图像数据,有了这个芯片,我们就可以很方便的获取图像数据了,而不再要求单片机具有高速IO,也不会耗费多少CPU,可以说,只要是个单片机,都可以通过正点原子OV7725摄像头模块实现拍照的功能。
FIFO芯片,型号是AL422B,本质是一种RAM存储器,容量为393216字节,不足以存放一帧VGA分辨率RGB格式的图像数据(6404802),但是能存放2帧QVGA分辨率RGB格式的图像数据(3202402)。由于AL422B写操作相关引脚和读操作相关引脚都是独立开来的,其引脚图如图46.1.4.1所示,所以支持同时写入和读出数据。

图46.1.4.1 AL422B引脚图
写操作相关引脚:WCK、WRST、DI7~0、WE
读操作相关引脚:RCK、RRST、DO7~0、RE、OE
以上引脚在前面表46.1.1.1已经做了介绍,接下来,我们看一下FIFO写时序和读FIFO时序,这两个时序图对应的就是“如何存储图像数据”和“如何读取图像数据”问题的解决。
首先,我们来看一下图46.1.4.2 FIFO写时序图,了解一下如何存储图像数据。

图46.1.4.2 FIFO写时序图
上图中WCK是写FIFO时钟,它与OV_PCLK相连,也就是OV7725的PCLK时钟信号直接提供写FIFO时钟。WRST是FIFO写指针复位引脚,由MCU控制,低电平时,写指针会复位到FIFO的0地址处。WE是FIFO写使能引脚即FIFO_WE,低电平时,FIFO允许写入,但该引脚的电平是通过一个与非门进行决定,详看图46.1.1.3模块原理图中的SN74LVC1G00部分(FIFO_WEN和OV_HREF都为高电平,FIFO_WE才为低电平),具体使用:当OV_HREF为高电平就是一行图像数据到来,而FIFO_WEN引脚是引出来的,所以我们只需要在此刻拉高FIFO_WEN引脚,WE引脚就输出低电平,允许图像数据写入到FIFO。而DI70是直接与OV7725传感器的数据引脚D2D9相连。
总的来说,写FIFO时序就是:OV7725输出有效的行图像数据时(HREF高电平),我们需要保持写使能引脚为低电平(FIFO_WEN拉高)。在复位写指针(拉低后又重新拉高)后,需要一定的复位周期,然后才开始往FIFO的0地址处写数据,且数据会按地址递增方式存入FIFO。
通常,我们会根据帧同步信号进而以上操作,这个存储图像数据的工程为:

  1. 等待OV7725帧同步信号;
  2. FIFO写指针复位;
  3. FIFO写使能;
  4. 等待第二个OV7725帧同步信号;
  5. FIFO写禁止。
    通过以上5个步骤,我们就可以完成1帧图像数据在AL422B的存储。注意:FIFO写禁止操作不是必须的,只有当你想将一帧图片数据存储在FIFO,并在外部MCU读取完这帧图片数据之前,不再采集新的图片数据的时候,才需要进行FIFO写禁止。
    接下来,继续看图46.1.4.2所示的读FIFO时序图,了解一下如何读取图像数据。

图46.1.4.2 读FIFO时序图
上图中的RCK是读FIFO时钟,由MCU控制;RRST是FIFO读指针复位引脚,由MCU控制,低电平时,读指针会复位到FIFO的0地址处;RE是读使能引脚,硬件设计直接接地;OE是输出使能,由MCU控制,要保持低电平才能使能FIFO数据输出;DO7~0是数据引脚,获取图像数据。
在存储完一帧图像以后,我们就可以开始读取图像数据了。读取过程如下:

  1. FIFO读指针复位(RRST);
  2. 给FIFO读时钟(FIFO_RCLK);
  3. 读取第一个像素高字节(D[7:0]);
  4. 给FIFO读时钟(FIFO_RCLK);
  5. 读取第一个像素低字节(D[7:0]);
  6. 给FIFO读时钟(FIFO_RCLK);
  7. 读取第二个像素高字节(D[7:0]);
  8. 循环读取剩余像素;
  9. 结束。
    可以看出,摄像头模块数据的读取也是十分简单,比如QVGA模式,RGB565格式,我们总共循环读取3202402次,就可以读取1帧图像数据,把这些数据写入LCD模块,我们就可以看到摄像头捕捉到的画面了。
    注意:如果摄像头要使用VGA模式输出,由于FIFO是没办法缓存一帧的VGA图像,就需要在FIFO写满之前开始读FIFO数据,否则数据可能被覆盖。OV7725还可以对输出图像进行各种设置,数据手册和应用笔记详见光盘《OV7725_datasheet.pdf》和《OV7725 Software Application Note.pdf》。对AL422B的操作时序,请大家参考AL422B的数据手册。以上资料,大家可以通过正点原子的资料下载中心获取《正点原子OV7725摄像头模块资料》。
    46.2 硬件设计
  1. 例程功能
    开机后,检测和初始化OV7725摄像头模块。初始化成功后需要先通过KEY0和KEY1选择为QVGA或VGA输出模式,然后LCD才会显示拍摄到的画面。
    正常显示拍摄画面后,我们可以通过KEY0设置光照模式、通过KEY1设置色饱和度,通过KEY2设置亮度,通过KEY_UP设置对比度,通过TPAD设置特效(总共7种特效)。通过串口,我们可以查看当前的帧率(这里是指LCD显示的帧率,而不是指OV7725的输出帧率),同时可以借助USMART设置OV7725的寄存器,方便大家调试。LED0指示程序运行状态。
  2. 硬件资源
    1)LED灯
    LED0 – PB5
    2)串口1(PA9/PA10连接在板载USB转串口芯片CH340上面)
    3)正点原子 2.8/3.5/4.3/7/10寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
    4)独立按键: KEY0 – PE4、KEY1 – PE3、KEY2 – PE2、WK_UP – PA0
    5)电容按键: PA1,用于控制触摸按键TPAD。
    6)外部中断8(连接PA8,用于检测OV7725的帧信号)
    7)定时器6(用于打印摄像头帧率等信息)
    8)正点原子OV7725摄像头模块,通讯线的连接关系为:
    OV7725模块 STM32开发板 OV7725模块 STM32开发板
    OV_D0~D7 PC0~7 FIFO_OE PG15
    OV_SCL PD3 FIFO_WRST PD6
    OV_SDA PG13 FIFO_WEN PB3
    OV_VSYNC PA8 FIFO_RCLK PB4
    FIFO_RRST PG14
    表46.2.1 OV7725模块与开发板连接关系
    这部分的连线,模块与开发板上的P6座子已经对应好了,安装时直接把模块镜头背离开发板的方向安装即可(建议安装所有模块的时候断电安装)。

图46.2.1 开发板上连接OV7725模块的座子
46.3 程序设计
OV7725模块驱动步骤
1)初始化OV7725
这里的初始化工作包括:初始化用到的IO口以及SCCB接口、读取传感器ID以及执行初始化序列(配置参数)。
2)存储图像数据
依照OV7725帧时序和FIFO写时序进行操作,详细过程参考前面对时序的分析。
3)读取图像数据
依照FIFO读时序进行,详细过程参考前面对时序的分析。
46.3.1 程序流程图

图46.3.1.1 摄像头实验程序流程图
46.3.2 程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。SCCB驱动源码包括两个文件:sccb.c和sccb.h。OV7725驱动源码包括五个文件:ov7725.c、ov7725.h、ov7725cfg.h、exti.c和exti.h。

  1. SCCB驱动代码
    我们已经介绍过OV7725是通过SCCB协议进行驱动的了,所以在实现OV7725驱动前,需要先加定义好SCCB对应IO的初始化、读写函数,这部分代码我们在sccb.c/h中实现。由于sccb与I2C的实现类似,但是根据sccb协议在数据收/发周期时必须实现第九位的传输。
    我们直接复制myiic.c/h的代码,把它们改成sccb驱动,除了读写函数存在一点区别外,其他基本上没有太大的变化。这里看一下读写函数,代码如下:
/**
 * @brief     	SCCB 发送一个字节
 * @param     	data: 要发送的数据
 * @retval     	无
 */
uint8_t sccb_send_byte(uint8_t data)
{
    uint8_t t, res;
    
    for (t = 0; t < 8; t++)
    {
        SCCB_SDA((data & 0x80) >> 7);	/* 高位先发送 */
        sccb_delay();
        SCCB_SCL(1);
        sccb_delay();
        SCCB_SCL(0);
        data <<= 1;						/* 左移1位,用于下一次发送 */
}

    SCCB_SDA(1);        					/* 发送完成, 主机释放SDA线 */
    sccb_delay();
    SCCB_SCL(1);        					/* 接收第九位,以判断是否发送成功 */
    sccb_delay();

    if (SCCB_READ_SDA)
    {
        res = 1;        					/* SDA=1发送失败,返回1 */
    }
    else 
    {
        res = 0;        					/* SDA=0发送成功,返回0 */
    } 

    SCCB_SCL(0);   
    return res;
}

/**
 * @brief      	SCCB 读取一个字节
 * @param      	无
 * @retval     	读取到的数据
 */
uint8_t sccb_read_byte(void)
{
uint8_t i, receive = 0;

    for (i = 0; i < 8; i++ )			/* 接收1个字节数据 */
    {
        receive <<= 1;          			/* 高位先输出,所以先收到的数据位要左移 */
        SCCB_SCL(1);
        sccb_delay();

        if (SCCB_READ_SDA)
        {
            receive++;
        }
        
        SCCB_SCL(0);
        sccb_delay();
    }
    return receive;
}
  1. OV7725驱动代码
    OV7725驱动代码实现对OV7725摄像头模块的操作。
    首先看一下基于SCCB基本接口的OV7725读寄存器函数和写寄存器函数,代码如下:
/**
 * @brief      	OV7725 读寄存器
 * @param      	reg : 寄存器地址
 * @retval    	读到的寄存器值
 */
uint8_t ov7725_read_reg(uint16_t reg)
{
uint8_t data = 0;

    sccb_start();                 			/* 起始信号 */
    sccb_send_byte(OV7725_ADDR);   		/* 写通讯地址 */
    sccb_send_byte(reg);           		/* 寄存器地址 */
    sccb_stop();							/* 停止信号 */
    
    /* 设置寄存器地址后,才是读 */
    sccb_start();						/* 起始信号 */
    sccb_send_byte(OV7725_ADDR | 0X01);	/* 读通讯地址 */
    data = sccb_read_byte();       	 	/* 读取数据 */
    sccb_nack();					      	/* 非应答信号 */
sccb_stop();							/* 停止信号 */

    return data;
}

/**
 * @brief     	OV7725 写寄存器
 * @param      	reg : 寄存器地址
 * @param      	data: 要写入寄存器的值
 * @retval     	0, 成功; 1, 失败;
 */
uint8_t ov7725_write_reg(uint8_t reg, uint8_t data)
{
uint8_t res = 0;

sccb_start();   /* 起始信号 */

    if (sccb_send_byte(OV7725_ADDR)) res = 1;	/* 写通讯地址 */

    if (sccb_send_byte(reg)) res = 1;      		/* 寄存器地址 */

    if (sccb_send_byte(data)) res = 1;      		/* 写数据 */

sccb_stop();		/* 停止信号 */

    return res;
}
按厂商建议的初始化序列,我们封装了需要进行初始化的寄存器,我们再结合AL422B的读写特性操作相关IO。实现的初始化函数如下:
/**
 * @brief     	初始化 OV7725
 * @param      	无
 * @retval    	0, 成功; 1, 失败;
 */
uint8_t ov7725_init(void)
{
    uint16_t i = 0;
    uint16_t reg = 0;

/* .......这里删去IO和时钟初始化部分代码........ */

__HAL_RCC_AFIO_CLK_ENABLE();
/* 禁止JTAG,使能SWD,释放PB3,PB4两个引脚做普通IO用 */
    __HAL_AFIO_REMAP_SWJ_NOJTAG();
    
    OV7725_WRST(1);		/* WRST = 1 */
    OV7725_RRST(1);		/* RRST = 1 */
    OV7725_OE(1); 		/* OE = 1 */
    OV7725_RCLK(1);		/* RCLK = 1 */
    OV7725_WEN(1); 		/* WEN = 1 */

    sccb_init(); 		/* 初始化SCCB 的IO口 */

    if (ov7725_write_reg(0x12, 0x80))	/* 软件复位 */
    {
        return 1;  
    }

    delay_ms(50);
    reg = ov7725_read_reg(0X1c); 	/* 读取厂家ID 高八位 */
    reg <<= 8;
    reg |= ov7725_read_reg(0X1d); 	/* 读取厂家ID 低八位 */

    if ((reg != OV7725_MID) && (reg != OV7725_MID1))    /* MID不正确 ? */
    {
        printf("MID:%d\r\n", reg);
        return 1;
    }

    reg = ov7725_read_reg(0X0a);	/* 读取厂家ID 高八位 */
    reg <<= 8;
    reg |= ov7725_read_reg(0X0b); 	/* 读取厂家ID 低八位 */

    if (reg != OV7725_PID)      		/* PID不正确 ? */
    {
        printf("HID:%d\r\n", reg);
        return 2;
    }

    /* 初始化 OV7725,采用QVGA分辨率(320*240) */
for (i=0; i<sizeof(ov7725_init_reg_tb1)/sizeof(ov7725_init_reg_tb1[0]); i++)
    {
        ov7725_write_reg(ov7725_init_reg_tb1[i][0], ov7725_init_reg_tb1[i][1]);
    }

    return 0;   						/* ok */
}

通过ov7725_init函数就完成了OV7725的基本配置,OV7725就会以配置的QVGA模式输出图像数据。每当OV7725输出一个帧信号VSYNC,就代表一帧图像数据就要通过数据引脚进行输出了,因此我们可以利用STM32的外部中断来捕获这个信号,进而在中断服务函数里,把图像数据写入到FIFO中。这里过程的实现,详看exti.c新添加的函数exti_ov7725_vsync_init和中断服务函数,源码如下:

/**
 * @brief  		OV7725 VSYNC外部中断初始化程序
 * @param      	无
 * @retval     	无
 */
void exti_ov7725_vsync_init(void)
{
GPIO_InitTypeDef gpio_init_struct;

    gpio_init_struct.Pin = OV7725_VSYNC_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_IT_RISING; 		/* 上升沿触发 */
HAL_GPIO_Init(OV7725_VSYNC_GPIO_PORT, &gpio_init_struct); 

HAL_NVIC_SetPriority(OV7725_VSYNC_INT_IRQn, 0, 0);	/* 抢占0,子优先级0 */
    HAL_NVIC_EnableIRQ(OV7725_VSYNC_INT_IRQn);           /* 使能中断线8 */
}

/**
 * @brief      	OV7725 VSYNC 外部中断服务程序
 * @param      	无
 * @retval      	无
 */
void OV7725_VSYNC_INT_IRQHandler(void)
{   /* 是OV7725_VSYNC_GPIO_PIN 线的中断? */
    if (__HAL_GPIO_EXTI_GET_IT(OV7725_VSYNC_GPIO_PIN))
    {   
        if (g_ov7725_vsta == 0)	/* 上一帧数据已经处理了? */
        {
            OV7725_WRST(0);     	/* 复位写指针 */
            OV7725_WRST(1);     	/* 结束复位 */
            OV7725_WEN(1);      	/* 允许写入FIFO */
            g_ov7725_vsta = 1;  	/* 标记帧中断 */
        }
        else
        {
            OV7725_WEN(0);      	/* 禁止写入FIFO */
        }
    }
/* 清除OV7725_VSYNC_GPIO_PIN上的中断标志位 */
    __HAL_GPIO_EXTI_CLEAR_IT(OV7725_VSYNC_GPIO_PIN);
}

因为OV7725的帧同步信号(OV_VSYNC)接在PA8,所以用到的是EXTI9_5_IRQHandler。在中断服务函数中,需要先判断中断是不是来自中断线8再进行处理。
中断处理的部分流程:每当帧中断到来后,先判断g_ov7725_vsta的值是否为0,如果是0,说明可以往FIFO里面写入数据,执行复位FIFO写指针,并允许FIFO写入。此时,AL422B将从地址0开始,存储新一帧的图像数据。然后设置g_ov7725_vsta为1,标记新的一帧数据正在存储中。如果g_ov7725_vsta不为0,说明之前存储在FIFO里面的一帧数据还未被读取过,直接禁止FIFO写入,等待MCU读取FIFO数据,以免数据覆盖。
然而,STM32只需要判断g_ov7725_vsta是否为1,来读取FIFO里面的数据,读完一帧后,设置g_ov7725_vsta为0,以免重复读取,同时还可以使能FIFO新一帧数据的写入。
2. main.c代码
我们在实现main函数前,定义了一个ov7725_camera_refresh()函数,用于读取摄像头模块自带FIFO里面的数据并显示在LCD:

uint16_t g_ov7725_wwidth = 320; 	/* 默认窗口宽度为320 */
uint16_t g_ov7725_wheight = 240; 	/* 默认窗口高度为240 */
/**
 * @brief      	更新LCD显示
 * @note     	该函数将OV7725模块FIFO里面的数据拷贝到LCD屏幕上
 * @param      	无
 * @retval    	无
 */
void ov7725_camera_refresh(void)
{
    uint32_t i, j;
    uint16_t color;

    if (g_ov7725_vsta)							/* 有帧中断更新 */
    {
        lcd_scan_dir(U2D_L2R); 					/* 从上到下, 从左到右 */
        lcd_set_window((lcddev.width - g_ov7725_wwidth) / 2,
                          (lcddev.height - g_ov7725_wheight) / 2, g_ov7725_wwidth, g_ov7725_wheight);		/* 将显示区域设置到屏幕中央 */
        lcd_write_ram_prepare();					/* 开始写入GRAM */

        OV7725_RRST(0);							/* 开始复位读指针 */
        OV7725_RCLK(0);
        OV7725_RCLK(1);
        OV7725_RCLK(0);
        OV7725_RRST(1);				/* 复位读指针结束 */
        OV7725_RCLK(1);

        for (i = 0; i < g_ov7725_wheight; i++)
        {
            for (j = 0; j < g_ov7725_wwidth; j++)
            {
                OV7725_RCLK(0);
                color = OV7725_DATA;	/* 读数据 */
                OV7725_RCLK(1);
                color <<= 8;
                OV7725_RCLK(0);
                color |= OV7725_DATA;	/* 读数据 */
                OV7725_RCLK(1);
                LCD->LCD_RAM = color;
            }
        }

        g_ov7725_vsta = 0;            /* 清零帧中断标记 */
        g_ov7725_frame++;
        lcd_scan_dir(DFT_SCAN_DIR); /* 恢复默认扫描方向 */
    }
}

对于OV7725,我们可以通过g_ov7725_wwidth和g_ov7725_wheight两个全局变量设置图像窗口输出的大小。对于分辨率大于320*240的屏幕,则通过开窗函数(lcd_set_window)将显示区域开窗在屏幕的正中央。注意,为了提高FIFO读取速度,我们将OV7725_RCLK的控制,采用快速IO控制,关键代码如下(在ov7725.h里面):
#define OV7725_RCLK(x) x ?(OV7725_RCLK_GPIO_PORT->BSRR = OV7725_RCLK_GPIO_PIN): (OV7725_RCLK_GPIO_PORT->BRR = OV7725_RCLK_GPIO_PIN)
控制OV7725_RCLK输出高电平或者低电平就用到BSRR和BRR两个寄存器,以实现快速IO设置,从而提高读取速度。
最后介绍的是main函数,其定义如下:

const char *LMODE_TBL[6] = {"Auto", "Sunny", "Cloudy", "Office", 
"Home", "Night"}; 					/* 6种光照模式 */
const char *EFFECTS_TBL[7] = {"Normal", "Negative", "B&W", "Redish", 
"Greenish", "Bluish", "Antique"};/* 7种特效 */

int main(void)
{
    uint8_t key;
    uint8_t i = 0;
    char msgbuf[15];                        	/* 消息缓存区 */
    uint8_t tm = 0;
    uint8_t lightmode = 0, effect = 0;
uint8_t saturation = 4, brightness = 4, contrast = 4;

    HAL_Init();                              	/* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9); 	/* 设置时钟, 72Mhz */
    delay_init(72);                         	/* 延时初始化 */
    usart_init(115200);                    	/* 串口初始化为115200 */
    usmart_dev.init(72);                   	/* 初始化USMART */
    led_init();                             		/* 初始化LED */
    lcd_init();                             		/* 初始化LCD */
    key_init();                             		/* 初始化按键 */
    tpad_init(6);                           		/* TPAD 初始化 */

    lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
    lcd_show_string(30, 70, 200, 16, 16, "OV7725 TEST", RED);
    lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
    lcd_show_string(30, 110, 200, 16, 16, "KEY0:Light Mode", RED);
    lcd_show_string(30, 130, 200, 16, 16, "KEY1:Saturation", RED);
    lcd_show_string(30, 150, 200, 16, 16, "KEY2:Brightness", RED);
    lcd_show_string(30, 170, 200, 16, 16, "KEY_UP:Contrast", RED);
    lcd_show_string(30, 190, 200, 16, 16, "TPAD:Effects", RED);
    lcd_show_string(30, 210, 200, 16, 16, "OV7725 Init...", RED);

    while (1) 
    {
        if (ov7725_init() == 0)				/* 初始化OV7725 */
        {
            lcd_show_string(30, 210, 200, 16, 16, "OV7725 Init OK       ", RED);

            while (1)
            {
                key = key_scan(0);

                if (key == KEY0_PRES)
                {   /* QVGA模式输出 */
                    g_ov7725_wwidth = 320;		/* 默认窗口宽度为320 */
                    g_ov7725_wheight = 240; 	/* 默认窗口高度为240 */
                    ov7725_window_set(g_ov7725_wwidth, g_ov7725_wheight, 0);
                    break;
                }
                else if (key == KEY1_PRES)
                {   /* VGA模式输出 */
                    g_ov7725_wwidth = 320; 	/* 默认窗口宽度为320 */
                    g_ov7725_wheight = 240;  	/* 默认窗口高度为240 */
                    ov7725_window_set(g_ov7725_wwidth, g_ov7725_wheight, 1);
                    break;
                }

                i++;

                if (i == 100)
                    lcd_show_string(30,230,210,16,16, "KEY0:QVGA  KEY1:VGA", RED); 

                if (i == 200)
                {
                    lcd_fill(30, 230, 210, 250 + 16, WHITE);
                    i = 0;
                }

                delay_ms(5);
            }

            ov7725_light_mode(lightmode);
            ov7725_color_saturation(saturation);
            ov7725_brightness(brightness);
            ov7725_contrast(contrast);
            ov7725_special_effects(effect);

            OV7725_OE(0); /* 使能OV7725 FIFO数据输出 */

            break;
        }
        else
        {
            lcd_show_string(30, 190, 200, 16, 16, "OV7725 Error!!", RED);
            delay_ms(200);
            lcd_fill(30, 190, 239, 246, WHITE);
            delay_ms(200);
        }
}

    btim_timx_int_init(10000, 7200 - 1);	/* 10Khz计数频率,1秒钟中断 */
    exti_ov7725_vsync_init();           		/* 使能OV7725 VSYNC外部中断 */
lcd_clear(BLACK);

    while (1)
    {
        key = key_scan(0); 		/* 不支持连按 */

        if (key)
        {
            tm = 20;

            switch (key)
            {
                case KEY0_PRES: 	/* 灯光模式Light Mode */
                    lightmode++;
                    if (lightmode > 5)	 lightmode = 0;
                    ov7725_light_mode(lightmode);
                    sprintf((char *)msgbuf, "%s", LMODE_TBL[lightmode]);
                    break;

                case KEY1_PRES: 	/* 饱和度Saturation */
                    saturation++;
                    if (saturation > 8) saturation = 0;
                    ov7725_color_saturation(saturation);
                    sprintf((char *)msgbuf, "Saturation:%d", saturation);
                    break;

                case KEY2_PRES: 	/* 饱和度Saturation */
                    brightness++;
                    if (brightness > 8)  brightness = 0;
                    ov7725_brightness(brightness);
                    sprintf((char *)msgbuf, "Brightness:%d", brightness);
                    break;

                case WKUP_PRES: 	/* 对比度Contrast */
                    contrast++;
                    if (contrast > 8)	contrast = 0;
                    ov7725_contrast(contrast);
                    sprintf((char *)msgbuf, "Contrast:%d", contrast);
                    break;
                }
        }

        if (tpad_scan(0))		/* 检测到触摸按键 */
        {
            effect++;

            if (effect > 6)  effect = 0;

            ov7725_special_effects(effect);	/* 设置特效 */
            sprintf((char *)msgbuf, "%s", EFFECTS_TBL[effect]);
            tm = 20;
        }

        ov7725_camera_refresh();	/* 更新显示 */

        if (tm)
        {
            lcd_show_string((lcddev.width - 240) / 2 + 30,
(lcddev.height-320)/2 + 60,200,16,16, msgbuf,BLUE);
            tm--;
        }

        i++;

        if (i >= 15) 	
        {
            i = 0;
            LED0_TOGGLE(); 	/* LED0闪烁 */
        }
    }
}

main函数的具体流程可以参考程序流程图,代码比较简单,这里我们就不展开说明了。
到此摄像头的使用过程我们讲解完了,大家可以参考光盘中的源码进行测试和修改,也可以在USMART中加入OV7725的测试接口ov7725_write_reg和ov7725_read_reg,轻松调试摄像头。
最后,为了得到最快的显示速度,我们可以把MDK的代码优化等级设置为-O2级别(在C/C++选项卡设置),这样OV7725的显示帧率可达23帧。注意:因为tpad_scan扫描会占用比较多的时间,所以帧率比较慢,屏蔽该函数,也可以提高帧率。
46.4 下载验证
在代码编译成功之后,下载代码到正点原子战舰STM32F103开发板上,得到如图46.4.1所示界面:

图46.4.1程序运行效果图
随后,我们通过KEY0和KEY1选择模式。当选择QVGA模式时,OV7725直接输出320240分辨率图像数据,该模式相对VGA模式视角较广但画面没有那么清晰细腻。当选择VGA模式时,实质是将640480窗口截取中间320*240的图像输出,该模式拍出的图像较为清晰细腻但视角较小。
我们可以按不同的按键(KEY0~KEY2、KEY_UP、TPAD等),来设置摄像头的相关参数和模式,得到不同的成像效果。同时,我们可以通过USMART调用ov7725_write_reg等函数,来设置OV7725的各寄存器等,达到调试测试的目的,具体如图46.4.2所示:

图46.4.2 USMART调试OV7725
从上图还可以看出,LCD显示帧率为10帧左右(没有代码优化),而实际上OV77225的输出速度是30帧。图中,我们是可以通过USMART发送ov7725_write_reg(0x66, 0x20)设置OV7725输出彩条,方便测试。
摄像头的实验我们就讲解到这里。

物联沃分享整理
物联沃-IOTWORD物联网 » 正点原子STM32连载 第46章 摄像头实验(摘自正点原子STM32F103战舰开发指南V1.2)

发表评论