
简介针对STM32F103单片机与OV7725摄像头模块联调的图像采集显示例程这套源码面向嵌入式初学者和需要快速部署摄像头功能的开发者完整演示了从摄像头配置、图像数据传输到液晶屏实时预览的全过程。资源共561个文件压缩包16.3MB以.c/.h源码为主体配合.o编译中间文件、.uvprojx工程文件、.hex烧录文件及调试备份便于直接打开工程对照学习。核心代码覆盖OV7725初始化与分辨率设置、图像数据读取与处理、LCD显示驱动和主循环调度还包含部分调试辅助代码可帮助验证模块状态。目前已有59人学习浏览适合作为STM32图像应用开发的入门参考和项目模板。1. OV7725 拍照例程难点不在“拍”而在“搬”很多第一次碰 OV7725 的人都会问一句话摄像头不是把图像直接送给单片机吗为什么例程能跑通却总出花屏实际把工程调通之后才明白STM32F103 和 OV7725 之间的“拍照”本质不是让单片机理解图像内容而是把像素字节按顺序搬到液晶屏上。搬得对图就清楚搬错一个同步信号图像花得毫无规律。整个例程里真正的技术点有三个OV7725 通过 DVP 并口输出 RGB565 数据FIFO 模块负责隔离两边的时钟液晶屏负责按行列坐标把像素还原出来。这套方案常出现在毕业设计、课程实验和入门级视觉项目里适合手里有一块 STM32F103 最小系统板和 OV7725 摄像头模块、想自己跑通采集显示链路的人。读懂这篇文章能帮你把“摄像头拍照 液晶显示”这条链路从时序到底层配置完整串起来。2. OV7725 与 STM32F103 之间为什么非要加 FIFO2.1 DVP 并口接口到底在传什么OV7725 是 CMOS 图像传感器输出接口最常见的是 DVPDigital Video Port并口。这个接口一共就两类信号线数据线和同步线。数据线是 D0~D7 共 8 根同步线是 PCLK像素时钟、VSYNC帧同步、HREF行同步。PCLK 每来一个上升沿数据线上就出现一个字节HREF 为高电平时这些字节属于有效像素VSYNC 表示一帧图像的开始或结束。这三根线组合在一起就是一套完整的视频时序。OV7725 输出 RGB565 格式时一个像素占 2 个字节分两次出现在 D[7:0] 上。以常见的 QVGA 分辨率 320×240 为例一帧完整图像是 320×240×2 字节大约 153600 字节。如果直接让 STM32F103 用 GPIO 在 PCLK 的每个上升沿去读数据理论上可以但实际非常吃力。STM32F103 的 GPIO 翻转频率手册标称 18MHz而 OV7725 在常见配置下 PCLK 能到 6MHz 到 24MHz 之间。GPIO 读口速度勉强够低帧率但 CPU 会被完全占死任何其他任务都跑不了液晶刷屏更没机会。更麻烦的是摄像头输出的时钟是它自己的晶振产生的和 STM32 的时钟不同源两个时钟域直接对接采样稍有不稳就会出现字节错位。2.2 FIFO 模块用一块 384K 内存解决时钟域问题市面上的 OV7725 模块几乎都会在传感器旁边放一颗 FIFO 芯片最常见的是 AL422B容量 384K 字节。这颗 FIFO 的作用是摄像头的像素数据持续写入 FIFOSTM32 这边空闲时再慢慢读出来。摄像头写它的时钟是 PCLK单片机读它的时钟是自己生成的 GPIO 翻转信号两边各走各的互不干扰。这就是“异步 FIFO 隔离时钟域”的典型用法。AL422B 的容量正好能放下两帧 QVGA 数据。实际使用时只存一帧读写指针不会追尾省去很多指针管理的麻烦。模块上会有 WRST写指针复位、RRST读指针复位、WEN写使能、REN读使能、WCLK写时钟、RCLK读时钟这些引脚软件要做的核心操作就是控制它们的时序。2.3 一个最小系统的完整引脚分配把颗粒做到整机先用一个最小系统理解链路后面换板子只改引脚映射。常见接法是这样的STM32F103 引脚OV7725 模块信号说明PB0~PB7D0~D78 位像素数据读 FIFO 用PA8PCLKFIFO 写时钟由摄像头提供PA9VSYNC帧同步信号输入捕获或查询PA10HREF行同步信号读 FIFO 时配合PC13FIFO_WRSTFIFO 写指针复位PC14FIFO_RRSTFIFO 读指针复位PC15FIFO_WENFIFO 写使能PB12FIFO_RENFIFO 读使能PB13FIFO_RCLKFIFO 读时钟由单片机产生PB14SCCB_SCL摄像头寄存器配置时钟PB15SCCB_SDA摄像头寄存器配置数据提示不同商家的模块引脚顺序不同接线前先看模块原理图确认 FIFO 型号和引脚标注再接线。接错 RCLK 或 REN后续采集大概率直接全黑。搞清这套硬件结构之后再回来看例程源码会发现代码逻辑其实非常固定初始化液晶、初始化 SCCB、配置 OV7725 寄存器、控制 FIFO 读写指针、把数据搬到液晶。下面按这个顺序拆开讲。3. 用 SCCB 配置 OV7725 的关键寄存器3.1 SCCB 时序和 I2C 的差别OV7725 的寄存器配置接口叫 SCCB长相和 I2C 几乎一样一根时钟 SCL、一根数据 SDA设备地址是 0x217 位地址写操作时左移一位变成 0x42。STM32F103 可以用硬件 I2C 外设也可以 GPIO 模拟。实验例程里更推荐 GPIO 模拟原因是省去调试 I2C 外设的麻烦而且 SCCB 速率只需要几百 kHzGPIO 模拟完全够用。模拟时序按 I2C 协议写就行起始条件、发送设备地址 写位、等待应答、发送寄存器地址、发送数据、停止条件。读写顺序是“先写寄存器地址再写数据”。读寄存器时需要先发送寄存器地址然后重新发送起始条件和设备地址 读位再读一个字节。3.2 必调的 8 个寄存器网上能找到的 OV7725 初始化代码普遍有几十行寄存器配置但真正影响出图的就几个。这里列一个最小验证配置先把图点亮再做画质微调。寄存器地址寄存器名常见配置值作用0x12COM70x46使能 RGB 输出选择 QVGA 分辨率0x11CLKRC0x01内部 PLL 配置影响 PCLK 频率0x0DCOM30x04垂直翻转控制图像倒立时改这里0x15HSTART0x00水平窗口起始位置0x17HSTOP0x3F水平窗口结束位置0x19VSTART0x00垂直窗口起始位置0x1BVSTOP0x3F垂直窗口结束位置0x32HREF0x80HREF 极性控制和模块硬件设计配合COM7 是核心寄存器。0x46 的二进制是 0100 0110其中 bit6 是 RGB 输出使能bit1 和 bit0 组合选择 QVGA 分辨率。如果把这里改成 0x26会切换成 RAW RGB 格式屏幕上会出现彩色噪点一样的条纹改成 0x40 则变成 VGA 分辨率FIFO 容量紧张而且刷新速度明显变慢。CLKRC 配置 0x01 的含义是开启内部 PLL 分频让输出时钟落在单片机好处理的范围内这个值影响整帧采集速度后面的帧率统计部分会再提到。3.3 初始化代码与参数说明GPIO 模拟 SCCB 的写寄存器函数实验例程里的核心实现通常长这样// 发送起始条件: SCL 高电平期间 SDA 拉低 void SCCB_Start(void) { SDA_GPIO_HIGH(); SCL_GPIO_HIGH(); delay_us(5); SDA_GPIO_LOW(); delay_us(5); SCL_GPIO_LOW(); // 进入数据传输状态 } // 写一个字节并等待应答 // 返回 0 表示收到应答返回 1 表示无应答 uint8_t SCCB_WriteByte(uint8_t data) { uint8_t i; for (i 0; i 8; i) { if (data 0x80) SDA_GPIO_HIGH(); else SDA_GPIO_LOW(); data 1; delay_us(2); SCL_GPIO_HIGH(); delay_us(2); SCL_GPIO_LOW(); // SCL 下降沿后从机采样 SDA } SDA_GPIO_INPUT(); // 释放 SDA改为输入方向读取应答 SCL_GPIO_HIGH(); delay_us(2); uint8_t ack SDA_READ(); // 为 0 表示从机应答 SCL_GPIO_LOW(); SDA_GPIO_OUTPUT(); return ack; }写寄存器时先发设备地址 0x42再发寄存器地址最后发数据uint8_t OV7725_WriteReg(uint8_t reg, uint8_t value) { SCCB_Start(); if (SCCB_WriteByte(0x42)) return 1; // 设备地址 写位 if (SCCB_WriteByte(reg)) return 1; // 寄存器地址 if (SCCB_WriteByte(value)) return 1; // 寄存器值 SCCB_Stop(); return 0; }代码里delay_us(2)对应的是 SCL 半周期这里时序精度要求不高关键是确保 SCL 高电平宽度足够让 OV7725 采样。SDA 方向切换这一步容易忽略写完 8 位数据后必须把 SDA 引脚改成输入模式读应答如果一直保持输出模式读到的应答永远无效初始化函数会连续报错。整套初始化里还有个容易踩的坑是上电时序OV7725 芯片上电后需要等 10ms 以上再开始 SCCB 配置有些模块还要求先给它一个复位脉冲否则前几个寄存器写入会静默失败。实验例程里如果发现寄存器配置没问题但图像全黑先检查上电延时。配置完成后可以用一个读寄存器函数回读 COM7验证值是不是 0x46。这个回读操作能快速判断 SCCB 接线和时序是否正确比直接看屏幕靠谱得多。4. 帧同步与 FIFO 读写的完整流程4.1 用状态机理解一帧图像的采集过程寄存器配置完成后OV7725 已经开始持续输出图像数据。接下来的问题是怎么知道“一帧开始”了以及怎么把这一帧完整存进 FIFO。AL422B 的硬件设计天然适合这种场景OV7725 的 PCLK 直接接 FIFO 的 WCLK数据线接 D[7:0]HREF 接 WEN这样只有行有效期间的数据才会写进 FIFO。软件只需要在 VSYNC 的合适时机复位写指针然后在帧结束时关闭写使能、复位读指针就可以把一整帧读出来。这个过程本身就是一个三态状态机等待帧起始、等待帧结束、读取数据。手写状态机比在 while 循环里死等更清晰也方便后续扩展连续拍照功能。typedef enum { STATE_WAIT_VSYNC, // 等待帧起始 STATE_WAIT_FRAME, // 等待帧结束 STATE_READ_FIFO, // 读取整帧数据 STATE_SHOW_LCD // 刷新液晶 } CAM_STATE;主循环按状态机运行每一帧图像就是一个完整的状态循环。这样做的好处是后续想改成“按下按键拍一张”“连续拍 10 张”或者“边采边显示”只需在状态切换处加条件不必重写采集逻辑。这也是实验例程和随手 while 循环最本质的区别。4.2 帧同步控制两个 VSYNC 之间的完整时序FIFO 写指针复位必须发生在 OV7725 正在输出一帧图像但该帧还没结束时。最常见做法是先等待第一个 VSYNC 下降沿到来此时一帧图像已经开始立即拉低 WRST 复位写指针再拉高 WRST 释放等待第二个 VSYNC 下降沿此时一整帧已经全部写入 FIFO拉低 WEN 关闭写使能。注意这个“先等 VSYNC 再复位指写针”的顺序不能反如果先复位再等待FIFO 会从帧中间开始存读出来的图像就是上下错位的。void OV7725_StartCapture(void) { // 等待第一个 VSYNC 下降沿表示新一帧开始 while (GPIO_READ(VSYNC_PIN) HIGH); // 等待 VSYNC 上升沿确认帧起始有效 while (GPIO_READ(VSYNC_PIN) LOW); // 复位写指针FIFO 将从地址 0 开始存放 GPIO_WRITE(FIFO_WRST_PIN, LOW); GPIO_WRITE(FIFO_WRST_PIN, HIGH); // 使能 FIFO 写入数据开始累积 GPIO_WRITE(FIFO_WEN_PIN, LOW); // WEN 低电平有效 // 等待第二个 VSYNC 下降沿表示一帧写入完成 while (GPIO_READ(VSYNC_PIN) HIGH); while (GPIO_READ(VSYNC_PIN) LOW); // 帧结束关闭写使能停止累积 GPIO_WRITE(FIFO_WEN_PIN, HIGH); }代码里的注释把每个引脚电平的含义都标出来了。FIFO_WEN 是低电平有效这是 AL422B 的硬件规定很多初学例程的人在这里踩坑按直觉写成高电平使能结果是 FIFO 一直不写入数据画面全白。等待 VSYNC 的循环也要加超时保护防止摄像头没初始化成功时程序死锁在这个函数里出不来。实验例程里通常加一个毫秒级计数器超时直接返回错误这在调试硬件问题上能省很多时间。4.3 读 FIFO 到液晶PCLK 换成了 RCLK帧数据已经完整存在 FIFO 里STM32F103 这边的“读”和摄像头侧的“写”完全解耦。读数据时先把 RRST 拉低再拉高把读指针复位到 FIFO 的起点然后使能 REN低电平有效每产生一个 RCLK 上升沿FIFO 的 D[7:0] 就吐出一个字节。对 STM32F103 来说这一步就是把 PB0~PB7 读进来再按 RGB565 格式写给液晶屏模块。void OV7725_ReadFIFO_To_LCD(uint16_t x, uint16_t y, uint16_t w, uint16_t h) { // 复位读指针准备好从头读取 GPIO_WRITE(FIFO_RRST_PIN, LOW); GPIO_WRITE(FIFO_RRST_PIN, HIGH); // 使能 FIFO 读低电平有效 GPIO_WRITE(FIFO_REN_PIN, LOW); // 设置液晶显示窗口告诉 LCD 接下来要填充的区域 LCD_SetWindow(x, y, x w - 1, y h - 1); uint32_t pixel_count w * h; for (uint32_t i 0; i pixel_count; i) { // RCLK 上升沿FIFO 输出低字节 GPIO_WRITE(FIFO_RCLK_PIN, HIGH); uint8_t low GPIO_READ(FIFO_DATA_PORT); // RCLK 下降沿准备输出高字节 GPIO_WRITE(FIFO_RCLK_PIN, LOW); uint8_t high GPIO_READ(FIFO_DATA_PORT); // RGB565: 高字节在前低字节在后 LCD_WriteData((high 8) | low); } // 关闭读使能结束本次读取 GPIO_WRITE(FIFO_REN_PIN, HIGH); }这段代码里有两个细节值得注意。第一每读取一个字节都要把 RCLK 翻转一次这个 RCLK 是 STM32 的 GPIO 模拟出来的翻转速度决定了整帧读取时间。320×240 分辨率共 153600 字节每字节一次翻转按 GPIO 翻转 10MHz 算读一帧大约 15ms加上液晶刷屏时间整体帧率在 20fps 上下视觉上已经算流畅。第二RGB565 的字节顺序是模块硬件决定的不同模块高低字节顺序可能相反如果图像颜色发红发蓝大概率是这里高字节和低字节写反了。验证方法很简单拍一张大面积纯色物体看屏幕颜色通道是否正常。4.4 白屏、黑屏、花屏的定位顺序图像出问题时先分清是哪一段链路。表里列的排查顺序基本就是我调例程的实际顺序从头到尾过一遍绝大多数问题能定位。现象优先检查项说明液晶白屏液晶初始化是否完成先刷纯色测试液晶排除显示链路液晶黑屏SCCB 配置、片选引脚回读 COM7 是否为 0x46全屏彩色噪点COM7 输出格式RAW 格式被当 RGB565 显示图像上下颠倒COM3 寄存器改 0x0D 的值做垂直翻转图像左右偏移错位FIFO 写指针复位时机确认 WRST 是在 VSYNC 之后复位图像有横纹撕裂WEN 关闭过晚整帧写完立即关写不能等下一帧开始这几种问题里横纹撕裂是最隐蔽的。原因是 WEN 关得太晚第二帧的头几行数据覆盖了第一帧的尾部屏幕上就出现一条水平方向的错位线。处理办法是把关 WEN 的时机放在第二个 VSYNC 下降沿之后立刻执行中间不要穿插其他代码。如果主线任务多建议用外部中断来处理 VSYNC把帧结束标记写在中断里主循环只做状态切换时序稳定很多。5. 从“能拍照”到“拍得稳”帧率统计和按需裁剪5.1 用 SysTick 卡一帧的真实耗时图像能正常显示了下一步是量化性能。实验里我习惯把帧率统计直接做成一个调试接口用 SysTick 或者 DWT 计数器记录两次 VSYNC 之间的微妙数。最简单的方法是在帧采集开始前记录一个时间戳采集结束后再记录一次两者相减就是采集耗时。这个方法能很直观地看出配置修改的效果。比如把 CLKRC 的 0x01 改成 0x00PCLK 频率变高采集耗时下降把液晶刷屏函数换成只刷局部窗口也能看到耗时的变化。统计帧率还能反过来验证寄存器的含义调了某个寄存器帧率变了说明它确实影响了时钟链路帧率没变说明这个寄存器可能根本没生效需要回头查 SCCB 读写。使用 SysTick 时要注意它默认的计数方向是从重装载值递减到 0差值计算需要处理溢出。简单做法是用中断里递增一个全局计数器每次采集前后读这个计数器的差值。在例程里加这样一个小的统计函数调试效率会高很多。5.2 只拍图像的一部分跳过行列比改窗口更快很多应用并不需要完整 320×240 的图像比如只识别画面中部的文字区域这时候有两种做法。第一种是修改 OV7725 的窗口寄存器 HSTART、HSTOP、VSTART、VSTOP把传感器输出区域直接裁剪掉。这种做法的好处是 FIFO 里存的数据量本身就变小了读取时间缩短但 OV7725 修改窗口后输出分辨率可能变化有时还要同步调整输出格式容易引出奇怪的兼容性问题。第二种做法是读 FIFO 时按需跳过。整帧已经存进去了读像素时遇到不想显示的行列就多翻转 RCLK 不写 LCD 即可。比如只读中间的 160×120 区域大约只读 38400 字节时间缩短到原来的四分之一而且摄像头配置一行都不用改。代价是 FIFO 里还是存了整帧内存占用没减但对 STM32F103 这种内存本来就紧张的芯片来说图像数据压根不进内存这个方案反而最省事。实验例程跑稳定之后再用这种方法改成拍照裁剪逻辑改动最小回归验证最简单。5.3 排线稳定性和 ESD 对图像质量的影响最后说一个和代码无关但经常把人卡住的问题OV7725 模块到主板之间的排线。QVGA 的 PCLK 在几 MHz 到十几 MHz排线稍微长一点就会引入串扰和信号反射现象是图像随机出现彩色噪点或整帧闪断。常见的检查手段是先把排线压到最短或者直接焊接飞线验证模块底部的去耦电容也是关键因素有些便宜模块上电瞬间会有明显的电压跌落导致图像亮度跳动。调试这些症状时先怀疑信号完整性不要急着改寄存器。液晶显示模块的 FPC 排线同样如此靠近摄像头排线时会互相干扰布线时尽量让两路排线分开走。这一层问题排查掉整套例程才算真正收尾。本文还有配套的精品资源点击获取