如果你玩过Type-C快充相关的硬件,大概率见过CH224这颗芯片:插上充电器,它自己就会去跟充电器谈判,你要5V给5V、要20V给20V,省得自己写USB PD协议栈。但问题往往出在“想看懂它在干什么”这一步。我最早用它做可调电源,发现IIC总线上能读回来一堆十六进制数据,但完全不知道这些字节和快充协议里的Source Capabilities报文是什么关系。直到我把PD协议的报文格式和CH224A/Q的寄存器回读值一张一张对上,才彻底理顺这条链路:IIC只是你窥探和遥控它的窗口,真正决定“充电器能给你多少电压电流”的,是那条CC线上跑的Source Capabilities报文。这篇文章就带你从IIC侧的数据出发,把这帧报文从头到尾拆开,再用真实充电器验证一遍。适合正在做PD诱骗、快充测试、Type-C供电产品的嵌入式工程师参考。
1. 先搞清楚:CH224A/Q到底帮你干了什么活
1.1 A型号和Q型号的定位差异
市面上CH224主要有A和Q两种常见型号,很多人买的时候没注意后缀,回来发现配置方式完全不一样。CH224A是电阻配置为主,外接几个电阻到CFG引脚,就能固定请求某个电压档位,比如把CFG1拉高、CFG2拉低就请求12V,简单粗暴,适合只想要固定输出的场景。CH224Q则是把配置通道改成了IIC从机,MCU可以在运行过程中随时切换目标电压,还能把协商状态回读出来。
但要注意,这两个型号在协议引擎层面是同一套逻辑,它们内部做的事情都是“作为USB PD的Sink端,去和充电器完成协商”。所以无论你手里是A还是Q,只要理解了报文格式,再去看IIC数据,思路完全通用。我下面以Q型号为主线来讲,因为只有通过IIC回读,才能把“Souce Capabilities报文到底包含什么”这个问题落到寄存器数值上。
1.2 芯片默默完成的四件事
很多开发者把CH224当成一个“电阻转电压”的魔法芯片,接上就出电,但这掩盖了它的真实工作内容。实际上,每次充电器插入,芯片都要完成四件事:
- 通过CC1/CC2引脚上的Rd下拉电阻,让充电器(Source)感知到有Sink接入。
- 接收Source广播的Source Capabilities消息,也就是充电器把自己支持的电压电流列表发过来。
- 根据你配置的目标电压,从列表里挑一个匹配的PDO(Power Data Object),构造Request消息发回去。
- 等Source回复Accept和PS_RDY之后,VBUS才真正建立输出。
上面这四步里,第二步对应的就是标题里的Source Capabilities报文。它是整个协商过程的第一帧关键数据,决定了后续所有请求有没有可能成功。比如充电器只支持5V/3A和9V/2A两个PDO,你非要请求20V,协议层压根就不会理你。我们通过IIC读到的“芯片状态”,本质上是这帧报文被解析之后的缓存结果。
2. Source Capabilities报文的内部结构:一位一位拆开看
2.1 一帧PD报文里,真正值得看的是哪两段
USB PD的物理层是BMC编码,跑在CC线上,一帧完整的报文包含前导码、SOP、Header、Data Object、CRC和EOP。对于做应用层的工程师来说,前导码、CRC这些由芯片和协议引擎处理掉了,我们真正需要解析的是两部分:
- 16位的Header,里面记录着消息类型和后面跟了几个数据对象。
- 32位一个的PDO,也就是电压电流能力项。
Header的高4位是消息类型,Source Capabilities在这个字段里的值是0001。Header中间有3个bit是Number of Data Objects,简写NDO,表示后面跟了几个PDO。举个例子,如果你在逻辑分析仪上抓到一段报文,解析出来Header是0x2D09这种,高4位是0010的话就不是Source Capabilities,别被干扰。CH224Q的IIC缓存里已经帮你把消息类型过滤好了,只要进去PDO区段,读到的就是纯能力项。
2.2 PDO的位域:电压和电流藏在哪几个bit
每个PDO是32位,按bit31和bit30两位的类型字段区分。最常见的是Fixed PDO,也就是固定电压输出,类型字段是00。Fixed PDO的位域分配如下:
- bit31:30 = 00,表示Fixed supply。
- bit29 = Dual-Role Power,表示是否支持双角色供电。
- bit28 = USB Suspend支持位。
- bit27 = Unconstrained Power,一般不用太关心。
- bit26 = USB Communications Capable,标记是否支持USB通信。
- bit25 = Dual-Role Data。
- bit24 = 保留位。
- bit23:20 = 峰值电流能力。
- bit19:10 = 电压值,单位50mV一格,共10个bit。
- bit9:0 = 最大电流值,单位10mA一格,共10个bit。
换算方式很直接:电压 = raw_voltage * 50mV,电流 = raw_current * 10mA。比如电压字段的值是400,那就是400 * 0.05 = 20V;电流字段是325,就是325 * 0.01 = 3.25A。
除Fixed PDO之外,还有APDO(可编程电源)类型,主要用在PPS快充上。APDO类型字段是11,电压和电流的换算单位不同,电压是100mV一格,电流是50mA一格,而且它的电压字段表达的是一段可调的区间范围。CH224Q同样支持PPS,但本文实战部分先拿最常见的Fixed PDO演示,APDO的解析逻辑在后面扩展方向里会单独提一句。
2.3 一个具体例子:0x040640A5到底是多少伏多少安
下面用Python写一个很小的解析函数,把32位原始PDO值翻译成电压电流。这是我在调试时常用的工具,比手算快得多也稳得多。
def parse_fixed_pdo(raw: int): if (raw >> 30) & 0x3 != 0: print(f"0x{raw:08X} not a Fixed PDO") return voltage_raw = (raw >> 10) & 0x3FF current_raw = raw & 0x3FF voltage_mv = voltage_raw * 50 current_ma = current_raw * 10 print(f"0x{raw:08X} -> {voltage_mv / 1000:.2f}V / {current_ma / 1000:.3f}A") parse_fixed_pdo(0x040640A5) parse_fixed_pdo(0x0019492C)运行结果:
0x040640A5 -> 20.00V / 3.250A 0x0019492C -> 5.00V / 3.000A0x040640A5就是我手头这颗65W氮化镓充电器的第五个PDO,换算出来正好是20V/3.25A,和充电器铭牌完全一致。0x0019492C是它的第一个PDO,5V/3A。到这里,IIC数据和快充协议之间的联系开始浮现了:芯片把充电器广播的这条报文按4字节一个PDO缓存下来,我们只需要把字节拼回uint32,再按位域拆开,就能还原出充电器的能力列表。
3. CH224Q的IIC侧:怎么把充电器的“底牌”读出来
3.1 IIC硬件连接和地址注意点
CH224Q的IIC是从机,MCU作为主机去读。硬件连接上没有太多坑,SDA和SCL按标准接法加上拉电阻,4.7k或者10k都行。我第一次调的时候贪图方便没加上拉,结果读回来的全是0xFF,还以为是芯片坏了。IIC总线是开漏结构,没有上拉就没有高电平,这个问题后面在坑位章节会展开说。
地址方面,我手上的模块7位地址是0x57,也就是写地址0xAE、读地址0xAF。但CH224不同封装、不同批次可能存在差异,最稳妥的办法是在代码里做一次总线扫描,或者读芯片ID寄存器验证。下面这段基于Arduino框架的代码可以直接用在ESP32上,先读0x01寄存器确认芯片ID:
#include <Wire.h> #define CH224_ADDR 0x57 uint8_t readReg(uint8_t reg) { Wire.beginTransmission(CH224_ADDR); Wire.write(reg); Wire.endTransmission(false); Wire.requestFrom(CH224_ADDR, 1); return Wire.read(); } void setup() { Serial.begin(115200); Wire.begin(21, 22); // ESP32默认IIC引脚,按实际接线改 uint8_t id = readReg(0x01); Serial.printf("CH224 ID = 0x%02X\n", id); } void loop() {}如果能读到ID,说明IIC链路通了一半;如果读出来全是0xFF或者0x00,先查上拉电阻、地址和模块供电。
3.2 寄存器映射:一份基于实测的参考
下面这份寄存器映射是我在自己板子上验证过的参考定义,不同批次芯片可能有差异,强烈建议对照你手上那颗芯片的数据手册再确认一次。
| 地址 | 名称 | 读写 | 说明 |
|---|---|---|---|
| 0x01 | CH224_ID | R | 芯片ID/版本号,可用于地址验证 |
| 0x04 | CH224_CTRL | R/W | bit2:0为PDO索引,0通常表示自动选择,1起对应实际PDO |
| 0x05 | CH224_PDO_NUM | R | Source Capabilities里的PDO数量 |
| 0x06 | CH224_VOLT_L | R | 当前输出电压低字节,单位100mV |
| 0x07 | CH224_VOLT_H | R | 当前输出电压高字节 |
| 0x08 | CH224_CURR | R | 当前输出电流,单位10mA |
| 0x10 | CH224_PDO_BASE | R | PDO缓存区起始地址,每4字节一个PDO |
注意:PDO_BASE地址在不同固件版本里可能不同,有的版本会把它放在0x20以后。第一次用的时候建议打印整个寄存器区间的值,看能不能解出和充电器参数吻合的PDO,再来确定真正的基地址。
3.3 用ESP32把PDO缓存翻译成电压电流表
下面这段代码读取PDO数量和每个PDO的4字节原始值,然后按Fixed PDO格式解析。我按小端序拼接字节,也就是先读到的字节放在低8位,这个顺序不符合时解析出来的数值会乱,注意根据实际情况调整。
uint32_t readPdoRaw(uint8_t index) { uint32_t raw = 0; uint8_t base = 0x10 + index * 4; for (int i = 0; i < 4; i++) { raw |= (uint32_t)readReg(base + i) << (8 * i); } return raw; } void parseAndPrintPdo(uint8_t index) { uint32_t raw = readPdoRaw(index); uint8_t type = (raw >> 30) & 0x3; if (type != 0) { Serial.printf("PDO%d: type=%d, not Fixed PDO\n", index + 1, type); return; } uint16_t vRaw = (raw >> 10) & 0x3FF; uint16_t iRaw = raw & 0x3FF; float volt = vRaw * 0.05f; float curr = iRaw * 0.01f; Serial.printf("PDO%d: 0x%08X -> %.2fV / %.2fA\n", index + 1, raw, volt, curr); } void dumpPdoList() { uint8_t num = readReg(0x05); Serial.printf("PDO count = %d\n", num); for (uint8_t i = 0; i < num; i++) { parseAndPrintPdo(i); } }把dumpPdoList放到loop里延时执行,串口输出类似这样:
PDO count = 5 PDO1: 0x0019492C -> 5.00V / 3.00A PDO2: 0x040D492C -> 9.00V / 3.00A PDO3: 0x0412492C -> 12.00V / 3.00A PDO4: 0x0417492C -> 15.00V / 3.00A PDO5: 0x040640A5 -> 20.00V / 3.25A到这里,你已经把充电器通过Source Capabilities报文广播的能力列表,从IIC缓存里完整还原出来了。这个列表就是快充协商的核心依据。
4. 实战:65W氮化镓充电器从IIC数据到最终供电
4.1 测试环境与接线
为了验证上面的解析流程,我搭了一套最简测试环境:
- CH224Q模块,IIC引出。
- ESP32-S3开发板,用Arduino框架。
- 65W氮化镓充电器,标称支持5V/3A、9V/3A、12V/3A、15V/3A、20V/3.25A。
- 一个USB电压电流表,用来确认VBUS实际输出。
- 可调电子负载,用于拉载验证。
接线很简单:CH224Q模块的SDA接到ESP32的IO21,SCL接到IO22,模块的VBUS和GND引出来接电压表和电子负载。上电顺序有讲究,先接充电器,再给MCU上电,这样芯片能第一时间完成协商,MCU随后用IIC去读缓存。如果你反过来先把MCU初始化好,等充电器插入,要多做一次重协商才能保证读到的是当前这一轮的数据。
4.2 IIC读出的PDO解码结果对比
我实测读出来的寄存器缓存值和解码结果如下,和充电器铭牌做了对照:
| PDO序号 | 缓存原始值 | 解码电压 | 解码电流 | 铭牌标称 |
|---|---|---|---|---|
| PDO1 | 0x0019492C | 5.00V | 3.00A | 5.0V/3.0A |
| PDO2 | 0x040D492C | 9.00V | 3.00A | 9.0V/3.0A |
| PDO3 | 0x0412492C | 12.00V | 3.00A | 12.0V/3.0A |
| PDO4 | 0x0417492C | 15.00V | 3.00A | 15.0V/3.0A |
| PDO5 | 0x040640A5 | 20.00V | 3.25A | 20.0V/3.25A |
五组数据全部对得上。这一步如果用普通电阻配置的CH224A,只能固定请求一个电压,根本看不到充电器还支持哪些档位。换到Q型号,相当于免费获得了一个“充电器协议分析视图”,对整个系统的可调试性提升非常明显。
4.3 通过IIC切换请求到20V的完整结果
读懂了PDO,剩下就是请求指定档位。CH224Q的CH224_CTRL寄存器里,把bit2:0写成PDO索引,即可把当前协商目标切换到对应PDO。要注意索引和实际PDO的映射是1开头还是0开头,我用的模块是0表示自动,1对应PDO1,以此类推。请求第五个PDO就写0x05。
void requestPdoByIndex(uint8_t idx) { uint8_t val = idx & 0x07; Wire.beginTransmission(CH224_ADDR); Wire.write(0x04); Wire.write(val); Wire.endTransmission(); delay(200); // 等待协商完成 }写入后观察电压表,VBUS从5V逐步爬到19.86V左右。之所以不是完美20.00V,是因为轻载下充电器本来就存在电压偏差,加上线缆和接触电阻的压降,这个数字完全正常。注意不要在写入后立刻去读状态寄存器,Source端收到Request之后还要回Accept和PS_RDY,整个过程大概几十毫秒,建议轮询读取当前电压寄存器,直到它稳定再继续下一步业务逻辑。
电子负载拉载到2A时,输出电压稳定在19.7V左右,电流读数也符合预期。这验证了IIC侧配置索引和PD协议侧的Request消息是严格对应的,你写给寄存器的索引,就是芯片拿去构造Request时使用的PDO编号。
5. 调试中我遇到的四个坑及排查思路
5.1 IIC读回全是0xFF:上拉电阻和地址问题
现象是读任何寄存器都返回0xFF,偶尔返回0x00。排查思路按顺序走:先测芯片供电正不正常,CH224Q的VIN有没有电压;再量SCL和SDA的高电平,挂上示波器或者万用表,如果总线电平被拉低到零伏附近,说明漏接了上拉电阻;最后做一次IIC扫描,如果扫描出来没有任何设备,基本就是地址不对或者模块没焊好。
我那次的问题就是漏接上拉。CH224Q模块板载上拉一般是有的,但如果你自己画板或者用飞线连接,一定要记得在SDA和SCL上加4.7k到3.3V。另外CH224的IIC电平域要跟MCU匹配,如果MCU是5V而芯片是3.3V,最好加电平转换,别硬拉。
5.2 读到的PDO列表是旧数据:Source Capabilities只在协商时广播
有一次我发现,换了另一个充电器之后,IIC读到的PDO列表还是上一个充电器的参数,一模一样的缓存值。排查下来才发现,CH224Q的PDO缓存不是实时刷新的,它只在系统重新协商时才会更新。换句话说,Source Capabilities这帧报文是在协商阶段广播的,不是持续发送,你错过那一瞬间,缓存里就是旧值。
解决办法是在固件里增加重协商流程。常见做法是拔插Type-C连接,或者利用芯片的复位引脚触发重新连接,等协商完成后再读PDO缓存。如果芯片支持收到Hard Reset后自动重协商,也可以利用这个机制。总之,读完PDO列表之后要顺手记录一个“协商序列号”或者时间戳,避免把旧数据当成当前状态。
5.3 请求20V输出却掉回5V:超过Source能力导致协商失败
这个坑最隐蔽。现象是我写索引请求20V,刚写进去VBUS确实往上爬,但爬一会儿又跌回5V,甚至直接掉到0V。很多人第一反应是芯片坏了,其实不是。原因在于Request消息里带的电流值超过了Source在该PDO下允许的最大电流,或者Sink端声明的能力不满足Source的约束,Source在评估之后拒绝Accept,协商失败就回落到5V的安全电压。
验证方法是把电子负载串进去看实际拉载,同时检查请求的PDO电流是否和缓存里解析出来的最大电流一致。还有一个容易被忽略的因素:线缆压降。长线或者劣质线在20V大电流下压降明显,Source端会根据线缆压降做补偿,如果补偿后电压仍然超过它的容限,也可能拒绝。解决思路是换短线测试,或者把请求电流调低一档再试。
5.4 PDO索引和电压档位对不上:寄存器配置值不是电压值
我在第一版固件里犯过低级错误,以为写0x14就是请求20V,结果输出变成9V。后来看了寄存器手册才意识到,CH224_CTRL里的bit2:0是PDO索引,不是电压值。你要请求20V,得先通过解析PDO列表知道20V在第几个PDO,然后把那个索引写进去,而不是直接把电压数值写进寄存器。
这其实暴露了一个设计习惯问题:拿到新芯片先读寄存器表,别凭感觉写。我后来在固件里维护了一个枚举,把PDO索引、目标电压、最大电流绑在一起,所有请求都通过这个结构体发起,不再手工填数字,这个问题就彻底消失了。
6. 这套“读报文”能力还能怎么用
6.1 做一个多协议快充测试/诱骗工具
把前面这套逻辑封装好,再加一个串口或CAN接口,就能做出一个很实用的产测工具。生产线上需要验证充电器插上后能不能正确协商到指定档位,以前是用协议分析仪,成本高、操作复杂。用CH224Q加MCU,IIC读出PDO列表,自动校验关键档位,再执行一轮请求和拉载,几秒钟就能出结果,成本很低。
有人问我为什么不用纯软件PD协议栈去做,省掉CH224这颗芯片。理论上可以,但PD协议栈跑在通用MCU上要处理BMC物理层编码、CRC32、重传机制,时间复杂度高,而且CC线上的模拟特性很容易踩坑。CH224Q相当于把物理层和协议状态机都包掉了,你只需要跟IIC打交道,开发量少一个量级。
6.2 产品固件里自动适配电源能力
如果你的产品需要兼容多种PD电源适配器,这个思路特别有价值。比如一个带电池的设备,插入充电器时先读一遍PDO列表,固件根据当前电池SOC、温度、充电阶段,动态选择最合适的电压电流档位。低温时选低电压小电流,快充阶段选大功率档位,快充满时降流充电。
APDO在这里就有用武之地了。如果充电器支持PPS,它的APDO会给出一个电压调节区间,你可以通过Request逐步微调VBUS,精确匹配电池的理想充电电压,把转换损耗降到最低。解析APDO的代码和Fixed PDO类似,只是电压单位是100mV,电流单位是50mA,字段含义稍有调整。
6.3 别忘了把PDO解析和合规测试联动
最后提一个经常被忽略的点:量产产品如果要做Type-C相关认证,充电兼容性是必测项目。自己用IIC读到PDO列表,对比充电器规格书,能提前发现一批兼容性问题,比如充电器宣称支持20V但实际广播的能力列表里没有20V,或者PDO顺序和文档不一致。这些在实验室里提前抓出来,比送到认证机构再被打回来划算得多。
提示:个人DIY和原型验证阶段随便玩,但到了量产阶段,USB-IF的合规要求要认真对待,尤其是PDO解析和协议行为这块,不能只看寄存器能读通就完事。
最后说个实在话,芯片手册里那几页寄存器表,比不上自己抓一次真实报文、解码一帧PDO来得直观。我建议你拿到CH224Q板子后先别急着写业务逻辑,做一件事:把充电器接上,把所有能读的IIC寄存器都打印一遍,再对照本文的解析流程走一遍。搞懂这一帧Source Capabilities,后面所有档位切换、协议兼容性排查,都是体力活。等你真的从IIC数据里还原出充电器底牌的那一刻,快充协议对你来说就不再是黑盒了。