# 基于 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 系列。 掌握这一技巧,将使你的嵌入式系统在数据吞吐量和实时性上迈上一个新台阶。