2. USB协议核心概念:端点、管道、传输类型与描述符体系

说实话,刚接触USB协议的时候,我也被一堆术语搞得头晕。端点、管道、传输类型、描述符……这些概念到底怎么串起来的?今天我就用实战的视角,把这些核心概念给你讲透。

2.1 端点(Endpoint)——USB通信的最小单元

端点是什么?说白了就是USB设备里的一个数据收发点。每个USB设备可以有多个端点,就像你家的水龙头有多个出水口一样。

我个人习惯把端点想象成“邮箱”:主机往端点0发数据,设备从端点0收数据;设备往端点1发数据,主机从端点1收数据。每个端点都有自己的地址和传输方向。

关键特性:

  • 每个端点有唯一的端点号(0~15)
  • 方向分为IN(设备→主机)和OUT(主机→设备)
  • 端点0是默认控制端点,所有设备必须支持
  • 端点描述符定义了端点的传输类型、最大包长、轮询间隔

实战经验:我在调试一个HID设备时,发现端点描述符里的最大包长设成了64字节,但实际只发8字节。主机每次都要等64字节才处理,导致响应延迟。后来改成实际包长,问题解决。记住:描述符里的值一定要和固件实现一致。

2.2 管道(Pipe)——端点的逻辑连接

管道是主机和设备端点之间的逻辑通信通道。你可以理解为:管道 = 主机软件缓冲区 + 设备端点 + 传输协议。

USB规范定义了两种管道:

管道类型 说明 典型用途
消息管道 双向、有结构、主机发起的控制传输 枚举、配置、状态查询
流管道 单向、无结构、数据流传输 批量数据、中断数据、同步数据

嗯,这里要注意:管道是在设备配置完成后才建立的。枚举阶段只有默认管道(端点0)可用。

2.3 传输类型——四种通信模式

USB定义了四种传输类型,每种都有自己的脾气。我当年做U盘固件时,就因为在批量传输上栽过跟头。

2.3.1 控制传输

最复杂的传输类型,分三个阶段:Setup阶段、Data阶段(可选)、Status阶段。主要用于枚举和标准请求。

  • 保证传输,有重试机制
  • 最大包长:全速8/16/32/64字节,高速64字节
  • 每个设备必须支持端点0

2.3.2 批量传输

大块数据传输,比如U盘读写。不保证带宽,但保证数据正确。

  • 全速最大包长:8/16/32/64字节
  • 高速最大包长:512字节
  • 超高速最大包长:1024字节

避坑指南:我曾经在批量传输的固件里忘记处理NAK重试,结果主机发数据时设备一直NAK,导致总线挂死。记住:批量传输端点收到NAK后必须等待主机重试,不能自己主动发数据。

2.3.3 中断传输

适合小数据量、周期性传输,比如鼠标、键盘。主机保证轮询间隔。

  • 全速最大包长:1~64字节
  • 高速最大包长:1~1024字节(3个微帧内)
  • 轮询间隔在端点描述符中定义(1~255ms)

2.3.4 同步传输

实时性要求高,允许少量错误。比如音频、视频流。

  • 无重试机制,数据可能丢失
  • 全速最大包长:1~1023字节
  • 高速最大包长:1~1024字节
  • 需要精确的时钟同步
传输类型 可靠性 带宽保证 延迟 典型应用
控制 高 无 中 枚举、配置
批量 高 无 高 U盘、打印机
中断 高 有 低 鼠标、键盘
同步 低 有 极低 音频、摄像头

2.4 USB描述符体系——设备的身份证

描述符就是USB设备的自我介绍。主机通过读取描述符来了解设备是什么、能干什么、怎么用。

描述符的层级关系是这样的:

USB描述符层级结构 设备描述符 配置描述符 接口描述符 端点描述符 端点描述符 端点描述符 其他描述符(HID、UAC、MSOS等) 一个设备只有一个 设备描述符 一个设备可以有 多个配置描述符 一个配置可以有 多个接口描述符

2.4.1 设备描述符

最顶层的描述符,描述设备的整体信息。包括USB版本、厂商ID、产品ID、序列号等。

// 设备描述符结构(USB 2.0)
struct usb_device_descriptor {
    uint8_t  bLength;            // 描述符长度(18字节)
    uint8_t  bDescriptorType;    // 描述符类型(0x01)
    uint16_t bcdUSB;             // USB规范版本号(如0x0200)
    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; // 配置描述符数量
} __attribute__((packed));

实战技巧:我建议你在固件里把设备描述符定义成const数组,放在Flash里。这样既节省RAM,又方便调试时通过USB分析仪抓包比对。另外,bMaxPacketSize0这个字段特别重要——枚举阶段主机就是靠它知道端点0的包长。

2.4.2 配置描述符

描述设备的电源属性、接口数量等。一个设备可以有多个配置,但同一时刻只能激活一个。

2.4.3 接口描述符

描述一个功能单元。比如一个复合设备可能有多个接口:一个HID键盘接口、一个HID鼠标接口。

2.4.4 端点描述符

描述端点的传输类型、方向、最大包长、轮询间隔。每个非0端点都需要一个端点描述符。

2.4.5 字符串描述符

可选的描述符,提供人类可读的厂商名、产品名、序列号。主机通过索引号请求。

避坑指南:我曾经在字符串描述符里用了中文,结果在Linux下显示正常,Windows下乱码。后来发现Windows对UNICODE字符串的编码要求更严格。建议字符串描述符只用英文和数字,避免跨平台兼容性问题。

2.5 描述符的枚举流程

主机枚举设备时,会按顺序请求描述符:

  1. 主机发送GET_DESCRIPTOR请求获取设备描述符(前8字节)
  2. 主机根据bMaxPacketSize0重新设置端点0最大包长
  3. 主机再次获取完整设备描述符(18字节)
  4. 主机获取配置描述符及所有附属描述符
  5. 主机根据描述符信息选择合适的驱动
  6. 主机发送SET_CONFIGURATION激活配置

嗯,这里有个细节:步骤1只取前8字节是为了先知道端点0的包长,否则可能因为包长不匹配导致通信失败。我刚开始做USB时没注意这个顺序,结果枚举总是卡住。

好了,这些核心概念是USB协议栈的基石。端点负责收发数据,管道建立逻辑连接,传输类型决定通信方式,描述符让主机认识设备。搞懂这些,后面写固件、调驱动就顺了。