如何实现嵌入式异步?

解读

在国内嵌入式 Rust 岗位面试中,面试官问“如何实现嵌入式异步”并不是想听你背 tokio 源码,而是考察三点:

  1. 是否理解 “无标准库、无堆、中断+裸机” 的极端约束;
  2. 能否把 Rust 异步零成本抽象落地到 cortex-m、RISC-V 等裸机 MCU
  3. 是否掌握 ** embassy、rtic、async-embedded-hal** 等国内主流开源框架的实战套路。
    回答必须围绕“编译期静态分配 + 事件驱动 + 零成本状态机”展开,否则会被直接判定为“只玩过 PC 端异步”。

知识点

  1. 裸机异步三要素

    • 无 alloc:Future 状态机必须 #[repr(C)] 固定大小,编译期放入 .bss
    • 单线程:无需 Send/Sync,但中断要当“并发源”,需 Critical Section 保护共享资源;
    • 无系统时钟:依赖 cortex-m systick 或 RTC 实现 embassy-time 的 Ticker/Timer
  2. ** embassy 执行器模型**

    • executor-arch 宏在 build.rs 阶段根据芯片型号生成 IRQ-driven 调度表
    • 任务通过 #[embassy::main] 挂载到 中断向量表SWI0_Handler,中断唤醒即 poll
    • Waker 由 原子位图 实现,仅 32 bit 静态变量,满足 CMSIS 单核 MCU 的 lock-free 要求。
  3. RTIC 2.x 的异步扩展

    • 利用 cortex-m 的 NVIC 硬件优先级 做“免临界区”资源管理;
    • #[shared] 资源在编译期生成 RAII 锁 token,异步任务通过 async fn(lock) 零开销访问;
    • 中断向量即“最高优先级任务”,天然满足 硬实时 需求,国内汽车 ECU 项目普遍采用。
  4. 异步硬件抽象层

    • embedded-hal-async 把 SPI/I2C 读写抽象成 async fn read(&mut self, buf: &mut [u8])
    • DMA 完成中断直接 wake waker,避免传统裸机“状态机+flag”的冗余切换;
    • 国内芯片厂(沁恒 CH32、乐鑫 ESP32-C3)已官方发布 eh1-async 实现,面试时点名可加分。
  5. 工具链与调试

    • cargo-embed + probe-rs 一键烧录,支持 defmt 非阻塞日志,打印 Future 状态机编号;
    • flip-link.uninit 放到 RAM 末尾,防止 stack-overflow 踩任务块
    • 面试常问“如何抓异步死锁”——答:开启 embassy-tracetask_list 快照,结合 ITM/SWO 输出。

答案

在裸机 MCU 上实现 Rust 异步,核心是把 Future 状态机编译成 静态固定大小结构体,由 embassy 或 RTIC 提供的 零开销执行器 完成调度,全过程不依赖堆和线程。步骤如下:

  1. 选型:芯片需带 单核 Cortex-M0+/M33 或 RISC-V,RAM ≥ 32 kB,中断数 ≥ 8;
  2. 依赖:在 Cargo.toml 引入 embassy-stm32/esp32/ch32 系列 PAC 与 embassy-executor,关闭 stdalloc feature;
  3. 任务定义:
    #[embassy_executor::task]
    async fn blink(led: PIN<'_, Output>) {
        let mut ticker = Ticker::every(Duration::from_millis(500));
        loop {
            led.toggle();
            ticker.next().await;   // 编译后仅为 systick 比较寄存器更新
        }
    }
    
  4. 执行器初始化:
    #[embassy_executor::main]
    async fn main(spawner: Spawner) {
        let p = embassy_stm32::init(Default::default());
        spawner.spawn(blink(p.PA5)).unwrap();
    }
    
  5. 中断唤醒:DMA 传输完成回调里调用 Waker::wake(),执行器在 pendSV 里一次 poll 完成上下文切换,无堆栈保存开销
  6. 资源冲突:使用 critical_section::with(|cs| { ... }) 或 RTIC 的 优先级天花板 保证内存安全,编译期即可发现死锁。

最终镜像尺寸 < 64 kB,RAM 占用 < 8 kB,中断延迟 < 1 µs,满足国内车载、工业、IoT 的硬实时要求。

拓展思考

  1. 双核 MCU 的异步模型:ESP32-C3 采用 APP+PRO 核 架构, embassy 如何防止另一核并发访问同一 GPIO register?——答:使用 SMP-aware 的 critical-section 实现,在 ESP-IDF 提供的 esp-atomic 中封装,面试可展开“如何自定义 cs 实现”。
  2. 异步 Bootloader:国内量产要求 OTA 差分升级,若升级过程掉电需回滚。可把升级流程拆成 async fn update(),状态机持久化到 RTC RAM,利用 embassy 的 Flash async 驱动实现 断电续传,面试可谈“状态机序列化到 flash 的零拷贝技巧”。
  3. Rust 异步与 Zephyr RTOS 共存:部分客户强制使用 Zephyr。可用 rust-async-support 把 Rust Future 封装成 k_work_poll,实现 Rust 异步任务与 C 线程混合调度,考察你对 FFI + Waker 跨语言唤醒 的理解。