STM32 USART 串口通信深度解析:波特率、中断收发与 DMA 实战
👁 7 阅读 · 2026-08-13 · STM32 串口
串口是嵌入式开发中最常用的调试与通信接口,而 STM32 的 USART 外设远不止“发个字符串”那么简单。本文从波特率发生器原理切入,深入讲解中断收发与 DMA 传输的实现细节,对比轮询、中断和 DMA 三种模式的适用场景,并给出完整的 CubeMX 配置步骤与标准库/LL 库代码示例,帮助你彻底吃透 STM32 串口。
## 一、波特率发生器:没那么简单
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
计算波特率公式:
```c
Baud = fck / (16 * USARTDIV)
```
配置寄存器时,将 `USARTDIV` 写入 BRR 寄存器即可。由于 USARTDIV 可能为小数,STM32 支持小数分频(如 0.5 步进),但实际精度足够即可。
**常见误区:** 使用 CubeMX 时,工具会自动计算 BRR 值,但如果你手动改时钟树,请务必重新检查波特率是否偏差过大(误差 >2% 会导致丢包)。
```c
/* 以 84MHz 时钟,波特率 115200 为例 */
USART1->BRR = 84000000 / 115200; // 结果为 729,实际波特率 = 84000000/729 = 115226,误差 0.02%
```
## 二、三种收发模式:轮询、中断、DMA
### 1. 轮询模式(阻塞)
简单但浪费 CPU,适合初始化阶段或调试输出。
```c
HAL_UART_Transmit(&huart1, (uint8_t*)"Hello\r\n", 7, 1000);
HAL_UART_Receive(&huart1, buf, 1, 1000);
```
**缺点:** 大容量数据传输时,CPU 死等,无法处理其他任务。
### 2. 中断模式(异步非阻塞)
每次收发一个字节或一批数据时触发中断,CPU 在中断中处理数据,主循环可以干别的事。但高频数据下,频繁进中断会拖慢系统。
```c
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_TX` 和 `USART1_RX`,方向分别选 MemoryToPeripheral 和 PeripheralToMemory,模式 Normal(或 Circular,接收常用 Circular)。
5. 生成代码。
## 四、完整代码示例(HAL + 中断 + DMA 不定长接收)
下面演示一个典型场景:串口接收不定长数据(以 `\n` 结尾),收到后回显,并通过 DMA 发送长数据。
```c
/* 定义接收缓冲区 */
#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),中断接收示例:
```c
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 是嵌入式开发的基石,希望本文能帮你少踩坑,多产出。