端点(Endpoint)概念深入:地址、方向与特殊角色

端点,说白了就是 USB 设备里一个个独立的数据收发单元。我刚开始接触 USB 时,总觉得端点这概念有点虚——不就是个编号吗?后来调了一次批量传输的 bug,才发现端点的地址、方向、类型这些属性,任何一个配错了,设备就没法正常工作。

咱们这一节就把端点彻底讲透。从地址编码到方向判定,从端点 0 的特殊地位到状态机流转,每个点我都会结合项目里的实际案例来说。

端点地址与方向:0x81 到底是什么意思?

每个端点都有两个关键属性:地址和方向。地址是一个 4 位二进制数,范围 0~15;方向用 1 位表示,0 是输出(OUT),1 是输入(IN)。

你可能会问:那 0x81 是什么?

0x81 是端点地址和方向合并后的结果。高 4 位是端点号,低 1 位是方向。0x81 拆开看:

  • 0x8 = 端点号 1(高 4 位是 0001,但 USB 规范里端点号用 4 位表示,0x8 其实是 1000,对应端点 1?不对,这里容易混淆。实际上 0x81 的二进制是 1000 0001,高 4 位 1000 表示端点 1?不,USB 规范中端点地址字段是:bit 3~0 是端点号,bit 7 是方向。所以 0x81 = 1000 0001,端点号 = 0001 = 1,方向 = 1 = IN。所以 0x81 就是端点 1 IN。)
  • 方向位 = 1,表示主机从设备读取数据。

同理,0x01 是端点 1 OUT,0x82 是端点 2 IN,0x02 是端点 2 OUT。

关键点:端点地址 0~15 共 16 个,但每个端点号可以有两个方向(IN 和 OUT),所以最多支持 32 个端点(0~15 各两个方向)。不过端点 0 是双向的,实际可配置的端点是 1~15 各两个方向,共 30 个。

我在项目中遇到过一个问题:有人把端点描述符里的 bEndpointAddress 写成了 0x01,但代码里却用 0x81 去收数据。结果数据死活收不到。查了半天才发现,0x01 是 OUT 端点,主机只能往这个端点写数据,设备想从它读数据?门都没有。

我的习惯:写代码时,我一般用宏定义把端点地址和方向分开定义,比如:

#define EP1_OUT_ADDR  0x01
#define EP1_IN_ADDR   0x81
#define EP2_OUT_ADDR  0x02
#define EP2_IN_ADDR   0x82

这样一眼就能看出哪个端点是干什么的,不容易搞混。

端点 0 的特殊角色:控制传输的专属通道

端点 0 是所有 USB 设备必须实现的端点。它很特殊:

  • 双向的:同一个端点号 0,既能 IN 也能 OUT。主机通过端点 0 发送控制请求,设备通过端点 0 返回响应。
  • 默认存在的:设备上电后,端点 0 就已经配置好了,不需要额外配置。
  • 只支持控制传输:端点 0 只能用于控制传输,不能用于批量、中断或同步传输。
  • 最大包长有限:全速设备最大包长是 8、16、32 或 64 字节;高速设备固定 64 字节。

端点 0 的工作流程,说白了就是主机和设备之间的“握手通道”。设备枚举时,主机通过端点 0 发送各种标准请求:获取设备描述符、设置地址、配置设备……这些全走端点 0。

我曾经踩过的坑:有一次做 USB 音频设备,我在固件里把端点 0 的最大包长设成了 64 字节,但描述符里写的是 8 字节。结果主机枚举时直接报错,设备无法识别。后来查 USB 规范才发现,描述符里的 bMaxPacketSize0 必须和实际硬件配置一致,否则主机不认。

端点类型与传输类型的映射关系

USB 定义了四种传输类型,每种对应一种端点类型。这个映射关系是固定的,不能乱配。

端点类型 传输类型 典型应用 特点
控制端点 控制传输 设备枚举、配置、状态查询 双向、可靠、带宽有保证
批量端点 批量传输 U 盘、打印机、数据采集 可靠、无带宽保证、利用空闲带宽
中断端点 中断传输 鼠标、键盘、游戏手柄 低延迟、有轮询间隔、可靠
同步端点 同步传输 音频、视频、摄像头 实时、无重传、有带宽保证

