ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32F407无FIFO驱动OV7670:DCMI+DMA双缓冲图像采集实战

STM32F407无FIFO驱动OV7670:DCMI+DMA双缓冲图像采集实战 拿到一块不带FIFO的OV7670摄像头模块很多人第一反应是“这不找麻烦吗”——传感器输出的PCLK像流水一样直接冲过来MCU稍不留神就丢像素。但STM32F407这颗芯片偏偏有个DCMI接口天生就是干这个活的。只要配合DMA双缓冲无FIFO方案不仅能跑还比带FIFO模块省掉一颗AL422B缓冲芯片的成本和面积。这篇文章把我从硬件接线、SCCB寄存器配置、CubeMX图形化配置、DMA双缓冲到LCD实时显示的完整过程拆开讲重点解释每一步为什么这么做、参数怎么定、坑在哪里。适合手里有F407开发板想低成本切入图像采集但又不想被各种花屏黑屏折磨的读者。无论你是刚跑通流水灯想进阶的初学者还是已经在用FIFO模块想降成本的老手这套方案都值得花半小时看完。1. 为什么选择无FIFO方案先算清这笔账1.1 带FIFO与无FIFO差距到底在哪市面上常见的OV7670模块分两种带FIFO和不带FIFO。带FIFO的模块一般板载一颗AL422B容量是384KB能把一整帧图像先塞进芯片内部的缓冲区MCU什么时候有空就什么时候去读。这种模块的好处是处理器压力小哪怕你用没有DCMI接口的STM32F103也能靠GPIO慢慢把数据抠出来。缺点也很明显成本高、面积大、多一个芯片就多一个故障点。不带FIFO的模块就纯粹多了传感器引脚直接引出PCLK、VSYNC、HREF、D0到D7全部暴露在外面。MCU必须在场同步窗口内不折不扣地把数据接住一帧76800个像素QVGA分辨率少一个都不行。这对处理器提出了硬性要求必须有一个专门的摄像头接口还得配合DMA做数据搬运否则CPU光处理中断就能把自己卡死。我刚开始也纠结过要不要上带FIFO模块后来发现F407自带DCMI而DMA又是标配无FIFO方案等于把芯片里白送的外设利用起来。省下的钱虽然不多但PCB面积和供应链的简洁程度对做产品的朋友来说价值更大。注意如果你用的是STM32F103这类不带DCMI的芯片无FIFO方案基本走不通老老实实买带FIFO模块更省心。这也是很多教程默认用带FIFO模块的原因。1.2 STM32F407的底气DCMI DMA 等于白送一个摄像头控制器DCMIDigital Camera Interface是STM32F4系列里一个很容易被忽略的外设它专门用来接收CMOS图像传感器输出的并行数据流。OV7670的D0到D7正好对上DCMI的8位数据线VSYNC对应DCMI的垂直同步HREF对应水平同步PCLK对应像素时钟。硬件上几乎就是为这类摄像头准备的。光有DCMI还不够因为DCMI只是一个采集前端数据最终还是得落到内存里。这里的关键是F407的DMA2支持双缓冲模式可以在两个内存缓冲区之间自动切换。DMA正在往里写buffer0的时候CPU可以同时把buffer1里的图像刷到LCD上一帧采完硬件自动切到buffer1CPU又回头处理buffer0。这个乒乓机制就是“高效”二字的来源——采集和显示两条流水线互不干扰。带宽上算一笔账也挺直观。QVGA是320×240每个像素用RGB565占2字节一帧就是153600字节约150KB。30fps的情况下数据量是4.6MB/s左右。F407主频168MHzDMA跑4.6MB/s毫无压力。哪怕是VGA全分辨率640×48030fps也才18.4MB/s依然在DMA的能力范围内但留给CPU处理和LCD刷屏的时间就很紧张了所以本文以QVGA为主。1.3 硬性代价无FIFO方案对实时性的苛刻要求省了FIFO芯片代价是把实时性问题从硬件转移到了软件里。带FIFO模块时MCU什么时候读数据自己说了算无FIFO方案里数据一股脑儿地来你必须在规定时间内完成所有操作中间任何一个中断响应不及时画面就会出现错位或者撕裂。具体来说有两个核心约束DMA必须比数据流快以及帧同步必须准确。DMA本身是硬件搬运速度不是问题但你要保证缓冲区足够大能容纳一帧数据帧同步则要靠DCMI的VSYNC信号来触发确保每次刷屏都是从一帧的起始位置开始。很多初学者在这里栽跟头以为只要DMA配好了画面就正常结果发现花屏多半是帧起始位置没对齐。所以选择无FIFO方案实际上是在用一点调试的复杂度和实时性约束换取成本和面积的简化。对F407来说这笔交易挺划算。2. 硬件接线与电路设计别让低级错误毁掉整个项目2.1 引脚分配参考常规接法与CubeMX复用表怎么对照OV7670无FIFO模块和F407开发板的接线最常见的一套方案是D0接到PC6D1接PC7D2接PC8D3接PC9D4接PC11D5接PC12D6接PD3D7接PD6。同步信号方面PCLK接PA6HREF接PA5VSYNC接PA4。这套接法在很多开发板上都能直接跑通公开例程也最多。注意D4是PC11而不是PC10这个很容易记错。PC10虽然也带DCMI复用功能但DCMI的D4默认映射到PC11上接错线就是花屏。引脚可不可以用别的位置当然可以。DCMI的引脚在STM32F407上有多组复用映射你完全可以在CubeMX里看到完整的AF13复用表把数据线挪到其他引脚。问题在于OV7670模块的排针顺序是固定的你没法把模块的D0线随意接到MCU的任意引脚上只能整组换位置。所以最稳妥的做法是照抄成熟方案的引脚分配除非你自己画板子否则没必要折腾。提示拿到开发板先查原理图确认OV7670接口的丝印顺序和你预期一致。有些模块的D0到D7丝印是从右往左排的这个低级错误排查起来非常浪费时间。2.2 供电、去耦、时钟三个最容易出现隐形问题的点OV7670的核电压是1.8VI/O电压可以到3.3V大多数模块板上已经集成了一颗LDO稳压芯片直接给模块的3.3V供电就行。但这不意味着供电可以随便来摄像头的电流瞬态变化很剧烈尤其是PCLK翻转的时候对电源噪声非常敏感。我的习惯是在模块的电源引脚旁边加一个10uF钽电容和一个0.1uF陶瓷电容靠模块越近越好。如果电源纹波大图像会出现横向条纹这个现象很恶心因为代码怎么调都调不好。去耦电容同样重要。无FIFO模块把传感器的信号全部引出来了PCLK的边沿非常陡数据线上只要有轻微串扰就会导致采样出错。信号线建议串联33Ω的小电阻既能抑制过冲又能减少振铃。如果条件允许把摄像头到MCU的排线控制在10厘米以内越短越稳。时钟方面OV7670需要一颗外部时钟XCLK范围一般在10MHz到24MHz之间。STM32F407可以用PA8的MCO1输出HSE时钟比如开发板的HSE是8MHz晶体那MCO1输出8MHz给摄像头也能正常工作只是帧率会略低。如果想要更理想的12MHz或24MHz可以用定时器输出PWM来产生或者直接外挂一颗24MHz有源晶振。我最推荐外挂有源晶振简单粗暴不占用MCU资源排查问题的时候还少一个变量。2.3 引脚冲突检查DCMI与FSMC/LCD如何和平共处这是无FIFO方案里特别容易被忽略的一个坑DCMI数据线占用的引脚和FSMC地址线存在复用冲突。DCMI的D6接到PD3而PD3恰好是FSMC_A0的默认映射引脚DCMI的D7接到PD6PD6也和FSMC的部分控制信号有复用关系。如果你的LCD走的是FSMC接口那就必须确认开发板的LCD地址线有没有避开PD3和PD6。很多设计成熟的开发板会把LCD的RS/A0信号接到PF0而不是PD3这样两者就不冲突了。但不同板子设计不同一定要看原理图或者直接在CubeMX里同时勾选DCMI和FSMC软件会自动报冲突让你及时调整。我见过有人在这上面浪费了一整天摄像头单独测试正常LCD单独测试正常两个一起打开就黑屏。最后发现是DCMI的D6占用了FSMC的地址线LCD的寄存器地址全被摄像头的数据信号干扰了。别问我怎么知道的问就是泪。3. SCCB配置让OV7670按你想要的格式输出3.1 SCCB和I2C的差异为什么推荐软件模拟OV7670的寄存器配置走的是SCCB协议全称Serial Camera Control Bus是OmniVision自己定义的一套两线串行协议。它在很多地方和I2C相似但细节上又有区别。SCCB的从机地址是0x42写/0x43读写寄存器时发送从机地址、寄存器地址、寄存器值三个字节和I2C写操作几乎一样。读寄存器时要先写寄存器地址然后重新发送从机地址加读位再读回数据。很多人直接用STM32的硬件I2C去操作SCCB结果发现时好时坏。原因很简单SCCB的时序在某些情况下不允许连续读而硬件I2C控制器会自动按I2C的规则去处理两者存在细微差异。再加上STM32F4的硬件I2C本身就有一些著名的坑我索性用GPIO软件模拟反而最稳定。提示软件模拟SCCB时时钟频率控制在100kHz级别就够了配置寄存器又不频繁跑那么快没意义。关键是保证SCL高电平期间SDA不能变化否则会被当成起始或停止信号。3.2 关键寄存器RGB565、QVGA、PCLK设置OV7670寄存器很多但起步阶段只需关注几个关键的。0x12寄存器控制输出格式和分辨率bit7选VGA还是QVGA低4位选RGB输出格式。要设成QVGA加RGB565就把0x12写成0x84其中0x80表示QVGA0x04表示RGB565。0x11寄存器与帧率有关bit7到bit4控制内部时钟的分频值越大帧率越低。如果XCLK给的24MHz而这个寄存器配得不好PCLK可能飙到20MHz以上虽然DMA能接住但留给LCD刷屏的时间会急剧缩短。我的做法是优先让PCLK落在10MHz到12MHz左右宁可帧率低一点也要保证画面稳定。除了格式和时钟窗口设置也需要留意。0x17和0x18控制HREF的起始和结束位置0x19和0x1A控制垂直方向的起止位置。这些寄存器一般情况下用厂家推荐的默认值就能得到正确的320×240图像但如果你改了0x12的分辨率这些窗口参数也必须跟着调整否则画面会偏移甚至超出边界。SCCB驱动写好后第一步不是急着出图而是读回0x0A和0x0B两个ID寄存器。如果读回的是0x76和0x73说明摄像头通信正常再往下配置寄存器才有意义。3.3 XCLK与PCLK分配用MCO还是定时器PWM给OV7670提供XCLK时钟的办法有三种各有适用场景。最简单的方案是用PA8的MCO1功能把HSE时钟直接输出给摄像头。如果你的开发板HSE是8MHz那MCO1输出8MHzOV7670也能开机但帧率偏低画面有轻微闪烁感。第二种方案是用定时器输出PWM信号作为XCLK。比如TIM8的某个通道可以输出24MHz的PWM这就达到了OV7670比较理想的工作频率但代价是占用一个定时器和相关引脚如果工程里定时器资源紧张就得权衡一下。第三种方案是外挂一颗24MHz有源晶振直接给摄像头供时钟。这看起来多了一颗元件但好处非常明显晶振输出稳定不受MCU时钟树配置影响排查问题时也少一个不确定因素。我自己做板子的话基本都会预留一个外部晶振的位置紧急情况下还能跳过它改成MCO输出。4. CubeMX配置DCMI与DMA图形化工具背后的细节4.1 新建工程与基础外设别跳过时钟树打开STM32CubeMX芯片选STM32F407VET6之类具体型号。首先要配好RCC的HSE外部高速晶振然后进入Clock Configuration看看系统时钟是不是跑到168MHz。很多人跳过了这一步直接用默认的16MHz内部时钟跑结果DCMI和FSMC速度全都不对最后还以为是代码问题。Debug接口建议选Serial Wire方便用ST-Link调试。如果你打算用串口打印调试信息再把USART1或USART2勾选上。这个阶段虽然不影响摄像头功能但后面排查问题的时候没有串口输出简直寸步难行。4.2 DCMI参数配置极性选错就是黑屏在Connectivity分类下找到DCMI勾选使用摄像头接口。然后展开引脚配置把D0到D7、VSYNC、HSYNC、PIXCK全部使能。这时候CubeMX会自动把引脚映射到默认位置你可以对照前面说的接线方案检查是否一致。DCMI最关键的是极性问题。PIXCK代表像素时钟极性OV7670默认在PCLK下降沿数据变化上升沿数据稳定所以DCMI应该配置为上升沿采样也就是Polarity选择Rising Edge。如果你发现画面全是雪花或者严重错位优先把PIXCK极性反一下试试。VSYNC的极性取决于你的模块OV7670的VSYNC默认高有效但有些模块做了反相处理。HSYNC同理默认高有效时HREF拉高表示一行有效数据。如果极性配反图像要么完全没有要么就是一行一行错开的拉丝画面。最靠谱的办法是看模块原理图或者用示波器抓一下如果实在没法测就两个极性都试一遍出图的那个就是对的。4.3 DMA双缓冲实现“一边采集一边显示”的关键DCMI的数据最终要通过DMA搬到内存这是整个方案的核心。F407上DCMI的DMA请求映射到DMA2的Stream1通道选择Channel 1。在CubeMX的DMA设置里添加这个请求后Mode要选Circular循环模式这样摄像头一帧接一帧地输出DMA也能不间断地搬运。双缓冲在CubeMX里没有直接的图形化配置需要在生成的代码里手动调用HAL_DMAEx_ConfigDoubleBuffer函数来完成。可以先把DMA配置成普通Circular模式然后在代码里对hdma_dcmi调用双缓冲配置把两个内存地址都传进去。这样DMA会在两个缓冲区之间交替填充硬件自动切换CPU不需要干预。DMA中断必须打开而且优先级要合理。传输完成和半传输完成两个中断回调都会用到分别对应两个缓冲区的填充结束。中断优先级方面DMA2_Stream1的中断可以设为抢占优先级5、子优先级0太高了会挤压其他关键中断太低了又可能在高速采集时丢中断。我实测下来这个优先级在大多数项目里都够用。4.4 FSMC与LCD初始化刷屏带宽估算LCD部分我用的是最常见的FSMC接口TFT屏驱动芯片是ILI9341这类。在CubeMX里配置FSMC为Bank1 NOR/SRAM模式使能NE1片选数据线D0到D15全部打开地址线A0作为寄存器选择信号接到LCD的RS引脚。读写时序可以根据LCD控制芯片手册来调一般先用比较宽松的时序保证能点亮再逐步收紧优化速度。从带宽角度算一算一帧QVGA图像76800个像素一个像素2字节。FSMC写入一个像素大约需要两个总线周期以100MHz左右的FSMC时钟来算几百纳秒就能写完一个像素。这样算下来刷完整屏也就几毫秒完全追得上30fps的摄像头输入。所以只要用FSMC刷屏流畅度就没问题反过来如果你想用GPIO模拟时序刷屏那一帧图像传半天整个系统就废了。5. 图像采集与LCD实时显示实现从DMA中断到屏幕像素5.1 缓冲器设计与DMA双缓冲代码框架缓冲区我用一个二维数组两个缓冲轮流使用。在HAL库下DMA双缓冲的典型初始化代码如下#define IMG_WIDTH 320 #define IMG_HEIGHT 240 static uint16_t frameBuf[2][IMG_WIDTH * IMG_HEIGHT]; static volatile uint8_t currentBuf 0; static volatile uint8_t frameReady 0; // CubeMX中已经初始化了 hdma_dcmi HAL_DMAEx_ConfigDoubleBuffer(hdma_dcmi, (uint32_t)DCMI-DR, (uint32_t)frameBuf[0], (uint32_t)frameBuf[1], IMG_WIDTH * IMG_HEIGHT); HAL_DCMI_Start_DMA(hdcmi, DCMI_MODE_CONTINUOUS, (uint32_t)frameBuf[0], IMG_WIDTH * IMG_HEIGHT);这里有个细节DCMI的DMA传输单位建议设为半字也就是16位正好对应一个RGB565像素。外设地址固定为DCMI的数据寄存器内存地址按半字递增这样DMA每搬运一次就是一个像素计数直接填76800即可。双缓冲配置完成后DMA在什么时候告诉你“缓冲满了”答案是中断。在DMA中断服务函数里读取当前内存目标寄存器判断当前DMA正在写哪个缓冲另一个缓冲就是可以送去LCD显示的完整帧。代码看起来像这样void DMA2_Stream1_IRQHandler(void) { HAL_DMA_IRQHandler(hdma_dcmi); } void HAL_DMA_XferCpltCallback(DMA_HandleTypeDef *hdma) { if (hdma hdma_dcmi) { if (HAL_DMA_GetCurrentTarget(hdma_dcmi) DMA_MEMORY_0) currentBuf 1; // DMA正在写buffer0可读buffer1 else currentBuf 0; frameReady 1; } }5.2 帧同步与缓冲切换不撕裂画面的实操方法DMA把缓冲区写满后会触发中断这是“图像到了”的信号。主循环里哪怕什么都不干只要看到frameReady被置位就把对应的缓冲数据送到LCD。这个设计保证了你刷屏的时候DMA写的一定是另一个缓冲区不会出现读到一半被覆盖的撕裂问题。除了DMA中断DCMI还提供VSYNC帧同步中断可以通过HAL_DCMI_FrameEventCallback回调来获取帧起始信号。在数据流稳定的情况下DMA中断已经够用但如果你发现画面随机错位多半是帧起始位置偏了这时候就应该启用DCMI的帧事件中断在帧起始时对DMA缓冲计数做个校准。提示不要把刷屏操作直接放在DMA中断回调里。一帧76800像素FSMC写屏要几毫秒中断里耗这么久会把其他中断全部饿死。正确做法是中断里只置标志位刷屏放主循环。5.3 LCD刷屏与颜色格式偏色问题从这里找原因OV7670配置成RGB565后每个像素占2字节。DMA把两个字节作为一个半字搬进内存主循环把这段内存直接交给LCD写GRAM函数按理说不需要再做颜色转换。但实际操作中偏色是最常见的问题之一。根本原因在于RGB565的高低字节顺序。有些LCD控制器希望第一个字节是高字节有些希望是低字节OV7670输出字节序也是固定的两者一旦不匹配颜色就全乱红色显示成蓝色绿色显示成品红。解决办法是在代码里做一个字节交换。为了不每次刷屏都占用CPU可以预先把整个缓冲区的数据用查表或者DMA的方式交换一次或者更简单一点在初始化时给LCD写一个0xF800像素看看是不是纯红如果不是就对整个缓冲做一次高低字节交换。另外如果颜色看起来像“负片”或者偏绿严重先别急着调LCD拿示波器看看SCCB配置是否成功0x12寄存器是否真的写进了0x84。很多时候寄存器没写进去摄像头还在输出默认的YUV422格式LCD却按RGB565显示颜色自然一塌糊涂。6. 常见问题与排错黑屏、花屏、偏色、帧率低一次说清6.1 从现象到思路先把问题分成“硬件/时序/软件”项目出问题的时候最忌讳的是哪都怀疑最后哪都调不好。我的习惯是先分三层硬件层看供电、时钟、接线、信号完整性时序层看PCLK极性、VSYNC/HREF极性、SCCB配置软件层看DMA缓冲、中断、LCD驱动。每一层都有一个最简单的验证方法。硬件层出问题表现往往是完全黑屏或者图像极不稳定。这时先测3.3V和1.8V电压再用示波器看XCLK是否有时钟PCLK有没有输出。如果摄像头寄存器没配置PCLK也照样输出只是数据是无效的所以PCLK有波形不代表配置成功。时序层出问题表现是花屏、图像错位、颜色怪异。直接用CubeMX把DCMI的各个极性切换一下有时候比用示波器抓信号更快。PCLK极性配错了画面完全是雪花或者斜纹VSYNC极性配错了则一帧内没有任何有效数据。软件层出问题表现是偶尔黑屏、偶发错行、刷新卡顿。这时候打开DMA中断调试看frameReady标志是否以稳定频率置位再看currentBuf是否在0和1之间交替基本能定位问题。6.2 问题速查表现象可能原因排查与修复完全黑屏供电异常、XCLK没有查3.3V/1.8V电压示波器看XCLKSCCB通信失败从机地址错误、SDA/SCL接反读0x0A应返回0x76检查0x42/0x43花屏雪花PCLK极性配反、信号线过长DCMI的PIXCK极性切换缩短排线图像错位VSYNC/HREF极性错误、窗口未对齐切换极性检查0x17~0x1A窗口寄存器颜色偏绿偏蓝输出格式不是RGB565、字节序不对确认0x120x84交换高低字节测试横条干扰电源纹波、去耦不足加去耦电容减少信号线长度帧率很低XCLK频率太低、LCD刷屏用GPIO提高XCLK改用FSMC接口L偶发撕裂DMA缓冲不足、帧起始未对齐启用DCMI帧中断增大缓冲余量6.3 我踩过最深的坑PCLK极性判断方法有一段时间我被PCLK极性折磨到怀疑人生。OV7670规格书上写的是“数据在PCLK下降沿被更新上升沿时保持稳定”按道理DCMI配成上升沿采样就行。但实际模块上有些板子在线路上加了一级反向驱动器PCLK的相位就反了。如果还按上升沿采样每次采到的都是数据切换的中间态画面就像雪花屏一样。后来我总结出一个快速判断方法把PCLK极性设为上升沿如果画面模糊但能看出轮廓说明采样点太接近数据跳变沿九成是极性反了如果画面完全是乱的雪花也有可能是极性问题但还需要同时检查数据线顺序。最终用逻辑分析仪同时抓PCLK和D0数一下PCLK下降沿到数据稳定的时间一切就都清楚了。提示无FIFO方案做硬件调试时逻辑分析仪比示波器更好用。三根线同时抓PCLK、VSYNC、HREF一眼就能看清极性、频率和帧结构。没必要在这事上靠猜。最后再分享一个心得这套无FIFO方案第一次跑通后你会觉得“就这”但中间的坑都是实打实的经验。DCMIDMA双缓冲的每一处配置都值得反复琢磨尤其是缓冲切换和帧同步逻辑搞懂了以后不管换OV2640还是其他传感器思路都一样。如果你手头正好有F407和OV7670别犹豫直接上无FIFO方案折腾完这一轮你对STM32的DMA和中断体系的理解会上一个台阶。
返回列表