# 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_MODE` 和 `SYNCHRO` 位决定捕获时机。若配置为连续模式,DCMI 会持续捕获,直到软件停止;若为快照模式,则每帧一次。 ### 2.2 DMA 双缓冲机制 DMA 双缓冲(Double Buffer)允许在传输当前缓冲区时,另一个缓冲区可被 CPU 处理。STM32F4 的 DMA2 支持双缓冲模式,通过 `DMA_SxCR` 的 `DBM` 位启用,`DMA_SxM0AR` 和 `DMA_SxM1AR` 指定两个内存地址。 关键点:DMA 在传输完成一半或全部时触发中断,但若 DCMI 持续产生数据,而 DMA 切换不及时,会导致数据覆盖或丢失。 ## 3. 排查过程 ### 3.1 检查硬件时序 用示波器测量 VSYNC 和 HSYNC 信号,确认 OV5640 输出正常。发现 VSYNC 脉宽约 1ms,HSYNC 约 20us,符合规格。但注意:OV5640 的 VSYNC 极性可通过寄存器配置,默认高有效,需与 DCMI 设置匹配。 ### 3.2 检查 DCMI 配置 初始化代码片段: ```c 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_Frame` 或 `Quarter_Frame` 会丢帧。这里配置无误。 ### 3.3 检查 DMA 双缓冲配置 常见错误:DMA 的传输长度未设置为整帧大小。例如,640x480 RGB565 每帧 614400 字节,但 DMA 的 `NDTR` 寄存器是 16 位,最大 65535,因此需要分多次传输。 正确做法:使用 DMA 的循环模式,并设置传输长度为行大小(如 640*2=1280 字节),配合 DCMI 的行中断来切换缓冲区。但双缓冲模式下,DMA 会自动切换,需确保 `NDTR` 为一行大小,且 `M0AR`/`M1AR` 指向不同行缓冲区。 错误示例: ```c // 错误:NDTR 设置为整帧大小,导致溢出 DMA_SetCurrDataCounter(DMA2_Stream1, 614400); // 溢出! ``` 正确配置: ```c #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 状态,必要时重新同步。 ```c 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 缓冲区与帧对齐。 ```c 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 中断: ```c DCMI_ITConfig(DCMI_IT_VSYNC, ENABLE); DCMI_ITConfig(DCMI_IT_LINE, ENABLE); ``` ## 4. 完整代码示例 以下为整合后的初始化与中断处理: ```c // 缓冲区定义 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 的硬件同步机制,并善用中断进行软件同步。 希望本文能帮助你在嵌入式视觉开发中少走弯路。如有疑问,欢迎交流。