ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32F103无DCMI接口读取OV7725图像采集与TFT显示详解

STM32F103无DCMI接口读取OV7725图像采集与TFT显示详解 简介STM32F103与OV7725摄像头模块结合的图像采集与液晶显示例程面向嵌入式开发者和电子爱好者提供从摄像头初始化、图像数据捕获与传输、格式处理到屏幕显示的完整软件实现。资源共561个文件以C语言源码和工程配置文件为主同时包含编译生成的hex、axf、o等中间产物以及备份文件压缩包整体约16.3MB目录结构清晰便于直接导入Keil工程学习或二次开发。已有59人学习浏览。例程中不仅配置了OV7725的工作模式、分辨率与帧率还实现了LCD控制引脚初始化与像素映射等显示驱动逻辑主循环负责协调图像采集、处理和刷新周期。附带的调试状态检查代码有助于快速定位摄像头或显示模块的硬件问题非常适合作为STM32图像应用开发的入门模板和项目起点。1. OV7725 与 STM32F103没有 DCMI 也能读摄像头的工程现实第一次想在 F103 上挂 OV7725 的开发者多半会被参考手册里的一行字劝退STM32F103 没有 DCMI 数字摄像头接口。没有硬件接口不等于不能干这套 BH-F103 例程就是把 OV7725 的 8 位并行数据线直接接到 GPIOPCLK 触发 EXTI 中断一字节一字节地把像素搬进内存再用 FSMC 把 RGB565 数据刷到 TFT 液晶屏。工程里保留了 Keil MDK 的布局文件.uvguix和已编译的 OV7725.axf说明发布者实际编译并跑通过依赖的是标准外设库 V3.5对刚接触图像采集的人来说是一个能完整复现的最小骨架。对想往老项目里塞拍照功能的工程师也可以直接摘走 SCCB 驱动、PCLK 同步采样、FSMC 刷屏这三段链路。这篇就按这三条链路拆开讲。2. SCCB 寄存器配置把 OV7725 校准到 RGB565 输出2.1 用 GPIO 模拟 SCCB而不是硬件 I2COV7725 的配置通道叫 SCCB两根线 SIO_C 和 SIO_D时序和 I2C 大同小异差别在停止条件和重复起始的相位上。F103 的硬件 I2C 在标准外设库时代口碑一般errata 多与其跟硬件外设较劲不如直接在 GPIO 上用软件模拟反正配置摄像头的速率也就是几百 kHzCPU 完全扛得住。下面这段是例程里最核心的底层时序SDA 必须配置为开漏并外接上拉SCL 用推挽输出这样才能在第 9 个时钟读出从机 ACK。#define SCCB_GPIO GPIOB #define SCCB_SCL GPIO_Pin_10 #define SCCB_SDA GPIO_Pin_11 #define OV7725_WR 0x42 // 器件写地址7 位地址 0x21 左移 1 位 #define OV7725_RD 0x43 static void sccb_delay(void) { delay_us(2); } static void sccb_start(void) { GPIO_SetBits(SCCB_GPIO, SCCB_SCL | SCCB_SDA); // 总线空闲 sccb_delay(); GPIO_ResetBits(SCCB_GPIO, SCCB_SDA); // SCL 高时 SDA 拉低起始 sccb_delay(); GPIO_ResetBits(SCCB_GPIO, SCCB_SCL); } static void sccb_stop(void) { GPIO_ResetBits(SCCB_GPIO, SCCB_SDA); // SDA 先拉低 sccb_delay(); GPIO_SetBits(SCCB_GPIO, SCCB_SCL); // SCL 拉高 sccb_delay(); GPIO_SetBits(SCCB_GPIO, SCCB_SDA); // SCL 高时 SDA 拉高停止 } static void sccb_tx_byte(uint8_t dat) { uint8_t i; for (i 0; i 8; i) { if (dat 0x80) GPIO_SetBits(SCCB_GPIO, SCCB_SDA); else GPIO_ResetBits(SCCB_GPIO, SCCB_SDA); dat 1; GPIO_SetBits(SCCB_GPIO, SCCB_SCL); // 数据在 SCL 高电平期间有效 sccb_delay(); GPIO_ResetBits(SCCB_GPIO, SCCB_SCL); sccb_delay(); } GPIO_SetBits(SCCB_GPIO, SCCB_SDA); // 释放 SDA等从机 ACK GPIO_SetBits(SCCB_GPIO, SCCB_SCL); sccb_delay(); GPIO_ResetBits(SCCB_GPIO, SCCB_SCL); }这段代码里delay_us(2)对应 250kHz 左右的 SCCB 时钟大部分模块在这个速率下都能稳定工作。如果线长超过 10cm把延时提到 5us 更稳妥。sccb_tx_byte末尾的第九个时钟只负责读 ACK不判断 ACK 值因为后续写寄存器函数会忽略它真正需要判断结果的是读 ID 环节。2.2 读回传感器 ID先验证物理链路SCCB 的读操作比 I2C 多了一个重复起始过程先发写地址和寄存器子地址然后重新产生一次 START再发读地址把方向切过来。很多第一次调 OV7725 的人在这里卡住直接把 I2C 的读时序套上去结果 ID 永远读成 0xFF。正确写法如下。uint8_t sccb_read_reg(uint8_t addr) { uint8_t dat 0; sccb_start(); sccb_tx_byte(OV7725_WR); sccb_tx_byte(addr); // 写入寄存器子地址 sccb_start(); // 重复起始切换为读方向 sccb_tx_byte(OV7725_RD); dat sccb_rx_byte(); // 读 8 bit主机发 NACK sccb_stop(); return dat; }sccb_rx_byte的实现与发送对称每来一个 SCL 高电平把 SDA 电平移入变量即可这里不贴完整代码。验证物理链路的方法是读 0x0A 和 0x0B 两个寄存器正常应该分别返回 0x77 和 0x21拼起来就是 PID0x77、VER0x21。如果读回 0xFF优先检查 SDA 上拉电阻和电源如果读回 0x00重点查 SCL 是否虚焊。2.3 初始化参数的取舍关注格式别烧脑抠每个寄存器OV7725 的上电默认输出不一定是 RGB565常见模块上电默认输出 YUV422直接采数据会看到一片青绿色画面。初始化要做的事就是把输出格式切到 RGB565并确认分辨率在 VGA 还是 QVGA。工程中常见的关键寄存器如下表。寄存器名称配置/调试要点0x0A / 0x0BPID / VER只读读回 0x77 / 0x210x12COM7输出格式与分辨率RGB 输出关键位0x11CLKRCPCLK 分频F103 直读建议降频0x1EMVFP画面镜像/翻转装反时调这里0x27 ~ 0x2A窗口裁剪输出 QVGA 时调整行场起始与尺寸0x0DCOM4PCLK 分频及 PLL 配置按模块手册改下面是一段最精简的初始化函数。工程完整版里通常还有几百行寄存器配置但核心思路就这几条。uint8_t OV7725_Init(void) { uint16_t pid; pid ((uint16_t)sccb_read_reg(0x0A) 8) | sccb_read_reg(0x0B); if (pid ! 0x7721) return 1; // SCCB 链路不通 sccb_write_reg(0x12, 0x80); // COM7先软复位传感器寄存器 delay_ms(10); sccb_write_reg(0x12, 0x06); // COM7VGA 分辨率RGB 输出 sccb_write_reg(0x11, 0x00); // CLKRC外部时钟直通 sccb_write_reg(0x1E, 0x00); // MVFP不镜像不翻转 sccb_write_reg(0x15, 0x02); // COM10PCLK 输出使能等按实际模块调 return pid 0x7721 ? 0 : 1; }0x12的 bit2 置 1 表示 RGB 输出bit0 置 0 表示 RGB565 而不是 RGB555这也是为什么常见值落在 0x06 附近。如果画面整体偏色严重先怀疑这里如果画面上下颠倒就动 0x1E 的 bit7左右镜像动 0x1E 的 bit6。寄存器配置到这里已经够进下一步了剩下那些曝光、增益、白平衡参数等图像稳定后再调。3. PCLK 中断 GPIO 直读图像数据从摄像头到内存3.1 三种读取路线的选型对照OV7725 的像素数据是并行输出的常见接法有三种对应不同硬件投入和 CPU 开销。方案硬件改动CPU 占用适用场景带 AL422B FIFO 模块模块自带 FIFOF103 走 FSMC 读低产品化最稳但模块贵PCLK 接 EXTI 中断直读无需额外硬件高本套例程采用教学清晰PCLK 接定时器触发 DMA接线不变代码复杂中帧率要求高需处理行场同步F103 没有 DCMI 接口所以例程走的是第二种路线PCLK 每来一个上升沿就触发一次外部中断在中断里把 GPIO 口上的 8 位数据取走。这个方案最直观也最容易排查问题代价是 CPU 在图像采集期间被中断占满主循环里做不了重活。对这套“软件实验例程”来说这个取舍是对的。3.2 接线布局与帧缓冲定义典型的 F103 接线方式是OV7725 的 D0-D7 接 PC0-PC7PCLK 接 PA8 并映射到 EXTI8VSYNC 接 PA4 映射到 EXTI4HREF 接 PA5 用普通 GPIO 查询。PC 口的优势是GPIO_ReadInputData(GPIOC)一次能读回全部 16 位D0-D7 在低 8 位取出低字节就是当前像素字节。#define IMG_W 320 #define IMG_H 240 static uint16_t frame[IMG_H][IMG_W]; // QVGA RGB565 帧缓冲 static volatile uint16_t row, col; static volatile uint8_t pixel_low; // 凑像素时暂存第一个字节 static volatile uint8_t first_byte 1; static volatile uint8_t frame_ready 0;QVGA 一帧 320x240 像素、每像素 2 字节共 153.6KB。F103 的 SRAM 通常只有 20KB 到 64KB这张大数组已经占掉了可用内存的大半实际工程里如果芯片是 64KB 版本还能放两帧做乒乓缓冲20KB 版本只能压缩到 160x120 或者按行处理。3.3 EXTI 中断里逐字节凑像素OV7725 输出 RGB565 时一个像素要分两个 PCLK 周期送出第一个字节是低位部分第二个字节是高位部分合在一起才是 16 位 RGB565。代码里用first_byte做状态标记配合 HREF 用查询方式判断当前是否在行有效区间。void EXTI9_5_IRQHandler(void) { uint8_t d; if (EXTI_GetITStatus(EXTI_Line8) RESET) return; if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_5)) { // HREF 为高行有效 d GPIO_ReadInputData(GPIOC) 0xFF; // 取 D0-D7 if (first_byte) { pixel_low d; // 先到的字节存起来 first_byte 0; } else { uint16_t rgb ((uint16_t)d 8) | pixel_low; if (col IMG_W) { frame[row][col] rgb; col; if (col IMG_W) { col 0; if (row IMG_H) row; } } first_byte 1; } } EXTI_ClearITPendingBit(EXTI_Line8); }这里的高低字节顺序是按“先低位、后高位”写的实际取决于模块 PCB 上 D0-D7 与 F103 的接法如果画面出现每像素颜色错位、像噪点但轮廓清晰把pixel_low和d的位置换一下即可。中断里加了边界保护row到达IMG_H后不再写内存防止数组越界导致硬件错误。3.4 VSYNC 帧同步与 FreeRTOS 共享缓冲光有 PCLK 中断还不够必须在 VSYNC 到来时把行号和列号清零否则上一帧和这一帧的数据会拼在一起。void EXTI4_IRQHandler(void) // VSYNC 接 PA4 { if (EXTI_GetITStatus(EXTI_Line4) ! RESET) { row 0; col 0; first_byte 1; frame_ready 0; EXTI_ClearITPendingBit(EXTI_Line4); } }frame_ready在 PCLK 中断里当row走完一整帧位置 1主循环看到标志位后把帧数据交给 LCD然后清标志。如果例程里要跑 FreeRTOS这张frame数组会被中断写、任务读必须加临界区保护或者改成双缓冲否则 LCD 刷新到一半时摄像头又写入新数据画面会出现撕裂。这也是移植 FreeRTOS 到 F103 图像采集场景时最常踩的坑。4. FSMC 驱动 TFT-LCD 并口屏像素流从数组到屏幕4.1 一个地址对应一条控制线图像数据进了内存只是第一步把它显示出来用的是 FSMC。F103 的 FSMC 可以把外部并口设备映射到内部地址空间对开发者的意义是往某个地址写数据硬件自动产生片选、读、写时序。TFT-LCD 通常挂在 Bank1 的 NE1 片选上地址线中取一根常见 A16接 LCD 的 RS 引脚RS 为 0 时写寄存器、RS 为 1 时写显存。#define LCD_REG (*(volatile uint16_t *)0x60000000) // RS0命令/寄存器 #define LCD_RAM (*(volatile uint16_t *)0x60010000) // RS1显存数据这两个宏就是整个显示驱动的地基。0x60010000是在0x60000000基础上把 A16 置 1 得到的如果板子把 RS 接到了 A15 或 A0对应地址也要跟着改。用 FSMC 刷屏比 GPIO 模拟快一个数量级以上因为每次写显存就是一条汇编 store 指令CPU 不需要边翻转控制线边等延时。4.2 FSMC 时序参数怎么配FSMC 的时序参数以 HCLK 为基本单位F103 主频 72MHz 时一个 HCLK 约 13.9ns。下面是一段适用于多数并口 TFT-LCD 的配置SRAM 模式16 位数据宽度。static void FSMC_LCD_Init(void) { FSMC_NORSRAMInitTypeDef fsmc; FSMC_NORSRAMTimingInitTypeDef timing; timing.FSMC_AddressSetupTime 1; // 地址建立时间1 个 HCLK timing.FSMC_AddressHoldTime 0; timing.FSMC_DataSetupTime 3; // 数据建立时间3 个 HCLK 约 42ns timing.FSMC_BusTurnAroundDuration 0; timing.FSMC_CLKDivision 0; timing.FSMC_DataLatency 0; timing.FSMC_AccessMode FSMC_AccessMode_A; fsmc.FSMC_Bank FSMC_Bank1_NORSRAM1; fsmc.FSMC_MemoryType FSMC_MemoryType_SRAM; fsmc.FSMC_MemoryDataWidth FSMC_MemoryDataWidth_16b; fsmc.FSMC_WriteOperation FSMC_WriteOperation_Enable; fsmc.FSMC_ExtendedMode FSMC_ExtendedMode_Disable; fsmc.FSMC_ReadTiming timing; fsmc.FSMC_WriteTiming timing; FSMC_NORSRAMInit(fsmc); FSMC_NORSRAMCmd(FSMC_Bank1_NORSRAM1, ENABLE); }DataSetupTime是最关键的参数它决定了一次访问周期里数据保持时间。多数 ILI9341、ILI9325 并口屏要求写信号低电平脉宽不小于 20ns 到 50ns这里 3 个 HCLK 约 42ns 基本够用。如果屏上出现雪花点或者局部花屏优先把DataSetupTime加到 8代价是每像素写操作慢约 70ns对 320x240 的刷新率影响不明显。4.3 窗口化刷新与扫描方向LCD 初始化完成后刷一帧图像的正确姿势是先把显示窗口设到目标区域然后连续向显存写数据。多数 F103 例程基于 ILI9341指令是 0x2A 设置列范围、0x2B 设置行范围、0x2C 写显存。static void LCD_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { LCD_WriteCmd(0x2A); LCD_WriteData(x0 8); LCD_WriteData(x0 0xFF); LCD_WriteData(x1 8); LCD_WriteData(x1 0xFF); LCD_WriteCmd(0x2B); LCD_WriteData(y0 8); LCD_WriteData(y0 0xFF); LCD_WriteData(y1 8); LCD_WriteData(y1 0xFF); LCD_WriteCmd(0x2C); // 准备写 GRAM } void LCD_ShowRGB565(uint16_t x0, uint16_t y0, uint16_t w, uint16_t h, uint16_t *img) { uint32_t i; LCD_SetWindow(x0, y0, x0 w - 1, y0 h - 1); for (i 0; i (uint32_t)w * h; i) { LCD_RAM img[i]; // 连续写GRAM 地址自动递增 } LCD_SetWindow(0, 0, 239, 319); // 恢复全屏窗口 }这段代码最后的恢复全屏窗口很重要否则后续画点、画线函数会以为窗口还停在图像区域。像素方向问题则出在 ILI9341 的 MADCTL 寄存器地址 0x36其中 bit7 控制行方向、bit6 控制列方向、bit5 控制行列互换、bit3 控制 BGR 颜色顺序。摄像头一帧数据默认从左上角开始如果画面上宽下窄或者左右颠倒去改这个寄存器不要对像素数组做矩阵转置那会白白消耗 CPU。0x36 位功能调试现象bit7 MY行方向置 1 时上下翻转bit6 MX列方向置 1 时左右翻转bit5 MV行列互换屏幕旋转 90 度时用bit3 BGR通道交换颜色偏红/偏蓝时切换5. 帧错位、BMP 存图与几个现场坑5.1 帧同步错行用 HREF 上升沿校准行计数VSYNC 中断清零行列号的做法在多数情况下够用但 OV7725 每帧开头会有若干行消隐数据如果 VSYNC 和 PCLK 的相位没对齐画面上半部分会出现错行条纹。我一般会在 PCLK 中断里额外检测 HREF 的上升沿一旦发现当前行的列号不为零就强制对齐。uint8_t href_now GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_5); uint8_t href_prev 0; if (href_now !href_prev col ! 0) { col 0; // 行提前结束下一行重新计数 if (row IMG_H) row; } href_prev href_now;这段逻辑放在 PCLK 中断的最前面代价是每次中断多执行几条比较指令换来的是画面稳定。对 F103 直读摄像头来说这种“丢尾巴”的容错比追求每一行像素数绝对精确更实际。5.2 拍照存图先把 RGB565 转成 BMP24例程如果只显示不存图价值少一半。工程目录里能看到 cc936.c、cc949.c、cc950.c、cc932.c 这几个文件它们是 FATFS 文件系统的代码页支持说明固件里预留了带中文长文件名的 SD 卡写文件功能。存图前要做一步格式转换把 OV7725 的 RGB565 展开成 BMP 的 24 位 BGR 格式。void RGB565_To_BMP24(uint16_t *rgb565, uint8_t *bmp, uint32_t n) { while (n--) { uint16_t p *rgb565; uint8_t r (p 11) 0x1F; uint8_t g (p 5) 0x3F; uint8_t b p 0x1F; r (r 3) | (r 2); // 5 位扩展到 8 位 g (g 2) | (g 4); // 6 位扩展到 8 位 b (b 3) | (b 2); *bmp b; // BMP 文件按 BGR 存储 *bmp g; *bmp r; } }转换后的数据写给 FATFS 时注意 BMP 文件的行序是从下往上也就是图像最后一行要写在文件前面需要把转换函数改成按行倒序处理否则存出来的图片上下颠倒。这也是把 OV7725 接到 F103 上做拍照功能时最容易忽略的细节。5.3 串口调试、下载失败与 ESD 经验调试这套系统时串口打印是唯一靠谱的观察窗口。注意 USART1 挂在 APB2 上时钟 72MHzUSART3 挂在 APB1 上时钟 36MHz同样配置 115200 波特率USART3 的分频误差更大打印高频日志时容易丢字符。用 USART1 做日志输出会省很多事。下载失败先看 BOOT0 和 BOOT1 跳线BOOT0 拉高时会进入系统存储器DAP 连接再稳定也烧不进程序。最后说一个现场经验摄像头和 LCD 的 FPC 排线频繁插拔时静电打坏 PCLK 引脚的概率远高于数据线。PCLK 上串 33Ω 电阻并在模块端对地并一个 10pF 电容能有效抑制尖峰量产设计时在 PCLK 和 VSYNC 上加 TVS 管比事后换片子便宜得多。花屏先排除 ESD 损伤再怀疑时序这个顺序能省掉一整天的排查时间。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进