第4章 基本报文结构:前导码、SOP、报文头、数据对象、CRC校验、EOP的格式与长度

把报文结构拆开来看,其实就是一帧数据从开始到结束,每个部分都扮演特定角色。我刚开始接触PD握手协议时,总觉得不就发几个字节嘛,结果被各种不同长度的前导码坑了好几次……后来才明白,报文结构就像人的骨架——骨头长短、位置错了,整个数据就瘫了。

这一章我们把这六个字段逐个揉碎了讲。每个字段的长度、格式、作用,我都会结合项目里踩过的坑来说。嗯,你要是面试被问到“报文结构”,能把这些讲清楚,基本就过关了。

4.1 前导码 (Preamble)

前导码是报文的最前面一段固定序列,长度通常是16字节(128比特)。它的作用很简单——让接收端找到报文的起始位置,并完成同步。

格式:交替的“01b”和“00b”序列,以“00b”结尾。举个例子,16字节的前导码就是:01 00 01 00 01 00 ... 重复8次,最后多一个00。

我个人习惯把前导码叫做“敲门砖”——接收端本来在睡大觉(空闲态),前导码的电平翻转把它敲醒了,好准备开始接收数据。我曾经遇到一个设备,前导码长度只有14字节,结果对端总是丢第一个字节,排查了两天才发现是两端前导码长度没对齐。

长度建议:大多数PD控制器默认16字节。如果链路噪声大,可以适当加长到24字节,但会增加开销。我们项目里一般固定16字节,不改。

4.2 SOP (Start of Packet)

SOP是“数据包起始定界符”,告诉接收端:前导码结束了,真正有用的报文头从这里开始。

格式:由K码与D码交替组成,总长度为5字节。以USB PD为例,SOP序列是:K-code, K-code, D-code, D-code, K-code。K码用于特殊控制,D码是普通数据。

注意:SOP之后不能再有前导码的电平翻转。为什么?因为SOP是用非归零(NRZ)编码的,跟前导码的曼彻斯特编码不一样。接收端通过检测K码的特定模式来判断是否进入SOP阶段。

我曾经在产线调试时,有一批芯片的SOP序列最后一位K码错写成了D码,结果所有数据包都被当成“非法起始”丢弃了。这类问题用逻辑分析仪抓波形一眼就能看穿。

4.3 报文头 (Header)

报文头紧跟在SOP之后,长度固定为16比特(2字节)。它包含本次传输的关键控制信息。

结构:报文头由以下几个字段组成(参考USB PD 3.0规范):

字段比特位置长度(bit)说明
Message Type15:124消息类型,如GoodCRC、Request等
Port Data Role111端口数据角色:Source
Specification Revision10:92PD协议版本号(2.0/3.0)
Port Power Role81电源角色:Provider/Consumer
Message ID7:44消息编号,用于去重和重传
Number of Data Objects3:04后面跟随的数据对象个数(0~7)

报文头是整个报文的“大脑”——它告诉接收方这是什么类型的数据(电源请求?握手确认?还是错误报告?),版本是多少,角色是什么。我每次调协议栈,都会先打印报文头,确认字段没配错。

4.4 数据对象 (Data Object)

数据对象是报文的“信息载体”,长度可变,每个数据对象固定为4字节(32比特)。报文头中的Number of Data Objects决定后面跟几个数据对象(0~7个),所以报文总长 = 报文头(2B) + 数据对象个数 × 4B。

格式:每个数据对象内部又分不同子结构,取决于消息类型。比如电源能力(Source Capabilities)的数据对象包含电压、电流、峰值电流等信息。具体子字段我们后面章节再拆,现在你记住:数据对象就是一段4字节对齐的二进制信息。

常见场景:Request消息有1个数据对象(请求的电压/电流),电源能力消息有多个数据对象(最多7组电压电流组合)。

