3. UART数据帧结构:起始位、数据位、奇偶校验位、停止位、波特率的概念与计算

各位同学,咱们今天聊聊UART数据帧的“骨架”。说白了,就是数据在串口线上到底长什么样。

我刚开始学单片机时,总觉得串口通信很神秘——两根线就能传数据?后来调通了第一个串口程序,看到示波器上的波形,才恍然大悟。原来每一帧数据,都像一列火车,有车头、车厢、还有车尾。

3.1 数据帧的“五节车厢”

一个完整的UART数据帧,由五个部分组成。我习惯把它们叫做“五节车厢”:

  • 起始位:1位,永远是低电平(逻辑0)
  • 数据位:5~9位,常见的是8位
  • 奇偶校验位:0或1位,可选
  • 停止位:1、1.5或2位,永远是高电平(逻辑1)
  • 空闲位:高电平,表示总线空闲

你想想看,为什么起始位必须是低电平?因为空闲时总线是高电平,突然拉低,接收端就知道“有数据来了”。这个下降沿,就是通信的“发令枪”。

核心要点:UART是异步通信,没有时钟线。收发双方全靠起始位的下降沿来“对齐”时间。所以起始位的时序必须精确,否则后面全错。

3.2 起始位与停止位——帧的“头”和“尾”

起始位只有1位,但它的作用至关重要。我在项目中遇到过一个问题:两个设备通信时,偶尔会出现第一个字节丢失。查了半天,发现是接收端的起始位检测阈值设得太严了。稍微放宽一点,问题就解决了。

停止位的作用是给接收端一个“缓冲时间”。为什么需要这个时间?因为接收端处理完一帧数据需要时间。如果停止位太短,下一帧的起始位就来了,接收端还没准备好,就会丢数据。

停止位长度 适用场景 我的建议
1位 标准通信,大多数情况 默认选择,兼容性好
1.5位 低速设备,需要更多处理时间 很少用,特殊芯片才需要
2位 高噪声环境,或接收端处理慢 我曾经在长线通信时用过,确实稳定

3.3 数据位——真正的“货物”

数据位是帧的核心,承载着我们要传输的信息。常见的是8位数据,因为一个字节正好8位。但有些场合会用7位(比如传输ASCII字符),或者9位(比如某些工业总线协议)。

这里有个细节:数据位的发送顺序是LSB(最低有效位)在前。也就是说,如果你要发送0x55(二进制01010101),先发的是最低位1,然后依次是0、1、0、1、0、1、0。这个顺序别搞反了,我见过有人把数据位顺序搞错,调试了一整天。

小技巧:用示波器看串口波形时,先找到起始位的下降沿,然后按位依次读电平。LSB在前,读出来的二进制数要反转一下才是实际数据。我习惯在纸上画波形图,标出每一位,这样不容易出错。

3.4 奇偶校验位——数据的“体检员”

奇偶校验位是用来检测数据传输过程中有没有发生错误的。它只有1位,但能检测出奇数个位的翻转错误。

有两种模式:

  • 奇校验:数据位中1的个数 + 校验位 = 奇数
  • 偶校验:数据位中1的个数 + 校验位 = 偶数

举个例子,数据位是0x55(二进制01010101),有4个1(偶数)。如果采用偶校验,校验位就是0,保持偶数;如果采用奇校验,校验位就是1,凑成奇数。

注意:奇偶校验只能检测奇数个错误。如果两个位同时翻转(比如两个1变成0),校验位反而正确,检测不出来。所以对可靠性要求高的场合,建议用更高级的校验方式(比如CRC)。

我个人习惯在调试阶段开启奇偶校验,它能帮我快速发现线路问题。量产时如果通信环境稳定,有时会关掉校验位,省下1位的时间,提高传输效率。

3.5 波特率——通信的“节拍器”

波特率,就是每秒传输的符号数。在UART中,一个符号就是1位,所以波特率等于比特率。单位是bps(bits per second)。

