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
};
解析步骤:
- 先读Message Header:0x1241。按位拆开:Message Type=0x01(Source_Capabilities),Number of Data Objects=3,Rev=01(PD3.0),Port=0,Dir=0。
- 然后读第一个PDO:0x00F0012C。电压字段=0x0F0=240,240×50mV=12000mV=12V?不对!
- 等等,我搞错了。电压字段是bit 19-10,不是bit 15-6。重新算:0x00F0012C的bit 19-10是0x0F0=240,没错。但为什么是12V?
- 哦,我明白了——这个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协议里最复杂的部分。准备好了吗?