OV7670摄像头带FIFO实时显示,基于STM32F411CEU6
一、简单介绍
笔者使用带FIFO的OV7670模块,主控选择F411CEU6,将采集的图像显示在一块由ST7789芯片驱动的240*240分辨率的彩屏上。

最终效果

OV7670传感器芯片框图如下:

D[7:0]是输出的图片信息,宽度8的数据总线。
SCCB(Serial Camera Control Bus)接口,类似I2C,用于操作芯片内部寄存器,可以用HAL库的I2C接口读写。
![]()
左下角是一些视频时序信号。

芯片内有一个656*488大小的像素阵列,实际用到的是640*480。

可选VGA到CIF等大小的图片输出。

可选3种测试图案:8彩条,带灰度的彩条,移位1。

和别的模块相比,摄像头模块会涉及一些术语,如果不了解这些名词的含义,开发的时候就会很被动。部分如下:
YUY:一种颜色编码方法,RGB也是一种色彩编码方法。YUV 颜色编码采用的是 明亮度 和 色度 来指定像素的颜色。其中,Y 表示明亮度(Luminance、Luma),而 U 和 V 表示色度(Chrominance、Chroma)。
HREF:指示当前这一行的像素数据是有效的,在一整行像素期间保持高电平
PCLK:(Pixel Clock,像素时钟)是摄像头并行接口里最核心的时钟信号,PCLK 每跳变一次,就有一份像素数据要被采样。用来同步数据总线上的数据
VGA即“Video Graphics Array”。是IBM在1987年随PS/2机一起推出的一种视频传输标准,具有分辨率高、显示速率快、颜色丰富等优点,在彩色显示器领域得到了广泛的应用。VGA最早指的是显示器640X480。VGA的英文全称是Video GraphicArray,也叫显示绘图阵列。
QVGA即"Quarter VGA"。中VGA的四分之一尺寸,亦即在液晶屏幕(LCD)上输出的分辨率是240×320像素。QVGA支持屏幕旋转,可以开发出相应的程序,以显示旋转90°、180°、270°屏幕位置。由HandEra公司发布。多用于手持/移动设备。
CIF是常用的标准化图像格式(Common Intermediate Format)。在H.323协议簇中,规定了视频采集设备的标准采集分辨率。CIF = 352×288像素
QCIF全称Quarter common intermediate format。QCIF也是常用的标准化图像格式。在H.323中,规定QCIF = 176×144像素。
AGC (Automatic Gain Control)自动增益控制
AWB(Automatic White Balance )自动白平衡
AEC(Automatic Exposure Control)自动曝光控制

当图像信息有效时,HREF输出高电平,每来一个像素时钟PCLK,摄像头输出一个字节的图像信息。

如图为摄像头输出VGA的时序,VSYNC从高到低后等待17个tline(784tp)HREF被拉高,有效像素字节开始输出,在一行像素(640个)输出期间HREF一直保持高电平,消耗640个tp。输出完一帧(480行)的像素后,再等待10个tline,VSYNC引脚会被拉高。


在VSYNC的下降沿后,HREF会频繁变化,且两两VSYNC之间有480个HREF上升沿,代表480行像素。当HREF为高时,表明摄像头正在传输一行的内容,每行有640个像素点。

对于RGB565而言,一个像素包含2字节信息,因此一个tp等于2个像素时钟tpclk。
笔者使用的是带FIFO的OV7670模组,FIFO芯片的型号是AL422B,引脚定义如下:

OV7670传感器将采集到的数据D0~D7引脚连接到FIFO芯片的数据输入引脚,开发重点就是MCU从FIFO中把摄像头采集的图片信息取出,并显示到TFT彩屏上。

二、cubeMX配置
I2C引脚设置上拉

SPI用于刷新屏幕

开启DMA并使能SPI中断


GPIOA0~A7设置输入模式,用于读取FIFO数据

用于控制FIFO的引脚均设置低电平,低速率

