news 2026/7/22 10:20:48

嵌入式异步运行时设计:embassy 与 Tokio 在 no_std 环境中的架构对比

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
嵌入式异步运行时设计:embassy 与 Tokio 在 no_std 环境中的架构对比

嵌入式异步运行时设计:embassy 与 Tokio 在 no_std 环境中的架构对比

一、嵌入式异步的传统困境

嵌入式的经典编程模型是"超级循环"(Super Loop):一个loop {}中轮询所有外设状态。这种方法简单但效率极低——CPU 在绝大多数时间在空转,做着"有没有新数据?没有。有没有新数据?没有"的无用功。

对于电池供电的物联网设备,每一毫瓦功率都关乎续航。超级循环导致 CPU 永远无法进入深度休眠——因为每 1ms 就要醒过来检查一次所有外设。实测中,同样的 BLE 传感器应用,使用超级循环的功耗是 12mA,而使用事件驱动(中断+wakeup)的功耗仅 0.5mA——差距 24 倍。

事件驱动模式是异步运行时的核心思想:当没有事件时,CPU 休眠;当外设产生中断时,唤醒 CPU 并调度对应的处理 Task。这正是embassyTokio解决的问题——但它们的设计哲学完全不同。

Tokio 是为std环境设计的——有操作系统、有线程、有动态内存。embassy 是为no_std环境设计的——没有操作系统、没有线程、没有堆。这导致了架构上的根本差异:Tokio 使用 work-stealing 多线程调度,embassy 使用单线程协作式调度和一个名为Executor的核心组件。

二、embassy 与 Tokio 的架构对比

调度模型对比

Tokio 是抢占式多线程调度——Work-Stealing 算法在多个 Worker 线程间动态平衡负载。Task 被 Tokio 挂起时,可能被迁移到另一个线程继续执行。这提供了高吞吐但引入了线程切换的开销。

embassy 是协作式单线程调度——所有 Task 在同一个线程上执行。Task 必须主动.await出让控制权,否则会饥饿其他 Task。Executor 的核心是一个固定大小的数组(存储所有 Task 的 Future),依次轮询。当一个 Task 返回Poll::Pending时,Executor 跳到下一个 Task。

Waker 机制对比

Tokio 的 Waker 由 I/O Driver 管理:当epoll报告一个 socket 可读时,Driver 调用wake()将对应 Task 重新加入调度队列。

embassy 的 Waker 由中断驱动:当外设(UART、SPI、GPIO)产生中断时,ISR 中设置 Waker,Executor 在下次轮询时唤醒对应 Task。embed 的关键数据结构是WakerRegistration——一个存储 Task ID 的结构,中断处理函数通过它通知 Executor。

内存占用对比

Tokio Runtime 基线:~500KB(含 I/O Driver、Timer Wheel、线程栈)。对于一个 STM32F103(20KB RAM)来说,这根本放不下。

embassy Executor 基线:~200 bytes(存储 Task Future 的数组 + Waker 注册表)。每个 Task 的 Future 大小由 Rust 编译器在编译期确定——静态内存分配,无堆使用。

三、embassy 异步外设驱动实践

