STM32 DCMI + DMA 双缓冲:打造零 CPU 干预的 OV2640 图像采集系统
👁 1 阅读 · 2026-08-27 · 嵌入式
在嵌入式视觉应用中,图像采集常因 CPU 频繁介入而拖慢系统响应。本文深入讲解如何利用 STM32 的 DCMI 接口配合 DMA 双缓冲机制,实现 OV2640 摄像头图像的无 CPU 干预采集。文章涵盖 DCMI 与 DMA 的工作原理、双缓冲配置步骤、完整代码示例及关键注意事项,帮助开发者构建高效、实时的图像采集系统。
# 利用 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 仅在缓冲区切换时被短暂唤醒,极大降低了负载。本文提供的代码框架可直接应用于实际项目,开发者可根据具体需求调整缓冲区大小、图像格式和处理逻辑。这种设计在机器视觉、智能监控等场景中具有显著优势。