第2章:串口基础:UART协议原理、波特率、起始位/数据位/停止位/校验位详解
说到嵌入式通信,串口绝对是绕不开的第一道坎。我刚开始做单片机开发那会儿,调试代码全靠串口打印,那时候觉得这玩意儿简单得不能再简单了——不就是两根线一接,数据就飞出去了吗?后来踩的坑多了,才发现UART协议里藏着不少门道。今天咱们就把UART协议从头到尾捋一遍,把波特率、数据帧结构这些基础概念彻底讲透。
2.1 UART协议的本质:异步串行通信
UART的全称是Universal Asynchronous Receiver/Transmitter,通用异步收发器。说白了,它就是一种不需要时钟线的串行通信方式。你想想看,如果每次传数据都要单独拉一根时钟线,那线缆成本得多高?UART的巧妙之处就在于,收发双方事先约定好通信速率,然后各自用自己的时钟去采样数据。
这里有个关键点:异步。没有时钟线,怎么保证收发同步?答案是靠起始位和停止位来"对齐"。我习惯把UART通信比作两个人打电话——你先说一句"喂",对方听到后回应"嗯",然后才开始正式对话。起始位就是那个"喂",停止位就是"通话结束"。
核心要点:UART是异步通信,收发双方各自独立时钟,通过约定波特率和帧结构来保证数据正确传输。
2.2 波特率:通信的"节拍器"
波特率(Baud Rate)是UART通信中最基础也最容易出问题的参数。它表示每秒传输的符号数,对于UART来说,一个符号就是一个bit,所以波特率就等于比特率。
常见的波特率有:9600、19200、38400、115200等。我个人最常用的是115200,因为它在大多数MCU上都能稳定运行,而且传输速度够快。但要注意,波特率不是越高越好——我曾经在一个项目中把波特率设到921600,结果在长距离传输时数据全乱了,后来老老实实降回115200才解决问题。
| 波特率 | 每bit时间 | 传输1字节耗时(8N1) | 适用场景 |
|---|---|---|---|
| 9600 | 104.17 μs | 约1.04 ms | 长距离、抗干扰要求高 |
| 38400 | 26.04 μs | 约0.26 ms | 一般工业应用 |
| 115200 | 8.68 μs | 约86.8 μs | 板级调试、短距离通信 |
| 921600 | 1.09 μs | 约10.9 μs | 高速调试(需短距离) |
避坑指南:我曾经在一个项目中,收发双方的晶振精度不同(一个用内部RC振荡器,一个用外部晶振),结果在115200波特率下偶尔出现错位。后来我强制要求所有设备使用±1%以内的时钟源,问题才彻底解决。记住:波特率误差超过2%,通信基本就废了。
2.3 数据帧结构:起始位、数据位、校验位、停止位
UART的数据帧就像一列火车,每节车厢都有特定用途。标准的一帧数据包含:1个起始位 + 5~8个数据位 + 0/1个校验位 + 1~2个停止位。
2.3.1 起始位(Start Bit)
起始位永远是低电平(逻辑0),持续一个bit时间。它的作用就是告诉接收方:"注意,我要开始发数据了!"
为什么是低电平?因为UART总线在空闲时保持高电平。从高电平跳变到低电平,这个下降沿就是接收方的"闹钟"。我刚开始学的时候觉得这设计很巧妙——用一个电平跳变就完成了同步,省了一根时钟线。
2.3.2 数据位(Data Bits)
数据位是真正承载信息的车厢。常见的有7位和8位两种,ASCII码传输常用7位,二进制数据用8位。数据位的传输顺序是LSB first,也就是最低有效位先发。
举个例子,你要发送0x55(二进制01010101),发送顺序是:1→0→1→0→1→0→1→0。为什么这样设计?因为LSB先发可以让接收方更快地开始处理数据,这在老式硬件里是个优化技巧。
2.3.3 校验位(Parity Bit)
校验位是可选的,用于简单的错误检测。有三种模式:
- 无校验(None):不发送校验位,帧长最短
- 奇校验(Odd):数据位中1的个数 + 校验位 = 奇数
- 偶校验(Even):数据位中1的个数 + 校验位 = 偶数
说实话,校验位在现在的应用中越来越少了。为什么?因为它的检错能力太弱——只能检测奇数个bit错误,偶数个bit错误它完全发现不了。我一般只在调试阶段用偶校验,正式产品里直接关掉,用CRC或者校验和来保证数据完整性。
2.3.4 停止位(Stop Bit)
停止位是高电平,持续1、1.5或2个bit时间。它的作用是给接收方一个"缓冲期",让接收方有时间处理刚收到的数据,准备接收下一帧。
停止位长度怎么选?我个人的经验是:低速用1位,高速用2位。在115200波特率下,1位停止位就够了;但如果用921600,我建议用2位停止位,给接收方多留点处理时间。
实用技巧:如果你发现串口通信偶尔丢数据,先别急着怀疑硬件。试试把停止位从1改成2,很多时候问题就解决了。我曾经在一个STM32项目里就是这么干的,效果立竿见影。
2.4 完整的UART数据帧示例
咱们来看一个完整的例子。假设你要发送字符'A'(ASCII码0x41,二进制01000001),采用8N1格式(8数据位、无校验、1停止位):
空闲状态:高电平
起始位: 0
数据位: 1 0 0 0 0 0 1 0 (LSB first,注意顺序!)
停止位: 1
实际波形就是:高→低(起始位)→1→0→0→0→0→0→1→0(数据位)→高(停止位)→回到空闲高电平。
你可能会问:为什么数据位是1 0 0 0 0 0 1 0而不是0 1 0 0 0 0 0 1?因为LSB first,0x41的最低位是1,所以先发1。这个顺序搞反了,数据就全乱了。我见过不少新手在这个地方栽跟头。
2.5 波特率误差的计算与容忍
实际工程中,收发双方的时钟不可能完全一致。比如你用16MHz晶振产生115200波特率,分频系数是16MHz/115200 ≈ 138.89,取整后实际波特率会有偏差。
误差计算公式:
误差 = |(实际波特率 - 目标波特率) / 目标波特率| × 100%
UART能容忍的误差一般在±2%以内,但具体取决于帧长。帧越长,误差累积越大。举个例子:
- 8N1格式:共10个bit,误差容忍约±4%
- 8E1格式:共11个bit,误差容忍约±3.6%
- 8N2格式:共11个bit,误差容忍约±3.6%
重要提醒:我曾经在一个项目中,发送端用内部RC振荡器(误差±5%),接收端用外部晶振(误差±0.1%)。结果在9600波特率下偶尔能通,但一换到115200就完全不行。后来我测了一下,发送端的实际波特率是10080,误差高达5%,远超容忍范围。从那以后,我所有产品的串口通信都强制要求使用外部晶振或高精度内部振荡器。
2.6 硬件流控:RTS/CTS
虽然UART本身只需要TX、RX、GND三根线,但在高速或大数据量传输时,硬件流控就派上用场了。RTS(Request to Send)和CTS(Clear to Send)是两条握手线,用于控制数据流。
简单说:发送方在发数据前先拉高RTS,询问接收方"准备好了吗?";接收方如果准备好了,就拉高CTS回应"来吧";如果接收方忙,就拉低CTS,发送方就得等着。
我个人的看法是:能用软件流控就别用硬件流控。硬件流控多两根线,而且不同厂家的实现方式还不一样,容易出兼容性问题。除非是高速传输(比如921600以上),否则我一般不用RTS/CTS。
2.7 常见问题与调试技巧
最后分享几个我在项目中积累的调试经验:
- 数据全乱码?先检查波特率是否一致,再检查数据位、校验位、停止位配置是否匹配。80%的串口问题出在这里。
- 偶尔丢数据?试试增加停止位长度,或者降低波特率。如果还不行,检查接收缓冲区是否溢出。
- 完全不通?用示波器看TX引脚波形。如果没波形,检查MCU配置;如果有波形但接收不到,检查TX和RX是否交叉连接(TX接RX,RX接TX)。
- 干扰严重?长距离传输时,建议用RS232电平转换(±12V)或者RS485差分信号。TTL电平(0~3.3V)在1米以上就容易出问题。
我的调试利器:逻辑分析仪。几十块钱的USB逻辑分析仪就能抓取UART波形,比示波器方便得多。我每次调试串口都会先抓波形,看一眼起始位、数据位、停止位的位置对不对,问题基本就定位了。
好了,UART协议的基础就讲到这里。下一章咱们聊聊RS232和RS485,看看它们是怎么在UART的基础上解决长距离传输问题的。记住:串口通信看似简单,但每一个参数背后都有它的设计考量。理解了这些基础,后面学CAN总线、SPI、I2C都会轻松很多。