Rust in Everything
基于 embassy-executor 0.10.0

怎么读这一卷

主线位置:三条主线的总览。本页把定时器、中断、跨任务三条主线各拆成几步,标出每一步落在哪个函数、哪一页讲它;再说明三种实验办法和页面上的标注;最后讲执行器、time、stm32 三组 feature 各打开什么,哪些只能开一个。一次 Timer::after 和一次中断唤醒在 QEMU 上把前两条线逐步打点走一遍。

flowchart TB
  start[从这里开始] --> q{读这一卷为了什么}
  q -->|会用| use1["01 全景 再到 03 04"]
  q -->|读懂实现| all[00 到 11 按顺序]
  q -->|只关心执行器| ex["一次 Timer after 再到 02"]
  q -->|只关心 HAL| hal["一次中断唤醒 再到 05"]

锁定版本

embassy 的仓库里有几十个 crate,各自发版、各自打 tag,tag 名是 <crate>-v<版本>,例如 embassy-executor-v0.10.0。没有一个「embassy 1.x」那样的总版本号。本卷按 2026-10-29 一个新项目从 crates.io 实际解析到的版本锁定,版本表见卷首。要点:

项本卷说明
发布批次2026-03-20executor、time、time-driver、sync、hal-internal、embedded-hal、stm32、usb、net-driver-channel 的 tag 都指向 84444a19e(「chore: prepare embassy-hal-internal release」)
之后单独发的queue-utils 0.3.2(2026-04-21)、net 0.9.1(2026-04-16)、usb-driver 0.2.2(2026-05-28)都是补丁版本,新项目解析到的就是它们
更早的futures 0.1.2(2025-08-27)、net-driver 0.2.0(2023-10-18)、executor-timer-queue 0.1.0executor-timer-queue 没有 tag,按包里 .cargo_vcs_info.json 记的 a416aac9c 链接
crates.io 包与 tag源码相同executor 0.10.0 的包来自 ca966f8ff,time、embedded-hal 等来自 50c6aac5b,都在 84444a19e 之前一两个提交;三者之间只有两个 Cargo.toml 各差一行,见源码地图
芯片数据stm32-metapac 21.0.0由 stm32-data 生成,不在 embassy 仓库里;本卷只从 cargo 缓存读,引用时写「stm32-metapac 21.0.0 的 src/...」,不建链接
网络栈smoltcp 0.13.1链到 smoltcp 仓库的 tag v0.13.1

源码链接用文件所属 crate 的 tag:embassy-time/src/timer.rs 一律链到 embassy-time-v0.5.1,哪怕是在讲执行器的页上。2026-03-20 这一批大多指向同一个 commit,所以这条规矩在大多数文件上不改变内容,只在 queue-utils、net、usb-driver 这几个后发的 crate 上有区别。

仓库的 docs/ 下有一本 asciidoc 写的书,每个 crate 有 README.md 和 CHANGELOG.md。本卷把它们当资料读,引用之前在源码或实验里核实;对不上的地方会写出来。例如书的 runtime.adoc 说「Once a task yields, the executor enqueues the task at the end of the run queue」,而代码里返回 Pending 的任务根本不进运行队列,只有被唤醒的才进(wake_task),见一次 Timer::after。

三条主线

定时器主线:一次 Timer::after(100ms).await

flowchart TB
  m["main 宏展开"] --> run["Executor run"]
  run --> p1["第一次 poll"]
  p1 --> sw["schedule_wake"]
  sw --> q["进定时器队列"]
  q --> wfe["空闲 WFE"]
  wfe --> irq["闹钟中断"]
  irq --> wk["wake_task 加 SEV"]
  wk --> p2["再次 poll Ready"]
