## 一、波特率发生器:没那么简单 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 是嵌入式开发的基石,希望本文能帮你少踩坑,多产出。