做嵌入式设备的人应该都有同一种感受:Type-C口想真正跑出PD快充,难点从来不在"调一个寄存器"或者"抄一段代码",而是把CC检测、角色识别、电源协商、VBUS控制这一整条链路理清楚。FUSB302这颗芯片的定位很有意思,它把所有Type-C物理层和PD协议物理传输的事情都接了过去,留给主控的只有一个I2C接口和一根中断脚。
这篇东西基于我自己在i.MX与RK平台上用FUSB302做PD Source、PD Sink以及拓展坞供电的实际经历,把从画原理图、写设备树、确认通信、看驱动日志到最终测出9V/1.5A、12V/1.5A这类协商结果的完整路径讲一遍。适合正在做Type-C供电设备、电池充电管理、PD拓展坞,或者单纯想把普通Type-C口升级成可协商快充口的朋友,不管你用的是NXP、瑞芯微,还是全志、STM32MP1,能跑Linux就能参考。
1. 为什么是FUSB302:把CC检测、角色识别和PD物理层一次打包的方案
1.1 PD快充里MCU最头疼的部分其实不是协议栈
很多人第一次接触PD快充,第一反应是去研究PD协议栈怎么写,比如Source Capabilities消息怎么组、Request消息怎么解析、Hard Reset怎么处理。做了一圈之后才意识到,真正烦人的是底层那一堆模拟前端的事情。
CC引脚上要监测两个比较器电压,用于判断设备是DFP(主设备、电源提供方)、UFP(从设备、电源接收方)还是DRP(双角色),不同电压区间对应不同的角色和电流能力。同时,CC线上做BMC信号时要处理定时、脉冲宽度、双相编码、线缆补偿等一堆物理层细节。这些如果用MCU的GPIO和定时器去模拟,调试一次就够喝一壶的。FUSB302的价值就在这里:它内部集成了CC引脚比较器、BMC收发器、VBUS监测、以及一组用于控制外部路径开关的逻辑,主控通过I2C写几个寄存器和读中断状态,就能拿到完整的Type-C/PD底层信息。
对内层面,主控不再需要精确到微秒级去操作GPIO翻转,因为BMC物理层和收发缓冲都在FUSB302里完成。对外层面,CC引脚上该有的上拉/下拉电阻、死电池上拉、VCONN开关,这些也都可以由芯片的内部结构配合少量外部器件完成。你真正要自己做的,是把I2C调通、接好中断线、把VBUS路径上的开关管选对。
1.2 FUSB302在典型系统里的位置
一个以Linux主控为核心的PD快充系统,通常长这样:
- Type-C座子的CC1/CC2接FUSB302的CC1/CC2引脚
- FUSB302通过I2C接主控的I2C控制器
- FUSB302的INTB(中断输出,低有效)接主控的一个GPIO,用于通知主控有事件发生
- VBUS路径上有一个高边开关管(比如负载开关或带路径管理的芯片),开关控制脚由主控GPIO或FUSB302的GPIO输出控制
- VBUS电压检测连接到FUSB302的VBUS感知引脚,让芯片能识别电压状态
这种架构下,FUSB302承担了三个角色:Type-C连接检测器、PD BMC物理层收发器、VBUS信号采集器。Linux内核里的TCPM(Type-C Port Manager)框架则负责上层策略:读到CC电压之后判断设备类型,收到Source Capabilities之后决定要不要申请更高电压,DPM(Device Policy Manager)来决定具体请求9V还是5V还是12V。FUSB302驱动只是TCPM框架的一个底层实现。
2. 硬件连接:从Type-C座子到FUSB302再到主控的实际走线
2.1 引脚速览与最小原理图设计
FUSB302常见封装是QFN14,引脚不多,但每一个都不能大意。我按最小系统列一份:
| 引脚 | 名称 | 作用 | 连接建议 |
|---|---|---|---|
| 1 | VIN | 3.3V供电输入 | 接3.3V,就近放0.1uF退耦电容,另外至少预留4.7uF容值用于瞬态 |
| 2 | VDDIO | IO电源 | 与主控IO电平一致,通常3.3V,如果主控IO是1.8V可以接1.8V |
| 3 | I2C_SCL | I2C时钟 | 接主控I2C SCL,上拉电阻典型2.2k到VDDIO |
| 4 | I2C_SDA | I2C数据 | 接主控I2C SDA,上拉电阻同样2.2k |
| 5 | INTB | 中断输出,低有效,开漏 | 接主控GPIO,配置为输入、下降沿或低电平触发,外部加上拉到VDDIO |
| 6 | VBUS | VBUS电压感知 | 通过电阻分压接VBUS总线,分压比参考数据手册 |
| 7 | CC1 | Type-C CC1 | 直接连Type-C座子CC1,注意串接电阻位置 |
| 8 | CC2 | Type-C CC2 | 直接连Type-C座子CC2 |
| 9 | GND | 地 | 铺地处理,减少回流路径 |
| 10 | GPIO | 可配置GPIO | 可选,常用于控制VBUS开关或LED |
| 11 | GPIO | 可配置GPIO | 可选 |
| 12 | DP | D+/D-数据通道开关控制?——不,FUSB302这块DP/DM不是给USB2.0数据用的,是芯片加强/检测用途,通常悬空 |
这里有一个容易混淆的细节:FUSB302主要负责CC和PD BMC,它不处理USB2.0的D+/D-信号。如果你要做的是带数据传输的Type-C口,D+/D-需要单独走USB2.0线,并配合CC逻辑决定是否切换上下拉。FUSB302只是把DP/DM引脚做了一组通用输入输出,一般应用可以悬空。
2.2 PCB布线与供电要注意的事
CC走线:CC1和CC2是信号质量敏感线路,PD协商时BMC信号大约在300kbps级别,看起来频率不高,但CC引脚上的电压比较结果直接决定角色判断。走线尽量短而直,远离高频开关节点和电感。Type-C座子到FUSB302的CC引脚之间,串接4.7k到10k的电阻是常见做法,用于ESD保护异常电压情况下的限流,具体阻值参考FUSB302数据手册建议。
I2C走线:I2C的SCL/SDA上拉电阻要靠近FUSB302引脚,不要离得太远。如果主控I2C总线上还挂了其它器件,注意总线电容,FUSB302的I2C最高可跑到1MHz,但因为通常和多个从设备共享一条总线,工作频率一般设400k。
电源方面,VIN和VDDIO在大多数设计中直接接同一个3.3V,但要注意VIN的瞬态电流在PD协商切换电压时会明显波动,退耦电容不要省。VBUS感知分压电阻的精度会影响VBUS监测阈值,用1%精度电阻。
提示:FUSB302的INTB是开漏输出,外部上拉电阻一定不能忘。很多板子调试时I2C一切正常,就是中断触发不了或者不响应,十有八九是漏了这颗上拉。
2.3 上电自检:怎么确认硬件是好的
在跑Linux驱动之前,强烈建议先用简单的I2C工具或者最小固件程序做一次硬件自检,把问题隔离在底层。具体操作:
- 上电,用万用表确认FUSB302的VIN和VDDIO电压正常,3.3V别偏差超过5%。
- 示波器看CC1/CC2引脚上的静态电平。此时如果没有插入任何设备,且FUSB302配置为Source模式,CC引脚上应该能看到一个由芯片内部上拉产生的电压,通常接近0.7V到1.2V范围,具体取决于FUSB302内部电流源设置和外部电阻。
- 用I2C工具读取Device ID寄存器(地址0x01),正常返回0x81左右的值,不同批次可能略有差别,但高字节的device family id应该一致。
- INTB引脚在无事件时应该是高电平,触发事件时拉低。
如果你发现CC引脚电压量出来完全为零,而I2C通信正常,先去查FUSB302的开关寄存器(0x02)里MEAS_CC1/MEAS_CC2有没有启用,或者芯片是不是不小心进了Standby模式。这一步能用示波器量出来,比在Linux日志里瞎猜强得多。
3. Linux侧的准备:内核选项、设备树与I2C通信验证
3.1 内核里要打开哪些配置
Linux内核自4.x之后就在drivers/usb/typec/tcpm/下面有完整的FUSB302驱动实现(fusb302.c),基于TCPM框架。这比从头写一个驱动要靠谱得多,因为它把TCPC(Type-C Controller)的抽象层、端口角色管理、PD协议消息转发都做完了。你需要确认内核配置里有:
CONFIG_USB_TYPEC=y CONFIG_USB_TYPEC_TCPM=y CONFIG_USB_TYPEC_TCPCI=y CONFIG_USB_TYPEC_FUSB302=y CONFIG_USB_ROLE_SWITCH=y CONFIG_USB_PD_DP=y有些内核版本把TCPCI和FUSB302分开,FUSB302是独立的config项,别只开TCPCI就把FUSB302漏了。如果你用的是老内核,比如4.9之前,可能需要把TCPM相关补丁backport。实际项目里我遇到过在vendor SDK中发现CONFIG_USB_TYPEC_FUSB302没有打开导致设备树里挂上但驱动加载不上的情况,排查方法很简单,查看内核目录里有没有生成对应的.o文件即可。
如果PD中包含DisplayPort功能,还需要CONFIG_TYPEC_DP_ALTMODE。不过这里只聊纯充电的情况。
3.2 设备树里怎么描述这颗芯片
以i.MX8M系列为例,假设FUSB302挂在i2c2上,GPIO1_IO03作为中断。设备树节点写法一般是:
&i2c2 { clock-frequency = <400000>; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_i2c2>; status = "okay"; fusb302: typec@22 { compatible = "fcs,fusb302"; reg = <0x22>; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_fusb302_int>; interrupt-parent = <&gpio1>; interrupts = <3 IRQ_TYPE_LEVEL_LOW>; fcs,port-type = "source"; fcs,typec-power = "pd"; fcs,max-sink-mv = <20000>; fcs,max-sink-ma = <5000>; fcs,max-sink-mw = <100000>; vbus-supply = <®_vbus>; status = "okay"; }; };几个属性说明:
- reg = <0x22>:FUSB302的7位I2C地址是0x22。这是固定地址,无法通过引脚修改。
- fcs,port-type:可选source、sink、dual。如果做的是拓展坞电源输入口,一般设dual或者sink;如果是给设备供电的口,设source。
- fcs,max-sink-mv/ma/mw:这是Sink模式下允许请求的最大电压/电流/功率。设太高可能会导致策略协商时报错,设太低又浪费了适配器能力,按实际硬件设计能力来。
- interrupts = <3 IRQ_TYPE_LEVEL_LOW>:INTB是低有效,用LEVEL_LOW比EDGE_FALLING更稳妥,可以避免边沿丢失。但要注意如果是电平触发,中断处理函数不能太慢,否则会一直触发。
3.3 用i2c-tools确认芯片与主控通信正常
设备树挂上去之后,先别急着看PD协商。确认I2C通路是最基本的一步。
在Linux终端执行:
i2cdetect -y 2正常情况下,0x22位置会出现UU或者22。出现UU表示该地址已被内核驱动占用,22表示没有驱动占用、但芯片能响应地址探测。如果什么都没显示,优先检查I2C引脚是否错位、上拉电阻是否正确。
如果I2C总线上恰好没有其它设备竞争,也可以直接i2cget读寄存器:
i2cget -y 2 0x22 0x01返回结果应该是一个非0值。这个值如果是0xff,通常说明芯片没有应答或者SDA被拉死;如果是0x81左右,说明Device ID正常。
看驱动是否成功probe:
dmesg | grep -i fusb302能看到类似fusb302 0-0022: fusb302_probe: probe success或者切换到TCPM模式的日志,说明驱动加载成功。
4. 驱动调试:读懂寄存器、中断与PD协商过程
4.1 FUSB302寄存器地图:常用寄存器逐个过
FUSB302寄存器数量不多,但每个都决定了关键行为。我把调试中最常碰到的列出来:
| 地址 | 名称 | 关键位 | 调试用途 |
|---|---|---|---|
| 0x01 | DeviceID | 芯片版本 | 确认I2C通信正常 |
| 0x02 | Switch | MEAS_CC1, MEAS_CC2, VCONN_CC1/CC2 | 切换CC测量通道、VCONN输出 |
| 0x03 | Measure | MDAC值, MEAS_VBUS | 配置比较器阈值、触发VBUS测量 |
| 0x06 | Control0 | HOST_CUR, TX_START, RESET | 主机电流设置、发送启动、芯片复位 |
| 0x07 | Control1 | ENSOPC, ENSOPC_DB, TX_DMA_EN | SOP类型使能,调试PD消息时必须注意 |
| 0x08 | Control2 | TOGGLE, WAKE, MODE | 角色切换使能(DRP模式会自动切换角色检测) |
| 0x0A | Mask | 各中断屏蔽位 | 屏蔽不需要的中断源 |
| 0x0C | Reset | PD_RESET, SW_RESET | 软复位芯片 |
| 0x1C | Status0a | BC_LVL, VBUS | 连接检测状态 |
| 0x1D | Status1a | 当前角色的详细状态 | 判断是source还是sink |
| 0x1E | Interrupta | 中断源 | 调试中断风暴的关键 |
| 0x2A | Fifos | TXFIFO/RXFIFO | 读写PD消息FIFO |
调试中最容易忽略的是Control1寄存器(0x07)中的SOP类型使能位。PD消息分SOP、SOP'、SOP'',普通设备之间的协商用SOP就够了,但如果要支持线缆电子标记查询(E-Marker),需要使能SOP'。如果用默认配置,FUSB302收到SOP''消息时不会正确响应,排查半天往往都在这里。
4.2 中断驱动的上报机制
FUSB302把几乎所有事件都通过INTB脚上报,你只要维护好一个中断线程,在里面读Interrupt寄存器和Status寄存器,然后按事件类型喂给TCPM状态机就行。内核中的fusb302驱动就是这样实现的。实际调试时要关注中断类型:
- BC_LVL_CHANGE:CC引脚电压变化,通常是插入或拔出事件
- TOGDONE:角色检测完成,DRP模式下切换角色时会出现
- VBUS_CHANGE:VBUS电压变化
- RX_SOP:收到PD消息
- TX_SUCCESS / TX_FAILED:发送消息的结果
调试中断风暴问题有个很实用的技巧:在中断处理函数入口读一次Interrupta寄存器,打印二进制值到dmesg,连续插拔几次Type-C线缆,观察中断源的变化是否符合预期。如果某个中断位始终置位且无法清除,大概率是硬件状态没满足,比如CC引脚悬空导致比较器一直在翻转。
4.3 完整PD协商调试日志分析
在PD协商正常的情况下,dmesg里会出现类似这样的关键日志:
usb tcpm_port0: PD RX, header: 0x2b1: 0:1, 0:0 [0] usb tcpm_port0: PD RX, SRC_CAP: 0 caps usb tcpm_port0: PD TX, header: 0x1101: 1:0, 0:0 usb tcpm_port0: PD TX, REQUEST: 5000mV 3000mA usb tcpm_port0: PD RX, header: 0x0501: 1:1, 0:0 usb tcpm_port0: PD RX, ACCEPT usb tcpm_port0: PD RX, header: 0x0203: 1:1, 0:0 usb tcpm_port0: PD RX, PS_RDY usb tcpm_port0: PD TX, GOTOMIN这段日志说明Sink端收到了Source的SrcCap,然后发出了Request请求5V/3A,Source接受后发送PS_RDY。如果卡在SRC_CAP: 0 caps不往下走,说明FUSB302收到了消息但解析不出PDO,要检查Control1的SOP类型使能以及FIFO读取时序。
如果你想做的是Source端(给别的设备供电),日志会有些不同。当Sink接入并被识别后,Source端必须主动发送Source Caps:
usb tcpm_port0: PD TX, header: 0x2b1: 1:0, 0:0 usb tcpm_port0: PD TX, SRC_CAP: 1 caps usb tcpm_port0: PD RX, header: 0x1101: 1:0, 0:0 usb tcpm_port0: PD RX, REQUEST: 5000mV 3000mA usb tcpm_port0: PD TX, ACCEPT usb tcpm_port0: PD TX, PS_RDY如果Source端始终不发送SRC_CAP,先确认端口策略里有没有把fcs,port-type设为source或dual,再看CC引脚比较器有没有识别到Sink的Rp/Rd状态。
调试PD协商最有效的工具是逻辑分析仪,最便宜的那种采样率就够,接FUSB302的CC1线,用BMC解码器(或者直接看波形特征)确认消息内容和日志是否一致。我印象最深刻的一次,日志显示Source已发出PS_RDY,但充电器仍然不给电,逻辑分析仪一抓才发现PS_RDY的BMC脉冲宽度略有偏差,导致对端不认为这是一个合法消息——这种问题只能靠波形去找。
4.4 通过sysfs验证输出能力
Linux的TCPM框架会把Type-C端口信息暴露在sysfs,这是验证驱动工作的最直接途径:
ls /sys/class/typec/port0/ cat /sys/class/typec/port0/data_role cat /sys/class/typec/port0/power_role cat /sys/class/typec/port0/port_type插入PD充电器后,会多出一个partner节点:
cat /sys/class/typec/port0/partner/supply/0/current_now cat /sys/class/typec/port0/partner/supply/0/voltage_now这两个值显示当前协商到的电压和电流,与PDO中的值对应。如果能看到voltage_now变成9000mV,说明9V档位协商成功。有些平台需要CONFIG_TYPEC_DP_ALTMODE或具体平台的供电策略配合,sysfs才完整。
如果电源协商成功但实际电流上不去,问题往往不在FUSB302,而在更高层。比如平台PMIC里的充电策略限制、电源路径开关管的导通电阻过大、或者VBUS走线太细。FUSB302只能把协商结果放到寄存器里,真正的电流通路是你自己的硬件。
5. 调板过程中反复踩到的坑
5.1 I2C读写正常但中断不触发,或者中断风暴无休止
这是一个非常典型的FUSB302问题。I2C通信正常说明芯片活着,但插拔线缆时INTB引脚没有变化,排查方向:
- 检查INTB外部上拉。没有上拉,引脚浮空,中断状态无法被主控正确识别。
- 检查中断配置是否用了下降沿触发。FUSB302的INTB在事件发生时拉低,事件没被清除前一直为低。如果配置成边沿触发,驱动里只清了一次中断,下次事件就会被漏掉。建议配置成LEVEL_LOW,并在中断线程里循环读到中断寄存器为空为止。
- 中断风暴的时候,往往是Mask寄存器(0x0A)没有屏蔽不需要的中断源,比如CC电压毛刺触发BC_LVL_CHANGE,或者VBUS抖动触发VBUS_CHANGE。调试时先把Mask全部置1,只放开当前关心的中断源,然后一个一个加。
- 最关键的一点:每完成一个事件处理,必须把引起中断的Status位清掉。FUSB302的中断源在Status/Interrupt寄存器中,读取之后写特定值清除,有些驱动实现如果不读完全部事件,芯片会一直拉低INTB。
5.2 CC引脚电压不对导致角色协商失败
CC引脚上的电压是整个Type-C连接检测的根本。FUSB302内部有比较器,但外部电阻网络会影响分压结果。常见故障现象是:接线正确、I2C正常、但TCPM日志显示CC1: 0, CC2: 0,也就是检测不到任何连接。
排查步骤:
- 用万用表量Type-C座子CC1与GND之间的电压。Source模式未连接时应该有上拉电压,通常在0.7V~1.2V之间,具体取决于Rp设定。如果量到0V,检查FUSB302的MEAS_CC1对应的内部上拉有没有打开。
- 插入一个标准的UFP设备(比如带下拉电阻的转接板),CC1应该被拉到0.2V~0.6V区间,对应Rd下拉。FUSB302的比较器就能判定为UFP。
- 如果插入后电压区间仍然偏高,可能是外部串联电阻太大,改变了分压比。我之前用过一颗10k串联电阻,结果Rp和Rd分压后正好落在两个比较器阈值之间,FUSB302判定结果反复横跳。换成数据手册推荐的阻值就正常了。
注意:Type-C规范里Rp和Rd的电压区间和电流能力是强相关的。Source的Rp对应的默认电流能力有USB 2.0 500mA、1.5A、3A三个档位,由Rp电阻值决定。FUSB302在source模式下是根据你配置的Control3寄存器的电流能力来设定内部Rp的,别外部再并一路电阻,否则会改变电压区间。
5.3 PD协商失败时先别急着怀疑驱动源码
PD协商失败,日志里出现各种错误,很多人第一反应是驱动有问题,实际上硬件层面的坑远多于软件。我的排查顺序是:
- 确认电压域。PD协商时VBUS可能先从5V升到9V,或者先降到0V再重新拉起。用示波器观察VBUS波形,看切换波形是否陡峭、有没有明显过冲。
- 确认VBUS放电。PD规范要求电压切换前要放电。如果硬件上没有放电回路,或者放电时间太短,对端会认为你是非法切换,直接拒绝后续协商。FUSB302没啥好办法,这是外部电路设计问题。
- 抓RX FIFO。如果收到消息但解析出来的header永远不对,检查I2C时钟频率是不是太高。FUSB302的I2C最高支持1MHz,但布线比较长或者有其他负载时,400k更安全。一次调试中,我把I2C频率从400k改到1MHz后,RX FIFO几乎全是乱码,降到400k就好了。
- 对照PD规范看日志里的state。TCPM框架状态机里有明确的状态迁移,比如SNK_READY、SRC_READY、HARD_RESET等。如果卡在某个状态,先查对应状态的入口条件是否满足。
5.4 拓展坞场景下D+/D-和CC的关系
既然搜热词里出现了"pd快充拓展坞",就多提一句。拓展坞的Type-C口通常既要传数据又要供电,很多人被绕晕的地方在于:USB数据线的D+/D-和CC协议是两套独立的东西。FUSB302管的是CC和PD协商,D+/D-由USB2.0 PHY负责。做拓展坞时,CC逻辑决定的是"这个口当前是DFP还是UFP",进而控制D+/D-信号是否连接到主机端。如果CC协商成功但数据传输失败,多数是D+/D-通路上的切换开关没有正确联动,而不是FUSB302的问题。
另外,拓展坞如果支持PD Sink(充电口),一定要让FUSB302工作在dua l或者sink模式,并正确配置max-sink-mv/ma/mw,否则它只能看到5V的固定供电,永远不会向适配器请求9V/12V。
6. 调试工具与现场排查方法补充
6.1 除了dmesg还能怎么看状态
Linux内核TCMP框架提供了debugfs接口,调试PD协商的非常有用。挂载debugfs后:
mount -t debugfs none /sys/kernel/debug cat /sys/kernel/debug/usb/tcpm/port0这个文件会输出当前端口状态、role、negotiated current/voltage,以及最近一次接收到的PDO列表。比dmesg里翻日志要清晰很多。很多vendor SDK编译时默认打开。
6.2 修改PDO列表
如果是作为Source给别的设备供电,默认输出的电压档位由驱动和策略层决定。在设备树里或者驱动的tcpm_config中,可以定义自己的PDO数组。比如只输出5V和9V两档,Core里的port_type设为source,PDO设置为fixed 5V/3A和fixed 9V/3A。修改后重新编译内核,插入支持PD的Sink设备,能看到Sink收到了两组PDO,然后结合它自己的需求请求其中一档。
调试PDO时建议用USB PD协议分析仪,或者至少用一个支持不同电压请求的诱骗器。诱骗器可以把Sink能力固定为9V/3A,用来验证Source端是否真的能升到9V。
我个人在实际操作中还有一个习惯:每次改完设备树或内核配置,先花两分钟用i2cget读一遍FUSB302的Device ID。这样做不是为了确认硬件又活了,而是确保芯片确实从复位状态被正确初始化了。很多"改了配置不起作用"的问题,其实是设备树文件没被重新编译进boot镜像,或者i2c节点alias冲突导致FUSB302一直没被probe。FUSB302这颗芯片本身非常成熟,大多数调不通的项目最后要么死在硬件细节,要么死在条件编译和配置管理上,真正需要怀疑芯片本身的情况少之又少。
如果你正被PD快充整得焦头烂额,按我上面这套顺序走一遍:先确认硬件电压和CC波形,再确认I2C能读到Device ID,然后看dmesg里TCMP状态机的输出,最后再动PID配置。每一步都能明确结论的话,PD快充基本不会成为你项目里过不去的坎。