# 用 DMA 环形缓冲区 + 空闲中断实现 STM32 不定长串口接收,避免丢帧的边界条件分析 ## 一、为什么需要 DMA + 空闲中断? 传统串口接收(逐字节中断)在高速或长数据帧时,CPU 频繁进出中断,容易导致丢帧。而 DMA 可以在无需 CPU 干预的情况下,将串口接收到的数据搬运到内存缓冲区。但 DMA 本身不知道一帧数据何时结束,因此需要配合**空闲中断(IDLE)**来检测帧边界。 **核心思路**: - DMA 持续将串口数据存入缓冲区(环形结构)。 - 当串口总线空闲(即一帧数据接收完毕)时,触发空闲中断。 - 在空闲中断中,根据 DMA 当前写入位置,计算本次接收的数据长度,并处理数据。 ## 二、环形缓冲区设计 环形缓冲区(Ring Buffer)是一种先进先出的数据结构,通过读写指针循环使用固定内存。 ```c #define BUFFER_SIZE 256 // 必须是 2 的幂,便于取模 uint8_t rx_buffer[BUFFER_SIZE]; volatile uint16_t read_index = 0; // 读指针(应用层使用) volatile uint16_t write_index = 0; // 写指针(DMA 更新) ``` **关键点**: - `write_index` 由 DMA 传输完成或空闲中断更新,表示 DMA 已写入的位置。 - `read_index` 由应用层读取数据时更新。 - 当 `read_index == write_index` 时,缓冲区为空。 ## 三、DMA 配置与中断协同 ### 1. DMA 配置(以 STM32F1 为例) ```c void UART_DMA_Config(void) { // 使能 DMA1 时钟 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_InitTypeDef DMA_InitStructure; DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)rx_buffer; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; 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_Byte; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 循环模式 DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel5, &DMA_InitStructure); // 使能 DMA 半传输中断和传输完成中断 DMA_ITConfig(DMA1_IT_TC5, ENABLE); DMA_ITConfig(DMA1_IT_HT5, ENABLE); // 使能 DMA 通道 DMA_Cmd(DMA1_Channel5, ENABLE); } ``` ### 2. 空闲中断配置 ```c void UART_IDLE_Config(void) { USART_ITConfig(USART1, USART_IT_IDLE, ENABLE); // 注意:空闲中断标志在 USART_SR 的 IDLE 位,需要读 SR 再读 DR 清除 } ``` ### 3. 中断服务函数(关键) ```c void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_IDLE) != RESET) { // 清除空闲中断标志(读 SR 再读 DR) USART_ReceiveData(USART1); // 获取当前 DMA 写入位置(即 DMA 当前传输计数) uint16_t dma_remaining = DMA_GetCurrDataCounter(DMA1_Channel5); uint16_t current_write = BUFFER_SIZE - dma_remaining; // 计算本次接收的数据长度(考虑环形回绕) uint16_t data_len; if (current_write >= write_index) { data_len = current_write - write_index; } else { data_len = BUFFER_SIZE - write_index + current_write; } // 注意:这里需要处理 data_len 为 0 的情况(可能只是空闲中断误触发) if (data_len > 0) { // 将数据交给应用层处理(例如存入队列或直接处理) ProcessReceivedData(rx_buffer, write_index, data_len); // 更新写指针 write_index = current_write; } } } ``` ## 四、边界条件分析(重点) ### 1. 缓冲区回绕(Wrap-around) 环形缓冲区最大的陷阱是**回绕**。当 DMA 写入地址到达缓冲区末尾时,会回绕到开头。此时计算数据长度必须分段处理: ```c // 假设 write_index = 200,current_write = 50,BUFFER_SIZE = 256 // 数据实际分为两段:200~255 和 0~50 // 长度 = (256 - 200) + 50 = 106 ``` ### 2. 半传输中断与空闲中断的竞争 DMA 循环模式下,当传输过半时触发半传输中断(HT),传输完成时触发传输完成中断(TC)。这两个中断可以用来处理数据,但**必须与空闲中断协调**,否则可能重复处理或漏处理。 **推荐策略**: - 半传输中断和传输完成中断中,仅更新 `write_index`(或直接处理数据),但要注意与空闲中断的互斥。 - 更简单的方法:**只使用空闲中断**,因为空闲中断已经能确定一帧结束,且 DMA 循环模式会持续接收,不会丢失数据(只要应用层处理速度足够快)。 ### 3. 数据溢出(Overrun) 如果应用层处理速度慢于 DMA 写入速度,缓冲区会被覆盖。此时需要检测溢出: ```c // 在空闲中断中,如果 data_len 大于 BUFFER_SIZE - 1,说明溢出 if (data_len >= BUFFER_SIZE) { // 溢出处理:丢弃旧数据或重置缓冲区 read_index = current_write; write_index = current_write; } ``` ### 4. 空闲中断误触发 在某些情况下(如线路噪声),空闲中断可能在没有完整帧时触发。此时 `data_len` 可能为 0 或极小,应过滤掉: ```c if (data_len == 0) return; // 忽略空帧 ``` ## 五、完整代码示例(简化版) ```c // main.c int main(void) { // 初始化时钟、GPIO、USART1、DMA(略) UART_DMA_Config(); UART_IDLE_Config(); while (1) { // 应用层从缓冲区读取数据 if (read_index != write_index) { uint8_t data = rx_buffer[read_index]; read_index = (read_index + 1) % BUFFER_SIZE; // 处理数据... } } } // 空闲中断处理(已包含边界处理) void ProcessReceivedData(uint8_t *buf, uint16_t start, uint16_t len) { // 示例:将数据打印或存储 for (uint16_t i = 0; i < len; i++) { uint16_t idx = (start + i) % BUFFER_SIZE; // 处理 buf[idx] } } ``` ## 六、注意事项与优化建议 - **缓冲区大小**:建议设为 2 的幂,这样 `(index + 1) & (BUFFER_SIZE - 1)` 比取模运算更快。 - **临界区保护**:在中断中更新 `write_index`,在主循环中读取,需考虑原子性。可使用 `__disable_irq()` 或使用 volatile 并确保读写顺序。 - **DMA 半传输中断的用途**:如果应用层需要实时处理数据,可以在半传输中断中处理前半部分,在传输完成中断中处理后半部分,但需注意与空闲中断的同步。 - **多帧连续接收**:空闲中断可以处理连续多帧,但若两帧间隔极短,可能合并为一帧。此时需要应用层自定义协议(如帧头帧尾)来区分。 - **性能考量**:空闲中断中避免耗时操作,仅更新指针和拷贝数据,实际处理放到主循环。 ## 七、总结 通过 DMA 环形缓冲区 + 空闲中断,可以实现高效的 STM32 不定长串口接收。关键在于理解环形缓冲区的回绕、DMA 计数器的更新时机,以及空闲中断与 DMA 中断的协同。本文分析的边界条件(回绕、溢出、误触发)是实际项目中常见的坑,掌握后能显著提升代码的健壮性。建议读者在开发板上实际测试,观察不同数据长度和频率下的行为,加深理解。