3. USB描述符设计与emUSB配置

USB设备与主机之间的“自我介绍”完全依赖描述符。主机上电枚举设备时,会依次读取设备描述符、配置描述符、接口描述符、端点描述符和字符串描述符,据此判断设备类型、供电需求、可用接口与传输能力,并加载匹配的驱动。emUSB-Device 把这些描述符抽象成 C 结构体与配置 API,开发者只需按 USB 规范填写字段,再由协议栈在枚举阶段按正确顺序返回给主机。理解描述符的层级与字段含义,是 emUSB 开发中最基础也最容易出错的一环。

描述符的层级关系

USB 描述符是树状结构:一个设备描述符下挂一个或多个配置描述符,每个配置描述符下挂一个或多个接口描述符,每个接口描述符下挂零个或多个端点描述符,字符串描述符则被其它描述符通过索引引用。主机按“设备→配置→接口→端点”的顺序逐层解析。下图展示了这一层级以及各描述符之间的引用关系。

USB描述符层级与引用关系 设备描述符 VID / PID / bNumConfigurations 配置描述符 bNumInterfaces / bmAttributes / bMaxPower 接口描述符 0 bInterfaceClass / bNumEndpoints 接口描述符 1 bInterfaceClass / bNumEndpoints 端点 0x81 IN / 中断 端点 0x01 OUT / 批量 端点 0x82 IN / 批量 端点 0x02 OUT / 批量 字符串描述符(iManufacturer / iProduct / iSerial)

设备描述符与配置描述符

设备描述符是主机读取的第一个描述符,长度固定 18 字节。关键字段包括 bcdUSB(协议版本,如 0x0200 表示 USB 2.0)、bDeviceClass(设备类,0 表示由接口描述符定义)、idVendor 与 idProduct(厂商与产品 ID)、bNumConfigurations(配置数量)。在 emUSB 中,这些字段通过 USB_DEVICE_DESCRIPTOR 结构体填写,例如:

static const USB_DEVICE_DESCRIPTOR deviceDesc = {
  .bLength         = sizeof(USB_DEVICE_DESCRIPTOR),
  .bDescriptorType = USB_DEVICE_DESCRIPTOR_TYPE,
  .bcdUSB          = 0x0200,
  .bDeviceClass    = 0x00,
  .bMaxPacketSize0 = 64,
  .idVendor        = 0x1234,
  .idProduct       = 0x5678,
  .bcdDevice       = 0x0100,
  .iManufacturer   = 1,
  .iProduct        = 2,
  .iSerialNumber   = 3,
  .bNumConfigurations = 1
};

配置描述符描述设备的供电方式与接口数量。bmAttributes 的 bit7 保留为 1,bit6 表示自供电,bit5 表示支持远程唤醒;bMaxPower 以 2mA 为单位,例如 0x32 表示 100mA。emUSB 通常用 USB_CONFIGURATION_DESCRIPTOR 配合接口与端点描述符组成一个连续的配置块,通过 USB_AddConfiguration() 注册。

接口描述符与端点描述符

接口描述符决定主机加载哪类驱动。bInterfaceClass 取 0x03 表示 HID,0x08 表示 Mass Storage,0xFF 表示厂商自定义类。bNumEndpoints 必须与该接口下实际端点数量一致,否则枚举会失败。端点描述符中,bEndpointAddress 的 bit7 表示方向(1 为 IN,0 为 OUT),低 4 位为端点号;bmAttributes 指定传输类型(0 控制、1 同步、2 批量、3 中断);wMaxPacketSize 为最大包长,全速批量端点通常为 64 字节,高速为 512 字节;bInterval 仅对中断与同步端点有效,表示轮询间隔。

下面是一个厂商自定义接口加一对批量端点的配置示例,接口 0 使用 IN 端点 0x81 与 OUT 端点 0x01:

static const USB_ENDPOINT_DESCRIPTOR epIn = {
  .bLength          = sizeof(USB_ENDPOINT_DESCRIPTOR),
  .bDescriptorType  = USB_ENDPOINT_DESCRIPTOR_TYPE,
  .bEndpointAddress = 0x81,   /* IN, EP1 */
  .bmAttributes     = 0x02,   /* 批量传输 */
  .wMaxPacketSize   = 64,
  .bInterval        = 0
};

static const USB_INTERFACE_DESCRIPTOR iface = {
  .bLength            = sizeof(USB_INTERFACE_DESCRIPTOR),
  .bDescriptorType    = USB_INTERFACE_DESCRIPTOR_TYPE,
  .bInterfaceNumber   = 0,
  .bAlternateSetting  = 0,
  .bNumEndpoints      = 2,
  .bInterfaceClass    = 0xFF,  /* 厂商自定义 */
  .bInterfaceSubClass = 0x00,
  .bInterfaceProtocol = 0x00,
  .iInterface         = 0
};

字符串描述符与编码

字符串描述符使用 UTF-16LE 编码,首字节为长度,第二字节为类型 0x03,其后每个字符占两字节。emUSB 提供 USB_AddString() 或静态字符串表来注册,索引 0 固定为语言 ID 数组(如 0x0409 表示美式英语)。设备描述符中的 iManufacturer、iProduct、iSerialNumber 即指向这些字符串索引。若某字段填 0,表示无对应字符串,主机不会请求。

使用emUSB配置工具与API

emUSB-Device 提供两种定制途径。其一是图形化配置工具(如 emUSB-Device Configurator),在界面中填写 VID、PID、接口类、端点方向与包长,工具自动生成描述符 C 文件与初始化代码,适合快速搭建原型。其二是直接调用 API,在 USB_Init() 之前通过 USB_AddConfiguration()、USB_AddInterface()、USB_AddEndpoint() 动态构建描述符树,适合需要运行时切换配置或多配置设备的场景。

无论用哪种方式,都要保证 bNumEndpoints、bNumInterfaces、bNumConfigurations 与实际注册数量严格一致;端点地址不能重复,IN 与 OUT 可共用端点号但方向位不同。

常见错误与验证方法

描述符错误最典型的症状是主机反复枚举失败、设备管理器显示“未知 USB 设备”或驱动无法加载。常见原因包括:bMaxPacketSize0 与端点 0 实际能力不符;配置描述符总长度 wTotalLength 计算错误;字符串索引指向不存在的字符串;端点包长超过该传输类型上限。验证时可用 USB 协议分析仪抓取枚举过程,或在 emUSB 中开启调试输出,观察主机请求的描述符类型与返回长度是否匹配。若主机在 GET_DESCRIPTOR 配置阶段停止,通常说明配置块长度或接口数量有误。

掌握描述符的层级与字段后,emUSB 的配置工作就变成“填结构体 + 注册 + 校验”三步。下一阶段即可在此基础上绑定具体的类驱动或自定义端点处理函数,让设备真正开始收发数据。