2. USB系统架构:主机控制器、USB子系统与设备框架
好,咱们今天聊聊USB系统的骨架。说白了,就是USB怎么组织起来的。
我刚开始接触USB时,最头疼的就是搞不清谁管谁。主机、设备、控制器、根集线器……一堆名词绕来绕去。后来我画了一张图,才真正理清楚。嗯,咱们就从这张图开始。
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 枚举流程回顾
把上面这些串起来,就是完整的枚举流程:
- 设备插入,HC检测到D+/D-电平变化
- HC复位设备(持续10ms以上的SE0信号)
- HC读取设备描述符的前8字节(获取bMaxPacketSize0)
- HC分配地址(SetAddress请求)
- HC重新读取完整设备描述符(18字节)
- HC读取配置描述符(包括接口和端点)
- HC选择配置(SetConfiguration)
- 设备进入配置状态,可以正常通信
嗯,这个流程我背得滚瓜烂熟。因为每次调试新硬件,都得拿逻辑分析仪盯着这一步一步走。哪一步卡住了,问题就在哪。
好了,USB系统架构就聊到这儿。记住一句话:主机控制器是大脑,描述符是身份证,枚举是握手。这三样搞明白了,USB的大门你就跨进去了。