1. 从一个真实场景说起:为什么我要啃USB协议栈
做嵌入式Linux开发的朋友大概率都遇到过这样的场景:板子插上USB设备,dmesg里刷出一堆日志,设备节点也出来了,但数据就是不通;或者更诡异的是,同一个U盘在PC上好好的,插到自研板子上枚举都过不去。这时候你去问别人,得到的回答往往是"看看USB驱动"——废话,我当然知道看驱动,问题是看哪一层?
USB协议栈在Linux内核里算是一个"看起来简单、用起来复杂、调起来要命"的子系统。它不像字符设备那样一条线走到底,而是分了主机侧(Host)和设备侧(Gadget)两大阵营,中间还夹着USB核心层(USB Core)、HCD(Host Controller Driver)、UDC(USB Device Controller)、各类Class驱动、Gadget Function等等。层次一多,出问题时定位就变成了"猜谜游戏"。
这篇内容我打算把Linux USB协议栈的框架从头到尾捋一遍,不是照本宣科翻译内核文档,而是按照一个实际开发者理解它的路径来组织:先搞清楚整体分层和数据流向,再逐层拆解关键结构体和注册流程,然后落到实操——怎么加打印、怎么看枚举过程、怎么排查最常见的几类问题。适合正在做USB驱动开发、调试USB外设、或者准备面试被问到USB子系统的朋友。看完之后,你至少能做到:拿到一份USB相关的内核日志,知道每一行大概来自哪一层,出了问题该往哪个方向查。
2. Linux USB协议栈的整体分层与设计思路
2.1 主机侧与设备侧:两条独立的栈
很多人初学USB协议栈时最大的困惑就是:为什么内核里drivers/usb/目录下既有host/又有gadget/,还有core/?它们之间是什么关系?
答案其实很直接:Linux USB子系统实际上是两套并行的栈,一套负责"我作为主机去控制别的USB设备"(Host侧),另一套负责"我作为一个USB设备被别的主机控制"(Gadget侧)。这两套栈共享的只有最底层的USB协议规范定义和部分核心数据结构,代码路径基本是分开的。
Host侧的典型场景:你的开发板作为主机,插U盘、插键盘、插4G模块。Gadget侧的典型场景:你的开发板作为设备,被PC识别成一个串口、一个网卡、一个U盘。现在很多SoC的USB控制器(比如DWC3、MUSB)是双角色的,通过OTG或Type-C的CC引脚来切换角色,但软件栈依然是两套。
这个设计的好处是职责清晰:Host侧关心的是"如何调度多个设备的带宽、如何管理枚举、如何给上层Class驱动提供统一接口";Gadget侧关心的是"如何响应主机的标准请求、如何把数据从Function层搬到UDC层"。两者混在一起反而会让代码难以维护。
2.2 主机侧的四层结构
Host侧的层次从下往上依次是:
- USB Host Controller Hardware:硬件控制器,比如xHCI(USB 3.x)、EHCI(USB 2.0高速)、OHCI/UHCI(USB 1.1)。
- HCD(Host Controller Driver):对应
drivers/usb/host/下的xhci-hcd.c、ehci-hcd.c等,负责把USB Core下发的请求翻译成控制器能理解的TD(Transfer Descriptor)或TRB(Transfer Request Block)。 - USB Core:
drivers/usb/core/,这是整个Host侧的中枢,负责设备枚举、配置管理、URB(USB Request Block)的分配与提交、驱动与设备的匹配。 - USB Class Drivers:
drivers/usb/class/、drivers/hid/usbhid/、drivers/usb/storage/等,具体处理某一类设备,比如mass storage、HID、CDC-ACM。
数据流向是这样的:上层Class驱动构造一个URB,提交给USB Core;USB Core把URB挂到对应设备的端点队列上,交给HCD;HCD把URB拆成硬件能执行的传输描述符,写寄存器触发传输;传输完成后硬件产生中断,HCD在中断处理里回收URB,回调上层完成函数。
2.3 设备侧的层次结构
Gadget侧从下往上:
- UDC(USB Device Controller):硬件控制器驱动,比如
dwc3-gadget.c、musb_gadget.c。 - Gadget Core:
drivers/usb/gadget/udc/下的udc-core.c,管理UDC的注册、Gadget Driver的绑定。 - Composite Layer:
drivers/usb/gadget/composite.c,负责把多个Function组合成一个复合设备,处理配置描述符的拼装。 - Function Drivers:
f_acm.c、f_mass_storage.c、f_ecm.c等,实现具体的功能。
设备侧的核心是"描述符"和"端点"。主机发来标准请求(GET_DESCRIPTOR、SET_CONFIGURATION等),Gadget Core和Composite层负责解析并回应,Function层负责具体端点的数据收发。
2.4 为什么这样分层
这个分层不是拍脑袋定的,背后有几个硬约束:
第一,硬件差异巨大。xHCI和EHCI的寄存器模型完全不同,但上层驱动不应该关心这些差异,所以必须有HCD这一层做抽象。
第二,设备类型繁多。USB-IF定义了上百种Class,如果每加一种设备就改Core,那Core会爆炸。所以Class驱动独立出来,通过usb_driver结构体注册,由Core负责匹配。
第三,Gadget侧需要动态组合。同一个硬件可能今天做串口,明天做网卡,后天做U盘,所以Composite层要能把Function像积木一样拼起来。
理解了这三条,再看代码就不会迷路。
3. 核心数据结构与注册流程拆解
3.1 usb_device、usb_interface与usb_driver
Host侧最核心的三个结构体是usb_device、usb_interface和usb_driver。
usb_device代表一个物理USB设备,对应内核里的一个struct device。它包含了设备描述符、配置描述符数组、当前配置、端点信息等。每个usb_device下面挂着若干个usb_interface,一个interface对应设备的一个功能。比如一个USB耳机可能有两个interface:一个Audio Control,一个Audio Streaming。
usb_driver是驱动开发者最常打交道的结构体,关键字段包括:
static struct usb_driver my_driver = { .name = "my_usb_driver", .id_table = my_id_table, .probe = my_probe, .disconnect = my_disconnect, };id_table是匹配表,里面用USB_DEVICE(vendor, product)或USB_INTERFACE_CLASS(class)这样的宏来声明本驱动支持哪些设备。内核在枚举到一个新设备时,会遍历所有已注册的usb_driver,用id_table去匹配,匹配成功就调用probe。
这里有个容易踩的坑:匹配的粒度是interface而不是device。也就是说,如果你的设备有多个interface,probe会被调用多次,每次传入一个usb_interface。很多新手在probe里做全局初始化,结果被调用多次导致资源重复申请。正确做法是把每个interface当作独立实例来处理。
3.2 URB:USB传输的载体
URB(USB Request Block)是Host侧数据传输的基本单位。你可以把它理解成"一个USB传输请求的快递单":上面写着发给哪个端点、什么传输类型、数据缓冲区在哪、多长、完成后回调哪个函数。
一个典型的URB提交过程:
struct urb *urb = usb_alloc_urb(0, GFP_KERNEL); usb_fill_bulk_urb(urb, udev, usb_sndbulkpipe(udev, ep), buf, len, my_complete, context); usb_submit_urb(urb, GFP_KERNEL);usb_submit_urb之后,URB就交给了USB Core,Core再交给HCD。传输完成后,HCD在中断上下文里调用my_complete回调。注意:完成回调运行在中断上下文,里面不能睡眠,不能调用可能引起调度的函数。如果需要做耗时操作,用工作队列或tasklet转一下。
URB的生命周期管理是USB驱动最容易出bug的地方。常见问题包括:URB提交后设备被拔掉,回调里访问已释放的内存;URB还没完成就usb_free_urb;在disconnect里忘记usb_kill_urb导致回调在设备结构体释放后触发。这些后面会专门讲。
3.3 Gadget侧的usb_gadget与usb_ep
Gadget侧的核心结构体是usb_gadget和usb_ep。
usb_gadget代表一个UDC实例,关键字段有ep_list(端点链表)、speed(当前速度)、ops(操作函数集)。usb_ep代表一个端点,关键字段有name、maxpacket、ops。
Gadget Driver通过usb_gadget_probe_driver注册自己,UDC Core在合适的时机调用Gadget Driver的bind函数。在bind里,Gadget Driver要遍历gadget->ep_list,找到自己需要的端点并保存下来。
端点的命名规则是ep1in、ep1out这样的形式,in表示设备到主机,out表示主机到设备。注意这是从主机视角命名的,初学者容易搞反。
3.4 描述符的组织方式
USB设备的描述符是分层的:设备描述符 -> 配置描述符 -> 接口描述符 -> 端点描述符。在Gadget侧,这些描述符通常用宏来定义:
static struct usb_device_descriptor device_desc = { .bLength = USB_DT_DEVICE_SIZE, .bDescriptorType = USB_DT_DEVICE, .bcdUSB = cpu_to_le16(0x0200), .bDeviceClass = USB_CLASS_PER_INTERFACE, .idVendor = cpu_to_le16(0x1d6b), .idProduct = cpu_to_le16(0x0104), .bNumConfigurations = 1, };Composite层会把这些描述符拼成一个完整的配置描述符块,在主机发GET_DESCRIPTOR时一次性返回。这里的关键是描述符的长度和偏移必须严格正确,一个字节错了主机就可能枚举失败。调试时可以用lsusb -v在PC侧看实际枚举出来的描述符,和代码里定义的对比。
4. 实操:从枚举日志到驱动加载的完整追踪
4.1 打开USB Core的调试日志
内核里USB Core有比较完善的调试日志,通过CONFIG_USB_DEBUG(老内核)或动态调试(新内核)打开。新内核推荐用dynamic debug:
# 挂载debugfs mount -t debugfs none /sys/kernel/debug # 打开usb core的所有调试信息 echo 'file drivers/usb/core/* +p' > /sys/kernel/debug/dynamic_debug/control # 打开hcd的调试信息 echo 'file drivers/usb/host/* +p' > /sys/kernel/debug/dynamic_debug/control打开后dmesg里会看到非常详细的枚举过程,包括每一次控制传输的请求类型、请求码、数据长度、返回状态。这是排查枚举问题的第一手资料。
4.2 一次完整枚举的日志解读
插上一个USB设备,典型的日志序列是这样的:
usb 1-1: new high-speed USB device number 2 using xhci_hcd usb 1-1: New USB device found, idVendor=0781, idProduct=5567 usb 1-1: New USB device strings: Mfr=1, Product=2, SerialNumber=3 usb 1-1: Product: Cruzer Blade usb 1-1: Manufacturer: SanDisk usb 1-1: SerialNumber: 4C530001120523116195 usb-storage 1-1:1.0: USB Mass Storage device detected scsi host0: usb-storage逐行解读:
- 第一行:HCD检测到端口状态变化,复位后设备以high-speed接入,分配了设备号2。
- 第二行:设备描述符读取成功,打印VID/PID。
- 第三到六行:读取字符串描述符,打印厂商、产品、序列号。
- 第七行:
usb-storage驱动匹配到了interface 0,开始probe。 - 第八行:SCSI子系统注册了一个host。
如果枚举卡在某一步,比如只打印了第一行就没有后续,那问题通常出在控制传输阶段:可能是硬件信号完整性问题、可能是描述符返回长度不对、也可能是HCD的TD调度有问题。
4.3 用usbmon抓USB总线数据
usbmon是内核提供的USB抓包工具,能看到总线上实际传输的每一个包。用法:
# 加载usbmon模块 modprobe usbmon # 查看有哪些总线 ls /sys/kernel/debug/usb/usbmon/ # 抓1号总线的数据 cat /sys/kernel/debug/usb/usbmon/1u > usb.log抓到的数据可以用Wireshark打开(Wireshark支持usbmon格式),能看到SETUP包、DATA包、ACK包的完整内容。排查"主机发了请求但设备没响应"这类问题时,usbmon是终极武器——它能明确告诉你问题出在主机侧还是设备侧。
4.4 在probe里加打印定位驱动问题
如果设备枚举成功了但功能不正常,下一步是在驱动的probe里加打印:
static int my_probe(struct usb_interface *intf, const struct usb_device_id *id) { struct usb_device *udev = interface_to_usbdev(intf); int ep_in, ep_out; dev_info(&intf->dev, "probe: VID=%04x PID=%04x\n", le16_to_cpu(udev->descriptor.idVendor), le16_to_cpu(udev->descriptor.idProduct)); ep_in = intf->cur_altsetting->endpoint[0].desc.bEndpointAddress; ep_out = intf->cur_altsetting->endpoint[1].desc.bEndpointAddress; dev_info(&intf->dev, "ep_in=0x%02x ep_out=0x%02x\n", ep_in, ep_out); return 0; }注意endpoint[0]不一定是IN端点,顺序取决于描述符里的定义。稳妥的做法是遍历intf->cur_altsetting->endpoint数组,根据bEndpointAddress的最高位判断方向。
4.5 Gadget侧的功能验证
Gadget侧验证相对麻烦,因为需要另一台机器做主机。常用方法是配置一个g_serial或g_mass_storage,然后在PC上看是否识别:
# 加载g_serial模块 modprobe g_serial # 在PC上应该能看到一个新的串口设备 # Linux下是/dev/ttyACM0,Windows下是COM口如果PC没反应,先在开发板上dmesg看UDC是否成功注册、Gadget Driver是否bind成功。常见问题是UDC驱动没加载、或者/sys/class/udc/下没有控制器实例。
5. 常见问题与排查技巧实录
5.1 枚举失败类问题速查
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 完全无日志 | 硬件未检测到 | 检查VBUS、D+/D-接线、时钟 |
| 有"new device"但无后续 | 控制传输失败 | usbmon抓包看SETUP阶段 |
| 读到描述符但长度错误 | 描述符定义有误 | 对比lsusb -v输出 |
| 枚举成功但驱动不probe | id_table不匹配 | 检查VID/PID/Class |
| probe成功但功能异常 | 端点配置或URB问题 | 检查端点地址、传输类型 |
5.2 URB相关的经典坑
坑一:在disconnect里忘记kill URB。设备拔掉后,已提交的URB会以-ESHUTDOWN完成,回调仍会被调用。如果回调里访问了已经释放的私有数据结构,就是use-after-free。正确做法:
static void my_disconnect(struct usb_interface *intf) { struct my_dev *dev = usb_get_intfdata(intf); usb_set_intfdata(intf, NULL); if (dev) { usb_kill_urb(dev->urb); // 确保所有URB完成 usb_free_urb(dev->urb); kfree(dev); } }usb_kill_urb会等待URB完成或取消,返回后就不会再有回调了。
坑二:在完成回调里睡眠。完成回调在中断上下文,kmalloc(GFP_KERNEL)、mutex_lock、msleep都会出问题。需要睡眠的操作丢到工作队列。
坑三:URB重复提交。同一个URB在未完成前不能再次提交,否则内核会打印警告并拒绝。如果需要连续传输,在完成回调里重新提交。
5.3 Gadget侧的常见问题
Gadget侧最常见的问题是端点资源不足。一个UDC的端点数量有限,如果Composite里配置了太多Function,每个Function又要多个端点,就会bind失败。排查方法是看/sys/kernel/debug/usb/<udc>/下的端点使用情况,或者直接在bind失败时打印gadget->ep_list里还剩哪些端点。
另一个常见问题是描述符不匹配。Composite层会根据Function的fs_descriptors、hs_descriptors、ss_descriptors来拼装不同速度下的配置描述符。如果只定义了高速描述符,全速主机来枚举时就会失败。稳妥做法是三种速度都定义,或者用usb_copy_descriptors做转换。
5.4 性能调优的几个点
如果USB传输吞吐上不去,可以从这几个方向查:
- URB大小:批量传输的URB缓冲区太小会导致频繁中断,建议至少4KB以上。
- URB数量:单个URB串行提交会有等待间隙,可以用多个URB做流水线。
- 中断合并:xHCI支持中断合并(IMOD),调整
/sys/bus/pci/devices/.../imod_interval可以减少中断频率。 - 内存对齐:DMA缓冲区最好按cache line对齐,避免cache一致性问题导致的性能损失。
6. 我踩过的几个真实坑与经验总结
第一个坑是把interface当成device。早期写一个USB转串口驱动时,我在probe里申请了一个全局的接收缓冲区,结果设备有两个interface,probe被调用两次,第二次把第一次的缓冲区覆盖了,数据就乱了。后来改成每个interface一份私有数据,用usb_set_intfdata挂上去,问题解决。这个教训是:USB驱动的实例粒度是interface,不是device。
第二个坑是在完成回调里直接处理业务逻辑。有个项目需要在收到数据后做协议解析,解析函数里用了mutex,结果在中断上下文里直接崩了。后来改成回调里只做数据拷贝和唤醒工作队列,解析放到工作队列里做。虽然多了一次上下文切换,但稳定性完全不一样。
第三个坑是Gadget描述符的wTotalLength算错。Composite层拼装描述符时,wTotalLength是所有描述符长度之和。我手动改了一个Function的描述符,忘了更新wTotalLength,结果主机枚举时读到的配置描述符不完整,直接枚举失败。这个问题的隐蔽性在于:代码编译没问题,运行时也不报错,就是主机不认。后来养成了习惯,改完描述符一定用lsusb -v在PC侧核对一遍。
第四个坑是忽略了USB 3.0的SS描述符。一个项目从USB 2.0升级到USB 3.0,硬件换了xHCI控制器,但Gadget侧只定义了hs_descriptors,没有ss_descriptors。结果设备插到USB 3.0口上时,主机尝试以SuperSpeed枚举失败,回退到HighSpeed才成功。虽然功能能用,但性能没上去。后来补了SS描述符,才真正跑在USB 3.0速率上。
最后分享一个调试习惯:每次改USB相关代码,先在PC侧用lsusb -v和dmesg确认枚举正常,再测功能。枚举是所有USB功能的基础,枚举不过,后面都是白搭。把枚举日志看熟,能省掉大量瞎猜的时间。