用于控制屏幕的引脚设置输出模式。笔者为减少杜邦线的使用,将控制屏幕背光的BL引脚短接至VCC,读者若是使用GPIO控制,请开启代码中注释的相关接口和函数。

PB12作为外部中断,下降沿触发


使用HSE时钟

三、驱动开发
摄像头使用的SCCB协议很相似,实测可以用I2C控制读写。
读数据
HAL_StatusTypeDef OV7670_RD(uint8_t addr, uint8_t* val)
{
HAL_I2C_Master_Transmit(&hi2c1, 0x42, (uint8_t*)(&addr), 1, 0xFF);
HAL_I2C_Master_Receive(&hi2c1, 0x43, val, 1, 0xff);
}
写数据
HAL_StatusTypeDef OV7670_WR(uint8_t addr, uint8_t val)
{
uint8_t send[2] = {addr, val};
HAL_I2C_Master_Transmit(&hi2c1, 0x42, send, 2, 0xFF);
}
读OV7670的ID
uint8_t OV7670_CheckID()
{
uint8_t re[2];
OV7670_RD(0xA, re);
OV7670_RD(0xB, re + 1);
if (re[0] == 0x76 && re[1] == 0x73)
{
return 1;
}
return 0;
}


读到0x76和0x73说明通信正常。
摄像头的寄存器配置较为繁琐,笔者使用网上大神分享的寄存器表格。

输出像素点数量选择QVGA(240*320),格式选择RGB565。
COM7寄存器0x12写入0x14。写入后可以发现每个VSYNC之间的上升沿变成240个,表明有240行像素。

摄像头输出的窗口大小也可以设置,分别有四个:水平起始,水平结束,垂直起始,垂直结束。

寄存器地址0x17~0x1A分别是它们的高8位,剩余的低3位和低2位在HREF寄存和VREF寄存器中,如下:


0x70和0x71寄存器用于显示测试图案,建议刚开始调试的时候开启来,方便查找问题。

0x70的最高位和0x71的最高位共同决定显示的测试图案。
|
0x70 |
0x71 |
图案 |
|
0x80 |
0x80 |
带灰度的彩条 |
|
0x80 |
0x00 |
移位1 |
|
0x00 |
0x80 |
彩条 |
FIFO写入时序


当/WE是低电平时,WCK的上升沿来到,就写入FIFO,写地址自增。

WCK被连接到摄像头的OV_PLCK像素时钟,摄像头输出的OV_HREF和排针FIFO_WEN经过一个与非门,输出到FIFO的写使能引脚FIFO_WE。摄像头输出数据总线被连接至FIFO数据输入总线。这样当排针拉高时,FIFO由摄像头OV_HREF控制写入,排针拉低时,FIFO_WE一直是高,FIFO被禁止写入。用这种方式,FIFO可以存放摄像头高速输出的图片信息,同时也提供给我们一个控制是否允许摄像头写入的接口。
FIFO读取时序


/RE控制能否从FIFO中读出数据,模块已经将/RE接地了就不用考虑。


/OE拉低时,在RCK(读时钟)的上升沿就可以对数据总线引脚读取,如果/OE是高电平,此时数据总线是高阻态。除了基本的读写时序,还要注意读地址和写地址的控制:应先让FIFO的读地址复位至0,使摄像头输出字节从FIFO的0地址依次存放,存放完一帧的内容。再将读地址复位至0,从0地址开始把刚才写入的图像数据读出。
写地址复位

