STM32F4 DCMI接口OV2640寄存器级配置与DMA双缓冲实战详解
2026/9/23 7:16:26 网站建设 项目流程

做图像采集的哥们儿肯定都有体会: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_PIXCLKPC6来自 OV2640 的 PCLK
DCMI_HSYNCPA4行同步
DCMI_VSYNCPB7场同步
DCMI_D0-D7PC6? 不,D0-D7 为 PC6? 注意:PC6 上面用了重新按标准映射:D0-D7 为 PB8? 不,标准是 PA4? 我再给准确的一组
DCMI_D0PC6
DCMI_D1PC7
DCMI_D2PC8
DCMI_D3PC9
DCMI_D4PC11
DCMI_D5PC12
DCMI_D6PD3
DCMI_D7PD6
SCCB_SCLPH7? 一般用 PB10/PB11 或任意 GPIO这里我实际用 PB10/PB11
SCCB_SDAPB11注意 OV2640 的 SIO_C/SIO_D 要上拉
XVCLKPA8 输出 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:12ESS内嵌同步信号使能,外部同步模式写 0
11:10PCKPOL像素时钟极性,0:上升沿采集,1:下降沿采集
9HSPOLHSYNC 极性,0:低有效,1:高有效
8VSPOLVSYNC 极性,0:低有效,1:高有效
7FCRC帧捕获率控制,00:捕获全部帧
6:5EDM外部数据模式,01:8 位数据
4CDM捕获模式,0:连续,1:快照
3CM捕获使能,1:使能,0:关闭
2CROP裁剪使能,0:关闭
1JPEGJPEG 模式,1:开启
0CAPTURE捕获使能,置 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,问题就分开了。

完整顺序是:

  1. 初始化调试串口(能用串口打印状态信息,后面排错救命)。
  2. 初始化 SCCB 的 GPIO。
  3. 延时 10ms 等 OV2640 上电稳定。
  4. 通过 SCCB 写 OV2640 寄存器初始化数组。
  5. 读回几个关键寄存器(0x12 等)验证 SCCB 通信正常。
  6. 配置 DCMI GPIO 复用功能。
  7. 配置 DCMI_CR 控制寄存器。
  8. 配置 DMA2 Stream1。
  9. 配置 DCMI 和 DMA 中断。
  10. 开启 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 的计数单位搞对,剩下的都是时序问题。希望你少踩我当年踩过的坑,一次点亮。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询