源码地图
主线位置:三条主线经过的 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.0 | embassy-executor-macros、embassy-executor-timer-queue | cordyceps(运行队列用的侵入式链表)、critical-section;cortex-m 只在 platform-cortex-m 时 |
embassy-time 0.5.1 | embassy-time-driver;std / wasm / mock-driver 时加 embassy-time-queue-utils | embedded-hal 0.2 和 1.0、embedded-hal-async(实现 DelayNs) |
embassy-time-driver 0.2.2 | 无 | 无(只有生成文档用的 document-features) |
embassy-time-queue-utils 0.3.2 | embassy-executor-timer-queue | heapless(通用队列) |
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.0 | embassy-futures、embassy-sync、embassy-hal-internal 0.4.0 | embedded-hal 各版本 |
embassy-stm32 0.6.0 | embassy-hal-internal 0.5.0、embassy-sync、embassy-futures、embassy-embedded-hal、embassy-net-driver、embassy-usb-driver、embassy-usb-synopsys-otg | stm32-metapac 21.0.0、cortex-m、cortex-m-rt |
embassy-net 0.9.1 | embassy-net-driver、embassy-sync、embassy-time | smoltcp 0.13.1 |
embassy-usb 0.6.0 | embassy-usb-driver、embassy-sync、embassy-futures、embassy-time、embassy-net-driver-channel | usbd-hid |
从表里能直接读出三条:
futures 和 sync 不依赖执行器。 两个 crate 都只认
core::task::Waker,用在 tokio 下、裸block_on下都可以。sync 的Channel、Mutex、Signal这些类型都带一个M: RawMutex类型参数,由用户决定靠什么保证互斥,见原始互斥。executor 不依赖 time,time 也不依赖 executor。 在
intro-qemu里反查谁依赖执行器,只有应用自己:它们之间的接口放在两个小 crate 里,靠链接期符号配对,下一节讲。所以 Cargo 层面互不依赖,链接时却少不了对方:集成定时器队列要调执行器定义的符号,queue-utils 的文档写的是「It will fail to build if you try to use it without an embassy executor in the project」。embassy-executor v0.10.0 └── intro-qemu v0.1.0 (~/exp-embassy/intro-qemu)HAL 实现时间驱动,但图里照样有
embassy-time。 embassy-stm32 的time-driver-*打开的是内部 feature_time-driver = ["dep:embassy-time-driver", "time", "dep:embassy-time-queue-utils"]:驱动本身(RtcDriver)只实现embassy-time-driver的Drivertrait、用embassy-time-queue-utils的Queue,但这个 feature 同时打开time,也就依赖了embassy-time。intro-qemu没开time-driver-*,embassy-time照样被embassy-usb-synopsys-otg0.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_wake | embassy-time-driver 的 now()、schedule_wake() | 时间驱动,用 time_driver_impl!:embassy-stm32 的 TIM 驱动、embassy-time 的 std 驱动,或者本卷 QEMU 实验里手写的 SysTick 驱动 | 读时间;登记「到 at 时叫醒这个 waker」 |
__embassy_time_queue_item_from_waker | embassy-executor-timer-queue 的 TimerQueueItem::from_embassy_waker | embassy-executor:从 waker 的数据指针还原 TaskRef,返回任务头里的 timer_queue_item | 集成定时器队列把任务串成链表,不用另外分配节点 |
__pender | embassy-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 字节,见定时器队列。
#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-executor | src/lib.rs:check_at_most_one!、按 platform-* 选 platform/ 下的文件 | raw/mod.rs(TaskHeader、TaskStorage、SyncExecutor)、raw/run_queue.rs、platform/cortex_m.rs | 3258(3998) |
embassy-executor-macros | src/lib.rs:#[task]、#[main] 的入口 | macros/task.rs、macros/main.rs | 839 |
embassy-executor-timer-queue | src/lib.rs(只有这一个文件) | TimerQueueItem | 104 |
embassy-time | src/lib.rs | timer.rs、instant.rs、duration.rs、driver_std.rs | 1615 |
embassy-time-driver | src/lib.rs:Driver trait、time_driver_impl! | tick.rs 是生成的(488 行全是 #[cfg] 常量) | 666 |
embassy-time-queue-utils | src/lib.rs:按 feature 选队列实现 | queue_integrated.rs、queue_generic.rs | 350 |
embassy-sync | src/lib.rs | blocking_mutex/、waitqueue/、channel.rs | 8911(9048) |
embassy-futures | src/lib.rs | select.rs、join.rs、block_on.rs | 1246 |
embassy-hal-internal | src/lib.rs | interrupt.rs(interrupt_mod!、Handler、Binding)、peripheral.rs、macros.rs | 2229(2330) |
embassy-embedded-hal | src/lib.rs | shared_bus/ | 1910 |
embassy-stm32 | src/lib.rs:bind_interrupts!、Config、init | build.rs(生成代码)、exti/、gpio.rs、time_driver/、rcc/ | 83273(86129) |
embassy-net | src/lib.rs:Stack、Runner、new | tcp.rs、udp.rs | 3927 |
embassy-net-driver / -channel | src/lib.rs | Driver trait;channel 的 State、Runner | 217 / 680 |
embassy-usb / -driver | src/lib.rs | builder.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,不会撞类型。