USB设备架构:端点、接口、配置的层次关系
说实话,刚开始学USB的时候,我也被配置、接口、端点这些词绕晕过。感觉就像走进了一栋大楼,有楼层、有房间、有门牌号,但搞不清谁管谁。今天我们就把这层窗户纸捅破。
从最底层说起:端点
端点,英文叫Endpoint,是USB通信的最小单元。你可以把它理解成一个「数据信箱」。主机往这个信箱里丢数据,设备从里面取;或者设备往里面放数据,主机来拿。
每个USB设备最多有32个端点。为什么?硬件上就是靠那4位地址线来区分的,0到15一共16个编号,每个编号又有IN和OUT两个方向。所以总共32个。
- 端点0:所有设备必备的默认端点。双向的,用于控制传输。枚举过程里,主机就是靠它跟设备交换描述符。
- 端点1~15:可选端点。每个方向独立配置。比如批量传输用端点1的OUT,中断传输用端点2的IN。
重要特性:端点的配置是「建好就不能变」的。设备在配置阶段告诉主机端点有哪些属性,之后就不能改了。除非你重新配置设备。
我记得第一次自己写USB固件时,犯过一个低级错误——把端点1的IN和OUT方向搞反了。结果主机一收发包就超时,折腾了两天才发现是端点方向位没设置对。唉,从那以后我读芯片手册时,都会在端点方向寄存器上画个红圈。
端点的类型
USB 3.0定义了四种传输类型,每种对应不同的端点特性。嗯,这里要注意,不是所有端点类型都能随便搭,要根据你的应用场景选。
| 传输类型 | 端点特点 | 典型应用 |
|---|---|---|
| 控制传输 | 双向,保证送达,带宽可控 | 枚举、标准请求、状态获取 |
| 批量传输 | 非实时,但数据准确,带宽大 | U盘、硬盘、打印机 |
| 中断传输 | 低时延,定期查询 | 鼠标、键盘、HID设备 |
| 等时传输 | 固定速率,但不保证数据完整 | 音频、摄像头实时流 |
你想想看,如果你的设备是U盘,批量端点足够了;但如果你要搞一个USB摄像头,等时端点才是对的路子。强行用批量端点传输视频流,帧率和延迟会让你崩溃。
接口——功能的集合
一个接口由一组逻辑相关的端点组成。它代表一个完整的USB功能。比如,一个U盘设备可能只有一个接口,包含一个批量输出端点和一个批量输入端点。
但你有没有见过那种多功能设备?比如带麦克风的摄像头,或者带读卡器的键盘。这类设备会用多个接口来表达多个功能。
举个例子:一个USB耳机,可能包含:
- 接口0:音频控制功能(使用控制端点0)
- 接口1:音频输出流(使用等时输出端点)
- 接口2:音频输入流(使用等时输入端点)
每个接口都有独立的接口描述符(Interface Descriptor),里面说明了接口编号、端点数量、功能类型等信息。主机在枚举时,会读取这些描述符,然后决定加载哪个驱动程序。
设计建议:我个人习惯在设计复合设备时,把每个功能分到独立的接口里。比如摄像头功能用接口0,麦克风功能用接口1。这样主机端可以分别绑定驱动,互不干扰。万一麦克风驱动挂了,摄像头照样能工作。
配置——设备的「工作模式」
配置是最高层级的概念。一个USB设备可以有多个配置,但同一时刻只能有一个配置生效。打个比方:同一个设备,可以当U盘用,也可以切换到读卡器模式,这就是不同的配置。
为什么需要多配置?举个实际例子——我曾经做过一款工业数据采集器。它既需要高速批量传输大数据(配置1),又需要低功耗中断传输上报关键状态(配置2)。设备在初始化时,主机根据用户选择加载对应的配置。
每个配置都有自己的配置描述符(Configuration Descriptor),里面描述了该配置下所有接口和端点的信息。枚举过程中,主机通过Get_Descriptor请求拿到配置描述符后,才能决定如何与设备通信。
注意:切换配置会导致设备逻辑状态复位。也就是说,之前打开的端点、分配好的缓存全都没了。你得重新初始化。我曾经在调试配置切换时,忘记重新设置端点0以外的其它端点,结果切换后所有批量传输都失败。排查了整整一个下午才发现问题出在哪里。
三层架构:配置→接口→端点
现在把这三层串起来讲。一个USB设备的结构,就是一棵这样的树:
USB Device
├── 配置1(Configuration 1)
│ ├── 接口0(Interface 0)
│ │ ├── 端点1 IN (批量传输)
│ │ └── 端点2 OUT(批量传输)
│ └── 接口1(Interface 1)
│ └── 端点3 IN (中断传输)
├── 配置2(Configuration 2)—— 可选
│ └── 接口0(Interface 0)
│ ├── 端点1 IN (等时传输)
│ └── 端点2 OUT(等时传输)
主机在枚举时,会按照这个层次逐级获取描述符:设备描述符→配置描述符→接口描述符→端点描述符。拿到这些信息后,主机就知道该设备是什么、能干嘛、怎么通信了。
枚举要点:配置描述符里还包含了电源管理信息(比如mA电流需求)和接口关联信息。USB 3.0枚举时,主机要求设备在配置描述符中明确报告总线供电还是自供电,以及最大电流消耗。如果写错了,轻则枚举失败,重则烧坏主机USB口。这个我会在后面枚举实战中详细讲。
从上到下的视角
咱们换个角度,从主机的视角看这个三层结构。
- 主机先问设备:「你有几个配置?」(从设备描述符获得bNumConfigurations)
- 然后逐个配置问:「这个配置里有多少接口?每个接口需要什么驱动?」(读配置描述符和接口描述符)
- 最后到端点层面:「每个端点怎么用?方向是IN还是OUT?传输类型是什么?包多大?」(读端点描述符)
你看,这个过程就像剥洋葱,一层一层搞清楚设备的内部结构。
我之前调一个复合设备的枚举时,发现主机只能识别第一个接口,第二个接口怎么都认不出来。百思不得其解,最后用USB分析仪抓包一看,才发现配置描述符里第二个接口关联的端点描述符地址写错了。端点号重复了,主机自然就拒绝加载。
SVG结构图:USB设备三层架构
这张图把USB设备的三层结构一目了然地展示了出来。一个设备可以有多个配置,每个配置下有多个接口,每个接口下又挂载着多个端点。枚举时,主机就是按照这个树形结构,从上到下拿描述符的。
嗯,这里最关键的一点:同一个设备、同一个端点,在不同的配置下可能有完全不同的含义。所以枚举其实是一个「设备自描述」的过程。主机从不猜测设备会干什么,而是等设备自己把配置描述符交出来,然后按规矩使用。
个人建议:在读芯片手册时,先把你的设备的描述符层次画出来。就像我们上面的图一样,用笔纸画一遍。这能帮你理清思路,也能在调试时快速定位问题——比如枚举到第几个描述符时失败了,对应的是哪个接口、哪个端点。
枚举实战中的配置描述符例子
我放一个实际项目中用到的配置描述符结构代码,你们感受一下:
// 配置描述符(描述了一个接口,两个端点)
0x09, // bLength: 描述符长度 = 9
0x02, // bDescriptorType: 配置描述符类型 = 2
0x27, 0x00, // wTotalLength: 配置描述符总长 = 39字节
0x01, // bNumInterfaces: 这个配置下有1个接口
0x01, // bConfigurationValue: 配置编号为1
0x00, // iConfiguration: 字符串描述符索引 = 0(无)
0xC0, // bmAttributes: 自供电, 远程唤醒支持
0x32, // bMaxPower: 最大电流 = 100mA
// 接口描述符
0x09, // bLength: 9
0x04, // bDescriptorType: 接口描述符 = 4
0x00, // bInterfaceNumber: 接口编号 = 0
0x00, // bAlternateSetting: 备用设置 = 0
0x02, // bNumEndpoints: 有2个端点(不含端点0)
0x08, // bInterfaceClass: 大容量存储类
0x06, // bInterfaceSubClass: SCSI命令集
0x50, // bInterfaceProtocol: Bulk-Only传输
0x00, // iInterface: 无字符串
// 端点1 IN(批量端点)
0x07, // bLength: 7
0x05, // bDescriptorType: 端点描述符 = 5
0x81, // bEndpointAddress: 端点1, IN方向
0x02, // bmAttributes: 批量传输
0x00, 0x02, // wMaxPacketSize: 512字节
0x00, // bInterval: 批量端点的轮询间隔 = 0
// 端点2 OUT(批量端点)
0x07,
0x05,
0x02, // 端点2, OUT方向
0x02,
0x00, 0x02,
0x00
这段代码看起来枯燥?但我要告诉你,我当时调试U盘枚举时,就是对着这个结构一个一个字节地查。宿主Windows说设备无法识别,我抓包后发现配置描述符里的wTotalLength写错了,导致主机读不全。修改一个字节,问题解决。
所以在USB开发中,别小看这些基础概念。端点、接口、配置的清晰理解,能帮你省下大量排错时间。