STM32 环形缓冲区无锁设计:DMA 半传输与完成中断的实战指南
👁 3 阅读 · 2026-08-27 · 嵌入式
在嵌入式系统中,DMA 是高效数据传输的核心,而环形缓冲区则是处理连续数据流的经典方案。本文将深入探讨如何利用 STM32 的 DMA 传输完成中断与半传输中断,实现一个无锁(Lock-Free)的环形缓冲区设计。通过巧妙利用双缓冲机制,我们可以在中断服务程序(ISR)与主循环之间安全地交换数据,避免竞态条件,同时最大化 CPU 效率。文中将涵盖原理剖析、寄存器配置、完整代码示例及关键注意事项,帮助开发者构建高性能的数据采集或通信系统。
# 基于 STM32 的 DMA 传输完成中断与半传输中断在环形缓冲区中的无锁设计
## 1. 引言:为什么需要无锁环形缓冲区?
在嵌入式实时系统中,数据采集(如 ADC、UART、SPI)通常依赖 DMA 将外设数据传输到内存。传统做法是使用一个简单的线性缓冲区,当 DMA 传输完成后触发中断,CPU 在中断中处理数据。但这种方式存在两个问题:
- **中断频率高**:每次传输完成都触发中断,CPU 频繁介入,影响实时性。
- **数据覆盖风险**:若 CPU 处理不及时,新数据可能覆盖未处理的数据。
环形缓冲区(Ring Buffer)通过循环读写指针解决了数据覆盖问题,但多线程(或中断与主循环)访问时仍需同步机制,如关中断或互斥锁。然而,锁会引入阻塞和优先级反转,在实时系统中可能致命。
**无锁设计**利用硬件特性(DMA 双缓冲)和原子操作,避免显式锁,实现高效、安全的并发访问。STM32 的 DMA 控制器支持传输完成中断(TC)和半传输中断(HT),这为无锁环形缓冲区提供了天然基础。
## 2. 原理:DMA 半传输与完成中断如何实现无锁?
### 2.1 环形缓冲区与 DMA 的结合
设想一个环形缓冲区,大小为 `BUF_SIZE`(通常为 2 的幂,便于取模)。我们将其划分为两个等大的半区:`[0, BUF_SIZE/2)` 和 `[BUF_SIZE/2, BUF_SIZE)`。DMA 配置为循环模式(Circular Mode),从外设(如 ADC)连续写入缓冲区。
- 当 DMA 写满前半区时,触发**半传输中断(HT)**。
- 当 DMA 写满整个缓冲区(即后半区也满)时,触发**传输完成中断(TC)**。
此时,CPU 可以在中断中处理已满的半区数据,而 DMA 继续写入另一个半区。由于两个半区在时间上交替使用,且中断处理速度远快于 DMA 填充速度,因此不会发生数据覆盖。
### 2.2 无锁的关键:读写指针的原子性
在无锁设计中,我们维护两个索引:
- `read_index`:CPU 当前读取的位置(由主循环或中断更新)。
- `write_index`:DMA 当前写入的位置(由硬件更新,但我们可以通过中断标志推断)。
由于 DMA 是硬件写入,我们无法直接读取其内部指针。但通过中断标志,我们可以知道 DMA 已写满哪个半区。因此,我们将 `read_index` 设计为仅由 CPU 更新,而 `write_index` 隐含在中断状态中。这样,CPU 在中断中处理数据时,只需更新 `read_index`,而主循环读取时,只需检查 `read_index` 与中断状态,无需锁。
**关键点**:确保对 `read_index` 的读写是原子的(在单核 Cortex-M 上,32 位对齐的读写是原子的),并且使用 `volatile` 修饰,防止编译器优化。
## 3. 配置步骤:以 STM32F4 系列为例
### 3.1 硬件资源
- 外设:ADC1(或 UART、SPI),启用 DMA2 Stream 0(示例)。
- 缓冲区:`uint16_t buffer[BUFFER_SIZE]`,其中 `BUFFER_SIZE = 512`,半区大小 `HALF_SIZE = 256`。
### 3.2 初始化 DMA
```c
#define BUFFER_SIZE 512
#define HALF_SIZE (BUFFER_SIZE / 2)
volatile uint16_t buffer[BUFFER_SIZE];
volatile uint8_t half_ready = 0; // 0: 无半区就绪, 1: 前半区就绪, 2: 后半区就绪
void DMA_Init(void) {
// 使能 DMA2 时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_Channel = DMA_Channel_0; // ADC1 映射到 DMA2 Stream0 Channel0
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)buffer;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;
DMA_InitStructure.DMA_BufferSize = BUFFER_SIZE;
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_Mode = DMA_Mode_Circular; // 循环模式
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;
DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
DMA_Init(DMA2_Stream0, &DMA_InitStructure);
// 使能半传输和传输完成中断
DMA_ITConfig(DMA2_Stream0, DMA_IT_HT | DMA_IT_TC, ENABLE);
// 使能 DMA 流
DMA_Cmd(DMA2_Stream0, ENABLE);
}
```
### 3.3 中断服务程序(ISR)
```c
void DMA2_Stream0_IRQHandler(void) {
if (DMA_GetITStatus(DMA2_Stream0, DMA_IT_HT)) {
DMA_ClearITPendingBit(DMA2_Stream0, DMA_IT_HT);
half_ready = 1; // 前半区已满
}
if (DMA_GetITStatus(DMA2_Stream0, DMA_IT_TC)) {
DMA_ClearITPendingBit(DMA2_Stream0, DMA_IT_TC);
half_ready = 2; // 后半区已满
}
}
```
### 3.4 主循环处理数据
```c
void ProcessData(uint16_t *data, uint16_t size) {
// 处理数据,例如计算平均值或发送到上位机
}
int main(void) {
// 系统初始化...
DMA_Init();
ADC_Init(); // 配置 ADC 并启动 DMA 传输
while (1) {
if (half_ready == 1) {
ProcessData((uint16_t*)buffer, HALF_SIZE); // 处理前半区
half_ready = 0;
} else if (half_ready == 2) {
ProcessData((uint16_t*)buffer + HALF_SIZE, HALF_SIZE); // 处理后半区
half_ready = 0;
}
// 其他任务...
}
}
```
## 4. 完整代码示例(基于 HAL 库)
以下使用 STM32 HAL 库,更通用。
```c
// 缓冲区定义
#define BUFFER_SIZE 512
#define HALF_SIZE (BUFFER_SIZE / 2)
uint16_t buffer[BUFFER_SIZE];
volatile uint8_t half_ready = 0;
// DMA 初始化(HAL)
void MX_DMA_Init(void) {
__HAL_RCC_DMA2_CLK_ENABLE();
hdma_adc.Instance = DMA2_Stream0;
hdma_adc.Init.Channel = DMA_CHANNEL_0;
hdma_adc.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_adc.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_adc.Init.MemInc = DMA_MINC_ENABLE;
hdma_adc.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
hdma_adc.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
hdma_adc.Init.Mode = DMA_CIRCULAR;
hdma_adc.Init.Priority = DMA_PRIORITY_HIGH;
hdma_adc.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
HAL_DMA_Init(&hdma_adc);
// 关联 DMA 到 ADC
__HAL_LINKDMA(&hadc, DMA_Handle, hdma_adc);
// 使能中断
HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn);
}
// 中断回调(HAL 会自动调用)
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc) {
half_ready = 1;
}
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
half_ready = 2;
}
// 主循环
int main() {
HAL_Init();
// ... 时钟、GPIO、ADC 初始化 ...
MX_DMA_Init();
HAL_ADC_Start_DMA(&hadc, (uint32_t*)buffer, BUFFER_SIZE);
while (1) {
if (half_ready == 1) {
ProcessData(buffer, HALF_SIZE);
half_ready = 0;
} else if (half_ready == 2) {
ProcessData(buffer + HALF_SIZE, HALF_SIZE);
half_ready = 0;
}
}
}
```
## 5. 注意事项与优化建议
- **缓冲区大小**:必须为 2 的幂,以便使用位运算代替取模(`index & (SIZE-1)`),提高效率。
- **内存对齐**:确保缓冲区地址按 4 字节对齐,避免 DMA 访问错误。
- **中断优先级**:DMA 中断应设置为较高优先级,确保及时处理,但不要高于系统节拍,以免影响其他实时任务。
- **数据一致性**:在中断中修改 `half_ready` 时,主循环读取时需使用 `volatile`,并考虑内存屏障(在 Cortex-M 上通常不需要,但若使用编译器优化,可加 `__DMB()`)。
- **处理时间**:确保 `ProcessData` 的执行时间远小于 DMA 填充一个半区的时间,否则可能丢失数据。若处理耗时,可考虑将数据拷贝到另一缓冲区,或使用双缓冲加标志。
- **扩展性**:若需处理更多数据,可增加半区数量(如四分之一中断),但需注意 DMA 中断类型有限(HT 和 TC),可结合定时器或外部中断。
## 6. 总结
通过利用 STM32 DMA 的半传输和完成中断,我们实现了一个高效的无锁环形缓冲区。该设计避免了锁的阻塞,降低了中断频率,且代码简洁。它特别适用于高速数据采集(如音频、传感器)和通信协议解析(如 UART 接收不定长数据)。开发者可根据实际需求调整缓冲区大小和中断处理逻辑,轻松移植到其他 STM32 系列。
掌握这一技巧,将使你的嵌入式系统在数据吞吐量和实时性上迈上一个新台阶。