- 嵌入式
- 物联网
- 异步编程
【免费下载链接】embassy
Modern embedded framework, using Rust and async.
本指南面向使用 Rust 与 async 进行嵌入式开发的工程师,系统讲解 Embassy 框架中embassy-usb-synopsys-otgcrate 的定位、架构与实现原理。该 crate 是embassy-usb-drivertrait 在 Synopsys DesignWare USB OTG(又称 DWC2)内核上的参考实现,它剥离了芯片特定的时钟与引脚初始化,只保留跨芯片通用的驱动核心,被embassy-stm32(STM32 系列)与esp-hal(ESP32 系列)等 HAL 直接复用。读完本文,你将掌握该驱动核心的目录结构、设备/主机两种模式的关键 API、Config配置项的语义与适用场景,以及如何在自己的 HAL 中完成集成。
一、crate 定位:embassy-usb-driver 的 Synopsys OTG 实现
embassy-usb-synopsys-otg的职责非常单一且明确:为 Synopsys USB OTG 设备(IP 核在 STM32 上通常被称作 OTG_FS / OTG_HS,在 ESP32 上则由 DWC2 演化而来)实现embassy-usb-driver定义的那套异步 USB 驱动接口。
从它的 Cargo.toml 可以看到其依赖关系,这是理解它"不做什么"的关键:
embassy-usb-driver = 0.2.2:非可选依赖,Bus、ControlPipe、EndpointIn、EndpointOut等 trait 全部来自这里;embassy-sync = 0.8.0:提供AtomicWaker、CriticalSectionRawMutex等无栈并发原语,用于中断上下文与任务之间的事件传递;embassy-time = 0.5.1(可选):仅在启用embassy-time/hostfeature 时启用,用于握手超时与远端唤醒延时;portable-atomic:在没有原生原子指令的平台上(如 Xtensa 架构)提供原子操作;defmt/log(均可选):两套互斥的日志后端。
依赖面如此之窄,意味着它只关心"USB 协议 + 寄存器操作"这一层。时钟树配置、GPIO 复用、VBUS 电源管理这些与具体芯片强相关的工作,全部留给上层 HAL 完成。README 对此有明确说明:"It contains the 'core' of the driver that is common across all chips using the Synopsys OTG IP, but it doesn't contain chip-specific initialization such as clock setup and GPIO muxing."(它包含所有使用 Synopsys OTG IP 的芯片共通的驱动核心,但不包含时钟配置、GPIO 复用等芯片特定初始化。)
正因为这种分层设计,绝大多数使用者不应该直接依赖本 crate,而应通过集成它的 HAL 使用,例如embassy-stm32提供的usb::Driver与usb::Otg。
二、架构分层:驱动核心与 HAL 的边界在哪里
README 明确列出了两个已经集成该驱动的 HAL:
embassy-stm32:面向 ST 的 STM32 芯片;esp-hal:面向乐鑫的 ESP32 芯片。
如果你要为自己的设备集成这个 crate,就需要补上"设备特定初始化"这一层。README 给出的集成路径是:参考上述两个 crate 的集成方式,为你的芯片补齐初始化代码。
在仓库源码中可以清晰地看到这条边界是如何被embassy-stm32补全的。HAL 侧需要做三件事:
- 构造
OtgInstance:把寄存器指针regs、共享状态T::state()、FIFO 深度、PHY 类型、TX FIFO 数量以及 TRDT 计算函数一起打包交给OtgDriver::new; - 注册中断处理:把
embassy_usb_synopsys_otg::on_interrupt接到芯片的 USB 中断向量上(InterruptHandler<T>中on_interrupt_impl(r, &T::state())); - 提供
Instancetrait:让芯片模块给出regs()与state(),同时负责时钟、引脚的初始化。
从源码结构看,embassy-stm32的这一层同时也承担了DpPin/DmPin(内部 FS PHY 的 D+/D- 引脚)或 ULPI 接口的接线工作——例如在new_hs构造路径中会要求传入两个引脚。这正是 README 所说的"device-specific initialization"。
三、源码构成:四个模块各司其职
crate 的src/目录只有四个文件,职责划分非常清晰:
| 文件 | 职责 |
|---|---|
| lib.rs | 驱动主体:设备模式(Device)的全部实现,包括Driver、Bus、ControlPipe、Endpoint、Config、PhyType、State/StateStorage、OtgInstance以及核心中断处理on_interrupt |
| host.rs | 主机模式(Host)实现,通过hostfeature 启用:OtgHost、HostStateStorage、OtgHostInstance、Channel等 |
| otg_v1.rs | DWC2 内核的寄存器与位域定义(PAC),约 5000 行,是整个驱动触碰硬件的地基 |
| fmt.rs | 日志格式化辅助(如Bytes包装),为defmt/log提供可读的二进制输出 |
otg_v1.rs的注释值得留意:DWC2 内核"以公开文档稀少而闻名"(well known for being poorly documented publicly),驱动作者通过Otg结构体上的方法(如gintsts()、grxstsp()、fifo()、diepctl()等)提供了类型化的寄存器访问,这是整个驱动与硬件交互的唯一通道。
四、设备模式核心 API 详解
设备模式是本 crate 的默认能力(不启用任何 feature 即可使用)。下面按使用与实现的顺序拆解核心类型。
4.1 Config:三个配置项的真实语义
Config是驱动构造时传入的配置结构,仅三个字段,但每一个都对应着 USB 协议或芯片勘误表中的真实问题:
| 字段 | 类型 | 默认值 | 语义与适用场景 |
|---|---|---|---|
vbus_detection | bool | false | 是否启用 VBUS 检测。USB 规范要求设备监测 USB 线缆的插拔;若设备由总线供电(bus-powered,即从 VBUS 取电),拔线后设备本来就断电,可以始终假定"线缆已插入",因此可不检测;若设备自供电(self-powered,拔线后仍保持供电),则必须置true,否则设备在拔线后仍会以为处于连接状态。置true时硬件上需要把 VBUS 分压后接入检测引脚 |
vbus_valid_override | bool | false | VBUS 检测的软件覆盖。当vbus_detection关闭时,允许把分压后的 VBUS 接到任意数字输入引脚,由软件控制vbvaloven/vbvaloval(VBUS 有效覆盖)、avaloen/avaloval(A 外设会话有效覆盖)、bvaloen/bvaloval(B 外设会话有效覆盖)这几组寄存器位 |
xcvrdly | bool | false | 收发器延时。某些 ULPI PHY(如 Microchip USB334x 系列)要求在 ULPI 寄存器写入(发起 HS Chirp)与随后的发送命令之间插入延时,否则 HS Chirp 不会执行、设备会退化为 Full-Speed 枚举。STM32H7 等芯片的 USB 链路 IP 支持加入该延时以兼容这些 PHY |
Default实现给出了三个字段的默认值,全部为false,也就是说驱动默认假定:总线供电、不做 VBUS 检测、不需要收发器延时。
Config在运行时如何生效?从 Bus::poll 可以看到它的关键作用点:
- 若
vbus_detection与vbus_valid_override都为false,驱动在首次初始化后直接上报一次Event::PowerDetected,跳过对 VBUS 中断的等待; - 若任一为
true,则依赖srqint(会话请求/上电检测)与otgint中的sedet(会话结束)中断,分别产生PowerDetected与PowerRemoved事件; PowerRemoved会触发disable_all_endpoints(),把 USBAEP 位清零、唤醒所有等待中的端点 future 使其上报Disabled。
4.2 PhyType:四种 PHY 形态
PhyType枚举了驱动支持的四种 PHY 组合:
InternalFullSpeed:内部 Full-Speed PHY,大多数带高速外设的芯片上可用;InternalHighSpeed:内部 High-Speed PHY,少数 STM32 芯片可用;ExternalFullSpeed:外部 ULPI Full-Speed PHY(或高速 PHY 运行在 FS 模式);ExternalHighSpeed:外部 ULPI High-Speed PHY。
它提供两个辅助方法internal()(是否为内部 PHY)与high_speed()(是否为高速 PHY),并内部映射到寄存器中的Dspd(设备速度)位域。PHY 类型直接决定 configure_as_device 中GUSBCFG寄存器的写法:内部 FS PHY 走physel路径,内部 HS PHY 走 UTMI+ 路径并依据GHWCFG4硬件配置位宽,外部 PHY 则走 ULPI 单数据率路径。
4.3 State 与 StateStorage:中断与任务共享的内存
由于驱动同时被中断处理函数(ISR)和异步任务访问,状态管理是嵌入式驱动设计的核心问题。本 crate 的解法是编译期定长的StateStorage+ 类型擦除的State借用:
StateStorage<const EP_COUNT: usize, M>:以常量泛型声明端点数量(EP0 + 用户端点),内部持有ControlPipeSetupState(SETUP 包缓存)、[EpState; EP_COUNT](每个端点的 waker、缓冲区指针、分配信息)、AtomicWaker(总线事件唤醒器)和互斥锁M。它可以在const fn new中构造,因此能够以static形式分配;State::as_state()返回State<'d, M>——一个只包含引用与类型参数的轻量视图,可以在OtgInstance、Driver、Bus以及on_interrupt之间自由复制传递(Copy)。
EpState内部有个值得注意的设计点:in_enabled/out_enabled是DIEPCTL/DOEPCTL.USBAEP 的 RAM 镜像。源码注释说明,在部分芯片(如 nRF54LM20A)上,无 VBUS 供电时读取该寄存器位会让整个芯片挂死,因此端点 future 检查 RAM 镜像而非直接读寄存器。这是一个"跨芯片通用核心"必须回避硬件坑的典型例子。
4.4 Driver 与 OtgInstance:构造驱动的两个参数
Driver::new接受三个参数:
ep_out_buffer: &'d mut [u8]——内部临时缓冲区,用于暂存收到的 OUT 包,必须能容纳所有 OUT 端点的最大包长之和,否则端点分配会失败(对应EndpointAllocError);instance: OtgInstance<'d, M>——硬件相关的打包描述;config: Config。
OtgInstance是 HAL 集成层与驱动核心的握手结构,字段包括:
regs: Otg:外设寄存器入口;state: State:共享状态(来自StateStorage::as_state);fifo_depth_words: u16:专用 FIFO 总深度(按 32 位字计);phy_type: PhyType:PHY 类型;extra_rx_fifo_words: u16:某些实现额外需要的 RX FIFO 字长;tx_fifo_count: u8:TX FIFO 数量,可以小于端点数量,无空闲 TX FIFO 时 IN 端点分配失败;calculate_trdt_fn: fn(speed: Dspd) -> u8:根据内核时钟速度计算 TRDT(收发器往返延时)值的回调,这是少数"因芯片而异"的函数指针。
4.5 端点分配:FIFO 记账与失败场景
alloc_endpoint是端点分配的核心逻辑,可以从中学到 DWC2 资源管理的全部约束:
- OUT 端点:从
ep_out_buffer中划出一段与max_packet_size等长的缓冲,越界则EndpointAllocError; - FIFO 字长:OUT 端点按
(max_packet_size + 3) / 4取整;IN 端点取max(该值, 16)——因为 DWC2 的INEPTXFD要求TX FIFO 最小为 16 个字; - 容量检查:
fifo_size_words + 已分配 FIFO 字长 > fifo_depth_words时报错; - 槽位选择:可以显式指定
ep_addr(要求索引合法、EP0 仅限 Control 类型、未被占用),也可以让驱动自动找空槽;EP0 被保留给控制管道; - TX FIFO 分配:EP0 使用 TX FIFO 0,其余 IN 端点从 1 号开始找空闲 FIFO(见
alloc_tx_fifo)。
Driver::start会先自动分配 EP0 的 IN/OUT 两个控制端点并断言索引为 0,随后返回Bus与ControlPipe。
4.6 on_interrupt:ISR 的核心工作流
on_interrupt是设备模式中断处理的唯一入口,HAL 只需把它挂到中断向量上。它处理四类事件:
- 全局事件:
wkupint(唤醒)、usbsusp(挂起)、usbrst(总线复位)、enumdne(枚举完成)、otgint(OTG 中断)、srqint(会话请求)——统一遮蔽 IN/OUT 端点与 RX FIFO 中断后唤醒Bus::poll; - RX FIFO 非空(共享 RX FIFO,所有 OUT 端点共用):按
grxstsp中的pktstsd分派SETUP_DATA_RX(8 字节 SETUP 包,从 FIFO 读出写入setup_data原子缓存)、OUT_DATA_RX(将数据拷入端点 OUT 缓冲、写入长度、唤醒 waker)、OUT_DATA_DONE等状态; - IN 端点中断:清中断、处理 TXFE(TX FIFO 空)、唤醒对应 IN waker;
- OUT 端点中断:处理 STUP(SETUP 就绪)、唤醒 OUT waker;
- 不完整等时 IN 传输(
iisoixfr):帧结束发现等时 IN 端点仍持有未发送的包时,翻转包极性(even/odd frame)并在下一帧重发——这是等时传输在 slave 模式下的标准补救手法。
4.7 控制管道与端点数据传输
ControlPipe:setup()从setup_data原子缓存重组 8 字节 SETUP 包并清 NAK;data_in在发送最后一包后等待主机状态阶段;accept_set_address在写入DCFG.DAD后必须再走一次accept()——源码注释明确这是 Synopsys 驱动的要求;Endpoint::read与write:read通过out_size原子变量与 ISR 同步缓冲区独占权(配合EP_OUT_BUFFER_EMPTY哨兵值),等时端点会依据当前帧号设置奇偶位,并在重新使能时注意"新版 DWC2 内核(如 nRF54LM20A)每次传输后自动清 EPENA";write在 FIFO 空间不足时使能 TXFE 中断等待,且把"写 FIFO"放入互斥锁内以规避勘误(向 FIFO 的写入序列若被其他 OTG_FS 寄存器访问打断会损坏数据);- EP0 的 MPSIZ:
ep0_mpsiz(lib.rs#L1992)把 EP0 最大包长映射为特殊编码:8→0b11、16→0b10、32→0b01、64→0b00,其他值直接 panic——这是 DWC2 寄存器层面的硬约束。
4.8 远端唤醒
remote_wakeup(lib.rs#L1577)仅在启用embassy-timefeature 时可用:先解除挂起期间的 PHY 时钟门控(PCGCCTL.STPPCLK,HS 内核还需清GATEHCLK),再置DCTL.RWUSIG发起 K 态恢复信号并保持 10ms(USB 2.0 规范要求 1–15ms),最后撤销。未启用embassy-time时直接返回Unsupported。
五、主机模式:host feature 下的 OtgHost
自 0.4.0 起,crate 通过hostfeature 引入完整的 USB Host 模式支持,实现在 host.rs 中。embassy-stm32的usb-hostfeature 即透传启用它。
5.1 主机侧的结构对应关系
主机模式的构件与设备模式一一对应:
| 设备模式 | 主机模式 | 说明 |
|---|---|---|
StateStorage<EP_COUNT> | HostStateStorage<CH_COUNT> | 以常量泛型声明通道数(channel),每个通道有自己的AtomicWaker、事件标志、RX 缓冲区指针与分配标记 |
State::as_state | HostStateStorage::as_host_state | 返回可复制的HostState<'d, M>视图 |
OtgInstance | OtgHostInstance | 打包regs、state、FIFO 深度与 PHY 类型 |
on_interrupt | on_host_interrupt | 主机中断入口 |
主机 ISR 的处理重点是:端口事件(连接/断开/过流,通过port_event位掩码与port_waker上报)、RX FIFO 非空(按通道号读取 IN 数据)、以及通道中断(把 XFRC/STALL/NAK/NYET/TXERR/BBERR 等事件位按位或进通道result原子邮箱)。源码中还处理了一个 slave 模式专属的坑:每个 IN 数据包到达后 DWC2 会把该通道从调度器中摘除,即使HCCHAR.CHENA仍为 1 且HCTSIZ.PKTCNT > 0,因此 ISR 需要重新写回 CHENA=1 以排队下一个 IN 令牌,否则多包传输会在第一包后停止。
5.2 主机初始化与总线复位
OtgHost在首次wait_for_device_event时完成两阶段初始化:
configure_as_host:按CID寄存器匹配 DWC2 内核版本(v1 如 0x1100/0x1200、v2/v3 如 0x2000/0x2300/0x3000、v5 如 0x5000/0x6100),分别配置GCCFG的 PHY 上下电与 VBUS 传感位,然后强制fhmod进入主机模式,轮询cmod位等待切换生效(最长 50ms);init_host:配置HCFG的 PHY 时钟(HS PHY 用 30/60MHz,FS PHY 用 48MHz),按"RX FIFO 一半、非周期 TX FIFO 四分之一、周期 TX FIFO 四分之一"的比例切分总 FIFO,冲掉所有 FIFO,给端口上电(HPRT.PPWR),最后使能端口、通道、断开、RX FIFO 与 SOF 中断。
bus_reset实现标准的复位时序:先停掉残留活动的硬件通道、清中断、冲 FIFO,然后置PRST保持 50ms(USB 规范要求 ≥10ms),解除复位后再等待 20ms。
5.3 通道(Pipe)与传输原语
Channel是UsbPipe的具体实现,通过OtgHostAllocator::alloc_pipe以原子 CAS 抢占空闲通道槽位获得。它的传输原语对 USB 语义的处理很讲究:
do_out_transfer/do_in_transfer:对 NAK/NYET 无限重试——注释明确"NAK 是合法的 '再试一次' 响应,没有时间上限,需要超时由调用方负责";HS 设备返回 NYET 后会在下一轮重新置doping(PING 令牌);do_control_in/do_control_out:实现完整的控制传输三阶段(SETUP → DATA → STATUS),数据阶段按最大包长切分并交替 DATA0/DATA1,STATUS 阶段固定用 DATA1 的零长包,且"无数据阶段"时按请求方向(bmRequestType的 bit7)决定 STATUS 方向;- 事件分类
classify_events按优先级处理:断开 > STALL > 数据翻转错误 > 串扰 > 坏响应 > 完成 > NYET > NAK > 通道停止; Drop实现自动请求硬件停止通道、屏蔽该通道中断并释放槽位。
此外,驱动通过request_in/request_out内部维护 data toggle,并根据实际传输的包数(div_ceil)翻转;在高速根端口后接低速/全速设备时,若未提供 split 传输支持则明确返回错误而不是发出设备无法响应的令牌。
六、功能特性一览
crate 的 features 定义如下:
| Feature | 依赖效果 | 用途 |
|---|---|---|
default | 空 | 无额外能力 |
defmt | dep:defmt+embassy-usb-driver/defmt | 使用 defmt 格式日志(与log二选一) |
log | dep:log | 使用logcrate 日志 |
host | embassy-time | 启用 USB 主机模式支持(host.rs仅在此时编译) |
embassy-time | dep:embassy-time | 启用设备模式下的远端唤醒(remote wakeup)支持 |
其中host与embassy-time两个 feature 的注释分别写明:"Enables USB host mode support" 与 "Enables remote wakeup support in device mode"。crate 的 CI 构建矩阵(Cargo.toml#L12-L27)覆盖了thumbv7m-none-eabi(Cortex-M)与xtensa-esp32s2-none-elf(ESP32-S2)两类目标,并逐一验证defmt/log、embassy-time、host的组合,这从侧面说明了它对 STM32 与 ESP32 两大平台的双重支持。
七、如何集成到自己的 HAL
README 的结尾给出了明确指引:如果你想把这个 crate 集成进自己设备的 HAL,需要补上设备特定的初始化,参考embassy-stm32与esp-hal的做法。结合源码,一个最小集成需要完成:
- 定义
Instancetrait,提供regs()(Otg寄存器指针)与state()(StateStorage的as_state()); - 在中断处理中调用
embassy_usb_synopsys_otg::on_interrupt(r, &state)(设备模式)或on_host_interrupt(主机模式); - 构造
OtgInstance,填好fifo_depth_words(查参考手册的 OTG_FS/OTG_HS FIFO RAM 大小)、phy_type、tx_fifo_count、extra_rx_fifo_words与calculate_trdt_fn; - 调用
OtgDriver::new(ep_out_buffer, instance, config)得到驱动,交给embassy-usb协议栈使用。
至于更上层(embassy-usb栈、类驱动、embassy-executor任务调度)如何使用这个驱动,可以参考 docs/pages/examples.adoc 中列出的 USB 示例。
八、小结
embassy-usb-synopsys-otg是 Embassy 生态中"跨芯片驱动核心"分层思想的范本:它把 Synopsys OTG/DWC2 内核那套晦涩难懂的寄存器与 FIFO 管理封装成符合embassy-usb-drivertrait 的异步接口,同时把所有芯片相关性(时钟、引脚、PHY 上电顺序、TRDT 计算)通过OtgInstance和 HAL 层剥离出去。理解它的Config语义、StateStorage共享状态设计、FIFO 分配规则与设备/主机两套中断工作流,不仅有助于在 STM32、ESP32 上正确使用 USB 功能,也能为编写其他外设驱动提供良好的参考模式。
- 嵌入式
- 物联网
- 异步编程
【免费下载链接】embassy
Modern embedded framework, using Rust and async.
相关推荐
embassy-usb-driver 深度解析:embassy 的 USB 驱动抽象层设计与 0.2.x 关键变更全解读
embassy usb driver 深度解析:embassy 的 USB 驱动抽象层设计与 0.2.x 关键变更全解读 embassy usb driver
嵌入式物联网异步编程embassy-usb-host 演进全解读:从 CHANGELOG 看异步 USB Host 协议栈的枚举、类驱动与可靠性设计
embassy usb host 演进全解读:从 CHANGELOG 看异步 USB Host 协议栈的枚举、类驱动与可靠性设计 本文以 embassy usb
嵌入式物联网异步编程embassy-usb-driver:为 Rust 异步 USB 设备栈编写硬件驱动所需的全部 Trait 指南
embassy usb driver:为 Rust 异步 USB 设备栈编写硬件驱动所需的全部 Trait 指南 导读 embassy usb driver 是
嵌入式物联网异步编程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考