第3章:第一个裸机工程——从零搭建 `#![no_std]` 项目
Part1 了解嵌入式rust
第 3 章:开始上路——第一个裸机工程#
本章要回答的问题:Rust 裸机工程长什么样?每个文件的作用是什么?怎么编译、烧录、调试?
3.1 从 C 到 Rust:工程结构的映射#
如果你用 Keil 或 STM32CubeIDE 开发过 STM32 项目,你一定熟悉这样的工程结构:
my_stm32_project/
├── Core/
│ ├── Src/
│ │ ├── main.c ← 应用入口
│ │ ├── stm32f4xx_it.c ← 中断服务函数
│ │ └── system_stm32f4xx.c ← 系统初始化(时钟等)
│ └── Inc/
│ ├── main.h
│ └── stm32f4xx_it.h
├── Drivers/
│ ├── CMSIS/ ← ARM 官方核心支持包
│ └── STM32F4xx_HAL_Driver/ ← ST 官方 HAL 库
├── startup_stm32f407xx.s ← 启动汇编(向量表 + Reset_Handler)
├── STM32F407VGTX_FLASH.ld ← 链接脚本
├── Makefile / .cproject ← 构建系统
└── .mxproject ← CubeMX 工程文件plaintextRust 的裸机工程结构要简洁得多:
my_rust_project/
├── Cargo.toml ← 项目配置 + 依赖声明(≈ Makefile + 库管理)
├── Cargo.lock ← 依赖版本锁定(自动生成)
├── memory.x ← 内存布局(≈ 链接脚本中的 MEMORY 段)
├── build.rs ← 构建脚本(≈ Makefile 中的预处理步骤)
├── .cargo/
│ └── config.toml ← Cargo 配置(默认 target、runner 等)
└── src/
└── main.rs ← 应用入口(≈ main.c + startup 的 Rust 部分)plaintext逐项对照表#
| C 工程文件 | Rust 工程文件 | 说明 |
|---|---|---|
main.c | src/main.rs | 应用入口 |
startup_stm32f407xx.s | cortex-m-rt crate | 向量表 + Reset_Handler(由库提供,无需手写) |
STM32F407VGTX_FLASH.ld | memory.x | 内存布局定义 |
Makefile / .cproject | Cargo.toml + .cargo/config.toml | 构建配置 |
system_stm32f4xx.c | HAL crate 的初始化代码 | 时钟配置等 |
stm32f4xx_it.c | #[interrupt] 宏标注的函数 | 中断服务函数 |
| CMSIS + HAL 库 | PAC + HAL crate(从 crates.io 下载) | 硬件抽象层 |
手动下载库源码到 Drivers/ | Cargo.toml 中声明依赖 | 包管理 |
关键区别:
-
启动代码不需要手写:C 工程中那个几百行的
startup.s(定义向量表、初始化.data、清零.bss),在 Rust 中由cortex-m-rtcrate 自动提供。你不需要写一行汇编。 -
库管理是自动的:C 工程中你需要手动下载 HAL 库源码、配置 include 路径、管理版本。Rust 中只需在
Cargo.toml里写一行依赖声明,cargo build会自动下载、编译、链接。 -
构建系统内置:不需要 Makefile、CMake、Keil 工程文件。
cargo build就是构建命令,cargo run就是编译+烧录+运行。
3.2 创建项目与配置依赖#
创建项目#
# 创建一个新的 Rust 项目
cargo new my_first_bare_metal
cd my_first_bare_metalbash这会生成一个标准的 Rust 项目结构。但默认生成的是”主机程序”(运行在 Linux/Windows 上),我们需要将它改造为”裸机程序”。
Cargo.toml:项目的”身份证”#
打开 Cargo.toml,你会看到:
[package]
name = "my_first_bare_metal"
version = "0.1.0"
edition = "2021"
[dependencies]toml现在,添加裸机开发所需的依赖:
[package]
name = "my_first_bare_metal"
version = "0.1.0"
edition = "2021"
[dependencies]
# Cortex-M 运行时:提供向量表、启动代码、入口点宏
cortex-m-rt = "0.7"
# Cortex-M 核心外设访问(NVIC、SCB、SysTick 等)
cortex-m = "0.7"
# Panic 处理:程序崩溃时的行为(这里选择死循环)
panic-halt = "1.0"
# 日志框架(可选,但强烈推荐)
defmt = "0.3"
defmt-rtt = "0.4"
[profile.release]
# 优化配置(详见第 14 章)
opt-level = "s" # 优化体积
lto = true # 链接时优化
codegen-units = 1 # 单编译单元(更好的优化)
panic = "abort" # 不生成 unwind 代码(嵌入式不需要)toml关键依赖说明#
| Crate | 作用 | C 的对应 |
|---|---|---|
cortex-m-rt | 启动代码、向量表、#[entry] 宏 | startup.s |
cortex-m | 访问 Cortex-M 核心外设(NVIC、SCB) | CMSIS Core |
panic-halt | panic 时死循环 | while(1){} in HardFault |
defmt | 高效日志框架 | printf(但体积小 10 倍以上) |
defmt-rtt | defmt 的 RTT 传输后端 | SEGGER RTT 库 |
Feature Flag:Rust 的 #ifdef#
在 C 中,你用 #ifdef 来条件编译:
// C:条件编译
#ifdef USE_HAL_DRIVER
HAL_Init();
#endif
#ifdef STM32F407xx
SystemClock_Config_F407();
#elif defined(STM32F103xB)
SystemClock_Config_F103();
#endifc在 Rust 中,Feature Flag 是 Cargo 的一等公民:
# Cargo.toml 中的 feature 声明
[features]
default = ["stm32f407"]
stm32f407 = []
stm32f103 = []toml// Rust 中的条件编译
#[cfg(feature = "stm32f407")]
fn system_clock_config() {
// F407 的时钟配置
}
#[cfg(feature = "stm32f103")]
fn system_clock_config() {
// F103 的时钟配置
}rust与 C 的 #ifdef 的关键区别:
| 特性 | C 的 #ifdef | Rust 的 Feature Flag |
|---|---|---|
| 定义位置 | 编译器命令行 / 头文件 | Cargo.toml |
| 依赖传递 | 手动管理 | 自动传递(依赖的 feature 可以联动) |
| 冲突检测 | 无 | Cargo 会报告 feature 冲突 |
| 文档化 | 通常没有 | cargo doc 自动列出所有 feature |
| 组合爆炸 | 容易失控 | Cargo 有 resolver 处理 |
在实际的嵌入式 HAL crate 中,feature flag 通常用来选择芯片型号:
# 使用 embassy-stm32 时的典型配置
[dependencies.embassy-stm32]
version = "0.2"
features = ["stm32f407vg", "memory-x"]toml3.3 交叉编译与 target triple#
什么是 target triple?#
当你在 x86 电脑上运行 gcc main.c 时,编译器默认生成 x86 可执行文件。但嵌入式 MCU 是 ARM 架构——你需要交叉编译。
Rust 用 target triple(目标三元组)来指定编译目标:
thumbv7em-none-eabihf
│ │ │ │
│ │ │ └── 浮点 ABI:hf = 硬件浮点(使用 FPU 寄存器传参)
│ │ └─────── 操作系统:none = 裸机(无 OS)
│ └──────────── 指令集变体:e = 增强(DSP 指令),m = Thumb-2
└────────────────── 架构:ARM Thumb(Cortex-M 系列)plaintext完整的 Cortex-M target triple 对照表#
| Target Triple | 对应内核 | 典型芯片 |
|---|---|---|
thumbv6m-none-eabi | Cortex-M0 / M0+ | STM32F0、STM32L0、RP2040 |
thumbv7m-none-eabi | Cortex-M3 | STM32F1、STM32L1 |
thumbv7em-none-eabi | Cortex-M4/M7(无 FPU) | STM32F4(不用 FPU 时) |
thumbv7em-none-eabihf | Cortex-M4/M7(有 FPU) | STM32F4、STM32F7、STM32H7 |
thumbv8m.base-none-eabi | Cortex-M23 | STM32G0(部分型号) |
thumbv8m.main-none-eabi | Cortex-M33(无 FPU) | STM32L5、nRF53 |
thumbv8m.main-none-eabihf | Cortex-M33(有 FPU) | STM32U5、nRF53 |
riscv32imac-unknown-none-elf | RISC-V(如 ESP32-C3) | ESP32-C3/C6/H2 |
本书主力平台 STM32F407 使用
thumbv7em-none-eabihf(Cortex-M4F,带硬件 FPU)。
与 C 的交叉编译工具链对比#
| 概念 | C 工具链 | Rust 工具链 |
|---|---|---|
| 编译器 | arm-none-eabi-gcc | rustc --target thumbv7em-none-eabihf |
| 安装方式 | 下载 ARM GCC 工具链安装包 | rustup target add thumbv7em-none-eabihf |
| 链接器 | arm-none-eabi-ld(GCC 自带) | rust-lld(Rust 自带,基于 LLVM lld) |
| 标准库 | newlib / picolibc | core(Rust 自带,无需额外安装) |
| 构建命令 | make / IDE 构建 | cargo build --target thumbv7em-none-eabihf |
安装 target#
# 安装交叉编译目标(只需执行一次)
rustup target add thumbv7em-none-eabihf
# 验证安装
rustc --print target-list | grep thumbv7em
# 输出:thumbv7em-none-eabi
# thumbv7em-none-eabihfbash在 .cargo/config.toml 中设置默认 target#
每次 cargo build 都手动指定 --target 很麻烦。在 .cargo/config.toml 中设置默认值:
# .cargo/config.toml
[build]
target = "thumbv7em-none-eabihf"toml这样,直接 cargo build 就会自动使用正确的 target。
3.4 配置工程文件#
.cargo/config.toml:Cargo 的项目级配置#
这个文件是 Rust 嵌入式项目的”中枢配置”:
# .cargo/config.toml
# 1. 默认编译目标
[build]
target = "thumbv7em-none-eabihf"
# 2. 运行器:cargo run 时用什么工具烧录和运行
[target.thumbv7em-none-eabihf]
runner = "probe-rs run --chip STM32F407VGTx"
# 3. 链接器参数
rustflags = [
# 使用 memory.x 作为链接脚本的补充
"-C", "link-arg=-Tlink.x",
# 启用 defmt 日志(可选)
"-C", "link-arg=-Tdefmt.x",
]
# 4. 环境变量(defmt 日志格式)
[env]
DEFMT_LOG = "debug"toml逐项解释:
| 配置项 | 作用 | C 的对应 |
|---|---|---|
[build] target | 默认交叉编译目标 | Makefile 中的 CROSS_COMPILE=arm-none-eabi- |
runner | cargo run 时执行的命令 | Keil 的”Download”按钮 / OpenOCD 脚本 |
rustflags | 传递给 rustc 的额外参数 | CFLAGS / LDFLAGS |
-Tlink.x | 使用 cortex-m-rt 提供的链接脚本 | 链接器命令中的 -T STM32F407.ld |
-Tdefmt.x | 使用 defmt 的链接脚本片段 | 无对应(C 中 printf 不需要链接脚本) |
DEFMT_LOG | 日志级别过滤 | 无对应(C 中通常用 #ifdef DEBUG) |
memory.x:内存布局定义#
/* memory.x - STM32F407VGTx 的内存布局 */
MEMORY
{
/* 1024KB Flash,起始地址 0x08000000 */
FLASH : ORIGIN = 0x08000000, LENGTH = 1024K
/* 128KB SRAM,起始地址 0x20000000 */
RAM : ORIGIN = 0x20000000, LENGTH = 128K
/* 64KB CCM RAM(仅 CPU 可访问,DMA 不可达) */
CCMRAM : ORIGIN = 0x10000000, LENGTH = 64K
}plaintext与 C 链接脚本的关系:
在 C 工程中,链接脚本(如 STM32F407VGTX_FLASH.ld)通常有几百行,包含 MEMORY、SECTIONS、ENTRY 等完整定义。
在 Rust 中,cortex-m-rt crate 已经提供了一个通用的链接脚本(link.x),其中包含了 SECTIONS 定义(.text、.data、.bss、.vector_table 等)。你只需要提供 memory.x 来告诉它你的芯片有多少 Flash 和 RAM。
详细的
memory.x解析和多 Bank Flash 处理,见第 12 章。
build.rs:构建脚本#
build.rs 是 Cargo 在编译前自动执行的脚本,类似于 Makefile 中的预处理步骤:
// build.rs
use std::env;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
fn main() {
// 告诉 Cargo:如果 memory.x 变了,重新执行 build.rs
println!("cargo:rerun-if-changed=memory.x");
// 将 memory.x 复制到 OUT_DIR,让链接器能找到它
let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap());
File::create(out.join("memory.x"))
.unwrap()
.write_all(include_bytes!("memory.x"))
.unwrap();
// 将 OUT_DIR 加入链接器搜索路径
println!("cargo:rustc-link-search={}", out.display());
// 告诉 rustc 使用 link.x 链接脚本
println!("cargo:rustc-link-arg=-Tlink.x");
}rust它做了什么?
- 将
memory.x复制到构建输出目录 - 将该目录加入链接器搜索路径
- 这样
cortex-m-rt的link.x就能通过INCLUDE memory.x找到你的内存布局
在 C 中,这些工作由 IDE(Keil/CubeIDE)或 Makefile 隐式完成。在 Rust 中,
build.rs让你可以精确控制构建过程的每一步。
3.5 cortex-m-rt 的简要说明#
它做了什么?#
cortex-m-rt 是 Cortex-M 微控制器的最小运行时。它提供了从”芯片上电”到”你的 main 函数被调用”之间的一切:
芯片上电
│
▼
┌─────────────────────────────────────────┐
│ cortex-m-rt 提供的 Reset_Handler: │
│ │
│ 1. 从向量表读取初始 SP(栈指针) │
│ 2. 将 .data 段从 Flash 复制到 RAM │
│ 3. 将 .bss 段清零 │
│ 4. 调用用户定义的 main 函数 │
│ 5. 如果 main 返回,进入死循环 │
└─────────────────────────────────────────┘
│
▼
你的 #[entry] fn main()plaintext与 C 的 startup.s 的对应关系#
; C 的 startup_stm32f407xx.s(简化)
Reset_Handler:
; 1. 设置栈指针(由链接脚本提供 _estack)
LDR SP, =_estack
; 2. 复制 .data 段(从 Flash 到 RAM)
LDR R0, =_sdata ; RAM 中的起始地址
LDR R1, =_edata ; RAM 中的结束地址
LDR R2, =_sidata ; Flash 中的源地址
copy_data:
CMP R0, R1
BGE zero_bss
LDR R3, [R2], #4
STR R3, [R0], #4
B copy_data
; 3. 清零 .bss 段
zero_bss:
LDR R0, =_sbss
LDR R1, =_ebss
MOV R2, #0
zero_loop:
CMP R0, R1
BGE call_main
STR R2, [R0], #4
B zero_loop
; 4. 调用 main
call_main:
BL main
; 5. main 返回后死循环
hang:
B hangasm在 Rust 中,这整段汇编你都不需要写。cortex-m-rt 已经用 Rust + 少量内联汇编实现了完全相同的功能,并且:
- 向量表是自动生成的(通过
#[interrupt]和#[exception]宏注册) .data初始化和.bss清零是自动完成的- 你只需要用
#[entry]宏标记你的入口函数
// src/main.rs
#![no_std] // 不链接标准库
#![no_main] // 不使用默认的 main 入口
use cortex_m_rt::entry;
use panic_halt as _; // panic 时死循环
#[entry]
fn main() -> ! {
// 你的应用代码从这里开始
// 此时 .data 已初始化,.bss 已清零,栈已设置好
loop {
// 主循环
}
}rust
cortex-m-rt的深度解析(向量表生成机制、链接脚本细节、中断注册原理),见第 12 章。
3.6 异常与中断的简要说明#
C 的中断服务函数#
在 C 中,中断服务函数(ISR)通常定义在 stm32f4xx_it.c 中:
// C:中断服务函数
void USART2_IRQHandler(void) {
if (USART2->SR & USART_SR_RXNE) {
uint8_t data = USART2->DR;
// 处理接收数据...
}
// 清除中断标志(如果硬件不自动清除)
}
void SysTick_Handler(void) {
HAL_IncTick();
}c这些函数名必须与向量表中的名称完全匹配——这是由链接脚本和启动文件约定的。
Rust 的中断处理:#[interrupt] 和 #[exception] 宏#
// Rust:中断服务函数
use cortex_m_rt::exception;
use stm32f4xx_hal::pac::interrupt;
// 系统异常(SysTick、HardFault 等)
#[exception]
fn SysTick() {
// SysTick 中断处理
}
// 外设中断(名称必须与 PAC 中定义的一致)
#[interrupt]
fn USART2() {
// USART2 中断处理
// 注意:这里需要通过 unsafe 访问外设寄存器
// (因为中断可能和主循环并发访问同一外设)
}rust与 C 的对比#
| 方面 | C | Rust |
|---|---|---|
| 函数名约定 | 必须精确匹配(USART2_IRQHandler) | 由宏和 PAC 定义(USART2) |
| 注册方式 | 链接脚本中的向量表 | #[interrupt] 宏自动注册 |
| 并发安全 | 无保护(程序员自己管) | 编译器要求 unsafe 或安全抽象 |
| 优先级配置 | NVIC_SetPriority() | NVIC 的 Rust 封装 |
| 拼写错误 | 链接通过但中断不触发(静默失败) | 编译错误(PAC 中找不到该名称) |
最后一行是关键区别:在 C 中,如果你把 USART2_IRQHandler 拼错为 USART2_IRQHander,代码会正常编译和链接,但中断永远不会被触发——这是一个极难排查的 bug。在 Rust 中,#[interrupt] 宏会检查名称是否存在于 PAC 中,拼写错误直接编译失败。
优先级配置#
// Rust:配置中断优先级
use cortex_m::peripheral::NVIC;
// 在 #[entry] 中配置
let mut nvic = cp.NVIC;
unsafe {
nvic.set_priority(stm32f4xx_hal::pac::Interrupt::USART2, 1);
nvic.set_priority(stm32f4xx_hal::pac::Interrupt::TIM2, 2);
NVIC::unmask(stm32f4xx_hal::pac::Interrupt::USART2);
}rust中断优先级与 Embassy 的抢占式调度的关系,见第 9 章。
3.7 查看编译产物#
编译#
# Debug 编译(快速,不优化)
cargo build
# Release 编译(优化,用于实际部署)
cargo build --releasebash编译产物位于:
target/thumbv7em-none-eabihf/
├── debug/
│ └── my_first_bare_metal ← Debug ELF 文件
└── release/
└── my_first_bare_metal ← Release ELF 文件plaintext查看 Flash 和 RAM 占用:cargo size#
# 安装 cargo-binutils(只需一次)
cargo install cargo-binutils
rustup component add llvm-tools
# 查看各段大小
cargo size --releasebash输出示例:
text data bss dec hex filename
2847 12 1024 3883 f2b my_first_bare_metalplaintext| 段 | 含义 | 存储位置 | C 的对应 |
|---|---|---|---|
text | 代码 + 常量 | Flash | .text + .rodata |
data | 有初始值的全局/静态变量 | Flash → 启动时复制到 RAM | .data |
bss | 零初始化的全局/静态变量 | RAM(启动时清零) | .bss |
dec | 总大小(text + data + bss) | — | — |
与 C 的对比:
# C:使用 arm-none-eabi-size
arm-none-eabi-size build/my_project.elf
# 输出格式完全相同bash反汇编查看:cargo objdump#
# 反汇编整个程序
cargo objdump --release -- --disassemble
# 只看某个函数
cargo objdump --release -- --disassemble --symbol=main
# 查看特定地址范围
cargo objdump --release -- --disassemble --start-address=0x08000200 --stop-address=0x08000300bash输出示例(LED 闪烁的核心循环):
08000200 :
8000200: push {r7, lr}
8000202: mov r7, sp
8000204: bl #0x8000240 ; 初始化 GPIO
8000208: bl #0x8000260 ; 设置高电平
800020c: bl #0x8000280 ; 延时
8000210: bl #0x80002a0 ; 设置低电平
8000214: bl #0x8000280 ; 延时
8000218: b #-0x14 ; 跳回 0x8000208(循环)asm与 C 的对比:
# C:使用 arm-none-eabi-objdump
arm-none-eabi-objdump -d build/my_project.elfbash命令格式几乎相同——因为底层都是 LLVM/ELF 工具链。
生成 HEX / BIN 文件#
如果你需要将固件交给生产部门烧录:
# 生成 Intel HEX 格式
cargo objcopy --release -- -O ihex firmware.hex
# 生成原始二进制格式
cargo objcopy --release -- -O binary firmware.binbash这与 C 中 arm-none-eabi-objcopy -O ihex / -O binary 完全对应。
3.8 烧录与 RTT 输出#
probe-rs:新一代调试探针工具#
probe-rs 是用 Rust 编写的嵌入式调试工具集,它一个工具替代了 C 工具链中的三个工具:
| C 工具链 | probe-rs 对应 | 功能 |
|---|---|---|
| OpenOCD | probe-rs(核心库) | 与调试探针通信(SWD/JTAG) |
| GDB | probe-rs(VSCode 插件) | 断点、单步、变量查看 |
| SEGGER RTT Viewer | probe-rs run | 实时日志输出 |
| ST-Link Utility / STM32CubeProgrammer | probe-rs flash | 烧录固件 |
支持的调试探针:
- ST-Link(V2/V2.1/V3)——STM32 开发板自带
- J-Link(SEGGER)
- CMSIS-DAP / DAPLink
- ESP-Prog(乐鑫)
安装 probe-rs#
# 方法 1:通过 cargo 安装(推荐)
cargo install probe-rs-tools --locked
# 方法 2:通过系统包管理器
# macOS
brew install probe-rs
# Ubuntu/Debian
sudo apt install probe-rs
# Windows (scoop)
scoop install probe-rs
# 验证安装
probe-rs --version
# probe-rs 0.27.0 (2026-xx-xx)
# 验证探针连接
probe-rs list
# 输出示例:
# The following debug probes were found:
# [0]: STLink V2 (VID: 0483, PID: 3748, Serial: 066DFF555051897267013127)bashcargo run:一键编译 + 烧录 + 日志#
这是 Rust 嵌入式开发最优雅的体验——一条命令完成所有操作:
cargo run --releasebash这条命令做了什么?
cargo run --release
│
├── 1. 编译项目(rustc + linker)
│
├── 2. 调用 runner(.cargo/config.toml 中配置的)
│ └── probe-rs run --chip STM32F407VGTx
│
├── 3. probe-rs 通过 SWD 连接芯片
│
├── 4. 擦除 Flash + 烧录固件
│
├── 5. 复位芯片,程序开始运行
│
└── 6. 通过 RTT 实时输出日志到终端plaintext终端输出示例:
Running `probe-rs run --chip STM32F407VGTx target/thumbv7em-none-eabihf/release/my_first_bare_metal`
Erasing sectors ✔ [00:00:00] [#################################] 16.00 KiB/16.00 KiB @ 54.23 KiB/s
Programming pages ✔ [00:00:00] [#################################] 16.00 KiB/16.00 KiB @ 23.11 KiB/s
Finished in 0.789s
INFO Hello from Rust! (src/main.rs:15)
INFO Counter: 0 (src/main.rs:22)
INFO Counter: 1 (src/main.rs:22)
INFO Counter: 2 (src/main.rs:22)
^Cplaintext对比 C 的工作流:
C 的工作流(至少 3 步):
1. make / IDE 编译 → 生成 .elf
2. OpenOCD 烧录 → 需要单独的配置文件和命令
3. 打开串口终端 / RTT Viewer → 需要另一个窗口
Rust 的工作流(1 步):
1. cargo run → 编译 + 烧录 + 日志,全部完成plaintextRTT(Real-Time Transfer)原理#
RTT 是 SEGGER 发明的调试数据传输技术,现在已成为嵌入式调试的事实标准。probe-rs 完整实现了 RTT 协议。
核心思想:利用调试接口(SWD/JTAG)的内存访问能力,在芯片 RAM 中开辟一块”共享内存区域”,主机通过调试接口直接读写这块内存。
┌─────────────────────────────────────────────────────────┐
│ 芯片 RAM │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ RTT 控制块(Control Block) │ │
│ │ │ │
│ │ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ Up Buffer 0 │ │ Down Buffer 0│ │ │
│ │ │ (芯片→主机) │ │ (主机→芯片) │ │ │
│ │ │ 1024 bytes │ │ 16 bytes │ │ │
│ │ └──────────────┘ └──────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ 你的应用代码: │
│ defmt::info!("Counter: {}", count); │
│ │ │
│ ▼ │
│ 将格式化数据写入 Up Buffer │
└─────────────────────────────────────────────────────────┘
│
│ SWD 调试接口(2 根线:SWDIO + SWCLK)
│
┌────────┴────────┐
│ probe-rs │
│ (主机端) │
│ │
│ 轮询 Up Buffer │
│ 读取新数据 │
│ 输出到终端 │
└─────────────────┘plaintextRTT 的优势:
| 特性 | RTT | 串口(UART) |
|---|---|---|
| 需要额外引脚 | ❌ 不需要(复用调试口) | ✅ 需要 TX/RX |
| 传输速度 | ~1 MB/s | 通常 115200 bps(~11 KB/s) |
| 对实时性影响 | 极小(写入 RAM 缓冲区) | 较大(等待 UART 发送完成) |
| 双向通信 | ✅ 支持 | ✅ 支持 |
| 需要额外硬件 | ❌ 只需调试器 | ✅ 需要 USB-TTL 模块 |
| 量产时可用 | ❌ 需要调试器连接 | ✅ 可以 |
注意:RTT 是调试工具,不是产品通信接口。量产产品中如果需要日志,仍然使用 UART 或 SWO。
与 C 的 OpenOCD + GDB 流程对比#
| 步骤 | C 工具链 | Rust + probe-rs |
|---|---|---|
| 编译 | make / IDE 构建 | cargo build |
| 烧录 | openocd -f board/stm32f4discovery.cfg -c "program build/main.elf verify reset exit" | probe-rs run --chip STM32F407VGTx(或 cargo run) |
| 调试 | 启动 OpenOCD → 启动 GDB → target remote :3333 → load → break main → continue | VSCode 中按 F5(probe-rs 插件) |
| 日志 | 打开串口终端(PuTTY / minicom) | 自动输出到 cargo run 的终端 |
| 配置文件 | openocd.cfg(需要知道芯片型号、接口类型、频率) | 只需 --chip STM32F407VGTx |
probe-rs 的最大优势:它内置了所有主流芯片的 Flash 算法和内存映射,不需要你手动编写配置文件。
3.9 工具链层次全景图#
从源码到芯片的完整链路#
┌─────────────────────────────────────────────────────────────────┐
│ 开发阶段 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ src/main.rs ──→ rustc ──→ LLVM IR ──→ LLVM ──→ .o 文件 │
│ (你的代码) (Rust (中间 (优化 + (目标 │
│ 前端) 表示) 代码生成) 机器码) │
│ │
│ 依赖 crates ──→ 同样经过 rustc + LLVM 编译为 .rlib │
│ │
│ .o + .rlib ──→ rust-lld ──→ ELF 文件 │
│ (所有目标文件) (链接器) (可执行文件, │
│ 包含调试信息) │
│ │
│ memory.x ──→ 链接脚本(定义 Flash/RAM 地址) │
│ link.x ────→ cortex-m-rt 提供的链接脚本模板 │
│ │
├─────────────────────────────────────────────────────────────────┤
│ 烧录阶段 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ELF 文件 ──→ probe-rs ──→ SWD/JTAG ──→ 芯片 Flash │
│ (解析 ELF, (调试 (物理 │
│ 提取二进制, 接口) 写入) │
│ 调用 Flash │
│ 算法) │
│ │
├─────────────────────────────────────────────────────────────────┤
│ 运行阶段 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 芯片上电 ──→ 读取向量表 ──→ Reset_Handler ──→ 初始化 │
│ (0x08000000) (cortex-m-rt) (.data/.bss) │
│ │ │
│ ▼ │
│ 你的 main() │
│ │ │
│ ▼ │
│ 主循环 / 任务 │
│ │ │
│ RTT 日志输出 │
│ (写入 RAM 缓冲区) │
│ │ │
│ probe-rs 读取 │
│ (通过 SWD) │
│ │ │
│ 终端显示 │
│ │
└─────────────────────────────────────────────────────────────────┘plaintext各层工具的职责#
| 层次 | 工具 | 职责 | 可替换性 |
|---|---|---|---|
| 编译器前端 | rustc | Rust → LLVM IR | 不可替换 |
| 编译器后端 | LLVM | 优化 + 代码生成 | 不可替换(rustc 内置) |
| 链接器 | rust-lld | 合并目标文件、应用链接脚本 | 可替换为 arm-none-eabi-ld |
| 烧录/调试 | probe-rs | Flash 编程、调试、RTT | 可替换为 OpenOCD + GDB |
| 日志 | defmt + defmt-rtt | 高效日志传输 | 可替换为 UART printf |
| 运行时 | cortex-m-rt | 启动代码、向量表 | 可替换为 riscv-rt(RISC-V) |
每一层的详细工作原理,见第 12 章。
3.10 Rust 库生态与使用方式#
crates.io:Rust 的包管理器#
crates.io 是 Rust 官方的包注册中心(类似于 C 世界中”所有开源库的集合”),截至 2026 年已有超过 17 万个 crate。
# 搜索 crate
cargo search embassy
# 输出:
# embassy-executor = "0.7.0" # Async executor for embedded systems
# embassy-time = "0.4.0" # Timer and timing primitives
# embassy-stm32 = "0.2.0" # STM32 HAL for Embassy
# embassy-sync = "0.6.0" # Synchronization primitives
# 添加依赖(自动修改 Cargo.toml)
cargo add cortex-m-rt
cargo add defmt
cargo add embassy-stm32 --features stm32f407vgbash与 C 的”手动下载源码 + 加入工程”对比#
| 方面 | C 的做法 | Rust 的做法 |
|---|---|---|
| 获取库 | 从 GitHub 下载 ZIP / git clone | cargo add <crate> |
| 版本管理 | 手动记录版本号 / git submodule | Cargo.toml 中声明,Cargo.lock 锁定 |
| 依赖解析 | 手动处理(A 依赖 B,B 依赖 C…) | Cargo 自动解析整棵依赖树 |
| 编译集成 | 手动添加到 Makefile / IDE 工程 | 自动编译和链接 |
| 更新 | 手动下载新版本、处理 API 变更 | cargo update(受 semver 约束) |
| 文档 | README + 源码注释 | cargo doc --open(自动生成 HTML 文档) |
| 示例 | 通常没有 / 散落在 wiki | 很多 crate 有 examples/ 目录 |
版本管理与 Cargo.lock#
# Cargo.toml 中的版本声明(语义化版本)
[dependencies]
cortex-m-rt = "0.7" # 兼容 0.7.x 的任何版本
defmt = "0.3" # 兼容 0.3.x 的任何版本toml# Cargo.lock(自动生成,精确锁定版本)
name = "cortex-m-rt"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abc123..."
name = "defmt"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "def456..."toml最佳实践:
Cargo.toml提交到版本控制(声明”我需要什么”)Cargo.lock也提交到版本控制(锁定”我实际用了什么版本”)- 这确保了团队中每个人、CI 服务器、生产构建都使用完全相同的依赖版本
嵌入式常用的 crate 一览#
| Crate | 功能 | 成熟度 |
|---|---|---|
cortex-m / cortex-m-rt | Cortex-M 核心支持 | ⭐⭐⭐⭐⭐ 非常成熟 |
embassy-executor | 异步任务执行器 | ⭐⭐⭐⭐⭐ 生产级 |
embassy-time | 异步定时器 | ⭐⭐⭐⭐⭐ 生产级 |
embassy-sync | 任务间通信 | ⭐⭐⭐⭐⭐ 生产级 |
embassy-stm32 | STM32 HAL | ⭐⭐⭐⭐ 覆盖广,持续完善 |
defmt / defmt-rtt | 日志框架 | ⭐⭐⭐⭐⭐ 生产级 |
heapless | 无堆数据结构 | ⭐⭐⭐⭐⭐ 非常成熟 |
critical-section | 临界区抽象 | ⭐⭐⭐⭐⭐ 非常成熟 |
embedded-hal | 硬件抽象接口 | ⭐⭐⭐⭐⭐ 1.0 已稳定 |
probe-rs | 调试/烧录工具 | ⭐⭐⭐⭐⭐ 生产级 |
3.11 本章小结#
完整的最小裸机工程#
让我们把本章的所有内容汇总为一个完整的最小工程:
文件结构:
hello_bare_metal/
├── Cargo.toml
├── memory.x
├── build.rs
├── .cargo/
│ └── config.toml
└── src/
└── main.rsplaintextCargo.toml:
[package]
name = "hello_bare_metal"
version = "0.1.0"
edition = "2021"
[dependencies]
cortex-m-rt = "0.7"
cortex-m = "0.7"
panic-halt = "1.0"
defmt = "0.3"
defmt-rtt = "0.4"
[profile.release]
opt-level = "s"
lto = true
codegen-units = 1
panic = "abort"toml.cargo/config.toml:
[build]
target = "thumbv7em-none-eabihf"
[target.thumbv7em-none-eabihf]
runner = "probe-rs run --chip STM32F407VGTx"
rustflags = [
"-C", "link-arg=-Tlink.x",
"-C", "link-arg=-Tdefmt.x",
]
[env]
DEFMT_LOG = "debug"tomlmemory.x:
MEMORY
{
FLASH : ORIGIN = 0x08000000, LENGTH = 1024K
RAM : ORIGIN = 0x20000000, LENGTH = 128K
}plaintextbuild.rs:
use std::env;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
fn main() {
println!("cargo:rerun-if-changed=memory.x");
let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap());
File::create(out.join("memory.x"))
.unwrap()
.write_all(include_bytes!("memory.x"))
.unwrap();
println!("cargo:rustc-link-search={}", out.display());
}rustsrc/main.rs:
#![no_std]
#![no_main]
use cortex_m_rt::entry;
use defmt_rtt as _;
use panic_halt as _;
#[entry]
fn main() -> ! {
defmt::info!("Hello from bare-metal Rust!");
let mut counter: u32 = 0;
loop {
defmt::info!("Counter: {}", counter);
counter += 1;
// 简单的忙等待延时(后续章节会用 Timer 替代)
cortex_m::asm::delay(8_000_000);
}
}rust运行#
# 一条命令:编译 + 烧录 + 运行 + 日志
cargo run --releasebash本章要点回顾#
- Rust 裸机工程比 C 简洁得多:不需要手写启动汇编、不需要手动管理库、不需要复杂的构建脚本
cortex-m-rt替代了startup.s:向量表、.data初始化、.bss清零全部自动完成- target triple 指定编译目标:
thumbv7em-none-eabihf= Cortex-M4/M7 + 硬件 FPU + 裸机 - probe-rs 一个工具替代 OpenOCD + GDB + RTT Viewer:
cargo run一键完成编译+烧录+日志 - Cargo 是包管理器 + 构建系统:
Cargo.toml声明依赖,cargo build自动下载、编译、链接 - Feature Flag 是类型安全的
#ifdef:由 Cargo 管理,支持依赖传递和冲突检测
下一步#
在第 4 章中,我们将深入探讨 Rust 最让 C 工程师”又爱又恨”的特性——零成本抽象。你会看到:
- GPIO 的类型状态模式如何在编译期阻止非法操作
- 反汇编对比证明 Rust 的抽象层不产生任何额外机器码
unsafe在嵌入式中的正确角色- 单例模式如何保证”一个芯片只有一个 GPIOA”
这些概念是理解后续 Embassy HAL 设计的基础。
下一章:第 4 章——零成本抽象:当类型系统遇见寄存器。