Chapter 02 · Rust 嵌入式开发

no_std 与工具链

std vs no_std、core 与 alloc、交叉编译目标、memory.x 链接脚本、cortex-m-rt 启动流程、#[entry] 入口与 panic handler——彻底搞懂 Rust 裸机项目如何从零构建与启动。

课程进度17%

关键概念

std / core / alloc
Rust 标准库分三层:core 是最底层、不依赖操作系统的核心库(Option、Result、迭代器、基础类型),任何环境都能用;alloc 提供需要堆分配的类型(Box、Vec、String),需要一个全局分配器;std 在前两者之上加入文件、线程、网络等 OS 相关能力。裸机没有 OS,只能用 core(外加可选的 alloc)。
#![no_std]
Crate 级属性,告诉编译器不链接 std,只链接 core。这样就去掉了对操作系统的依赖,产物可运行在没有 OS 的裸机上。代价是不能用 VecStringprintln! 等依赖堆或 OS 的 API——需要用 heaplessdefmt 替代。
#![no_main]
告诉编译器不使用标准的 main 入口(标准 main 依赖运行时初始化 argc/argv/环境)。裸机程序的真正入口是复位向量指向的 Reset 处理函数,由 cortex-m-rt 提供;用户用 #[entry] 宏标注自己的入口函数。
cortex-m-rt (Runtime)
Cortex-M 的最小启动运行时 crate。它提供复位向量表、Reset 处理函数(负责初始化 .data/.bss 段、设置栈指针、可选启用 FPU),并暴露 #[entry]#[exception]#[interrupt] 等属性宏。它是 PAC/HAL 之下、最贴近硬件的启动层。
链接脚本与 memory.x
链接器需要知道芯片的 Flash 和 RAM 分别在哪个地址、多大。cortex-m-rt 提供通用链接脚本 link.x,但它引用一个由用户提供的 memory.x 文件,其中用 MEMORY { } 块声明 FLASH 和 RAM 的 ORIGIN 与 LENGTH。这是唯一需要按芯片型号手工填写的文件。
panic handler
Rust 程序 panic 时需要一个处理函数。std 环境默认展开栈并打印,裸机没有这些,必须自己提供 #[panic_handler]。常用现成 crate:panic-halt(停机死循环)、panic-probe(配合 defmt 打印 panic 信息再停机)、panic-reset(复位芯片)。
.cargo/config.toml
Cargo 的项目级配置。嵌入式项目在这里设置默认编译目标(build.target)、传给链接器的参数(-C link-arg=-Tlink.x 引入链接脚本),以及 runner(让 cargo run 自动调用 probe-rs 烧录)。配好后 cargo run 就能一键编译烧录运行。
段(section):.text/.data/.bss
程序的内存布局分段:.text 是代码和只读常量(放 Flash);.rodata 只读数据;.data 是有初值的全局变量(初值存 Flash,运行时拷贝到 RAM);.bss 是零初值全局变量(只在 RAM 占位,启动时清零)。Reset 函数启动时会把 .data 从 Flash 搬到 RAM、把 .bss 清零,之后才调用 main。

std vs no_std 分层图

理解三层标准库的关系,就理解了嵌入式为什么"少了很多东西",以及缺的部分该用什么补:

Rust 标准库分层 ═════════════════════════════════════════════════════ ┌───────────────────────────────────────────────┐ │ std 文件·线程·网络·进程·println! │ 需要操作系统 │ └── 桌面/服务器可用,裸机 ✗ │ └───────────────────────┬───────────────────────┘ │ 建立在 ┌───────────────────────▼───────────────────────┐ │ alloc Box·Vec·String·BTreeMap │ 需要全局分配器 │ └── 裸机可选:定义 #[global_allocator] │ └───────────────────────┬───────────────────────┘ │ 建立在 ┌───────────────────────▼───────────────────────┐ │ core Option·Result·迭代器·Ordering·数值 │ 零依赖,任何环境 │ └── 裸机默认可用(#![no_std] 保留它) │ └────────────────────────────────────────────────┘ 裸机缺失的补充: Vec/String → heapless::Vec / heapless::String(固定容量,栈上) println! → defmt::info!(高效日志,见第 11 章)
大多数嵌入式项目只用 core

no_std(不含 alloc)是嵌入式的默认与首选:内存完全静态可预测、无碎片、无分配失败风险。只有在确实需要动态大小集合(如解析可变长协议)时才引入 alloc 并配一个分配器。第 9 章会详细讲 heapless 如何在不用堆的前提下提供 Vec/String/队列。

一个完整的 no_std 项目结构

典型的 Rust 嵌入式项目目录如下,每个文件各司其职:

blinky/
├── Cargo.toml            # 依赖与元数据
├── memory.x              # 芯片内存布局(Flash/RAM 地址与大小)
├── build.rs              # 构建脚本:把 memory.x 放进链接器搜索路径
├── .cargo/
│   └── config.toml       # 默认 target、链接参数、runner
└── src/
    └── main.rs           # #![no_std] 应用代码

Cargo.toml

[package]
name = "blinky"
version = "0.1.0"
edition = "2021"

[dependencies]
cortex-m = "0.7"            # 内核外设访问(NVIC/SysTick 等)
cortex-m-rt = "0.7"         # 启动运行时 + #[entry]
panic-halt = "0.2"          # panic 时停机
stm32f4xx-hal = { version = "0.21", features = ["stm32f411"] }

# 嵌入式几乎总是启用优化,即使 dev 构建也建议开 opt-level
[profile.dev]
opt-level = "s"            # 优化体积,保留一定调试信息

