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设备的自我介绍。主机通过读取描述符来了解设备是什么、能干什么、怎么用。
描述符的层级关系是这样的:
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 描述符的枚举流程
主机枚举设备时,会按顺序请求描述符:
- 主机发送GET_DESCRIPTOR请求获取设备描述符(前8字节)
- 主机根据bMaxPacketSize0重新设置端点0最大包长
- 主机再次获取完整设备描述符(18字节)
- 主机获取配置描述符及所有附属描述符
- 主机根据描述符信息选择合适的驱动
- 主机发送SET_CONFIGURATION激活配置
嗯,这里有个细节:步骤1只取前8字节是为了先知道端点0的包长,否则可能因为包长不匹配导致通信失败。我刚开始做USB时没注意这个顺序,结果枚举总是卡住。
好了,这些核心概念是USB协议栈的基石。端点负责收发数据,管道建立逻辑连接,传输类型决定通信方式,描述符让主机认识设备。搞懂这些,后面写固件、调驱动就顺了。