第3章 PD消息结构基础:Message Header、Data Object
说到USB PD协议,很多人一上来就啃规范文档,结果被那些位域和偏移量搞得头大。我当年也是一样,看着几十页的Message Header定义,脑子里全是浆糊。后来慢慢捋清楚了,其实核心就两层:消息头(Message Header)和数据对象(Data Object)。这就好比寄快递——快递单是消息头,包裹里的货才是数据对象。
我们这一章就把这两个概念彻底掰开揉碎。先别急着背字段,先搞清楚它们是怎么拼成一条PD消息的。
3.1 一条PD消息长什么样?
一条完整的PD消息在物理层传输时是一串比特流。但在协议层,它由三个部分组成:
- Preamble(前导码,4字节)——同步用的,接收方靠它锁定时钟。
- SOP(帧起始,2字节)——标识消息从哪里开始,还区分了消息是发给谁(比如Source→Sink还是Sink→Source)。
- Message Body(消息体,可变长度)——这才是我们关心的,包含Message Header和若干个Data Object。
消息体的结构特别清晰,我画了张图帮你秒懂:
Message Header 永远在最前面,后面跟着一个或多个 Data Object。注意:不是每条消息都有 Data Object,有些控制消息(比如 Hard Reset、Accept)只有 Header,没有 Data Object。这种消息我们叫命令消息(Control Message),而带 Data Object 的叫数据消息(Data Message)。
3.2 Message Header 详解
Message Header 只有 2 字节(16 位),但信息密度极高。我习惯把它拆成三个功能区:
| 位域(Bit) | 字段名 | 长度(位) | 说明 |
|---|---|---|---|
| 15 | Extended | 1 | 0=普通消息,1=扩展消息(比如VDM)。 |
| 14:12 | Port Number / Reserved | 3 | 用于多端口设备,一般为0。 |
| 11:9 | Number of Data Objects | 3 | 后面跟着几个Data Object(0~7)。 |
| 8 | Message Type (MSB) | 1 | 与位[5:0]合并为完整Message Type。 |
| 7:6 | Specification Revision | 2 | 01b=3.0, 10b=3.1 等。 |
| 5:0 | Message Type (LSB) | 6 | 具体的消息类型(比如SourceCapabilities=0x01)。 |
我最常关注的是 Number of Data Objects 和 Message Type。Number of Data Objects 告诉你该解析多少个4字节数据块;Message Type 告诉你这些数据块的含义。举个例子:如果你拿到的 Message Header 是 0x0C21,二进制拆开:
0x0C21 = 0000 1100 0010 0001
位15(Extended) = 0
位14:12(Port) = 000
位11:9(NumDO) = 110 = 6 → 有6个Data Object
位8(MSB) = 0
位7:6(Rev) = 10 = 2 → 对应PD 3.0
位5:0(LSB) = 000001 = 1 → Source Capabilities
嗯,这就是一个标准的 Source Capabilities 消息,带 6 个 Data Object。我遇到过好几次调试中因为 Message Header 解析错误,导致解析出错。所以我会在初始化时先打印 Message Header 的原始十六进制值,再逐位核对。
小技巧:用Python解析Message Header时,可以用struct.unpack('>H', raw_bytes[:2])[0]拿到两个字节,然后做位操作。我通常写个函数parse_msg_header(header)返回字典,包含num_objects、message_type、spec_rev等字段。这样排查问题快很多。
3.3 Data Object 家族
Data Object 虽然是 4 字节块,但里面的内容完全取决于 Message Type。在 Source Capabilities 消息中,常见的 Data Object 有:
- PDO(Power Data Object):描述一个Source能提供的固定电压和电流。
- BatChg(Battery Charging PDO):旧版电池充电规格,现在很少用了。
- APDO(Augmented PDO):PD 3.0 引入的可编程电源能力,支持电压和电流动态调整。
每个 PDO 内部又分为固定PDO(Fixed PDO)、可变PDO(Variable PDO)、电池PDO(Battery PDO),以及 APDO。它们的位域分配各不相同。比如一个固定PDO:
| 位域 | 字段 | 说明 |
|---|---|---|
| 31:30 | Power Reserved 1 | 必须为01b或10b?不同修订版有差异。 |
| 29 | Unconstrained Power | 1表示电源不受限(比如内部电池供电时也能提供)。 |
| 28 | USB Communications Capable | 1表示支持USB通信。 |
| 27 | Dual Role Power | 1表示支持DRP(双角色电源)。 |
| 26 | Suspend in Fixed PDO | PD 3.1 新增的挂起标志。 |
| 25:20 | Voltage in 50 mV units | 编码电压值,比如50mV × 100 = 5V。 |
| 19:10 | Maximum Current in 10 mA units | 编码电流值,比如10mA × 300 = 3A。 |
| 9:0 | Reserved | 填0。 |
为什么要把电压和电流编码成这些奇怪的倍数?说白了是为了用较少的位表示足够范围。50mV步进可以覆盖0~51.15V,10mA步进可以覆盖0~10.23A——对目前PD应用完全够用。
曾经踩过的坑:我在一个项目中用了一个第三方USB PD协议栈,它把Data Object解析成电压/电流时,没有检查Power Reserved bits。结果一个“非法编码”的PDO被当成有效电源推给了PMIC,导致充电异常。所以我建议,解析时一定要校验保留位和规范限定的合法范围。该拒绝的就拒绝,别手软。
3.4 消息类型速查
为了方便你平时快速识别,我把PD 3.0/3.1 常见的消息类型整理了一下(不完全,但够你用):
| Message Type | 码值(二进制) | 含义 | 带不带Data Object? |
|---|---|---|---|
| GoodCRC | 0x01 | 硬件层确认,由物理层自动回复 | 否 |
| Source_Capabilities | 0x02 | Source宣告自己能提供的功率组合 | 是(PDO列表) |
| Request | 0x03 | Sink向Source提出功率请求 | 是(1个RDO) |
| Accept | 0x04 | Source接受请求 | 否 |
| Reject | 0x05 | Source拒绝请求 | 否 |
| PS_RDY | 0x06 | 电源已就绪 | 否 |
| Get_Source_Cap | 0x07 | Sink请求Source重新宣告能力 | 否 |
| VDM | 0x0F | 厂商自定义消息 | 视情况 |
你注意看,很多控制消息(像GoodCRC、Accept、PS_RDY)不带Data Object。它们的Message Header里Number of Data Objects字段为0。解析时如果发现这个字段为0,就别去读后面的4字节了,否则会读到上层消息的残留数据。
3.5 动手解析:一个SourceCapability帧
假设我们在USB PD总线上嗅探到一个数据包(Raw Hex):
2C 21 03 B0 19 01 2C F1 19 01 4B F1 19 01 69 F1 19 01 88 F1 19 01 A6 F1 19 01 C5 F1
我们来手动拆解一下(我会用Python风格写一点伪代码,其实你也可以在Terminal里跑真的Python):
raw = bytes.fromhex('2C2103B019012CF119014BF1190169F1190188F11901A6F11901C5F1')
# 前2字节是Message Header
header = raw[:2]
header_int = struct.unpack('>H', header)[0] # 0x2C21
num_objects = (header_int >> 9) & 0x7 # = 6
msg_type = ((header_int >> 8) & 0x1) << 6 | (header_int & 0x3F) # = 0x02
# 后面跟着6个Data Object,每个4字节
for i in range(num_objects):
do_bytes = raw[2 + i*4: 2 + (i+1)*4]
do_int = struct.unpack('>I', do_bytes)[0]
# 解析固定PDO的电压和电流
voltage_mv = ((do_int >> 20) & 0x3F) * 50
current_ma = ((do_int >> 10) & 0x3FF) * 10
print(f'PDO{i+1}: {voltage_mv}mV / {current_ma}mA')
运行后你会看到:
PDO1: 5000mV / 3000mA (5V 3A)
PDO2: 9000mV / 3000mA (9V 3A)
PDO3: 12000mV / 3000mA (12V 3A)
PDO4: 15000mV / 3000mA (15V 3A)
PDO5: 20000mV / 3000mA (20V 3A)
PDO6: 20000mV / 5000mA (20V 5A)
这就是一个标准的 Source Capabilities 宣告列表。注意看:前5个PDO的电流都是3A,第6个突然变成5A——这是因为有些Source支持在20V下提供更大电流(比如配合EPR线缆)。
个人建议:实际开发中不要手动去算位域,直接用现成的PD库(比如Python的pypd或C的tcpmdrv)解析。但一定要理解底层结构——否则遇到诡异的数据包你根本不知道错在哪。我每次写PD解析器,第一件事就是写一个Message Header的打印函数,输出所有字段的十进制/二进制值。
3.6 本章小结
这一章我们搞清楚了:
- PD消息体 = Message Header(固定2字节) + Data Object列表(每个4字节)。
- Message Header里最关键的是 Number of Data Objects 和 Message Type。
- Data Object的具体结构取决于Message Type。在Source Capabilities中,我们面对的是PDO,里面编码了电压、电流和一些特性标志。
- 不用死记所有位域,但要具备手动拆解的能力——遇到bug时能快速定位。