☰
USB协议从零到精通:通信模型、端点机制与枚举流程深度解析
2026/10/13 21:41:11 网站建设 项目流程

USB 这东西,做嵌入式的几乎天天见,但说句实话,真正把它从物理层到枚举流程彻底吃透的人并不多。大部分时候我们调 I2C、SPI 习惯了,总觉得 USB 复杂、协议栈厚,遇到问题只能靠示波器瞎猜,或者网上搜一段初始化代码抄过来能用就行。可一旦碰到设备枚举不稳定、兼容性差、或者需要自己写底层驱动的时候,短板就暴露了。

这个系列我打算把 USB 协议从头到尾捋一遍,不按标准文档的死板顺序讲,而是按照一个开发者从零开始接触 USB 时会遇到的真实问题来组织。第一篇先搭框架,把通信模型、核心概念和枚举流程这些东西讲透。这一篇你要是吃透了,后面讲描述符、类协议、驱动开发和调试技巧时,就能直接往上挂。

1. 通信模型与物理层基础

1.1 主从架构与拓扑结构

USB 是典型的主从架构,这一点和 I2C 有点像,但又不完全一样。总线上只有一个主机(Host),所有通信都由主机发起,设备(Device)永远不能主动开口说话。这个设计从根本上决定了 USB 协议的诸多特性,包括它的轮询机制、中断传输的伪实时性,以及为什么 USB 设备之间没法直接互通。

拓扑结构上,USB 是分层星型结构。主机下面挂根集线器(Hub),Hub 再往下分叉,层层扩展,最多支持 127 个设备地址(实际上 0 是默认地址,可用的是 1 到 127)。这个层级深度被限制在 7 层以内,也就是说设备到主机之间最多经过 5 个 Hub。设计这个限制主要是为了保证时序可控——每经过一层 Hub 都会有延迟,如果层级太深,低速设备的响应时间就无法被准确预测了。

我在实际调试中踩过这个坑。曾经有一个项目,设备端是定制的复合设备,主机端挂在 USB 2.0 Hub 后面,而那个 Hub 又是通过另一个 Hub 接在主板上的。设备在直连主板时一切正常,一挂到这两级 Hub 后面就偶发枚举失败,重试几次才能成功。后来查根因,就是层级变深后复位和枚举的超时窗口被压缩了,设备端响应稍慢就超出主机容忍的时限。

1.2 物理层的差分信号与速度识别

USB 的物理层是一对差分线:D+ 和 D-。数据通过这两根线的电平差来传输,这种差分结构天然具备抗共模干扰的能力。但差分只是基础,真正有意思的是速度识别机制。

设备插入后,主机是怎么知道它是什么速度的?答案在于上拉电阻的位置。全速和高速设备在 D+ 线上接了一个 1.5kΩ 上拉电阻到 3.3V,低速设备则把同样的电阻接在 D- 线上。主机端的 D+ 和 D- 各有一个 15kΩ 下拉电阻。设备一插上去,主机检测到 D+ 被拉高,就知道这是一个全速或高速设备;D- 被拉高,则是低速设备。

这个机制第一次看很巧妙,但实际调试时也有个容易忽略的细节:上拉电阻的电源域。有些设计图省事,把上拉电阻直接接到 3.3V 主供电上,这在芯片单独供电时问题不大,但如果芯片支持动态切换速度,或者需要进入挂起模式,这个上拉就可能造成漏电或速度识别错误。正确的做法是接到芯片内部专门的上拉电源引脚,并且切换速度时要有严格的状态机控制。

还有高速模式下的终端电阻匹配。全速信号用的是 1.5kΩ 上拉配合主机端 15kΩ 下拉,构成分压,信号摆幅较高。当从全速切换到高速时,设备端要断开这个上拉,并在 D+ 和 D- 之间接入 45Ω 终端电阻。这个切换动作是在主机发出 Chirp K 信号后由设备检测并执行的,时序要求非常严格。

1.3 版本演进与速率对照

USB 各版本的速率差异是基础知识,但很多人对实际有效带宽的理解有偏差。这里列一个我常用的对照表:

