4. 消息头结构:Message Header 各字段解析
拿到一个 USB PD 消息,你第一眼看到的就是 16 位的 Message Header。这个头可不简单,它直接告诉解析器该做什么操作。说白了,它是整个 PD 通信的调度指令。我在调试项目中遇到过好几次,明明信息体(Data Object)的数据完全正确,但就是解析不出来。最后发现,是 Header 里某个位写错了。
今天我们就把它彻底拆开。你对照协议看,会更清楚。
4.1 消息头,到底长什么样?
嗯,这张图基本把 16 个位(Bit15 到 Bit0)划成了 8 个区域。每个区域的任务都不一样。我们一个一个来看。
4.2 字段逐个拆解
4.2.1 Extended(Bit15) — 扩展标志
这个位在 PD 3.0 引入,3.1 版本里进一步强化。你想想,PD 2.0 就没有它。它用来指示后面是否跟着 Extended Message。说白了,当你需要传递超过 260 字节的数据时(比如固件升级、大容量 VDM),就需要把这个位置 1。
我的建议:调试时如果发现解析工具报错,先检查这个位。我曾经在抓包时发现解析器把一段 500 字节的长消息当成多个短消息拆分,就是因为 Extended 位没置对。其实 PD 协议的 GoodCRC 永远不会有扩展消息,别搞混。
4.2.2 Number of Data Objects(Bit[14:12])— 数据对象数量
这 3 个位决定了消息体里带了几个 Data Object。范围是 0 到 7。我刚开始接触 PD 协议时犯过个错误:以为至少有 1 个数据包才有数据。实际上,某些控制消息(比如 Hard Reset、Soft Reset)的 NOD 完全是 0。如果解析时强行读数据体,就会读到乱码。
| Bit[14:12] | 数值 | 含义 |
|---|---|---|
| 000 | 0 | 无数据对象(仅控制消息) |
| 001 | 1 | 1个数据对象 |
| 010 | 2 | 2个数据对象 |
| … | … | … |
| 111 | 7 | 7个数据对象 |
4.2.3 Message Type(Bit[11:8])— 消息类型
每个消息都有类型。控制消息(Ctrl Msg)和数据消息(Data Msg)的区别就在这里。控制消息的 NOD 固定为 0,而数据消息至少 1 个。我建议你把这个字段看成“交换机”。
举个例子:GoodCRC 的 Message Type 是 0x01,Source_Capabilities 是 0x01 但它是数据消息。同样数值,但靠 Header 中的另一个位(Bit4)区分。我们先看基础。
| Message Type 值 | 控制消息示例 | 数据消息示例 |
|---|---|---|
| 0x01 | GoodCRC | Source_Capabilities |
| 0x02 | GotoMin | Request |
| 0x03 | Accept | BIST |
| 0x04 | Reject | Sink_Capabilities |
| 0x08 | PS_RDY | — |
4.2.4 Reserved(Bit[7:6])— 保留位
这两个位必须写 0,接收端必须忽略。我在兼容性测试时见过一种奇怪现象:某款芯片把保留位写成了 01b,结果被另一款芯片踢出协议。后来看勘误表,那款芯片的固件有 bug。你写代码时注意,别往上写任何值。
4.2.5 Port Role(Bit5)— 端口角色
这个位标记你当前是 DFP(下行端口,Host)还是 UFP(上行端口,Device)。1 表示 DFP,0 表示 UFP。你知道为什么重要?因为 PD 协议的消息格式随角色不同有细微差异。有一次我在调试 Source 侧,发现 Sink 发过来的 Request 始终被 Source 拒绝。查了一天,最后发现 Sink 的 Port Role 写成了 DFP。角色定义乱了,协议就全错了。
避坑:DRP(双角色端口)切换角色时,这个位要跟着硬件状态一起变。如果硬件角色已经变了,Header 里还写着旧角色,协议就会死锁。我曾经用逻辑分析仪对比过几次,才锁定是这个原因。
4.2.6 Specification Revision(Bit[4:3])— 协议版本
| Bit[4:3] | 对应版本 |
|---|---|
| 00b | PD 2.0 |
| 01b | PD 3.0 |
| 10b | PD 3.1 |
| 11b | 保留 |
我个人习惯直接看这个位判定协议版本。很多解析工具根据它决定后面的消息体格式。比如 PD 2.0 的 Source_Capabilities 只能带 7 个 PDO,PD 3.0 可以带更多。你如果写错版本位,接收方解析出的数据格式就是错的。
关键点:USB-IF 要求 PD 3.0 设备必须兼容 2.0 消息,但解析这个位时,接收端必须按收到的版本号处理。不能假设所有消息都是 3.0。
4.2.7 Cable Plug(Bit2)— 线缆插拔
这个位在普通场景用得少,主要用在 E-marked Cable 的识别和配置。我建议初学者先知道它存在即可,具体字段含义在后续章节会展开。
4.2.8 CMD / Data Role(Bit0)— 角色与命令指示
这个位有两种解释:控制消息里作为命令类型(比如是否携带 Vendor Defined Message),数据消息里作为数据角色。它是个复用的位。调试时最容易被忽略。你看协议里,有些消息要求这个位固定为 0,有些又必须为 1。对照 spec 表一比对,基本不会错。
我的实战总结:写 PD 固件时,我通常把 Message Header 打包成 16 位整数再填充。对照这张表格依次移位,基本不会漏位。
4.3 你可能会遇到的坑
- NOD 越读越乱:控制消息 NOD 是 0,强行读数据体,导致指针越界。解析前先判断 Message Type。
- 版本次数认定:同一个总线,Source 发 3.0 版本消息,Sink 回 2.0 版本消息,协议会不会崩?不一定。但解析时必须按各自收到的版本处理。
- 位序反转:有些人搞不清楚 MSB 和 LSB。我曾经见过有人把 Message Type 的 4 位顺序装反了,结果 GoodCRC 被解析成 Reject。收端一连拒绝了十几次。
4.4 从 Header 开始自己动手读取一个消息
假设你从一个 PD 总线捕获到 16 位原始数据:0x2D00
我们一步步拆:
- 二进制:
0010 1101 0000 0000 - Bit15=0,不是扩展消息。
- Bit[14:12]=
001(1),有 1 个 Data Object。 - Bit[11:8]=
1101(0xD),不是标准控制消息,查表可知是 Source_Capabilities(PD 3.0 的定义)。 - Bit[7:6]=00,正常。
- Bit5=1,Port Role 是 DFP。
- Bit[4:3]=01,版本为 PD 3.0。
- Bit2=0,线缆插拔未设置。
- Bit0=0,数据角色。
// 伪代码示例:解析 Message Header
uint16_t header = 0x2D00;
uint8_t extended = (header >> 15) & 0x01;
uint8_t numObjects = (header >> 12) & 0x07;
uint8_t msgType = (header >> 8) & 0x0F;
uint8_t portRole = (header >> 5) & 0x01;
uint8_t specRev = (header >> 3) & 0x03;
uint8_t cablePlug = (header >> 2) & 0x01;
uint8_t cmdBit = header & 0x01;
printf("Num DOs: %u, MsgType: 0x%X, PortRole: %s\n",
numObjects, msgType, portRole ? "DFP" : "UFP");
提示:如果你手头有逻辑分析仪或 USB PD 抓包工具,可以抓一条 Source_Capabilities 消息,对照 Header 的 16 位和这个解析流程过一遍,很容易掌握。