引言

STM32 的串口(USART/UART)在嵌入式项目中无处不在,但很多开发者仍采用逐字节中断收发,导致 CPU 在每次收发时都被打断,尤其在 921600 甚至更高波特率下,中断频率可达 90kHz,系统实时性严重受损。DMA(直接内存访问)可以解放 CPU,而环形缓冲区则解决数据连续性与边界问题。本文将带你设计一套基于 DMA + 环形缓冲区的串口驱动,实现高效、可靠的数据收发。

一、DMA 收发原理

1.1 DMA 工作模式

STM32 的 DMA 支持两种模式:

  • Normal 模式:传输完成后停止,需重新配置。
  • Circular 模式:传输完成后自动回绕到起始地址,持续工作,适合流式数据。

对于串口接收,我们使用 Circular 模式,DMA 将外设数据寄存器(DR)的数据源源不断写入内存缓冲区,无需 CPU 干预。发送则使用 Normal 模式,每次发送指定长度后停止,由 CPU 或中断触发下一次发送。

1.2 空闲中断(IDLE)

串口空闲中断在接收线上检测到空闲(即一帧数据结束)时触发,配合 DMA 可判断当前接收到的数据长度。这是实现不定长数据接收的关键。

二、环形缓冲区设计

环形缓冲区(Ring Buffer)是一种先进先出(FIFO)的数据结构,通过读写指针实现循环存储。在 DMA 接收中,缓冲区被 DMA 硬件写入,而应用程序通过读指针读取,两者互不干扰。

2.1 数据结构

#define UART_RX_BUF_SIZE  256   // 接收缓冲区大小(需为2的幂)
#define UART_TX_BUF_SIZE  256   // 发送缓冲区大小

typedef struct {
    uint8_t rx_buf[UART_RX_BUF_SIZE];   // DMA 接收缓冲区
    volatile uint16_t rx_read_index;    // 应用读指针
    uint16_t rx_last_index;             // 上次处理时的 DMA 写指针
    
    uint8_t tx_buf[UART_TX_BUF_SIZE];   // 软件发送缓冲区
    volatile uint16_t tx_write_index;   // 应用写指针
    volatile uint16_t tx_read_index;    // DMA 发送读指针
    volatile uint8_t tx_busy;           // 发送忙标志
} UART_Handle;

2.2 接收逻辑

DMA 在 Circular 模式下持续将数据写入 rx_buf,硬件写指针可通过 __HAL_DMA_GET_COUNTER() 获取剩余计数,从而算出当前写位置。我们利用空闲中断来更新 rx_last_index,并计算新数据长度。

void UART_RxIdleCallback(UART_Handle *huart) {
    uint16_t current_index = UART_RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(huart->hdmarx);
    uint16_t len;
    
    if (current_index >= huart->rx_last_index) {
        len = current_index - huart->rx_last_index;
    } else {
        len = UART_RX_BUF_SIZE - huart->rx_last_index + current_index; // 回绕
    }
    
    // 将新数据拷贝到应用层或直接处理
    ProcessReceivedData(&huart->rx_buf[huart->rx_last_index], len);
    
    huart->rx_last_index = current_index;
}

注意:rx_last_index 必须由中断更新,而 rx_read_index 用于应用层读取,两者分离避免竞争。

2.3 发送逻辑

发送采用软件缓冲区 + DMA Normal 模式。应用层将数据写入 tx_buf,然后启动 DMA 发送。发送完成中断中更新读指针,并检查是否有新数据待发。

void UART_SendData(UART_Handle *huart, uint8_t *data, uint16_t len) {
    // 将数据拷贝到 tx_buf(需处理回绕)
    for (uint16_t i = 0; i < len; i++) {
        huart->tx_buf[huart->tx_write_index++] = data[i];
        huart->tx_write_index &= (UART_TX_BUF_SIZE - 1); // 取模
    }
    
    if (!huart->tx_busy) {
        UART_StartTxDMA(huart);
    }
}

