STM32 USART 串口通信:波特率、中断收发与 DMA 实战指南
👁 7 阅读 · 2026-08-13 · STM32 串口
USART 是 STM32 最常用的外设之一,但不少开发者对波特率配置、中断收发和 DMA 传输的协同工作缺乏系统理解。本文将从原理出发,剖析波特率计算、基于中断的非阻塞收发以及 DMA 如何减轻 CPU 负载,并给出基于 HAL 库的完整工程示例和常见坑点,帮助嵌入式开发者快速掌握 STM32 串口的高效用法。
## 一、USART 核心原理
STM32 的 USART(通用同步异步收发器)支持全双工异步通信,底层由发送移位寄存器、接收移位寄存器和相关的控制/状态寄存器构成。数据帧通常包含起始位、数据位(8/9 位)、可选的校验位和停止位。
### 波特率计算
波特率由外设时钟分频而来。以 STM32F1 为例,USART1 挂载在 APB2(最高 72MHz),USART2/3 挂载在 APB1(最高 36MHz)。HAL 库中初始化函数 `HAL_UART_Init()` 内部会自动计算分频值,但底层公式为:
```c
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` 固定长度,因为实际串口报文长度不确定。推荐做法:
```c
// 启动空闲行中断 + 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. 生成初始化代码。
初始化代码关键部分:
```c
// 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 发送回显,实现不定长数据接收,并原样返回。
```c
// 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 指令等)中游刃有余。希望本文能为你的开发提供实用参考。