常见的波特率有:9600、19200、38400、115200。为什么是这些数字?因为它们都是从一个基准时钟分频得来的。早期的UART芯片使用1.8432MHz晶振,分频后得到这些标准波特率。

波特率的计算公式很简单:

波特率 = 时钟频率 / (16 × 分频系数)

这里的16是UART内部采样时钟的倍数。每个位被采样16次,取中间3次的值做多数表决,提高抗干扰能力。

举个例子,如果系统时钟是16MHz,想要115200的波特率:

分频系数 = 16MHz / (16 × 115200) ≈ 8.68
取整后为9,实际波特率 = 16MHz / (16 × 9) ≈ 111111 bps
误差 = (115200 - 111111) / 115200 ≈ 3.57%

这个误差在允许范围内吗?一般来说,UART能容忍2%~3%的误差。超过这个范围,就可能出现误码。

避坑指南:我曾经在一个项目里,用了两个不同晶振的单片机通信。一个16MHz,一个12MHz。算出来的分频系数都有误差,加起来误差超过了5%。结果就是,偶尔能通,偶尔乱码。最后统一换了晶振才解决。所以,通信双方的波特率误差必须控制在2%以内,这是硬指标。

3.6 数据帧的完整波形

说了这么多,咱们用一张图把整个数据帧串起来。下面是我手绘的UART数据帧结构图,展示了发送0x55(8位数据,无校验,1位停止位)时的完整波形。

UART数据帧结构图(发送0x55,8N1格式) 空闲 高电平 起始 低电平 D0 1 (LSB) D1 0 D2 1 D3 0 D4 1 D5 0 D6 1 D7 0 (MSB) 停止 高电平 关键说明 • 空闲时总线为高电平,起始位拉低表示开始传输 • 数据位从LSB(最低位)开始发送,到MSB(最高位)结束 • 停止位恢复高电平,为下一帧做准备 • 每位的持续时间 = 1 / 波特率,例如115200bps时每位约8.68μs

3.7 常见数据帧格式

实际项目中,最常用的数据帧格式是8N1:8位数据、无校验、1位停止位。为什么?因为8位正好一个字节,无校验省时间,1位停止位够用。

但有些场合需要不同的配置。我整理了一个表格,方便你对照选择:

格式 数据位 校验位 停止位 典型应用
8N1 8 无 1 通用通信,最常用
8E1 8 偶校验 1 Modbus协议,工业控制
8O1 8 奇校验 1 某些老式设备
7E1 7 偶校验 1 ASCII字符传输
9N1 9 无 1 某些特殊协议(如LIN总线)

我的经验:如果你不确定用什么格式,就用8N1。这是最通用的配置,几乎所有UART设备都支持。等调试通了,再根据需求调整。

3.8 波特率误差的计算与容忍

前面提到了波特率误差,这里再深入说一下。误差的来源有两个:

  1. 晶振精度:普通晶振精度±50ppm,温漂后可能更大
  2. 分频取整:分频系数必须是整数,导致实际波特率偏离目标值

误差的计算公式:

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

举个例子,系统时钟16MHz,目标波特率115200:

理想分频系数 = 16000000 / (16 × 115200) = 8.68
实际分频系数 = 9(取整)
实际波特率 = 16000000 / (16 × 9) = 111111
误差 = (111111 - 115200) / 115200 = -3.55%

这个误差有点大。怎么办?换一个目标波特率,比如用115200的整数分频:

目标波特率 = 16000000 / (16 × 8) = 125000
误差 = 0%

所以,选择波特率时,优先选那些能被系统时钟整除的。比如16MHz下,125000、250000、500000都是零误差的。

重要提醒:通信双方的波特率误差之和不能超过2%。如果一方误差+2%,另一方误差-2%,加起来4%,通信就会不稳定。我曾经吃过这个亏,两个板子单独测都没问题,连在一起就乱码。后来发现一个晶振偏快,一个偏慢,误差叠加了。