void UART_StartTxDMA(UART_Handle *huart) {
    uint16_t len;
    if (huart->tx_write_index >= huart->tx_read_index) {
        len = huart->tx_write_index - huart->tx_read_index;
    } else {
        len = UART_TX_BUF_SIZE - huart->tx_read_index; // 先发送到缓冲区尾
    }
    
    if (len > 0) {
        huart->tx_busy = 1;
        HAL_UART_Transmit_DMA(&huart->huart, &huart->tx_buf[huart->tx_read_index], len);
    }
}

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
    UART_Handle *my_uart = GetMyUart(huart); // 关联自定义结构
    my_uart->tx_read_index += my_uart->last_tx_len;
    my_uart->tx_read_index &= (UART_TX_BUF_SIZE - 1);
    
    if (my_uart->tx_write_index != my_uart->tx_read_index) {
        UART_StartTxDMA(my_uart); // 继续发送剩余数据
    } else {
        my_uart->tx_busy = 0;
    }
}

注意:last_tx_len 需在启动 DMA 前保存,因为 DMA 完成后无法获取传输长度。

三、配置步骤(以 STM32CubeMX + HAL 库为例)

  1. CubeMX 配置

    • 使能 USARTx,设置波特率、数据位等。
    • 添加 DMA 请求:USARTx_RX 选择 Circular 模式,USARTx_TX 选择 Normal 模式。
    • 使能 USARTx 全局中断和 DMA 中断。
  2. 初始化代码

    • main() 中调用 MX_USARTx_UART_Init()MX_DMA_Init()
    • 启动 DMA 接收:HAL_UART_Receive_DMA(&huart, rx_buf, UART_RX_BUF_SIZE)
    • 使能空闲中断:__HAL_UART_ENABLE_IT(&huart, UART_IT_IDLE)
  3. 中断处理

    • USARTx_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);
        UART_RxIdleCallback(&my_uart);
    }
}

四、完整代码示例

以下是一个简化的完整驱动实现(基于 STM32F4 HAL 库):

// uart_ring.h
#ifndef UART_RING_H
#define UART_RING_H

#include "stm32f4xx_hal.h"

#define RX_BUF_SIZE  256
#define TX_BUF_SIZE  256

typedef struct {
    UART_HandleTypeDef *huart;
    DMA_HandleTypeDef *hdma_rx;
    DMA_HandleTypeDef *hdma_tx;
    
    uint8_t rx_buf[RX_BUF_SIZE];
    volatile uint16_t rx_last_index;
    
    uint8_t tx_buf[TX_BUF_SIZE];
    volatile uint16_t tx_write_index;
    volatile uint16_t tx_read_index;
    volatile uint8_t tx_busy;
    uint16_t last_tx_len;
} UART_Ring;

void UART_Ring_Init(UART_Ring *ring, UART_HandleTypeDef *huart);
void UART_Ring_StartRx(UART_Ring *ring);
void UART_Ring_Send(UART_Ring *ring, uint8_t *data, uint16_t len);
void UART_Ring_HandleIdle(UART_Ring *ring);
void UART_Ring_TxCplt(UART_Ring *ring);

#endif
// uart_ring.c
#include "uart_ring.h"

void UART_Ring_Init(UART_Ring *ring, UART_HandleTypeDef *huart) {
    ring->huart = huart;
    ring->hdma_rx = huart->hdmarx;
    ring->hdma_tx = huart->hdmatx;
    ring->rx_last_index = 0;
    ring->tx_write_index = 0;
    ring->tx_read_index = 0;
    ring->tx_busy = 0;
}

void UART_Ring_StartRx(UART_Ring *ring) {
    HAL_UART_Receive_DMA(ring->huart, ring->rx_buf, RX_BUF_SIZE);
    __HAL_UART_ENABLE_IT(ring->huart, UART_IT_IDLE);
}

