Chapter 01 · Rust 嵌入式开发

为何用 Rust 做嵌入式

内存安全无 GC、零成本抽象、所有权防数据竞争——理解 Rust 为裸机开发带来的价值,认识 embedded-hal / RTIC / embassy 生态,写下第一个 blinky。

课程进度8%

关键概念

内存安全 (Memory Safety)
Rust 在编译期通过所有权、借用、生命周期三大规则,杜绝空指针解引用、悬垂指针、缓冲区越界、use-after-free 等一类内存错误。嵌入式领域这类 bug 往往表现为间歇性死机、看门狗复位,极难复现——Rust 把它们挡在编译器之前,且不依赖任何运行时。
零成本抽象 (Zero-Cost Abstraction)
Rust 的高层抽象(迭代器、trait、泛型、Option/Result)经过单态化(monomorphization)和内联优化后,生成的机器码与手写 C 几乎等价,不引入额外运行时开销。"你不用的东西不必为它付费,用的东西也无法手写得更快"——这使得在 KB 级 RAM 的 MCU 上使用现代语言特性成为可能。
无 GC (No Garbage Collector)
Rust 不使用垃圾回收,内存通过所有权在编译期确定的时机自动释放(RAII)。这意味着没有不可预测的 GC 停顿,满足硬实时要求;同时可以完全不使用堆(no heap),仅靠栈和静态内存运行,非常适合资源受限的裸机环境。
所有权与数据竞争 (Ownership & Data Race)
Rust 的 Send/Sync trait 与借用检查器,在编译期保证同一数据不会被多个上下文(如 main 与中断服务程序 ISR)同时可变访问。C 语言中主循环与 ISR 共享全局变量导致的竞态,是嵌入式最隐蔽的一类 bug;Rust 强制你用 Mutex、原子操作或所有权转移来正确共享,否则无法编译。
embedded-hal
Rust 嵌入式生态的核心 trait 抽象层,定义了 OutputPinInputPinSpiBusI2cDelayNs 等平台无关接口。设备驱动只依赖这些 trait,就能在 STM32、nRF、RP2040、ESP32 等任意 HAL 上复用——一次编写、处处运行的驱动生态。
RTIC (Real-Time Interrupt-driven Concurrency)
基于 Cortex-M 硬件中断优先级(NVIC)的并发框架。以任务和共享资源为核心,编译期用 SRP(Stack Resource Policy)算法分析优先级,保证资源访问无死锁、无数据竞争,且不需要传统 RTOS 的线程栈开销,极其轻量。
embassy
基于 Rust async/await 的现代嵌入式框架。用异步任务替代 RTOS 线程,一个执行器(executor)在单栈上调度多个 .await 任务,空闲时自动进入低功耗睡眠(WFI)。相比 RTIC 更接近应用层写法,特别适合 I/O 密集、需要并发等待多个外设的场景。
目标三元组 (Target Triple)
描述交叉编译目标的字符串,格式 架构-厂商-系统-ABI。嵌入式常用 thumbv7em-none-eabihf(Cortex-M4F/M7,硬浮点)、thumbv6m-none-eabi(Cortex-M0)、riscv32imac-unknown-none-elf(RISC-V)。其中 none 表示无操作系统(裸机)。

Rust 嵌入式生态全景

Rust 嵌入式采用清晰的分层架构,从底层寄存器到上层应用,每一层职责单一、可替换。理解这张分层图,是学好后续所有章节的地基:

Rust 嵌入式分层架构 ═══════════════════════════════════════════════════════════ ┌─────────────────────────────────────────────────────┐ │ 应用层 / 并发框架 │ │ RTIC(中断驱动) embassy(async/await) │ └───────────────────────┬─────────────────────────────┘ │ 依赖 trait ┌───────────────────────▼─────────────────────────────┐ │ 设备驱动 crate(平台无关) │ │ bme280 · ssd1306 · ws2812 · sht4x ... │ └───────────────────────┬─────────────────────────────┘ │ 只依赖 embedded-hal trait ┌───────────────────────▼─────────────────────────────┐ │ embedded-hal(trait 抽象层) │ │ OutputPin · SpiBus · I2c · DelayNs · ... │ └───────────────────────┬─────────────────────────────┘ │ 由各 HAL 实现 ┌───────────────────────▼─────────────────────────────┐ │ HAL crate(芯片家族) │ │ stm32f4xx-hal · nrf52840-hal · rp2040-hal · esp-hal │ └───────────────────────┬─────────────────────────────┘ │ 封装 ┌───────────────────────▼─────────────────────────────┐ │ PAC(Peripheral Access Crate,svd2rust 生成)│ │ 类型安全的寄存器访问:RCC · GPIOA · TIM2 ... │ └───────────────────────┬─────────────────────────────┘ │ ┌───────────────────────▼─────────────────────────────┐ │ cortex-m / riscv 内核 crate + cortex-m-rt 启动 │ └──────────────────────────────────────────────────────┘
分层的意义:驱动可复用

关键在于设备驱动只依赖 embedded-hal 的 trait,而不绑定任何具体芯片。这意味着你为 STM32 写的 SSD1306 OLED 驱动,把它接到 RP2040 或 ESP32 上时,只需换掉底层 HAL、传入不同的 SPI/I2C 实例即可——驱动代码一行不改。这是 Rust 嵌入式生态相比 C(厂商 SDK 各不相同、驱动无法移植)的最大结构性优势。

Rust vs C:嵌入式场景对比