// Cargo.toml 依赖: // embassy-stm32 = { version = "0.1", features = ["stm32f407", "defmt"] } // embassy-executor = "0.5" // embassy-time = "0.3" // embassy-usart = "0.1" // embassy-net = "0.4" // static_cell = "2" #![no_std] #![no_main] use embassy_executor::Spawner; use embassy_stm32::{ gpio::{Level, Output, Speed}, peripherals::{USART1, DMA2_CH7}, usart::{self, Uart, Config, DataBits, Parity, StopBits}, bind_interrupts, }; use embassy_time::{Timer, Duration}; use static_cell::StaticCell; // 中断绑定 —— embassy 要求显式声明哪些中断由哪些外设使用 // 这是编译期安全检查:同一中断不能分配给多个外设 bind_interrupts!(struct Irqs { USART1 => usart::InterruptHandler<embassy_stm32::peripherals::USART1>; }); // 全局分配 —— StaticCell 提供类似 lazy_static 但更轻量的初始化 // 在 no_std 环境中没有 Box/Arc,所有数据结构必须在编译期确定大小 static UART_BUF: StaticCell<[u8; 256]> = StaticCell::new(); /// Task 1: LED 闪烁 —— 每 500ms 切换状态 #[embassy_executor::task] async fn led_blink(mut led: Output<'static>) { loop { led.set_high(); Timer::after(Duration::from_millis(500)).await; led.set_low(); Timer::after(Duration::from_millis(500)).await; } } /// Task 2: UART 回显 —— 读取一个字节并立即写回 #[embassy_executor::task] async fn uart_echo(mut uart: Uart<'static, embassy_stm32::mode::Async>) { let mut buf = [0u8; 1]; loop { // read_until_idle 等待 UART 接收数据 // 内部实现:注册 Waker → 使能 RXNE 中断 → 休眠 → // 中断触发 → 唤醒 Task → 继续执行 match uart.read_until_idle(&mut buf).await { Ok(_) => { // 回显收到的字节 let _ = uart.write(&buf).await; } Err(e) => { // 错误处理:帧错误/噪声错误 → 忽略并继续 // 在生产代码中应记录错误类型用于调试 let _ = e; } } } } /// Task 3: 传感器数据采集与网络上报 /// 展示多外设协作:I2C读取 + WiFi发送 #[embassy_executor::task] async fn sensor_reporter( mut i2c: embassy_stm32::i2c::I2c<'static, embassy_stm32::mode::Async>, stack: &'static embassy_net::Stack<cyw43::NetDriver<'static>>, ) { let mut read_buf = [0u8; 6]; loop { // 1. 通过 I2C 读取温湿度传感器 // SHT30 的命令: 0x2C06 (高精度测量) match i2c.write_read(0x44, &[0x2C, 0x06], &mut read_buf).await { Ok(_) => { // 原始数据解析: SHT30 返回 6 字节 // [0-1]: 温度 raw, [2]: CRC, [3-4]: 湿度 raw, [5]: CRC let temp_raw = u16::from_be_bytes([read_buf[0], read_buf[1]]); let temperature = -45.0 + 175.0 * (temp_raw as f32 / 65535.0); } Err(_) => { // I2C 错误 —— 传感器可能未连接 // 不阻塞整体流程,跳过本次采集 Timer::after(Duration::from_secs(1)).await; continue; } } // 2. 通过 WiFi 发送数据到云端 // 使用 embassy-net 提供的 TCP Socket // 实际代码需要:获取 Socket → connect → write → close // (embassy-net TCP 客户端代码省略) // 3. 休眠到下一次采集 —— 10 秒间隔 // Timer::after 实现:设置 RTC 闹钟 → 进入低功耗模式 // → RTC 中断唤醒 → 继续执行 Timer::after(Duration::from_secs(10)).await; } } /// 主入口 —— embassy 宏替代了 cortex_m_rt::entry #[embassy_executor::main] async fn main(spawner: Spawner) { // 外设初始化 —— embassy_stm32 的泛型外设抽象 let p = embassy_stm32::init(Default::default()); // 初始化 LED let led = Output::new(p.PC13, Level::High, Speed::Low); // 初始化 UART1: 115200, 8N1 let uart_config = Config::default(); // UART 接收缓冲区 —— DMA 接收模式 // StaticCell 确保缓冲区在程序生命周期内有效 let rx_buf = UART_BUF.init([0u8; 256]); let uart = Uart::new( p.USART1, p.PA10, // TX pin: PA10 p.PA9, // RX pin: PA9 Irqs, p.DMA2_CH7, rx_buf, uart_config, ).unwrap(); // 拆分为读写两端:Tx 和 Rx 可以独立传递到不同 Task let (uart_tx, uart_rx) = uart.split(); // 启动 Task —— spawner.spawn 接受一个 Future // 所有 Task 在编译期确定——没有动态创建 // 这保证了 exec 的内存占用在编译期完全可知 spawner.spawn(led_blink(led)).unwrap(); spawner.spawn(uart_echo(uart_rx)).unwrap(); // 如果初始化了 WiFi,启动 sensor_reporter // spawner.spawn(sensor_reporter(i2c, &stack)).unwrap(); // embassy::main 宏自动创建 Executor 并启动调度循环 // Executor 在 main 函数返回后持续运行 }

关键设计决策:

  • StaticCell全局静态内存分配:所有缓冲区大小在编译期确定。与使用BoxVec不同——在embassy中不存在OOM的概念,因为根本不涉及动态分配。
  • bind_interrupts!编译期中断资源绑定:如果同一个中断号被声明给两个外设,编译失败。这是对"中断冲突"这一嵌入式开发中高频 Bug 的编译期消除。
  • #[embassy_executor::task]属性宏:将async fn转换为固定大小的 Future。这与 Tokio 的 Task 不同——embassy的 Task Future 大小在编译期就确定了,存储在 Executor 的静态数组中。而 Tokio 的 Task 在堆上分配。
  • Timer::after的低功耗实现:在等待期间,CPU 进入 WFI(Wait For Interrupt)模式,功耗从 100mW 降至 1mW 级别。这是异步运行时的核心节能机制。

