一、波特率发生器:没那么简单
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 为例)
- 打开 CubeMX,选择芯片,进入
Connectivity -> USART1,模式选Asynchronous。 - 在
Parameter Settings设置波特率 115200、数据位 8、停止位 1、无校验。 - 开启中断:在
NVIC Settings勾选USART1 global interrupt。 - 如需 DMA:在
DMA Settings添加USART1_TX和USART1_RX,方向分别选 MemoryToPeripheral 和 PeripheralToMemory,模式 Normal(或 Circular,接收常用 Circular)。 - 生成代码。
四、完整代码示例(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 是嵌入式开发的基石,希望本文能帮你少踩坑,多产出。