Embassy 深度实战:Rust 嵌入式异步编程的多任务架构与生产级模式

摘要:Embassy 已经成为 Rust 嵌入式开发的事实标准异步框架。但把 async/await 搬到裸机上远不止"不用写 RTOS 了"这么简单——用不好,生产环境的定时偏差、优先级反转和静默故障会让你付出惨痛代价。本文从架构对比、任务设计、通信模式到生产踩坑复盘,系统梳理 Embassy 在真实项目中的工程实践。

目录


1. 为什么 Rust 嵌入式需要 Async?

传统嵌入式 C 固件有两种并发模型:

  • 超级循环(Superloop):在 while(1) 里轮询所有外设。简单直接,但复杂度一上来就失控——CPU 在空转,功耗下不来,代码逻辑纠缠不清。
  • RTOS 多线程:FreeRTOS/Zephyr 提供抢占式调度。功能强大,但每个线程都要独立栈(256B ~ 4KB),十个线程就是几十 KB RAM,在小 MCU 上捉襟见肘。

Embassy 走了第三条路:

// 不再写 while(1) 轮询,而是用 async/await 描述并发逻辑
#[embassy_executor::task]
async fn blink(mut led: Output<'static>) {
    loop {
        led.set_high();
        Timer::after_millis(500).await;  // 这个 await 让出 CPU
        led.set_low();
        Timer::after_millis(500).await;
    }
}

每个 .await 是一个自愿的让出点。任务在等待定时器、UART 字节、DMA 完成时,CPU 可以运行其他任务,或者直接进入 WFI(Wait For Interrupt)低功耗休眠。没有轮询,没有空转,没有独立栈。


2. RTOS 线程模型 vs Embassy 状态机模型

这一节的对比是整个决策框架的基础。

对比维度FreeRTOS 线程模型Embassy 异步模型
每任务栈256B ~ 4KB(手动预估)无(状态机,编译期计算)
上下文切换硬件抢占(SysTick/PendSV)协作式让出(.await 点)
优先级抢占式基于中断
内存开销固定每线程精确等于状态变量大小
阻塞 I/O原生(线程阻塞)async/.await
休眠机制需要 idle taskWFI/WFE 自动休眠

核心差异:RTOS 里你有 10 个任务,每个任务 2KB 栈 → 20KB RAM 只是栈开销。Embassy 里 10 个 async 任务 → 零栈开销,总内存约等于所有状态变量的实际大小之和。

Rust 编译器把每个 async fn 转成一个状态机枚举(state machine enum)。每个 .await 点是状态机的一个分支,跨越 .await 的局部变量被保存在结构体里。这一切在编译期完成——没有堆分配,没有动态 dispatch。

// 你写的代码:
async fn example(x: u32) -> u32 {
    let a = read_sensor().await;  // 状态 0 → 1
    let b = a + x;                // 状态 1:a 被保存在状态机
    let c = send_data(b).await;   // 状态 1 → 2
    c + 1                         // 状态 2
}

// 编译器生成类似这样的结构:
enum ExampleStateMachine {
    State0 { x: u32 },
    State1 { x: u32, a: u32 },
    State2 { x: u32, a: u32, b: u32 },
    Done,
}

3. Embassy 核心架构拆解

Embassy 不是单一 crate,而是一组松耦合的 crate 家族:

Crate职责
embassy-executor异步任务执行器,跑在裸机上
embassy-time异步定时器:Timer、Ticker、Instant
embassy-sync异步同步原语:Channel、Signal、Mutex、PubSubChannel
embassy-net基于 smoltcp 的异步 TCP/IP 协议栈
embassy-usb异步 USB 设备栈(CDC-ACM、HID)
HAL crates芯片适配:embassy-stm32、embassy-nrf、embassy-rp

3.1 执行器如何工作?

中断唤醒

Task A await

Task B await

无可运行任务

WFI 休眠

执行器是协作式的。当 Task A 执行到 Timer::after_millis(100).await 时:

  1. 定时器硬件被设置为 100ms 后触发中断
  2. Task A 的状态机被挂起
  3. 执行器检查 Task B 是否可运行
  4. 如果所有任务都在等待,执行 WFI 指令,CPU 时钟停止
  5. 100ms 后定时器中断触发 → 唤醒 CPU → 执行器恢复 Task A

