
做硬件的人应该都遇到过这种尴尬手里明明有一个65W的PD充电头插上一块只要12V的板子示波器一看VBUS纹丝不动还是5V。反过来有些板卡只有一个Type-C口想靠它给电池组充电却怎么都拉不起功率。这些问题表面上都像“供电不行”但真正在背后做决定的就是USB PD协议——谁先说话、谁听谁的、什么时候升压、什么时候断电全都由一套协议栈说了算。这篇文章我会从一个实际做电源和嵌入式开发的角度把USB PD协议的物理层、协商流程、状态机和常见坑完整讲清楚。不管你是准备给自己的产品加Type-C受电口是想做个PD诱骗器从充电头里榨出20V还是单纯在调试充电异常都能在文章里找到对应的答案。我不会只贴规范条文而是尽量把“规范为什么这样设计”和“实际调试时的表现”一起说。1. 为什么USB PD协议能成为充电事实标准1.1 从USB2.0的500mA到240W电源能力跃迁USB接口最开始根本不是用来做大功率供电的。USB 2.0时代默认只有5V/500mA2.5W连个移动硬盘都带不动。后来出现BC 1.2Battery Charging把充电电流提高到1.5A但通信识别要靠D/D-短路本质上是模拟域的检测不是真正意义上的协商。Type-C口普及之后接口物理形态统一了供电能力却没跟上手机、平板、笔记本都需要更高的功率于是USB-IF在Type-C之上定义了USB PDPower Delivery协议。这里有个常见误解很多人以为Type-C就等于PD其实不是。Type-C只是物理接口PD是一套跑在CC线上的独立通信协议。一条普通Type-C线插上充电头如果充电头不支持PD那就是5V/3A顶天了。而支持PD的充电头可以通过CC线发送消息商量出一个双方都接受的电压和电流。PD协议本身的版本演进也很有意思。PD 1.0时代用了BFSK调制速度慢、兼容性一般实际市面上很少见到。PD 2.0改成了BMC调制定义了5V/9V/15V/20V这一组标准电压最大冲到100W这时候笔记本充电器才开始统一到Type-C口上。PD 3.0增加了PPS也就是可编程电源手机快充彻底活了。PD 3.1又加了一组EPR扩展电压范围28V/36V/48V最大240W这就覆盖到了游戏本、显示器甚至小型电动工具。协议版本主要电压档位最大功率关键变化PD 1.05V/12V/20V100W引入通信协商概念但BFSK调制市场少见PD 2.05V/9V/15V/20V100WBMC调制Type-C普及笔记本充电统一PD 3.0固定档位PPS100W可编程电源手机快充大量采用PD 3.1增加28V/36V/48V240WEPR扩展覆盖大功率设备1.2 只有PD做不到的事QC、BC1.2和私有协议各管一摊聊PD之前先把几个容易混淆的“快充”概念理清。高通的QC快充是另一套协议走的是D/D-两根线用不同的电压电平告诉充电头“我要9V”还是“我要12V”。OPPO的VOOC、华为的SuperCharge也都有自己的私有协议。这些协议跟PD完全不兼容物理层不同、消息格式不同更不能互相通信。其实USB-IF一直想把充电协议统一成PD但现实是手机厂商都想保留私有协议于是市面上出现了大量“多协议融合”的充电头。这些充电头内部往往有一颗协议芯片比如SC2001、SW2303之类同时支持QC、PD、VOOC等靠CC和D/D-上的检测结果自动切换。所以你在一个支持QC的充电头上插PD诱骗器是绝对协商不出20V的因为充电头此刻压根没启用PD通路。这也是很多新手做诱骗器最容易忽略的一点先确认目标充电头是真的支持PD再谈协议解析。2. 物理层与CC通道一次握手从电阻开始2.1 Rp和Rd5.1k电阻为什么是Sink的身份证USB PD通信不是随便找两根线就能跑的。Type-C口里有专门的CC引脚全称是Configuration Channel。这个引脚承担两件事第一检测连接的方向和角色第二在上面跑BMC通信。检测角色的原理特别朴素。Source端供电方比如充电头会在CC1或CC2上拉一个电阻RpSink端受电方比如你的板子则下拉一个Rd标准值是5.1kΩ。当Type-C插头插入时Source通过ADC或比较器看到CC引脚电压就能判断出对面是个Sink。同时Sink也能判断出自己插的是哪个方向——如果CC1上有电压说明线缆方向是CC1如果是CC2有电压则反一反。这就是为什么Type-C正反插都能工作因为它定义了CC1和CC2两个引脚来处理镜像。顺着这个思路还有人会想到如果我在CC上下拉一个电阻就相当于“骗”充电头以为我是Sink这就是最原始的PD诱骗雏形。但要注意只靠5.1k电阻充电头只会为你提供默认的5V/3A因为此时还没有发生任何PD协商。想拿到9V、15V、20V就必须在CC线上发出真正的PD消息。Rp的阻值也不是随便选的。Type-C规范里56k、22k、10k分别对应不同的源端供电能力56k表示只能提供USB默认电流22k表示1.5A10k表示3A。这些属于Type-C层面的物理握手跟PD协商没关系。很多协议芯片会同时检测Rp电压和后续PD能力两套系统并行工作。2.2 BMC编码、消息帧和CRC低速通信也可以很可靠PD通信的物理层用BMC编码全称是Biphase Mark Coding。这个概念可以用一个很简单的方式理解每一比特的时间是固定的但每个比特内部必须有跳变用跳变的位置编码“1”和“0”。这种编码方式不依赖绝对电平高低只依赖跳变节奏所以抗干扰能力强也很容易用边缘检测恢复时钟。PD 2.0之后的最大波特率是300kbps上下。有人第一次听到这个速率会笑觉得太慢了。但这就是PD工程的务实之处CC线上只有一个引脚还要同时供电检测和通信不需要高速。你想传的内容无非是几个PDO电源数据对象和一个请求几十个字节就能完成一次协商300kbps完全够用。消息帧也不是光秃秃地丢几个字节。一个完整的PD消息包含起始位SOP、消息头、若干数据对象、CRC32校验和结束位。SOP还分SOP1、SOP2等专门用于跟线缆内部的E-Marker芯片通信这在后面讲线缆坑时还会提到。CRC32是整个消息可靠性的关键接收方一旦CRC校验失败就不会回复GoodCRC发送方会等超时重发。这套机制听起来复杂但实际在调试时看到CC线上有波形但协商没反应十有八九就是CRC不过、电平不对或者Source根本就没发出能力包。3. 源设备与受电设备的“谈判桌”PDO、RDO和PPS3.1 PDO是菜单RDO是点菜最实用的解析方法USB PD最核心的设计概念是把“供电能力”定义成一串PDO。PDO就是Power Data Object相当于充电头提供的菜单。充电头会把自己的能力用一条Source_Capabilities消息发给SinkSink从里面挑一个最合适的然后用RDORequest Data Object回复给充电头。PDO有几种类型我在实际调试中见得最多的就是固定电压PDOFixed Supply。它在32位数据里的格式很直接最高2位是类型编码固定电压是00中间10位是电压值以50mV为单位最低10位是最大电流以10mA为单位。我常在调试脚本里写这样一个Python函数来解析def parse_fixed_pdo(pdo_bytes): val int.from_bytes(pdo_bytes, byteorderbig) pdo_type val 30 max_current_ma (val 0x3FF) * 10 voltage_mv ((val 10) 0x3FF) * 50 return pdo_type, voltage_mv, max_current_ma # 假设从充电头上抓到一个完整PDO值 pdo_type, volt_mv, curr_ma parse_fixed_pdo(bytes([0x2D, 0xC1, 0x44, 0x00])) print(PDO类型:, pdo_type) print(电压(mV):, volt_mv) print(最大电流(mA):, curr_ma)解析出来的值只要除以1000就是常见的“9V/3A”“20V/5A”这种标称。除了固定电压PDO还有可变电压PDO、电池PDO以及PD 3.0新增的PPS类型的APDO。可变电压PDO通常用于一些特殊设备允许Sink在某个区间内选择一个电压但电流上限不随电压变化。电池PDO则是为了对接那些内部直接充电电池的设备设计上更像一个功率总包而不是某个固定电压档位。PDO类型编码常见用途特点Fixed Supply00手机、笔记本、板卡供电固定电压电流上限固定Battery01电池直充类设备以功率为维度描述能力Variable Supply10老式设备、特殊模块电压范围可调但电流上限固定APDO / PPS11手机直充、精密调压电压可编程步进约20mVRDO这边的关键字段不是电压电流而是Object Position也就是“我选菜单里的第几个”。Sink端在发Request时会把自己期望的电压、电流、以及所选PDO的序号都填进去Source收到后如果同意就会回Accept然后开始把VBUS调到那个电压。整个流程像一个点餐过程菜单摆上来我选一个菜后厨说能做到然后上菜。理解了这个类比后面看状态机就轻松了。3.2 一次完整协商的抓包视角我习惯用示波器并联在CC1和VBUS上看整个协商过程这套视角比单纯看芯片手册直观得多。完整流程大概是这样的插上充电头VBUS先输出5V。这个5V是Type-C物理握手成功后Source主动给的默认电压。Source在CC线上发一条Source_Capabilities里面带一串PDO。Sink收到后在自己支持的电压集合里找一个交集比如Source有20V正好我也想要20V就发一条RequestObject Position指向20V那个PDO。Source收到Request检查参数合理发Accept然后把VBUS从5V往20V切换。切换完成后Source发PS_RDY告诉Sink电压已稳定。Sink收到PS_RDY才开始真正拉大电流。整个协商过程通常只有几十毫秒但对示波器来说足够了。我在实际调试中发现很多自研Sink的问题出在第4步和第5步之间Source切换电压需要时间Sink必须等待PS_RDY才能开负载如果Sink提前开负载VBUS会被瞬间拉垮Source于是进入保护或者复位。这个问题在规范里叫“Transtion Time”不同电压之间的变化需要满足一定时序后面专门讲状态机时会展开。3.3 PPS给手机直充准备的可变油门PD 3.0引入PPS之后手机快充才真正进入“软件控制电源”的阶段。PPS的全称是Programmable Power Supply它不再只提供固定的几个电压档而是允许Sink在某个范围内精细调节电压最小步进是20mV电流步进50mA。也就是说Sink可以从15V一路调到20V中间每一档都能选。这个能力对手机直充特别重要。锂电池充电最理想的策略是恒流恒压电池电压会随电量上升如果能跟着电池电压微调充电电压就能始终让充电板内部的降压电路工作在最高效率点。传统固定PDO只能给9V或者12V手机内部的充电IC还得再降一档效率损失不小。PPS相当于把“粗调”变成“细调”给手机厂商留出了巨大的调优空间。我调试过的很多手机原装头特别是那些标称“55W”“66W”“120W”的几乎全是PPS方案。它们的PDO列表里往往藏着一条APDO比如3.3V到11V、5A这样一个范围。如果你用一个不支持PPS的普通PD诱骗器去套很可能只能拿到5V或者9V拿不到标称大功率就是因为没去解析APDO或者没有按APDO格式发Request。4. 状态机与错误恢复调试PD时最容易被忽略的半壁江山4.1 你的Sink代码本质上是个状态机很多刚开始做PD Sink的人会以为只要把PDO解析出来、发个Request就行结果实际调的时候发现各种奇怪现象。原因就是PD协议本身被实现成一个状态机Source和Sink都有严格的初始化和状态跳转流程。Sink侧的状态大致是Attached.Snk表示物理层检测到CC上拉进入SnkStartup这时候Sink会开始等待Source发送Source_Capabilities对应状态是SnkWaitCapabilities收到能力包后进入SnkEvaluateCapability判断哪个PDO匹配自己的需求然后进入SnkSelectCapability发送Request等待Accept最后进入SnkReady等待PS_RDY并开始供电。Source那边则是另一套对称状态SrcStartup、SrcSendCapabilities、SrcNegotiateCapability、SrcReady。我在调试过程中发现很多问题出在“状态没对齐”上。比如Sink已经进入等待PS_RDY状态但Source因为某些原因发起了新的Source_Capabilities或者直接复位了Sink如果没有按时序去处理这个新的消息就会卡在旧状态表现为VBUS电压不对、电流拉不上来甚至完全没有反应。我给自己的Sink代码写宏定义时会把每个状态都定义成独立的整型然后每个状态里只处理对应的事件。实际上没有别的捷径PD协议的状态表是USB-IF官方文档里画好的照着实现就能少踩一半坑。4.2 Hard Reset为什么设备会反复重启如果你在调试中看到设备连着又断开、充电指示灯一亮一灭大概率是触发了Hard Reset。PD协议里Hard Reset是一个应急动作Source会把VBUS快速拉低让Sink重新开始一次完整的物理层和协议层初始化。这个过程不是“断电重来”那么简单它有一套专门的时序Source拉低VBUS保持一段时间然后重新输出5V重新进入协商流程。触发Hard Reset的原因很多。最常见的是Sink在收到Source_Capabilities后一直没回复RequestSource等超时了就通过Hard Reset来强制恢复。还有一种是Sink发了Source无法满足的请求有些充电头设计得比较“暴躁”直接复位而不回Reject消息。所以当你看到设备反复重启时不要急着骂硬件先抓CC线上的波形看看是不是有Hard Reset信号出现再往前排查Sink的Request内容是否合法。我遇到过一个很经典的案例自研Sink代码里没有处理Reject/ Hard Reset这两个消息结果Sink一直停留在等待状态Source超时后进入Hard Reset整个系统就像呼吸一样周期性地重启。后来在状态机里补全了对Hard Reset的响应强制回到SnkStartup重新协商问题立刻消失了。4.3 示波器上该盯的时序窗口调试PD协议时序和对错一样重要。我常用的做法是同时看三路CC1、VBUS、负载电流用示波器的Trigger抓Single模式专等电压突变。关键要看的窗口包括Source发出Accept之后VBUS开始上升的延迟VBUS上升过程中是否存在明显过冲PS_RDY出现前VBUS是否已经稳定在目标电压Sink开始拉电流VBUS有没有跌落。这些时间参数在规范里有定义比如Source在Accept后多久必须切换电压、PS_RDY发送的要求等。实际项目中我见过很多Sink电路把VBUS上的输入电容放得特别大导致Source切换电压时充电时间变长触发了Source的超时保护。这个属于电源路径设计问题跟协议芯片关系不大会让调试者误以为是PD时序出错。遇到这种情况先降低输入电容试一次往往就能定位问题。5. 把协议跑起来诱骗器、Sink设计与芯片选型实战5.1 最速原型用FUSB302自己发Request很多人的需求其实不是做完整的产品级Sink而是想从PD充电头里掏出一个高电压来给设备供电比如DIY便携电源、给锂电池组充电、给电机驱动器供电。这时候最快的路子是做一枚“PD诱骗器”。诱骗器的本质就是一个主动型Sink它不会真正消耗多少电流只负责跟Source完成协商把电压“骗”出来真正的功率由后级电路从VBUS获取。我做过一个最小的原型用的是FUSB302这颗PD PHY芯片加一颗MCU。FUSB302只处理物理层和BMC编解码协议逻辑全部由MCU实现用I2C读写寄存器。它的好处是灵活任何PD逻辑都能自己写坏处是寄存器配置繁琐新手容易卡在初始化上。核心步骤大致是这样的初始化I2C配置FUSB302的开关和中断。读取PD状态寄存器判断当前CC1还是CC2上检测到Source。使能对应的CC通道打开接收Source_Capabilities消息。解析消息里的PDO找到目标电压档位。构造一条Request消息Object Position填目标PDO的序号。等待Accept和PS_RDY然后通知后级系统“可以上电了”。FUSB302的代码框架在网上能搜到很多但驱动部分我建议直接参考官方例程特别是BMC收发和CRC校验不要自己从头写容易在物理层细节上折腾很久。如果你想更快可以直接买现成的PD诱骗模块然后自己改后级电路但那样乐趣也就少了大半。5.2 产品化路线STUSB4500这类Sink专用芯片如果要做产品而不是玩具我会选STUSB4500这类内置协议状态机的Sink专用芯片。它最大的优势是不需要MCU参与协商芯片自己就能完成Source_Capabilities的接收、PDO选择、Request发送、Accept等待、PS_RDY检测这一整套流程。你只需要通过I2C接口把期望的电压电流配置到芯片里的NVM里剩下的事情它自己处理。STUSB4500支持配置多个PDO比如我们常配成5V/9V/12V/15V/20V几个档位芯片上电后会自动从中选一个Source支持的来请求。这种芯片尤其适合对稳定性要求高的嵌入式设备相当于把PD协议栈固化成了硬件。代价是灵活性低改电压需要重新配置NVM不像FUSB302那样改代码就行。选型判断标准其实很简单如果板子上本来就有MCU且你需要动态切换不同电压来适配不同场景用FUSB302这类PHY芯片更合理如果设备是固定需求电源路径里面不想塞复杂的协议代码STUSB4500是最稳的。5.3 VBUS电容、浪涌和负载开启顺序PD Sink的电源路径设计比传统5V供电要讲究因为VBUS可能从5V跳到20V幅度很大。我自己的经验是VBUS输入端不要放超大容量的电容尤其不要在协议协商完成前就把它充满。你想想电容两端电压从5V跳到20V如果电容量是几百微法充电瞬间的冲击电流会很大轻则触发Source的保护重则拉低VBUS导致协议复位。更稳妥的做法是先让协议协商完成接收PS_RDY后再逐步开启后级负载。如果必须放较大电容我会在VBUS和电容之间加一级电子开关或通路MOS协商完成后才把电容和后级电路切入。另外VBUS路径上的TVS管也建议选低钳位电压的型号因为20V瞬间可能超过某些后端器件的极限。负载开启顺序同样重要。很多设备在协商成功后CPU或主控会瞬间拉大电流如果前端没有做缓启动电流斜率太陡充电头会以为发生了短路。我会在代码里做“软启动”先开一个小负载比如几十毫安稳定几十毫秒后再开主负载。这个习惯帮我避开了很多莫名其妙的保护。6. 翻车高发区E-Marker线缆、第三方头和兼容性6.1 E-Marker你的5A线可能并不“聪明”USB-C线缆里面有一种带芯片的“聪明线”芯片叫E-Marker它挂在CC线上通过内置的SOP’通信把自己支持的能力广播给Source和Sink。E-Marker里存了线缆的电流能力、数据传输速率、线材长度等信息。为什么要这个芯片因为USB-C口太通用一根看起来一模一样的线可能只支持600mA也可能支持5A。物理外形区分不出来那就在线缆里放芯片来“自报家门”。按规范支持5A大电流的线缆或者支持USB 3.2/4高速信号的线缆基本都该带E-Marker。但是现实很骨感市面上大量廉价“5A线”压根没有E-Marker甚至用了劣质线芯大电流一过就发热。在PD协商里如果Sink向Source请求超过3A电流Source往往会读取线缆的E-Marker确认线缆能力读不到或者芯片数据不对就拒绝你拉大电流。很多第三方便携显示器、笔记本扩展坞推荐你“必须用原装线”本质原因就在这。我自己调小板子时常备几根确认带E-Marker、并且线芯足够粗的线才敢做满载测试否则出了问题真分不清是板子问题还是线的问题。6.2 兼容性问题的根源不同Source的“性格”市面上支持PD的设备行为并不完全一致。苹果充电头、笔记本原装头、手机头、杂牌头同样是PD 3.0处理细节五花八门。有些充电头会先发一轮Source_Capabilities如果Sink不理它它不会一直等而是直接复位重来有些充电头对不支持的Request会返回Reject而有些干脆不响应还有些充电头虽然标称支持PPS但PPS的电压范围和电流范围写得跟宣传不一致。我在做兼容性测试时标准流程就是拿一桌不同品牌的充电头逐个试把CC信号和VBUS波形记录下来再跟预期的协商序列对比。这个工作看起来琐碎但恰恰能提前暴露产品里那些“偶发充不进电”的bug因为很多问题只有在某个特定充电头的特定时序下才触发。如果自己做Sink建议至少把Reject、Wait、Hard Reset、SenderResponse超时这些情况都处理到位。代码里可以用一个默认优先级策略“如果我请求的电压不支持就退回最高可用的固定电压档如果协商失败就保持在5V工作而不是直接死掉。”这种降级策略在产品里非常重要毕竟用户用一根烂线或者烂头时至少还能充上电而不是“不通”。6.3 用什么工具把PD握手“看”明白最后说工具。我调试PD协议常用的有三类协议分析仪、示波器和电子负载。协议分析仪里比较常见的有Power-Z这类带PD解码的仪器能把PDO列表、消息类型直接解析出来太省事了。我第一次用Power-Z时看到它把Source_Capabilities里的每个PDO都列成电压电流表格心里直呼以前手抓波形纯属浪费生命。不过示波器还是不可替代的。协议分析仪只告诉你“逻辑上发生了什么”示波器能看到“电上发生了什么”——VBUS过冲、掉电、CC上的毛刺这些才是电源问题的根源。调试时我会把示波器探头接在VBUS和CC上波特率对不上时示波器里能看到明显的BMC波形CRC不过时能看到消息发出去但没有GoodCRC回复。电子负载用来做最终负载测试。协商出20V后不能拿个灯珠就算完应该用电子负载从0.1A逐级加到标称电流观察VBUS是否稳定、电流是否被Source限制。尤其是做诱骗器时我建议带上电子负载把电流一步步拉满顺便验证线缆和接插件的压降。很多诱骗器能骗出电压但一上大电流就掉压重新协商就是内部接触电阻太大导致的。做协议类东西急不来。我最初调第一块自研PD板时连续三个晚上都在跟Hard Reset较劲翻来覆去就是没意识到Sink端少处理了Reject消息被Source强制复位了。后来养成的习惯是每个PD消息类型都规定自己该怎么处理宁愿多打印状态也不放过一个分支。现在拿到任何一款充电头我第一件事已经不是看标称功率了而是看它的PDO列表和CC波形——标称可以通过电流虚标但PDO和时序不会骗人。这些细节就是协议它最让人着迷的地方。