四、embassy 与 Tokio 的适用边界与权衡

embassy 适用场景

  • MCU 级别的嵌入式设备(ARM Cortex-M / RISC-V),RAM < 1MB。
  • 电池供电设备,功耗是第一优先级。
  • 外设驱动的异步封装——UART、SPI、I2C、USB 等外设操作天然适合异步模型。

Tokio 适用场景

  • 运行 Linux 的 ARM/x86 边缘服务器(如树莓派、Jetson)。
  • 需要多线程并行的 CPU 密集型应用。
  • 需要与大量 std 生态库(数据库驱动、HTTP 客户端、gRPC)交互的项目。

主要权衡

  1. Task 内存:embassy 的所有 Task Future 是静态分配的,大小在编译期确定。这保证了无 OOM,但限制了 Task 的最大大小——如果某个 Task 的 Future 太大(如局部变量过多),可能编译失败。
  2. 调度公平性:embassy 协作式调度要求所有 Task 必须短暂执行后.await。一个忘记.await的 Task 会饥饿所有其他 Task。Tokio 的抢占式调度无此问题。
  3. 中断处理的安全性:embassy 的中断处理函数编译期保证不持有锁、不阻塞 Executor。Tokio 不涉及中断——信号处理由 OS 代劳。

不完全互斥:实际上 embassy 可以作为 Tokio 的下层——在树莓派等 dev 上,embassy 用于处理 GPIO 实时控制(延迟 < 1μs),Tokio 处理网络和文件 I/O。两者通过signalfdeventfd桥接。

五、总结

  1. embassy 是专为no_std设计的异步运行时——没有线程、没有堆、没有操作系统。所有调度都在单线程上完成。
  2. Tokio 是多线程抢占式调度,适合有 OS 的环境;embassy 是单线程协作式调度,适合裸机环境。
  3. embassy 的 Task Future 大小在编译期确定,存储于静态数组——消除了 OOM 的可能性。
  4. bind_interrupts!宏实现了中断资源的编译期绑定检查,消除了中断冲突的运行时 Bug。
  5. Timer::after在等待期间让 CPU 进入 WFI 低功耗模式,是实现 μA 级别功耗的基础。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/22 10:20:20

嵌入式系统时序参数解析:从建立时间到PCB布局的实战指南

1. 项目概述与核心价值 在嵌入式系统&#xff0c;尤其是基于DSP或ARM的复杂SoC设计中&#xff0c;硬件工程师和底层驱动开发者经常会遇到一个看似枯燥却至关重要的环节&#xff1a;解读芯片数据手册中的时序参数表。这些表格里密密麻麻的“最小”、“最大”、“典型”值&#x…

作者头像 李华
网站建设 2026/7/22 10:16:55

销氪斩获 “AI 驱动智能 CRM 技术创新标杆”,彰显硬核实力

近期&#xff0c;由商界集团主办的中原商业创新峰会在河南郑州隆重举行。本次盛会汇聚全国各行各业企业家、行业专家、数字化领军人物共 500 余人齐聚一堂&#xff0c;聚焦产业数字化升级、AI 技术落地、企业高效增长等核心议题&#xff0c;打造高规格、高含金量的行业交流盛典…

作者头像 李华
网站建设 2026/7/22 10:10:24

CUDA编程与异构计算优化实战指南

1. 异构计算的核心价值与架构解析 在计算密集型应用领域&#xff0c;CPUGPU异构计算已经成为突破性能瓶颈的关键技术方案。这种架构设计的精妙之处在于&#xff1a;CPU作为通用处理器擅长处理复杂的控制流和任务调度&#xff0c;而GPU凭借其众核架构在并行计算任务中展现出惊人…

作者头像 李华
网站建设 2026/7/22 9:58:09

接口测试与抓包工具实战指南:从协议分析到自动化

1. 接口测试与协议分析基础接口测试作为软件测试的关键环节&#xff0c;主要验证不同系统组件间的数据交互是否正确。与UI测试不同&#xff0c;它直接检查数据传输层&#xff0c;能更早发现潜在问题。典型的接口测试流程包括&#xff1a;请求构造、发送请求、响应验证和性能监控…

作者头像 李华