步骤源码讲它的页
1. #[embassy_executor::main] 在 Cortex-M 上展开成一个 cortex_m_rt::entry 的 fn main() -> !:在栈上建 Executor,用 transmute 把引用升成 'static,调 run,把 async fn main 当作第一个任务 spawn 进去(Flavor::Standard 分支)embassy-executor-macros/src/macros/main.rs宏
2. 每个 #[task] 函数背后是一个 static POOL(task.rs),类型是 TaskPool,里面是 pool_size 个 TaskStorage:一个 TaskHeader 加上 future 本身raw/mod.rs任务存储
3. Executor::run 是一个 loop { poll(); wfe };SyncExecutor::poll 一次取走整个运行队列(dequeue_all),逐个调 TaskStorage::pollplatform/cortex_m.rs、raw/run_queue.rs状态与运行队列、各平台的执行器
4. Timer::after 只算出到期时刻;第一次 poll 调 embassy_time_driver::schedule_wake,它经链接期符号 _embassy_time_schedule_wake 落到 time_driver_impl! 声明的那个驱动embassy-time/src/timer.rs、embassy-time-driver/src/lib.rsTimer、Ticker 与超时、时间驱动
5. 驱动把 waker 交给 Queue::schedule_wake;默认的集成队列用 TimerQueueItem::from_embassy_waker 从 waker 找回任务头里那块内存,把任务串进一条链表embassy-time-queue-utils/src/queue_integrated.rs、embassy-executor-timer-queue/src/lib.rs定时器队列
6. 队列有变化时,stm32 的 TIM 驱动把最早的到期时刻写进比较寄存器(set_alarm);本卷 QEMU 实验用的 SysTick 驱动没有闹钟,每个 tick 都检查一遍队列embassy-stm32/src/time_driver/gp16.rs时间驱动
7. 运行队列空了,run 执行 wfe,核心睡下platform/cortex_m.rs各平台的执行器
8. 闹钟中断里 next_expiration 叫醒到期的 waker → wake_task 置运行队列标志、入队 → 队列原来为空就调 __pender,线程模式执行器的 pender 就是一条 sevraw/mod.rs、raw/waker.rs、platform/cortex_m.rs状态与运行队列
9. wfe 返回,下一轮 poll 只 poll 被唤醒的任务;Timer::poll 看到时间已到,返回 Readyembassy-time/src/timer.rs一次 Timer::after

中断主线:一次 ExtiInput::wait_for_rising_edge().await

flowchart TB
  b["bind_interrupts"] --> v["向量 EXTI0"]
  n["ExtiInput new"] --> cfg["配 EXTI 线"]
  cfg --> reg["登记 waker"]
  reg --> pend["任务挂起"]
  v --> h["on_irq 屏蔽并唤醒"]
  pend --> h
  h --> r["再次 poll Ready"]
步骤源码讲它的页
1. bind_interrupts! 为每个列出的中断生成一个 #[unsafe(no_mangle)] extern "C" fn EXTI0(),依次调各处理函数的 Handler::on_interrupt,并为用户的结构体实现 Bindingembassy-stm32/src/lib.rs、embassy-hal-internal/src/interrupt.rs中断绑定
2. ExtiInput::new 要一个 impl Binding<..., InterruptHandler<...>> 参数,没有绑定就编不过;NVIC 里的 EXTI 中断早在 embassy_stm32::init 里由 exti::init 全部打开exti/mod.rsGPIO 与 EXTI
3. wait_for_rising_edge 新建一个 future,configure_and_enable_exti 选端口、选边沿、清挂起位、打开这条线的中断屏蔽位(IMR);poll 把 waker 登记进这条线的 AtomicWaker,IMR 还开着就返回 Pendingexti/mod.rs、exti/low_level.rsGPIO 与 EXTI、等待队列
4. 中断到来,on_irq 读挂起寄存器,把触发了的线在 IMR 里关掉,唤醒这些线的 waker,再清挂起位exti/mod.rsGPIO 与 EXTI
5. 唤醒走的是和定时器主线同一条路:wake_task → 入队 → sevraw/mod.rs状态与运行队列
6. 任务再被 poll,看到 IMR 里这条线已经被关掉,就知道事件发生过,返回 Readyexti/mod.rs一次中断唤醒

跨任务主线:一次 Channel::send / receive

