FUSB302 Linux驱动实战:从Type-C无法枚举到PD握手成功

📅 发布时间:2026/10/21 3:15:04
FUSB302 Linux驱动实战:从Type-C无法枚举到PD握手成功
简介USB Type-C控制器FUSB302的Linux驱动源码包面向嵌入式Linux驱动开发工程师重点解决RK原厂内核驱动中正反插切换管脚未定义导致的USB3.0识别异常问题。原厂驱动在反插时只能识别USB2.0设备而本驱动通过引入fusb340-switch GPIO控制逻辑使正插与反插均可完整识别USB3.0设备。压缩包包含fusb302.c驱动主体与fusb302_reg.h寄存器定义头文件共2个文件整体约12KB代码量少且便于阅读移植。使用时只需在设备树fusb302节点下配置fusb340-switch-gpios属性即可复用相关功能适合在RK平台及类似主控上快速集成也可作为Type-C驱动移植与调试的参考范例。目前该资源已有266人学习下载对正在处理USB Type-C识别或驱动二次开发的工程师具有直接借鉴价值。1. FUSB302驱动实践一块Type-C主控板从“无法枚举”到PD握手成功某个下午我拿到一块带FUSB302的Type-C主控板接上电脑后设备管理器里毫无反应dmesg刷了满屏I2C读写失败。排查了一整天才发现问题出在CC检测阈值配置和FIFO空读时序上。这让我意识到FUSB302这类USB PD控制芯片的Linux驱动拼的并不是协议栈本身而是对状态机、定时器和I2C时序的细节把控。本篇我把完整的驱动解析和排障过程记录下来覆盖从驱动框架、Type-C状态机、PD消息收发到避坑指南和二次开发定制。适合正在调Type-C口、做Power Delivery产品、或者准备把FUSB302接入自家嵌入式Linux平台的开发者。2. 驱动骨架总线模型、核心结构与初始化流程2.1 FUSB302在Linux里的角色定位FUSB302是一颗I2C接口的USB Type-C控制器它内部集成了CC引脚检测、BMC物理层编解码、CRC校验和一部分PD协议引擎但完整的PD策略比如协商电压、选PDO、处理硬复位仍然需要主控端的Linux驱动来完成。所以它天然适合挂在I2C总线下通过regmap或者直接i2c_transfer来操作寄存器。在内核里最常见的做法是把它实现为一个i2c_driver加一个typec_class或者tcpm的适配层。有些方案直接用内核自带的tcpm框架把FUSB302作为TCPM的phy提供者更轻量的做法是自己维护状态机绕开tcpm直接向typec子系统注册端口。我拆的这个项目选的是自维护状态机的轻量路线。好处是对芯片行为完全可控坏处是PD协议升级时你得自己跟上规范。如果你只是想让设备能充电、能枚举这套足够如果你想兼容各种怪异充电器建议还是上tcpm框架。2.2 驱动入口与I2C探测流程驱动入口没什么特别的标准的i2c_driver结构。重点在于probe函数里要对FUSB302做一次完整的芯片复位和基础配置否则后续状态机跑起来全是脏数据。static int fusb302_probe(struct i2c_client *client, const struct i2c_device_id *id) { struct fusb302_chip *chip; int ret; chip devm_kzalloc(client-dev, sizeof(*chip), GFP_KERNEL); if (!chip) return -ENOMEM; i2c_set_clientdata(client, chip); chip-client client; chip-dev client-dev; /* 复位芯片清除所有中断和FIFO残留 */ ret fusb302_sw_reset(chip); if (ret 0) { dev_err(chip-dev, software reset failed: %d\n, ret); return ret; } /* 配置芯片使能CC检测、屏蔽不需要的中断 */ ret fusb302_cc_pin_init(chip); if (ret 0) return ret; ret fusb302_irq_init(chip); if (ret 0) return ret; /* 注册Type-C端口 */ ret fusb302_typec_port_register(chip); if (ret 0) return ret; dev_info(chip-dev, FUSB302 probe success\n); return 0; }这段代码的逻辑是先申请芯片私有结构体再软件复位然后初始化CC引脚检测和中断最后注册Type-C端口。顺序不能乱尤其是软件复位必须放在最前面因为芯片上电后内部寄存器是随机状态不复位直接配置会导致CC检测逻辑混乱。参数说明fusb302_sw_reset会写RESET寄存器清空中断和FIFOfusb302_cc_pin_init设置CC1/CC2的检测使能和内部上下拉电阻fusb302_irq_init则是把MASK寄存器按需打开比如插入检测中断、PD消息接收中断。2.3 核心数据结构与寄存器映射关系FUSB302的寄存器数量不多也就十几个但每个位都值得认真对待。驱动里一般会把这些寄存器定义枚举然后通过i2c_smbus_read_byte_data和i2c_smbus_write_byte_data访问。#define FUSB302_DEVICE_ID 0x01 #define FUSB302_SW_RESET 0x02 #define FUSB302_CONTROL0 0x03 #define FUSB302_CONTROL1 0x04 #define FUSB302_STATUS0 0x1B #define FUSB302_STATUS1 0x1C #define FUSB302_INTERRUPT 0x1E #define FUSB302_FIFO 0x43 #define FUSB302_STATUS0_BC_LVL GENMASK(2, 0) #define FUSB302_STATUS0_BC_LVL_0 BIT(0) #define FUSB302_STATUS0_BC_LVL_1 BIT(1) #define FUSB302_STATUS0_BC_LVL_2 BIT(2)驱动里一般会维护一个fusb302_chip结构体保存当前端口角色、CC状态、PD消息队列和定时器。比如这样struct fusb302_chip { struct i2c_client *client; struct device *dev; struct typec_port *typec_port; enum typec_role cc_role; enum typec_data_role data_role; enum fusb302_cc_state cc_state; bool vbus_on; struct mutex lock; struct delayed_work bc_lvl_work; struct work_struct event_work; /* PD消息缓冲区 */ u8 msg_buf[64]; int msg_len; };这个结构体贯穿整个驱动生命周期。cc_role记录当前是Source还是Sinkcc_state记录的是Detached、Attached等待接入的具体状态bc_lvl_work用来做BCBattery Charging电平检测的延时任务。中断来了之后底半部会解析状态寄存器然后推进状态机。2.4 中断设计与延迟工作队列FUSB302的中断引脚是开漏输出低电平有效接在SoC的一个GPIO上。驱动的中断处理函数里只做一件事读取中断寄存器并清中断然后调度工作队列去解析具体状态。不能在中断上下文里做I2C读取FIFO这类耗时操作否则会拖死系统。static irqreturn_t fusb302_irq_thread(int irq, void *data) { struct fusb302_chip *chip data; unsigned int status; mutex_lock(chip-lock); status i2c_smbus_read_byte_data(chip-client, FUSB302_STATUS0); if (status 0) { mutex_unlock(chip-lock); return IRQ_HANDLED; } /* 清中断标志 */ i2c_smbus_write_byte_data(chip-client, FUSB302_INTERRUPT, 0xFF); /* 解析待处理的中断事件 */ fusb302_handle_event(chip, status); mutex_unlock(chip-lock); return IRQ_HANDLED; }这里有个容易忽略的细节FUSB302的I2C读操作本身会清掉一部分中断状态位所以读STATUS0之后再写INTERRUPT清所有标志属于双保险。但如果先写了清中断寄存器再去读状态可能读到的已经是清零后的值导致事件丢失。工作队列event_work在probe时初始化中断线程里只是把事件塞进去真正的CC检测和PD消息处理在进程上下文完成。这种设计在嵌入式Linux里很常见避免I2C操作阻塞硬中断上下文。3. 核心模块拆解Type-C状态机与PD消息收发的完整链路3.1 CC检测与角色判定逻辑FUSB302的状态机第一步永远是对CC引脚做检测。CC1和CC2上连接了Rp/Rd电阻通过检测引脚电压可以判断对端是设备还是供电方也能判断正反插。static int fusb302_cc_detect(struct fusb302_chip *chip) { unsigned int status0; unsigned int cc1_lvl, cc2_lvl; int ret; /* 依次读取CC1和CC2的检测结果 */ ret i2c_smbus_read_byte_data(chip-client, FUSB302_STATUS0); if (ret 0) return ret; status0 ret; cc1_lvl status0 FUSB302_STATUS0_BC_LVL; cc2_lvl (status0 3) FUSB302_STATUS0_BC_LVL; if (cc1_lvl FUSB302_BC_LVL_RD cc2_lvl FUSB302_BC_LVL_OPEN) { chip-cc_state FUSB302_CC_STATE_ATTACHED_SRC; chip-cc_role TYPEC_SOURCE; } else if (cc1_lvl FUSB302_BC_LVL_RP cc2_lvl FUSB302_BC_LVL_OPEN) { chip-cc_state FUSB302_CC_STATE_ATTACHED_SNK; chip-cc_role TYPEC_SINK; } else { chip-cc_state FUSB302_CC_STATE_DETACHED; } dev_info(chip-dev, CC state: %d, role: %d\n, chip-cc_state, chip-cc_role); return 0; }这段逻辑的关键在于电平判据。Rp是指上拉电阻一般由Source端提供Rd是下拉电阻由Sink端提供。所以读到RD说明对面是设备本机就是Source读到RP说明对面是充电器或供电方本机就是Sink。两个CC都读到OPEN就是断开状态。实测中有个坑某些劣质线缆内部把CC短接到了地导致两个引脚都读到OPEN之外的杂散电平这时候状态机会在几个状态间跳变需要在驱动里加一个去抖延时连续读到同一电平超过30ms才切换状态。3.2 PD消息收发从FIFO读取到CRC校验PD协议的消息格式是SOP头 消息头 数据对象 CRC32。FUSB302硬件已经处理了BMC物理层编解码和CRC校验驱动只需要从FIFO寄存器把完整消息读出来再做协议层的解析。static int fusb302_pd_read_message(struct fusb302_chip *chip, struct pd_message *msg) { u8 header[2]; u8 data[64]; int len; int i; /* 先读FIFO中的消息头 */ for (i 0; i 2; i) { if (i2c_smbus_read_byte_data(chip-client, FUSB302_FIFO) 0) return -EIO; header[i] i2c_smbus_read_byte_data(chip-client, FUSB302_FIFO); } len PD_HEADER_GET_DATABYTES(header); if (len PD_MAX_DATA_BYTES) return -EMSGSIZE; /* 再读数据部分 */ for (i 0; i len; i) { data[i] i2c_smbus_read_byte_data(chip-client, FUSB302_FIFO); } /* 将数据复制到消息结构体供上层解析 */ memcpy(msg-header, header, 2); memcpy(msg-data, data, len); msg-len len; return 0; }每次读FIFO寄存器FUSB302会把FIFO指针自动后移一位所以读完一个字节再读下一个字节就能拿到连续数据。但如果中途读多了或者读少了FIFO指针就偏移了后续消息全错。这也是为什么很多驱动在每次读完一条消息后要检查STATUS1寄存器中的FIFO空标志。协议层的解析相对简单拿到消息头之后取出消息类型#define PD_MSG_TYPE_CTRL GENMASK(4, 0) #define PD_MSG_TYPE_DATA GENMASK(4, 0) #define PD_MSG_HEADER_EXT BIT(15) static int fusb302_pd_handle_message(struct fusb302_chip *chip, struct pd_message *msg) { u8 type msg-header[1] PD_MSG_TYPE_CTRL; switch (type) { case PD_CTRL_GOODCRC: dev_dbg(chip-dev, GOODCRC received\n); break; case PD_CTRL_PS_RDY: chip-vbus_on true; dev_info(chip-dev, PS_RDY: power ready\n); break; case PD_DATA_SOURCE_CAP: fusb302_pd_handle_source_cap(chip, msg); break; case PD_CTRL_HARD_RESET: fusb302_pd_handle_hard_reset(chip); break; default: dev_warn(chip-dev, unhandled PD message type: %d\n, type); break; } return 0; }收到GOODCRC说明对端正确收到了我们上一条消息这个标志常用于确认发送成功。PS_RDY是电力协商完成的关键信号收到它之后才能安全地打开VBUS。SOURCE_CAP则是充电器广播它能提供的PDO列表Sink端拿到这个列表后才能请求合适的电压。3.3 PD协商状态机从Source Cap到Request/PS_RDYPD协商的核心流程是Sink收到SOURCE_CAP从PDO列表里挑一个发REQUEST消息Source接受后回ACCEPT再回PS_RDY至此电源路径正式打开。static int fusb302_pd_request_pdo(struct fusb302_chip *chip, u32 pdo_index, u32 voltage_mv) { struct pd_message msg; u32 pdo 0; int ret; /* 构建REQUEST消息头, 包含目标PDO序号 */ mythings_pd_prepare_request(msg, pdo_index, voltage_mv); /* 写入FIFO */ ret fusb302_pd_write_message(chip, msg); if (ret 0) { dev_err(chip-dev, failed to send REQUEST: %d\n, ret); return ret; } /* 启动超时定时器等待ACCEPT或REJECT */ schedule_delayed_work(chip-pd_timeout_work, msecs_to_jiffies(500)); return 0; }REQUEST消息里最关键的是Object Position字段它告诉Source“我要的是你广播的第几个PDO”。如果Source同意会回ACCEPT不同意会回REJECT如果Source实现有问题可能什么都不回所以必须有超时定时器兜底。常见做法是500ms超时超时后重试三次仍无响应就放弃请求回到默认的5V供电。这个重试逻辑在量产设备里非常重要否则遇到一个协议栈有bug的充电器整机卡死在协商阶段设备不充电不枚举。3.4 VBUS控制与角色切换的联动FUSB302本身不控制VBUS路径它只是检测和管理CC/PD信号VBUS的开关通常由外部负载开关或电池充电芯片控制。所以驱动里需要一个回调函数把PD协商的状态转成对VBUS的GPIO操作。static int fusb302_vbus_set(struct fusb302_chip *chip, bool on) { int ret; if (!chip-vbus_gpio) { dev_err(chip-dev, no VBUS GPIO configured\n); return -EINVAL; } gpiod_set_value(chip-vbus_gpio, on); chip-vbus_on on; if (on) { /* 等待VBUS稳定再继续 */ msleep(20); fusb302_typec_set_vbus(chip-typec_port, true); } else { fusb302_typec_set_vbus(chip-typec_port, false); } return 0; }角色切换时比如从Sink切到Source要先断掉旧的VBUS路径等CC检测稳定再打开新的VBUS输出。如果顺序反了两个电源互相灌流轻则烧保险重则炸MOS管。驱动里一般会加一个简单的电源状态机确保断开和打开之间留出安全间隔。4. 避坑避坑I2C时序、FIFO空读与中断触发的五大实战教训4.1 I2C时钟频率过高导致读取随机失败现象驱动偶尔能识别到Type-C设备但大部分时间probe失败dmesg报i2c transfer error。原因FUSB302在3.3V供电下I2C最快支持400KHz但如果板子上I2C上拉电阻阻值偏大而总线电容又偏高400KHz的边沿斜率不达标波形振铃严重芯片就偶发不能正确采样地址位。解决把i2c_bus-timings里的总线速率降到100KHz或者在设备树里给FUSB302节点加上clock-frequency 100000。实际项目中这种问题最玄学逻辑分析仪看起来波形正常但挂上示波器才能看到抬升沿的振铃降速是百试百灵的后悔药。4.2 FIFO空读导致数据错位现象PD协商过程随机失败概率大概五分之一每次失败时收到的消息头都是乱码。原因中断里收到消息后驱动直接开读FIFO但FUSB302的FIFO数据在中断置位后还需要几个微秒才能真正准备好此时FIFO是空的驱动读回去的数据全是0xFF或随机值。解决在进入fusb302_pd_read_message之前必须检查STATUS1寄存器的FIFO非空位确认有数据再读。如果没有数据延迟1ms再检查最多重试5次。4.3 硬复位后不重新检测CC现象充电器拔出再插入后设备要等很久才能重新充电甚至需要断电重启。原因FUSB302收到硬复位消息后状态机被重置为Detached但驱动没有重新跑CC检测流程而是停留在等待PD消息的状态导致插拔事件被忽略。解决在fusb302_pd_handle_hard_reset里除了发送GOODCRC硬复位不响应之外还要清掉当前连接状态然后主动触发一次fusb302_cc_detect。从那以后我每次处理硬复位都会强制走一遍完整的重新检测流程。4.4 中断触发方式配置错误导致丢事件现象手机插上电脑电脑没有任何反应但拔掉再插一次就能正常工作。原因FUSB302的INT引脚是电平触发、低有效但如果SoC的GPIO控制器配置成边沿触发第一次插入时电平变化已经发生在中断注册之前事件就丢了。解决把设备树里的中断属性改成IRQ_TYPE_LEVEL_LOW并且不能用devm_request_irq的默认配置要显式指定触发类型。这个坑在飞思卡尔和高通平台上各遇到过一次每回都是插拔两次才让人怀疑是触发方式的问题。4.5 寄存器写入未确认导致配置丢失现象驱动初始化配置的上下拉电阻偶尔不生效CC检测永远停在Detached。原因FUSB302的某些寄存器是需要先置位写使能位才能修改的而直接写值会被硬件忽略但I2C层面仍然返回ACK驱动看起来写入成功实际没生效。解决对照手册逐位检查CONTROL0/CONTROL1的写保护位在写配置之前先置位对应的使能位再用一次读回验证寄存器值。我在驱动里加了一个fusb302_read_reg的回读函数每次写完配置都核对一遍。5. 二次开发加私有PDO、改PPS参数与CC检测阈值调优5.1 增加私有PDO来支持定制电压如果你的产品是小体积的充电设备想通过PD协商输出私有档位比如 9V2A、12V1.5A最直接的方式就是修改Source Capability消息里的PDO数组static const u32 fusb302_pdo_list[] { PD_PDO_FIXED(5000, 3000, 0), PD_PDO_FIXED(9000, 2000, 0), PD_PDO_FIXED(12000, 1500, 0), };参数含义PD_PDO_FIXED第一个参数是电压mV第二个是最大电流mA第三个是标志位。改完这个数组后需要同步修改get_sink_cap和get_source_cap两个回调的返回值否则协商时广播的PDO列表和实际支持的功率不匹配可能过流。5.2 配置PPS可编程电源的电压电流步进PPS是PD 3.0里比较实用的能力允许Sink在协商好的范围内动态请求电压和电流FUSB302驱动支持这一功能需要在SOURCE_CAP里添加APDO条目#define PD_PDO_APDO_PPS(mv1, mv2, ma) \ (BIT(30) | BIT(24) | \ (((mv2) / 100) 0xFF) 8 | \ (((mv1) / 100) 0xFF) 17 | \ (((ma) / 50) 0x7F) 6) static const u32 fusb302_pdo_list[] { PD_PDO_FIXED(5000, 3000, 0), PD_PDO_APDO_PPS(5000, 20000, 3000), };APDO的最小电压和最大电压单位都是100mV电流单位是50mA。改这个的时候要格外谨慎如果步进计算有误Sink请求的电压可能溢出芯片承受范围这时FUSB302不会主动保护只能靠外部电路兜底。我在调PPS时习惯用逻辑分析仪抓取REQUEST消息确认Object Position和数据字段符合预期再上真负载。5.3 调整CC检测阈值兼容非标线缆有些工程线缆的CC引脚电阻值不在标准范围内FUSB302默认的阈值会把它们识别成无连接。这时可以调整CONTROL0寄存器的检测阈值位让它更宽松static int fusb302_set_cc_threshold(struct fusb302_chip *chip, u8 threshold) { unsigned int ctrl0; ctrl0 i2c_smbus_read_byte_data(chip-client, FUSB302_CONTROL0); if (ctrl0 0) return ctrl0; ctrl0 ~FUSB302_CTRL0_BC_THRESHOLD_MASK; ctrl0 | threshold FUSB302_CTRL0_BC_THRESHOLD_MASK; return i2c_smbus_write_byte_data(chip-client, FUSB302_CONTROL0, ctrl0); }阈值范围从0到7数值越小越灵敏。但调低之后误检概率也会上升正常U盘也可能被判定为充电器。建议只在设备出厂校准阶段调这个值量产固件里保持标准值。5.4 从模块化到可扩展把驱动改成分层数据结构如果你打算在同一个平台上兼容FUSB302、FUSB301甚至其他厂家的Type-C芯片建议把芯片差异封装成一个fusb302_ops结构体里面放read_message、write_message、cc_detect等函数指针。这样上层状态机完全不用改换芯片时只需要换一套ops。我后来重构驱动时就是这么做的新芯片接入只花了一天而上层协议逻辑一行没动。从那以后每次做驱动的芯片适配我都强制把底层寄存器操作和上层状态机拆开不再图省事写在同一个文件里。希望这篇FUSB302驱动的拆解和避坑整理能帮到你少走点弯路。本文还有配套的精品资源点击获取