
简介《CCG Host Processor Interface 001-97863-0P-V Apr 2021》是面向USB Type-C、PD及雷电相关端口控制器开发者的技术接口文档适合硬件工程师、嵌入式软件工程师与系统设计者查阅。内容围绕CCG系列主机处理器接口HPI讲解I2C读写、INTR GPIO、命令—响应协议、寄存器访问与PD消息处理并区分CCG1/CCG2的HPIv1和CCG3/CCG4/CCG5/CCG6DF/CCG6SF的HPIv2实现同时涉及传统启动与双固件模式。压缩包仅含1个PDF文件大小约3.12MB便于按目录检索HPI传输、协议及闪存架构等章节。已有200人学习可帮助读者理解主机处理器或EC与CCG控制器间的通信机制为Type-C/PD功能调试、接口移植和系统策略设计提供参考。1. 从一块 PD 端口控制器说起为什么主机侧要单独定义一套 HPI做 Type-C PD 端口方案的人大多撞过同一个坎PD 控制器自己能把协议跑完但「这个口给多少电、什么时候断电、插进来的设备要不要切到 Alternate Mode」这类系统级策略必须由 EC 或主机处理器拍板。CCG 主机处理器接口HPIHost Processor Interface就是为这件事定下的一套寄存器级约定——主机用 I2C 下发命令、回读状态、接收异步事件把 PD 控制器当成一颗可编程外设来驱动。一份 273 页的接口文档把传输层、命令响应协议、寄存器地图、PD 消息处理、固件更新、EC 与 Billboard 的交互全部铺开写说明它对接的从来不是单颗芯片而是整条系统链路。它适合正在做多口笔记本、扩展坞、桌面主机电源域设计的固件工程师同样适合要交一份完整 Type-C PD 接口方案的毕设与课设——因为这份材料本身就是一份现成的分层设计范本。2. HPI 总体架构I2C 传输层、命令响应协议与中断 GPIO 的职责划分HPI 在文档里被拆成传输Transport、协议Protocol、接口Interfaces、操作Operations四大块落到代码上其实是三层最底下是 I2C 总线加一根 INTR 中断线中间是「命令—响应 异步事件」两条信息流最上面才是设备信息寄存器、PD 策略寄存器和数据存储器这些语义层。这三层分开写直接决定了同一套主机侧代码能不能从 CCG1 一直复用到 CCG6SF因为器件换代时变的往往只是寄存器内容和事件码传输层和事务模型基本不动。很多人一上手就把 HPI 写成「读寄存器—判断—再写寄存器」的裸循环短时间能跑接上第二个端口、加上固件升级和休眠唤醒就开始翻车。根本原因是忽略了一个事实HPI 是一条主从总线主机侧任何一次阻塞式轮询都会占住总线而器件侧的事件是随时可能来的。所以架构设计的第一条原则是——所有对器件的访问都必须收敛到同一个事务函数里业务代码不许直接碰 I2C。2.1 一帧读写怎么走I2C 写、重复起始读与 INTR 握手一次完整的 HPI 事务由三段组成。第一段是写起始条件、从地址加写位、寄存器偏移、数据字节、停止条件主机用这一帧把命令和参数塞进器件。第二段是等器件处理完之后拉低 INTR 引脚告诉主机「有结果了」。第三段是读起始、从地址加写位、寄存器偏移、重复起始Repeated Start、从地址加读位、读回数据、主机回 NAK、停止条件。重复起始这一步是关键如果中间插了停止条件再重新起始某些主机控制器会在两次传输之间释放总线被同一总线上的其他设备插队读回来的就是别人的数据。HPIv1CCG1/CCG2和 HPIv2CCG3/CCG4/CCG5/CCG6/CCG6DF/CCG6SF在帧结构上有差异最典型的是寄存器寻址宽度和读操作的偏移阶段处理不一样所以传输层一定要做成可替换的适配层别把帧格式硬编码进业务逻辑。/* hpi_transport.c —— HPI 传输层封装字段名按你手上器件手册对齐 */ #include stdint.h #define HPI_I2C_ADDR 0x08 /* 7 位从地址由硬件引脚或配置表决定不是固定值 */ #define REG_RESPONSE 0x00 /* 响应寄存器命令执行结果的统一回读入口 */ typedef enum { HPI_OK 0x00, HPI_ERR_CMD 0x01, /* 命令不支持或参数越界 */ HPI_ERR_BUSY 0x02 /* 上一条命令还没处理完 */ } hpi_status_t; int hpi_write_reg(uint16_t reg, const uint8_t *buf, uint8_t len); int hpi_read_reg(uint16_t reg, uint8_t *buf, uint8_t len); int gpio_wait_low(int pin, uint32_t timeout_ms); /* 带中断的完整事务写命令 - 等 INTR - 读响应 */ hpi_status_t hpi_cmd(uint16_t cmd_reg, const uint8_t *req, uint8_t req_len, uint8_t *resp, uint8_t resp_len, uint32_t timeout_ms) { if (hpi_write_reg(cmd_reg, req, req_len) ! 0) { return HPI_ERR_CMD; } /* INTR 通常低电平有效。等不到就直接判失败 不要退化成轮询响应寄存器——轮询会把总线占死事件也一起丢了 */ if (gpio_wait_low(HPI_INTR_PIN, timeout_ms) ! 0) { return HPI_ERR_BUSY; } uint8_t status 0; hpi_read_reg(REG_RESPONSE, status, 1); return (hpi_status_t)status; }这段代码里有两个参数值得单独说。HPI_I2C_ADDR不是写死的常量文档里专门有一节讲从地址配置实际产品中它往往由板上的地址选择引脚或者固件配置表决定同一块板子上挂了 PD 控制器和 Billboard 设备时两个器件的地址必须错开。timeout_ms也不能随便给个 10ms 了事HPI 的响应时间在手册里是有明确章节约束的不同命令的处理耗时差别很大固件擦写类命令动辄几百毫秒给短了必然误判成总线故障。2.2 命令-响应模型与异步事件通道两条独立的信息流HPI 协议层最容易被误解的地方是很多人把它当成一条单一的请求响应通道。实际上器件往主机方向送的信息有两条完全独立的流一条是命令响应主机写命令、器件在 RESPONSE 或 PD_RESPONSE 寄存器里回码是被动的另一条是异步事件器件检测到插拔、合约变化、Alternate Mode 状态更新、Billboard 连接这些情况时主动拉 INTR把事件码和事件数据放进事件寄存器是主动的。这两条流混在一起处理就会出现经典的丢事件现象主机发完一条读版本的命令等 INTR 拉低结果这次 INTR 其实是端口刚插上触发的合约事件主机却按命令响应的格式去解析读出来一堆看似合法的垃圾数据然后合约事件就这么丢了。正确的做法是在中断服务里先读 INTR_REG 判断中断源是事件就走事件分发是命令完成才唤醒等待中的事务。事件码本身是有分类的合约信息、电源角色交换状态、DisplayPort 状态更新、VBUS 控制事件、Billboard 连接事件、Alternate Mode 事件各有各的数据结构长度和字段都不一样解析时不能共用一个定长缓冲区。文档里把事件码和事件结构单独列了表这部分建议直接照抄成一张表驱动的解析器事件码做索引每项挂一个「长度 字段偏移 回调」的结构体新增事件只要加一行表项比一长串 switch-case 好维护得多。2.3 HPIv1 与 HPIv2 的差异对照从 CCG1/2 到 CCG3~CCG6 的迁移成本选型阶段最容易犯的错是拿一份旧项目的 HPIv1 代码直接往新器件上套编译过了就以为能跑。两代接口在传输细节、事件上报、固件更新路径上的差别足以让联调卡好几天。下面这张表是我按文档梳理的对照实际迁移时可以当 checklist 用。对比项HPIv1CCG1/CCG2HPIv2CCG3/CCG4/CCG5/CCG6/CCG6DF/CCG6SF寄存器寻址偏移宽度较窄部分操作依赖固定位置寻址空间更大设备信息区与 PD 策略区分区更清晰数据存储器读写有独立的读写实现路径路径统一读写时序更规整异步事件上报事件集合较少结构体字段精简事件种类多含 Alternate Mode 硬件事件、Billboard 事件Alternate Mode 控制以 DisplayPort Alt Mode 为主控制方式偏静态引入统一的 Alternate Mode 处理流程支持动态配置固件更新传统启动模式为主支持双固件模式带校验与回滚路径典型迁移工作量——传输层重写 事件解析器重写业务层可保留这张表最有价值的一行其实是最后一行业务层能保留说明前面强调的分层是值得的。如果当初把 I2C 帧格式和事件解析写进了端口管理逻辑里换一代器件就等于重做一遍项目。3. 寄存器地图设备信息区、PD 策略区与数据存储器怎么切分HPI 的寄存器空间粗略分三块设备信息寄存器负责回答「你是谁、跑的是什么固件、现在什么状态」PD 策略与状态寄存器负责回答「这个口该怎么供电、现在谈到哪一步了」数据存储器和 Flash 读写区负责承载配置表、固件镜像这类大块数据。三块的访问频率、访问时机、出错后果完全不同混着写会让调试变得极其难受。我给这套东西定的规矩是设备信息区只在初始化阶段和异常诊断时读PD 策略区在运行期高频访问但必须走事务封装数据存储器只在配置变更和固件更新时碰。按这个频率梯度去设计主机侧的实时性压力会小很多。3.1 设备信息寄存器从芯片标识到固件版本的一次性摸底上电之后第一件事不是配端口是摸底。文档里这一组寄存器至少包含设备模式当前跑的是应用固件还是 bootloader、启动模式原因冷启动、看门狗复位、固件校验失败、芯片标识、bootloader 最后一行地址、中断寄存器、跳转 boot、复位、进入烧录模式、固件校验、Flash 行读写、全部版本号、第二份固件版本、固件二进制位置、端口使能、休眠控制、电源状态、应用优先级、客户信息、电池状态、看门狗复位计数、配置表版本、HPI 版本、厂商自定义寄存器。这里面有几个是诊断金矿。启动模式原因能直接区分「正常上电」和「固件校验失败后回退」客户现场反馈「设备偶尔不认口」往往就是这一位在提示固件校验没过。看门狗复位计数则是判断器件是否在反复重启的唯一线索没有它你只能看着日志猜。版本号寄存器通常分成基础版本、应用固件版本、第二份固件版本三段格式各不相同解析时要按各自的位域拆别当成一个 32 位数直接打印。配置表版本这个寄存器值得单独提一句。PD 的行为很大程度上由配置表决定端口数量、支持的电压档、电流上限、Alternate Mode 开关都在里面。产品出厂后现场换了配置表却没更新版本号会出现「配置文件明明改了、行为没变」的玄学问题查半天最后发现是版本号没动主机侧缓存了旧配置。3.2 PD 策略与状态寄存器PD_RESPONSE、PDO/RDO 与端口使能这一区是运行期的主战场。PD_RESPONSE 是 PD 相关命令的统一回码入口和通用 RESPONSE 分开读的时候别搞混。策略寄存器覆盖 Source PDO、Sink PDO、端口配置、电源角色、数据角色这些内容状态寄存器则反映当前协商结果。设计上有个细节容易漏PDO 不是写进去就立刻生效的。写完 PDO 之后需要触发一次端口配置变更器件才会用新参数重新发起或响应协商。如果只写 PDO 不触发变更表现出来就是「寄存器读回来是对的实际供电还是老样子」这种问题靠读寄存器永远查不出来必须结合协商过程的抓包才能定位。另外PD 策略寄存器在双端口或多端口器件上是分端口编址的。写的时候端口索引算错会出现「配了 A 口B 口行为变了」的诡异现象。我的习惯是把端口索引做成编译期常量加运行期断言端口号越界直接 assert别等到现场才发现。3.3 数据存储器与 Flash 行读写大块数据的搬运通道配置表、固件镜像、厂商自定义数据都走这条通道。Flash 是按行组织的读写的最小单位就是行不能按字节随便写。文档里给了 Flash 行读写寄存器和固件二进制位置寄存器配合进入烧录模式、固件校验这几个控制寄存器构成完整的写入流程。这条通道的使用要点是「先读后写、写完必校验」。写入前把目标行的原内容读出来备份写入后立刻回读比对不一致就重试一次两次都失败就上报错误而不是继续往下写。听起来啰嗦但固件更新失败导致设备变砖的代价远高于多花的那几百毫秒。数据存储器还有一类用途是放运行时参数比如用户的端口偏好、厂商自定义的功率策略。这类数据要注意掉电保持和写入次数限制别把高频变化的状态往 Flash 里写那是在拿寿命换方便。4. 三条主流程的落地实现初始化、固件更新、合约协商文档第五章把操作类内容归成五块设备复位与启动、固件更新、PD 合约协商、PD 消息传输、Alternate Mode 控制外加响应时间约束。真正落到项目里能决定成败的是前三条主流程因为它们串起了绝大多数寄存器和事件。写这三条流程时我有个固定套路先把时序画成「主机动作 / 器件动作 / 等待条件」三列再照着写代码。凡是画不出等待条件的步骤说明这一步还没想清楚先别写。4.1 识别启动模式并完成 HPI 初始化初始化顺序不是随便排的它取决于器件的启动模式。传统启动模式下器件直接跑应用固件双固件模式下有两份固件镜像器件会根据校验结果决定从哪一份启动。所以第一步永远是读设备模式和启动模式原因而不是急着配端口。/* hpi_init.c —— 主机侧上电初始化顺序 */ hpi_status_t hpi_bringup(uint8_t port_mask) { uint8_t mode 0, reason 0, ver[6] {0}; /* 1. 先确认器件跑在哪应用固件还是 bootloader */ if (hpi_read_reg(REG_DEVICE_MODE, mode, 1) ! 0) { return HPI_ERR_IO; } if (mode MODE_BOOTLOADER) { /* 停在 bootloader说明应用固件没起来走恢复流程 */ return hpi_recover_from_bootloader(); } /* 2. 读启动原因区分冷启动 / 看门狗复位 / 固件校验失败 */ hpi_read_reg(REG_BOOT_MODE_REASON, reason, 1); /* 3. 读全部版本号写进系统日志后面排查全靠它 */ hpi_read_reg(REG_READ_ALL_VERSION, ver, sizeof(ver)); /* 4. 使能需要工作的 PD 端口 */ hpi_write_reg(REG_PDPORT_ENABLE, port_mask, 1); /* 5. 最后开中断前面几步用同步读完成中断一开就进入事件驱动 */ hpi_clear_intr(); gpio_enable_irq(HPI_INTR_PIN, EDGE_FALLING); return HPI_OK; }顺序上的讲究在于最后两步。中断必须在所有同步配置完成之后才打开否则配置过程中的事件会提前涌进来而此时事件队列还没初始化。port_mask这个参数按位对应物理端口多口器件上少写一位就是少一个口工作这类 bug 在单口开发板上永远暴露不出来一到多口样机就原形毕露。初始化里还应该包含一次配置表版本比对把器件里的配置表版本和主机侧固件期望的版本对一下不一致就记一条告警。这条告警在量产阶段能省下大量返修沟通成本。4.2 双固件模式下的固件更新与回滚固件更新是整套接口里风险最高的一条流程因为它一旦中断设备可能再也起不来。文档给了 Flash 存储映射、固件更新流程以及双固件模式下的两份镜像管理。流程大致是进入烧录模式、按行写镜像、写完后做固件校验、复位、确认新版本生效。关键在于校验和回滚这两步不能省。写完镜像不校验等于把「写得对不对」这件事交给了运气双固件模式存在的意义就是留一颗后悔药——新固件校验失败时器件能回到另一份可用镜像继续工作主机侧至少还能通过 HPI 把设备救回来。实践中的做法是更新前先记录当前两份镜像的版本和校验状态更新过程每一步都写日志写到哪一行、耗时多少、返回码是什么更新后读回版本号比对。日志要能落到主机侧存储上不能只留在内存里因为设备复位后内存就没了。我见过不止一次「升级失败但没有任何记录」的情况最后只能靠重新烧录器解决。还有一个坑是更新期间的端口行为。固件更新过程中 PD 端口通常会掉电或进入不可用状态如果这台设备正在给外部供电更新就等于给负载断了一次电。产品化方案里一般会加前置判断检测到端口正在带载时要么拒绝更新要么先通知上层做电源切换。4.3 Source/Sink PDO 更新与端口配置切换PD 合约协商的本质是双方交换能力集然后挑一个共同支持的最高档。主机侧要做的就是按系统策略更新 Source PDO 或 Sink PDO再触发端口配置变更让器件用新能力重新谈。Source 侧更新 PDO 时要注意顺序电压档一般按从低到高排列第一个 PDO 是默认档器件在没谈成之前会先给这一档。如果第一个 PDO 写了个很高的电压插入一个只支持 5V 的设备协商前的短暂窗口里可能出现不受控的输出。这个细节手册里不会大书特书但它是硬件同事最关心的一条。Sink 侧更新则涉及电流上限和 PPS 档位。PPS 档位的步进和电压范围必须严格落在器件支持区间内超范围写入可能被静默忽略表现出来就是「配了 PPS 但设备仍按固定档充电」。端口配置变更这一步我一般做成一个独立的「提交」动作业务代码只改内存里的策略结构体最后由一个 commit 函数统一写寄存器并触发变更。这样避免了「改一半被打断」导致器件处于半新半旧配置的状态调试时也只需要在一个地方打断点看全部策略。5. 常见问题排查I2C 异常、事件丢失与合约谈不成的坑联调阶段的问题有很强的共性把这几类搞明白能省掉大半夜的抓瞎。下面五条是我在不同项目里反复遇到的按「现象 → 原因 → 解决」的方式写遇到同类问题时可以直接对照。第一条I2C 偶发 NAK重试一两次就通。现象是通信大部分时间正常每隔几十上百次出现一次 NAK重试后恢复随时间推移频率上升。原因通常有三个上拉电阻阻值偏大导致上升沿变缓、总线速率超过器件在该电压下的支持上限、以及多设备共享总线时地址冲突。解决路径是先量波形看上升沿有没有超过规范再把速率降一档验证如果降速后消失就是时序余量问题最后核对同一总线上的所有从地址特别是 PD 控制器和 Billboard 设备共存的情况两个器件的地址选择引脚很容易被硬件同事配成同一个。地址冲突的表现还有一种变体读回来的数据看起来合法但总是不对因为读到的是另一个设备的寄存器。第二条事件时有时无插拔几次后状态就乱了。现象是端口事件偶尔丢失设备管理器里端口状态和实际不符重启主机后恢复。原因一般不是器件没发事件而是主机侧的 INTR 处理写错了用了边沿触发却没在中断服务里把中断状态寄存器读干净第二个边沿来的时候引脚已经是低电平触发不了或者用了电平触发但忘了清中断退出中断后立刻又进去CPU 被中断风暴打满。解决办法是「边沿触发 进中断先读中断状态寄存器 按状态位分发」同时给事件解析加一个队列中断服务只做入队解析放到任务上下文里做避免在中断里做长时间 I2C 访问。第三条PD 合约谈不成或者谈成了电流不对。现象是插入设备后反复重试协商最终停在一个较低的电压档或者协商成功但实际输出电流低于预期。原因之一是 PDO 顺序或档位配置不符合预期器件按第一个 PDO 做默认输出原因之二是写完 PDO 后没有触发端口配置变更寄存器看着改对了实际生效的还是旧配置原因之三是端口使能寄存器里对应的位没打开器件根本没参与协商。排查顺序建议反过来先读端口使能再读 PDO 内容最后确认变更触发。抓包工具在这里比寄存器有用得多能直接看到协商报文里双方交换了哪些能力。第四条固件升级后设备变砖或者又跑回旧版本。现象是升级流程走完、复位后版本号没变或者器件干脆不响应。原因通常是没走固件校验这一步器件认为新镜像无效自动回到旧镜像或者写入了不该写的保留行把 bootloader 区域覆盖了或者双固件模式下两份镜像都被写坏没有可回退的备份。解决办法是把升级流程做成严格的状态机进入烧录模式、写镜像、校验、复位、回读版本每一步失败都要中止并保留现场升级前强制读取并记录另一份镜像的版本和校验状态确保至少有一份可用。第五条休眠唤醒之后端口全部失效。现象是系统睡眠再唤醒Type-C 端口不认设备重新插拔也没用只能重启主机。原因是休眠前没有按休眠控制寄存器的约定通知器件或者唤醒后主机侧没有重新走一遍初始化。器件的电源状态寄存器在唤醒后可能是异常的中间态需要主机显式清一次再重新配端口。做法是在系统电源管理回调里挂一对钩子休眠前保存端口状态并下发休眠命令唤醒后重新读设备模式、清中断、重新使能端口。这对钩子要能被单独测试不能只有在整机休眠场景下才触发否则回归测试根本覆盖不到。这五条有个共同的排查思路凡是「偶发」「重启后恢复」类的问题先怀疑时序和状态机别怀疑芯片。器件侧的固件是验证过的主机侧的状态机是自己写的概率分布完全不对等。6. 验收与答辩用寄存器快照和时序波形把链路证明清楚方案交上去之前得能回答一个很朴素的问题你怎么证明这条链路是通的光有一份能跑的代码不够评审和答辩场景下对方要的是可复核的证据。我一般会准备一份验收材料清单按下面的结构组织。验收项交付物通过标准链路连通性I2C 读写波形截图重复起始完整无中途停止ACK 正常器件识别设备模式、芯片标识、全部版本号快照与应用固件版本、配置表版本一致事件通道插拔与合约变更的事件日志事件码、事件数据与实际动作一一对应无丢失合约协商协商过程抓包 PDO/RDO 记录协商档位与策略表一致无重试风暴固件更新升级前后版本比对 更新过程日志版本变化正确校验通过可回滚异常恢复注入断电、注入 NAK 后的恢复记录能自恢复或给出明确错误码不静默失败寄存器快照这件事别手工复制粘贴。写个小脚本把关键寄存器批量 dump 成结构化文本前后两次快照做 diff改动一目了然。# hpi_snapshot.py —— 批量 dump 关键寄存器并做前后对比 KEY_REGS { DEVICE_MODE: 0x10, BOOT_MODE_REASON: 0x11, READ_ALL_VERSION: 0x20, CFG_TABLE_VERS: 0x21, PDPORT_ENABLE: 0x30, POWER_STAT: 0x31, } def snapshot(bus, addr0x08): data {} for name, reg in KEY_REGS.items(): # 每个寄存器读完立刻转成十六进制字符串 # 避免后续 diff 时被 int/bytes 类型差异干扰 data[name] bus.read_i2c_block_data(addr, reg, 4).hex() return data脚本本身没什么技术含量价值在于它把「凭记忆描述现象」变成了「两份可以 diff 的快照」。评审时把升级前、升级后、异常注入后三份快照并排一放比讲十分钟流程有说服力。答辩环节最常被追问的三个问题也提前准备好答案第一为什么用轮询不用中断——如果你的方案里真有轮询得说清楚哪些场景允许、超时怎么兜底第二事件丢了怎么办——要能说出手上有没有事件序号或计数寄存器可以做丢包检测第三固件升级失败怎么救——双固件模式的镜像管理和校验流程就是标准答案。我从踩过一次「升级失败没有任何日志」的坑之后每次做带固件更新的方案都强制走一遍「先记录再动手」的流程动手前把两份镜像的版本和校验状态 dump 出来存盘更新中每一步落日志更新后再 dump 一次做比对。这套动作多花不了几分钟但它决定了出问题时你是在排查还是在猜。希望帮到你。本文还有配套的精品资源点击获取