## 一、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 指令等)中游刃有余。希望本文能为你的开发提供实用参考。