Arduino 用 C++,写起来快,但内存只有几十 KB 的单片机最怕两件事:数组越界和悬垂指针——C++ 编译期不会拦你,炸了就是现场复位。Rust 的裸机模式(no_std)把所有权和借用检查带进了单片机,而 Embassy 又用 async/await 解决了「裸机多任务怎么写」这个老大难。
本文用一块二十块钱的树莓派 Pico(RP2040)带你跑通第一个 Embassy 程序:让板载 LED 每秒闪一次。
为什么是 Embassy,而不是裸写寄存器
传统裸机要么用阻塞式 delay 干等,要么上 RTOS。Embassy 的思路不一样:
- 协作式异步执行器:每个任务是一个
async fn,在 I/O 上.await时让步,调度器去跑别的任务。切换成本极低,不需要每个任务一个内核栈。 - 安全的硬件抽象层:GPIO、UART、I2C、SPI、USB 都有安全 Rust API,不用手抖去戳寄存器。
- DMA 天然适合 async:像串口收发这种等外设的场景,async 比阻塞调用优雅太多。
一句话:比 RTOS 轻,比裸写寄存器安全。
准备工具链
# 1. 安装 Rust 目标三元组(RP2040 是 Cortex-M0+)
rustup target add thumbv6m-none-eabi
# 2. 安装 probe-rs(用来烧录和串口监视,替代 OpenOCD)
cargo install probe-rs --locked
STM32 等 Cortex-M4/M7 板子用的是
thumbv7em-none-eabihf,注意别装错。
把 Pico 通过 USB 连电脑,按住 BOOTSEL 键再插线,它会变成一个 U 盘;probe-rs 烧录时也需要这个状态(部分板子会自动进 bootloader)。
项目结构
pico-blinky/
├── Cargo.toml
├── .cargo/
│ └── config.toml
├── memory.x
├── build.rs
└── src/
└── main.rs
Cargo.toml
[package]
name = "pico-blinky"
version = "0.1.0"
edition = "2021"
[dependencies]
embassy-rp = { version = "0.1", features = ["rp2040"] }
embassy-executor = { version = "0.1", features = ["arch-cortex-m", "executor-thread", "integrated-timers"] }
embassy-time = "0.1"
static-cell = "0.2"
cortex-m = { version = "0.7", features = ["inline-asm"] }
cortex-m-rt = "0.7"
defmt = "0.3"
defmt-rtt = "0.4"
panic-probe = { version = "0.3", features = ["print-defmt"] }
[profile.release]
opt-level = "s"
debug = 0
lto = true
版本号以 crates.io 最新为准,不同版本的
features名字可能微调,照着编译器提示改即可。
.cargo/config.toml
[build]
target = "thumbv6m-none-eabi"
[target.thumbv6m-none-eabi]
runner = "probe-rs run --chip RP2040"
memory.x
MEMORY
{
BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100
FLASH : ORIGIN = 0x10000100, LENGTH = 2048K - 0x100
RAM : ORIGIN = 0x20000000, LENGTH = 264K
}
build.rs
fn main() {
// 让链接器使用 embassy 提供的链接脚本
println!("cargo:rustc-link-arg-bins=-Tlink.x");
}
src/main.rs —— 核心来了
#![no_std]
#![no_main]
use defmt::info;
use embassy_executor::Spawner;
use embassy_rp::gpio::{Level, Output};
use embassy_time::Timer;
use {defmt_rtt as _, panic_probe as _};
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
// 初始化 RP2040 外设
let p = embassy_rp::init(Default::default());
// 板载 LED 在 GPIO 25
let mut led = Output::new(p.PIN_25, Level::Low);
loop {
info!("led on");
led.set_high();
Timer::after_secs(1).await; // 异步等待,期间调度器可跑别的任务
info!("led off");
led.set_low();
Timer::after_secs(1).await;
}
}
注意 #[embassy_executor::main] 这个宏——它帮你把 async fn main 包进 Embassy 的 executor,你不用自己写 executor 启动代码。Timer::after_secs(1).await 这行是关键:它是异步等待,CPU 在等待的 1 秒里可以去干别的,而不是空转。
烧录运行
cargo run --release
第一次烧录请确认 Pico 处于 BOOTSEL 状态。成功后会看到板载 LED 一秒一闪,终端打印:
0.001 INFO led on
1.002 INFO led off
2.002 INFO led on
5 条新手避坑指南
- 目标三元组装错:RP2040 是
thumbv6m-none-eabi,STM32F4 是thumbv7em-none-eabihf,装错直接编译失败。 - 看不到日志:必须同时有
defmt+defmt-rtt+panic-probe,且.cargo/config.toml里的 runner 指向正确的芯片(--chip RP2040)。 - 忘记按 BOOTSEL:第一次
probe-rs run连不上板子,九成是没进 bootloader。 - 死循环里不
.await会饿死任务:Embassy 是协作式调度,某个任务一直占着不放,其他任务永远没机会跑。 - 误以为能用
println!/Vec:no_std下没有标准库,动态分配要用heapless或embedded-alloc显式引入。
下一步
- 用
embassy-rp的cyw43包给 Pico W 加上 WiFi,做一个联网温湿度上报。 - 用
embassy-usb把 Pico 变成一个 USB 键盘或串口设备。 - 多个
async fn用spawner.spawn(...)并发跑,体会协作式多任务。
Rust 把「内存安全」从运行时提前到了编译期,而 Embassy 把它带到了连操作系统都没有的单片机上——这组合值得每个做物联网的人试一次。