Rust in Everything
基于 embassy-executor 0.10.0

源码地图

主线位置:三条主线经过的 crate 和它们之间的边。定时器主线横跨 executor、time、time-driver、time-queue-utils、executor-timer-queue 五个 crate 和一个 HAL;中断主线在 stm32、hal-internal、sync 里;跨任务主线只在 sync 里。本页说明这些 crate 谁依赖谁、靠什么接起来、每个从哪个文件读起。
flowchart TB
  app["应用 二进制 crate"] --> exec[executor]
  app --> time[time]
  app --> stm32[stm32]
  exec --> tq["executor-timer-queue"]
  exec --> macros["executor-macros"]
  time --> drv["time-driver"]
  stm32 --> drv
  stm32 --> qu["time-queue-utils"]
  qu --> tq
  stm32 --> hi["hal-internal"]
  stm32 --> sync[sync]
  stm32 --> ehal["embedded-hal"]
  ehal --> sync
  ehal --> futs[futures]
  net[net] --> sync
  net --> time
  usb[usb] --> sync
  usb --> time

图里没有从 executor 到 time 的边,也没有反过来的边;stm32 到 time-driver、time-queue-utils 的边只在开了 time-driver-* 时才有。下面逐条核实。

分层与依赖方向

依赖关系按两个实验项目实际解析出来的看(都是 cargo tree --offline -e normal,只看正常依赖):intro-std(std 模板的拷贝:executor、time、sync、futures、net、usb、embedded-hal)和 intro-qemu(QEMU 模板的拷贝:stm32 stm32l475vg、executor platform-cortex-m、time、time-driver、time-queue-utils)。各 crate 的直接 embassy 依赖:

crate直接依赖的 embassy crate其他值得一提的依赖
embassy-executor 0.10.0embassy-executor-macros、embassy-executor-timer-queuecordyceps(运行队列用的侵入式链表)、critical-section;cortex-m 只在 platform-cortex-m 时
embassy-time 0.5.1embassy-time-driver;std / wasm / mock-driver 时加 embassy-time-queue-utilsembedded-hal 0.2 和 1.0、embedded-hal-async(实现 DelayNs)
embassy-time-driver 0.2.2无无(只有生成文档用的 document-features)
embassy-time-queue-utils 0.3.2embassy-executor-timer-queueheapless(通用队列)
embassy-executor-timer-queue 0.1.0无无
embassy-sync 0.8.0无critical-section、heapless、futures-core、embedded-io-async
embassy-futures 0.1.2无无
embassy-hal-internal 0.5.0无cortex-m、critical-section、num-traits
embassy-embedded-hal 0.6.0embassy-futures、embassy-sync、embassy-hal-internal 0.4.0embedded-hal 各版本
embassy-stm32 0.6.0embassy-hal-internal 0.5.0、embassy-sync、embassy-futures、embassy-embedded-hal、embassy-net-driver、embassy-usb-driver、embassy-usb-synopsys-otgstm32-metapac 21.0.0、cortex-m、cortex-m-rt
embassy-net 0.9.1embassy-net-driver、embassy-sync、embassy-timesmoltcp 0.13.1
embassy-usb 0.6.0embassy-usb-driver、embassy-sync、embassy-futures、embassy-time、embassy-net-driver-channelusbd-hid

从表里能直接读出三条:

  • futures 和 sync 不依赖执行器。 两个 crate 都只认 core::task::Waker,用在 tokio 下、裸 block_on 下都可以。sync 的 Channel、Mutex、Signal 这些类型都带一个 M: RawMutex 类型参数,由用户决定靠什么保证互斥,见原始互斥。

  • executor 不依赖 time,time 也不依赖 executor。 在 intro-qemu 里反查谁依赖执行器,只有应用自己:

    embassy-executor v0.10.0
    └── intro-qemu v0.1.0 (~/exp-embassy/intro-qemu)
    
    它们之间的接口放在两个小 crate 里,靠链接期符号配对,下一节讲。所以 Cargo 层面互不依赖,链接时却少不了对方:集成定时器队列要调执行器定义的符号,queue-utils 的文档写的是「It will fail to build if you try to use it without an embassy executor in the project」。
  • HAL 实现时间驱动,但图里照样有 embassy-time。 embassy-stm32 的 time-driver-* 打开的是内部 feature _time-driver = ["dep:embassy-time-driver", "time", "dep:embassy-time-queue-utils"]:驱动本身(RtcDriver)只实现 embassy-time-driver 的 Driver trait、用 embassy-time-queue-utils 的 Queue,但这个 feature 同时打开 time,也就依赖了 embassy-time。intro-qemu 没开 time-driver-*,embassy-time 照样被 embassy-usb-synopsys-otg 0.3.3 带进来:

    embassy-time v0.5.1
    ├── embassy-usb-synopsys-otg v0.3.3
    │   └── embassy-stm32 v0.6.0 (~/exp-embassy/qemu-patched/embassy-stm32)
    │       └── intro-qemu v0.1.0 (~/exp-embassy/intro-qemu)
    └── intro-qemu v0.1.0 (~/exp-embassy/intro-qemu)
    

