no_std 与工具链
std vs no_std、core 与 alloc、交叉编译目标、memory.x 链接脚本、cortex-m-rt 启动流程、#[entry] 入口与 panic handler——彻底搞懂 Rust 裸机项目如何从零构建与启动。
std vs no_std、core 与 alloc、交叉编译目标、memory.x 链接脚本、cortex-m-rt 启动流程、#[entry] 入口与 panic handler——彻底搞懂 Rust 裸机项目如何从零构建与启动。
core 是最底层、不依赖操作系统的核心库(Option、Result、迭代器、基础类型),任何环境都能用;alloc 提供需要堆分配的类型(Box、Vec、String),需要一个全局分配器;std 在前两者之上加入文件、线程、网络等 OS 相关能力。裸机没有 OS,只能用 core(外加可选的 alloc)。std,只链接 core。这样就去掉了对操作系统的依赖,产物可运行在没有 OS 的裸机上。代价是不能用 Vec、String、println! 等依赖堆或 OS 的 API——需要用 heapless 或 defmt 替代。main 入口(标准 main 依赖运行时初始化 argc/argv/环境)。裸机程序的真正入口是复位向量指向的 Reset 处理函数,由 cortex-m-rt 提供;用户用 #[entry] 宏标注自己的入口函数。#[entry]、#[exception]、#[interrupt] 等属性宏。它是 PAC/HAL 之下、最贴近硬件的启动层。cortex-m-rt 提供通用链接脚本 link.x,但它引用一个由用户提供的 memory.x 文件,其中用 MEMORY { } 块声明 FLASH 和 RAM 的 ORIGIN 与 LENGTH。这是唯一需要按芯片型号手工填写的文件。#[panic_handler]。常用现成 crate:panic-halt(停机死循环)、panic-probe(配合 defmt 打印 panic 信息再停机)、panic-reset(复位芯片)。build.target)、传给链接器的参数(-C link-arg=-Tlink.x 引入链接脚本),以及 runner(让 cargo run 自动调用 probe-rs 烧录)。配好后 cargo run 就能一键编译烧录运行。.text 是代码和只读常量(放 Flash);.rodata 只读数据;.data 是有初值的全局变量(初值存 Flash,运行时拷贝到 RAM);.bss 是零初值全局变量(只在 RAM 占位,启动时清零)。Reset 函数启动时会把 .data 从 Flash 搬到 RAM、把 .bss 清零,之后才调用 main。理解三层标准库的关系,就理解了嵌入式为什么"少了很多东西",以及缺的部分该用什么补:
纯 no_std(不含 alloc)是嵌入式的默认与首选:内存完全静态可预测、无碎片、无分配失败风险。只有在确实需要动态大小集合(如解析可变长协议)时才引入 alloc 并配一个分配器。第 9 章会详细讲 heapless 如何在不用堆的前提下提供 Vec/String/队列。
典型的 Rust 嵌入式项目目录如下,每个文件各司其职:
blinky/
├── Cargo.toml # 依赖与元数据
├── memory.x # 芯片内存布局(Flash/RAM 地址与大小)
├── build.rs # 构建脚本:把 memory.x 放进链接器搜索路径
├── .cargo/
│ └── config.toml # 默认 target、链接参数、runner
└── src/
└── main.rs # #![no_std] 应用代码
[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 { /* 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 末端 */
如果 LENGTH 写得比实际大,程序体积超出真实 Flash 时链接器不报错,但烧录后运行到越界地址会 HardFault。如果 ORIGIN 写错(比如某些芯片 Flash 不在 0x0800_0000),程序根本无法启动。填写前务必查阅目标芯片数据手册的 "Memory mapping" 章节。用错芯片型号导致的 memory.x 错误,是初学者最常见的 "板子没反应" 原因。
[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", ]
从芯片上电到你的 main() 被调用,中间发生了这些事——这正是 C 语言里那个"神秘的 startup.s 汇编"在 Rust 中做的工作,只不过它被封装进了 cortex-m-rt:
#![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(); } }
use core::panic::PanicInfo; // #[panic_handler] 全局唯一,签名固定:接收 &PanicInfo,返回 ! #[panic_handler] fn panic(_info: &PanicInfo) -> ! { // 裸机里通常:停机 / 触发断点 / 复位 loop { cortex_m::asm::bkpt(); // 若接了调试器则在此断下 } }
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 以压缩固件体积。