
从把开发板伪装成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端的UDCUSB 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.cU盘、f_hid.cHID设备、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. 不写内核代码也能实现gadgetconfigfs动态配置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和debugfsgadget调试第一反应永远是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属性设为1Windows会把它当移动设备弹出和刷新频率都会更高对SD卡这类介质反而不友好。建议固定介质设0可移动介质设1。5.3 复合设备配置顺序影响枚举在同一份configfs里同时挂多个function比如mass_storage加acm加rndis配置顺序会影响接口描述符的排列顺序。Windows对接口顺序敏感有些组合在Linux下枚举正常、插到Windows就报设备描述符请求失败。解决方法是调整ln -s的先后顺序把最标准的类放前面。5.4 端点不够用复合设备发挥不稳定复合设备每个function都要占用端点和buffer而SoC的UDC端点资源是有限的。当你同时挂三个以上function时容易出现cant 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-慢慢熬了。