简介:面向STM32F1平台与OV7670无FIFO摄像头调试的学习者,这份工程源码针对网上资料零散、缺少可直接运行参考的问题,给出了一套自写实现:在无FIFO条件下完成图像采集与显示,并集成亮度、饱和度、对比度调节;按下KEY1可切换二值化与灰度化,同时预留颜色识别接口,便于继续做视觉实验。压缩包共312个文件、约10.8MB,以71个C源文件与63个头文件为核心,辅以Keil工程配置、编译中间文件、PNG截图和说明文本,覆盖摄像头、LCD显示、SD卡与FatFs文件系统等模块,适合按工程结构梳理驱动关系。已有4561人学习下载,可作为STM32图像采集入门、课程设计或毕设原型的参考底稿;代码中的图像处理接口也便于继续扩展阈值调节、颜色识别与显示效果验证,减少从零搭建底层驱动与调试时序的时间。
1. 项目定位:OV7670 无 FIFO 在 STM32F1 上到底卡在哪
一块 OV7670 无 FIFO 模块二十块钱不到,接上 STM32F1 就能出图——这话对,但第一次接上的人多半看到的是满屏雪花。原因很实在:无 FIFO 版本没有 AL422B 那颗缓存芯片做缓冲,像素数据会跟着 PCLK 一路往外输出,MCU 必须在每个时钟边沿把那 8 位数据及时接收过来,晚一步就少一个字节,整幅图立刻错位。这份资源包解决的就是这个时序问题:用 STM32F1 的 EXTI 中断配合 GPIO 整字节读取,把 OV7670 无 FIFO 的裸时序跑通,做到低分辨率实时采集、显示、串口导出。适合手上只有 F103、预算有限又想试试图像采集的开发者,也适合已经在用带 FIFO 版本、想省一颗芯片成本、看看能不能不牺牲效果的人。
2. 硬件与工程结构:引脚分配、时钟树和内存预算
OV7670 无 FIFO 的接线方案不是随便凑出来的,每一个引脚都得为“中断响应要快、数据要一次性读走”这个目标服务。把它当成一个“带并行总线的慢速传感器”来看待,而不是当成 SPI 外设,接线的取舍就清楚了。
2.1 引脚连接:把 8 位数据口排在同一个 IDR 字节里
数据口我建议固定接 PB0~PB7,原因不是方便画板,而是GPIOB->IDR的低 8 位刚好对应 PB0~PB7,一条GPIOB->IDR & 0x00FF就能把 8 位像素值一次读走,省掉八次位带操作。这个细节在 PCLK 中断处理中直接决定能不能跟上节奏。
| 信号 | 引脚 | 说明 |
|---|---|---|
| D0~D7 | PB0~PB7 | 数据总线,必须同一端口连续 8 位 |
| PCLK | PA0 | 像素时钟,EXTI0 下降沿触发 |
| HREF | PA1 | 行有效,EXTI1 双沿触发 |
| VSYNC | PA2 | 帧同步,EXTI2 下降沿触发 |
| SCL | PB10 | SCCB 时钟,软件模拟 |
| SDA | PB11 | SCCB 数据,开漏加 4.7k 上拉 |
| XCLK | PA8 | MCO 输出 8MHz 给摄像头主时钟 |
| RESET | PC13 | 复位,低有效 |
| PWDN | PC14 | 掉电控制,低有效(正常工作拉低) |
XCLK 用 MCO 输出是最省事的方式。F1 的 MCO 只能选 HSE、SYSCLK/2、PLL/2 三档,72MHz 系统时钟下 SYSCLK/2 是 36MHz,超过 OV7670 允许的输入范围,所以直接选 HSE 直出 8MHz,RCC->CFGR里把 MCO 位设成0b110。这里有个反直觉的地方:很多人以为主频越高越好,其实 OV7670 的 PCLK 是从 XCLK 分频出来的,XCLK 给得高,PCLK 就跟着高,中断反而更追不上。8MHz 是个甜点值。
SCCB 虽然名字像 I2C,但时序不完全兼容标准 I2C,用硬件 I2C 外设经常会遇到 ACK 收不到的问题,所以我一般用两根 GPIO 软件模拟,频率压到 100kHz 以下,写四五十个寄存器也就几毫秒。
2.2 工程目录:驱动、采集、渲染三层切开
ov7670_nofifo/ ├── Core/ │ ├── main.c 主循环:初始化 -> 起采 -> 出图 │ └── stm32f1xx_it.c 中断入口,只做转发 ├── Drivers/ │ ├── ov7670/ │ │ ├── sccb.c/.h SCCB 时序,读写寄存器 │ │ ├── ov7670_regs.c 寄存器配置表(按格式分组) │ │ └── ov7670.c/.h 上电序列、格式切换 │ ├── capture/ │ │ ├── cam_capture.c/.h EXTI 中断、像素拼装、帧切换 │ │ └── cam_buffer.c/.h 双缓冲管理 │ └── bsp/ │ ├── delay.c us 级延时(DWT 或 SysTick) │ ├── uart.c 串口导出 │ └── tft_ili9341.c SPI 屏驱动(可选) └── App/ └── app_main.c 采集 -> 处理 -> 显示的业务串分层的意义在于:sccb.c只管时序,ov7670_regs.c只管一堆寄存器数值,cam_capture.c只管中断里的字节流,三者可以单独替换。我见过把寄存器表和中断服务函数全塞进main.c的写法,调分辨率的时候改一个数要翻三百行,改完之后连自己都不知道动了什么。这个项目的拆分方式,换分辨率只需要改cam_buffer.h里的宽高宏加ov7670_regs.c里的一行,其余代码一行不用碰。
cam_buffer.c单独抽出来是有讲究的。无 FIFO 方案里,缓冲区是最容易被写坏的公共资源:中断在往里写,主循环在往外读,一旦交叠,画面就会撕裂或者整帧花掉。把缓冲的申请、切换、占用标志全收在一个文件里,能保证这套规则不至于散落到五个地方各写一遍。
2.3 内存与时间预算:先把账算清楚再动手
OV7670 无 FIFO 在 F1 上最大的限制不是 CPU 快慢,而是 SRAM 容量和中断耗时。先算内存:
| 分辨率 | 格式 | 单帧字节数 | F103C8T6(20KB) | F103ZET6(64KB) |
|---|---|---|---|---|
| 320×240 | RGB565 | 153600 | 放不下 | 放不下 |
| 160×120 | RGB565 | 38400 | 放不下 | 可以(剩 25KB) |
| 160×120 | 灰度(取高字节) | 19200 | 勉强,几乎无余量 | 可以 |
| 80×60 | RGB565 | 9600 | 可以(双缓冲 19200 太紧) | 可以 |
这张表是选型的起点。C8T6 只有 20KB,做双缓冲 RGB565 基本没戏,实际落地要么降到灰度、要么砍到 80×60。ZET6 有 64KB,160×120 RGB565 单缓冲能放下,双缓冲 76.8KB 又超了,所以常见做法是“单采集缓冲 + 单显示行缓冲”,边采边刷屏。
再算时间。EXTI 中断在 72MHz 下的进出开销(压栈、跳转、清挂起、读 IDR、写内存、返回)保守按 1.5~2μs 估。也就是说每秒最多能进大约 50 万次中断。QQVGA 160×120 的 RGB565 每帧需要 160×120×2 = 38400 个 PCLK,按 500k 次/秒算,理论天花板 13fps,扣掉消隐和主循环占用,实际能跑到 5~8fps 就算不错。
所以 OV7670 的 PCLK 必须压到几百 kHz 量级,靠寄存器 0x11(CLKRC)做内部分频,把 XCLK 8MHz 分下来。具体分到多少,别信计算,拿示波器夹 PA0 看实际频率,能配合中断耗时跑通就行。
提示:F1 上没有 DCMI 外设,GPIO 也不是 DMA 请求源,所以“PCLK 触发 DMA 直接搬 GPIO->IDR”这条路在 F103 上走不通。看到别人这么写的方案,多半是 F4 或者外挂了 CPLD,别照抄到 F1 上浪费时间。
3. OV7670 寄存器配置:SCCB 写入与输出格式取舍
寄存器配置错了,后面所有代码都是白调。这部分最容易出问题,因为网上流传的配置表版本多、注释少,有的人直接抄了一份 VGA 的表跑 QQVGA,结果图像缩在一角。
3.1 SCCB 三字节写时序
OV7670 的写操作是固定三字节:设备地址 0x42、寄存器地址、数据。读要分两阶段,先写地址再读,比标准 I2C 少一个重复起始位。
/* 开漏输出,外部 4.7k 上拉 */ #define SCL_H() (GPIOB->BSRR = GPIO_BSRR_BS10) #define SCL_L() (GPIOB->BSRR = GPIO_BSRR_BR10) #define SDA_H() (GPIOB->BSRR = GPIO_BSRR_BS11) #define SDA_L() (GPIOB->BSRR = GPIO_BSRR_BR11) #define SDA_IN() (GPIOB->IDR & GPIO_IDR_IDR11) static void sccb_delay(void) { delay_us(5); } /* 约 100kHz */ static void sccb_start(void) { SDA_H(); SCL_H(); sccb_delay(); SDA_L(); sccb_delay(); /* SCL 高时 SDA 下降 = 起始 */ SCL_L(); sccb_delay(); } static void sccb_stop(void) { SDA_L(); SCL_H(); sccb_delay(); SDA_H(); sccb_delay(); /* SCL 高时 SDA 上升 = 停止 */ } /* 返回 0 表示收到 ACK,非 0 表示从机没应答 */ static uint8_t sccb_write_byte(uint8_t dat) { for (uint8_t i = 0; i < 8; i++) { if (dat & 0x80) SDA_H(); else SDA_L(); dat <<= 1; sccb_delay(); SCL_H(); sccb_delay(); SCL_L(); } SDA_H(); /* 释放 SDA 给从机拉低 */ sccb_delay(); SCL_H(); sccb_delay(); uint8_t ack = SDA_IN(); /* 0 = ACK */ SCL_L(); sccb_delay(); return ack; } uint8_t ov7670_write_reg(uint8_t reg, uint8_t val) { uint8_t err; sccb_start(); err = sccb_write_byte(0x42); /* 写地址 */ err |= sccb_write_byte(reg); err |= sccb_write_byte(val); sccb_stop(); return err; }这里面err |=的写法是有意为之:三次字节传输只要有一次 NACK,整体就判为失败,调用方拿到非 0 直接打印寄存器号,比在每一行手写判断省事得多。sccb_delay里的 5μs 对应约 100kHz 时钟,OV7670 手册允许到 400kHz,但面包板加杜邦线的场景下我一般压到 100kHz 以内,出问题的概率明显低。
SDA_H()之后一定要给从机拉低的机会,所以在读 ACK 之前先释放总线并多等一个sccb_delay()。漏掉这一拍,ACK 采样点会落在 SDA 还没稳定的时候,表现出来就是初始化时零星几个寄存器写不进去,重上电有时候又好了——典型的时序边缘问题。
3.2 关键寄存器:格式、分频、窗口
下面是我在 160×120 RGB565 场景下用的一组核心值。表中只列真正影响结果的几项,其余(伽马、自动增益、白平衡)用芯片默认值即可,新手阶段不建议动。
typedef struct { uint8_t reg, val; } reg_pair_t; const reg_pair_t ov7670_init_seq[] = { /* 软复位后必须留 >=1ms 再写后续寄存器 */ {0x12, 0x80}, /* COM7:复位 */ {0x11, 0x1F}, /* CLKRC:内部分频,PCLK 压到几百 kHz */ {0x12, 0x14}, /* COM7:RGB 输出 + QVGA 分辨率位 */ {0x8C, 0x00}, /* RGB444 关闭,走 RGB565 */ {0x40, 0xD0}, /* COM15:RGB565 输出,数据范围 0~255 */ {0x14, 0x00}, /* COM10:VSYNC 负极性、HREF 高有效 */ {0x17, 0x13}, /* HSTART */ {0x18, 0x01}, /* HSTOP */ {0x32, 0x80}, /* HREF:水平有效宽度 */ {0x19, 0x02}, /* VSTART */ {0x1A, 0x7A}, /* VSTOP */ {0x03, 0x0A}, /* VREF:垂直有效高度 */ {0x13, 0x00}, /* COM8:先关自动白平衡/增益,稳定后再开 */ };0x11这一行是整个项目里最关键的参数。值越大,内部分频越狠,PCLK 越低,中断越轻松,但帧率也越低。我一般在 0x0F 到 0x1F 之间试,配合示波器看 PA0 的频率,落到 300~600kHz 这个区间基本能稳定跑。
0x12 = 0x14的位含义是 bit[2] 置 1 选 RGB 输出、bit[6:4] 选分辨率档位。不同批次的模块对 QQVGA 这一档的编码不完全一致,有人用 0x0C 出 QQVGA、有人用 0x1C,这一点在手册里写得绕,我是直接试出来的:写完读回来,用串口打印一行的字节数,对得上就是对的。这也解释了为什么网上配置表看起来互相打架——不是谁抄错了,是模块版本不同。
0x40 = 0xD0配上0x8C = 0x00是 RGB565 的组合;如果0x8C误设成 0x01,输出会掉到 RGB444,一个像素还是两字节但只用了半个字节的信息,画面会明显偏灰、细节全丢,很容易被误判成“镜头脏了”。
4. 采集核心:PCLK 中断 + IDR 整体读取的完整链路
真正让无 FIFO 方案跑通的部分都在这里。中断服务函数里做的每个动作都会占用一个 PCLK 周期的时间,所以代码要写得极简,任何函数调用和浮点运算都得挪出去。
4.1 EXTI0 中断:一个像素两个字节怎么拼
OV7670 在 RGB565 模式下,一个像素输出两个字节,先高字节后低字节,中间夹着一个 PCLK。所以中断里要做的是两拍状态机,而不是一次写完一个像素。
#define OV_W 160 #define OV_H 120 #define OV_BYTES (OV_W * OV_H * 2) static uint8_t frame_buf[OV_BYTES]; static uint16_t byte_idx = 0; /* 当前帧已写入的字节数 */ static uint8_t phase = 0; /* 0=等高字节 1=等低字节 */ static uint8_t high_byte = 0; static volatile uint8_t cap_en = 0; /* HREF 高电平期间为 1 */ static volatile uint8_t fr_rdy = 0; /* 一帧完整,通知主循环 */ void EXTI0_IRQHandler(void) { EXTI->PR = EXTI_PR_PR0; /* 写 1 清挂起,别用读改写 */ if (!cap_en) return; /* HREF 无效时直接丢 */ uint8_t d = (uint8_t)(GPIOB->IDR & 0x00FF); /* PB0~PB7 一次读走 */ if (phase == 0) { high_byte = d; phase = 1; } else { if (byte_idx < OV_BYTES) { frame_buf[byte_idx++] = high_byte; /* 高字节在前 */ frame_buf[byte_idx++] = d; } phase = 0; } }EXTI->PR必须用直接赋值而不是|=,因为 PR 是写 1 清除,读回来全是 0 再写回去等于什么都没清,中断会一直挂在里面反复进出。这个坑我在别的外设上踩过一次,现象是程序卡死但看门狗不喂就复位。
byte_idx < OV_BYTES那个边界判断不能省。HREF 有效期间摄像头可能多吐出几个字节(取决于窗口寄存器和 HREF 的实际宽度是否对齐),没有这个判断就会踩穿数组,跑到相邻变量上去,表现出来就是“跑久了画面莫名其妙变成全黑”。
4.2 HREF 与 VSYNC:行同步和帧同步怎么挂
PCLK 中断只管搬字节,至于什么时候开始搬、搬多少算一行、什么时候算一帧结束,全靠 HREF 和 VSYNC 两个中断来定。
/* HREF:上升沿开始一行,下降沿结束一行 */ void EXTI1_IRQHandler(void) { EXTI->PR = EXTI_PR_PR1; if (GPIOA->IDR & GPIO_IDR_IDR1) { cap_en = 1; phase = 0; /* 每行开头对齐字节相位,防止奇数丢失累积 */ } else { cap_en = 0; } } /* VSYNC:下降沿表示一帧结束,切换缓冲 */ void EXTI2_IRQHandler(void) { EXTI->PR = EXTI_PR_PR2; if (GPIOA->IDR & GPIO_IDR_IDR2) return; /* 只处理下降沿 */ if (byte_idx >= OV_BYTES) { /* 收到完整一帧才算数 */ fr_rdy = 1; byte_idx = 0; /* 复位索引,准备下一帧 */ } else { byte_idx = 0; /* 不完整直接丢弃 */ } }phase = 0这一行放在 HREF 上升沿里,是为了处理一种边界情况:如果某一行因为丢字节导致相位错位,下一行开头强行归零就能把错误限制在当前行,不会像多米诺骨牌一样把后面所有行全部带歪。第一次调试没加这行的时候,画面表现是整体斜切,从第 3 行开始一路歪到底。
VSYNC 的极性由COM10寄存器决定。我在配置表里写的是0x14 = 0x00,也就是 VSYNC 负极性,下降沿代表帧开始/结束。如果极性反了,fr_rdy永远不会被置位,主循环一直等在那里,看起来就像“程序跑飞了但其实没跑飞”。
4.3 主循环:出图与串口导出
中断只负责填缓冲,剩下的渲染、上传、处理全部放主循环,两者通过fr_rdy这个标志握手。
int main(void) { clock_init(); /* 72MHz,MCO 输出 HSE 8MHz */ delay_init(); uart_init(115200); sccb_gpio_init(); data_gpio_init(); /* PB0~PB7 上拉输入 */ exti_init(); /* PA0/PA1/PA2 三路 EXTI */ tft_init(); if (ov7670_init() != 0) { printf("ov7670 sccb fail\r\n"); while (1); } ov7670_set_format(FMT_RGB565_QQVGA, 0x1F); /* 分辨率 + 分频 */ while (1) { if (fr_rdy) { fr_rdy = 0; /* 屏显:逐行推,别整帧 memcpy 到显存,F1 扛不住 */ tft_blit_rgb565(0, 0, OV_W, OV_H, frame_buf); /* 串口导出:加两字节帧头方便上位机对齐 */ uart_send_frame(frame_buf, OV_BYTES); } } }tft_blit_rgb565里做的是“按行设置窗口 + 连续写”,每行 320 字节,一次 SPI 突发写完,避免每写一个像素就拉一次 DC 和 CS。这里如果把整帧先复制到一个显存数组再刷,F103 的 64KB RAM 直接爆掉,所以必须边读边发。
串口导出同理,加0xAA 0x55帧头比一上来就灌数据有用得多。上位机靠帧头同步,丢一个字节之后能自己找回来,否则一旦错位,后面整段数据全是垃圾,你还得反复拔插重启。115200 波特率下传一帧 38400 字节大约 3.3 秒,所以串口导出只适合抓单帧做验证,实时预览还是得靠屏。
5. 避坑排查:雪花、斜切、偏色这五类问题
无 FIFO 方案调不通的时候,画面症状和原因通常是一一对应的,把现象对上号能省掉大半天。下面五条都是我在这个组合上真实遇到过的。
现象一:整屏雪花,偶尔闪过一条横带。原因基本是 PCLK 中断根本没进来,或者进了但cap_en一直是 0。先查 EXTI 的触发边沿:OV7670 的 PCLK 是在数据稳定后输出上升沿,配置时选下降沿采样,选反了读到的就是相邻像素的残值。再查EXTI->IMR有没有使能对应通道,以及 NVIC 里的优先级分组别把三路中断排到同一个抢占级导致互相打断。 解决:拿示波器同时看 PA0 和 PA1,确认 HREF 为高的区间内确实有 PCLK 脉冲;再看 CubeMX 生成的中断优先级,把 PCLK 设成最高抢占级,HREF/VSYNC 次之。
现象二:图像整体向右下方斜切,越往下越歪。原因是行内字节数计算和实际 HREF 宽度对不上。160 宽 RGB565 应该是 320 字节一行,如果OV_W写成 160 但缓冲按 160 字节一行算,每行就会多写或漏写。另一种情况是phase在某行开头没归零,导致后面的行全部差半个字节。 解决:在 HREF 下降沿里把byte_idx % 320打印出来,正常应该是 0;不是 0 就说明这一行的字节数和预期不符,先回去核对COM7选的分辨率档位和OV_W/OV_H宏是否对应。
现象三:画面只有上半部分有内容,下半部分重复上一帧或者全黑。这是帧率超了缓冲承受能力。中断里写完一帧但主循环还没处理完,下一帧直接把同一块内存覆盖掉,读到的东西就是半新半旧的。 解决:加双缓冲,或者在fr_rdy为 1 期间在 HREF 上升沿里直接cap_en = 0停止采集,等主循环处理完再放行。代价是帧率掉一半,但画面至少是对的。C8T6 这种 20KB 的芯片,双缓冲放不下就只能走“采一帧、停一帧、刷一帧”的路子。
现象四:SCCB 写寄存器返回非 0,摄像头初始化直接退出。三个常见原因:SDA/SCL 没加上拉电阻(很多模块板上没有,必须外部补 4.7k);XCLK 没给,OV7670 内部逻辑不工作,从机自然不应答;PWDN 引脚悬空被拉高,芯片处于掉电态。 解决:先量 PA8 有没有 8MHz 方波,再量 SDA 空闲时是不是被上拉到 3.3V,最后确认 PWDN 被拉低、RESET 已经释放。这三步按顺序走,比盲目改延时靠谱。
现象五:图像能出但颜色不对,绿得像夜视仪,或者整体偏紫红。偏绿通常是字节序反了,RGB565 的高低字节在缓冲里位置颠倒,G 分量被拼到了最高位;偏紫红多半是格式选错,实际输出的是 YUV422 却按 RGB565 解释。 解决:先确认0x40 = 0xD0、0x8C = 0x00这两项;再在 EXTI0 里把high_byte和d的写入顺序对调试一次,两种顺序各拍一帧对比,颜色正常的那一组就是对的。这个排查过程不到两分钟,比查手册快。
6. 二次开发:换分辨率、搬到 F4、加一层简单识别
这套采集链路跑通之后,改动的空间比想象中大,但每一步都要重新算账,不能只改宏定义。
换分辨率是最常见的需求,对照表如下:
| 目标分辨率 | 格式 | 单帧字节 | 建议分频(0x11) | 备注 |
|---|---|---|---|---|
| 80×60 | RGB565 | 9600 | 0x0F | C8T6 可双缓冲 |
| 160×120 | 灰度 | 19200 | 0x13 | 只取高字节,省一半内存 |
| 160×120 | RGB565 | 38400 | 0x1F | ZET6 单缓冲,帧率 5~8fps |
| 320×240 | 灰度 | 76800 | 0x3F | ZET6 极限,帧率 1~2fps |
改的时候三处要同步:OV_W/OV_H宏、ov7670_regs.c里COM7的分辨率位、窗口寄存器HSTART/HSTOP/VSTART/VSTOP。只改前两处不改窗口,出来的图会是原分辨率裁出来的一块,边缘还会带黑边。灰度模式不需要改摄像头配置,在 EXTI0 里只存high_byte就行,一个像素一个字节,中断耗时直接砍半,帧率能翻倍。
搬到 F4 或者 F7 上,整个采集层可以扔掉重写。这些片子有 DCMI 外设,配 DMA 之后 CPU 完全不参与搬运,OV7670 的 PCLK 接 DCMI_PIXCLK、8 位数据接 DCMI_D0~D7、HREF 接 DCMI_HSYNC、VSYNC 接 DCMI_VSYNC,一帧采完才进一次中断。我在 F407 上做过同样的摄像头,320×240 RGB565 稳定 30fps,和 F1 上的 5fps 完全不是一个量级。但 F1 这套代码的价值在于把时序搞明白了——知道 PCLK 是什么、HREF 什么时候有效、字节相位怎么对齐,换到 DCMI 上配寄存器时心里有底,不会照抄例程然后对着花屏干瞪眼。
加简单识别也不难,在主循环拿到frame_buf之后做就行,不用碰中断:
/* 在 160x120 RGB565 里找红色色块,返回中心坐标 */ int find_red_blob(const uint8_t *buf, int *cx, int *cy) { int sum_x = 0, sum_y = 0, cnt = 0; for (int y = 0; y < OV_H; y += 2) { /* 隔行扫,省一半时间 */ for (int x = 0; x < OV_W; x += 2) { int i = (y * OV_W + x) * 2; uint16_t px = (buf[i] << 8) | buf[i + 1]; /* 高字节在前 */ int r = (px >> 11) & 0x1F; int g = (px >> 5) & 0x3F; int b = px & 0x1F; if (r > 20 && g < 12 && b < 12) { /* 阈值按现场光调 */ sum_x += x; sum_y += y; cnt++; } } } if (cnt < 20) return -1; /* 太小当噪声丢掉 */ *cx = sum_x / cnt; *cy = sum_y / cnt; return 0; }阈值那几个数没有通用值,白光灯和暖光灯下差得很远,我一般是先把cnt和几个采样点的 RGB 打印出来,看着实际分布定,而不是凭感觉写 15、10、10。隔行隔列扫描是有意为之:160×120 全扫 19200 个像素,在 72MHz 上大概 8ms,隔行隔列能压到 2ms 左右,对质心计算的影响可以忽略。
验证改造是否成功,我有个固定套路:先降到 80×60 灰度、PCLK 分频拉到最大,确保能出稳定的一帧;然后逐项放开——先提分辨率、再换 RGB565、最后提帧率。每放开一项就用串口抓一帧存成文件看,不和屏幕较劲。这条路径慢,但每次只改一个变量,出问题时原因一定是刚改的那一项,不用回头怀疑已经验证过的东西。
第一次调这个方案时,我图省事把分辨率、格式、分频三个参数一起改,结果花屏花了两个晚上,最后发现只是 CLKRC 设得太大导致 PCLK 慢到 HREF 提前拉低。从那以后,每次动 OV7670 的寄存器,我都强制自己一次只改一行,改完必须用示波器确认 PCLK 和 HREF 的关系再往下走。这个习惯看着笨,但确实省下了大量来回折腾的时间。希望帮到你。
本文还有配套的精品资源,点击获取