一、原理讲解:DMA+空闲中断为何需要边界处理

在STM32中,利用DMA接收串口数据并结合空闲中断(IDLE)是处理不定长数据的经典方案。其基本流程为:

  • 串口初始化时启动DMA接收,将数据搬运至缓冲区。
  • 使能空闲中断,当总线空闲时触发中断,此时DMA已接收完一帧数据。
  • 在中断中计算已接收数据长度,并通知应用层处理。

然而,实际应用中存在两个关键问题:

  1. 缓冲区边界问题:DMA通常配置为循环模式或普通模式。若为普通模式,每次接收前需重新设置DMA地址和长度;若为循环模式,DMA指针会回绕,导致数据覆盖。此外,空闲中断触发时,DMA可能尚未完成最后一次传输(尤其在高波特率下),直接读取长度可能不准确。
  2. 溢出风险:当应用层处理速度跟不上数据接收速度时,缓冲区可能被写满。若未及时处理,新数据将覆盖旧数据,造成数据丢失甚至解析错误。

因此,必须设计合理的缓冲区管理策略,包括边界检测、长度计算和溢出恢复机制。

二、配置步骤:CubeMX与代码初始化

2.1 CubeMX配置

  • 使能USART1,模式为Asynchronous。
  • 在DMA Settings中添加USART1_RX,模式选择Circular(循环模式),数据宽度Byte。
  • 使能USART1全局中断,并在NVIC中使能USART1中断。

2.2 初始化代码

#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 MX_USART1_UART_Init(void) {
  huart1.Instance = USART1;
  huart1.Init.BaudRate = 115200;
  huart1.Init.WordLength = UART_WORDLENGTH_8B;
  huart1.Init.StopBits = UART_STOPBITS_1;
  huart1.Init.Parity = UART_PARITY_NONE;
  huart1.Init.Mode = UART_MODE_TX_RX;
  huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart1.Init.OverSampling = UART_OVERSAMPLING_16;
  HAL_UART_Init(&huart1);

  // 启动DMA接收(循环模式)
  HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUFFER_SIZE);
  // 使能空闲中断
  __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
}

三、完整代码示例:边界处理与溢出恢复

3.1 空闲中断处理函数

在stm32f1xx_it.c中实现USART1_IRQHandler,调用HAL_UART_IRQHandler后检查空闲标志:

void USART1_IRQHandler(void) {
  HAL_UART_IRQHandler(&huart1);
  if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) {
    __HAL_UART_CLEAR_IDLEFLAG(&huart1);
    // 计算已接收数据长度
    uint16_t dma_remaining = __HAL_DMA_GET_COUNTER(huart1.hdmarx);
    static uint16_t last_pos = 0;
    uint16_t current_pos = RX_BUFFER_SIZE - dma_remaining;
    if (current_pos >= last_pos) {
      rx_len = current_pos - last_pos;
    } else {
      // 发生回绕,数据分两段
      rx_len = RX_BUFFER_SIZE - last_pos + current_pos;
    }
    // 标记数据就绪,通知应用层
    rx_flag = 1;
    last_pos = current_pos;
    // 若数据长度超过缓冲区一半,触发溢出恢复
    if (rx_len > RX_BUFFER_SIZE / 2) {
      // 可在此处增加错误计数或复位DMA
    }
  }
}

3.2 应用层处理与溢出恢复

主循环中检查rx_flag,处理数据并重置标志。为防止处理过程中新数据覆盖,采用双缓冲或拷贝方式:

void process_uart_data(void) {
  if (rx_flag) {
    rx_flag = 0;
    // 拷贝数据到临时缓冲区(避免处理时被DMA覆盖)
    uint8_t temp[RX_BUFFER_SIZE];
    uint16_t len = rx_len;
    // 根据last_pos计算起始地址
    static uint16_t last_pos = 0;
    if (last_pos + len <= RX_BUFFER_SIZE) {
      memcpy(temp, &rx_buffer[last_pos], len);
    } else {
      uint16_t first_part = RX_BUFFER_SIZE - last_pos;
      memcpy(temp, &rx_buffer[last_pos], first_part);
      memcpy(temp + first_part, rx_buffer, len - first_part);
    }
    // 处理temp中的数据
    parse_data(temp, len);
    // 更新last_pos
    last_pos = (last_pos + len) % RX_BUFFER_SIZE;
  }
}

3.3 溢出恢复策略

当检测到接收长度异常(如超过缓冲区80%)或DMA错误时,执行恢复:

void uart_overflow_recovery(void) {
  // 停止DMA
  HAL_UART_DMAStop(&huart1);
  // 清空缓冲区
  memset(rx_buffer, 0, RX_BUFFER_SIZE);
  // 重置状态
  rx_len = 0;
  rx_flag = 0;
  // 重新启动DMA接收
  HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUFFER_SIZE);
  // 重新使能空闲中断
  __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
}

四、注意事项

  • DMA模式选择:循环模式适合连续接收,但需自行管理指针回绕;普通模式需每次重新启动,适合低频率数据。
  • 空闲中断与DMA完成中断的竞态:高波特率下,空闲中断可能早于DMA传输完成触发,此时__HAL_DMA_GET_COUNTER返回值可能未更新。建议在中断中稍作延时或结合DMA传输完成中断判断。
  • 缓冲区大小:根据最大帧长和数据处理速度合理设置,通常为最大帧长的2倍以上。
  • 临界区保护:在读写rx_len和rx_flag时,若主循环与中断可能并发,需使用__disable_irq()/__enable_irq()或原子操作。
  • 溢出检测:定期检查DMA剩余计数,若长时间不变且数据未处理,说明处理阻塞,应触发恢复。
  • 错误处理:使能UART错误中断(如ORE、FE),在错误回调中执行恢复流程。

通过上述策略,可显著提升串口通信的鲁棒性,适应复杂嵌入式环境。