简介:这份资源面向使用STM32F1系列驱动OV7670摄像头、且不依赖FIFO缓冲的嵌入式开发者与电子爱好者,尤其适合正在做图像采集入门实验或课程设计的学生。作者基于自身摸索经验编写了完整源码,并额外加入图像处理函数,可实现亮度、饱和度、对比度调节,按下KEY1即可完成二值化与灰度化转换,同时预留了颜色识别接口,方便二次开发。压缩包共312个文件,约10.8MB,以71个c源文件与63个h头文件为核心,配合Keil工程文件、编译中间产物及少量图片与说明文本,结构完整可直接编译。目前已有4558人学习下载,适合想跳过从零搭建、直接研究无FIFO采集时序与图像算法移植的读者参考借鉴。
1. 为什么我劝你先别急着上 FIFO:OV7670 无 FIFO 直连 STM32F1 的真实门槛
很多人第一次在 STM32F1 上接 OV7670,看到模块上没焊 AL422B 那颗 FIFO 芯片,心里就凉了半截——网上铺天盖地的教程都在说“没 FIFO 根本读不出来”“必须加 FIFO 才能用”。但实际情况是,OV7670 无 FIFO 方案在 STM32F1 上不仅能跑,而且跑通之后你对 DCMI 时序、DMA 搬运、行中断同步的理解会上一个台阶。这份资源包就是围绕“无 FIFO 直连”这个场景整理的,核心解决的是:在没有外部帧缓存的情况下,如何用 STM32F1 的 GPIO 配合定时器或外部中断,把 OV7670 输出的 8 位并行数据稳定地抓进内存,并拼出一帧可用的 RGB565 图像。它适合已经点过灯、调过串口,想往图像采集方向走的嵌入式开发者;也适合手头只有 F1 核心板加 OV7670 模块、不想再额外买 FIFO 转接板的穷折腾派。前提是你得接受一个现实:无 FIFO 方案对时序极其敏感,代码里每一个延时、每一个中断优先级都可能决定你是收到雪花还是收到图像。
2. 无 FIFO 直连的硬件底子:引脚分配与时钟树怎么定
2.1 为什么 F1 的 GPIO 能直接啃 OV7670 的 8 位并口
OV7670 输出的是 8 位并行数据(D0~D7),外加 PCLK(像素时钟)、HREF(行参考)、VSYNC(帧同步)和 XCLK(主时钟输入)。无 FIFO 方案的本质,是让 STM32F1 在 PCLK 的每个有效边沿去读一组 D0~D7。F1 没有 DCMI 外设,所以只能靠 GPIO 轮询或者外部中断触发。常见做法是把 D0~D7 分散接到同一个 GPIO 端口的连续 8 个引脚上,比如 PA0~PA7,这样一次GPIOA->IDR & 0x00FF就能把 8 位数据全取出来,比分散在不同端口快得多。PCLK 接到一个支持外部中断的引脚,比如 PA8,配置为上升沿触发。HREF 和 VSYNC 可以接到普通输入引脚,用轮询方式判断行、帧边界。XCLK 则由 STM32 的 MCO 引脚或者定时器 PWM 输出提供,OV7670 要求 XCLK 范围在 10~48MHz,一般给 24MHz 或 12MHz 比较稳。
这里有个血泪经验:不要试图把 D0~D7 接到不同端口,然后在中断里逐个读引脚再拼字节,那样一个像素周期根本来不及。必须让数据口落在同一个 IDR 可覆盖的范围内。
2.2 时钟树配置:72MHz 系统时钟下怎么喂 XCLK
STM32F1 通常跑 72MHz。如果你用 MCO 输出 XCLK,可以配置 MCO 为 SYSCLK 的 1/2 或 1/3,但 72/2=36MHz 偏高,72/3=24MHz 正好。配置代码如下:
// 使能 GPIOA 和 AFIO 时钟 RCC_APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_AFIOEN; // PA8 配置为复用推挽输出,作为 MCO GPIOA->CRH &= ~(0xF << 0); // 清除 PA8 配置位 GPIOA->CRH |= (0xB << 0); // 复用推挽,50MHz // MCO 输出 SYSCLK/3 = 24MHz RCC_CFGR &= ~(0x7 << 24); // 清除 MCO 配置位 RCC_CFGR |= (0x6 << 24); // 110: SYSCLK/3逻辑说明:RCC_CFGR的 bit26~24 控制 MCO 输出源,110对应 SYSCLK/3。参数上,如果你用的是 8MHz 晶振倍频到 72MHz,那 MCO 就是 24MHz;如果系统时钟是 56MHz,那 MCO 就是 18.67MHz,也在 OV7670 允许范围内。注意 MCO 引脚本身有驱动能力限制,如果 OV7670 模块的 XCLK 输入阻抗较低,可能需要在中间串一个 22Ω~100Ω 的电阻,否则波形会畸变,表现为图像整体偏色或者根本不出图。
2.3 SCCB 初始化:把 OV7670 配成 RGB565 输出
OV7670 的寄存器通过 SCCB 总线配置,SCCB 和 I2C 很像,但没有重复起始条件。F1 可以用硬件 I2C,也可以用 GPIO 模拟。无 FIFO 方案里我一般用模拟 SCCB,因为硬件 I2C 在 F1 上容易卡死,而且 SCCB 速率不高,模拟完全够用。关键寄存器配置如下表:
| 寄存器地址 | 值 | 作用 |
|---|---|---|
| 0x12 | 0x80 | 复位所有寄存器 |
| 0x12 | 0x14 | 使能 RGB565 输出,PCLK 不反相 |
| 0x0C | 0x00 | 关闭 DCW,保留原始时序 |
| 0x11 | 0x80 | 内部时钟分频,配合 XCLK 24MHz |
| 0x40 | 0xD0 | 行场信号极性调整,适配 F1 中断触发 |
| 0x3A | 0x04 | 输出格式设为 RGB565 |
| 0x1E | 0x1F | 窗口水平起始高位 |
| 0x17 | 0x13 | 水平窗口宽度 |
| 0x32 | 0x80 | 垂直窗口控制 |
| 0x03 | 0x0A | 垂直窗口行数低位 |
这些值不是唯一解,但这一套在 24MHz XCLK、QVGA 输出下比较稳。配置完以后,VSYNC 和 HREF 的极性要拿示波器或者逻辑分析仪看一眼,确认和代码里中断边沿一致,否则你会收到一帧全是 0x00 或者 0xFF。
3. 无 FIFO 采集的代码骨架:中断、DMA 与行同步怎么配合
3.1 VSYNC 中断开启一帧,HREF 中断开启一行
无 FIFO 方案最怕的是“读得不够快”。一个 QVGA 帧有 320×240=76800 个像素,每个像素 2 字节,总共 153600 字节。F1 的 SRAM 通常只有 20KB 或 64KB,根本存不下一整帧 RGB565。所以常见做法是:只抓一行或者几行,或者把图像降级到 QQVGA(160×120),再或者用 DMA 把数据直接搬到 FSMC 驱动的 LCD 上,不经过内存缓存。这份资源包里的代码骨架是按“抓一行、处理一行、丢弃一行”的思路写的,适合做简单图像处理或者颜色识别。
VSYNC 接 PA9,配置为下降沿触发(具体看模块极性)。HREF 接 PA10,配置为上升沿触发。PCLK 接 PA8 的外部中断,但注意:PCLK 频率在 24MHz XCLK 下大约是 12MHz 左右,F1 的中断响应根本来不及处理每个像素。所以实际代码里不能用 PCLK 中断,而是用 HREF 中断开启 DMA 请求,让 DMA 在 PCLK 的触发下自动搬运 GPIO 数据。但 F1 的 DMA 不能直接由外部时钟触发 GPIO 读取,所以更现实的做法是:用定时器产生一个和 PCLK 同步的采样时钟,或者干脆用 SPI 的 MISO 模式——但 OV7670 不是 SPI 输出。最终能跑通的方案是:HREF 有效期间,用一轮紧凑的 GPIO 轮询循环,靠 __NOP() 对齐时序,把 8 位数据读进来。这听起来很玄学,但配合正确的编译优化等级(-O2 或 -O3),在 72MHz 下读 QVGA 一行是可行的。
3.2 行采集函数:GPIO 轮询 + 数组填充
#define LINE_LEN 320 uint8_t line_buf[LINE_LEN * 2]; // RGB565 一行,2 字节/像素 void capture_line(void) { uint16_t i; // 等待 HREF 变高,行有效 while (!(GPIOA->IDR & (1 << 10))); for (i = 0; i < LINE_LEN; i++) { // 等待 PCLK 上升沿(PA8) while (!(GPIOA->IDR & (1 << 8))); // 读低 8 位数据(PA0~PA7) line_buf[i * 2] = (uint8_t)(GPIOA->IDR & 0x00FF); // 等待 PCLK 下降沿,准备下一个像素 while (GPIOA->IDR & (1 << 8)); // 第二个字节(RGB565 高字节) while (!(GPIOA->IDR & (1 << 8))); line_buf[i * 2 + 1] = (uint8_t)(GPIOA->IDR & 0x00FF); while (GPIOA->IDR & (1 << 8)); } }逻辑说明:OV7670 在 RGB565 模式下,每个像素输出两个字节,第一个字节是低字节(R 和 G 的低位),第二个字节是高字节(G 的高位和 B)。所以循环里要连续读两次。参数上,LINE_LEN根据你设置的窗口宽度改,QVGA 是 320,QQVGA 是 160。注意这个函数里没有做超时处理,如果 HREF 一直不拉高,程序会死循环。实际使用时要加一个计数器,超过一定周期就退出,否则你会以为单片机“烧了”,其实只是在等一个永远不会来的信号。
3.3 帧组装与内存取舍:为什么我建议先跑 QQVGA
一帧 QVGA RGB565 是 153600 字节,F1 的 20KB SRAM 连十分之一都放不下。所以无 FIFO 方案通常只做以下三种事之一:第一,只采集感兴趣区域(ROI),比如 80×60 的小窗口,这样一帧只要 9600 字节,20KB 内存能放下;第二,采集一行就通过串口发一行,上位机拼图;第三,直接驱动 LCD,把数据实时写进 LCD 的 GRAM,不占 MCU 内存。这份资源包里的示例是按第二种做的,串口波特率建议 460800 或 921600,否则一帧传完要好几秒。如果你只是想验证图像能不能出来,先把窗口设成 QQVGA,内存占用降到 38400 字节,还是超了 20KB,那就再降到 80×60。别一上来就怼 QVGA,翻车概率极高。
4. 避坑与排查:无 FIFO 方案里最容易翻车的五个点
4.1 现象:图像全是雪花,偶尔有几行正常
原因:PCLK 采样相位不对。OV7670 在 PCLK 上升沿输出数据,但 F1 的 GPIO 读取有建立保持时间要求。如果你在上升沿读,可能读到的是上一个像素的残留值。解决:把采样点改到 PCLK 下降沿,或者调整 OV7670 的 0x12 寄存器 bit4(PCLK 反相)。我一般会先用逻辑分析仪抓 PCLK 和数据线的时序,确认数据稳定窗口在哪个边沿,再改代码。
4.2 现象:串口收到的数据全是 0x00 或 0xFF
原因:SCCB 配置没写进去,或者 XCLK 没输出。先拿示波器量 PA8(MCO)有没有 24MHz 波形,没有的话检查 RCC_CFGR 配置和 GPIO 复用设置。如果有波形但数据还是 0,用 SCCB 读一下 0x0A 和 0x0B 寄存器(产品 ID),正常应该读到 0x76 和 0x73。读不到就说明 SCCB 时序有问题,重点查 SCL 和 SDA 的上拉电阻,4.7kΩ 到 10kΩ 之间。
4.3 现象:图像颜色整体偏绿或偏紫
原因:RGB565 字节顺序搞反了。OV7670 先输出低字节,再输出高字节,但有些模块或者某些寄存器配置下顺序会反过来。解决:在capture_line里把line_buf[i*2]和line_buf[i*2+1]对调一下试试。另外检查 0x3A 寄存器是不是 0x04,有些教程写 0x14,那是 RGB555,颜色会不对。
4.4 现象:图像上半部分正常,下半部分错位
原因:HREF 中断和 VSYNC 中断的优先级冲突,导致行计数错乱。F1 的 NVIC 里,如果 VSYNC 中断优先级高于 HREF,那么帧同步来了以后会打断正在进行的行采集。解决:把 HREF 中断优先级设得比 VSYNC 高,或者干脆不用 VSYNC 中断,只在主循环里轮询 VSYNC 引脚,等它变低以后再开始抓行。这样虽然会丢几帧,但至少不会错位。
4.5 现象:编译优化开到 -O3 后图像反而更差
原因:编译器把 GPIO 轮询循环优化掉了,或者重排了读取顺序。解决:在关键循环里加__NOP()或者用volatile修饰指针。更稳妥的做法是把采集函数放到一个单独的 .c 文件里,对该文件单独设置 -O2,其他文件 -O3。这个坑很隐蔽,因为 -O0 能出图,-O3 出雪花,你会以为是硬件问题,其实是编译器在帮你“倒忙”。
5. 进阶技巧:用定时器触发 DMA 做半自动行采集
如果你已经能把图像稳定抓出来,下一步可以试试用定时器触发 DMA 来减轻 CPU 负担。思路是:TIM2 配置成外部时钟模式,把 PCLK 接到 TIM2_ETR 引脚(PA0),这样每个 PCLK 上升沿 TIM2 计数一次。然后让 TIM2 的更新事件触发 DMA,DMA 把 GPIOA->IDR 的低 8 位搬到内存数组。但这里有个硬件限制:F1 的 DMA 请求映射里,TIM2_UP 不能直接触发 GPIO 读取,只能触发外设数据寄存器。所以这个方案需要把 OV7670 的数据线接到某个外设的数据口上,比如 FSMC 或者 SPI。常见做法是:把 D0~D7 接到 FSMC 的数据线上,用 FSMC 的 NOR 模式,把 OV7670 当成一个 8 位外设,然后 DMA 从 FSMC 数据地址搬运。这样 CPU 几乎不参与,只靠 PCLK 和 HREF 做硬件同步。代价是接线复杂很多,而且 FSMC 的地址建立时间要调,否则读到的数据不稳定。
另一个更实用的技巧是:只采集 YUV 的 Y 分量做灰度图。OV7670 支持 YUV 输出,Y 分量每个像素只有一个字节,数据量减半,而且灰度图对颜色识别、边缘检测足够用了。配置寄存器 0x3A 为 0x00 或者 0x01 切到 YUV,然后在采集函数里只读第一个字节,丢弃第二个字节。这样一帧 QQVGA 灰度图只要 19200 字节,20KB 内存刚好能放下。我后来做简单巡线或者光斑定位,都是先跑这个模式,等验证完算法再考虑上 RGB。
从那以后我每次调 OV7670 无 FIFO,都强制先跑一遍“示波器看 XCLK、逻辑分析仪看 HREF 和 PCLK、SCCB 读产品 ID”这三步,确认硬件底子没问题再动代码。希望帮到你。
本文还有配套的精品资源,点击获取