PD 描述符结构详解
聊 USB PD 协商,如果你不懂描述符结构,那基本就是盲人摸象。我记得刚接触 PD 协议时,盯着抓到的报文看了半天,满眼都是“怎么这么多 0x00”。后来才明白,是我根本没看懂 PDO 里的每个 bit 在说什么。
这一章,我们就把 Source Capabilities 和 Sink Capabilities 拆开来,一个个字段过一遍。你放心,不需要背,理解了逻辑,以后看任何 PD 报文你都能一眼找到问题。
Source Capabilities 的结构
Source 端第一次协商会先发一个 Source Capabilities 消息。这个消息里面包含若干个 PDO(Power Data Object),每个 PDO 就是一条供电能力声明。说白了,就是充电器告诉设备:“我能给这些电压电流组合,你选一个。”
每个 PDO 固定 32 位,但根据供电类型不同,bit 分配完全不同。我个人习惯把 PDO 分成三大类:
- 固定供电 (Fixed Supply):大部分充电器常用的类型,比如 5V/3A、9V/2A。
- 可变供电 (Variable Supply):电压和电流可以在指定范围内调整,多见于一些老款 PD 充电器。
- 电池供电 (Battery Supply):用于大功率场景,比如笔记本充电,电压和电流范围更宽。
我们看一个固定供电 PDO 的位域结构(参考 USB PD 3.1 规范):
固定供电 PDO 位分配 (31..0)
| 位范围 | 字段名 | 说明 |
|---|---|---|
| 31:30 | 保留 (Reserved) | 必须为 00 |
| 29:28 | 供电类型 (Fixed Supply = 00) | 00 表示固定供电 |
| 27:20 | 电压 (Voltage, 50mV/step) | 数值 × 50mV,例如 100 = 5V |
| 19:10 | 电流 (Current, 10mA/step) | 数值 × 10mA,例如 300 = 3A |
| 9:0 | 标志位 | bit9 双角色、bit8 快速角色交换、bit7 备用等 |
举个例子:你抓到一个 PDO 原始值是 0x000191C8,怎么拆?
0x000191C8 = 0b 0000 0000 0000 0001 1001 0001 1100 1000
位31:30 = 00 → 保留
位29:28 = 00 → Fixed Supply
位27:20 = 0000 0110 → 6 → 电压 = 6 × 50mV = 0.3V? 不对,继续看。
等等,我搞错了。应该从最高位开始数起:
位31:30 (bits 31-30) = 00
位29:28 (bits 29-28) = 00 (Fixed Supply)
位27:20 (bits 27-20) = ? 我们换种方式:
0x000191C8 按字节拆:0x00 0x01 0x91 0xC8
二进制:0000 0000 | 0000 0001 | 1001 0001 | 1100 1000
bits 27:20 对应第1、2字节的低半?其实规范是按小端序定义的,但在报文里 PDO 是 big-endian 传输。我建议直接看规范原文。这里我们记住:固定供电的电压字段占8位,电流占10位。
实际项目中,我更推荐用工具解析。不过你手动算一次,以后调试起来心里有数。
Sink Capabilities 的结构
Sink 端在接收到 Source Capabilities 后,会回复一个 Request 消息。这个消息包含的 RDO(Request Data Object)也是 32 位,但结构大不同。
我遇到过一个坑:设备请求的电压电流必须和 Source 提供的一个 PDO 匹配,否则 Source 会拒接。嗯,这里要注意——不是随便填的。
RDO 的主要字段:
- Object Position (bits 31:28):指明你要选第几个 PDO(1-based)。比如 Source 发了两个 PDO,你填 1 就是选第一个。
- Operating Current (bits 19:10):请求的工作电流,单位 10mA。
- Max Current (bits 9:0):允许的峰值电流,一般和 Operating Current 相同。
- 还有一些标志位,比如 GiveBack、Capability Mismatch 等,用于快速角色交换或错误处理。
我的习惯:调试时先看 Object Position 对不对。有一次产线反馈充电器一直无输出,结果发现设备选了位置 2 而充电器只发了 1 个 PDO——Source 收到后直接发拒绝。改固件后立马正常。
PDO 类型对比
三种 PDO 的适用场景差异很大,我整理了一张表方便你查阅:
| 类型 | 电压表示 | 电流/功率表示 | 典型应用 |
|---|---|---|---|
| Fixed Supply | 固定电压,50mV/步 | 最大电流,10mA/步 | 手机充电、小功耗设备 |
| Variable Supply | 最小/最大电压,50mV/步 | 最大电流,10mA/步 | 老款 PD 充电器、可调压适配器 |
| Battery Supply | 最小/最大电压,50mV/步 | 最大功率(以 mW 为单位) | 笔记本、大功率充电站 |
你想想看,如果你需要支持 20V/5A 这种大功率,用 Fixed Supply 其实也够,但 Battery Supply 的功率范围更宽,而且可以用更少的 PDO 覆盖更多场景。我个人喜欢在固件里同时放一个 Fixed Supply(5V/3A)给兼容设备,再加一个 Battery Supply(5-20V @ 100W),这样基本都能握手成功。
流程图:Source Capabilities 消息内的 PDO 组织
我们用一个 SVG 把整条消息的结构画出来,这样更直观。
我曾经调试过一个设备,Source 明明发了 3 个 PDO,设备却一直无法协商 9V。后来抓日志发现,设备只解析了第一个 PDO,因为代码里写死了“只读取一个 PDO”。所以牢记:一定要循环处理整个 PDO 数组,不要想当然。
Sink 端的 RDO 细节
Sink 端发回 Request 消息时,RDO 里还有一个容易被忽略的字段:Capability Mismatch(位 28)。如果 Sink 发现 Source 提供的所有 PDO 都不满足自己的要求,就会把这个位置 1,然后 Source 可以决定是否提供一个新的 Capabilities 或者断开。我在项目中见过有些充电器收到 Mismatch 后直接无响应,其实就是人家没实现这一条。
避坑:不要依赖 Capability Mismatch 来做重协商。最好的做法是 Source 端主动根据设备的需求调整 PDO 顺序或者提供更符合的电压等级。我曾经在这里栽过——设备反复发 Mismatch,充电器不理,导致循环超时,最后强制重启才解决问题。
简单总结一下本章核心:
- PDO 是 Source 的供电能力声明,每个 32 位,类型不同位域不同。
- RDO 是 Sink 的供电请求,必须引用一个存在的 PDO 位置。
- 解析时注意字节序和位偏移,推荐用规范附录里的位图。
- 实际工作中多看看有问题的抓包,你会发现大多数协商失败都是描述符写错了。
嗯,这一章就到这里。你只要把上面几种 PDO 的结构记熟,后面看任何 PD 报文都能快速定位问题。