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位。

我画了一张图,把数据帧的结构展示清楚:

UART 数据帧结构(8N1格式) 空闲 起始 D0 D1 D2 D3 D4 D5 D6 D7 停止 空闲 时间方向 → 说明: • 8N1 表示:8位数据位 + 无校验 + 1位停止位 • 数据位从最低位(D0)开始发送,即 LSB first • 起始位固定为低电平,停止位固定为高电平 • 空闲时总线保持高电平,接收方靠下降沿检测起始位

常见的帧格式配置有:

  • 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看着简单,但波特率误差、帧格式匹配、流控制选择、物理层标准,每个环节都可能出问题。做驱动开发的时候,先把这些基础搞扎实了,后面调试才能事半功倍。