USB
USB 设备要做的事比串口多得多:上电后先让主机枚举,回答设备、配置、字符串描述符,接受地址和配置,然后才在端点上收发数据。这一部分讲 embassy-usb 0.6.0(tag embassy-usb-v0.6.0,.rs 共 7419 行,其中 class/ 下的八个类占一半多)和它下面的 embassy-usb-driver(新项目解析到 0.2.2,发布于 2026-05-28)。HAL 那一侧只读源码:L475 的 OTG_FS 由 embassy-stm32 的 usb/otg.rs 包一层 embassy-usb-synopsys-otg 0.3.3。
- 设备栈:
Drivertrait 把「分配端点」和「运行」分成两个阶段;Builder把描述符写进四块调用方的缓冲区,各个缓冲区和接口数、Handler数不够时的 panic 原文;UsbDevice::run是一个select(bus.poll(), control.setup())循环;用假驱动在 std 上走一遍枚举,打印并拆解 CDC-ACM 的 70 字节配置描述符;synopsys-otg 的中断处理函数、ep_out_buffer的下限、QEMU 上卡在哪一个寄存器;一个 CDC-ACM 设备的代码体积。 - 类:类不是 trait,是一个往
Builder写描述符、拿端点、登记Handler的构造函数;CDC-ACM 的控制线和线路编码怎么叫醒应用任务,数据为什么按包收发、flush为什么不补空包、BufferedReceiver一次交出多少;HID 的报告描述符由谁应答;cdc_ncm接到embassy-net。
flowchart TB app["应用任务"] --> cls["类 CdcAcmClass"] cls --> eps["端点 In Out"] cls --> hdl["Handler"] run["UsbDevice run"] --> hdl run --> busp["Bus 与 ControlPipe"] eps --> drv["驱动 synopsys-otg"] busp --> drv drv --> irq["OTG_FS 中断"] irq --> drv
栈和类都是 no_std、不分配,所有大小都在建设备时定下来;驱动只要实现 embassy-usb-driver 的 Driver、Bus、ControlPipe、EndpointIn、EndpointOut。中断到任务的那一跳和 GPIO 与 EXTI 是同一套做法:处理函数只搬数据、叫醒 AtomicWaker。
实验:QEMU 11.1.2 的 b-l475e-iot01a 没有 OTG_FS 模型(读寄存器得 0),USB 在 QEMU 上枚举不了,所以枚举和收发在 std 上用一个自己写的假驱动完成(M2 Max、macOS 27、rustc 1.99.0);QEMU(embassy-stm32 打了注释掉 rcc/bd.rs 里 while !cr.read().dbp() {} 的一行补丁)只用来验证不碰 USB 寄存器的那部分,交叉编译用来量体积。假驱动没有模拟时序、NAK 和数据翻转位,凡是它给出的结果都只说明栈和类调用了驱动的哪些方法、传了什么字节。