维度C 语言Rust
内存安全依赖程序员,越界/悬垂难查编译期保证,无运行时开销
并发/竞态ISR 与主循环共享变量易竞态Send/Sync 编译期挡住数据竞争
寄存器操作裸指针 + 位运算,无类型检查PAC 类型安全 API,非法值编译报错
包管理手动拷贝头文件/库,无统一方案cargo + crates.io 一条命令拉依赖
驱动复用绑定厂商 SDK,跨平台需重写embedded-hal 一次编写处处运行
错误处理返回错误码,易被忽略Result + ? 强制处理,忽略即警告
生态成熟度三十年积累,芯片支持齐全快速增长,主流芯片已可用
Rust 不是要取代 C,而是消除一类错误

嵌入式行业超过一半的严重缺陷来自内存与并发问题。Rust 的价值不在于"更快"(它和 C 一样快),而在于把这些错误从"运行时偶发死机"变成"编译时明确报错"。对于安全关键(汽车、医疗、工业)和长期运行(网关、节点)的固件,这种确定性极其宝贵。

安装 Rust 与嵌入式工具链

Rust 嵌入式开发不需要庞大的 IDE,一套命令行工具即可。以下以 Cortex-M4F(STM32F4)为例:

# 1. 安装 rustup(Rust 官方工具链管理器)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# 2. 添加交叉编译目标(Cortex-M4/M7 硬浮点)
rustup target add thumbv7em-none-eabihf

# 3. 安装烧录 / 调试工具(probe-rs 生态)
cargo install probe-rs-tools    # 提供 cargo flash / cargo embed

# 4. 安装二进制查看工具(可选,看反汇编/体积)
rustup component add llvm-tools
cargo install cargo-binutils     # cargo size / cargo objdump

第一个程序:blinky(点灯)

下面是一个在 STM32F411 上闪烁 LED 的最小程序,用 stm32f4xx-hal。现在不必理解每一行,重点感受 Rust 嵌入式代码的"形状"——后续章节会逐层拆解:

#![no_std]   // 不链接标准库(无操作系统)
#![no_main]  // 不使用标准 main 入口,由 cortex-m-rt 提供

use panic_halt as _;              // panic 时停机(提供 panic handler)
use cortex_m_rt::entry;             // #[entry] 宏,定义程序入口
use stm32f4xx_hal::{pac, prelude::*};

#[entry]
fn main() -> ! {              // 返回类型 ! 表示永不返回
    // 取得外设单例:设备 PAC + Cortex-M 内核外设
    let dp = pac::Peripherals::take().unwrap();
    let cp = cortex_m::Peripherals::take().unwrap();

    // 配置时钟树:外部晶振 25MHz,系统 84MHz
    let rcc = dp.RCC.constrain();
    let clocks = rcc.cfgr.use_hse(25.MHz()).sysclk(84.MHz()).freeze();

    // 拆分 GPIOC,取 PC13(很多板子板载 LED 在此)
    let gpioc = dp.GPIOC.split();
    let mut led = gpioc.pc13.into_push_pull_output();

    // 基于 SysTick 的阻塞式延时器
    let mut delay = cp.SYST.delay(&clocks);

    loop {
        led.set_high();          // 引脚输出高电平
        delay.delay_ms(500_u32);
        led.set_low();           // 引脚输出低电平
        delay.delay_ms(500_u32);
        // 或用 led.toggle(); 一行翻转
    }
}
注意 take() 只能调用一次

Peripherals::take() 返回 Option,内部有一个静态标志确保外设单例在整个程序中只能被取出一次。第二次调用返回 None。这是 Rust 用类型系统保证"外设不会被两处代码同时拥有"的核心机制——它把"谁拥有这个外设"变成了编译期/运行期可追踪的所有权,第 7 章会深入讲解。

烧录到开发板

连接 ST-Link 或任意 CMSIS-DAP 调试器后,一条命令完成编译、烧录、运行:

# 编译并烧录(release 优化,嵌入式几乎总是用 release)
cargo flash --release --chip STM32F411CEUx

# 或用 cargo embed:烧录 + 打开 RTT 日志终端
cargo embed --release

# 查看固件体积(Flash/RAM 占用)
cargo size --release -- -A
Rust 嵌入式:从源码到运行 ┌──────────────┐ │ src/main.rs │ #![no_std] 源码 │ Cargo.toml │ 依赖声明(hal、rt、panic handler) │ memory.x │ 链接脚本(Flash/RAM 布局) └──────┬───────┘ │ cargo build(rustc + LLVM 交叉编译) ┌──────▼───────┐ │ firmware ELF│ thumbv7em-none-eabihf 目标 └──────┬───────┘ │ probe-rs(cargo flash / embed) ┌──────▼───────┐ │ MCU Flash │ 0x0800_0000 起写入 └──────┬───────┘ │ 复位 → Reset_Handler → main() LED 开始闪烁
本章小结

Rust 为嵌入式带来三大核心价值:内存安全(编译期消除越界/悬垂/use-after-free,无运行时开销)、零成本抽象(高层特性经单态化后与手写 C 等价)、无数据竞争(Send/Sync 保证 ISR 与主循环不会竞态)。Rust 无 GC,可完全不使用堆,满足硬实时。生态采用清晰分层:cortex-m-rt 启动 → PAC 类型安全寄存器 → HAL crate 芯片实现 → embedded-hal trait 抽象 → 平台无关驱动 → RTIC/embassy 并发框架。驱动只依赖 embedded-hal trait,因此可跨芯片复用。工具链极简:rustup target add 加交叉目标、cargo flash/embed 烧录调试、cargo size 看体积。第一个 blinky 展示了 #![no_std]#[entry]、外设 take() 单例、时钟 freeze()、GPIO split()into_push_pull_output() 的基本骨架,这些将在后续章节逐一深入。