☰
PD、PPS、QC 的代码简讲——充电协议进阶(上)
2026/11/6 1:58:09 网站建设 项目流程

这是充电协议系列进阶篇的上篇,只干一件事:把 PD、PPS、QC 的代码实现拆开看。基础篇讲了它们怎么握手、怎么谈定电压电流,下篇讲 Type-C 口里谁先开口的五种角色,这篇专讲合同条文是怎么写进代码的。

读 PD 代码,先分清三层

基础篇里我们把充电器插上那几毫秒讲成了一桩生意:充电器把能给的档位列出来、手机从中挑一个、成交。这篇继续往后挖——合同条文在代码里长什么样。分三层讲:

  • 一次协商,就是两边各跑一个状态机,状态名直接抄规范。

  • 报文怎么打包、怎么签收,一个报文就是一辆小货车。

  • 充电器的档位表、手机的请求对象,都是 32 个比特的位运算;PPS 的对账循环、QC 的电压暗号,各自怎么落成代码。

一次协商,两边各跑一个状态机

Linux tcpm 的状态名几乎是规范状态机的直译,Source 侧和 Sink 侧各跑各的,靠 CC 上的报文对表:

代码里就是 switch(state) 大分发,配一个超时定时器。状态对不上就 HARD_RESET 或 SOFT_RESET 重来——你抓包偶尔能看到一对 Soft Reset,就是两边对不上表在“重新自我介绍”。

一条报文长什么样

PD 报文物理上是一串 4b/5b 编码的比特,逻辑上分四段:前导码 → SOP → 消息头 + 数据对象 → CRC。

SOP 分三种:SOP 是主机和设备之间,SOP' 发给线缆,SOP'' 发给插头里的芯片——所以 PD 能跟“一根线”单独说话,报线缆能力。

消息头 16 个比特,最重要的几个字段:

消息头字段

位置

干嘛用的

消息类型

bit 11-9

Source_Capabilities、Request、Accept、PS_RDY、Get_PPS_Status 全在这编码

数据对象个数

bit 14-12

后面跟 0 到 7 个 32 位数据对象

扩展消息标志

bit 15

1 表示这是带扩展头的大报文

还有个细节:每个报文都得回一个 GoodCRC 确认,像快递签收。所以你抓包看到的报文永远成双成对——一发一签,签收完才算数。

档位表是怎么拼出来的

充电器广播的档位表叫 Source Capabilities,每个档位是一个 32 位 PDO。拼法就是位运算,看着唬人,拆开全是“挪几位、填个数”:

// Fixed PDO:电压按 50mV 一档存,电流按 10mA 一档存 uint32_t fixed_pdo(uint32_t mv, uint32_t ma) { uint32_t v = mv / 50; // 20000mV -> 400 uint32_t i = ma / 10; // 5000mA -> 500 return (0b00 << 30) // 类型:Fixed Supply | (0b00 << 28) // 属性位 | (v << 10) // 电压放 20-11 位 | i; // 电流放 10-0 位 } // 20V/5A 拼出来就是:(0b00 << 30) | (400 << 10) | 500

PPS 档是另一种类型,叫 APDO。它的电压电流范围是充电器“报区间”,不是报死值——这就是为什么你能在抓包里看到 5-11V、5-20V 这种浮动区间:

// APDO:最大/最小电压按 100mV 一档存,最大电流按 50mA 一档存 uint32_t pps_apdo(uint32_t max_mv, uint32_t min_mv, uint32_t max_ma) { return (0b11 << 30) // 类型:APDO | (0b00 << 28) // 子类型:PPS | (max_mv / 100 << 17) // 最大电压(11000mV -> 110) | (min_mv / 100 << 8) // 最小电压(5000mV -> 50) | (max_ma / 50); // 最大电流(3000mA -> 60) }

手机怎么选档

手机回 Request,里面填 Object Position 指向档位表第几档,再带上运行参数。PPS 档的 RDO 特别一点:输出电流 50mA 一档、输出电压 20mV 一档——这就是“8.2V”这种小数电压的来历。它不是档位表里的固定值,是手机根据电池电压现场算出来、填进 RDO 的:

// 请求第 pos 档,运行电压 mv、运行电流 ma uint32_t pps_rdo(uint8_t pos, uint32_t mv, uint32_t ma) { return (0b11 << 30) // 类型:APDO 请求 | (0b00 << 28) // 子类型:PPS | (ma / 50 << 21) // 运行电流,50mA 一档 | (mv / 20 << 9) // 输出电压,20mV 一档 | pos; // 第几档(1 起算) }

PPS 的“对账”循环,代码里怎么转

PPS 最“磨叽”也最实用的地方在协商完之后。固定档谈完,手机基本闭嘴了;PPS 不一样,电压要一直微调。

基础篇我们放过抓包:手机先 Request PPS 8.2V/1A,充电器把电压顶到 8.303V,然后手机每隔一秒多就 Get PPS Status 轮询一次实际输出——8.025V/1.019A、8.097V/0.614A……像对账一样。代码视角就是一个循环:

