4. USB PD消息结构:消息头、字节序、消息类型与ID

协议这东西,光看规范文档很容易晕。我记得自己第一次啃《USB PD 3.0规范》第6章时,满眼都是位域、偏移量和保留位,差点睡着。后来在调试台上抓着一包一包的数据对着看,才慢慢把那些0和1跟实际行为对应上。今天咱们就把消息结构一层层剥开,说白了就是:一个PD消息长什么样?里面每段bit都干嘛用的?

嗯,咱们从最核心的消息头(Message Header,简称Header)说起。所有PD消息——不管是你发个请求还是对端回个应答——都得先带上这个16比特的头,它就像快递单上的地址栏。

4.1 消息头(Header)结构

消息头固定占用2字节(16比特),但注意:字节序是小端序(Little-Endian)。什么意思?就是低字节在前(Byte0),高字节在后(Byte1)。我在项目里吃过这个亏,拿示波器抓出来的Raw数据直接用大端去解析,结果所有字段都串位,折腾了两小时才发现是字节序搞反了。

Bit位置 字段名称 长度(bit) 说明
15 Extended 1 0=标准消息,1=长消息(可附带数据对象)
14:12 Number of Data Objects (NumDO) 3 数据对象个数:0~7(0表示无Data Objects)
11:9 Message Type 3 消息类型编码(如0=GoodCRC,1=GotoMin等)
8 Port Power Role (PortPR) 1 0=Source(供电方),1=Sink(受电方)
7 Specification Revision (SpecRev) 1 0=rev 2.0/3.0(早期),1=rev 3.1+
6 Port Data Role (PortDR) 1 0=DFP,1=UFP
5:4 Reserved 2 必须为0
3:0 Message ID (MID) 4 消息ID,0~15循环,用于去重和重传
我个人习惯:在固件里用位域结构体定义Header,比如C语言里用__attribute__((packed))的结构体,然后直接memcpy到发送缓冲区。但要注意字节序——小端CPU直接对齐,大端CPU就得手动字节交换。

4.2 字节序与打包细节

再看一眼那个表格:Bit15是高位,Bit0是低位。但物理传输时,先发Byte0(低字节),再发Byte1(高字节)。举个例子:

  • 假设Header值为0x1234(大端表示)
  • 实际发送字节顺序:0x34(Byte0),然后0x12(Byte1)
  • 接收方拿到这两字节后,重组为小端值:0x3412(如果直接按大端拼是0x1234,就错了)

我曾经写过一段工具代码,从串口打印原始报文,结果打印出的Header值对不上。排查下来,是printf用的格式%04X默认把连续两个字节当作大端……嗯,从那以后我都在协议解析层统一先做字节序转换。

4.3 消息类型与分类

消息类型由Header的Bit[11:9]决定(加上Extended标志)。核心分两大类:

  • 控制消息(Control Message):只有Header,不带Data Object。比如GoodCRC、GotoMin、Accept、Reject等。
  • 数据消息(Data Message):Header后面跟着数据对象(Data Object),每个Data Object固定4字节。NumDO字段指示个数(1~7)。比如Source_Capabilities、Request、BIST等。

下面这张图把消息骨架画清楚——你看着它,再回头读规范会轻松得多。

USB PD消息结构(物理帧内部) SOP* 起始定界符 消息头(Message Header) — 4字节的SOF之后 Byte0(低), Byte1(高) Data Objects(0~7个,每个4字节,小端序) 由Header中NumDO字段指定数量 (若NumDO=0,此段不存在) CRC(4字节) + EOP 结束定界符
核心要点:消息头里的Message Type字段只有3比特,最大编码7。但实际类型不止8个,原因是加上Extended标志后,可以区分“标准控制消息”和“扩展消息”两个表。你不必背所有编码,但一定要养成查表的习惯——我每次写驱动都开着规范PDF的Table 6-2。

4.4 消息ID(MID)的作用

