简介:面向嵌入式Linux开发者的USB Type-C控制芯片FUSB302驱动源码,聚焦RK平台内核中正反插切换能力缺失的实际问题。原厂驱动未定义正反插切换管脚,导致正插USB3.0设备正常、反插仅能识别USB2.0设备,本驱动通过新增fusb340-switch控制管脚定义补齐这一功能,并在设备树节点下配置fusb340-switch-gpios即可启用,适合从事Type-C接口调试、内核驱动移植的中高级工程师参考。资源包共2个文件,以C源文件与H寄存器头文件为主,分别承载驱动逻辑实现与寄存器定义,压缩包约12KB,体量轻便、便于直接并入内核源码树阅读。已有266人学习下载,可作为排查Type-C正反插识别异常、理解FUSB302管脚控制流程的实用参考,帮助读者快速定位问题并完成驱动适配。
1. 一块 Type-C 口背后的“翻译官”:fusb302 驱动到底在管什么
插上一根 Type-C 线,设备是充电还是传数据、正插反插怎么自动识别、5V 还是 9V 该给多少,这些决定都不是主控芯片自己拍脑袋想出来的。中间站着一个叫 FUSB302 的小芯片,它负责盯着 CC 引脚上的电平变化,把“对面插了个啥、要多少电、能不能走 PD 快充”翻译成寄存器里的状态位。Linux 内核里对应的 fusb302 驱动,干的就是把这块芯片的状态读出来、上报给 Type-C 框架和 PD 协议栈,再反向配置芯片动作的活。做嵌入式 Linux 的开发者,只要板子上有 Type-C 口又不想自己从零写 CC 逻辑,基本绕不开它。这篇笔记按“它管什么 → 怎么跑起来 → 怎么改”的顺序拆,源码结构、设备树、调试手段都会落到能抄的程度。
2. 驱动架构拆解:从 I2C 客户端到 Type-C 框架的三层结构
fusb302 驱动不是一个孤立的字符设备,它嵌在 Linux 的 Type-C 子系统里,往上接tcpm(Type-C Port Manager),往下走 I2C 读写寄存器。理解这三层关系,后面改代码才不会改错地方。
2.1 三层职责划分:I2C 层、TCPM 层、芯片寄存器层
先看整体分层。最底下是 FUSB302 芯片本身,通过 I2C 挂在 SoC 上,地址通常是0x22(7 位地址,部分板子会因 SA 引脚接法不同而变化,常见做法是查原理图确认)。中间是驱动里的 I2C 读写封装,负责把“读某个寄存器”翻译成i2c_smbus_read_byte_data之类的调用。最上面是 TCPM 框架,驱动向它注册一个tcpm_port,之后所有 CC 状态变化、PD 消息收发都通过 TCPM 的回调和状态机流转。
这样分层的好处是:PD 协议栈(tcpm/pd.c)是通用的,fusb302 只负责“芯片相关”的部分——怎么触发一次 CC 检测、怎么发一个 PD 包、怎么读中断状态。换一颗别的 Type-C 控制器,只要再写一个 TCPM 驱动,上层协议栈不用动。
驱动注册的入口大致长这样:
static int fusb302_probe(struct i2c_client *client, const struct i2c_device_id *id) { struct fusb302_chip *chip; struct tcpm_port *port; int ret; chip = devm_kzalloc(&client->dev, sizeof(*chip), GFP_KERNEL); if (!chip) return -ENOMEM; chip->client = client; chip->dev = &client->dev; i2c_set_clientdata(client, chip); /* 初始化 GPIO:中断脚、复位脚、VBUS 检测脚 */ ret = fusb302_gpio_init(chip); if (ret) return ret; /* 读芯片版本,确认通信正常,顺便区分 fusb302 与兼容型号 */ ret = fusb302_read_version(chip); if (ret) return ret; /* 向 TCPM 注册端口,回调函数表在这里挂上去 */ port = tcpm_register_port(chip->dev, &chip->tcpm_port_ops); if (IS_ERR(port)) return PTR_ERR(port); chip->port = port; return 0; }这段probe的逻辑顺序有讲究:先分配私有结构体,再初始化 GPIO,然后读版本号做一次“握手”确认 I2C 通,最后才注册 TCPM。参数上,client是 I2C 框架传下来的设备句柄,id用于匹配设备表;chip是整个驱动的私有上下文,后面所有状态都挂在它上面。tcpm_register_port的第二个参数是一组回调,TCPM 需要发 PD 包、设置 CC 方向、读 VBUS 时都会回调到这里。
提示:
probe里读版本号失败是最常见的“驱动加载了但没反应”的原因,先确认 I2C 地址和上拉电阻,再怀疑芯片。
2.2 设备树与 I2C 节点:板级配置怎么写
驱动能不能 probe 成功,一半取决于设备树写对没有。fusb302 节点挂在对应的 I2C 控制器下,典型写法如下:
&i2c4 { status = "okay"; clock-frequency = <400000>; fusb302: typec-port@22 { compatible = "fcs,fusb302"; reg = <0x22>; interrupt-parent = <&gpio1>; interrupts = <12 IRQ_TYPE_LEVEL_LOW>; pinctrl-names = "default"; pinctrl-0 = <&fusb302_pins>; vbus-supply = <&vbus_5v>; status = "okay"; }; };逐项说:compatible必须和驱动里的of_match_table对上,写错一个字母就匹配不上;reg是 I2C 从地址,要和原理图上 SA 引脚的电平一致;interrupts指定中断脚和触发方式,FUSB302 的中断是低电平有效,写成IRQ_TYPE_EDGE_FALLING在某些板子上会丢中断,血泪经验是优先用LEVEL_LOW;vbus-supply指向 VBUS 的 regulator,驱动在需要给对面供电时会去使能它。
pinctrl那两行容易被忽略。中断脚如果没配成上拉或正确复用,表现是驱动加载成功但永远收不到中断,CC 插拔完全无感。我一般会在设备树里把中断脚单独拉一个 pinctrl 组,明确写上bias-pull-up,省得后面查半天。
2.3 中断处理与状态机:一次插拔在代码里怎么走完
FUSB302 的中断脚一拉低,说明芯片里有状态变了。驱动在中断线程里读FUSB302_REG_INTERRUPT系列寄存器,判断是 CC 变化、VBUS 变化还是 PD 包到达,然后分发给 TCPM。
static irqreturn_t fusb302_irq_intn(int irq, void *dev_id) { struct fusb302_chip *chip = dev_id; /* 中断线程化处理,这里只做唤醒,重活放到线程里 */ return IRQ_WAKE_THREAD; } static irqreturn_t fusb302_irq_thread(int irq, void *dev_id) { struct fusb302_chip *chip = dev_id; u8 status0, status1; /* 读中断状态寄存器,读操作本身会清标志 */ status0 = fusb302_i2c_read(chip, FUSB302_REG_STATUS0A); status1 = fusb302_i2c_read(chip, FUSB302_REG_STATUS1A); if (status1 & FUSB302_STATUS1_BC_LVL) { /* CC 电平变化,交给 TCPM 重新判断连接状态 */ tcpm_cc_change(chip->port); } if (status0 & FUSB302_STATUS0_VBUSOK) { /* VBUS 上电或掉电 */ tcpm_vbus_change(chip->port); } if (status1 & FUSB302_STATUS1_RX_EMPTY_0) { /* 收到 PD 包,读 FIFO 后上报 */ fusb302_pd_rx_handler(chip); } return IRQ_HANDLED; }这里的关键设计是“中断只做分发,状态机在 TCPM 里跑”。tcpm_cc_change会让 TCPM 重新走一遍 CC 检测流程,判断对面是 sink、source 还是音频配件;tcpm_vbus_change触发 VBUS 状态更新;PD 包则要先从芯片 FIFO 里读出来,做 CRC 校验,再交给tcpm_pd_receive。参数上,status0和status1是芯片的两个状态寄存器,位定义在数据手册里,驱动头文件里一般有对应的宏。
注意:中断线程里不要做耗时操作,读 FIFO 如果包长较大,建议先拷贝到本地缓冲再处理,避免长时间占用中断线程导致后续中断堆积。
3. 把驱动跑起来:编译、加载与验证的完整步骤
架构清楚了,接下来是让它在板子上真正工作。这一章按“内核配置 → 编译 → 加载 → 验证”的顺序走,每一步都给可执行的命令和判断标准。
3.1 内核配置与编译:CONFIG 选项一个都不能少
fusb302 依赖 Type-C 子系统,配置项漏一个就会编译不过或者 probe 失败。需要打开的主要有:
# 进入内核源码目录,用 menuconfig 或直接改 .config make menuconfig # 关键配置项 CONFIG_TYPEC=y CONFIG_TYPEC_TCPM=y CONFIG_TYPEC_TCPCI=y CONFIG_TYPEC_FUSB302=y CONFIG_TYPEC_TCPM_PD=y CONFIG_EXTCON=yCONFIG_TYPEC_FUSB302是驱动本体,CONFIG_TYPEC_TCPM是上层框架,CONFIG_TYPEC_TCPM_PD打开 PD 协议栈(不打开就只能做基础 CC 检测,快充协商走不了)。CONFIG_EXTCON用于把 Type-C 状态导出给别的子系统,比如 USB 控制器切换 host/device 角色。
编译时如果报undefined reference to tcpm_register_port,基本就是CONFIG_TYPEC_TCPM没开或者编成了模块而 fusb302 编进了内核。常见做法是两者都编成模块,或者都编进内核,别混着来。
# 编译设备树和模块 make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- dtbs -j8 make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- modules -j8 # 产物位置 # 驱动模块:drivers/usb/typec/tcpm/fusb302.ko # 设备树:arch/arm64/boot/dts/<厂商>/<板子>.dtb交叉编译工具链前缀按自己平台改,ARM32 平台换成arm-linux-gnueabihf-。设备树编译完记得确认 dtb 里确实有 fusb302 节点,可以用dtc -I dtb -O dts反编译出来 grep 一下。
3.2 加载与 dmesg 判读:怎么确认驱动真的活了
模块和 dtb 都烧到板子上后,加载驱动并看日志:
# 加载依赖模块(如果编成模块) modprobe typec modprobe tcpm modprobe fusb302 # 或者直接 insmod insmod /lib/modules/$(uname -r)/kernel/drivers/usb/typec/tcpm/fusb302.ko # 看驱动是否 probe 成功 dmesg | grep -i fusb302正常的日志里应该能看到类似fusb302 4-0022: fusb302 initialized或者 TCPM 打印的端口注册信息。如果只有probe failed with error -5,那是 I2C 通信失败,回去查地址和上拉;如果是-517,是-EPROBE_DEFER,说明依赖的 regulator 或 pinctrl 还没就绪,检查设备树里vbus-supply指向的节点是否存在。
验证 Type-C 状态最直接的是看 sysfs:
# 查看 Type-C 端口状态 cat /sys/class/typec/port0/data_role cat /sys/class/typec/port0/power_role cat /sys/class/typec/port0/vbus_voltage # 插拔线时观察变化 watch -n 0.5 'cat /sys/class/typec/port0/data_role'插上设备后data_role应该从nothing变成device或host,power_role变成sink或source。如果一直是nothing,说明 CC 检测没跑起来,优先怀疑中断脚。
3.3 用 I2C 工具直接读寄存器:绕过驱动定位硬件问题
驱动行为不对时,别急着改代码,先用i2c-tools直接和芯片对话,确认硬件层面是通的:
# 确认 I2C 总线上能看到设备 i2cdetect -y 4 # 期望在 0x22 位置看到 22 # 读芯片版本寄存器(地址以数据手册为准,常见为 0x01) i2cget -y 4 0x22 0x01 # 读状态寄存器,插拔线对比数值变化 i2cget -y 4 0x22 0x41 i2cget -y 4 0x22 0x42如果i2cdetect都扫不到0x22,那和驱动无关,是硬件或设备树 I2C 节点的问题:确认 I2C 控制器status = "okay"、引脚复用正确、上拉电阻焊了。能扫到但读出来全是0xff,常见原因是芯片没供电或者复位脚一直拉着。
提示:
i2cget读某些寄存器会触发副作用(比如清中断标志),调试时读之前先想清楚,别把现场破坏了。
4. 核心代码精读:CC 检测、PD 收发与角色切换
跑通之后,想改功能就得看懂关键路径。这一章挑三个最常被改动的部分:CC 检测怎么触发、PD 包怎么发、角色切换怎么配。
4.1 CC 检测流程:从寄存器配置到 tcpm_cc_change
CC 检测是 Type-C 的基础。FUSB302 需要被配置成“测量 CC 电平”的模式,然后驱动读结果判断对面接了什么。核心配置大致如下:
static int fusb302_set_cc(struct fusb302_chip *chip, u8 mode) { int ret; /* 切换到 CC 测量模式,关闭 PD 收发以降低干扰 */ ret = fusb302_i2c_write(chip, FUSB302_REG_SWITCHES0, FUSB302_SWITCHES0_MEASURE_CC); if (ret) return ret; /* 根据 mode 选择测量 CC1 还是 CC2 */ switch (mode) { case TYPEC_CC_OPEN: /* 都不测,进入待机 */ ret = fusb302_i2c_write(chip, FUSB302_REG_SWITCHES0, 0); break; case TYPEC_CC_RD: /* 作为 sink,CC 上拉电流源打开,测电压判断 source 能力 */ ret = fusb302_i2c_write(chip, FUSB302_REG_MEASURE, FUSB302_MEASURE_MDAC_MV(0)); break; case TYPEC_CC_RP: /* 作为 source,CC 下拉,测电流 */ ret = fusb302_i2c_write(chip, FUSB302_REG_MEASURE, FUSB302_MEASURE_MDAC_MV(0)); break; default: return -EINVAL; } return ret; }mode参数对应 TCPM 传下来的角色意图:TYPEC_CC_RD表示本端做 sink(被供电),TYPEC_CC_RP表示做 source(对外供电)。配置完测量模式后,芯片会在 CC 电平变化时产生中断,驱动在中断里调tcpm_cc_change,TCPM 再读FUSB302_REG_STATUS0A里的 BC_LVL 位判断对面是默认、1.5A 还是 3A。
这里有个容易翻车的点:测量 CC 和 PD 收发共用部分模拟前端,切换模式时如果时序不对,会出现“插上能识别但 PD 协商失败”。常见做法是在切换前先关掉 PD 收发使能,切完再打开,中间留一点延时。
4.2 PD 消息发送:FIFO 写入与重试逻辑
PD 快充协商靠的是在 CC 线上跑 BMC 编码的包。驱动发一个 PD 包,本质是往芯片的 TX FIFO 里按顺序写字节,然后触发发送。
static int fusb302_pd_send_message(struct fusb302_chip *chip, const struct pd_message *msg) { u8 buf[PD_MAX_MESSAGE_SIZE + 4]; int len, ret, i; /* 构造 FIFO 内容:前导码 + 包头 + 数据 + CRC */ len = fusb302_build_pd_frame(msg, buf, sizeof(buf)); if (len < 0) return len; /* 先复位 TX FIFO,避免残留数据 */ ret = fusb302_i2c_write(chip, FUSB302_REG_CONTROL0, FUSB302_CONTROL0_TX_FLUSH); if (ret) return ret; /* 逐字节写入 TX FIFO */ for (i = 0; i < len; i++) { ret = fusb302_i2c_write(chip, FUSB302_REG_FIFOS, buf[i]); if (ret) return ret; } /* 触发发送,并打开发送完成中断 */ ret = fusb302_i2c_write(chip, FUSB302_REG_CONTROL0, FUSB302_CONTROL0_TX_START); if (ret) return ret; /* 等待 TX 完成中断,超时则重试,最多 3 次 */ return fusb302_wait_tx_complete(chip, 3); }msg是 TCPM 协议栈构造好的 PD 消息,包含消息类型、数据对象个数和 payload。buf里拼的是芯片要求的完整帧格式,前导码和 CRC 由驱动补。FUSB302_CONTROL0_TX_FLUSH那一步别省,之前调试时遇到过 FIFO 里残留上一个包导致 CRC 错,现象是 PD 协商偶发失败,查了很久。
重试逻辑放在fusb302_wait_tx_complete里,超时时间一般给 5ms 左右,PD 规范对响应时间有要求,太长会触发对方重传。重试次数别设太多,3 次足够,再多说明链路本身有问题。
4.3 角色切换与 VBUS 控制:source/sink 怎么切
设备既能当 host 又能当 device 时,角色切换是重点。TCPM 决定切角色后,会回调驱动的set_role或set_vbus:
static int fusb302_set_vbus(struct tcpc_dev *tcpc, bool on, bool sink) { struct fusb302_chip *chip = tcpc_get_devdata(tcpc); int ret; if (sink) { /* 作为 sink,不需要驱动 VBUS,只需确保不对外供电 */ return 0; } /* 作为 source,使能或关闭 VBUS regulator */ if (on) { ret = regulator_enable(chip->vbus); if (ret) return ret; /* 打开 VBUS 检测,防止对已带电的线缆硬上电 */ fusb302_i2c_write(chip, FUSB302_REG_POWER, FUSB302_POWER_MEASURE_VBUS); } else { regulator_disable(chip->vbus); } return 0; }on表示是否输出 VBUS,sink表示本端是不是被供电方。作为 sink 时直接返回,因为 VBUS 是对方给的。作为 source 时通过regulator_enable控制外部电源开关,同时打开芯片的 VBUS 测量,避免“线缆另一头已经有电还硬输出”导致电流倒灌。
注意:
regulator_enable之前最好先读一次 VBUS 电压,确认接近 0 再上电,这个保护在很多参考设计里都有,漏掉可能烧接口。
5. 避坑与排查:五个真实踩过的坑
驱动调不通的时候,现象往往很迷惑。这一章按“现象 → 原因 → 解决”记五条,都是实际调试中反复遇到的。
5.1 驱动 probe 成功但插拔无反应
现象:dmesg显示 fusb302 初始化完成,sysfs 里端口也在,但插线后data_role一直是nothing。
原因:中断脚没配好。设备树里interrupts写了但 pinctrl 没把该引脚复用成 GPIO 中断功能,或者触发方式写成了边沿触发而芯片输出的是电平信号,导致中断丢失。
解决:确认 pinctrl 组里该引脚功能是 GPIO 输入、带上拉;interrupts改成IRQ_TYPE_LEVEL_LOW;用cat /proc/interrupts看该中断号计数在插拔时是否增长,不增长就是硬件或配置问题。
5.2 PD 协商失败但普通充电正常
现象:插上充电器能充,但功率上不去,日志里 PD 消息发出去没回应。
原因:TX FIFO 写入时序不对,或者 CC 测量模式和 PD 发送模式切换时没留延时,导致发出的包对方收不全。
解决:在fusb302_pd_send_message里确认每次发送前做了TX_FLUSH;模式切换后加usleep_range(100, 200);用逻辑分析仪抓 CC 线波形,看包的前导码是否完整。
5.3 i2cdetect 扫不到 0x22
现象:驱动加载报 I2C 通信错误,i2cdetect -y 4在0x22位置显示--。
原因:芯片没供电、复位脚被拉住、I2C 上拉电阻缺失,或者设备树里 I2C 控制器没使能。
解决:万用表量芯片 VDD 和复位脚电平;确认 I2C SDA/SCL 上有 2.2k 到 10k 上拉;检查设备树&i2c4的status和引脚复用。这一步和驱动代码无关,别在代码里找。
5.4 角色切换后 USB 不识别
现象:Type-C 口切到 host 模式后,插 U 盘没反应,但供电正常。
原因:extcon或usb-role-switch没接上,USB 控制器不知道要切 host 模式。
解决:确认设备树里 USB 控制器节点有role-switch或extcon指向 Type-C 端口;检查CONFIG_TYPEC相关的角色切换回调是否注册;用cat /sys/class/usb_role/*/role看角色是否真的切过去了。
5.5 休眠唤醒后 Type-C 失效
现象:系统 suspend/resume 之后,插拔线不再有反应,重启才恢复。
原因:resume 时芯片寄存器被复位成默认值,驱动没重新初始化。
解决:在驱动的resume回调里重新配置 CC 测量模式、中断使能和 PD 收发;确认struct dev_pm_ops挂上了。这个坑在低功耗产品上很常见,别指望芯片自己记住配置。
6. 二次开发改造:从能跑到好用的几个动手点
驱动跑通只是起点,真正做产品时总要根据板子改点东西。这一章说几个我实际改过的方向,以及怎么验证改对了。
第一个改造点是多端口支持。一块板子有两个 Type-C 口时,驱动实例会 probe 两次,私有结构体fusb302_chip是每个实例独立的,但全局资源比如 VBUS regulator 可能共用。改的时候注意devm_系列接口的释放顺序,两个口同时申请同一个 regulator 会失败,常见做法是在设备树里给每个口配独立的vbus-supply,或者用regulator_get_optional加引用计数。
第二个是调试信息增强。原驱动日志偏少,排查 PD 协商问题时不够用。我一般会在fusb302_pd_rx_handler和fusb302_pd_send_message里加dev_dbg,把消息类型和 header 打出来,配合dynamic_debug按需打开:
dev_dbg(chip->dev, "PD rx: type=0x%02x cnt=%d\n", PD_HEADER_TYPE(msg->header), PD_HEADER_CNT(msg->header));验证方法是echo 'file fusb302.c +p' > /sys/kernel/debug/dynamic_debug/control,然后插拔看dmesg,确认收发的消息类型符合预期(比如 source capabilities 是 0x01,request 是 0x02)。
第三个是兼容非标准线缆。有些廉价线缆 CC 电阻偏差大,标准阈值判断会误判成音频配件。可以在fusb302_set_cc之后读 BC_LVL 时加一点容差判断,但别改太松,否则真配件识别不了。改完用几种不同线缆各插拔二十次,统计识别成功率,比单次测试靠谱。
| 改造方向 | 改动位置 | 验证手段 |
|---|---|---|
| 多端口 | probe 与 regulator 申请 | 两口同时插拔,看 dmesg 无报错 |
| 调试日志 | PD 收发函数 | dynamic_debug 打开后看消息类型 |
| 线缆兼容 | CC 电平判断阈值 | 多线缆重复插拔统计成功率 |
最后说个习惯。每次改完 fusb302 相关代码,我都会强制走一遍“冷启动 → 插 sink 设备 → 插 source 设备 → 拔掉 → 休眠唤醒 → 再插”这条链路,少一步都可能漏掉 resume 没重新初始化这类问题。早期图省事只测热插拔,结果量产前才发现休眠唤醒后端口失效,返工成本比多测五分钟高得多。希望这些能帮你在自己的板子上少走点弯路。
本文还有配套的精品资源,点击获取