2. USB系统架构:主机控制器、USB子系统与设备框架

好,咱们今天聊聊USB系统的骨架。说白了,就是USB怎么组织起来的。

我刚开始接触USB时,最头疼的就是搞不清谁管谁。主机、设备、控制器、根集线器……一堆名词绕来绕去。后来我画了一张图,才真正理清楚。嗯,咱们就从这张图开始。

USB系统架构总览 主机(Host) 主机控制器(HC) USB子系统(软件栈) 根集线器(Root Hub) USB 总线(物理层 + 协议层) 设备(Device) 设备框架(Device Framework) 描述符(Descriptors) 端点(Endpoints) 配置描述符 接口描述符 端点描述符 设备描述符 字符串描述符 控制端点0 中断端点 批量端点 枚举交互

2.1 主机控制器(Host Controller)—— 总指挥

主机控制器,我习惯叫它HC。它是USB世界的总指挥。所有USB通信,都得听它的调度。

你想想看,USB总线上挂着一堆设备:鼠标、键盘、U盘、摄像头……它们不能同时说话,否则就乱套了。HC就是那个「谁先讲、谁后讲」的裁判。

HC的核心职责:

  • 管理总线上的数据传输(帧/微帧调度)
  • 处理设备枚举(发现新设备、分配地址)
  • 错误检测与恢复(CRC校验、重试机制)
  • 电源管理(挂起、唤醒)

我记得有一次调试一个USB摄像头,死活枚举不过去。抓包一看,HC发出去的SETUP包设备根本没响应。后来发现是HC的帧起始包(SOF)频率不对,设备没同步上。嗯,这种问题最坑人。

目前主流的HC规范有几种:

规范 速度 特点
UHCI(Intel) USB 1.1 全速/低速 软件实现大部分协议,CPU负担重
OHCI(Microsoft/Compaq) USB 1.1 全速/低速 硬件实现更多,CPU负担轻
EHCI USB 2.0 高速 向下兼容UHCI/OHCI
xHCI USB 3.x 统一架构,支持所有速度

个人经验:做项目时,我建议优先选xHCI。虽然它复杂一些,但兼容性最好。USB 3.0的UASP协议,只有xHCI支持得最完整。

2.2 USB子系统(USB Subsystem)—— 软件栈

HC是硬件,但光有硬件不行。还得有软件来管它。这就是USB子系统,也叫USB软件栈。

说白了,USB子系统就是操作系统里那一套驱动框架。它把HC的寄存器操作封装成API,让上层应用不用管底层细节。

我画个层级给你看:

应用层(App)
    ↓
类驱动(Class Driver)—— 比如HID、Mass Storage
    ↓
USB核心层(USB Core)—— 枚举、地址分配、管道管理
    ↓
主机控制器驱动(HCD)—— 操作HC寄存器
    ↓
主机控制器(硬件)

这个结构,我当年啃了整整两周才搞明白。尤其是USB Core和HCD之间的接口,不同操作系统实现方式还不一样。

避坑指南:我曾经在Linux上写一个自定义HID设备驱动,发现枚举成功了但数据传不上来。查了三天,结果是USB Core层的urb请求没设置正确的端点号。记住:urb->pipe必须和描述符里的端点号一致。

USB子系统里有个关键概念——管道(Pipe)。管道是HC和设备端点之间的逻辑连接。每个管道对应一个端点,有方向(IN/OUT)和传输类型(控制、批量、中断、等时)。

2.3 设备框架(Device Framework)与描述符

好,现在说说设备这边。USB设备不是随便插上就能用的。它得先「自我介绍」。这个自我介绍,就是描述符。

设备框架,说白了就是USB设备的标准组织结构。每个USB设备,从逻辑上可以拆成三层:

  • 设备层:一个设备只有一个设备描述符,描述整体信息
  • 配置层:一个设备可以有多个配置(但同一时间只能激活一个)
  • 接口层:每个配置下有多个接口,接口代表功能
  • 端点层:每个接口下有多个端点,端点是实际通信的管道口

描述符就是这些层级的数据结构。HC通过枚举过程,一个个地把它们读回来。

2.3.1 设备描述符(Device Descriptor)

这是第一个被读的描述符。它告诉主机:我是谁?

struct usb_device_descriptor {
    uint8_t  bLength;            // 描述符长度(固定18字节)
    uint8_t  bDescriptorType;    // 描述符类型(0x01)
    uint16_t bcdUSB;             // USB规范版本号(如0x0200表示USB 2.0)
    uint8_t  bDeviceClass;       // 设备类
    uint8_t  bDeviceSubClass;    // 设备子类
    uint8_t  bDeviceProtocol;    // 设备协议
    uint8_t  bMaxPacketSize0;    // 端点0最大包大小
    uint16_t idVendor;           // 厂商ID(VID)
    uint16_t idProduct;          // 产品ID(PID)
    uint16_t bcdDevice;          // 设备版本号
    uint8_t  iManufacturer;      // 厂商字符串索引
    uint8_t  iProduct;           // 产品字符串索引
    uint8_t  iSerialNumber;      // 序列号字符串索引
    uint8_t  bNumConfigurations; // 配置描述符数量
};

