第 7 章:Embassy 入门——思想、对比与代价
Part2 深入embassy框架
第 7 章:Embassy 入门——思想、对比与代价#
本章要回答的问题:Embassy 的 async/await 到底是怎么工作的?它和你在第 5 章看到的手写状态机有什么关系?采用 Embassy 需要付出什么代价?
本章定位:建立对 Embassy 的整体认知——设计哲学、与传统方案的对比、真实的代价。不涉及具体 API 用法(那是第 8 章的事),也不深入运行时内部(那是第 9 章的事)。
7.1 Embassy 的设计哲学#
一句话概括#
Embassy 把”手写状态机 + 中断驱动”的嵌入式并发模式,用 Rust 的 async/await 语法重新表达,让编译器自动生成状态机代码。
这不是一个全新的发明——它的底层机制(状态机 + 协作式调度)在第 5 章已经讲过了。Embassy 的创新在于:用编译器的力量,把”程序员手动管理状态”变成了”程序员写顺序代码”。
三个核心设计原则#
原则一:零开销(Zero-Cost)#
Embassy 不引入任何运行时抽象层:
| 传统 RTOS | Embassy |
|---|---|
| 调度器是一个运行时循环 | 调度器是编译期生成的 poll() 调用链 |
| 每个任务有独立的栈 | 所有任务共享一个栈 |
| 上下文切换保存/恢复寄存器 | 没有上下文切换(状态保存在 Future 结构体中) |
| 队列/信号量是内核对象 | Channel/Signal 是普通的 static 变量 |
验证方式:你可以对 Embassy 代码和等价的手写状态机代码做反汇编对比——机器码几乎相同(第 9 章会展示)。
原则二:无动态分配(No Heap Allocation)#
Embassy 的所有数据结构都是静态分配的:
// Embassy 的 Channel:编译期确定大小
static CHANNEL: Channel = Channel::new();
// ↑
// 缓冲区大小在编译期确定
// 不需要 malloc
// Embassy 的任务:编译期确定栈帧大小
#[embassy_executor::task]
async fn sensor_task() {
// 这个 async fn 被编译为一个固定大小的 Future 结构体
// 大小在编译期完全确定
// 不需要运行时分配
}rust为什么这很重要?
在嵌入式中,堆分配(malloc/free)是万恶之源:
- 分配失败怎么办?(嵌入式没有”内存不足”的优雅处理方式)
- 碎片化怎么办?(运行几个月后,内存碎片导致分配失败)
- 时间不确定性怎么办?(
malloc的最坏情况时间无法预测)
Embassy 通过完全消除堆分配来避免这些问题。所有资源在编译期确定大小,在 static 变量中分配。
原则三:基于 embedded-hal-async(标准化接口)#
Embassy 的 HAL 层实现了 embedded-hal-async trait(第 6 章介绍的异步版本):
// Embassy 的 I2C 实现了 embedded_hal_async::i2c::I2c
impl embedded_hal_async::i2c::I2c for embassy_stm32::i2c::I2c<'_, Async> {
type Error = Error;
async fn read(&mut self, address: u8, read: &mut [u8]) -> Result<(), Error> {
// 通过 DMA 异步读取,CPU 在等待期间可以执行其他任务
self.dma_read(address, read).await
}
async fn write(&mut self, address: u8, write: &[u8]) -> Result<(), Error> {
self.dma_write(address, write).await
}
async fn write_read(
&mut self,
address: u8,
write: &[u8],
read: &mut [u8],
) -> Result<(), Error> {
self.dma_write_read(address, write, read).await
}
}rust这意味着:任何依赖 embedded-hal-async 的第三方驱动,都可以在 Embassy 中直接使用。
Embassy 不是什么#
为了避免误解,明确 Embassy 不是什么:
| Embassy 不是 | 说明 |
|---|---|
| 不是 RTOS | 没有内核、没有调度器循环、没有上下文切换 |
| 不是 Linux | 没有进程、没有文件系统、没有系统调用 |
| 不是”Rust 的 FreeRTOS” | 编程模型完全不同(async vs 线程) |
| 不是万能的 | 不适合硬实时(用 RTIC)、不适合安全关键(暂无认证) |
| 不是”魔法” | 底层就是状态机 + 中断唤醒,你在第 5 章已经理解了 |
7.2 从超级循环到 async/await:一个完整对比#
场景:温湿度监测器#
让我们用一个完整的例子来对比三种实现方式。需求:
- 每 1 秒读取一次 BME280 传感器(I2C)
- 每 100ms 刷新一次 OLED 显示(SPI)
- 监听 UART 命令(“GET” → 返回当前数据)
- 每 5 秒通过 WiFi 上报一次数据
方案 A:C 超级循环 + 状态机#
// C:超级循环 + 手写状态机(~200 行,仅展示框架)
#include "stm32f4xx_hal.h"
#include "bme280.h"
#include "ssd1306.h"
// ========== 全局状态 ==========
typedef struct {
int16_t temperature;
int16_t humidity;
uint32_t last_update;
} SensorData;
static SensorData g_data = {0};
static volatile uint8_t uart_cmd_buf[32];
static volatile uint8_t uart_cmd_len = 0;
static volatile bool uart_cmd_ready = false;
// ========== 任务状态机 ==========
typedef enum { SENSE_IDLE, SENSE_READING, SENSE_DONE } SenseState;
typedef enum { DISP_IDLE, DISP_UPDATING } DispState;
typedef enum { RPT_IDLE, RPT_CONNECTING, RPT_SENDING } ReportState;
static SenseState sense_state = SENSE_IDLE;
static DispState disp_state = DISP_IDLE;
static ReportState report_state = RPT_IDLE;
static uint32_t sense_timer, disp_timer, report_timer;
// ========== 任务函数(每个只执行"一步") ==========
void task_sensor(void) {
switch (sense_state) {
case SENSE_IDLE:
if (millis() - sense_timer >= 1000) {
bme280_start_read(); // 启动 I2C 读取(非阻塞)
sense_state = SENSE_READING;
}
break;
case SENSE_READING:
if (bme280_read_done()) {
g_data.temperature = bme280_get_temp();
g_data.humidity = bme280_get_hum();
g_data.last_update = millis();
sense_timer = millis();
sense_state = SENSE_IDLE;
}
break;
}
}
void task_display(void) {
switch (disp_state) {
case DISP_IDLE:
if (millis() - disp_timer >= 100) {
ssd1306_clear();
ssd1306_printf("T: %d.%d C", g_data.temperature/10, g_data.temperature%10);
ssd1306_printf("H: %d.%d %%", g_data.humidity/10, g_data.humidity%10);
disp_timer = millis();
}
break;
}
}
void task_uart(void) {
if (uart_cmd_ready) {
uart_cmd_ready = false;
if (memcmp(uart_cmd_buf, "GET", 3) == 0) {
uart_printf("T=%d H=%d\n", g_data.temperature, g_data.humidity);
}
}
}
void task_report(void) {
switch (report_state) {
case RPT_IDLE:
if (millis() - report_timer >= 5000) {
wifi_connect();
report_state = RPT_CONNECTING;
}
break;
case RPT_CONNECTING:
if (wifi_connected()) {
wifi_send_json(g_data.temperature, g_data.humidity);
report_state = RPT_SENDING;
} else if (millis() - report_timer > 3000) {
report_state = RPT_IDLE;
}
break;
case RPT_SENDING:
if (wifi_send_done()) {
report_timer = millis();
report_state = RPT_IDLE;
}
break;
}
}
// ========== UART 中断 ==========
void USART2_IRQHandler(void) {
if (USART2->SR & USART_SR_RXNE) {
uint8_t byte = USART2->DR;
if (byte == '\n') {
uart_cmd_ready = true;
} else if (uart_cmd_len < 32) {
uart_cmd_buf[uart_cmd_len++] = byte;
}
}
}
// ========== 主循环 ==========
int main(void) {
system_init();
bme280_init();
ssd1306_init();
wifi_init();
while (1) {
task_sensor();
task_display();
task_uart();
task_report();
}
}c这段代码的问题:
- 状态爆炸:4 个任务 × 每个 2-4 个状态 = 需要管理 10+ 个状态变量
- 逻辑碎片化:
task_report()的”连接→发送→等待”逻辑被拆成 3 个case,难以理解完整流程 - 全局变量泛滥:
g_data、uart_cmd_buf等全局状态,任何函数都可以修改 - 时序耦合:如果
task_report()中的wifi_connect()变成阻塞的(等待 3 秒),所有其他任务都会卡住 - 新增任务困难:添加第 5 个任务需要:定义新状态枚举、写新状态机、加入主循环、处理与现有任务的交互
方案 B:C + FreeRTOS#
// C:FreeRTOS 版本(~100 行,逻辑更清晰)
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
static QueueHandle_t sensor_queue;
static QueueHandle_t uart_queue;
void sensor_task(void *params) {
TickType_t last_wake = xTaskGetTickCount();
while (1) {
int16_t temp = bme280_read_temp(); // 阻塞式 I2C 读取
int16_t hum = bme280_read_hum();
SensorData data = { .temp = temp, .hum = hum };
xQueueOverwrite(sensor_queue, &data);
vTaskDelayUntil(&last_wake, pdMS_TO_TICKS(1000));
}
}
void display_task(void *params) {
SensorData data;
while (1) {
if (xQueuePeek(sensor_queue, &data, pdMS_TO_TICKS(100)) == pdTRUE) {
ssd1306_clear();
ssd1306_printf("T: %d.%d C", data.temp/10, data.temp%10);
ssd1306_printf("H: %d.%d %%", data.hum/10, data.hum%10);
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}
void uart_task(void *params) {
uint8_t byte;
char cmd_buf[32];
uint8_t cmd_len = 0;
while (1) {
if (xQueueReceive(uart_queue, &byte, portMAX_DELAY) == pdTRUE) {
if (byte == '\n') {
if (strncmp(cmd_buf, "GET", 3) == 0) {
SensorData data;
xQueuePeek(sensor_queue, &data, 0);
uart_printf("T=%d H=%d\n", data.temp, data.hum);
}
cmd_len = 0;
} else {
cmd_buf[cmd_len++] = byte;
}
}
}
}
void report_task(void *params) {
SensorData data;
while (1) {
vTaskDelay(pdMS_TO_TICKS(5000));
if (xQueuePeek(sensor_queue, &data, 0) == pdTRUE) {
wifi_connect(); // 可能阻塞 3 秒——但不影响其他任务!
wifi_send_json(data.temp, data.hum);
wifi_disconnect();
}
}
}
// UART 中断:将字节送入队列
void USART2_IRQHandler(void) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
if (USART2->SR & USART_SR_RXNE) {
uint8_t byte = USART2->DR;
xQueueSendFromISR(uart_queue, &byte, &xHigherPriorityTaskWoken);
}
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
int main(void) {
system_init();
sensor_queue = xQueueCreate(1, sizeof(SensorData));
uart_queue = xQueueCreate(64, sizeof(uint8_t));
xTaskCreate(sensor_task, "sensor", 512, NULL, 2, NULL);
xTaskCreate(display_task, "display", 512, NULL, 1, NULL);
xTaskCreate(uart_task, "uart", 256, NULL, 3, NULL);
xTaskCreate(report_task, "report", 1024, NULL, 1, NULL);
vTaskStartScheduler();
while (1);
}c比超级循环好在哪里:
- 每个任务的逻辑是完整的(不需要拆成状态机)
wifi_connect()阻塞 3 秒不影响其他任务- 任务间通信通过队列,不需要全局变量
仍然存在的问题:
- 每个任务需要独立栈(512 + 512 + 256 + 1024 = 2304 字节 RAM)
- FreeRTOS 内核占用 ~10KB Flash
- 上下文切换有开销
- 栈溢出无保护(
sensor_task的 512 字节栈够用吗?只能靠经验猜) - 优先级配置需要仔细设计(优先级反转风险)
- C 语言无并发安全保证(共享数据仍需手动保护)
方案 C:Rust + Embassy#
// Rust:Embassy 版本(~60 行,逻辑最清晰)
#![no_std]
#![no_main]
use embassy_executor::Spawner;
use embassy_stm32::{self as hal, i2c::I2c, usart::Uart, spi::Spi, gpio::Output};
use embassy_sync::channel::Channel;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_time::{Duration, Ticker, Timer};
use defmt_rtt as _;
use panic_probe as _;
// 共享数据通道(编译期确定大小,无堆分配)
static SENSOR_CH: Channel = Channel::new();
static UART_CH: Channel = Channel::new();
#[derive(Clone, Copy)]
struct SensorData {
temperature: i16,
humidity: i16,
}
// 任务 1:读取传感器(每 1 秒)
#[embassy_executor::task]
async fn sensor_task(mut i2c: I2c<'static, hal::i2c::I2c1>) {
let mut ticker = Ticker::every(Duration::from_secs(1));
loop {
ticker.next().await; // 异步等待 1 秒(CPU 可以执行其他任务)
let mut buf = [0u8; 6];
i2c.write_read(0x76, &[0xF7], &mut buf).await.unwrap(); // DMA 异步读取
let data = SensorData {
temperature: parse_temp(&buf[3..6]),
humidity: parse_hum(&buf[0..3]),
};
SENSOR_CH.send(data).await;
}
}
// 任务 2:刷新显示(每 100ms)
#[embassy_executor::task]
async fn display_task(mut spi: Spi<'static, hal::spi::Spi1>, mut dc: Output<'static>) {
let mut ticker = Ticker::every(Duration::from_millis(100));
loop {
ticker.next().await;
if let Ok(data) = SENSOR_CH.try_receive() {
let msg = format!("T:{}.{:01}C H:{}.{:01}%",
data.temperature / 10, (data.temperature % 10).abs(),
data.humidity / 10, (data.humidity % 10).abs());
oled_write(&mut spi, &mut dc, msg.as_bytes()).await;
}
}
}
// 任务 3:处理 UART 命令
#[embassy_executor::task]
async fn uart_task(mut uart: Uart<'static, hal::usart::Usart2>) {
use embedded_io_async::Read;
let mut byte = [0u8; 1];
let mut cmd = [0u8; 32];
let mut len = 0;
loop {
uart.read(&mut byte).await.unwrap(); // 异步等待 UART 数据
if byte[0] == b'\n' {
if &cmd[..len] == b"GET" {
if let Ok(data) = SENSOR_CH.try_receive() {
let resp = format!("T={} H={}\n", data.temperature, data.humidity);
uart.write(resp.as_bytes()).await.unwrap();
}
}
len = 0;
} else if len < 32 {
cmd[len] = byte[0];
len += 1;
}
}
}
// 任务 4:上报数据(每 5 秒)
#[embassy_executor::task]
async fn report_task() {
loop {
Timer::after(Duration::from_secs(5)).await; // 异步等待 5 秒
if let Ok(data) = SENSOR_CH.try_receive() {
// WiFi 操作也是异步的——等待期间其他任务正常运行
wifi_connect().await;
wifi_send_json(data.temperature, data.humidity).await;
wifi_disconnect().await;
}
}
}
// 主入口:初始化 + 启动所有任务
#[embassy_executor::main]
async fn main(spawner: Spawner) {
let p = hal::init(Default::default());
// 配置外设
let i2c = I2c::new(p.I2C1, p.PB6, p.PB7, p.DMA1_CH0, p.DMA1_CH1,
hal::i2c::Config::default());
let spi = Spi::new(p.SPI1, p.PA5, p.PA7, p.PA6, p.DMA2_CH3, p.DMA2_CH0,
hal::spi::Config::default());
let uart = Uart::new(p.USART2, p.PA3, p.PA2, p.DMA1_CH5, p.DMA1_CH6,
hal::usart::Config::default());
let dc = Output::new(p.PA4, hal::gpio::Level::Low, hal::gpio::Speed::Low);
// 启动所有任务(非阻塞,立即返回)
spawner.spawn(sensor_task(i2c)).unwrap();
spawner.spawn(display_task(spi, dc)).unwrap();
spawner.spawn(uart_task(uart)).unwrap();
spawner.spawn(report_task()).unwrap();
// main 本身也是一个任务,可以做其他事
loop {
Timer::after(Duration::from_secs(60)).await;
defmt::info!("System alive for 60s");
}
}rust三种方案的逐维度对比#
| 维度 | C 超级循环 | C + FreeRTOS | Rust + Embassy |
|---|---|---|---|
| 代码行数 | ~200 行 | ~100 行 | ~60 行 |
| 逻辑完整性 | ❌ 碎片化(状态机) | ✅ 完整(每任务一个函数) | ✅ 完整(每任务一个 async fn) |
| 阻塞安全 | ❌ 阻塞会卡死全部 | ✅ 阻塞只影响当前任务 | ✅ 阻塞只影响当前任务 |
| RAM 开销 | ~200B(全局变量) | ~2.5KB(任务栈)+ 内核 | ~500B(Future 结构体) |
| Flash 开销 | ~4KB | ~14KB(+内核) | ~6KB(+运行时) |
| 并发安全 | ❌ 无保证 | ❌ 无保证(C 语言) | ✅ 编译器保证(借用检查) |
| 栈溢出风险 | 无(单栈) | ⚠️ 有(每任务独立栈) | 无(共享栈,编译期确定) |
| 新增任务 | 困难(新状态机) | 容易(新 xTaskCreate) | 容易(新 async fn + spawn) |
| 调试难度 | 低(单线程) | 高(多线程竞态) | 中(单线程协作式) |
| 实时性 | 差 | 好 | 好(需配合 InterruptExecutor) |
| 可移植性 | ❌ 绑定 HAL | ❌ 绑定 HAL + RTOS | ✅ embedded-hal-async |
关键洞察:Embassy 代码 = 顺序写法 + 状态机语义#
仔细看 Embassy 版本的 report_task:
async fn report_task() {
loop {
Timer::after(Duration::from_secs(5)).await; // ① 等待 5 秒
if let Ok(data) = SENSOR_CH.try_receive() {
wifi_connect().await; // ② 等待连接
wifi_send_json(data).await; // ③ 等待发送完成
wifi_disconnect().await; // ④ 等待断开
}
}
}rust这段代码读起来像是顺序执行的:等 5 秒 → 连接 → 发送 → 断开。
但实际执行时,每个 .await 都是一个”让出点”——CPU 在等待期间去执行其他任务。
对比 C 超级循环的状态机:
// C 状态机(等价逻辑)
void task_report(void) {
switch (report_state) {
case RPT_IDLE: // ← 对应 ① 之前的等待
if (millis() - report_timer >= 5000) {
wifi_connect();
report_state = RPT_CONNECTING;
}
break;
case RPT_CONNECTING: // ← 对应 ② 的等待
if (wifi_connected()) {
wifi_send_json(data);
report_state = RPT_SENDING;
}
break;
case RPT_SENDING: // ← 对应 ③ 的等待
if (wifi_send_done()) {
wifi_disconnect();
report_state = RPT_DISCONNECTING;
}
break;
case RPT_DISCONNECTING: // ← 对应 ④ 的等待
if (wifi_disconnected()) {
report_timer = millis();
report_state = RPT_IDLE;
}
break;
}
}cEmbassy 的 async/await 就是编译器自动生成的这种状态机。 你写 4 行顺序代码,编译器生成 4 个状态的 switch/case。你不需要手动管理 report_state 变量——编译器帮你做了。
这就是为什么 Embassy 被称为”零开销”——它没有引入任何新的运行时机制,只是让编译器替你写了你本来就要手写的代码。
7.3 Embassy 的代价与权衡#
诚实的代价清单#
Embassy 不是免费的午餐。采用它需要付出以下代价:
代价一:学习曲线#
| 你需要学的 | 难度 | 时间估计 |
|---|---|---|
| Rust 基础(所有权、trait、泛型) | ⭐⭐⭐ | 2-4 周 |
| async/await 概念 | ⭐⭐⭐ | 1-2 周 |
| Embassy API(HAL、Executor、同步原语) | ⭐⭐ | 1-2 周 |
| 调试异步代码 | ⭐⭐⭐ | 持续 |
| 总计(从 C 零基础开始) | 6-10 周 |
对比:学 FreeRTOS 大约需要 2-3 周(如果你已经会 C)。
缓解策略:本书的设计就是为了解决这个问题——Part 1 覆盖 Rust 基础,Part 2 覆盖 Embassy,每章都有 C 对比。
代价二:工具链要求#
Embassy 开发环境:
├── Rust 工具链(rustup + cargo) ~500 MB
├── target: thumbv7em-none-eabihf ~200 MB
├── probe-rs(调试/烧录) ~50 MB
├── VSCode + rust-analyzer + probe-rs 插件 ~300 MB
└── 总计 ~1 GB
对比 Keil MDK:
├── Keil 安装包 ~2 GB
├── 芯片 Pack ~100 MB
└── 总计 ~2.1 GBplaintext工具链体积相当,但 Rust 工具链是跨平台的(Windows/macOS/Linux),而 Keil 只支持 Windows。
代价三:编译时间#
| 项目规模 | C(Keil/GCC) | Rust(cargo build) | Rust(cargo build —release) |
|---|---|---|---|
| 空项目 | ~1s | ~2s | ~3s |
| 中等项目(~5K 行) | ~5s | ~15s | ~30s |
| 大项目(~20K 行) | ~15s | ~45s | ~90s |
| 增量编译(改 1 行) | ~2s | ~5s | ~10s |
Rust 的编译时间确实比 C 长——这是 LLVM 优化和泛型单态化的代价。但对于嵌入式项目(通常 < 50K 行),这个差异在日常开发中可以接受。
缓解策略:
- 开发时用
cargo build(debug,快) - 只在烧录测试时用
cargo build --release(慢但优化好) - 使用
sccache缓存编译结果 - 使用
mold链接器加速链接阶段
代价四:调试体验#
| 调试操作 | C(Keil/GDB) | Rust(probe-rs + VSCode) |
|---|---|---|
| 设置断点 | ✅ 成熟 | ✅ 支持 |
| 单步执行 | ✅ 成熟 | ✅ 支持 |
| 查看变量 | ✅ 成熟 | ✅ 支持(但 async fn 内部变量有时显示不完整) |
| 查看寄存器 | ✅ 成熟 | ✅ 支持 |
| 调用栈 | ✅ 清晰 | ⚠️ async 任务的调用栈是”展平”的,不如 C 直观 |
| 实时表达式 | ✅ 成熟 | ⚠️ 有限支持 |
| 逻辑分析仪集成 | ✅ 成熟(Keil + Logic) | ❌ 需要外部工具 |
诚实评价:Rust 的嵌入式调试体验在 2026 年已经可用,但尚未达到 Keil 的成熟度。最大的痛点是 async 任务的调试——因为一个 async fn 被编译为状态机,断点行为和 C 的线性代码不同。
缓解策略:
- 大量使用
defmt日志(比断点更适合异步代码) - 使用
probe-rs的 RTT 日志功能 - 对于复杂问题,可以临时将 async 代码改为阻塞式调试
代价五:生态成熟度#
| 方面 | C 生态(STM32) | Rust 生态(Embassy) |
|---|---|---|
| 芯片覆盖 | ✅ 所有型号 | ⚠️ 主流型号覆盖,冷门型号可能缺失 |
| 外设覆盖 | ✅ 所有外设 | ⚠️ 常用外设覆盖,冷门外设可能需要 PAC 直接操作 |
| 第三方库 | ✅ 极其丰富 | ⭐⭐⭐ 快速增长中,但仍有缺口 |
| 参考设计 | ✅ 厂商提供 | ⚠️ 社区提供,质量参差 |
| 认证(ISO/IEC) | ✅ 有 | ❌ 暂无(Ferrocene 在推进) |
| 技术支持 | ✅ 厂商 FAE | ⚠️ 社区(Matrix/GitHub) |
| 文档 | ✅ 完整(数据手册+参考手册) | ⭐⭐⭐ 改善中,但仍有缺口 |
诚实评价:如果你使用的是 STM32F4/F7/H7、nRF52/53、RP2040、ESP32 这些主流芯片,Embassy 的覆盖已经足够。但如果你使用的是冷门芯片或需要特殊外设(如某些加密加速器),可能需要自己写 PAC 级代码。
代价六:async 的”传染性”#
Rust 的 async 有一个被称为”函数颜色问题”(function coloring problem)的特性:
// 一旦某个函数是 async 的,调用它的函数也必须是 async 的
async fn read_sensor() -> i16 { /* ... */ }
// ❌ 不能在非 async 函数中调用 async 函数
fn sync_function() {
let temp = read_sensor(); // 编译错误!
}
// ✅ 必须在 async 函数中调用
async fn async_function() {
let temp = read_sensor().await; // 正确
}rust这意味着:一旦你选择使用 Embassy,你的大部分代码都需要是 async 的。 这不是一个可以”局部采用”的方案——它是全栈的。
对比 C:在 C 中,你可以在超级循环中局部使用 RTOS(只把部分任务放到 RTOS 中)。Embassy 没有这种”渐进式迁移”的路径。
权衡总结:什么时候该用 Embassy,什么时候不该?#
| 场景 | 建议 | 理由 |
|---|---|---|
| 新项目,I/O 密集型(传感器+通信+显示) | ✅ 用 Embassy | 最佳匹配 |
| 新项目,需要 WiFi/BLE/USB | ✅ 用 Embassy | 唯一有完整协议栈的 Rust 框架 |
| 新项目,硬实时(电机控制 < 10μs 周期) | ⚠️ 考虑 RTIC | 编译期保证更严格 |
| 已有 C 项目,需要维护 5 年以上 | ⚠️ 谨慎评估 | 迁移成本高,团队学习曲线 |
| 安全关键(汽车/医疗/航空) | ❌ 暂不适合 | 无认证,工具链未鉴定 |
| 极简资源(< 16KB Flash) | ⚠️ 评估 | Embassy 运行时 ~2-5KB,可能占比过大 |
| 团队只有 C 经验,项目紧急 | ❌ 不建议 | 学习曲线会拖慢进度 |
| 团队愿意投资学习,追求长期质量 | ✅ 强烈推荐 | 编译器捕获的 bug 远超学习成本 |
7.4 Embassy 项目的高层结构#
一个 Embassy 项目的完整文件结构#
my_embassy_project/
├── Cargo.toml ← 依赖声明
├── Cargo.lock ← 版本锁定
├── memory.x ← 内存布局
├── build.rs ← 构建脚本
├── .cargo/
│ └── config.toml ← target + runner + rustflags
├── rust-toolchain.toml ← Rust 工具链版本锁定(可选)
└── src/
├── main.rs ← 入口 + 任务定义
├── sensors/ ← 业务逻辑模块
│ ├── mod.rs
│ └── bme280.rs
├── display/
│ ├── mod.rs
│ └── ssd1306.rs
└── comms/
├── mod.rs
└── wifi.rsplaintextCargo.toml:Embassy 项目的依赖#
[package]
name = "my_embassy_project"
version = "0.1.0"
edition = "2021"
[dependencies]
# ===== Embassy 核心 =====
embassy-executor = { version = "0.7", features = ["arch-cortex-m", "executor-thread", "defmt"] }
embassy-time = { version = "0.4", features = ["defmt"] }
embassy-sync = { version = "0.6", features = ["defmt"] }
# ===== 芯片 HAL =====
embassy-stm32 = { version = "0.2", features = [
"stm32f407vg", # 芯片型号
"memory-x", # 使用 memory.x
"time-driver-any", # 自动选择定时器作为时间驱动
"defmt", # 日志支持
] }
# ===== Cortex-M 支持 =====
cortex-m = { version = "0.7", features = ["critical-section-single-core"] }
cortex-m-rt = "0.7"
# ===== 日志 =====
defmt = "0.3"
defmt-rtt = "0.4"
# ===== Panic 处理 =====
panic-probe = { version = "0.3", features = ["print-defmt"] }
# ===== 异步 I/O 接口 =====
embedded-hal-async = "1.0"
embedded-io-async = "0.6"
[profile.release]
opt-level = "s"
lto = true
codegen-units = 1
panic = "abort"
debug = 2 # 保留调试信息(probe-rs 需要)toml与第 3 章裸机工程的对比#
| 文件/配置 | 第 3 章裸机工程 | Embassy 工程 | 新增原因 |
|---|---|---|---|
Cargo.toml | 5 个依赖 | ~12 个依赖 | Embassy 运行时 + HAL + 同步原语 |
src/main.rs | #[entry] fn main() | #[embassy_executor::main] async fn main() | 入口宏不同 |
| 任务定义 | 无(单线程) | #[embassy_executor::task] async fn | 多任务支持 |
| 时间驱动 | 无 | time-driver-any feature | Embassy 需要硬件定时器作为时钟源 |
| Panic 处理 | panic-halt(死循环) | panic-probe(输出日志后死循环) | 更好的调试体验 |
| Flash 占用 | ~3 KB | ~6-8 KB | Embassy 运行时 + 异步基础设施 |
| RAM 占用 | ~1 KB | ~2-4 KB | 任务 Future + 通道缓冲区 |
#[embassy_executor::main] 做了什么?#
#[embassy_executor::main]
async fn main(spawner: Spawner) {
// 你的初始化代码
}rust这个宏展开后,大致等价于:
// 宏展开后的等价代码(简化)
#[cortex_m_rt::entry]
fn main() -> ! {
// 1. 初始化 Embassy 运行时
// - 配置时间驱动(选择一个硬件定时器)
// - 初始化 Executor 的任务队列
// - 设置 Waker 机制
// 2. 创建 Spawner(任务启动器)
let spawner = Spawner::new();
// 3. 将你的 async fn main 作为第一个任务启动
// 注意:async fn 被编译为 Future 结构体
// 这个结构体是静态分配的,不需要堆
static MAIN_FUTURE: StaticCell> = StaticCell::new();
let future = MAIN_FUTURE.init(async move {
// 你的 main 函数体
});
spawner.spawn(future);
// 4. 进入 Executor 主循环
// 这就是"调度器"——但它不是一个 OS 内核
// 它只是反复调用每个就绪 Future 的 poll() 方法
loop {
executor.run(); // 轮询所有就绪的 Future
// 如果没有就绪的 Future,进入 WFI(低功耗等待)
cortex_m::asm::wfi();
}
}rust关键理解:
#[embassy_executor::main]不是”启动一个操作系统”——它只是设置了一个事件循环Spawner不是”创建线程”——它只是把一个 Future 加入就绪队列- Executor 主循环不是”调度器”——它只是反复调用
poll() - 没有上下文切换、没有独立栈、没有内核对象
Executor 的内部工作原理(poll 机制、Waker、时间驱动),见第 9 章。
Embassy 的 Feature Flag 体系#
Embassy 使用大量 feature flag 来控制编译内容:
# embassy-stm32 的常用 feature
[dependencies.embassy-stm32]
features = [
"stm32f407vg", # 芯片型号(必选)
"memory-x", # 使用外部 memory.x(必选)
"time-driver-any", # 时间驱动:自动选择定时器
# "time-driver-tim2", # 或者:指定使用 TIM2
"defmt", # 日志支持
"unstable-pac", # 暴露 PAC(用于 HAL 未覆盖的外设)
# "exti", # 外部中断支持
# "low-power", # 低功耗模式支持
]
# embassy-executor 的常用 feature
[dependencies.embassy-executor]
features = [
"arch-cortex-m", # 目标架构(必选)
"executor-thread", # 线程模式 Executor(必选)
# "executor-interrupt", # 中断模式 Executor(用于抢占式任务)
"defmt", # 日志支持
# "nightly", # 使用 nightly 特性(不推荐)
]toml与 C 的 #ifdef 对比:
| C 的做法 | Embassy 的做法 |
|---|---|
#ifdef STM32F407xx | features = ["stm32f407vg"] |
#ifdef USE_HAL_DRIVER | 依赖 embassy-stm32 crate |
#ifdef DEBUG | cfg(debug_assertions)(自动) |
Makefile 中的 -D 定义 | Cargo.toml 中的 features |
| 条件编译散落在代码中 | 集中在 Cargo.toml 中声明 |
7.5 本章小结#
核心认知#
-
Embassy 不是 RTOS——它是一个编译期代码生成框架,把 async/await 转换为状态机。没有内核、没有上下文切换、没有独立任务栈。
-
Embassy 的”零开销”是可验证的——编译后的机器码与手写状态机几乎相同。抽象的成本为零,安全性是免费的。
-
Embassy 的代价是真实的——学习曲线(6-10 周)、编译时间、调试体验、生态成熟度、async 传染性。这些代价需要诚实面对。
-
Embassy 最适合 I/O 密集型场景——传感器读取、通信协议、显示刷新、网络通信。对于硬实时场景(< 10μs 周期),RTIC 可能更合适。
-
Embassy 是全栈方案——一旦采用,你的大部分代码都将是 async 的。这不是一个可以”局部试用”的方案。
从本章到后续章节的路线图#
第 7 章(本章):Embassy 是什么?为什么选它?代价是什么?
│
▼
第 8 章:动手!第一个 Embassy 工程
│ GPIO、UART、SPI、I2C、Timer 的异步 API
│ 与 C HAL 的逐 API 对比
│
▼
第 9 章:Executor 的内部原理
│ Future、poll、Waker、时间驱动
│ 反汇编验证"零开销"
│
▼
第 10 章:任务间通信
│ Channel、Signal、Mutex、PubSub
│ 与 FreeRTOS 队列/信号量的对比
│
▼
第 11 章:完整项目实战
传感器 + 显示 + 通信 + OTAplaintext一个思维转变#
如果你只记住本章的一件事:
在 C 中,你告诉 CPU “做什么”(写寄存器、调用函数)。 在 Embassy 中,你告诉 CPU “等什么”(
.await),CPU 自己决定在等待期间做什么。
这个思维转变——从”命令式”到”声明式”——是从 C 到 Embassy 最核心的认知跳跃。一旦你接受了”我不需要控制每一微秒 CPU 在做什么,我只需要声明我的任务在等什么”,Embassy 的编程模型就会变得极其自然。
下一章:第 8 章——第一个 Embassy 工程:GPIO、UART、SPI、I2C 与 Timer。我们将真正动手,用 Embassy 点亮 LED、读取传感器、驱动显示器。