2. USB Gadget框架简介:Linux内核中的ConfigFS和Legacy Gadget框架
聊USB Gadget之前,我们先搞清楚一个问题:你的Linux设备要模拟成一个U盘、网卡或者串口,谁在背后干活?
答案是Linux内核里的USB Gadget框架。说白了,它就是一个软件层,让硬件能“扮演”不同的USB设备角色。我这些年排查枚举失败,碰到的坑有一半都跟没搞懂这个框架有关。
目前有两套框架在并行:一套是老的Legacy Gadget,另一套是新的ConfigFS Gadget。你都得认,因为你在实际项目中很可能都会遇到。
2.1 老大哥:Legacy Gadget框架
Legacy Gadget是Linux内核最早支持的Gadget实现方式。它的核心思想是:你在内核编译时就把Gadget的功能定死。
怎么操作呢?通过在内核源码的drivers/usb/gadget/目录下,直接编写或者选择一个现成的gadget driver。比如g_ether.ko是模拟网卡,g_mass_storage.ko是模拟U盘,g_serial.ko是模拟串口。
Legacy Gadget的特点:
- 功能在编译时决定,不能动态改变
- 每个Gadget驱动独立,想换功能就得重新加载内核模块
- 配置很直接,适合功能单一的嵌入式设备
我记得早期做一个工业采集器,就用的Legacy方式。设备只需要虚拟成一个串口,编译好g_serial.ko,insmod加载就完事了。简单、稳定,出问题的概率很小。
但这里有个隐含的坑:Legacy Gadget框架里,一个驱动只能实现一个USB复合设备。如果你想同时模拟网卡和串口,事情就麻烦了。要么自己写复合驱动,要么用后面要讲的ConfigFS。
2.2 复合设备与Legacy的局限
USB复合设备是什么?就是你的设备插到电脑上,电脑能认出来有多个功能接口。比如同时是一个U盘和一个串口。
Legacy框架对于复合设备支持得很别扭。它虽然有个gadgetfs的方式,但需要你在用户空间自己处理USB协议。我试过一次,写起来非常痛苦。大部分工程师最终都会放弃这条路。
为什么会这样?因为Legacy框架的驱动模型是一驱动一功能。它没有提供标准的“组合”能力。你想复合?可以,自己在内核里把两个功能驱动合并成一个新驱动。但这工作量,嗯……
避坑提醒:
我曾经图省事,在Legacy框架下硬改g_ether加上mass storage功能。结果USB枚举时断时续,排查了整整两天。最后发现是复合设备的描述符长度计算错误。所以,如果功能需求大于1个,我强烈建议直接上ConfigFS。
2.3 新方案:ConfigFS Gadget框架
ConfigFS(Configuration File System)是Linux内核引入的一个更灵活的方案。它把Gadget的配置从内核空间搬到了用户空间。
你不需要改内核代码,不需要重新编译内核模块。只需要在/sys/kernel/config/usb_gadget/目录下创建目录、写文件,就能动态拼装出一个USB设备。
听起来是不是像搭积木?的确是这样。
ConfigFS的核心组件有:
- Gadget:一个USB设备的整体描述,包含制造商ID、产品ID、序列号等
- Config:配置描述符,一个Gadget可以有多个配置
- Function:功能实例,比如mass storage、ECM网卡、acm串口
- UDC:底层的USB Device Controller驱动,负责和硬件通信
我在实际项目中是怎么做的呢?直接给个例子:
# 创建gadget目录
cd /sys/kernel/config/usb_gadget/
mkdir g1
cd g1
# 设置VID/PID
echo 0x1d6b > idVendor
echo 0x0104 > idProduct
# 创建配置
mkdir configs/c.1
mkdir configs/c.1/strings/0x409
echo "Config 1: ECM+ACM" > configs/c.1/strings/0x409/configuration
# 创建功能
mkdir functions/ecm.usb0
mkdir functions/acm.usb0
# 绑定功能到配置
ln -s functions/ecm.usb0 configs/c.1/
ln -s functions/acm.usb0 configs/c.1/
# 绑定UDC
echo 49000000.dwc3 > UDC
看到没?每一条命令都对应一个配置动作。你不需要重新编译,不需要重启。想加个功能?再创建个function目录,链进去就行。
我的建议:
初期调试验证时,用ConfigFS能极大缩短你的开发周期。我曾经用脚本封装了上述流程,配合udev规则,做到插拔USB线自动切换功能。效率提升非常明显。
2.4 ConfigFS vs Legacy:核心差异对比
我把两个框架的核心差异整理成了表格,方便你对照:
| 对比维度 | Legacy Gadget | ConfigFS Gadget |
|---|---|---|
| 配置方式 | 内核编译时固定,模块参数有限 | 用户空间文件系统动态配置 |
| 功能切换 | 需要卸载/加载内核模块 | 修改文件系统条目即可 |
| 复合设备支持 | 差,需要自写复合驱动 | 原生支持,通过符号链接组合function |
| 调试友好度 | 低,改一次编译一次 | 高,可以随时调整配置 |
| 内核版本要求 | 无特殊要求 | 需要Linux 3.11+,且开启CONFIGFS_FS |
| 适用场景 | 功能单一、生产环境稳定的产品 | 开发调试、多功能产品、原型验证 |
2.5 内核中的“胶水”:Composite Framework
不管你用哪种框架,最终都要落到Composite Framework上。它是Linux内核里负责把所有功能“粘”成一个USB设备的中间层。
实际上,ConfigFS背后调用的就是Composite Framework的API。Legacy框架里那些g_*.ko驱动,内部也是调用Composite接口来注册描述符和请求处理函数。
你想想看,为什么枚举总会失败?很多时候是因为Composite层在组装描述符时出现了问题。比如功能之间的接口号冲突、端点地址重复、配置长度溢出。
我参与排查过的一个案例就是:两个function都声称要使用同一组端点,Composite层没有冲突检测,结果插到Windows上直接报“未知USB设备”。最后在ConfigFS里调整了功能的加载顺序才解决。
记住这条线:
用户空间操作ConfigFS → 内核触发Composite Framework → 生成USB描述符 → 驱动UDC硬件 → 主机识别。任何一环出问题,枚举都不会成功。
2.6 框架选择的实际考量
你可能会问,那我到底该用哪个?这里我说点个人经验:
- 开发阶段:毫无疑问选ConfigFS。快速验证、动态调整、不用反复编译。
- 产品量产:如果你的功能需求固定不变,用Legacy框架可以减少用户空间的依赖,更稳定。但如果你需要OTA后改变功能,那就必须用ConfigFS。
- 调试阶段:我建议你在ConfigFS环境下做枚举测试。因为可以随时修改VID/PID序列号,排查描述符问题非常方便。
另外,要注意一个细节:很多SoC原厂的BSP里,默认内核配置可能没有开启CONFIGFS_FS或者对应的function驱动。一上来就发现/sys/kernel/config/usb_gadget/目录都不存在。嗯,又得重新编译内核了。
这部分内容就讲到这里。搞清楚了框架,下一步就是看代码里的具体数据结构,然后动手去操作系统里面的文件节点,慢慢就能摸到枚举失败的门道。