两个小 crate,四个链接期符号

executor 和 time 不互相依赖,却要合作完成定时器主线:time 要从一个 Waker 找到任务里存定时器队列项的那块内存,executor 要知道被唤醒后怎么叫醒自己。embassy 的做法是把接口拆成两个几乎没有代码的 crate,再用 extern "Rust" 声明、由另一方 #[unsafe(no_mangle)] 定义的符号接起来,链接器负责配对:

符号谁声明(调用方)谁定义做什么
_embassy_time_now / _embassy_time_schedule_wakeembassy-time-driver 的 now()、schedule_wake()时间驱动,用 time_driver_impl!:embassy-stm32 的 TIM 驱动、embassy-time 的 std 驱动,或者本卷 QEMU 实验里手写的 SysTick 驱动读时间;登记「到 at 时叫醒这个 waker」
__embassy_time_queue_item_from_wakerembassy-executor-timer-queue 的 TimerQueueItem::from_embassy_wakerembassy-executor:从 waker 的数据指针还原 TaskRef,返回任务头里的 timer_queue_item集成定时器队列把任务串成链表,不用另外分配节点
__penderembassy-executor 的 Pender::pend平台模块,例如 Cortex-M 的 __pender运行队列由空变非空时叫醒执行器

embassy-executor-timer-queue 整个 crate 104 行,只定义一个不透明的 TimerQueueItem:[usize; ITEM_WORDS],按 8 字节对齐。ITEM_WORDS 由 timer-item-size-{4,6,8}-words 三个 feature 决定,都不开时是 0。executor 依赖它时不开任何一个;开的是队列的实现者 embassy-time-queue-utils(features = ["timer-item-size-6-words"])。feature 在依赖图里统一,所以「每个任务头里多大一块定时器内存」由图里有没有 queue-utils 决定,executor 自己并不知道。

这一点可以直接量(intro-xc:只依赖 executor 和 cortex-m 的空程序,一个什么都不做的任务;thumbv7em-none-eabihf,release,llvm-nm -S):

features: ''
20000000 00000018 b pool::__embassy_main::POOL
20000018 00000018 b pool::idle::POOL
features: '--features queue'
20000000 00000030 b pool::__embassy_main::POOL
20000030 00000030 b pool::idle::POOL

--features queue 只是多依赖了一个 embassy-time-queue-utils,代码里一行都没用它,每个任务的存储就从 24 字节变成 48 字节:多出的 24 字节正好是 6 个 32 位字。任务头里还有什么,见任务存储;队列怎么用这 24 字节,见定时器队列。

📝NOTE
冷知识:定时器队列项在 0.9.0 才搬出执行器

#4550「Make timer queue item opaque, move to new crate」2025-08-25 合并,首次发布于 embassy-executor 0.9.0 和 embassy-time-queue-utils 0.3.0。在这之前,TimerQueueItem 定义在 executor 的 raw/timer_queue.rs 里,字段是明的:next: Cell<Option<TaskRef>>、expires_at: Cell<u64>,外加一个由 timer-item-payload-size-{1,2,4,8} 决定大小的不透明 payload。队列实现只能用这几个固定字段,executor 每改一次布局,所有队列实现都得跟着发版。#4550 之后 executor 只提供一块按字数配置的原始内存,布局完全交给队列实现(queue_integrated.rs 里的 QueueItem 是 next、expires_at、waker 三个字段)。0.9.0 的 CHANGELOG 记的就是这几条:「Added extern "Rust" fn __embassy_time_queue_item_from_waker」「Moved the TimeQueueItem struct and timer-item-payload-size-* features (as timer-item-size-X-words) into embassy-executor-timer-queue」。embassy-executor-timer-queue 的文档写明了用意:「to decouple the timer queue's release cycle from embassy-executor」。

新项目里的两个 embassy-hal-internal

intro-std 和 intro-qemu 里都有一个 0.4.0 的 embassy-hal-internal:

embassy-hal-internal v0.4.0
└── embassy-embedded-hal v0.6.0
    └── embassy-stm32 v0.6.0 (~/exp-embassy/qemu-patched/embassy-stm32)
        └── intro-qemu v0.1.0 (~/exp-embassy/intro-qemu)

带进来的是 embassy-embedded-hal 0.6.0,而 tag embassy-embedded-hal-v0.6.0 上的 Cargo.toml 写的是 embassy-hal-internal = { version = "0.5.0", ... }。原因在发布顺序:crates.io 上的 0.6.0 包是从 50c6aac5b 打的(包里 .cargo_vcs_info.json 记的 commit),那时这一行还是 0.4.0;之后 84444a19e「chore: prepare embassy-hal-internal release」把 embassy-embedded-hal、embassy-stm32 和另外八个 crate(nrf、rp、mspm0 等 HAL)里的这一行一起改成 0.5.0,embassy-embedded-hal 的 tag 也打在这个 commit 上,包却没有重新发:本机 2026-10-29 的 crates.io 索引缓存里,embassy-embedded-hal 最新的仍是 0.6.0。另一处包与 tag 的差别是 executor 的 Cargo.toml:包里可选依赖 embassy-time-driver 写 0.2.1,tag 上写 0.2.2,解析结果一样。

