3. 协议层核心:SOP/SOP'/SOP''包类型、Message Header解析、Data Object结构

好,咱们进入协议层。说实话,这是整个PD协议里最绕、也最核心的部分。很多工程师调了几个月,最后发现是SOP类型搞错了,或者Message Header里一个bit位没对齐。嗯,今天咱们就把这块彻底掰开揉碎。

3.1 三种SOP包类型:谁在跟谁说话?

先问个问题:当你的Type-C口插上一个充电器,是谁在跟谁通信?

答案不是简单的「手机跟充电器」。PD协议里定义了三种通信路径,分别用SOP、SOP'、SOP''来标识。我刚开始做PD项目时,一直以为SOP就是Source和Sink之间的通信,后来才发现——太天真了。

包类型 通信双方 典型场景
SOP Source ↔ Sink 主电源协商、电压电流请求
SOP' Source ↔ Cable(线缆) 读取线缆的E-Marker信息
SOP'' Sink ↔ Cable(线缆) 部分线缆管理功能

你想想看,如果一根线缆里嵌了芯片(就是E-Marker),那Source端要跟它通信,就得用SOP'包。用SOP包发过去,线缆芯片根本不理你。我在项目中就遇到过这个问题——Source端一直发SOP包去读线缆信息,结果线缆没反应,我还以为是硬件坏了。后来查了半天,发现是包类型选错了。

关键点:SOP、SOP'、SOP''的区别在于包头中的K码不同。物理层通过检测K码来区分这三种包。固件层面,你只需要在发送时指定正确的包类型即可。

3.2 Message Header:每个包的门牌号

每个PD数据包,开头都是一个16位的Message Header。它就像门牌号,告诉接收方:这个包是干嘛的、有多长、谁发的。

咱们直接看结构:

Bit 15  14  13  12  11  10  9  8  7  6  5  4  3  2  1  0
+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
| Extended|  Number of  | Message Type |  Rev |  Port |  Reserved |  Message |
|   (1)   | Data Objs(3)|    (5)       |  (2) |  (3)  |    (1)    |   Dir(1) |
+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+

我来拆开讲几个关键字段:

  • Message Type(5位):标识消息类型。比如0x01是Source_Capabilities,0x02是Request,0x03是Accept。一共能定义32种,目前用了20多种。
  • Number of Data Objects(3位):后面跟着几个Data Object。0表示没有Data Object,比如Control Message。最大7个。
  • Message Dir(1位):0表示Host→Device,1表示Device→Host。说白了就是谁发的。
  • Port(3位):多口充电器用这个区分是哪个端口发的。单口设备一般填0。
  • Extended(1位):是不是扩展消息。PD3.0引入了扩展消息,这个位就是标记。

我的习惯:在固件里定义一个结构体来解析Message Header,不要手动去移位。比如:

typedef struct {
    uint8_t msg_dir : 1;
    uint8_t reserved : 1;
    uint8_t port : 3;
    uint8_t rev : 2;
    uint8_t msg_type : 5;
    uint8_t num_data_objs : 3;
    uint8_t extended : 1;
} __attribute__((packed)) pd_msg_header_t;

这样代码可读性高,也不容易出错。

3.3 Data Object结构:真正的数据载体

Message Header后面跟着的就是Data Object。每个Data Object是32位(4字节)。不同类型的消息,Data Object的格式完全不同。

咱们拿最常见的Source_Capabilities消息举例。它里面包含一个或多个Power Data Object(PDO),每个PDO描述了一组电压电流能力。

3.3.1 固定电源PDO(Fixed Supply PDO)

Bit 31  30  29  28  27  26  25  24  23  22  21  20  19  18  17  16
+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
|  Reserved  |  FR  |  DR  |  PR  |  USB |  Peak |  Voltage (10位)  |  Current  |
|    (5)     | Swap | Swap | Swap | Cmpl | Current|   (50mV/bit)   |   (10位)  |
+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
                                                                                 0

电压字段是10位,单位50mV。所以如果电压值是5000mV,那这个字段就是5000/50=100(0x64)。电流字段也是10位,单位10mA。

我曾经在调试时,发现Source发过来的PDO电压值总是对不上。后来一查,是我把电压字段的偏移算错了——忘了加50mV的基准偏移。嗯,这种细节问题,调试起来真的很头疼。

3.3.2 可变电源PDO(Variable Supply PDO)

这个跟固定电源类似,但电压是一个范围:

Bit 31-20: 最大电压(50mV/bit)
Bit 19-10: 最小电压(50mV/bit)
Bit 9-0:   最大电流(10mA/bit)

说白了就是告诉Sink:「我能在5V到20V之间调,但电流不能超过3A。」

3.3.3 电池PDO(Battery Supply PDO)

这个比较特殊,它不直接给电压电流,而是给一个功率范围。比如「我能提供10W到60W」。Sink端需要自己根据电池充电曲线来请求合适的电压电流。

注意:PD3.1引入了AVS(Adjustable Voltage Supply)和SPR(Standard Power Range)/EPR(Extended Power Range)的概念。EPR模式下电压可以到48V。如果你在做PD3.1的Source,一定要检查PDO中的Sink路径是否支持EPR。我见过有人把48V的PDO发给只支持20V的设备,结果...嗯,你懂的。

3.4 实战:如何解析一个Source_Capabilities消息

假设你收到一个PD数据包,物理层已经帮你剥离了前导码和CRC。你手里是一个完整的协议层数据:

// 假设收到的数据(16进制)
uint8_t rx_buffer[] = {
    0x41, 0x12,  // Message Header
    0x2C, 0x01, 0xF0, 0x00,  // PDO 1: 5V/3A
    0x2C, 0x03, 0xE8, 0x00,  // PDO 2: 9V/3A
    0x2C, 0x05, 0xDC, 0x00   // PDO 3: 15V/3A
};

解析步骤:

  1. 先读Message Header:0x1241。按位拆开:Message Type=0x01(Source_Capabilities),Number of Data Objects=3,Rev=01(PD3.0),Port=0,Dir=0。
  2. 然后读第一个PDO:0x00F0012C。电压字段=0x0F0=240,240×50mV=12000mV=12V?不对!
  3. 等等,我搞错了。电压字段是bit 19-10,不是bit 15-6。重新算:0x00F0012C的bit 19-10是0x0F0=240,没错。但为什么是12V?
  4. 哦,我明白了——这个PDO的电压是12V,不是5V。我刚才假设错了。实际项目中,Source的PDO顺序和电压值完全由Source决定,不能想当然。

避坑指南:我曾经在解析PDO时,直接用了memcpy把数据拷贝到结构体里,结果因为字节序问题(大端vs小端),电压值全乱了。建议的做法是:

uint32_t pdo_raw = (rx_buffer[3] << 24) | (rx_buffer[2] << 16) | 
                   (rx_buffer[1] << 8) | rx_buffer[0];
uint16_t voltage = (pdo_raw >> 10) & 0x3FF;  // 10位电压
uint16_t current = pdo_raw & 0x3FF;          // 10位电流

手动移位,确保字节序正确。

3.5 小结

协议层说白了就是三件事:搞清楚谁在说话(SOP类型)、说什么(Message Header)、具体内容是什么(Data Object)。

我个人觉得,最容易出问题的地方有两个:一是SOP类型选错,导致通信对象不对;二是Data Object的位域解析出错,导致电压电流值算错。嗯,这两个坑我都踩过,所以特别提醒你注意。

下一章咱们会讲状态机——PD协议里最复杂的部分。准备好了吗?