步骤源码讲它的页
1. Channel 的全部状态在一个 Mutex<M, RefCell<ChannelState<T, N>>> 里,M 是一个 RawMutex:关中断、什么都不做,或者只允许线程模式channel.rs、blocking_mutex/原始互斥
2. Channel::send 返回 SendFuture,它每次 poll 都在锁里调 try_send_with_context:放得进就唤醒接收方,放不进就登记发送方的 wakerchannel.rsChannel
3. Channel::receive 的 future 落到 ChannelState::poll_receive:队列满时先唤醒发送方,取到就 Ready,取不到就登记接收方的 wakerchannel.rsChannel
4. 两边的 waker 各存在一个 WakerRegistration 里,只放得下一个;换成另一个任务的 waker 时,先把旧的唤醒waitqueue/waker_registration.rs等待队列
5. 中断处理函数里不能 .await,用 try_send,满了就得自己决定丢掉还是覆盖channel.rsChannel

三条线最后都汇到同一个地方:某个 Waker 被调用,wake_task 把任务挂进运行队列,pender 叫醒执行器。叶子 future 不同(Timer、ExtiInputFuture、SendFuture),叫醒它们的东西不同(时间驱动的中断、外设中断、另一个任务),执行器那一半是同一套。

三种实验办法

办法能回答什么不能回答什么
std:platform-std 执行器 + embassy-time 的 std 驱动执行器、embassy-sync、embassy-futures、embassy-net(两个栈用 embassy-net-driver-channel 接起来)的逻辑:唤醒顺序、谁先谁后、报错原文中断、WFE、真正的外设;std 执行器睡在条件变量上(platform/std.rs 的 __pender 是 notify_one),计时受宿主机负载影响
QEMU:qemu-system-arm -M b-l475e-iot01a(STM32L475VG,Cortex-M4)线程模式执行器、WFE / SEV、NVIC、SysTick、EXTI(SWIER 软件触发)、GPIO、RCC;用 -icount 得到确定的指令数TIM、DMA、PWR、FLASH 都是未实现设备;USART 发送没有跑通;指令数不是周期数,WFE 的功耗更无从谈起
只交叉编译:thumbv7em-none-eabihf、thumbv6m-none-eabi、riscv32imac-unknown-none-elf代码体积、任务池在 .bss 里占多少(llvm-size、llvm-nm)、build.rs 生成的代码、编译错误任何运行期行为

QEMU 实验的模板做了两处改动,页面上凡是用到 QEMU 的结果都会写明:

  • embassy-stm32 打了一行补丁。 QEMU 没有给 PWR 建模(读出来恒为 0),embassy_stm32::init 在 rcc/bd.rs 里的 while !cr.read().dbp() {} 永远等不到。实验用 [patch.crates-io] 换成一份注释掉这一行的 embassy-stm32,其他和 0.6.0 逐字相同。这也是一个现成的例子:HAL 假设硬件会回应,硬件不回应时就是死等。
  • 时间驱动是手写的 SysTick 驱动。 embassy-stm32 自带的时间驱动用 TIM,QEMU 里没有 TIM。实验用一个 1 kHz 的 SysTick 驱动:embassy-time-driver 的 Driver 实现加 embassy-time-queue-utils 的 Queue,每个 tick 进一次中断。它和 TIM 驱动的差别见时间驱动。

输出走 semihosting(hprintln!),程序用 debug::exit 结束 QEMU。页面上的标注是:

  • std:M2 Max、macOS 27、rustc 1.99.0,写明 debug 或 release;计时取多次里的最小值并注明负载。
  • QEMU:QEMU 11.1.2 b-l475e-iot01a,加上上面两条;时间要么用 SysTick 的 tick,要么用 -icount shift=N 下的指令数。QEMU 的墙钟时间不代表硬件时间。
  • 交叉编译:目标三元组和 profile。

feature 各打开什么

embassy-executor

