STM32F407与OV7670摄像头DCMI+DMA图像采集实战

发布时间:2026/10/15 12:16:10
STM32F407与OV7670摄像头DCMI+DMA图像采集实战 简介这份资源是面向嵌入式初学者与进阶开发者的STM32F407驱动OV7670摄像头实验工程重点解决图像采集与LCD实时显示问题涉及GPIO、I2C/SPI接口配置、图像数据读取、色彩转换与帧缓冲管理等环节适合作为微控制器外设交互与图像处理的练手项目。压缩包共639个文件约22.73MB以c与h源码为主配合d、o、crf等编译中间文件另有asm汇编、uvprojx与uvoptx等Keil工程配置、hex与map输出及少量说明文档完整保留了从驱动到应用层的工程结构。目前已有3299人学习下载。工程内含uCOS实时操作系统相关移植代码与延时模块读者可借此理解摄像头初始化、分辨率与帧率设置、中断接收数据、LCD接口适配及双缓冲防撕裂等实现思路对掌握STM32与OV7670协同开发有较高参考价值。1. 从一块闲置的 F407 和一颗 OV7670 说起手头同时有 STM32F407 开发板和 OV7670 摄像头模块的人大概率都动过同一个念头能不能让这块板子把图像采回来再显示到屏幕上。这个念头听起来简单真正动手时却会发现OV7670 没有 FIFO 时对时序极其敏感F407 的 DCMI 接口虽然硬件上能接但时钟极性、行场信号、数据位对齐任何一项配错屏幕上就是一片雪花或者纯色。STM32F407 加 OV7670 摄像头实验这个组合本质上是在解决一个嵌入式视觉的入门问题如何用一颗没有 FIFO 的廉价摄像头在 Cortex-M4 上稳定拿到一帧可用的 RGB565 图像。它适合已经点过 LED、调过串口、能看懂时钟树配置的嵌入式开发者也适合想从单片机过渡到图像采集方向的学生和工程师。这篇文章不讲空泛的架构只讲我实际接线、配置 DCMI、调 DMA、处理色彩异常时踩过的那些坑以及一套能直接复现的最小工程路径。2. 硬件连线与时钟树OV7670 在 F407 上到底怎么接2.1 为什么 OV7670 的 XCLK 不能随便给OV7670 需要外部提供一个主时钟通常叫 XCLK典型值是 24MHz。很多教程直接拿 F407 的 MCO1 输出 8MHz 或者用 PWM 凑一个频率结果图像能出但颜色偏、噪点多。原因在于 OV7670 内部 PLL 对输入时钟的占空比和抖动有要求XCLK 不稳PCLK 就跟着抖DCMI 采样窗口就会错位。我一般会这样处理用 TIM3 的 PWM 输出 24MHz 方波或者直接用一个 24MHz 有源晶振给摄像头。如果板子上有 MCO1也可以配置 PLLI2S 输出但要注意 MCO 的驱动能力有限线长了波形会变差。下面是一段用 TIM3 产生 24MHz XCLK 的配置代码系统时钟按 168MHz 算// TIM3 时钟 84MHzPWM 频率 84MHz / (ARR1) / (PSC1) // 目标 24MHz取 PSC0ARR2占空比 50% 即 CCR1 void OV7670_XCLK_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; TIM_HandleTypeDef htim3 {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_TIM3_CLK_ENABLE(); // PB4 复用为 TIM3_CH1 GPIO_InitStruct.Pin GPIO_PIN_4; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_VERY_HIGH; GPIO_InitStruct.Alternate GPIO_AF2_TIM3; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); htim3.Instance TIM3; htim3.Init.Prescaler 0; htim3.Init.CounterMode TIM_COUNTERMODE_UP; htim3.Init.Period 2; // ARR23 个计数周期 htim3.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(htim3); TIM_OC_InitTypeDef sConfigOC {0}; sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 1; // CCR1占空比约 50% sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(htim3, sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1); }这段代码的关键参数是 ARR 和 CCR。ARR2 意味着计数器每 3 个时钟周期溢出一次84MHz 除以 3 正好是 28MHz不是精确的 24MHz。如果要精确 24MHz可以把系统时钟或 TIM3 时钟改一下比如 TIM3 挂到 72MHz 的 APB1 上ARR2 得到 24MHz。实际调试时用示波器量 PB4频率偏差在 5% 以内 OV7670 一般能锁住但颜色会轻微偏暖这是 PLL 抖动带来的。2.2 DCMI 与 OV7670 的信号对应关系OV7670 的引脚不少但真正接进 DCMI 的只有几类PCLK、VSYNC、HREF、D0~D7。F407 的 DCMI 接口引脚是固定的不能随便映射必须查数据手册的复用表。下面这张表是我实际用过的对应关系基于常见的 F407 开发板排针布局OV7670 信号F407 引脚DCMI 功能备注PCLKPA6DCMI_PIXCLK像素时钟上升沿采样VSYNCPB7DCMI_VSYNC帧同步高电平有效HREFPA4DCMI_HSYNC行同步高电平有效D0PC6DCMI_D0数据位最低位D1PC7DCMI_D1D2PC8DCMI_D2D3PC9DCMI_D3D4PE4DCMI_D4D5PB6DCMI_D5D6PE5DCMI_D6D7PE6DCMI_D7数据位最高位XCLKPB4TIM3_CH124MHz 主时钟SCLPB10I2C2_SCLSCCB 配置时钟SDAPB11I2C2_SDASCCB 配置数据PWDN任意 GPIO输出低电平工作RESET任意 GPIO输出低电平复位接线时最容易翻车的是 D0~D7 的顺序。OV7670 输出的数据是 RGB565两个字节拼成一个像素如果 D0 和 D7 接反图像会变成条纹或者颜色完全错乱。我习惯先用杜邦线接好然后写一个只读一个像素的测试程序打印出原始值确认字节顺序后再继续。2.3 SCCB 配置OV7670 的寄存器不是 I2C 但很像OV7670 用 SCCB 协议配置寄存器电气特性和 I2C 几乎一样但不支持重复起始条件也不要求 ACK。很多人的做法是直接用 STM32 的硬件 I2C 去写结果卡在等待 ACK 上。我的做法是用软件模拟 I2C把 SCL 和 SDA 当普通 GPIO 翻转不检测 ACK写完就过。下面是一段写寄存器的核心代码// 软件 SCCB 写一个字节到指定寄存器 void SCCB_WriteReg(uint8_t reg, uint8_t data) { SCCB_Start(); SCCB_SendByte(0x42); // OV7670 写地址7 位地址 0x21 左移一位 SCCB_SendByte(reg); SCCB_SendByte(data); SCCB_Stop(); HAL_Delay(1); // 部分寄存器需要延时尤其是时钟相关 } // 初始化序列里最关键的一步设置输出格式为 RGB565 void OV7670_SetRGB565(void) { SCCB_WriteReg(0x12, 0x80); // 复位所有寄存器 HAL_Delay(10); SCCB_WriteReg(0x12, 0x14); // 设置输出格式0x14 对应 RGB565 SCCB_WriteReg(0x40, 0xD0); // 行场信号极性等 SCCB_WriteReg(0x11, 0x80); // 内部时钟分频影响帧率 SCCB_WriteReg(0x0C, 0x00); // 关闭一些默认滤波 // 更多寄存器按需配置但 0x12 和 0x40 是必须的 }这里 0x12 寄存器的值 0x14 是 RGB565 输出如果写成 0x04 就是 RGB444颜色会少很多。0x40 的 0xD0 决定了 VSYNC 和 HREF 的极性如果极性反了DCMI 会认为一帧永远不结束DMA 传满缓冲区就停不下来。写完这些寄存器后最好读回来验证一下SCCB 读操作和写类似只是地址最低位置 1。3. DCMI 加 DMA 的采集链路从寄存器到内存的完整配置3.1 DCMI 初始化里那几个不能填错的参数DCMI 的 HAL 库初始化结构体里有几个参数直接决定能不能采到图。我见过最多的问题是HSPolarity和VSPolarity填反导致行场信号识别错误DMA 收到的数据全是 0 或者 0xFFFF。下面是我实际用的配置void DCMI_Init(void) { DCMI_HandleTypeDef hdcmi {0}; hdcmi.Instance DCMI; hdcmi.Init.SynchroMode DCMI_SYNCHRO_HARDWARE; // 硬件同步用 VSYNC/HREF hdcmi.Init.PCKPolarity DCMI_PCKPOLARITY_RISING; // PCLK 上升沿采样 hdcmi.Init.VSPolarity DCMI_VSPOLARITY_HIGH; // VSYNC 高电平有效 hdcmi.Init.HSPolarity DCMI_HSPOLARITY_HIGH; // HREF 高电平有效 hdcmi.Init.CaptureRate DCMI_CR_ALL_FRAME; // 全帧捕获 hdcmi.Init.ExtendedDataMode DCMI_EXTEND_DATA_8B; // 8 位数据 hdcmi.Init.JPEGMode DCMI_JPEG_DISABLE; // 不用 JPEG HAL_DCMI_Init(hdcmi); }PCKPolarity选上升沿还是下降沿取决于 OV7670 在 PCLK 的哪个边沿输出稳定数据。大部分模块在上升沿输出所以 DCMI 在上升沿采样。如果图像出现水平方向的错位或者重影把这里改成下降沿试试。CaptureRate选全帧意味着每来一个 VSYNC 就抓一帧如果选半帧图像会只更新一半。3.2 DMA 双缓冲为什么单缓冲会撕裂DCMI 采集数据的速度是 PCLK 决定的OV7670 在 24MHz XCLK 下 PCLK 大概 12MHz 到 24MHz一帧 320x240 的 RGB565 是 153600 字节。如果用单缓冲DMA 传完一半的时候 CPU 去读读到的可能是上半帧旧数据和下半帧新数据的混合屏幕上就是撕裂。F407 的 DMA 支持双缓冲模式DCMI 的 DMA 流是 DMA2_Stream1通道 1。配置如下// DMA 双缓冲配置buf0 和 buf1 各 153600 字节 void DCMI_DMA_Init(uint8_t *buf0, uint8_t *buf1, uint32_t len) { __HAL_RCC_DMA2_CLK_ENABLE(); DMA_HandleTypeDef hdma_dcmi {0}; 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_WORD; hdma_dcmi.Init.MemDataAlignment DMA_MDATAALIGN_WORD; hdma_dcmi.Init.Mode DMA_CIRCULAR; // 循环模式配合双缓冲 hdma_dcmi.Init.Priority DMA_PRIORITY_HIGH; hdma_dcmi.Init.FIFOMode DMA_FIFOMODE_ENABLE; hdma_dcmi.Init.FIFOThreshold DMA_FIFO_THRESHOLD_FULL; hdma_dcmi.Init.MemBurst DMA_MBURST_SINGLE; hdma_dcmi.Init.PeriphBurst DMA_PBURST_SINGLE; HAL_DMA_Init(hdma_dcmi); __HAL_LINKDMA(hdcmi, DMA_Handle, hdma_dcmi); // 启动双缓冲采集 HAL_DCMI_Start_DMA(hdcmi, DCMI_MODE_CONTINUOUS, (uint32_t)buf0, len); }这里PeriphDataAlignment和MemDataAlignment都设成 WORD是因为 DCMI 的数据寄存器是 32 位的虽然实际有效数据是 8 位但 DMA 按字传输效率更高。Mode设成 CIRCULAR 后DMA 会在 buf0 和 buf1 之间自动切换每传完一个缓冲区触发一次传输完成中断。在中断里切换显示缓冲区就能避免撕裂。3.3 帧率与缓冲区大小的取舍320x240 的 RGB565 一帧 153600 字节F407 的 SRAM 一共 192KB放两个缓冲区就占了 300KB明显不够。所以实际做的时候要么用外部 SRAM要么把分辨率降到 160x120一帧 38400 字节两个缓冲区 76800 字节内部 SRAM 勉强够。我一般会先跑 160x120 验证链路确认图像正常后再考虑加外部 SRAM 上 320x240。下面是一个分辨率与内存占用的对照表分辨率每帧字节数双缓冲占用是否适合内部 SRAM160x1203840076800可以176x14450688101376勉强320x240153600307200需要外部 SRAM640x4806144001228800必须外部 SRAM如果非要在内部 SRAM 跑 320x240可以只用一个缓冲区加行中断每收到若干行就搬运一次但这样 CPU 占用高图像也容易出横纹。我的建议是先用小分辨率把 DCMI 和 SCCB 调通再根据项目需求决定要不要扩内存。4. 避坑与排查图像出不来时先看这五个地方4.1 现象屏幕全黑或全白DMA 传输完成中断不触发原因通常是 VSYNC 或 HREF 极性配反DCMI 认为没有有效的帧开始信号DMA 一直不启动。解决方法是把VSPolarity和HSPolarity都改成相反的值试一遍同时用示波器量 OV7670 的 VSYNC 引脚确认在输出图像时有没有电平跳变。如果 VSYNC 一直不动检查 OV7670 的复位和电源PWDN 引脚必须拉低RESET 要先低后高。4.2 现象图像有但颜色整体偏绿或偏紫这是 RGB565 字节顺序问题。OV7670 输出的是高字节在前还是低字节在前取决于寄存器 0x40 的 bit3。如果颜色不对先把 0x40 的值改成 0xD8 或 0xC8 试试。另一个可能是 D0~D7 接反了用测试程序读一个已知颜色的像素比如让摄像头对着红色物体看读回来的值是不是 0xF800 附近。如果差得远逐位检查接线。4.3 现象图像有横条纹滚动横条纹一般是 PCLK 太快DMA 来不及搬或者 XCLK 抖动太大。先把 XCLK 降到 12MHz 试试如果条纹消失说明是时序余量不够。另外检查 DMA 的 FIFO 阈值设成 FULL 有时候反而会丢数据改成 HALF 或 QUARTER 可能更稳。还有一点DCMI 的 PCLK 和 DMA 的 AHB 时钟比例要合适AHB 至少是 PCLK 的 4 倍以上。4.4 现象SCCB 写寄存器没反应读回来全是 0xFFSCCB 没有 ACK所以不能靠等待 ACK 判断成功。如果读回来全是 0xFF先确认 SCL 和 SDA 有没有上拉电阻OV7670 模块上一般自带上拉但有些廉价模块没有需要在 STM32 侧加 4.7k 上拉。然后确认写地址是 0x42 而不是 0x21SCCB 的地址是 7 位左移一位。最后用逻辑分析仪抓一下波形看 SCL 频率是不是太高软件模拟 I2C 建议降到 100kHz 以下。4.5 现象图像能出但帧率极低只有几帧帧率低通常是 XCLK 频率不够或者 OV7670 内部时钟分频寄存器 0x11 设得太大。0x11 的默认值 0x80 对应 PCLK XCLK / 2如果改成 0x40 就是 XCLK / 4帧率会减半。另外检查 DCMI 的CaptureRate如果设成了半帧模式每两帧才输出一帧。还有 DMA 的传输宽度如果设成 BYTE 而不是 WORD传输次数翻倍也会拉低帧率。5. 把图像送到屏幕TFT 显示与内存搬运的一个实用技巧图像采回来之后最终要显示到 TFT 屏上。F407 驱动 TFT 一般用 FSMC把屏幕当成外部 SRAM 来写。如果 DCMI 的缓冲区和屏幕的显存是同一块内存可以直接用 DMA 把摄像头数据搬到 FSMC 地址但这样会占用 DMA 资源。更常见的做法是 CPU 搬运在 DCMI 的帧中断里把缓冲区数据逐行写到屏幕。这里有一个我常用的技巧不要等一整帧都采完再搬而是在 DCMI 的行中断里搬一行。DCMI 支持行中断每收到一行 HREF 就触发一次在中断里把这一行数据从 DMA 缓冲区复制到 TFT 的对应行地址。这样 CPU 占用分散也不会撕裂。下面是一段行中断搬运的示意代码// DCMI 行中断回调每行触发一次 void HAL_DCMI_LineEventCallback(DCMI_HandleTypeDef *hdcmi) { static uint16_t line 0; uint16_t *src (uint16_t *)frame_buffer line * WIDTH; // TFT 的显存地址假设 FSMC 基址为 0x6C000000 uint16_t *dst (uint16_t *)(0x6C000000 line * WIDTH * 2); // 搬运一行WIDTH 是图像宽度 for (uint16_t i 0; i WIDTH; i) { dst[i] src[i]; } line; if (line HEIGHT) { line 0; // 一帧结束回到第一行 } }这段代码里line是静态变量记录当前搬运到第几行。src指向 DMA 缓冲区dst指向 TFT 显存。搬运一行的时间取决于 WIDTH160 宽度下大概几百个周期在行中断里完全来得及。如果屏幕是 ILI9341 这类写显存前不需要额外命令直接按地址写就行。如果屏幕需要设置窗口那就要在帧开始时设一次窗口然后连续写。验证这个方法是否生效可以拿一个秒表放在摄像头前看屏幕上的秒表数字有没有拖影。如果数字清晰说明行中断搬运和 DCMI 采集同步得不错。如果数字有重影可能是行中断优先级太低被其他中断打断了把 DCMI 中断优先级调到最高试试。最后说一个我自己的习惯每次调摄像头先不接屏幕用串口把一帧图像的前几十个字节打印出来确认数据在变化。数据在变说明采集链路通了数据不变说明 DCMI 根本没启动。这个习惯帮我省了很多来回插拔屏幕的时间。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询