实战:异步环境监测节点
把前 11 章拧成一个完整项目:用 embassy 异步架构,I2C 周期读温湿度传感器、串口上报数据、LED 指示状态、按键切换模式,全程无堆、无锁、低功耗。附完整 Cargo.toml、任务代码、架构图与量产 checklist。
把前 11 章拧成一个完整项目:用 embassy 异步架构,I2C 周期读温湿度传感器、串口上报数据、LED 指示状态、按键切换模式,全程无堆、无锁、低功耗。附完整 Cargo.toml、任务代码、架构图与量产 checklist。
这个"环境监测节点"每 2 秒读一次温湿度、通过串口上报、用 LED 显示健康状态、按键可切换上报频率。它把整套 Rust 嵌入式技艺串起来:
#[embassy_executor::task]:sensor 任务周期读 I2C 传感器并发进 Channel;report 任务从 Channel 收数据、格式化后经串口发出;ui 任务等按键中断切换频率、控制 LED 闪烁。任务间用 embassy 的 Channel/Signal 通信,无锁、无共享可变全局。embassy_sync::channel::Channel 是异步多生产者多消费者队列,容量固定、无堆。sensor 任务 sender.send(reading).await(满则挂起等空位),report 任务 receiver.receive().await(空则挂起等数据)。它把"采集"与"上报"解耦,是异步嵌入式任务通信的地道方式,替代了裸机的 Mutex<RefCell>。embassy_sync::signal::Signal 存"最新一个值",新值覆盖旧值。适合传"当前上报周期"这种只关心最新状态的场景:ui 任务按键后 signal.signal(new_period),sensor 任务 signal.wait().await 拿到新周期。与 Channel(不丢数据的队列)互补。i2c.write_read(ADDR, &cmd, &mut buf).await。等待传输时任务挂起、CPU 让给别的任务或睡眠。传感器驱动(如 SHT3x/BME280)建议选支持 embedded-hal-async 的版本,直接 .await;否则用阻塞驱动包一层。Uart/BufferedUart,uart.write(bytes).await 底层走 DMA + 中断(呼应第 10 章),发送期间不占 CPU。数据用 heapless::String(第 9 章)在栈上格式化成一行,如 "T=23.5,H=45.2\r\n",避免任何堆分配。button.wait_for_falling_edge().await(第 8 章)等待按键,无需轮询。按下后循环切换上报周期(2s→5s→10s),通过 Signal 通知 sensor 任务;同时用 LED 反馈:正常慢闪、故障快闪。Err,不 unwrap 崩溃,而是记日志、点故障灯、跳过本次。配合独立看门狗 IWDG:主循环定期喂狗,若某任务卡死导致长时间不喂,芯片自动复位——这是量产设备的安全网。WFI 睡眠(第 8 章)。因为读传感器、串口发送、等按键全是 .await 挂起,绝大部分时间 CPU 都在睡,靠中断唤醒。相比"轮询 + delay 空转"的写法,平均功耗低一个数量级,适合电池供电节点。# 目标 STM32F411(Black Pill);换芯片改 embassy-stm32 的 feature [package] name = "env-node" version = "0.1.0" edition = "2021" [dependencies] embassy-executor = { version = "0.6", features = ["arch-cortex-m", "executor-thread", "integrated-timers"] } embassy-time = { version = "0.3", features = ["tick-hz-32_768"] } embassy-stm32 = { version = "0.1", features = ["stm32f411ce", "time-driver-any", "exti", "memory-x", "unstable-pac"] } embassy-sync = "0.6" embassy-futures = "0.1" # 日志与 panic(第 11 章) defmt = "0.3" defmt-rtt = "0.4" panic-probe = { version = "0.3", features = ["print-defmt"] } # 无堆动态集合(第 9 章) heapless = "0.8" # async HAL trait(第 4 章) embedded-hal-async = "1.0" [profile.release] opt-level = "s" # 优化体积 lto = true debug = 2 # 保留符号供 defmt 解码,不进最终 Flash
#![no_std] #![no_main] use defmt_rtt as _; use panic_probe as _; use core::fmt::Write; use heapless::String; use embassy_executor::Spawner; use embassy_time::{Timer, Duration}; use embassy_sync::{blocking_mutex::raw::ThreadModeRawMutex, channel::Channel, signal::Signal}; use embassy_stm32::{gpio::{Level, Output, Speed, Input, Pull}, i2c::I2c, usart::Uart, exti::ExtiInput}; // 一条温湿度读数 #[derive(Clone, Copy, defmt::Format)] struct Reading { temp_c: f32, hum_pct: f32 } // 任务间通信原语:静态分配、无堆 static CHAN: Channel<ThreadModeRawMutex, Reading, 4> = Channel::new(); static PERIOD: Signal<ThreadModeRawMutex, u64> = Signal::new(); // —— sensor 任务:按周期读 I2C,发进 Channel —— #[embassy_executor::task] async fn sensor_task(mut i2c: I2c<'static, embassy_stm32::mode::Async>) { let mut period_ms: u64 = 2000; loop { // 若 ui 改了周期,取最新值(非阻塞判断) if let Some(p) = PERIOD.try_take() { period_ms = p; } let mut buf = [0u8; 6]; // SHT3x:发测量命令 0x2400,读 6 字节(温+湿+CRC) match i2c.write_read(0x44, &[0x24, 0x00], &mut buf).await { Ok(()) => { let raw_t = u16::from_be_bytes([buf[0], buf[1]]); let raw_h = u16::from_be_bytes([buf[3], buf[4]]); let reading = Reading { temp_c: -45.0 + 175.0 * raw_t as f32 / 65535.0, hum_pct: 100.0 * raw_h as f32 / 65535.0, }; CHAN.send(reading).await; // 满则挂起等消费 } Err(e) => defmt::warn!("i2c read failed: {:?}", defmt::Debug2Format(&e)), } Timer::after(Duration::from_millis(period_ms)).await; // 挂起,期间可睡眠 } } // —— report 任务:收数据,格式化后串口发出 —— #[embassy_executor::task] async fn report_task(mut uart: Uart<'static, embassy_stm32::mode::Async>) { loop { let r = CHAN.receive().await; // 空则挂起等数据 let mut line: String<32> = String::new(); let _ = write!(line, "T={=f32},H={=f32}\r\n", r.temp_c, r.hum_pct); let _ = uart.write(line.as_bytes()).await; // DMA 发送,不占 CPU defmt::info!("reported {:?}", r); } } // —— ui 任务:按键切周期 + LED 状态 —— #[embassy_executor::task] async fn ui_task(mut button: ExtiInput<'static>, mut led: Output<'static>) { let periods = [2000u64, 5000, 10000]; let mut idx = 0; loop { button.wait_for_falling_edge().await; // 挂起等按键 idx = (idx + 1) % periods.len(); PERIOD.signal(periods[idx]); // 通知 sensor 任务 led.toggle(); defmt::info!("period -> {} ms", periods[idx]); Timer::after(Duration::from_millis(50)).await; // 消抖 } } #[embassy_executor::main] async fn main(spawner: Spawner) { let p = embassy_stm32::init(Default::default()); // 初始化外设(时钟/引脚配置随芯片,此处示意) let led = Output::new(p.PC13, Level::High, Speed::Low); let button = ExtiInput::new(p.PA0, p.EXTI0, Pull::Up); let i2c = /* I2c::new(...) 配 PB6/PB7 + DMA */ todo!(); let uart = /* Uart::new(...) 配 PA9/PA10 + DMA */ todo!(); // 启动三个并发任务;它们各自 await,空闲时 CPU 睡眠 spawner.spawn(sensor_task(i2c)).unwrap(); spawner.spawn(report_task(uart)).unwrap(); spawner.spawn(ui_task(button, led)).unwrap(); }
三个任务之间没有任何共享可变全局:数据通过 Channel 流动、控制信号通过 Signal 传递,所有权在任务间清晰移交。这意味着编译器已经帮你证明了"不存在数据竞争"——不是靠加锁靠自觉,而是结构上就无从竞争。加上无堆(heapless + 静态 Channel)、空闲即睡眠,你得到的是一个确定内存、无竞争、低功耗的系统,而写法却像同步顺序代码一样直白。这就是 Rust 嵌入式的终极价值。
| 阶段 | 检查项 | 对应章节 |
|---|---|---|
| 构建 | target 三元组正确、opt-level="s"+LTO、memory.x 匹配芯片 | 第 1-3 章 |
| 内存 | 启用 flip-link 防栈溢出、确认无堆(或堆区足够)、heapless 容量够大 | 第 9 章 |
| 并发 | 无共享可变全局、Channel 容量不溢、无 await 处忙等/长阻塞 | 第 6-8 章 |
| 健壮 | 外设错误不 unwrap、启用 IWDG 看门狗、panic 有明确处理策略 | 第 7、11 章 |
| 调试 | 发布版 DEFMT_LOG 调到 warn/off、保留 ELF 供事后解码 | 第 11 章 |
| 功耗 | 确认空闲进 WFI、关闭未用外设时钟、测量实际平均电流 | 第 8 章 |
| 烧录 | 用 probe-rs 批量烧录、校验 Flash、可选加读保护 RDP | 第 11 章 |
① 时钟对了吗?HSE 晶振频率、PLL 倍频要和实际硬件一致,配错会导致串口波特率偏、定时器不准。② 引脚复用对了吗?I2C/UART 的引脚复用号(AF)要查数据手册核对。③ 上拉/下拉对了吗?I2C 的 SCL/SDA 需要上拉(内部或外部 4.7k),按键接法决定 Pull 方向。这三处是新板子最常见的"编译通过但跑不起来"的坑。
本章用一个异步环境监测节点把全书串起:embassy 三任务架构(sensor 读 I2C、report 串口上报、ui 按键控灯),任务间用静态 Channel(不丢数据的队列)与 Signal(最新值通知)通信,无共享可变全局——编译器直接证明无数据竞争。数据格式化用 heapless::String(无堆),I2C/串口用 async + DMA(等待不占 CPU),按键用 wait_for_falling_edge().await 中断驱动,空闲自动 WFI 睡眠(低功耗)。健壮性靠"错误不 unwrap + IWDG 看门狗",调试靠 defmt+probe-rs。回望全书:从 no_std/core 的裸机基础(1-3 章)、embedded-hal 生态与 typestate GPIO(4-5 章)、中断/RTIC 与所有权并发(6-7 章)、embassy 异步与内存管理(8-9 章)、到 DMA 安全与调试工具链(10-11 章),Rust 嵌入式的主线始终是一句话:把"靠程序员自觉"的一整类 bug(竞态、悬垂、越界、资源冲突)搬到编译期消灭,同时保持零成本抽象与裸机级性能。你现在已经拥有从点灯到量产节点的完整能力,去做点真正上电运行的东西吧。