一、问题现象与背景
在嵌入式开发中,使用STM32的串口接收不定长数据时,DMA+空闲中断(IDLE) 是一种高效且常见的方案。但许多开发者会遇到:数据量较大或发送间隔极短时,偶尔丢失一帧数据,或接收到的数据长度不正确。
典型现象:
- 连续发送多帧数据,中间某帧丢失
- 接收缓冲区数据被覆盖,长度字段异常
- 空闲中断触发后,DMA计数器(NDTR)值不符合预期
二、原理回顾:DMA+空闲中断如何工作
- DMA循环模式:串口接收DMA配置为循环模式,DMA自动将数据搬运到缓冲区,无需CPU干预。
- 空闲中断:当串口总线空闲(一个字节时间无新数据)时触发IDLE中断,此时可读取DMA剩余计数(NDTR)计算已接收长度。
-
常规处理流程:在IDLE中断中,
len = BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart_rx),然后处理数据,最后重新设置DMA接收(或使用循环模式自动继续)。
三、丢包根源分析
3.1 DMA传输完成与空闲中断的竞争
当一帧数据长度恰好等于DMA缓冲区大小时,DMA传输完成中断(TC)与IDLE中断可能同时或先后触发。若在TC中断中重置了DMA,而IDLE中断又读取了错误的NDTR值,会导致长度计算错误或数据被覆盖。
3.2 缓冲区管理不当
- 使用单缓冲区且未及时处理数据,新数据覆盖旧数据。
- 在IDLE中断中处理数据耗时过长,期间DMA继续搬运新数据,导致缓冲区被改写。
3.3 中断优先级配置错误
- IDLE中断优先级低于DMA中断,导致DMA中断抢占,NDTR值在读取前已被修改。
- 未开启DMA传输完成中断,无法处理缓冲区满的情况。
3.4 HAL库的“半传输”与“传输完成”回调干扰
HAL库默认开启DMA半传输中断和传输完成中断,若用户未正确处理回调,可能误清除标志或重置DMA,破坏接收流程。
四、排查步骤
-
确认DMA模式:检查
hdma_usart_rx.Init.Mode是否为DMA_CIRCULAR(循环模式)。 - 检查中断优先级:确保USART全局中断优先级高于或等于DMA中断,避免NDTR读取被抢占。
- 验证IDLE中断处理:在IDLE中断中打印NDTR值,观察是否与预期长度一致。
- 监控DMA传输完成中断:若使能了TC中断,检查是否在TC回调中错误重置了DMA。
- 使用双缓冲区或环形缓冲区:避免数据处理期间新数据覆盖。
五、修复方案与完整代码
5.1 配置要点
- DMA配置为循环模式,数据宽度字节。
- 开启USART的IDLE中断,关闭DMA半传输/传输完成中断(除非必要)。
- 在IDLE中断中快速读取数据并拷贝到安全缓冲区,再处理。
5.2 代码示例
#define RX_BUFFER_SIZE 256
uint8_t rx_dma_buffer[RX_BUFFER_SIZE];
uint8_t rx_process_buffer[RX_BUFFER_SIZE];
volatile uint16_t rx_len = 0;
volatile uint8_t rx_flag = 0;
// 串口初始化(略),确保调用HAL_UART_Receive_DMA(&huart1, rx_dma_buffer, RX_BUFFER_SIZE);
// 并开启IDLE中断:__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
// IDLE中断处理函数(在stm32f1xx_it.c中)
void USART1_IRQHandler(void)
{
if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE) != RESET)
{
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
// 停止DMA以获取准确长度(可选,循环模式下也可直接读)
HAL_UART_DMAStop(&huart1);
uint16_t remaining = __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
rx_len = RX_BUFFER_SIZE - remaining;
if(rx_len > 0)
{
memcpy(rx_process_buffer, rx_dma_buffer, rx_len);
rx_flag = 1;
}
// 重新启动DMA接收
HAL_UART_Receive_DMA(&huart1, rx_dma_buffer, RX_BUFFER_SIZE);
}
HAL_UART_IRQHandler(&huart1);
}
// 主循环中处理数据
if(rx_flag)
{
rx_flag = 0;
// 处理rx_process_buffer中的rx_len字节数据
// ...
}
5.3 关键改进
- 停止DMA再读取NDTR:避免读取过程中DMA继续搬运导致长度误差。
- 使用独立处理缓冲区:中断中仅拷贝数据,主循环处理,减少中断耗时。
- 重新启动DMA:确保下一次接收正常。
六、注意事项
- 若数据帧间隔极短,建议增大DMA缓冲区或使用双缓冲(DMA双缓冲模式需高级型号支持)。
- 避免在IDLE中断中调用耗时函数(如printf)。
- 检查HAL库版本,某些版本存在IDLE标志清除不彻底的问题,需手动清除。
- 若使用RTOS,注意中断与任务间的数据同步(如使用消息队列传递缓冲区指针)。
七、总结
DMA+空闲中断接收不定长数据是高效方案,但丢包问题多源于竞争条件和缓冲区管理。通过合理配置中断优先级、停止DMA后读取长度、使用独立处理缓冲区,可彻底解决丢包问题。实际项目中建议结合环形缓冲区与信号量,构建更健壮的接收框架。