知识门户

返回

第 14 章:性能与体积优化

Part 3:工程实践与生态全景

views | comments

本章要回答的问题:如何控制固件体积?如何优化性能?异步运行时的开销有多大?常见的陷阱和解决方案是什么?


14.1 编译优化配置#

14.1.1 C 工程师的优化直觉#

在 C 的世界里,你已经熟悉这些编译选项:

# Makefile 中的典型优化配置
CFLAGS += -Os          # 优化体积
CFLAGS += -flto        # 链接时优化
CFLAGS += -ffunction-sections -fdata-sections  # 分段,配合 --gc-sections
LDFLAGS += -Wl,--gc-sections  # 移除未使用的段
makefile

Rust 的等价配置在 Cargo.toml[profile.release] 段中。好消息是:Rust 的默认 Release 配置已经比 C 的默认 -O2 更激进。坏消息是:如果你不额外配置,仍然有优化空间。

14.1.2 [profile.release] 关键配置#

14.1.3 各配置项的效果量化#

以一个典型的 Embassy 工程(STM32F407,3 个任务,UART + I2C + Timer)为例:

配置Flash 占用RAM 占用编译时间
默认 Release(opt-level=3, lto=false)42.3 KB8.2 KB12s
opt-level=“z”35.1 KB8.2 KB14s
+ lto=true31.8 KB8.0 KB28s
+ codegen-units=130.9 KB8.0 KB35s
+ panic=“abort”29.7 KB7.8 KB35s
+ strip=true28.4 KB7.8 KB35s

关键观察

  • opt-level="z" 比默认减少 ~17% Flash
  • lto=true 在此基础上再减少 ~9%
  • panic="abort" 移除约 1.2 KB 的 unwind 代码
  • 总优化幅度:从 42.3 KB → 28.4 KB,减少 33%

14.1.4 与 C 编译选项的完整对照#

C (GCC)Rust (Cargo.toml)说明
-O0opt-level = 0无优化(调试用)
-O1opt-level = 1基本优化
-O2opt-level = 2标准优化(Rust 默认)
-O3opt-level = 3激进优化
-Osopt-level = "s"体积优化
-Ozopt-level = "z"极致体积优化
-fltolto = true链接时优化
-fno-exceptionspanic = "abort"移除异常/展开代码
-sstrip = true移除符号
-gdebug = 2调试信息
-ffunction-sections默认启用Rust 总是按函数分段
-Wl,--gc-sections默认启用Rust 链接器总是 GC 未用段

C 工程师注意:Rust 默认就启用了 -ffunction-sections--gc-sections 的等价行为。你不需要手动配置。这意味着未使用的函数和数据总是会被移除——不像 C 中经常忘记加这些选项。

14.1.5 开发阶段的快速编译配置#

Release 配置虽然产物小,但编译慢。开发阶段可以使用专门的 dev 配置:

[profile.dev]
opt-level = 0          # 不优化(编译最快)
debug = 2              # 完整调试信息
overflow-checks = true # 整数溢出检查(开发时有用)

# 对依赖库使用更高优化(你的代码不优化,但库优化)
[profile.dev.package."*"]
opt-level = 2          # 依赖库用 O2(加速 HAL 和 Embassy 的运行)
toml

技巧[profile.dev.package."*"] 让依赖库以 O2 编译,而你自己的代码以 O0 编译。这样既保持了你自己代码的快速编译,又让 HAL/Embassy 等库运行得更快。


14.2 代码体积分析#

14.2.1 cargo size:查看 Flash 和 RAM 占用#

# 安装
cargo install cargo-binutils
rustup component add llvm-tools-preview

# 查看各段大小
cargo size --release
bash

输出示例:

   text    data     bss     dec     hex filename
  29184     128    7936   37248    9180 my-app
plaintext
含义存储位置C 的对应
text代码 + 只读数据FlashCode + RO-data
data已初始化的全局/静态变量Flash → RAMRW-data
bss未初始化的全局/静态变量RAM(启动时清零)ZI-data

