1. UART基础与物理层:RS232/RS485/TTL电平标准、DB9接口定义、波特率误差计算、硬件流控(RTS/CTS)实战

做嵌入式开发这么多年,我调试过最多的接口就是UART。说白了,串口就是嵌入式工程师的「眼睛」——没有它,你根本不知道芯片在干什么。但很多人一上来就调Python脚本,结果硬件连不上,还以为是代码写错了。其实问题往往出在物理层。

这一章,咱们先把UART的「底裤」扒清楚。电平标准、接口定义、波特率误差、硬件流控,这些搞不明白,后面写再多代码也是白搭。

1.1 三种电平标准:TTL、RS232、RS485

UART本身只定义了数据帧格式(起始位、数据位、校验位、停止位),但没规定用多少伏代表0和1。于是就有了不同的物理层标准。我见过不少新手把TTL电平直接怼到RS232接口上,结果烧了芯片——嗯,这里要注意。

标准 逻辑1(高电平) 逻辑0(低电平) 典型电压摆幅 最大距离
TTL 2.0V ~ 5.0V 0V ~ 0.8V 0~5V <1m
RS232 -3V ~ -15V +3V ~ +15V ±12V ~15m
RS485 A-B > +200mV A-B < -200mV 差分±5V ~1200m
⚠️ 常见坑: 我曾经遇到过一块STM32板子,TTL串口直接连到DB9的RS232口,结果芯片冒烟了。TTL是0~5V,RS232是±12V,电平不匹配必须加MAX232这类转换芯片。

TTL电平:最常见,单片机直接输出。3.3V或5V系统,空闲时高电平,起始位拉低。适合板级通信,线长别超过30cm。

RS232:用正负电压表示逻辑,抗干扰能力强。DB9接口就是它的标准载体。我习惯在调试工控设备时用RS232,因为能走十几米。

RS485:差分信号,半双工,支持多节点。适合工业现场总线。注意它只有A/B两根线,没有时钟,靠电压差判断逻辑。

1.2 DB9接口定义与接线

DB9是RS232最常用的连接器。很多人只知道用2、3、5三根线,但硬件流控需要用到7、8脚。我画个图帮你理清楚。

DB9 公头引脚定义(从正面看) 1 2 3 4 5 6 7 8 9 常用接线(三线制): 2(RXD) ↔ 3(TXD) 3(TXD) ↔ 2(RXD) 5(GND) ↔ 5(GND) 硬件流控需额外接 7(RTS) 和 8(CTS)

实际项目中,我习惯这样接线:

  • 三线制(最常用):2-RXD、3-TXD、5-GND。交叉连接,即设备的TXD接对方的RXD。
  • 五线制(带流控):增加7-RTS、8-CTS。同样交叉:RTS接对方CTS。
  • 全串口(九线制):很少用,除非是古老Modem设备。
💡 个人习惯: 调试阶段我永远只用三线制。硬件流控等调通基本通信后再加,否则出问题了不知道是流控没配好还是线接错了。

1.3 波特率误差计算

波特率是UART通信的「心跳」。发送方和接收方必须用同一个节奏采样。但晶振有误差,分频器有舍入,所以实际波特率和标称值总有偏差。

误差公式很简单:

误差 = |(实际波特率 - 标称波特率) / 标称波特率| × 100%

举个例子。我用STM32F103,系统时钟72MHz,要产生115200bps:

USARTDIV = 72,000,000 / (16 × 115200) = 39.0625
整数部分 = 39
小数部分 = 0.0625 × 16 = 1
实际波特率 = 72,000,000 / (16 × (39 + 1/16)) = 72,000,000 / 625 = 115200
误差 = 0%  // 完美匹配

但换一个时钟频率就没这么幸运了。比如用8MHz晶振产生115200:

USARTDIV = 8,000,000 / (16 × 115200) = 4.3403
整数部分 = 4
小数部分 = 0.3403 × 16 = 5.44 → 取整为5
实际波特率 = 8,000,000 / (16 × (4 + 5/16)) = 8,000,000 / 69 = 115,942
误差 = |115942 - 115200| / 115200 × 100% = 0.64%
📌 经验阈值: 误差在 ±2% 以内通常能正常工作。超过 3% 就可能出现乱码。我曾经用8MHz晶振跑250000波特率,误差到了3.5%,结果每隔几十个字节就丢一个——查了一下午才发现是波特率没算对。

不同波特率下的常见误差表现:

误差范围 通信表现 建议
< 1% 完全正常 放心使用
1% ~ 2% 偶尔丢字节 短帧可用,长帧建议调整
2% ~ 3% 频繁乱码 必须换晶振或改波特率
> 3% 基本不通 重新计算分频系数

1.4 硬件流控 RTS/CTS 实战

很多人觉得流控没用——「我115200从来没丢过数据啊」。那是你没遇到高速率或长距离。我做过一个项目,用921600波特率通过10米线缆传输,不加流控,每帧必丢。

RTS/CTS 工作原理:

  • RTS(Request To Send):发送方告诉接收方「我要发数据了,你准备好没?」
  • CTS(Clear To Send):接收方回复「准备好了,发吧」或「忙,等一下」。

说白了就是一对握手信号。接收方缓冲区快满时,拉低CTS,发送方看到CTS为低就暂停发送。等缓冲区空了再拉高CTS,继续发。

硬件流控接线:

设备A                 设备B
TXD    ————————→    RXD
RXD    ←————————    TXD
RTS    ————————→    CTS
CTS    ←————————    RTS
GND    ————————→    GND

在Python中启用硬件流控很简单:

import serial

ser = serial.Serial(
    port='COM3',
    baudrate=921600,
    bytesize=serial.EIGHTBITS,
    parity=serial.PARITY_NONE,
    stopbits=serial.STOPBITS_ONE,
    rtscts=True,          # 启用硬件流控
    timeout=1
)

# 发送数据
ser.write(b'Hello, UART with flow control!')

# 接收数据
data = ser.read(100)
print(data)

ser.close()
⚠️ 注意: 启用 rtscts=True 后,Python的serial库会自动管理RTS/CTS引脚。但前提是你的USB转串口模块(如FT232、CP2102)必须把RTS/CTS引脚引出来了。很多便宜的CH340模块只引了TXD/RXD/GND三根线——这种就没法用硬件流控。

我个人的调试流程是这样的:

  1. 先用三线制调通基本通信,确认波特率、数据格式没问题。
  2. 如果发现长帧丢数据,或者高速率下不稳定,就加上RTS/CTS。
  3. 用示波器抓RTS和CTS波形,确认握手时序正确。
  4. 最后在Python代码里设置 rtscts=True,重新测试。
💡 避坑指南: 我曾经遇到过一个问题——硬件流控打开了,但数据反而发不出去。查了半天发现是CTS引脚电平反了。有些模块的CTS是低电平有效,有些是高电平有效。用万用表量一下,确保逻辑一致。