一、为什么需要半满中断与空闲中断协同?

在STM32中使用DMA接收串口数据时,传统做法是每收到一个字节就触发中断,但这会带来巨大的CPU开销。HAL库提供了两种更高效的机制:

  • 半满中断(Half Transfer):当DMA缓冲区接收数据达到一半时触发,适合处理定长或流式数据。
  • 空闲中断(IDLE):当串口总线在一个字节时间内无新数据时触发,适合处理不定长数据帧。

单独使用任一种都有局限:仅用半满中断无法及时处理短帧;仅用空闲中断在高速大数据量时可能因频繁中断而丢包。协同处理能兼顾实时性与效率:半满中断负责批量搬运,空闲中断负责帧尾判定。

二、协同处理原理

假设DMA接收缓冲区大小为BUFFER_SIZE,工作流程如下:

  1. DMA循环模式接收数据,计数器NDTR递减。
  2. 当接收量达到BUFFER_SIZE/2时触发半满中断,此时可处理前半部分数据(或仅作标记)。
  3. 当总线空闲时触发IDLE中断,此时根据NDTR计算已接收数据长度,处理剩余数据。
  4. 在中断服务程序中重置DMA计数器或重新启动接收,避免数据覆盖。

关键点:半满中断与空闲中断共享同一个DMA缓冲区,需通过状态变量协调读写位置,防止数据竞争。

三、CubeMX配置步骤

  1. 使能USART1,模式为Asynchronous,波特率115200。
  2. 在NVIC中使能USART1全局中断。
  3. 添加DMA通道:USART1_RX,模式为Circular,数据宽度Byte,优先级High。
  4. 生成代码后,在main.c中手动开启IDLE中断和DMA接收。

四、完整代码示例

#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 USART1_Init(void)
{
    // CubeMX已生成基础配置,此处补充中断开启
    __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
    HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUFFER_SIZE);
}

// 串口中断服务函数
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;
        
        // 重新启动DMA接收
        HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUFFER_SIZE);
    }
}

// DMA半满/完成回调(可选,用于流式处理)
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        // 前半部分数据已就绪,可在此处理
        // 注意:不要在此重启DMA,否则会破坏循环模式
    }
}

// 主循环中处理数据
void Process_Data(void)
{
    if (rx_flag) {
        rx_flag = 0;
        // 处理rx_buffer中长度为rx_len的数据
        // 例如:解析协议、存入队列等
    }
}

五、丢包排查指南

实际项目中常遇到“数据收不全”问题,按以下顺序排查:

  • 检查DMA模式:必须为Circular,否则一次接收后停止。
  • 检查中断优先级:USART中断优先级应高于或等于DMA中断,避免IDLE响应延迟。
  • 检查缓冲区大小:若数据帧长度接近缓冲区大小,半满中断与空闲中断可能冲突,建议缓冲区为最大帧长的2倍以上。
  • 检查HAL_UART_DMAStop调用时机:在IDLE中断中停止DMA会丢失后续字节,若数据流连续,应改用__HAL_DMA_DISABLE并快速重启,或使用双缓冲。
  • 检查__HAL_UART_CLEAR_IDLEFLAG顺序:必须先清标志再处理数据,否则可能重复进入中断。
  • 检查编译器优化:rx_len和rx_flag需加volatile,防止被优化。
  • 使用逻辑分析仪:确认实际波形与波特率误差,误差>3%会导致误码丢包。

六、注意事项

  • 半满中断与空闲中断同时使能时,若在空闲中断中重启DMA,需确保半满回调不会访问已失效的缓冲区。
  • 对于高速连续数据流,建议采用“双缓冲+空闲中断”方案,避免停止DMA造成的窗口丢失。
  • 调试时可在IDLE中断中翻转GPIO,用示波器观察中断响应时间,评估系统负载。
  • HAL库的HAL_UART_Receive_DMA在循环模式下不会自动重装计数器,需在IDLE中断中手动重启。

通过合理协同半满与空闲中断,并遵循上述排查方法,可构建出稳定可靠的串口接收框架,满足大多数嵌入式应用需求。