与 Keil 的对比:Keil 编译后输出 Program Size: Code=7492 RO-data=556 RW-data=72 ZI-data=11688。Rust 的 text ≈ Code + RO-data,data ≈ RW-data,bss ≈ ZI-data。

14.2.2 cargo bloat:找出体积大户#

# 安装
cargo install cargo-bloat

# 按函数大小排序(前 20 个最大的函数)
cargo bloat --release -n 20

# 按 crate 大小排序(哪个依赖库占空间最多)
cargo bloat --release --crates -n 20
bash

输出示例(按函数):

Analyzing target/thumbv7em-none-eabihf/release/my-app

 File  .text     Size Crate Name
 3.2%   9.1%  2.6KB my_app my_app::tasks::comm::run::h1a2b3c4d
 2.8%   8.0%  2.3KB embassy_stm32 embassy_stm32::uart::Uart::write::h5e6f7g8h
 2.1%   6.0%  1.7KB my_app my_app::protocol::modbus::encode::h9i0j1k2l
 1.9%   5.4%  1.5KB embassy_stm32 embassy_stm32::i2c::I2c::write_read::hm3n4o5p
 1.5%   4.3%  1.2KB core core::fmt::write::hq6r7s8t
 ...
plaintext

输出示例(按 crate):

Analyzing target/thumbv7em-none-eabihf/release/my-app

 File  .text     Size Crate
38.2%  38.2% 10.8KB embassy_stm32
22.1%  22.1%  6.3KB my_app
15.3%  15.3%  4.3KB embassy_executor
 8.7%   8.7%  2.5KB embassy_time
 5.2%   5.2%  1.5KB defmt
 3.1%   3.1%  0.9KB cortex_m_rt
 ...
plaintext

关键洞察:在这个典型工程中,embassy_stm32(HAL)占了 38%,你的应用代码只占 22%。这意味着:

  1. HAL 是体积大户——但这是”必要开销”,它替代了你在 C 中手写的寄存器操作代码
  2. embassy_executor(运行时)只占 15%——异步运行时的开销远小于 RTOS 内核
  3. defmt 只占 5%——日志框架的体积开销很小

14.2.3 与同等功能 C 代码的体积对比#

以”UART 回显 + LED 闪烁 + I2C 传感器读取”为例:

实现方式Flash 占用RAM 占用备注
C + STM32Cube HAL24.5 KB6.8 KB使用 HAL 库
C + 寄存器直接操作8.2 KB3.1 KB手写寄存器
Rust + embassy-stm32(默认配置)42.3 KB8.2 KB未优化
Rust + embassy-stm32(完整优化)28.4 KB7.8 KBopt-z + lto + abort
Rust + 裸机 PAC(无 HAL)12.6 KB4.0 KB直接操作寄存器

分析

  1. Rust + Embassy(优化后)vs C + HAL:Rust 版本大约 16%。这个差距主要来自:

    • Embassy 运行时代码(~4 KB)
    • defmt 日志框架(~1.5 KB)
    • Rust 的泛型单态化产生的额外代码
    • 更完整的错误处理路径
  2. Rust + 裸机 PAC vs C + 寄存器:Rust 版本大约 54%。差距缩小到可接受范围,且 Rust 版本有类型安全保证。

  3. 公平对比:如果 C 版本也加上 RTOS(FreeRTOS),体积会增加 ~10-15 KB。此时 Rust + Embassy(28.4 KB)反而比 C + FreeRTOS + HAL(~38 KB)更小。

结论:不要拿”Rust + 完整框架”和”C + 裸机手写”对比。公平的对比是”Rust + Embassy”vs”C + RTOS + HAL”——在这个维度上,Rust 的体积劣势很小,甚至可能更优。

14.2.4 减小体积的实用技巧#

技巧 1:只启用需要的外设

# 错误:启用所有外设(默认)
embassy-stm32 = { version = "0.2", features = ["stm32f407vg"] }

