一、原理讲解: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为例)

  1. 使能USART1,配置波特率115200,8N1。
  2. 使能USART1全局中断(NVIC中使能USART1中断)。
  3. 添加DMA通道:USART1_RX,模式为循环模式(Circular)或普通模式(Normal),数据宽度Byte。
  4. 生成代码,在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函数(若支持),它内部已处理了大部分边界情况。