3. UART数据帧结构:起始位、数据位、奇偶校验位、停止位的详细解析

UART通信说白了就是一根线发、一根线收,没有时钟线。那接收端怎么知道什么时候开始读数据?什么时候读完一个字节?答案就在数据帧结构里。

我刚开始做嵌入式时,总觉得帧结构就是“发个字节加个校验”这么简单。直到有一次调试一个GPS模块,数据总是偶尔错位,折腾了两天才发现——停止位配置错了。从那以后,我对帧结构的每个bit都不敢马虎。

3.1 数据帧的完整组成

一个标准的UART数据帧,由4个部分组成:

字段 长度 电平状态 作用
起始位 1 bit 低电平(0) 通知接收端准备接收数据
数据位 5~9 bit(常用8 bit) LSB先发 实际传输的数据内容
奇偶校验位 0 或 1 bit 奇/偶校验结果 检错(可选)
停止位 1 / 1.5 / 2 bit 高电平(1) 帧结束标识,给接收端处理时间

空闲时,TX线保持高电平。一旦拉低,接收端就知道:要开始了。

核心要点:起始位的下降沿是接收端的“同步信号”。没有这个边沿,接收端无法对齐数据位采样点。

3.2 起始位——通信的“发令枪”

起始位只有1个bit,固定为低电平。它的作用很简单:让接收端知道数据要来了。

接收端内部有一个采样时钟,通常是波特率的16倍或8倍。检测到TX线从高变低后,接收端会等待1.5个bit时间(如果是16倍过采样,就是24个时钟周期),然后开始采样数据位。

为什么等1.5个bit?因为要避开起始位边沿附近的抖动区域,采样到数据位的中间位置。我个人习惯在驱动里把过采样倍数设成16倍,这样采样窗口更稳定。

实战技巧:如果通信环境噪声大,起始位误触发是个常见问题。我建议在硬件上加一个低通滤波,或者在软件里做“连续采样到低电平超过半个bit时间才确认起始位”。

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

数据位是帧的核心,长度可配置为5、6、7、8或9 bit。最常用的是8 bit,刚好一个字节。

发送顺序是LSB(最低有效位)先发,MSB最后发。举个例子,要发送0x55(二进制01010101),实际线上顺序是:

起始位 → 1 → 0 → 1 → 0 → 1 → 0 → 1 → 0 → 校验位 → 停止位
         LSB                          MSB

为什么LSB先发?这是历史原因——早期的电传打字机就是这么设计的,后来成了标准。你想想看,如果MSB先发,接收端得先知道数据长度才能解析,多麻烦。

数据位长度 典型应用场景 备注
5 bit 老式电传机、Baudot码 现在几乎不用
7 bit ASCII字符传输 早期Modem常用
8 bit 通用数据、二进制传输 最常用,默认选择
9 bit 多机通信地址帧 某些工业总线使用

注意:发送端和接收端的数据位长度必须一致。我曾经遇到过设备A配了7位数据,设备B配了8位,结果每个字节都少了一位,数据全乱套了。

3.4 奇偶校验位——简单的错误检测

奇偶校验位是可选字段,长度1 bit。它的值由数据位中“1”的个数决定。

有两种模式:

  • 偶校验(Even Parity):数据位中1的个数 + 校验位 = 偶数
  • 奇校验(Odd Parity):数据位中1的个数 + 校验位 = 奇数

举个例子,数据位是0x55(二进制01010101),有4个1:

  • 偶校验:4已经是偶数,校验位=0
  • 奇校验:4是偶数,需要加1变成奇数,校验位=1

接收端收到数据后,会重新计算校验位,跟收到的校验位对比。不一致就说明数据出错了。

局限性:奇偶校验只能检测奇数个bit翻转。如果两个bit同时翻转(比如00变成11),校验结果不变,错误就漏掉了。所以它只适合噪声不严重的场景。

我记得在一个工业项目中,现场电机启动时总会出现偶发乱码。用奇偶校验能检出大部分错误,但偶尔还是有漏网之鱼。后来我加了CRC校验才彻底解决。嗯,奇偶校验就是个“便宜货”,够用但别指望它万能。

3.5 停止位——帧的“句号”

停止位固定为高电平,长度可以是1、1.5或2 bit。它的作用有两个:

  1. 帧结束标识:告诉接收端这一帧完了
  2. 处理缓冲时间:给接收端留出时间处理收到的数据

为什么需要1.5或2个停止位?

  • 1个停止位:标准配置,大多数场景够用
  • 1.5个停止位:用于某些老式设备或特殊波特率
  • 2个停止位:给接收端更多处理时间,或者用于低速设备

你想想看,如果接收端的中断响应比较慢,或者数据缓冲区还没腾空,多一个停止位就能避免数据覆盖。我在调试一个老旧的热敏打印机时就遇到过——必须配2个停止位才能稳定工作。

经验之谈:如果通信不稳定,先试试把停止位从1改成2。虽然会降低一点有效数据率,但能解决很多时序问题。

3.6 完整帧时序图

下面我用SVG画一个完整的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) 校验 偶=0 停止 高电平 空闲 高电平 时间 → 每个bit宽度 = 1/波特率 例如9600bps → 约104μs

从图上可以清楚看到:空闲高电平 → 起始位拉低 → 8个数据位(LSB到MSB) → 校验位 → 停止位拉高 → 回到空闲。接收端就是靠检测这个下降沿来同步的。

3.7 常见配置速查表

实际项目中,UART配置通常用类似“9600 8N1”这样的简写表示。我来解释一下:

简写格式 波特率 数据位 校验 停止位 典型场景
9600 8N1 9600 8 无 1 最通用,调试首选
115200 8N1 115200 8 无 1 高速通信,模块间互联
19200 8E1 19200 8 偶校验 1 工业设备,需要检错
4800 7O2 4800 7 奇校验 2 老式终端设备

避坑指南:我曾经在调试一个蓝牙模块时,手册写的是“8N1”,但实际必须配成“8E1”才能通信。后来发现是模块固件版本跟手册不一致。所以拿到新设备,先用逻辑分析仪抓一下实际波形,确认帧结构再写代码。

3.8 帧结构对驱动开发的影响

理解了帧结构,写驱动时就知道该关注什么:

  • 起始位检测:接收中断里要检测下降沿,或者用硬件自动检测
  • 数据位采样:过采样时钟要精确,波特率误差最好控制在±2%以内
  • 校验位处理:发送时计算并填充,接收时验证并标记错误
  • 停止位检查:如果停止位不是高电平,说明帧错误(Framing Error)

嗯,这里要注意:很多MCU的UART外设会自动处理帧结构,你只需要配置寄存器就行。但如果你在做软UART(用GPIO模拟),那每个bit都得自己管,帧结构就变得特别重要。

我个人习惯在驱动初始化时,把帧结构参数打印出来确认一遍。别嫌麻烦,我见过太多因为配置不一致导致的“灵异”通信故障了。