资讯动态

STM32驱动OV7670无FIFO方案:DCMI与DMA时序实战

发布时间:2026/10/14 18:34:47 来源:尧图企业网站定制
简介这份资源面向使用STM32F1系列驱动OV7670摄像头、且不依赖FIFO缓冲的嵌入式开发者与电子爱好者尤其适合正在做图像采集入门实验或课程设计的学生。作者基于自身摸索经验编写了完整源码并额外加入图像处理函数可实现亮度、饱和度、对比度调节按下KEY1即可完成二值化与灰度化转换同时预留了颜色识别接口方便二次开发。压缩包共312个文件约10.8MB以71个c源文件与63个h头文件为核心配合Keil工程文件、编译中间产物、少量png截图与说明文档覆盖摄像头驱动、LCD显示、SD卡读写及图像算法等模块。目前已有4558人学习下载适合希望跳过从零搭建、直接研究无FIFO采集流程与图像处理实现的读者参考借鉴。1. STM32 驱动 OV7670 无 FIFO 方案为什么它值得你花一个周末如果你手头有一块 STM32 开发板和一颗 OV7670 摄像头模组又恰好没买带 FIFO 的版本你大概率已经在网上搜过一圈然后被两种极端答案劝退一种说“没 FIFO 根本没法用直接换带 FIFO 的”另一种甩给你一坨寄存器配置连时钟怎么设都不讲。实际情况是OV7670 无 FIFO 方案完全可跑只是它把难度从“接线”转移到了“时序控制”上。带 FIFO 的模组本质上是帮你加了一颗缓存芯片让 MCU 可以慢慢读无 FIFO 则要求 MCU 在摄像头输出像素的那一瞬间就接住数据。这听起来吓人但对 STM32 来说只要用对 DCMI 外设加 DMA再配合正确的时钟极性就能稳定抓取 QVGA 甚至 VGA 帧。这套方案适合想真正理解图像采集时序的嵌入式开发者也适合预算有限、手头只有无 FIFO 模组的场景。它不是什么玄学而是一道关于时钟、极性和缓冲区的算术题。2. 无 FIFO 采集的时序账DCMI 到底在等什么2.1 像素时钟、行同步与场同步的三方握手OV7670 输出图像时会同时给出三个关键信号PCLK像素时钟、HREF行参考有的模组标 HSYNC、VSYNC场同步。每来一个 PCLK 上升沿或下降沿数据总线上就出现一个字节的像素数据。HREF 拉高表示一行有效像素正在输出VSYNC 拉低或拉高表示一帧的开始。STM32 的 DCMI 外设就是专门用来接这套信号的它内部有行计数器、场计数器和像素计数器能自动把有效像素打包进 DMA 缓冲区。关键点在于DCMI 必须知道 PCLK 的哪个边沿采样数据以及 HREF 和 VSYNC 的有效极性。OV7670 默认输出 PCLK 在下降沿变化、上升沿稳定所以 DCMI 通常配置为上升沿采样。如果你把极性配反抓到的图像会整体偏移或直接花屏这是最常见的翻车点之一。2.2 为什么无 FIFO 必须用 DMA 而不是轮询有人会想我用 GPIO 轮询读数据行不行理论上行实际上不行。QVGA 分辨率下PCLK 频率通常在 8MHz 到 24MHz 之间意味着每 40 到 125 纳秒就有一个新字节。用 CPU 轮询光是判断 HREF 和读 GPIO 的开销就超过这个时间结果就是丢行、丢像素图像出现斜纹或撕裂。DMA 的作用是把 DCMI 接收到的每个字节自动搬到内存CPU 只在整帧结束后处理一次。这样即使主频不高的 STM32F1 系列也能稳定接收 QVGA 帧。常见做法是配置 DMA 为循环模式缓冲区大小设为一帧的字节数DCMI 每收一个字节就触发一次 DMA 请求。注意 DMA 的传输宽度要设成字节因为 OV7670 输出的是 8 位数据。2.3 时钟树配置别让 PCLK 超过 DCMI 的承受范围STM32 的 DCMI 外设对 PCLK 有上限要求具体值取决于芯片型号和供电电压。以常见的 STM32F407 为例DCMI 的 PCLK 最高可以到 54MHz但实际用 OV7670 时PCLK 由 OV7670 内部的分频寄存器决定通常设在 8MHz 到 24MHz 之间。问题在于OV7670 的时钟输入来自 STM32 输出的 MCO微控制器时钟输出或外部晶振。如果你用 STM32 的 MCO 输出 24MHz 给 OV7670再让 OV7670 分频出 PCLK就要确保 MCO 的配置正确。很多新手在这里踩坑MCO 没使能或者输出频率不对导致 OV7670 根本不工作。我一般会先用示波器量 MCO 引脚确认有稳定时钟后再调 OV7670 的寄存器。// STM32F4 配置 MCO1 输出 24MHz 给 OV7670 作为 XVCLK // 假设 HSE 为 8MHzPLL 配置略这里只展示 MCO 部分 void MCO1_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; // 使能 GPIOC 时钟 __HAL_RCC_GPIOC_CLK_ENABLE(); // PC8 作为 MCO1 输出引脚 GPIO_InitStruct.Pin GPIO_PIN_8; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_VERY_HIGH; GPIO_InitStruct.Alternate GPIO_AF0_MCO; HAL_GPIO_Init(GPIOC, GPIO_InitStruct); // 选择 HSE 作为 MCO1 时钟源分频系数 1则输出 8MHz // 若需要 24MHz需先配置 PLL 并选择 PLL 作为 MCO1 源 HAL_RCC_MCOConfig(RCC_MCO1, RCC_MCO1SOURCE_HSE, RCC_MCODIV_1); }上面代码展示了 MCO 的基本配置逻辑。参数说明RCC_MCO1SOURCE_HSE表示用外部高速晶振作为时钟源RCC_MCODIV_1表示不分频。如果你需要 24MHz而 HSE 是 8MHz就得先把 PLL 配好然后选择RCC_MCO1SOURCE_PLLCLK并设置合适的分频。注意 MCO 引脚要配置为复用推挽模式否则没有输出。逻辑上这一步是整条链路的第一环MCO 不对后面全白搭。3. 从寄存器到第一帧图像OV7670 无 FIFO 的初始化顺序3.1 SCCB 写寄存器先软复位再改时钟分频OV7670 用 SCCB 协议配置寄存器它和 I2C 几乎一样但时序略有差异。STM32 可以用硬件 I2C 或软件模拟我一般用软件模拟因为 OV7670 的 SCCB 对时钟拉伸不敏感软件模拟更省事。初始化顺序很重要先写0x12寄存器做软复位延时至少 10ms然后写0x11设置内部时钟分频。0x11的值决定 PCLK 频率公式是 PCLK XVCLK / (2 * (CLKRC[5:0] 1))。假设 XVCLK 是 24MHz你想得到 12MHz 的 PCLK就写0x11 0x00因为 24 / (2 * (0 1)) 12MHz。如果写0x11 0x01PCLK 就是 8MHz。这个寄存器直接影响后续 DCMI 的采样时序必须先定下来。// 软件模拟 SCCB 写一个字节到 OV7670 寄存器 // sccb_start, sccb_stop, sccb_write_byte 等底层函数略 void OV7670_WriteReg(uint8_t reg, uint8_t data) { SCCB_Start(); SCCB_WriteByte(0x42); // OV7670 写地址 SCCB_WriteByte(reg); // 寄存器地址 SCCB_WriteByte(data); // 写入数据 SCCB_Stop(); HAL_Delay(1); // 每个寄存器写入后稍作延时 } void OV7670_Init(void) { // 软复位 OV7670_WriteReg(0x12, 0x80); HAL_Delay(50); // 等待复位完成 // 设置时钟分频XVCLK24MHzPCLK12MHz OV7670_WriteReg(0x11, 0x00); // 后续配置输出格式、分辨率等 // ... }逻辑说明0x12写0x80是软复位命令所有寄存器恢复默认值。延时 50ms 是保险做法手册要求至少 10ms。0x11写0x00得到 12MHz PCLK这个频率对 STM32F1 和 F4 都比较友好。参数上如果你用的 XVCLK 不是 24MHz需要重新计算0x11的值。注意每次写寄存器后加 1ms 延时避免 SCCB 总线冲突。3.2 输出格式与分辨率QVGA RGB565 的寄存器组合OV7670 支持多种输出格式无 FIFO 方案最常用的是 RGB565因为每个像素两个字节DCMI 可以直接按字节接收。分辨率选 QVGA320x240一帧就是 320 * 240 * 2 153600 字节。寄存器配置上0x12的 bit4 控制输出格式RGB565 对应0x12 0x04复位后默认是 YUV需要改。0x40到0x4B是一组伽马和色彩矩阵寄存器通常用推荐值。0x17和0x18控制 HREF 和 VSYNC 的极性默认值一般可用但如果图像上下颠倒或左右镜像就改这两个。0x32到0x36是行场时序参数QVGA 下用常见配置即可。我一般会准备一个初始化数组把几十个寄存器一次性写进去这样代码干净也方便换分辨率。// OV7670 QVGA RGB565 初始化寄存器序列部分 const uint8_t ov7670_qvga_rgb565[][2] { {0x12, 0x04}, // 输出格式RGB565 {0x40, 0xD0}, // 伽马曲线 {0x41, 0x08}, {0x42, 0x00}, {0x43, 0x00}, {0x44, 0x10}, {0x45, 0x10}, {0x46, 0x10}, {0x47, 0x10}, {0x48, 0x10}, {0x49, 0x10}, {0x4A, 0x10}, {0x4B, 0x10}, {0x17, 0x14}, // HREF 极性等 {0x18, 0x00}, {0x32, 0x80}, // 行时序 {0x33, 0x00}, {0x34, 0x00}, {0x35, 0x00}, {0x36, 0x00}, // ... 更多寄存器 }; void OV7670_Config(void) { for (int i 0; i sizeof(ov7670_qvga_rgb565) / 2; i) { OV7670_WriteReg(ov7670_qvga_rgb565[i][0], ov7670_qvga_rgb565[i][1]); } }这段代码把寄存器配置和写入逻辑分开方便维护。参数说明0x12 0x04是 RGB565 输出0x17 0x14是常见的 HREF 和 VSYNC 极性设置具体值可能因模组厂商略有差异。如果图像出现上下颠倒改0x1E的 bit4左右镜像改0x1E的 bit5。注意这些寄存器值不是绝对的不同批次的 OV7670 可能需要微调但这一组在多数模组上能出图。3.3 DCMI 与 DMA 联动一帧图像怎么落进内存DCMI 的配置分几步使能 DCMI 时钟配置 DCMI 引脚为复用模式设置 DCMI 参数采样边沿、HREF 极性、VSYNC 极性配置 DMA 通道最后使能 DCMI 和 DMA。DMA 的源地址是 DCMI 数据寄存器目标地址是内存缓冲区传输长度是一帧字节数。关键参数DCMI_InitTypeDef里的HSPolarity和VSPolarity要和 OV7670 输出一致PCLKPolarity选上升沿PCLKPeriod一般设 0。DMA 模式选循环模式这样一帧结束后自动开始下一帧不会停。但要注意如果缓冲区只有一帧大小DMA 循环模式会覆盖前一帧数据所以要么用双缓冲要么在帧中断里及时处理。// DCMI 和 DMA 配置示例STM32F4 HAL 库 void DCMI_DMA_Init(uint8_t *buffer, uint32_t length) { DCMI_HandleTypeDef hdcmi; DMA_HandleTypeDef hdma; // 使能时钟略 // 配置 DCMI 引脚略 hdcmi.Instance DCMI; hdcmi.Init.HSPolarity DCMI_HSPOLARITY_LOW; hdcmi.Init.VSPolarity DCMI_VSPOLARITY_LOW; hdcmi.Init.PCLKPolarity DCMI_PCLKPOLARITY_RISING; hdcmi.Init.PCLKPeriod 0; hdcmi.Init.SynchroMode DCMI_SYNCHRO_HARDWARE; hdcmi.Init.ExtendedDataMode DCMI_EXTEND_DATA_8B; hdcmi.Init.PCKPolarity DCMI_PCKPOLARITY_RISING; HAL_DCMI_Init(hdcmi); // DMA 配置 hdma.Instance DMA2_Stream1; hdma.Init.Channel DMA_CHANNEL_1; hdma.Init.Direction DMA_PERIPH_TO_MEMORY; hdma.Init.PeriphInc DMA_PINC_DISABLE; hdma.Init.MemInc DMA_MINC_ENABLE; hdma.Init.PeriphDataAlignment DMA_PDATAALIGN_BYTE; hdma.Init.MemDataAlignment DMA_MDATAALIGN_BYTE; hdma.Init.Mode DMA_CIRCULAR; hdma.Init.Priority DMA_PRIORITY_HIGH; HAL_DMA_Init(hdma); __HAL_LINKDMA(hdcmi, DMA_Handle, hdma); // 启动 DCMI DMA 接收 HAL_DCMI_Start_DMA(hdcmi, DCMI_MODE_CONTINUOUS, (uint32_t)buffer, length); }逻辑说明HSPolarity和VSPolarity设为低有效对应 OV7670 默认输出。PCLKPolarity设为上升沿采样因为 OV7670 在下降沿变化数据。ExtendedDataMode设为 8 位因为 OV7670 输出 8 位数据。DMA 配置为循环模式PeriphDataAlignment和MemDataAlignment都是字节。HAL_DCMI_Start_DMA的最后一个参数是传输长度QVGA RGB565 就是 153600。注意 DMA 流和通道要查芯片手册不同 STM32 系列不一样。如果图像出现横向条纹多半是 PCLK 极性反了改成下降沿试试。4. 避坑与排查无 FIFO 方案最容易翻车的五个地方4.1 图像花屏或斜纹PCLK 极性和采样边沿不匹配现象抓到的图像全是彩色斜纹或者像电视雪花。原因DCMI 的采样边沿和 OV7670 的数据变化边沿重合了导致数据不稳定。OV7670 默认在 PCLK 下降沿变化数据上升沿稳定所以 DCMI 应该配上升沿采样。如果配成下降沿就会采到变化中的值。解决把PCLKPolarity改成DCMI_PCLKPOLARITY_RISING或者改 OV7670 的0x11寄存器 bit4 反转 PCLK 极性。我一般先用示波器看 PCLK 和数据的关系确认稳定点后再配 DCMI。4.2 只有半屏或图像错位HREF 极性搞反了现象图像只显示左半部分或者上下两半错位。原因HREF 的有效极性配反了DCMI 把无效行也当成了有效行。OV7670 的 HREF 默认高有效但有些模组出厂配置不同。解决改HSPolarity为DCMI_HSPOLARITY_HIGH或LOW同时检查 OV7670 的0x17寄存器 bit1。如果图像左右镜像改0x1E的 bit5上下颠倒改 bit4。4.3 DMA 传输一半就停缓冲区大小和帧长度不匹配现象DMA 传输完成中断只触发一次之后不再更新。原因DMA 配置成了普通模式而不是循环模式或者传输长度设错了。QVGA RGB565 一帧是 153600 字节如果长度写成 320*240就少了一半。解决确认HAL_DCMI_Start_DMA的长度参数是320 * 240 * 2并且 DMA 模式是DMA_CIRCULAR。另外如果用了双缓冲要确保两个缓冲区都足够大。4.4 图像全黑或全白OV7670 寄存器没写进去现象DCMI 有数据但图像全黑或全白。原因SCCB 写入失败OV7670 还在默认的 YUV 模式或者时钟分频没设对。解决用逻辑分析仪抓 SCCB 波形确认从机地址是0x42写和0x43读。另外写0x12软复位后要延时足够否则后续寄存器写不进去。我习惯在初始化后读回0x12的值确认是0x04而不是0x80。4.5 帧率过低或丢帧PCLK 太快或 DMA 优先级不够现象图像能出但帧率只有几帧或者偶尔丢帧。原因PCLK 频率超过了 DCMI 或 DMA 的处理能力或者 DMA 优先级被其他外设抢占。解决降低 PCLK把0x11的值加大比如从0x00改成0x01PCLK 从 12MHz 降到 8MHz。同时把 DMA 优先级设为DMA_PRIORITY_VERY_HIGH。如果还不行检查内存总线是否被 LCD 或其他 DMA 占用。5. 进阶技巧用双缓冲和帧中断把采集做成流水线5.1 双缓冲机制一帧采集时另一帧可以处理单缓冲的问题是DMA 在写缓冲区的时候CPU 不能读否则数据会撕裂。双缓冲用两个缓冲区交替DMA 写缓冲区 A 时CPU 处理缓冲区 BDMA 写满 A 后自动切到 B并触发中断CPU 再处理 A。STM32 的 DMA 支持双缓冲模式配置DMA_Mode为DMA_DOUBLE_BUFFER然后指定两个内存地址。这样图像采集和处理可以并行帧率能提升一倍。注意两个缓冲区都要是连续内存大小为一帧字节数。// DMA 双缓冲配置STM32F4 hdma.Init.Mode DMA_DOUBLE_BUFFER; hdma.Init.MemBurst DMA_MBURST_SINGLE; hdma.Init.PeriphBurst DMA_PBURST_SINGLE; // 启动时指定两个缓冲区 HAL_DMAEx_MultiBufferStart(hdma, (uint32_t)DCMI-DR, (uint32_t)buffer0, (uint32_t)buffer1, length);参数说明buffer0和buffer1是两个帧缓冲区length是一帧字节数。HAL_DMAEx_MultiBufferStart会自动在两者之间切换。逻辑上当 DMA 写满buffer0时会触发传输完成中断此时buffer1正在被写CPU 可以安全处理buffer0。注意中断里要清除标志位否则会反复触发。5.2 帧中断里做轻量处理别在中断里跑图像算法DCMI 有帧中断和行中断帧中断在一帧结束时触发。很多人想在帧中断里直接做图像处理比如二值化或边缘检测结果导致下一帧丢失。正确做法是帧中断里只设置一个标志位主循环检测到标志位后再处理。处理时如果用了双缓冲要确保处理时间小于一帧的采集时间。QVGA 一帧 153600 字节如果主频 168MHz处理时间通常够用但复杂的浮点运算还是可能超时。我一般会在帧中断里翻转一个 LED用示波器看帧率这样能直观判断处理是否超时。5.3 用 DCMI 的行中断做 ROI 采集有时候你只关心图像的一部分比如中间 100x100 的区域。DCMI 支持行中断可以在收到指定行数时触发。结合 DMA 的传输长度可以实现 ROI 采集只把感兴趣的行搬进内存。具体做法是在行中断里重新配置 DMA 的目标地址和长度跳过不需要的行。这比全帧采集再裁剪节省内存和带宽。注意行中断频率很高中断服务函数要尽量短否则会丢行。我一般只在行中断里改 DMA 寄存器不做其他操作。5.4 验证采集是否稳定的三个土办法第一个办法在帧中断里翻转 GPIO用示波器量频率如果频率稳定在预期帧率说明采集没丢帧。第二个办法往缓冲区里填固定图案比如全红或全蓝然后看屏幕显示是否一致如果有杂色说明 DMA 传输有误。第三个办法用串口把一帧的校验和发出来连续采集 100 帧看校验和是否稳定。如果校验和跳动很大说明时序有问题。这三个办法不需要额外工具一个示波器或串口助手就能定位大部分问题。我自己的习惯是每次调无 FIFO 采集先不接屏幕直接用串口打印帧计数和校验和确认数据稳定后再接显示。这样能把显示问题和采集问题分开少走很多弯路。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