while (charging) { uint32_t target = battery_target_mv(); // 按电池电压现算目标 if (abs(target - current_mv) >= 20) // 差一格(20mV)就调 send_request(target, target_ma); // 发 Request(PPS 档) pps_status_t st = get_pps_status(); // 轮询充电器实际输出 current_mv = st.output_mv; // 拿实测值记账 delay(1000); // 一秒多对一次账 }

20mV 一格、50mA 一格,是 USB PD 3.0 规范给 PPS 定的最小调节粒度。为什么这么细?因为要贴着电池的最佳充电曲线走——电压调过头一点,电流就可能超标,电池温度就压不住。固定档谈完就定死,PPS 得一直盯着电压调。

QC 没有报文,只有电压暗号

QC 跟 PD 是两个路子。PD 是数字化谈判——CC 引脚、报文、状态机;QC 是模拟暗号——D+/D- 两根数据线的电压。QC 的便宜之处也在这:D+/D- 是数据线,本来就在,充电器加个 ADC 和几个电阻就够,不用上协议芯片。老安卓机和一大堆“QC 兼容”充电器都是这么干的。

QC2.0 的握手像对暗号,拆开看是五步:

  1. 充电器刚上电时把 D+ 和 D- 短接在一起。手机按老协议 BC1.2 一测——DCP(专用充电口),先按 5V 普通充电对待。

  2. 手机想升压,就在 D+ 上拉一个 0.325V 的小电压,保持 1.25 秒以上。这就是暗号的第一句:“我要快充”。

  3. 充电器检测到这个电平维持够久,断开 D+/D- 短接——D- 电压跟着掉下来。

  4. 手机看到 D- 电压掉了,知道充电器认账了,按目标档位把 D+、D- 拉成对应的组合电平。

  5. 充电器读到组合电平,把输出电压切到对应档位。

注意第二步和第四步不是一回事:0.325V 保持 1.25s 是“我要快充”的敲门信号,真正定档靠的是 D+/D- 的组合电平。QC2.0 的档位表长这样:

目标电压

D+

D-

说明

5V(默认)

0.6V

0V

没谈成,老实 5V

9V

3.3V

0.6V

最常用的升压档

12V

0.6V

0.6V

A 级就到这

20V

3.3V

3.3V

B 级才有,一般配 Type-C

QC3.0 更进一步。它进“连续模式”后,用 D- 上的一串方波脉冲告诉充电器“再高 200mV”“再低 200mV”,在 3.6V-20V 之间 0.2V 一档连续调。所以 QC3.0 的代码里多一个“数脉冲”的活:

// classify 把 ADC 读值归成三档:0V / 0.6V / 3.3V,带迟滞窗口抗干扰 enum level { LVL_ZERO, LVL_MID, LVL_HIGH }; enum level dp = classify(adc_read(D_PLUS)); enum level dm = classify(adc_read(D_MINUS)); if (dp == LVL_HIGH && dm == LVL_MID) set_output(9000); // 9V else if (dp == LVL_MID && dm == LVL_MID) set_output(12000); // 12V else if (dp == LVL_HIGH && dm == LVL_HIGH) set_output(20000); // 20V(B 级) else set_output(5000); // 默认 5V

QC3.0 在连续模式里还多了个“数脉冲”的分支:D- 上每来一个上升沿,输出就往上顶 200mV,下降沿就往下收。原理就是这么朴素——电压对上了,就切;对不上,就 5V。

一张表收尾

把四种协议的“谈法”并排看一遍,差异一目了然:

协议

通道

协商方式

电压调节

一句话

PD Fixed

CC(报文)

报档位 + 选档

固定档位

数字化谈生意

PD PPS

CC(报文)

报档位 + 选档 + 对账

20mV 连续调

电压现算现调

QC2.0

D+/D-(电压)

敲门信号 + 组合电平

固定档位

模拟暗号

QC3.0

D+/D-(脉冲)

敲门信号 + 数脉冲

200mV 连续调

暗号加密码本

最后一句话总结:PD 靠 CC 上的报文把每一条都写成明码,QC 靠 D+/D- 的电压把档位藏在电平里。前者贵在协议芯片,后者便宜在几颗电阻——这也是为什么到今天,两套协议还能同时活在同一个充电器里。

参考资料

正文里的报文细节和取整规则,主要依据以下公开资料,可按需点开核对:

  • [1] USB-IF《USB Power Delivery》培训材料——PDO/APDO 位定义与取整规则。D2T2-1.pdf

  • [2] ST 应用笔记 AN5562——PPS 20mV/50mV 调节粒度与 VBUS 控制算法。AN5562.pdf

  • [3] Linux 内核 USB Type-C 驱动文档与 tcpm 源码(drivers/usb/typec/tcpm.c)——状态机命名。typec.html

  • [4] 高通官方博客《Qualcomm Quick Charge 2.0 technology explained》——QC2.0 背景与功率档位。Qualcomm 原文

  • [5] 腾讯云开发者社区《QC 快充协议实战:从 QC2.0 握手到 QC3.0 动态调压》——D+/D- 组合电平与脉冲波形实测。QC 快充实战

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询