第 9 章:Embassy 运行时——Executor、Future 与 Waker
Part2 深入embassy框架
第 9 章:Embassy 运行时——Executor、Future 与 Waker#
本章要回答的问题:async/await 在裸机上到底是怎么运行的?Future 是什么?Waker 和中断有什么关系?如何验证”零开销”?
本章定位:打开 Embassy 的”引擎盖”。从 Rust 的 Future trait 讲起,逐步深入到 Executor 的调度循环、Waker 的中断唤醒机制、时间驱动的硬件实现。最后通过反汇编对比,验证”零开销”不是营销话术。
9.1 Future:async 的底层真相#
9.1.1 一个 async fn 到底是什么?#
当你在第 8 章写下这段代码时:
async fn blink() {
loop {
led.set_high();
Timer::after(Duration::from_millis(500)).await;
led.set_low();
Timer::after(Duration::from_millis(500)).await;
}
}rust编译器把它变成了什么?
答案:一个实现了 Future trait 的状态机结构体。
// 编译器生成的等价代码(简化,帮助理解)
use core::future::Future;
use core::pin::Pin;
use core::task::{Context, Poll};
// 状态枚举:每个 .await 点是一个状态
enum BlinkState {
Start,
WaitingTimer1 { timer: TimerFuture },
AfterTimer1,
WaitingTimer2 { timer: TimerFuture },
AfterTimer2,
}
// Future 结构体:保存所有跨 .await 的局部变量
struct BlinkFuture {
state: BlinkState,
led: Output<'static>, // 跨 await 存活的变量
}
// 实现 Future trait
impl Future for BlinkFuture {
type Output = (); // 这个 async fn 返回 ()
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
loop {
match &mut self.state {
BlinkState::Start => {
self.led.set_high();
// 创建 Timer Future,进入等待状态
let timer = Timer::after(Duration::from_millis(500));
self.state = BlinkState::WaitingTimer1 { timer };
// 不 break,继续 loop(立即 poll 新状态)
}
BlinkState::WaitingTimer1 { timer } => {
// 关键:poll 内部的 Timer Future
match Pin::new(timer).poll(cx) {
Poll::Ready(()) => {
// Timer 完成了!进入下一个状态
self.state = BlinkState::AfterTimer1;
}
Poll::Pending => {
// Timer 还没完成,返回 Pending
// Executor 会稍后再来 poll 我们
return Poll::Pending;
}
}
}
BlinkState::AfterTimer1 => {
self.led.set_low();
let timer = Timer::after(Duration::from_millis(500));
self.state = BlinkState::WaitingTimer2 { timer };
}
BlinkState::WaitingTimer2 { timer } => {
match Pin::new(timer).poll(cx) {
Poll::Ready(()) => {
self.state = BlinkState::AfterTimer2;
}
Poll::Pending => {
return Poll::Pending;
}
}
}
BlinkState::AfterTimer2 => {
// 循环回到开始
self.state = BlinkState::Start;
}
}
}
}
}rust9.1.2 与第 5 章状态机的对应关系#
现在,把这个编译器生成的代码与第 5 章你手写的 C 状态机对比:
| 第 5 章 C 状态机 | 编译器生成的 Future | 说明 |
|---|---|---|
typedef enum { STATE_0, STATE_1, ... } | enum BlinkState { Start, WaitingTimer1, ... } | 状态枚举 |
static TaskState task_state | struct BlinkFuture { state: BlinkState, ... } | 状态变量 |
switch (state) { case STATE_0: ... } | match &mut self.state { ... } | 状态分发 |
break;(让出 CPU) | return Poll::Pending; | 让出控制权 |
| 全局变量保存跨步骤数据 | struct BlinkFuture 的字段 | 跨 await 的局部变量 |
主循环中调用 task_step() | Executor 调用 future.poll() | 调度器推进状态机 |
核心等式:
async fn = 编译器自动生成的状态机
.await = return Poll::Pending(让出 CPU)
Executor = 反复调用 poll() 的循环plaintext9.1.3 Future trait 的定义#
// core::future::Future —— Rust 标准库中的定义
pub trait Future {
/// Future 完成时的返回值类型
type Output;
/// 尝试推进 Future
///
/// - Poll::Ready(value):Future 完成,返回结果
/// - Poll::Pending:Future 未完成,稍后再来
///
/// 调用 poll 时,必须传入 Waker(通过 Context)
/// 当 Future 可以从 Pending 变为 Ready 时,它应该调用 waker.wake()
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll;
}rust三个关键概念:
| 概念 | 含义 | 类比 |
|---|---|---|
Poll::Pending | ”我还没完成,别管我了” | C 状态机中的 break(让出) |
Poll::Ready(v) | ”我完成了,结果给你” | C 状态机中的”最终状态” |
Waker(在 Context 中) | “我完成了的时候叫你” | C 中的中断回调 / 标志位 |
9.1.4 Pin:为什么 Future 不能移动?#
你可能注意到 poll 的签名是 self: Pin<&mut Self>,而不是普通的 &mut self。
为什么?
因为 Future 内部可能包含自引用(self-reference)。考虑这个 async fn:
async fn example() {
let data = [1u8, 2, 3, 4]; // 局部变量
let reference = &data[0]; // 指向 data 的引用
some_async_op().await; // ← await 点!
println!("{}", reference); // await 之后仍然使用 reference
}rust编译器生成的状态机:
struct ExampleFuture {
state: u8,
data: [u8; 4], // 跨 await 存活
reference: *const u8, // 指向 self.data[0]!
}rust如果这个结构体被移动(mem::swap、Vec::push 等),reference 就会变成悬垂指针——它仍然指向旧的内存地址。
Pin<&mut T> 的保证是:被 Pin 包裹的值不会被移动。 这保证了自引用的安全性。
对你的影响:作为 Embassy 用户,你几乎不需要直接处理 Pin。#[embassy_executor::task] 宏和 spawner.spawn() 已经帮你处理了。但理解它的存在,有助于理解为什么 Embassy 的任务是静态分配的(static 变量天然不可移动)。
9.2 Executor:调度器的主循环#
9.2.1 Executor 的本质#
Embassy 的 Executor 不是一个操作系统内核——它是一个极其简单的循环:
// Embassy Executor 的核心逻辑(极度简化)
pub struct Executor {
// 就绪队列:存放所有"需要被 poll"的 Future
run_queue: AtomicQueue<*mut TaskHeader>,
// 所有已注册的任务
tasks: &'static [TaskStorage],
}
impl Executor {
/// Executor 的主循环——这就是"调度器"的全部
pub fn run(&'static self) -> ! {
loop {
// 1. 从就绪队列取出一个任务
if let Some(task) = self.run_queue.dequeue() {
// 2. 调用它的 poll 方法
let waker = task.waker();
let mut cx = Context::from_waker(&waker);
// 安全:task 是 Pin 的(静态分配,不可移动)
let future = unsafe { task.future_as_pin_mut() };
let result = future.poll(&mut cx);
// 3. 如果 Future 完成了(Ready),不再重新入队
// 如果返回 Pending,任务不入队——等 Waker 唤醒
if result.is_ready() {
task.set_completed();
}
} else {
// 4. 就绪队列为空:所有任务都在等待
// 进入低功耗模式,等待中断唤醒
cortex_m::asm::wfi(); // Wait For Interrupt
}
}
}
}rust就这么简单。 没有优先级队列,没有时间片轮转,没有上下文切换。整个”调度器”就是一个 loop + 一个队列。
9.2.2 与 FreeRTOS 调度器的对比#
| 方面 | FreeRTOS 调度器 | Embassy Executor |
|---|---|---|
| 数据结构 | 优先级链表(每个优先级一个列表) | 单个 FIFO 队列 |
| 调度策略 | 抢占式(高优先级随时打断低优先级) | 协作式(任务主动让出) |
| 切换机制 | PendSV 中断 + 上下文切换 | 无切换(poll 返回即完成) |
| 时间管理 | SysTick 中断 + tick 计数 | 时间驱动(独立定时器) |
| 阻塞实现 | 任务从就绪列表移到阻塞列表 | Future 返回 Poll::Pending |
| 唤醒实现 | xTaskResumeFromISR() | waker.wake() |
| 代码量 | ~3000 行(portable 层) | ~500 行(核心逻辑) |
| RAM 开销 | 每任务一个栈(256-1024B) | 每任务一个 Future 结构体(编译期确定) |
9.2.3 任务的内存布局#
每个 Embassy 任务在内存中是什么样的?
// 当你写:
#[embassy_executor::task]
async fn blink(led: Output<'static>) {
loop {
led.toggle();
Timer::after(Duration::from_millis(500)).await;
}
}rust编译器 + Embassy 宏生成的内存布局:
┌─────────────────────────────────────────────────────────┐
│ TaskStorage(静态分配,在 .bss 段) │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ TaskHeader │ │
│ │ ├── state: AtomicU8 (IDLE/RUNNING/QUEUED) │ │
│ │ ├── next: AtomicPtr (就绪队列链表指针) │ │
│ │ ├── waker: Waker (唤醒函数指针 + 数据指针) │ │
│ │ └── executor: *const Executor │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Future 结构体(编译器生成) │ │
│ │ ├── state: u8 (状态枚举) │ │
│ │ ├── led: Output<'static> (跨 await 的变量) │ │
│ │ │ ├── pin: *const GPIOx (引脚寄存器地址) │ │
│ │ │ └── _phantom: PhantomData │ │
│ │ └── timer: TimerFuture (当前活跃的 Timer) │ │
│ │ ├── expires_at: u64 (到期时间戳) │ │
│ │ └── registered: bool │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ 总大小:~48-80 字节(取决于 async fn 的复杂度) │
│ │
└─────────────────────────────────────────────────────────┘
对比 FreeRTOS 任务:
┌─────────────────────────────────────────────────────────┐
│ TCB(Task Control Block)+ 独立栈 │
├─────────────────────────────────────────────────────────┤
│ TCB: ~100 字节 │
│ 栈: 256-1024 字节(必须预估,溢出无保护) │
│ 总计: ~356-1124 字节 │
└─────────────────────────────────────────────────────────┘plaintext关键差异:
- Embassy 任务的”栈”就是 Future 结构体本身——大小在编译期完全确定
- 没有栈溢出风险(因为根本没有独立的栈)
- 所有任务共享
main的栈(用于poll调用时的临时变量)
9.2.4 #[embassy_executor::task] 宏做了什么?#
// 你写的:
#[embassy_executor::task]
async fn blink(led: Output<'static>) {
loop {
led.toggle();
Timer::after(Duration::from_millis(500)).await;
}
}
// 宏展开后(极度简化):
// 1. 创建一个静态的 TaskStorage
static BLINK_TASK: TaskStorage = TaskStorage::new();
// 2. 生成一个"构造函数",将参数打包进 Future
fn blink(led: Output<'static>) -> impl Future {
async move {
loop {
led.toggle();
Timer::after(Duration::from_millis(500)).await;
}
}
}
// 3. 生成 spawn 的辅助代码
impl Spawner {
fn spawn_blink(&self, led: Output<'static>) -> Result<(), SpawnError> {
// 将 Future 初始化到静态 TaskStorage 中
let future = blink(led);
BLINK_TASK.init(future);
// 将任务加入 Executor 的就绪队列
self.executor.enqueue(&BLINK_TASK);
Ok(())
}
}rust为什么任务参数必须是 'static?
因为 Future 被存储在 static 变量中,它引用的所有数据必须活得和程序一样长。这就是为什么 Embassy 的外设对象(Output、Uart、I2c)都是 'static 生命周期——它们引用的是硬件寄存器,硬件寄存器永远存在。
9.3 Waker:中断如何唤醒 Future#
9.3.1 Waker 的角色#
回顾 Future::poll 的签名:
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll;rustContext 中包含一个 Waker。Waker 的作用是:
当 Future 从 Pending 变为 Ready 时,通知 Executor “这个任务可以继续了”。
在裸机上,“Future 变为 Ready”通常意味着:
- DMA 传输完成(中断触发)
- 定时器到期(中断触发)
- UART 收到数据(中断触发)
- GPIO 边沿到来(中断触发)
Waker 就是中断和 Executor 之间的桥梁。
9.3.2 Waker 的工作原理#
┌─────────────────────────────────────────────────────────────────┐
│ Waker 的完整生命周期 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. Executor 调用 future.poll(cx) │
│ └── cx 中包含 Waker(指向任务的 TaskHeader) │
│ │
│ 2. Future 内部启动 DMA 传输 │
│ └── 将 Waker 注册到 DMA 中断处理程序 │
│ "DMA 完成时,调用这个 Waker" │
│ │
│ 3. Future 返回 Poll::Pending │
│ └── Executor 知道:这个任务暂时不需要了 │
│ │
│ 4. CPU 执行其他任务(或进入 WFI 低功耗模式) │
│ │
│ 5. DMA 传输完成 → 硬件触发中断 │
│ │
│ 6. 中断处理程序执行: │
│ └── waker.wake() │
│ └── 将任务重新加入 Executor 的就绪队列 │
│ │
│ 7. Executor 从 WFI 醒来(或完成当前任务后) │
│ └── 从就绪队列取出任务 │
│ └── 再次调用 future.poll(cx) │
│ └── 这次返回 Poll::Ready(DMA 数据已就绪) │
│ │
└─────────────────────────────────────────────────────────────────┘plaintext9.3.3 用 C 类比理解 Waker#
// C 的等价机制:中断回调 + 标志位
volatile bool dma_done = false;
// DMA 中断处理程序
void DMA1_Channel5_IRQHandler(void) {
if (DMA1->ISR & DMA_ISR_TCIF5) {
DMA1->IFCR = DMA_IFCR_CTCIF5; // 清除标志
dma_done = true; // ← 这就是 waker.wake()!
}
}
// 主循环
while (1) {
// 启动 DMA
start_dma_transfer();
// 等待完成(轮询标志)
while (!dma_done) {
__WFI(); // 低功耗等待
}
dma_done = false;
// 处理数据
process_data();
}cEmbassy 的 Waker 就是把这个”设置标志 + 主循环检查”的模式自动化了:
| C 的手动模式 | Embassy 的 Waker |
|---|---|
volatile bool flag | Waker 内部的原子状态 |
ISR 中 flag = true | ISR 中 waker.wake() |
主循环 while(!flag) | Executor 的就绪队列 |
| 手动清除标志 | Executor 自动管理 |
多个标志 → 多个 if | 队列自动调度 |
9.3.4 Waker 的底层实现#
// Waker 的内部结构(简化)
pub struct Waker {
// 一个"胖指针":数据指针 + 函数指针表
data: *const (), // 指向 TaskHeader
vtable: &'static WakerVTable,
}
struct WakerVTable {
wake: fn(*const ()), // 唤醒函数
wake_by_ref: fn(*const ()),
drop: fn(*const ()),
clone: fn(*const ()) -> Waker,
}
// Embassy 的 wake 实现
fn embassy_wake(data: *const ()) {
let task = data as *const TaskHeader;
// 原子操作:将任务状态从 IDLE 改为 QUEUED
// 如果已经是 QUEUED,不重复入队
if task.state.compare_exchange(IDLE, QUEUED).is_ok() {
// 将任务加入 Executor 的就绪队列
let executor = unsafe { &*task.executor };
executor.run_queue.enqueue(task);
}
// 如果 CPU 在 WFI 中,发送事件唤醒它
cortex_m::asm::sev(); // Send Event(唤醒 WFI)
}rust关键细节:
wake()可以在中断上下文中调用(它是unsafe的,但 Embassy 保证了安全性)- 使用原子操作防止重复入队(同一个任务可能被多个中断唤醒)
sev()指令确保 CPU 从 WFI 中醒来
9.3.5 完整示例:UART 接收的 Waker 流程#
时间线:UART 接收一个字节
t=0 Executor poll uart_task
└── uart.read(&mut buf).await
└── 内部:配置 DMA,注册 Waker 到 DMA 中断
└── 返回 Poll::Pending
└── Executor:任务入队失败(Pending),继续下一个任务
t=1 Executor poll blink_task
└── led.toggle()
└── Timer::after(500ms).await → Pending
└── Executor:无就绪任务,执行 WFI
t=50μs UART 收到一个字节
└── DMA 自动将字节搬到内存
└── DMA 传输完成,触发 DMA1_Channel5_IRQHandler
t=50μs+12cycles 中断处理程序执行
└── 清除 DMA 中断标志
└── 调用 waker.wake()
└── 原子设置 task.state = QUEUED
└── 将 uart_task 加入 run_queue
└── SEV(唤醒 WFI)
t=50μs+20cycles CPU 从 WFI 醒来
└── Executor 从 run_queue 取出 uart_task
└── 调用 uart_task.poll(cx)
└── DMA 已完成,buf[0] 有数据
└── 返回 Poll::Ready(Ok(()))
└── uart_task 继续执行 .await 之后的代码plaintext整个过程没有上下文切换,没有独立栈,没有 RTOS 内核。 只有:中断 → 原子操作 → 队列 → poll。
9.4 时间驱动:Timer 的硬件基础#
9.4.1 Timer::after() 是怎么工作的?#
当你在第 8 章写 Timer::after(Duration::from_millis(500)).await 时,底层发生了什么?
┌─────────────────────────────────────────────────────────────────┐
│ Timer::after(500ms) 的内部实现 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 读取当前时间戳 │
│ let now = Instant::now(); // 从硬件定时器计数器读取 │
│ │
│ 2. 计算到期时间 │
│ let expires = now + Duration::from_millis(500); │
│ │
│ 3. 创建 TimerFuture │
│ TimerFuture { │
│ expires_at: expires, │
│ waker: None, // 稍后在 poll 中注册 │
│ } │
│ │
│ 4. 首次 poll: │
│ - 将 (expires_at, waker) 注册到时间驱动的最小堆 │
│ - 如果 expires_at 是堆中最小的,重新编程硬件定时器 │
│ - 返回 Poll::Pending │
│ │
│ 5. 硬件定时器中断(到期时触发): │
│ - 从最小堆中取出所有已到期的 Timer │
│ - 对每个 Timer 调用 waker.wake() │
│ - 重新编程下一个到期时间 │
│ │
│ 6. 再次 poll: │
│ - 检查 now >= expires_at │
│ - 是 → 返回 Poll::Ready(()) │
│ │
└─────────────────────────────────────────────────────────────────┘plaintext9.4.2 时间驱动的硬件实现#
Embassy 需要一个硬件定时器作为”系统时钟”。在 Cargo.toml 中:
embassy-stm32 = { features = ["time-driver-any"] }
# ↑
# 自动选择一个空闲的硬件定时器tomltime-driver-any 会选择 TIM2/TIM3/TIM4/TIM5 中的一个(避免与 PWM 等应用冲突)。
// 时间驱动的核心(极度简化)
static ALARM_HEAP: Mutex> = Mutex::new(MinHeap::new());
struct AlarmEntry {
expires_at: u64, // 到期时间戳(64 位,不会溢出)
waker: Waker, // 到期时唤醒的任务
}
// 硬件定时器中断(例如 TIM2_IRQHandler)
fn time_driver_irq() {
let now = read_timer_counter(); // 读取当前计数值
// 取出所有已到期的 Alarm
while let Some(entry) = ALARM_HEAP.lock().peek() {
if entry.expires_at <= now {
let entry = ALARM_HEAP.lock().pop().unwrap();
entry.waker.wake(); // 唤醒等待的任务!
} else {
break; // 堆顶还没到期,后面的更不会到期
}
}
// 重新编程定时器:下一个到期时间
if let Some(next) = ALARM_HEAP.lock().peek() {
set_timer_compare(next.expires_at);
} else {
set_timer_compare(u32::MAX); // 没有待处理的 Alarm
}
}rust9.4.3 与 C 的 SysTick 对比#
| 方面 | C(FreeRTOS SysTick) | Embassy 时间驱动 |
|---|---|---|
| 硬件 | SysTick(Cortex-M 内核自带) | 通用定时器(TIM2/3/4/5) |
| 周期 | 固定 1ms tick | 按需编程(只在有 Alarm 时触发) |
| 功耗 | 每 1ms 一次中断(即使无事可做) | 无 Alarm 时不产生中断 |
| 精度 | 1ms(tick 周期) | 取决于定时器时钟(通常 ~1μs) |
| 溢出 | 32 位 tick 计数(~49 天溢出) | 64 位时间戳(~58 万年溢出) |
| 多定时器 | 需要软件定时器列表 | 内置最小堆,自动管理 |
功耗优势:
FreeRTOS(SysTick 1ms):
- 每秒 1000 次中断
- 即使所有任务都在等 5 秒,SysTick 仍然每 1ms 触发一次
- 每次中断:~20 个时钟周期 × 1000 次/秒 = 20,000 周期/秒 的"无用功"
Embassy(按需定时器):
- 如果最近的 Alarm 在 5 秒后,定时器 5 秒内不产生中断
- CPU 可以进入深度睡眠(Stop/Standby 模式)
- 功耗降低 10-100 倍(取决于空闲时间占比)plaintext9.4.4 Ticker vs Timer 的内部差异#
// Timer:一次性
Timer::after(Duration::from_secs(1)).await;
// 内部:注册一个 Alarm,触发后从堆中移除
// Ticker:周期性
let mut ticker = Ticker::every(Duration::from_millis(100));
loop {
ticker.next().await;
// 内部:每次 poll Ready 后,自动注册下一个 Alarm
// expires_at += period(而非 now + period)
// 这就是为什么 Ticker 不漂移!
}rustTicker 不漂移的原因:
Timer 方式(有漂移):
t=0: 开始
t=5ms: 工作完成
t=5ms: Timer::after(100ms) → 到期时间 = 105ms
t=105ms: 触发
t=110ms: 工作完成
t=110ms: Timer::after(100ms) → 到期时间 = 210ms ← 漂移了 10ms!
Ticker 方式(无漂移):
t=0: 创建 Ticker,next_expires = 100ms
t=100ms: 触发,next_expires = 200ms(固定递增)
t=105ms: 工作完成
t=200ms: 触发,next_expires = 300ms ← 无漂移!plaintext9.5 反汇编验证:“零开销”是真的吗?#
9.5.1 实验设计#
让我们对比两段功能等价的代码:
版本 A:Embassy async
#[embassy_executor::task]
async fn blink_async(mut led: Output<'static>) {
loop {
led.set_high();
Timer::after(Duration::from_millis(500)).await;
led.set_low();
Timer::after(Duration::from_millis(500)).await;
}
}rust版本 B:手写状态机(模拟 C 风格)
#[derive(PartialEq)]
enum BlinkState { High, WaitHigh, Low, WaitLow }
struct BlinkManual {
state: BlinkState,
led: Output<'static>,
timer_start: u64,
}
impl BlinkManual {
fn poll(&mut self, now: u64) -> bool {
match self.state {
BlinkState::High => {
self.led.set_high();
self.timer_start = now;
self.state = BlinkState::WaitHigh;
false
}
BlinkState::WaitHigh => {
if now - self.timer_start >= 500 {
self.state = BlinkState::Low;
}
false
}
BlinkState::Low => {
self.led.set_low();
self.timer_start = now;
self.state = BlinkState::WaitLow;
false
}
BlinkState::WaitLow => {
if now - self.timer_start >= 500 {
self.state = BlinkState::High;
}
false
}
}
}
}rust9.5.2 反汇编对比#
使用 cargo objdump --release -- -d 查看机器码:
版本 A(Embassy async)的 poll 函数:
; blink_async 的 poll 实现
; 大小:~120 字节
08001234 :
push {r4, r5, r6, lr}
mov r4, r0 ; r4 = self (Future 指针)
ldrb r5, [r4, #0] ; r5 = self.state
cmp r5, #0 ; state == High?
beq .Lstate_high
cmp r5, #1 ; state == WaitHigh?
beq .Lstate_wait_high
cmp r5, #2 ; state == Low?
beq .Lstate_low
b .Lstate_wait_low ; state == WaitLow
.Lstate_high:
ldr r0, [r4, #4] ; 加载 GPIO 基地址
movs r1, #1
str r1, [r0, #0x14] ; GPIOx->BSRR = 1 (set_high)
bl instant_now ; 获取当前时间
str r0, [r4, #8] ; 保存 timer_start
movs r5, #1
strb r5, [r4, #0] ; state = WaitHigh
; 继续 fall-through 到 WaitHigh
.Lstate_wait_high:
bl instant_now
ldr r1, [r4, #8]
subs r0, r0, r1 ; elapsed = now - timer_start
cmp r0, #500
blt .Lreturn_pending ; 没到时间,返回 Pending
movs r5, #2
strb r5, [r4, #0] ; state = Low
; fall-through
.Lstate_low:
ldr r0, [r4, #4]
movs r1, #1
lsls r1, r1, #16
str r1, [r0, #0x14] ; GPIOx->BSRR = 1<<16 (set_low)
bl instant_now
str r0, [r4, #8]
movs r5, #3
strb r5, [r4, #0] ; state = WaitLow
.Lstate_wait_low:
bl instant_now
ldr r1, [r4, #8]
subs r0, r0, r1
cmp r0, #500
blt .Lreturn_pending
movs r5, #0
strb r5, [r4, #0] ; state = High(循环)
b .Lstate_high
.Lreturn_pending:
movs r0, #0 ; Poll::Pending
pop {r4, r5, r6, pc}asm版本 B(手写状态机)的 poll 函数:
; BlinkManual::poll 实现
; 大小:~116 字节
08001340 :
push {r4, r5, r6, lr}
mov r4, r0
ldrb r5, [r4, #0]
cmp r5, #0
beq .Lm_state_high
cmp r5, #1
beq .Lm_state_wait_high
cmp r5, #2
beq .Lm_state_low
b .Lm_state_wait_low
.Lm_state_high:
ldr r0, [r4, #4]
movs r1, #1
str r1, [r0, #0x14]
bl instant_now
str r0, [r4, #8]
movs r5, #1
strb r5, [r4, #0]
.Lm_state_wait_high:
bl instant_now
ldr r1, [r4, #8]
subs r0, r0, r1
cmp r0, #500
blt .Lm_return_false
movs r5, #2
strb r5, [r4, #0]
; ... 后续代码结构完全相同 ...asm9.5.3 对比结论#
| 指标 | Embassy async | 手写状态机 | 差异 |
|---|---|---|---|
| 代码大小 | ~120 字节 | ~116 字节 | +4 字节(< 4%) |
| 指令数 | ~45 条 | ~43 条 | +2 条 |
| 寄存器使用 | R4-R6 + LR | R4-R6 + LR | 相同 |
| 栈使用 | 16 字节(push/pop) | 16 字节 | 相同 |
| 分支结构 | 完全相同 | 完全相同 | — |
| GPIO 操作 | str r1, [r0, #0x14] | str r1, [r0, #0x14] | 完全相同 |
结论:Embassy 的 async/await 编译后的机器码与手写状态机几乎完全相同。额外的 4 字节来自 Poll 枚举的返回值处理(Ready vs Pending),这在 C 中也需要(返回 bool 或 enum)。
“零开销”不是营销话术——它是可验证的事实。
9.5.4 与 C HAL 的对比#
; C 版本:HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET)
; 这是一个函数调用!
08001450 :
...
movs r0, #0 ; GPIOA 基地址(部分)
movt r0, #0x4002 ; 0x40020000
movs r1, #0x20 ; GPIO_PIN_5
movs r2, #1 ; GPIO_PIN_SET
bl HAL_GPIO_WritePin ; ← 函数调用!~20 个周期
...
; HAL_GPIO_WritePin 内部:
08002100 :
push {r4, lr}
cmp r2, #0
beq .Lreset
lsls r1, r1, #0 ; pin << 0
str r1, [r0, #0x14] ; GPIOx->BSRR = pin
pop {r4, pc}
.Lreset:
lsls r1, r1, #16 ; pin << 16
str r1, [r0, #0x14]
pop {r4, pc}asm| 操作 | C(HAL 函数调用) | Embassy(内联) |
|---|---|---|
| 点亮 LED | bl HAL_GPIO_WritePin(~20 周期) | str r1, [r0, #0x14](~2 周期) |
| 原因 | HAL 是运行时库,函数调用有开销 | 泛型单态化,编译期内联 |
| 额外开销 | 参数传递、分支判断、函数返回 | 无 |
Embassy 不仅”零开销”——它比 C HAL 更快。 因为 Rust 的泛型单态化让所有操作在编译期内联,而 C 的 HAL 库函数是运行时调用。
9.6 InterruptExecutor:抢占式任务#
9.6.1 为什么需要 InterruptExecutor?#
标准 Executor 是协作式的——一个任务不让出 CPU,其他任务就饿死。对于大多数 I/O 任务,这不是问题(.await 点足够密集)。
但对于硬实时任务(如 100kHz 电机 PWM 控制),你需要抢占——高优先级任务必须能打断低优先级任务。
Embassy 的解决方案:InterruptExecutor——在中断中运行的 Executor。
9.6.2 工作原理#
┌─────────────────────────────────────────────────────────────────┐
│ 双 Executor 架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 主 Executor(线程模式,低优先级) │
│ ├── blink_task(LED 闪烁) │
│ ├── uart_task(串口通信) │
│ └── display_task(显示刷新) │
│ │
│ InterruptExecutor(中断模式,高优先级) │
│ ├── motor_control_task(100kHz PWM 控制) │
│ └── adc_sample_task(高速 ADC 采样) │
│ │
│ 硬件中断(最高优先级) │
│ ├── DMA 完成中断 → 唤醒主 Executor 中的任务 │
│ └── 定时器中断 → 唤醒 InterruptExecutor 中的任务 │
│ │
│ 优先级关系: │
│ 硬件中断 > InterruptExecutor > 主 Executor > WFI │
│ │
└─────────────────────────────────────────────────────────────────┘plaintext9.6.3 代码示例#
#![no_std]
#![no_main]
use embassy_executor::Spawner;
use embassy_stm32::{
self as hal,
gpio::{Level, Output, Speed},
interrupt,
};
use embassy_time::{Duration, Ticker, Timer};
use defmt_rtt as _;
use panic_probe as _;
// 静态分配 InterruptExecutor
static EXECUTOR_HIGH: interrupt::InterruptExecutor = interrupt::InterruptExecutor::new();
// 高优先级中断(绑定到 TIM3)
#[interrupt]
unsafe fn TIM3() {
EXECUTOR_HIGH.on_interrupt();
}
// 高优先级任务:电机控制(10kHz)
#[embassy_executor::task]
async fn motor_control() {
let mut ticker = Ticker::every(Duration::from_micros(100)); // 10kHz
loop {
ticker.next().await;
// 读取编码器、计算 PID、更新 PWM
// 这个任务可以抢占主 Executor 中的任何任务
update_motor_pwm();
}
}
// 低优先级任务:LED 闪烁(可能被 motor_control 抢占)
#[embassy_executor::task]
async fn blink(mut led: Output<'static>) {
loop {
led.toggle();
Timer::after(Duration::from_millis(500)).await;
}
}
#[embassy_executor::main]
async fn main(spawner: Spawner) {
let p = hal::init(Default::default());
// 启动高优先级 Executor(在 TIM3 中断中运行)
let spawner_high = EXECUTOR_HIGH.start(interrupt::Priority::P2);
spawner_high.spawn(motor_control()).unwrap();
// 启动低优先级任务(在主 Executor 中运行)
let led = Output::new(p.PA5, Level::Low, Speed::Low);
spawner.spawn(blink(led)).unwrap();
loop {
Timer::after(Duration::from_secs(1)).await;
defmt::info!("主循环心跳");
}
}rust9.6.4 与 FreeRTOS 优先级的对比#
| 方面 | FreeRTOS | Embassy InterruptExecutor |
|---|---|---|
| 优先级数量 | 可配置(通常 5-32 级) | 2 级(主 + 中断)或更多(多个中断 Executor) |
| 优先级反转 | 可能(需要互斥量保护) | 不可能(中断天然不可被线程抢占) |
| 配置复杂度 | 每个任务设置优先级 + 调度策略 | 只需选择中断优先级 |
| 确定性 | 取决于调度器实现 | 完全确定(硬件中断优先级) |
| 开销 | 上下文切换(~20-40 周期) | 中断进入/退出(~24 周期,硬件自动) |
9.6.5 何时使用 InterruptExecutor?#
| 场景 | 是否需要 InterruptExecutor |
|---|---|
| LED 闪烁 + 传感器读取 + UART | ❌ 不需要(标准 Executor 足够) |
| 电机控制(> 10kHz 循环) | ✅ 需要 |
| 高速 ADC 采样(> 100kHz) | ✅ 需要 |
| 通信协议栈(严格时序) | ⚠️ 视情况(通常标准 Executor 足够) |
| 音频处理(44.1kHz 采样) | ✅ 需要 |
经验法则:如果你的任务周期 < 1ms 且有严格截止时间,考虑 InterruptExecutor。否则,标准 Executor 的协作式调度已经足够。
9.7 多核支持(简述)#
9.7.1 RP2040 双核架构#
RP2040 有两个 Cortex-M0+ 核心。Embassy 支持在两个核心上各运行一个 Executor:
// RP2040 双核示例(简化)
use embassy_rp::multicore::{spawn_core1, Stack};
static CORE1_STACK: Stack<4096> = Stack::new();
// Core 0 的任务
#[embassy_executor::task]
async fn core0_task() {
// 通信、显示、用户交互
}
// Core 1 的任务
#[embassy_executor::task]
async fn core1_task() {
// 电机控制、高速采样
}
#[embassy_executor::main]
async fn main(spawner: Spawner) {
let p = embassy_rp::init(Default::default());
// 在 Core 1 上启动 Executor
spawn_core1(
p.CORE1,
&CORE1_STACK,
move || {
// Core 1 的 Executor 在这里运行
let executor = StaticCell::new().init(Executor::new());
executor.run(|spawner| {
spawner.spawn(core1_task()).unwrap();
});
},
);
// Core 0 的任务
spawner.spawn(core0_task()).unwrap();
}rust9.7.2 多核通信#
两个核心的 Executor 之间通过 embassy-sync 的通信原语交互:
use embassy_sync::channel::Channel;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
// 跨核通道(使用临界区保护)
static CORE_CH: Channel = Channel::new();
// Core 0:发送命令
async fn core0_send() {
CORE_CH.send(MotorCommand::SetSpeed(1500)).await;
}
// Core 1:接收命令
async fn core1_receive() {
let cmd = CORE_CH.receive().await;
apply_motor_command(cmd);
}rust多核的详细内容(RP2040 双核、ESP32 双核)将在第 15 章展开。
9.8 本章小结#
核心机制回顾#
┌─────────────────────────────────────────────────────────────────┐
│ Embassy 运行时知识图谱 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Future │
│ ├── 本质:编译器生成的状态机结构体 │
│ ├── poll():推进状态机一步 │
│ ├── Poll::Pending:让出 CPU(= C 状态机的 break) │
│ ├── Poll::Ready:完成(= C 状态机的最终状态) │
│ └── Pin:保证自引用安全(静态分配天然满足) │
│ │
│ Executor │
│ ├── 本质:loop { dequeue → poll → enqueue_if_ready } │
│ ├── 无优先级、无时间片、无上下文切换 │
│ ├── 空闲时:WFI(低功耗等待) │
│ └── InterruptExecutor:在中断中运行,实现抢占 │
│ │
│ Waker │
│ ├── 本质:中断 → Executor 的桥梁 │
│ ├── wake():原子入队 + SEV 唤醒 │
│ ├── 注册时机:Future 首次 poll 时 │
│ └── 触发时机:DMA/Timer/GPIO 中断 │
│ │
│ 时间驱动 │
│ ├── 硬件:通用定时器(TIM2/3/4/5) │
│ ├── 数据结构:最小堆(按到期时间排序) │
│ ├── 按需中断:无 Alarm 时不触发(低功耗) │
│ └── 64 位时间戳:永不溢出 │
│ │
│ 零开销验证 │
│ ├── async 代码 ≈ 手写状态机(差异 < 4%) │
│ ├── 泛型内联 → 比 C HAL 函数调用更快 │
│ └── 无堆分配、无上下文切换、无内核对象 │
│ │
└─────────────────────────────────────────────────────────────────┘plaintext从 C 到 Embassy 的心智模型转换#
| C 的思维 | Embassy 的思维 |
|---|---|
| ”我要配置中断、写 ISR、设置标志、在主循环中检查" | "我 .await 一个事件,编译器帮我做剩下的" |
| "我要管理任务状态变量、写 switch/case" | "我写顺序代码,编译器生成状态机" |
| "我要配置 SysTick、维护 tick 计数、实现软件定时器" | "我 Timer::after().await,时间驱动帮我管理" |
| "我要小心 volatile、临界区、中断优先级" | "Waker 是原子操作,Executor 是单线程,无需锁" |
| "我要预估每个任务的栈大小" | "编译器计算 Future 大小,静态分配,无溢出风险” |
一个关键认知#
如果你只记住本章的一件事:
Embassy 的 Executor 不是一个”操作系统”——它是一个
while循环。Future 不是一个”线程”——它是一个结构体。Waker 不是一个”IPC 机制”——它是一个函数指针。async/await 不是”魔法”——它是编译器帮你写的switch/case。
所有这些机制的复杂度加起来,不超过 500 行核心代码。没有内核,没有调度算法,没有上下文切换。这就是为什么 Embassy 能在 64KB Flash 的 MCU 上运行,而 FreeRTOS 需要至少 32KB 内核代码。
下一步#
你现在理解了 Embassy 的运行时机制。但在实际项目中,多个任务之间需要通信:
- 传感器任务读取数据后,如何告诉显示任务?
- UART 任务收到命令后,如何通知控制任务?
- 多个任务如何安全地共享一个资源?
这些问题的答案在第 10 章:任务间通信——Channel、Signal、Mutex 与 PubSub。
下一章:第 10 章——任务间通信:Channel、Signal、Mutex 与 PubSub。我们将学习 Embassy 的同步原语,并与 FreeRTOS 的队列/信号量/事件组逐一对比。