2. USB基础协议
USB(Universal Serial Bus)自1996年规范发布以来,已从最初的外设连接总线演变为嵌入式系统、移动设备、PC外设乃至工业控制中最核心的通信接口之一。对于USB Gadget驱动开发者而言,理解USB协议栈的底层逻辑——从物理拓扑到逻辑管道,从描述符结构到枚举握手——是编写可靠、兼容性强的设备端驱动的先决条件。本章将围绕USB体系结构、传输类型、端点、描述符与枚举流程五个核心维度展开,帮助您建立从硬件信号到软件驱动的完整认知闭环。
上图展示了USB 2.0协议中主机、设备、逻辑设备、端点与传输类型之间的整体关系。主机通过物理总线与设备相连,设备内部以端点为基本通信单元,端点组合成逻辑设备,而传输类型则定义了数据交换的时序与可靠性要求。理解这张图,是后续编写Gadget驱动的认知基础。
2.1 USB体系结构:分层拓扑与角色分工
USB采用星型拓扑结构,由主机(Host)作为唯一主控端,通过根集线器(Root Hub)向下扩展,最多支持127个设备地址。在USB 2.0规范中,通信模式为主从式——所有事务均由主机发起,设备被动响应。这种设计简化了设备端逻辑,但也意味着Gadget驱动必须严格遵循主机的时间片调度。
从协议栈角度,USB体系可分为三层:物理层(负责差分信号、电气特性、NRZI编码)、链路层(处理包格式、握手协议、CRC校验)、功能层(面向应用的数据流与管道管理)。Gadget驱动主要工作在功能层,但必须理解底层机制才能正确配置端点描述符与传输类型。
2.2 端点(Endpoint):通信的基本单元
端点是USB设备中唯一可寻址的通信实体,每个端点由设备地址、端点号、方向(IN/OUT)唯一确定。端点0是所有设备必须实现的默认控制端点,用于枚举和标准请求处理。除端点0外,其他端点需在配置描述符中声明,并在Set Configuration后生效。
端点描述符的关键字段包括:bEndpointAddress(端点号+方向)、bmAttributes(传输类型)、wMaxPacketSize(最大包大小)、bInterval(查询间隔)。对于高速设备,批量端点最大包为512字节,中断端点最大为1024字节(但受bInterval限制),同步端点则需预留带宽。
| 端点类型 | 方向 | 最大包大小(FS/HS) | 典型应用 | 错误重试 |
|---|---|---|---|---|
| 控制(Control) | 双向 | 64 / 64 | 枚举、标准请求、类请求 | 协议层重试 |
| 批量(Bulk) | 单向 | 64 / 512 | U盘、MTP、RNDIS | 硬件重试(CRC/ACK) |
| 中断(Interrupt) | 单向 | 64 / 1024 | HID鼠标、键盘、调试日志 | 硬件重试 |
| 同步(Isochronous) | 单向 | 1023 / 1024 | 音频流、摄像头、实时传感器 | 无重试,丢包不补偿 |
2.3 传输类型:时序与可靠性的权衡
USB定义了四种传输类型,每种对应不同的服务特性。控制传输用于设备配置和命令交互,具有最高的协议优先级,但带宽占用小。批量传输面向大块数据,利用空闲带宽,保证可靠交付但不保证延迟。中断传输保证最大查询延迟,适合低频但需及时响应的数据。同步传输则预留固定带宽,保证恒定速率,但出错时不重传。
在Gadget驱动中,选择传输类型需考虑以下因素:
- 数据量:大于1KB且可容忍延迟,选批量;小于几百字节且需周期性,选中
- 实时性:音频/视频流必须用同步,否则丢帧;HID报告用中断即可
- 可靠性:控制与批量有硬件重试,同步无重试,需应用层容错
- 带宽预算:同步传输需在枚举时声明带宽,主机在每帧(1ms)或微帧(125µs)内调度
2.4 描述符:设备的“自我介绍”
描述符是USB设备向主机声明自身能力的数据结构,采用TLV(Type-Length-Value)格式。标准描述符包括设备描述符(Device)、配置描述符(Configuration)、接口描述符(Interface)、端点描述符(Endpoint)以及字符串描述符(String)。此外还有类特定描述符(如HID Report Descriptor)和厂商自定义描述符。
设备描述符是枚举时第一个被读取的,包含USB版本号、设备类、厂商ID(VID)、产品ID(PID)等关键信息。配置描述符则描述设备的供电方式、接口数量、端点配置。一个设备可以有多个配置,但同一时刻只能激活一个;一个配置下可有多个接口,接口下可有多个端点(端点0除外)。
// 典型设备描述符(Linux内核中定义)
struct usb_device_descriptor {
__u8 bLength; // 18
__u8 bDescriptorType; // 0x01
__le16 bcdUSB; // 0x0200
__u8 bDeviceClass; // 0x00(由接口定义)
__u8 bDeviceSubClass;
__u8 bDeviceProtocol;
__u8 bMaxPacketSize0; // 64
__le16 idVendor; // 0x1234
__le16 idProduct; // 0x5678
__le16 bcdDevice; // 0x0100
__u8 iManufacturer;
__u8 iProduct;
__u8 iSerialNumber;
__u8 bNumConfigurations;
} __attribute__ ((packed));
在Gadget驱动中,描述符通常通过usb_ep_autoconfig或静态定义的方式注册。需要注意的是,描述符中的
2.5 枚举流程:从设备插入到驱动绑定
枚举是USB设备生命周期中最关键的阶段,它决定了设备能否被主机正确识别并加载驱动。完整枚举流程如下:
- 设备插入与复位:主机检测到D+上拉(全速/高速)或D-上拉(低速),向设备发送复位信号(SE0持续10ms以上)。
- 默认地址通信:设备以地址0响应主机请求,主机发送GET_DESCRIPTOR(Device)获取设备描述符前8字节(获取MaxPacketSize)。
- 分配地址:主机发送SET_ADDRESS请求,设备进入Address状态,此后所有通信使用新地址。
- 完整读取描述符:主机再次GET_DESCRIPTOR(Device)获取完整18字节,随后读取配置描述符(含接口、端点、类描述符)。
- 配置设备:主机发送SET_CONFIGURATION(1),设备激活指定配置,所有非0端点可用。
- 类特定请求:对于HID、Mass Storage等类设备,主机发送类请求(如HID Report Descriptor)完成初始化。
2.6 案例拆解:一个自定义HID设备的枚举排障
假设我们基于Linux Gadget框架实现一个自定义HID设备(VID=0x1234, PID=0x5678),在枚举过程中遇到主机无法识别设备的问题。通过dmesg查看内核日志,发现以下错误:
usb 1-1: new full-speed USB device number 5 using xhci_hcd
usb 1-1: device descriptor read/64, error -71
usb 1-1: device descriptor read/64, error -71
usb 1-1: new full-speed USB device number 6 using xhci_hcd
usb 1-1: device descriptor read/64, error -71
错误-71表示协议错误(EPROTO),通常意味着设备返回的描述符格式错误或时序不满足规范。我们通过逻辑分析仪抓取总线数据,发现设备在收到GET_DESCRIPTOR请求后,返回的数据长度只有16字节,而主机期望至少18字节。进一步检查Gadget驱动代码,发现设备描述符结构体中bNumConfigurations字段未初始化,导致内存中的随机值被发送。
修复方法:在描述符定义处显式初始化所有字段,并使用usb_assign_descriptors函数注册。修复后重新枚举,设备成功识别为HID设备,dmesg输出正常:
usb 1-1: new full-speed USB device number 7 using xhci_hcd
input: USB HID v1.11 Device [Custom Gadget] on usb-0000:00:14.0-1
复盘经验:USB描述符是严格的结构化数据,任何字段缺失或长度错误都会导致枚举失败。建议在驱动初始化时使用memset清零描述符缓冲区,避免栈或堆中的残留数据影响协议解析。
2.7 常见误区与总结
在USB基础协议的学习与实践中,以下误区值得特别关注:
- 误区一:认为端点0可以用于批量数据传输。端点0仅用于控制传输,且MaxPacketSize通常为64字节(全速)或8字节(低速),不适合大数据传输。
- 误区二:忽略高速与全速的包大小差异。批量端点在全速下最大64字节,高速下为512字节。若Gadget驱动未根据速度调整描述符,可能导致高速枚举失败。
- 误区三:混淆bInterval的单位。对于全速中断端点,bInterval单位是1ms;对于高速中断端点,单位是125µs(微帧)。设置不当会导致主机拒绝配置。
- 误区四:在同步传输中期望硬件重传。同步传输不保证交付,Gadget驱动需自行处理丢包(如填充静音帧或重复上一帧)。