第2章:Linux USB驱动栈架构 —— 从硬件到应用的“快递链”

聊USB驱动,绕不开它的分层架构。在调试USB OTG之前,你得先摸透这四层骨架。我个人习惯把这套架构理解成一条“快递链”——数据从用户的手指(硬件)传到屏幕里的进度条(应用),中间要经过好几个,每一站都有专人负责。

USB驱动栈从下往上,依次是:USB核心层、HCD(主机控制器驱动)、UDC(设备控制器驱动),以及最上层的Gadget框架。你可能会有疑问:OTG设备既当主机又当设备,那这两条路径在栈里怎么共存?别急,这张图能帮你理清脉络。

Linux USB OTG 驱动栈架构 用户应用 Gadget 框架 USB 核心层 HCD (主机侧) UDC (设备侧) USB OTG 控制器硬件 (DWC2 / DWC3 / Chipidea) 说明:OTG设备运行时,驱动栈会动态选择路径。作为Host时走HCD,作为Device时走UDC/Gadget。核心层负责仲裁与协议解析。 右侧深色节点内文字为 #ffffff,保证高对比。如需其他对比色,请遵循固定模板色板。

2.1 USB核心层:交通调度中心

USB核心层位于中间,是驱动栈的枢纽。它不直接操作硬件,而是提供一套统一的数据结构和API,供上层的Gadget和下层的HCD/UDC调用。我常把它比作交通调度中心——它管理USB总线、设备、接口、端点,以及最重要的URB(USB Request Block)。

核心层干了哪些事?

  • 设备枚举:当设备插入后,由核心层调度HCD做地址分配、配置解析。
  • URB管理:USB通信的最小单元URB由核心层分配、提交、回收。这层出问题,往往直接crash。
  • 电源管理:挂起、唤醒、远程唤醒的协议步骤全部由核心层控制。
  • 事件通知:硬件产生的中断和事件,需经核心层回调给驱动。
调试提醒: 如果你在dmesg里看到 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组合 自定义复合设备
一句话总结: HCD管“主机怎么发”,UDC管“设备怎么收”,Gadget管“收上来之后假扮成什么”,核心层管“它们之间的通信规则”。

2.5 实践中的边界与数据流

现在假设你的嵌入式设备正作为U盘,接入PC。那么整个数据流是怎么走的?

  1. PC发SCSI命令(比如读扇区) -> USB总线传输 -> 你的设备硬件(物理层)收到数据。
  2. 硬件触发中断 -> UDC驱动收到后,从端点FIFO读出数据,封装成 usb_request。
  3. UDC把 usb_request 丢给USB核心层,核心层根据端点号和配置,找到对应的Gadget function。
  4. 在这个场景中,是 g_mass_storage 的function驱动接收请求,它调用底层的文件系统或块设备接口,读取真正的存储数据。
  5. 数据原路返回:Gadget -> 核心层 -> UDC -> 硬件 -> USB线 -> PC。

你想想看,任何一个环节的DMA地址对齐没做好、端点配置不对、或者usb_request的complete回调没注册,都会让传输卡住。我调试时最常干的,就是在每个驱动层入口加一个 dev_dbg 打印请求的序号和状态码,配合 usbmon 抓USB总线报文,一下就能定位到是哪层丢包或崩了。

曾经踩过的坑: 一个OTG设备做Device时,U盘功能正常工作,但切到Host接U盘反而识别不了。排查发现Gadget框架注册时把UDC和核心层的权限搞混了——核心层认为当前是Device模式,所以把Host侧需要的root hub功能给disable了。后来在 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