1. UART基础与STM32串口硬件架构

各位同学,咱们今天聊聊串口。说实话,串口通信在嵌入式领域就像空气一样——平时你可能感觉不到它的存在,但一旦出了问题,整个系统就憋得慌。我做了这么多年嵌入式,调试时用得最多的外设就是USART,没有之一。

这一章,咱们不搞虚的。直接深入到USART外设的寄存器级工作原理,把波特率发生器、发送/接收移位寄存器、硬件流控这些核心模块彻底讲透。你把这些搞明白了,后面做DMA传输优化才有底气。

1.1 USART整体架构速览

先看一张整体框图,我习惯把USART分成三个功能区:

核心要点:USART = 时钟生成 + 数据收发 + 控制逻辑。三者缺一不可。

STM32 USART 内部架构框图 时钟生成模块 波特率发生器 USARTDIV 分频器 16倍过采样 → 发送/接收时钟 数据收发模块 发送移位寄存器 接收移位寄存器 TX/RX 引脚 数据位/校验位/停止位 控制逻辑模块 状态寄存器 (SR) 控制寄存器 (CR1/2/3) 中断/DMA 请求 硬件流控 (RTS/CTS) 寄存器总线 (AHB/APB) 关键数据流 ① 发送:CPU/DMA → 发送数据寄存器(TDR) → 发送移位寄存器 → TX 引脚 ② 接收:RX 引脚 → 接收移位寄存器 → 接收数据寄存器(RDR) → CPU/DMA ③ 时钟:APB时钟 → 波特率发生器 → 过采样时钟 → 移位寄存器

这张图我建议你多看两眼。三个模块之间通过内部总线交换数据,时钟模块是心脏,数据模块是手脚,控制模块是大脑。嗯,就是这么回事。

1.2 波特率发生器——串口的节拍器

波特率说白了就是每秒传多少位。但STM32内部是怎么生成这个时钟的?我直接给你看公式:

// 波特率计算公式(标准模式,16倍过采样)
Tx/Rx 波特率 = f_CK / (16 * USARTDIV)

// 其中 USARTDIV 是一个 16 位无符号定点数
// 高 12 位为整数部分,低 4 位为小数部分

举个例子,假设APB时钟是72MHz,我想要115200bps:

USARTDIV = 72,000,000 / (16 * 115200) = 39.0625
整数部分 = 39 = 0x27
小数部分 = 0.0625 * 16 = 1 = 0x1
BRR 寄存器值 = (39 << 4) | 1 = 0x271

我的经验:实际项目中,我习惯把波特率误差控制在±1.5%以内。超过这个范围,长数据帧就容易丢字节。尤其是115200以上,晶振精度和分频误差都会放大,建议用示波器实测TX脚波形确认。

这里有个坑——很多人以为BRR寄存器直接写波特率数值就行。其实不是,你得先算USARTDIV,再按格式填入。我刚开始用STM32时就在这栽过跟头,串口打印全是乱码,查了半天才发现是分频算错了。

1.3 发送与接收移位寄存器

这两个寄存器是串口通信的真正执行者。我简单说说它们的工作流程:

发送流程

  1. CPU或DMA把数据写入发送数据寄存器(TDR)
  2. 硬件自动将TDR内容转移到发送移位寄存器
  3. 移位寄存器按波特率时钟,逐位从TX引脚送出
  4. 发送完成后,TXE(发送寄存器空)标志置位
  5. 全部数据发送完毕,TC(发送完成)标志置位

接收流程

  1. RX引脚检测到起始位(高→低跳变)
  2. 移位寄存器按波特率时钟采样数据位
  3. 接收完成后,数据转移到接收数据寄存器(RDR)
  4. RXNE(接收寄存器非空)标志置位
  5. CPU或DMA读取RDR获取数据

注意:RDR和TDR在物理上是两个独立的寄存器,但映射到同一个地址。读操作访问RDR,写操作访问TDR。千万别搞混了——我曾经见过有人用读-修改-写的方式操作这个地址,结果数据全乱套了。

1.4 硬件流控机制

硬件流控,说白了就是让收发双方互相打个招呼:「我准备好了,你发吧」或者「我忙不过来,你先等等」。STM32支持RTS和CTS两个信号:

信号 方向 作用
RTS (Request To Send) 输出 告诉对方:我可以接收数据了
CTS (Clear To Send) 输入 检测对方是否准备好接收

启用硬件流控后,发送端在发送每个字节前会检查CTS引脚电平。如果CTS为低(有效),就正常发送;如果CTS为高(无效),就暂停发送,直到CTS变低。反过来,接收端通过RTS引脚通知发送端自己的接收缓冲区状态。

什么时候该用硬件流控?我一般在以下场景必开:

  • 高速通信(921600bps以上),软件流控来不及响应
  • 长距离传输(超过3米),信号容易受干扰
  • 多设备共享总线,需要避免数据碰撞

不过说实话,大部分板级通信(同一个PCB上两个芯片之间)用不上硬件流控。我做过一个项目,两个STM32之间用UART通信,距离不到10厘米,115200bps跑得稳稳的,开流控反而多占两个GPIO,得不偿失。

1.5 寄存器映射速查

下面这几个寄存器,你写驱动时天天要打交道:

寄存器 偏移地址 核心作用
SR 0x00 状态标志:TXE, TC, RXNE, IDLE, ORE, NE, FE, PE
DR 0x04 数据寄存器:读=RDR,写=TDR
BRR 0x08 波特率分频值:USARTDIV编码格式
CR1 0x0C 控制寄存器1:UE, M, WAKE, PCE, PS, PEIE, TXEIE, TCIE, RXNEIE, IDLEIE, TE, RE, RWU, SBK
CR2 0x10 控制寄存器2:STOP位配置, LIN模式, 时钟极性/相位
CR3 0x14 控制寄存器3:CTSIE, CTSE, RTSE, DMAT, DMAR, SCEN, NACK, HDSEL, IRLP, IREN, EIE

调试小技巧:我调试串口问题时,第一步永远是读SR寄存器。看TXE和TC有没有置位,RXNE有没有被及时清掉。很多诡异的问题,比如发一次数据却收到两次,或者收不到数据,根源都在SR标志位的处理顺序上。

1.6 避坑指南——我踩过的三个坑

最后,分享几个我实际项目中遇到的坑,你遇到了能少走弯路:

  • 坑一:波特率误差累积。我曾经用8MHz晶振跑921600波特率,算出来分频值不是整数,误差到了2.3%。短数据没问题,但一传256字节的包就丢数据。后来换了16MHz晶振,误差降到0.2%,问题解决。
  • 坑二:TXE和TC搞混。TXE表示发送寄存器空了,你可以写下一个字节了。TC表示整个帧(包括停止位)都送出去了。如果你在TXE置位时就关串口,最后一个字节会丢。我因为这个被客户投诉过,印象特别深。
  • 坑三:接收溢出没处理。高速接收时,如果CPU没及时读RDR,新数据会覆盖旧数据,ORE标志置位。如果不清ORE,后续数据全收不到。我的习惯是在中断里先读SR再读DR,顺序不能反。

好了,这一章的内容就到这。USART的硬件架构是后面所有优化的基础,你把它吃透了,后面讲DMA传输、双缓冲、环形队列这些高级玩法时,才能跟得上节奏。