STM32F4 DCMI 与 OV5640 数据同步丢失:DMA 双缓冲深度排查实录

引言

在嵌入式视觉应用中,STM32F4 的 DCMI(Digital Camera Interface)配合 OV5640 摄像头是常见组合。DMA 双缓冲能有效提升帧率,但数据同步丢失(如帧错位、行断裂)常让开发者抓狂。本文记录一次真实排查过程,从原理到实践,彻底解决该问题。

1. 问题现象

项目使用 STM32F407,DCMI 连接 OV5640(RGB565 模式,640x480),DMA 双缓冲传输。现象:

  • 图像偶发上下错位(帧头丢失)
  • 部分行数据重复或缺失
  • 开启双缓冲后概率增加,单缓冲时几乎不出现

2. 原理剖析

2.1 DCMI 同步机制

DCMI 依赖硬件同步信号(HSYNC/VSYNC)或内嵌码。OV5640 默认使用硬件同步,VSYNC 标记帧开始,HSYNC 标记行有效。DCMI 在 VSYNC 上升沿开始捕获,HSYNC 有效期间采集像素。

关键点:DCMI 的 DCMI_CR 寄存器中的 CAPTURE_MODESYNCHRO 位决定捕获时机。若配置为连续模式,DCMI 会持续捕获,直到软件停止;若为快照模式,则每帧一次。

2.2 DMA 双缓冲机制

DMA 双缓冲(Double Buffer)允许在传输当前缓冲区时,另一个缓冲区可被 CPU 处理。STM32F4 的 DMA2 支持双缓冲模式,通过 DMA_SxCRDBM 位启用,DMA_SxM0ARDMA_SxM1AR 指定两个内存地址。

关键点:DMA 在传输完成一半或全部时触发中断,但若 DCMI 持续产生数据,而 DMA 切换不及时,会导致数据覆盖或丢失。

3. 排查过程

3.1 检查硬件时序

用示波器测量 VSYNC 和 HSYNC 信号,确认 OV5640 输出正常。发现 VSYNC 脉宽约 1ms,HSYNC 约 20us,符合规格。但注意:OV5640 的 VSYNC 极性可通过寄存器配置,默认高有效,需与 DCMI 设置匹配。

3.2 检查 DCMI 配置

初始化代码片段:

void DCMI_Init(void) {
    DCMI_InitTypeDef dcmi_init;
    dcmi_init.DCMI_CaptureMode = DCMI_CaptureMode_Continuous;
    dcmi_init.DCMI_SynchroMode = DCMI_SynchroMode_Hardware;
    dcmi_init.DCMI_PCKPolarity = DCMI_PCKPolarity_Rising;
    dcmi_init.DCMI_VSPolarity = DCMI_VSPolarity_High;
    dcmi_init.DCMI_HSPolarity = DCMI_HSPolarity_High;
    dcmi_init.DCMI_ExtendedDataMode = DCMI_ExtendedDataMode_8b;
    dcmi_init.DCMI_CaptureRate = DCMI_CaptureRate_All_Frame;
    DCMI_Init(&dcmi_init);
}

注意:DCMI_CaptureRate_All_Frame 表示每帧都捕获,若改为 Half_FrameQuarter_Frame 会丢帧。这里配置无误。

3.3 检查 DMA 双缓冲配置

常见错误:DMA 的传输长度未设置为整帧大小。例如,640x480 RGB565 每帧 614400 字节,但 DMA 的 NDTR 寄存器是 16 位,最大 65535,因此需要分多次传输。

正确做法:使用 DMA 的循环模式,并设置传输长度为行大小(如 640*2=1280 字节),配合 DCMI 的行中断来切换缓冲区。但双缓冲模式下,DMA 会自动切换,需确保 NDTR 为一行大小,且 M0AR/M1AR 指向不同行缓冲区。

错误示例:

// 错误:NDTR 设置为整帧大小,导致溢出
DMA_SetCurrDataCounter(DMA2_Stream1, 614400); // 溢出!

正确配置:

#define LINE_SIZE  (640 * 2)  // 一行字节数
#define BUFFER_SIZE (LINE_SIZE * 480) // 整帧

// 双缓冲,每行切换
DMA_InitStructure.DMA_BufferSize = LINE_SIZE;
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 循环模式
DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)buffer0;
DMA_InitStructure.DMA_Memory1BaseAddr = (uint32_t)buffer1;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Enable;
DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
DMA_Init(DMA2_Stream1, &DMA_InitStructure);

3.4 关键问题:DCMI 与 DMA 的同步丢失

即使配置正确,仍可能丢数据。原因:DCMI 的像素时钟(PCLK)与 DMA 请求不同步。当 DCMI 产生行结束事件时,DMA 可能尚未完成当前行传输,导致下一行数据覆盖。

解决方案:启用 DCMI 的行中断(LINE interrupt),在中断中检查 DMA 状态,必要时重新同步。