你想想看,为什么 U 盘用批量端点而不是中断端点?因为批量传输利用总线空闲带宽,数据量大时效率高。而鼠标用中断端点,因为需要低延迟,但数据量小。

我个人的经验是:选端点类型时,先问自己三个问题:

  1. 数据量大不大? 大 → 批量;小 → 中断或控制。
  2. 延迟要求高不高? 高 → 中断或同步;不高 → 批量。
  3. 数据能不能丢? 不能丢 → 批量或中断;可以丢 → 同步。

注意:端点类型是在端点描述符里通过 bmAttributes 字段指定的。这个字段必须和实际硬件支持的端点类型一致。比如你硬件只实现了批量端点,但描述符里写中断端点,主机枚举时就会报错。

端点状态机:从禁用到就绪

每个端点都有自己的状态机。USB 规范定义了端点的几种状态:

  • 禁用(Disabled):端点未启用,不响应任何 USB 事务。
  • 使能(Enabled):端点已配置,可以正常收发数据。
  • 暂停(Stalled):端点遇到错误,返回 STALL 握手包。
  • 空闲(Idle):端点就绪,等待主机发起事务。

状态转换的典型流程是这样的:

  1. 设备上电后,所有端点处于禁用状态。
  2. 主机通过 Set Configuration 请求配置设备,端点被使能。
  3. 使能后的端点进入空闲状态,等待主机发起 IN 或 OUT 事务。
  4. 如果端点遇到协议错误或功能错误,设备可以返回 STALL,端点进入暂停状态。
  5. 主机发送 Clear Feature (ENDPOINT_HALT) 请求,端点恢复空闲状态。

下面这张图展示了端点状态机的核心流转逻辑:

USB 端点状态机 USB 端点状态机 禁用 (Disabled) 使能 (Enabled) 空闲 (Idle) 暂停 (Stalled) Set Configuration 配置完成 错误/STALL Clear Feature USB 复位 状态说明 • 禁用:端点未启用 • 使能:端点已配置 • 空闲:等待事务 • 暂停:错误状态

实际调试时,端点状态机是排查问题的利器。我记得有一次调试 USB 打印机,设备打印几页后就卡死了。用 USB 分析仪抓包一看,批量端点返回了 STALL。原来是打印机缓存满了,固件没正确处理,直接让端点进入了暂停状态。主机收到 STALL 后停止传输,设备就卡死了。

调试技巧:如果设备突然不响应了,先检查端点状态。用 USB 分析仪看设备是否返回了 STALL。如果是,查一下固件里什么条件下触发了 STALL。很多时候是缓存溢出、协议错误或者功能未实现导致的。

端点配置的实战要点

配置端点时,有几个容易忽略的点,我列出来供你参考:

  • 端点号不要重复:同一个端点号不能同时配置为 IN 和 OUT(端点 0 除外)。比如你不能同时有端点 1 IN 和端点 1 OUT,但可以有端点 1 IN 和端点 2 OUT。
  • 最大包长要匹配:全速批量端点的最大包长必须是 8、16、32 或 64 字节;高速批量端点固定 512 字节。写错了主机不认。
  • 描述符顺序要正确:配置描述符里,端点描述符必须跟在接口描述符后面,而且顺序不能乱。主机按顺序解析,顺序错了设备枚举会失败。
  • 端点数量有限:USB 2.0 最多支持 32 个端点(含端点 0 的两个方向)。别贪多,够用就行。

我曾经犯过的错:有一次做复合设备,同时实现了 HID 和 MSC 功能。我在接口描述符里写了两个接口,每个接口各有两个端点。结果枚举时主机只识别了第一个接口。查了半天才发现,我把两个接口的端点号写重复了——接口 1 用了端点 1 IN/OUT,接口 2 也用了端点 1 IN/OUT。USB 规范要求不同接口的端点号不能冲突,除非是同一个接口的备用设置。

端点配置看似简单,但细节很多。你想想看,一个端点地址写错、方向搞反、类型配错,都可能导致设备无法正常工作。所以我的建议是:写代码前先把端点分配表画出来,明确每个端点的号、方向、类型、最大包长,再动手写描述符和固件。