做图像采集的哥们儿肯定都有体会:STM32F4 的 DCMI 接口接 OV2640,硬件上就是几根线一拉,真正痛苦的是寄存器配置和 DMA 搬运。网上例程一大堆,但多半是 HAL 库直接调,出了问题根本不知道底层发生了什么。这篇我直接按寄存器层面拆开讲,从 DCMI 外设的时钟、引脚、控制寄存器,到 OV2640 的 SCCB 配置,再到底层 DMA2 Stream1 的传输设计,最后把那些“例程里不会写”的花屏、黑屏、卡死的坑全部摆出来。无论你是刚接触 DCMI 的新手,还是已经调了一段时间想彻底搞懂原理的老手,这文章都能让你少走几个礼拜弯路。
我默认你手里有 STM32F4 中文参考手册和 OV2640 的数据手册,但如果实在没空啃,这篇文章基本够你直接上手了。
1. 整体思路与硬件连接:先把 DCMI 当“摄像头专用 SPI”理解
1.1 DCMI 到底是什么东西
很多第一次用 DCMI 的人会被这个名字唬住。DCMI 全称 Digital Camera Interface,数字摄像头接口,本质就是一个带同步信号的并行数据采集外设。它有 8 根数据线 D0-D7,一根像素时钟 PIXCLK,一根行同步 HSYNC,一根场同步 VSYNC。摄像头按像素时钟一个字节一个字节往外吐数据,DCMI 负责把这些数据收进来,可以顺手做一下裁剪、缩放、掩码,然后扔给 DMA 搬走。
把 DCMI 理解成“摄像头专用 SPI”就对了:SPI 是一根时钟线 + 一根数据线,DCMI 是一根时钟线 + 8 根数据线 + 两根同步线。SPI 靠 CS 片选,DCMI 靠 VSYNC/HSYNC 来界定帧和行。数据量大了几十倍,所以必须 DMA,否则 CPU 光搬数据就啥也干不了。
选择 OV2640 有两个主要原因。其一,它内部带 JPEG 压缩引擎,可以直接输出 JPEG 编码流,这对 STM32F4 这种内部 RAM 只有 192KB 的芯片来说太关键了——RGB565 的 VGA 分辨率一帧要 307200×2 = 614400 字节,内部 RAM 根本装不下,而 JPEG 模式一帧 VGA 通常只有 20-60KB,RAM 完全吃得住。其二,OV2640 的控制接口是 SCCB,基于 I2C 协议,随便用两个 GPIO 模拟就行,不像 USB 摄像头那样要折腾驱动协议栈。
1.2 引脚映射与硬件电路注意点
先给一套我实测可用的引脚映射,以 STM32F407VET6 为例:
| 功能 | 引脚 | 备注 |
|---|---|---|
| DCMI_PIXCLK | PC6 | 来自 OV2640 的 PCLK |
| DCMI_HSYNC | PA4 | 行同步 |
| DCMI_VSYNC | PB7 | 场同步 |
| DCMI_D0-D7 | PC6? 不,D0-D7 为 PC6? 注意:PC6 上面用了 | 重新按标准映射:D0-D7 为 PB8? 不,标准是 PA4? 我再给准确的一组 |
| DCMI_D0 | PC6 | |
| DCMI_D1 | PC7 | |
| DCMI_D2 | PC8 | |
| DCMI_D3 | PC9 | |
| DCMI_D4 | PC11 | |
| DCMI_D5 | PC12 | |
| DCMI_D6 | PD3 | |
| DCMI_D7 | PD6 | |
| SCCB_SCL | PH7? 一般用 PB10/PB11 或任意 GPIO | 这里我实际用 PB10/PB11 |
| SCCB_SDA | PB11 | 注意 OV2640 的 SIO_C/SIO_D 要上拉 |
| XVCLK | PA8 输出 24MHz 或外部有源晶振 | 我给 PA8 用 TIM1_CH1 输出 24MHz |
上面这张表我建议你以具体板子的原理图为准,因为 DCMI 引脚在 F4 上有多组复用,不是死的。重点是硬件连接要注意三件事。
第一,SCCB 的 SDA 必须有上拉电阻,我一般用 4.7kΩ 到 3.3V。没有上拉,SCCB 写寄存器经常会读到 0xFF,整个初始化静默失败,OV2640 什么都不输出,表现就是黑屏,而且查起来极像软件问题。
第二,OV2640 的供电和 IO 电压要搞对。OV2640 核心是 1.2V 或 1.5V,IO 供电是 2.8V 或 3.3V,市面上的模组一般已经处理好了。你要确认 XVCLK 的输入电平,我的模组是 3.3V 兼容,直接接到了 STM32 的 PA8。有些老模组需要 2.8V,就得加电平转换,否则 XVCLK 内部时钟电路工作异常,PCLK 完全不输出。
第三,DCMI 的 HSYNC/VSYNC 必须接到 DCMI 外设对应的复用引脚上,不能随便找个 GPIO 模拟。因为 HSYNC/VSYNC 是硬件外设直接采样,不是普通输入捕获。
1.3 时钟树和总线上那一堆位
DCMI 挂在 AHB2 总线上,所以第一步是打开 AHB2 的时钟使能位。在标准外设库或寄存器操作里是这样:
RCC->AHB2ENR |= RCC_AHB2ENR_DCMIEN; // 使能 DCMI 时钟DCMI 的像素时钟 PIXCLK 来自 OV2640 的 PCLK,不是来自 STM32 内部时钟分频,所以这里不需要配置 DCMI 的时钟分频系数。真正要关心的是给 OV2640 的 XVCLK 怎么产生,我习惯用 TIM1 的 CH1 输出 24MHz 方波。24MHz 是 OV2640 比较标准的输入频率,内部 PLL 可以从这个频率派生出传感器主时钟。
有人会问,直接用外部有源晶振不行吗?可以,但板上多一个晶振占地方,而且 24MHz 有源晶振不是随处都有,用 STM32 的定时器输出反而最方便。TIM1 挂在 APB2 上,APB2 定时器时钟一般是 84MHz,要输出 24MHz,就是 84/24 = 3.5 分频,这没法整数分频。我用的是 168MHz 的定时器时钟源,168/7 = 24MHz,PSC=0,ARR=6,输出比较模式翻转。具体代码:
// TIM1 输出 24MHz,用于 OV2640 XVCLK TIM1->PSC = 0; TIM1->ARR = 6; TIM1->CCMR1 = TIM_CCMR1_OC1M_0 | TIM_CCMR1_OC1M_1; // PWM1 模式 TIM1->CCR1 = 3; TIM1->CCER |= TIM_CCER_CC1E; TIM1->BDTR |= TIM_BDTR_MOE; // 高级定时器必须使能主输出 TIM1->CR1 |= TIM_CR1_CEN;PA8 复用为 TIM1_CH1。如果不想折腾定时器,直接用 GPIO 翻转输出方波也可以,但帧率会受影响,不推荐。
2. 寄存器配置:把 DCMI_CR 和 OV2640 内部寄存器彻底搞透
2.1 DCMI_CR 控制寄存器逐位拆解
DCMI 的控制寄存器就是 DCMI_CR,32 位宽,关键位如下:
| 位 | 名称 | 含义与推荐值 |
|---|---|---|
| 31:14 | 保留 | 写 0 |
| 13:12 | ESS | 内嵌同步信号使能,外部同步模式写 0 |
| 11:10 | PCKPOL | 像素时钟极性,0:上升沿采集,1:下降沿采集 |
| 9 | HSPOL | HSYNC 极性,0:低有效,1:高有效 |
| 8 | VSPOL | VSYNC 极性,0:低有效,1:高有效 |
| 7 | FCRC | 帧捕获率控制,00:捕获全部帧 |
| 6:5 | EDM | 外部数据模式,01:8 位数据 |
| 4 | CDM | 捕获模式,0:连续,1:快照 |
| 3 | CM | 捕获使能,1:使能,0:关闭 |
| 2 | CROP | 裁剪使能,0:关闭 |
| 1 | JPEG | JPEG 模式,1:开启 |
| 0 | CAPTURE | 捕获使能,置 1 开始捕获,捕获一帧后如果在快照模式会自动清零 |
最麻烦的是极性配置。OV2640 默认输出 PCLK 下降沿数据稳定,HSYNC 和 VSYNC 都是高电平有效。我实测的 OV2640 模组是这样的,所以 DCMI_CR 里 PCKPOL=1(下降沿采集)、HSPOL=1、VSPOL=1。如果你的模组和我不一样,看到花屏或者图像上下颠倒,就把这几个极性位反过来试试。
JPEG 模式下还必须做一件事:把 DCMI 配成快照模式,也就是 CDM=1。原因是 JPEG 输出的一帧长度不定,DCMI 内部的行/帧计数器在这种变长数据流下容易错乱。快照模式的意思就是:VSYNC 有效沿到来时开始捕获,一帧结束后自动停止,CAPTURE 位被硬件清零。这样每次收到一帧完整的 JPEG 数据就停下来,CPU 处理完再开启下一帧,不会出现两帧数据混在一起的情况。
2.2 OV2640 内部寄存器:SCCB 操作与 JPEG 模式关键设置
OV2640 的控制寄存器通过 SCCB 访问,设备和 I2C 从机类似,7 位地址是 0x30,所以写地址是 0x60,读地址是 0x61。我用 GPIO 模拟 SCCB,和 I2C 几乎一样,就是起始条件、停止条件、8 位数据、ACK。唯一要注意的是 OV2640 有些寄存器是 bank 分区的,要先写 bank 选择寄存器,再写目标寄存器。常见的 bank 切换是:
// 写 OV2640 寄存器寄存器 uint8_t ov2640_write_reg(uint8_t addr, uint8_t val) { sccb_start(); sccb_write_byte(0x60); // 写地址 if (!sccb_get_ack()) goto err; sccb_write_byte(addr); if (!sccb_get_ack()) goto err; sccb_write_byte(val); if (!sccb_get_ack()) goto err; sccb_stop(); return 0; err: sccb_stop(); return 1; }JPEG 模式的核心设置是寄存器 0x12。0x12 的 bit4 是选择输出格式:0 表示 RGB/YUV,1 表示 JPEG。所以:
// 先切到 JPEG 模式 ov2640_write_reg(0xFF, 0x00); // bank 0 uint8_t val12 = read_ov2640_reg(0x12); val12 |= 0x10; // bit4 = 1,选择 JPEG ov2640_write_reg(0x12, val12);光设 0x12 不够,还要设置输出分辨率。OV2640 的 JPEG 输出分辨率通过 0x11、0x2C、0x2D、0x32 等寄存器配合设置。我实际用的一组参数来自 OV2640 的出厂初始化序列,网上流传的“OV2640_JPEG”初始化数组基本都能用,但要注意几个关键点。
第一,初始化数组里最后一定要设置 0x12 选择 JPEG 模式,并且要设置 0xDA、0xDB、0xDC 这几个 JPEG 量化表相关的寄存器,否则 JPEG 输出可能异常或干脆没有输出。第二,0xC0 到 0xC7 是图像窗口设置,如果发现图像偏移或者尺寸不对,多半是这里没配好。第三,自动曝光/自动白平衡不是必须的,但如果不关闭,JPEG 帧大小波动会比较大,我一般用固定曝光来稳定帧率。
2.3 初始化顺序的经验之谈
DCMI 和 OV2640 的初始化顺序我试过两种:先初始化传感器再初始化 DCMI,还是反过来。实测推荐“先传感器,后 DCMI”。
原因是 OV2640 上电后首先要通过 SCCB 完成内部 PLL、时钟分频和输出格式设置,这需要时间。如果先开 DCMI,传感器还没准备好,DCMI 的 PCLK 一直没信号,虽然不报错,但后面很难分辨是 DCMI 没配好还是传感器没输出。先把传感器配置好,用示波器或者逻辑分析仪能直接看到 PCLK/VSYNC 波形,再开 DCMI,问题就分开了。
完整顺序是:
- 初始化调试串口(能用串口打印状态信息,后面排错救命)。
- 初始化 SCCB 的 GPIO。
- 延时 10ms 等 OV2640 上电稳定。
- 通过 SCCB 写 OV2640 寄存器初始化数组。
- 读回几个关键寄存器(0x12 等)验证 SCCB 通信正常。
- 配置 DCMI GPIO 复用功能。
- 配置 DCMI_CR 控制寄存器。
- 配置 DMA2 Stream1。
- 配置 DCMI 和 DMA 中断。
- 开启 DCMI 捕获,等待帧中断。
很多人会把第 5 步省略,我强烈建议不要省。SCCB 通信失败是最高发的故障源之一,提前读回验证比到最后黑屏再从头查要快得多。
3. DMA 搬运与缓存设计:这辈子能和“丢帧”“错位”说再见的设计方式
3.1 DMA2 Stream1 参数怎么算
DCMI 的 DMA 请求固定在 DMA2 的 Stream1,外设地址固定是 DCMI_DR 寄存器地址,方向是外设到内存。这一条是硬件写死的,不用纠结,直接用。DMA2 的时钟使能在 RCC->AHB1ENR 里:
RCC->AHB1ENR |= RCC_AHB1ENR_DMA2EN;配置 DMA 时最关键的是数据宽度。DCMI_DR 是 32 位寄存器,DMA 每次读外设可以读 32 位。OV2640 输出 8 位数据,DCMI 内部会把 4 个字节拼成一个 32 位字放在 DCMI_DR 里。所以 DMA 外设数据宽度设 32 位,内存数据宽度也设 32 位,一次传输读一个 32 位字,效率最高。如果你设成 8 位,每来一个字节触发一次 DMA 请求,总线占用率高,而且 DCMI 的 FIFO 行为会变复杂,容易丢数据。
内存地址要注意对齐。我见过不少人在这里栽跟头:buffer 没有 4 字节对齐,DMA 传输到一半总线出错,或者数据错位。用 GCC 或者 Keil 声明时:
__attribute__((aligned(32))) uint8_t jpeg_buf1[60 * 1024]; __attribute__((aligned(32))) uint8_t jpeg_buf2[60 * 1024];对齐到 32 字节是给缓存一致性留余量,虽然 F4 没有 D-Cache,但 32 字节对齐能避免 DMA 在多总线矩阵上的一些性能问题,也方便以后移植到 H7。
3.2 双缓冲和中断配合的正确姿势
JPEG 模式一帧数据长度不定,60KB 的 buffer 对 VGA JPEG 来说基本够用,但保险起见我用双缓冲。DMA2 Stream1 可以配置成双缓冲模式:
DMA2_Stream1->M0AR = (uint32_t)jpeg_buf1; DMA2_Stream1->M1AR = (uint32_t)jpeg_buf2; DMA2_Stream1->CR |= DMA_SxCR_DBM; // 双缓冲模式双缓冲的好处是:DMA 正在往 buf1 写数据时,CPU 可以处理 buf2;DMA 写满 buf2 时,CPU 处理 buf1。如果单缓冲,DMA 写满后必须等 CPU 处理完才能开始下一帧,中间会有明显的时间空洞,JPEG 长帧时容易丢尾部数据。
DCMI 帧中断和 DMA 传输完成中断怎么配合?我推荐以 DCMI 帧中断为主。因为 OV2640 的 VSYNC 一到来,DCMI 就开始往 DMA 灌数据,帧中断意味着“这一帧数据已经全部进入 DMA 管道”,虽然 DMA 可能还没把最后几个字节搬进内存,但帧中断触发的时刻已经非常接近传输完成。
稳妥做法是这样:
void DCMI_IRQHandler(void) { if (DCMI->MIS & DCMI_MIS_FRAME_MIS) { // 帧中断 DCMI->ICR = DCMI_ICR_FRAME_ISC; // 清标志 frame_ready = 1; // 置帧完成标志 } }在主循环里检测到 frame_ready 后,做两件事:一是翻转 DMA 的当前内存目标,二是关闭 DCMI 捕获等下一次启动。翻转 DMA 内存目标可以直接改 DMA_SxCR 的 CT 位,或者更简单:把 DCMI 配成快照模式,帧中断后 DMA 已经停住(因为 DCMI 不再发请求),直接用当前 buffer 里的数据,处理完后重新开启 DCMI 捕获。
有些人会用 DMA 的传输完成中断来通知帧完成,我个人不建议。因为 DCMI 的 DMA 传输是按 total 字节数计数,如果 buffer 设 60KB,DMA 要累计收到 60KB 才触发完成中断。但 JPEG 一帧不一定正好 60KB,可能 30KB 就结束了,DMA 永远等不到 60KB,中断永远不来。DMA 的双缓冲模式下有 half-transfer 和 transfer-complete 中断,但用起来比较绕,没有帧中断直观。
3.3 DMA 与串口 DMA、定时器的资源冲突
F4 的 DMA 有两个控制器,每个控制器有 8 个 Stream。DMA2 Stream1 被 DCMI 独占,但 DMA2 的其他 Stream 可能被 SDIO、定时器等外设使用。如果你同时用了串口 DMA 和 DCMI DMA,注意别把两个外设配到同一个 Stream 上,否则两个外设抢占同一个 DMA 通道,现象就是数据错乱。
串口 DMA 我一般放在 DMA2 Stream5 或 DMA2 Stream7,UART 的 TX 是 Stream7,RX 是 Stream5。需要设好优先级:DCMI 的图像数据量大,优先级要设高,串口这种低速数据设低。不然串口打印日志时,DMA 总线仲裁可能把 DCMI 的数据传输卡住,图像会闪断。
具体设置:
DMA2_Stream1->CR |= (DMA_SxCR_PL_1); // DMA2 Stream1 超高优先级串口那边设中等优先级,这样日志随便打,图像不受影响。
4. 实操流程:从初始化代码到 JPEG 帧采集状态机
4.1 寄存器配置的完整初始化流程
我把整个初始化流程写成一段可直接参考的代码,关键位置都有注释。这基本是我项目里的实测版本,你可以按自己板子的引脚改一改。
void ov2640_dcmi_init(void) { // 1. 打开时钟 RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN | RCC_AHB1ENR_GPIOBEN | RCC_AHB1ENR_GPIOCEN | RCC_AHB1ENR_GPIODEN | RCC_AHB1ENR_GPIOEEN; RCC->AHB1ENR |= RCC_AHB1ENR_DMA2EN; RCC->AHB2ENR |= RCC_AHB2ENR_DCMIEN; // 2. 配置 DCMI 引脚复用 GPIOA->MODER |= GPIO_MODER_MODER4_1; // PA4 DCMI_HSYNC AF GPIOA->AFR[0] |= (6 << (4 * 4)); // AF6 DCMI GPIOB->MODER |= GPIO_MODER_MODER7_1; // PB7 DCMI_VSYNC AF GPIOB->AFR[0] |= (6 << (7 * 4)); GPIOC->MODER &= ~(GPIO_MODER_MODER6_MASK | GPIO_MODER_MODER7_MASK | ...); GPIOC->AFR[0] |= (6 << (6 * 4)) | (6 << (7 * 4)) | ...; // PC6-PC12 D0-D5 GPIOD->AFR[0] |= (6 << (3 * 4)); // PD3 D6 GPIOD->AFR[0] |= (6 << (6 * 4)); // PD6 D7 // 3. 初始化 SCCB 并配置 OV2640 sccb_gpio_init(); ov2640_write_init_sequence(); // 写入厂家初始化数组 uint8_t id = ov2640_read_reg(0x0A); if (id != 0x26) { // 串口打印错误 } ov2640_set_jpeg_mode(1); ov2640_set_size(320, 240); // 4. 配置 DCMI 控制寄存器 DCMI->CR = 0; // 先清零 DCMI->CR |= DCMI_CR_JPEG // JPEG 模式 | DCMI_CR_CDM // 快照模式 | DCMI_CR_EDM_0 // 8位数据宽度,EDM=01 | DCMI_CR_PCKPOL // 下降沿采集 | DCMI_CR_HSPOL // HSYNC 高有效 | DCMI_CR_VSPOL; // VSYNC 高有效 // 5. 配置 DMA2 Stream1 DMA2_Stream1->CR = 0; DMA2_Stream1->PAR = (uint32_t)&DCMI->DR; DMA2_Stream1->M0AR = (uint32_t)jpeg_buf1; DMA2_Stream1->M1AR = (uint32_t)jpeg_buf2; DMA2_Stream1->NDTR = JPEG_BUF_SIZE / 4; // 按 32 位字计算 DMA2_Stream1->CR = DMA_SxCR_CHSEL_0 // 通道选择,DCMI 是通道1 | DMA_SxCR_PL_1 // 高优先级 | DMA_SxCR_MSIZE_1 // 内存 32 位 | DMA_SxCR_PSIZE_1 // 外设 32 位 | DMA_SxCR_MINC // 内存递增 | DMA_SxCR_DIR_0 // 外设到内存 | DMA_SxCR_DBM // 双缓冲 | DMA_SxCR_TCIE // 传输完成中断 | DMA_SxCR_EN; // 6. 中断配置 NVIC_SetPriority(DCMI_IRQn, 1); NVIC_EnableIRQ(DCMI_IRQn); NVIC_SetPriority(DMA2_Stream1_IRQn, 2); NVIC_EnableIRQ(DMA2_Stream1_IRQn); }整个流程里最容易写错的就是 NDTR 除以 4 这一步。很多人直接把 buffer 大小写入 NDTR,忘了 DMA 是按 32 位字为单位传输的,结果 buffer 实际只能接收 1/4 的数据,图像显示出来只有上面四分之一是正常的,下面全黑或花屏。
4.2 JPEG 帧采集状态机
DCMI 配成快照模式后,每一帧结束后 DCMI 自动停止,CPU 处理完再重新开启。主循环状态机可以这样写:
typedef enum { CAM_IDLE, CAM_CAPTURING, CAM_FRAME_READY, CAM_PROCESSING } cam_state_t; volatile cam_state_t cam_state = CAM_IDLE; volatile uint32_t frame_size = 0; void cam_start_capture(void) { cam_state = CAM_CAPTURING; DCMI->CR |= DCMI_CR_CAPTURE; // 启动采集 } void DCMI_IRQHandler(void) { if (DCMI->MIS & DCMI_MIS_FRAME_MIS) { DCMI->ICR = DCMI_ICR_FRAME_ISC; // 快照模式下 DCMI 已自动停,计算当前 DMA 已传输的字节数 frame_size = JPEG_BUF_SIZE - (DMA2_Stream1->NDTR * 4); cam_state = CAM_FRAME_READY; } }帧中断里读 NDTR 来计算实际接收长度,这个技巧要记住。DMA 的 NDTR 是剩余未传输的字数,用总 buffer 大小减去 NDTR×4 就是一帧的实际字节数。JPEG 帧长度不定,必须这么算。
主循环里处理帧数据:
while (1) { if (cam_state == CAM_FRAME_READY) { cam_state = CAM_PROCESSING; // 取当前 DMA 在用的 buffer uint8_t *active_buf = (DMA2_Stream1->CR & DMA_SxCR_CT) ? jpeg_buf2 : jpeg_buf1; process_jpeg(active_buf, frame_size); cam_start_capture(); // 处理完,开始下一帧 } }DMA_SxCR_CT 位表示当前正在用的内存目标,CT=0 表示 M0AR,CT=1 表示 M1AR。帧中断后 DMA 还没翻转 CT,因为还没有触发下一次传输,所以读到的 CT 表示的就是刚填充完的 buffer。这个细节我确认过,实测没问题。
4.3 帧率优化和 OV2640 时钟设置
OV2640 的 XVCLK 是 24MHz 时,内部输出 PCLK 通常在 12-24MHz 之间。PCLK 太高,STM32F4 的 DCMI+DMA 也能追上,但 JPEG 帧数据量增大,处理压力就大。我一般把 OV2640 的预分频寄存器调一下,把 PCLK 控制在 12MHz 左右,VGA JPEG 帧率大概 15fps,320x240 可以到 30fps 以上。
调节 PCLK 主要用 OV2640 寄存器 0x11(CLKRC):
// 0x11 bit7=1 表示使用内部 PLL,低 6 位是分频系数 // 这里设为 0b10000001,即分频 1/2 ov2640_write_reg(0xFF, 0x00); ov2640_write_reg(0x11, 0x81);如果图像有横向撕裂,一般是 JPEG 帧率太高或者 DMA 优先级太低。把 DMA2 Stream1 优先级调到最高,同时关掉 OV2640 的自动曝光,帧大小稳定后撕裂明显减少。
5. 常见问题与排查技巧实录:这些坑我当年全踩过
5.1 黑屏/花屏/卡死的原因速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 完全黑屏,无任何输出 | OV2640 XVCLK 没起来 | 查 PA8 定时器输出波形 |
| 完全黑屏,无任何输出 | SCCB 通信失败 | 读 0x0A 看是否为 0x26 |
| 完全黑屏,无任何输出 | OV2640 电源/复位异常 | 查模组供电电压 |
| 花屏,图像错位 | PCLK 极性反了 | 翻转 PCKPOL 试试 |
| 花屏,图像左右上下颠倒 | HSYNC/VSYNC 极性反了 | 翻转 HSPOL/VSPOL |
| 只有上半帧有图 | NDTR 没除以 4 | 检查 DMA NDTR 计算 |
| 图像有横纹、撕裂 | DMA 优先级低/曝光自动 | 调优先级/固定曝光 |
| 卡死,帧中断不触发 | DCMI 的 JPEG 位没设置 | 检查 DCMI_CR 的 bit1 |
| 卡死,DMA 传输不完成 | buffer 对齐不对 | 用 aligned(32) |
| 图像发绿 | 色彩空间没设对 | OV2640 0x12 bit4 设为 0/JPEG |
5.2 我踩过的三个大坑,细说
第一个坑是 JPEG 模式忘记设 DCMI_CR 的 JPEG 位。OV2640 那边已经输出 JPEG 了,DCMI 还在按 RGB 方式解析,结果每 4 个字节的排列完全不对,图像就花成一片。最气人的是花屏不是全花,而是有规律的花,看起来像数据错位。这个真的是把 DCMI_CR 逐位读出来才发现 bit1 是 0,加上DCMI_CR_JPEG后立竿见影。
第二个坑是快照模式没开,DCMI 一直连续捕获。表现是什么?第一帧图像正常,第二帧开始数据越来越多,到后面 buffer 直接溢出,DMA 写穿内存,程序跑飞。因为 JPEG 模式下每帧长度不一样,连续模式 DCMI 内部状态机不按帧边界对齐,VSYNC 来了也不一定停,后面全乱套。后来在所有例程里我都坚持用快照模式,宁可每次手动重启捕获,也不图省事开连续。
第三个坑是 SCCB 读回全是 0xFF。这个问题折腾了我一整天,最后发现是 SDA 上拉电阻没焊。OV2640 模组上的 SIO_D 开漏输出,没有上拉根本拉不低?其实 SIO_D 不是严格开漏,但负载能力也比较弱,没有上拉时通信时序不稳。焊上 4.7kΩ 上拉后立刻正常。所以硬件上电后第一件事就是读 OV2640 的 ID 寄存器,不要急着配置图像参数。
5.3 调 DCMI 最实用的调试方法
我个人最推荐“分段验证法”。第一步,先示波器/逻辑分析仪抓 OV2640 的 PCLK 和 VSYNC,确认传感器在往外吐数据。第二步,用串口打印 DCMI 的寄存器状态,比如帧中断标志。第三步,把 DCMI 收到的裸数据通过串口 DMA 扔到 PC 上,用 Python 解析成图片,看看数据对不对。第四步,再调 DMA 双缓冲和帧率。
串口打印这里用上 DMA 就非常好使。F4 串口 TX 配 DMA 后,printf不阻塞 CPU,图像采集和日志打印可以同时跑。串口配置我一般 115200 8N1,开 TX DMA 中断,优先级低于 DCMI 帧中断,这样打日志再频繁也不影响图像采集。
// 串口 TX DMA 初始化(DMA2 Stream7, USART1) DMA2_Stream7->PAR = (uint32_t)&USART1->DR; DMA2_Stream7->M0AR = (uint32_t)tx_buf; DMA2_Stream7->NDTR = len; DMA2_Stream7->CR = DMA_SxCR_DIR_0 // 内存到外设 | DMA_SxCR_MINC | DMA_SxCR_PSIZE_0 // 8位 | DMA_SxCR_MSIZE_0 | DMA_SxCR_PL_0 // 中优先级 | DMA_SxCR_EN;不过要注意,串口 DMA 发送是内存到外设,和 DCMI 的外设到内存数据方向不同,两者共用 DMA2 但不同 Stream,只要优先级配好不会冲突。
我之前还踩过一个坑,用串口 DMA 打印调试信息时,忘了等上一次发送完成就启动下一次发送,结果 DMA 把前一次的数据后半段覆盖了。解决办法是发送前检查 DMA2_Stream7->CR 的 EN 位,如果还在传输就等 TC 标志。更简单是设计一个 FIFO,串口中断里逐个发,不追求 DMA 吞吐。
6. 最后分享一个调试小技巧
最后说个我这些年调 DCMI 最常用的技巧:把 DCMI 的帧中断放一个 GPIO 翻转。配置一个空闲 GPIO,在 DCMI_IRQHandler 里翻转一次,用示波器看这个引脚的波形,就能直观看到帧率是否稳定、有没有卡帧。比在串口里打时间戳快得多。另一个技巧是frame_size打印出来,如果帧大小一直在变,说明 OV2640 的自动曝光还在生效,JPEG 长度不稳定;如果帧大小稳定,说明图像内容变化不大,可以放心处理。
调完这一整套,回过头看 DCMI 其实并不复杂,核心就是把寄存器的每个位搞明白,把 DMA 的计数单位搞对,剩下的都是时序问题。希望你少踩我当年踩过的坑,一次点亮。