void UART_Ring_HandleIdle(UART_Ring *ring) {
    uint16_t current = RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(ring->hdma_rx);
    uint16_t len;
    
    if (current >= ring->rx_last_index) {
        len = current - ring->rx_last_index;
    } else {
        len = RX_BUF_SIZE - ring->rx_last_index + current;
    }
    
    // 此处将数据交给应用层,例如通过回调函数
    if (len > 0) {
        // 示例:直接回显
        UART_Ring_Send(ring, &ring->rx_buf[ring->rx_last_index], len);
    }
    
    ring->rx_last_index = current;
}

void UART_Ring_Send(UART_Ring *ring, uint8_t *data, uint16_t len) {
    for (uint16_t i = 0; i < len; i++) {
        ring->tx_buf[ring->tx_write_index++] = data[i];
        ring->tx_write_index &= (TX_BUF_SIZE - 1);
    }
    
    if (!ring->tx_busy) {
        uint16_t send_len;
        if (ring->tx_write_index >= ring->tx_read_index) {
            send_len = ring->tx_write_index - ring->tx_read_index;
        } else {
            send_len = TX_BUF_SIZE - ring->tx_read_index;
        }
        
        if (send_len > 0) {
            ring->tx_busy = 1;
            ring->last_tx_len = send_len;
            HAL_UART_Transmit_DMA(ring->huart, &ring->tx_buf[ring->tx_read_index], send_len);
        }
    }
}

void UART_Ring_TxCplt(UART_Ring *ring) {
    ring->tx_read_index += ring->last_tx_len;
    ring->tx_read_index &= (TX_BUF_SIZE - 1);
    
    if (ring->tx_write_index != ring->tx_read_index) {
        uint16_t send_len;
        if (ring->tx_write_index >= ring->tx_read_index) {
            send_len = ring->tx_write_index - ring->tx_read_index;
        } else {
            send_len = TX_BUF_SIZE - ring->tx_read_index;
        }
        ring->last_tx_len = send_len;
        HAL_UART_Transmit_DMA(ring->huart, &ring->tx_buf[ring->tx_read_index], send_len);
    } else {
        ring->tx_busy = 0;
    }
}

// 中断回调示例(在 stm32f4xx_it.c 中)
void USART1_IRQHandler(void) {
    HAL_UART_IRQHandler(&huart1);
    if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) {
        __HAL_UART_CLEAR_IDLEFLAG(&huart1);
        UART_Ring_HandleIdle(&my_ring);
    }
}

void DMA1_Stream4_IRQHandler(void) { // 根据实际 DMA 通道
    HAL_DMA_IRQHandler(&hdma_usart1_tx);
}

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART1) {
        UART_Ring_TxCplt(&my_ring);
    }
}

五、注意事项

  • 缓冲区大小:必须为 2 的幂,以便用位与运算取模,提高效率。
  • 内存对齐:DMA 缓冲区建议按 4 字节对齐,避免某些 DMA 控制器对齐要求。
  • 竞争条件rx_last_index 在中断中更新,应用层读取时需关中断或使用临界区保护。
  • 溢出处理:如果 DMA 写指针追上读指针,数据会覆盖,需在设计中考虑溢出标志或增大缓冲区。
  • 空闲中断清除:在 STM32F4 中,清除空闲中断需先读 SR 再读 DR,或使用 __HAL_UART_CLEAR_IDLEFLAG 宏。
  • 发送 DMA 的 Normal 模式:每次传输后需重新配置,因此发送完成回调中必须处理剩余数据。
  • 调试建议:先用回环测试验证收发,再接入实际外设。

六、总结

通过 DMA 的 Circular 模式接收和 Normal 模式发送,配合环形缓冲区,我们实现了几乎零 CPU 干预的串口通信。空闲中断解决了不定长数据的边界问题,而软件发送缓冲区则平滑了数据发送的突发性。这套驱动架构在工业控制、物联网网关等场景中表现出色,可轻松应对高波特率下的持续数据流。希望本文能帮助你构建更高效的串口驱动,提升系统整体性能。