一、USART 核心原理

STM32 的 USART(通用同步异步收发器)支持全双工异步通信,底层由发送移位寄存器、接收移位寄存器和相关的控制/状态寄存器构成。数据帧通常包含起始位、数据位(8/9 位)、可选的校验位和停止位。

波特率计算

波特率由外设时钟分频而来。以 STM32F1 为例,USART1 挂载在 APB2(最高 72MHz),USART2/3 挂载在 APB1(最高 36MHz)。HAL 库中初始化函数 HAL_UART_Init() 内部会自动计算分频值,但底层公式为:

USARTDIV = fck / (16 * baudrate)

实际写入寄存器的值会做四舍五入。因此,如果外设时钟不是标准值,会产生波特率误差。例如使用 72MHz 时,115200 波特率的分频值为 39.06,误差约 0.1%,可正常通信。但若时钟偏差过大,务必用示波器或逻辑分析仪验证。

二、中断收发:非阻塞的核心

中断收发的核心思想:接收数据时,每收到 1 字节触发一次 RXNE 中断;发送数据时,当发送数据寄存器空(TXE)时触发中断,或发送完成后(TC)触发。HAL 库将这些细节封装成回调函数。

接收中断流程

  1. 使能 USARTx 全局中断。
  2. 调用 HAL_UART_Receive_IT(&huart, rx_buf, size) 启动中断接收,size 表示要接收的字节数。
  3. 每收到 1 字节,HAL 库将数据存入 rx_buf,当收满 size 后调用 HAL_UART_RxCpltCallback()
  4. 若想连续接收,每次执行完回调后需重新调用 HAL_UART_Receive_IT()

更高效的处理方式是使用空闲行中断 + DMA,先不要用 HAL 的 HAL_UART_Receive_DMA 固定长度,因为实际串口报文长度不确定。推荐做法:

// 启动空闲行中断 + DMA 不定长接收
__HAL_UART_ENABLE_IT(&huart, UART_IT_IDLE);
HAL_UART_Receive_DMA(&huart, rx_buff, BUFFER_SIZE);

void USARTx_IRQHandler(void)
{
    // 处理空闲中断
    if (__HAL_UART_GET_FLAG(&huart, UART_FLAG_IDLE)) {
        __HAL_UART_CLEAR_IDLEFLAG(&huart);
        // 计算实际接收长度
        uint16_t len = BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usartx_rx);
        // 处理数据 len
        process_rx_data(rx_buff, len);
        // 重新启动 DMA 接收
        HAL_UART_Receive_DMA(&huart, rx_buff, BUFFER_SIZE);
    }
}

三、DMA 发送:让 CPU 专注于业务

DMA(直接内存访问)可以在内存与外设之间搬运数据,无需 CPU 干预。发送一长串数据时,用 DMA 可以显著提升 CPU 利用率。

配置步骤(以 CubeMX 为例)

  1. 在 CubeMX 中打开 USART,设置模式为 Asynchronous,波特率 115200,字长 8,无校验,1 停止位。
  2. 在 DMA Settings 中添加 RX 和 TX 通道,方向分别为 PeripheralToMemory 和 MemoryToPeripheral,模式 Normal。
  3. 使能 USART 中断和 DMA 中断。
  4. 生成初始化代码。

初始化代码关键部分:

// UART 句柄(已由 CubeMX 生成)
UART_HandleTypeDef huart;
DMA_HandleTypeDef hdma_usart1_tx;
DMA_HandleTypeDef hdma_usart1_rx;

// DMA 发送函数(示例)
void uart_debug_send(uint8_t *data, uint16_t len)
{
    // 若上一次发送未完成,等待完成,否则会冲突
    while (HAL_UART_GetState(&huart) == HAL_UART_STATE_BUSY_TX);
    HAL_UART_Transmit_DMA(&huart, data, len);
}

四、完整代码示例(基于 STM32F4 + HAL)

以下示例演示 DMA 接收 + 空闲中断 + DMA 发送回显,实现不定长数据接收,并原样返回。

// main.c 中定义缓冲区
#define RX_BUFFER_SIZE 256
uint8_t rx_buffer[RX_BUFFER_SIZE];
uint16_t rx_len = 0;

// 初始化 DMA 接收和空闲中断
void uart_dma_init(void)
{
    HAL_UART_Receive_DMA(&huart, rx_buffer, RX_BUFFER_SIZE);
    __HAL_UART_ENABLE_IT(&huart, UART_IT_IDLE);
}

// USART1 中断服务函数
void USART1_IRQHandler(void)
{
    HAL_UART_IRQHandler(&huart);
    
    // 空闲中断处理
    if (__HAL_UART_GET_FLAG(&huart, UART_FLAG_IDLE)) {
        __HAL_UART_CLEAR_IDLEFLAG(&huart);
        
        // 计算实际收到的数据长度
        rx_len = RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
        
        // 重新启动 DMA 接收
        HAL_UART_Receive_DMA(&huart, rx_buffer, RX_BUFFER_SIZE);
    }
}

// DMA 接收完成回调(这里没用,但必须保留,因为 HAL 会调用)
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    // 若使用定长接收,在此处处理;当前未使用
}

// 主循环中处理接收到的数据
int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_DMA_Init();
    MX_USART1_UART_Init();
    
    uart_dma_init();
    
    while (1)
    {
        if (rx_len > 0)
        {
            // 回显数据
            HAL_UART_Transmit_DMA(&huart, rx_buffer, rx_len);
            rx_len = 0;
        }
    }
}

注意:上述代码中,DMA 的模式需设置为 Normal,这样每次接收前需要重新调用 HAL_UART_Receive_DMA()。若设置为 Circular,则 DMA 计数器会循环,不过需要更复杂的长度解析,建议初学者先用 Normal。

五、注意事项

  • 波特率误差:外部晶振频率和倍频系数会影响波特率,长线通信建议误差小于 2%。用示波器测量 TX 引脚的波形,确认实际波特率。
  • 中断优先级:串口中断优先级不宜过低,否则可能丢数据。特别是同时使用多个串口时,建议把 DMA 中断优先级设为最高。
  • DMA 缓冲区大小:尽量使用 2 的幂次方,避免环形缓冲时取模运算开销。
  • 发送与接收冲突:发送 DMA 和接收 DMA 共用一个句柄时,发送前检查 HAL_UART_GetState() 是否为 HAL_UART_STATE_READY
  • 空闲中断误触发:在 DMA 接收刚启动时,总线空闲会立即触发一次空闲中断,此时长度可能为 0,需要过滤。
  • 数据一致性:DMA 接收的数据可能在主循环读取时被新数据覆盖,建议在 DMA 中断中拷贝数据或使用双缓冲。

六、总结

STM32 串口通信要追求效率和稳定性,需要将波特率、中断、DMA 三者有机结合。波特率决定物理层速率,中断实现事件驱动,DMA 解放 CPU。实际开发中可根据需求灵活搭配:简单场景用轮询,中等场景用中断收发,高速/大数据量场景用 DMA + 空闲中断。掌握这套核心方法,即可在各类通信协议(Modbus、串口屏、AT 指令等)中游刃有余。希望本文能为你的开发提供实用参考。