Rust嵌入式开发初探:no_std环境

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();
    }
}

几个值得注意的点:

  1. Peripherals::take() 返回 Option,保证外设只被获取一次——这是 Rust 所有权系统在硬件层面的体现。
  2. 时钟配置 是嵌入式开发的第一步,几乎所有外设都依赖时钟。freeze() 会锁定时钟配置,之后不能再改。
  3. 类型状态模式(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 定义了 InputPinOutputPinReadWrite 等通用 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 常见坑

  1. 没有 println!:调试输出要么用 RTT(rtt-target crate),要么用串口。
  2. 没有 String / Vec:要用 heapless crate 提供的定长容器,如 heapless::String<64>heapless::Vec<u8, 128>
  3. 浮点运算:Cortex-M0/M3 没有 FPU,浮点运算会软件模拟,很慢。尽量用定点数。
  4. 中断处理:用 #[interrupt] 属性标记中断处理函数,通过 Mutex<RefCell<Option<T>>> 在中断和主循环间共享数据。

总结

Rust 嵌入式的开发体验比预期好很多。类型系统能在编译期捕获大量硬件相关的错误(引脚状态、外设所有权),embedded-hal trait 系统让驱动代码具有很好的可移植性。生态还在快速发展中,Embassy(异步嵌入式框架)也值得关注。