一、为什么需要DMA+空闲中断接收不定长数据
在嵌入式开发中,串口接收不定长数据是常见需求。传统逐字节中断方式占用CPU资源高,而DMA+空闲中断组合能大幅降低CPU负载:
- DMA自动将串口数据搬运到内存缓冲区,无需CPU干预
- 空闲中断(IDLE)在总线空闲时触发,标志一帧数据接收完成
- 结合两者可高效处理不定长数据帧
二、典型实现与隐藏陷阱
2.1 基础配置步骤
- 初始化USART,使能DMA接收(
HAL_UART_Receive_DMA) - 使能空闲中断(
__HAL_UART_ENABLE_IT(&huart, UART_IT_IDLE)) - 在USART中断服务函数中检测IDLE标志,计算接收长度
- 处理数据后重新启动DMA接收
2.2 常见陷阱:缓冲区切换错误
许多开发者会在空闲中断中直接调用HAL_UART_Receive_DMA重新启动接收,但忽略了DMA传输计数器的状态,导致:
- 数据覆盖:未及时处理的数据被新数据覆盖
-
长度计算错误:
__HAL_DMA_GET_COUNTER返回值未正确重置 - DMA指针未复位:重新启动时DMA从上次位置继续,而非缓冲区起始地址
根本原因:HAL库的HAL_UART_Receive_DMA在DMA传输完成前调用会先中止当前传输,但若未正确清除标志或重置计数器,可能引发竞争条件。
三、陷阱复现与原理分析
考虑以下典型错误代码:
void USART1_IRQHandler(void) {
if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) {
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
uint16_t len = RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
// 处理数据...
HAL_UART_Receive_DMA(&huart1, rx_buf, RX_BUFFER_SIZE); // 重新启动
}
HAL_UART_IRQHandler(&huart1);
}
问题在于:HAL_UART_Receive_DMA内部会调用HAL_DMA_Start_IT,但若DMA尚未完成上一次传输(计数器未归零),直接重启会导致DMA状态机混乱。更隐蔽的是,在中断中处理数据时若耗时较长,新数据可能已开始写入缓冲区,造成数据撕裂。
四、可靠修复方案:双缓冲区+状态机
4.1 设计思路
- 使用两个缓冲区交替接收,避免处理时被新数据覆盖
- 在空闲中断中仅切换缓冲区并标记事件,数据处理放到主循环
- 确保DMA重新启动前正确复位计数器
4.2 完整代码示例
#define BUF_SIZE 128
typedef struct {
uint8_t buf[2][BUF_SIZE];
volatile uint16_t len[2];
volatile uint8_t active; // 当前DMA写入的缓冲区索引
volatile uint8_t ready; // 待处理缓冲区索引,0xFF表示无
} UartDmaRing_t;
UartDmaRing_t uart_ring;
void UART_DMA_Init(void) {
uart_ring.active = 0;
uart_ring.ready = 0xFF;
HAL_UART_Receive_DMA(&huart1, uart_ring.buf[0], BUF_SIZE);
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
}
void USART1_IRQHandler(void) {
if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) {
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
// 停止DMA以获取准确长度
HAL_UART_DMAStop(&huart1);
uint16_t remain = __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
uint16_t len = BUF_SIZE - remain;
if(len > 0) {
uint8_t cur = uart_ring.active;
uart_ring.len[cur] = len;
uart_ring.ready = cur;
uart_ring.active = cur ^ 1; // 切换缓冲区
}
// 重新启动DMA到新缓冲区
HAL_UART_Receive_DMA(&huart1, uart_ring.buf[uart_ring.active], BUF_SIZE);
}
HAL_UART_IRQHandler(&huart1);
}
// 主循环中处理数据
void process_uart_data(void) {
if(uart_ring.ready != 0xFF) {
uint8_t idx = uart_ring.ready;
uint16_t len = uart_ring.len[idx];
// 处理uart_ring.buf[idx]中的len字节数据
// ...
uart_ring.ready = 0xFF;
}
}
4.3 关键修复点
- 先停止DMA再计算长度:避免计数器在读取过程中变化
- 双缓冲区交替:处理一个缓冲区时,DMA写入另一个,彻底避免覆盖
- 中断中只做标记:将耗时数据处理移至主循环,减少中断延迟
- 正确切换active索引:使用异或操作简洁切换
五、注意事项与优化建议
- 缓冲区大小权衡:根据最大帧长和数据处理速度选择,过小易溢出,过大浪费RAM
- 中断优先级:USART中断优先级应高于DMA,确保空闲中断及时响应
- 错误处理:添加DMA传输错误中断回调,检测溢出(ORE)并恢复
-
内存屏障:在中断与主循环共享变量时,使用
volatile并考虑编译器优化屏障 -
HAL库版本差异:不同版本HAL的
HAL_UART_DMAStop行为略有不同,建议测试验证 -
空闲中断标志清除:必须先读SR再读DR,或使用
__HAL_UART_CLEAR_IDLEFLAG宏
通过上述双缓冲区方案,可彻底解决DMA+空闲中断接收不定长数据时的缓冲区切换陷阱,实现稳定可靠的数据接收。
暂无评论,来抢沙发~