WCK被连接到PCLK,一直有信号,我们只需要控制WRST引脚从高变低再变高就完成写地址的复位。
void FIFO_WReset()
{
HAL_GPIO_WritePin(WRST_GPIO_Port, WRST_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(WRST_GPIO_Port, WRST_Pin, GPIO_PIN_SET);
}
读地址复位
void FIFO_RDReset()
{
HAL_GPIO_WritePin(RRST_GPIO_Port, RRST_Pin, GPIO_PIN_RESET);
FIFO_RCK(0);
FIFO_RCK(1);
FIFO_RCK(0);
FIFO_RCK(1);
HAL_GPIO_WritePin(RRST_GPIO_Port, RRST_Pin, GPIO_PIN_SET);
}

四、总体设计
对于摄像头来说,FIFO很快就被填满,而MCU读取很慢,所以在摄像头填满FIFO后,禁止摄像头继续写入FIFO,直到FIFO内容被MCU读出。
VSYNC的下降沿来临时,表明摄像头要开始传输一帧的数据,MCU拉低WR,允许摄像头写入FIFO。当下降沿再次到来时,表明刚才的一帧数据写入完毕,拉高WR,复位写指针。读取FIFO内容到RAM并显示,读取时,应先复位读指针,再用for循环读取。
主函数
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_I2C1_Init();
MX_SPI1_Init();
MX_TIM10_Init();
/* USER CODE BEGIN 2 */
HAL_TIM_Base_Start_IT(&htim10);
OV7670_Reset();
// HAL_Delay(5);
ST7789_Init();
ST7789_Clear(RED);
OV7670_READY = OV7670_CheckID();
if (OV7670_READY)
{
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);
OV7670_Config();
OV7670_SetWindow(20,176,240,240);
}
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
if (OV7670_READY)
{
if (gather == 1)
{
/* read it*/
FIFO_OE(0);
FIFO_RDReset();
for (uint32_t i = 0; i < 240 * 240 * 2; i++)
{
FIFO_RCK(0);
FIFO_RCK(1);
TFT[i] = GPIOA->IDR & 0xFF;
}
ST7789_Address_Set(0, 0, 239, 239);
for (uint16_t k = 0; k < 2; k++)
{
ST7789_SendMultiByte(TFT + k * 240 * 240, 240 * 240);
}
FIFO_OE(1);
gather = 0;
}
sprintf(str, "%d", show);
ST7789_ShowString(0, 0, str, Font_16x26, RED, WHITE);
}
else
{
ST7789_ShowString(0, 110, "dont detect OV7670", Font_16x26, RED, WHITE);
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
显示帧率
使用一个变量计算一秒钟有多少图像显示到彩屏上就可以得到帧率。开启一个定时周期是1s的定时器中断,中断触发时,赋值计数值给显示值并复位计数值。
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
show = cnt;
cnt = 0;
}
外部中断
每完成一帧图像的读取,LED就翻转一次,图像计数器自增。
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (gather == 0)
{
FIFO_WReset();
FIFO_WR(1);
gather = 1;
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
cnt++;
}
else
{
/* FIFO stored a whole frame */
FIFO_WR(0);
}
}
五、后记
注意点
-
窗口寄存器0x17~0x1A,0x03,0x32的设置比较关键。
-
如果彩条可以正常显示,但关闭彩条后摄像头白茫茫的一片,是焦距问题,拧几圈镜头就可以变得清晰。
-
如果彩条无法显示,大概率是摄像头寄存器配置问题,关注窗口的配置。当没有头绪的时候也不要灰心,保持耐心,请相信自己。
-
使用杜邦线连接易受干扰,注意数据总线和VSYNC线,FIFO控制线保持一定距离,减少干扰,可以试试把GPIO配低速。

用逻辑分析仪监测VSYNC引脚,每隔大约30ms跳变一次,理论上如果摄像头输出的数据直接显示在屏幕上,帧率约30。

对FIFO数据线和屏幕片选线采样,完成一次屏幕刷新和FIFO读取耗时大约134ms,也就是一秒钟7帧。读取FIFO占用较多时间,接近100ms;屏幕刷新大约32ms。
在读取FIFO的代码中使用寄存器操作RCK引脚而非使用HAL库可以大幅提高读取速度,但也会变得极为敏感,用杜邦线极易受到干扰。笔者测试可以达到每秒10帧,提升50%,后续设计PCB再测试。使用5V供电,整体电流约100mA。
完整工程可在笔者的公众号“田甲电子”私信 15_OV7670_FIFO 获取。
提前祝大家马年快乐,万事如意,马到成功!
更多推荐



所有评论(0)