
把OV7670摄像头模块接到STM32F103上再让图像在LCD上显示出来这件事本身并不复杂但大多数新手的第一次尝试都会卡在几个高度雷同的坑里买了不带FIFO的模块、SCCB初始化一直无应答、画面花成一团完全没法看。我在这个项目上前后折腾过三个晚上今天把整个流程从选型、接线、初始化到显示完整梳理一遍给后来人一份可以直接照着操作、不用反复试错的保姆级路线图。要理解这套方案的核心首先要接受一个反直觉的事实F103本身并不适合直接采集摄像头数据它的主频72MHz、没有摄像头专用的DCMI接口、GPIO翻转速度有限如果强行按照OV7670输出的像素时钟去读数据CPU会被完全占死。这个方案真正能用起来靠的是板载FIFO芯片把摄像头和MCU彻底解耦。所以这篇教程会围绕“带FIFO的OV7670模块”来展开这也是最值得先去确认的硬件选型问题。1. 硬件选型为什么OV7670要选带FIFO的版本1.1 不带FIFO的噩梦PCLK与CPU实时冲突OV7670本身是一颗输出能力很强的传感器在VGA分辨率下最高能跑30fps像素时钟PCLK最高可达24MHz左右。24MHz意味着每秒钟有2400万个像素点从D0-D7引脚上流过每个像素大约41ns就要采样一次。STM32F103在72MHz主频下纯GPIO读操作最快也差不多要用十几个时钟周期单字节读取最快接近200ns而摄像头的像素周期只有41ns这种速度差距根本没法实时接收数据。更重要的是就算用定时器中断或外部中断慢慢取数CPU也没机会再做别的事。显示、串口打印、后续图像处理全都挤不进来。很多人把不带FIFO的模块接上去之后发现要么花屏、要么白屏大概率不是代码问题而是硬件方案本身就不适合F103这种没有DCMI接口的芯片。1.2 带FIFO后MCU的工作模型带FIFO的模块核心是板上多了一颗AL422B芯片。AL422B是一颗384KB的异步FIFO存储器能够缓存一整帧VGA图像640x480x2614400字节实际上是放不下的所以实际应用中通常工作在QVGA 320x240一帧约153600字节FIFO容错余量很充足。摄像头按照自己的节奏把像素数据不断写入FIFO写完一帧后通过VSYNC引脚通知MCU“这一帧已经缓存好了”。MCU收到VSYNC之后完全按照自己的速度慢慢地把FIFO里的数据读出来再送到LCD显示。读到的是完整的一帧图像不丢行、不丢列和摄像头那边的PCLK、HREF、VSYNC时序彻底无关。这就是带FIFO模块最大的价值它将实时性要求极高的图像采集任务变成了一个没有任何实时性压力的数据搬移任务。F103的GPIO读速度虽然不够实时接摄像头但用来读FIFO完全够用这也是这个方案能成立的根本原因。1.3 完整物料清单与选型提醒进入搭建之前先确认手头的物料是否齐全物料规格/型号用途主控板STM32F103C8T6最小系统板或F103ZET6开发板核心控制摄像头模块OV7670带FIFO模块板载AL422B图像采集LCD屏幕2.4寸或3.5寸并口屏控制器ILI9341或ILI9488图像显示调试器ST-Link V2 或 J-Link程序下载调试USB转串口CH340模块选配跑日志调试电源稳定的3.3V输出建议电流1A以上模块供电连接线杜邦线母对母/公对母若干接线选型时有两个必须注意的地方。第一摄像头模块一定要认准“OV7670带FIFO”字样不带FIFO的裸模块便宜几块钱但后续调试成本会高很多。第二LCD屏如果追求显示流畅度优先选FSMC并口屏如果手头只有SPI串口屏也能出图像但帧率会低到让人怀疑人生后面5.1节会展开对比。2. 接线对照与信号避坑点2.1 一张图看懂所有信号线应该接哪带FIFO的OV7670模块引脚会比裸模块多一些除了传感器的D0-D7、PCLK、VSYNC、HREF之外还有FIFO的控制引脚WEN、WRST、RRST、OE、RD。下面是整套系统的一套实测可用的引脚对照表OV7670模块信号STM32F103引脚信号方向说明VCC3.3V电源模块供电GNDGND电源共地SCLPB10输出SCCB时钟可复用I2C2_SCL也可GPIO模拟SDAPB11双向SCCB数据可复用I2C2_SDA也可GPIO模拟XCLKPA8输出MCO输出时钟给传感器提供主时钟VSYNCPB0输入帧同步信号接外部中断HREF3.3V输入接高电平即可带FIFO模块不需要检测PCLK不接输入读FIFO时用不到D0-D7PC0-PC7输入FIFO数据总线一一对应WEN悬空输入写使能模块内部已处理WRSTPA3输出FIFO写指针复位低有效RRSTPA2输出FIFO读指针复位低有效OEPA1输出FIFO输出使能低有效RDPA0输出FIFO读时钟低有效脉冲RESETPA4或接高输出摄像头复位低有效这里要特别说明HREF信号在不带FIFO的模块里非常重要MCU需要靠它来判断一行的开始和结束但在带FIFO的模块上HREF已经被板载逻辑用于控制FIFO写入MCU不需要再关心直接接3.3V即可。PCLK同理FIFO模块内部已经用PCLK驱动了写侧逻辑MCU读数据时走的是RD引脚完全绕开了像素时钟。2.2 三路电源与共地处理OV7670传感器模组正常工作时电流大约20到40mA加上AL422B和板上其他元件整体负载建议预留100mA以上。开发板上的3.3V LDO一般都能扛住但我遇到过两种情况容易出问题一是劣质USB线压降太大板子标称3.3V实际只有2.8V摄像头直接罢工二是把摄像头和舵机、电机这类感性负载共用一个电源一启动电机画面就花。建议摄像头单独从开发板的3.3V引脚取电不要和电机驱动器共用电源树。共地是整个接线里最容易被忽略的环节。开发板、LCD屏、摄像头模块的地必须连到同一个参考点否则SCCB通信会时好时坏画面还会出现莫名其妙的横纹干扰。我调试时就遇到过因为LCD和摄像头共地不良图像上半部分一直有两条周期性滚动的条纹最后重新飞了一根粗地线解决。另一个容易被坑的是XCLK信号线。12MHz方波在杜邦线上走20cm以上波形会明显劣化可能导致摄像头内部逻辑工作不稳定。建议XCLK线越短越好最好和其他信号线分开走不要贴着电源线或LCD排线。如果实在没法缩短可以考虑在模块XCLK引脚就近对地加一个10pF电容实测对波形质量改善明显。2.3 几个容易接错但很难查的信号线WRST和RRST这两个引脚名称非常相似只差一个字母接反后现象也很具有迷惑性画面不会全黑而是会出现很规律的彩色斜条纹看起来像电视信号没锁定的效果。因为写指针和读指针角色互换FIFO内部各个区域的读取顺序就完全乱了。排查时优先用万用表量一下两脚通断或者直接看电路板上丝印。OE引脚如果悬空问题更隐蔽。AL422B的输出使能OE为低时D0-D7才会真正输出FIFO内部数据OE为高时数据总线处于高阻态。高阻态下GPIO读到的是不确定值最常见的结果是画面整片白屏或者花成雪花点。有的模块上OE默认有上拉电阻不接就默认关闭输出所以必须明确接到PA1并拉低。RD引脚的接法也值得单独说。AL422B的RD是读时钟每次下降沿内部读指针加一同时把当前数据送到D0-D7上。如果RD长期保持低电平读指针不会自动推进读出来的每一字节都是同一个位置的数据画面会变成一条纯色竖带。正确的操作是每读一个字节RD就要完成一次低脉冲。3. 时钟、复位与SCCB初始化先把摄像头叫醒3.1 XCLK从哪来用MCO输出需要注意的问题OV7670必须有一个外部输入时钟XCLK才能工作范围一般在8MHz到24MHz之间推荐用12MHz。STM32F103的PA8引脚可以复用为MCO输出直接输出系统时钟源。具体配置方式需要区分板载晶振频率。如果开发板用的是8MHz晶振MCO可以配置为直接输出HSE也就是8MHz如果用的是12MHz晶振MCO输出就是12MHz。OV7670对8MHz时钟也能正常出图但内部帧率会比12MHz时低一些实测大概在QVGA分辨率下差3到5fps。如果不想折腾PLL倍频直接MCO输出当前晶振频率即可教程示例也按这个最省事的方案来写。MCO配置代码很简单标准库下就在RCC初始化里加一段void XCLK_Config(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_HSE); // 板载晶振为8MHz时输出8MHz }这里有一个很多人踩过的坑MCO输出在芯片上电后默认是关闭的必须调用RCC_MCOConfig之后才能对外输出时钟。有人把MCO引脚配置成普通推挽输出去翻转电平模拟时钟这种做法波形毛刺大实测会导致画面水波纹不建议用在摄像头这种对时钟敏感的器件上。3.2 模拟I2C的SCCB时序与ACK检查OV7670的寄存器配置走的是SCCB协议它和I2C很接近但不完全一样。SCCB的器件地址是0x42写、0x43读支持单字节读写和连续读写。对F103来说有两种实现方式用硬件I2C外设或者GPIO模拟。硬件I2C在F103上有不少已知问题而且SCCB的时序和标准I2C有些差异所以我更推荐GPIO模拟代码量不大出错更容易排查。写寄存器时序是起始位、器件写地址、ACK、寄存器地址、ACK、数据、ACK、停止位。读寄存器时序略复杂要先发送寄存器地址然后重新发起起始位切换为读地址读取数据主机回NACK停止。完整实现如下#define SCCB_SCL_H GPIOB-BSRR GPIO_Pin_10 #define SCCB_SCL_L GPIOB-BRR GPIO_Pin_10 #define SCCB_SDA_H GPIOB-BSRR GPIO_Pin_11 #define SCCB_SDA_L GPIOB-BRR GPIO_Pin_11 #define SCCB_SDA_READ (GPIOB-IDR GPIO_Pin_11) static void SCCB_Delay(void) { uint8_t i 4; while (i--); } static void SCCB_Start(void) { SCCB_SDA_H; SCCB_SCL_H; SCCB_Delay(); SCCB_SDA_L; SCCB_Delay(); SCCB_SCL_L; SCCB_Delay(); } static void SCCB_Stop(void) { SCCB_SDA_L; SCCB_SCL_L; SCCB_Delay(); SCCB_SCL_H; SCCB_Delay(); SCCB_SDA_H; SCCB_Delay(); } static uint8_t SCCB_WriteByte(uint8_t dat) { uint8_t i; for (i 0; i 8; i) { if (dat 0x80) SCCB_SDA_H; else SCCB_SDA_L; dat 1; SCCB_SCL_H; SCCB_Delay(); SCCB_SCL_L; SCCB_Delay(); } // 接收ACK GPIOB-CRH 0xFFFF0FFF; GPIOB-CRH | 0x00008000; // PB11设为上拉输入 SCCB_SDA_H; SCCB_Delay(); SCCB_SCL_H; SCCB_Delay(); i SCCB_SDA_READ; SCCB_SCL_L; SCCB_Delay(); GPIOB-CRH 0xFFFF0FFF; GPIOB-CRH | 0x00003000; // PB11恢复推挽输出 return i ? 1 : 0; // 返回0表示有ACK } static void SCCB_ReadByte(uint8_t *dat) { uint8_t i; GPIOB-CRH 0xFFFF0FFF; GPIOB-CRH | 0x00008000; // PB11设为上拉输入 *dat 0; for (i 0; i 8; i) { SCCB_SCL_H; SCCB_Delay(); *dat 1; if (SCCB_SDA_READ) (*dat); SCCB_SCL_L; SCCB_Delay(); } // 主机回NACK GPIOB-CRH 0xFFFF0FFF; GPIOB-CRH | 0x00003000; // PB11恢复推挽输出 SCCB_SDA_H; SCCB_SCL_H; SCCB_Delay(); SCCB_SCL_L; }时序上有一个细节SCCB_SCL高电平期间SDA必须保持稳定只能在SCL低电平期间改变SDA这和I2C的规则一致。上面的实现把每次操作都夹在SCL低到高的跳变之间可以保证不违反这个约束。初始化完成后最值得做的一件事是回读传感器ID。OV7670的PID寄存器地址是0x0AVER寄存器地址是0x0B正常读取应该得到0x76和0x73。如果读不到说明SCCB链路没有打通这时候去查代码没有任何意义老老实实用万用表量一下SCL和SDA的电压正常空闲时应该都是3.3V上拉。3.3 关键寄存器配置与一组可用的初始化序列OV7670的寄存器数量和内部状态机比一般传感器复杂但驱动一个基本出图的摄像头真正关键的寄存器只有几个。0x12是COM7控制输出格式和分辨率bit5写1进入RGB模式bit4写1选择QVGA0x11是CLKRC控制内部时钟分频写0x01表示外部时钟不分频直接作为系统时钟0x40是COM15bit6到bit4控制RGB输出格式0xD0代表RGB5650x3D是COM13bit3要置1才能正确输出RGB格式。窗口配置和缩放参数也是容易出问题的点。QVGA模式要求传感器从VGA全窗口采集后缩放到320x240这个过程由缩放寄存器和窗口寄存器配合完成。初始化序列里0x17、0x18、0x19、0x1A分别对应HSTART、HSTOP、VSTART、VSTOP这些值如果和缩放寄存器不匹配出图就会出现画面偏移或者边缘有黑边。这里给出一组实测可以在大部分OV7670带FIFO模块上直接出图的初始化数组typedef struct { uint8_t reg; uint8_t val; } ov7670_reg_t; const ov7670_reg_t ov7670_regs[] { {0x12, 0x80}, // COM7复位寄存器组 {0x12, 0x10}, // COM7QVGA、RGB模式 {0x11, 0x01}, // CLKRC外部时钟不分频 {0x40, 0xD0}, // COM15RGB565 {0x3D, 0x08}, // COM13RGB {0x3A, 0x04}, // TSLB {0x3B, 0x01}, // COM11 {0x3C, 0x0F}, // COM12 {0x0C, 0x00}, // COM3 {0x0D, 0x00}, // COM4 {0x0E, 0x00}, // COM5 {0x17, 0x13}, // HSTART {0x18, 0x01}, // HSTOP {0x19, 0x02}, // VSTART {0x1A, 0x7B}, // VSTOP {0x32, 0xB6}, // HREF {0x2A, 0x00}, // 缩放因子高字节 {0x2B, 0x00}, {0x2C, 0x00}, {0x2D, 0x00}, {0x6B, 0x0A}, // 自动增益控制上限 {0x6F, 0x0A}, // 自动增益控制下限 {0x70, 0x0A}, // 自动白平衡 {0x71, 0x0A}, {0x13, 0xFF}, // COM8开启自动增益、自动曝光、自动白平衡 {0x14, 0x1E}, // COM9增益倍数 }; void OV7670_Init(void) { uint8_t pid, ver; // 硬件复位 GPIO_ResetBits(GPIOA, GPIO_Pin_4); delay_ms(10); GPIO_SetBits(GPIOA, GPIO_Pin_4); delay_ms(150); for (uint16_t i 0; i sizeof(ov7670_regs) / sizeof(ov7670_reg_t); i) { OV7670_WriteReg(ov7670_regs[i].reg, ov7670_regs[i].val); } OV7670_ReadReg(0x0A, pid); OV7670_ReadReg(0x0B, ver); // pid应为0x76ver应为0x73 }初始化复位后必须等待足够的时间一般150ms以上。有些模块板载稳压器和晶振起振比较慢时间给太短会导致SCCB写寄存器时内部时钟还没稳定部分寄存器写不进去出图就会出现颜色偏绿这种诡异现象。4. 图像读取逻辑FIFO时序、DMA搬运与缓存设计4.1 一帧图像在FIFO里是怎么存的AL422B本质上是一个环形缓存写入侧由模块板载逻辑控制OV7670的PCLK和HREF会驱动WEN和WE引脚把有效的像素数据连续写入。而MCU读一侧完全独立通过RRST复位读指针、RD产生读时钟、OE打开输出把FIFO里的数据按地址顺序依次读出来。OV7670在RGB565模式下每个像素占两个字节FIFO里存储的顺序和传感器输出的顺序一致先输出的数据存在低地址后输出的存在高地址。QVGA分辨率下一帧是320x24076800个像素也就是153600字节。ST星形F103的RAM通常是20KB到192KB不等即使是F103ZET6的192KB也不能同时存放多帧数据所以实时显示方案很少在RAM里整帧缓存而是读一个像素、显示一个像素FIFO作为唯一的帧缓冲。4.2 读FIFO的完整时序与代码读一帧图像的标准流程是等VSYNC下降沿触发在中断里把RRST拉低再拉高完成读指针复位然后把OE拉低等待读取主循环读到一帧结束后把OE拉高等待下一帧VSYNC。VSYNC中断处理void EXTI0_IRQHandler(void) // 这里示例用的是PA0实际按你的引脚调整 { if (EXTI_GetITStatus(EXTI_Line0) ! RESET) { frame_ready 1; // 复位FIFO读指针 GPIO_ResetBits(GPIOA, GPIO_Pin_2); // RRST 低 delay_us(1); GPIO_SetBits(GPIOA, GPIO_Pin_2); // RRST 高 // 打开FIFO输出 GPIO_ResetBits(GPIOA, GPIO_Pin_1); // OE 低 EXTI_ClearITPendingBit(EXTI_Line0); } }主循环里的像素读取uint8_t OV7670_ReadFIFOByte(void) { uint8_t data; GPIO_ResetBits(GPIOA, GPIO_Pin_0); // RD 低数据输出到D0-D7 delay_us(1); // 等待数据稳定 data GPIO_ReadInputData(GPIOC) 0xFF; // 读取D0-D7 GPIO_SetBits(GPIOA, GPIO_Pin_0); // RD 高完成一次读取 return data; } void OV7670_ReadFrameToLCD(void) { for (uint16_t y 0; y 240; y) { for (uint16_t x 0; x 320; x) { uint8_t h OV7670_ReadFIFOByte(); // 高字节 uint8_t l OV7670_ReadFIFOByte(); // 低字节 uint16_t color (h 8) | l; LCD_WritePixel(color); } } // 关闭FIFO输出 GPIO_SetBits(GPIOA, GPIO_Pin_1); // OE 高 }这里有一个时序细节很容易被忽略RRST复位完后不能立刻开始读AL422B的内部逻辑需要一点时间来把读指针归位到起始地址。延时1微秒足够这个时间可以让模块内部的地址计数逻辑稳定下来。如果复位后直接开始读取最开始的几个像素可能是乱码画面上方会出现一条窄窄的彩色花边。读FIFO时GPIO速度和循环开销直接决定帧率。上面这段代码每个字节包含一次GPIO拉低、一次等待、一次IDR读取、一次GPIO拉高在72MHz主频下实测大约需要150到200ns一个像素两次读操作约300到400ns一帧76800个像素约30ms加上LCD写入时间最终帧率在10到15fps之间作为实时预览完全够用。4.3 颜色字节序RGB565的高低字节问题RGB565模式下OV7670输出的每个像素是16位高5位红色、中间6位绿色、低5位蓝色。在FIFO里的存储顺序是第一个字节是高字节R4-R0和G5-G3第二个字节是低字节G2-G0和B4-B0。代码里先读高字节后读低字节组合成(h 8) | l。这个顺序如果反了画面不会花而是颜色会明显错乱比如红色和蓝色互换、整个画面像底片一样。我遇到过一批模块厂商在PCB走线上把D0-D7的顺序接反了导致读取字节序完全反了过来现象就是颜色极其诡异最后交换高低字节后恢复正常。所以建议在调试阶段就做一个简单测试把画面颜色数据累加统计红色通道数值偏高且蓝色通道偏低大概率是字节序反了。当然最直接的办法还是把(h 8) | l改成(l 8) | h试一次颜色正常就说明模块的硬件走线确实反了。4.4 用DMA和FSMC提速的思路上面GPIO逐字节读取的方式虽然能用但CPU开销比较大。如果要把帧率再往上提有两种成熟思路。第一种是把FIFO挂在FSMC总线上利用FSMC的时序模拟RD信号。这个思路需要把AL422B的RD接到FSMC的NOE、OE接读使能、D0-D7挂到FSMC数据总线低8位。由于FIFO没有地址线FSMC的每次读访问都落在同一个地址上CPU或DMA连续读这个地址就能持续取数。这套配置可以在不占用CPU的情况下让DMA把数据直接从FIFO搬运到LCD或内存。难度在于FSMC时序和AL422B的RD时序不一定完全匹配需要反复调FSMC的扩展模式参数。第二种是利用DMA的存储器到存储器传输。先把FIFO数据读到一块SRAM缓冲区再用DMA把这块缓冲区内容搬运到LCD的FSMC地址。但前面也说了从GPIO读数据无法用DMA完成所以提速效果有限真正的瓶颈还是在GPIO读取。对新手来说先用GPIO逐字节读通整个链路是最稳的做法。等图像正常显示后再考虑FSMC映射方案去优化性能一步到位反而会引入太多变量出问题时分不清是硬件还是时序配置的问题。5. LCD显示与帧率实测5.1 为什么推荐并口屏FSMC方案OV7670带FIFO模块输出的是16位RGB565颜色数据LCD屏幕也需要接收RGB565格式两者之间最自然的对接方式就是并口。并口屏配合FSMC外设MCU把像素数据写入一个固定地址FSMC硬件自动产生片选、读/写、数据建立等时序写入一个像素只需要一条C语句速度非常快。SPI串口屏虽然接线少但数据是一位一位传的。假设SPI时钟18MHz传输一个16位像素需要约0.9微秒一帧76800个像素就是69毫秒也就是说光刷新一帧画面就需要接近70毫秒再加上读取FIFO的时间帧率很难超过8fps。并口屏在FSMC下写一个像素大约100到200纳秒一帧画4096像素写入只要十几毫秒差距很大。5.2 FSMC初始化与显示循环FSMC初始化主要配三部分GPIO复用、FSMC时序、片选地址。这里以2.4寸ILI9341并口屏为例命令/数据选择线使用A6引脚片选使用NE1因此命令地址是0x60000000数据地址是0x60000000 (1 6)也就是0x60000040。void LCD_FSMC_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; FSMC_NORSRAMInitTypeDef FSMC_NORSRAMInitStructure; FSMC_NORSRAMTimingInitTypeDef readWriteTiming; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_FSMC, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD | RCC_APB2Periph_GPIOE | RCC_APB2Periph_AFIO, ENABLE); // 配置FSMC数据线D0-D15、控制线NOE、NWE、NE1、A6复用推挽输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; // D0-D3 - PD14-PD15, PE7-PE9 等按实际接线配置 // 这里以常见最小系统板并口屏为例需要根据PCB实际走线补全 readWriteTiming.FSMC_AddressSetupTime 1; readWriteTiming.FSMC_AddressHoldTime 0; readWriteTiming.FSMC_DataSetupTime 2; readWriteTiming.FSMC_BusTurnAroundDuration 0; readWriteTiming.FSMC_CLKDivision 0; readWriteTiming.FSMC_DataLatency 0; readWriteTiming.FSMC_AccessMode FSMC_AccessMode_A; 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_16b; FSMC_NORSRAMInitStructure.FSMC_BurstAccessMode FSMC_BurstAccessMode_Disable; FSMC_NORSRAMInitStructure.FSMC_AsynchronousWait FSMC_AsynchronousWait_Disable; FSMC_NORSRAMInitStructure.FSMC_WaitSignalPolarity FSMC_WaitSignalPolarity_Low; FSMC_NORSRAMInitStructure.FSMC_WrapMode FSMC_WrapMode_Disable; FSMC_NORSRAMInitStructure.FSMC_WaitSignalActive FSMC_WaitSignalActive_BeforeWaitState; FSMC_NORSRAMInitStructure.FSMC_WriteOperation FSMC_WriteOperation_Enable; FSMC_NORSRAMInitStructure.FSMC_MemoryAddress 0x60000000; FSMC_NORSRAMInitStructure.FSMC_WriteBurst FSMC_WriteBurst_Disable; FSMC_NORSRAMInitStructure.FSMC_WaitSignal FSMC_WaitSignal_Disable; FSMC_NORSRAMInitStructure.FSMC_ExtendedMode FSMC_ExtendedMode_Disable; FSMC_NORSRAMInitStructure.FSMC_ReadWriteTimingStruct readWriteTiming; FSMC_NORSRAMInitStructure.FSMC_WriteTimingStruct readWriteTiming; FSMC_NORSRAMInit(FSMC_NORSRAMInitStructure); FSMC_NORSRAMCmd(FSMC_Bank1_NORSRAM1, ENABLE); } #define LCD_REG (*(volatile uint16_t *)0x60000000) #define LCD_RAM (*(volatile uint16_t *)0x60000040) void LCD_WriteCmd(uint16_t cmd) { LCD_REG cmd; } void LCD_WriteData(uint16_t data) { LCD_RAM data; }显示循环就是不停地在FIFO和LCD之间搬运数据。每次VSYNC中断把frame_ready置1主循环检测到后先通过LCD命令设置窗口为全屏然后连续调用读像素和写像素函数。这样做的优点是读取和显示天然同步中间不需要额外的RAM缓冲。实际使用中要注意LCD初始化必须先于摄像头初始化完成。如果LCD还没配置好就开始读FIFO读取的数据无处可去帧中断又不断触发会导致LCD初始化时被摄像头中断频繁打断屏幕初始化不完整出现半边白半边黑的现象。5.3 QVGA实测帧率与流畅度在8MHz XCLK、QVGA RGB565配置下用GPIO逐字节读FIFO FSMC写LCD的实测结果大约是12fps整体流畅度可以满足大部分预览场景。如果做以下优化帧率还能进一步提升优化手段预期帧率提升实现复杂度提高XCLK到12MHz2fps左右低改MCO配置使用FSMC映射FIFO用DMA搬运5fps左右高需要硬件改线关闭LCD窗口设置使用连续写模式1fps左右低改显示循环优化读字节函数去掉延时2fps左右低减少delay这个帧率已经把CPU基本榨干了如果还想做更复杂的图像处理建议要么换更高主频的MCU要么用DMA把整帧数据搬走后再处理。F103方案更适合做图像采集和显示这类简单任务。6. 排错手册黑屏、花屏、偏色和图像撕裂的处理6.1 黑屏从电源、时钟到SCCB的三层排查黑屏是占比最高的故障但黑屏的原因往往不在代码而在硬件链路。按照我的排查习惯顺序是电源、时钟、通信每一层都有明确的检查方法。第一层电源。用万用表量摄像头模块的VCC和GND之间电压正常应该在3.2V到3.4V之间。如果低于3.0V检查USB线压降、开发板LDO是否过热。常见坑是用了面包板供电面包板内部簧片老化导致接触电阻大摄像头一启动电流上去电压就跌落。第二层时钟。用示波器或逻辑分析仪量PA8引脚应该有持续的方波输出。如果PA8无波形检查MCO配置是否执行了RCC_MCOConfig以及GPIO是否配置为复用推挽。另一种情况是开发板晶振没起振这时可以用示波器直接量晶振脚正常会有波形。第三层SCCB通信。黑屏最常见的原因是SCCB初始化没有成功摄像头内部寄存器没被正确写入。判断方法是在初始化代码里回读PID和VER打印出来。如果读出来是0xFF或0x00说明SDA线没通或者模块没有正常上电。SCL和SDA线上建议各加一个4.7k欧上拉电阻到3.3V有的模块板载上拉有的没有加了更保险。6.2 花屏FIFO指针与缩放配置的坑花屏分很多种每种对应的原因完全不同。如果画面整片花但能隐约看出是图像内容大概率是FIFO读指针复位时序不对。RRST拉低再拉高后一定要给一点延时等地址计数器稳定。如果复位后立刻开始读第一个数据可能落在FIFO中间位置整帧就都错位了。如果画面是规律性的斜条纹而且是多个方向交叉问题多半出在窗口寄存器或缩放寄存器配置上。OV7670的默认输出是VGA全窗口如果寄存器里设置的分辨率是QVGA但窗口参数没有配对传感器输出行数和FIFO里期待的行数不匹配就会出现斜条纹。解决方法是恢复一组确认可用的初始化序列不要自己乱改窗口寄存器值。如果花屏但颜色看起来正常只有位置错乱可能是读出的行数少了或多了。检查主循环里的行列计数是否和实际分辨率一致多读或少读一行都会导致后续所有行错位。6.3 颜色异常与镜像/翻转问题颜色偏红或偏蓝优先检查RGB565的字节序。把(h 8) | l改成(l 8) | h如果颜色恢复正常就说明模块的D0-D7走线顺序反了或者FIFO存储顺序和预期不同。这个改动只影响颜色通道的排列顺序不影响图像结构。画面整体偏绿通常不是字节序问题而是RGB输出格式配错。OV7670有RGB565、RGB555、RGB444几种格式如果寄存器配置成了RGB555绿色通道多占一位画面就会明显偏绿同时红色和蓝色通道的深度不足图像对比度会显得很怪。重点检查0x40寄存器是否配成0xD0。画面上下颠倒或左右镜像摄像头安装方向和传感器扫描方向不一致就会这样。0x1E寄存器MVFP的bit4控制水平镜像bit5控制垂直翻转按需修改即可。镜头朝下安装时一般建议同时开启水平和垂直翻转保证显示器上看到的是正像。6.4 图像撕裂读帧与显示帧不同步的解法撕裂现象表现为画面上有一条清晰的分界线分界线一侧是当前帧内容另一侧是上一帧内容看起来像画面被错位拼接了。根本原因是MCU读取FIFO的速度太慢还没读完当前帧OV7670已经开始写入下一帧下一帧数据把FIFO里尚未读完的区域覆盖了。解决撕裂第一个思路是提高读取速度。把读字节函数里的延时去掉或者用FSMC映射FIFO配合DMA读取都能有效压缩读帧时间。第二个思路是降低摄像头输出帧率在CLKRC寄存器里做分频让每帧数据写入FIFO的时间拉长给MCU留出更充裕的读取窗口。第三个思路是改用双缓冲但这需要额外的一片大RAMF103内部资源有限实际做起来不划算。阻断式读取是另一个有效手段。在VSYNC中断里检测到前一帧还没读完就直接丢弃这一帧等下一帧再读。这样虽然帧率会小幅下降但每一帧显示都是完整的观感上比撕裂好很多。实现上只需要在中断处理函数里用一个标志判断当前是否处于读取状态如果正在读取则置一个overrun标志主循环里读取完一帧后检查这个标志决定是否立即处理下一帧。撕裂问题在QVGA下如果读取优化得当基本不会出现但如果硬要在VGA模式下跑这个坑就非常深了FIFO容量和读取速度都捉襟见肘不建议在F103上挑战VGA实时显示。这套OV7670F103方案跑通之后后续的玩法其实很多。我最近就在尝试在读取FIFO的过程中顺便做一帧灰度直方图统计用来做环境光检测完全不需要额外硬件只靠CPU在搬数据时顺带计算就能实现。你也可以在读取循环里加一个人脸框或者做运动检测只要图像数据经过CPU就总有办法榨出点额外的信息。这个项目虽然看起来只是“点亮一个摄像头”但把FIFO时序、FSMC、SCCB这些基础模块全部串了一遍对有志于做嵌入式视觉方向的朋友来说是个性价比极高的练手项目。