STM32F103驱动OV7670带FIFO模块:FSMC+DMA图像采集实战
2026/9/24 13:00:08 网站建设 项目流程

OV7670 这一代摄像头传感器,在嵌入式圈子里算得上“入门必修课”了。我当年第一次把它点亮,中间踩的坑比写的代码还多:SCCB 半天不通、屏幕全是雪花、颜色绿得发慌、画面斜着一条条撕开……后来把带 FIFO 的模块、FSMC 总线和 DMA 组合起来以后,才真正理顺这条链路的脾气。这篇教程就按我实际调试时的顺序来:方案选型、硬件接线、软件实现、问题排查,一步步把 STM32F103 驱动 OV7670 这件事讲透,让你少走我当初走过的弯路。

先说清楚一个关键结论:F103 没有 DCMI 摄像头接口,所以纯 OV7670(不带 FIFO)那种靠 PCLK、HREF、VSYNC 直接送像素的方案,在 F103 上基本跑不起来,或者帧率惨到没法看。正确做法是选带 FIFO 的 OV7670 模块,通过 FSMC 总线把 FIFO 当 SRAM 一样读,再用 DMA 搬运到内存,最后刷到显示屏上。这就是我这篇教程要带你完整走通的硬件链路和代码框架,适合刚从 51 或基础外设学习过渡到图像采集的同学参考。

1. 项目整体设计与方案选型:为什么 F103 必须搭配带 FIFO 的 OV7670

1.1 带 FIFO 与不带 FIFO 模块的本质区别

OV7670 模块在市面上主要有两种形态,一个是传感器直接引出的“裸输出”,另一个是在传感器和接口之间多加了一片 AL422B 这类 FIFO 缓存芯片。它们的区别,决定了你在 F103 上的开发难度天差地别。

不带 FIFO 的模块,D0~D7 像素数据线上随时都在输出当前扫描行的字节,MCU 必须在 PCLK 的每个有效沿及时读走数据。这类时序通常要求 20MHz 以上的连续采样,而且 VSYNC、HREF、PCLK 三个信号还得精确配合。ST 官方给的标准出路是 F4/F7/H7 系列上的 DCMI 接口,它由硬件自动同步行场信号,DMA 直接往内存搬。但 STM32F103 根本没有 DCMI 外设,如果强行用 GPIO 模拟采样,你的中断响应延迟、GPIO 翻转速度都会成为瓶颈,QVGA 分辨率下能跑到 5 帧都算运气好,而且 CPU 完全被拖死,别的事都干不了。

带 FIFO 的模块,本质上是把传感器输出的像素流先灌到一片 AL422B 内部,MCU 这边不需要跟随 PCLK 节奏。AL422B 就像一个 3Mbit 的“小仓库”,传感器往里面连续写,你随时可以像读普通 SRAM 一样把数据读出来,读快读慢都不会丢数据,只要保证一帧时间内读完即可。对 F103 这种没有专用摄像头接口的单片机来说,这是唯一稳定可行的方案。

所以,这篇教程的选型结论很明确:STM32F103 就买带 FIFO 的 OV7670 模块,省下的调试时间足够你多试十几套寄存器参数。

1.2 数据链路设计:FSMC + DMA 解决读取性能瓶颈

确定了带 FIFO 模块,下一个问题就是用谁来读 FIFO。OV7670 配置成 QVGA RGB565 后,一帧数据量是 320×240×2 = 153600 字节。如果全部用 GPIO 软件模拟时序去读,即使主频 72MHz,每个字节都要消耗好几个 GPIO 写读周期,一帧读下来 CPU 几乎满负荷,帧率还上不去。

我的做法是用 FSMC 的 NOR/SRAM 控制器。AL422B 的读时序和 SRAM 高度相似:给一个读信号,数据引脚上就冒出一个字节。把 FIFO 的数据线、读信号接到 FSMC 对应引脚,再把 FSMC 配置成带片选的 SRAM 模式,访问某个地址就等价于读取 FIFO 输出。这样一来,FSMC 硬件自己产生地址建立时间、数据建立时间的时序,不需要你一条一条翻转 GPIO。

