STM32与OV7725图像采集:FIFO缓冲与DVP接口实战解析
2026/9/13 19:23:21 网站建设 项目流程

简介:针对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~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,设备地址是 0x21(7 位地址,写操作时左移一位变成 0x42)。STM32F103 可以用硬件 I2C 外设,也可以 GPIO 模拟。实验例程里更推荐 GPIO 模拟,原因是省去调试 I2C 外设的麻烦,而且 SCCB 速率只需要几百 kHz,GPIO 模拟完全够用。

模拟时序按 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 改成 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 排线同样如此,靠近摄像头排线时会互相干扰,布线时尽量让两路排线分开走。这一层问题排查掉,整套例程才算真正收尾。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询