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循环,用于去重和重传 |
__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等。
下面这张图把消息骨架画清楚——你看着它,再回头读规范会轻松得多。
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端一直收不到有效应答,超时后自动输出复位。
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 |
4.6 解析示例
假设我们抓到一个PD消息的原始字节(不含SOP和CRC):
0x41 0x12
步骤:
- 小端重组:0x1241
- 拆位域:
- 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时就像拿钥匙开门——每个字段的作用都会反复用到。下次咱们直接开干:怎么用代码构造一个标准消息并发送出去。