简介:这份文档是英飞凌(原赛普拉斯)CCG USB Type-C与USB PD控制器主机处理器接口(HPI)的官方技术规范,文档编号001-97863 Rev.*P,2021年4月发布,面向从事Type-C与PD端口控制器开发的硬件工程师、嵌入式软件工程师及系统设计者。内容系统讲解HPI传输机制、命令—响应协议、寄存器定义与PD消息处理,并区分HPIv1(CCG1/CCG2)与HPIv2(CCG3/CCG4/CCG5/CCG6/CCG6DF/CCG6SF)的实现差异,同时覆盖闪存架构中的传统启动模式与双固件模式、I2C读写时序、INTR GPIO、从机地址配置及EC与Billboard设备共享I2C访问等关键设计点。资源为单个PDF文件,压缩包约3.12MB,共273页,目录层级清晰,便于按章节检索。目前已有196人学习下载,适合需要深入理解CCG器件与主机处理器通信机制、排查PD协商与固件升级问题的开发者参考。
1. 从一份 2021 年 4 月的接口手册说起:CCG Host Processor Interface 到底在解决什么问题
如果你手头有一块带 CCG 系列控制器的板子,主控和它之间那根线到底怎么接、寄存器怎么读写、上电时序怎么卡,答案基本都藏在一份叫 Host Processor Interface 的文档里。标题里的 001-97863-0P-V 是文档编号,Apr 2021 是版本日期,说明这是一份 2021 年 4 月定稿的接口规范。它不讲芯片内部架构,只讲一件事:外部主处理器怎么跟 CCG 控制器对话。做嵌入式的人拿到这种文档,第一反应不是通读,而是翻到寄存器映射和时序图那几页,因为那里决定了你的驱动能不能跑起来。这篇笔记就按这个思路,把这份接口规范拆成能落地的步骤,从总线选型一路讲到排错。
2. 先搞清楚 CCG Host Processor Interface 的通信模型:I2C、SPI 还是 UART
2.1 三种物理层接口的适用边界
CCG 控制器的 Host Processor Interface 通常不会只支持一种总线。常见做法是同时提供 I2C、SPI 和 UART 三条路径,但它们的定位完全不同。I2C 适合寄存器级读写,速率在 100 kHz 到 1 MHz 之间,走的是标准的 START-地址-寄存器-数据-STOP 流程,主控作为 master,CCG 作为 slave,地址一般由硬件引脚 strapping 决定。SPI 适合需要批量传输固件或事件日志的场景,四线全双工,速率可以到几 MHz 甚至十几 MHz,但片选和时钟极性必须跟文档里的时序图对齐。UART 则多用于调试和低速命令通道,波特率常见 115200 或 921600,帧格式通常是 8N1,但要注意流控引脚是否被复用成 GPIO。
选哪条路,取决于你的主控资源。如果主控的 I2C 总线已经挂了五六个从设备,再挂 CCG 就要算地址冲突和总线电容。如果主控有空闲的 SPI 控制器,而且你不需要频繁读寄存器,SPI 会更省 CPU 中断。我一般会先看主控的 datasheet 里 I2C 的上升沿时间能不能满足 CCG 的时序要求,再决定要不要换 SPI。
2.2 寄存器映射与命令帧格式
Host Processor Interface 的核心是一张寄存器映射表。每个寄存器有固定的偏移地址,比如 0x00 是设备 ID,0x04 是状态寄存器,0x10 开始是命令 FIFO。读写方式取决于总线:I2C 下是先发 7 位从地址加写位,再发 16 位寄存器偏移,然后 repeated START 加读位,最后读数据。SPI 下则是拉低片选,发命令字节(读是 0x03,写是 0x02),再发 16 位偏移,然后跟数据。
命令帧格式在文档里通常用表格给出,字段包括命令码、长度、payload 和校验。校验常见的是 CRC-16 或简单的 checksum。如果你跳过校验直接发命令,短帧可能没事,长帧一旦有位翻转,CCG 可能返回一个错误码,也可能直接不响应。下面这段 Python 代码演示了用 I2C 读一个 16 位寄存器的基本流程,用的是 smbus2 库,实际移植到 MCU 时把 i2c_msg 换成 HAL 层调用即可。
from smbus2 import SMBus, i2c_msg CCG_I2C_ADDR = 0x3A # 7位从地址,具体值看硬件strapping REG_DEVICE_ID = 0x0000 def read_reg16(bus: SMBus, reg_addr: int) -> int: # 先写寄存器偏移,高字节在前 write = i2c_msg.write(CCG_I2C_ADDR, [reg_addr >> 8, reg_addr & 0xFF]) # 再读两个字节 read = i2c_msg.read(CCG_I2C_ADDR, 2) bus.i2c_rdwr(write, read) data = list(read) return (data[0] << 8) | data[1] with SMBus(1) as bus: bus.write_byte = None # 避免误用 dev_id = read_reg16(bus, REG_DEVICE_ID) print(f"Device ID: 0x{dev_id:04X}")这段代码的关键点在于 repeated START。很多 MCU 的 I2C 外设默认在写完之后发 STOP,再重新 START 读,这样 CCG 可能不认。必须配置成 repeated START 模式,或者用底层寄存器手动控制。参数方面,CCG_I2C_ADDR 不是固定的,文档里会写清楚地址由哪几个引脚决定,常见的是 0x3A 或 0x3B。REG_DEVICE_ID 的偏移也要以你手上那份 001-97863-0P-V 为准,不同版本可能有微调。
2.3 中断与事件通知机制
Host Processor Interface 不只是轮询。CCG 通常有一根 INT 引脚,当有事件(比如命令完成、错误、状态变化)时拉低或拉高,具体极性看文档。主控可以配置成边沿触发或电平触发。边沿触发省 CPU,但如果你在中断里处理太慢,可能丢事件。电平触发则要求你在中断服务程序里读状态寄存器并清除中断源,否则会反复进中断。
我一般会在初始化时把 INT 引脚配成下降沿触发,然后在 ISR 里只做一件事:置一个标志位,让主循环去读事件 FIFO。这样避免在中断上下文里做 I2C 读写,因为很多 MCU 的 I2C 在中断里调用会死锁。事件 FIFO 的深度有限,文档里会写最多缓存多少条,超过就会覆盖或丢弃,所以主循环的轮询周期要算好。
3. 用 I2C 在本地跑通 CCG 寄存器读写的最小命令集
3.1 硬件连接与上电时序检查
在写任何代码之前,先拿万用表确认三件事:VDD 电压是不是文档里写的典型值(常见 3.3V 或 1.8V),I2C 上拉电阻是不是接在正确的一侧,INT 引脚有没有被其他电路拉死。CCG 的上电时序通常要求 VDD 先稳定,然后 RESET 引脚保持低电平至少 1 ms,再拉高,之后等待内部初始化完成。文档里会给一个 t_init 参数,常见是 10 ms 到 50 ms。如果你在 t_init 之前就去读寄存器,I2C 可能返回 NACK。
我遇到过一块板子,RESET 引脚被一个 100 nF 电容拖慢,导致上升沿太缓,CCG 一直不启动。后来把电容换成 10 nF 就好了。这种问题看波形最直接,示波器抓 RESET 和 VDD 的上升沿,对比文档里的时序图。
3.2 写一个寄存器读写的最小 Python 脚本
下面这个脚本在上一段的基础上增加了写寄存器和 CRC 校验。注意写寄存器时,I2C 帧是地址+写位、寄存器偏移高字节、低字节、数据高字节、数据低字节。如果 CCG 要求 CRC,还要在末尾追加两个字节的 CRC-16。
import crcmod.predefined crc16 = crcmod.predefined.mkCrcFun('modbus') def write_reg16(bus: SMBus, reg_addr: int, value: int, use_crc: bool = False): payload = [reg_addr >> 8, reg_addr & 0xFF, value >> 8, value & 0xFF] if use_crc: crc = crc16(bytes(payload)) payload += [crc & 0xFF, crc >> 8] msg = i2c_msg.write(CCG_I2C_ADDR, payload) bus.i2c_rdwr(msg) def read_reg16_crc(bus: SMBus, reg_addr: int) -> int: write = i2c_msg.write(CCG_I2C_ADDR, [reg_addr >> 8, reg_addr & 0xFF]) read = i2c_msg.read(CCG_I2C_ADDR, 4) # 数据2字节 + CRC2字节 bus.i2c_rdwr(write, read) data = list(read) recv_crc = data[2] | (data[3] << 8) calc_crc = crc16(bytes(data[:2])) if recv_crc != calc_crc: raise ValueError(f"CRC mismatch: recv {recv_crc:04X}, calc {calc_crc:04X}") return (data[0] << 8) | data[1]CRC 的多项式在文档里会写,常见的是 Modbus CRC-16,多项式 0x8005,初值 0xFFFF。如果你用的库默认不是这个,就要手动指定。参数 use_crc 取决于 CCG 的配置寄存器,有些版本默认关闭 CRC,有些默认开启。最稳妥的办法是先读一个已知寄存器(比如设备 ID),如果返回的数据长度不对或 CRC 报错,就说明 CRC 配置不匹配。
3.3 用逻辑分析仪抓一次完整事务
代码跑通之后,别急着写上层逻辑。拿逻辑分析仪或者带 I2C 解码的示波器,抓一次读设备 ID 的完整波形。重点看几个地方:START 条件是否干净,地址字节的 ACK 位是否被 CCG 拉低,repeated START 有没有出现,数据字节的 ACK 是否正常。如果地址字节没有 ACK,说明从地址错了或者 CCG 没上电。如果数据字节没有 ACK,可能是寄存器偏移越界或者 CCG 正忙。
我习惯把抓到的波形和文档里的时序图并排看,尤其是 t_SU;DAT 和 t_HD;DAT 这两个参数。很多 MCU 的 I2C 默认速率是 100 kHz,但 CCG 可能要求最低 400 kHz 才能及时响应。如果波形上看到时钟被拉低很久,说明 CCG 在 clock stretching,这时候主控必须支持这个特性,否则数据会错。
4. 避坑:CCG Host Processor Interface 调试中最容易翻车的 5 个点
4.1 现象:I2C 读回来全是 0xFF
原因通常有三个:从地址不对、上拉电阻没接、CCG 没退出复位。先量电压,再确认地址引脚的电平。如果地址引脚是浮空的,有些芯片会默认到某个地址,但 CCG 一般要求明确拉高或拉低。解决方法是查文档里的地址表,用镊子把地址引脚短接到 GND 或 VDD,再读一次。
4.2 现象:写寄存器成功但读回来没变化
这多半是因为你写的是命令寄存器,不是配置寄存器。CCG 的某些寄存器是只读的状态位,写进去会被忽略。文档里每个寄存器都有 R/W 属性,写之前先确认。另外,有些配置寄存器需要先写解锁序列,比如先写 0x5A 到某个地址,再写目标寄存器。跳过解锁步骤,写操作看起来成功,实际没生效。
4.3 现象:中断频繁触发,主循环卡死
INT 引脚是电平触发,但你在 ISR 里没有清除中断源。CCG 的中断状态寄存器通常是读清或写清,文档里会写明。如果是读清,你必须在 ISR 里读一次状态寄存器;如果是写清,要写 1 到对应位。漏掉这一步,INT 会一直保持有效,CPU 反复进中断。解决方法是把清除操作放在 ISR 的第一条语句,然后再置标志位。
4.4 现象:SPI 通信时数据错位
SPI 的时钟极性和相位(CPOL/CPHA)必须和文档一致。常见的是 Mode 0(CPOL=0, CPHA=0),但 CCG 可能用 Mode 3。如果你发现读回来的数据整体偏移一位,或者命令码被当成数据,先检查 CPHA。另外,片选信号在帧与帧之间必须拉高至少一个时钟周期,否则 CCG 可能把两帧当成一帧。解决方法是调整 SPI 初始化结构体里的 CLKPolarity 和 CLKPhase,并在两次传输之间加一个小延时。
4.5 现象:固件升级到一半失败,设备变砖
CCG 的固件升级通常走 SPI 或 I2C 的批量写通道,文档里会要求先进入 bootloader 模式,再分块写,每块写完要读回校验。如果你跳过校验,或者块大小超过 FIFO 深度,就会写失败。更严重的是,有些 CCG 在升级失败后会停留在 bootloader,不再加载旧固件。解决办法是严格按照文档里的升级流程,每写一块就读回比对,并且保留一个已知能用的固件备份。如果设备已经变砖,看有没有 recovery 引脚,拉高后重新上电可以强制进入 bootloader。
5. 进阶:用状态机封装 Host Processor Interface 的读写与错误恢复
5.1 把轮询、中断和重试揉进一个状态机
直接在主循环里调 read_reg16 和 write_reg16,短时间没问题,但一旦 CCG 忙或者 I2C 总线被其他设备占用,就会阻塞。更好的做法是写一个非阻塞状态机,把一次事务拆成 IDLE、START、SEND_ADDR、SEND_REG、READ_DATA、CHECK_CRC、DONE、ERROR 几个状态。每个状态只做一件事,超时就跳 ERROR,然后根据错误类型决定重试还是上报。
下面是一个简化的状态机骨架,用 Python 的生成器模拟,实际移植到 C 就是 switch-case。
class CCGTransaction: def __init__(self, bus, addr, reg, is_write, value=0): self.bus = bus self.addr = addr self.reg = reg self.is_write = is_write self.value = value self.state = 'IDLE' self.retry = 0 self.max_retry = 3 def step(self): if self.state == 'IDLE': self.state = 'SEND_REG' elif self.state == 'SEND_REG': try: if self.is_write: write_reg16(self.bus, self.reg, self.value) self.state = 'DONE' else: self.state = 'READ_DATA' except Exception: self.state = 'ERROR' elif self.state == 'READ_DATA': try: self.result = read_reg16(self.bus, self.reg) self.state = 'DONE' except Exception: self.state = 'ERROR' elif self.state == 'ERROR': if self.retry < self.max_retry: self.retry += 1 self.state = 'IDLE' else: self.state = 'FATAL' return self.state这个状态机的关键参数是 max_retry。I2C 总线偶尔会因为电磁干扰丢一个 ACK,重试三次基本能覆盖。但如果连续三次都失败,就要检查硬件,而不是继续重试。另外,每次重试之间最好加一个 1 ms 到 5 ms 的延时,给 CCG 时间恢复。
5.2 用示波器验证重试逻辑是否生效
状态机写完之后,怎么确认重试真的在工作?我一般会人为制造一次错误:在 I2C 总线上并一个 MOS 管,用 GPIO 在特定时刻把 SDA 拉低,模拟总线冲突。然后看状态机是不是从 ERROR 回到 IDLE 再重试,最后成功。如果直接跳到 FATAL,说明重试计数或异常捕获有问题。这种验证方法比看日志可靠,因为日志可能被缓冲,而波形是实时的。
5.3 一个我踩过的坑:状态机里的超时单位
有一次我把超时设成 100,以为是 100 ms,结果代码里用的是 tick 计数,每个 tick 是 10 ms,实际超时变成 1 秒。CCG 在 1 秒内早就返回 NACK 了,但状态机还在等,导致整个系统响应变慢。后来统一用毫秒做单位,并且在初始化时打印一次 tick 频率,确认换算关系。这个习惯我一直保留到现在:任何跟时间相关的参数,先打印实际值,再写进配置。
5.4 验证方法:用已知寄存器做回归测试
每次修改驱动或状态机之后,跑一遍回归测试。测试用例不用多,覆盖三个场景就行:读设备 ID 是否返回预期值,写一个配置寄存器再读回是否一致,连续读 100 次是否出现 CRC 错误。如果这三个都过,基本可以认为 Host Processor Interface 的底层是稳的。回归测试的脚本可以挂在 CI 上,每次提交自动跑,硬件用一块固定的测试板,通过 USB 转 I2C 适配器连接。
我自己的习惯是,在测试脚本里加一行打印,把每次读到的设备 ID 和 CRC 错误计数输出到 CSV,跑完之后用 pandas 画个趋势图。如果 CRC 错误计数随时间上升,说明硬件接触不良或者电源纹波变大,这时候就该查板子了,而不是改代码。希望帮到你。
本文还有配套的精品资源,点击获取