Rust in Everything
基于 embassy-executor 0.10.0

embassy 源码解析

embassy 是 Rust 嵌入式的 async 框架:没有堆,没有线程,任务在编译期静态分配;任务等外设时挂起,中断把它叫醒;没有任务要跑时,CPU 执行 WFE 睡眠。仓库根目录的 README 这样介绍它:「Tasks get transformed at compile time into state machines that get run cooperatively. It requires no dynamic memory allocation and runs on a single stack, so no per-task stack size tuning is required.」这几句话分别落在哪段代码上,和轮询主循环、RTOS、RTIC、tokio 有什么不同,见 embassy 解决什么问题。

锁定的版本

embassy 是一个仓库里几十个 crate,每个 crate 各自发版、各自打 tag(<crate>-v<版本>)。本卷按一个新项目从 crates.io 实际解析到的版本锁定,核心 crate 都是 2026-03-20 这一轮发布的:

crate版本tagtag 指向
embassy-executor / embassy-executor-macros0.10.0 / 0.8.0embassy-executor-v0.10.0 / embassy-executor-macros-v0.8.084444a19e(2026-03-20)
embassy-executor-timer-queue0.1.0没有 tag,按发布 commit a416aac9c2025-08-25
embassy-time / embassy-time-driver0.5.1 / 0.2.2embassy-time-v0.5.1 / embassy-time-driver-v0.2.284444a19e
embassy-time-queue-utils0.3.2embassy-time-queue-utils-v0.3.258979cace(2026-04-21)
embassy-sync0.8.0embassy-sync-v0.8.084444a19e
embassy-futures0.1.2embassy-futures-v0.1.23e8d8fec1(2025-08-27)
embassy-hal-internal / embassy-embedded-hal0.5.0 / 0.6.0各自的 tag84444a19e
embassy-stm320.6.0embassy-stm32-v0.6.084444a19e;芯片数据来自 stm32-metapac 21.0.0
embassy-net0.9.1embassy-net-v0.9.19328e65e0(2026-04-16);底层 smoltcp 0.13.1(tag v0.13.1)
embassy-net-driver / embassy-net-driver-channel0.2.0 / 0.4.0各自的 tag35ffdf214(2023-10-18)/ 84444a19e
embassy-usb / embassy-usb-driver0.6.0 / 0.2.2各自的 tag84444a19e / 664d4ead3(2026-05-28)

源码链接一律指向文件所属 crate 的 tag,例如 embassy-time 的文件链到 embassy-time-v0.5.1,哪怕是在执行器的页面上。crates.io 上的包和 tag 是同一份源码,只有两个 Cargo.toml 各差一行,其中一行让新项目里出现两个版本的 embassy-hal-internal,见源码地图。仓库从 2020-09-22 开始,到 84444a19e 共 13720 个提交;之后 main 又走了七个月,executor、time、sync 三个目录有 159 个提交,还没有发版,本卷不跟。

讲什么

  • 核心:embassy-executor(任务存储、运行队列、spawner、各平台的执行器、宏、0.10.0 新增的优先级 / EDF 调度),embassy-time 一家(Instant / Duration、驱动接口、定时器队列、Timer / Ticker),embassy-sync(原始互斥、等待队列、channel、signal / watch / pubsub、async mutex),embassy-futures。
  • HAL:只讲 embassy-stm32 一个:外设单例、中断绑定、stm32-metapac 与 build.rs 生成的代码、init 与时钟、GPIO 与 EXTI、USART 与 DMA。embassy-hal-internal 和 embassy-embedded-hal 在相应的页里讲。embassy-nrf、embassy-rp 只在设计取舍页对照。
  • 网络和 USB:embassy-net(smoltcp 之上的 async 栈)、embassy-usb(设备栈和类)。
  • 不讲:bootloader(embassy-boot)、各 WiFi / 以太网芯片驱动(cyw43、esp-hosted、wiznet 等)、stm32-wpan、其他 HAL、USB host。

三条主线

