多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

STM32F407驱动无FIFO版OV7670:DCMI+DMA图像采集与LCD显示全解析

STM32F407驱动无FIFO版OV7670:DCMI+DMA图像采集与LCD显示全解析 可能你也遇到过这种尴尬网上搜OV7670十篇教程有九篇用的是带FIFO的模块模块上焊着一颗AL422B异步FIFO芯片单片机只要在VSYNC变低之后慢慢把数据读走就行。但我这次手里的板子偏偏是裸的——D0~D7、PCLK、VSYNC、HREF一排引脚直接伸出来上面没有任何缓存芯片。也就是说摄像头每来一个像素时钟STM32F407就必须在这个PCLK的节奏里把数据一个不落地接走晚一点都不行。这篇笔记是我在STM32F407上把不带FIFO的OV7670图像采集、LCD显示完整跑通的全过程包括SCCB寄存器配置、DCMIDMA采样链路、RGB565数据如何送进LCD以及我修过的那些花屏错位问题。如果你正卡在“不带FIFO的OV7670到底怎么接F407”这件事上这篇应该能帮你省不少时间。1. 先搞明白FIFO到底帮你分担了什么1.1 AL422B一个把摄像头和单片机“解耦”的仓库带FIFO的OV7670模块之所以流行是因为AL422B这颗异步FIFO芯片从硬件上把摄像头的输出节奏和单片机的读取节奏解开了。摄像头端的PCLK往FIFO里写像素数据FIFO满到一定程度后单片机端再用自己的时钟把数据慢慢读出来。写入端和读取端各自独立互不等待只要FIFO不溢出单片机想什么时候读都行。这种方案对入门非常友好——GPIO模拟时序都能凑合读出来因为你不必关心像素时钟的精确时刻。很多教程里用来“练手”的OV7670模块本质都是先让摄像头自己拍完一张再让单片机从容地搬走。这个思路的核心价值是把一个“实时性极强”的问题退化成了一个“容量问题”。1.2 不带FIFO时你要直面PCLK这个实时时钟不带FIFO的OV7670输出引脚就是裸的D0~D7、PCLK、VSYNC、HREF。摄像头发出一帧图像时PCLK会像心跳一样持续跳D0~D7上的数据只在PCLK采样的那个瞬间有效。这意味着单片机必须在每个PCLK周期内完成一次数据读取而且不能漏。漏掉的后果是什么不是丢一个像素那么简单而是整帧图像从漏掉的点开始错位、斜切、花屏。因为下一行数据来了之后你的缓冲区起点已经和帧的实际起点对不上了。侥幸用GPIO轮询去拼图像基本不可能除非你把帧率和分辨率降到极低。说白了不带FIFO的OV7670就是一台“不等人”的机器你要么跟上它的节奏要么放弃。1.3 F407能不能接答案是DCMISTM32F407正好有一颗DCMI外设全称Digital Camera Interface数字摄像头接口。它支持8位、10位、12位、14位并行数据输入带VSYNC和HSYNC同步信号内置一个很浅的同步FIFO只有4个字32位宽采集到的数据可以自动触发DMA搬运。DCMI的存在就是专门解决“实时像素流”这个问题的。你把PCLK接到DCMI的像素时钟输入VSYNC接帧同步HREF接行同步D0~D7接数据线配置好极性之后DCMI自己会在PCLK的边沿把数据锁存进内部FIFO然后DMA自动把数据搬进内存。中文字典里“接口”这个词用在DCMI上再准确不过——它把外部像素流和内部总线之间的协议适配问题全包了。2. 硬件连接里那些容易被忽略的前提2.1 引脚映射与接线表不带FIFO的OV7670引脚上通常有SIO_C、SIO_DSCCB控制总线、VSYNC、HREF、PCLK、D0~D7、XCLK、3.3V、GND。STM32F407的DCMI引脚有一组标准复用功能映射常见接法如下。OV7670引脚STM32F407引脚备注SIO_CPB6I2C1_SCL或任意GPIO模拟SCCB时用普通GPIO更稳SIO_DPB7I2C1_SDA或任意GPIO同上VSYNCPA4DCMI_VSYNCAF13HREFPA5DCMI_HSYNCAF13PCLKPA6DCMI_PIXCKAF13D0~D7PE4~PE11DCMI_D0~D7AF13XCLKPA8MCO1或定时器输出脚给摄像头提供主时钟3.3V3.3V必须稳定供电GNDGND共地PA4、PA5、PA6、PE4~PE11是F407数据手册上DCMI的标准AF13映射大部分开发板都兼容。如果你用的是卖家自带的摄像头排线接口务必先看原理图确认引脚有没有被其他外设复用。特别是PE口有些板子PE2、PE3接了SD卡或其他功能要避开冲突。2.2 供电和上电时序OV7670脾气不小OV7670对3.3V供电的质量很敏感。它的工作电流不大但瞬间电流尖峰不少尤其是传感器内部模拟电路和数字电路同时切换时。用杜邦线直接接到开发板的3.3V排针上线稍微长一点就可能看到图像上滚动的横纹和噪点。正确的做法是摄像头的VCC单独走一条短粗线最好在摄像头供电脚附近并一颗10uF电解电容和一颗0.1uF瓷片电容。如果手头有LDO用一颗单独的3.3V LDO给摄像头供电效果比直接从开发板引要好很多。我这块板上没有LDO实测发现3.3V引脚电压纹波在图像上直接表现为“彩色雪花”后来换成独立LDO供电后画面立刻干净了一个数量级。上电顺序也值得注意。理想情况下先给F407上电再给OV7670上电如果同时上电SCCB配置代码大概率会超时失败。很多模块上电后需要几十毫秒稳定时间所以初始化代码最前面务必备一个至少100ms的延时。2.3 PA8的三角关系MCO输出、VBUS使能、XCLK时钟源这里有个网搜热词“stm32f407 pa8 vbus typec”。PA8这颗引脚在F407上至少有三种身份一是MCO1时钟输出脚可以把HSE时钟通常8MHz引出二是一些TypeC接口开发板上PA8需要拉高来使能USB的VBUS供电三是在DCMI项目里有人想用PA8的MCO1输出给OV7670当XCLK。问题就出在这几件事互相冲突。如果PA8已经接到板子的TYPE-C VBUS使能电路上你再把它配成MCO1输出那VBUS控制就失效了板子可能直接掉电或者USB识别不了。反过来如果你已经用PA8控USB那就别指望MCO1输出。我这次用的是定时器PWM方式产生XCLK把PA8留给USB控制。具体来说选一个定时器输出通道给它配置PWM模式输出24MHz或12MHz方波给OV7670的XCLK。OV7670的XCLK范围很宽资料上写10MHz到48MHz都能工作典型值是24MHz但不是非得24MHz不可。如果定时器分频算不出整数比如APB1定时器84MHz分不出24MHz用12MHz或者16MHz照样能出图只是帧率会有差异。反正记住一个原则XCLK不要超过48MHz我建议24MHz以内PCLK输出越低DCMI采集压力越小。2.4 上电先测这几根线省得瞎猜示波器是排查摄像头问题最强的工具没有之一。上电后先别急着看LCD按下面顺序测一遍。XCLK测PA8或者定时器输出脚确认有方波输出频率对不对。PCLK测OV7670的PCLK引脚初始化成功后应当能看到持续脉冲。如果时序正常频率大概在几兆赫兹到十几兆赫兹之间。VSYNC测到帧同步脉冲说明传感器已经出帧了。如果VSYNC一直死高或死低多半是初始化没成功或供电不对。D0~D7这些线上应该能看到不规律跳变的数字信号不必分析具体内容有跳变就说明数据通路是活的。没有示波器的话逻辑分析仪也能凑合但采样率得够否则PCLK高频信号会失真。连示波器都没有的话就先用串口把OV7670的ID寄存器打印出来——读到0x76和0x73至少说明SCCB通信和传感器上电是正常的。3. SCCB初始化先让OV7670“说人话”3.1 SCCB与I2C那点事OV7670的控制总线叫SCCB全称Serial Camera Control Bus和I2C非常接近。SIO_C是时钟SIO_D是数据协议上有开始、停止、应答等概念和I2C的字节读写流程几乎一样。但SCCB有一个特点它支持三线模式多了一根SCCB_E片选线。对OV7670这种单设备的场景SCCB_E直接接地就行用两线模式操作。可以用F407的硬件I2C外设来跑SCCB也可以直接用两个GPIO模拟时序。我个人的建议是能模拟就模拟。硬件I2C在F1时代有一堆著名的坑F4虽然好多了但SCCB有些读时序对clock stretching等细节要求比较暧昧模拟时序虽然代码啰嗦一点但完全可控出问题好排查。反正摄像头初始化也就开机跑一次性能不是瓶颈。3.2 寄存器配置链路从软件复位到RGB565OV7670的寄存器非常多但真正要摸清的也就一组固定的初始化序列。这里整理一份我实测能出图的最小配置QVGA 320x240RGB565格式供参考。寄存器地址寄存器名写入值作用0x12COM70x80软件复位0x12COM70x04选择RGB输出关闭颜色条0x11CLKRC0x01内部PLL倍频外部时钟分频10x3ASCALING_XSC0x04水平缩放0x3BSCALING_YSC0x04垂直缩放0x3CSCALING_PCLK_DELAY0x02PCLK延迟调整0x3DSCALING_DCWCTR0x0FDSP缩放系数0x3ECOM140x1APCLK分频降低像素时钟0x40COM150xD0RGB565输出格式0x14COM90x1E自动增益上限这个表不是完整手册而是一条“能出图”的链路。COM7先写0x80复位再写0x04切到RGB模式这两步必须先后分开中间加个几毫秒延时。CLKRC的0x01表示开启内部PLL由它决定传感器内部时钟。COM15的0xD0就是RGB565bit7~bit4对应RGB格式选择0xD0恰好是RGB565。寄存器表后面的行进方向里0x3A~0x3E这组是缩放相关。OV7670的感光阵列是VGA分辨率要在320x240下工作就得靠内部DSP缩放。如果这组寄存器没配好输出的行时序和场时序会对不上LCD上看到的往往是压缩变形或者只有左上角一块有图。3.3 验证初始化成功的三种手段写代码时最怕的就是寄存器没生效后面所有时序分析都建立在错误前提上。我习惯用三个手段确认SCCB初始化真的成功了。第一读芯片ID。OV7670的0x0A寄存器值为0x760x0B寄存器值为0x73。初始化开始时读一次如果能正确读到这两个值说明SCCB总线物理连接是通的设备地址也没搞错。我一直建议把它放在初始化函数的最前面读不到就直接报错不要继续往下走。第二读COM7确认模式。初始化完成后读回0x12寄存器确认bit4RGB模式标志已经置位。寄存器能写能读比单纯写成功更可靠。第三看VSYNC波形。这要配合示波器或逻辑分析仪。初始化成功后VSYNC引脚上应该能看到周期性的帧同步信号。如果VSYNC完全静止那多半是寄存器配置里某个关键值写错了或者芯片根本没进入工作状态。4. DCMIDMA把像素流接入F407的主动脉4.1 DCMI的同步信号连接与极性配置DCMI支持硬件同步和嵌入式同步两种模式。OV7670这种传统传感器用硬件同步就够了也就是把VSYNC和HREF直接接到DCMI的对应引脚上。DCMI_CR寄存器里HSPOL和VSPOL两个位分别控制HSYNC和VSYNC的有效极性。这里有个很容易搞反的地方OV7670的HREF是高电平有效即高电平期间D0~D7上的数据有效VSYNC默认是低电平脉冲表示一帧开始。我实测下来HSPOL配置为高有效对应0VSPOL配置为低有效对应1能出图但这个不一定适用于所有模块不同批次OV7670的极性寄存器配置可能不一样。如果你的画面出现整帧上下颠倒、或者画面整体偏移优先翻转VSPOL试试这个判断成本最低。DCMI的像素时钟极性也分为上升沿采样和下降沿采样由PCKPOL位控制。OV7670的数据手册建议在PCLK下降沿采样数据但实际接F407时必须按你示波器看到的真实关系来。如果采样沿选错典型症状是图像每个像素都错乱、颜色不对、看起来像“油画”一样糊掉。4.2 DMA2 Stream1循环模式与双缓冲DCMI的DMA请求映射到DMA2的Stream1具体通道号查参考手册DCMI对应的是Channel 1。配置DMA时要把外设地址设为DCMI的数据寄存器地址DCMI_DR内存地址设为你的图像缓冲区数据宽度外设和内存都用32位方向是外设到内存模式用循环模式。为什么用32位而不是8位或16位因为DCMI_DR一次可以包含4字节数据用32位搬运效率最高DMA的突发传输也能发挥出来。一个32位字里装的是两个RGB565像素正好对应LCD上两个点的数据。F407内部DMA带宽完全跟得上QVGA的像素流。双缓冲是个非常实用的技巧。DMA的DBM位使能后可以设置两个内存地址M0AR和M1ARDMA在当前缓冲区写满后自动切到另一个缓冲区同时触发传输完成中断。这样一来CPU可以在DMA写缓冲B的时候处理缓冲A里的完整帧处理完再去切下一帧两不耽误。对图像采集这种需要持续吞吐的场景双缓冲几乎是最优雅的解法。4.3 帧中断在哪处理才不丢数据DCMI有两个关键中断帧结束中断和行结束中断。行结束中断千万别开每行都进中断的话CPU直接被打爆帧率会惨不忍睹。帧结束中断是必须开的因为一帧数据全部采完后你需要在这里做帧标志置位、缓冲切换、通知LCD刷屏这些事。帧结束中断里要做的事越少越好。我的习惯是中断里只置一个标志位顺便把DMA当前使用的缓冲索引读出来保存然后立刻退出。真正把数据送去LCD显示的操作放在主循环里做。如果直接在中断里执行刷屏函数刷屏本身会被其他中断打断反而更容易出错而且主程序被长时间阻塞实时性没法保证。5. 从RGB565到LCD屏一条不走回头路的显示链路5.1 一个必须正视的容量问题F407的RAM装不下一帧QVGA不带FIFO的OV7670在RGB565模式下输出QVGA320x240一帧的数据量是320乘240乘2字节算下来153600字节也就是150KB。而STM32F407的内存布局是128KB可被DMA访问的SRAM外加64KB只能CPU访问的CCM RAM。DMA根本够不到CCM所以能被DCMI的DMA用作缓冲区的只有那128KB。这就很尴尬了。一帧QVGA RGB565要150KB一次都装不下更别说双缓冲。很多第一次做这个项目的人开开心心定义一个320x240的数组编译一跑就溢出或者DMA写飞了。这也是“不带FIFO的OV7670”和“带FIFO的OV7670”在工程上差异最大的地方——带FIFO的版本你可以读一行处理一行而不带FIFO的版本数据是持续涌来的你没有一整个帧缓冲就接不住。5.2 方案一DCMI-DMA直接把像素灌进FSMC映射的LCD GRAM容量不够那就不要存。思路是把LCD控制器的GRAM直接映射成DMA的目标地址让DCMI采到的像素经由DMA通过FSMC总线直接写进LCD的显存完全绕开F407的SRAM。ILI9341、ST7789这类显示屏控制器内部都自带GRAM通过FSMC映射后写一个16位数据就是往GRAM里写一个像素。FSMC的地址线和LCD的D/CX引脚相连用不同地址段区分命令和数据。比如把LCD映射到BANK1的NE1片选基地址为0x60000000加上偏移量0x00020000把A18拉到高电平写这个地址就是写数据。这个偏移量没有统一标准取决于你的A几接了D/CX需要看原理图。访问FSMC映射地址看起来就是普通内存写所以DMA完全可以搬运。配置好LCD的显示窗口为320x240后DCMI的DMA目标地址设成LCD的数据地址DMA传输长度设成一帧的总字数。帧结束中断来临时整幅图已经被DMA自动刷上屏幕了。这个方案的极致之处在于F407内部RAM基本没占用CPU也只是在帧结束瞬间做点收尾工作。5.3 方案二行缓冲方案与裁剪配合如果你用的LCD不是FSMC并口而是SPI接口那方案一就行不通了因为DMA直接从DCMI搬数据到SPI外设节奏上很难和屏幕的刷新窗口对齐。这种情况下比较稳的做法是行缓冲。行缓冲的意思就是DCMI采一行DMA把这一行搬到SRAM里的一个小缓冲区然后CPU再把这行数据通过SPI发到LCD。一行320像素RGB565一共640字节缓冲区根本不需要多大。关键在于DCMI要和行同步信号配合好每次HREF有效开始时清一行缓冲HREF结束后立刻启动SPI发送。这里要善用DCMI的裁剪功能。DCMI可以配置只采集指定行列区域比如只采集屏幕中间320x240的区域忽略周围不需要的行消隐区和列消隐区能省去不少无效DMA传输。对行缓冲方案来说裁剪还有一个好处像素流从头到尾都是连续的不会出现行尾和下一行开头之间突然多出几个无效像素省掉很多边界判断逻辑。5.4 叠加中文、菜单的显示思路摄像头图像显示出来之后很多人还想在画面上叠加中文标题或者菜单。直接操作LCD屏幕上的像素需要“读改写”流程先读出当前像素的RGB565值再根据字模点阵决定要不要替换成别的颜色最后写回去。但如果你用的是DCMI直接灌GRAM的方案采集和刷屏是并行的读改写会被帧流冲掉这时候就要换个思路。更稳妥的做法是在图像数据到达LCD之前做叠加。比如走行缓冲方案时每一行像素在RAM里先过一遍把字模点阵叠上去再把合成后的数据发给LCD。这样屏幕上的中文菜单实际上是“嵌”在图像流里的不会因为帧刷新而闪烁。如果不想让中文透明度太复杂直接用颜色替换就行白底黑字、黑底白字都行。GB2312汉字取模通常是16x16点阵一个汉字32字节。用PCtoLCD2002这类工具选逐行式取模生成的字模数组按国标区位码索引排列代码里用UTF-8转GB2312或者直接查表就能定位到要显示的字模。这部分和普通LCD显示中文的流程完全一致只是混叠进摄像头图像流时注意一下扫描方向和LCD窗口设置别搞反。6. 花屏、错位、黑屏、低帧率我的排查记录6.1 满屏彩色噪点先查极性再查驱动现象是LCD上满屏都是乱跳的彩色噪点图像完全看不出内容像电视雪花一样。我排查时第一反应是DCMI的像素时钟极性反了。把PCKPOL位翻转之后噪点变成稳定的彩色条格再翻转回来又变回噪点——确认就是采样沿问题。OV7670输出数据在PCLK下降沿附近变化在上升沿相对稳定但做项目时不要背死结论直接测波形哪个沿采样干净就用哪个。如果翻转极性没用第二步查D0~D7接线顺序。之前我有一根杜邦线松了D3没接上画面立刻变成满屏带规则竖条的怪色。也可以用万用表对着原理图逐一量D0~D7到PE4~PE11的连通性确认没有错位和虚接。6.2 图像斜切或整帧偏移同步极性背锅图像能出但整体斜着切左右两半上下错开跟扑克牌被从中间斜着剪了一刀似的。这个问题基本锁定在行场同步信号的处理上。DCMI靠HREF来判断哪些PCLK是有效像素如果HSPOL极性配错DCMI会在行消隐期间也采集数据那数据流里的“行长度”就和真实不符显示出来自然是斜的。把HSPOL翻转后斜切就变成了正常的横向偏移再调整VSPOL整帧位置终于归位。整个过程没有任何代码逻辑错误纯粹是同步极性和传感器实际波形不匹配。也可能是行有效期间包含了过多的消隐后肩像素。OV7670的输出时序里HREF高电平期间并不是每一行都从第0列开始输出有效数据行首可能会有几个像素的消隐。如果DCMI没做裁剪这些像素会被当成有效数据存进缓冲区表现出来的就是画面整体向右偏移一行。解决方法是微调DCMI的起始坐标和宽度参数或者检查缩放寄存器里有没有把行首的无效列裁掉。6.3 黑屏和下半屏无图像初始化链路和缓冲容量黑屏分两种。一种是完全没有画面连彩色条都没有多半在初始化环节SCCB没通、OV7670没起振、XCLK没给上。按2.4节的方法示波器从XCLK一路测到VSYNC能快速定位问题在电源、时钟还是配置。另一种是下半屏有图像但上半屏黑或者反过来。这个现象我印象特别深——当时用的是一个38400字节的数组当缓冲区理论上QVGA RGB565得要150KB我只开了一个零头DMA写到缓冲区末尾就溢出后面的数据被系统内存管理直接掐了结果就是屏幕上只有区开始的一小段有内容。把方案改成DCMI-DMA直接灌FSMC LCD GRAM后这个问题彻底消失因为DMA的目标地址是LCD显存不存在内部RAM不够的问题。6.4 帧率上不去的真实瓶颈很多人在QVGA下只能跑到十几帧每秒还以为是DCMI或DMA慢其实瓶颈往往在LCD刷屏。SPI屏如果跑20MHz一帧320x240的RGB565数据约1.23Mbit光传输就要60多毫秒换算下来帧率上限也就16fps左右。FSMC并口屏会快很多一帧数据量一样但总线宽16位几毫秒就能刷完帧率瓶颈反而转移到摄像头输出端。调试时还有一个隐蔽的性能杀手——串口打印。有人习惯在帧中断里加printf打印帧计数串口波特率115200时打印一个字符都要近0.1ms每帧打印几次帧率肉眼可见往下掉。建议把串口打印关掉或者只在按键触发时才打印一帧。6.5 排查顺序总结现象优先怀疑验证手段处理方式满屏噪声PCLK极性、D0~D7接线示波器看PCLK与D0关系翻转PCKPOL查接线画面斜切HSPOL/VSPOL翻转极性格看变化按波形设置极性黑屏XCLK、SCCB通信测XCLK读ID查供电、查初始化代码图像右移行消隐未裁掉对比HREF与有效数据DCMI裁剪或调整寄存器下半屏黑缓冲容量不足计算一帧字节数用FSMC直灌GRAM或行缓冲帧率低LCD刷屏、printf屏蔽刷屏/关串口对比换FSMC屏去掉调试打印7. 最小工程骨架能跑起来的代码框架7.1 DCMIDMA初始化HAL风格下面这段代码是HAL库风格的初始化框架目的不是给你一个完整可编译工程而是把关键配置串起来讲明白。DCMI_HandleTypeDef hdcmi; DMA_HandleTypeDef hdma_dcmi; void DCMI_Init(void) { GPIO_InitTypeDef gpio; __HAL_RCC_DCMI_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOE_CLK_ENABLE(); __HAL_RCC_DMA2_CLK_ENABLE(); gpio.Pin GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6; gpio.Mode GPIO_MODE_AF_PP; gpio.Pull GPIO_NOPULL; gpio.Speed GPIO_SPEED_FREQ_VERY_HIGH; gpio.Alternate GPIO_AF13_DCMI; HAL_GPIO_Init(GPIOA, gpio); gpio.Pin GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7 | GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11; HAL_GPIO_Init(GPIOE, gpio); 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_WORD; hdma_dcmi.Init.MemDataAlignment DMA_MDATAALIGN_WORD; hdma_dcmi.Init.Mode DMA_CIRCULAR; hdma_dcmi.Init.Priority DMA_PRIORITY_HIGH; HAL_DMA_Init(hdma_dcmi); __HAL_LINKDMA(hdcmi, DMA_Handle, hdma_dcmi); hdcmi.Instance DCMI; hdcmi.Init.SynchroMode DCMI_SYNC_HARD; hdcmi.Init.PCKPolarity DCMI_PCKPOLARITY_FALLING; hdcmi.Init.VSPolarity DCMI_VSPOLARITY_LOW; hdcmi.Init.HSPolarity DCMI_HSPOLARITY_HIGH; hdcmi.Init.CaptureRate DCMI_CR_ALL_FRAME; hdcmi.Init.ExtendedDataMode DCMI_EXTEND_DATA_8B; hdcmi.Init.JPEGMode DCMI_JPEG_DISABLE; hdcmi.Init.ByteSelectMode DCMI_BSM_ALL; hdcmi.Init.LineSelectMode DCMI_LSM_ALL; hdcmi.Init.LineStart 0; hdcmi.Init.LineCount 240; hdcmi.Init.CaptureStart 0; hdcmi.Init.CaptureCount 320; HAL_DCMI_Init(hdcmi); }7.2 OV7670初始化驱动骨架SCCB的部分用GPIO模拟的话核心就是几个延时和信息翻转的时序函数。完整工程里需要写SCCB_Start、SCCB_Stop、SCCB_WriteByte、SCCB_ReadByte这几个底层函数然后在上层用一个循环把寄存器表写进去。static const uint8_t ov7670_regs[][2] { {0x12, 0x80}, // COM7 software reset {0x12, 0x04}, // COM7 RGB {0x11, 0x01}, // CLKRC PLL {0x3A, 0x04}, // SCALING_XSC {0x3B, 0x04}, // SCALING_YSC {0x3C, 0x02}, // PCLK delay {0x3D, 0x0F}, // SCALING_DCWCTR {0x3E, 0x1A}, // COM14 {0x40, 0xD0}, // COM15 RGB565 {0x14, 0x1E}, // COM9 }; int OV7670_Init(void) { if (SCCB_ReadReg(0x0A) ! 0x76 || SCCB_ReadReg(0x0B) ! 0x73) { return -1; // 读不到ID检查接线和供电 } for (int i 0; i sizeof(ov7670_regs) / 2; i) { SCCB_WriteReg(ov7670_regs[i][0], ov7670_regs[i][1]); } HAL_Delay(200); return 0; }一个经验写完寄存器后等几百毫秒再开始DCMI采集让摄像头内部锁相环和自动曝光先稳定下来。否则第一帧图像往往偏暗或者颜色不对容易被误判成配置错误。7.3 刷新LCD和帧同步逻辑如果采用FSMC直灌LCD GRAM的方案主循环的逻辑可以写得非常清爽volatile uint8_t frame_ready 0; void DCMI_IRQHandler(void) { if (__HAL_DCMI_GET_FLAG(hdcmi, DCMI_FLAG_FRAME)) { __HAL_DCMI_CLEAR_FLAG(hdcmi, DCMI_FLAG_FRAME); frame_ready 1; } } int main(void) { // 初始化时钟、GPIO、LCD、OV7670、DCMIDMA LCD_SetWindow(0, 0, 320, 240); HAL_DCMI_Start_DMA(hdcmi, DCMI_MODE_CONTINUOUS, (uint32_t)LCD_DATA_ADDR, 320 * 240 * 2 / 4); while (1) { if (frame_ready) { frame_ready 0; // 这里可以做帧计数、图像分析、菜单叠加 } } }注意HAL_DCMI_Start_DMA的最后一个参数是传输的32位字数不是字节数。320乘240乘2字节再除以4正好是38400个32位字。这个数写错会导致DMA传输长度不对屏幕上出现固定位置的横向截断。7.4 这个项目后续还能往哪个方向扩展跑通图像采集和显示只是第一步。同一个硬件平台往后面能接的事情其实很多。比如把RGB565转成灰度图再做简单的边缘检测或颜色识别配合F407的FPU可以做不少轻量级图像处理如果外挂一颗编码器或电机驱动就能组成一个色块追踪小车的基本雏形。也可以把采集到的图像通过串口或USB发到上位机形成一套简单的图像采集系统。如果想深入玩DCMI值得研究一下它的裁剪功能、JPEG模式、以及不同像素格式下的EDM字节交换配置。F407的DCMI虽然不如新一代MCU的摄像头接口功能丰富但作为理解图像采集链路“像素时钟到底怎么和DMA协同”的教材恰到好处。我个人做完这个项目的最大体会是真正难的从来不是配置几行寄存器而是理解那条数据通路在每个时钟周期里到底在干什么——理解了这一点不管换什么摄像头、什么屏幕都只是换张皮而已。
返回列表