在此基础上再叠加 DMA。DMA 可以把 FSMC 映射地址里的数据连续搬运到 RAM 缓冲区,一帧 153600 字节的搬运过程完全不需要 CPU 干预。配合“帧同步中断启动 DMA + 传输完成中断通知主循环”的结构,CPU 只在帧开始和帧结束时各干一点活,中间可以安心做刷新显示或者其他业务逻辑。

整条链路画出来就是:OV7670 传感器 → FIFO 缓存 → FSMC 总线 → DMA → 内存缓冲区 → LCD。传感器写入是连续异步的,MCU 读取是按帧批量进行的,中间靠 FIFO 解耦,这也是工业摄像头采集里非常经典的生产者-消费者模型。

2. 硬件接线与时钟复位:把每一根线都讲清楚再动手

2.1 一张接线表搞定所有引脚

接线之前先在开发板上确认两件事:第一,你手上 STM32F103 的具体型号,最好是有 FSMC 的 100 脚以上型号,比如 STM32F103ZET6 或 VET6;第二,LCD 屏幕的接口类型。我推荐使用 SPI 接口的 LCD,这样 FSMC 总线可以专供 FIFO 读取,避免并行屏和摄像头抢同一组数据线,对初学者最友好。

下面是我实际用的一套接线,不同开发板引脚编号可能略有差异,请对应你的原理图调整。重点是把“总线关系”理清,而不是死记硬件编号。

OV7670 模块引脚方向STM32F103 引脚说明
D0~D7输出FSMC_D0~FSMC_D7(PD14、PD15、PD0、PD1、PE7~PE10)图像数据线,8 位并行输出
FIFO_RRST输入PB0读指针复位,低电平有效,读帧前拉一下
FIFO_OE输入PB1输出使能,低电平有效,读取期间拉低
FIFO_WRST输入PB2写指针复位,上电后复位一次
FIFO_RCK / 读信号输入FSMC_NOE(PD4)每个读信号让 FIFO 内部读指针前进一个字节
VSYNC输出PA0帧同步信号,配置下降沿触发外部中断
SIO_C输入PB12SCCB 时钟线
SIO_D双向PB13SCCB 数据线
XCLK输入PA8摄像头工作时钟,由 MCU 提供
RESET输入PA5(可选)摄像头复位,低有效
PWDN输入PA6(可选)电源模式,低电平正常工作
3.3V / GND电源3.3V / GND模块供电,注意电压匹配

这里的重点在 FIFO_RCK 和 FIFO_OE 的配合。FSMC 每次读地址时,NOE 会产生一个低电平脉冲,把 NOE 接到 FIFO_RCK 就能让 AL422B 内部地址自动加一。FIFO_OE 则用普通 GPIO 控制,读整帧数据期间拉低,读完拉高。这样设计是因为 FIFO 和 LCD 如果挂在同一组总线上,FIFO 的 OE 必须平时保持高阻态,否则会和屏幕数据线冲突。即使你的模块没有别的设备共用数据线,我也建议保留这个控制机制,排查问题时会省很多麻烦。

你可能会问:FSMC 的 NE1 片选信号要不要接?我在实际工程里见过两种接法:一种是不接,只要 FSMC 访问 0x60000000 地址就认为选中了 FIFO;另一种是把 NE1 接到模块上的某个片选脚,但很多便宜模块根本没引出这个脚。最稳妥的做法是接一个 GPIO 到模块的 CS/片选位置,读取期间拉低,其余时间拉高。如果模块没有片选脚,就直接忽略,靠 NOE 控制读时序。

2.2 XCLK 时钟源:用 PA8 的 MCO 输出最省事

OV7670 需要外部输入一个时钟,通常范围在 10MHz~48MHz,常见配置是 12MHz、16MHz、24MHz。F103 的 PA8 引脚可以通过 MCO 功能输出内部时钟,这是给摄像头供时钟最简单的方式,不需要外部晶振,也不需要定时器模拟 PWM。

