ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

不带FIFO的OV7670与STM32F407图像采集实战:DCMI+DMA驱动与LCD显示

不带FIFO的OV7670与STM32F407图像采集实战:DCMI+DMA驱动与LCD显示 0. 项目背景把不带FIFO缓冲的OV7670摄像头接到STM32F407上做图像采集并在LCD上实时显示这件事听起来简单实际上有不少坑。网上很多教程用的都是带FIFO的OV7670模块那样MCU可以慢悠悠地读数据但不带FIFO的版本像素数据是实时往外吐的主控必须按像素时钟一个接一个地收时序稍微不对画面就花给你看。我做这个实验用的是STM32F407VET6核心板、一个很普通的OV7670摄像头模块无FIFO版本、一块2.8寸TFT LCD屏ILI9341驱动8080并口以及一块很常见的逻辑分析仪。整体调试下来从硬件连线到画面稳定显示大概花了两个晚上的时间中间折腾最多的不是摄像头本身反而是LCD初始化地址和DCMI的极性配置。这篇文章把整个过程和踩过的坑整理出来适合正在学STM32图像采集和摄像头驱动的朋友尤其是那些手里只有一个不带FIFO的OV7670模块、又不想再花钱去买带FIFO版本的人。1. 方案选型为什么用不带FIFO的OV76701.1 带FIFO与不带FIFO的本质区别市面上常见的OV7670模块分为两大类带FIFO通常是AL422B芯片和不带FIFO。带FIFO的模块摄像头先把一帧图像数据写进板载的FIFO芯片里MCU想什么时候读就什么时候读完全不关心摄像头的像素输出时序。不带FIFO的模块OV7670的D0~D7数据线直接连到MCU摄像头在PCLK的节拍下持续输出像素数据MCU必须在每个PCLK上升沿把数据取走慢一步就丢像素。带FIFO方案的优点是MCU几乎不挑时序随便一个GPIO模拟时序都能把数据读出来缺点是多了一层缓冲成本高一些而且画面刷新率受限于MCU从FIFO读数据的速度。不带FIFO方案的优点是电路更简单、成本更低数据链路短延迟小但这对MCU的接口要求高了很多普通GPIO模拟是扛不住的最好用MCU自带的DCMI接口加上DMA。1.2 为什么STM32F407是合适的搭配STM32F407这颗芯片在图像采集方面有一个非常关键的外设DCMIDigital Camera Interface数字摄像头接口。它可以接收8位或16位并行的摄像头数据同时支持VSYNC、HREF、PCLK三个同步信号而且DCMI自带FIFO缓冲。这意味着即使外部的OV7670没有FIFOMCU内部也有一个小缓冲来应对DMA响应延迟的问题不至于因为CPU忙别的事情而丢数据。再配合DMADCMI每收到一个像素数据就可以自动搬运到内存里不需要CPU参与。F407的主频最高168MHz工作在144MHz或168MHz时DCMIDMA的组合能够轻松应付OV7670在VGA分辨率640x480下30fps的数据量。当然实际上为了显示流畅我会把输出分辨率配置成QVGA320x240甚至更小。2. 硬件连接与初始化配置2.1 OV7670引脚定义与接线OV7670模块的引脚看起来有点多但梳理清楚后并不多。核心信号有:供电3.3V和GND、SCCB接口SIOC和SIOD用于配置寄存器、像素数据输出D0~D7、像素时钟PCLK、行同步HREF、帧同步VSYNC以及主时钟输入XCLK。还有一个RESET引脚和PWDN引脚分别用于复位和掉电控制。我实际接线如下基于STM32F407VET6使用DCMI的固定引脚映射OV7670引脚STM32F407引脚说明SIOCPB10SCCB时钟用软件I2C模拟SIODPB11SCCB数据用软件I2C模拟D0~D7PB8, PB6, PB9, PB7, PE5, PE6, PE7, PE8像素数据线对应DCMI_D0~DCMI_D7PCLKPA6像素时钟对应DCMI_PCKHREFPA4行同步信号对应DCMI_HSYNCVSYNCPB7帧同步信号这里要注意我用了DCMI_VSYNC是PB7XCLKPA8摄像头主时钟由MCU的TIM1输出或使用MCORESET3.3V直接拉高不复位PWDNGND拉低正常工作模式有个细节需要注意STM32F407的DCMI引脚是MUX映射固定的不能随便改。DCMI_PCK必须占用PA6DCMI_D0~D7也有固定位置改不了。如果核心板已经把这些引脚占用那就只能用带FIFO的模块用GPIO模拟方案了或者换板子这是选型时就要确认的。2.2 LCD屏的接线与FSMC初始化LCD我用的2.8寸TFT屏驱动芯片是ILI9341接口是8080并口16位模式。并口的好处是速度快配合STM32F407的FSMC外设可以把LCD当成外部SRAM来操作写像素数据只需要一条写语句效率远高于SPI模式。接线方面LCD模块上一排引脚包括CS片选接FSMC的NE1对应PD7、RS寄存器/数据选择接FSMC的A0对应PD11、WR写使能接FSMC的NWR对应PD5、RD读使能接FSMC的NOE对应PD4、RESET复位接到普通GPIO、DB0~DB1516根数据线接到FSMC的数据总线D0~D15对应PD14~PD15、PE7~PE15、PD8~PD10等。FSMC的Bank1接LCD时地址映射到0x60000000这一片。实际使用中我们通过地址最低位A0来区分命令和数据的操作访问0x60000000地址时A0为低电平LCD认为是命令操作访问0x60020000地址时0000 0000 0010 0000 0000对应A17看FSMC总线的地址映射方式LCD指令和数据就能分开。不同屏的A0接的位置不一样只要记住这个思路就行代码里定义两个宏一个写命令一个写数据。2.3 SCCB协议与软件I2C实现OV7670的寄存器配置接口叫SCCBSerial Camera Control Bus本质上是类似I2C的协议但时序细节有点差别。写一个寄存器的流程是发送起始条件发送设备地址0x42OV7670的写地址等待ACK发送寄存器地址等待ACK发送要写入的数据等待ACK最后发送停止条件。需要注意SCCB的第9位是“Dont Care”位虽然我们的软件I2C实现里按ACK处理也没问题。我建议用软件模拟I2C而不是使用STM32F407的硬件I2C主要原因有两点一是OV7670对时序的要求比较宽容软件模拟完全能满足且不需要关心硬件I2C的各种标志位问题二是在调试SCCB时软件模拟可以用逻辑分析仪直观查看波形出错时更容易定位。实际经验是SCCB时钟频率可以设置在100kHz~400kHz之间我一般用100kHz左右稳定优先。关键要配置的一批寄存器包括输出分辨率QVGA还是VGA、输出格式RGB565还是YUV、像素时钟分频、镜像翻转、色度饱和度等。初始化顺序往往调不好会出各种怪问题网上有很多参考序列但要特别注意先设置分辨率再设置输出格式最后再设置一些与窗口相关的参数。3. 图像采集中枢DCMI与DMA的配合逻辑3.1 DCMI是怎么工作的DCMI接口的工作方式可以理解为当VSYNC信号有效帧同步到来时一帧图像开始在一帧内每来一个HREF有效信号表示一行开始每来一个PCLK上升沿D0~D7上的8位数据被DCMI捕获写入内部的接收FIFO。DCMI还可以配置为每两个PCLK采一次从而支持16位宽的数据输入但我们的OV7670输出的是8位RGB565格式一个像素分两次输出低字节、高字节所以DCMI配置成8位模式。这里有个容易出错的地方OV7670的输出格式配置成RGB565后一个像素由两个字节组成而LCD的RGB565恰好也是两个字节一个像素。如果不做处理直接将DCMI收到的字节流写入LCD画面会显示为“半个像素错位”的彩色噪点。解决办法有三个第一种是在DCMI中断里把两个字节拼成一个16位像素再写LCD费CPU第二种是配置LCD直接接8位数据总线但16位屏可用数据又浪费了第三种是让DMA把DCMI接收到的数据直接搬运到LCD的GRAM地址虽然字节顺序和像素边界可能不对但可以通过调整DCMI的字节选择位来解决。实际操作中我选择的是让DCMI工作在8位模式直接用DMA把数据搬运到LCD的帧缓存或直接搬运到LCD的GRAM然后用一个简单的函数判断奇偶字节将2字节数据合并成RGB565像素。这样做的好处是灵活缺点是DMA要停一下。为了追求20fps以上的帧率真正生产级的做法一般是把DCMI和DMA配置为16位模式但那样需要把OV7670的两路8位数据在硬件上拼成16位或者使用QVGA模式下逐行读出再组合。3.2 DMA循环模式与双缓冲DCMI产生的数据量很大如果不用DMA单纯靠中断搬运168MHz的CPU也扛不住。我配置DMA工作在循环模式每次传输一帧数据量的1/2或1/4触发传输完成中断。在中断里通过一个标志来通知主程序当前帧已经采集到哪个缓冲区。双缓冲的做法是很实用的准备两个大小等于一帧数据量的缓冲区A和B。DMA先往A里写写满后自动切换为往B写同时产生中断提示A已经完成。主循环里把A的数据处理后显示此时DMA还在往B里写。下一帧反过来以此类推。这样CPU处理和DMA传输互不干扰画面不会出现撕裂。不过双缓冲在DCMI上有一些小细节DCMI的DMA请求是逐像素触发的所以DMA配置的是存储器到存储器的搬运还是外设到存储器的搬运答案是外设到存储器外设地址是DCMI的数据寄存器地址。要注意DMA的传输单位必须是半字16位或字节8位取决于我们的拼接方案。我这边用的是8位传输所以缓冲区是uint8_t数组每次中断收到一行或半帧再由主程序逐像素合成。3.3 帧同步和画面撕裂问题画面撕裂的本质是在LCD显示当前帧时DMA写入了下一帧的新数据导致屏幕上半部分是旧帧、下半部分是新帧。最简单的解决办法是双缓冲但这需要额外的内存。我用的F407VET6有192KB的SRAM其中CCM内存有64KB不能用DMA访问所以我只能用普通SRAM。QVGA分辨率RGB565一帧是320x240x2153600字节约150KB一块缓冲区都放不下这里有个很关键的经验不能直接缓存整个QVGA帧内存不够。我的方案是缩小采集分辨率为160x120QQVGA一帧是160x120x238400字节约37.5KB放两块缓冲区刚好75KB剩下的内存还可以放其他变量。如果一定要QVGA则必须在DCMI中断里做“边采边显示”也就是每一行数据到了就拼接成RGB565写入LCD的相应GRAM地址这样不需要全帧缓冲区但DMA和FSMC写LCD的时序会互相争抢总线实测帧率会下降。经过实测160x120分辨率的双缓冲方案在168MHz主频下可以稳定跑到30fps左右画面也很流畅。如果你能用外部SRAM那可以上QVGA但很多核心板没有外扩SRAM这点要提前想清楚。4. 实操过程从初始化到点亮画面的全流程4.1 系统时钟与引脚复用配置第一步是配置系统时钟。我直接用库函数把系统时钟设置为168MHz。STM32F407的时钟树不复杂外部8MHz晶振经过PLL倍频到168MHz是典型配置。关键代码如下void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct {0}; RCC_OscInitStruct.OscillatorType RCC_OSCILLATORTYPE_HSE; RCC_OscInitStruct.HSEState RCC_HSE_ON; RCC_OscInitStruct.HSEPredivValue RCC_HSE_PREDIV_DIV1; RCC_OscInitStruct.HSIState RCC_HSI_ON; RCC_OscInitStruct.PLL.PLLState RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLM 8; RCC_OscInitStruct.PLL.PLLN 336; RCC_OscInitStruct.PLL.PLLP RCC_PLLP_DIV2; RCC_OscInitStruct.PLL.PLLQ 7; HAL_RCC_OscConfig(RCC_OscInitStruct); RCC_ClkInitStruct.ClockType RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.SYSCLKSource RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider RCC_HCLK_DIV4; RCC_ClkInitStruct.APB2CLKDivider RCC_HCLK_DIV2; HAL_RCC_ClockConfig(RCC_ClkInitStruct, FLASH_LATENCY_5); }PLLM8意味着8MHz/81MHz作为PLL输入PLLN336使得VCO输出336MHzPLLP2分频得到168MHz这就是典型配置算下来PLL输出是168MHz完全符合F407在168MHz下Flash等待周期需要5个周期的要求。接着配置GPIO复用。DCMI的引脚要复用为AF13比如PA6、PA4、PB7这些。LCD的FSMC引脚要复用为AF12。GPIO初始化不复杂但要注意复用功能的正确设置搞错的话引脚不工作。4.2 DCMI与DMA的HAL配置要点HAL库对DCMI的封装比较简洁。配置DCMI时要设置同步信号极性、像素时钟极性、选择8位还是16位数据模式。我的配置如下DCMI_HandleTypeDef hdcmi; void DCMI_Init(void) { hdcmi.Instance DCMI; hdcmi.Init.SynchroMode DCMI_SYNCHRO_HARDWARE; hdcmi.Init.PCKPolarity DCMI_PCKPOLARITY_FALLING; hdcmi.Init.VSPolarity DCMI_VSPOLARITY_LOW; hdcmi.Init.HSPolarity DCMI_HSPOLARITY_LOW; hdcmi.Init.CaptureRate DCMI_CR_ALL_FRAME; hdcmi.Init.ExtendedDataMode DCMI_EXTEND_DATA_8B; HAL_DCMI_Init(hdcmi); }这里有个关键点PCKPolarity配置成FALLING还是RISING取决于OV7670在PCLK什么沿输出数据。OV7670的数据手册说数据在PCLK上升沿变化、下降沿稳定所以通常配置成FALLING即下降沿采样。VSPolarity和HSPolarity配置成LOW还是HIGH要看OV7670的同步信号默认极性——VSYNC高有效、HREF高有效所以DCMI的对应极性配置为LOW表示“低电平有效”需要注意这个反直觉点DCMI的VSPolarity是配置同步信号在有效时是高还是低OV7670的VSYNC是低电平无效、高电平有效因此要把VSPolarity设为LOW意思是“VSYNC低才是有效”不对我实际测试后才发现DCMI的极性设置不是看有效电平而是看在哪个沿触发。这个配置调了好几次才稳定最后是VSYNC高有效就配VSPolarity为LOWHREF高有效就配HSPolarity为LOWPCLK数据稳定在下降沿就配PCKPolarity为FALLING。这些极性设反了画面不是全花就是偏移而且很难通过逻辑分析看出问题。DMA的配置也很简单但要把方向设为外设到内存数据宽度匹配DMA_HandleTypeDef hdma_dcmi; void DMA_Init(void) { hdma_dcmi.Instance DMA2_Stream1; hdma_dcmi.Init.Channel DMA_CHANNEL_1; hdma_dcmi.Init.Direction DMA_PERIPH_TO_MEMORY; hdma_dcmi.Init.PeriphInc DMA_PINC_DISABLE; hdma_dcmi.Init.MemInc DMA_MINC_ENABLE; hdma_dcmi.Init.PeriphDataAlignment DMA_PDATAALIGN_BYTE; hdma_dcmi.Init.MemDataAlignment DMA_MDATAALIGN_BYTE; hdma_dcmi.Init.Mode DMA_CIRCULAR; hdma_dcmi.Init.Priority DMA_PRIORITY_HIGH; HAL_DMA_Init(hdma_dcmi); HAL_DMA_Start_IT(hdma_dcmi, (uint32_t)DCMI-DR, (uint32_t)frame_buffer, BUFFER_SIZE); }我特别想强调DMA优先级。如果优先级不够高画面会出现随机横条纹这是因为LCD的FSMC写操作也在使用AHB总线DMA数据没能及时取走时DCMI的FIFO溢出丢数据。建议把DMA优先级设成HIGH或VERY_HIGH实测从默认MEDIUM调成HIGH后横条纹消失了。4.3 OV7670寄存器配置与分辨率选择OV7670的寄存器根据数据手册很多地址在不同文档里标注不一致而且网上流传的初始化序列都有细微差异。我的建议是不要照搬某个人的全部寄存器值而是先配置最基本的寄存器跑通RGB565输出再逐步调整其他功能。最关键的寄存器有地址0x12选择输出格式RGB565时需要设置为0x04RGB565输出或0x06RGB444不RGB555实际上OV7670的RGB565是0x04RGB444是0x02RGB555是0x06YUV是0x00。这个务必查手册确认。地址0x40选择像素时钟分频合理配置可以让PCLK适配DCMI的采样能力。配置为0x10时PCLK频率等于XCLK输入频率配置为0x11时是2分频以此类推。地址0x32、0x17、0x18、0x19等控制输出窗口大小和位置。我采用的是一个精简初始化序列关闭所有自动功能先设置0x12为0x80复位延时再设为0x04RGB565然后配置输出窗口为QVGA或QQVGA配置像素时钟分频最后开启输出。经过测试160x120分辨率下PCLK分频可以配为XCLK/4这样帧率约30fps比较稳定。有几个“坑”要特别留意OV7670初始化完成后不要立刻开始采集要延时几百毫秒等内部自动曝光和自动增益稳定再开启DCMI采集否则前几帧画面亮度会剧烈变化。还有如果画面颜色偏品红紫红色通常是因为RGB565的高低字节顺序反了把DCMI的字节交换功能打开或者在初始化里调整RGB565的输出顺序寄存器如0x12的RGB565和RGB444位也可以直接在软件里把每个像素的高低字节交换。4.4 LCD显示与图像数据搬运LCD初始化我直接抄ILI9341的经典初始化序列这部分网上很多不再赘述。关键点是在显示图像前要把LCD的扫描方向设置为从上到下、从左到右并且设置好GRAM的起始地址和窗口大小。LCD初始化完成后可以先用纯色刷屏测试确认LCD本身没问题再接入图像数据。显示图像时我用了一个简单的处理流程DMA中断里收到一帧图像数据后将缓冲区的字节逐对拼接为16位像素然后调用LCD的写数据接口写入GRAM。由于160x120分辨率不大纯软件拼接的耗时很小实测拼接加写入一帧图像约15ms占一帧周期的45%CPU还能做点别的事。如果你追求更高的帧率有一个小技巧在DCMI中断里直接启用“字节交换后写入LCD”也就是在DMA的搬运地址上绕弯子。但说实话对于学习目的先把功能跑通再考虑优化更稳妥。5. 常见问题与调试技巧5.1 画面全花/全条纹这是最让人头疼的问题。按我排查的顺序来第一检查极性配置。PCKPolarity、VSPolarity、HSPolarity三个极性任何一个不对画面都是花的。用逻辑分析仪抓PCLK、VSYNC、HREF和D0线上的波形确认OV7670实际输出的时序和DCMI配置的极性对照基本能排查掉这方面的问题。第二检查DMA配置的缓冲区大小。如果缓冲区大小和实际一帧数据量不匹配DMA可能写爆缓冲区导致HardFault也可能只采了半帧就中断画面表现为上半部分正常、下半部分重复上一帧。第三检查LCD显示方向。如果LCD显示方向与摄像头数据方向不一致比如摄像头输出的是320x240横向扫描但LCD配置成了240x320纵向扫描画面会非常奇怪表现为斜向撕裂或旋转90度后的扭曲。5.2 画面偏色/颜色不对颜色不对通常不是摄像头坏而是RGB格式配置问题。OV7670可以输出RGB565、RGB555、RGB444每种格式下像素在数据线上的高低字节顺序都不同。如果你配置和DCMI采集、LCD显示三者不匹配出现的颜色是完全错乱的。调试方法也很简单让摄像头对准一个纯红色物体用逻辑分析仪抓几个像素周期的数据。如果D0~D7上的十六进制数据是0xF8、0x00这样的组合说明RGB565正确如果抓到0xFF、0x00可能是RGB666或RGB555需要检查寄存器配置。这里说一句OV7670的数据手册里关于RGB565的寄存器位定义写得不够直白多试几种值是最快的办法。还有一个小坑OV7670感光器件的色彩排列顺序Bayer排列不需要我们处理因为RGB输出的颜色数据已经经过内部处理。但它的自动白平衡默认开启对着不同色温的物体颜色会慢慢漂移刚上电时颜色可能偏蓝或偏红属于正常现象等几秒就恢复了。5.3 图像亮度忽明忽暗发生这种现象多半是OV7670的自动增益AGC和自动曝光AEC在剧烈调整。解决方法是在初始化序列里把AGC、AEC功能关掉手动设置固定的增益值和曝光时间。具体寄存器是0x13的低几位和0x14、0x0D、0x0F等。手动设置后画面亮度会稳定下来但前提是你设置的值在合理范围内否则画面会过暗或过曝。如果只是做图像采集实验我建议手动设置一个中等亮度的参数因为自动调节带来的变化会让程序调试变得很困难你很难判断是代码问题还是摄像头自动调节导致的画面变化。5.4 SCCB/I2C写寄存器失败OV7670的SCCB地址是0x42写地址很多初学者容易搞错。注意0x42是包含写位的8位地址如果按I2C的7位地址理解应该是0x21。HAL库的I2C地址参数有的要传7位地址有的要传8位地址搞混了I2C会报NACK。解决办法是统一使用软件模拟I2C自己控制起始、停止和ACK判断就不会有地址位宽的问题。软件I2C的延时用4~5us时钟约100kHz非常稳定。另外OV7670的SCCB总线可以多设备挂载但一般模块上只有它一个设备不涉及地址冲突。5.5 帧率低/CPU占用过高首先要区分瓶颈在哪里。如果CPU占用高多半是因为在DCMI中断里做了大量处理比如拼接、复制、循环等待。优化方向是把数据拼接和LCD写入放到主循环处理中断里只置标志位。如果CPU占用不高但帧率上不去多半是DCMI或DMA的配置导致每个像素丢失实际一帧只存了一半数据画面帧率看起来很低。实测下来在168MHz主频、160x120分辨率下把DMA中断的负载降到最低只置标志位主循环负责显示最高能跑到35fps30fps稳定。如果优化编码上QVGA320x240并配合外部SRAM应该能到20fps左右但内存是个硬门槛。6. 实验总结与扩展想法这次实验让我对DCMI、DMA、FSMC和摄像头时序有了更直观的理解。在不带FIFO的OV7670模块上关键不在于摄像头本身而在于MCU如何精准地配合它的输出节奏极性配置、DMA乒乓传输、像素格式匹配这三者缺一不可。跑通一次后再去看带FIFO的模块就会觉得那纯粹是“作弊”因为时序压力全部转移到FIFO硬件上了。如果后续想扩展我建议沿着三个方向一是加入图像处理比如颜色识别、边缘检测趁数据在DMA中断里直接做这样可以省去二次遍历内存的时间二是加入显示增强比如叠加中文或字符信息需要在LCD上做透明图层ILI9341没有硬件图层软件实现也不难三是把分辨率提升到QVGA同时用F407的外部FMC总线扩一片SRAM这样一帧数据可以完全缓存下来处理空间会大很多。顺便提一句网上有些资料提到STM32F407的FPU浮点运算单元需要手动开启才能使用硬件浮点这在图像处理中计算颜色阈值时确实有用别忘记在Keil或IAR的选项里开启FPU。这个实验做完后我对“嵌入式图形采集”这套流程算是彻底通透了。现在看到任何摄像头模块第一反应都是先找它的输出时序、接口类型再想主控能不能接而不是急着抄初始化代码。这也是这次踩坑最大的收获。
返回列表