USB 版本标称速率实际信号速率常见用途
USB 1.0/1.11.5 Mbps (Low Speed)1.5 Mbps鼠标、键盘等低交互设备
USB 1.112 Mbps (Full Speed)12 Mbps音频、基础 HID 设备
USB 2.0480 Mbps (High Speed)480 Mbps存储、视频、网卡
USB 3.x Gen 15 Gbps (SuperSpeed)5 Gbps外置存储、高速采集
USB 3.x Gen 210 Gbps10 Gbps视频采集、NVMe 硬盘盒
USB420/40 Gbps基于 Thunderbolt扩展坞、高性能外设

关键点是“标称速率”不等于“有效吞吐”。USB 2.0 的 480Mbps 是信号线上的比特率,但每个事务都要包含同步字段、包标识符、地址和端点号、CRC、握手包,还有帧间隔和 Hub 转发延迟。实际块传输吞吐能跑到 35~40MB/s 已经很不错了。如果你的项目拿全速 USB 传视频流,12Mbps 标称速率,刨掉协议开销和带宽分配,实际可用的大概只有 8~9Mbps,想传 720p 视频基本不可能。

2. 核心概念:端点、管道与传输类型

2.1 端点与管道的映射关系

端点是 USB 设备内部的数据收发单元,本质上是设备端的一块缓冲区,配有一组状态和控制逻辑。每个端点由设备描述符中的端点描述符来定义,用端点地址唯一标识,这个地址由端点号和传输方向两部分构成。

举个例子,端点地址 0x81,十六进制拆开看:低 4 位是端点号 1,最高位为 1 表示 IN 方向(设备到主机)。硬件设计上,每个端点有两个寄存器组,分别负责 IN 和 OUT,但一个端点地址只对应一个方向。

管道是主机视角的概念,主机通过管道与设备的端点建立逻辑连接。这种区分在实际调试中很有意义。我曾经帮同事查一个问题:设备固件里定义了端点 1 IN,用于上报数据,代码逻辑对着端点 1 一直发包,但主机那边就是收不到数据。后来发现同事在初始化时把端点基地址算错了,实际硬件上启用的是端点 2。固件里的“端点 1”只是逻辑编号,硬件上要配置对应的物理寄存器组,这个映射关系搞错就会导致数据丢失。

端点的性质由传输类型和最大包大小来决定。控制端点通常固定为 0 号端点,双向,最大包大小低速 8 字节、全速 8/16/32/64 字节、高速 64 字节。非零端点则是单向的,IN 或 OUT 二选一。这个双向/单向的区别很关键:控制传输虽然逻辑上是双向的,但实际的 Setup 阶段、数据阶段和状态阶段用的是同一个端点 0 在不同方向上的事务组合。

2.2 四种传输类型的应用场景

USB 定义了四种传输类型,每种都有明确的应用场景和带宽保证策略,选错类型是新手常见问题。

控制传输(Control):只用于端点 0,负责设备配置、查询状态、命令下发等。传输过程分为 Setup 阶段、数据阶段(可有可无)和状态阶段。控制传输是最可靠但效率最低的传输方式,因为每个事务都要经过完整的握手确认。设备枚举过程中的所有描述符读取都是通过控制传输完成的。

中断传输(Interrupt):名字里带“中断”,但千万别被误导。USB 的中断传输实际上主机会按照轮询间隔周期性发起 IN 事务,查询设备是否有数据要发。设备端并不会“主动打断”主机。轮询间隔以帧为单位配置,全速帧间隔 1ms,高速则细分到 125μs 的微帧。这种传输特别适合人机交互设备,比如鼠标、键盘、游戏手柄,因为这些设备数据量小,但对延迟有要求。

批量传输(Bulk):主打可靠性,每个事务都带 CRC 校验,出错就重传,直到成功。它不保证带宽和延迟,只保证数据最终一定到达。最适合 U 盘、串口转接芯片这类大流量数据通信。但批量传输有个特点:只有在总线上没有其他更高优先级传输时才占用带宽,所以长时间大批量传输时,中断传输和控制传输会被延迟。

