Rust 在嵌入式领域越来越受关注。零成本抽象、内存安全和没有运行时开销使它天然适合资源受限的环境。最近用 Rust 在 STM32 上点了个灯,记录一下 no_std 环境的基本概念和开发流程。
为什么需要 no_std
标准库 std 依赖操作系统提供的功能:堆分配、文件系统、线程、网络等。裸机(bare-metal)嵌入式环境没有操作系统,自然不能用 std。
#![no_std] 告诉编译器不链接标准库,只使用 core 库——它提供基本类型、trait、迭代器等不依赖 OS 的功能。如果需要堆分配,可以额外引入 alloc 库并提供一个全局分配器。
项目结构
以 STM32F103(经典的"蓝药丸")为例,用 cortex-m-rt 运行时 crate:
cargo new --bin stm32-blink
cd stm32-blink
Cargo.toml:
[package]
name = "stm32-blink"
version = "0.1.0"
edition = "2021"
[dependencies]
cortex-m = "0.7"
cortex-m-rt = "0.7"
panic-halt = "0.2" # panic 时直接 halt
stm32f1xx-hal = { version = "0.10", features = ["stm32f103", "rt"] }
embedded-hal = "0.2"
[[bin]]
name = "stm32-blink"
bench = false
test = false
.cargo/config.toml:
[target.thumbv7m-none-eabi]
runner = "probe-rs run --chip STM32F103C8"
rustflags = ["-C", "link-arg=-Tlink.x"]
[build]
target = "thumbv7m-none-eabi"
memory.x(链接脚本,描述芯片的内存布局):
MEMORY
{
FLASH : ORIGIN = 0x08000000, LENGTH = 64K
RAM : ORIGIN = 0x20000000, LENGTH = 20K
}
最小可运行程序
先看一个最小的 no_std 程序结构:
#![no_std]
#![no_main]
use panic_halt as _; // panic handler:直接死循环
use cortex_m_rt::entry;
#[entry]
fn main() -> ! {
// 嵌入式 main 永远不返回,所以返回类型是 !(never type)
loop {}
}
两个关键属性:
#![no_std]:不链接标准库#![no_main]:不使用标准的 main 入口,由cortex-m-rt提供启动代码
panic_halt 提供了一个最简单的 panic handler——直接进入死循环。在嵌入式环境中必须显式提供 panic handler,否则编译不过。
LED 闪烁示例
用 HAL(Hardware Abstraction Layer)库控制 GPIO:
#![no_std]
#![no_main]
use panic_halt as _;
use cortex_m_rt::entry;
use stm32f1xx_hal::{pac, prelude::*, timer::Timer};
#[entry]
fn main() -> ! {
// 获取外设访问权
let dp = pac::Peripherals::take().unwrap();
let cp = cortex_m::Peripherals::take().unwrap();
// 配置时钟
let mut flash = dp.FLASH.constrain();
let rcc = dp.RCC.constrain();
let clocks = rcc.cfgr.freeze(&mut flash.acr);
// 配置 GPIO:PC13 是蓝药丸板载 LED
let mut gpioc = dp.GPIOC.split();
let mut led = gpioc.pc13.into_push_pull_output(&mut gpioc.crh);
// 配置定时器用于延时
let mut timer = Timer::syst(cp.SYST, &clocks).counter_hz();
timer.start(2.Hz()).unwrap(); // 2Hz -> 500ms 周期
loop {
led.set_high(); // LED 灭(低电平点亮)
nb::block!(timer.wait()).unwrap();
led.set_low(); // LED 亮
nb::block!(timer.wait()).unwrap();
}
}
几个值得注意的点:
Peripherals::take()返回Option,保证外设只被获取一次——这是 Rust 所有权系统在硬件层面的体现。- 时钟配置 是嵌入式开发的第一步,几乎所有外设都依赖时钟。
freeze()会锁定时钟配置,之后不能再改。 - 类型状态模式(Typestate Pattern):
pc13.into_push_pull_output()把引脚从默认状态转换为推挽输出状态。不同状态是不同的类型,编译器会阻止你在错误的状态下操作引脚。
HAL 层次结构
Rust 嵌入式生态的抽象层次:
你的应用代码
│
▼
HAL crate (stm32f1xx-hal) -- 芯片级 HAL
│
▼
embedded-hal traits -- 通用硬件抽象 trait
│
▼
PAC (Peripheral Access Crate) -- 寄存器级访问,由 SVD 自动生成
│
▼
cortex-m / cortex-m-rt -- CPU 核心支持 + 启动代码
embedded-hal 定义了 InputPin、OutputPin、Read、Write 等通用 trait,驱动库只要基于这些 trait 编写,就能跨芯片使用。这是 Rust 嵌入式生态的核心设计。
烧录与调试
安装 probe-rs(替代 OpenOCD 的现代工具):
cargo install probe-rs --features cli
# 烧录并运行
cargo run --release
# 也可以单独烧录
probe-rs run --chip STM32F103C8 target/thumbv7m-none-eabi/nelease/stm32-blink
如果用 cargo-embed,在项目根目录创建 Embed.toml:
[default.general]
chip = "STM32F103C8"
[default.rtt]
enabled = true
[default.gdb]
enabled = false
然后 cargo embed 即可烧录并打开 RTT(Real-Time Transfer)终端查看日志。
no_std 常见坑
- 没有
println!:调试输出要么用 RTT(rtt-targetcrate),要么用串口。 - 没有
String/Vec:要用heaplesscrate 提供的定长容器,如heapless::String<64>、heapless::Vec<u8, 128>。 - 浮点运算:Cortex-M0/M3 没有 FPU,浮点运算会软件模拟,很慢。尽量用定点数。
- 中断处理:用
#[interrupt]属性标记中断处理函数,通过Mutex<RefCell<Option<T>>>在中断和主循环间共享数据。
总结
Rust 嵌入式的开发体验比预期好很多。类型系统能在编译期捕获大量硬件相关的错误(引脚状态、外设所有权),embedded-hal trait 系统让驱动代码具有很好的可移植性。生态还在快速发展中,Embassy(异步嵌入式框架)也值得关注。