为何用 Rust 做嵌入式
内存安全无 GC、零成本抽象、所有权防数据竞争——理解 Rust 为裸机开发带来的价值,认识 embedded-hal / RTIC / embassy 生态,写下第一个 blinky。
内存安全无 GC、零成本抽象、所有权防数据竞争——理解 Rust 为裸机开发带来的价值,认识 embedded-hal / RTIC / embassy 生态,写下第一个 blinky。
OutputPin、InputPin、SpiBus、I2c、DelayNs 等平台无关接口。设备驱动只依赖这些 trait,就能在 STM32、nRF、RP2040、ESP32 等任意 HAL 上复用——一次编写、处处运行的驱动生态。.await 任务,空闲时自动进入低功耗睡眠(WFI)。相比 RTIC 更接近应用层写法,特别适合 I/O 密集、需要并发等待多个外设的场景。架构-厂商-系统-ABI。嵌入式常用 thumbv7em-none-eabihf(Cortex-M4F/M7,硬浮点)、thumbv6m-none-eabi(Cortex-M0)、riscv32imac-unknown-none-elf(RISC-V)。其中 none 表示无操作系统(裸机)。Rust 嵌入式采用清晰的分层架构,从底层寄存器到上层应用,每一层职责单一、可替换。理解这张分层图,是学好后续所有章节的地基:
关键在于设备驱动只依赖 embedded-hal 的 trait,而不绑定任何具体芯片。这意味着你为 STM32 写的 SSD1306 OLED 驱动,把它接到 RP2040 或 ESP32 上时,只需换掉底层 HAL、传入不同的 SPI/I2C 实例即可——驱动代码一行不改。这是 Rust 嵌入式生态相比 C(厂商 SDK 各不相同、驱动无法移植)的最大结构性优势。
| 维度 | C 语言 | Rust |
|---|---|---|
| 内存安全 | 依赖程序员,越界/悬垂难查 | 编译期保证,无运行时开销 |
| 并发/竞态 | ISR 与主循环共享变量易竞态 | Send/Sync 编译期挡住数据竞争 |
| 寄存器操作 | 裸指针 + 位运算,无类型检查 | PAC 类型安全 API,非法值编译报错 |
| 包管理 | 手动拷贝头文件/库,无统一方案 | cargo + crates.io 一条命令拉依赖 |
| 驱动复用 | 绑定厂商 SDK,跨平台需重写 | embedded-hal 一次编写处处运行 |
| 错误处理 | 返回错误码,易被忽略 | Result + ? 强制处理,忽略即警告 |
| 生态成熟度 | 三十年积累,芯片支持齐全 | 快速增长,主流芯片已可用 |
嵌入式行业超过一半的严重缺陷来自内存与并发问题。Rust 的价值不在于"更快"(它和 C 一样快),而在于把这些错误从"运行时偶发死机"变成"编译时明确报错"。对于安全关键(汽车、医疗、工业)和长期运行(网关、节点)的固件,这种确定性极其宝贵。
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
下面是一个在 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(); 一行翻转 } }
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 为嵌入式带来三大核心价值:内存安全(编译期消除越界/悬垂/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() 的基本骨架,这些将在后续章节逐一深入。