简介:这份资源面向使用 STM32F1 系列驱动 OV7670 摄像头、又不打算外接 FIFO 的嵌入式学习者与开发者,解决的是无 FIFO 方案缺少可参考工程、网上难以找到完整源码的问题。作者在自行摸索后整理出可运行的工程,并在基础采集之外补充了实用的图像处理函数,可实现亮度、饱和度与对比度调节,按下 KEY1 即可完成灰度化与二值化转换,同时预留了颜色识别接口,便于在循迹、色块识别、简单机器视觉等场景中继续扩展。压缩包共 312 个文件,约 10.8MB,包含 71 个 c 源文件与 63 个 h 头文件、Keil 工程文件(uvprojx、uvoptx)与编译中间产物,以及 hex、axf、map 等固件与链接结果,另附若干 png 截图和 txt 说明,方便对照调试。资源已有 4561 人学习,适合具备一定 C 语言与 STM32 基础、希望快速跑通无 FIFO 摄像头采集与基础图像处理的读者参考。
1. 一块 F103 加一个十几块的摄像头:这个无 FIFO 工程到底解决了什么
很多人第一次把 OV7670 无 FIFO 版本接到 STM32F1 上,遇到的第一件事并不是成功读到图像,SCCB 能读回 ID、串口也能打印寄存器值,但屏幕上始终只有雪花或一条条彩带。原因不复杂:无 FIFO 的模块不会帮你缓存数据,每个 PCLK 周期它就把 8 位数据推到总线上,下一个周期立刻换掉,MCU 哪怕慢半个微秒,这个像素也永久丢了。这跟你用 SPI 读个温湿度传感器完全是两码事。
这个工程就是围绕这个限制搭建的一套完整实现:TIM1 输出 8MHz 的 XCLK 给摄像头当主时钟,GPIO 模拟 SCCB 把寄存器表刷进去,PA0~PA7 一次性读走 8 位数据,再用 VSYNC、HREF、PCLK 三个外部中断组成一个三级状态机,把字节流拼成帧缓存。它适合已经会配置 F1 的 GPIO、中断和定时器,想把摄像头真正接进自己板子上的开发者;如果你只是想找个能立刻出图的傻瓜模块,带 FIFO 的方案更省事,但你会失去对时序的控制权。
2. 硬件连线与时钟:让 OV7670 先输出一个像素
2.1 电源、复位与 PWDN 这三个“不起眼但要命”的脚
OV7670 模组本身是 2.8V 核心器件,市面上的小板大多板载了一颗 LDO,所以你给 3.3V 进去是没问题的,但要留意两件事。
第一是 PWDN 和 RESET。PWDN 是高电平进入掉电模式,很多人按“悬空即默认”的习惯不接,结果模块时好时坏,本质是引脚浮空被干扰拉高了。稳妥的做法是直接飞线到 GND,把它钉死在唤醒状态。RESET 是低电平复位,接一个 10k 上拉到 3.3V,或者干脆接 MCU 的一个 GPIO 由程序统一拉高,这样上电顺序可控。
第二是电平匹配。OV7670 的输出高电平按 2.8V 算,F1 在 3.3V 供电下 VIH 大约 2.31V,理论上能识别,但余量不大。实际布线里如果数据线拉得长、旁边又有 PWM 走线,采样错位就是这么来的。模组板上一般串了限流电阻,问题不大;如果是自己画的板,建议数据线上各串 33Ω 到 100Ω。
提示:带 FIFO 的 OV7670 小板和不带 FIFO 的板子外形接近、排针定义不同。带 FIFO 的板你从 D0~D7 读到的是 AL422B 吐出来的缓存数据,不是摄像头的实时输出。插错板子以后,时序怎么调都不对。
2.2 8 位数据口与 VSYNC/HREF/PCLK 的引脚分配
引脚分配有一套讲究,核心原则是:数据口必须在同一个 GPIO 端口上连续排列,这样一条GPIO_ReadInputData()就能把 8 位一次取回,省掉逐位拼装的几十个时钟周期。中断采样里这点时间很关键,慢一拍数据就没了。
工程里的分配如下:
| 信号 | 引脚 | 方向 | 说明 |
|---|---|---|---|
| D0~D7 | PA0~PA7 | 输入 | 一次读回 8 位,浮空输入 |
| PCLK | PB0 | 输入 | 像素时钟,EXTI0 下降沿触发 |
| HREF | PB1 | 输入 | 行有效,EXTI1 双边沿 |
| VSYNC | PB2 | 输入 | 帧同步,EXTI2 上升沿 |
| SIOC | PB10 | 输出 | SCCB 时钟 |
| SIOD | PB11 | 双向 | SCCB 数据 |
| XCLK | PA8 | 复用推挽 | TIM1_CH1 输出 8MHz |
| PWDN | GND | — | 钉死在工作模式 |
| RESET | 3.3V | — | 上拉,不复位 |
这里有个绕不开的代价:数据口占了 PA0~PA7,等于把 EXTI0~EXTI7 这八条外部中断线全占了。而 EXTI 线号是按引脚号映射的,PA0 和 PB0 共用 EXTI0,同一时刻只能选一个。所以三个同步信号只能全部挪到 B 口,用 EXTI0/1/2 分别对应 PB0/PB1/PB2。这个映射关系是在 AFIO 的GPIO_EXTILineConfig()里指定的,配错的话中断永远不会进。
数据口的初始化没有花头,浮空输入即可,因为 OV7670 是强推挽输出,不需要 MCU 这边提供上拉:
static void CameraDataPort_Init(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); gpio.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING; /* 摄像头侧强驱动,浮空即可 */ gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); }GPIO_Mode_IN_FLOATING是关键参数。如果误配成GPIO_Mode_IPU,内部上拉会和摄像头的输出形成分压,暗像素(数据 0x00)会被拉成 0x03 之类的怪值,画面表现为整体泛白、暗部细节全丢,这种问题查起来非常费时间。
2.3 用 TIM1 PWM 给 XCLK 喂一个 8MHz 主时钟
OV7670 没有内部振荡器,XCLK 不给它,它一个像素都不会输出。这个时钟可以直接用定时器的 PWM 通道产生,占空比固定 50%,频率决定了 PCLK 的上限(内部还有分频寄存器可以再降)。
F103 的 TIM1 挂在 APB2 上,72MHz 主频下计数时钟就是 72MHz。取 PSC=0、ARR=8,输出频率为 72MHz / (0+1) / (8+1) = 8MHz:
static void CameraXclk_Init(void) { GPIO_InitTypeDef gpio; TIM_TimeBaseInitTypeDef tb; TIM_OCInitTypeDef oc; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_TIM1, ENABLE); gpio.GPIO_Pin = GPIO_Pin_8; /* TIM1_CH1 */ gpio.GPIO_Mode = GPIO_Mode_AF_PP; /* 复用推挽,由定时器直接驱动 */ gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); TIM_TimeBaseStructInit(&tb); tb.TIM_Prescaler = 0; /* 72MHz 不分频 */ tb.TIM_Period = 8; /* 72 / (8+1) = 8MHz */ tb.TIM_ClockDivision = TIM_CKD_DIV1; tb.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM1, &tb); TIM_OCStructInit(&oc); oc.TIM_OCMode = TIM_OCMode_PWM1; oc.TIM_OutputState = TIM_OutputState_Enable; oc.TIM_OCPolarity = TIM_OCPolarity_High; oc.TIM_Pulse = 4; /* 4/8,占空比 50% */ TIM_OC1Init(TIM1, &oc); TIM_CtrlPWMOutputs(TIM1, ENABLE); /* 高级定时器必须开这一句 */ TIM_Cmd(TIM1, ENABLE); }TIM_CtrlPWMOutputs()是 TIM1/TIM8 这类高级定时器特有的,普通定时器没有这个调用,漏掉它的话引脚上就是一条直线,示波器一测就知道。
频率怎么定?8MHz 的 XCLK 经过内部寄存器分频后,PCLK 大概落在 4MHz 上下,也就是每秒 400 万个字节。QVGA 的 RGB565 一帧是 320×240×2 = 153600 字节,理论上限约 26 fps,但中断方式采样每字节要几十个时钟周期,实际能到 1~3 fps 就不错了。如果你把 XCLK 提到 24MHz,PCLK 跟着涨,采到的只会是错位数据。降压比升压有用,这是这套方案的铁律。
3. SCCB 配置链路:把 OV7670 从复位态叫到 RGB565 输出
3.1 软件模拟 SCCB:为什么硬件 I2C 在这里容易翻车
SCCB 是 OV 系列自己的控制总线,电气上跟 I2C 几乎一样,但协议细节有差异:它不支持重复起始条件(Repeated Start),从机在第 9 位返回的是“don't care”,主机侧不需要判 ACK/NACK。
STM32 的硬件 I2C 外设是按标准 I2C 状态机设计的,碰上不返回 ACK 的从机,很容易进 BUSY 卡死或者触发 AF 错误,得靠反复 DeInit 才能恢复。所以工程里直接用两个 GPIO 手打时序,稳定得多,也方便在示波器上看波形。
#define SCCB_SCL_H() GPIO_SetBits(GPIOB, GPIO_Pin_10) #define SCCB_SCL_L() GPIO_ResetBits(GPIOB, GPIO_Pin_10) #define SCCB_SDA_H() GPIO_SetBits(GPIOB, GPIO_Pin_11) #define SCCB_SDA_L() GPIO_ResetBits(GPIOB, GPIO_Pin_11) #define SCCB_SDA_IN() GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) #define OV7670_ADDR_W 0x42 /* 0x21 << 1,写地址 */ static void SCCB_Delay(void) { /* 72MHz 下若干空操作,实测拉出约 200kHz 的 SCL */ __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); } static void SCCB_Start(void) { SCCB_SDA_H(); SCCB_SCL_H(); SCCB_Delay(); SCCB_SDA_L(); SCCB_Delay(); /* SCL 高电平期间 SDA 下降 = 起始 */ SCCB_SCL_L(); SCCB_Delay(); } static void SCCB_Stop(void) { SCCB_SDA_L(); SCCB_SCL_H(); SCCB_Delay(); SCCB_SDA_H(); SCCB_Delay(); /* SCL 高电平期间 SDA 上升 = 停止 */ } static uint8_t SCCB_WriteByte(uint8_t dat) { uint8_t i, ack; for (i = 0; i < 8; i++) { if (dat & 0x80) SCCB_SDA_H(); else SCCB_SDA_L(); dat <<= 1; SCCB_Delay(); SCCB_SCL_H(); SCCB_Delay(); SCCB_SCL_L(); SCCB_Delay(); } SCCB_SDA_H(); /* 释放 SDA,让从机驱动第 9 位 */ SCCB_Delay(); SCCB_SCL_H(); SCCB_Delay(); ack = SCCB_SDA_IN(); /* SCCB 里这一位是 don't care,读回来仅作参考 */ SCCB_SCL_L(); SCCB_Delay(); return ack; } void OV7670_WriteReg(uint8_t reg, uint8_t val) { SCCB_Start(); SCCB_WriteByte(OV7670_ADDR_W); SCCB_WriteByte(reg); SCCB_WriteByte(val); SCCB_Stop(); Delay_ms(1); /* 给寄存器一个生效窗口,批量写时尤其重要 */ }几个参数值得单独说。OV7670_ADDR_W是 0x42,也就是 7 位地址 0x21 左移一位。写寄存器时千万别把 0x21 直接填进去,那样从机根本不会响应,读回全是 0xFF。末尾的Delay_ms(1)看着多余,但连续写几十个寄存器时,某些位(特别是时钟控制和复位位)需要时间落地,省掉它就会出现“前面写的被后面覆盖”的怪现象。
3.2 初始化寄存器表的组织方式
寄存器有上百个,逐个调函数写会非常难看,工程里用结构体数组加一个循环搞定:
typedef struct { uint8_t reg; uint8_t val; } ov7670_reg_t; /* 只列关键项,完整表按数据手册补 */ static const ov7670_reg_t ov7670_init_seq[] = { {0x12, 0x80}, /* COM7:软复位所有寄存器 */ {0x12, 0x04}, /* COM7:输出 RGB(VGA 基准) */ {0x11, 0x80}, /* CLKRC:内部时钟不分频 */ {0x0C, 0x00}, /* COM3:关闭缩放,保留全分辨率 */ {0x3A, 0x04}, /* TSLB:RGB 模式下字节顺序 */ {0x40, 0xD0}, /* COM15:RGB565 输出范围 */ {0x8C, 0x00}, /* RGB444 关闭,保持 565 */ {0x15, 0x00}, /* COM10:PCLK/VSYNC/HREF 默认极性 */ {0x1E, 0x01}, /* MVFP:垂直翻转关闭 */ }; void OV7670_Init(void) { uint16_t i; for (i = 0; i < sizeof(ov7670_init_seq) / sizeof(ov7670_init_seq[0]); i++) { OV7670_WriteReg(ov7670_init_seq[i].reg, ov7670_init_seq[i].val); } }COM10(0x15)这一项要重点盯。它同时控制 PCLK 极性、VSYNC 极性、HREF 极性三个位,默认 0x00 表示“PCLK 上升沿有效、VSYNC 高有效、HREF 高有效”。如果你的中断按下降沿配,而这里是 0x00,那采到的就是移位后的数据,画面表现为整体偏色加斜纹。极性必须和中断配置一一对上,这是排查时第一个要看的地方。
COM15(0x40)设 0xD0 是 RGB565 的标准值,设成 0x10 会变成 RGB444,虽然也能出图,但颜色会严重失真,而且每像素字节数变了,帧缓存的偏移全要跟着改。
3.3 写完必须读回:PID/VER 校验与失败处理
配置完不能直接开始采图,先读两个身份寄存器确认通信链路是通的:PID(0x0A)应该返回 0x76,VER(0x0B)返回 0x73。
uint8_t OV7670_ReadReg(uint8_t reg) { uint8_t val; SCCB_Start(); SCCB_WriteByte(OV7670_ADDR_W); SCCB_WriteByte(reg); SCCB_Stop(); /* SCCB 不允许重复起始,必须停一次再起 */ Delay_us(10); SCCB_Start(); SCCB_WriteByte(OV7670_ADDR_W | 0x01); /* 读地址 0x43 */ val = SCCB_ReadByte(); SCCB_Stop(); return val; } uint8_t OV7670_CheckID(void) { if (OV7670_ReadReg(0x0A) != 0x76) return 1; /* PID 不对,接线或地址有问题 */ if (OV7670_ReadReg(0x0B) != 0x73) return 2; /* VER 不对,供电或复位有问题 */ return 0; }读时序里那个SCCB_Stop()是 SCCB 和标准 I2C 最大的一处差异。I2C 习惯用重复起始把“写寄存器地址”和“读数据”连在一起,SCCB 不行,必须先发停止位、总线空闲,再重新发起始位。如果照抄 I2C 的读时序,从机不会吐出数据,读回来永远是 0xFF 或者 0x00。
如果 PID 读出来是 0xFF,先量 SIOD 上有没有上拉电阻——很多模组板自带 4.7k,裸芯片没有。如果读到 0x00,通常是 SDA 被硬件拉死,检查是不是把 SIOC/SIOD 接反了。这两种情况在第一批板子上我各遇到过一次。
4. 无 FIFO 采样的时序状态机:VSYNC、HREF、PCLK 三级怎么配合
4.1 一帧数据的结构:从 VSYNC 脉冲到最后一个 PCLK
要采样,先得看懂 OV7670 输出一帧的节奏。
一帧开始时,VSYNC 会打一个脉冲,宽度几十个 PCLK。脉冲之后进入若干行的消隐期,这段时间 HREF 是低的,数据总线上有电平但不代表有效像素,必须丢掉。然后 HREF 拉高,表示一行有效数据开始,接下来每来一个 PCLK,总线上就换一个字节。一行采完 HREF 拉低,进入行消隐,接着下一行的 HREF 又拉高。重复 480 次(VGA)之后,VSYNC 再来一个脉冲,下一帧开始。
所以三个信号的分工非常清晰:VSYNC 负责“帧对齐”,HREF 负责“行对齐”,PCLK 负责“字节对齐”。三个中断各管一级,构成一个天然的三层状态机。
RGB565 模式下每个像素占两个 PCLK、两个字节,先高字节后低字节。灰度模式(YUV 里的 Y 分量,或者直接配成灰度输出)每像素一个字节。这一点直接决定了帧缓存大小的算法。
4.2 三个外部中断的具体分工与优先级
先配 EXTI。注意GPIO_EXTILineConfig()里指定的是端口源,EXTI 线号由引脚号决定:
static void CameraSync_Init(void) { GPIO_InitTypeDef gpio; EXTI_InitTypeDef exti; NVIC_InitTypeDef nvic; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); gpio.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2; gpio.GPIO_Mode = GPIO_Mode_IPU; /* 同步信号用上拉输入,防浮空误触发 */ gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &gpio); GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource0); /* PB0 -> EXTI0 */ GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource1); /* PB1 -> EXTI1 */ GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource2); /* PB2 -> EXTI2 */ /* PCLK:下降沿,COM10 配成默认极性时数据在下降沿稳定 */ exti.EXTI_Line = EXTI_Line0; exti.EXTI_Mode = EXTI_Mode_Interrupt; exti.EXTI_Trigger = EXTI_Trigger_Falling; exti.EXTI_LineCmd = ENABLE; EXTI_Init(&exti); /* HREF:双边沿,上升沿开始一行,下降沿结束一行 */ exti.EXTI_Line = EXTI_Line1; exti.EXTI_Trigger = EXTI_Trigger_Rising_Falling; EXTI_Init(&exti); /* VSYNC:上升沿,一帧的起点 */ exti.EXTI_Line = EXTI_Line2; exti.EXTI_Trigger = EXTI_Trigger_Rising; EXTI_Init(&exti); /* 优先级:PCLK > HREF > VSYNC,PCLK 被打断就等于丢像素 */ NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&nvic); NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_Init(&nvic); NVIC_InitStructure.NVIC_IRQChannel = EXTI2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; NVIC_Init(&nvic); }优先级的安排有依据:PCLK 的频率最高,一旦它在等待 HREF 或 VSYNC 的中断返回,这段时间里新来的像素就会覆盖掉旧数据,采出来的图就是错位的。所以 PCLK 给最高的抢占优先级 0,HREF 次之,VSYNC 最低,因为一帧的开头丢几个时钟不影响整体。
再来看三个 ISR 的实现:
#define IMG_W 160 #define IMG_H 120 #define IMG_BPP 1 /* 1=灰度 2=RGB565,C8T6 只有 20KB SRAM */ #define FRAME_BYTES (IMG_W * IMG_H * IMG_BPP) volatile uint8_t g_frame[FRAME_BYTES]; volatile uint32_t g_byte_idx = 0; /* 帧内已写入字节数 */ volatile uint16_t g_line_idx = 0; /* 已采到的有效行数 */ volatile uint8_t g_href = 0; /* HREF 当前电平 */ volatile uint8_t g_frame_done = 0; /* 一帧封口标志 */ /* VSYNC:一帧的起点,所有指针归零 */ void EXTI2_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line2) != RESET) { EXTI_ClearITPendingBit(EXTI_Line2); g_byte_idx = 0; g_line_idx = 0; g_href = 0; g_frame_done = 0; /* 上一帧还没处理完就直接作废 */ } } /* HREF:行边界,负责计数和封帧 */ void EXTI1_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line1) != RESET) { EXTI_ClearITPendingBit(EXTI_Line1); g_href = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1); if (g_href) { g_line_idx++; /* 新的一行开始 */ } else { if (g_line_idx >= IMG_H) { /* 采够目标行数就封帧 */ g_frame_done = 1; } } } } /* PCLK:真正的数据搬运,每来一个下降沿取一个字节 */ void EXTI0_IRQHandler(void) { uint8_t b; if (EXTI_GetITStatus(EXTI_Line0) != RESET) { EXTI_ClearITPendingBit(EXTI_Line0); if (!g_href) return; /* 行消隐期的 PCLK 一律丢弃 */ if (g_byte_idx >= FRAME_BYTES) return; /* 越界保护,这条是保命的 */ b = (uint8_t)(GPIO_ReadInputData(GPIOA) & 0x00FF); g_frame[g_byte_idx++] = b; } }EXTI_GetITStatus和EXTI_ClearITPendingBit这两句不能用EXTI_GetFlagStatus替代,后者查的是挂起标志而不是中断使能状态,在某些边沿组合下会漏清标志,导致中断不停地重入。这是 F1 标准库上的老问题。
g_byte_idx >= FRAME_BYTES这个判断看着是废话,但它防的是“HREF 抖动导致多采一行”的情况。没有它,一旦行计数错位,缓冲区后面的全局变量会被顺着写穿,表现为程序莫名其妙跑飞。
4.3 字节流到图像的拼接与缓存
中断只负责把原始字节按顺序塞进数组,至于这些字节怎么组成像素,留给主循环处理,别在 ISR 里做运算。
灰度模式下,一个字节就是一个像素,主循环直接拿去送串口或者刷 OLED:
int main(void) { uint32_t i; uint16_t x, y; SystemInit(); CameraXclk_Init(); CameraDataPort_Init(); OV7670_SCCB_Init(); CameraSync_Init(); if (OV7670_CheckID() != 0) { while (1); /* ID 不对就停在这里,方便打断点 */ } OV7670_Init(); while (1) { if (!g_frame_done) continue; g_frame_done = 0; for (y = 0; y < IMG_H; y++) { for (x = 0; x < IMG_W; x++) { i = y * IMG_W + x; /* 灰度:直接输出;RGB565 时这里要按 16 位拼 */ LCD_DrawPixel(x, y, g_frame[i]); } } } }RGB565 模式下IMG_BPP要改成 2,字节缓冲里每个像素占两个位置,高字节在前,拼装方式如下:
uint16_t px = ((uint16_t)g_frame[2 * i] << 8) | g_frame[2 * i + 1];高字节在前是 OV7670 的默认输出顺序。如果显示出来颜色整体偏绿或者红蓝互换,八成是字节序反了,把移位方向调过来就能修好,不需要去改摄像头寄存器。这个现象我碰到过一次,调了半天寄存器,最后发现是这两行写反了。
帧缓存大小需要提前算好。F103C8T6 只有 20KB SRAM,QQVGA(160×120)的 RGB565 需要 38400 字节,根本放不下;灰度只要 19200 字节,勉强能跑但要留出栈空间。如果一定要 RGB565,得换 RAM 大一点的型号,或者把窗口裁到 160×60。这个限制是硬件决定的,代码怎么优化都绕不过去。
5. 避坑与排查:无 FIFO 采样最常见的几个翻车点
5.1 上电自检的排查顺序
出问题的时候不要一头扎进中断代码里看,按信号链从后往前查效率最高。
先量电源:3.3V 进了模组没有,PWDN 是不是真的在 0V。再量 XCLK:示波器接 PA8,应该看到一个 8MHz 的方波,幅值接近 3.3V。这一步没过,后面全白搭。然后量 VSYNC:上电后应该有周期性脉冲,如果是一条直线,说明摄像头根本没在输出,回去查 SCCB
本文还有配套的精品资源,点击获取