feature打开什么规则
platform-cortex-m / -cortex-ar / -riscv32 / -std / -wasm / -avr / -spin选 src/platform/ 下的一个文件,提供 Executor、__pender 和对应的 main 宏最多一个(check_at_most_one!);一个都不开时 main 是 main_unspecified,要在宏参数里自己写 entry = ... 和 executor = ...
executor-thread线程模式执行器(Cortex-M 上 WFE / SEV,其他嵌入式平台 WFI)和下一个可以同时开
executor-interruptInterruptExecutor:借一个空闲中断当调度器,只在 Cortex-M 上有
scheduler-priority / scheduler-deadline0.10.0 新增:运行队列换成按优先级、按截止时间排序的链表可以同时开,先比优先级再比截止时间
trace在 poll、入队、空闲等处调 _embassy_trace_* 七个外部函数七个都要自己定义;std 上只开 trace 不定义,链接报 Undefined symbols 列出全部七个
rtos-trace接 rtos-trace 框架,顺带开 metadata-name
metadata-name任务元数据里多一个名字
defmt / log内部日志不能同时开(fmt.rs 里的 compile_error!)

本卷的实验用 platform-cortex-m + executor-thread + executor-interrupt(QEMU)和 platform-std + executor-thread(std);打点实验另外开了 trace。

📝NOTE
冷知识:platform-* 在 0.10.0 才叫这个名字,CHANGELOG 没写

0.9.1 里这组 feature 叫 arch-std、arch-cortex-m、arch-cortex-ar……#5099「executor: rename arch-* to platform-*」在 2026-03-12 合并,首次发布于 embassy-executor 0.10.0;其中改名的提交 ed661a3c8 一次改了 100 个文件,各个 HAL、例子、测试的 Cargo.toml 一起改。0.10.0 的 CHANGELOG 段落标着 2026-03-10,比这次合并早两天,里面没有这一条,反而还写着「The feature name arch-cortex-ar remains the same」和「Added run_until to arch-std variant of Executor」。旧名字没有留别名,照着旧文档写 arch-std 的结果(intro-std-arch):

error: failed to select a version for `embassy-executor`.
...
package `intro-std-arch` depends on `embassy-executor` with feature `arch-std` but `embassy-executor` does not have that feature.
help: available features: _any_trace, _platform, defmt, embassy-time-driver, executor-interrupt, executor-thread, log, metadata-name, nightly, platform-avr, platform-cortex-ar, platform-cortex-m, platform-riscv32, platform-spin, platform-std, platform-wasm, rtos-trace, scheduler-deadline, scheduler-priority, trace, turbowakers

宏那边也没跟上。embassy-executor-macros 0.8.0 和 executor 0.10.0 是同一个 commit 发的,可是一个平台都不开时,#[embassy_executor::main] 的报错还在叫你去开 arch-*(intro-std-arch 去掉 platform-std,debug):