等时传输(Isochronous):特点是没有重传机制,出错就丢包,但保证固定的带宽和时序间隔。适用于音视频采集、实时传感器数据流,这类场景丢一帧还能接受,延迟卡顿和时序抖动才致命。等时传输的数据阶段不带握手包,这是它与其他传输类型最显眼的区别。

2.3 包结构与事务处理精读

USB 的最小传输单位是包(Packet),而主机和设备之间的交互是通过事务(Transaction)来完成的。一个典型的事务由三到四个包组成:令牌包、数据包、握手包,部分传输类型还有帧号包等前置阶段。

令牌包(Token Packet)由主机发出,包含 PID(包标识符)、地址字段、端点号字段和 CRC5 校验。它定义了这个事务要做什么、和谁做。数据包(Data Packet)携带实际数据,包含 PID、数据区和 CRC16 校验。握手包(Handshake Packet)由数据接收方发出,返回 ACK(确认)、NAK(忙)或 STALL(错误)。

这里有个容易困惑的地方:只有主机能发起令牌包,但主机不一定总是主动发送数据。比如主机要读取设备数据,它发一个 IN 令牌包,设备收到后如果数据准备好了,就返回数据包,主机再回 ACK。如果设备没有数据,就会返回 NAK 握手包。NAK 不是错误,只是表示“我暂时没准备好,你过会儿再问”。高速批量传输里还有一个 PING 机制,主机先发 PING 令牌包探测设备状态,设备回 ACK 或 NAK,这样避免总在数据阶段浪费带宽。

事务结束后,总线上会有短暂的空闲,称为帧内间隔(Inter-Packet Gap)。这个间隔的时长与速度有关,全速需要至少两个位时间的空闲。对时序敏感的调试场景,这个间隔也能作为观察事务边界的一个参考点。

3. 枚举过程与状态机

3.1 从插线到设备就绪的完整链路

枚举(Enumeration)是 USB 协议中设备从连接到可用必须经历的全部过程。这个过程是理解 USB 最重要、也最容易出错的部分。我这里按实际信号流程展开讲。

设备插入主机端口,D+ 或 D- 被上拉电阻拉高,主机检测到这个状态变化,认为有新设备接入。主机向 Hub 发出命令,Hub 对端口执行复位信号,这个复位信号持续至少 10ms(USB 2.0 规范要求 Hub 驱动复位状态至少 10ms,设备应该在这个时间内完成复位并准备好响应)。

复位结束后,设备地址还是 0(默认地址),此时主机发送第一个控制请求:获取设备描述符的前 8 个字节。这一步非常关键,主机需要先从这 8 个字节里得知设备端点 0 的最大包长度。为什么只先读 8 字节?因为在不知道端点 0 最大包大小之前,主机不知道后续请求应该以多大的包来发送和接收,前置读取可以避免因包长度不匹配导致的通信失败。

主机拿到端点 0 的最大包长度后,会发送一个设置地址请求,分配一个新的唯一地址给设备。设备收到后必须在请求内规定的时限内完成地址切换,之后所有通信都使用新地址。主机接着重新读取完整的设备描述符(18 字节),然后依次读取配置描述符、接口描述符、端点描述符和字符串描述符。

配置描述符的读取有一个特殊细节:主机第一次请求配置描述符时,只请求 9 字节,得到的就是配置描述符本身,其中有一个字段叫“配置描述符总长度”,包含了其下所有接口、端点描述符的总和。主机根据这个长度,再次发起请求,一次性取回完整配置信息。很多设备枚举失败就败在这里:配置描述符里的总长度字段算错了,主机请求的数据量和设备实际回复的不一致,协议栈直接报错。

最后,主机发送设置配置请求(Set Configuration),设备收到后切换到配置完成状态,按配置描述符里的设定把对应端点激活,至此枚举完成,设备进入正常工作模式。

3.2 地址分配与配置选择的决策逻辑

枚举过程中主机会给设备分配一个地址,这个地址不是随便选的,而是主机端 USB 驱动管理的资源。在同一个总线拓扑下,新插入的设备会被分配当前可用的最小地址。设备端只需要做一件事:正确响应设置地址请求,并在应答后立刻启用新地址。

