一、原理讲解

1.1 DMA+空闲中断接收机制

  • DMA循环模式:将串口接收数据寄存器(DR)自动搬运到用户缓冲区,无需CPU干预。
  • 空闲中断(IDLE):当串口总线空闲(一个字节时间无新数据)时触发,标志一帧不定长数据接收完成。
  • 典型流程:初始化DMA为循环模式,开启串口空闲中断;在IDLE中断中计算已接收数据长度,处理数据,并重置DMA接收位置。

1.2 缓冲区边界问题

  • DMA循环模式下的覆盖风险:若数据处理不及时,DMA会回绕覆盖未处理数据。
  • 半传输/传输完成中断:HAL库提供HAL_UART_RxHalfCpltCallback和HAL_UART_RxCpltCallback,可用于分段处理,但需注意与IDLE中断的协同。
  • 长度计算:使用__HAL_DMA_GET_COUNTER(&hdma_usart_rx)获取剩余传输数量,结合缓冲区总大小计算已接收长度。

1.3 溢出检测与恢复

  • 溢出条件:当DMA写指针追上读指针(环形缓冲区满)时发生溢出。
  • 检测方法:维护读指针和写指针(或利用DMA计数器),比较距离判断是否溢出。
  • 恢复策略:溢出时丢弃旧数据或重置DMA,并通知应用层;建议使用双缓冲区或环形缓冲区+临界区保护。

二、配置步骤

2.1 CubeMX配置

  1. 启用USART1,模式为Asynchronous。
  2. 在DMA Settings中添加USART1_RX,模式为Circular,优先级High。
  3. 开启USART1全局中断(NVIC)。
  4. 生成代码,HAL库初始化DMA和串口。

2.2 关键代码初始化

#define RX_BUFFER_SIZE 256
uint8_t rx_buffer[RX_BUFFER_SIZE];
volatile uint16_t rx_read_pos = 0;
volatile uint8_t rx_overflow = 0;

void UART_Init(void) {
    HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUFFER_SIZE);
    __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
}

三、完整代码示例

3.1 空闲中断处理与数据提取

void USART1_IRQHandler(void) {
    if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) {
        __HAL_UART_CLEAR_IDLEFLAG(&huart1);
        UART_IdleCallback();
    }
    HAL_UART_IRQHandler(&huart1);
}

void UART_IdleCallback(void) {
    static uint16_t old_pos = 0;
    uint16_t curr_pos;
    uint16_t dma_remain = __HAL_DMA_GET_COUNTER(huart1.hdmarx);
    curr_pos = RX_BUFFER_SIZE - dma_remain;

    if (curr_pos != old_pos) {
        uint16_t len;
        if (curr_pos > old_pos) {
            len = curr_pos - old_pos;
            // 处理数据从 old_pos 到 curr_pos-1
            ProcessData(&rx_buffer[old_pos], len);
        } else {
            // 回绕情况
            len = RX_BUFFER_SIZE - old_pos;
            ProcessData(&rx_buffer[old_pos], len);
            len = curr_pos;
            ProcessData(&rx_buffer[0], len);
        }
        old_pos = curr_pos;
    }
}

3.2 溢出检测与恢复

// 在数据处理函数中检查读指针与写指针距离
void ProcessData(uint8_t *data, uint16_t len) {
    // 假设应用层维护读指针 rx_read_pos
    uint16_t write_pos = RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(huart1.hdmarx);
    uint16_t distance = (write_pos >= rx_read_pos) ? (write_pos - rx_read_pos) : (RX_BUFFER_SIZE - rx_read_pos + write_pos);
    if (distance > RX_BUFFER_SIZE - len) {
        // 溢出:丢弃最旧数据或重置
        rx_overflow = 1;
        rx_read_pos = write_pos; // 简单丢弃
    }
    // 正常处理...
}

// 溢出恢复:重新初始化DMA
void UART_Recover(void) {
    HAL_UART_DMAStop(&huart1);
    rx_read_pos = 0;
    rx_overflow = 0;
    HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUFFER_SIZE);
    __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
}

四、注意事项

  • 临界区保护:读写指针的修改需在中断与主循环间加锁(如__disable_irq())。
  • DMA计数器读取:__HAL_DMA_GET_COUNTER需在DMA使能时读取,否则值无效。
  • 空闲中断清除:必须使用__HAL_UART_CLEAR_IDLEFLAG,否则会连续触发。
  • 缓冲区大小:根据数据速率和處理延迟合理设置,建议≥最大帧长的2倍。
  • 溢出恢复:重置DMA会丢失当前数据,应在应用层允许时执行。
  • 调试技巧:使用GPIO翻转或打印溢出标志,辅助定位边界问题。

通过以上设计,可有效处理不定长数据的边界与溢出,提升串口通信的鲁棒性。