error: No architecture selected for embassy-executor. Make sure you've enabled one of the `arch-*` features in your Cargo.toml.
       
       Alternatively, if you would like to use a custom executor implementation, specify it with the `executor` argument.
       For example: `#[embassy_executor::main(entry = ..., executor = "some_crate::Executor")]

embassy-time 与 embassy-time-driver

feature打开什么规则
tick-hz-*(embassy-time-driver 上有 161 个,embassy-time 转发)选一个 TICK_HZ 常量;tick.rs 是 gen_tick.py 生成的,每个 feature 一行 #[cfg] 的 pub const最多一个;一个都不开时默认 1 MHz
std / wasm / mock-driverembassy-time 自带的三个时间驱动,各自顺带开 tick-hz-1_000_000 和 embassy-time-queue-utils和别的时间驱动不能共存:两个驱动定义同一个符号 _embassy_time_now
generic-queue-8 … generic-queue-128换成固定容量的通用定时器队列,不再依赖 embassy 执行器最多一个;都不开时用集成队列
defmt-timestamp-uptime-*defmt 日志前加时间戳最多一个

tick-hz-* 开两个时,编不过的是 embassy-time-driver(intro-std-arch 里 std 已经带了 1 MHz,再加 tick-hz-32_768,debug):

error[E0428]: the name `TICK_HZ` is defined multiple times
  --> .../embassy-time-driver-0.2.2/src/tick.rs:86:1
   |
54 | pub const TICK_HZ: u64 = 32_768;
   | -------------------------------- previous definition of the value `TICK_HZ` here
...
86 | pub const TICK_HZ: u64 = 1_000_000;
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `TICK_HZ` redefined here

这条规矩没有专门的检查,靠的是同名常量定义两次。feature 在整个依赖图里统一,所以库 crate 的文档都说「you should not enable any tick-* feature」:选 tick 率是最终的二进制 crate 或时间驱动的事。tick 率怎么影响换算和取整,见 Instant 与 Duration。

embassy-stm32

feature打开什么规则
芯片名(本卷 stm32l475vg)build.rs 按它从 stm32-metapac 里选一份芯片元数据,生成外设单例、中断表、引脚表恰好一个:没有时 build.rs panic「No stm32xx Cargo feature enabled」,多个时「Multiple stm32xx Cargo features enabled」
time-driver-any / time-driver-tim2 / … / time-driver-lptim1 …用某个 TIM(或 LPTIM)实现时间驱动,顺带开 time、embassy-time-driver、embassy-time-queue-utils最多一个(build.rs:「Multiple time-driver-xxx Cargo features enabled」);开了就不能再自己写时间驱动
time依赖 embassy-time,打开驱动里要用时间的 API(超时等)
memory-x按芯片元数据生成链接脚本用的 memory.x不开就得自己提供
exti编译 exti 模块,init 里打开所有 EXTI 中断
rt(默认)stm32-metapac/rt:中断向量表
unstable-pac把 stm32-metapac 导出成 embassy_stm32::pac文档写明它不受 semver 保护;本卷的 EXTI 实验用它直接写 SWIER
executor-thread / executor-interruptembassy-stm32 自己的低功耗执行器,会开 low-power 和 time和 embassy-executor 的同名 feature 不是一回事

本卷 QEMU 模板开的是 stm32l475vg、memory-x、exti、unstable-pac,没有开任何 time-driver-*,时间驱动由实验程序自己提供。

阅读路线

前置知识

  • Rust 的 Future、Poll、Waker、Pin;async fn 编译成状态机。tokio 卷的 poll 与唤醒 讲的是同一套接口在有操作系统时怎么用。
  • Cargo feature 怎么在依赖图里统一(上面 tick-hz-* 的报错就是它的后果)。
  • Cortex-M 的基本概念:线程模式和处理模式、NVIC、中断向量表(vector table)、SysTick、WFE / SEV / WFI。
  • 外设寄存器的读写:知道「写 1 清零」「屏蔽位」是什么意思,读 05 会快很多。

小结

  • embassy 没有总版本号,本卷锁 2026-03-20 那一轮(多数 tag 指向 84444a19e)加之后的三个补丁版本;源码链接用文件所属 crate 的 tag。
  • 定时器主线:main 宏 → 静态 TaskPool → run 循环 → Timer::poll → schedule_wake → 定时器队列 → 硬件闹钟 → WFE → 中断里 wake_task + sev → 再次 poll。
  • 中断主线:bind_interrupts! 生成向量函数和 Binding → ExtiInput::new 要求绑定 → 配线、登记 waker、开 IMR → on_irq 关 IMR、唤醒 → poll 看到 IMR 已关。
  • 跨任务主线:Channel 的状态在 Mutex<M, RefCell<...>> 里;满 / 空时把 waker 登记进只有一个位置的 WakerRegistration。
  • 三种实验办法:std 看逻辑,QEMU 看中断和 WFE(PWR 补丁、SysTick 驱动、-icount 指令数),交叉编译看体积。
  • platform-* 最多一个(0.10.0 从 arch-* 改名,CHANGELOG 没写);tick-hz-* 最多一个,开两个报 E0428;stm32 的芯片 feature 恰好一个,time-driver-* 最多一个。

💬 关联讨论

发起讨论
加载中...