# 利用 STM32 DCMI 与 DMA 双缓冲实现无 CPU 干预的 OV2640 图像采集 ## 引言 在嵌入式视觉系统中,图像采集的实时性往往决定整体性能。传统方式下,CPU 需要频繁响应中断并搬运数据,导致大量开销。STM32 的 DCMI(Digital Camera Interface)配合 DMA 的双缓冲模式,可让图像数据直接由外设搬运至内存,全程无需 CPU 干预,极大提升系统效率。本文将以 OV2640 摄像头为例,详细讲解实现方案。 ## 原理概述 ### DCMI 接口 DCMI 是 STM32 系列(如 F4、F7、H7 等)提供的并行摄像头接口,支持 8/10/12/14 位数据宽度,可接收来自摄像头的同步信号(VSYNC、HSYNC、PIXCLK)和数据字节。它支持两种捕获模式:快照(单帧)和连续(视频流)。在连续模式下,DCMI 持续接收数据,并通过 DMA 将数据搬运至内存。 ### DMA 双缓冲 DMA 双缓冲(Double Buffer)模式允许 DMA 在两个内存缓冲区之间交替传输数据。当 DMA 正在填充缓冲区 A 时,CPU 可以处理缓冲区 B 中的数据;反之亦然。这样,CPU 无需等待 DMA 完成,也无需在每次传输后重新配置 DMA,实现真正的零干预。 ### 系统架构 - **OV2640**:输出 JPEG 或 RGB565 数据,通过 DCMI 接口连接。 - **DCMI**:接收数据并触发 DMA 请求。 - **DMA**:在双缓冲模式下,将数据从 DCMI 数据寄存器搬运至内存。 - **CPU**:仅在 DMA 传输完成中断时切换缓冲区,并处理已填充的数据。 ## 硬件连接 | OV2640 引脚 | STM32 引脚 | 说明 | |------------|------------|------| | D0-D7 | PD0-PD7 | 数据线 | | PCLK | PA6 | 像素时钟 | | HSYNC | PA4 | 行同步 | | VSYNC | PA5 | 帧同步 | | SCL/SDA | PB6/PB7 | I2C 控制 | | XCLK | PA8 | 主时钟(由定时器产生) | 注意:具体引脚因芯片型号而异,请参考数据手册。 ## 配置步骤 ### 1. 初始化时钟和 GPIO 首先,使能 DCMI、DMA、GPIO 和 I2C 时钟,并配置相关引脚为复用功能。 ```c void GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_DCMI_CLK_ENABLE(); __HAL_RCC_DMA2_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOD_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); // 数据线 PD0-PD7 GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; GPIO_InitStruct.Alternate = GPIO_AF13_DCMI; HAL_GPIO_Init(GPIOD, &GPIO_InitStruct); // 控制线 PA4(HSYNC), PA5(VSYNC), PA6(PCLK) GPIO_InitStruct.Pin = GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6; GPIO_InitStruct.Alternate = GPIO_AF13_DCMI; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // I2C 引脚 PB6/PB7 GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode = GPIO_MODE_AF_OD; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Alternate = GPIO_AF4_I2C1; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); } ``` ### 2. 配置 DCMI 设置 DCMI 为连续模式,8 位数据宽度,并启用硬件同步。 ```c void DCMI_Init(void) { DCMI_HandleTypeDef hdcmi; hdcmi.Instance = DCMI; hdcmi.Init.SynchroMode = DCMI_SYNCHRO_HARDWARE; // 硬件同步 hdcmi.Init.PCKPolarity = DCMI_PCKPOLARITY_RISING; // 像素时钟上升沿采样 hdcmi.Init.VSPolarity = DCMI_VSPOLARITY_LOW; // VSYNC 低有效 hdcmi.Init.HSPolarity = DCMI_HSPOLARITY_LOW; // HSYNC 低有效 hdcmi.Init.ExtendedDataMode = DCMI_EXTEND_DATA_8B; hdcmi.Init.CaptureMode = DCMI_MODE_CONTINUOUS; // 连续模式 hdcmi.Init.ByteSelectMode = DCMI_BSM_ALL; hdcmi.Init.LineSelectMode = DCMI_LSM_ALL; hdcmi.Init.HalfLine = DCMI_HALFLINE_DISABLE; HAL_DCMI_Init(&hdcmi); } ``` ### 3. 配置 DMA 双缓冲 使用 DMA2 Stream1(DCMI 专用),设置双缓冲模式,内存地址为两个缓冲区。 ```c #define BUFFER_SIZE (320 * 240 * 2) // 假设 RGB565 格式,320x240 uint8_t bufferA[BUFFER_SIZE]; uint8_t bufferB[BUFFER_SIZE]; void DMA_Init(void) { DMA_HandleTypeDef hdma; hdma.Instance = DMA2_Stream1; hdma.Init.Channel = DMA_CHANNEL_1; hdma.Init.Direction = DMA_PERIPH_TO_MEMORY; hdma.Init.PeriphInc = DMA_PINC_DISABLE; hdma.Init.MemInc = DMA_MINC_ENABLE; hdma.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD; hdma.Init.MemDataAlignment = DMA_MDATAALIGN_WORD; hdma.Init.Mode = DMA_CIRCULAR; // 循环模式,配合双缓冲 hdma.Init.Priority = DMA_PRIORITY_HIGH; hdma.Init.FIFOMode = DMA_FIFOMODE_DISABLE; HAL_DMA_Init(&hdma); // 关联 DMA 到 DCMI __HAL_LINKDMA(&hdcmi, DMA_Handle, hdma); // 配置双缓冲 HAL_DMAEx_MultiBufferStart(&hdma, (uint32_t)&DCMI->DR, (uint32_t)bufferA, (uint32_t)bufferB, BUFFER_SIZE); } ``` ### 4. 启动捕获 在配置完 OV2640 后,启动 DCMI 捕获。 ```c HAL_DCMI_Start_DMA(&hdcmi, DCMI_MODE_CONTINUOUS, (uint32_t)bufferA, BUFFER_SIZE); ``` 注意:`HAL_DCMI_Start_DMA` 内部会启动 DMA,但双缓冲模式需要额外调用 `HAL_DMAEx_MultiBufferStart` 或使用 HAL 库的 `HAL_DCMI_Start_DMA` 配合 `HAL_DMAEx_ChangeMemory` 实现切换。推荐使用 HAL 库的 `HAL_DCMI_Start_DMA`,它内部支持双缓冲,但需在中断回调中切换内存。 ## 完整代码示例 以下是一个简化的示例,展示了如何配置并使用双缓冲。 ```c // 全局变量 DCMI_HandleTypeDef hdcmi; DMA_HandleTypeDef hdma; volatile uint8_t frame_ready = 0; volatile uint8_t active_buffer = 0; // 缓冲区定义 #define IMG_WIDTH 320 #define IMG_HEIGHT 240 #define IMG_SIZE (IMG_WIDTH * IMG_HEIGHT * 2) // RGB565 uint8_t bufferA[IMG_SIZE]; uint8_t bufferB[IMG_SIZE]; // 初始化函数 void DCMI_DMA_Init(void) { // 时钟使能 __HAL_RCC_DCMI_CLK_ENABLE(); __HAL_RCC_DMA2_CLK_ENABLE(); // GPIO 配置(略,见上文) // DCMI 配置 hdcmi.Instance = DCMI; hdcmi.Init.SynchroMode = DCMI_SYNCHRO_HARDWARE; hdcmi.Init.PCKPolarity = DCMI_PCKPOLARITY_RISING; hdcmi.Init.VSPolarity = DCMI_VSPOLARITY_LOW; hdcmi.Init.HSPolarity = DCMI_HSPOLARITY_LOW; hdcmi.Init.ExtendedDataMode = DCMI_EXTEND_DATA_8B; hdcmi.Init.CaptureMode = DCMI_MODE_CONTINUOUS; hdcmi.Init.ByteSelectMode = DCMI_BSM_ALL; hdcmi.Init.LineSelectMode = DCMI_LSM_ALL; hdcmi.Init.HalfLine = DCMI_HALFLINE_DISABLE; HAL_DCMI_Init(&hdcmi); // DMA 配置 hdma.Instance = DMA2_Stream1; hdma.Init.Channel = DMA_CHANNEL_1; hdma.Init.Direction = DMA_PERIPH_TO_MEMORY; hdma.Init.PeriphInc = DMA_PINC_DISABLE; hdma.Init.MemInc = DMA_MINC_ENABLE; hdma.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD; hdma.Init.MemDataAlignment = DMA_MDATAALIGN_WORD; hdma.Init.Mode = DMA_CIRCULAR; hdma.Init.Priority = DMA_PRIORITY_HIGH; HAL_DMA_Init(&hdma); // 连接 DMA 到 DCMI __HAL_LINKDMA(&hdcmi, DMA_Handle, hdma); // 启动双缓冲 DMA HAL_DMAEx_MultiBufferStart(&hdma, (uint32_t)&DCMI->DR, (uint32_t)bufferA, (uint32_t)bufferB, IMG_SIZE); // 启动 DCMI 捕获 HAL_DCMI_Start_DMA(&hdcmi, DCMI_MODE_CONTINUOUS, (uint32_t)bufferA, IMG_SIZE); // 使能 DMA 传输完成中断 HAL_NVIC_EnableIRQ(DMA2_Stream1_IRQn); HAL_NVIC_SetPriority(DMA2_Stream1_IRQn, 0, 0); } // DMA 中断回调 void DMA2_Stream1_IRQHandler(void) { HAL_DMA_IRQHandler(&hdma); } void HAL_DMAEx_MultiBufferCpltCallback(DMA_HandleTypeDef *hdma) { // 当当前缓冲区填满时,切换缓冲区 if (hdma->Instance == DMA2_Stream1) { // 切换内存地址 if (active_buffer == 0) { HAL_DMAEx_ChangeMemory(hdma, (uint32_t)bufferB, MEMORY0); active_buffer = 1; } else { HAL_DMAEx_ChangeMemory(hdma, (uint32_t)bufferA, MEMORY0); active_buffer = 0; } frame_ready = 1; // 通知主循环处理数据 } } // 主循环 int main(void) { HAL_Init(); SystemClock_Config(); GPIO_Init(); I2C_Init(); OV2640_Init(); // 配置 OV2640 寄存器 DCMI_DMA_Init(); while (1) { if (frame_ready) { frame_ready = 0; // 处理当前缓冲区数据(例如图像处理、显示、传输) if (active_buffer == 1) { // bufferA 已填满,可处理 process_image(bufferA); } else { // bufferB 已填满 process_image(bufferB); } } } } ``` ## 注意事项 - **缓冲区大小**:必须与图像尺寸匹配,且建议按 4 字节对齐,以满足 DMA 要求。 - **中断优先级**:DMA 中断应设为较高优先级,避免数据覆盖。 - **同步信号极性**:务必根据 OV2640 数据手册配置 VSYNC/HSYNC 极性,否则采集图像会错乱。 - **DCMI 时钟**:确保 PIXCLK 频率在 DCMI 允许范围内(通常不超过 25MHz)。 - **双缓冲切换**:在回调中切换内存时,要确保 DMA 已停止当前传输,否则可能出错。HAL 库的 `HAL_DMAEx_ChangeMemory` 会处理,但需注意时序。 - **内存对齐**:使用 `__attribute__((aligned(4)))` 或 `ALIGN_32BYTES` 宏确保缓冲区对齐。 - **JPEG 格式**:若 OV2640 输出 JPEG,缓冲区大小需根据最大帧大小预留,且 DCMI 需配置为可变长度模式(需额外处理)。 ## 总结 通过 DCMI 与 DMA 双缓冲的配合,STM32 可以完全自主地接收摄像头数据,CPU 仅在缓冲区切换时被短暂唤醒,极大降低了负载。本文提供的代码框架可直接应用于实际项目,开发者可根据具体需求调整缓冲区大小、图像格式和处理逻辑。这种设计在机器视觉、智能监控等场景中具有显著优势。