3.2 一个完整的 Embassy 应用

#![no_std]
#![no_main]

use defmt::*;
use embassy_executor::Spawner;
use embassy_stm32::gpio::{Level, Output, Speed};
use embassy_time::{Duration, Timer};
use {defmt_rtt as _, panic_probe as _};

#[embassy_executor::task]
async fn blink(mut led: Output<'static>) {
    loop {
        led.set_high();
        Timer::after_millis(500).await;
        led.set_low();
        Timer::after_millis(500).await;
    }
}

#[embassy_executor::task]
async fn sensor_reader() {
    loop {
        // 通过异步 I2C/SPI 读取传感器
        Timer::after_secs(1).await;
        info!("Sensor reading complete");
    }
}

#[embassy_executor::main]
async fn main(spawner: Spawner) {
    let p = embassy_stm32::init(Default::default());
    let led = Output::new(p.PB7, Level::Low, Speed::Low);

    spawner.spawn(blink(led)).unwrap();
    spawner.spawn(sensor_reader()).unwrap();

    // main 函数本身也是 async task
    // 执行器会持续调度,直到所有任务退出
}

几个关键点:

  • 没有 loop {} 主循环——执行器管理调度
  • 没有 unsafe——外设所有权由 Rust 类型系统保证
  • 零堆分配——所有状态在编译期确定
  • CPU 自动休眠——任务全部等待时进入 WFI

3.3 Ticker:防漂移的周期定时器

如果你用 Timer::after 做周期任务,处理时间会累积误差(漂移)。Ticker 解决了这个问题:

use embassy_time::Ticker;

#[embassy_executor::task]
async fn periodic_sensor() {
    let mut ticker = Ticker::every(Duration::from_millis(100));
    loop {
        let value = read_sensor().await;
        process(value);

        // 如果处理花了 30ms,这里只等 70ms——自动补偿
        ticker.next().await;
    }
}

4. 多任务通信:Channel、Signal、Mutex 实战

真正的嵌入式系统不会只有一个 LED 闪烁。多个任务需要交换数据。Embassy 提供了三种主要通信原语,全部静态分配,零堆开销。

4.1 Channel:MPMC 消息队列

最常用的任务间通信方式。支持多生产者、多消费者。

use embassy_sync::channel::Channel;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;

// 容量 4 的通道,存放 ButtonEvent 枚举
static BUTTON_CHANNEL: Channel<CriticalSectionRawMutex, ButtonEvent, 4> = Channel::new();

#[derive(Clone, Copy, defmt::Format)]
enum ButtonEvent {
    ShortPress,
    LongPress,
    DoubleClick,
}

#[embassy_executor::task]
async fn button_task() {
    loop {
        let event = detect_button().await;
        // 通道满时阻塞等待(让出 CPU,不忙等)
        BUTTON_CHANNEL.send(event).await;
    }
}

#[embassy_executor::task]
async fn led_control_task() {
    loop {
        let event = BUTTON_CHANNEL.receive().await;
        match event {
            ButtonEvent::ShortPress => toggle_led().await,
            ButtonEvent::LongPress => change_pattern().await,
            ButtonEvent::DoubleClick => enter_sleep_mode().await,
        }
    }
}

生产经验:通道深度不要设太大。一个工业项目中,把队列深度从 16 降到 4,最坏情况延迟显著下降。深队列让人误以为"不会丢数据",但突发流量来时延迟会飙升。配上显式的背压处理才是正道。

4.2 Signal:最新值通知

Signal 只保留最新值,适合"当前状态"类信息。多次 signal 后,消费者只拿到最后一次的值。

use embassy_sync::signal::Signal;

static BLINK_PATTERN: Signal<CriticalSectionRawMutex, BlinkMode> = Signal::new();

#[derive(Clone, Copy)]
enum BlinkMode {
    Slow,
    Fast,
    Sos,
}

// 生产者:按键切换模式
#[embassy_executor::task]
async fn button_handler() {
    loop {
        wait_for_button().await;
        BLINK_PATTERN.signal(BlinkMode::Sos);
    }
}

// 消费者:根据最新模式闪烁
#[embassy_executor::task]
async fn led_blinker() {
    loop {
        let mode = BLINK_PATTERN.wait().await;
        match mode {
            BlinkMode::Slow => blink_slow().await,
            BlinkMode::Fast => blink_fast().await,
            BlinkMode::Sos => blink_sos().await,
        }
    }
}