这里有个隐藏的坑:设备固件在收到设置地址请求后,不能等到状态阶段完成才切换地址。协议规定的顺序是,设备接收 Setup 包和数据包后,就已经拿到了新地址,状态阶段用的是新地址来发握手包。如果固件实现时等到状态阶段结束才切换地址,主机端的行为就会不可预测。

配置选择上,一个设备可以定义多个配置,但同一时刻只能激活一个。最常见的例子是同一个 USB 设备可以同时支持高功耗模式和低功耗模式,或者在不同主机能力下暴露不同的接口组合。主机通过读取配置描述符集合来决定选择哪个配置,而最终用哪个配置是由系统策略决定的。Windows 上倾向于选择第一个配置,Linux 也类似。如果你的设备有多配置需求,把默认配置放在第一个,并确保第一个配置下的所有设置都是稳定的。

3.3 枚举失败的常见触发点

枚举过程涉及的时序和状态切换非常敏感,我总结几个高频故障场景:

复位时序异常占枚举失败的很大比例。设备端如果对复位信号处理不当,比如复位过程中访问外部存储器导致响应超时,主机就会判定设备无响应,直接放弃枚举。

描述符返回错误是第二大常见原因。包括:长度字段写错、描述符类型标识写错、字符串描述符的索引引用了不存在的字符串、端点描述符中配置的端点号与接口描述符的端点数量不匹配,等等。这类错误的表现很有迷惑性,因为有些主机会尝试重试,有些则直接报“无法识别的 USB 设备”,两个操作系统还可能出现截然不同的表现。

还有一个新手容易踩的坑:设备在复位之后主动发数据。USB 协议里这个阶段一切通信由主机主导,设备唯一的合法行为是等待和响应。任何主动发出的包都会导致总线状态混乱,轻则影响本设备枚举,重则可能拖垮整个总线。

调试枚举问题,我一直建议先用协议分析仪看总线上的实际包序列,不要急着猜。确认主机发了什么请求、设备回了什么数据、握手包是 ACK 还是 STALL,这个信息就能排除掉绝大多数问题。

4. 描述符体系与应用要点

4.1 标准描述符的字节级完整拆解

描述符就是 USB 设备的“身份证”,主机通过读取一系列描述符来了解设备的能力和结构。所有 USB 设备都必须支持标准描述符,这些描述符的格式和含义在规范中有严格定义,字节级别的偏差都会导致枚举失败。

以最基础的设备描述符为例,共 18 字节:

// USB 设备描述符(18字节) typedef struct { uint8_t bLength; // 描述符长度 = 18 uint8_t bDescriptorType; // 描述符类型 = 0x01 (Device) 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; // 配置描述符数量 } usb_device_desc_t;

每个字段的选择背后都有讲究。bcdUSB 表示设备支持的 USB 规范版本,这个值会直接影响主机选择的速度模式。bDeviceClass 如果设为 0x00,表示设备类定义在接口描述符中,即复合设备;如果设为 0xFF,表示厂商自定义设备。0x00 和 0xFF 是两种完全不同的设计思路,后面讲类协议时会展开。

配置描述符的结构往往让新手头疼,因为它不是单一结构,而是一个包含接口描述符、端点描述符、可能还有类特殊描述符的集合。配置描述符本身还是 9 字节,但紧随其后是一组或几组接口描述符和端点描述符。这个嵌套关系在代码里容易写乱,我建议用数组来维护整个配置描述符集合,而不是分散定义多个结构体。

4.2 接口与端点的组合设计

接口描述符描述的是一个功能单元。比方说,一个 USB 声卡设备可能有音频控制接口和音频数据接口,两个接口各自拥有独立端点。对复合设备来说,每个接口可以有独立的类代码和协议,这使得一个物理设备可以同时呈现为键盘和存储设备。

接口描述符中有几个字段值得细品:bInterfaceNumber 是接口编号,bAlternateSetting 是备用设置编号。一个接口的默认设置是 0,如果配置了其他备用设置,主机可以通过 Set Interface 请求切换。最典型的就是 UVC 摄像头,默认设置可能不传输数据,备用设置 1 才启用等时传输端点。这种设计的目的是让主机在不需要视频流时节省带宽。

