☰
深入解读 embassy-usb-synopsys-otg:Synopsys USB OTG 内核的异步驱动核心
2026/9/25 11:29:54 网站建设 项目流程
  • 嵌入式
  • 物联网
  • 异步编程

【免费下载链接】embassy

Modern embedded framework, using Rust and async.

项目地址:https://gitcode.com/gh_mirrors/em/embassy
点击查看免费下载

本指南面向使用 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 侧需要做三件事:

  1. 构造OtgInstance:把寄存器指针regs、共享状态T::state()、FIFO 深度、PHY 类型、TX FIFO 数量以及 TRDT 计算函数一起打包交给OtgDriver::new;
  2. 注册中断处理:把embassy_usb_synopsys_otg::on_interrupt接到芯片的 USB 中断向量上(InterruptHandler<T>中on_interrupt_impl(r, &T::state()));
  3. 提供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.rsDWC2 内核的寄存器与位域定义(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_detectionboolfalse是否启用 VBUS 检测。USB 规范要求设备监测 USB 线缆的插拔;若设备由总线供电(bus-powered,即从 VBUS 取电),拔线后设备本来就断电,可以始终假定"线缆已插入",因此可不检测;若设备自供电(self-powered,拔线后仍保持供电),则必须置true,否则设备在拔线后仍会以为处于连接状态。置true时硬件上需要把 VBUS 分压后接入检测引脚
vbus_valid_overrideboolfalseVBUS 检测的软件覆盖。当vbus_detection关闭时,允许把分压后的 VBUS 接到任意数字输入引脚,由软件控制vbvaloven/vbvaloval(VBUS 有效覆盖)、avaloen/avaloval(A 外设会话有效覆盖)、bvaloen/bvaloval(B 外设会话有效覆盖)这几组寄存器位
xcvrdlyboolfalse收发器延时。某些 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接受三个参数:

  1. ep_out_buffer: &'d mut [u8]——内部临时缓冲区,用于暂存收到的 OUT 包,必须能容纳所有 OUT 端点的最大包长之和,否则端点分配会失败(对应EndpointAllocError);
  2. instance: OtgInstance<'d, M>——硬件相关的打包描述;
  3. 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 只需把它挂到中断向量上。它处理四类事件:

  1. 全局事件:wkupint(唤醒)、usbsusp(挂起)、usbrst(总线复位)、enumdne(枚举完成)、otgint(OTG 中断)、srqint(会话请求)——统一遮蔽 IN/OUT 端点与 RX FIFO 中断后唤醒Bus::poll;
  2. RX FIFO 非空(共享 RX FIFO,所有 OUT 端点共用):按grxstsp中的pktstsd分派SETUP_DATA_RX(8 字节 SETUP 包,从 FIFO 读出写入setup_data原子缓存)、OUT_DATA_RX(将数据拷入端点 OUT 缓冲、写入长度、唤醒 waker)、OUT_DATA_DONE等状态;
  3. IN 端点中断:清中断、处理 TXFE(TX FIFO 空)、唤醒对应 IN waker;
  4. OUT 端点中断:处理 STUP(SETUP 就绪)、唤醒 OUT waker;
  5. 不完整等时 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_stateHostStateStorage::as_host_state返回可复制的HostState<'d, M>视图
OtgInstanceOtgHostInstance打包regs、state、FIFO 深度与 PHY 类型
on_interrupton_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空无额外能力
defmtdep:defmt+embassy-usb-driver/defmt使用 defmt 格式日志(与log二选一)
logdep:log使用logcrate 日志
hostembassy-time启用 USB 主机模式支持(host.rs仅在此时编译)
embassy-timedep: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的做法。结合源码,一个最小集成需要完成:

  1. 定义Instancetrait,提供regs()(Otg寄存器指针)与state()(StateStorage的as_state());
  2. 在中断处理中调用embassy_usb_synopsys_otg::on_interrupt(r, &state)(设备模式)或on_host_interrupt(主机模式);
  3. 构造OtgInstance,填好fifo_depth_words(查参考手册的 OTG_FS/OTG_HS FIFO RAM 大小)、phy_type、tx_fifo_count、extra_rx_fifo_words与calculate_trdt_fn;
  4. 调用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.

项目地址:https://gitcode.com/gh_mirrors/em/embassy
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询