注意:数据对象的字节序是小端(Little-Endian)!低地址存放低字节。我见过有人搞反了,结果电压值读出来是0x3C 0x00(正确是60mV),实际却是0x00 0x3C(0.06mV),设备直接休眠了——因为电压太低。

4.5 CRC校验

CRC(循环冗余校验)用来保证从SOP到最后数据对象的完整性。长度固定为4字节,使用CRC-32算法:0x04C11DB7(多项式)。

计算范围:从SOP的第一个字节开始,包含报文头和所有数据对象,不包括前导码。接收端收到后重新算一遍CRC,如果与发送过来的CRC不同,就说明传输有错误,直接丢弃整个包。

实现细节:实际计算时,SOP的K码需要先转成对应的D码值再参与计算。嗯,这里有坑——我处理过一版固件,它没做码字转换,CRC永远算不对。

避坑指南:我曾经在低功耗产品上用了软件CRC计算,结果每次发包都延迟几十微秒,导致握手超时。后来换成硬件CRC模块,速度提升了10倍。如果你用MCU,尽量用内置CRC外设。

4.6 EOP (End of Packet)

EOP是报文的结束标记,长度固为2字节,格式为固定的K码对:K1, K2。它的作用是告诉接收端:数据已经发完,后面都是空闲状态了。

EOP之后,发送端总线转为空闲电平(通常为高阻或0V),接收端就知道这个帧到此为止。如果接收端检测到EOP,就会开始处理这个报文;如果没检测到,就会一直等,等到超时,然后丢弃当前缓存。

我遇到过一个问题:EOP后总线空闲电平没保持住,被噪声干扰了一下,接收端误认为是下一个SOP,结果报文错位。解决办法是在EOP后增加一段总线保持时间(例如10µs),让电平稳定再释放。

报文结构总览

下面这张图把整个报文结构串起来,你可以对着看每个字段的顺序和大致长度。

PD握手与PSK报文结构 前导码 16字节,曼彻斯特 SOP 5字节,K/D码 报文头 2字节,固定 数据对象 0~7 × 4字节 CRC 4字节 EOP 2字节 16B 5B 2B N × 4B 4B 2B 说明: • CRC的计算范围包含SOP、报文头和所有数据对象,不包括前导码与EOP。 前导码 & SOP • 前导码:16字节,曼彻斯特码,用于同步 • SOP:5字节,包含K码/D码,标志报文开始 • 注意:SOP之后必须停止曼彻斯特编码 报文头 & 数据对象 & CRC & EOP • 报文头:2字节,固定,包含类型、角色等 • 数据对象:每项4字节,小端序 • CRC:4字节,CRC-32,多项式0x04C11DB7

各字段长度与格式汇总

字段长度(字节)编码方式是否参与CRC
前导码 (Preamble)16 (可调)曼彻斯特否
SOP5K/D码 (NRZ)是
报文头 (Header)2二进制小端是
数据对象 (Data Object)N×4 (N=0~7)二进制小端是
CRC4CRC-32自身不参与
EOP2K码对否

你看这张表,CRC参与范围是SOP、报文头、数据对象——这三个字段加起来,就是接收端需要“验证完整性”的部分。前导码和EOP不参与CRC,因为它们只是同步和结束的标志,不携带有效信息。

一个完整的PD报文最小长度=16(前导码)+5(SOP)+2(报文头)+0(数据对象个数)+4(CRC)+2(EOP)=29字节。如果带一个数据对象,就是33字节。这个最小值经常在单片机启动时的握手阶段出现,比如设备只发一个Hard Reset命令——无数据对象,报文头里Number of Data Objects = 0。

嗯,最后我提醒一句:实际解析报文时,**一定**要从SOP开始算CRC范围,而且前导码可能会因为噪声干扰而出现几比特的毛刺,导致接收设备误判起始位置。我调试时习惯在捕获到的完整数据帧之前,先丢掉前几个疑似垃圾的比特,再重新对齐前导码。这个习惯帮我省了不少事儿。