4.3 Mutex:跨任务的共享状态

需要多任务读/写同一个数据结构时使用。

use embassy_sync::mutex::Mutex;

static SHARED_CONFIG: Mutex<CriticalSectionRawMutex, SystemConfig> =
    Mutex::new(SystemConfig::default());

#[embassy_executor::task]
async fn config_updater() {
    loop {
        let mut cfg = SHARED_CONFIG.lock().await;
        cfg.baud_rate = 115200;
        cfg.timeout_ms = 5000;
        // 锁在 cfg 离开作用域时自动释放
        Timer::after_secs(10).await;
    }
}

#[embassy_executor::task]
async fn serial_transmitter() {
    loop {
        let cfg = SHARED_CONFIG.lock().await;
        send_at_baud_rate(cfg.baud_rate).await;
        // 锁在这里释放——不会跨越 await 持有
    }
}

注意:Embassy 的 Mutex 是 async 感知的。如果锁被持有,lock().await 会挂起当前任务而非忙等——这在协作式调度中至关重要。

4.4 选择指南

场景推荐原语
事件队列(每次都要处理)Channel
最新状态(只关心当前值)Signal
共享数据结构Mutex
广播给多个消费者PubSubChannel
简单数值共享(≤32位)AtomicU32

5. 生产级架构模式

5.1 纯 Embassy 的适用边界

纯 Embassy(不加 RTIC)在硬实时要求 ≤ 1~5ms、系统负载可预测的场景表现优异。一位资深开发者分享了他用于医疗可穿戴设备的控制循环:

#[embassy_executor::task]
async fn main_control_loop() {
    loop {
        let timestamp = Instant::now();

        let sensors = gather_sensor_data().await;
        let command = calculate_control_output(sensors);
        apply_actuators(command).await;

        // 关键:强制执行时间上限检查
        if timestamp.elapsed() > Duration::micros(1850) {
            log_overrun(timestamp.elapsed());
        }

        embassy_time::Timer::at(timestamp + Duration::millis(2)).await;
    }
}

这段代码的核心在于显式的时序守卫——不靠运气,靠测量。

5.2 混合架构:Embassy + RTIC

当硬实时要求进入 < 800µs 级别时,纯 Embassy 的协作式调度不再可靠。业界的最佳实践是混合架构:

RTIC 层

控制循环 · 信号处理 · 安全联锁
紧急停机 · 传感器融合

Embassy 层

BLE栈 · TCP/IP · 日志 · OTA
配置管理 · 诊断 · 用户交互

软实时(ms 级)

硬实时(μs 级)

RTIC 处理绝对不能错过的硬实时中断,Embassy 跑"软的"部分。这种架构已经成为 2025-2026 年安全关键和汽车电子项目的标准选择。

5.3 系统监控任务

一个高优先级的看门狗监控任务,检查所有关键任务的"心跳"计数器——这个简单的模式在生产中数次拯救了静默故障:

#[embassy_executor::task]
async fn system_monitor() {
    loop {
        check_all_heartbeats().await;

        if any_deadlines_missed() {
            error!("Deadline missed, entering safe mode");
            enter_safe_mode();
        }

        Timer::after_secs(1).await;
    }
}

5.4 共享 I2C/SPI 总线的坑

embassy_embedded_hal::shared_bus 在轻负载下够用,但 8 个传感器共享一条 I2C 总线会导致严重的竞争延迟。解决方案:

  • 拆分到多条物理总线
  • 给共享 Mutex 加显式的优先级和超时处理
  • 突发密集型外设单独给一条总线

6. 实战:多任务传感器采集系统

用一个 IoT 传感器节点的例子把上面所有知识点串起来。场景:ESP32-C3,SHT40 温湿度传感器(I2C),土壤湿度传感器(ADC),WiFi 上报数据。

#![no_std]
#![no_main]

use embassy_executor::Spawner;
use embassy_sync::channel::Channel;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_time::{Duration, Timer, Ticker};
use embassy_sync::signal::Signal;
use defmt::*;
use {defmt_rtt as _, esp_hal_embassy as _, panic_probe as _};

