USB系统架构:主机、设备、集线器、端点与管道

USB系统架构,说白了就是一套「谁说了算、谁干活、怎么传数据」的规则。我刚开始接触USB时,总觉得这些概念很抽象——主机、设备、端点,听着像网络术语。后来调了两年枚举问题,才真正理解它们之间的关系。

嗯,咱们从最核心的五个角色说起。

1. 主机(Host)—— 整个系统的老板

主机是USB通信的唯一发起者。所有事务都由主机发起,设备只能被动响应。我见过不少新手问:「设备能不能主动给主机发数据?」答案是不能。设备想发数据,得等主机来「轮询」它。

主机的核心职责:

  • 检测设备插拔(通过集线器状态变化)
  • 管理总线带宽(分时调度)
  • 发起所有传输事务(IN/OUT/SETUP)
  • 分配总线地址(枚举时给设备分配唯一地址)

主机内部通常包含三部分:主机控制器(Host Controller)、主机软件栈(Host Software Stack)和系统总线接口(如PCIe)。我在调试一个Linux主机枚举失败时,发现是主机控制器寄存器配置错了——嗯,那是个很隐蔽的时序问题。

2. 设备(Device)—— 听话的打工仔

设备就是U盘、鼠标、键盘这些外设。它们不能主动说话,但可以「暗示」主机——比如通过远程唤醒信号。设备内部有设备描述符,像身份证一样告诉主机「我是谁、我能干什么」。

设备分为两类:

  • 全功能设备:有自己的CPU和固件,比如USB转串口适配器
  • 复合设备:一个物理设备包含多个功能,比如带麦克风的摄像头(一个设备,两个功能)

我的一点经验:调试设备枚举时,先看设备描述符的前两个字节——bLength和bDescriptorType。如果这两个字节不对,主机根本不会继续读后面的描述符。我曾经被一个bLength写错成0x11(应该是0x12)的bug卡了整整两天。

3. 集线器(Hub)—— 总线的交通警察

集线器负责扩展USB端口,同时检测端口状态变化。每个USB系统中至少有一个根集线器(Root Hub),集成在主机控制器内部。

集线器的工作流程:

  1. 检测端口电平变化(设备插入/拔出)
  2. 通知主机(通过中断传输)
  3. 主机查询端口状态
  4. 主机控制集线器复位端口、使能端口

我记得有一次调试一个USB 3.0集线器,设备插上去就是没反应。用逻辑分析仪抓信号,发现集线器根本没发出「连接状态变化」的中断。最后查出来是集线器的中断端点配置错了——端点描述符里的bInterval写成了0,主机直接忽略了它。

4. 端点(Endpoint)—— 数据的收发窗口

端点是设备内部的数据缓冲区,每个端点有唯一的端点号和方向。你可以把端点想象成设备的「邮箱」——主机往邮箱里放数据(OUT端点),或者从邮箱里取数据(IN端点)。

端点有四种类型:

端点类型 传输方式 典型用途 带宽保证
控制端点 控制传输 枚举、命令控制 有(10%保留带宽)
批量端点 批量传输 U盘、打印机 无
中断端点 中断传输 鼠标、键盘 有(轮询间隔保证)
同步端点 同步传输 音频、视频 有(固定带宽)

注意:端点0是每个设备必须有的控制端点,双向的。其他端点号从1开始,每个方向最多15个。我曾经见过一个设备固件把端点0的wMaxPacketSize写成了0,结果主机枚举到「获取设备描述符」那一步直接超时——因为主机不知道一次该传多少字节。

5. 管道(Pipe)—— 主机与端点的逻辑通道

管道是主机软件与设备端点之间的抽象连接。主机通过管道向端点发送数据,或者从端点接收数据。每个管道对应一个端点,方向固定。

管道分为两种:

  • 默认管道(Default Pipe):对应端点0,用于枚举和控制命令
  • 消息管道(Message Pipe):用于控制传输,数据有结构(如SETUP包+数据+状态)
  • 流管道(Stream Pipe):用于批量、中断、同步传输,数据无结构(纯字节流)

你想想看,管道其实就是主机驱动里的一个数据结构。在Linux内核里,每个管道对应一个struct usb_host_endpoint,里面记录了端点类型、最大包长、轮询间隔等信息。

6. 它们之间的关系 —— 一张图说清楚

下面这张图展示了主机、集线器、设备、端点和管道之间的层级关系:

USB系统架构关系图 USB系统架构:主机 → 集线器 → 设备 → 端点 → 管道 主机(Host) 唯一发起者 | 分配地址 | 调度总线 | 管理带宽 集线器(Hub) 端口扩展 | 状态检测 | 复位/使能 设备(Device) 描述符 | 端点集合 | 被动响应 端点(Endpoint) 端点0(控制) | 端点1 IN | 端点1 OUT | 端点2 IN | ... 管道(Pipe) 每个管道对应一个端点,方向固定。主机通过管道与端点通信。

7. 实际调试中的映射关系

在固件开发中,这些概念会直接映射到代码里。比如你写一个USB设备固件,需要定义:

  • 设备描述符:告诉主机你是谁(VID/PID、设备类、最大包长)
  • 配置描述符:告诉主机你有哪些功能(接口数量、供电方式)
  • 接口描述符:每个接口对应一个功能(如HID类、Mass Storage类)
  • 端点描述符:每个端点的类型、方向、最大包长

一个典型的枚举流程(主机视角):

  1. 集线器检测到设备插入 → 通知主机
  2. 主机复位设备(通过集线器发送复位信号)
  3. 主机读取设备描述符(前8字节,获取端点0最大包长)
  4. 主机再次复位设备
  5. 主机分配地址(SET_ADDRESS请求)
  6. 主机读取完整设备描述符(18字节)
  7. 主机读取配置描述符(获取接口和端点信息)
  8. 主机选择配置(SET_CONFIGURATION请求)
  9. 设备进入配置状态,可以正常通信

我调试枚举问题时,最常用的工具就是USB分析仪。抓一次枚举过程,看主机发了什么请求、设备回了什么数据。如果设备在「获取配置描述符」那一步返回了错误数据,主机就会直接放弃这个设备——嗯,这种情况我遇到过不下十次。

8. 避坑指南

我曾经踩过的坑:

  • 端点0的wMaxPacketSize写错:USB 2.0全速设备必须是8、16、32、64之一,写错主机直接不认
  • 配置描述符总长度算错:wTotalLength必须包含所有接口和端点描述符的长度,少一个字节都不行
  • 接口描述符的bInterfaceNumber重复:同一个配置下接口号必须唯一,否则主机驱动加载会失败
  • 端点描述符的bEndpointAddress方向位搞反:bit7=1是IN,bit7=0是OUT,我见过有人把IN端点写成了0x01(应该是0x81)

说白了,USB系统架构就是一套严格的层级体系。主机在最顶层,设备在最底层,中间靠集线器连接。端点负责收发数据,管道负责逻辑通道。理解了这个架构,你调试枚举问题就有方向了——先看主机有没有检测到设备,再看设备描述符对不对,最后看端点配置是否合理。