news 2026/9/28 17:47:26

从IIC数据解码USB PD报文:CH224Q快充实战解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从IIC数据解码USB PD报文:CH224Q快充实战解析

如果你玩过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.000A

0x040640A5就是我手头这颗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 寄存器映射:一份基于实测的参考

下面这份寄存器映射是我在自己板子上验证过的参考定义,不同批次芯片可能有差异,强烈建议对照你手上那颗芯片的数据手册再确认一次。

地址名称读写说明
0x01CH224_IDR芯片ID/版本号,可用于地址验证
0x04CH224_CTRLR/Wbit2:0为PDO索引,0通常表示自动选择,1起对应实际PDO
0x05CH224_PDO_NUMRSource Capabilities里的PDO数量
0x06CH224_VOLT_LR当前输出电压低字节,单位100mV
0x07CH224_VOLT_HR当前输出电压高字节
0x08CH224_CURRR当前输出电流,单位10mA
0x10CH224_PDO_BASERPDO缓存区起始地址,每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序号缓存原始值解码电压解码电流铭牌标称
PDO10x0019492C5.00V3.00A5.0V/3.0A
PDO20x040D492C9.00V3.00A9.0V/3.0A
PDO30x0412492C12.00V3.00A12.0V/3.0A
PDO40x0417492C15.00V3.00A15.0V/3.0A
PDO50x040640A520.00V3.25A20.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数据里还原出充电器底牌的那一刻,快充协议对你来说就不再是黑盒了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 17:46:58

LVGL 9菜单开发实战:从卡顿到5分钟构建可商用HMI导航系统

1. 为什么嵌入式UI开发总卡在“菜单”这一步&#xff1f;你有没有遇到过这样的场景&#xff1a;STM32跑着FreeRTOS&#xff0c;屏幕也点亮了&#xff0c;LVGL库也编译进去了&#xff0c;但一到做主界面——尤其是带多级导航、状态切换、按钮反馈的菜单系统——就卡住&#xff1…

作者头像 李华
网站建设 2026/9/28 17:46:25

Grok 4.7半价PK ZCode开源:AI编程工具选型避坑指南

1. 今天的看点&#xff1a;Grok打价格战&#xff0c;ZCode开源自救1.1 两条新闻&#xff0c;同一个主题先说今天最值得盯的两件事&#xff1a;Grok 4.7半价开战&#xff0c;智谱ZCode正式开源。一个是xAI用价格屠刀直接切进AI编程市场&#xff0c;一个是智谱在口碑风波之后公开…

作者头像 李华
网站建设 2026/9/28 17:46:04

Qt5.12安装配置全指南:工业级稳定部署实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 17:44:54

从PID到ADRC:用控制论打造稳定可靠的AI Agent

智能体开发做到第三个月的时候&#xff0c;我遇到了一个特别典型的问题&#xff1a;一个用来做数据清洗的Agent&#xff0c;在测试集上跑得漂漂亮亮&#xff0c;任务完成率能到92%&#xff0c;但只要上游数据格式稍微抖一下——比如某个字段从字符串变成了数字&#xff0c;或者…

作者头像 李华
网站建设 2026/9/28 17:43:34

Superpowers开发者工具链:AI编程能力治理框架

1. “Superpowers”不是超能力&#xff0c;是开发者工具链的隐喻性命名体系最近在多个开发工具社区、技术论坛和 Discord 频道里&#xff0c;“superpowers”这个词高频出现&#xff0c;但它既不是 Marvel 漫画新出的 API&#xff0c;也不是某家初创公司注册的商标——它是一套…

作者头像 李华
网站建设 2026/9/28 17:43:22

StackReplay:本地回放AI编码历史,像看电影一样复盘每次代码变更

许多开发者应该都有过这种瞬间&#xff1a;让AI助手写了一坨代码&#xff0c;初看没问题&#xff0c;运行却报错&#xff0c;或者逻辑玩出了花。于是你打开Git历史&#xff0c;想看看它是怎么一步步写出来的——结果发现只有一次丑陋的commit message&#xff0c;或者什么都没有…

作者头像 李华