端点描述符里的 bmAttributes 决定了端点的传输类型,bit1-0:0 控制、1 等时、2 批量、3 中断。与传输类型相对应的还有 bInterval,中断传输的单位是帧或微帧,批量传输在高速模式下可能用于管理延迟。

4.3 类描述符与 HID 描述符的扩展

标准描述符之上,各类别还有自己的特殊描述符。最典型的是 HID(人机交互设备)类。HID 设备在接口描述符之后要带 HID 描述符,里面声明了所使用的 HID 规范版本、国家代码和报告描述符的数量。HID 报告描述符本身又是一套复杂的自制语法,用来描述设备发送的数据格式,比如鼠标的 X/Y 轴偏移量是多少位、按键用几个 bit。

类描述符的加载顺序是一个容易出错的地方:主机在读取配置描述符集合时,会一并读取 HID 描述符,但 HID 报告描述符通常是单独的类请求(Get Report Descriptor)来获取。如果固件把报告描述符错误地嵌在配置描述符集合里,或者类请求的索引写错,就会导致主机拿不到报告数据,设备表现为枚举成功但输入无效。

5. 调试工具与问题排查心得

5.1 逻辑分析仪和协议分析仪的选择

调试 USB 的底层问题,工具的选择很影响效率。普通示波器可以看到 D+ 和 D- 的模拟波形,但对于解码数据包、定位协议逻辑问题帮助有限。逻辑分析仪采样率高、通道多,适合抓取低速和全速信号,配上解码插件就能直接看到 PID 和地址字段。

USB 2.0 高速模式下信号频率达到 480Mbps,普通的逻辑分析仪很难直接采样。这种情况下就得用带 USB 解码功能的协议分析仪,或者主机端的软件抓包工具。Linux 下可以用 usbmon 或者 WireShark 配合抓取 USB 流量,Windows 下可以用商业分析软件或者是内核调试器抓总线事件。

我个人的习惯是分层排查:先看主机端软件是否识别设备、返回什么错误码,再看描述符对不对,最后才用示波器测物理层信号。大部分问题在软件层就能定位,没必要一上来就上硬件工具。

5.2 常见问题速查与排查策略

现象可能原因排查思路
主机完全检测不到设备D+/D- 上拉缺失、供电异常先量 VBUS 和 GND,再查上拉电阻和信号线
检测到设备但枚举失败描述符错误、地址响应超时用协议分析仪查看主机发送的请求序列
设备反复断开重连复位时序异常、电源跌落捕捉插入瞬间的 VBUS 波形,观察压降
枚举成功但数据不通端点配置错误、中断间隔异常检查接口描述符和端点描述符的匹配关系
在某些电脑上正常、某些不行兼容性问题、主控差异对比不同主机的枚举请求序列差异

有一条经验分享:电源问题比协议问题更常见。USB 端口供电标准是 5V 500mA(USB 2.0)或 900mA(USB 3.0),但实际劣质 Hub 的输出能力可能远低于标称。设备端一定要设计足够的去耦电容,并且在固件里对供电跌落保持宽容,不要一检测到电压波动就执行复位操作。

5.3 从一次兼容性事故中总结的教训

之前做过一个数据采集设备,设备端用全速模式,主机连接电路板上也有 USB 转串口芯片。首发批之后收到不少用户报障,说在部分老电脑上设备无法识别,但在新款电脑上完全正常。现象比较典型:旧机器上每次都是枚举到一半就中断,设备端看起来没有响应痕迹。

排查过程首先怀疑上拉电阻焊接问题,但量了多块板子没问题。后来用协议分析仪对比新老主机端的枚举请求,发现问题出在设备描述符里 bcdUSB 字段。设备固件里写的是 0x0200,声明支持 USB 2.0。老电脑的主机控制器按 USB 2.0 高速规则和全速设备通信,但在某些老主板上,设备复位之后主机等待设备 Chirp 的时序比较严苛,全速设备不回 Chirp 但声明的版本又是 2.0,导致主机的时序判断产生偏差。

