3. UART协议深度解析:异步串行通信原理、波特率、数据帧格式、奇偶校验、流控制、RS232/RS485标准
UART,说白了就是让两个设备之间用一根线来回传数据。我最早接触它是在大学做单片机实验的时候,当时觉得这东西太简单了,不就是发个字节嘛。后来真正做产品,才发现坑多得很。今天咱们就把UART从头到尾捋一遍。
核心认知:UART是异步的,没有时钟线。收发双方得靠约定好的波特率来对齐每一位。这是它和I2C、SPI最大的区别。
3.1 异步串行通信原理
为什么叫异步?因为没有独立的时钟信号线。发送方和接收方各自用自己的时钟来采样。你想想看,如果两边时钟跑得不一样快,数据肯定对不上。
我遇到过这样一个问题:一个模块用16MHz晶振,另一个用12MHz晶振,波特率都配成115200,结果通信就是乱码。后来查了半天,发现是晶振误差累积导致的。嗯,这里要注意,异步通信对时钟精度是有要求的。
异步通信的基本流程是这样的:
- 空闲时,数据线保持高电平
- 要发送数据了,先把线拉低一个位的时间——这叫起始位
- 然后依次发送数据位(通常是LSB在前)
- 最后发送停止位(高电平),表示这一帧结束了
接收方怎么知道什么时候开始采样?它一直在检测数据线的下降沿。一旦检测到起始位,就开始按波特率计时,在每个位的中间点采样。这就是所谓的“起始位同步”。
我的习惯:在画原理图的时候,我会把UART的TX和RX交叉连接,并且预留一个测试点。调试的时候示波器一夹,波形对不对一眼就能看出来。
3.2 波特率详解
波特率就是每秒传输的符号数。对于UART来说,一个符号就是一个位,所以波特率就等于比特率。常见的波特率有:9600、19200、38400、115200、921600。
波特率怎么算出来的?拿STM32举例,它的UART外设时钟通常是APB总线时钟,比如72MHz。你要得到115200的波特率,就得配一个分频系数:
// 假设APB时钟为72MHz,目标波特率115200
// USARTDIV = 72000000 / (16 * 115200) = 39.0625
// 整数部分:39 = 0x27
// 小数部分:0.0625 * 16 = 1 = 0x1
// 所以BRR寄存器写 0x271
uint16_t brr = (39 << 4) | 1;
USART1->BRR = brr;
这里有个关键点:分频系数不一定是整数。小数部分用4位来表示,精度是1/16。所以实际波特率和目标波特率之间会有误差。误差超过2%就容易丢数据。
| 目标波特率 | 分频系数 | 实际波特率 | 误差 |
|---|---|---|---|
| 9600 | 468.75 | 9600.0 | 0% |
| 19200 | 234.375 | 19200.0 | 0% |
| 115200 | 39.0625 | 115200.0 | 0% |
| 250000 | 18.0 | 250000.0 | 0% |
| 1000000 | 4.5 | 1000000.0 | 0% |
避坑指南:我曾经在一个项目里用了非标准的波特率——200000。结果跟一个工业设备死活连不上。后来发现那个设备只支持标准的波特率列表。所以,能用标准波特率就别自己发明。
3.3 数据帧格式
UART的数据帧由这几部分组成:起始位、数据位、校验位、停止位。数据位可以是5到9位,最常用的是8位。停止位可以是1位、1.5位或2位。
我画了一张图,把数据帧的结构展示清楚:
常见的帧格式配置有:
- 8N1:8数据位,无校验,1停止位。最常用,效率高
- 8E1:8数据位,偶校验,1停止位
- 8O1:8数据位,奇校验,1停止位
- 7E1:7数据位,偶校验,1停止位。老设备常见
- 8N2:8数据位,无校验,2停止位。抗干扰好
3.4 奇偶校验
奇偶校验是最简单的错误检测方法。发送方根据数据位中1的个数,决定校验位是0还是1。
- 偶校验:数据位+校验位中1的总数为偶数
- 奇校验:数据位+校验位中1的总数为奇数
举个例子,你要发送0x55(二进制01010101),里面有4个1:
- 偶校验:校验位=0,因为4已经是偶数了
- 奇校验:校验位=1,需要凑成奇数个1
避坑指南:我曾经在一个项目里用了奇偶校验,结果发现接收方收到的数据总是对的,但校验位却一直报错。查了半天,原来是发送方和接收方一个配了奇校验,一个配了偶校验。两边对不上,当然一直报错。
奇偶校验只能检测奇数个位的错误。如果两个位同时翻转,校验位反而对了。所以它只适合噪声不大的场景。工业现场建议用更可靠的校验方式,比如CRC。
3.5 流控制
流控制解决的是“发送方太快,接收方来不及处理”的问题。有两种方式:硬件流控制和软件流控制。
硬件流控制(RTS/CTS):
- RTS(Request To Send):发送方告诉接收方“我要发了”
- CTS(Clear To Send):接收方告诉发送方“可以发”
- 需要额外两根线,但效率高,响应快
软件流控制(XON/XOFF):
- 接收方缓冲区快满了,发一个XOFF字符(0x13)让发送方暂停
- 缓冲区空了,发一个XON字符(0x11)让发送方继续
- 不需要额外引脚,但传输二进制数据时可能冲突
我个人的习惯是:能用硬件流控制就用硬件流控制。软件流控制在传输二进制文件时,万一数据里正好有0x11或0x13,那就乱套了。我在一个项目里就吃过这个亏,传输图片时总是莫名其妙中断,后来发现是图片数据里包含了XON/XOFF字符。
3.6 RS232与RS485标准
UART是协议层,RS232和RS485是物理层。它们定义了电平标准、连接器和传输距离。
| 特性 | RS232 | RS485 |
|---|---|---|
| 信号方式 | 单端(不平衡) | 差分(平衡) |
| 电压范围 | ±3V ~ ±15V | ±1.5V ~ ±6V |
| 逻辑1 | -3V ~ -15V | A-B > +200mV |
| 逻辑0 | +3V ~ +15V | A-B < -200mV |
| 最大距离 | 约15米 | 约1200米 |
| 最大节点数 | 点对点(1发1收) | 最多256个节点 |
| 常用连接器 | DB9 | 接线端子 |
RS232的特点:
- 电压摆幅大,抗干扰能力一般
- 只能点对点通信
- 距离短,适合设备间近距离连接
- 需要电平转换芯片(如MAX232)把3.3V/5V转成RS232电平
RS485的特点:
- 差分信号,共模抑制能力强,适合工业环境
- 支持多点通信,可以挂多个设备
- 距离远,1200米没问题
- 需要终端电阻(120Ω)来消除反射
- 半双工,需要软件控制收发切换
我的经验:做RS485总线时,终端电阻一定要加。不加的话,长线反射会把信号搞成一团糟。我见过一个项目,不加终端电阻,10米以内还能用,拉到50米就完全没法通信了。加上120Ω电阻,问题立刻解决。
RS485的收发切换是个容易出问题的地方。发送完数据后,要立刻把芯片切回接收模式。如果切换慢了,可能会漏掉对方的应答。我一般会在发送完最后一个字节后,加一个微秒级的延时再切换:
// RS485发送完成后的处理
void uart_send_done(void)
{
// 等待发送移位寄存器清空
while (!(USART1->SR & USART_SR_TC));
// 加一点延时,确保最后一个位已经发出
delay_us(10);
// 切换回接收模式
RS485_DE_LOW();
RS485_RE_LOW();
}
总结一下:UART看着简单,但波特率误差、帧格式匹配、流控制选择、物理层标准,每个环节都可能出问题。做驱动开发的时候,先把这些基础搞扎实了,后面调试才能事半功倍。