# 正确:只启用需要的外设驱动
embassy-stm32 = { version = "0.2", features = [
    "stm32f407vg",
    "memory-x",
    # 不要启用不用的外设 feature
] }
toml

实际上,embassy-stm32 的体积主要来自你实际使用的外设。LLVM 的 dead code elimination 会移除未调用的外设驱动代码。但某些 feature flag 会引入额外的初始化代码。

技巧 2:减少泛型实例化

// 不好:每种配置都生成一份代码
fn process<I2C: embedded_hal_async::i2c::I2c>(i2c: &mut I2C) { /* ... */ }
// 如果 I2C1 和 I2C2 都调用,会生成两份代码

// 好:如果只有一种 I2C 实例,用具体类型
fn process(i2c: &mut embassy_stm32::i2c::I2c<'static>) { /* ... */ }
// 只生成一份代码
rust

技巧 3:使用 heapless 替代格式化

// 不好:core::fmt 的格式化代码体积较大
let mut buf = [0u8; 64];
let s = core::fmt::format(format_args!("T:{}.{:02}", temp / 100, temp % 100));

// 好:手动格式化,避免引入 fmt 基础设施
fn format_temp(buf: &mut [u8; 8], temp: i16) -> usize {
    let sign = if temp < 0 { buf[0] = b'-'; 1 } else { 0 };
    let val = temp.unsigned_abs();
    let mut pos = sign;
    buf[pos] = b'0' + (val / 100) as u8; pos += 1;
    buf[pos] = b'.'; pos += 1;
    buf[pos] = b'0' + ((val / 10) % 10) as u8; pos += 1;
    buf[pos] = b'0' + (val % 10) as u8; pos += 1;
    pos
}
rust

技巧 4:条件编译移除调试代码

// 在 Release 构建中完全移除调试逻辑
#[cfg(debug_assertions)]
fn debug_dump_state(state: &SystemState) {
    defmt::debug!("state: {}", state);
}

#[cfg(not(debug_assertions))]
fn debug_dump_state(_state: &SystemState) {
    // 空函数,编译器会完全移除
}
rust

14.3 异步运行时的开销分析#

14.3.1 C 工程师的核心疑虑#

“Embassy 的异步运行时到底占多少资源?比我直接写超级循环慢多少?比 FreeRTOS 省多少?”

这是每个 C 工程师在考虑 Embassy 时最关心的问题。让我们用数据回答。

14.3.2 Embassy Executor 的 RAM 开销#

Embassy Executor 的 RAM 开销由以下部分组成:

组成部分大小说明
Executor 本身~64 字节就绪队列、状态标志
每个任务的元数据~16 字节任务状态、Waker 指针
每个任务的 Future取决于任务代码编译器生成的状态机
每个任务的栈(这是关键!)没有独立的调用栈

关键区别:Embassy 的任务没有独立的调用栈。这与 FreeRTOS 完全不同:

维度FreeRTOSEmbassy
每个任务的 RAMTCB (~100B) + 栈 (128-4096B)Future 大小 + 16B 元数据
典型任务开销256-1024 字节32-256 字节
栈溢出风险有(栈大小需要预估)无(没有栈)
上下文切换开销保存/恢复所有寄存器 (~32B × 2)无(没有切换)

实际测量:一个包含 3 个典型任务的 Embassy 工程:

任务 1(传感器读取):Future 大小 = 88 字节
任务 2(UART 通信):Future 大小 = 144 字节
任务 3(LED 控制):  Future 大小 = 48 字节
Executor 本身:       64 字节
─────────────────────────────────────
总 RAM 开销:         344 + 48(元数据)= ~400 字节
plaintext

同等功能的 FreeRTOS 工程:

任务 1:TCB (100B) + 栈 (512B) = 612 字节
任务 2:TCB (100B) + 栈 (512B) = 612 字节
任务 3:TCB (100B) + 栈 (256B) = 356 字节
调度器 + 定时器:     ~200 字节
─────────────────────────────────────
总 RAM 开销:         ~1780 字节
plaintext

结论:Embassy 的 RAM 开销约为 FreeRTOS 的 1/4 到 1/5。对于 RAM 只有 8-20 KB 的小型 MCU(如 STM32F030、STM32G030),这个差距是决定性的。

14.3.3 .await 的运行时成本#

.await 在运行时到底做了什么?答案是:几乎什么都没有

让我们看一个 .await 展开后的等价代码:

运行时成本分析

操作CPU 周期说明
match self.state1-2 周期一次比较 + 跳转
led.toggle()3-5 周期直接寄存器操作(与 C 相同)
timer.poll(cx)5-10 周期检查时间是否到达
return Poll::Pending1 周期函数返回
总计(每次 poll)~10-20 周期在 168 MHz 下约 60-120 ns

与 C 的对比:如果你在 C 中写一个状态机来实现同样的功能,你需要:

  • 一个 switch(state) 语句(1-2 周期)
  • 手动保存/恢复局部变量(0 周期,因为已经在结构体中)
  • 手动检查定时器(5-10 周期)

总成本完全相同.await 不是”额外的开销”——它就是状态机本身。编译器只是帮你写了你本来就要写的 switch-case

14.3.4 与裸机轮询的性能对比#

维度Embassy 异步C 阻塞轮询
I2C 传输期间 CPU 状态空闲(可执行其他任务)忙等(浪费 CPU)
100ms 延时期间 CPU 状态空闲(WFI 低功耗)忙等(浪费 CPU + 功耗)
多任务并发自然并发需要手动状态机
响应延迟取决于 poll 频率(通常 < 1μs)取决于轮询周期

关键优势:Embassy 的”开销”不是性能损失,而是性能增益。因为 .await 让 CPU 在等待 I/O 时去做别的事(或进入低功耗模式),而 C 的阻塞调用只能傻等。

14.3.5 与 RTOS 的资源开销对比#

维度FreeRTOSEmbassy裸机超级循环
内核 Flash 占用~5-10 KB~3-4 KB0
每任务 RAM256-1024 B32-256 B0(无任务概念)
上下文切换延迟5-20 μs0(无切换)N/A
调度延迟1-10 μs< 1 μsN/A
优先级反转风险无(协作式)N/A
栈溢出风险N/A
死锁风险极低N/A

14.4 运行时性能优化#

14.4.1 关键路径的优化策略#

在嵌入式系统中,“关键路径”通常是:

  • 中断响应延迟
  • 通信协议的帧处理时间
  • 控制环路的周期抖动

策略 1:将时间关键代码放在中断中,而非任务中

策略 2:使用 InterruptExecutor 实现抢占式高优先级任务

14.4.2 #[inline]#[inline(always)]#

// 默认行为:编译器自行决定是否内联
fn crc8(data: &[u8]) -> u8 { /* ... */ }

// 建议内联:对小函数有效
#[inline]
fn to_celsius(raw: u16) -> f32 {
    (raw as f32) * 0.01 - 40.0
}

// 强制内联:对极小的热路径函数
#[inline(always)]
fn ring_buffer_push(buf: &mut [u8; 256], head: &mut usize, val: u8) {
    buf[*head] = val;
    *head = (*head + 1) & 0xFF;
}
rust

C 工程师类比#[inline]inline#[inline(always)]__attribute__((always_inline))。区别在于 Rust 的 #[inline] 是”建议”,编译器可能忽略;#[inline(always)] 是”强制”。

何时使用

  • 函数体 < 5 行:考虑 #[inline(always)]
  • 函数在热循环中被调用:考虑 #[inline]
  • 函数体 > 20 行:不要内联(会增大代码体积)

14.4.3 DMA 减少 CPU 负载#

DMA 是 Embassy 异步模型的最佳搭档。没有 DMA,异步就失去了大部分意义:

// 没有 DMA:CPU 逐字节搬运(浪费 CPU)
// 即使用了 async,底层仍然是 CPU 轮询
let mut byte = 0u8;
for i in 0..256 {
    uart.read(&mut byte).await;  // 每次只读 1 字节
    buf[i] = byte;
}

// 有 DMA:CPU 发起传输后完全释放
let mut buf = [0u8; 256];
uart.read(&mut buf).await;  // DMA 搬运 256 字节,CPU 去做别的事
// 在 DMA 传输期间,CPU 可以执行其他任务或进入 WFI
rust

性能对比(256 字节 UART 接收,波特率 115200):

方式CPU 占用时间CPU 利用率
轮询(无 DMA)~22 ms(全程忙等)100%
中断(逐字节)~22 ms(256 次中断)~15%(每次中断 ~1μs)
DMA + 异步~0 ms(CPU 完全释放)~0%

14.4.4 中断优先级调优#

与 C 的对比:在 C 中你通过 HAL_NVIC_SetPriority() 配置优先级。Rust 版本的区别是:优先级配置是类型安全的——你不能给一个不存在的中断设置优先级(编译错误)。


14.5 常见陷阱与解决方案#

14.5.1 陷阱 1:在 .await 循环中忘记 yield#

症状:某个任务”霸占” CPU,其他任务响应变慢。

原因:循环中没有 .await 点,或者 .await 立即返回 Ready

诊断方法:在循环中加 defmt::trace!,观察输出频率。如果某个任务的日志”刷屏”,说明它没有让出 CPU。

14.5.2 陷阱 2:任务栈溢出(静态分配的栈没有保护)#

症状:随机 HardFault,难以复现,PC 指向无效地址。

原因:Embassy 任务的”栈”实际上是 Future 结构体中的局部变量。如果函数调用链过深,或者局部变量过大,Future 结构体会超出预期。

关键点:在 async Rust 中,嵌套的 .await 会导致 Future 大小叠加。这与 C 的栈帧叠加类似,但有一个重要区别:

  • C 的栈是动态增长的(直到溢出)
  • Rust 的 Future 大小是编译期确定的(编译器会告诉你)
# 查看 Future 大小
# 在代码中添加:
fn assert_send<T: Send>() {}
fn check_size<T>() {
    println!("size of {} = {}", core::any::type_name::<T>(), core::mem::size_of::<T>());
}

# 或者使用编译器警告:
# RUSTFLAGS="-Zprint-type-sizes" cargo build  (nightly)
bash

修复

14.5.3 陷阱 3:defmt 日志在 Release 模式下的体积影响#

症状:Release 固件比预期大 2-5 KB。

原因:即使设置了 DEFMT_LOG=infotracedebug 级别的日志代码仍然会被编译(只是不输出)。某些情况下,日志参数中的复杂表达式仍会生成代码。

// 问题:即使日志不输出,format 参数仍可能被计算
defmt::trace!("state: {}", compute_expensive_debug_info());
// compute_expensive_debug_info() 可能仍被编译和调用!
rust

修复

// 方案 1:使用条件编译
#[cfg(feature = "verbose-log")]
defmt::trace!("state: {}", compute_expensive_debug_info());

// 方案 2:确保 defmt 的级别过滤在编译期生效
// 在 .cargo/config.toml 中:
// [env]
// DEFMT_LOG = "info"
// 这会让 trace/debug 的整个调用(包括参数)被编译器移除

// 方案 3:对于特别大的日志,使用 if 守卫
if cfg!(debug_assertions) {
    defmt::debug!("dump: {=[u8]:x}", &large_buffer);
}
rust

验证方法

# 比较有无日志的固件大小
cargo size --release  # 有日志
# 临时注释掉所有 defmt 调用
cargo size --release  # 无日志
# 差值就是日志的实际体积开销
bash

14.5.4 陷阱 4:泛型单态化导致的代码膨胀#

症状cargo bloat 显示多个”看起来一样”的函数占用了大量空间。

原因:Rust 的泛型是单态化的——每种类型参数组合都会生成一份独立的机器码。

// 这个泛型函数会被实例化多次
async fn read_sensor<I2C: embedded_hal_async::i2c::I2c>(i2c: &mut I2C, addr: u8) -> u16 {
    let mut buf = [0u8; 2];
    i2c.write_read(addr, &[0x00], &mut buf).await.unwrap();
    u16::from_be_bytes(buf)
}

// 如果你用 I2C1 和 I2C2 都调用了这个函数:
read_sensor(&mut i2c1, 0x76).await;  // 生成一份代码
read_sensor(&mut i2c2, 0x68).await;  // 又生成一份代码(几乎相同)
rust

诊断

cargo bloat --release -n 50 | grep read_sensor
# 如果看到多个 read_sensor 的实例,说明发生了单态化膨胀
bash

修复

// 方案 1:如果只有一种 I2C 实例,用具体类型
async fn read_sensor(i2c: &mut embassy_stm32::i2c::I2c<'static>, addr: u8) -> u16 {
    // 只生成一份代码
}

// 方案 2:使用 trait object(动态分发,有运行时开销)
async fn read_sensor(i2c: &mut dyn embedded_hal_async::i2c::I2c<Error = SomeError>, addr: u8) -> u16 {
    // 只生成一份代码,但有虚函数调用开销(~5-10 周期)
}

// 方案 3:接受膨胀(如果代码很小,膨胀可忽略)
// 对于 < 50 字节的函数,单态化的额外开销通常 < 100 字节
// 不值得为此牺牲类型安全
rust

经验法则

  • 函数体 < 100 字节:不用管,膨胀可忽略
  • 函数体 100-500 字节:考虑用具体类型替代泛型
  • 函数体 > 500 字节:必须避免不必要的单态化

14.5.5 陷阱 5:Mutex 持有时间过长#

症状:系统响应变慢,某些任务”卡顿”。

原因:异步 Mutex 在持有期间,其他任务无法获取锁。如果持有锁的任务在锁内执行了耗时操作(特别是 .await),其他任务会被长时间阻塞。

C 工程师类比:这与 C 中”关中断时间不能太长”是同一个原则。在 FreeRTOS 中,你被教导”不要在持有 Mutex 时调用可能阻塞的 API”。Rust 的异步 Mutex 同理——但 Rust 编译器不会阻止你在锁内 .await(这是一个已知的 API 设计权衡)。

最佳实践

  1. 锁的持有时间 < 10 μs
  2. 锁内不要 .await
  3. 如果必须 .await,使用 RefCell(单线程场景)或重新设计

14.5.6 陷阱 6:跨平台移植中的时钟/DMA/GPIO 差异#

症状:代码在 STM32 上正常工作,移植到另一个平台后出现通信错误、时序异常或硬件损坏。

原因:不同平台的硬件特性存在细微但关键的差异。

差异 1:时钟源导致波特率不准

// STM32F4:默认 HSI 16 MHz,PLL 倍频到 168 MHz
// UART 波特率 = APB1_CLK / (16 * USARTDIV)
// 如果 APB1 = 42 MHz,115200 baud 的 USARTDIV = 22.786

// nRF52840:默认 64 MHz(内部 RC 或外部晶振)
// UART 波特率由 BAUDRATE 寄存器直接设置(预定义值)
// 115200 对应 0x01D7E000

// ESP32-C3:默认 80 MHz(PLL)
// UART 波特率 = CLK / (divider + fraction)
rust

解决方案:不要手动计算分频值。使用 HAL 提供的波特率配置 API:

// 正确做法:让 HAL 根据实际时钟频率计算
let config = uart::Config::default();
config.baudrate = 115200;  // HAL 内部会根据当前时钟计算正确的分频
let uart = Uart::new(p.UART1, tx, rx, Irqs, dma_tx, dma_rx, config);
rust

差异 2:GPIO 电气特性

平台5V 容忍最大输出电流上拉/下拉
STM32F4部分引脚(FT 标记)25 mA可配置
nRF52840❌ 仅 3.3V15 mA可配置
RP2040❌ 仅 3.3V12 mA可配置
ESP32-C3❌ 仅 3.3V40 mA可配置

解决方案:移植前查阅目标芯片的 GPIO 电气特性表。如果外部电路是 5V 逻辑,需要加电平转换。

差异 3:DMA 缓冲区对齐

差异 4:中断优先级位宽

平台优先级位数可用级别最高优先级
STM32F44 位0-150
nRF528403 位0-70
RP20402 位0-30
ESP32-C35 位0-311(0 保留)

解决方案:使用 Embassy 提供的优先级抽象,避免硬编码数值:

// 不好:硬编码优先级值
interrupt::UART4.set_priority(2);  // 在 RP2040 上无效(只有 0-3)

// 好:使用 Embassy 的优先级枚举
use embassy_stm32::interrupt::Priority;
interrupt::UART4.set_priority(Priority::P2);
// 如果目标平台不支持 P2,编译时会报错
rust

14.6 优化检查清单#

在发布固件之前,逐项检查:

体积优化#

检查项命令/操作预期效果
使用 opt-level = "z""s"Cargo.toml-15~20% Flash
启用 lto = trueCargo.toml-5~10% Flash
设置 codegen-units = 1Cargo.toml-2~5% Flash
设置 panic = "abort"Cargo.toml-1~2 KB Flash
设置 strip = trueCargo.toml-0.5~1 KB Flash
运行 cargo bloat 检查大户命令行发现优化目标
移除未使用的 featureCargo.toml视情况
检查 defmt 日志级别.cargo/config.toml-1~3 KB Flash

性能优化#

检查项操作预期效果
所有 I/O 使用 DMA代码审查CPU 利用率 ↓ 80%+
时间关键逻辑用中断驱动架构调整抖动 < 1 μs
热路径函数加 #[inline]代码标注-5~10% 延迟
中断优先级合理配置优先级表避免优先级反转
Mutex 持有时间 < 10 μs代码审查避免任务卡顿
循环中有 .await 让出点代码审查避免任务饿死

RAM 优化#

检查项操作预期效果
大缓冲区用 static 分配代码重构减少 Future 大小
Channel 容量按需设置配置调整减少静态 RAM
任务数量最小化架构审查减少元数据开销
检查 Future 大小size_of::<T>()发现异常大的任务

14.7 本章小结#

本章我们用数据和代码回答了 C 工程师最关心的性能问题。关键结论:

  1. 编译优化配置opt-level="z" + lto=true + panic="abort" + strip=true 可以将固件体积减少 30%+。这些配置与 C 的 -Oz -flto -fno-exceptions -s 完全对应。

  2. 体积分析cargo bloat 是定位体积大户的利器。在典型 Embassy 工程中,HAL 占 ~38%,运行时占 ~15%,应用代码占 ~22%。

  3. 异步运行时开销极小:Embassy Executor 本身只占 ~64 字节 RAM + ~3-4 KB Flash。每个任务的开销是 Future 大小 + 16 字节元数据——没有独立的栈。总 RAM 开销约为 FreeRTOS 的 1/4。

  4. .await 的运行时成本几乎为零:它就是编译器帮你写的状态机 switch-case。每次 poll 的额外开销约 10-20 个 CPU 周期(< 120 ns @ 168 MHz)。

  5. DMA 是性能的关键:没有 DMA 的异步只是”换了个写法的轮询”。有了 DMA,CPU 在 I/O 期间完全释放,可以执行其他任务或进入低功耗模式。

  6. 六大陷阱:忘记 yield、Future 过大、日志体积、泛型膨胀、Mutex 持有过长、跨平台差异。每个都有明确的诊断方法和修复方案。

下一章预告:第 15 章是全书的收尾——我们将纵览 Rust 嵌入式的完整生态,讨论安全关键领域的 Rust 应用(Ferrocene、ISO 26262),给出从 C 项目迁移到 Rust 的实用策略,并为不同场景的读者提供选型建议。

Comment seems to stuck. Try to refresh?✨