void DCMI_IRQHandler(void) {
    if (DCMI_GetITStatus(DCMI_IT_LINE)) {
        // 检查 DMA 当前缓冲区
        if (DMA_GetFlagStatus(DMA2_Stream1, DMA_FLAG_TCIF1)) {
            // 传输完成,切换缓冲区(DMA 自动完成)
        } else {
            // 传输未完成,可能丢失,重新初始化 DMA
            DMA_Cmd(DMA2_Stream1, DISABLE);
            DMA_SetCurrDataCounter(DMA2_Stream1, LINE_SIZE);
            DMA_Cmd(DMA2_Stream1, ENABLE);
        }
        DCMI_ClearITPendingBit(DCMI_IT_LINE);
    }
}

3.5 终极方案:使用帧中断同步

更稳妥的做法是使用 VSYNC 中断来同步帧开始,确保 DMA 缓冲区与帧对齐。

void DCMI_IRQHandler(void) {
    if (DCMI_GetITStatus(DCMI_IT_VSYNC)) {
        // 帧开始,重置 DMA 计数器
        DMA_Cmd(DMA2_Stream1, DISABLE);
        DMA_SetCurrDataCounter(DMA2_Stream1, LINE_SIZE);
        DMA_Cmd(DMA2_Stream1, ENABLE);
        DCMI_ClearITPendingBit(DCMI_IT_VSYNC);
    }
}

注意:需在 DCMI 初始化时使能 VSYNC 中断:

DCMI_ITConfig(DCMI_IT_VSYNC, ENABLE);
DCMI_ITConfig(DCMI_IT_LINE, ENABLE);

4. 完整代码示例

以下为整合后的初始化与中断处理:

// 缓冲区定义
uint16_t buffer0[640*480];
uint16_t buffer1[640*480];

void DCMI_DMA_Init(void) {
    // 使能时钟
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_DCMI, ENABLE);

    // GPIO 配置(略)

    // DCMI 初始化(如前)
    DCMI_Init(&dcmi_init);

    // DMA 初始化(如前,使用双缓冲)
    DMA_Init(DMA2_Stream1, &DMA_InitStructure);

    // 使能 DMA 中断(传输完成和半传输)
    DMA_ITConfig(DMA2_Stream1, DMA_IT_TC | DMA_IT_HT, ENABLE);

    // 使能 DCMI 中断
    DCMI_ITConfig(DCMI_IT_VSYNC | DCMI_IT_LINE, ENABLE);

    // 使能 DMA 和 DCMI
    DMA_Cmd(DMA2_Stream1, ENABLE);
    DCMI_Cmd(ENABLE);
}

void DMA2_Stream1_IRQHandler(void) {
    if (DMA_GetITStatus(DMA2_Stream1, DMA_IT_TCIF1)) {
        // 一行传输完成,可处理 buffer0 或 buffer1(根据当前地址)
        DMA_ClearITPendingBit(DMA2_Stream1, DMA_IT_TCIF1);
    }
}

void DCMI_IRQHandler(void) {
    if (DCMI_GetITStatus(DCMI_IT_VSYNC)) {
        // 帧同步,确保 DMA 计数器正确
        DMA_Cmd(DMA2_Stream1, DISABLE);
        DMA_SetCurrDataCounter(DMA2_Stream1, LINE_SIZE);
        DMA_Cmd(DMA2_Stream1, ENABLE);
        DCMI_ClearITPendingBit(DCMI_IT_VSYNC);
    }
    if (DCMI_GetITStatus(DCMI_IT_LINE)) {
        // 行同步,可做调试计数
        DCMI_ClearITPendingBit(DCMI_IT_LINE);
    }
}

5. 注意事项

  • 缓冲区大小:确保缓冲区足够大,避免溢出。RGB565 每像素 2 字节,640x480 需 614400 字节,建议使用外部 SRAM。
  • DMA 优先级:DMA2 Stream1 优先级应设为最高,避免被其他 DMA 抢占。
  • FIFO 配置:启用 FIFO 并设置阈值,可减少总线冲突。
  • DCMI 时钟:PCLK 频率不能超过 DCMI 最大支持(通常 25MHz 左右),否则数据采样不稳定。
  • 调试技巧:在 VSYNC 中断中翻转 GPIO,用示波器观察帧率;在 LINE 中断中计数,检查行数是否等于 480。

6. 总结

通过本次排查,确认数据同步丢失的根源在于 DMA 传输长度与 DCMI 行同步不匹配,以及未在帧边界重置 DMA。采用 VSYNC 中断同步 + 行大小传输后,问题彻底解决。双缓冲模式下,务必理解 DCMI 的硬件同步机制,并善用中断进行软件同步。

希望本文能帮助你在嵌入式视觉开发中少走弯路。如有疑问,欢迎交流。