4. 消息头结构:Message Header 各字段解析

拿到一个 USB PD 消息,你第一眼看到的就是 16 位的 Message Header。这个头可不简单,它直接告诉解析器该做什么操作。说白了,它是整个 PD 通信的调度指令。我在调试项目中遇到过好几次,明明信息体(Data Object)的数据完全正确,但就是解析不出来。最后发现,是 Header 里某个位写错了。

今天我们就把它彻底拆开。你对照协议看,会更清楚。

4.1 消息头,到底长什么样?

USB PD 消息头结构 USB PD 消息头(Message Header)结构 Extended Bit15 Num Data Objects Bit[14:12] Message Type Bit[11:8] Reserved Bit[7:6] Port Role Bit[5] Spec Rev Bit[4:3] Cable Plug Bit[2] CMD Bit[0] 字段说明 • Extended (Bit15):扩展标志,PD 3.1 新增。 • Number of Data Objects (Bit[14:12]):数据对象数量,3位表示0~7。 • Message Type (Bit[11:8]):消息类型,4位,区分 GoodCRC / Source_Capabilities 等。 • Reserved (Bit[7:6]):保留位,须写0。 • Port Role (Bit5):端口角色,1=DFP / 0=UFP。 • Spec Revision (Bit[4:3]):协议版本,PD3.0 写 01b。 • Cable Plug (Bit2):线缆插拔事件。 • CMD / Data Role (Bit0):控制命令或数据角色。

嗯,这张图基本把 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] 数值 含义
0000无数据对象(仅控制消息)
00111个数据对象
01022个数据对象
………
11177个数据对象

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 值 控制消息示例 数据消息示例
0x01GoodCRCSource_Capabilities
0x02GotoMinRequest
0x03AcceptBIST
0x04RejectSink_Capabilities
0x08PS_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] 对应版本
00bPD 2.0
01bPD 3.0
10bPD 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

我们一步步拆:

  1. 二进制:0010 1101 0000 0000
  2. Bit15=0,不是扩展消息。
  3. Bit[14:12]= 001(1),有 1 个 Data Object。
  4. Bit[11:8]= 1101(0xD),不是标准控制消息,查表可知是 Source_Capabilities(PD 3.0 的定义)。
  5. Bit[7:6]=00,正常。
  6. Bit5=1,Port Role 是 DFP。
  7. Bit[4:3]=01,版本为 PD 3.0。
  8. Bit2=0,线缆插拔未设置。
  9. 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 位和这个解析流程过一遍,很容易掌握。