USB PD 消息类型:控制消息、数据消息、扩展消息
PD 协议的核心就是靠消息对话。消息分三类,每一类的用法和结构都不一样。我刚接触 PD 时,觉得这些分类有点多余,后来才发现——分不清类型,抓包分析都无从下手。
控制消息(Control Messages)
控制消息是最短的一类。它们没有数据体,只有消息头。说白了,就是协议里的“感叹词”——Yes、No、准备好了、重试、CRC 校验通过……
控制消息的长度固定为 0 字节数据,消息头的 Type 字段标识具体含义。常用的控制消息有这些:
- GoodCRC:接收方确认收到上一条消息,CRC 无误。我调试时最先看的就是它——链路有没有通,GoodCRC 说了算。
- Accept:响应方同意对方的 Request。
- Reject:响应方拒方的 Request,常见于电压/电流请求超出能力。
- PS_RDY:电源已经切换到目标电压,Source 告诉 Sink 可以开始用新电压了。
- Get_Source_Capabilities:Sink 主动请求 Source 的能力清单。
- Soft_Reset / Hard_Reset:复位链路,重新协商。
避坑提醒: 我曾经以为 GoodCRC 和 ACK 是一回事,其实它们分别在协议层和事务层。如果只盯着应用层的 ACK,忽略 GoodCRC 是否及时回复,那在噪声环境下很容易丢消息。我建议你在抓包时先看 GoodCRC 的往返延迟,超过 200μs 就该查 PCB 走线了。
数据消息(Data Messages)
数据消息携带了实际的数据对象(SOP 或 SOP' 后的 Data Object / VDM Header / …)。它们才是真正的“谈判筹码”——能力清单、请求、供应商自定义指令都靠它。
数据消息的长度不固定,最长为 26B(含消息头)。消息头里除了 Type,还有一个 Number of Data Objects 字段,告诉接收方后面跟了几个数据对象。
常见的数据消息:
- Source_Capabilities:Source 广播自己能提供的 PDO(最多 7 个电源数据对象)。我习惯先看它的 PDO 顺序——通常第一个是 5V/3A 默认。
- Request:Sink 从 Source_Capabilities 里选一个 PDO,提出具体电压电流请求。
- BIST(Built-In Self Test):用于生产测试,消息类型编号 8。
- VCONN 相关的 VDM:厂商自定义消息,也归入数据消息大类。
个人经验: 有一次客户设备总是在收到 Source_Capabilities 后回复 Request,但 Source 一直不进入 Ready 状态。我一看抓包,好家伙——Request 里填的 PDO 序号是 0,而 Source 的 PDO 序号从 1 开始。这就是数据消息结构理解不到位踩的坑。
下面是一个简化的数据消息头(SOP Packet)结构示例:
// USB PD 数据消息头(16 bits)
struct pd_msg_header {
unsigned ext : 1; // 0=数据消息, 1=扩展消息
unsigned num_objs : 3; // 数据对象个数(0~7)
unsigned message_type : 4; // 消息类型编码
unsigned rev : 2; // 版本
unsigned port : 1; // 端口号
unsigned sid : 5; // 发送方 ID
};
解析时一定要先读 ext 位,如果为 1,那就不属于数据消息,而是下一节要讲的扩展消息。
扩展消息(Extended Messages)
PD 3.0 引入了扩展消息,本质上是把数据消息的字节数上限从 26B 扩到了 260B。它们依然通过 SOP* 帧传递,但消息头的 ext 位置为 1,并且后面会跟一个扩展头(Extended Header)来描述数据块块号、总长度等信息。
扩展消息的出现主要是为了:
- 传输更大的安全证书、认证数据
- 支持 OCPP(电源路径控制协议)等复杂功能
- 厂商自定义扩展(OEM 扩展消息)
举个实际例子——Extended Source_Capabilities:它可以携带比普通 Source_Capabilities 更多的电压电流组合(PDO 扩展),以及非电气参数(比如电池充电曲线)。
曾经的教训: 我原来以为扩展消息和普通数据消息可以混着发,结果设备解析老是失败。后来查规范才明白——一段事务流里,控制消息和数据消息必须严格交替,而扩展消息必须由源端发起,且接收方回复 GoodCRC 后才能继续。所以设计协议栈时一定要为扩展消息单独维护状态机。
三类消息对比
| 特性 | 控制消息 | 数据消息 | 扩展消息 |
|---|---|---|---|
| 消息头 ext 位 | 0 | 0 | 1 |
| 有效数据长度(不含头) | 0 字节 | 1~26 字节 | 1~256 字节 |
| 数据对象个数 | 0 | 1~7 | 1~7(扩展头另计) |
| 典型用途 | 握手、确认、复位 | 协商能力、请求、VDM | 厂商扩展、安全认证、大容量传递 |
| 编码示例 | GoodCRC (0x01) | Source_Capabilities (0x01) | Extended Source_Cap (0x21) |
核心记忆点: 抓包时先看 Message Header 的 Bit[15](ext 位)。
0 → 再看 Bit[12:10](num_objs),如果 num_objs==0,那就是控制消息;否则是数据消息。
1 → 扩展消息,需要额外解析 Extended Header。
我个人习惯在协议栈代码里把消息入口写成三个分支:
switch (msg_header.ext) {
case 0:
if (msg_header.num_objs == 0) handle_control_msg();
else handle_data_msg();
break;
case 1:
handle_extended_msg();
break;
}
注意: 扩展消息的 Extended Header 中有 Data Block Number 和 Total Number of Blocks 字段,如果你的设备不支持分块接收,务必在 SOP 层的 Extended Capabilities 里把 Blocked Extended 位设为 0,否则对方可能会发分块消息导致解析失败。