3. UART数据帧格式详解:起始位、数据位、校验位、停止位、波特率配置
UART通信说白了就是两个设备之间通过一根TX、一根RX线,按约定好的节奏一位一位地传数据。这个“约定”就是数据帧格式。我刚开始做嵌入式时,总觉得帧格式就是填几个寄存器的事,直到有一次在工业现场调试,两台设备死活对不上话,折腾了两天才发现是停止位配错了。从那以后,我对帧格式的每一个比特都不敢马虎。
3.1 数据帧的完整结构
一个标准的UART数据帧,就像一列火车。有车头、车厢、还有车尾。车头告诉接收方“我要发数据了”,车厢是真正的数据,车尾告诉接收方“这趟发完了”。
标准UART数据帧结构(从左到右发送顺序):
起始位(1bit) → 数据位(5~9bit,常用8bit) → 校验位(0/1bit,可选) → 停止位(1/1.5/2bit)
3.2 起始位:通信的“发令枪”
起始位只有1个bit,固定为低电平。为什么是低电平?因为空闲时TX线是高电平,从高跳变到低,这个下降沿就是接收方的“发令枪”。
我记得有一次调试一个GPS模块,数据死活收不到。用逻辑分析仪一看,发现TX线一直低电平。原来模块在上电初始化时把TX拉低了,接收方误以为这是起始位,一直在等后面的数据。嗯,这里要注意:起始位之前必须保证总线空闲(高电平)至少一个bit时间。
实战技巧:接收方通常以16倍波特率的时钟采样RX线。检测到下降沿后,在起始位的中间点(第8个采样点)再次确认是否为低电平。这是为了抗干扰——如果只是毛刺,中间点采样会判为高电平,直接丢弃。
3.3 数据位:真正的“货物”
数据位是帧的核心,长度可以是5、6、7、8、9 bit。我个人习惯用8 bit数据位,因为一个字节正好8位,和内存、寄存器对齐,处理起来最方便。
发送顺序是LSB(最低有效位)在前,MSB(最高有效位)在后。举个例子,要发送0x55(二进制01010101),先发最低位1,再发0、1、0、1、0、1,最后发最高位0。
| 数据位长度 | 典型应用场景 | 说明 |
|---|---|---|
| 5 bit | 老式电传打字机 | 现在基本不用了 |
| 6 bit | 某些ASCII子集 | 很少见 |
| 7 bit | 标准ASCII码 | 0~127,不需要扩展字符时用 |
| 8 bit | 绝大多数嵌入式设备 | 最常用,一个字节对齐 |
| 9 bit | 多机通信地址/数据标志 | 第9位用来区分地址帧和数据帧 |
我曾经踩过的坑:用7位数据位传二进制数据(比如固件升级包),结果0x80~0xFF的数据全丢了。因为7位只能表示0~127。如果你要传任意字节,务必用8位数据位。
3.4 校验位:数据完整性的“小卫士”
校验位是可选的,有3种模式:无校验、奇校验、偶校验。它的作用很简单:让数据位+校验位中“1”的个数满足奇偶约定。
- 无校验(None):不发送校验位,帧长度最短,效率最高。适合短距离、低噪声环境。
- 奇校验(Odd):数据位中“1”的个数 + 校验位 = 奇数。比如数据位有3个1,校验位就是0(3+0=3奇数);有4个1,校验位就是1(4+1=5奇数)。
- 偶校验(Even):数据位中“1”的个数 + 校验位 = 偶数。
你想想看,校验位能发现什么?能发现奇数个bit翻转的错误。比如数据位有3个1,传输中一个1变成了0,接收方算出来只有2个1,加上校验位0还是2(偶数),但约定是奇校验,于是报错。但如果两个bit同时翻转,奇偶性不变,校验就失效了。
我的建议:在工业现场或长距离通信中,别只依赖奇偶校验。它只能检测奇数位错误,而且无法纠错。对于关键数据,建议加上CRC校验或校验和。奇偶校验更适合作为“快速筛查”手段。
3.5 停止位:帧结束的“句号”
停止位固定为高电平,长度可以是1、1.5或2个bit。为什么要有停止位?说白了就是给接收方一点时间处理刚收到的字节,准备好接收下一帧。
| 停止位长度 | 适用场景 | 说明 |
|---|---|---|
| 1 bit | 大多数标准通信 | 效率最高,最常用 |
| 1.5 bit | 老式设备、某些工控协议 | 只在5位数据位时使用,现在很少见 |
| 2 bit | 低速设备、高噪声环境 | 给接收方更多处理时间,但降低吞吐量 |
我一般默认用1位停止位。只有在接收方处理速度跟不上时,才会考虑2位停止位。比如早期的一些单片机,UART FIFO很浅,CPU又慢,用2位停止位能减少溢出风险。
3.6 波特率:通信的“节拍器”
波特率就是每秒传输的bit数,单位是bps(bits per second)。常见的波特率有9600、19200、38400、115200等。发送方和接收方必须用相同的波特率,否则数据会错位。
为什么会错位?举个例子,发送方以9600bps发一个字节(1+8+1=10bit),每bit约104μs。接收方如果配成19200bps,每bit只有52μs。接收方采样时,可能把起始位后的第2个bit当成第1个数据位,整个字节就全乱了。
波特率误差容忍度:一般要求收发双方的波特率误差不超过±2%。我习惯用晶振频率精确计算分频系数,而不是用近似值。比如11.0592MHz晶振配9600bps,分频系数是整数,误差为0。如果用12MHz晶振,分频后会有微小误差,短帧没问题,长帧累积误差就会导致错位。
3.7 完整帧格式配置示例
下面是一个实际项目中常用的配置。我习惯用8N1这个简写,意思是8位数据位、无校验、1位停止位。
// STM32 UART配置示例:115200-8N1
UART_HandleTypeDef huart1;
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200; // 波特率
huart1.Init.WordLength = UART_WORDLENGTH_8B; // 8位数据位
huart1.Init.StopBits = UART_STOPBITS_1; // 1位停止位
huart1.Init.Parity = UART_PARITY_NONE; // 无校验
huart1.Init.Mode = UART_MODE_TX_RX; // 收发使能
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE; // 无硬件流控
huart1.Init.OverSampling = UART_OVERSAMPLING_16; // 16倍过采样
HAL_UART_Init(&huart1);
这个配置下,一帧的总长度是:1(起始位)+ 8(数据位)+ 0(校验位)+ 1(停止位)= 10 bit。有效数据率是 115200 × 8/10 = 92160 bps,约11.5 KB/s。
帧效率对比(以115200bps为例):
| 配置 | 帧长(bit) | 有效数据率(bps) | 效率 |
|---|---|---|---|
| 8N1 | 10 | 92160 | 80% |
| 8E1 | 11 | 83781 | 72.7% |
| 8N2 | 11 | 83781 | 72.7% |
| 9N1 | 11 | 94254 | 81.8% |
可以看到,8N1是效率和可靠性之间的最佳平衡点。这也是为什么它成为嵌入式领域事实上的标准配置。
3.8 帧格式配置的避坑指南
最后,分享几个我这些年积累的经验:
- 收发双方必须完全一致:数据位、校验位、停止位、波特率,一个都不能错。我曾经遇到过两边都配成8N1,但一方用了1位停止位,另一方用了2位,结果每帧都错位。
- 校验位会影响数据位长度:如果使能了校验位,数据位要包含校验位。比如STM32中,配置WordLength=9且使能校验,实际数据位是8位+1位校验。这个细节很容易搞混。
- 波特率不是越高越好:115200在大多数场景下够用。超过1Mbps时,线缆长度、寄生电容、地线干扰都会成为问题。我一般超过460800就会考虑RS485或CAN。
- 用逻辑分析仪验证:配置完UART后,先用逻辑分析仪抓一下波形。看起始位、数据位、停止位是否和预期一致。这比看代码靠谱多了。
嗯,帧格式这部分就讲到这里。理解了这些基础,后面讲多机通信时,你就能明白为什么9位数据位和地址帧那么重要了。