Embassy 深度实战:Rust 嵌入式异步编程的多任务架构与生产级模式
Embassy 深度实战:Rust 嵌入式异步编程的多任务架构与生产级模式
摘要:Embassy 已经成为 Rust 嵌入式开发的事实标准异步框架。但把 async/await 搬到裸机上远不止"不用写 RTOS 了"这么简单——用不好,生产环境的定时偏差、优先级反转和静默故障会让你付出惨痛代价。本文从架构对比、任务设计、通信模式到生产踩坑复盘,系统梳理 Embassy 在真实项目中的工程实践。
目录
- 1. 为什么 Rust 嵌入式需要 Async?
- 2. RTOS 线程模型 vs Embassy 状态机模型
- 3. Embassy 核心架构拆解
- 4. 多任务通信:Channel、Signal、Mutex 实战
- 5. 生产级架构模式
- 6. 实战:多任务传感器采集系统
- 7. 踩坑复盘:那些让我吃了苦头的教训
- 8. 总结与思考
- 参考资料
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 task | WFI/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 执行到 Timer::after_millis(100).await 时:
- 定时器硬件被设置为 100ms 后触发中断
- Task A 的状态机被挂起
- 执行器检查 Task B 是否可运行
- 如果所有任务都在等待,执行
WFI指令,CPU 时钟停止 - 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 跑"软的"部分。这种架构已经成为 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);
}
架构解析
- 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 或裸中断 |
| 极度资源受限 | 裸中断 + 状态机 |
关键原则
- 把每个
.await当作潜在的上下文切换——不要假设它"很快" - 测量一切——时序偏差靠数据,不靠直觉
- 文档化时序假设——你的队友(和三个月后的你)需要知道每个任务的截止时间
- 小通道 + 显式背压——深度 4 的通道比深度 32 更可预测
- 监控任务不是可选——心跳计数器 + 截止时间检测是最便宜的保险
Embassy 不是银弹,但它确实改变了 Rust 嵌入式的游戏规则。用对架构,代码更清晰、更省电、RAM 更省;用错架构,debug 时间翻倍,生产事故不断。决定权在架构师手里。
参考资料
- Embassy 官方文档 — Embassy 框架官方站点,包含 HAL 文档和示例
- Practical no_std Async Patterns for Hard Real-Time Embedded Systems with Embassy in 2026 — 生产级 Embassy 架构经验,混合架构与背压处理
- Rust Embedded Development in 2026: no_std, Embassy, RTIC, probe-rs — no_std 生态全景与 Embassy 架构深度解析
- Embassy Async Fundamentals — Embassy 异步基础,Channel/Signal 通信实战
- Sharing Data Among Tasks in Rust Embassy: Synchronization Primitives — Embassy 同步原语全景对比
- Rust + Embassy on the ESP32 — ESP32 上 Embassy 实战指南
- RISC-V + Rust Firmware: Building an IoT Sensor Node with Embassy-rs — RISC-V 平台 Embassy 多任务传感器系统实战
- PSOC 6 Rust Wi-Fi Demo — Infineon RustWeek 2025 Embassy Wi-Fi Demo
更多推荐

所有评论(0)