本文是对相关 USB 驱动技术文章的补充与拓展。内容围绕 Linux USB 体系结构展开,深入补充了四个核心板块:USB 总线 的设备查询与 Hub 传输机制;usb-skeleton 框架 中的 DMA 机制与读写逻辑;USB 子系统核心 的事件队列(usb_hub_wq)与主控初始化流程;以及 USB 鼠标驱动 中 usb_mouse 与 input_dev 的结构体绑定和 URB 中断处理细节,旨在补全原文章的底层技术实现,让读者更好地掌握 USB 驱动框架。
1. USB总线
补充知识点:
1.1 设备查询链路
将 USB 的物理传输机制与 Linux 内核的软件响应流程结合起来,可以完整还原一个 USB 设备从"插入端口"到"完成枚举"的全链路工作机制。
系统的核心设计哲学可以概括为:物理总线层靠"硬件自动轮询",系统内核层靠"中断异步驱动"。
全链路协同工作流程
text
======================= 1. 物理总线层 (纯硬件自动维护) =======================
[Host 控制器硬件 (xHCI/EHCI)] ───(每 8ms 自动发 IN 包)───> [Hub 芯片]
│
┌────────────────────────────────────────────────────────────┘
├─►【无插拔事件】:Hub 回复 NAK ───> 物理层握手结束,无 DMA 写入,CPU 0% 占用 (休眠/做其他事)
│
└─►【有插拔事件】:物理引脚检测到电平变化 ───> Hub 寄存器置位
│
======================= 2. 硬件与内核交接 (DMA + IRQ) =======================
[Host 控制器再次发 IN 包] ───> [Hub 回复 DATA 包]
│
(Host 控制器通过 DMA 将数据写入内存)
│
(Host 控制器拉高中断线,触发 CPU 物理中断 IRQ)
│
======================= 3. 内核软件层 (事件驱动异步处理) =====================
[CPU 响应 IRQ] ───> [进入 HCD ISR 中断服务程序]
│
(解析为 Hub 端口状态变更,完成 URB)
│
(唤醒内核工作队列 Workqueue 中的 hub_event 线程)
│
[hub_event 线程] ───> 执行复位端口、分配地址、读取描述符、匹配设备驱动 (枚举流程)
│
[处理完毕,线程重新进入休眠]
关键协同节点深度剖析
1. 硬件轮询与 CPU 解耦
USB 规范规定只能由 Host 发起通信,因此在物理层必须存在"周期性查询"。
- 硬件分工 :这一查询完全由 Host 控制器的物理 PHY 状态机与 DMA 自动执行(如以 8ms 为周期向 Hub 的中断端点发送
IN包)。 - 对 CPU 的保护 :在没有插拔事件发生时,Hub 始终回复
NAK包。此过程在芯片硬件电路内部消化,既不产生数据写入,也不向 CPU 发送中断信号,彻底解耦了总线物理轮询与 CPU 算力。
2. 从物理事件到内核中断的转接
当物理端口发生改变(如插入 U 盘)时,Hub 芯片内部寄存器置位:
- 当 Host 控制器硬件下一次发出
IN包时,Hub 回复DATA包。 - Host 控制器将返回的状态数据通过 DMA 自动写入 预先分配好的系统内存。
- 数据搬运完成后,Host 控制器芯片硬件拉高中断线,向 CPU 发送 物理 CPU 中断(IRQ)。此时,内核才正式被介入。
3. 软件层的异步延迟处理
为了避免在中断上下文(ISR)中占用 CPU 时间过长,Linux 内核采用了典型的"上下半部"异步设计:
- 上半部(HCD ISR):快速识别并接收该 URB,通知内核"Hub 状态改变了",随后立即退出中断。
- 下半部(Workqueue 线程) :内核唤醒处于阻塞休眠状态的
hub_event()异步线程。由该线程在后台完成复位端口、分配地址、读取描述符及绑定驱动等相对耗时的枚举操作。 - 复位休眠 :枚举完成后,
hub_event()重新进入阻塞休眠状态,等待下一次硬件中断的唤醒。
总结:
这种"硬件轮询 + 内核中断"结合的设计,巧妙兼顾了 USB 规范的严苛要求与操作系统的高效运行:
- 总线合规性 :依靠 Host 控制器硬件逻辑 定期轮询,满足了 USB 严格的单主机主从通信规范。
- 系统高性能 :依靠 物理中断 + 内核工作队列 的事件驱动机制,保证了 CPU 在平时零开销、有事件时按需唤醒,绝不会因为死循环轮询而浪费算力与功耗。
1.2 集线器与根集线器的传输差异
在 Linux USB 主机子系统(USB Core & HCD)的设计中,将 URB(USB Request Block)的传输和处理明确划分为根集线器(Root Hub)与 普通外部集线器/设备(Non-Root Hub)两种情况,主要是由硬件架构的物理差异 与内核软件抽象的统一性需求决定的。
1. 硬件本质的差异:物理总线 vs 芯片内部寄存器
-
普通设备 / 外部 Hub(Non-Root Hub)
- 硬件实体 :通过物理 USB 线缆( D + / D − D+/D- D+/D− 或 Type-C 差分线)连接在板外的实体硬件。
- 通信方式:必须遵循严格的 USB 物理层与链路层协议。Host 控制器(如 xHCI/EHCI)需要通过 DMA 将 URB 中的数据打包,在物理总线上产生真实的电平信号(发包、收包、Handshake 应答)。
-
根集线器(Root Hub)
- 硬件实体 :它是集成在 Host 控制器芯片内部(SoC 内部)的逻辑电路,并不是一个物理上接在总线上的外设。
- 通信方式 :Root Hub 根本没有物理 USB 线缆,它的端口状态、控制寄存器(如端口复位、供电使能、插拔检测状态)直接映射在 Host 控制器的 MMIO(内存映射 I/O)地址空间或寄存器 中。
2. 内核调度的差异:软件仿真 vs 硬件 DMA 调度
当上层驱动(如 hub 驱动)发起一个 URB 请求时,USB Core 会根据目标设备做出不同的路由分发:
① 对非 Root Hub 的 URB 处理
URB 提交后,HCD 驱动会将其翻译为硬件 Controller 能理解的传输描述符(TRB/TD),写进系统的内存环形缓冲区,然后启动 DMA 引擎 往物理总线上发包。传输完成后通过硬件中断返回状态。
② 对 Root Hub 的 URB 处理(软件仿真 / Emulation)
既然 Root Hub 只是 Host 控制器内部的一堆寄存器,如果像普通设备那样去发物理包,在硬件上根本行不通。
因此,Linux 内核中的 HCD 驱动为 Root Hub 做了软件仿真(Virtual Hub / Emulation):
- 当
hub驱动给 Root Hub 提交控制 URB(如读取描述符、复位端口)时,HCD 驱动完全不触发物理 DMA 传输。 - HCD 会直接拦截这个 URB,用 CPU 直接读取 Host 控制器的内部 MMIO 寄存器,将寄存器里的状态信息(如端口是否有设备插入)手动拼装成标准 USB 描述符格式,填入 URB 的缓冲区中。
- 接着,HCD 手动将
urb->status设为0,并直接调用/模拟触发urb->complete()回调函数。
3. 设计哲学:用统一的接口屏蔽底层硬件差异
既然 Root Hub 是芯片内部寄存器,为什么不单独为它写一套 API,而要强行让它也走 URB 流程呢?
- 软件架构的统一性(Software Abstraction) :
在 Linux 内核上层看来,无论是芯片内部的第一个端口(Root Hub Port),还是用户通过外接 Hub 扩展出来的第 10 个端口,在业务逻辑上都必须是同等对待的 USB Hub。 - 复用集线器驱动代码(
drivers/usb/core/hub.c) :
如果将 Root Hub 和外部 Hub 区分对待,Linux 内核就必须写两套 Hub 设备枚举与状态监听逻辑。而通过将 Root Hub 的寄存器操作包装成 URB 响应 ,上层的hub驱动就可以用完全相同的一套状态机、完全相同的hub_event()工作队列来同时管理 Root Hub 和外部 Hub。
总结:
Linux 将 URB 传输划分为根集线器和非根集线器,本质上是一种"用软件仿真(Emulation)统一硬件差异"的设计:
| 维度 | 非根集线器 (External Device / Hub) | 根集线器 (Root Hub) |
|---|---|---|
| 物理本质 | 芯片外部的物理外设 | Host 控制器芯片内部的逻辑寄存器 |
| 数据传输 | 走真实物理总线 + DMA 硬件发包 | CPU 直接读写 MMIO 寄存器 模拟传输 |
| URB 完成方式 | 物理总线传输完成 → \rightarrow → 硬件中断 → \rightarrow → complete 回调 | HCD 直接读取寄存器填充缓冲区 → \rightarrow → 手动触发 complete 回调 |
| 划分目的 | 按照 USB 物理协议规范收发数据 | 欺骗上层内核,实现 Root Hub 与外部 Hub 驱动逻辑的统一 |
2. usb-skeleton.c usb驱动源码分析
补充知识点:
2.1 skel_driver
这段代码是 Linux USB 接口驱动(usb_driver)的注册结构体定义与模块入口宏,是所有 USB 接口驱动(Interface Driver)接入 Linux USB Core 框架的核心基础。
c
/*
* 定义并初始化一个 usb_driver 结构体变量 skel_driver。
* 该结构体包含了 USB 核心层(USB Core)管理该驱动所需的基本信息与回调函数指针。
*/
static struct usb_driver skel_driver = {
/* 驱动程序的名称,注册后会在 /sys/bus/usb/drivers/ 下生成对应的目录 */
.name = "skeleton",
/*
* 设备探测回调函数(关键)
* 当 USB 总线上插入一个新设备,且该设备的 VID/PID 与 id_table 匹配成功时,
* USB Core 会自动调用此函数,用于初始化该设备、分配内存并创建字符设备节点。
*/
.probe = skel_probe,
/*
* 设备拔出/断开回调函数(关键)
* 当 USB 设备从主机物理拔出,或驱动被卸载时调用。
* 用于释放 probe 中分配的内存、注销接口节点并终止所有未完成的 URB。
*/
.disconnect = skel_disconnect,
/*
* 电源管理:挂起回调函数
* 当系统进入休眠状态,或该 USB 设备触发自动挂起(Autosuspend)时调用,
* 用于暂停数据传输、释放/取消正在进行中的 URB,降低设备功耗。
*/
.suspend = skel_suspend,
/*
* 电源管理:恢复回调函数
* 当系统从休眠唤醒,或设备从挂起状态恢复时调用,
* 用于重新初始化设备并恢复正常的 I/O 传输。
*/
.resume = skel_resume,
/*
* 复位前回调函数
* 当 USB Core 或其他驱动准备对该设备发起总线复位(Reset)之前调用,
* 驱动需要在此处停止提交新的 URB,并等待当前传输完成,防止复位过程中发生数据错乱。
*/
.pre_reset = skel_pre_reset,
/*
* 复位后回调函数
* 当总线复位操作完成后调用,
* 驱动需要在此处重新配置接口、重新分配端点资源并恢复 I/O 传输。
*/
.post_reset = skel_post_reset,
/*
* 设备 ID 匹配表(关键)
* 指向一个 struct usb_device_id 数组,列出了该驱动能够支持的所有 USB 设备的 VID(厂商 ID)和 PID(产品 ID)。
* USB Core 依靠这张表来进行总线上的"驱动与设备匹配(Match)"。
*/
.id_table = skel_table,
/*
* 标记该驱动是否支持运行时的自动挂起(Autosuspend)功能。
* 设置为 1 表示该驱动具备 PM(电源管理)能力,当总线空闲一段时间后,允许内核将设备置入低功耗模式。
*/
.supports_autosuspend = 1,
};
/*
* 模块驱动注册便捷宏(内核辅助宏)
* 这是一个极简宏,用以替换传统的 module_init() 与 module_exit() 模版代码。
* 它会在模块加载时自动调用 usb_register(&skel_driver),
* 在模块卸载时自动调用 usb_deregister(&skel_driver)。
*/
module_usb_driver(skel_driver);
关键机制补充说明:
-
id_table与probe的联动 :当用户插入 USB 设备时,内核的 USB 总线扫描程序读取设备的硬件描述符,获取其 VID/PID。随后遍历所有已注册的
usb_driver中的id_table。一旦匹配成功,内核就会自动触发.probe指针指向的skel_probe函数。 -
module_usb_driver()宏展开后的实质 :如果不使用
module_usb_driver(skel_driver),开发者需要手动编写如下代码:cstatic int __init skel_init(void) { return usb_register(&skel_driver); } static void __exit skel_exit(void) { usb_deregister(&skel_driver); } module_init(skel_init); module_exit(skel_exit);module_usb_driver()的作用就是自动生成上述模板代码,简化内核模块的入口与出口声明。
总结:
skel_driver 是 Linux 官方提供的一个 USB 骨架驱动实例(Skeleton Driver)。
它的核心作用是:作为编写 Linux USB 字符设备驱动的"官方标准模板"与"框架入口"。
具体作用体现在以下三个方面:
- 向内核注册骨架驱动 :通过
module_usb_driver(skel_driver)统一向 Linux USB Core 声明该骨架驱动的存在,使其能够接入内核的总线管理体系。 - 定义骨架驱动的行为规范 :绑定了该模板处理硬件插拔(
skel_probe/skel_disconnect)、电源管理(skel_suspend/skel_resume)以及总线复位时的具体函数实现。 - 建立设备识别规则 :通过绑定
skel_table,指定该模板驱动用来匹配和测试哪些具体的 USB 硬件(VID/PID)。
2.2 usb_skel
struct usb_skel 是 USB 骨架驱动的核心自定义结构体。在 Linux 驱动开发中,它被称为驱动的私有数据结构(Private Data Structure)。
它的核心作用是:把"一个具体的物理 USB 设备"在软件层面上所需要的所有状态、数据缓冲区、端点地址、并发锁以及同步等待队列装入同一个结构体中,实现设备资源的统一集中管理。
c
struct usb_skel {
/* 指向该设备对应的 Linux USB 核心设备结构体 (代表整个物理 USB 设备) */
struct usb_device *udev;
/* 指向当前驱动所绑定的 USB 接口结构体 (代表该设备上的某个具体接口) */
struct usb_interface *interface;
/* 信号量:用于限制当前正在处理中的写 URB 数量,防止并发写入过多导致内存耗尽或硬件崩溃 */
struct semaphore limit_sem;
/* USB 锚点:用于集中管理所有已提交(In-flight)的 URB。当设备拔出时,可以通过锚点一键取消所有挂起的 URB */
struct usb_anchor submitted;
/* 用于执行 Bulk IN(批量读取)传输的 URB 结构体指针 */
struct urb *bulk_in_urb;
/* 驱动在内核空间分配的读缓冲区指针,用于接收从 USB 设备端点传来的原始数据 */
unsigned char *bulk_in_buffer;
/* 读缓冲区(bulk_in_buffer)的总内存容量大小 (字节数) */
size_t bulk_in_size;
/* 当前读缓冲区中已被硬件填充的有效数据总字节数 */
size_t bulk_in_filled;
/* 当前读缓冲区中的数据已被 copy_to_user() 拷贝给用户空间的字节数 (用于偏移指针计算) */
size_t bulk_in_copied;
/* 设备的批量输入端点地址(Bulk IN Endpoint Address,含方向标志位) */
__u8 bulk_in_endpointAddr;
/* 设备的批量输出端点地址(Bulk OUT Endpoint Address,含方向标志位) */
__u8 bulk_out_endpointAddr;
/* 记录上一次 I/O 传输过程中发生的错误码(如超时、硬件断开等) */
int errors;
/* 标记位:表示当前是否有一个 Bulk IN 读取传输正在物理总线上执行 */
bool ongoing_read;
/* 自旋锁:用于在中断上下文与线程上下文之间互斥保护 errors 字段的读写 */
spinlock_t err_lock;
/* 内核引用计数器(Kernel Reference):用于管理该结构体自身的生命周期,防止"设备已拔出但用户程序仍打开文件"时引发野指针崩溃 */
struct kref kref;
/* 互斥锁:用于同步用户空间的读写 I/O 操作与内核的 disconnect(设备拔出)流程,防止并发竞态 */
struct mutex io_mutex;
/* 1-bit 位域标记:标记设备是否已被物理拔出(1 表示已断开,0 表示正常连接) */
unsigned long disconnected:1;
/* 等待队列头:当应用层发起 read() 但当前正有一个物理读取操作在进行时,用于将当前进程挂起休眠 */
wait_queue_head_t bulk_in_wait;
};
结构体设计核心功能分类:
为了更清晰地理解,该结构体内部的成员可以划分为 5 大功能模块:
-
硬件基础信息 (
udev,interface,bulk_in_endpointAddr,bulk_out_endpointAddr)- 记录设备在 USB Core 中的句柄以及通讯所必需的端点地址。
-
读数据缓冲区管理 (
bulk_in_urb,bulk_in_buffer,bulk_in_size,bulk_in_filled,bulk_in_copied)- 管理从物理设备读到的数据流。配合
ongoing_read和bulk_in_wait实现当内存无数据时发起物理 I/O 并阻塞等待的逻辑。
- 管理从物理设备读到的数据流。配合
-
并发流量控制与 URB 追回 (
limit_sem,submitted)limit_sem避免应用层高频write()导致内核分配过多 URB 耗尽内存。submitted(struct usb_anchor)将驱动发出的所有 URB 锚定在一起,拔出设备时只需调用usb_kill_anchored_urbs()即可一次性安全取消所有在途传输。
-
安全退场与生命周期保护 (
kref,io_mutex,disconnected)- 解决 Linux 驱动中最棘手的"热插拔竞态问题":如果用户打开了
/dev/skel0接口,然后突发拔掉 USB 设备,kref会确保usb_skel结构体不会被立即kfree()释放,直到应用层关闭文件句柄;io_mutex则确保拔出事件不会破坏正在进行的读写流程。
- 解决 Linux 驱动中最棘手的"热插拔竞态问题":如果用户打开了
-
异常与同步 (
errors,err_lock,bulk_in_wait)- 收集底层 ISR(中断服务程序)异步返回的传输错误,并通过等待队列让应用层线程按需休眠/唤醒。
2.3 URB_NO_TRANSFER_DMA_MAP
这里的标志名 URB_NO_TRANSFER_DMA_MAP 中的 NO 并不是针对"要不要进行 DMA 传输"的限制,而是针对"USB Core(USB 核心层)要不要去帮你做 DMA 映射(Mapping)"的控制。
1. 核心概念辨析:传输 vs 映射(Map)
- 做 DMA 传输(Transfer):指的是 USB Host 控制器硬件通过 DMA 引擎直接从 RAM 中读取或写入数据。
- 做 DMA 映射(DMA Mapping) :指的是将内核中的虚拟地址(
transfer_buffer) 转换为物理/设备能直接访问的 DMA 物理地址(transfer_dma,即dma_addr_t) 的过程。这个转换需要调用系统底层接口(如dma_map_single()),在某些平台上甚至要操作 IOMMU 和刷 CPU Cache,是有一定性能开销的。
2. 标志位的真正含义
默认情况(标志位未设置,NO 为假):
- 驱动层的行为 :驱动开发者只申请并填充了普通的内存缓冲区(
transfer_buffer,比如用kmalloc出来的虚拟内存),并没有自己去映射 DMA 地址。 - USB Core 的行为 :USB Core 检查到标志位未置位,就会意识到:"设备驱动还没做 DMA 映射,我必须在帮它把 URB 发给 HCD 之前,调用
dma_map_single()自动建立 DMA 映射 ,并将得到的物理地址填入transfer_dma。"
设置了 URB_NO_TRANSFER_DMA_MAP(NO 为真):
- 含义 :"USB 核心层,请【不要】帮我做 DMA 映射了!"
- 原因 :因为驱动程序(如高性能驱动或设备控制器驱动)自己已经提前用
dma_alloc_coherent()申请好了 DMA 缓冲区 ,并且把 DMA 物理地址手动写进了transfer_dma中。 - USB Core 的行为 :USB Core 看到这个标志后,就会跳过
dma_map_single()的映射步骤,直接将transfer_dma中的物理地址传给 Host 控制器去执行 DMA 传输。
3. 同理:URB_NO_SETUP_DMA_MAP
对于 USB 控制传输(Control Transfer)特有的 Setup 包,逻辑完全一致:
- 设置了标志 :通知 USB Core 不要去映射
setup_packet(虚拟地址),因为驱动已经提前映射好并把 DMA 物理地址放在了setup_dma中,请直接使用setup_dma进行传输。
4. 为什么要设计这个标志位?(性能优化)
如果一个驱动需要高频、大批量地复用同一块缓冲区发 URB(例如连续传输 4K 视频流):
- 如果每次提交 URB 都让 USB Core 去做
dma_map_single()和dma_unmap_single(),频繁刷 Cache 会严重拖慢性能。 - 驱动开发者可以事先一次性申请好一块 DMA 物理连续内存(或者只映射一次),后续每次发 URB 时只填
transfer_dma并挂上URB_NO_TRANSFER_DMA_MAP标志,从而省去了每次传输时 USB Core 进行 DMA 映射的软件开销。
总结:
URB_NO_TRANSFER_DMA_MAP的全称理解应该是:URB_NO_[USB_CORE_DOING_]_TRANSFER_DMA_MAP(USB 核心层无须进行 DMA 映射)。- 它不是禁用 DMA ,而是告诉系统:"DMA 物理地址我已经自己映射并准备好了(在
transfer_dma里),你直接拿去用,别再多此一举帮我做映射了。"
2.4 skel_read 与 skel_do_read_io
以下通过一个具体的数值场景,说明应用层、skel_read()、skel_do_read_io() 与 USB 硬件设备之间的协同过程。
初始状态:
- 用户空间 :准备调用
read()。 - 驱动缓冲区(
dev->bulk_in_buffer) :容量为 4096 字节,当前有效数据量为0字节。 - USB 硬件端点 FIFO :存有设备采集到的
2048字节数据。
场景一:缓冲区为空时发起读取(触发物理 I/O)
1. 应用层发起请求
用户程序执行:
c
char buf[1024];
read(fd, buf, 1024); // 请求读取 1024 字节
2. 进入 skel_read() 判定
skel_read()读取内核结构体,发现dev->bulk_in_buffer中有效数据量为0(小于请求的1024字节)。- 内存中没有可用数据,
skel_read()调用skel_do_read_io(dev, 1024)。
3. 执行 skel_do_read_io() 向硬件要数据
- 配置与提交 URB :分配 Bulk IN URB,将接收缓冲区指向
dev->bulk_in_buffer,调用usb_submit_urb()发送到总线。 - 休眠等待 :调用
wait_for_completion(),当前进程进入休眠状态。 - 总线传输 :Host 控制器向设备发送 IN 包,USB 硬件将 FIFO 中的数据发送出来。假设硬件本次实际通过 DMA 向
dev->bulk_in_buffer写入了2048字节,并触发中断。 - 唤醒与返回 :中断服务函数唤醒进程,
skel_do_read_io()更新缓冲区记录(当前有效数据量 =2048字节),随后返回。
4. skel_read() 完成拷贝
- 此时
dev->bulk_in_buffer内已有2048字节新数据。 skel_read()调用copy_to_user()将前1024字节拷贝给用户程序的buf。- 更新缓冲区状态:偏移指针后移
1024字节,剩余有效数据量变为1024字节(2048 - 1024)。 skel_read()返回1024。
场景二:缓冲区有剩余数据时发起读取(不触发物理 I/O)
1. 应用层再次发起请求
用户程序紧接着执行:
c
char buf2[512];
read(fd, buf2, 512); // 请求读取 512 字节
2. 进入 skel_read() 判定
skel_read()检查发现dev->bulk_in_buffer中仍有1024字节上次留下的数据,大于本次请求的512字节。- **不调用
skel_do_read_io()**,完全不向 USB 总线和硬件发送任何请求。
3. 直接拷贝内存数据
skel_read()直接调用copy_to_user(),从dev->bulk_in_buffer当前偏移位置取出512字节拷贝给用户程序的buf2。- 更新缓冲区状态:剩余有效数据量变为
512字节(1024 - 512)。 skel_read()返回512。
3. USB总线核心和主控驱动
3.1 khubd → usb_hub_wq
从早期的专用内核线程(khubd)演进到高版本的共享/专用工作队列(usb_hub_wq),这一架构重构主要是为了解决资源开销、系统并发性能以及内核线程滥用的问题。
具体原因可以归纳为以下四个核心维度:
1. 降低系统内存与调度开销(避免内核线程膨胀)
在低版本内核中,khubd 是一个独立的内核线程(struct task_struct)。
- 独立线程的代价:每一个内核线程都需要占用独立的内核栈空间(通常为 8KB 或 16KB)、线程控制块(TCB)以及调度器资源。
- 实际利用率极低 :在大部分平静状态下,系统没有 USB 设备插拔,
khubd线程 99.9% 的时间都在阻塞休眠。为这样一个低频触发的任务长期维持一个独立的内核线程,是对内存和 CPU 调度资源的浪费。 - 工作队列的优势 :工作队列(Workqueue)将"任务逻辑"与"执行线程"解耦。工作队列由内核统一管理(Workqueue 子系统),只有在真正产生插拔事件时,才会将工作项(
work_struct)调度到内核工作线程上执行,平峰期零额外线程开销。
2. 拥抱 Linux 内核 CMWQ(并发管理工作队列)架构
Linux 3.x 及更高版本内核重构了工作队列子系统,引入了 CMWQ(Concurrency Managed Workqueue)。
- 旧机制的痛点 :在引入 CMWQ 之前,驱动开发人员倾向于为各种子系统创建独立的内核线程(如
khubd),导致系统中充斥着大量kworker和专用守护线程,极易引发上下文切换开销过大和 CPU 缓存失效。 - CMWQ 的优化 :CMWQ 能够根据系统负载动态创建和销毁内核线程,并在多核 CPU 之间智能分配工作项。USB 子系统使用
usb_hub_wq后,可以直接利用 CMWQ 的动态并发能力:当有多个 USB 端口同时发生插拔时,CMWQ 会自动并行处理;而在空闲时则不占用额外的线程资源。
3. 简化内核代码逻辑与生命周期管理
- 旧线程模式(
khubd) :必须由 USB Core 显式维护线程的创建(kthread_run)、循环体中的条件等待(wait_event_freezable)、唤醒逻辑(wake_up_process)以及卸载时的停止(kthread_stop)。 - 工作队列模式(
usb_hub_wq):把复杂的线程休眠、唤醒和状态同步完全委托给内核成熟的 Workqueue 框架。USB Core 只需要执行: - 初始化 :
usb_hub_wq = alloc_ordered_workqueue("usb_hub_wq", WQ_FREEZABLE); - 触发事件 :
queue_work(usb_hub_wq, &hub->events); - 销毁 :
destroy_workqueue(usb_hub_wq);
代码简洁度与健壮性显著提升。
4. 更好的系统电源管理(PM & Freeze Support)
USB 集线器必须良好支持系统的休眠(Suspend)与唤醒(Resume):
- 在配置
usb_hub_wq时,内核传入了WQ_FREEZABLE标志。 - 这使得系统在进入低功耗休眠(System Suspend)时,电源管理子系统可以自动冻结
usb_hub_wq中挂起的工作项,防止休眠过程中突然插入 USB 设备导致系统唤醒逻辑被打乱。如果使用自定义内核线程,则需要编写大量的冻结检测代码(如try_to_freeze())。
总结对比:
| 维度 | 低版本内核:khubd 内核线程 |
高版本内核:usb_hub_wq 工作队列 |
|---|---|---|
| 资源占用 | 长期占用独立的 task_struct 与内核栈 |
仅在有事件时借用/分配工作线程,无事件时零开销 |
| 并发能力 | 单线程顺序处理,多端口同时插拔时效率较低 | 基于 CMWQ,可根据需要自动实现多核并发处理 |
| 代码维护 | 需要手动维护线程循环、唤醒与停止逻辑 | 规范化的 queue_work() 接口,逻辑封装更完善 |
| 电源管理 | 需手动处理线程冻结(Freezing) | 设置 WQ_FREEZABLE 标志后由 PM 子系统自动托管 |
3.2 hub_event
c
static void hub_event(struct work_struct *work)
{
struct usb_device *hdev;
struct usb_interface *intf;
struct usb_hub *hub;
struct device *hub_dev;
u16 hubstatus;
u16 hubchange;
int i, ret;
/* 1. 上下文获取:通过 work 指针反查出包含它的 struct usb_hub 结构体 */
hub = container_of(work, struct usb_hub, events);
hdev = hub->hdev; /* 获取 Hub 对应的 USB 设备指针 */
hub_dev = hub->intfdev; /* 获取 Hub 接口的 struct device 指针 */
intf = to_usb_interface(hub_dev); /* 获取 Hub 的 USB 接口结构体 */
/* 开启内核动态覆盖率跟踪 (KCOV) 工具,用于内核安全测试与 Fuzzing */
kcov_remote_start_usb((u64)hdev->bus->busnum);
dev_dbg(hub_dev, "state %d ports %d chg %04x evt %04x\n",
hdev->state, hdev->maxchild,
(u16) hub->change_bits[0],
(u16) hub->event_bits[0]);
/* 2. 安全检查:上锁保护,防止并发处理竞态 */
usb_lock_device(hdev);
/* 如果在等待锁的过程中该 Hub 设备已经被物理拔出,直接退出 */
if (unlikely(hub->disconnected))
goto out_hdev_lock;
/* 如果 Hub 设备本身掉线/失效,清理 Hub 驱动资源并退出 */
if (hdev->state == USB_STATE_NOTATTACHED) {
hub->error = -ENODEV;
hub_quiesce(hub, HUB_DISCONNECT); /* 停止该 Hub 所有的 URB 传输 */
goto out_hdev_lock;
}
/* 3. 运行休眠管理:唤醒 Hub (Autoresume) */
/* 在访问 Hub 寄存器前,先通过 Runtime PM 机制确保 Hub 处于供电正常工作状态 */
ret = usb_autopm_get_interface(intf);
if (ret) {
dev_dbg(hub_dev, "Can't autoresume: %d\n", ret);
goto out_hdev_lock;
}
/* 如果 Hub 当前正在停止/静默过程中,不做任何处理 */
if (hub->quiescing)
goto out_autopm;
/* 4. 异常恢复:如果 Hub 之前记录了严重传输错误,尝试做重置复位 */
if (hub->error) {
dev_dbg(hub_dev, "resetting for error %d\n", hub->error);
ret = usb_reset_device(hdev); /* 执行硬件重置 */
if (ret) {
dev_dbg(hub_dev, "error resetting hub: %d\n", ret);
goto out_autopm;
}
hub->nerrors = 0;
hub->error = 0;
}
/* ================= 5. 下属端口状态变更处理 (核心逻辑 1) ================= */
/* 遍历该 Hub 上的每一个物理下行端口 (Port 1 ~ maxchild) */
for (i = 1; i <= hdev->maxchild; i++) {
struct usb_port *port_dev = hub->ports[i - 1];
/* 检查该端口是否有事件发生 (事件位、变更位或远程唤醒位) */
if (test_bit(i, hub->event_bits)
|| test_bit(i, hub->change_bits)
|| test_bit(i, hub->wakeup_bits)) {
/* 保护该端口在处理过程中不会被系统后台 Runtime PM 强制挂起休眠 */
pm_runtime_get_noresume(&port_dev->dev);
pm_runtime_barrier(&port_dev->dev);
/* 上锁保护该端口,防止并发处理 */
usb_lock_port(port_dev);
/* 核心子函数:处理该端口的具体事件 (如插拔枚举、复位、唤醒等) */
port_event(hub, i);
usb_unlock_port(port_dev);
pm_runtime_put_sync(&port_dev->dev);
}
}
/* ================= 6. Hub 自身状态变更处理 (核心逻辑 2) ================= */
/* event_bits 位 0 代表 Hub 自身的状态改变 */
if (test_and_clear_bit(0, hub->event_bits) == 0)
; /* 没有 Hub 自身状态事件,什么都不做 */
/* 向 Hub 发送标准 USB 控制命令获取最新的 Hub Status */
else if (hub_hub_status(hub, &hubstatus, &hubchange) < 0)
dev_err(hub_dev, "get_hub_status failed\n");
else {
/* 处理 Hub 电源模式改变 (如从总线供电切换到外接电源供电) */
if (hubchange & HUB_CHANGE_LOCAL_POWER) {
dev_dbg(hub_dev, "power change\n");
clear_hub_feature(hdev, C_HUB_LOCAL_POWER);
if (hubstatus & HUB_STATUS_LOCAL_POWER)
hub->limited_power = 1;
else
hub->limited_power = 0;
}
/* 处理 Hub 过流警告 (Over-Current) */
if (hubchange & HUB_CHANGE_OVERCURRENT) {
u16 status = 0;
u16 unused;
dev_dbg(hub_dev, "over-current change\n");
clear_hub_feature(hdev, C_HUB_OVER_CURRENT);
msleep(500); /* 冷却等待 500ms,防止硬件烧毁 */
hub_power_on(hub, true); /* 尝试重新供电 */
hub_hub_status(hub, &status, &unused);
if (status & HUB_STATUS_OVERCURRENT)
dev_err(hub_dev, "over-current condition\n");
}
}
/* ================= 7. 清理与释放资源退出 ================= */
out_autopm:
/* 平衡上面调用过的 usb_autopm_get_interface() 计数 */
usb_autopm_put_interface_no_suspend(intf);
out_hdev_lock:
usb_unlock_device(hdev);
/* 释放引用计数,允许 Hub 在空闲时再次进入自动休眠 (Autosuspend) */
usb_autopm_put_interface(intf);
kref_put(&hub->kref, hub_release);
kcov_remote_stop();
}
hub_event 函数是 Linux 内核 USB Hub(集线器)子系统的核心事件处理函数。
当物理层检测到 USB 端口有设备插入、拔出、过流或电源状态改变时,Hub 中断端点会接收到数据并向系统提交 Work 项。usb_hub_wq 工作队列正是通过调度并执行 hub_event 函数,来统一处理该 Hub 及其下属所有下行端口的改变。
函数的核心执行主线总结:
hub_event 函数的工作可以总结为以下五个关键步骤:
-
设备状态校验与安全互斥:
- 优先获取
usb_lock_device,防止在处理端口事件时发生热插拔竞态; - 确认 Hub 是否在线,若已断开则调用
hub_quiesce停止所有 URB 传输。
- 优先获取
-
电源唤醒与自愈(Auto-Resume & Self-Healing):
- 调用
usb_autopm_get_interface将可能处于低功耗休眠(Suspend)的 Hub 重新唤醒; - 如果该 Hub 之前出现过严重的总线错误,调用
usb_reset_device对 Hub 进行总线复位。
- 调用
-
处理端口级变化(Port Event Handles):
- 遍历所有的物理下行端口,查找
event_bits/change_bits标志。 - 一旦发现端口有状态改变(例如:插入了新设备、拔出了旧设备、端口过流等),调用**最核心的子函数
port_event(hub, i)**来执行设备的复位、地址分配以及驱动绑定(枚举流程)。
- 遍历所有的物理下行端口,查找
-
处理 Hub 自身事件(Hub Status Handles):
- 检查标志位 0,读取 Hub 整体状态。处理 Hub 本身的本地供电状态变化 以及整机过流(Over-current)保护(切断电源 → \rightarrow → 冷却 500ms → \rightarrow → 尝试重新上电)。
-
电源平衡与资源释放:
- 减少电源管理计数,重新允许 Hub 在无事可做时进入 Runtime Autosuspend 低功耗状态,释放引用计数退出。
3.3 usb 主机控制器设备
c
// arch/arm/mach-s3c/mach-smdk2440.c
/* 声明该文件属于 S3C2440 开发板 (SMDK2440) 的板级描述文件 */
/* MACHINE_START 是一个宏,用来定义 struct machine_desc 结构体,描述特定开发板的信息 */
/* 参数 S3C2440 是开发板的 Machine ID,"SMDK2440" 是内核启动时打印的开发板名称 */
MACHINE_START(S3C2440, "SMDK2440")
/* Maintainer: Ben Dooks <ben-linux@fluff.org> */ /* 维护者注释信息 */
.atag_offset = 0x100, /* Bootloader (如 U-Boot) 传参 (ATAGS) 存放在 SDRAM 内存起始地址后的偏移量 (0x30000100) */
.init_irq = s3c2440_init_irq, /* 函数指针:用于初始化 S3C2440 的中断控制器 (VIC/IRQ) */
.map_io = smdk2440_map_io, /* 函数指针:建立外设物理寄存器到内核虚拟地址空间的静态映射 */
.init_machine = smdk2440_machine_init, /* 函数指针:板级设备初始化入口函数 (注册 platform_device 的核心) */
.init_time = smdk2440_init_time, /* 函数指针:初始化系统定时器/时钟源 (System Timer/Clock Source) */
MACHINE_END /* 结合 MACHINE_START 闭合结构体定义,把该结构体编译进 .arch.info.init 段 */
/* 板级设备初始化函数,__init 标记表示该函数在系统初始化完成后,占用的内存会被内核释放 */
static void __init smdk2440_machine_init(void)
{
/* 设置 LCD 控制器的平台私有数据 (Platform Data),包含屏幕分辨率、时序等硬件参数 */
s3c24xx_fb_set_platdata(&smdk2440_fb_info);
/* 设置 I2C0 控制器的平台私有数据,传递 NULL 表示使用默认时钟频率与配置 */
s3c_i2c0_set_platdata(NULL);
/* 配置 I2S 音频接口需要的 GPIO 引脚复用模式 */
/* 参数含义:从 GPE0 开始,连续配置 5 个引脚,设为功能 2 (S3C_GPIO_SFN(2),即 I2S 模式),禁用内部上拉/下拉电阻 */
s3c_gpio_cfgall_range(S3C2410_GPE(0), 5, S3C_GPIO_SFN(2),
S3C_GPIO_PULL_NONE);
/* 核心逻辑:向内核平台总线 (platform_bus) 批量注册 smdk2440_devices 数组中定义的硬件设备 */
platform_add_devices(smdk2440_devices, ARRAY_SIZE(smdk2440_devices));
/* 执行 SMDK 系列开发板通用的板级后处理初始化 */
smdk_machine_init();
}
/* 定义该开发板挂载的所有 platform_device 指针数组 */
/* __initdata 标记表示该数组在内核完成初始化后会被内核自动释放 */
static struct platform_device *smdk2440_devices[] __initdata = {
&s3c_device_ohci, /* USB 1.1 Host 控制器 (OHCI 规范) 设备 */
&s3c_device_lcd, /* Framebuffer LCD 控制器设备 */
&s3c_device_wdt, /* 看门狗定时器 (Watchdog Timer) 设备 */
&s3c_device_i2c0, /* I2C 总线控制器 0 设备 */
&s3c_device_iis, /* IIS (I2S) 音频总线控制器设备 */
};
// arch/arm/mach-s3c/devs.c
/* 位于 SoC 通用外设定义文件中:描述 S3C2440 芯片内部整合的 USB OHCI 主控平台设备 */
struct platform_device s3c_device_ohci = {
.name = "s3c2410-ohci", /* 设备名称:内核驱动模型通过该名字与对应的 platform_driver 进行匹配 */
.id = -1, /* 设备 ID:-1 表示系统中只有一个该类型的设备,无需在名字后追加 .0, .1 等编号 */
.num_resources = ARRAY_SIZE(s3c_usb_resource), /* 该设备占用的资源 (Resource) 数量 */
.resource = s3c_usb_resource, /* 包含寄存器物理内存基地址 (MMIO) 和中断号 (IRQ) 的资源数组 */
.dev = {
.dma_mask = &samsung_device_dma_mask, /* 该设备能够寻址的 DMA 地址范围掩码指针 */
.coherent_dma_mask = DMA_BIT_MASK(32), /* 一致性 DMA (Coherent DMA) 的 32 位寻址掩码 */
}
};
}; /* 提示:末尾多余的闭合花括号为源码复制时的语法残留 */
核心机制与逻辑说明
1. Linux 内核驱动模型:设备与驱动分离
这段代码反映了在设备树 (Device Tree) 引入之前,Linux 内核对 SoC 板级硬件的组织方式:
+-------------------------------------------------------+
| Platform Bus (平台总线) |
+-------------------------------------------------------+
|
Match 匹配机制:比对 platform_device.name
与 platform_driver.driver.name
|
+---------------------+---------------------+
| |
v v
+--------------------------+ +--------------------------+
| platform_device | | platform_driver |
| (如: s3c_device_ohci) | | (如: ohci-hcd-s3c2410) |
+--------------------------+ +--------------------------+
| .name = "s3c2410-ohci" | <=== 匹配 ===> | .driver.name = |
| .resource (寄存器/IRQ) | | "s3c2410-ohci" |
+--------------------------+ +--------------------------+
|
匹配成功,调用
.probe() 初始化硬件
platform_device(描述硬件) :负责告知内核"系统里有什么硬件、物理基地址在哪、使用什么中断号"。例如s3c_device_ohci。platform_driver(编写软件逻辑) :驱动程序(如drivers/usb/host/ohci-s3c2410.c)注册时提供操作硬件的代码逻辑。- Platform Bus(解耦桥梁) :当
platform_add_devices()被调用时,总线会将注册的设备与已经挂载的驱动进行字符串比对,名字一致即触发驱动的.probe()入口。
2. 系统启动链路 (Boot Flow)
-
setup_arch()阶段 :Bootloader (U-Boot) 传入 Machine ID,内核在.arch.info.init段中找到匹配的MACHINE_START(S3C2440, ...)结构体。 -
初始化钩子执行:
- 调用
.map_io建立 IO 内存映射。 - 调用
.init_irq挂载中断向量表。
- 调用
-
smdk2440_machine_init()被调用:- 配置特定 GPIO 复用引脚(如 I2S)。
- 通过
platform_add_devices批量将smdk2440_devices挂载到平台总线上。
-
触发驱动
probe:注册s3c_device_ohci后,USB OHCI 控制器驱动匹配成功并启动,进一步向 USB 子系统注册 Root Hub,从而使能芯片的 USB Host 功能。
3.4 usb 主机控制器驱动
c
// .config
CONFIG_ARCH_S3C2410=y /* 内核配置选项:开启三星 S3C24XX 平台架构支持 */
c
// drivers/usb/host/ohci-s3c2410.c
/* 模块初始化函数,当驱动被内核加载或静态编译打包启动时执行 */
static int __init ohci_s3c2410_init(void)
{
/* 检查系统命令行是否禁用了 USB(如 bootargs 中传入了 usb-disabled) */
if (usb_disabled())
return -ENODEV;
/* 打印驱动加载日志信息 */
pr_info("%s: " DRIVER_DESC "\n", hcd_name);
/* ------------ (1) 初始化通用 OHCI 驱动结构体 ------------ */
/* 使用内核标准的 OHCI 框架初始化结构体 ohci_s3c2410_hc_driver (struct hc_driver) */
ohci_init_driver(&ohci_s3c2410_hc_driver, NULL);
/*
* 注释说明:
* 三星 S3C2410/2440 的硬件 OHCI 控制器存在一些特殊 Quirks(硬件缺陷/特性),
* 需要三星特有的 Workarounds(变通修复方案)。
* 这里显式重写(Override)通用 hc_driver 中的 Hub 状态轮询和 Hub 控制函数。
*/
/* 重写 Hub 状态变化检测函数,用于适配三星芯片的端口状态寄存器行为 */
ohci_s3c2410_hc_driver.hub_status_data = ohci_s3c2410_hub_status_data;
/* 重写 Hub 控制请求处理函数,用于适配端口电源使能、复位等引脚控制 */
ohci_s3c2410_hc_driver.hub_control = ohci_s3c2410_hub_control;
/* ------------ (2) 向平台总线注册平台驱动 ------------ */
/* 将 ohci_hcd_s3c2410_driver 注册到 Linux 平台总线 (platform_bus) 上 */
/* 注册过程会触发总线匹配,寻找名为 "s3c2410-ohci" 的 platform_device */
return platform_driver_register(&ohci_hcd_s3c2410_driver);
}
/* 指定 ohci_s3c2410_init 为该模块的入口函数 */
module_init(ohci_s3c2410_init);
/* 定义平台驱动结构体 (struct platform_driver) */
static struct platform_driver ohci_hcd_s3c2410_driver = {
/* 当平台总线匹配设备成功后调用的探测函数 (完成硬件初始化、申请中断、创建 Root Hub) */
.probe = ohci_hcd_s3c2410_probe,
/* 当设备被卸载或卸载驱动时调用的清理函数 */
.remove = ohci_hcd_s3c2410_remove,
/* 系统关机时的关机回调函数 */
.shutdown = usb_hcd_platform_shutdown,
/* 内核标准设备驱动信息块 */
.driver = {
/* 关键匹配标志:名字必须与 platform_device 的 .name (即 "s3c2410-ohci") 完全一致 */
.name = "s3c2410-ohci",
/* 电源管理回调函数 (休眠/唤醒) */
.pm = &ohci_hcd_s3c2410_pm_ops,
/* 设备树 (Device Tree) 匹配表,用于兼容现代内核 DTS 模式 */
.of_match_table = ohci_hcd_s3c2410_dt_ids,
},
};
这段代码是 S3C2410/S3C2440 平台 USB OHCI 主控驱动(Host Controller Driver, HCD) 的模块初始化与平台驱动注册入口。它与你之前提到的板级文件 mach-smdk2440.c(注册 platform_device)构成了完整的 总线-设备-驱动 闭环。
核心机制与全流程串联:
这段代码在整个 Linux USB 驱动架构中起到了连接 Linux 通用 OHCI 框架与三星特有硬件的承上启下作用:
+-----------------------------------------------------------------------+
| 板级代码 (mach-smdk2440.c / devs.c) |
| 注册 platform_device (.name = "s3c2410-ohci") |
+-----------------------------------------------------------------------+
|
| (挂载到 platform_bus)
v
+-----------------------------------------------------------------------+
| 平台总线 (Platform Bus) 匹配机制 |
| 比较 device.name与 driver.driver.name ("s3c2410-ohci") |
+-----------------------------------------------------------------------+
|
| 匹配成功,触发调用
v
+-----------------------------------------------------------------------+
| 驱动代码 (ohci-s3c2410.c) |
| 1. ohci_s3c2410_init() 覆写芯片特定 hook (hub_status_data / control) |
| 2. 执行 .probe() -> ohci_hcd_s3c2410_probe() |
| 3. 调用 usb_add_hcd() 注册根集线器 (Root Hub) |
+-----------------------------------------------------------------------+
|
v
+-----------------------------------------------------------------------+
| USB Core / usb_hub_wq 框架 |
| 唤醒 hub_thread,开始检测 USB 端口物理插拔事件 (hub_event) |
+-----------------------------------------------------------------------+
关键点解析:
- 硬件缺陷兼容(Quirks Handling) :
标准的 OHCI 规范定义了一套通用的 Root Hub 寄存器,但三星 S3C2410/2440 的 USB 控制器在处理端口电源使能、过流检测以及状态查询时与通用规范有细微差异。因此驱动在(1)处先用ohci_init_driver填入标准的 OHCI 函数,随后立即将.hub_status_data和.hub_control替换为三星特有的实现。 - 设备树无缝过渡 :
在ohci_hcd_s3c2410_driver中,既保留了用于传统板级代码匹配的.name = "s3c2410-ohci",也加入了.of_match_table。这使得同一个驱动既能兼容旧版的板级 C 代码时代,也能直接在新的设备树(DTS)内核中工作。
3.5 USB驱动 系统级联初始化
核心主线是:"平台总线(Platform Bus)创建控制通道 → \rightarrow → 驱动控制器创建 Root Hub → \rightarrow → USB 总线(USB Bus)创建设备与接口通道"。
整体级联初始化架构图
整个过程由 Platform 总线 触发,中途交接给 USB 总线,形成双层总线模型的联动:
========================= 【第一阶段:Platform 总线层】 =========================
1. 板级代码/设备树注册 platform_device ("s3c2410-ohci")
2. 主控驱动注册 platform_driver ("s3c2410-ohci")
3. platform_bus 匹配成功 (strcmp 名字相同)
4. 触发调用 ohci_hcd_s3c2410_probe()
|
v
========================= 【第二阶段:USB 控制器与 Root Hub 创建】 =========================
5. usb_add_hcd() 申请 Root Hub 设备结构体 (struct usb_device)
6. register_root_hub() -> usb_new_device() -> device_add()
7. Root Hub 作为 "USB 设备" 被挂载到 usb_bus_type (USB 总线) 上
|
v
========================= 【第三阶段:USB 总线层 - 设备级匹配】 =========================
8. 触发 usb_bus_type 的 device_attach() 遍历 USB 驱动链表
9. 调用 usb_device_match():
- 检测到当前是 Root Hub (is_usb_device == true)
- 筛选出专管设备的 usb_device_driver (for_devices == 1)
10. 成功匹配到内核通用的设备驱动:usb_generic_driver
11. 执行 usb_generic_driver_probe():
- 读取配置描述符,选择 Configuration
- 调用 usb_set_configuration() 创建并注册 "USB 接口设备" (usb_interface)
|
v
========================= 【第四阶段:USB 总线层 - 接口级匹配】 =========================
12. 接口设备 (usb_interface) 再次执行 device_add(),重新触发 usb_bus_type 遍历
13. 调用 usb_device_match():
- 检测到当前是接口设备 (is_usb_device == false)
- 过滤掉设备驱动,只在 usb_driver (接口驱动) 中匹配 id_table
14. 成功匹配到系统中的 Hub 接口驱动:hub_driver (.id_table 匹配 USB_CLASS_HUB)
15. 执行 hub_probe():
- 申请并提交 status URQ (中断传输/轮询)
- 绑定中断回调函数 hub_irq
- 创建/唤醒 hub_wq (内核工作队列) 监听端口插拔事件
这三个阶段的递进关系和回调链条(__driver_attach 与 __device_attach 的对偶调用)全部串联闭环了。这种分层与解耦的思想,正是 Linux 内核设备驱动模型最精妙的地方。
4. USB鼠标驱动源码分析
4.1 usb_mouse_probe
先介绍 usb_mouse 与 input_dev
4.1.1 usb_mouse
c
struct usb_mouse {
char name[128]; /* 设备的完整产品名称字符串(如 "Logitech USB Optical Mouse"),用于注册给 Input 子系统 */
char phys[64]; /* 设备在系统中的物理拓扑路径字符串(如 "usb-s3c2410-1.2/input0"),用于区分多设备 */
struct usb_device *usbdev; /* 指向该鼠标所挂载的通用 USB 设备结构体 (struct usb_device),方便在驱动中获取设备信息 */
struct input_dev *dev; /* 指向 Linux Input 子系统的输入设备结构体,内核用它向用户态 (/dev/input/eventX) 上报鼠标事件 */
struct urb *irq; /* 专门用于接收鼠标按键和移动坐标数据的"中断 URB" (Interrupt URB) 指针 */
signed char *data; /* 存放从鼠标接收到的原始硬件数据包(Raw Data)的虚拟内存地址缓冲区(通常为 4 字节,含按键状态、X/Y 偏移值) */
dma_addr_t data_dma; /* 上述 data 缓冲区对应的 DMA 总线物理地址,供 USB 主控芯片通过 DMA 直接传输数据,避免 CPU 搬运 */
};
这是内核官方经典示例驱动 drivers/usb/input/usbmouse.c 中定义的USB 鼠标私有结构体 (Private Data Structure)。它是一个典型的"桥梁"结构体,将 USB 子系统 和 Linux Input(输入)子系统 紧密连接在一起。
关键设计点拆解
-
为何使用
dma_addr_t data_dma?USB 鼠标使用中断传输(Interrupt Transfer)来上报数据。由于鼠标传输频率高(如每秒轮询 125Hz~1000Hz),为了降低 CPU 占用率,驱动在
usb_alloc_coherent()申请内存时,会同时返回 CPU 访问的虚拟地址data和主控硬件访问的 DMA 物理地址data_dma。USB 主控收到硬件数据后,直接通过 DMA 搬运到data_dma,搬完后才发中断通知内核。 -
数据缓冲区
signed char *data为何是有符号类型?鼠标上报的相对位移(Rel Position)是有方向的:
- X 轴:正数代表向右移动,负数代表向左移动。
- Y 轴 :正数代表向下移动,负数代表向上移动。
因此使用signed char(有符号 8 位整数,范围 -128 到 127)可以天然表示这个相对位移量。
-
两个子系统的连接纽带
在驱动的
.probe函数被触发时,内核会分配并初始化这个struct usb_mouse:- 通过 USB 子系统 拿到
usbdev并申请irqURB。 - 通过 Input 子系统 申请
input_dev,配置鼠标支持的事件类型(如EV_KEY按键事件、EV_REL相对坐标事件)。 - 将
usb_mouse结构体挂载到input_dev的dev->dev.platform_data或通过usb_set_intfdata()挂载到 USB 接口私有数据中,确保任何回调函数(如 URB 完成回调usb_mouse_irq)都能随时取回完整的上下文数据。
- 通过 USB 子系统 拿到
4.1.2 input_dev
c
struct input_dev {
/* ================= 1. 设备基本身份与属性标志 ================= */
const char *name; /* 设备名称(如 "Logitech USB Optical Mouse"),出现在 /proc/bus/input/devices 中 */
const char *phys; /* 系统拓扑物理路径(如 "usb-s3c2410-1/input0") */
const char *uniq; /* 设备唯一识别码(如 MAC 地址或序列号) */
struct input_id id; /* 硬件 ID 结构体,包含总线类型 (bus)、厂商 ID (vendor)、产品 ID (product)、版本号 (version) */
unsigned long propbit[BITS_TO_LONGS(INPUT_PROP_CNT)]; /* 设备属性位图(如 INPUT_PROP_DIRECT 标识直接触摸屏/指针) */
/* ================= 2. 能力位图 (Capabilities Bitmaps) - 关键 ================= */
/* 位图机制用于告知内核:该设备"能够支持哪些事件类型"以及"支持哪些具体的按键/坐标轴" */
unsigned long evbit[BITS_TO_LONGS(EV_CNT)]; /* 支持的事件类型位图(如 EV_KEY按键, EV_REL相对坐标, EV_ABS绝对坐标) */
unsigned long keybit[BITS_TO_LONGS(KEY_CNT)]; /* 支持的按键/键值位图(如 KEY_A, BTN_LEFT, BTN_RIGHT) */
unsigned long relbit[BITS_TO_LONGS(REL_CNT)]; /* 支持的相对坐标位图(如 REL_X, REL_Y, REL_WHEEL 滚轮) */
unsigned long absbit[BITS_TO_LONGS(ABS_CNT)]; /* 支持的绝对坐标位图(如 ABS_X, ABS_Y 触摸屏坐标) */
unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)]; /* 支持的杂项事件位图(如 MSC_SCAN 扫描码) */
unsigned long ledbit[BITS_TO_LONGS(LED_CNT)]; /* 支持的指示灯位图(如 LED_NUML, LED_CAPSL 键盘灯) */
unsigned long sndbit[BITS_TO_LONGS(SND_CNT)]; /* 支持的声音事件位图(如 SND_BELL, SND_TONE 蜂鸣器) */
unsigned long ffbit[BITS_TO_LONGS(FF_CNT)]; /* 支持的力反馈事件位图(如游戏手柄震动) */
unsigned long swbit[BITS_TO_LONGS(SW_CNT)]; /* 支持的开关事件位图(如 SW_LID 滑盖/盖子开关) */
unsigned int hint_events_per_packet; /* 性能优化提示:单次事件数据包预计产生的事件平均数量 */
/* ================= 3. 键码映射 (Keymap Management) ================= */
unsigned int keycodemax; /* 键码表最大容量 */
unsigned int keycodesize; /* 键码表中单项的大小(字节) */
void *keycode; /* 扫描码 (Scan Code) 到 Linux 键码 (Key Code) 的映射表指针 */
/* 函数指针:修改和获取映射表的钩子 */
int (*setkeycode)(struct input_dev *dev,
const struct input_keymap_entry *ke,
unsigned int *old_keycode);
int (*getkeycode)(struct input_dev *dev,
struct input_keymap_entry *ke);
/* ================= 4. 特殊硬件扩展功能 ================= */
struct ff_device *ff; /* 指向力反馈 (Force Feedback) 设备结构体 */
/* 键盘连按自动重复 (Auto-repeat / Key Repeat) 机制 */
unsigned int repeat_key; /* 当前正在按下的按键键码 */
struct timer_list timer; /* 负责触发按键长按连发的内核定时器 */
int rep[REP_CNT]; /* 连发参数数组:rep[REP_DELAY] 首次延迟,rep[REP_PERIOD] 连发间隔 */
struct input_mt *mt; /* 指向多点触摸 (Multi-Touch) 协议上下文结构体 */
struct input_absinfo *absinfo; /* 绝对坐标轴参数数组(包含 ABS_X/Y 的最小值、最大值、消抖范围等) */
/* ================= 5. 设备当前状态缓存 (Current State) ================= */
/* 记录设备"当前时刻"的状态,防止上报重复无用数据 */
unsigned long key[BITS_TO_LONGS(KEY_CNT)]; /* 记录当前有哪些按键正处于按下状态 */
unsigned long led[BITS_TO_LONGS(LED_CNT)]; /* 记录当前有哪些 LED 指示灯点亮 */
unsigned long snd[BITS_TO_LONGS(SND_CNT)]; /* 记录当前发声状态 */
unsigned long sw[BITS_TO_LONGS(SW_CNT)]; /* 记录当前开关状态(开/关) */
/* ================= 6. 控制回调函数 (Control Callbacks) ================= */
int (*open)(struct input_dev *dev); /* 当用户态程序第一次 open("/dev/input/eventX") 时被调用(可在此使能硬件中断) */
void (*close)(struct input_dev *dev); /* 当所有应用关闭该设备节点时调用(可在此禁用硬件以节省功耗) */
int (*flush)(struct input_dev *dev, struct file *file); /* 刷盘/刷新缓冲区回调 */
int (*event)(struct input_dev *dev, unsigned int type, unsigned int code, int value); /* 核心下发回调:当内核要向设备下发命令(如亮键盘灯、触发震动)时调用 */
/* ================= 7. Input 子系统匹配与并发控制 ================= */
struct input_handle __rcu *grab; /* 独占句柄(当某个进程独占该输入设备时指向对应的 handle) */
spinlock_t event_lock; /* 保护上报事件队列与内部状态的自旋锁 */
struct mutex mutex; /* 保护 open/close 与设备配置变化的互斥锁 */
unsigned int users; /* 当前打开该设备的应用程序数量 (引用计数) */
bool going_away; /* 设备正在注销/注销标志 */
struct device dev; /* 嵌入 Linux 标准设备模型结构体,在 /sys/class/input/ 下创建设备节点 */
struct list_head h_list; /* 链表头:挂载与该 input_dev 绑定的所有 input_handle(如 evdev, mousedev) */
struct list_head node; /* 链表节点:用于将本 input_dev 挂载到全系统的 input_dev_list 主链表上 */
/* 批量事件上报优化相关缓冲区 */
unsigned int num_vals;
unsigned int max_vals;
struct input_value *vals;
bool devres_managed; /* 标识该结构体是否由 devm_input_allocate_device 托管释放内存 */
ktime_t timestamp[INPUT_CLK_MAX]; /* 时间戳缓存,支持单调时钟、实时时钟等不同基准 */
};
位图机制:
input_dev 采用了非常高效的 unsigned long array 作为位图(Bitmap)。
-
为什么需要?
Linux 输入设备千奇百怪。内核必须通过这些位图明确设备能做什么。 -
驱动开发范例(以鼠标为例) :
c/* 1. 声明支持的事件类型 */ set_bit(EV_KEY, dev->evbit); /* 支持按键事件 */ set_bit(EV_REL, dev->evbit); /* 支持相对坐标事件 */ /* 2. 声明具体的按键和坐标 */ set_bit(BTN_LEFT, dev->keybit); /* 支持左键 */ set_bit(BTN_RIGHT, dev->keybit); /* 支持右键 */ set_bit(REL_X, dev->relbit); /* 支持 X 轴移动 */ set_bit(REL_Y, dev->relbit); /* 支持 Y 轴移动 */
4.1.3 代码解释
c
static int usb_mouse_probe(struct usb_interface *intf, const struct usb_device_id *id)
{
struct usb_device *dev = interface_to_usbdev(intf); // 通过接口结构体获取对应的 USB 设备指针 usb_device
struct usb_host_interface *interface;
struct usb_endpoint_descriptor *endpoint; // 端点描述符指针
struct usb_mouse *mouse; // 本驱动自定义的私有数据结构体指针
struct input_dev *input_dev; // 内核 Input 子系统的设备结构体指针
int pipe, maxp;
int error = -ENOMEM;
interface = intf->cur_altsetting; // 获取当前接口的活跃设置 (Altsetting)
/* ------------------ 1. 硬件合规性校验 ------------------ */
if (interface->desc.bNumEndpoints != 1) // 启动校验:Boot Protocol 标准 USB 鼠标的端点数量必须有且仅有 1 个
return -ENODEV;
endpoint = &interface->endpoint[0].desc; // 获取这唯一的端点描述符
if (!usb_endpoint_is_int_in(endpoint)) // 校验端点传输类型:必须是"中断输入"端点 (Interrupt IN)
return -ENODEV;
/* ------------------ 2. USB 端点与管道配置 ------------------ */
pipe = usb_rcvintpipe(dev, endpoint->bEndpointAddress); // 创建中断输入管道 (Pipe),结合了设备号和端点地址
maxp = usb_maxpacket(dev, pipe, usb_pipeout(pipe)); // 获取端点支持的最大包长 (Max Packet Size)
/* ------------------ 3. 驱动内存与资源申请 ------------------ */
mouse = kzalloc(sizeof(struct usb_mouse), GFP_KERNEL); // 申请本驱动私有结构体 usb_mouse
input_dev = input_allocate_device(); // 向内核 Input 核心申请分配 input_dev 结构体
if (!mouse || !input_dev)
goto fail1;
/* 分配一致性 DMA 缓冲区 (8字节),避免 Cache 一致性问题,DMA 直接将数据写入 mouse->data */
mouse->data = usb_alloc_coherent(dev, 8, GFP_ATOMIC, &mouse->data_dma);
if (!mouse->data)
goto fail1;
mouse->irq = usb_alloc_urb(0, GFP_KERNEL); // 为中断传输申请一个 URB (USB Request Block)
if (!mouse->irq)
goto fail2;
/* ------------------ 4. 交叉指针绑定与设备信息构建 ------------------ */
mouse->usbdev = dev; // mouse 私有结构体记录对应的 usb_device 指针
mouse->dev = input_dev; // mouse 私有结构体记录对应的 input_dev 指针
/* 拼接设备名称字符串(厂商名 + 产品名),存入 mouse->name */
if (dev->manufacturer)
strlcpy(mouse->name, dev->manufacturer, sizeof(mouse->name));
if (dev->product) {
if (dev->manufacturer)
strlcat(mouse->name, " ", sizeof(mouse->name));
strlcat(mouse->name, dev->product, sizeof(mouse->name));
}
if (!strlen(mouse->name))
snprintf(mouse->name, sizeof(mouse->name),
"USB HIDBP Mouse %04x:%04x",
le16_to_cpu(dev->descriptor.idVendor),
le16_to_cpu(dev->descriptor.idProduct));
/* 构建物理拓扑路径字符串 (如 "usb-0000:00:14.0-1/input0"),存入 mouse->phys */
usb_make_path(dev, mouse->phys, sizeof(mouse->phys));
strlcat(mouse->phys, "/input0", sizeof(mouse->phys));
/* 将构建好的字符串和属性同步给 input_dev 结构体 */
input_dev->name = mouse->name; // 设置 input 设备的名称
input_dev->phys = mouse->phys; // 设置 input 设备的物理路径
usb_to_input_id(dev, &input_dev->id); // 提取 USB 设备的 Vendor/Product/Version ID 填入 input_dev
input_dev->dev.parent = &intf->dev; // 设置 Linux 设备模型中的父设备节点为当前 USB 接口
/* ------------------ 5. 配置 input_dev 的能力位图 (Capabilities) ------------------ */
// evbit:表明该设备能够产生"按键事件 (EV_KEY)"和"相对位移事件 (EV_REL)"
input_dev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REL);
// keybit:声明支持的按键类型(鼠标左键、右键、中键)
input_dev->keybit[BIT_WORD(BTN_MOUSE)] = BIT_MASK(BTN_LEFT) |
BIT_MASK(BTN_RIGHT) | BIT_MASK(BTN_MIDDLE);
// relbit:声明支持的相对坐标轴(X 轴、Y 轴)
input_dev->relbit[0] = BIT_MASK(REL_X) | BIT_MASK(REL_Y);
// 进一步扩展 keybit:追加支持侧键 (BTN_SIDE) 和额外扩展键 (BTN_EXTRA)
input_dev->keybit[BIT_WORD(BTN_MOUSE)] |= BIT_MASK(BTN_SIDE) |
BIT_MASK(BTN_EXTRA);
// 进一步扩展 relbit:追加支持滚轮 (REL_WHEEL)
input_dev->relbit[0] |= BIT_MASK(REL_WHEEL);
/* 将 mouse 设置为 input_dev 的私有驱动数据 (存入 input_dev->dev.driver_data) */
input_set_drvdata(input_dev, mouse);
/* 绑定 input 设备的打开和关闭回调函数(控制硬件供电/URB 的提交与取消) */
input_dev->open = usb_mouse_open;
input_dev->close = usb_mouse_close;
/* ------------------ 6. 初始化 URB 与 DMA 关联 ------------------ */
/* 填充中断 URB:指定接收端点、缓冲区地址、回调函数 (usb_mouse_irq)、上下文参数 (mouse) 及轮询间隔 */
usb_fill_int_urb(mouse->irq, dev, pipe, mouse->data,
(maxp > 8 ? 8 : maxp),
usb_mouse_irq, mouse, endpoint->bInterval);
mouse->irq->transfer_dma = mouse->data_dma; // 关联之前申请的 DMA 物理地址
mouse->irq->transfer_flags |= URB_NO_TRANSFER_DMA_MAP; // 标记 URB 使用现成的 DMA 映射,无需 USB 核心重复映射
/* ------------------ 7. 注册到内核框架 ------------------ */
error = input_register_device(mouse->dev); // 向内核注册 input 设备,此时系统会在 /dev/input/ 创建 eventX 节点
if (error)
goto fail3;
usb_set_intfdata(intf, mouse); // 将 mouse 设置为 USB 接口 (usb_interface) 的私有数据
return 0; // 探测成功,返回 0
/* ------------------ 错误处理与清理逻辑 ------------------ */
fail3:
usb_free_urb(mouse->irq);
fail2:
usb_free_coherent(dev, 8, mouse->data, mouse->data_dma);
fail1:
input_free_device(input_dev);
kfree(mouse);
return error;
}
这段代码是典型的 Linux USB 鼠标驱动的探测函数 (probe) ,运行于 USB 接口与该驱动匹配成功时。它的核心使命是:完成硬件校验、内存资源申请、底层 USB URB 配置,并将 USB 硬件数据转译连接到内核 Input 输入子系统中。
usb_mouse 与 input_dev 的关系解析
在 Linux 驱动开发中,usb_mouse 与 input_dev 代表了架构分层思想 中的两个核心维度:硬件物理层 与逻辑事件层 。它们通过交叉绑定实现了数据和控制流的解耦。
1. 职责分工
| 结构体 | 扮演角色 | 核心职责 |
|---|---|---|
struct usb_mouse |
硬件/私有层 | 代表真实的 USB 物理设备。负责管理 USB URB 传输、DMA 内存缓冲区、USB 描述符解析以及控制底层 USB 中断通信。 |
struct input_dev |
系统/逻辑层 | 代表抽象的 Linux 输入设备 。向内核及用户态注册 /dev/input/eventX,声明设备具备的能力(按键、坐标轴),并提供标准事件上报接口。 |
2. 指针绑定关系图解
代码通过双向指针建立了相互调用的通道:
+----------------------------------+ +----------------------------------+
| struct usb_mouse | | struct input_dev |
| | mouse->dev | |
| (底层 USB 通信私有结构体) | -------------------> | (Linux Input 子系统抽象设备) |
| | | |
| - struct usb_device *usbdev | input_get_drvdata() | - evbit / keybit / relbit |
| - struct urb *irq | <------------------- | - dev.driver_data |
| - signed char *data (DMA 缓存) | (input_set_drvdata) | - int (*open) / int (*close) |
+----------------------------------+ +----------------------------------+
3. 关键的"双向协作"机制
① 正向数据上报:usb_mouse → \rightarrow → input_dev
当硬件产生位移或点击时,USB 硬件响应中断:
-
USB 控制器收到数据包,触发 URB 回调函数
usb_mouse_irq(struct urb *urb)。 -
回调函数的参数
urb->context取出的就是mouse指针。 -
驱动解析
mouse->data中的原始 DMA 数据包。 -
驱动通过
mouse->dev找到绑定的input_dev指针,调用内核标准接口进行上报:cinput_report_key(mouse->dev, BTN_LEFT, data[0] & 0x01); // 上报左键按下/抬起 input_report_rel(mouse->dev, REL_X, data[1]); // 上报 X 轴偏移量 input_sync(mouse->dev); // 发送同步信号 (EV_SYN)
② 反向控制控制:input_dev → \rightarrow → usb_mouse
当用户态程序(如 X11 或嵌入式 GUI)打开或关闭设备节点(如 /dev/input/event0)时:
- Input Core 触发
input_dev->open(即usb_mouse_open)或close回调。 - 回调函数传入的是
struct input_dev *dev指针。 - 通过
input_get_drvdata(dev)反向检索出关联的struct usb_mouse *mouse指针。 - 拿到
mouse后,执行底层的 USB 硬件操作:open时 :调用usb_submit_urb(mouse->irq, GFP_KERNEL)开启中断 URB,开始轮询 USB 硬件。close时 :调用usb_kill_urb(mouse->irq)停止 URB,让 USB 设备进入休眠以节约功耗。
4.2 usb_mouse_irq
c
static void usb_mouse_irq(struct urb *urb)
{
/* 1. 上下文与变量提取 */
/* 获取填充 URB 时传入的 context 参数(即 probe 中绑定的 struct usb_mouse 指针) */
struct usb_mouse *mouse = urb->context;
/* 指向 DMA 接收缓冲区,存放的是 USB 鼠标硬件返回的原始 HID 字节数据 */
signed char *data = mouse->data;
/* 获取绑定的 Linux Input 设备指针,用于向子系统上报事件 */
struct input_dev *dev = mouse->dev;
int status;
/* 2. URB 传输状态检查 (Status Checking) */
switch (urb->status) {
case 0: /* 0 表示传输成功,接收到了鼠标发来的有效数据 */
break;
case -ECONNRESET: /* 异步解除链接 (usb_unlink_urb) */
case -ENOENT: /* URB 被同步强行取消 (usb_kill_urb) */
case -ESHUTDOWN: /* USB 主控制器或 Hub 被拔出/禁用/关闭 */
/* 以上三种情况属于设备正在被卸载或停用,直接返回,绝不能重新提交 URB */
return;
/* -EPIPE: 端点被挂起 (halted),常规驱动开发中通常需要调用 usb_clear_halt 清除 */
default: /* 发生了其他传输错误(如 CRC 校验错误、Noise 干扰等) */
goto resubmit; /* 忽略本次异常数据,直接跳过解析,尝试重新提交 URB */
}
/* 3. 解析 HID 数据并向 Input 子系统上报 (Data Parsing & Reporting) */
/* ------------------ 按键状态解析 (data[0]) ------------------ */
/* data[0] 的每一位 (Bit) 对应一个按键状态:1 为按下,0 为抬起 */
input_report_key(dev, BTN_LEFT, data[0] & 0x01); // Bit 0: 鼠标左键
input_report_key(dev, BTN_RIGHT, data[0] & 0x02); // Bit 1: 鼠标右键
input_report_key(dev, BTN_MIDDLE, data[0] & 0x04); // Bit 2: 鼠标中键(滚轮按下)
input_report_key(dev, BTN_SIDE, data[0] & 0x08); // Bit 3: 侧键(如前进键)
input_report_key(dev, BTN_EXTRA, data[0] & 0x10); // Bit 4: 扩展键(如后退键)
/* ------------------ 相对坐标偏移量解析 (data[1]~data[3]) ------------------ */
/* 注意:data 是 signed char 类型,代表补码表示的有符号相对位移量 */
input_report_rel(dev, REL_X, data[1]); // data[1]: X 轴物理位移(正数向右,负数向左)
input_report_rel(dev, REL_Y, data[2]); // data[2]: Y 轴物理位移(正数向下,负数向上)
input_report_rel(dev, REL_WHEEL, data[3]); // data[3]: 滚轮滚动值(正数向前,负数向后)
/* ------------------ 事件同步 ------------------ */
/* 上报 EV_SYN 标识(SYN_REPORT),通知 Input Core 将上述所有改变打包一次性推送到用户态 */
input_sync(dev);
/* 4. 重新提交 URB (Resubmit URB) */
resubmit:
/*
* USB 中断传输本质上仍是主控器发起的轮询(One-shot)。
* 每次 URB 完成后必须手动重新提交,驱动才能在下一个轮询周期(bInterval)继续收到数据。
* 注意:此回调处于中断上下文,内存分配标志必须使用 GFP_ATOMIC!
*/
status = usb_submit_urb(urb, GFP_ATOMIC);
if (status)
dev_err(&mouse->usbdev->dev,
"can't resubmit intr, %s-%s/input0, status %d\n",
mouse->usbdev->bus->bus_name,
mouse->usbdev->devpath, status);
}
这段代码是 USB 鼠标驱动的核心------URB 中断传输完成回调函数(usb_mouse_irq)。
当 USB 主控芯片通过中断端点成功轮询到鼠标的数据(或者发生传输错误)时,系统底层会在 中断上下文 中自动调用该函数。它的使命是:解析物理数据包、上报 Input 事件,并重新提交 URB 以维持连续的硬件轮询。
核心机制深度说明:
1. 硬件数据包协议
标准的 USB HID Boot 协议鼠标返回的数据长度通常为 4 字节,mouse->data 数组各字节含义如下:
| 字节偏移 | 数据类型 | 对应变量 | 物理含义与位定义 |
|---|---|---|---|
data[0] |
unsigned char |
按键掩码 | Bit 0: 左键 |
data[1] |
signed char |
X 轴相对坐标 | -128 ~ +127,负数代表向左移动,正数代表向右移动 |
data[2] |
signed char |
Y 轴相对坐标 | -128 ~ +127,负数代表向上移动,正数代表向下移动 |
data[3] |
signed char |
滚轮相对位移 | -128 ~ +127,负数代表向下/后滚,正数代表向上/前滚 |
2. 中断传输的"乒乓轮询"机制
USB 规范中不存在真正的"硬件主动中断"。所谓的"USB 中断传输",是 USB Host 控制器按照 endpoint->bInterval 指定的时间间隔,周期性地向鼠标发包询问是否有数据。
-
当鼠标没有移动时,硬件不会返回数据完成包;
-
一旦鼠标发生位移或按键点击,Host 控制器读取到数据,触发
usb_mouse_irq; -
函数末尾的
usb_submit_urb(urb, GFP_ATOMIC)是维持这个周期的关键。如果没有重新提交,USB 轮询就会终止,鼠标也会"冻结"不再响应。cswitch (urb->status) { case 0: /* 0 表示传输成功,接收到了鼠标发来的有效数据 */ break; case -ECONNRESET: /* 异步解除链接 (usb_unlink_urb) */ case -ENOENT: /* URB 被同步强行取消 (usb_kill_urb) */ case -ESHUTDOWN: /* USB 主控制器或 Hub 被拔出/禁用/关闭 */ /* 以上三种情况属于设备正在被卸载或停用,直接返回,绝不能重新提交 URB */ return; /* -EPIPE: 端点被挂起 (halted),常规驱动开发中通常需要调用 usb_clear_halt 清除 */ default: /* 发生了其他传输错误(如 CRC 校验错误、Noise 干扰等) */ goto resubmit; /* 忽略本次异常数据,直接跳过解析,尝试重新提交 URB */ }URB 传输状态 执行路线 结果与目的 成功 ( case 0)case 0→ \rightarrow →break跳出switch→ \rightarrow → 顺序执行数据解析与input_sync→ \rightarrow → 顺路走到resubmit:→ \rightarrow → 执行usb_submit_urb数据有效,先上报给应用层,再重新提交 URB 以等待下一次数据。 硬件临时错误 ( default)default→ \rightarrow →goto resubmit;跨跃 → \rightarrow → 跳过中间的数据解析 → \rightarrow → 降落到resubmit:→ \rightarrow → 执行usb_submit_urb数据损坏不可用(丢弃),防止误报导致光标乱飞;但设备还在,直接重新提交 URB 给下一次尝试的机会。 设备拔出/停用 ( -ESHUTDOWN等)匹配 case→ \rightarrow →return彻底退出函数设备已断开,必须强制停止 URB 的循环提交,防止死循环和内存崩溃。