// ---- 数据结构 ----
#[derive(Clone, Copy, defmt::Format)]
struct SensorData {
    temperature: f32,
    humidity: f32,
    soil_moisture: u16,
}

// ---- 通信通道 ----
static SENSOR_CHANNEL: Channel<CriticalSectionRawMutex, SensorData, 4> = Channel::new();
static WIFI_STATUS: Signal<CriticalSectionRawMutex, bool> = Signal::new();
static ALERT_FLAG: Signal<CriticalSectionRawMutex, Alert> = Signal::new();

#[derive(Clone, Copy, defmt::Format)]
enum Alert {
    None,
    HighTemp,
    LowMoisture,
    SensorError,
}

// ---- 任务 1: 传感器采集 ----
#[embassy_executor::task]
async fn sensor_task(i2c: I2c<'static, Async>, adc: Adc<'static, ADC1>) {
    let mut ticker = Ticker::every(Duration::from_secs(5));

    loop {
        ticker.next().await;

        match read_sht40(&i2c).await {
            Ok((temp, hum)) => {
                let soil = read_soil_moisture(&adc).await.unwrap_or(0);

                let data = SensorData {
                    temperature: temp,
                    humidity: hum,
                    soil_moisture: soil,
                };

                // 发送到处理通道
                SENSOR_CHANNEL.send(data).await;

                // 阈值告警
                if temp > 45.0 {
                    ALERT_FLAG.signal(Alert::HighTemp);
                }
                if soil < 200 {
                    ALERT_FLAG.signal(Alert::LowMoisture);
                }
            }
            Err(_) => {
                ALERT_FLAG.signal(Alert::SensorError);
            }
        }
    }
}

// ---- 任务 2: WiFi 上报 ----
#[embassy_executor::task]
async fn network_task(
    stack: Stack<'static>,
    rx_buffer: &'static mut [u8; 1024],
    tx_buffer: &'static mut [u8; 1024],
) {
    // 先连 WiFi
    connect_wifi(&stack).await;
    WIFI_STATUS.signal(true);
    info!("WiFi connected");

    loop {
        // 等待传感器数据
        let data = SENSOR_CHANNEL.receive().await;

        // 构造 JSON 上报(使用 postcard 序列化也可以)
        let payload = format_sensor_json(data);
        match send_to_server(&stack, &payload, rx_buffer, tx_buffer).await {
            Ok(_) => info!("Data sent: {:?}", data),
            Err(_) => warn!("Send failed, will retry next cycle"),
        }
    }
}

// ---- 任务 3: 告警处理 ----
#[embassy_executor::task]
async fn alert_task(mut buzzer: Output<'static>) {
    loop {
        let alert = ALERT_FLAG.wait().await;

        match alert {
            Alert::HighTemp => {
                warn!("HIGH TEMPERATURE ALERT!");
                // 蜂鸣器快闪 3 次
                for _ in 0..3 {
                    buzzer.set_high();
                    Timer::after_millis(200).await;
                    buzzer.set_low();
                    Timer::after_millis(200).await;
                }
            }
            Alert::LowMoisture => {
                warn!("LOW MOISTURE ALERT!");
                // 慢闪 1 次
                buzzer.set_high();
                Timer::after_secs(1).await;
                buzzer.set_low();
            }
            Alert::SensorError => {
                error!("SENSOR READ ERROR!");
            }
            Alert::None => {}
        }
    }
}

// ---- 任务 4: 系统心跳 ----
#[embassy_executor::task]
async fn heartbeat_task(mut led: Output<'static>) {
    let mut ticker = Ticker::every(Duration::from_secs(10));
    loop {
        ticker.next().await;
        led.set_high();
        Timer::after_millis(50).await;
        led.set_low();
    }
}

// ---- 主入口 ----
#[embassy_executor::main]
async fn main(spawner: Spawner) {
    let p = esp_hal::init(esp_hal::Config::default().with_cpu_clock(CpuClock::max()));

    // 配置硬件
    let i2c = I2c::new(p.I2C0, Config::default().with_frequency(400_000))
        .unwrap()
        .with_sda(p.GPIO6)
        .with_scl(p.GPIO7);
    let adc = Adc::new(p.ADC1, AdcConfig::default());
    let led = Output::new(p.GPIO8, Level::Low);
    let buzzer = Output::new(p.GPIO3, Level::Low);

    // 启动所有任务
    spawner.spawn(sensor_task(i2c, adc)).unwrap();
    spawner.spawn(network_task(stack, rx_buf, tx_buf)).unwrap();
    spawner.spawn(alert_task(buzzer)).unwrap();
    spawner.spawn(heartbeat_task(led)).unwrap();

    info!("System started — {} tasks running", 4);
}