全卷沿三条主线展开,每页开头标出自己在哪条线上:

  • 定时器主线(一次 Timer::after(100ms).await):任务第一次被 poll,Timer 把到期时刻和 waker 交给时间驱动 → 驱动把任务挂进定时器队列、设好硬件闹钟 → 执行器无事可做,WFE 睡眠 → 闹钟中断 → 队列里到期的任务被唤醒,进运行队列 → 再次 poll,Ready。
  • 中断主线(一次 ExtiInput::wait_for_rising_edge().await):bind_interrupts! 把 EXTI 中断向量接到 embassy-stm32 的处理函数 → 驱动配置 EXTI 线、登记 waker、打开中断 → 任务挂起 → 中断到来,处理函数屏蔽这条线、唤醒任务 → 任务被 poll,看到线已被屏蔽,返回 Ready。
  • 跨任务主线(一次 Channel::send / receive):两个任务经 embassy-sync 的 Channel 通信,RawMutex 保护内部状态,满 / 空时登记 waker,对端操作后唤醒。

每条线的每一步落在哪个函数、哪一页讲,见怎么读这一卷。

flowchart TB
  intro[00 导读] --> overview[01 全景]
  overview --> exec[02 执行器]
  exec --> time[03 时间]
  time --> sync[04 同步]
  sync --> hal["05 HAL stm32"]
  hal --> net[06 网络]
  hal --> usb[07 USB]
  net --> design[08 设计取舍]
  usb --> design
  design --> pit[09 常见坑]
  pit --> site[10 在本站中]
  site --> hands[11 动手]

实验环境

本机没有开发板。关于行为的说法(一次唤醒经过哪些函数、各步多少条指令、报什么错)都配一个小程序和它的真实输出,用三种办法之一:

  1. std 上跑:platform-std 执行器加 embassy-time 的 std 时间驱动,平台是 M2 Max、macOS 27、rustc 1.99.0。
  2. QEMU 仿真 Cortex-M4:QEMU 11.1.2 的 b-l475e-iot01a(STM32L475VG)。QEMU 没有给 PWR 建模,embassy_stm32::init 会在 rcc/bd.rs 的 while !cr.read().dbp() {} 死等,所以实验用的 embassy-stm32 注释掉了这一行,其余和 0.6.0 相同;QEMU 也没有 TIM,时间驱动换成一个手写的 1 kHz SysTick 驱动。凡是 QEMU 上的结果都会写明这两点。指令数用 -icount 得到,可重复;它不是真硬件上的周期数。
  3. 只交叉编译:thumbv7em-none-eabihf 等目标,看代码体积、.bss 里的任务池、build.rs 生成的代码。

三种办法分别能回答什么、不能回答什么,见怎么读这一卷。

各部分

  1. 00 导读:锁定的版本和 tag 规则;三条主线每一步落在哪;三种实验办法与 QEMU 的限制;各 crate 的 feature,哪些互斥;crate 依赖图(cargo tree 核实)和规模。
  2. 01 全景:裸机上的几种并发写法和 embassy 的位置;一次 Timer::after、一次中断唤醒在 QEMU 上逐步打点走一遍。
  3. 02 执行器:任务存储、状态与运行队列、Spawner、各平台的执行器、宏、调度策略。
  4. 03 时间:Instant 与 Duration、时间驱动、定时器队列、Timer / Ticker 与超时。
  5. 04 同步:原始互斥、等待队列、Channel、Signal / Watch / PubSub、async Mutex 与 Semaphore、embassy-futures。
  6. 05 HAL:embassy-stm32:外设单例、中断绑定、芯片数据与代码生成、init 与时钟、GPIO 与 EXTI、USART 与 DMA。
  7. 06 网络:Stack 与 Runner、驱动接口、TCP 与 UDP 套接字。
  8. 07 USB:设备栈、类。
  9. 08 设计取舍:静态任务、中断唤醒、链接期符号做插件、RawMutex 类型参数、代码生成覆盖上千款芯片,和 tokio、RTIC 对照。
  10. 09 常见坑:报错原文做标题,每个都先跑出最小复现。
  11. 10 在本站中:把本站两个 tokio 后台定时循环搬到 embassy 的 std 执行器上对照。
  12. 11 动手:不依赖 embassy,约 300 行写一个静态任务池加 SysTick 定时器的 mini 执行器。

适合谁读

  • 写过裸机 C 或 RTOS、想知道 async 在没有线程的芯片上怎么跑起来的读者:从导读开始,再读 01 全景 的三页。
  • 读过 tokio 卷、想看同一套 Future / Waker 换到没有操作系统的环境里变成什么样的读者:先读 embassy 解决什么问题 和一次 Timer::after,最后读 08 设计取舍。
  • 需要的前置知识:Rust 的 Future、Poll、Waker;Cargo feature;知道中断、中断向量表、外设寄存器是什么。

💬 关联讨论

发起讨论
加载中...