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 边缘计算和超低功耗传感器场景中具有不可替代的优势。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部