手搓 USB 协议栈:枚举过程与描述符解析

Evek Golden Lv4

提到 USB,很多嵌入式工程师会“谈虎色变”。毕竟相比于 UART/SPI 的几页寄存器手册,USB 2.0 规范洋洋洒洒 650 页。但如果只想实现一个简单的 USB 设备(Device),比如把开发板模拟成一个串口或键盘,其实并不需要啃完整个协议栈。

1. 物理层:D+/D- 的舞蹈

USB 使用差分信号 D+ 和 D-。

  • **全速 (Full Speed, 12Mbps)**:D+ 上拉 1.5k 电阻到 3.3V。
  • **高速 (High Speed, 480Mbps)**:握手成功后切换。

当我们将 USB 设备插入电脑(Host)时,Host 检测到 D+ 或 D- 被拉高,就会发起复位(Reset)信号(拉低 D+ / D- 至少 10ms),开始枚举过程。

2. 核心概念:端点 (Endpoint)

USB 通信的终点是端点(Endpoint, EP)。

  • EP0:控制端点,双向,所有设备必须支持。用于传输控制命令(枚举就是靠它)。
  • EP1~EP15:数据端点,单向(IN 或 OUT)。
    • IN:Device -> Host
    • OUT:Host -> Device

3. 枚举状态机 (Enumeration)

枚举就是 Host 问 Device:“你是谁?你能干什么?” Device 乖乖交出“简历”(描述符)的过程。

  1. Powered:设备插入,上电。
  2. Default:Host 发送复位。此时设备地址为 0
  3. Address
    • Host 发送 SET_ADDRESS 命令(如设为 5)。
    • 设备确认,从此以后只响应地址 5 的包。
  4. Configured
    • Host 索要各种描述符。
    • Host 发送 SET_CONFIGURATION,设备进入工作状态,驱动加载成功。

4. 描述符 (Descriptors)

这即使 USB 的“简历”结构,这也是手写协议栈最繁琐的部分。

1
2
3
4
5
6
7
8
Device Descriptor (设备描述符,只有1个)
├── Configuration Descriptor (配置描述符)
│ ├── Interface Descriptor 0 (接口描述符,如 CDC 控制接口)
│ │ └── Endpoint Descriptor (端点描述符,如中断传输 EP)
│ └── Interface Descriptor 1 (接口描述符,如 CDC 数据接口)
│ ├── Endpoint Descriptor (端点描述符,如批量传输 OUT)
│ └── Endpoint Descriptor (端点描述符,如批量传输 IN)
└── String Descriptors (字符串描述符,厂商名、产品名)

关键坑点

  • **小端序 (Little Endian)**:USB 协议规定数据低字节在前。构建多字节字段时要小心。
  • 字节对齐:某些结构体最好加上 __attribute__((packed)),防止编译器自动填充导致数据错位。

5. 实现一个 CDC (虚拟串口)

CDC (Communication Device Class) 是最常用的类。

  • 无需驱动:Windows 10+ 和 Linux 自带驱动。
  • 实现要点
    • 需要两个接口:
      1. **通信接口类 (CIC)**:管理串口状态(波特率等),使用 1 个中断 IN 端点。
      2. **数据接口类 (DIC)**:传输实际数据,使用 1 个批量 IN 和 1 个批量 OUT 端点。

总结

自己手写一遍 USB 枚举代码,是对协议理解最好的检验。当你看到电脑右下角弹出“USB 设备已识别”的那一刻,那种成就感是调用厂商库无法比拟的。

  • Title: 手搓 USB 协议栈:枚举过程与描述符解析
  • Author: Evek Golden
  • Created at : 2024-02-05 10:00:00
  • Updated at : 2026-06-12 08:57:02
  • Link: https://blog.cocodemo.uno/posts/usb8w2n/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments