【ALIENTEK 战舰STM32开发板例程系列连载+教学】第四十一章 摄像头实验

2019-08-16 20:51发布

 

第四十一章 摄像头实验

ALIENTEK战舰STM32开发板板载了一个摄像头接口(P8),该接口可以用来连接ALIENTEK OV7670摄像头模块。本章,我们将使用STM32驱动ALIENTEK OV7670摄像头模块,实现摄像头功能。本章分为如下几个部分: 41.1 OV7670简介 41.2 硬件设计 41.3 软件设计 41.4 下载验证

41.1 OV7670简介

OV7670OVOmniVision)公司生产的一颗1/6寸的CMOS VGA图像传感器。该传感器体积小、工作电压低,提供单片VGA摄像头和影像处理器的所有功能。通过SCCB 总线控制,可以输出整帧、子采样、取窗口等方式的各种分辨率8位影像数据。该产品VGA图像最高达到30/秒。用户可以完全控制图像质量、数据格式和传输方式。所有图像处理功能过程包括伽玛曲线、白平衡、度、 {MOD}度等都可以通过SCCB接口编程。OmmiVision 图像传感器应用独有的传感器技术,通过减少或消除光学或电子缺陷如固定图案噪声、托尾、浮散等,提高图像质量,得到清晰的稳定的彩 {MOD}图像。 OV7670的特点有: l  高灵敏度、低电压适合嵌入式应用 l  标准的SCCB接口,兼容IIC接口 l  支持RawRGBRGB(GBR4:2:2RGB565/RGB555/RGB444)YUV(4:2:2)YCbCr4:22)输出格式 l  支持VGACIF,和从CIF40*30的各种尺寸输出 l  支持自动曝光控制、自动增益控制、自动白平衡、自动消除灯光条纹、自动黑电平校准等自动控制功能。同时支持 {MOD}饱和度、 {MOD}相、伽马、锐度等设置。 l  支持闪光灯 l  支持图像缩放 OV7670的功能框图图如图41.1.1所示:
41.1.1 OV7670功能框图        OV7670传感器包括如下一些功能模块。        1.感光整列(Image Array OV7670总共有656*488个像素,其中640*480个有效(即有效像素为30W)。        2.时序发生器(Video Timing Generator 时序发生器具有的功能包括:整列控制和帧率发生(7种不同格式输出)、内部信号发生器和分布、帧率时序、自动曝光控制、输出外部时序(VSYNCHREF/HSYNCPCLK)。 3.模拟信号处理(Analog Processing 模拟信号处理所有模拟功能,并包括:自动增益(AGC)和自动白平衡(AWB)。 4.A/D 转换(A/D 原始的信号经过模拟处理器模块之后 ,分GBR两路进入一个10 位的A/D 转换器,A/D 转换器工作在12M频率,与像素频率完全同步(转换的频率和帧率有关)。 A/D转换器外,该模块还有以下三个功能: l  黑电平校正(BLC) l  U/V通道延迟 l  A/D范围控制 A/D范围乘积和A/D的范围控制共同设置A/D的范围和最大值,允许用户根据应用调整图片的亮度。 5.测试图案发生器(Test Pattern Generator 测试图案发生器功能包括:八 {MOD}彩 {MOD}条图案、渐变至黑白彩 {MOD}条图案和输出脚移位“1”。 6.数字处理器(DSP 这个部分控制由原始信号插值到RGB 信号的过程,并控制一些图像质量: l  边缘锐化(二维高通滤波器) l  颜 {MOD}空间转换( 原始信号到RGB 或者YUV/YCbYCr) l  RGB {MOD}彩矩阵以消除串扰 l  {MOD}相和饱和度的控制 l  /白点补偿 l  降噪 l  镜头补偿 l  可编程的伽玛 l  十位到八位数据转换 7.缩放功能(Image Scaler 这个模块按照预先设置的要求输出数据格式,能将YUV/RGB信号从VGA缩小到CIF以下的任何尺寸。 8.数字视频接口(Digital Video Port 通过寄存器COM2[1:0],调节IOL/IOH的驱动电流,以适应用户的负载。 9.SCCB接口(SCCB Interface SCCB接口控制图像传感器芯片的运行,详细使用方法参照光盘的《OmniVision Technologies Seril Camera Control Bus(SCCB) Specification》这个文档 10.LED和闪光灯的输出控制(LED and Storbe Flash Control Output OV7670有闪光灯模式,可以控制外接闪光灯或闪光LED的工作。 OV7670的寄存器通过SCCB时序访问并设置,SCCB时序和IIC时序十分类似,在本章我们不做介绍,请大家参考光盘的相关文档。 接下来我们介绍一下OV7670的图像数据输出格式。首先我们简单介绍几个定义: VGA,即分辨率为640*480的输出模式; QVGA,即分辨率为320*240的输出格式,也就是本章我们需要用到的格式; QQVGA,即分辨率为160*120的输出格式; PCLK,即像素时钟,一个PCLK时钟,输出一个像素(或半个像素) VSYNC,即帧同步信号。 HREF /HSYNC,即行同步信号。 OV7670的图像数据输出(通过D[7:0])就是在PCLKVSYNCHREF/ HSYNC的控制下进行的。首先看看行输出时序,如图41.1.2所示:
41.1.2 OV7670行输出时序 从上图可以看出,图像数据在HREF为高的时候输出,当HREF变高后,每一个PCLK时钟,输出一个字节数据。比如我们采用VGA时序,RGB565格式输出,每2个字节组成一个像素的颜 {MOD}(高字节在前,低字节在后),这样每行输出总共有640*2PCLK周期,输出640*2个字节。 再来看看帧时序(VGA模式),如图41.1.3所示:
41.1.3 OV7670帧时序 上图清楚的表示了OV7670VGA模式下的数据输出,注意,图中的HSYNCHREF其实是同一个引脚产生的信号,只是在不同场合下面,使用不同的信号方式,我们本章用到的是HREF 因为OV7670的像素时钟(PCLK)最高可达24Mhz,我们用STM32F103ZET6IO口直接抓取,是非常困难的,也十分占耗CPU(可以通过降低PCLK输出频率,来实现IO口抓取,但是不推荐)。所以,本章我们并不是采取直接抓取来自OV7670的数据,而是通过FIFO读取,ALIENTEK OV7670摄像头模块自带了一个FIFO芯片,用于暂存图像数据,有了这个芯片,我们就可以很方便的获取图像数据了,而不再需要单片机具有高速IO,也不会耗费多少CPU,可以说,只要是个单片机,都可以通过ALIENTEK OV7670摄像头模块实现拍照的功能。 接下来我们介绍一下ALIENTEK OV7670摄像头模块。该模块的外观如图41.1.4
41.1.4 ALIENTEK OV7670摄像头模块外观图 模块原理图如图41.1.5所示:
41.1.5 ALIENTEK OV7670摄像头模块原理图        从上图可以看出,ALIENTEK OV7670摄像头模块自带了有源晶振,用于产生12M时钟作为OV7670XCLK输入。同时自带了稳压芯片,用于提供OV7670稳定的2.8V工作电压,并带有一个FIFO芯片(AL422B),该FIFO芯片的容量是384K字节,足够存储2QVGA的图像数据。模块通过一个2*9的双排排针(P1)与外部通信,与外部的通信信号如表41.1.1所示: 信号 作用描述 信号 作用描述 VCC3.3 模块供电脚,接3.3V电源 FIFO_WEN FIFO写使能 GND 模块地线 FIFO_WRST FIFO写指针复位 OV_SCL SCCB通信时钟信号 FIFO_RRST FIFO读指针复位 OV_SDA SCCB通信数据信号 FIFO_OE FIFO输出使能(片选) FIFO_D[7:0] FIFO输出数据(8位) OV_VSYNC OV7670帧同步信号 FIFO_RCLK FIFO时钟 41.1.1 OV7670模块信号及其作用描述        下面我们来看看如何使用ALIENTEK OV7670摄像头模块(以QVGA模式,RGB565格式为例)。对于该模块,我们只关心两点:1,如何存储图像数据;2,如何读取图像数据。        首先,我们来看如何存储图像数据。        ALIENTEK OV7670摄像头模块存储图像数据的过程为:等待OV7670同步信号àFIFO写指针复位àFIFO写使能à等待第二个OV7670同步信号àFIFO写禁止。通过以上5个步骤,我们就完成了1帧图像数据的存储。        接下来,我们来看看如何读取图像数据。      在存储完一帧图像以后,我们就可以开始读取图像数据了。读取过程为:FIFO读指针复位àFIFO读时钟(FIFO_RCLKà读取第一个像素高字节àFIFO读时钟à读取第一个像素低字节àFIFO读时钟à读取第二个像素高字节à循环读取剩余像素à结束。        可以看出,ALIENTEK OV7670摄像头模块数据的读取也是十分简单,比如QVGA模式,RGB565格式,我们总共循环读取320*240*2次,就可以读取1帧图像数据,把这些数据写入LCD模块,我们就可以看到摄像头捕捉到的画面了。        OV7670还可以对输出图像进行各种设置,详见光盘《OV7670 中文数据手册1.01》和《OV7670 software application note》这两个文档,对AL422B的操作时序,请大家参考AL422B的数据手册。        了解了OV7670模块的数据存储和读取,我们就可以开始设计代码了,本章,我们用一个外部中断,来捕捉帧同步信号(VSYNC),然后在中断里面启动OV7670模块的图像数据存储,等待下一次VSHNC信号到来,我们就关闭数据存储,然后一帧数据就存储完成了,在主函数里面就可以慢慢的将这一帧数据读出来,放到LCD即可显示了,同时开始第二帧数据的存储,如此循环,实现摄像头功能。 本章,我们将使用摄像头模块的QVGA输出(320*240),刚好和战舰STM32开发板使用的LCD模块分辨率一样,一帧输出就是一屏数据,提高速度的同时也不浪费资源。注意:ALIENTEK OV7670摄像头模块自带的FIFO是没办法缓存一帧的VGA图像的,如果使用VGA输出,那么你必须在FIFO写满之前开始读FIFO数据,保证数据不被覆盖。       

41.2 硬件设计

本章实验功能简介:开机后,初始化摄像头模块(OV7670),如果初始化成功,则在LCD模块上面显示摄像头模块所拍摄到的内容。我们可以通过KEY0设置光照模式(5种模式)、通过KEY1设置 {MOD}饱和度,通过KEY2设置亮度,通过WK_UP设置对比度,通过TPAD设置特效(总共7种特效)。通过串口,我们可以查看当前的帧率(这里是指LCD显示的帧率,而不是指OV7670的输出帧率),同时可以借助USMART设置OV7670的寄存器,方便大家调试。DS0指示程序运行状态。 本实验用到的硬件资源有: 1)  指示灯DS0 2)  5个按键(包括TPAD触摸按键) 3)  串口 4)  TFTLCD模块 5)  摄像头模块 ALIENTEK OV7670摄像头模块在41.1节已经有详细介绍过,这里我们主要介绍该模块与ALIETEK 战舰STM32开发板的连接。 在开发板的左下角的2*9P8排座,是摄像头模块/OLED模块共用接口,在第十七章,我们曾简单介绍过这个接口。本章,我们只需要将ALIENTEK OV7670摄像头模块插入这个接口即可,该接口与STM32的连接关系如图41.2.1所示:
41.2.1 摄像头模块接口与STM32连接图       从上图可以看出,OV7670摄像头模块的各信号脚与STM32的连接关系为:        OV_SDAPG13 OV_SCLPD3 FIFO_RCLKPB4 FIFO_WENPB3        FIFO_WRSTPD6        FIFO_RRSTPG14 FIFO_OEPG15 OV_VSYNCPA8        OV_D[7:0]PC[7:0] 这些线的连接,战舰STM32的内部已经连接好了,我们只需要将OV7670摄像头模块插上去就好了。实物连接如图41.2.2所示:
41.2.2 OV7670摄像头模块与开发板连接实物图

41.3 软件设计

打开上一章的工程,首先在HARDWARE文件夹下新建一个OV7670的文件夹。然后新建如下文件:ov7670.csccb.cov7670.hsccb.hov7670cfg.h5个文件,将他们保存在OV7670文件夹下,并将这个文件夹加入头文件包含路径。        本章总共新增了5个文件,代码比较多,我们就不一一列出了,仅挑两个重要的地方进行讲解。首先,我们来看ov7670.c里面的OV7670_Init函数,该函数代码如下: u8 OV7670_Init(void) {        u8 temp; u16 i=0;          //设置IO        RCC->APB2ENR|=1<<2;            //先使能外设PORTA时钟        RCC->APB2ENR|=1<<3;            //先使能外设PORTB时钟       RCC->APB2ENR|=1<<4;            //先使能外设PORTC时钟       RCC->APB2ENR|=1<<5;            //先使能外设PORTD时钟        RCC->APB2ENR|=1<<8;            //先使能外设PORTG时钟              GPIOA->CRH&=0XFFFFFFF0;           GPIOA->CRH|=0X00000008;     //PA8 输入         GPIOA->ODR|=1<<8;      GPIOB->CRL&=0XFFF00FFF;            GPIOB->CRL|=0X00033000;      //PB3/4 输出        GPIOB->ODR|=3<<3;            GPIOC->CRL=0X88888888;       //PC0~7 输入           GPIOC->ODR|=0x00ff;      GPIOD->CRL&=0XF0FFFFFF; //PD6 输出         GPIOD->CRL|=0X03000000;              GPIOD->ODR|=1<<6;      GPIOG->CRH&=0X00FFFFFF;          GPIOG->CRH|=0X33000000;                 GPIOG->ODR=7<<14;                         //PG14/15  输出高                 JTAG_Set(SWD_ENABLE);       SCCB_Init();                                      //初始化SCCB IO                    if(SCCB_WR_Reg(0x12,0x80))return 1;     //复位SCCB            delay_ms(50);        //读取产品型号       temp=SCCB_RD_Reg(0x0b);          if(temp!=0x73)return 2;        temp=SCCB_RD_Reg(0x0a);          if(temp!=0x76)return 2;        //初始化序列           for(i=0;i<sizeof(ov7670_init_reg_tbl)/sizeof(ov7670_init_reg_tbl[0]);i++)        {             SCCB_WR_Reg(ov7670_init_reg_tbl[0],ov7670_init_reg_tbl[1]);               delay_ms(2);       }      return 0x00;   //ok }        此部分代码先初始化OV7670相关的IO口(包括SCCB_Init),然后最主要的是完成OV7670的寄存器序列初始化。OV7670的寄存器特多(百几十个),配置特麻烦,幸好厂家有提供参考配置序列(详见OV7670 software application note),本章我们用到的配置序列,存放在ov7670_init_reg_tbl这个数组里面,该数组是一个2维数组,存储初始化序列寄存器及其对应的值,该数组存放在ov7670cfg.h里面。        接下来,我们看看ov7670cfg.h里面ov7670_init_reg_tbl的内容,ov7670cfg.h文件的代码如下: //初始化寄存器序列及其对应的值 const u8 ov7670_init_reg_tbl[][2]= {        //以下为OV7670 QVGA RGB565参数       {0x3a, 0x04},//        {0x40, 0x10},        {0x12, 0x14},//QVGA,RGB输出        ……省略部分设置        {0x6e, 0x11},//100        {0x6f, 0x9f},//0x9e for advance AWB     {0x55, 0x00},//亮度     {0x56, 0x40},//对比度     {0x57, 0x80},//0x40,  change according to Jim's request   };        以上代码,我们省略了很多(全部贴出来太长了),我们大概了解下结构,每个条目的第一个字节为寄存器号(也就是寄存器地址),第二个字节为要设置的值,比如{0x3a, 0x04},就表示在0X03地址,写入0X04这个值。        通过这么一长串(110多个)寄存器的配置,我们就完成了OV7670的初始化,本章我们配置OV7670工作在QVGA模式,RGB565格式输出。 在完成初始化之后,我们既可以开始读取OV7670的数据了。        OV7670文件夹里面的其他代码我们就不逐个介绍了,请大家参考光盘该例程源码。        因为本章我们还用到了帧率(LCD显示的帧率)统计和中断处理,所以我们还需要修改timer.ctimer.hexti.cexti.h这几个文件。        timer.c里面,我们新增TIM6_Int_InitTIM6_IRQHandler两个函数,用于统计帧率,增加代码如下: u8 ov_frame;        //统计帧数 //定时器6中断服务程序     void TIM6_IRQHandler(void) {                                                              if(TIM6->SR&0X0001)//溢出中断        {                                            printf("frame:%dfps ",ov_frame);    //打印帧率               ov_frame=0;                                                                                                }                                    TIM6->SR&=~(1<<0);//清除中断标志位        } //基本定时器6中断初始化 //这里时钟选择为APB12倍,而APB136M //arr:自动重装值。 //psc:时钟预分频数 //这里使用的是定时器3! void TIM6_Int_Init(u16 arr,u16 psc) {        RCC->APB1ENR|=1<<4;//TIM6时钟使能          TIM6->ARR=arr;        //设定计数器自动重装值//刚好1ms           TIM6->SC=psc;         //预分频器7200,得到10Khz的计数时钟        TIM6->DIER|=1<<0;   //允许更新中断                  TIM6->CR1|=0x01;    //使能定时器3       MY_NVIC_Init(1,3,TIM6_IRQChannel,2);//抢占1,子优先级3,组2                        } 这里,我们用到基本定时器TIM6来统计帧率,也就是1秒钟中断一次,打印ov_frame的值,ov_frame用于统计LCD帧率。 再在timer.h里面添加TIM6_Int_Init函数的定义,就完成对timer.ctimer.h的修改了。 exti.c里面添加EXTI8_InitEXTI9_5_IRQHandler函数,用于OV7670模块的FIFO写控制,exti.c文件新增部分代码如下: u8 ov_sta;  //外部中断5~9服务程序 void EXTI9_5_IRQHandler(void) {                               if(EXTI->R&(1<<8))//8线的中断        {                   if(ov_sta<2)               {                      if(ov_sta==0)                      {                             OV7670_WRST=0;            //复位写指针                                                    OV7670_WRST=1; OV7670_WREN=1;//允许写入FIFO                                            }else                      {                             OV7670_WREN=0;             //禁止写入FIFO                             OV7670_WRST=0; OV7670_WRST=1;//复位写指针                             }                      ov_sta++;               }        }        EXTI->R=1<<8;     //清除LINE8上的中断标志位                                          } //外部中断8初始化 void EXTI8_Init(void) {                                                                                           Ex_NVIC_Config(GPIO_A,8,RTIR);                      //任意边沿触发                            MY_NVIC_Init(0,0,EXTI9_5_IRQChannel,2);          //抢占0,子优先级0,组2       } 因为OV7670的帧同步信号(OV_VSYNC)接在PA8上面,所以我们这里配置PA8作为中端输入,因为STM32的外部中断5~9共用一个中端服务函数(EXTI9_5_IRQHandler),所以在该函数里面,我们需要先判断中断是不是来自中断线8的,然后再做处理。 中断处理部分很简单,通过一个ov_sta来控制OV7670模块的FIFO写操作。当ov_sta=0的时候,表示FIFO存储的数据已经被成功读取了(ov_sta在读完FIFO数据的时候被清零),然后只要OV_VSYNC信号到来,我们就先复位一下写指针,然后ov_sta=1,标志着写指针已经复位,目前正在往FIFO里面写数据。再等下一个OV_VSYNC到来,也就表明一帧数据已经存储完毕了,此时我们设置OV7670_WREN0,禁止再往OV7670写入数据,此时ov_sta自增为2。其他程序,只要读到ov_sta2,就表示一帧数据已经准备好了,可以读出,在读完数据之后,程序设置ov_sta0,则开启下一轮FIFO数据存储。 再在exti.h里面添加EXTI8_Init函数的定义,就完成对exti.cexti.h的修改了。 最后,打开test.c文件,修改代码如下: const u8*LMODE_TBL[5]={"Auto","Sunny","Cloudy","Office","Home"};     //5种光照模式 const u8*EFFECTS_TBL[7]={"Normal","Negative","B&W","Redish","Greenish","Bluish", "Antique"};//7种特效 extern u8 ov_sta;           //exit.c里面定义 extern u8 ov_frame;      //timer.c里面定义           //更新LCD显示 void camera_refresh(void) { &
友情提示: 此问题已得到解决,问题已经关闭,关闭后问题禁止继续编辑,回答。
该问题目前已经被作者或者管理员关闭, 无法添加新回复
33条回答
fayuanye
1楼-- · 2019-08-17 02:29
沙发!!谢谢斑竹分享!!学习学习!
ilikerome
2楼-- · 2019-08-17 04:01
回复【楼主位】正点原子:
---------------------------------
请教原子大侠,我的显示只有2fps,直接刷你的源程序也是这样,可能是什么原因啊?怎么排查?谢谢!
正点原子
3楼-- · 2019-08-17 06:15
回复【3楼】ilikerome:
---------------------------------
2帧不止吧。。。
战舰板上测试的?
ilikerome
4楼-- · 2019-08-17 07:20
回复【4楼】正点原子:
---------------------------------
是的,找到原因了,跳线帽没有把ADC与TPAD连到一起去,导致在TPAD检测这里循环了很长时间。
原子大侠什么时候推一个带任务调度的比较实用的教程吧,现在这个delay实在伤不起。
还有你的xmos HIFI啥时候搞好啊,等着买个呢。
正点原子
5楼-- · 2019-08-17 07:31
 精彩回答 2  元偷偷看……
ilikerome
6楼-- · 2019-08-17 13:26
回复【6楼】正点原子:
---------------------------------
还没搞到ucosii那里呢。
ok,先买的能不能等你后续的改进型搞好了,换购啊,哈哈。

一周热门 更多>