3. Linux USB子系统架构:USB核心层、USB主机控制器驱动、USB设备驱动
好,咱们今天来聊聊Linux USB子系统的整体架构。说实话,我刚接触USB驱动开发那会儿,看着那一堆代码也挺懵的。后来我画了一张图,把整个框架理了一遍,才慢慢摸到门道。
Linux USB子系统,说白了就是三层结构:USB核心层、USB主机控制器驱动、USB设备驱动。这三层各司其职,又紧密配合。我习惯把核心层比作「交通指挥中心」,主机控制器驱动是「高速公路」,设备驱动就是「收费站」。
核心要点:USB子系统采用分层设计,上层驱动不需要关心底层硬件细节,底层驱动也不需要知道上层在传输什么数据。这种解耦设计,让开发变得清爽很多。
3.1 USB核心层——承上启下的枢纽
USB核心层,我习惯叫它「USB Core」。它不直接跟硬件打交道,也不管你的设备是鼠标还是U盘。它的任务就一个:把上层驱动和下层控制器驱动串起来。
核心层主要干这几件事:
- 设备枚举:当USB设备插上时,核心层负责读取设备描述符、分配地址、配置端点。嗯,这个过程我调试过很多次,每次看到枚举成功,心里就踏实了。
- URB管理:URB(USB Request Block)是USB数据传输的基本单元。核心层负责URB的分配、提交、回收。说白了,就是数据快递的中转站。
- 驱动匹配:核心层维护着一张驱动表,当新设备插入时,它会遍历所有注册的usb_driver,找到匹配的驱动并调用probe()。
- 热插拔事件:设备插入或拔出时,核心层会生成uevent,通知用户空间和内核空间的相关模块。
我的小技巧:调试USB枚举问题时,我经常在核心层的hub_event()函数里加printk。别看这方法土,但真的管用。我曾经靠这个定位过一个USB3.0兼容性问题,折腾了两天,最后发现是hub初始化时序不对。
3.2 USB主机控制器驱动——硬件的翻译官
USB主机控制器驱动(HCD),是直接跟硬件寄存器打交道的底层驱动。不同的USB规范对应不同的控制器:
| 控制器类型 | 对应驱动 | 速度 | 特点 |
|---|---|---|---|
| UHCI | uhci-hcd | USB 1.1 (全速/低速) | Intel主导,软件开销大 |
| OHCI | ohci-hcd | USB 1.1 (全速/低速) | Compaq主导,硬件开销大 |
| EHCI | ehci-hcd | USB 2.0 (高速) | 只处理高速传输,低速转给companion控制器 |
| xHCI | xhci-hcd | USB 3.0/3.1/3.2 | 统一处理所有速度,支持更多端点 |
你想想看,为什么要有这么多控制器驱动?说白了,不同厂商的硬件实现不一样。Intel的UHCI和Compaq的OHCI,虽然都支持USB 1.1,但寄存器布局、传输描述符格式完全不同。
我记得有一次做ARM平台移植,板子上用的是DWC3控制器,但内核默认加载的是ehci-hcd。结果呢?设备根本枚举不出来。后来我查了datasheet才发现,DWC3需要xhci-hcd驱动。嗯,这种坑踩过一次就记住了。
注意:在调试USB主机控制器驱动时,一定要确认硬件对应的控制器类型。加载错误的HCD驱动,轻则设备不工作,重则内核panic。我曾经在客户现场遇到过这种情况,当时真是冷汗都下来了。
3.3 USB设备驱动——跟设备对话的人
USB设备驱动,才是我们平时写的最多的部分。它负责跟具体的USB设备「聊天」,理解设备的数据格式,完成用户想要的功能。
一个典型的USB设备驱动,需要实现这些回调:
- probe():设备匹配成功时调用。在这里初始化设备、分配端点、注册接口。
- disconnect():设备拔出时调用。清理资源、释放内存。
- suspend()/resume():电源管理相关。我刚开始做驱动时经常忽略这两个函数,结果设备休眠后唤不醒,被测试同事追着改bug。
来看一个最简单的USB设备驱动框架:
/* 一个简化的USB设备驱动示例 */
static int my_usb_probe(struct usb_interface *interface,
const struct usb_device_id *id)
{
struct usb_device *dev = interface_to_usbdev(interface);
struct usb_host_interface *iface_desc;
struct usb_endpoint_descriptor *endpoint;
int ret;
/* 获取接口描述符 */
iface_desc = interface->cur_altsetting;
/* 遍历端点,找到我们需要的 */
for (int i = 0; i < iface_desc->desc.bNumEndpoints; i++) {
endpoint = &iface_desc->endpoint[i].desc;
if (usb_endpoint_is_bulk_in(endpoint)) {
/* 找到了批量输入端点 */
dev_info(&interface->dev, "Found bulk IN endpoint\n");
}
}
/* 分配URB,准备数据传输 */
/* ... 实际项目中这里会有更多初始化代码 ... */
return 0;
}
static void my_usb_disconnect(struct usb_interface *interface)
{
/* 清理工作:释放URB、取消未完成的传输 */
dev_info(&interface->dev, "Device disconnected\n");
}
/* USB设备ID表 */
static const struct usb_device_id my_usb_table[] = {
{ USB_DEVICE(0x1234, 0x5678) }, /* VID:PID */
{ } /* 结束标记 */
};
MODULE_DEVICE_TABLE(usb, my_usb_table);
/* USB驱动结构体 */
static struct usb_driver my_usb_driver = {
.name = "my_usb_device",
.probe = my_usb_probe,
.disconnect = my_usb_disconnect,
.id_table = my_usb_table,
};
module_usb_driver(my_usb_driver);
这段代码看着简单,但实际项目中,probe()函数里往往要处理很多细节。比如端点方向判断、传输类型选择、缓冲区对齐等等。我习惯在probe()里加一些调试信息,把设备的VID/PID、端点信息都打印出来,方便后续排查问题。
避坑指南:写USB设备驱动时,千万别忘了检查端点描述符的有效性。我曾经遇到过一款山寨设备,它的端点描述符里bEndpointAddress字段居然填了0xFF,导致内核在urb提交时直接报错。后来我在probe()里加了端点有效性检查,才把这个坑填上。
3.4 三层之间的协作流程
这三层是怎么配合工作的?我举个例子你就明白了。
假设你插上一个U盘:
- HCD层检测到端口状态变化,通知核心层「有个设备插上来了」。
- 核心层开始枚举:复位设备、分配地址、读取设备描述符、配置描述符。这个过程会通过HCD层发送标准的USB请求。
- 核心层根据设备的bInterfaceClass等信息,查找匹配的驱动。比如U盘会匹配usb-storage驱动。
- 设备驱动的probe()被调用,驱动开始初始化,注册块设备或字符设备。
- 用户空间就能看到/dev/sda了,可以mount使用。
整个过程,HCD层负责「怎么传」,核心层负责「传什么」,设备驱动负责「传完怎么用」。各层各司其职,互不干扰。
总结一下:理解USB子系统架构,关键是记住三层之间的接口。核心层向上提供usb_driver注册接口,向下提供usb_hcd注册接口。你写设备驱动时,只需要关心usb_driver那一套API,不用管底层是EHCI还是xHCI。这种抽象,说白了就是Linux内核设计哲学的体现——「机制与策略分离」。
好了,这一章的内容就到这里。USB子系统的三层架构,是后续所有USB驱动开发的基础。把这个框架吃透了,后面写驱动就会顺手很多。
公众号:蓝海资料掘金营,微信deep3321