# STM32F4 DCMI 与 OV5640 高速切换分辨率时的 FIFO 溢出排查与规避 ## 1. 问题现象与背景 在嵌入式视觉项目中,STM32F4 通过 DCMI 接口连接 OV5640 摄像头,当系统需要从 1080p 切换到 720p(或反之)时,经常出现图像上半部分正常、下半部分花屏或全黑,甚至 DCMI 中断风暴导致系统卡死。这通常不是传感器配置错误,而是 DCMI 内部 FIFO 在切换瞬间发生溢出(Overrun)。 ## 2. 溢出根因分析 ### 2.1 DCMI 数据通路 DCMI 接收并行数据(8/10/12/14-bit),先写入内部 32x32-bit FIFO,再由 DMA 搬运到内存。FIFO 深度有限,若写入速率持续大于读出速率,则溢出。 ### 2.2 切换分辨率的三个关键突变 - **PCLK 频率跳变**:OV5640 切换分辨率时,像素时钟 PCLK 会瞬间变化(例如从 24MHz 跳到 48MHz),而 DCMI 的同步逻辑需要几个时钟周期重新锁定。 - **DMA 带宽竞争**:FIFO 通过 DMA 请求将数据搬到内存,若此时系统总线被其他高优先级外设(如 SDIO、USB)占用,DMA 响应延迟增大。 - **FIFO 阈值配置不当**:DCMI 的 FIFO 中断阈值(FIFO_TH)决定何时触发 DMA 请求,阈值过高或过低都会影响读出节奏。 ### 2.3 溢出发生的时序窗口 切换分辨率时,OV5640 会输出若干无效帧(通常 2-3 帧),这些帧的 PCLK 和行场同步信号不稳定。若 DCMI 未及时禁用,FIFO 可能被无效数据填满,而 DMA 尚未配置好新帧的缓冲区,导致溢出。 ## 3. 排查流程(寄存器级) ### 3.1 检查溢出标志 DCMI 的 MISR 寄存器中的 OVR 位(bit 1)会置 1。在中断服务函数中读取并清除: ```c if (__HAL_DCMI_GET_FLAG(&hdcmi, DCMI_FLAG_OVR)) { __HAL_DCMI_CLEAR_FLAG(&hdcmi, DCMI_FLAG_OVR); // 记录溢出次数 } ``` ### 3.2 测量切换时的 PCLK 波形 用逻辑分析仪抓取 PCLK 和 VSYNC/HSYNC,确认切换瞬间是否存在毛刺或频率突变。若 PCLK 在切换时出现 >50% 的瞬时过冲,需在传感器端增加软启动。 ### 3.3 监控 DMA 响应时间 在 DMA 中断中记录时间戳,对比正常帧和切换帧的 DMA 请求到传输完成的时间差。若差异 >50%,说明总线竞争严重。 ## 4. 规避方案与代码实现 ### 4.1 方案一:双缓冲 + 帧同步切换 核心思想:切换分辨率前,先停止 DCMI 采集,等待当前帧结束,再重新配置传感器和 DCMI,最后重启采集。使用双缓冲避免 DMA 在切换期间指向无效内存。 ```c // 切换分辨率函数 void switch_resolution(uint32_t new_width, uint32_t new_height) { // 1. 停止 DCMI 采集 HAL_DCMI_Stop(&hdcmi); // 2. 等待当前帧结束(若正在传输) while (HAL_DCMI_IsReady(&hdcmi) != HAL_OK) {} // 3. 重新配置传感器(I2C 写 OV5640 寄存器) ov5640_set_resolution(new_width, new_height); // 4. 重新配置 DCMI 时序参数 hdcmi.Init.HSPolarity = DCMI_HSPOLARITY_LOW; hdcmi.Init.VSPolarity = DCMI_VSPOLARITY_LOW; hdcmi.Init.PCKPolarity = DCMI_PCKPOLARITY_RISING; hdcmi.Init.ExtendedDataMode = DCMI_EXTEND_DATA_8B; hdcmi.Init.CaptureRate = DCMI_CAPTURE_RATE_ALL_FRAME; hdcmi.Init.HSyncPulse = 0; hdcmi.Init.VSyncPulse = 0; HAL_DCMI_Init(&hdcmi); // 5. 重新配置 DMA 缓冲区(双缓冲交替) HAL_DCMI_Start_DMA(&hdcmi, DCMI_MODE_CONTINUOUS, (uint32_t)buffer_a, (uint32_t)buffer_b); } ``` ### 4.2 方案二:降低 PCLK 切换速率 在传感器端,通过 I2C 写入 OV5640 的 PLL 寄存器,使 PCLK 变化更平缓。例如,先设置中间频率,再设置目标频率,分两步切换。 ```c // 分步切换 PCLK void ov5640_set_pclk_smooth(uint32_t target_freq) { uint32_t mid_freq = (target_freq > 24) ? 24 : target_freq/2; ov5640_write_reg(0x3035, mid_freq); // 中间频率 delay_ms(10); // 等待稳定 ov5640_write_reg(0x3035, target_freq); // 目标频率 } ``` ### 4.3 方案三:动态调整 FIFO 阈值 DCMI 的 FIMR 寄存器可设置 FIFO 阈值(0-3),对应 1/4、1/2、3/4、满。在切换期间,将阈值调低(如 1/4),使 DMA 更早触发,减少 FIFO 堆积。 ```c // 设置 FIFO 阈值为 1/4 void dcmi_set_fifo_threshold(uint32_t threshold) { MODIFY_REG(hdcmi.Instance->FIMR, DCMI_FIMR_FIFO_TH, threshold << DCMI_FIMR_FIFO_TH_Pos); } // 切换时调用 dcmi_set_fifo_threshold(DCMI_FIFO_TH_1_4); ``` ## 5. 完整示例:切换分辨率并规避溢出 以下代码整合了上述方案,实现安全切换: ```c // 全局变量 DCMI_HandleTypeDef hdcmi; uint8_t buffer_a[1920*1080*2]; // 假设 RGB565 uint8_t buffer_b[1920*1080*2]; volatile uint32_t ovr_count = 0; // 初始化 DCMI 和 DMA(略) // 切换分辨率主函数 void safe_switch_resolution(uint32_t w, uint32_t h) { // 停止采集 HAL_DCMI_Stop(&hdcmi); // 等待 DMA 停止 while (HAL_DMA_GetState(&hdma_dcmi) != HAL_DMA_STATE_READY) {} // 配置传感器(分步 PCLK) ov5640_set_pclk_smooth(ov5640_get_pclk_for_res(w, h)); ov5640_set_resolution(w, h); // 重新初始化 DCMI hdcmi.Init.CaptureRate = DCMI_CAPTURE_RATE_ALL_FRAME; HAL_DCMI_Init(&hdcmi); // 设置低 FIFO 阈值 dcmi_set_fifo_threshold(DCMI_FIFO_TH_1_4); // 启动 DMA 双缓冲 HAL_DCMI_Start_DMA(&hdcmi, DCMI_MODE_CONTINUOUS, (uint32_t)buffer_a, (uint32_t)buffer_b); } // DCMI 中断回调 void HAL_DCMI_ErrorCallback(DCMI_HandleTypeDef *hdcmi) { if (__HAL_DCMI_GET_FLAG(hdcmi, DCMI_FLAG_OVR)) { __HAL_DCMI_CLEAR_FLAG(hdcmi, DCMI_FLAG_OVR); ovr_count++; // 可在此处重新同步 } } ``` ## 6. 注意事项 - **DMA 优先级**:将 DCMI 的 DMA 流优先级设为最高(DMA_PRIORITY_HIGH),减少总线竞争。 - **内存对齐**:DMA 缓冲区必须按 32 字节对齐,否则可能触发总线错误。 - **切换时序**:传感器切换后,必须等待至少 100ms 或几帧稳定后再启动 DCMI,否则仍可能溢出。 - **中断优先级**:DCMI 中断优先级应高于普通外设,但低于系统节拍,避免中断风暴。 - **调试建议**:使用 `ovr_count` 变量监控溢出次数,若不为 0,则需调整阈值或切换流程。 ## 7. 总结 FIFO 溢出在高速切换分辨率时几乎不可避免,但通过“先停后切”的流程、降低 PCLK 突变幅度、动态调整 FIFO 阈值,可以完全规避。实际项目中,建议结合双缓冲和阈值自适应,并监控溢出标志,确保系统稳定。希望本文的排查思路和代码能帮助你快速定位问题,让图像采集更加可靠。