后果有限:embassy-embedded-hal 只在共享 SPI 总线的实现里用了 hal-internal 的 OnDrop,公开 API 里没有 hal-internal 的类型,两个版本不会在类型上撞车;代价是多编译一份 crate。

每个 crate 从哪读起

行数是 tag 上 src/ 下所有 .rs 文件的行数(含 #[cfg(test)] 模块,不含 tests/、build.rs),括号里是 crate 目录下全部 .rs:

crate入口先读src/ 行数
embassy-executorsrc/lib.rs:check_at_most_one!、按 platform-* 选 platform/ 下的文件raw/mod.rs(TaskHeader、TaskStorage、SyncExecutor)、raw/run_queue.rs、platform/cortex_m.rs3258(3998)
embassy-executor-macrossrc/lib.rs:#[task]、#[main] 的入口macros/task.rs、macros/main.rs839
embassy-executor-timer-queuesrc/lib.rs(只有这一个文件)TimerQueueItem104
embassy-timesrc/lib.rstimer.rs、instant.rs、duration.rs、driver_std.rs1615
embassy-time-driversrc/lib.rs:Driver trait、time_driver_impl!tick.rs 是生成的(488 行全是 #[cfg] 常量)666
embassy-time-queue-utilssrc/lib.rs:按 feature 选队列实现queue_integrated.rs、queue_generic.rs350
embassy-syncsrc/lib.rsblocking_mutex/、waitqueue/、channel.rs8911(9048)
embassy-futuressrc/lib.rsselect.rs、join.rs、block_on.rs1246
embassy-hal-internalsrc/lib.rsinterrupt.rs(interrupt_mod!、Handler、Binding)、peripheral.rs、macros.rs2229(2330)
embassy-embedded-halsrc/lib.rsshared_bus/1910
embassy-stm32src/lib.rs:bind_interrupts!、Config、initbuild.rs(生成代码)、exti/、gpio.rs、time_driver/、rcc/83273(86129)
embassy-netsrc/lib.rs:Stack、Runner、newtcp.rs、udp.rs3927
embassy-net-driver / -channelsrc/lib.rsDriver trait;channel 的 State、Runner217 / 680
embassy-usb / -driversrc/lib.rsbuilder.rs、control.rs、class/7323(7419)/ 896

embassy-stm32 一个 crate 的 src/ 是表里其他 crate 加起来(34171 行)的 2.4 倍,而它的 build.rs 还会在编译时再生成一大批代码(外设单例、中断表、引脚表),见芯片数据与代码生成。

仓库里不讲的部分

84444a19e 上仓库顶层有 48 个目录,本卷不讲的各一句:

  • embassy-boot、embassy-boot-nrf、embassy-boot-rp、embassy-boot-stm32、embassy-usb-dfu:bootloader 和固件升级,README 的说法是掉电安全,带试启动和回滚。
  • cyw43、cyw43-pio、cyw43-firmware、embassy-net-adin1110、embassy-net-enc28j60、embassy-net-esp-hosted、embassy-net-nrf91、embassy-net-wiznet:具体网卡 / WiFi 芯片的 embassy-net-driver 实现。
  • embassy-net-ppp、embassy-net-tuntap:PPP 和 Linux tun/tap 驱动(tuntap 只在 Linux 上能跑,本机没有)。
  • embassy-nrf、embassy-rp、embassy-mspm0、embassy-imxrt、embassy-mcxa、embassy-microchip、embassy-nxp、embassy-stm32-wpan:其他 HAL 和 STM32WB 的无线协议栈;nrf、rp 在 08 设计取舍里对照。
  • embassy-usb-synopsys-otg:Synopsys OTG 控制器的 USB 驱动,embassy-stm32 依赖它,在 07 USB 里提一句;embassy-usb-logger:把 log 的输出送到 USB 串口。
  • examples/、tests/、docs/:按芯片家族分的例子、按芯片家族分的测试(如 tests/stm32)、那本 asciidoc 书。

小结

  • futures、sync 只认 Waker,不依赖执行器;sync 的互斥策略由 M: RawMutex 类型参数决定。
  • executor 和 time 互不依赖,靠 embassy-time-driver(_embassy_time_now、_embassy_time_schedule_wake)和 embassy-executor-timer-queue(__embassy_time_queue_item_from_waker)两个小 crate 加链接期符号接起来;执行器自己靠 __pender 被叫醒。
  • 任务头里的定时器队列项大小由图里有没有 embassy-time-queue-utils 决定:没有时每个任务 24 字节,有时 48 字节。
  • embassy-stm32 实现时间驱动,但 time-driver-* 也会打开 time;而且 embassy-usb-synopsys-otg 总会把 embassy-time 带进来。
  • 新项目里有两个 embassy-hal-internal,因为 crates.io 上的 embassy-embedded-hal 0.6.0 是在把依赖改成 0.5.0 之前发的;只用了 OnDrop,不会撞类型。

💬 关联讨论

发起讨论
加载中...