第2章 Linux USB子系统框架
做USB gadget驱动开发,第一件事就是要搞明白Linux里USB的软件体系是怎么搭起来的。
说白了,这就像你要装修一栋楼,得先知道承重墙在哪、水电管线怎么走的。否则你糊里糊涂就开始敲墙,最后可能整栋楼都塌了。
Linux USB子系统分三层:USB核心、主机控制器驱动、设备驱动。这三层各司其职,又互相配合。我习惯把核心层看作“交通枢纽”,主机控制器是“高速公路”,设备驱动则是“收费站”。你想想看,没有枢纽,车流就乱了;没有路,车没法跑;没有收费站,车辆就没法有序上下高速。
USB核心层 - 粘合剂与交通枢纽
USB核心层是Linux内核管理所有USB设备的中枢。它负责三大块:
- 设备枚举:当USB设备插上时,核心层负责读取设备的描述符,分配地址,加载合适的驱动程序。
- 数据传输管理:它定义了URB(USB Request Block)这个数据结构,所有USB数据传输都通过URB完成。
- 驱动注册与匹配:为USB设备驱动和USB主机控制器驱动提供统一的注册接口。
我个人认为,理解USB核心层的关键,就是搞懂它如何把硬件细节藏起来,给上层提供一个干净的接口。
核心数据结构: usb_device 代表一个物理设备,usb_interface 代表设备的一个功能接口,usb_driver 代表一个驱动程序。这三个结构体就是你写驱动时打交道最多的对象。
主机控制器层 - 硬件层面的总指挥
主机控制器驱动,就是直接和USB硬件寄存器打交道的代码。
Linux支持多种主机控制器规范:
| 协议标准 | 速度 | 常见硬件 |
|---|---|---|
| OHCI | USB 1.1 全速/低速 | 早期嵌入式芯片,如S3C2440 |
| UHCI | USB 1.1 全速/低速 | Intel早期主板 |
| EHCI | USB 2.0 高速 | 大多数PC和SoC |
| XHCI | USB 3.0/3.1 超高速 | 现代PC和高端ARM芯片 |
我记得早期内核里,这四种控制器驱动是完全独立的代码。后来社区引入了HCD(Host Controller Driver)抽象层,把共性抽出来。嗯,这里要注意:
在Gadget驱动开发中,我们一般不用关注主机控制器细节。但当你做Gadget+Host双角色切换时,一定要确认硬件是否支持OTG,以及对应的驱动是否加载正确。我曾经在一个项目里遇到设备插入后枚举失败,排查了半天,最后发现是EHCI驱动没编进内核。
设备驱动层 - 让硬件干活的人
这一层就是我们自己写驱动的地方。
Linux USB设备驱动分为两类:
- USB Gadget驱动:让设备模拟成U盘、网卡、串口等。这是本课程的重点。
- USB Host侧驱动:驱动连接到主机的USB设备,比如键盘、鼠标、存储设备。
对于Gadget驱动,核心接口是 struct usb_gadget_driver。你只需要填充回调函数,然后注册到内核。
static struct usb_gadget_driver my_gadget_driver = {
.function = my_gadget_bind,
.unbind = my_gadget_unbind,
.setup = my_gadget_setup,
.disconnect = my_gadget_disconnect,
.driver = {
.name = "my_gadget",
.owner = THIS_MODULE,
},
};
说白了,核心层帮你把URB的收发、端点管理等脏活累活都干了。你只需要关注怎么响应主机的请求。
个人习惯:写Gadget驱动时,先仔细看usb_gadget_driver结构体的注释。里面每个回调在什么时机调用,文档里写得很清楚。我刚开始做时,就是没注意.setup回调的参数含义,导致控制传输解析一直出错。
三层架构如何协同工作
数据流是这样的:
- 主机发送一个USB请求(比如读U盘的一个扇区)。
- 主机控制器层把这个请求组装成USB事务,通过总线发送出去。
- 在Gadget端,硬件收到数据后触发中断。
- USB核心层(Gadget侧)解析中断,调用你注册的
.setup回调。 - 你在回调里处理请求,构造响应数据,通过
usb_ep_queue把数据发回去。
每个层次只负责自己那一块。核心层不关心你用的是EHCI还是XHCI,驱动层不关心数据是怎么在总线上传输的。
为了让你更直观地理解这三层的关系,我画了一张框架图:
这张图我画得比较简洁。你仔细看,用户空间和内核空间之间有明显的界限。设备驱动调用核心层的API,核心层再调用主机控制器层的接口。反过来,数据从硬件上来时,也是逐层回调。
一个小技巧:调试Gadget驱动时,可以在USB核心层的drivers/usb/gadget/目录下加打印。我遇到疑难杂症时,经常在udc-core.c的usb_gadget_udc_start函数里加dev_dbg,看驱动注册和启动的完整流程。
一个常见的误解
很多人以为写Gadget驱动就是直接操作硬件寄存器。其实不是。
你写的驱动跑在设备驱动层,调用的是USB核心提供的API,比如usb_ep_enable、usb_ep_queue。真正的硬件操作,是由主机控制器层(在Gadget端通常叫UDC驱动)完成的。
举个例子,你要发送一个数据包:
- 你的驱动调用
usb_ep_queue,把URB交给核心层。 - 核心层把请求转发给UDC驱动。
- UDC驱动把数据写入硬件FIFO,启动DMA传输。
所以,你写的驱动实际上是在和核心层对话,而不是直接和硬件寄存器打交道。这个思路捋顺了,后面学起来会轻松很多。