STM32F103 的 MCO 可以选择的时钟源包括:HSE、HSE/2、SYSCLK/2、PLLCLK/2 等。如果外部晶振是 8MHz,我常用的做法是配置 MCO 输出 PLLCLK/2,也就是 72MHz / 2 = 36MHz,正好在 OV7670 的允许范围内。然后在初始化寄存器时通过 CLKRC(寄存器 0x11)对内部时钟做分频,把像素时钟压低到适合 FIFO 写入的水平,避免数据灌得太猛造成 FIFO 写满丢帧。

配置 MCO 的代码非常简单:

void mco_init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); RCC_MCOConfig(RCC_MCO_PLLCLK_Div2); // MCO 输出 36MHz }

一个很常见的坑是:有的模块默认需要 PWDN 脚低电平才进入正常工作模式,如果这个脚悬空被拉高,摄像头会一直处于低功耗状态,SCCB 通信可能“假死”。所以我建议把 PWDN 和 RESET 都用 GPIO 管起来,初始化时明确写出电平顺序,不要依赖模块默认状态。

2.3 上电时序:先把摄像头“唤醒”再谈配置

很多新手一上来就急着写 SCCB 初始化,结果摄像头没反应,其实是上电时序不对。OV7670 这块传感器不算挑,但有几个基本时序要求必须满足:

第一步,给模块供上稳定的 3.3V 电源,等待电源稳定至少 10ms。第二步,把 RESET 拉低保持至少 1ms,再拉高,让传感器内部完成复位。第三步,把 PWDN 拉低,确保摄像头处于正常运行模式。第四步,给 XCLK 时钟,也就是打开 MCO 输出。第五步,等待至少 5ms 后,再开始 SCCB 配置寄存器。

我一般把这五步封装成一个函数,放在摄像头初始化最前面:

