一、原理讲解:DMA+空闲中断如何接收不定长数据
在STM32中,使用DMA接收串口数据可以大幅减轻CPU负担。结合串口空闲中断(IDLE),可以实现不定长数据的接收:
- DMA循环模式或普通模式下,串口接收到的数据自动搬运到缓冲区。
- 当总线空闲(一段时间没有新数据)时,触发IDLE中断。
- 在IDLE中断中计算已接收的数据长度,并处理数据。
关键点:HAL库提供了HAL_UARTEx_ReceiveToIdle_DMA函数(部分系列支持),但很多开发者仍使用传统方式:HAL_UART_Receive_DMA + 手动开启IDLE中断。
二、常见问题现象
-
溢出(Overrun):数据丢失,
HAL_UART_ErrorCallback被调用,错误标志ORE置位。 - 半包:一帧数据被分成多次接收,或IDLE中断触发时数据长度不正确。
- 数据错位:缓冲区内容与预期不符,甚至出现旧数据。
三、根本原因分析
3.1 溢出问题
- DMA传输速度跟不上串口接收速度(高波特率下尤其明显)。
- 中断处理时间过长,导致DMA未及时重新使能。
- 未正确处理ORE错误,未清除标志导致后续接收异常。
3.2 半包问题
- IDLE中断触发时,DMA可能还未完成最后一次传输(尤其在高波特率下,IDLE中断与DMA传输存在竞争)。
- 计算长度时未考虑DMA当前剩余计数(
__HAL_DMA_GET_COUNTER)的瞬时值。 - 缓冲区未及时清零或索引未重置,导致数据拼接错误。
四、配置步骤(以STM32CubeMX为例)
- 使能USART1,配置波特率115200,8N1。
- 使能USART1全局中断(NVIC中使能USART1中断)。
- 添加DMA通道:USART1_RX,模式为循环模式(Circular)或普通模式(Normal),数据宽度Byte。
- 生成代码,在
main.c中添加用户代码。
五、完整代码示例
5.1 初始化与启动接收
#define RX_BUFFER_SIZE 256
uint8_t rx_buffer[RX_BUFFER_SIZE];
volatile uint16_t rx_len = 0;
volatile uint8_t rx_flag = 0;
void UART_Start_Receive(void)
{
// 启动DMA接收,循环模式
HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUFFER_SIZE);
// 使能空闲中断
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
}
5.2 空闲中断处理(在stm32f1xx_it.c中)
void USART1_IRQHandler(void)
{
HAL_UART_IRQHandler(&huart1);
// 检查空闲中断
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(huart1.hdmarx);
rx_len = RX_BUFFER_SIZE - remaining;
rx_flag = 1;
// 重新启动接收
HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUFFER_SIZE);
}
}
5.3 主循环处理数据
while (1)
{
if (rx_flag)
{
rx_flag = 0;
// 处理rx_buffer中rx_len字节的数据
ProcessData(rx_buffer, rx_len);
// 可选:清空缓冲区
memset(rx_buffer, 0, RX_BUFFER_SIZE);
}
}
5.4 错误回调处理
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1)
{
// 清除溢出错误
__HAL_UART_CLEAR_OREFLAG(huart);
// 重新启动接收
HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUFFER_SIZE);
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
}
}
六、注意事项与排查技巧
- 使用循环模式DMA:避免频繁重启DMA,减少竞争。但需注意缓冲区覆盖问题,建议配合双缓冲或及时处理。
- IDLE中断中先停止DMA:确保获取的剩余计数准确,防止半包。
- 高波特率下:考虑提高中断优先级,或使用DMA双缓冲(如STM32F4/F7的DMA双缓冲模式)。
-
错误处理:务必在
HAL_UART_ErrorCallback中清除ORE标志并重启接收,否则后续数据全部丢失。 -
调试技巧:使用逻辑分析仪或串口助手发送固定长度数据,观察
rx_len是否与发送长度一致;若不一致,检查DMA计数器读取时机。 - 避免在中断中处理数据:仅置标志,主循环处理,减少中断延迟。
- 缓冲区对齐:确保DMA缓冲区地址对齐,避免传输错误。
七、总结
DMA+空闲中断是接收不定长数据的经典方案,但溢出与半包问题往往源于细节处理不当。通过合理配置DMA模式、在IDLE中断中正确计算长度、及时处理错误标志,可以稳定可靠地接收数据。建议在高要求场景下使用HAL库的HAL_UARTEx_ReceiveToIdle_DMA函数(若支持),它内部已处理了大部分边界情况。