3. USB 描述符体系:设备描述符、配置描述符、接口描述符、端点描述符、字符串描述符
USB 描述符(Descriptor)是 USB 协议中用于描述设备属性、功能和配置的数据结构。主机通过枚举过程读取这些描述符,从而识别设备类型、加载对应驱动并建立通信管道。在 Linux USB Gadget 驱动中,描述符的构造与注册是驱动开发的核心环节。
3.1 描述符层级结构总览
USB 描述符体系遵循“设备 → 配置 → 接口 → 端点”的层级关系。字符串描述符作为可选补充,提供人类可读的文本信息。下图展示了完整的描述符树状结构:
3.2 设备描述符(Device Descriptor)
设备描述符是 USB 设备的第一份“身份证”,位于描述符树的根节点。主机通过控制传输的 GET_DESCRIPTOR 请求获取,长度固定为 18 字节。
| 偏移 | 字段 | 大小 | 说明 |
|---|---|---|---|
| 0 | bLength | 1 | 描述符长度,固定为 18 |
| 1 | bDescriptorType | 1 | 类型码,设备描述符为 0x01 |
| 2-3 | bcdUSB | 2 | USB 规范版本号(BCD 码),如 0x0200 表示 USB 2.0 |
| 4 | bDeviceClass | 1 | 设备类代码(由 USB-IF 定义),0x00 表示在接口描述符中定义 |
| 5 | bDeviceSubClass | 1 | 设备子类代码 |
| 6 | bDeviceProtocol | 1 | 设备协议代码 |
| 7 | bMaxPacketSize0 | 1 | 端点 0 的最大包大小(8/16/32/64) |
| 8-9 | idVendor | 2 | 厂商 ID(由 USB-IF 分配) |
| 10-11 | idProduct | 2 | 产品 ID(厂商自定义) |
| 12-13 | bcdDevice | 2 | 设备版本号(BCD 码) |
| 14 | iManufacturer | 1 | 厂商字符串描述符索引 |
| 15 | iProduct | 1 | 产品字符串描述符索引 |
| 16 | iSerialNumber | 1 | 序列号字符串描述符索引 |
| 17 | bNumConfigurations | 1 | 支持的配置数量 |
struct usb_device_descriptor 结构体定义,驱动开发者需填充该结构体并注册到 Gadget 框架中。
/* Linux 内核中的设备描述符结构体 */
struct usb_device_descriptor {
__u8 bLength;
__u8 bDescriptorType;
__le16 bcdUSB;
__u8 bDeviceClass;
__u8 bDeviceSubClass;
__u8 bDeviceProtocol;
__u8 bMaxPacketSize0;
__le16 idVendor;
__le16 idProduct;
__le16 bcdDevice;
__u8 iManufacturer;
__u8 iProduct;
__u8 iSerialNumber;
__u8 bNumConfigurations;
} __attribute__ ((packed));
3.3 配置描述符(Configuration Descriptor)
配置描述符描述设备的电源管理属性和接口数量。一个设备可以有多个配置(如低功耗模式和高性能模式),但同一时刻只能激活一个配置。配置描述符长度不固定,因为它需要包含其下所有接口和端点的描述符总长度。
| 字段 | 大小 | 说明 |
|---|---|---|
| bLength | 1 | 固定为 9 |
| bDescriptorType | 1 | 配置描述符类型码 0x02 |
| wTotalLength | 2 | 整个配置返回的数据总长度(含接口和端点描述符) |
| bNumInterfaces | 1 | 该配置支持的接口数量 |
| bConfigurationValue | 1 | 用于 SET_CONFIGURATION 请求的配置值 |
| iConfiguration | 1 | 配置字符串描述符索引 |
| bmAttributes | 1 | 电源属性:D7=总线供电,D6=自供电,D5=远程唤醒 |
| bMaxPower | 1 | 最大电流需求,单位 2mA |
wTotalLength 字段必须精确计算,包含该配置下所有接口描述符、端点描述符以及可能的类特定描述符的总字节数。计算错误会导致主机枚举失败。
3.4 接口描述符(Interface Descriptor)
接口描述符描述一个功能单元(如键盘、音频流、大容量存储)。一个配置可以包含多个接口,接口可以备用设置(Alternate Setting),用于在运行时切换接口参数(如改变端点带宽)。
| 字段 | 大小 | 说明 |
|---|---|---|
| bLength | 1 | 固定为 9 |
| bDescriptorType | 1 | 接口描述符类型码 0x04 |
| bInterfaceNumber | 1 | 接口编号(从 0 开始) |
| bAlternateSetting | 1 | 备用设置编号 |
| bNumEndpoints | 1 | 该接口使用的端点数量(不包括端点 0) |
| bInterfaceClass | 1 | 接口类代码(如 HID=0x03, Mass Storage=0x08) |
| bInterfaceSubClass | 1 | 接口子类代码 |
| bInterfaceProtocol | 1 | 接口协议代码 |
| iInterface | 1 | 接口字符串描述符索引 |
3.5 端点描述符(Endpoint Descriptor)
端点描述符定义数据传输的端点属性,包括地址、方向、传输类型和最大包大小。除端点 0(默认控制端点)外,每个非零端点都需要一个端点描述符。
| 字段 | 大小 | 说明 |
|---|---|---|
| bLength | 1 | 固定为 7 |
| bDescriptorType | 1 | 端点描述符类型码 0x05 |
| bEndpointAddress | 1 | Bit 3-0: 端点号;Bit 7: 方向(0=OUT, 1=IN) |
| bmAttributes | 1 | Bit 1-0: 传输类型(00=控制, 01=等时, 10=批量, 11=中断) |
| wMaxPacketSize | 2 | 最大包大小(高 2 位用于高速模式下的额外事务数) |
| bInterval | 1 | 中断/等时端点的轮询间隔(单位:帧/微帧) |
- 控制传输:仅端点 0,用于枚举和控制命令。
- 批量传输:用于大块数据(如 U 盘),保证数据完整性。
- 中断传输:用于周期性小数据(如鼠标、键盘),保证延迟。
- 等时传输:用于实时数据流(如音频、视频),不保证重传。
3.6 字符串描述符(String Descriptor)
字符串描述符提供人类可读的文本信息,如厂商名称、产品名称和序列号。字符串使用 Unicode(UTF-16LE)编码,通过索引被设备/配置/接口描述符中的 i* 字段引用。字符串描述符 0 是特殊的语言 ID 描述符,用于声明设备支持的语言。
| 字段 | 大小 | 说明 |
|---|---|---|
| bLength | 1 | 描述符总长度(含头部) |
| bDescriptorType | 1 | 字符串描述符类型码 0x03 |
| bString | 可变 | Unicode 字符串(UTF-16LE 编码) |
/* Linux Gadget 驱动中字符串描述符的典型定义 */
static struct usb_string strings_dev[] = {
{ .id = 0, .s = "My USB Gadget" }, /* 产品名称 */
{ .id = 1, .s = "Vendor Inc." }, /* 厂商名称 */
{ .id = 2, .s = "SN1234567890" }, /* 序列号 */
{ } /* 结束标记 */
};
static struct usb_gadget_strings stringtab_dev = {
.language = 0x0409, /* 英语(美国) */
.strings = strings_dev,
};
3.7 描述符在枚举过程中的交互
主机通过标准设备请求获取描述符,典型的枚举流程如下:
- 获取设备描述符:主机发送
GET_DESCRIPTOR(DEVICE),设备返回 18 字节设备描述符。 - 设置地址:主机发送
SET_ADDRESS,设备进入地址状态。 - 再次获取设备描述符:使用新地址重新获取,验证设备。
- 获取配置描述符:主机发送
GET_DESCRIPTOR(CONFIGURATION),设备返回配置描述符及其下所有接口和端点描述符。 - 获取字符串描述符:主机根据索引请求字符串描述符(可选)。
- 选择配置:主机发送
SET_CONFIGURATION,激活选中的配置。
3.8 Linux Gadget 驱动中的描述符注册
在 Linux Gadget 驱动中,描述符通过 usb_gadget_driver 结构体中的 bind 回调函数注册。驱动开发者需要构造描述符数组,并调用 usb_assign_descriptors() 或手动绑定到 Gadget 实例。
/* Linux Gadget 驱动中描述符注册的典型模式 */
static int my_gadget_bind(struct usb_gadget *gadget)
{
struct usb_descriptor_header *fs_descriptors[] = {
(struct usb_descriptor_header *)&fs_device_desc,
(struct usb_descriptor_header *)&fs_config_desc,
(struct usb_descriptor_header *)fs_interface_desc,
(struct usb_descriptor_header *)fs_endpoint_desc,
NULL,
};
struct usb_descriptor_header *hs_descriptors[] = {
(struct usb_descriptor_header *)&hs_device_desc,
(struct usb_descriptor_header *)&hs_config_desc,
(struct usb_descriptor_header *)hs_interface_desc,
(struct usb_descriptor_header *)hs_endpoint_desc,
NULL,
};
/* 注册全速和高速描述符 */
return usb_assign_descriptors(fs_descriptors, hs_descriptors,
NULL, NULL);
}
usb_assign_descriptors() 可以自动根据当前连接速度选择合适的描述符集合。
3.9 描述符常见问题与调试
- wTotalLength 计算错误:导致主机无法正确解析配置描述符,枚举失败。建议使用内核提供的
usb_descriptor_fillbuf()辅助函数。 - 端点地址冲突:同一接口内不能有相同端点号和方向的端点。
- bMaxPacketSize 超限:全速批量端点最大 64 字节,高速批量端点最大 512 字节。
- 字符串索引无效:如果
iManufacturer等字段指向不存在的字符串索引,主机可能跳过字符串读取或导致枚举异常。 - 调试工具:使用
lsusb -v查看设备描述符树,使用内核usbmon或ftrace跟踪枚举过程。