简介:针对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 之间为什么非要加 FIFO
2.1 DVP 并口接口到底在传什么
OV7725 是 CMOS 图像传感器,输出接口最常见的是 DVP(Digital 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 的作用是:摄像头的像素数据持续写入 FIFO,STM32 这边空闲时再慢慢读出来。摄像头写它的时钟是 PCLK,单片机读它的时钟是自己生成的 GPIO 翻转信号,两边各走各的,互不干扰。这就是“异步 FIFO 隔离时钟域”的典型用法。
AL422B 的容量正好能放下两帧 QVGA 数据。实际使用时只存一帧,读写指针不会追尾,省去很多指针管理的麻烦。模块上会有 WRST(写指针复位)、RRST(读指针复位)、WEN(写使能)、REN(读使能)、WCLK(写时钟)、RCLK(读时钟)这些引脚,软件要做的核心操作就是控制它们的时序。
2.3 一个最小系统的完整引脚分配
把颗粒做到整机,先用一个最小系统理解链路,后面换板子只改引脚映射。常见接法是这样的:
| STM32F103 引脚 | OV7725 模块信号 | 说明 |
|---|---|---|
| PB0~PB7 | D0~D7 | 8 位像素数据,读 FIFO 用 |
| PA8 | PCLK | FIFO 写时钟,由摄像头提供 |
| PA9 | VSYNC | 帧同步信号,输入捕获或查询 |
| PA10 | HREF | 行同步信号,读 FIFO 时配合 |
| PC13 | FIFO_WRST | FIFO 写指针复位 |
| PC14 | FIFO_RRST | FIFO 读指针复位 |
| PC15 | FIFO_WEN | FIFO 写使能 |
| PB12 | FIFO_REN | FIFO 读使能 |
| PB13 | FIFO_RCLK | FIFO 读时钟,由单片机产生 |
| PB14 | SCCB_SCL | 摄像头寄存器配置时钟 |
| PB15 | SCCB_SDA | 摄像头寄存器配置数据 |
提示:不同商家的模块引脚顺序不同,接线前先看模块原理图,确认 FIFO 型号和引脚标注再接线。接错 RCLK 或 REN,后续采集大概率直接全黑。
搞清这套硬件结构之后再回来看例程源码,会发现代码逻辑其实非常固定:初始化液晶、初始化 SCCB、配置 OV7725 寄存器、控制 FIFO 读写指针、把数据搬到液晶。下面按这个顺序拆开讲。
3. 用 SCCB 配置 OV7725 的关键寄存器
3.1 SCCB 时序和 I2C 的差别
OV7725 的寄存器配置接口叫 SCCB,长相和 I2C 几乎一样:一根时钟 SCL、一根数据 SDA,设备地址是 0x21(7 位地址,写操作时左移一位变成 0x42)。STM32F103 可以用硬件 I2C 外设,也可以 GPIO 模拟。实验例程里更推荐 GPIO 模拟,原因是省去调试 I2C 外设的麻烦,而且 SCCB 速率只需要几百 kHz,GPIO 模拟完全够用。
模拟时序按 I2C 协议写就行:起始条件、发送设备地址 + 写位、等待应答、发送寄存器地址、发送数据、停止条件。读写顺序是“先写寄存器地址,再写数据”。读寄存器时需要先发送寄存器地址,然后重新发送起始条件和设备地址 + 读位,再读一个字节。
3.2 必调的 8 个寄存器
网上能找到的 OV7725 初始化代码普遍有几十行寄存器配置,但真正影响出图的就几个。这里列一个最小验证配置,先把图点亮,再做画质微调。
| 寄存器地址 | 寄存器名 | 常见配置值 | 作用 |
|---|---|---|---|
| 0x12 | COM7 | 0x46 | 使能 RGB 输出,选择 QVGA 分辨率 |
| 0x11 | CLKRC | 0x01 | 内部 PLL 配置,影响 PCLK 频率 |
| 0x0D | COM3 | 0x04 | 垂直翻转控制,图像倒立时改这里 |
| 0x15 | HSTART | 0x00 | 水平窗口起始位置 |
| 0x17 | HSTOP | 0x3F | 水平窗口结束位置 |
| 0x19 | VSTART | 0x00 | 垂直窗口起始位置 |
| 0x1B | VSTOP | 0x3F | 垂直窗口结束位置 |
| 0x32 | HREF | 0x80 | HREF 极性控制,和模块硬件设计配合 |
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 改成 0x00,PCLK 频率变高,采集耗时下降;把液晶刷屏函数换成只刷局部窗口,也能看到耗时的变化。统计帧率还能反过来验证寄存器的含义:调了某个寄存器,帧率变了,说明它确实影响了时钟链路;帧率没变,说明这个寄存器可能根本没生效,需要回头查 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 排线同样如此,靠近摄像头排线时会互相干扰,布线时尽量让两路排线分开走。这一层问题排查掉,整套例程才算真正收尾。
本文还有配套的精品资源,点击获取