做嵌入式这几年,只要跟Type-C充电打过交道的人,基本都绕不开CH224这颗芯片。它的定位很明确:一颗PD诱骗/取电芯片,接在设备端充当Sink,让设备可以从PD充电器里拉到5V、9V、12V、20V甚至PPS可调电压。今天我想聊的不是怎么搭电路让它输出固定电压,而是把数据链路彻底翻一遍——从充电器发出来的Source Capabilities报文,到CH224A/Q通过IIC接口暴露给MCU的数据,中间到底发生了什么。
这篇文章适合三类人:一是正在用CH224A/Q做Type-C取电项目、但只停留在“照着参考电路抄”的工程师;二是想用IIC动态切换PD档位、却搞不清寄存器里那些十六进制数据含义的开发者;三是单纯想搞懂USB PD协议里Source Capabilities报文结构、但又不想啃几百页英文规范的朋友。看完之后,你至少能回答三个问题:寄存器里的PDO数据怎么解读?IIC读回来的字节和CC线上跑的报文什么关系?请求电压不生效时该往哪个方向排查?
1. 先搞清楚:Source Capabilities报文到底在说什么
1.1 PD会话里的“Source”和“Sink”分别扮演什么角色
USB PD协议把连接双方分成两个角色:Source是供电端,也就是充电器、电源适配器;Sink是受电端,也就是你的设备、你的板子。CH224A/Q在整条链路里就是标准Sink。
这里有个很常见的误解:很多人以为插上CC线,充电器就直接把20V送出来了。实际上完全不是这样。USB PD所有功率协商都是“请求-应答”模式,充电器默认只输出一个基准电压(通常是5V),然后通过CC线广播自己能提供哪些电压和电流档位,等Sink端选一个档位发回Request,Source确认后才会真正把VBUS电压切换过去。
Source Capabilities这个报文,就是Source端用来“自我推销”的。它会告诉Sink:我有几个档位,每个档位能输出多少伏、多少安,是固定电压还是可编程PPS,电流支持能力是多少。这个过程发生在PD物理连接建立之后、Sink发Request之前。可以理解为双方握手后,供电方向受电方递过来一张菜单,上面列着所有可点的菜和价格。
1.2 Source Capabilities报文的外壳:SOP、Message Header和Data Object
USB PD报文不是纯裸数据,它有完整的“信封”结构。抓包工具或者逻辑分析仪在CC线上看到的物理帧大概长这样:前导码、SOP标志、Message Header、Data Object数组、CRC32。对开发者来说,真正需要关心的是中间三个部分。
SOP是用来标识“这条报文是Source发给Sink的”还是“Sink发给Source的”。SOP表示Source到Sink方向,SOP'表示Sink到Source方向。所以Source Capabilities报文带的是SOP,而CH224回过去的Request、GoodCRC等带的是SOP'。以前有人抓报文时发现方向怪异,多半是没理解SOP和SOP'的差别。
Message Header是16bit的报文头,内容包含Message Type、Message ID、Number of Data Objects(NDO)等信息。Message Type告诉接收方这是一条什么报文,比如GoodCRC、Request、Source Capabilities。NDO直接决定这条报文后面跟了几个Data Object,也就是几个PDO功率描述符。
Data Object才是真正有含金量的部分。每个Data Object固定32bit,对应一个功率档位。Source Capabilities理论上最多可以带7个PDO,所以报文长度是可变的。NDO等于几,后面就有几个32bit的PDO数据。CH224A/Q内部拿到这些PDO之后,会按顺序解析并存储,然后通过IIC寄存器暴露给外部MCU。
1.3 手动解码一个真实的固定PDO
拿最常见的固定电压PDO来举例。一个标准Fixed Supply PDO长这样:
- bits[31:30] = 00b,表示固定电压档
- bits[29:24] = 各种能力标志位,比如是否支持双角色、是否支持USB通信、是否支持无约束供电
- bits[23:20] = 保留位或扩展标志
- bits[19:10] = 输出电压值,单位50mV
- bits[9:0] = 最大输出电流,单位10mA
打个比方,如果某个PDO原始值是0x0401912C,怎么拆?先看bits[31:30]是00,确认是固定PDO。电压字段取bits[19:10],数值是0x064,换算一下:100乘以50mV等于5V。电流字段取低10位,0x12C等于300,乘以10mA等于3A。所以这个PDO就是5V/3A档。
再举一个实际充电器常见的组合。某充电器的Source Capabilities报文里带了四个PDO:
| PDO序号 | 原始值(十六进制) | 电压 | 电流 |
|---|---|---|---|
| PDO1 | 0x0401912C | 5V | 3A |
| PDO2 | 0x0402D12C | 9V | 3A |
| PDO3 | 0x0403C12C | 12V | 3A |
| PDO4 | 0x0406412C | 20V | 3A |
看到0x640、0x2D0、0x3C0、0x640这些电压字段,对应的就是100、180、240、400这串十进制数,乘以50mV就得到5V、9V、12V、20V。这套换算关系最好刻在脑子里,因为IIC寄存器读回来的原始值完全就是这套格式,只是字节序可能不同。
2. CH224A/Q在这条链路里干了什么
2.1 一颗Sink诱骗芯片的定位
CH224A/Q本质上是一个完整的USB PD Sink控制器。它内部集成了物理层收发器、BMC编解码、协议状态机,以及PDO解析和Request生成逻辑。外部MCU不需要参与PD报文级的交互,只需要通过IIC或者电阻配置告诉芯片“我要哪个档位”,芯片自己就能完成从接收Source Capabilities到发出Request的一整套协议流程。
我最初用这颗芯片的时候有个误区:以为它像一颗普通IIC外设一样,MCU写寄存器就能直接控制VBUS。实际上它的工作模式是“协议处理后映射寄存器”。充电器发来Source Capabilities报文,CH224内部协议引擎先把报文解析成PDO列表,存进内部RAM,再把关键信息映射到IIC寄存器空间。MCU通过IIC去读这些寄存器时,本质上是读芯片已经解析好的“结果”。
所以搞懂Source Capabilities报文结构,跟搞懂CH224A/Q的IIC寄存器,其实是一件事的两面。不懂报文结构,你读回0x0401912C只会一脸懵;懂报文结构,你看到这个值就知道当前充电器在5V/3A档上。
2.2 IIC模式怎么把“报文”变成“寄存器”
CH224A/Q在IIC模式下,芯片作为IIC从机,外部MCU作为主机。芯片会把当前连接状态、PDO数量、每个PDO的原始值、当前请求电压电流、芯片版本等信息映射到一段连续的寄存器地址上。
实际使用中,寄存器空间大致包含这几类:
- 状态类寄存器:当前是否已建立PD通信、是否已获取Source Capabilities、当前VBUS电压状态等
- PDO信息寄存器:PDO数量、每个PDO的完整32bit原始值
- 配置寄存器:目标PDO序号、目标电压、目标电流等
- 只读信息寄存器:芯片版本号等
需要特别提醒的是,不同批次、不同型号的CH224,寄存器偏移地址可能有差异。网上流传的寄存器表不一定适用于你手上的版本,拿到芯片后第一件事就是对着最新数据手册核对寄存器地址,这一步能省掉后面一大半排查时间。
IIC读回的数据格式一般是小端模式,也就是低字节在前。比如某个PDO的32bit值是0x0401912C,读回来四个字节可能是2C、91、01、04。如果你按大端方式拼,解析结果就完全错乱了。后面我会给一段解析代码,重点就是处理这个字节序问题。
2.3 为什么要用IIC,而不是硬件电阻
CH224系列早期很多型号是纯电阻配置,通过CFG引脚接不同阻值来固定请求某个档位。这种方式的优点是简单、不需要MCU参与,上电即用。缺点也很明显:档位是写死的,想要切换电压必须改硬件电阻。
IIC配置模式的价值在于“动态”。你可以让MCU在运行时读取充电器实际支持的PDO,再根据当前任务动态选择档位。比如同一块板子,待机时用5V低功耗,工作时请求20V跑电机,全部可以由软件控制。甚至可以做自动协商:读回所有PDO之后,找最大电压或者最大功率的档位自动请求,不需要人工干预。
另一个使用IIC模式的好处是调试方便。通过IIC寄存器可以读到芯片当前协商状态,判断问题出在PD通信上还是配置上。纯电阻模式下你只能靠万用表量VBUS,排查手段非常有限。
3. 实操:用IIC把Source Capabilities读出来并解析
3.1 硬件连接与IIC基础
先把最小系统讲清楚。CH224A/Q与MCU之间的IIC接口就两根线:SCL和SDA。这两根线必须接上拉电阻,阻值怎么选是有讲究的。IIC总线是开漏结构,上拉电阻太小会增大灌电流,太大则上升沿变慢。我实测下来,3.3V系统、总线长度在10cm以内、只有一两个从机时,4.7k上拉是稳妥选择;如果走线较长或者挂载设备多,建议降到2.2k到3.3k。超过20cm的飞线,优先用2.2k,否则高速模式下可能直接通信失败。
硬件连接注意事项:
- SCL、SDA引线尽量短,避免和VBUS、CC线平行走线
- 上拉电阻的上端接芯片IIC引脚的参考电压,通常是3.3V,不要接5V
- CH224A/Q的CC1、CC2接Type-C母座的对应引脚,注意不要接反
- VBUS输出端建议预留一个100uF以上的电解电容,PD协商过程中电压切换会产生瞬间跌落
3.2 寄存器读取流程与代码框架
IIC读取的流程说穿了并不复杂:先读状态寄存器确认PD通信已经建立,再读PDO数量寄存器,然后按PDO数量依次读取每个PDO的四个字节,最后按位解析。我通常会把这段逻辑封装成独立模块,方便后续工程复用。
下面的代码是典型读取流程,寄存器地址以数据手册为准,示例中用了宏定义方便修改。
// 伪代码框架:具体寄存器地址以CH224A/Q数据手册为准 #define CH224_I2C_ADDR 0x5E // 7位从机地址,实际以手册为准 #define REG_DEV_STATE 0x01 // 设备状态寄存器 #define REG_PDO_NUM 0x02 // PDO数量寄存器 #define REG_PDO_BASE 0x10 // PDO数据起始寄存器 uint32_t pdo[7]; uint8_t pdo_count = 0; uint8_t buf[4]; // 1. 读取状态,确认已经连上PD Source uint8_t state = i2c_read_reg(CH224_I2C_ADDR, REG_DEV_STATE); if ((state & 0x01) == 0) { printf("PD Source not detected\n"); return; } // 2. 读取PDO数量 pdo_count = i2c_read_reg(CH224_I2C_ADDR, REG_PDO_NUM) & 0x07; // 3. 循环读取每个PDO的4字节原始值 for (uint8_t i = 0; i < pdo_count; i++) { i2c_read_bytes(CH224_I2C_ADDR, REG_PDO_BASE + i * 4, buf, 4); // 小端拼接 pdo[i] = (uint32_t)buf[0] | ((uint32_t)buf[1] << 8) | ((uint32_t)buf[2] << 16) | ((uint32_t)buf[3] << 24); }这段代码有几个关键细节。第一个是状态寄存器的位定义,不同版本芯片可能不同,但通常最低位表示“是否检测到PD Source并完成首次握手”。第二个是PDO数量寄存器,实际有效位只有低3位,高5位可能是其他状态,所以要用0x07掩码过滤。第三个是字节序,我在代码注释里已经明确标注小端拼接,如果你发现解析出来的电压数值明显不对,优先检查这一块。
3.3 解析PDO的C代码示例
拿到PDO原始值之后,解析逻辑就很直白了。我按PD规范写了一个解析函数,能识别固定电压PDO和PPS/APDO,并打印出关键信息。
void parse_pdo(uint32_t pdo) { uint8_t pdo_type = (pdo >> 30) & 0x03; if (pdo_type == 0x00) { // Fixed Supply PDO uint16_t voltage_raw = (pdo >> 10) & 0x3FF; uint16_t current_raw = pdo & 0x3FF; float voltage = voltage_raw * 0.05f; float current = current_raw * 0.01f; printf("Fixed PDO: %.1fV / %.2fA\n", voltage, current); } else if (pdo_type == 0x03) { // APDO,常见的是PPS档位 printf("PPS/APDO detected: 0x%08X\n", pdo); } else { printf("Other PDO type: 0x%08X\n", pdo); } }PPS档位解析起来比固定PDO复杂一些,因为它的电压是一个范围而不是固定值,字段包含最大电压、最小电压、最大电流,单位也不同。如果你只是做诱骗固定电压档位,可以先不处理PPS PDO,按顺序跳过即可。但如果你的充电器是纯PPS输出,那就必须完整解析APDO结构才能正确请求电压。
解析结果打印出来之后,你就能看到类似这样的输出:
Fixed PDO: 5.0V / 3.00A Fixed PDO: 9.0V / 3.00A Fixed PDO: 12.0V / 3.00A Fixed PDO: 20.0V / 3.00A看到这串输出,说明IIC链路已经打通,CH224收到的Source Capabilities报文也成功解析出来了。到了这一步,“从IIC数据到快充协议”这条路就走通了一半。
4. 实测波形与报文比对
4.1 用逻辑分析仪抓IIC上的数据
IIC调试最有效的工具就是逻辑分析仪。把SCL和SDA接到逻辑分析仪上,采样率建议设到2MHz以上,然后跑一遍读寄存器流程,就能看到完整的IIC波形。
我习惯先抓一次纯读操作来验证基础通信。正常读操作波形应该是:主机发START,然后发从机地址加写位,收到ACK,再发寄存器地址,收到ACK,然后重启IIC总线,发从机地址加读位,接着读回数据。如果波形在地址阶段就出现NACK,那基本可以断定地址写错了或者芯片没进入IIC模式。
抓到的字节序列要和代码里读到的值做交叉验证。比如读PDO数量时,如果逻辑分析仪上显示读回0x04,那说明充电器发了4个PDO,后面循环读16个字节也是对的。如果数量对但PDO值解析出来不合理,就要检查是不是IIC时钟太快导致数据错位。
IIC通信速度也要注意。很多MCU默认硬件IIC是400kHz快速模式,但某些CH224A/Q的IIC接口对时序要求偏保守,实测400kHz在某些飞线环境下会出现偶发错误。我一般先用100kHz标准模式把功能跑通,确认无误后再尝试提升速度。
4.2 用CC分析仪抓PD报文的典型时序
IIC抓的是芯片和MCU之间的“内部数据”,但真正能验证完整链路的是抓CC线上的PD报文。市面上有专门的USB PD分析仪,比如带PD抓包功能的Type-C协议分析仪,可以解析出完整的报文交互过程。
典型场景下,你会看到这样的时序:
- Source先发Source Capabilities报文,里面带4个PDO
- Sink(也就是CH224)回一个GoodCRC确认收到
- Sink发Request报文,指定要向第几个PDO请求电压和电流
- Source回Accept确认可以执行
- Source回PS_RDY,表示VBUS电压已经稳定到目标值
我之前调试时遇到过一个有意思的现象:CH224请求20V档位,但实测VBUS只有5V。抓CC报文发现Request报文里Object Position填的是3,对应12V档,不是20V档。为什么会这样?查下来发现是PDO排序问题,充电器的20V档在PDO5的位置,而我在配置寄存器里填的是“第3档”,没有先读PDO列表确认顺序。
4.3 两类数据如何对应起来
把IIC读到的寄存器和CC线上的报文放在一起对照,整个链路就非常清晰了。
比如PD分析仪显示Source Capabilities报文有四个PDO,原始值分别是0x0401912C、0x0402D12C、0x0403C12C、0x0406412C,那么IIC读回来的PDO数量寄存器应该是4,四个PDO寄存器组的拼值应该和上面完全一致。如果PD分析仪显示有PPS档位,而IIC读回来的PDO数量比分析仪报文里的Data Object数量少,那要注意是不是芯片固件版本不支持PPS解析,需要看芯片手册确认。
| 调试对象 | 工具 | 关键信息 | 对应关系 |
|---|---|---|---|
| PD物理层 | 逻辑分析仪/PD分析仪 | SOP、Message Header、PDO数组 | 协议原始的“信” |
| PD协议层 | PD分析仪 | Source Capabilities、Request、PS_RDY状态 | 双方交互过程 |
| 芯片内部 | IIC逻辑分析仪 | 状态寄存器、PDO数量、PDO原始值 | 芯片解析后的“结果” |
我强烈建议至少抓一次PD分析仪的完整报文,哪怕只看一次都好。因为只有看到报文层面的完整流程,你才会真正理解CH224A/Q在为你处理多少复杂的协议工作——那些BMC编解码、CRC校验、状态机跳转,全被它一颗芯片消化掉了。
5. 常见问题与排查技巧实录
5.1 IIC读不到数据或读到0xFF
这是最常遇到的问题,基本可以按三个方向排查。
第一个方向是地址错了。CH224A/Q的IIC从机地址在网上有多个版本流传,有人说0x5E,有人说0x7F,还有人说0x54。我在不同批次芯片上确实遇到过地址不一致的情况,最可靠的办法是看芯片丝印和最新数据手册,不要拿网上的老代码直接抄。如果你用的是一个错误地址,回读数据大概率是0xFF或者干脆NACK。
第二个方向是芯片没有进入IIC配置模式。CH224A/Q可能有多种配置方式共存,封装上通常有模式选择引脚或者通过外部电阻决定配置模式。如果芯片工作在电阻模式而非IIC模式,SCL和SDA可能不会响应通信。排查方法是检查模式配置引脚的电平是否和数据手册要求一致。
第三个方向是上电时序问题。PD协商需要一个过程,芯片从插入到拿到Source Capabilities报文需要几百毫秒。如果MCU上电后立刻就去读IIC寄存器,此时PD协商还没完成,状态寄存器显示未就绪,PDO数量可能是0。建议MCU启动后延时几百毫秒再开始读,或者做重试机制。
5.2 请求高电压不生效
明明读到了20V的PDO,配置也写了要20V,但VBUS就是出不来20V,这个问题要分几层看。
第一层,确认你的Request报文里的电流值没有超过PDO的电流能力。很多充电器的20V档只是“标称”支持,实际电流能力可能只有2A甚至1.5A。RDO里面Operating Current和Max Operating Current都是10mA单位,如果填的电流超过PDO最大值,Source可能会Reject请求,甚至直接不给升压。
第二层,有些Source对多档位跳变有限制。直接从5V跳到20V可能失败,但先请求中间档位再跳就能成功。我遇到过某些充电器必须按PDO序号顺序升压,跳档请求会被拒绝。针对这种充电器,可以做一个渐进请求策略:先请求第二档,稳定后再请求目标档。
第三层,检查是否请求到了PPS档位。PPS档位的Request格式和固定PDO不一样,需要填目标电压和电流,而不是填PDO序号。如果你把PPS PDO当成固定PDO来请求,配置可能完全不生效。
5.3 新手最容易忽略的坑
第一个坑是PDO序号和电压值搞混。很多人以为“第4档”就是20V,其实PDO序号是充电器自己定义的,顺序没有统一标准。有的充电器把5V、9V、PPS放前面,20V放最后;有的充电器把所有固定电压放完才放PPS。正确做法永远是先读PDO列表,再从列表里找目标电压对应的序号,绝不要凭经验硬编码。
第二个坑是电流配置存的是A还是mA。CH224相关例程里,电流寄存器有两种单位混用的情况,有的需要写实际安培值的整数,有的需要写10mA单位的原始值。写错一个数量级,请求就会被Source拒绝。我建议在代码里统一用原始值运算,注释里标明单位,避免不同版本芯片之间做移植时踩坑。
第三个坑是CC线质量问题。PD协商对CC线上的信号完整性有一定要求,用了劣质Type-C线或者转接头,可能出现协商失败或者频繁断开。调试阶段尽量用短而粗的线,排除物理层问题后再去查代码。
第四个坑是供电端不兼容。市面上的“PD充电器”鱼龙混杂,有些只是第3档固定输出,根本不是标准PD,有些虽然支持PD但实现有缺陷。测芯片、测代码都排除了问题,还是协商不成功,那就换个充电器试试。我一般手边常备三四个不同品牌的充电器做兼容性测试。
最后分享一个小技巧。调试CH224A/Q时,不要一上来就写完整业务代码,先把一个最简的IIC读取例程跑通,把状态寄存器、PDO数量和每个PDO的原始值都打印出来。确认这些基础信息全部正确,再去写“请求第几档”“动态切压”这些业务逻辑。这样出了问题,你能快速定位是协议层的问题还是应用层的问题,而不是在一个500行的工程里漫无目的地加打印信息。我每次换芯片型号、换充电器、改硬件板子,都会重复一遍这个过程,看起来多花十分钟,实际上省掉的是半天起步的排错时间。