从"把开发板伪装成U盘、键盘、网卡"这个需求出发,linux usb gadget driver代码其实没有想象中那么神秘。我第一次写gadget驱动时,对着include/linux/usb/gadget.h里的结构体看了一下午,满脑子都是"这玩意儿到底怎么和设备控制器扯上关系的"。如果你也在啃gadget源码、或者正准备让手头的主控芯片在USB Device模式下跑起来,这篇就把整个链路——从驱动骨架、注册流程到configfs用户态配置、再到排查手段——从头到尾拆一遍。
这篇内容适合三类人:一是想在内核里新增一个gadget function的驱动开发者,二是只想用现有function(比如mass_storage、hid、ecm)快速配置出设备的嵌入式工程师,三是被"枚举失败、设备不认"这类问题折磨的调试者。
1. 先搞清楚gadget驱动到底在驱动什么
1.1 它和host驱动、UDC驱动是三条线
很多人一上来就搞混。USB驱动分三大块:host端的Host Controller驱动(比如EHCI、XHCI)、Device端的UDC(USB Device Controller)驱动、以及运行在Device端之上、负责实现具体"功能"的gadget驱动。
gadget驱动不是什么底层硬件驱动,它跑在UDC驱动之上,它的任务只有一个:让硬件控制器在USB总线上表现得像一个"外设"。同一个开发板,插上PC能被识别为U盘、串口、网卡、键盘——全看gadget驱动怎么描述自己。
你要是写过PCI驱动,会发现套路很熟:内核用usb_gadget_driver这个结构体描述一个gadget驱动,驱动注册到总线框架之后,UDC驱动负责找到可用的硬件控制器,然后两边绑定。硬件枚举成功之后,PC端看到的设备描述符、配置描述符、接口描述符全都要在这个驱动里准备好。
1.2 源码目录应该怎么逛
先认路。drivers/usb/gadget/下面分三块:
udc/:各家SoC的Device Controller驱动,比如dwc2、chipidea、dwc3,这些是"和硬件寄存器打交道"的底层驱动。你写的gadget驱动正常情况下不需要碰它们。function/:具体功能的实现,比如f_mass_storage.c(U盘)、f_hid.c(HID设备)、f_ecm.c(以太网控制模型)、f_serial.c(虚拟串口),每个文件实现一种USB类设备的行为。legacy/:把上面几个function串成一个完整"复合设备"的旧式框架,现在多用configfs动态组装,legacy模式只在新手教程里容易见到。
另外不要漏掉drivers/usb/gadget/composite.c,它是整个gadget框架的粘合剂。你在configfs里看到的那些目录,最终就是靠composite.c里的逻辑映射成USB配置描述符的。
理解了这个地盘划分,再回来看"写一个gadget驱动"其实有两种完全不同的做法:如果只是把现有function拼装起来,那是"配置"不是"开发";如果要做一个USB规范里没有的奇特设备,那才需要在内核里写新的function驱动。
2. 从源码看最简gadget驱动的骨架和注册流程
2.1 核心结构体:usb_gadget_driver的字段到底是什么意思
直接看代码。一个最底层的gadget驱动长这样(这里参考内核gadget框架的定义,省略了大部分注释):
#include <linux/module.h> #include <linux/kernel.h> #include <linux/usb/gadget.h> #include <linux/platform_device.h> static int demo_bind(struct usb_gadget *gadget, struct usb_gadget_driver *driver) { // 在这里创建设备描述符、配置描述符、申请端点 // 注册完成之后,host端就能识别到设备 return 0; } static void demo_unbind(struct usb_gadget *gadget) { // 释放bind中申请的资源 } static int demo_setup(struct usb_gadget *gadget, const struct usb_ctrlrequest *ctrl) { // 处理端点0上的控制传输,比如GET_DESCRIPTOR、SET_CONFIGURATION // host枚举设备时,第一步就走这里 return -EOPNOTSUPP; } static void demo_disconnect(struct usb_gadget *gadget) { // 拔出USB线时触发 } static struct usb_gadget_driver demo_driver = { .function = "demo_gadget", .max_speed = USB_SPEED_HIGH, .bind = demo_bind, .unbind = demo_unbind, .setup = demo_setup, .disconnect = demo_disconnect, }; static int __init demo_init(void) { return usb_gadget_driver_register(&demo_driver); } module_init(demo_init); static void __exit demo_exit(void) { usb_gadget_driver_unregister(&demo_driver); } module_exit(demo_exit); MODULE_LICENSE("GPL");你要是只看了usb_gadget_driver结构体就开始动手,会发现bind被调用时代码还没法正常工作。原因是:usb_gadget_driver_register只负责把驱动挂到框架里,真正"让设备可以被PC识别"还差一套描述符和端点处理。
2.2 bind里到底要做什么
demo_bind里必须干三件事:分配并填充设备描述符、分配并填充配置描述符、申请gadget端点并注册中断处理。参考g_serial驱动的简化写法:
static struct usb_device_descriptor demo_device_descriptor = { .bLength = sizeof(struct usb_device_descriptor), .bDescriptorType = USB_DT_DEVICE, .bcdUSB = cpu_to_le16(0x0200), .bDeviceClass = USB_CLASS_VENDOR_SPEC, .idVendor = cpu_to_le16(0x1234), .idProduct = cpu_to_le16(0x5678), .bNumConfigurations = 1, }; static int demo_bind(struct usb_gadget *gadget, struct usb_gadget_driver *driver) { int ret; ret = usb_gadget_ep_alloc(gadget, &gadget->ep0); if (ret) return ret; // 保存gadget指针,后续收发数据要用 // demo_dev.gadget = gadget; // 如果要做批量传输,还要额外申请IN/OUT端点 // demo_dev.ep_in = usb_ep_autoconfig(gadget, &demo_ep_desc); // demo_dev.ep_in->driver_data = &demo_dev; return 0; }注意usb_ep_autoconfig这个函数很有意思,它会在UDC驱动的帮助下寻找没被占用的端点,并自动匹配地址和方向。这也是gadget驱动和普通驱动差别最大的地方——你不能像写PCI网卡驱动那样硬编码寄存器地址,因为不同SoC的UDC支持端点数可能不一样,必须动态分配。
2.3 setup回调是设备枚举的"第一现场"
pC插上USB线后,第一步是发GET_DESCRIPTOR(DEVICE)请求,这个请求会走到你驱动的setup回调。处理方式基本是固定的:
static int demo_setup(struct usb_gadget *gadget, const struct usb_ctrlrequest *ctrl) { int ret = -EOPNOTSUPP; if ((ctrl->bRequestType & USB_TYPE_MASK) != USB_TYPE_STANDARD) return -EOPNOTSUPP; switch (ctrl->bRequest) { case USB_REQ_GET_DESCRIPTOR: // 根据wValue里的描述符类型返回对应数据 // 用usb_ep_queue把数据从ep0发回去 break; case USB_REQ_SET_CONFIGURATION: // 配置好之后,真正开始数据传输 break; default: break; } return ret; }大多数新手在这里犯的错是:忘记主机可能会反复发送SET_CONFIGURATION、SET_FEATURE甚至CLEAR_FEATURE,只处理了"理想情况"下的枚举流程,结果换个操作系统或换根线就出问题。
3. 不写内核代码也能实现gadget:configfs动态配置
3.1 configfs方式为什么成为主流
直接从零写一个能用的function驱动工作量相当大。好在内核把mass_storage、hid、ecm、rndis、serial这些常规功能都实现成了"可插拔模块",通过configfs可以在运行态任意拼装成一个复合设备,不需要编译内核、不用写一行C代码。
这玩意儿的配置逻辑很简单:configfs在/sys/kernel/config/usb_gadget/下暴露成一棵目录树,你在目录里mkdir就是创建一个设备或一个功能,echo到文件里就是设置参数,ln -s就是把功能绑定到配置上。整个配置过程就是操作文件和目录,不需要任何代码编译。
3.2 实测:把开发板配置成一个"U盘+串口"复合设备
下面这组命令是我在i.MX6ULL平台上验证过的完整流程,用的是内核自带libcomposite框架:
# 1. 挂载configfs,加载composite框架 mount -t configfs none /sys/kernel/config modprobe libcomposite # 2. 创建一个名为g1的gadget设备 mkdir /sys/kernel/config/usb_gadget/g1 cd /sys/kernel/config/usb_gadget/g1 # 3. 设置VID/PID/设备描述符 echo 0x1234 > idVendor echo 0x5678 > idProduct echo 0x0100 > bcdDevice echo 0x0200 > bcdUSB # 4. 设置字符串描述符 mkdir strings/0x409 echo "MyCompany" > strings/0x409/manufacturer echo "MyGadget" > strings/0x409/product echo "SN123456" > strings/0x409/serialnumber # 5. 创建配置 mkdir configs/c.1 mkdir configs/c.1/strings/0x409 echo "Conf1" > configs/c.1/strings/0x409/configuration # 6. 创建mass_storage功能(U盘) mkdir functions/mass_storage.0 echo /dev/mmcblk0p1 > functions/mass_storage.0/lun.0/file echo 0 > functions/mass_storage.0/lun.0/removable # 7. 创建acm串口功能 mkdir functions/acm.usb0 # 8. 绑定到配置 ln -s functions/mass_storage.0 configs/c.1/ ln -s functions/acm.usb0 configs/c.1/ # 9. 绑定UDC控制器 echo ci_hdrc.0 > UDC第9步是最容易出问题的。你得先查一下开发板上的UDC名字,再把它写进UDC文件:
ls /sys/class/udc/执行完最后一步后,PC端会立刻弹出U盘提示,同时多出一个虚拟串口/dev/ttyACM0。
注意:UDC文件只能写一次。如果绑错了或者想重新配置,要先执行echo "" > UDC清空绑定,否则会报device or resource busy。
3.3 configfs和内核代码结合时:functionfs的玩法
如果你的需求很特殊,既不想写完整的内核驱动,又不想用现成的function,还有一个折中方案:用FunctionFS。
FunctionFS的思路是:内核只提供一个透传端点,真正的"USB逻辑"放在用户态程序里实现。比如你想做一个自定义的USB采集设备,可以先在内核里加载usb_f_fs模块,然后在用户态用libusb风格的程序往端点里写数据,协议逻辑全在应用层。对很多原型验证项目来说,这比维护一个内核模块省事得多。
4. 调试工具链:日志、usb抓包与硬件级排查
4.1 从内往外看:dmesg和debugfs
gadget调试,第一反应永远是dmesg。
dmesg | grep -i usb如果UDC驱动正常,你会看到类似这样一行:
ci_hdrc ci_hdrc.0: registered gadget driver demo_gadget如果设备枚举失败,dmesg里通常会留下具体原因,比如config 1 interface 0 altsetting 0 bulk ep 0x81 has invalid maxpacket这种描述符错误。
另外不要忽略/sys/kernel/debug/usb/,不同UDC驱动会在这里暴露寄存器状态、端点使用情况。比如chipidea的UDC调试节点可以看到当前端点分配和传输状态:
cat /sys/kernel/debug/usb/ci_hdrc.0/registers4.2 从外往内看:usbmon配合wireshark抓包
有时候问题出在"主机发来的数据根本没到达gadget驱动",这时候光看dmesg没用,必须抓USB总线上的包。
内核自带的usbmon就是干这个的:
modprobe usbmon然后用root权限运行wireshark,在抓包界面选择usbmon0(抓全部总线)或者对应的usbmonX(某个bus)。抓到包之后,可以清楚地看到SETUP阶段主机发了什么请求、设备回了什么数据、是否STALL。这套工具链的价值在于:它能帮你分清"是硬件控制器没响应,还是驱动程序回了错误描述符"。
比如典型的STALL问题,在wireshark里会看到主机反复发同一个请求,设备一直STALL。这时多半是setup回调里对某个请求的处理返回了-EOPNOTSUPP,或者描述符数据格式不对导致host端校验失败。
4.3 结合逻辑分析仪排查硬件层问题
司,如果dmesg显示UDC驱动已经注册、usbmon也抓到主机在枚举,但设备就是"无法识别",那就要考虑物理层问题了。USB D+上拉电阻、时钟频率、VBUS检测这些硬件因素,都有可能让设备停在Reset状态。此时usbmon是抓不到任何包的——因为链路根本没起来。这种问题用逻辑分析仪看D+/D-上的包最直接。
实际项目中,我发现70%的"枚举失败"都是硬件问题引发的,剩下的30%才是描述符配置错误。所以排查顺序建议是:先看硬件(换线、换口、测D+/D-波形),再抓总线包,最后才去抠代码。
5. 几个典型的翻车现场和解决思路
5.1 UDC名称不对:怎么绑定都不成功
很多新手最后一步echo ci_hdrc.0 > UDC报错,就是因为不了解UDC名称是动态生成的。不同平台的名字不一样,有叫dwc3的,有叫musb-hdrc的,有叫ai_musb的。正确做法是先ls /sys/class/udc/,拿到准确的UDC名字。有的平台支持多UDC,还要确认你用的是哪一个控制器。
5.2 Mass Storage识别成raw设备,分区表不生效
functions/mass_storage.0/lun.0/file指向的是一个块设备(比如/dev/mmcblk0p1),如果它是个没有分区表的裸分区,PC端会把它识别成"未格式化磁盘"。这是正常的,不是bug。另外如果 lUN 的removable属性设为1,Windows会把它当移动设备,弹出和刷新频率都会更高,对SD卡这类介质反而不友好。建议固定介质设0,可移动介质设1。
5.3 复合设备配置顺序影响枚举
在同一份configfs里同时挂多个function(比如mass_storage加acm加rndis),配置顺序会影响接口描述符的排列顺序。Windows对接口顺序敏感,有些组合在Linux下枚举正常、插到Windows就报"设备描述符请求失败"。解决方法是调整ln -s的先后顺序,把最"标准"的类放前面。
5.4 端点不够用,复合设备发挥不稳定
复合设备每个function都要占用端点和buffer,而SoC的UDC端点资源是有限的。当你同时挂三个以上function时,容易出现can't find endpoint或者No more endpoints available的报错。这跟写普通驱动不一样,不能自己指定端点号,必须依赖usb_ep_autoconfig自动分配。碰到这种问题,只能砍掉不用的function,或者换用带更多端点的UDC。
5.5 字符串描述符不显示或不规范
如果PC端看不到厂商名和产品名,多半是忘了建strings/0x409目录,或者没把字符串目录和配置目录关联。每个配置也要有自己的strings/0x409/configuration。还有一点:string descriptor里的语言ID0x409不能随意改,内核默认只支持USB_GADGET_STORAGE_NUM_BUFFERS这个配置依赖的几种语言。
5.6 驱动卸载后无法重新加载
gadget驱动在rmmod后,如果有些请求没释放会导致refcount不为0,模块卸载不干净,再次insmod时提示Resource temporarily unavailable。这个问题的根源多半是没在unbind回调里释放usb_request和ep。理解为:usb_gadget_driver_unregister会触发unbind,但不会替你回收dma缓冲区,忘了释放就等着二次加载翻车。
5.7 USB3.0口上速率不对,枚举成HighSpeed而不是SuperSpeed
如果你的gadget驱动希望跑SuperSpeed,除了设置max_speed = USB_SPEED_SUPER,还得确保配置描述符里的bcdUSB是0x0300,并且真的实现了相应的SuperSpeed端点描述符。很多时候代码是从g_serial拷贝的,里面只写了FullSpeed/HighSpeed描述符,插到USB3.0口当然只能以HighSpeed枚举。这事一开始没注意,排查花了我一晚上。
6. 在内核源码里快速定位gadget问题的路径总结
最后分享一个我自己的排查路径,省得每次从头翻:
- 先看硬件:
dmesg | grep usb有没有UDC驱动加载记录,没加载就先查设备树。 - 看注册:
ls /sys/class/udc/是否有控制器,没有就查kernelconfig有没有开CONFIG_USB_GADGET。 - 看配置:
cat /sys/kernel/config/usb_gadget/g1/UDC是否已经绑定,绑错了就清空重来。 - 看端点:
cat /sys/kernel/debug/usb/ci_hdrc.0/endpoints确认资源分配是否正常。 - 抓包:usbmon + wireshark看枚举过程卡在哪一步。
这套路径几乎能覆盖90%的gadget问题。剩下10%,多数是硬件电气问题或者SoC errata,那种就只能拿着示波器对着D+/D-慢慢熬了。