关键点:bMaxPacketSize0这个字段特别重要。它决定了控制传输时每次能传多少字节。USB 2.0高速设备必须是64字节,全速设备可以是8/16/32/64字节。设错了,枚举直接失败。

2.3.2 配置描述符(Configuration Descriptor)

读完设备描述符,HC会继续读配置描述符。配置描述符描述了这个配置的功耗、接口数量等信息。

struct usb_configuration_descriptor {
    uint8_t  bLength;             // 长度(9字节)
    uint8_t  bDescriptorType;     // 类型(0x02)
    uint16_t wTotalLength;        // 配置描述符总长度(包括接口和端点)
    uint8_t  bNumInterfaces;      // 接口数量
    uint8_t  bConfigurationValue; // 配置值(用于SetConfiguration)
    uint8_t  iConfiguration;      // 配置字符串索引
    uint8_t  bmAttributes;        // 属性(自供电/总线供电/远程唤醒)
    uint8_t  bMaxPower;           // 最大功耗(单位2mA)
};

这里有个坑:wTotalLength是整个配置描述符树的总长度,包括它自己、所有接口描述符和端点描述符。我见过有人只算了配置描述符自己的9字节,结果HC读了一半就卡住了。

2.3.3 接口描述符(Interface Descriptor)

接口描述符描述了一个功能接口。比如一个U盘可能有两个接口:一个用于Bulk传输数据,一个用于中断传输状态。

struct usb_interface_descriptor {
    uint8_t  bLength;            // 长度(9字节)
    uint8_t  bDescriptorType;    // 类型(0x04)
    uint8_t  bInterfaceNumber;   // 接口编号
    uint8_t  bAlternateSetting;  // 备用设置
    uint8_t  bNumEndpoints;      // 端点数量(不包括端点0)
    uint8_t  bInterfaceClass;    // 接口类
    uint8_t  bInterfaceSubClass; // 接口子类
    uint8_t  bInterfaceProtocol; // 接口协议
    uint8_t  iInterface;         // 接口字符串索引
};

我的习惯:写设备固件时,我会把bInterfaceClass、bInterfaceSubClass、bInterfaceProtocol这三个字段和Windows的INF文件对照着设。设错了,Windows会弹「未知设备」。

2.3.4 端点描述符(Endpoint Descriptor)

端点描述符是最后一层。它描述了每个端点的传输属性。

struct usb_endpoint_descriptor {
    uint8_t  bLength;          // 长度(7字节)
    uint8_t  bDescriptorType;  // 类型(0x05)
    uint8_t  bEndpointAddress; // 端点地址(bit7方向,bit3-0端点号)
    uint8_t  bmAttributes;     // 传输类型(0:控制,1:等时,2:批量,3:中断)
    uint16_t wMaxPacketSize;   // 最大包大小
    uint8_t  bInterval;        // 轮询间隔(单位:帧/微帧)
};

bInterval这个字段,不同传输类型含义不同。中断传输时,它表示轮询间隔。全速设备单位是1ms帧,高速设备单位是125μs微帧。我曾经设错过这个值,导致鼠标光标一跳一跳的。

2.3.5 字符串描述符(String Descriptor)

字符串描述符是可选的。它提供了人类可读的厂商名、产品名、序列号。HC通过索引值(iManufacturer、iProduct、iSerialNumber)来请求对应的字符串。

struct usb_string_descriptor {
    uint8_t  bLength;         // 长度
    uint8_t  bDescriptorType; // 类型(0x03)
    uint16_t wString[];       // Unicode字符串
};

注意:字符串描述符使用UTF-16LE编码。我见过有人直接塞ASCII字符,结果在Linux上显示正常,Windows上全是乱码。老老实实用UTF-16LE吧。

2.4 枚举流程回顾

把上面这些串起来,就是完整的枚举流程:

  1. 设备插入,HC检测到D+/D-电平变化
  2. HC复位设备(持续10ms以上的SE0信号)
  3. HC读取设备描述符的前8字节(获取bMaxPacketSize0)
  4. HC分配地址(SetAddress请求)
  5. HC重新读取完整设备描述符(18字节)
  6. HC读取配置描述符(包括接口和端点)
  7. HC选择配置(SetConfiguration)
  8. 设备进入配置状态,可以正常通信

嗯,这个流程我背得滚瓜烂熟。因为每次调试新硬件,都得拿逻辑分析仪盯着这一步一步走。哪一步卡住了,问题就在哪。

好了,USB系统架构就聊到这儿。记住一句话:主机控制器是大脑,描述符是身份证,枚举是握手。这三样搞明白了,USB的大门你就跨进去了。


专注资料整理