这个案例的教训是:如果设备实际只跑全速,bcdUSB 按真实的实现填写反而更稳妥。比如只支持全速,填 0x0110 或者 0x0200 都合法,但不同主机控制器在这个字段上的处理逻辑有差异。遇到兼容性问题时,优先检查这个字段是否与设备实际能力匹配。

6. 数据流与带宽规划的实践方法

6.1 帧/微帧结构下的带宽分配规则

USB 总线上,时间被划分为固定长度的帧。全速和低速模式下帧长度为 1ms,高速模式下进一步细分为 8 个微帧,每个微帧 125μs。主机在每个帧开始时发送一个帧起始包(SOF),所有传输都必须在帧的时间槽内完成。

每帧能装的传输量是有限制的。全速模式下,每帧最多可以传输 19 个批量事务(每个事务 64 字节数据加上协议开销约 110 字节),理论上传满也就 1.2MB/s 左右。中断传输和等时传输在每帧中会优先获得带宽预留,剩下的一部分才是批量数据传输的空间。

设计阶段做带宽预算时,我会把等时和中断传输先按最坏情况计算预留量,剩下再分配给批量传输,并且保留约 20% 余量。全速中断传输的最小间隔是 10ms,不过很多人会把鼠标的轮询间隔设成 10ms,采用 1ms 间隔会导致高速轮询在总线上占用高频时间槽,影响其他批量传输的性能。

6.2 批量传输的高吞吐优化方法

做 USB 存储或大数据上传,要达到接近理论带宽需要多方面配合。最基础的是包大小:全速下批量传输最大包 64 字节,高速下是 512 字节。选错包大小会直接影响传输效率,比如全速设备用 32 字节包,每个事务只能传 32 字节但协议开销不变,有效带宽直接减半。

主机端和设备端的缓冲管理同样重要。设备端要使用双缓冲甚至多缓冲结构,让 DMA 持续填充数据,USB 控制器的数据包发送不因 CPU 介入而出现空档。主机端调用读接口时,请求的缓冲区大小最好是接口描述符中设定的最大包大小的整数倍。一次读 4096 字节和一次读 512 字节的吞吐差距非常明显。

批量传输的包与包之间还有纳秒级的间隔,如果固件在每包之间做耗时操作,比如处理中断、开关 GPIO,就会显著拖慢总吞吐。优化思路是把数据搬移放到 DMA 中断里做,主循环只做业务流程。

6.3 等时传输的时序设计要点

等时传输讲究准时性。全速等时传输每帧可以传最多 1023 字节,高速等时传输每微帧最多 3072 字节。设计等时传输时,首先决定每帧数据量,然后根据这个数据量选择一个不超过预算的包大小。

等时传输没有重传,数据包出错就直接丢弃,因此必须做前向纠错或者接受丢帧的现实。比如音频设备,一帧数据丢了,播放可能会有几毫秒的卡顿或爆音,可以接受的范围内靠缓冲来吸收。摄像头场景丢帧表现为画面短暂卡顿,也可以接受,但要注意设备端发送节奏必须严格贴合帧时序,不能想发就发,否则会导致主机端流重组错位。

等时传输还有一个隐蔽问题:如果配置描述符里多个等时端点分布在同一个帧内,主机可能无法在时间上同时满足它们的发送间隔。这种情况主机可能会拒绝加载驱动,或者在切换到备用设置时报错。多等时端点场景下,务必提前核算每帧总等时负载不超过带宽预算。

最后的几条心得

USB 调试里我一直觉得,“慢就是快”这个原则特别适用。遇到问题,不要急着改代码试运气,先确认物理层没有问题,再看主机端枚举请求和设备端的响应是否匹配,最后才考虑数据路径。

工具链也很重要。一套能解码 USB 包的逻辑分析仪,加上主机端的抓包工具,能覆盖绝大多数场景。硬件条件受限时,Linux 的 usbmon 抓包几乎是零成本的调试利器,强烈建议花半小时熟悉一下。

下一篇文章我打算深入讲描述符的完整构造过程,用一套真实项目的配置描述符集合做例子,手把手带着拆解每个字节的含义。那样你就能从“枚举成功但功能不正常”进阶到“随时能徒手写出一份干净的描述符表”。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询