void ov7670_hw_reset(void) { GPIO_ResetBits(GPIOA, GPIO_Pin_5); // RESET low GPIO_ResetBits(GPIOA, GPIO_Pin_6); // PWDN low DelayMs(5); GPIO_SetBits(GPIOA, GPIO_Pin_5); // RESET high DelayMs(10); mco_init(); // 提供 XCLK DelayMs(20); }

注意,不是所有模块都把 RESET 和 PWDN 引出来了,有些模块直接接死成工作状态。这种模块上电就能用,但你还是应该按“复位—使能—时钟—延时”的顺序做软件初始化,只是省略 GPIO 控制的部分。顺序对了,很多莫名其妙的初始化失败就自动消失了。

3. 软件架构与核心实现:SCCB、FSMC、DMA 三板斧

3.1 SCCB 读写驱动:和 I2C 长得像,但别直接照搬

OV7670 的配置接口叫 SCCB,全称 Serial Camera Control Bus,时序上和 I2C 非常接近,协议上有一个重要区别:SCCB 的每个数据字节后面只有 ACK 位,没有 NACK 的概念;另外多字节传输时中间也必须是完整的数据结束状态。很多现成的 I2C 驱动直接改个引脚就能用,会调通的概率其实挺高,但一旦遇到从机不响应的情况,排查起来就很混乱。我建议自己写一个精简的 SCCB 驱动,规则清楚,出了问题也好定位。

SCCB 的通信格式是:起始条件 → 设备地址(8 位)→ 从机 ACK → 寄存器地址(8 位)→ 从机 ACK → 写数据(8 位)→ 从机 ACK → 停止条件。OV7670 的 7 位设备地址是 0x21,左移一位后读写地址分别是 0x42 和 0x43。我见过有人把地址写成 0x21 然后一直调不通,这里提醒一句,读写函数里用的是带方向位的 8 位地址。

模拟时序的核心代码大概是这个结构:

#define SCCB_SCL_H() GPIO_SetBits(GPIOB, GPIO_Pin_12) #define SCCB_SCL_L() GPIO_ResetBits(GPIOB, GPIO_Pin_12) #define SCCB_SDA_H() GPIO_SetBits(GPIOB, GPIO_Pin_13) #define SCCB_SDA_L() GPIO_ResetBits(GPIOB, GPIO_Pin_13) #define SCCB_SDA_READ() GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_13) void sccb_start(void) { SCCB_SCL_H(); SCCB_SDA_H(); DelayUs(5); SCCB_SDA_L(); DelayUs(5); SCCB_SCL_L(); } void sccb_stop(void) { SCCB_SCL_L(); SCCB_SDA_L(); DelayUs(5); SCCB_SCL_H(); DelayUs(5); SCCB_SDA_H(); DelayUs(5); } uint8_t sccb_write_byte(uint8_t dev_addr, uint8_t reg_addr, uint8_t data) { sccb_start(); sccb_send_byte(dev_addr); // 写地址 sccb_wait_ack(); sccb_send_byte(reg_addr); sccb_wait_ack(); sccb_send_byte(data); sccb_wait_ack(); sccb_stop(); return 0; }

我强烈建议在初始化寄存器之前,先做一次“设备探测”:读取 OV7670 的制造商 ID 寄存器(地址 0x1C 和 0x1D),正常读回来应该是 0x7F 和 0xA2。如果读不到,别急着往下写寄存器,先回头检查 SIO_C、SIO_D 的引脚配置、上拉电阻、电平转换,因为后面所有型号配置都建立在这个通信链路上。

3.2 OV7670 寄存器配置:为什么不能随便抄一段就完事

OV7670 的寄存器配置是整个项目里最容易让人血压升高的一环。网上随便一搜就是一大段初始化数组,但很多数组连基础格式都没写对,直接抄过来十有八九是花屏或者黑屏。我必须强调一个原则:不要直接抄,先理解三个关键寄存器的意思。

第一个是 COM7,寄存器地址 0x12。bit7 是软件复位位,写 1 复位;bit5 是颜色格式选择位,RGB 模式下要置 1;低 3 位决定分辨率,000 是 VGA,100 是 QVGA,101 是 QCIF。所以输出 QVGA RGB565 时,COM7 通常设成 0x04(QVGA 且 RGB 模式),但前提是 COM15 里的 RGB 格式也得配套。

第二个是 COM15,寄存器地址 0x40。高两位决定输出格式,00 是 YUV,01 是 RGB,11 是 Bayer Raw;低两位决定 RGB 位宽,00 是 RGB565,01 是 RGB555,10 是 RGB444。也就是说,常见例程里会把 0x40 写成 0xD0,高两位 11 代表 RGB 输出,但这里其实是“选择 RGB565 编码 00”的意思,写成 0xD0 是很多例程的固定习惯。

第三个是 CLKRC,寄存器地址 0x11。bit7 为 1 表示使用内部 PLL 倍频,低 6 位是对外部时钟的分频系数。外部 XCLK 太高时,比如 24MHz 甚至 36MHz,就需要在这里设置分频,让内部像素时钟工作在合理范围。分频设置不对,最典型的现象是颜色偏色、图像闪动,因为 FIFO 的写入速度跟不上或溢出。

一个能跑通的初始化流程应该包含:软件复位、等待复位完成、配置输出格式和分辨率、配置窗口裁剪、配置帧率相关寄存器、最后再设置一次输出格式作为确认。下面是我常用的初始化片段思路,不是完整数组,但结构是标准的:

static void ov7670_init(void) { ov7670_write(0x12, 0x80); // COM7: 软件复位 DelayMs(100); ov7670_write(0x11, 0x80); // CLKRC: 开启内部 PLL ov7670_write(0x6B, 0x0A); // PLL 倍频配置 ov7670_write(0x12, 0x04); // COM7: QVGA RGB ov7670_write(0x40, 0xD0); // COM15: RGB565 // 其他与窗口、增益、白平衡相关的寄存器按需配置 }

这里有一个关键经验:修改寄存器后要稍等一小段时间再继续,因为传感器内部有些配置是逐行生效的,写太快可能被后续寄存器覆盖。在调试阶段,我建议每写一个寄存器就读回来比对,确认写入成功,而不是一次性灌几十个寄存器再去看结果,那样出了错根本不知道是哪一步引起的。

3.3 FSMC 配置与 DMA 搬运:把 FIFO 当 SRAM 读

FSMC 是 F103 连接外部存储器的接口,这里利用它产生 SRAM 读时序。具体的初始化流程是:先把 FSMC 引脚配置为复用推挽输出,再配置 NOR/SRAM Bank1、8 位数据宽度、禁用地址数据复用、设置读写时序参数,最后使能。

关键代码如下:

void fsmc_fifo_init(void) { GPIO_InitTypeDef GPIO_InitStructure; FSMC_NORSRAMInitTypeDef FSMC_NORSRAMInitStructure; FSMC_NORSRAMTimingInitTypeDef FSMC_NORSRAMTimingInitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_FSMC, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOD | RCC_APB2Periph_GPIOE, ENABLE); // FSMC_D0~D7、FSMC_NOE、FSMC_NE1 全部配置为 AF_PP 50MHz // PD0、PD1、PD4、PD7、PD14、PD15,PE7~PE10 FSMC_NORSRAMInitStructure.FSMC_Bank = FSMC_Bank1_NORSRAM1; FSMC_NORSRAMInitStructure.FSMC_DataAddressMux = FSMC_DataAddressMux_Disable; FSMC_NORSRAMInitStructure.FSMC_MemoryType = FSMC_MemoryType_SRAM; FSMC_NORSRAMInitStructure.FSMC_MemoryDataWidth = FSMC_MemoryDataWidth_8b; FSMC_NORSRAMInitStructure.FSMC_BurstAccessMode = FSMC_BurstAccessMode_Disable; FSMC_NORSRAMInitStructure.FSMC_AsyncWait = FSMC_AsyncWait_Disable; FSMC_NORSRAMInitStructure.FSMC_WriteOperation = FSMC_WriteOperation_Disable; FSMC_NORSRAMInitStructure.FSMC_ExtendedMode = FSMC_ExtendedMode_Disable; FSMC_NORSRAMTimingInitStructure.FSMC_AddressSetupTime = 0x01; FSMC_NORSRAMTimingInitStructure.FSMC_AddressHoldTime = 0x00; FSMC_NORSRAMTimingInitStructure.FSMC_DataSetupTime = 0x02; FSMC_NORSRAMTimingInitStructure.FSMC_BusTurnAroundDuration = 0x00; FSMC_NORSRAMTimingInitStructure.FSMC_CLKDivision = 0x00; FSMC_NORSRAMTimingInitStructure.FSMC_DataLatency = 0x00; FSMC_NORSRAMTimingInitStructure.FSMC_AccessMode = FSMC_AccessMode_A; FSMC_NORSRAMInit(&FSMC_NORSRAMInitStructure); FSMC_NORSRAMCmd(FSMC_Bank1_NORSRAM1, ENABLE); }

配置完成后,你每读一次地址 0x60000000,FSMC 就会自动拉低 NOE 一个周期,FIFO 内部读指针前进一次,数据线上出现下一个字节。配合 DMA 的 memory-to-memory 模式,就可以一次性读出整帧:

void dma_read_fifo(uint8_t *buf, uint16_t len) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel1); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)0x60000000; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)buf; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize = len; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_M2M = DMA_M2M_Enable; // 关键:内存到内存模式 DMA_Init(DMA1_Channel1, &DMA_InitStructure); DMA_ITConfig(DMA1_Channel1, DMA_IT_TC, ENABLE); DMA_Cmd(DMA1_Channel1, ENABLE); }

