第2章:Linux USB驱动栈架构 —— 从硬件到应用的“快递链”
聊USB驱动,绕不开它的分层架构。在调试USB OTG之前,你得先摸透这四层骨架。我个人习惯把这套架构理解成一条“快递链”——数据从用户的手指(硬件)传到屏幕里的进度条(应用),中间要经过好几个,每一站都有专人负责。
USB驱动栈从下往上,依次是:USB核心层、HCD(主机控制器驱动)、UDC(设备控制器驱动),以及最上层的Gadget框架。你可能会有疑问:OTG设备既当主机又当设备,那这两条路径在栈里怎么共存?别急,这张图能帮你理清脉络。
2.1 USB核心层:交通调度中心
USB核心层位于中间,是驱动栈的枢纽。它不直接操作硬件,而是提供一套统一的数据结构和API,供上层的Gadget和下层的HCD/UDC调用。我常把它比作交通调度中心——它管理USB总线、设备、接口、端点,以及最重要的URB(USB Request Block)。
核心层干了哪些事?
- 设备枚举:当设备插入后,由核心层调度HCD做地址分配、配置解析。
- URB管理:USB通信的最小单元URB由核心层分配、提交、回收。这层出问题,往往直接crash。
- 电源管理:挂起、唤醒、远程唤醒的协议步骤全部由核心层控制。
- 事件通知:硬件产生的中断和事件,需经核心层回调给驱动。
hub_port_connect_change 或 USB device not accepting address,问题多半出在核心层的枚举阶段。我排查时习惯先检查 /sys/kernel/debug/usb/devices 看设备描述符是否读全。
2.2 HCD:主机侧的马达
HCD是Host Controller Driver的缩写。它位于核心层下方,专门负责跟主机控制器(比如EHCI、xHCI)打交道。作为主机时,所有的USB传输请求(URB)最终都由HCD翻译成硬件可执行的Transfer Descriptor(TD)。
我见过坑最多的一个地方,就是HCD里的DMA映射。内核里 usb_hcd_map_urb_for_dma 负责把URB里的buffer映射给硬件DMA。如果你的平台IOMMU没配好,或者buffer在cache里没刷干净,就等着数据错乱吧。我记得有一次调试U盘写文件丢数据,折腾了一天,最后发现是DMA映射传错了长度。
HCD的关键职责:
- 创建和销毁主机控制器实例(root hub就是一个虚拟hub)。
- 将URB分解为硬件能处理的TD或TRB。
- 处理中断、完成回调、错误恢复。
- 实现
hc_driver结构体里的函数指针——这是HCD与核心层的契约。
2.3 UDC:设备侧的反向齿轮
UDC(USB Device Controller)驱动与HCD对称,但方向相反。当你的设备作为USB从设备(比如被PC识别为网卡、串口、U盘)时,真正干活的是UDC。它把Gadget框架生成的请求(usb_request)转换成硬件的端点描述符和DMA传输。
这么说吧,HCD是“我主动发请求给别人”,UDC则是“我等待别人发请求给我”。UDC驱动里最常见的问题是什么?端点停止和stall处理。协议要求设备在遇到错误时必须stall端点,但有些硬件实现很奇怪,你发了stall信号后还得手动重置端点状态机,不然之后的数据传不出去。我曾在一个Chipidea控制器上被这个坑了整整两天。
2.4 Gadget框架:设备侧的“乐高”
Gadget框架是USB驱动栈里最灵活、也最容易让人懵的一层。它运行在设备侧,作用是让你在软件层面把一个设备“组装”成任何标准USB设备——文件存储(Mass Storage)、网络设备(Ethernet Control Model)、串口(ACM/ECM),甚至自定义HID设备。
为什么说它像乐高?因为Gadget框架里提供了各种功能驱动(Function Driver),你可以声明一个 config,里面挂载一个或多个function。比如我突然想做一个“带串口的U盘”,那就组合 g_mass_storage 和 g_serial,再通过ConfigFS在用户态动态绑定。
| 功能驱动 | 内核模块 | 典型应用 |
|---|---|---|
| Mass Storage | g_mass_storage.ko | U盘、读卡器 |
| Ethernet (ECM) | g_ether.ko | USB网络共享 |
| Serial (ACM) | g_serial.ko | 调试串口、Modem |
| HID | g_hid.ko | 键盘、鼠标、游戏手柄 |
| Custom / Composite | 通过ConfigFS组合 | 自定义复合设备 |
2.5 实践中的边界与数据流
现在假设你的嵌入式设备正作为U盘,接入PC。那么整个数据流是怎么走的?
- PC发SCSI命令(比如读扇区) -> USB总线传输 -> 你的设备硬件(物理层)收到数据。
- 硬件触发中断 -> UDC驱动收到后,从端点FIFO读出数据,封装成
usb_request。 - UDC把
usb_request丢给USB核心层,核心层根据端点号和配置,找到对应的Gadget function。 - 在这个场景中,是
g_mass_storage的function驱动接收请求,它调用底层的文件系统或块设备接口,读取真正的存储数据。 - 数据原路返回:Gadget -> 核心层 -> UDC -> 硬件 -> USB线 -> PC。
你想想看,任何一个环节的DMA地址对齐没做好、端点配置不对、或者usb_request的complete回调没注册,都会让传输卡住。我调试时最常干的,就是在每个驱动层入口加一个 dev_dbg 打印请求的序号和状态码,配合 usbmon 抓USB总线报文,一下就能定位到是哪层丢包或崩了。
drivers/usb/common/usb-otg-fsm.c 里加了一个role切换的日志,才抓到问题。
最后总结一张检查清单,你在调试USB驱动栈时,按这四层逐级过一遍,大部分问题都能缩小范围。
| 层次 | 常见症状 | 优先检查点 |
|---|---|---|
| USB核心层 | 枚举失败、地址分配超时 | /sys/kernel/debug/usb/devices、URB超时 |
| HCD | 传输超时、DMA错误、root hub不枚举 | DMA掩码、IOMMU配置、hc_driver 回调 |
| UDC | 无法被Host识别、端点stall异常 | 端点描述符、UDC IRQ、usb_ep_queue 返回值 |
| Gadget框架 | 功能驱动加载失败、复合设备配置无效 | ConfigFS绑定、function驱动依赖模块、bind_config |