2. 协议栈架构解析:Linux USB Gadget框架 vs Host端协议栈、核心数据结构与URB生命周期
好,咱们进入第二章。这一章我打算把USB协议栈的骨架给你拆开看看。很多工程师做了好几年USB驱动,问起Gadget和Host到底有啥区别,还是一脸懵。说白了,这两套东西就像一枚硬币的两面——一个管发号施令,一个管听令行事。但它们的代码路径、核心数据结构、数据流动方式,差别还真不小。
2.1 Host端协议栈:谁在发号施令?
先看Host端。你电脑上插个U盘,系统怎么知道它是啥?靠的就是Host协议栈。它负责枚举设备、分配地址、发送控制请求、调度传输。
Host端的核心逻辑,我习惯把它分成三层:
- USB Core:最底层,管理总线、设备枚举、URB的分配与回收。
- USB Hub驱动:检测设备插拔,复位端口,通知Core有新设备来了。
- 设备类驱动:比如存储类(usb-storage)、HID类(鼠标键盘)、CDC类(串口)。它们不直接操作硬件,而是通过URB跟Core打交道。
你想想看,一个U盘插上去,Hub驱动先检测到电平变化,然后Core开始枚举:发Get Descriptor、Set Address、再发Get Descriptor……这一串动作,全是Host主动发起的。设备只能被动应答。
关键认知:Host端是主控方,所有传输都由Host调度。设备端没有“主动发送”的权利,只能等Host来取数据。
2.2 Gadget框架:设备端的“反客为主”
Gadget框架就完全反过来了。它运行在设备端——比如你的开发板模拟成一个U盘、一个串口、或者一个网卡。它不主动发起传输,而是准备好数据,等Host来读。
Gadget框架的层次是这样的:
- UDC(USB Device Controller)驱动:直接操作硬件寄存器,处理中断。这是最底层。
- Gadget Core:管理端点、处理请求队列、回调通知上层。
- Function驱动:实现具体功能,比如Mass Storage、Serial、Ethernet。每个Function对应一个“设备功能”。
- Configuration层:把多个Function组合成一个配置,暴露给Host。
我记得第一次调Gadget的时候,踩过一个坑:Function驱动里申请了请求,但忘了在完成回调里重新提交。结果Host那边等半天没数据,直接超时断开。嗯,Gadget的请求是“一次性的”,每次用完必须重新入队。
个人经验:调试Gadget时,我习惯先在Function驱动里加个计数器,统计请求完成次数。如果计数不涨,八成是请求没重新提交。
2.3 核心数据结构:usb_device 与 usb_host_endpoint
不管Host还是Gadget,都绕不开两个核心结构体:usb_device 和 usb_host_endpoint。咱们一个一个看。
2.3.1 usb_device:设备的“身份证”
在Host端,每个USB设备对应一个 usb_device 实例。它包含了设备的所有信息:
struct usb_device {
int devnum; // 总线地址,枚举时分配
enum usb_device_state state; // 设备状态:ATTACHED, POWERED, DEFAULT, ADDRESS, CONFIGURED
enum usb_device_speed speed; // 速度:LOW, FULL, HIGH, SUPER
struct usb_device_descriptor descriptor; // 设备描述符
struct usb_host_config *config; // 当前配置
struct usb_host_endpoint *ep0; // 默认控制端点
struct usb_device *parent; // 父设备(Hub)
struct list_head children; // 子设备列表
// ... 还有一堆
};
这里我特别想提一下 state 字段。枚举过程中,设备状态会依次经历:ATTACHED → POWERED → DEFAULT → ADDRESS → CONFIGURED。如果卡在某个状态不动了,那八成是枚举失败了。我曾经调试过一个设备,总是卡在 ADDRESS 状态,后来发现是Set Address请求的响应超时——硬件上D+线虚焊了。
2.3.2 usb_host_endpoint:端点的“档案袋”
每个端点都有一个 usb_host_endpoint 结构体。它记录了端点的属性、当前URB队列、调度信息:
struct usb_host_endpoint {
struct usb_endpoint_descriptor desc; // 端点描述符(地址、方向、类型、最大包长)
struct list_head urb_list; // 挂在该端点上的URB队列
struct usb_device *dev; // 所属设备
unsigned int extra; // 额外信息(如轮询间隔)
// ...
};
注意 urb_list 这个字段。所有发往该端点的URB,都会挂到这个链表上。Host控制器驱动会从这个链表里取URB,按调度策略发送。如果链表为空,端点就处于空闲状态。
避坑指南:我曾经遇到过一个问题——批量端点上的URB队列太长,导致实时性要求高的中断端点被饿死。后来我调整了调度策略,给中断端点留了固定带宽。记住:USB是轮询总线,没有中断机制,所有传输都是Host主动发起的。
2.4 URB生命周期:从生到死
URB(USB Request Block)是USB数据传输的基本单元。你可以把它理解成一个“快递包裹”——里面装着数据、目标端点、回调函数。咱们来看看它的一生。
我画了一张图,把URB的完整生命周期串起来了:
这张图把URB的六个阶段画清楚了。我挑几个关键点展开说说。
2.4.1 分配与初始化
分配URB用 usb_alloc_urb(),初始化用 usb_fill_*_urb() 系列函数。比如你要发一个批量传输:
struct urb *urb;
urb = usb_alloc_urb(0, GFP_KERNEL);
if (!urb) {
dev_err(dev, "无法分配URB\n");
return -ENOMEM;
}
usb_fill_bulk_urb(urb, dev, usb_sndbulkpipe(dev, 1),
buffer, size, bulk_complete, context);
urb->transfer_flags |= URB_ZERO_PACKET; // 批量传输需要发零长度包结束
这里有个细节:usb_alloc_urb() 的 iso_packets 参数,等时传输才需要填非0值。普通传输填0就行。
2.4.2 提交与调度
提交URB用 usb_submit_urb()。这个函数会把URB挂到对应端点的 urb_list 上,然后唤醒Host控制器驱动去处理。注意:提交URB时不能持有锁,否则可能死锁。我见过有人直接在中断上下文里提交URB,结果内核直接panic。
个人习惯:我一般在工作队列或tasklet里提交URB,避免在硬中断上下文操作。如果非要在中断里提交,至少用 GFP_ATOMIC 标志。
2.4.3 完成回调
传输完成后,Host控制器驱动会调用URB的回调函数。回调里可以检查 urb->status 判断是否成功:
static void bulk_complete(struct urb *urb)
{
if (urb->status == 0) {
// 传输成功,处理数据
process_data(urb->transfer_buffer, urb->actual_length);
} else if (urb->status == -ECONNRESET) {
// 被取消,正常
} else if (urb->status == -ESHUTDOWN) {
// 设备断开
} else {
dev_err(urb->dev, "传输错误: %d\n", urb->status);
}
// 如果是批量传输,重新提交
if (need_continue) {
ret = usb_submit_urb(urb, GFP_ATOMIC);
if (ret)
dev_err(urb->dev, "重新提交失败: %d\n", ret);
}
}
注意看,批量传输和中断传输的URB,在完成回调里通常要重新提交,这样才能持续接收数据。控制传输和等时传输一般是一次性的。
2.4.4 取消与释放
取消URB用 usb_unlink_urb() 或 usb_kill_urb()。前者是非阻塞的,后者会等待URB完全停止。释放用 usb_free_urb()。
避坑指南:我曾经在驱动卸载时直接用 usb_free_urb() 释放还在队列里的URB,结果内核报“URB still active”的警告。正确做法:先 usb_kill_urb() 确保URB停止,再释放。顺序不能反。
2.5 Host vs Gadget:核心差异一览
我把Host端和Gadget框架的核心差异整理成了一张表,方便你对比:
| 对比维度 | Host端协议栈 | Gadget框架 |
|---|---|---|
| 角色 | 主控方,发起所有传输 | 从设备,被动响应 |
| 核心结构 | usb_device, usb_host_endpoint |
usb_gadget, usb_ep |
| 数据单元 | URB(struct urb) |
USB请求(struct usb_request) |
| 传输发起 | 驱动调用 usb_submit_urb() |
驱动调用 usb_ep_queue() |
| 调度方式 | HCD按帧/微帧调度 | UDC在中断中处理 |
| 典型应用 | U盘、鼠标、网卡(作为Host) | 模拟U盘、串口、网卡(作为设备) |
这张表你最好记在心里。面试的时候,面试官问“Host和Gadget有什么区别”,你把这几点说出来,基本就稳了。
2.6 小结
这一章咱们把USB协议栈的骨架拆了一遍。Host端是主控方,Gadget是从设备,两者的核心数据结构、数据单元、调度方式完全不同。URB的生命周期——分配、初始化、提交、调度、完成、释放——是驱动开发的基本功。