Rust 进军 MCU 嵌入式领域:ST 官方案例深度剖析与从零实战指南
一、背景:嵌入式开发的三十年困局
在软件工程领域,有一个被反复提及却始终难以解决的矛盾:嵌入式系统占据了全球计算设备的 90% 以上,从你手腕上的智能手表到工厂里的 PLC 控制器,从汽车里的 ECU 到家里的路由器——它们全部运行在资源极其受限的微控制器(MCU)上,而统治这片领域的语言,依然是诞生于 1972 年的 C 语言。
根据 2026 年工信部发布的行业数据,国内嵌入式软件外包市场规模已突破 1800 亿元,但 C/C++ 带来的内存安全漏洞始终是悬在行业头顶的达摩克利斯之剑。Google 在 Android 上的数据表明,Rust 将内存安全漏洞密度降低了 1000 倍,代码审查时间减少了 25%,回滚率降低了 4 倍。这些数字在 Linux 内核、Android 系统层面已经得到验证——但问题来了:在资源极其有限的 MCU 上,Rust 能行吗?
这个问题在 2026 年终于有了来自顶级芯片厂商的权威回答。
二、ST 官方案例:C vs Rust 的工业级对决
意法半导体(STMicroelectronics)在 2026 年发布了一项开创性的物联网工业案例研究。他们让两个团队并行开发同一款固件——一个用 C,一个用 Rust,目标硬件是基于 ARM Cortex-M33 的 STM32U585AI 微控制器(160MHz,2MiB Flash,786KiB SRAM)。这不是学术界的 toy project,这是 STAIoTCraft 平台的真实产品代码。
2.1 测试结果速览
先上结论:没有充分的理由在 MCU 固件开发中优先选择 C 而不是 Rust。
| 指标 | VDL-C (C语言) | VDL-Rust (Rust) | 差异 |
|---|---|---|---|
| 代码大小 (.text) | 66,240 B | 69,764 B | Rust 大 5% |
| 总 ROM 占用 | 76,744 B | 84,100 B | Rust 大 10% |
| 应用代码大小 | 18,084 B | 24,064 B | Rust 大 23% |
| 系统运行时代码 | — | — | Rust 反而小 10% |
| 栈使用 | 2,048 B | — | 两者相当 |
| 静态内存 | 14,960 B | 更低 | Rust 无堆分配 |
关键发现:
- 应用代码 Rust 大 23%,主要是 serde 编译时单态化的代价,但这是可控的
- 系统运行时代码 Rust 反而更小——Ariel OS + 启动代码 < newlib + CubeMX 启动代码
- Rust 完全使用静态内存,而 C 依赖 malloc/free 进行 JSON 解析
- 执行速度方面,通过逻辑分析仪测量,两者差异在可接受范围内
2.2 架构设计对比
VDL-C(C 语言方案)的架构:
┌──────────────────────────┐
│ 应用层 (FSM) │
├──────────────────────────┤
│ VDP 协议栈 + Parson JSON │
├──────────────────────────┤
│ STM32CubeMX HAL/BSP │
├──────────────────────────┤
│ ARM Cortex-M33 硬件 │
└──────────────────────────┘
C 方案基于 STM32CubeMX 自动生成代码,采用经典的有限状态机(FSM)模式,运行在裸机上。手动管理中断使能/禁用来实现并发,JSON 处理依赖 Parson 库的动态堆分配。
VDL-Rust(Rust 方案)的架构:
┌──────────────────────────┐
│ 业务逻辑层 (async) │
├──────────────────────────┤
│ VDP 协议 (serde + heapless) │
├──────────────────────────┤
│ Ariel OS (基于 Embassy) │
├──────────────────────────┤
│ PAC/PAC HAL │
├──────────────────────────┤
│ ARM Cortex-M33 硬件 │
└──────────────────────────┘
Rust 方案基于 Ariel OS(一个专为 MCU 设计的轻量级库操作系统),使用 async/await 进行并发编程,serde-json-core + heapless 实现零堆分配的 JSON 处理,embassy-sync 提供类型安全的 IPC 原语。
三、Rust 嵌入式核心概念:从 no_std 开始
3.1 #![no_std] 是什么?
标准 Rust 程序默认链接 std 库,它假设底层有操作系统、堆分配器、文件系统等。但在 MCU 上,这些东西都不存在。#![no_std] 告诉编译器:不要依赖任何操作系统假设。
#![no_std]
#![no_main]
use panic_halt as _;
#[cortex_m_rt::entry]
fn main() -> ! {
loop {
// 你的嵌入式代码
}
}
关键要素:
#![no_std]:禁用标准库#![no_main]:禁用标准入口点(MCU 没有操作系统的 main 约定)panic_halt:定义 panic 行为(嵌入式里没有 stderr 可以写)#[cortex_m_rt::entry]:标记入口函数,由 cortex-m-rt 提供启动代码
3.2 静态内存分配:告别 malloc/free
在 MCU 上使用动态内存是危险的——堆碎片、不确定的分配延迟、内存泄漏,任何一个都可能导致系统崩溃。Rust 的嵌入式生态提供了优雅的解决方案:
heapless crate——固定容量的集合类型
use heapless::{Vec, String, FnvIndexMap};
// 固定容量 32 字节的字符串
let mut greeting: String<32> = String::new();
greeting.push_str("Hello, MCU!").unwrap();
// 固定容量 8 个 i32 的向量
let mut buffer: Vec<i32, 8> = Vec::new();
buffer.push(42).unwrap();
buffer.push(7).unwrap();
// 固定容量的 HashMap
let mut config: FnvIndexMap<&str, i32, 4> = FnvIndexMap::new();
config.insert("baud_rate", 115200).unwrap();
config.insert("timeout", 5000).unwrap();
这些类型在栈上或静态存储区分配内存,大小在编译时确定,运行时零堆分配。
serde + heapless 的组合
use serde::{Serialize, Deserialize};
use heapless::String;
#[derive(Serialize, Deserialize)]
struct SensorReading {
sensor_id: u8,
temperature: f32,
humidity: f32,
timestamp: u32,
}
// 序列化到固定大小的缓冲区
let reading = SensorReading {
sensor_id: 1,
temperature: 25.6,
humidity: 60.3,
timestamp: 1703275200,
};
// 使用 serde_json_core 进行无堆分配的 JSON 序列化
let mut buf = [0u8; 128];
let serialized = serde_json_core::to_slice(&reading, &mut buf).unwrap();
3.3 Embassy:异步嵌入式编程
传统嵌入式开发中,并发靠裸写状态机或者引入 RTOS(FreeRTOS、RT-Thread 等)。Embassy 提供了一种更现代的方式——在 MCU 上使用 async/await。
#![no_std]
#![no_main]
use embassy_executor::main;
use embassy_time::{Timer, Duration};
use embassy_nrf::gpio::{Input, Output, Level};
use panic_halt as _;
#[main]
async fn main(_spawner: embassy_executor::Spawner) {
let p = embassy_nrf::init(Default::default());
let mut led = Output::new(p.P0_13, Level::Low);
let mut button = Input::new(p.P0_11, embassy_nrf::gpio::Pull::Up);
loop {
// 等待按钮按下(异步,不阻塞 CPU)
button.wait_for_low().await;
// LED 闪烁 500ms
led.set_high();
Timer::after(Duration::from_millis(500)).await;
led.set_low();
}
}
Embassy 的 async/await 不是 OS 级的线程切换,而是编译器生成状态机,内存开销极小(每个 async task 只需要几十字节的栈)。这比传统 RTOS 的任务切换更轻量,比手写状态机更清晰。
3.4 PAC、HAL 和 Board Support
Rust 嵌入式生态的硬件抽象分三层:
应用代码
↓
Board Support Crate (BSC) ← 特定开发板
↓
HAL Crate ← 某厂商的 MCU 系列
↓
PAC (Peripheral Access Crate) ← 寄存器级映射
↓
硬件
PAC 是直接映射硬件寄存器的薄封装,通过 svd2rust 工具从厂商的 SVD 文件自动生成:
// 直接操作寄存器(一般不需要这么做)
use stm32u5::stm32u585::GPIOA;
unsafe {
let gpioa = &*GPIOA::ptr();
gpioa.bsrr.write(|w| w.bits(1 << 5)); // 设置 PA5
}
HAL 提供类型安全的 API:
use embassy_stm32::gpio::{Output, Level};
let mut led = Output::new(p.PA5, Level::Low);
led.set_high(); // 类型安全,编译器保证正确性
Board Support Crate 针对特定开发板预配置所有外设:
use microbit::Board;
let board = Board::take().unwrap();
let mut display = board.display;
// 直接使用,不需要手动配置引脚映射
四、实战:从零搭建你的第一个 Rust MCU 项目
4.1 环境准备
# 安装 Rust(如果还没有)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# 添加嵌入式目标(以 ARM Cortex-M 为例)
rustup target add thumbv7em-none-eabihf
# 安装 cargo-binutils(用于调试和反汇编)
cargo install cargo-binutils
# 安装 probe-rs(开源调试工具,替代 OpenOCD + GDB)
cargo install probe-rs-tools
对于 RISC-V 架构的 MCU(如乐鑫 ESP32-C6、沁恒 CH32V 等):
rustup target add riscv32imc-unknown-none-elf
4.2 创建项目
cargo new --bin sensor-demo
cd sensor-demo
编辑 Cargo.toml:
[package]
name = "sensor-demo"
version = "0.1.0"
edition = "2021"
[dependencies]
cortex-m = { version = "0.7", features = ["critical-section-single-core"] }
cortex-m-rt = "0.7"
embassy-executor = { version = "0.6", features = ["task-arena-size-32768"] }
embassy-time = "0.3"
embassy-stm32 = { version = "0.1", features = ["stm32u585ai", "time-driver-any", "exti"] }
embassy-sync = "0.6"
panic-halt = "1.0"
heapless = "0.8"
serde = { version = "1", default-features = false, features = ["derive"] }
serde-json-core = "0.6"
embedded-hal = "1.0"
[profile.release]
opt-level = "z" # 优化代码大小
lto = true # 链接时优化
codegen-units = 1 # 单编译单元,更好的优化
strip = true # 去除符号
4.3 实现一个传感器数据采集器
#![no_std]
#![no_main]
use cortex_m_rt::entry;
use embassy_executor::main;
use embassy_stm32::i2c::{I2c, Config};
use embassy_stm32::usart::{Uart, Config as UartConfig};
use embassy_sync::blocking_mutex::raw::ThreadModeRawMutex;
use embassy_sync::channel::Channel;
use embassy_time::{Duration, Timer, Ticker};
use heapless::{String, Vec};
use panic_halt as _;
use serde::Serialize;
// 传感器数据结构
#[derive(Serialize, Clone)]
struct SensorData {
temperature: i16,
humidity: u16,
pressure: u32,
timestamp: u32,
}
// 系统状态
#[derive(Serialize)]
struct SystemStatus {
uptime_ms: u32,
samples_collected: u32,
errors: u32,
firmware_version: &'static str,
}
// 通道:传感器数据 → 串口发送
static SENSOR_CHANNEL: Channel<ThreadModeRawMutex, SensorData, 16> = Channel::new();
// 全局状态(使用临界区保护)
static mut SAMPLE_COUNT: u32 = 0;
static mut ERROR_COUNT: u32 = 0;
#[main]
async fn main(spawner: embassy_executor::Spawner) {
let p = embassy_stm32::init(Default::default());
// 配置 I2C(用于传感器通信)
let mut i2c = I2c::new(
p.I2C1, p.PB6, p.PB7,
Config::default(),
);
// 配置 UART(用于数据输出)
let mut uart = Uart::new(
p.USART1, p.PA9, p.PA10,
UartConfig::default(),
);
// 启动任务
spawner.spawn(sensor_task(i2c)).ok();
spawner.spawn(transmit_task(uart)).ok();
spawner.spawn(status_task()).ok();
}
/// 传感器采集任务
#[embassy_executor::task]
async fn sensor_task(mut i2c: I2c<'static>) {
// 模拟传感器地址(实际使用你的传感器 I2C 地址)
const SENSOR_ADDR: u8 = 0x44; // 例如 SHT40 温湿度传感器
let mut ticker = Ticker::every(Duration::from_secs(1));
loop {
ticker.next().await;
let data = match read_sensor(&mut i2c, SENSOR_ADDR).await {
Ok(d) => d,
Err(_) => {
unsafe { ERROR_COUNT += 1; }
continue;
}
};
unsafe { SAMPLE_COUNT += 1; }
// 发送到传输通道(非阻塞)
SENSOR_CHANNEL.send(data).await;
}
}
/// 串口传输任务
#[embassy_executor::task]
async fn transmit_task(mut uart: Uart<'static>) {
loop {
let data = SENSOR_CHANNEL.receive().await;
// 序列化为 JSON(零堆分配)
let mut json_buf = [0u8; 128];
if let Ok(len) = serde_json_core::to_slice(&data, &mut json_buf) {
// 通过 UART 发送
uart.write_all(&json_buf[..len]).await.ok();
uart.write_all(b"\r\n").await.ok();
}
}
}
/// 状态监控任务
#[embassy_executor::task]
async fn status_task() {
let mut ticker = Ticker::every(Duration::from_secs(10));
let start = embassy_time::Instant::now();
loop {
ticker.next().await;
let status = SystemStatus {
uptime_ms: start.elapsed().as_millis() as u32,
samples_collected: unsafe { SAMPLE_COUNT },
errors: unsafe { ERROR_COUNT },
firmware_version: env!("CARGO_PKG_VERSION"),
};
// 也可以序列化状态并发送...
let _ = status; // 实际项目中发送到日志通道
}
}
/// 从 I2C 传感器读取数据
async fn read_sensor(
i2c: &mut I2c<'static>,
addr: u8,
) -> Result<SensorData, ()> {
let mut buf = [0u8; 6];
// 发送测量命令
i2c.write(addr, &[0x2C, 0x06]).await.map_err(|_| ())?;
// 等待测量完成
Timer::after(Duration::from_millis(10)).await;
// 读取数据
i2c.read(addr, &mut buf).await.map_err(|_| ())?;
// 解析数据(以 SHT40 为例)
let raw_temp = (buf[0] as u16) << 8 | buf[1] as u16;
let raw_humi = (buf[3] as u16) << 8 | buf[4] as u16;
// 转换为实际值
let temperature = ((raw_temp as i32) * 17500 / 65535 - 4500) as i16;
let humidity = (raw_humi as u32) * 10000 / 65535;
Ok(SensorData {
temperature,
humidity,
pressure: 101325, // 假设标准大气压
timestamp: embassy_time::Instant::now().as_millis() as u32,
})
}
4.4 构建和烧录
# 构建发布版本
cargo build --release --target thumbv7em-none-eabihf
# 查看代码大小
cargo size --release --target thumbv7em-none-eabihf -- -A
# 通过 probe-rs 烧录(支持 ST-Link、J-Link 等)
probe-rs run --chip STM32U585AIIx target/thumbv7em-none-eabihf/release/sensor-demo
# 或者转出 hex/bin 文件,用厂商工具烧录
objcopy -O ihex target/thumbv7em-none-eabihf/release/sensor-demo firmware.hex
4.5 内存优化技巧
在 MCU 上每一字节都珍贵,以下是几个关键优化策略:
1. 使用 #[inline] 控制函数内联
#[inline(always)]
fn fast_gpio_toggle() {
// 热路径上的函数,强制内联减少函数调用开销
}
#[inline(never)]
fn error_handler() {
// 冷路径,不内联,节省代码空间
}
2. 使用 #[repr(C)] 控制 struct 布局
#[repr(C, packed)]
struct SensorPacket {
header: u8,
sensor_id: u8,
data: [u8; 4],
checksum: u16,
}
// packed 确保无填充字节,精确匹配硬件协议
3. 编译器提示优化
// 告诉编译器这个分支更可能执行
if likely(condition) {
// 热路径
} else {
// 冷路径
}
#[cold]
fn handle_error() {
// 标记为冷函数,编译器会优化布局
}
// likely/unlikely 需要自定义
#[inline(always)]
fn likely(b: bool) -> bool { if b { true } else { false } }
4. Cargo.toml 优化配置
[profile.release]
opt-level = "z" # 优化代码大小(也可以用 "s")
lto = true # 链接时优化
codegen-units = 1 # 更好的优化(编译更慢)
panic = "abort" # 去除 panic 展开代码
strip = true # 去除调试符号
# 进一步减小体积
[profile.release.package."*"]
opt-level = "z"
五、Ariel OS:Rust 的嵌入式操作系统
ST 案例中 VDL-Rust 使用的 Ariel OS 是一个值得关注的项目。它不是一个重量级的 RTOS,而是一个轻量级的库操作系统(Library OS),专为 Rust 编写的 MCU 应用设计。
5.1 Ariel OS 的核心特点
// Ariel OS 的 "Hello World" 极其简洁
#![no_std]
use ariel_os::debug::log::*;
use ariel_os::tasks;
#[tasks::task]
async fn hello_world() {
info!("Hello from Ariel OS!");
}
关键设计决策:
- 基于 Embassy:使用 async/await,编译器生成状态机,零动态内存
- 模块化:只编译你需要的部分(UART、I2C、SPI 等)
- 跨平台:支持多种 MCU 系列(STM32、nRF52、ESP32 等)
- 小于裸机 C 堆栈:ST 的测试表明 Ariel OS 的系统运行时代码比传统裸机 C 方案更小
5.2 为什么 Library OS 模式更适合 MCU?
传统 RTOS(如 FreeRTOS)是为 C 语言设计的,它提供了:
- 线程调度
- 信号量、互斥锁
- 消息队列
- 内存池
但这些都是运行时的抽象,有运行时开销。而 Ariel OS + Embassy 的做法是:让编译器来做这些事。
// 传统 RTOS 方式(运行时抽象)
void task_a(void *param) {
while (1) {
xSemaphoreTake(mutex, portMAX_DELAY);
process_data();
xSemaphoreGive(mutex);
vTaskDelay(100);
}
}
// Embassy 方式(编译时抽象)
#[embassy_executor::task]
async fn task_a(mut mutex: Mutex<'static>) {
loop {
mutex.lock().await;
process_data().await;
drop(mutex);
Timer::after(Duration::from_millis(100)).await;
}
}
Embassy 的每个 async task 编译成一个状态机,当它 await 时,编译器已经知道了应该保存哪些寄存器、恢复哪些状态。没有运行时的线程栈切换,没有上下文保存的固定开销——编译器只保存真正需要的变量。
六、Rust 1.94 对嵌入式的重要更新
Rust 1.94.0(2026 年 3 月发布)对嵌入式开发有几个值得关注的改进:
6.1 RISC-V 支持增强
# 现在可以更方便地构建 RISC-V 目标
[targets]
riscv32imc-unknown-none-elf # ESP32-C 系列
riscv32imac-unknown-none-elf # 更通用的 RISC-V MCU
riscv64gc-unknown-none-elf # 64 位 RISC-V
这对国内 MCU 厂商意义重大——沁恒 CH32V 系列、兆易创新 GD32V 系列、乐鑫 ESP32-C 系列,都是 RISC-V 架构。更好的 RISC-V 支持意味着 Rust 可以覆盖更大的嵌入式市场。
6.2 Cargo 配置管理增强
新的 Cargo 配置功能使得管理多个嵌入式目标更加方便:
# .cargo/config.toml
[target.thumbv7em-none-eabihf]
runner = "probe-rs run --chip STM32U585AIIx"
[target.riscv32imc-unknown-none-elf]
runner = "probe-rs run --chip ESP32C6"
[build]
# 根据 CI 环境自动选择目标
# rustflags = ["-C", "link-arg=-Tlink.x"]
七、真实世界中的 Rust 嵌入式应用
7.1 Google Android:Rust 实战数据
Google 从 Android 12 开始引入 Rust,到 2025 年,内存安全漏洞首次降至 20% 以下:
// Android 中 Rust 用于安全关键的组件
// 例如 Binder IPC 的部分实现
// 这些代码处理来自其他进程的不可信输入
fn parse_untrusted_input(data: &[u8]) -> Result<ParsedMessage, ParseError> {
// Rust 的类型系统在编译时就保证了:
// - 不会越界访问 data
// - 不会忘记处理错误
// - 不会出现 use-after-free
// 这些在 C 中都需要手动保证
}
7.2 微软 Azure Sphere:IoT 安全芯片
微软的 Azure Sphere MT3620 芯片使用 Rust 编写了部分安全关键代码,用于 IoT 设备的通信安全层。
7.3 Ferrocene:安全认证的 Rust 工具链
Ferrocene 是第一个通过 ISO 26262 功能安全认证的 Rust 编译器,这意味着 Rust 现在可以用于汽车电子、医疗设备等需要功能安全认证的领域。
# Ferrocene 提供经过认证的工具链
# 支持的功能安全标准:
# - ISO 26262(汽车电子)
# - IEC 61508(工业控制)
# - EN 50128(铁路信号)
7.4 openSUSE 的 qemu-stars:用 Rust 重写 QEMU 组件
openSUSE 团队在 2026 年启动了用 Rust 重写 QEMU 部分组件的项目,目标是用内存安全语言替代 C 代码,减少虚拟化层的漏洞。
八、从 C 迁移到 Rust:渐进式策略
没有人会建议你把一个运行了十年的嵌入式 C 项目一夜之间全部用 Rust 重写。正确的做法是渐进式迁移:
8.1 策略一:新增模块用 Rust
// 原有 C 代码保持不变
// main.c
extern void rust_sensor_init(void);
extern int rust_sensor_read(int *temp, int *humi);
int main(void) {
hal_init();
rust_sensor_init(); // 调用 Rust 函数
int t, h;
while (1) {
if (rust_sensor_read(&t, &h) == 0) {
send_data(t, h);
}
}
}
// sensor.rs - 编译为 C 可调用的静态库
#![no_std]
use libc::{c_int, c_void};
#[no_mangle]
pub extern "C" fn rust_sensor_init() {
// Rust 实现的传感器初始化
}
#[no_mangle]
pub extern "C" fn rust_sensor_read(temp: *mut c_int, humi: *mut c_int) -> c_int {
if temp.is_null() || humi.is_null() {
return -1;
}
unsafe {
*temp = 25;
*humi = 60;
}
0
}
8.2 策略二:协议层用 Rust
将通信协议(如 MQTT、CoAP、自定义协议)用 Rust 实现,编译为独立的库,C 代码通过 FFI 调用。协议层通常是安全风险最高的部分(处理外部输入),用 Rust 可以显著提升安全性。
8.3 策略三:测试先行
利用 Rust 的测试框架对嵌入式代码进行单元测试:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sensor_data_parsing() {
let raw = [0x61, 0xA4, 0x00, 0x00, 0x9E, 0x93];
let data = parse_sensor_raw(&raw).unwrap();
assert!((data.temperature - 2500).abs() < 10);
assert!((data.humidity - 4000).abs() < 10);
}
#[test]
fn test_json_serialization() {
let reading = SensorData {
temperature: 2500,
humidity: 4000,
pressure: 101325,
timestamp: 1703275200,
};
let mut buf = [0u8; 128];
let len = serde_json_core::to_slice(&reading, &mut buf).unwrap();
assert!(len > 10); // 基本长度检查
}
}
嵌入式代码的测试一直是个痛点——C 的单元测试框架(如 Unity、CMock)设置繁琐,Mock 困难。Rust 的测试框架是内置的,cargo test 直接跑,甚至可以对 embedded crate 进行主机端测试(不需要实际硬件)。
九、Rust 嵌入式生态全景
9.1 核心 Crate 清单
| 类别 | Crate | 说明 |
|---|---|---|
| 异步运行时 | embassy-executor | 轻量级 async executor |
| 定时器 | embassy-time | 统一时间抽象 |
| HAL | embassy-stm32 | STM32 系列 HAL |
| HAL | embassy-nrf | Nordic nRF 系列 HAL |
| HAL | esp-hal | 乐鑫 ESP32 系列 HAL |
| 序列化 | serde + serde-json-core | 零堆分配 JSON |
| 集合 | heapless | 固定容量集合 |
| 日志 | defmt | 高效的嵌入式日志框架 |
| USB | usb-device | USB 设备栈 |
| 网络协议 | smoltcp | TCP/IP 栈 |
| RTOS 绑定 | rtic | 实时中断驱动并发 |
| 调试 | probe-rs | 开源调试工具 |
| OS | ariel-os | 轻量级库操作系统 |
9.2 defmt:嵌入式日志的革命
传统的嵌入式日志使用 println! 通过 UART 输出,每次调用都会格式化字符串,消耗大量时间和 Flash 空间。defmt(deferred formatting)采用了革命性的方式:
use defmt::{info, warn, error, trace};
// defmt 不会在目标设备上格式化字符串
// 而是把格式化信息编码为紧凑的二进制格式
// 由 PC 端的 defmt-decoder 解码
info!("Temperature: {}°C, Humidity: {}%", temp, humi);
warn!("Sensor timeout after {}ms", elapsed);
error!("I2C error: {:?}", error_code);
trace!("GPIO state: pin={}, level={}", pin, level);
这意味着:
- 目标设备上没有格式化开销(不需要存储 "%d"、"%s" 等格式字符串)
- 日志输出极快(只写几个字节的二进制数据)
- PC 端可以完整还原格式化后的日志
十、性能优化的工程实践
10.1 实测:async/await 的开销
很多人担心 Rust 的 async/await 在 MCU 上会有性能问题。ST 的案例研究使用逻辑分析仪进行了实际测量。结果显示:
- async task 切换开销:约 20-50 个时钟周期(Cortex-M33 @ 160MHz ≈ 125-312ns)
- 相比 FreeRTOS 的任务切换(约 150-300 个时钟周期),更快
- 相比手写 FSM 的"切换"(基本零开销),有可测量的开销,但在传感器采集场景(1Hz-100Hz)中完全可忽略
10.2 编译时优化检查清单
# 查看各部分代码大小
cargo bloat --release --crates
# 查看最占用空间的函数
cargo bloat --release --release --functions --sort-by-bytes
# 生成汇编代码检查
cargo objdump --release -- --disassemble --no-show-raw-insn > disasm.asm
# 检查是否有多余的 panic 处理代码
arm-none-eabi-nm target/thumbv7em-none-eabihf/release/sensor-demo | grep panic
10.3 实战性能调优案例
// ❌ 不好的写法:每次分配新 Vec
fn process_batch(readings: &[SensorData]) -> Vec<u8, 256> {
let mut result = Vec::new(); // 分配可能失败
for r in readings {
result.extend_from_slice(&r.to_bytes());
}
result
}
// ✅ 好的写法:预分配,直接写缓冲区
fn process_batch(readings: &[SensorData], buf: &mut [u8; 256]) -> usize {
let mut offset = 0;
for r in readings {
let bytes = r.to_bytes();
let remaining = &mut buf[offset..];
if bytes.len() > remaining.len() {
break; // 缓冲区已满
}
remaining[..bytes.len()].copy_from_slice(&bytes);
offset += bytes.len();
}
offset
}
// ✅ 更好:使用 DMA,CPU 不参与数据搬运
async fn process_batch_dma(
readings: &[SensorData],
uart: &mut Uart<'static>,
) -> Result<(), Error> {
for r in readings {
let bytes = r.to_bytes();
uart.write_all(&bytes).await?; // DMA 传输
}
Ok(())
}
十一、挑战与未来
11.1 当前的主要挑战
- 生态成熟度:虽然核心 HAL 已覆盖主流 MCU,但某些冷门芯片和传感器驱动仍需自行编写
- 学习曲线:
no_std+ async + 嵌入式,三重门槛对新入行的嵌入式开发者来说不低 - 工具链集成:CubeMX、Keil、IAR 等 IDE 对 Rust 的支持仍然有限
- 团队迁移成本:现有 C 团队需要培训,代码评审需要新的思维模式
- 功能安全认证:虽然 Ferrocene 已经通过认证,但整个生态的安全认证覆盖还需要时间
11.2 2026 年的发展趋势
根据 Rust 项目管理团队 2026 年 4 月的月报:
- C++ 互操作取得新进展,使得 Rust 和现有 C++ 代码库的混编更加容易
- 2026 RustWeek(5 月,荷兰乌得勒支)将集中讨论嵌入式方向
- RISC-V 支持被列为年度重点目标
11.3 未来展望
展望 2026 年下半年和 2027 年,几个关键趋势将加速 Rust 在嵌入式领域的普及:
- 更多芯片厂商官方支持 Rust HAL——ST 已经带头,Nordic、Microchip 在跟进
- Ferrocene 认证范围扩大——覆盖更多安全标准,降低行业准入门槛
- AIoT 融合——边缘 AI 推理 + Rust 的内存安全,天然契合
- 供应链安全——在"软件供应链安全"成为监管要求的背景下,Rust 的编译时保证比运行时检查更可靠
十二、总结
ST 的案例研究给出了明确的结论:Rust 已经准备好进入 MCU 嵌入式开发领域。代码大小只多 5-10%,但在安全性、可维护性和开发效率上有显著优势。
对于个人开发者,现在是最好的入局时机:
- 核心工具链已经稳定(
probe-rs、Embassy、heapless) - 学习资源日益丰富(The Embedded Rust Book、Embassy 文档)
- 社区活跃,遇到问题能在 GitHub 和 Discord 获得帮助
对于团队决策者:
- 新项目建议直接用 Rust,特别是涉及通信协议和数据解析的模块
- 现有项目采用渐进式迁移策略,从安全关键模块开始
- 投资团队培训,Rust 的学习曲线虽然陡峭,但回报丰厚
嵌入式领域的 Rust 不是取代 C,而是在 C 难以保证安全的场景中提供了一个更可靠的选择。当你的设备连接到互联网、处理不可信输入、运行在无人值守的环境中——这些场景正是 Rust 发挥优势的地方。
Rust 的嵌入式革命不是会不会发生的问题,而是以多快的速度发生的问题。从 ST 的案例来看,这一天已经来了。
参考来源:STMicroelectronics STAIoTCraft 案例研究(2026)、Rust 项目管理团队月报(2026年4月)、Google Android 安全报告(2025)、Ariel OS 官方文档、Embassy 官方文档