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.5或2个停止位?
- 1个停止位:标准配置,大多数场景够用
- 1.5个停止位:用于某些老式设备或特殊波特率
- 2个停止位:给接收端更多处理时间,或者用于低速设备
你想想看,如果接收端的中断响应比较慢,或者数据缓冲区还没腾空,多一个停止位就能避免数据覆盖。我在调试一个老旧的热敏打印机时就遇到过——必须配2个停止位才能稳定工作。
经验之谈:如果通信不稳定,先试试把停止位从1改成2。虽然会降低一点有效数据率,但能解决很多时序问题。
3.6 完整帧时序图
下面我用SVG画一个完整的UART数据帧时序图,展示发送0x55(8位数据、偶校验、1个停止位)的过程:
从图上可以清楚看到:空闲高电平 → 起始位拉低 → 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都得自己管,帧结构就变得特别重要。
我个人习惯在驱动初始化时,把帧结构参数打印出来确认一遍。别嫌麻烦,我见过太多因为配置不一致导致的“灵异”通信故障了。