1. UART基础与STM32串口硬件架构
各位同学,咱们今天聊聊串口。说实话,串口通信在嵌入式领域就像空气一样——平时你可能感觉不到它的存在,但一旦出了问题,整个系统就憋得慌。我做了这么多年嵌入式,调试时用得最多的外设就是USART,没有之一。
这一章,咱们不搞虚的。直接深入到USART外设的寄存器级工作原理,把波特率发生器、发送/接收移位寄存器、硬件流控这些核心模块彻底讲透。你把这些搞明白了,后面做DMA传输优化才有底气。
1.1 USART整体架构速览
先看一张整体框图,我习惯把USART分成三个功能区:
核心要点:USART = 时钟生成 + 数据收发 + 控制逻辑。三者缺一不可。
这张图我建议你多看两眼。三个模块之间通过内部总线交换数据,时钟模块是心脏,数据模块是手脚,控制模块是大脑。嗯,就是这么回事。
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 发送与接收移位寄存器
这两个寄存器是串口通信的真正执行者。我简单说说它们的工作流程:
发送流程
- CPU或DMA把数据写入发送数据寄存器(TDR)
- 硬件自动将TDR内容转移到发送移位寄存器
- 移位寄存器按波特率时钟,逐位从TX引脚送出
- 发送完成后,TXE(发送寄存器空)标志置位
- 全部数据发送完毕,TC(发送完成)标志置位
接收流程
- RX引脚检测到起始位(高→低跳变)
- 移位寄存器按波特率时钟采样数据位
- 接收完成后,数据转移到接收数据寄存器(RDR)
- RXNE(接收寄存器非空)标志置位
- 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传输、双缓冲、环形队列这些高级玩法时,才能跟得上节奏。