[profile.release]
opt-level = "z"            # 最激进的体积优化
lto = true                 # 链接时优化,进一步缩小固件
codegen-units = 1         # 牺牲编译速度换取更好优化
debug = true               # 保留调试符号(不进 Flash,供 defmt/gdb 用)

memory.x(以 STM32F411CE 为例:512K Flash / 128K RAM)

/* 芯片存储器布局,链接器据此分配段地址 */
MEMORY
{
  /* NAME (属性) : ORIGIN = 起始地址, LENGTH = 大小 */
  FLASH : ORIGIN = 0x08000000, LENGTH = 512K
  RAM   : ORIGIN = 0x20000000, LENGTH = 128K
}

/* cortex-m-rt 的 link.x 会引用上面的 FLASH/RAM 符号,
   并自动把 .text 放 FLASH、.data/.bss 放 RAM、栈顶设在 RAM 末端 */
memory.x 写错的后果

如果 LENGTH 写得比实际大,程序体积超出真实 Flash 时链接器不报错,但烧录后运行到越界地址会 HardFault。如果 ORIGIN 写错(比如某些芯片 Flash 不在 0x0800_0000),程序根本无法启动。填写前务必查阅目标芯片数据手册的 "Memory mapping" 章节。用错芯片型号导致的 memory.x 错误,是初学者最常见的 "板子没反应" 原因。

.cargo/config.toml

[build]
# 默认交叉编译目标,这样 cargo build 不用每次带 --target
target = "thumbv7em-none-eabihf"

[target.thumbv7em-none-eabihf]
# 让 cargo run 自动调用 probe-rs 烧录并打开日志
runner = "probe-rs run --chip STM32F411CEUx"
rustflags = [
  # 引入 cortex-m-rt 的链接脚本
  "-C", "link-arg=-Tlink.x",
]

cortex-m-rt 启动流程

从芯片上电到你的 main() 被调用,中间发生了这些事——这正是 C 语言里那个"神秘的 startup.s 汇编"在 Rust 中做的工作,只不过它被封装进了 cortex-m-rt:

Cortex-M 复位到 main 的启动链 上电 / 复位 │ ▼ CPU 从地址 0x0000_0000 读取向量表 ├─ [0] 初始栈指针 MSP ← 指向 RAM 末端 └─ [1] 复位向量 ← 指向 Reset 函数 │ ▼ ┌─────────────────────────────────────────┐ │ cortex-m-rt 的 Reset 函数 │ │ ① 把 .data 从 Flash 拷贝到 RAM │ │ ② 把 .bss 段在 RAM 中清零 │ │ ③ 可选:初始化 FPU(硬浮点目标) │ │ ④ 调用 pre_init(如有) │ └───────────────────┬─────────────────────┘ │ ▼ 调用 #[entry] 标注的用户函数 → fn main() -> ! │ ▼ main 永不返回(loop {}),若返回则进入 DefaultHandler

最小 no_std 骨架

#![no_std]
#![no_main]

// 引入 panic handler。`as _` 表示只为副作用引入(注册 handler),
// 不直接使用其中的名字。
use panic_halt as _;
use cortex_m_rt::entry;

// #[entry] 宏做三件事:
// 1) 校验签名必须是 fn() -> !(永不返回)
// 2) 把它注册为 cortex-m-rt 的用户入口
// 3) 保证只有一个 #[entry]
#[entry]
fn main() -> ! {
    let mut counter: u32 = 0;
    loop {
        counter = counter.wrapping_add(1);
        // 真实项目里这里操作外设;现在只是空转
        cortex_m::asm::nop();
    }
}

自定义 panic handler(不依赖现成 crate 时)

use core::panic::PanicInfo;

// #[panic_handler] 全局唯一,签名固定:接收 &PanicInfo,返回 !
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    // 裸机里通常:停机 / 触发断点 / 复位
    loop {
        cortex_m::asm::bkpt();  // 若接了调试器则在此断下
    }
}
panic handler 缺失会编译失败

no_std 环境下如果既没有引入 panic-halt 这类 crate,也没有自己写 #[panic_handler],会得到 error: `#[panic_handler]` function required, but not found。这不是可选项——每个 no_std 二进制 crate 必须恰好有一个 panic handler。同理,链接时若缺少 #[entry] 或 memory.x,链接器会报缺少符号或段布局错误。

常用工具链命令

命令作用
rustup target add thumbv7em-none-eabihf添加 Cortex-M4F 交叉编译目标
cargo build --release交叉编译生成 ELF 固件
cargo run --release编译并通过 runner(probe-rs)烧录运行
cargo flash --chip <芯片> --release仅烧录,不打开日志
cargo embed --release烧录 + 打开 RTT 日志终端
cargo size --release -- -A按段查看 Flash/RAM 占用
cargo objdump --release -- -d反汇编,检查生成的机器码
本章小结

Rust 标准库分 core(零依赖核心)/ alloc(需分配器的 Box、Vec)/ std(需 OS)三层,裸机用 #![no_std] 只保留 core,缺失的动态集合用 heapless 补、日志用 defmt 补。#![no_main] + #[entry] 定义裸机入口,真正的复位入口由 cortex-m-rt 提供,它负责拷贝 .data、清零 .bss、初始化 FPU 后调用 main。项目必须提供 memory.x 声明 Flash/RAM 的 ORIGIN 与 LENGTH(按芯片手册填写,写错则无法启动或 HardFault),.cargo/config.toml 设置默认 target、链接参数 -Tlink.x 与 probe-rs runner。每个 no_std 二进制必须恰好有一个 #[panic_handler](可用 panic-halt/panic-probe 或自写)。release profile 建议 opt-level="z" + lto=true + codegen-units=1 以压缩固件体积。