架构解析

(Channel: SensorData, 4)

(Signal: Alert)

upward

upward

sensor_task

network_task

HTTP Server

alert_task

Buzzer

heartbeat_task

LED(独立)

  • sensor_task:Ticker 驱动 5 秒周期采集,通过 Channel 传出数据
  • network_task:等待 Channel 数据,构造 JSON 上报云端
  • alert_task:监听 Signal,条件触发时驱动蜂鸣器
  • heartbeat_task:独立心跳 LED,证明系统存活

所有任务共享一个栈,所有通道和信号零堆分配,整个系统内存使用在编译期就确定了。


7. 踩坑复盘:那些让我吃了苦头的教训

来自多位生产级 Embassy 开发者的真实经验:

坑 1:把 .await 当免费午餐

最经典的错误:以为 async task 自动就是"轻量线程"。一位开发者分享——传感器融合的 async task 偶尔处理花 12ms,直接导致 500µs 控制循环错过截止时间。代码编译零警告,硬件不会原谅你。 教训:把每个 .await 点当全局变量一样谨慎对待。加时序守卫,加超时。

坑 2:忽视背压

通道深度设太大(16、32),平时跑得好好的,突发流量一来,延迟爆炸。把深度降到 4,配显式背压处理,延迟反而更可预测。

坑 3:任务太多

超过 12-15 个任务后,执行器开销和调试复杂度指数级增长。该拆的拆,该合的合,把硬实时部分交给 RTIC。

坑 4:依赖里的隐藏堆分配

有些标注 no_std 的 crate 内部悄悄用动态内存。审计依赖,别想当然。

坑 5:一个三周才找到的 Bug

BLE 协议栈任务和主控制循环之间罕见的竞态条件——只在特定温度和 RF 条件下触发。最终靠密集时间戳追踪和专用监控任务才定位到。


8. 总结与思考

决策框架

场景推荐方案
软实时(≥2ms),复杂通信栈纯 Embassy
硬实时(<800µs),多关键等级Embassy + RTIC 混合
超硬实时(<100µs),需可证明性纯 RTIC 或裸中断
极度资源受限裸中断 + 状态机

关键原则

  1. 把每个 .await 当作潜在的上下文切换——不要假设它"很快"
  2. 测量一切——时序偏差靠数据,不靠直觉
  3. 文档化时序假设——你的队友(和三个月后的你)需要知道每个任务的截止时间
  4. 小通道 + 显式背压——深度 4 的通道比深度 32 更可预测
  5. 监控任务不是可选——心跳计数器 + 截止时间检测是最便宜的保险

Embassy 不是银弹,但它确实改变了 Rust 嵌入式的游戏规则。用对架构,代码更清晰、更省电、RAM 更省;用错架构,debug 时间翻倍,生产事故不断。决定权在架构师手里。


参考资料

  1. Embassy 官方文档 — Embassy 框架官方站点,包含 HAL 文档和示例
  2. Practical no_std Async Patterns for Hard Real-Time Embedded Systems with Embassy in 2026 — 生产级 Embassy 架构经验,混合架构与背压处理
  3. Rust Embedded Development in 2026: no_std, Embassy, RTIC, probe-rs — no_std 生态全景与 Embassy 架构深度解析
  4. Embassy Async Fundamentals — Embassy 异步基础,Channel/Signal 通信实战
  5. Sharing Data Among Tasks in Rust Embassy: Synchronization Primitives — Embassy 同步原语全景对比
  6. Rust + Embassy on the ESP32 — ESP32 上 Embassy 实战指南
  7. RISC-V + Rust Firmware: Building an IoT Sensor Node with Embassy-rs — RISC-V 平台 Embassy 多任务传感器系统实战
  8. PSOC 6 Rust Wi-Fi Demo — Infineon RustWeek 2025 Embassy Wi-Fi Demo

更多推荐