DMA 搬运完成会进入中断,在主函数里把缓存数据送 LCD 显示即可。这里要特别提醒:DMA_M2M 必须使能,因为我们是软件触发连续读取,没有外设发送请求;另外,FSMC 读时序里的 DataSetupTime 如果太小,FIFO 数据可能还没稳定就被读走,表现为整帧错位或花屏,具体数值要根据主频和 FIFO 的响应时间微调。

3.4 帧同步与显示逻辑:VSYNC 是整条链路的“节拍器”

读 FIFO 的时机取决于帧同步信号 VSYNC。OV7670 输出一帧数据时,VSYNC 会拉高一段时间,表示正在同步;之后开始输出有效像素行。对带 FIFO 模块来说,传感器内部会把有效像素写入 FIFO,所以在 VSYNC 下降到低电平的时刻,表示当前帧已经写完了。如果这个时候启动 DMA 从 FIFO 读取,读到的就是完整一帧,而且还没有新的写入介入,最安全。

因此我在代码里用 PA0 的外部中断检测 VSYNC 下降沿,中断服务函数里做三件事:拉低 FIFO_OE,复位 FIFO 读指针,启动 DMA 读取。DMA 传输完成中断里再把 FIFO_OE 拉高,表示这次读取结束。

void EXTI0_IRQHandler(void) { if(EXTI_GetITStatus(EXTI_Line0) != RESET) { EXTI_ClearITPendingBit(EXTI_Line0); if(ov7670_frame_ready == 0) { GPIO_ResetBits(GPIOB, GPIO_Pin_1); // FIFO_OE low GPIO_ResetBits(GPIOB, GPIO_Pin_0); // FIFO_RRST low DelayUs(1); GPIO_SetBits(GPIOB, GPIO_Pin_0); // FIFO_RRST high DelayUs(1); dma_read_fifo(frame_buffer, QVGA_WIDTH * QVGA_HEIGHT * 2); } } }

这里加一个 ov7670_frame_ready 标志位,是为了防止上一帧 DMA 还没读完、又来了一帧同步时重复启动 DMA。实际运行中如果出现画面撕裂、上下两帧叠在一起,多半就是这个标志位的同步逻辑没写好。DMA 传输完成后,主循环把 frame_buffer 里的数据按 RGB565 格式写入 SPI LCD 即可。注意 SPI LCD 的写屏也要尽量用 DMA,否则 CPU 刷屏期间什么事情都干不了,帧率会被拉到非常低。

4. 常见问题与排查技巧实录:我踩过的坑,你就不用再踩了

4.1 黑屏白屏:从 SCCB 和电源一路查起

如果 LCD 能正常显示但摄像头图像全黑或者全白,先别怀疑寄存器配置,大概率连 SCCB 通信都没建立起来。我的排查顺序是:第一步,用示波器或逻辑分析仪看 SIO_C 上有没有时钟翻转;第二步,用万用表测 SDA 线电压,静态时应该被上拉到高电平;第三步,读产品 ID 寄存器 0x1C 和 0x1D,确认能否返回 0x7F 和 0xA2。

如果读 ID 都失败,常见原因有三个:一是 SIO_C 和 SIO_D 接反了,这两根线交换一下再试;二是 SDA 缺少上拉电阻,OV7670 的引脚不是推挽输出,外部必须有 4.7kΩ 左右的上拉到 3.3V;三是供电不稳,很多模块是把 3.3V 输入再稳压到 1.8V 给传感器核心供电,如果输入电压偏低,整个芯片都不工作。当年我就遇到过 3.3V 电源线接触不良,SCCB 时通时不通,折腾了半天最后发现是杜邦线虚接。

4.2 花屏错位:先调 FSMC 时序,再查 FIFO 复位

花屏是最常见的第二种症状,特征是图像整体变成杂乱的彩色噪点,或者有明显的“错位条纹”。这种问题大概率不在摄像头配置,而在读取链路。我先检查 FIFO_OE 是否在整个读取期间保持低电平,如果中途被拉高,FIFO 数据线进入高阻态,读回来的数据就是乱的。接着查 FSMC 的 DataSetupTime,数据建立时间太短时,FIFO 输出还没稳定就被采样,图像会出现均匀的雪花噪点。

还有一种是每帧图像都“斜”着,比如本该在画面顶部的信息跑到了中间。这通常是复位读指针的时序不对。FIFO_RRST 拉低时间太短,或者拉低后没有延时就开始读,读指针没有真正归零。我习惯在拉低 RRST 后至少延时 1 微秒再拉高,继续延时 1 微秒再启动 DMA,留足内部刷新时间。

还有一个很容易被忽略的问题:如果初始化代码里把 COM7 配成了 VGA 分辨率,但 LCD 和显示缓冲按 QVGA 处理,画面就会每隔一段出现一次叠加错位。先确认寄存器配置和缓冲区尺寸一致,再去调整时序参数。

4.3 颜色异常:RGB565 格式和饱和度的联合诊断

颜色偏绿、偏紫或者整屏变成奇怪的单色,大概率是 RGB 格式寄存器配置不一致。OV7670 输出 RGB565 时,COM15(0x40)要设成对应编码,同时 COM7 里的颜色格式位也要设对。如果两者打架,传感器实际输出的可能是 RGB555 甚至 RGB444,而你还按 16 位 RGB565 解析,颜色自然不对。

另外,很多 OV7670 例程里还配置了 AGC、AWB、饱和度等参数。这些参数直接影响画面观感,过大的饱和度会让颜色显得“糊”或者“艳得发假”,过小则发灰。调试时可以先把 AGC 改成固定增益,排除自动曝光在低照度下疯狂拉高增益导致的偏色。我的做法是,先用默认参数在光线充足环境下看图像,确认“底色”正常后,再逐个调饱和度、亮度、对比度。

4.4 帧率上不去:从时钟分频和总线占用两头优化

QVGA 在 F103 上跑到 15fps 左右是我的一个常见目标,能稳定不闪烁就够用了。如果帧率明显偏低,第一排查对象是传感器的内部时钟。XCLK 太高导致 FIFO 写入过猛,反而可能引起丢帧;XCLK 太低则传感器输出帧率本身就慢。我一般把 XCLK 配到 24MHz 或 36MHz,再通过 CLKRC 把内部像素时钟控制在合理范围。

第二排查对象是显示刷新。SPI LCD 写一帧数据的时间如果比摄像头读一帧还慢,整体帧率就会被显示拖死。SPI 时钟尽量拉到 18MHz 以上,并且用 DMA 刷屏。另外,LCD 的写屏操作要放在 DMA 读取完成之后,不要在 VSYNC 中断里刷屏,否则中断服务函数占用时间过长,下一个 VSYNC 又到了,会造成“踩帧”。

第三排查对象是缓冲区结构。F103 的 RAM 只有 64KB,QVGA 一帧 RGB565 是 153600 字节,放不进片内。我通常把缓冲区放在外部 SRAM 上,或者退而求其次配置成 QVGA 灰度模式减少一半数据量。如果你用的是不带外部 SRAM 的最小系统板,建议降低分辨率到 QQVGA(160×120)或 QCIF(176×144),这样片内 RAM 勉强够用,帧率反而能上去。

现象优先排查点解决方向
SCCB 不通引脚接线、上拉电阻、供电交换 SIO_C/SIO_D,补 4.7kΩ 上拉
全黑 / 全白电源、复位时序、SCCB 初始化按 5 步上电时序重新初始化
花屏噪点FSMC 时序参数、FIFO_OE增大 DataSetupTime,检查 OE 电平
斜纹 / 错位FIFO_RRST、分辨率配置延长复位延时,对齐 QVGA 配置
颜色异常COM15 / COM7、AGC统一 RGB565 格式,固定自动增益
帧率过低XCLK、CLKRC、LCD SPI 速度优化时钟分频,改用 DMA 刷屏

结尾:最后再分享一个我自己的调机习惯

我每次拿到一个新的摄像头模块,都不会先把所有寄存器灌进去,而是先做最小化验证:点亮 LCD、配置 FSMC 读 FIFO、把读到的原始数据直接铺到屏幕上,哪怕图像花得不成样子。只要图像有“反应”——颜色变化、噪点随着镜头移动而变化,就说明链路通了,后面调寄存器才是有意义的。很多人一上来就复制一大段初始化,结果第一步就卡住,根本分不清是硬件接线问题还是软件配置问题。

调试顺序上,我强烈建议“先亮屏、再读 ID、再出图像、最后调参数”四步走。读 ID 是验证 SCCB 的试金石,出图像是验证 FSMC 和 DMA 的里程碑,调参数才是真正考验耐心和经验的地方。每一步都单独验证通过再进入下一步,你会发现自己其实不需要“玄学调参”,大多数问题都是某个环节没打通而已。

OV7670 虽然老,但它把传感器配置、并行接口时序、DMA 搬运、显示刷新这些嵌入式基本功全串到了一起。等你把这条链路彻底吃透,以后再接触 DCMI、CSI、MIPI 这些高级接口,会非常顺,因为底层思路是一样的:让数据流在正确的时钟节拍下,从源头稳定地流到目的地。

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

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

立即咨询