很多搞硬件的朋友和我说,调USB-C充电电路最痛苦的不是画板,而是不知道协议层到底发生了什么。插上充电头,示波器探在VBUS上只能看到5V或者9V跳变,真正的USB-C PD协议握手,其实发生在CC线上——一根你平时根本不会去关注的信号线。这篇文章我就用示波器把BMC编码和4B5B编码的握手过程完整抓下来给你看,从物理层波形一路解到协议包,顺便把踩过的坑也一起交代清楚。
这篇内容适合谁看?做USB-C接口设备的硬件工程师、写PD诱骗器固件的嵌入式开发者、以及那些用Type-C口给产品取电但始终没搞懂“为什么这个充电器认我、那个充电器不认我”的人。读完你会发现,示波器不只是用来量电源纹波和时序的,它还能当协议分析仪用,而且比协议分析仪更接近信号本质。
1. 先搞清楚:谁在CC线上“说话”,为什么VBUS看不出门道
1.1 从5V/9V跳变到真正的数字握手
很多工程师调试USB-C充电时习惯把探头怼在VBUS上,看到电压从5V跳到9V,就觉得“协议走了”。这个判断不能说错,但充其量只是知道结果,不知道过程。VBUS上的电压变化是Source端功率变换的结果,真正用来协商这个电压的多轮对话,全部发生在CC线上。
CC线全称Configuration Channel,在USB-C座子里只有两根,CC1和CC2。它们平时承担连接检测、角色发现、线缆检测这些任务,而在PD通信时,它就是一条半双工的总线。Source端和Sink端在CC线上来回发送BMC编码的数据包,协商出“你要多少电压”“我能给多少电流”“是否支持PPS”这些信息。
用示波器抓CC线,你能看到一串串脉冲按时间顺序排列,这些脉冲就是BMC编码后的数字信号。解析它们,等于把PD协议从前到后重新走了一遍。
1.2 Source与Sink的分工,以及那一对Rp/Rd电阻
在讲抓波形之前,必须先把CC线上的静态直流电平说清楚。
USB-C的Source端(比如充电器)会在CC1和CC2上分别接一个上拉电阻Rp到某个参考电压,Sink端(比如你的手机)则接一个下拉电阻Rd到地,标准阻值通常是5.1kΩ。当设备插入后,CC线被Source的上拉和Sink的下拉分压成一个固定的直流电平,Source检测到这个电平就知道有设备接入,而且是作为Sink接入的。
这个直流电平不是随便定的,它和Rp的阻值直接相关:
| Rp阻值 | 通讯前默认能力 | 典型CC线静态电压(以5V上拉估算) |
|---|---|---|
| 56kΩ | 默认USB 500mA | 约0.42V |
| 22kΩ | 1.5A | 约0.94V |
| 10kΩ | 3A | 约1.69V |
PD协商开始后,这个直流电平上会叠加BMC调制信号,也就是说CC线上的波形不是从0V开始跳的,而是从某个固定偏置电平上下摆动。这一点对示波器触发电平的设置影响很大,后面实操部分再细说。
1.3 PD和QC的本质区别
聊到这里顺带提一嘴,很多做快充兼容的人总把PD和QC混在一起说。Qualcomm的QC协议走的是D+/D-两根USB数据线上的电压调制,本质上是被动模拟检测,协议简单但扩展性有限。而PD完全不一样,它走CC线,是真正的双向数字通信协议,支持PPS电压连续调节、多档PDO、线缆模式检测等功能。
PD能成为统一标准,最重要的一点是它不依赖某个厂家的私有芯片,USB-IF把物理层、链路层、策略层都定义得明明白白。你只要按照规范写策略,就能在任何一个PD充电器上协商到合适的电压。这也是为什么我建议做硬件调试的人一定得学会抓CC线——你调QC还能靠一堆示波器量D+电压,调PD如果不会看BMC波形,那就真的只能靠猜。
2. 抓包前的工具准备:示波器、探头和转接夹具
2.1 带宽和采样率该怎么算
很多人一听“抓PD协议”,第一反应是“我那个示波器够不够用”。我直说结论:抓PD握手,带宽真不需要太高。
PD物理层的BMC波特率是300kbps,每位时间是3.3微秒,BMC编码后最小的脉冲宽度大约1.67微秒,换算成频率大概在600kHz上下。考虑到方波的谐波,用20MHz带宽限制是绰绰有余的。普通100MHz的示波器完全够用,哪怕你手头只有一台几十MHz的老机器,只要采样率能达到每bit采15个点以上,就能还原出准确的波形。
真正需要注意的反而是采样率和存储深度。我习惯把采样率设在50MSa/s左右,时基拉到每格200微秒到500微秒,这样一屏大概能覆盖1毫秒到5毫秒的时间窗口,足够抓下一组完整的PD数据包交换。
如果示波器存储深度比较浅,采样率就要适当降低,否则波形会自动压缩,细节全糊了。判断标准很简单:抓到的信号最小脉冲宽度(1.6微秒左右)至少要占到几个采样点,否则后续解码全是错的。
2.2 探头选择与接地注意事项
CC线上的信号是单端信号,参考地就是设备的GND,所以普通无源探头就能测,不需要差分探头,这点和抓CAN总线差分信号不一样。
但有一个细节非常影响波形质量,就是探头的接地。千万不要用那种长长的鳄鱼夹地线去接,探头地线长,寄生电感会把BMC信号的高频分量振出一串毛刺。我试过用长地线抓CC线,波形上每个边沿后面都跟着衰减振荡,虽然能看个大概,但后期做BMC解码的时候就头疼了。
正确的做法是使用探头标配的短接地弹簧,或者找一块铜箔直接贴在探头地线最短的位置。这个习惯养成之后,你抓所有数字信号都会省心很多。
另外,要把探头衰减比调到10x,档位选对之后带宽才对。很多示波器的1x档位带宽只有几MHz,直接把BMC信号的高频分量削掉了,抓出来波形圆乎乎的根本没法解码。
2.3 转接板:没有那根CC线焊盘,就只能碰运气
最理想的方式,是准备一块专门的USB-C测试转接板,把CC1、CC2、VBUS、GND全部引出到测试点。市面上现成的USB-C breadboard breakout模块很多,几块钱到几十块钱不等,选择时注意选那种CC1和CC2分开引出的,而不是把所有引脚都并在一起的简陋版本。
如果没有转接板,直接把探头扎进USB-C座的缝隙里也不是不行,但CC1和CC2引脚在Type-C座子里非常密,而且在不同插头方向下走的是不同的CC引脚,你很难确定自己扎对了。更麻烦的是,很多成品线缆把CC引脚藏在屏蔽层里,外面根本接触不到。
所以我的建议是:要么买转接板,要么自己画一个只有USB-C母座、电阻网络和测试点的简易小板。自己画的话,务必把Rp、Rd都做成可选电阻,因为后面调试不同档位时经常要换阻值。
3. BMC编码:示波器波形里的“长短信号”到底在说什么
3.1 BMC规则:每个比特都有两次跳变机会
BMC全称Bi-Phase Mark Coding,中文常译作双相标记编码。它属于一种自同步的线路编码,信息直接编码在跳变的位置上。
具体规则是这样的:每个比特周期固定为3.33微秒,每个比特周期的起始边界一定有一个跳变,用来给接收端恢复时钟。在这个前提下,如果该比特要表示逻辑1,那么在这个比特周期中间会额外增加一次跳变;如果表示逻辑0,则比特周期内部保持电平不变。
换句话说,在一个比特周期里,逻辑0表现为“一整个3.33微秒宽度内只有两次边沿(起点和终点)”,而逻辑1表现为“在这个比特周期里出现三次边沿(起点、中点、终点)”。
这个特性反映在示波器上就是一串间隔不等的脉冲:宽脉冲代表0,窄脉冲代表1。很多人拿到波形以为这就是普通UART的高低电平,直接按码型去解,结果全乱——因为BMC的本质是“边沿携带信息”,不是“电平携带信息”。
3.2 为什么PD选BMC而不是Manchester或UART
我曾经想,PD为什么不直接用UART或Manchester呢?Manchester编码和BMC其实很像,都是利用跳变位置表示数据,但PD选择BMC而非Manchester,主要原因有两个。
第一,BMC编码后信号的直流分量比Manchester更可控。USB-PD在CC线上传输时,接收端是通过检测信号的平均电平和有效值来判断连接状态的,BMC的功率谱密度在低频段的能量分布更集中,对CC线上的直流偏置影响更小。第二,BMC的接收端实现更简单,一个异或门加一个锁相环就能完成时钟恢复和数据解调,这在成本敏感的Type-C接口芯片里很有优势。
UART就不说了,它需要一个精确的波特率时钟,且没有自同步能力,一旦收发双方时钟偏差超过千分之几就会丢字节,完全不适合这种热插拔场景。
3.3 把波形还原成BIT流的小脚本
既然BMC是边沿编码,那么用示波器导出的CSV文件就能离线解码。我经常这么干:先把示波器的波形数据导出成CSV,然后用Python写个小脚本,先找跳变边沿,再根据边沿间隔判断是一串0还是一串1。
一个懒人版本的解码思路是这样的:
import numpy as np t, v = np.loadtxt('cc_wave.csv', delimiter=',', skiprows=1).T threshold = 0.8 # 根据实际波形调整 edges = np.where(np.diff(v > threshold) != 0)[0] # 得到每个边沿对应的时刻 edge_times = t[edges] dts = np.diff(edge_times) # BMC: bit 周期约3.33us bit_period = 3.33e-6 bits = [] for dt in dts: half = round(dt / (bit_period / 2)) # 相邻边沿间隔若为1个UI -> 连续1最多1个;若为2个UI -> 中间有1个0 # 细节需要结合实际起始边沿做修正 if half == 1: bits.append(1) elif half == 2: bits.append(0)这段代码只是演示核心思路,真正的完整解码还需要处理起始相位、去掉前导码等问题。实际调试时,其实可以先用示波器自带的解码功能或者开源工具对照一下,再写自己的脚本。我就是这么干的:先用工具确认一段波形是什么内容,再回头硬啃脚本,两相对照很快就通了。
4. 4B5B编码与数据包结构:从BIT流到能读懂的消息
4.1 4B5B映射表
BMC搞定的是物理层,但PD报文并不会直接把字节串变成BMC波形。在BMC之前,还有一层链路编码,就是4B5B。
4B5B的意思是把4bit数据映射成5bit符号。为什么要多做这一步?因为BMC虽然能自同步,但它对长串的连续相同电平比较敏感,如果物理层出现连续多个0,接收端的时钟恢复会越来越吃力。4B5B可以保证线路上不会出现太长的连续同电平,同时引入一定的冗余,让接收端更容易做对齐和校验。
PD协议里使用的4B5B映射表如下:
| 4bit数据 | 5bit编码 | 4bit数据 | 5bit编码 |
|---|---|---|---|
| 0000 | 11110 | 1000 | 10010 |
| 0001 | 01001 | 1001 | 10011 |
| 0010 | 10100 | 1010 | 10110 |
| 0011 | 10101 | 1011 | 10111 |
| 0100 | 01010 | 1100 | 11010 |
| 0101 | 01011 | 1101 | 11011 |
| 0110 | 01110 | 1110 | 11100 |
| 0111 | 01111 | 1111 | 11101 |
需要注意,除了数据符号之外,4B5B还定义了一些控制符号(K码),用来标识包的开始位置和结束位置。
4.2 一个PD数据包由哪些部分组成
当你把BMC波形解成bit流,再按5bit分组、查找K码对齐之后,你会得到一个类似这样的数据包结构:
- Preamble(前导码):一段固定模式的0/1交替序列,用来让接收端锁相环锁定时钟。
- SOP(Start Of Packet):由特定的K码序列组成,标识一个包开始,还能区分SOP1/SOP2/SOP3,用来识别是Source发给Sink、Sink发给Source,还是线缆E-Marker相关通信。
- Header(16 bit):消息类型、消息长度、报文ID等关键信息。
- Data Objects(可选):一个或多个32bit的数据对象,比如Source_Capabilities里的PDO列表,每项都写着电压、电流范围。
- CRC32(32 bit):对整个包的校验。
- EOP(End Of Packet):结束标志。
在示波器上,如果你只看物理层,能明显看到每个数据包是“一组密集脉冲”,包与包之间有一段安静的空闲期。这个空闲期大概会有好几个微秒甚至几十微秒,取决于数据链路层的策略。
4.3 Header字段里藏着什么
Header是整个PD消息里最容易提取价值的部分。16bit的Header里有几个核心字段:
- 消息类型(Message Type):比如Source_Capabilities、Request、Accept、Reject、PS_RDY、GoodCRC等。
- 对象数(Number of Data Objects):后面跟了几个32bit数据对象。
- 报文ID(MessageID):用来做重传和去重的。
- 端口角色(Port Role):标识发送方是Source还是Sink。
这些都是标准PD协议定义好的,知道消息类型之后,你再去解读后面的Data Objects就有的放矢了。比如抓到Source_Capabilities之后,后面跟着的几个Object就是它能提供的电压电流组合。
这一层信息没法直接从波形上肉眼读出来,需要把波形解成bit,再按4B5B表反向映射。所以实操时,建议先在PC上用脚本或示波器解码功能做一次自动解析,再对着波形核对关键位置。
5. 实操:示波器抓一次完整的PD握手
5.1 接线和触发参数怎么设置
接下来进入正题。我这里用充电器作为Source,一个带电池负载的PD诱骗器作为Sink,做一次完整的抓包示范。
接线方式如下:USB-C转接板的CC1测试点接示波器CH1探头,探头的接地弹簧接转接板的GND。如果你的转接板把CC1和CC2都引出来了,优先抓CC1,因为Type-C线缆默认使用的就是CC1,除非插反了才会切到CC2。
示波器参数按这套来:
- 通道耦合:DC
- 探头衰减比:10x
- 带宽限制:20MHz(如果示波器支持,把带宽限制打开可以滤掉高频噪声)
- 采样率:50MSa/s
- 时基:200us/div
- 触发电平:先根据CC静态电平估算,如果是0.9V左右,触发电平设在0.6V左右
- 触发方式:上升沿、Normal模式
然后执行关键一步:把示波器切到Single(单次)触发,再去给系统上电或者插入USB-C连接器。这时候示波器会捕获到一串完整的PD握手波形。
这里提醒一下,很多示波器默认的触发模式是Auto,Auto模式下没有信号时波形会一直刷新,容易错过握手过程。用Single触发能确保只在抓到时停下来,这样后续放大看细节非常方便。
5.2 典型波形解读:Source_Capabilities、Request、PS_RDY
第一次抓到完整握手波形的时候,你看到的画面大概是这样的:先是一段比较稳定的直流电平,然后突然冒出一簇脉冲,间隔几百微秒又来一簇,每簇脉冲都是一两个PD数据包。
按PD规范,连上之后通常会发生这样一个流程:
- Source端在CC线上发送Source_Capabilities包,告诉Sink自己支持哪些电压电流档位,例如5V/3A、9V/3A、12V/3A等。
- Sink收到后,发送Request包,请求其中一个档位。
- Source收到Request后,校验通过则回复Accept,然后完成电源输出切换并发送PS_RDY包。
- 之后双方进入稳定供电状态,CC线上安静下来,偶尔会有GoodCRC或者其他消息。
在示波器上,如果你抓的窗口足够长,你会先看到Source发的Source_Capabilities(带多个Data Object,脉冲串比较长),然后隔一段是Sink发的Request(短一些),再往后是关键的两个包:Accept和PS_RDY。
我最早抓的时候只关注了VBUS电压,觉得PS_RDY没什么好看的。实际上PS_RDY信号在CC线上的出现位置很关键,它代表电源转换器完成了档位切换,如果你的产品在那个时刻拉载导致电压跌落,就能明显看出协议状态和电源状态之间的时序关系。
5.3 如何判断抓到了“完整的”握手
判断你抓到的波形是否完整,最简单的标准是看CC线上有没有出现PS_RDY包之后的“安静期”。如果抓完一轮之后一直看到Source反复发送Source_Capabilities,说明Sink没有正确接收或者没有发Request,这种波形虽然也有脉冲,但不代表完成了一次协商。
更准确的做法是同时抓两路信号:CH1接CC1,CH2接VBUS。这样你能在同一个时间轴上看到CC线上的协议包和VBUS电压的切换对齐关系。我就是这么判断的:当VBUS从5V跳到9V的那个边沿,前面必然紧跟着一个PS_RDY包,如果这个对应关系对不上,说明抓到的包里可能有重传或者状态异常。
顺带一提,如果只是想快速测试自己的PD Sink设备能不能正常工作,最省事的办法是在CC线和VBUS之间并联一个合适的Rd电阻,让Source以为有Sink接入,然后手动触发拉载,再观察VBUS响应。这种方法能看到供电切换,但看不到协议细节,属于应急手段。
6. 踩坑记录:CC线抓包常见问题与排查
6.1 抓不到任何波形,先检查CC1/CC2
这是最最常见的问题,十个里有五个是CC1和CC2抓反了。USB-C插头正反插用的是不同的CC引脚,很多转接板做出来之后并没有把CC方向固定,你必须两条都接上探针或者用一根支持PD的线缆直接把其中一根CC引出来。
排查方法很简单:把示波器挂在CC1上,插上设备如果没反应,就换到CC2上。如果两个都没有,再用万用表量一下CC线上的静态电压,看看是不是0.4V到1.7V之间。如果CC电压是0V,大概率是没接对地,或者Sink端根本没有正确下拉,Source根本没识别到设备。
我见过一次最隐蔽的情况是转接板上的Rd焊错了,导致CC线上始终是高电平,Source以为没插线缆,根本不进入PD通信状态。这种问题不看CC电平,光抓波形永远抓不到信号。
6.2 波形乱跳、毛刺多
波形毛刺多,十有八九是探头接地线太长。我前面提过,用长鳄鱼夹地线抓高频数字信号,寄生电感会和探头电容形成谐振,在边沿处产生振铃。
另外还有一个容易被忽略的干扰源:如果你的转接板供电和示波器供电不共地,或者Type-C充电器是反激电源且没有良好接地,共模噪声会直接叠在CC线上。我处理这种问题的方法是在转接板GND和示波器GND之间用一根很粗的短线直接短接,尽量缩短回流路径。
如果BMC波形幅度看着很小,只有几十毫伏,那就要检查是不是探头挡位选错了,很多示波器在1x挡位下带宽降得很低,直接把信号细节削没了。
6.3 只抓到一半就没了
有时候你能看到Source_Capabilities的脉冲,但一直没抓到Sink回的Request,或者抓到了Request却没看到Accept/PS_RDY。这种情况通常是触发电平没设置到位,导致示波器在信号中间某个位置触发,只截到部分包。
解决思路是在Single模式下多试几个触发电平,比如把电平从0.6V调到0.3V或者0.9V,再重新插拔一次。每次握手发生的时间很短,多试几次总能抓到完整窗口。
另一个容易踩的坑是示波器时基太长,把整个握手过程压缩成一团,导致BMC脉冲挤在一起看不出个数。这时候重新把时基调短,只抓单个包。我习惯先长时基抓全貌,再存储波形后逐步放大去看细节,毕竟示波器的缩放功能是零成本的。
6.4 常用参数速查表
| 参数 | 数值 | 说明 |
|---|---|---|
| BMC波特率 | 300 kbps | 每位周期约3.33us |
| BMC最小脉冲宽度 | 约1.67us | 数据1时比特周期中间有一次跳变 |
| 典型CC静态电平 | 0.4V-1.7V | 与Rp/Rd分压有关 |
| 建议采样率 | 50MSa/s | 每bit至少采样15个点 |
| 建议时基 | 200us/div | 单次抓包窗口覆盖几个包 |
| 探头带宽 | 20MHz以上即可 | 带宽限制开启可滤噪 |
这套参数适用于绝大多数PD 2.0/PD 3.0协议调试场景,PPS、E-Marker通信这类扩展场景也能覆盖,只需要在分析时多关注SOP'类型的数据包即可。
6.5 给新手的最后提醒
示波器抓PD协议这件事,难点不在设备,而在你愿不愿意从一根波形里拆出所有信息。第一次抓到波形后,别急着换设备,先把波形放大,从第一个边沿开始数,看看BMC脉冲的宽窄分布,再对照4B5B解码表推几个字节出来,那种把一个无名波形彻底理解透的感觉,比直接抄一个协议分析仪结果来得踏实得多。
我个人在实际操作中还有一个小习惯:每次抓完包,都会把CSV导出来存一份,文件名按日期加充电器型号命名。时间长了之后,这就是一台“充电器协议数据库”,哪个牌子的充电器支持哪些PDO、握手时序有什么特点,都一目了然。后面再遇到兼容性问题,翻一翻旧文件往往就能找到答案。