一、波特率发生器:没那么简单

STM32 的 USART 波特率由外设时钟和 USARTDIV 分频得到。对于 USART 2/3/4/5,通常挂在 APB1 总线上(如 168MHz 系统下 APB1 为 42MHz),而 USART1/6 挂在 APB2(84MHz)。

关键点: 若 APB 预分频系数不为 1,波特率计算需要基于加倍后的时钟(即 fck 是 APB 时钟的 2 倍)。例如:

  • APB1 = 42MHz → 实际 USART 时钟 = 84MHz
  • APB2 = 84MHz → 实际 USART 时钟 = 84MHz

计算波特率公式:

Baud = fck / (16 * USARTDIV)

配置寄存器时,将 USARTDIV 写入 BRR 寄存器即可。由于 USARTDIV 可能为小数,STM32 支持小数分频(如 0.5 步进),但实际精度足够即可。

常见误区: 使用 CubeMX 时,工具会自动计算 BRR 值,但如果你手动改时钟树,请务必重新检查波特率是否偏差过大(误差 >2% 会导致丢包)。

/* 以 84MHz 时钟,波特率 115200 为例 */
USART1->BRR = 84000000 / 115200;  // 结果为 729,实际波特率 = 84000000/729 = 115226,误差 0.02%

二、三种收发模式:轮询、中断、DMA

1. 轮询模式(阻塞)

简单但浪费 CPU,适合初始化阶段或调试输出。

HAL_UART_Transmit(&huart1, (uint8_t*)"Hello\r\n", 7, 1000);
HAL_UART_Receive(&huart1, buf, 1, 1000);

缺点: 大容量数据传输时,CPU 死等,无法处理其他任务。

2. 中断模式(异步非阻塞)

每次收发一个字节或一批数据时触发中断,CPU 在中断中处理数据,主循环可以干别的事。但高频数据下,频繁进中断会拖慢系统。

HAL_UART_Transmit_IT(&huart1, data, len);
HAL_UART_Receive_IT(&huart1, rx_buf, len);

注意: 使用 HAL 库时,HAL_UART_Receive_IT 只能接收指定长度,收满回调 HAL_UART_RxCpltCallback。若需要不定长接收,常用空闲中断(IDLE)或 DMA。

3. DMA 模式(高效传输)

DMA 负责搬数据,CPU 零负担,适合大数据量和高速通信。STM32 的 USART 支持发送 DMA 和接收 DMA,配合空闲中断可轻松实现不定长接收。

三、CubeMX 配置步骤(以 STM32F407 为例)

  1. 打开 CubeMX,选择芯片,进入 Connectivity -> USART1,模式选 Asynchronous
  2. Parameter Settings 设置波特率 115200、数据位 8、停止位 1、无校验。
  3. 开启中断:在 NVIC Settings 勾选 USART1 global interrupt
  4. 如需 DMA:在 DMA Settings 添加 USART1_TXUSART1_RX,方向分别选 MemoryToPeripheral 和 PeripheralToMemory,模式 Normal(或 Circular,接收常用 Circular)。
  5. 生成代码。

四、完整代码示例(HAL + 中断 + DMA 不定长接收)

下面演示一个典型场景:串口接收不定长数据(以 \n 结尾),收到后回显,并通过 DMA 发送长数据。

/* 定义接收缓冲区 */
#define RX_BUFF_SIZE  256
uint8_t rx_buffer[RX_BUFF_SIZE];
uint16_t rx_len = 0;

/* 开启 DMA 接收,并使用空闲中断 */
HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUFF_SIZE);
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);

/* 空闲中断回调(在 stm32f4xx_it.c 中调用) */
void UART_IDLE_Callback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        __HAL_UART_CLEAR_IDLEFLAG(&huart1);
        HAL_UART_DMAStop(&huart1);  // 停止 DMA,防止覆盖
        rx_len = RX_BUFF_SIZE - __HAL_DMA_GET_COUNTER(huart1.hdmarx);
        // 处理收到的数据 ...
        
        // 重新开启 DMA 接收
        HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUFF_SIZE);
    }
}

/* 发送:使用 DMA 发送长字符串 */
void UART_Send_DMA(uint8_t *data, uint16_t len)
{
    // 等待上次发送完成(可选轮询标志)
    while (HAL_UART_GetState(&huart1) == HAL_UART_STATE_BUSY_TX);
    HAL_UART_Transmit_DMA(&huart1, data, len);
}

若使用标准外设库(SPL),中断接收示例:

void USART1_IRQHandler(void)
{
    if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {
        uint8_t ch = USART_ReceiveData(USART1);
        /* 处理字节 */
    }
}

五、注意事项与常见坑

  • 波特率误差: 使用外部晶振时,若误差过大,改用内部 HSI 可能更稳定?不,HSI 精度较差,推荐用外部晶振并核对 APB 时钟。
  • DMA 模式选择: 接收用 Circular 模式可自动循环,配合空闲中断可避免 DMA 溢出中断的麻烦;发送一般用 Normal
  • 空闲中断标志: 在 HAL 库中,UART_IT_IDLE 需要手动清除,否则会一直触发。__HAL_UART_CLEAR_IDLEFLAG 必须在停止 DMA 前调用,否则可能丢最后一个字节。
  • HAL 库的 DMA 接收回调: HAL_UART_RxCpltCallback 在 DMA 接收到指定长度后触发,但不定长场景用 IDLE 更实用。
  • 串口调试建议: 初期用 printf 重定向到 USART,但注意中断优先级配置,避免在中断里调用阻塞式 printf。
  • 电气特性: 长线传输用 RS232/RS485 转换芯片,检查 TX/RX 是否交叉,共地必须接。

六、总结

STM32 串口看似简单,实则涉及时钟分频、中断嵌套、DMA 控制器协同等多个知识点。建议按以下路径深入:

  • 先会用轮询收发,理解波特率的概念;
  • 再改用中断,体验异步处理的优势;
  • 最后上手 DMA + 空闲中断,解决不定长通信痛点;
  • 有条件可以深入研究环形缓冲区 + 状态机解析,构建健壮的通信协议。

掌握 USART 是嵌入式开发的基石,希望本文能帮你少踩坑,多产出。