4. USB PD 消息结构:控制消息与数据消息格式解析
咱们接着聊。上一章我们把 USB PD 的物理层——也就是那根 CC 线怎么传信号——给摸透了。这一章,我们来解剖 PD 协议的核心:消息。
说白了,PD 通信就是 Source 和 Sink 之间互发“电报”。你想想看,好比你用对讲机喊话:“我功率拉满了,你要多少?”对方回:“给我 20V 5A。”
这些“电报”不是乱发的。控制消息和数据消息,各有各的规矩。
4.1 控制消息 vs 数据消息——本质区别在哪?
我把 PD 协议栈想象成一个快递系统。控制消息就像那种薄薄一页的“收货确认单”——没有包裹内容,只告诉你“签收成功”或“拒收”。数据消息呢?它是货真价实的“包裹”——里面装着具体的能力数组、请求参数。
形式上,区别就在于包尾巴有没有那一截 4 字节的“数据”。
- 控制消息:只有 4 字节的头部(Header) + 2 字节的 CRC。没有数据对象(Data Object)。长度固定为 6 字节。
- 数据消息:头部 + 一个或多个数据对象 + CRC。长度根据数据对象的数量变长,最短 10 字节(带 1 个数据对象),最长 30 字节(带 7 个数据对象)。
我在项目中遇到的第一个坑,就是在收包时只分配了 6 字节的缓冲区,结果满脑子以为所有消息都那么短,直接越界。那个抓包日志啊,全是乱码,搞了我一整天。从那以后,我再也不写死消息长度了。
| 特征 | 控制消息 | 数据消息 |
|---|---|---|
| 组成 | Header(4B) + CRC(2B) | Header(4B) + Object(每 4B 一组,1~7组) + CRC(2B) |
| 总长度 | 固定 6 字节 | 10~30 字节 |
| 典型用途 | GoodCRC、Accept、Reject、Wait、PS_RDY | Source_Capabilities、Request、BIST、VDM |
| 带不带数据 | 不带 | 带(能力数组、电流请求、厂商自定义命令等) |
关键记忆点:接收方收到控制消息,只需回一个 GoodCRC;收到数据消息,就要按数据对象的内容去做决策。比如收到 Source_Capabilities,你得解析那几个电压电流二元组,然后才能发 Request。
4.2 解剖头部——那 4 个字节里藏着什么
嗯,无论控制还是数据消息,开头那 4 字节头部(Header)的格式是一样的。我习惯拿一张纸,把这 4 字节拆成比特来看。你想想看,16 比特的信息,决定了一切。
Bit15 ~ Bit12: 消息类型(Message Type)
Bit11 ~ Bit10: 端口角色(Port Role)
Bit9: 规格版本(Spec Revision)
Bit8: 端口数据角色(Port Data Role)
Bit7: 保留位(Reserved,必须为 0)
Bit6 ~ Bit5: 信息数量(Number of Data Objects,NOD)
Bit4 ~ Bit0: 消息 ID(Message ID)
我刚开始啃协议时,对这堆比特位很头疼。后来我用一个小技巧:只记住三个最重要的字段——消息类型、信息数量和消息 ID。其他的,抓包时让协议分析器帮你算就行。
- 消息类型(Message Type):取值 0~15。0x01 是 Source_Capabilities,0x02 是 Request,0x03 是 Accept,0x07 是 PS_RDY。这是整个协商流程的“主语”。
- 信息数量(NOD):只有数据消息才用。控制消息永远为 0。它告诉你,这个包后面跟着几个 4 字节的数据对象。
- 消息 ID(MID):用来去重。发送方每发一条新消息,MID 加 1。注意,是循环 0~7。我遇到过某个协议栈的实现,因为 MID 没处理好,导致重发了几条同样的消息,Sink 那边直接挂了。
调试小技巧:抓 PD 总线时,直接看头部的 Message Type 字段。如果 Accepted 和 GoodCRC 乱飘,大概率是时序问题,不是解析问题。
4.3 数据对象——那些“包裹”里的内容长什么样
咱们说数据消息,重点就在它的数据对象上。最常见的两种数据对象:Source_Capabilities 里的 PDO(Power Data Object)和 Request 里的 RDO(Request Data Object)。
PDO 的结构(32 位)
Bit31 ~ Bit30: 供电类型(00=Fixed, 01=Battery, 10=Variable, 11=PPS)
Bit29: Dual-Role Power
Bit28: USB Suspend Supported
Bit27: Unconstrained Power
Bit26: USB Communications Capable
Bit25: Dual-Role Data
Bit24: Unchunked Extended Messages Supported
Bit23~Bit20: Peak Current(仅 Fixed 类型有效)
Bit19~Bit10: Voltage in 50mV units(电压值)
Bit9~Bit0: Current in 10mA units(电流值)
比如,你看到一个 PDO 的电压字段为 0x190(400 步),电流字段为 0x1F4(500 步)。那实际电压 = 400 × 50mV = 20V;实际电流 = 500 × 10mA = 5A。嗯,这个 Source 确实能输出 20V 5A。
RDO 的结构(32 位)
Bit31~Bit28: Object Position(指向要请求的 PDO 索引,从 1 开始)
Bit27: Give Back Flag
Bit26: Capability MIS Match
Bit25~Bit24: USB Communications Capable
Bit23~Bit20: No USB Suspend
Bit19~Bit10: Operating Current in 10mA units
Bit9~Bit0: Max Current in 10mA units
平时容易出错的地方,就是 Object Position 字段。它告诉 Source:“我要第 3 个 PDO。” 注意,位置是从 1 计数的,不是 0。我曾经见过一个固件,直接拿数组下标 0 去填这个字段,Source 收到后懵了——找不到第 0 个 PDO,直接 Reject。
4.4 流程实例——看一次完整协商中的消息交换
咱们以一个充电器为 Source,手机为 Sink 为例,走一遍 4 类消息的交换过程。
- Source 发送数据消息:Source_Capabilities
类型 0x01,包含 3 个 PDO:5V/3A、9V/2A、12V/1.5A。NOD = 3。 - Sink 回复控制消息:GoodCRC
收到后拆出 MID,原样返回,告诉 Source:“我收到了。” - Sink 发送数据消息:Request
类型 0x02,RDO 中 Object Position = 2,指向 9V/2A。 - Source 回复控制消息:Accept
类型 0x03,没有数据对象。表示:“我接受你的请求。” - Source 发送控制消息:PS_RDY
类型 0x07,等电压转变完成后发出,告诉 Sink:“我已经准备好,开整吧。” - Sink 回复控制消息:GoodCRC
确认收到 PS_RDY。此时可以拉走 9V/2A 了。
你看看,整个过程 6 条消息,4 条控制消息、2 条数据消息。高压输出到位。
避坑指南:我刚开始做 PPS 调试时,发现状态机怎么都跳不过 SRC_TRANSITION,后来抓包一看,Source 发出 PS_RDY 之后,Sink 的 GoodCRC 回慢了,超时!Source 直接掉回 Default。所以要记得,所有控制消息的回复(GoodCRC 和 Not_Supported 等)必须在 1ms 内发出。
4.5 调试实战——我如何用逻辑分析仪确认消息类型
我不建议一上来就写代码。先拿逻辑分析仪抓一段完整的协商过程,看波形。我习惯用 PulseView,解析 USB PD 之后,你会看到类似这样的 ASCII 清单:
SOP: Source -> Sink | 消息类型: Source_Capabilities | 数据数: 3
SOP': Sink -> Source | 消息类型: GoodCRC | 数据数: 0
SOP': Sink -> Source | 消息类型: Request | 数据数: 1
SOP: Source -> Sink | 消息类型: Accept | 数据数: 0
重点观察三个地方:
- 消息类型字段是否对应正确的流程步骤
- 数据数量(NOD)是否正确:控制消息一定是 0,数据消息至少 1
- GoodCRC 是否紧跟在每个消息之后(除了 GoodCRC 本身,它后面不需要 GoodCRC)
我个人习惯:在固件里加一串调试日志,在发送/接收头部的瞬间,把 4 字节头部打印出来。这样当我发现协商卡死时,看一眼日志就能定位到到底是没收到 Accept,还是收到 Reject 后没处理。
4.6 总结一张表厘清各消息用途
| 消息名称 | 类型 | 方向 | 用途 | 是否需要数据对象 |
|---|---|---|---|---|
| GoodCRC | 控制 | 双向 | 接收确认 | 否 |
| Accept | 控制 | Source→Sink | 接受 Request | 否 |
| Reject | 控制 | Source→Sink | 拒绝 Request | 否 |
| Wait | 控制 | Source→Sink | 稍后再试 | 否 |
| PS_RDY | 控制 | Source→Sink | 电压转变完成通知 | 否 |
| Source_Capabilities | 数据 | Source→Sink | 通告供电能力 | 是(PDO) |
| Request | 数据 | Sink→Source | 请求指定电压/电流 | 是(RDO) |
| BIST | 数据 | 双向 | 测试与自检模式 | 是(BDO) |
| VDM | 数据 | 双向 | 厂商自定义信息 | 是(VDO) |
嗯,你把这些消息串起来,USB PD 的协商就不再是黑盒了。下一层我们开始玩策略——进入实际的状态迁移。