一、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 库将这些细节封装成回调函数。
接收中断流程
- 使能 USARTx 全局中断。
- 调用
HAL_UART_Receive_IT(&huart, rx_buf, size)启动中断接收,size表示要接收的字节数。 - 每收到 1 字节,HAL 库将数据存入
rx_buf,当收满size后调用HAL_UART_RxCpltCallback()。 - 若想连续接收,每次执行完回调后需重新调用
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 为例)
- 在 CubeMX 中打开 USART,设置模式为 Asynchronous,波特率 115200,字长 8,无校验,1 停止位。
- 在 DMA Settings 中添加 RX 和 TX 通道,方向分别为 PeripheralToMemory 和 MemoryToPeripheral,模式 Normal。
- 使能 USART 中断和 DMA 中断。
- 生成初始化代码。
初始化代码关键部分:
// 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 指令等)中游刃有余。希望本文能为你的开发提供实用参考。