2. USB基础协议

USB(Universal Serial Bus)自1996年规范发布以来,已从最初的外设连接总线演变为嵌入式系统、移动设备、PC外设乃至工业控制中最核心的通信接口之一。对于USB Gadget驱动开发者而言,理解USB协议栈的底层逻辑——从物理拓扑到逻辑管道,从描述符结构到枚举握手——是编写可靠、兼容性强的设备端驱动的先决条件。本章将围绕USB体系结构、传输类型、端点、描述符与枚举流程五个核心维度展开,帮助您建立从硬件信号到软件驱动的完整认知闭环。

USB 2.0 协议核心概念关系图 USB Host(主机) 根集线器 / 主机控制器 USB Device(设备) Gadget / 外设控制器 物理总线(D+/D- 差分对) 逻辑设备(Logical Device) 端点集合 + 设备地址 + 配置/接口/端点描述符 端点0(控制) 双向,枚举专用 端点1(批量) Bulk OUT/IN 端点2(中断) Interrupt IN 端点3(同步) Isochronous 四种传输类型 控制:配置/命令 批量:大块数据 中断:低延迟查询 同步:实时流 双向、可靠、带宽小 单向、可靠、带宽大 单向、可靠、周期查询 单向、无保证、带宽预留 枚举流程:复位 → 地址分配 → 读描述符 → 配置

上图展示了USB 2.0协议中主机、设备、逻辑设备、端点与传输类型之间的整体关系。主机通过物理总线与设备相连,设备内部以端点为基本通信单元,端点组合成逻辑设备,而传输类型则定义了数据交换的时序与可靠性要求。理解这张图,是后续编写Gadget驱动的认知基础。

2.1 USB体系结构:分层拓扑与角色分工

USB采用星型拓扑结构,由主机(Host)作为唯一主控端,通过根集线器(Root Hub)向下扩展,最多支持127个设备地址。在USB 2.0规范中,通信模式为主从式——所有事务均由主机发起,设备被动响应。这种设计简化了设备端逻辑,但也意味着Gadget驱动必须严格遵循主机的时间片调度。

从协议栈角度,USB体系可分为三层:物理层(负责差分信号、电气特性、NRZI编码)、链路层(处理包格式、握手协议、CRC校验)、功能层(面向应用的数据流与管道管理)。Gadget驱动主要工作在功能层,但必须理解底层机制才能正确配置端点描述符与传输类型。

核心要点:在USB体系中,设备无法主动向主机发送数据。所有IN事务(设备→主机)都必须等待主机发送IN令牌包后,设备才能将数据放到总线上。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 / 512U盘、MTP、RNDIS硬件重试(CRC/ACK)
中断(Interrupt)单向64 / 1024HID鼠标、键盘、调试日志硬件重试
同步(Isochronous)单向1023 / 1024音频流、摄像头、实时传感器无重试,丢包不补偿

2.3 传输类型:时序与可靠性的权衡

USB定义了四种传输类型,每种对应不同的服务特性。控制传输用于设备配置和命令交互,具有最高的协议优先级,但带宽占用小。批量传输面向大块数据,利用空闲带宽,保证可靠交付但不保证延迟。中断传输保证最大查询延迟,适合低频但需及时响应的数据。同步传输则预留固定带宽,保证恒定速率,但出错时不重传。

在Gadget驱动中,选择传输类型需考虑以下因素:

  • 数据量:大于1KB且可容忍延迟,选批量;小于几百字节且需周期性,选中
  • 实时性:音频/视频流必须用同步,否则丢帧;HID报告用中断即可
  • 可靠性:控制与批量有硬件重试,同步无重试,需应用层容错
  • 带宽预算:同步传输需在枚举时声明带宽,主机在每帧(1ms)或微帧(125µs)内调度
常见误区:许多初学者将中断传输理解为“设备主动发起中断”,实际上USB中断传输仍是主机轮询模式,只是主机保证在bInterval时间内至少查询一次。Gadget驱动中的中断回调是设备收到IN令牌后的响应,而非硬件中断。

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或静态定义的方式注册。需要注意的是,描述符中的字段对于中断和同步端点含义不同:中断端点的单位是帧(1ms)或微帧(125µs),而同步端点则需根据规范计算。

2.5 枚举流程:从设备插入到驱动绑定

枚举是USB设备生命周期中最关键的阶段,它决定了设备能否被主机正确识别并加载驱动。完整枚举流程如下:

  1. 设备插入与复位:主机检测到D+上拉(全速/高速)或D-上拉(低速),向设备发送复位信号(SE0持续10ms以上)。
  2. 默认地址通信:设备以地址0响应主机请求,主机发送GET_DESCRIPTOR(Device)获取设备描述符前8字节(获取MaxPacketSize)。
  3. 分配地址:主机发送SET_ADDRESS请求,设备进入Address状态,此后所有通信使用新地址。
  4. 完整读取描述符:主机再次GET_DESCRIPTOR(Device)获取完整18字节,随后读取配置描述符(含接口、端点、类描述符)。
  5. 配置设备:主机发送SET_CONFIGURATION(1),设备激活指定配置,所有非0端点可用。
  6. 类特定请求:对于HID、Mass Storage等类设备,主机发送类请求(如HID Report Descriptor)完成初始化。
实战提示:在Gadget驱动调试中,建议在枚举阶段加入日志输出,打印每个标准请求的和。若设备在SET_ADDRESS后无响应,优先检查端点0的MaxPacketSize是否与描述符一致;若配置描述符解析失败,检查bLength和bDescriptorType字段是否正确。

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驱动需自行处理丢包(如填充静音帧或重复上一帧)。