MID是Header的低4位(Bit[3:0]),范围0~15。别小看这4比特,它承担了去重和应答配对的功能。

  • 去重:接收方发现收到的MID跟上一个处理过的消息相同,且消息类型匹配,就直接丢弃——这能防止物理层重传造成重复处理。
  • 应答配对:发送方发一个非GoodCRC消息后,对端必须回一个GoodCRC应答。GoodCRC消息的MID必须等于原消息的MID。我见过一些第三方设备,GoodCRC里MID乱填,结果Source端一直收不到有效应答,超时后自动输出复位。
曾经有一次,我在调试PD诱骗器时发现总是发生Soft Reset。抓日志才发现:固件里有个bug,每次发Request前MID自增忘了回绕到0,跑到16就不再自增了,导致连续发多个相同MID的Request。对端一旦触发重传,MID一样,它就认为重复消息直接丢掉……恶性循环。解决办法很简单:MID = (MID + 1) & 0x0F。

4.5 常见消息类型速查

下面这个表是我从实战角度总结的——你看一遍有个印象,写代码时知道该查哪个方向就行。

消息类型编码(Bit11:9) 名称 分类 典型用途
000 GoodCRC 控制消息 应答所有非广播消息,必须带MID回显
001 GotoMin 控制消息 Source要求Sink切换到最小功率
010 Accept 控制消息 对方同意你的Request/Power_Nego等
011 Reject 控制消息 对方拒绝请求
100 PS_RDY 控制消息 电源已经准备好,可以拉载
101 Source_Capabilities 数据消息 Source广播自己的供电能力(含PDO)
110 Request 数据消息 Sink选择一个PDO,发送请求(含RDO)
111 BIST 控制消息(或数据消息) 测试模式,如进入Loopback
小技巧:当你用逻辑分析仪看PD总线时,先抓到一个Source_Capabilities消息,然后找它后面的Request→Accept→PS_RDY,这就像握手三步曲。如果中间出现Reject,多半是你的RDO参数超出Source能力——我在板级调试时,最常踩的坑就是PDO索引从1开始而不是0。

4.6 解析示例

假设我们抓到一个PD消息的原始字节(不含SOP和CRC):
0x41 0x12

步骤:

  1. 小端重组:0x1241
  2. 拆位域:
    • Bit15 Extended = 0
    • Bit14:12 NumDO = 001 (1个Data Object)
    • Bit11:9 MessageType = 100 (PS_RDY? 等一下,PS_RDY是控制消息,不应该有Data Object。所以这肯定不是PS_RDY,而是Source_Capabilities,MessageType=101才对?咱们再算:Bit11:9=100=4,查表是PS_RDY;但NumDO=1冲突。说明实际中可能出现错误,或者这个Header另有含义。嗯,这恰恰是实战中易错点:控制消息的NumDO必须为0,如果收到非0的控制消息,建议直接当无效丢弃。)

所以我建议你写解析函数时,先检查该消息类型是控制类还是数据类,如果控制类但NumDO≠0,记一次错误日志。我的解析库就加了这么个断言,曾经帮我抓到过一条虚假的芯片bug。

4.7 避坑总结

  • 字节序陷阱:永远记得从总线拿到的是小端,别直接当uint16_t去读。
  • MID溢出:一定要做& 0x0F处理,否则超过15后不归零会出大问题。
  • 控制消息 vs 数据消息:NumDO=0的不一定是控制消息(某些扩展消息也可能0个DO),但控制消息必须NumDO=0。
  • 保留位:Header的Bit[5:4]必须写0,发送时如果不小心置1,对端可能因为协议不符合规范而丢弃。
  • SpecRev字段:别搞混,最新的PD 3.1规范里Version字段已经扩展,但Header还是只占1bit,配合其他机制区分版本。

好了,消息结构这部分暂时到这。你把这些位域记熟,后面学Protocol Layer和Policy Engine时就像拿钥匙开门——每个字段的作用都会反复用到。下次咱们直接开干:怎么用代码构造一个标准消息并发送出去。