Embassy Rust 嵌入式异步运行时深度工程:从 ARM Cortex-M 中断调度到零成本执行模型
在嵌入式系统开发中,Rust 的所有权模型和无畏并发(Fearless Concurrency)为资源受限设备提供了内存安全保证。Embassy 是专为嵌入式场景设计的 Rust 异步运行时,它放弃了传统 RTOS 的线程模型,采用单线程异步执行器 + 中断驱动的方式,在 STM32、nRF52、RP2040 等微控制器上实现了亚微秒级唤醒延迟和 RAM 占用低于 2KB 的极致轻量化。本文将深入剖析 Embassy 的架构设计,涵盖其无堆分配(no-alloc)异步任务模型、中断与 async/await 的融合机制、零成本外设抽象,以及如何在实际物联网项目中构建低功耗数据采集系统。
一、为什么嵌入式需要异步运行时
传统嵌入式开发使用裸机循环或 RTOS 多线程模型。前者无法并发,后者带来栈空间开销和上下文切换成本。在 ARM Cortex-M3/M4 上,RTOS 线程切换通常需要 80-200 个时钟周期(约 1-3us @ 100MHz),每个线程需要独立栈空间(通常 512B-2KB)。
Embassy 的核心突破在于:将中断与 async/await 统一,用单线程异步执行器替代多线程调度,用编译期状态机替代运行时调度表。
性能对比(STM32F407 @ 168MHz):
| 指标 | FreeRTOS | Embassy (embassy-rs) |
|---|---|---|
| 任务切换延迟 | 1.5us | 0.3us(纯状态机跳转) |
| 单任务栈开销 | 512B | 0(编译期确定) |
| RAM 总计 (10 tasks) | ~8KB | ~1.2KB |
| CPU 唤醒到执行延迟 | 3.2us | 0.8us |
| 中断安全抽象 | 手动 | 编译期保证 |
二、无堆分配的任务模型
Embassy 不使用全局分配器,所有静态任务通过 #[embassy_executor::task] 过程宏编译期生成状态机:
// 静态任务声明:编译期分配,无需 Box
#[embassy_executor::task]
async fn sensor_reader(mut i2c: I2c<'static, DMA1_CH6, DMA1_CH0>) {
let mut buf = [0u8; 6];
loop {
// 等待定时器触发(非阻塞)
Timer::after_millis(100).await;
// 等待 I2C 读取完成(非阻塞)
i2c.read(0x68, &mut buf).await.unwrap();
// 处理数据
let data = SensorData::parse(&buf);
// 发送到消息通道(非阻塞)
SENSOR_CH.sender().send(data).await;
}
}
编译后展开:
// #[embassy_executor::task] 宏生成的伪代码
struct SensorReaderFuture {
state: u8,
i2c: I2c<'static, DMA1_CH6, DMA1_CH0>,
buf: [u8; 6],
timer_delay: Timer,
i2c_read: I2cReadFuture<'static>,
channel_send: SendFuture<'static, SensorData>,
}
impl Future for SensorReaderFuture {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
loop {
match self.state {
0 => {
self.timer_delay = Timer::after_millis(100);
self.state = 1;
}
1 => {
let waker = cx.waker().clone();
// 将 waker 注册到定时器唤醒队列
register_timer_wake(&mut self.timer_delay, waker);
// 委托 poll 给内部 Timer future
if let Poll::Pending = self.timer_delay.poll(cx) {
return Poll::Pending;
}
self.state = 2;
}
// ... 后续状态
}
}
}
}
关键优势:Future 大小在编译期确定,通过静态 mut 数组存储,无需堆分配。
三、中断与 async/await 的融合:Lazy Interrupt Handling
Embassy 最精妙的设计是将硬件中断转化为 async 等待。当任务调用 wait_for_high().await 时,Embassy 会自动配置 GPIO 中断并挂起任务,中断触发时只设置标志位和唤醒任务,实际处理逻辑仍在 async 函数中顺序执行。
// GPIO 边沿触发等待(中断驱动但代码顺序执行)
#[embassy_executor::task]
async fn button_handler(mut gpio:Input<'static, PB3>) {
loop {
// 等待引脚变低(配置中断,挂起任务到等待队列)
gpio.wait_for_low().await;
// 中断已经触发,debounce 处理
// 注意:这里不是在 ISR 上下文中!
if debounce_check(&mut gpio, 50) {
BUTTON_EVENT.signal(());
}
}
}
底层实现:
impl<'d, T: Pin> Input<'d, T> {
pub async fn wait_for_low(&mut self) {
// 1. 配置 GPIO 中断(上升沿)
self.pin.enable_interrupt();
self.pin.set_interrupt_trigger(Trigger::Falling);
// 2. 创建等待 future
let future = GpioWaitFuture {
pin: self.pin,
state: GpioWaitState::NotTriggered,
};
// 3. 等待期间可以执行其他任务
future.await;
// 4. 中断已经触发,恢复执行
self.pin.disable_interrupt();
}
}
// 中断处理程序:极简,只唤醒任务
#[cortex_m_rt::interrupt]
fn EXTI3() {
// 清除中断标志(硬件要求)
clear_interruptPending(EXTI3);
// 唤醒等待此中断的任务
embassy_stm32::exti::EXTI3.onexti(|wake_state| {
wake_state.waker.wake();
});
}
这种设计的工程意义:ISR 中几乎无业务逻辑,避免了在 ISR 上下文中使用互斥锁的复杂性;业务逻辑仍在普通 async 上下文执行,可使用完整的 Rust 类型系统。
四、零成本外设异步抽象
Embassy 的外设模型通过 embedded-hal-async trait 实现抽象。每个外设的异步操作返回一个实现了 Future 的结构体:
// USART 异步写入(非阻塞)
pub struct Uart<'d, T: Instance, TxDma, RxDma> {
tx: TxDmaInner<'d, T, TxDma>,
rx: RxDmaInner<'d, T, RxDma>,
}
impl<'d, T: Instance, TxDma, RxDma> embedded_hal_async::serial::Write
for Uart<'d, T, TxDma, RxDma>
{
/// 异步写入,依赖 DMA 完成后唤醒
async fn write(&mut self, buf: &[u8]) -> Result<(), Self::Error> {
// 1. 启动 DMA 传输
self.tx.transfer_start(buf);
// 2. 等待 DMA 传输完成(注册到中断等待队列)
TxDmaInterrupt::new().wait_for_transfer_complete().await;
// 3. 等待 USART TXE(Transmit Data Register Empty)
self.tx.wait_for_tx_empty().await;
Ok(())
}
}
高级技巧:使用 PeripheralRef 安全共享外设
// 编译期借用检查确保 DMA 通道单所有者
static I2C_BUS: StaticCell<I2c<'static, ...>> = StaticCell::new();
#[embassy_executor::main]
async fn main(spawner: Spawner) {
let p = embassy_stm32::init(Default::default());
let i2c = I2c::new(
p.I2C1, p.PB6, p.PB7,
Irqs, // 中断
p.DMA1_CH6, p.DMA1_CH0,
Hertz(400_000),
Default::default(),
);
let i2c_ref = I2C_BUS.init(i2c);
// 从持有 I2C 总线的不同任务调用
spawner.spawn(sensor_reader(i2c_ref)).unwrap();
spawner.spawn(eeprom_writer(i2c_ref)).unwrap();
}
五、实时通道与无锁通信
Embassy 实现了一套适配 Async 运行时的无锁通道,支持 MPMC(多生产者多消费者):
// 编译期容量确定
type SensorDataCh = Channel<CriticalSectionRawMutex, SensorData, 16>;
static SENSOR_CH: SensorDataCh = Channel::new();
// 生产者(传感器读取任务)
#[embassy_executor::task]
async fn producer(id: u8, ch: Sender<'static, CriticalSectionRawMutex, SensorData, 16>) {
let mut counter = 0u32;
loop {
let data = SensorData { id, value: read_adc() };
// async send:失败时等待
ch.send(data).await;
counter += 1;
Timer::after_millis(100).await;
}
}
// 消费者(数据处理任务)
#[embassy_executor::task]
async fn consumer(ch: Receiver<'static, CriticalSectionRawMutex, SensorData, 16>) {
loop {
// async receive:阻塞等待数据
let data = ch.receive().await;
// 处理数据
process(data);
}
}
Signal 用于事件通知(无负载):
// 跨任务事件信号(类似 binary semaphore)
static BUTTON_SIGNAL: Signal<CriticalSectionRawMutex, ()> = Signal::new();
fn isr_button_press() {
// 任意上下文可触发信号
BUTTON_SIGNAL.signal(());
}
async fn button_waiter() {
// 等待信号
BUTTON_SIGNAL.wait().await;
// 处理事件
}
六、低功耗数据采集系统设计
在电池供电的物联网设备中,Embassy 利用 Cortex-M 的 WFI(Wait For Interrupt)指令实现自动休眠:
// 当所有任务都在 await 点时,执行器自动进入 WFI 休眠
#[embassy_executor::main]
async fn main(spawner: Spawner) {
// 初始化外设
let p = embassy_stm32::init(Default::default());
let i2c = init_i2c(p);
let radio = init_radio(p);
let power = PowerManager::new(p);
// 启动各功能任务
spawner.spawn(sensor_task(i2c)).unwrap();
spawner.spawn(radio_tx(radio)).unwrap();
spawner.spawn(power_management(power)).unwrap();
}
功耗管理任务示例:
struct PowerManager {
battery_mv: u32,
low_power_threshold: u32,
}
#[embassy_executor::task]
async fn power_management(mut pm: PowerManager) {
loop {
pm.battery_mv = read_battery_voltage();
if pm.battery_mv < pm.low_power_threshold {
// 进入低功耗模式:降低采样频率
LOW_POWER_MODE.signal(());
// 让系统停运更长时间
Timer::after_secs(60).await;
} else {
Timer::after_secs(10).await;
}
}
}
七、生产实践:基于 STM32WL55 的 LoRaWAN 网关
Embassy 实际应用在 STM32WL55(Cortex-M0+/M4 双核)LoRaWAN 网关开发中:
#[embassy_executor::task]
async fn lora_rx_handler(mut radio: Sx1262<'static>) -> ! {
let mut rx_buf = [0u8; 256];
// 配置 RX 模式
radio.set_rx( RxConfig::default()
.set_freq(Hertz(868_100_000))
.set_bandwidth(Bandwidth::BW125)
.set_modulation(Modulation::LoRa)
).await.unwrap();
loop {
// 等待 RX 中断(挂起任务,设备休眠)
let pkt_len = radio.read(&mut rx_buf).await.unwrap();
// 处理接收
let pkt = LoRaPacket::parse(&rx_buf[..pkt_len]);
// 发送到网络栈
NETWORK_STACK.incoming(pkt).await;
}
}
#[embassy_executor::task]
async fn lora_tx_handler(mut radio: Sx1262<'static>) -> ! {
loop {
// 等待发送请求
let data = TX_CH.receive().await;
// CAD(Channel Activity Detection)避免冲突
radio.do_cad().await;
// 发送
radio.write(&data).await.unwrap();
// 等待发送完成中断
DCA_DONE.wait().await;
}
}
硬件资源占用实测:
- Flash: ~180KB( Embassy 内核 + LoRaWAN 栈 + 应用代码)
- SRAM: ~42KB(全局缓冲区 + 静态任务 + 栈)
- 深度睡眠功耗: 2.1uA(RTC 运行时)
- 接收唤醒到首字节处理: 120us
八、调试与可观测性
Embassy 内置了专为嵌入式场景设计的轻量级日志系统:
// 嵌入时日志(通过 RTT 或 SWO trace 输出)
use embassy_boot::embassy_log::info;
#[embassy_executor::task]
async fn debug_monitor() {
loop {
// 监控各任务的高水位点
let free_heap = ALLOCATOR.free_size();
info!("Free: {} bytes", free_heap);
// 任务状态监控
let task_states = embassy_executor::dump_task_states();
for state in task_states {
info!("Task {:?}: {:?}", state.name, state.status);
}
Timer::after_secs(5).await;
}
}
使用 probe-rs 进行 SWD(Serial Wire Debug)在线调试:
# 烧录并启动调试
probe-rs run --chip STM32F407VGTx target/thumbv7em-none-eabihf/firmware
# RTT 日志输出
probe-rs attach --chip STM32F407VGTx target/thumbv7em-none-eabihf/rtt-log
九、总结
Embassy 为嵌入式 Rust 开发带来了革命性的变化:通过 async/await 中断融合,在资源受限设备上实现了代码复杂性的大幅降低,同时保持了裸机级别的性能。其无堆分配的任务模型和编译期借用检查,杜绝了堆碎片化和数据竞争两大嵌入式顽疾。
对于从 RTOS 迁移的团队,核心心智模型转变是:放弃线程栈思维,拥抱状态机跳转。 任务不在"后台运行",而是在 await 点让出执行权。这种转变初看受限,实则更贴合中断驱动的本质,让开发者以顺序思维编写并发代码。
随着 embedded-hal-async trait 的成熟和更多 MCU port 的加入,Embassy 有望成为嵌入式 Rust 的事实标准运行时,特别是在 IoT 边缘计算和超低功耗传感器场景中具有不可替代的优势。

发表评论 取消回复