知识门户

返回

第 13 章:调试、测试与工程架构

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

views | comments

本章要回答的问题:异步代码怎么调试?Embassy 项目怎么组织?怎么测试?从原型到产品的工程实践是什么?


13.1 调试异步代码:从 GDB 思维到 async 思维#

13.1.1 C 工程师的调试习惯#

作为一个 C 工程师,你的调试流程大概是这样的:

1. 发现 bug(设备行为异常)
2. 连接调试器(J-Link / ST-Link)
3. 在可疑函数设断点
4. 单步执行,观察变量
5. 查看调用栈(backtrace),理解"谁调用了我"
6. 定位问题,修复
plaintext

这个流程的核心依赖是调用栈。在 C 中,函数调用是同步的、嵌套的:

// C 的调用栈是"深度优先"的
main()
  └── app_loop()
        └── read_sensor()
              └── i2c_transfer()
                    └── HAL_I2C_Master_Transmit()  ← 断点在这里
c

当你在 HAL_I2C_Master_Transmit() 处停下时,调用栈清晰地告诉你:是 read_sensor() 调用了你,read_sensor() 是被 app_loop() 调用的,app_loop() 是被 main() 调用的。一切井然有序。

13.1.2 异步代码的调试困境#

现在看 Embassy 的异步代码:

当你在 read_sensor() 内部设断点并停下时,调用栈是什么?

__cortex_m_rt_main_trampoline()
  └── embassy_executor::raw::Executor::run()
        └── <sensor_task as Future>::poll()
              └── read_sensor::poll()  ← 断点在这里
plaintext

你看到了什么?没有 comm_task,没有 app_loop,没有你熟悉的嵌套结构。 调用栈是”扁平”的——所有任务都被 Executor 的 run() 函数轮询,每个任务在某一时刻被 poll(),然后返回。

这就是异步调试的第一个认知冲击:

在 C 中,调用栈告诉你”谁调用了我”。在 async 中,没有人”调用”你——是调度器在”轮询”你。

13.1.3 心智模型转换#

维度C(同步)Rust async(异步)
执行模型函数调用 → 等待返回 → 继续poll → Pending → 被唤醒 → 再 poll
调用栈深度嵌套,反映调用关系扁平,只反映当前 poll 路径
”谁在等我”调用者(在栈上)Waker(不在栈上)
并发可见性中断可能随时打断其他任务在 .await 点交替执行
状态保存在栈上(局部变量)在 Future 结构体中(编译器生成)

关键洞察:在异步代码中,一个任务的”完整执行历史”不存在于调用栈中。它的状态被编译器编码为一个状态机(Future 结构体),存储在静态内存中。当你在断点处停下时,你只能看到当前这一次 poll 的路径,而不是任务的完整历史。

13.1.4 实用调试策略#

既然调用栈不再万能,C 工程师需要新的调试策略:

策略 1:日志优先于断点

在异步代码中,defmt 日志比断点更有用。因为:

  • 断点会暂停整个 Executor,所有任务同时停止
  • 日志可以记录任务的时间线,让你看到任务交替执行的全貌
#[embassy_executor::task]
async fn sensor_task(mut i2c: I2c<'static>) {
    loop {
        defmt::trace!("sensor: starting read");
        let temp = read_sensor(&mut i2c).await;
        defmt::debug!("sensor: got temp={=i16}", temp);
        
        defmt::trace!("sensor: sending to channel");
        CHANNEL.send(temp).await;
        defmt::trace!("sensor: sent, waiting 1s");
        
        Timer::after_secs(1).await;
    }
}
rust

策略 2:在 .await 前后打点

异步 bug 通常发生在 .await 的边界——任务在这里让出 CPU,其他任务可能修改共享状态。在每个 .await 前后打日志,可以精确定位问题:

defmt::trace!("before await: state={=u8}", self.state);
some_async_operation().await;
defmt::trace!("after await: state={=u8}", self.state);
// 如果 state 在这两行之间被改变了,说明有并发问题
rust

策略 3:用 defmt::timestamp 建立时间线

// 在 defmt 配置中启用时间戳
// .cargo/config.toml:
// [env]
// DEFMT_LOG = "trace"

// 输出示例:
// 0.000000 INFO  system boot
// 0.001234 TRACE sensor: starting read
// 0.001567 TRACE comm: waiting for data
// 0.002345 DEBUG sensor: got temp=253
// 0.002678 TRACE sensor: sending to channel
// 0.003012 TRACE comm: got data from channel
rust

通过时间戳,你可以重建任务的交替执行顺序——这是断点无法提供的信息。


13.2 defmt 实战:不只是 println#

13.2.1 defmt 的工作原理回顾#

第 12 章介绍了 defmt 的原理:固件只传输日志索引和原始数据,格式化在主机端完成。这里我们聚焦实战用法

核心优势(与 C 的 printf 对比):

维度C printfdefmt
Flash 占用格式字符串存储在 Flash 中("temp=%d\n" 占 10 字节)只存储索引(1-2 字节)
RAM 占用需要格式化缓冲区(通常 128-256 字节)几乎为零(直接写 RTT 缓冲区)
CPU 开销运行时格式化(除法、取模等)几乎为零(只写原始字节)
传输量完整字符串("temp=253\n" = 10 字节)索引 + 原始数据(1 + 2 = 3 字节)
类型安全无(%d 对应 float 不会报错)编译期检查(类型不匹配编译失败)

实际数据:在一个包含 50 条日志语句的固件中:

  • C printf 方案:Flash 增加 ~2.1 KB(格式字符串)+ 256 字节 RAM(缓冲区)
  • defmt 方案:Flash 增加 ~180 字节(索引表)+ 0 字节额外 RAM

13.2.2 结构化日志语法#

defmt 使用 {=type} 语法指定数据类型,这比 printf%d/%x/%s 更精确:

C 工程师注意#[derive(defmt::Format)] 类似于 C 中为结构体写一个 to_string() 函数,但它是编译期自动生成的,零运行时开销。

13.2.3 日志级别策略#

// 五个级别,从低到高
defmt::trace!("进入函数,参数: {=u8}", arg);   // 最详细,只在开发时用
defmt::debug!("中间状态: {=u16}", state);      // 调试用
defmt::info!("系统事件: 传感器已连接");          // 正常运行信息
defmt::warn!("异常但可恢复: 重试第 {=u8} 次", n); // 需要关注
defmt::error!("不可恢复错误: I2C 总线故障");     // 需要立即处理
rust

推荐策略

阶段编译时级别说明
开发调试trace所有日志都输出
集成测试debug过滤掉最细粒度的跟踪
量产固件infowarn只保留关键信息
故障排查临时改回 debug通过 OTA 推送调试固件

配置方式(.cargo/config.toml):

[env]
DEFMT_LOG = "trace"  # 开发时
# DEFMT_LOG = "info"  # 量产时
toml

13.2.4 用 defmt 追踪任务调度#

这是调试异步代码最实用的技巧。在每个任务的关键路径上插入 trace 日志:

输出示例(带时间戳):

0.000000 INFO  sensor_task: spawned
0.000012 TRACE sensor_task: poll start
0.000345 TRACE sensor_task: read done, t=253
0.000350 TRACE sensor_task: sent to channel
0.000355 TRACE sensor_task: timer done, loop
0.100355 TRACE sensor_task: poll start    ← 100ms 后再次被 poll
0.100678 TRACE sensor_task: read done, t=254
plaintext

通过观察时间戳间隔,你可以判断:

  • 任务是否按预期周期执行
  • 某个 .await 是否花了异常长的时间
  • 是否有其他任务”抢占”了执行时间

13.2.5 条件编译:量产时移除日志#

// 方法 1:通过 defmt 级别控制(推荐)
// 编译时设置 DEFMT_LOG=info,trace/debug 日志自动被优化掉

// 方法 2:通过 feature flag 控制
#[cfg(feature = "debug-log")]
defmt::trace!("detailed: {=[u8]:x}", &buf);

// Cargo.toml:
// [features]
// debug-log = []
// 开发时: cargo build --features debug-log
// 量产时: cargo build(不带 feature)
rust

重要:defmt 的级别过滤是编译期的。被过滤掉的日志不会占用任何 Flash 空间,不会有运行时分支判断。这与 C 的 #ifdef DEBUG 类似,但更优雅——你不需要手动加 #ifdef


13.3 probe-rs 调试实战#

13.3.1 probe-rs 是什么#

probe-rs 是 Rust 生态的新一代嵌入式调试工具,它同时替代了 C 世界中的多个工具:

C 世界probe-rs 对应功能
OpenOCD(调试服务器)probe-rs 内置 GDB server + DAP server
arm-none-eabi-gdb(调试器)probe-rs 内置调试器 + VSCode 集成
ST-Link Utility / J-Flash(烧录工具)probe-rs run / cargo embed
SEGGER RTT Viewer(日志查看)probe-rs 内置 RTT 支持
Keil / IAR(IDE 调试)VSCode + probe-rs 扩展

一个工具,五个功能。这是 Rust 工具链”集成化”理念的体现。

13.3.2 基本调试流程#

一键编译 + 烧录 + 日志

# 最简流程:编译 → 烧录 → 显示 RTT 日志
cargo run

# 等价于:
cargo build
probe-rs run --chip STM32F407VGTx target/thumbv7em-none-eabihf/debug/my-app
bash

VSCode 图形化调试

.vscode/launch.json 中配置:

配置完成后,在 VSCode 中:

  1. 在代码行号左侧点击设置断点
  2. 按 F5 启动调试
  3. 程序烧录到芯片并运行
  4. 命中断点时自动暂停
  5. 可以查看变量、调用栈、寄存器

13.3.3 在 async fn 中设断点的技巧#

技巧 1:在 .await 行设断点

async fn read_sensor(i2c: &mut I2c<'static>) -> i16 {
    let mut buf = [0u8; 2];
    i2c.write_read(0x76, &[0xFA], &mut buf).await;  // ← 断点 1:I2C 传输前
    // 命中断点 1 时,可以检查 buf 的初始值
    
    let raw = i16::from_be_bytes(buf);
    // ← 断点 2:传输完成后
    // 命中断点 2 时,可以检查 buf 的实际内容
    
    raw / 10
}
rust

技巧 2:条件断点

在 VSCode 中,右键断点 → “Edit Breakpoint” → 输入条件:

// 只在温度超过阈值时暂停
raw > 500
rust

技巧 3:日志断点(Logpoint)

不暂停程序,只在命中时输出日志。适合不想打断实时行为的场景:

// VSCode: 右键 → "Add Logpoint"
// 输入: "sensor read: buf={buf[0]:x} {buf[1]:x}"
plaintext

13.3.4 查看 Future 状态#

当你在断点处停下时,可以展开变量面板查看 Future 的内部状态。编译器为每个 async fn 生成了一个匿名结构体,其中包含:

  • 所有局部变量的当前值
  • 当前处于哪个 .await 点(状态机的 state 字段)
  • 被挂起的子 Future
// VSCode 变量面板示例:
▼ sensor_task::future
    state: 2          ← 当前在第 2 个 .await 点
    i2c: I2c { ... }
    buf: [0x01, 0x3A]  ← 局部变量仍然存活
    __state_1: TimerFuture { ... }  ← 被挂起的 Timer
plaintext

C 工程师类比:这类似于在 FreeRTOS 中查看任务的 TCB(Task Control Block),但信息更丰富——你可以看到任务”停在了哪一行代码”。

13.3.5 内存查看:检查任务栈#

Embassy 的任务栈是静态分配的。你可以通过查看内存来确认栈使用情况:

// 在代码中定义任务栈
#[embassy_executor::task]
async fn sensor_task(/* ... */) {
    // 任务代码
}

// 查看栈大小:编译后在 .map 文件中搜索任务名
// 或者使用 cargo size 查看各段大小
rust

在 probe-rs 调试器中,你可以直接查看内存区域:

// VSCode 调试控制台:
-memory read --address 0x20000000 --count 256
plaintext

栈溢出检测技巧:在任务栈底部填充魔术值,运行时检查是否被覆盖:

// 这不是 Embassy 内置功能,但可以作为调试手段
// 在 build.rs 或链接脚本中,将未使用的 RAM 填充为 0xDEADBEEF
// 运行时检查该区域是否被修改
rust

13.3.6 与 Keil / IAR 调试体验的对比#

维度Keil / IARVSCode + probe-rs
价格数千~数万元免费
芯片支持通过 Pack 扩展通过 target 描述文件
调试界面成熟、稳定现代、可定制
外设寄存器查看内置(SVD 解析)需安装 Embedded Viewer 扩展
逻辑分析仪内置(部分版本)需外部工具
RTT 支持需 SEGGER 授权内置
脚本化有限完全可编程(probe-rs 是 Rust 库)
学习曲线低(图形化向导)中(需配置 JSON)

实事求是:对于复杂的外设调试(如查看 SPI 波形、分析 DMA 传输),Keil/IAR 的内置工具仍然更方便。但对于日常开发(断点、变量、日志),VSCode + probe-rs 的体验已经足够好,而且免费。


13.4 常见运行时问题诊断#

13.4.1 问题 1:任务”卡死”#

症状:某个任务不再执行,其他任务正常。

原因:任务内部有一个不让出 CPU 的循环。

// 错误:这个循环永远不会让出 CPU
#[embassy_executor::task]
async fn bad_task() {
    loop {
        do_something();
        // 没有 .await!Executor 永远不会切换到其他任务
    }
}

// 错误:条件永远不满足
#[embassy_executor::task]
async fn bad_task2() {
    wait_for_condition().await;  // 如果条件永远不满足,任务永远挂起
    do_something();
}
rust

诊断方法

  1. 在可疑循环中加入 defmt::trace!,观察是否还在输出
  2. 检查所有循环中是否有 .await
  3. 检查 .await 的条件是否可能永远不满足

修复

13.4.2 问题 2:任务”饿死”#

症状:低优先级任务长时间不执行,高优先级任务正常。

原因:高优先级任务(通过 InterruptExecutor 运行)频繁被唤醒,低优先级任务得不到执行机会。

诊断方法

  1. 在每个任务入口加 defmt::trace!,观察执行频率
  2. 检查高优先级任务是否有频繁的 .await 让出点

修复

  • 确保高优先级任务在每次循环中都有 .await
  • 重新评估是否真的需要 InterruptExecutor(大多数场景不需要)
  • 考虑将高优先级任务改为中断驱动(不用 Executor)

13.4.3 问题 3:Channel 死锁#

症状:两个任务都不再执行,系统”挂起”。

原因:生产者等 Channel 有空间,消费者等 Channel 有数据,但双方都在等对方。

诊断方法

  1. 在所有 .send().await.receive().await 前后加日志
  2. 检查是否有任务在 Channel 操作之间 panic
  3. 检查 Channel 容量是否足够

修复

  • 增大 Channel 容量
  • 使用 try_send() / try_receive() 替代阻塞版本
  • 确保消费者任务不会意外退出

13.4.4 问题 4:栈溢出#

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

原因:Embassy 的任务栈是静态分配的,大小在编译期确定。如果任务的实际栈使用超过分配的大小,就会覆盖相邻内存,导致不可预测的行为。

与 C 的对比:在 FreeRTOS 中,栈溢出可以通过 vApplicationStackOverflowHook 检测(虽然也不完美)。在 Embassy 中,默认没有任何栈溢出保护。溢出就是 HardFault,没有警告。

诊断方法

  1. 观察 HardFault 寄存器
// 在 HardFault handler 中打印关键寄存器
#[exception]
unsafe fn HardFault() {
    // 读取 SCB 寄存器
    let cfsr = cortex_m::peripheral::SCB::cfsr();
    let hfsr = cortex_m::peripheral::SCB::hfsr();
    let mmfar = cortex_m::peripheral::SCB::mmfar();
    let bfar = cortex_m::peripheral::SCB::bfar();
    
    // 通过 RTT 输出(如果 RTT 还能工作)
    // 或者通过 LED 闪烁编码
    loop {}
}
rust
  1. 检查 CFSR 的 STKERR 位:如果 CFSR.STKERR == 1,说明是栈操作错误(压栈/弹栈时访问了无效地址),高度怀疑栈溢出。

  2. 增大栈大小测试:如果增大任务栈后问题消失,确认是栈溢出。

修复

预防措施

# .cargo/config.toml
# 启用栈探测(nightly 功能)
# [unstable]
# build-std = ["core", "alloc"]
# [build]
# rustflags = ["-Z", "stack-protector=all"]
toml

13.4.5 问题 5:Panic 后系统行为#

症状:设备突然停止工作,LED 全灭或全亮,无日志输出。

原因:Rust 的 panic! 在嵌入式中的默认行为是无限循环loop {})。设备不会复位,不会输出任何信息,只是”死”在那里。

诊断方法

  1. 连接调试器,查看 PC 指针是否停在 rust_begin_unwindpanic_handler
  2. 检查是否有 unwrap() / expect() 调用失败

修复——配置 panic 行为

// 方案 1:panic 时输出日志 + 复位(推荐用于开发)
// Cargo.toml: panic-probe = { version = "0.3", features = ["print-defmt"] }
use panic_probe as _;
// panic 时会通过 defmt 输出错误信息,然后触发断点(调试器连接时)

// 方案 2:panic 时复位(推荐用于量产)
// Cargo.toml: panic-reset = "0.1"
use panic_reset as _;
// panic 时直接触发系统复位,看门狗会重新启动

// 方案 3:panic 时停机(默认行为)
// Cargo.toml: panic-halt = "0.2"
use panic_halt as _;
// panic 时进入无限循环,设备"死"在那里
rust

最佳实践

# Cargo.toml
[dependencies]
# 开发时用 panic-probe(可以看到错误信息)
panic-probe = { version = "0.3", features = ["print-defmt"], optional = true }
# 量产时用 panic-reset(自动恢复)
panic-reset = { version = "0.1", optional = true }

[features]
default = ["dev"]
dev = ["panic-probe"]
release = ["panic-reset"]
toml

13.5 Embassy 项目的工程架构#

13.5.1 C 工程的典型分层#

一个典型的 C 嵌入式工程长这样:

问题:

  • 模块间通过全局变量回调函数通信
  • config.h 被所有文件 #include,改一个宏全部重编译
  • 没有强制的访问控制(任何文件都能 extern 任何全局变量)

13.5.2 Rust 工程的推荐分层#

13.5.3 各模块的职责与通信#

main.rs:只做初始化和任务生成

shared.rs:定义任务间通信原语

use embassy_sync::channel::Channel;
use embassy_sync::signal::Signal;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;

use crate::drivers::bme280::SensorReading;
use crate::protocol::Command;

// 传感器数据通道:sensor_task → control_task
pub static SENSOR_CHANNEL: Channel<CriticalSectionRawMutex, SensorReading, 4> = Channel::new();

// 命令信号:comm_task → control_task
pub static COMMAND_SIGNAL: Signal<CriticalSectionRawMutex, Command> = Signal::new();
rust

与 C 的对比:在 C 中,你可能用 volatile 全局变量 + 中断回调来实现类似功能。Rust 的 ChannelSignal 是类型安全的、编译期检查的、不会数据竞争的。

tasks/sensor.rs:传感器采集任务

drivers/bme280.rs:面向 embedded-hal 的驱动

关键设计决策:驱动面向 embedded_hal_async::i2c::I2c trait 编程,而不是面向 embassy_stm32::i2c::I2c。这意味着这个驱动可以在 STM32、nRF、ESP32 上复用,无需修改。

13.5.4 大型项目的 Workspace 组织#

当项目规模增大时,可以使用 Cargo Workspace 将驱动和协议分离为独立 crate:

# workspace/Cargo.toml
[workspace]
members = [
    "firmware",
    "drivers/bme280",
    "drivers/motor",
    "protocol/modbus",
    "protocol/frame",
    "shared-types",
]
resolver = "2"
toml

好处

  • 驱动库可以独立编译和测试(不需要硬件)
  • 驱动库可以发布到 crates.io,供其他项目使用
  • 修改驱动不影响固件的编译缓存(增量编译更快)

13.6 测试策略#

13.6.1 嵌入式测试的挑战#

在 C 的嵌入式开发中,测试通常是这样的:

  • 没有单元测试(“嵌入式代码没法测”)
  • 靠手动测试(“烧进去看看 LED 亮不亮”)
  • 靠 Code Review(“看看逻辑对不对”)

Rust 生态提供了更好的选择,但 no_std 环境确实带来了一些限制。

13.6.2 单元测试:纯逻辑代码#

对于不依赖硬件的纯逻辑代码(协议解析、数据转换、状态机),可以直接用 #[cfg(test)]

运行:cargo test(在主机上运行,不需要硬件)。

C 工程师对比:这相当于 C 的 Unity 框架,但内置在语言中,不需要额外安装。cargo test 一条命令搞定。

13.6.3 驱动测试:Mock embedded-hal trait#

对于依赖硬件的驱动,可以 Mock embedded-hal trait 来测试逻辑:

13.6.4 硬件在环测试(HIL)#

对于必须在真实硬件上运行的测试,probe-rs 提供了测试集成:

运行:

# probe-rs 可以自动运行 HIL 测试
probe-rs test --chip STM32F407VGTx tests/hil_test.rs
bash

13.6.5 测试策略总结#

测试类型运行环境覆盖范围频率
单元测试主机(cargo test纯逻辑:协议、算法、状态机每次提交
Mock 测试主机(cargo test驱动逻辑(不依赖真实硬件)每次提交
HIL 测试真实硬件硬件交互:I2C/SPI/UART 通信每日 / 每次发版
集成测试真实硬件完整系统行为每次发版

与 C 的对比:C 的嵌入式测试通常只有最后一种(手动集成测试)。Rust 的 trait 系统使得前两种测试变得可行且简单——这是 Rust 在工程质量上的巨大优势。


13.7 从原型到产品的检查清单#

13.7.1 看门狗配置#

C 工程师注意:在 C 中你可能在超级循环里喂狗。在 Embassy 中,看门狗任务应该是最低优先级的任务。如果其他任务卡死导致看门狗任务得不到执行,系统会自动复位。

13.7.2 低功耗模式#

use embassy_stm32::low_power::Executor;

// 使用支持低功耗的 Executor
// 当所有任务都在 Pending 状态时,CPU 自动进入 WFI(Wait For Interrupt)
// 这相当于 C 中的 __WFI(),但由 Executor 自动管理

// 更深的低功耗模式需要手动配置
use embassy_stm32::rcc::low_power::{LpConfig, StopMode};

let config = Config::default();
// 配置 Stop 模式:关闭大部分时钟,只保留 RTC 和唤醒中断
rust

13.7.3 OTA 更新策略#

具体实现参见第 11 章的 embassy-boot 部分。

13.7.4 日志与错误上报#

13.7.5 版本管理与固件签名#

13.7.6 生产烧录流程#

13.7.7 产品化检查清单总表#

检查项状态备注
看门狗已配置并定期喂狗超时时间根据最长任务周期确定
Panic 行为已配置(复位或停机)量产用 panic-reset
低功耗模式已配置电池供电产品必须
OTA 更新机制已实现A/B 分区 + 回滚
错误日志持久化写入 Flash 或外部 EEPROM
固件版本可查询通过通信接口上报
固件签名/校验防止篡改
读保护已启用防止固件被读取
关键参数存储在 Flash掉电不丢失
EMC 测试通过硬件相关
长期老化测试通过72h+ 连续运行

13.8 本章小结#

本章我们从”方法论”的角度审视了 Embassy 开发。关键要点:

  1. 调试思维转换:异步代码的调用栈是扁平的,不能依赖 C 的”看调用栈”习惯。日志(defmt)比断点更适合追踪异步行为。

  2. defmt 是嵌入式日志的最佳实践:编译期格式化、类型安全、极低的 Flash/RAM/CPU 开销。在 .await 前后打点是调试异步代码的核心技巧。

  3. probe-rs 一站式调试:替代了 C 世界中 OpenOCD + GDB + RTT Viewer + 烧录工具的组合。VSCode 集成提供了接近 Keil/IAR 的图形化体验。

  4. 五类常见运行时问题:任务卡死、任务饿死、Channel 死锁、栈溢出、Panic 静默。每类都有明确的诊断方法和修复方案。

  5. 工程架构:用模块(mod)替代 C 的文件组织,用 Channel/Signal 替代全局变量,用 trait 替代函数指针表。大型项目使用 Workspace 分离驱动和协议。

  6. 测试分层:纯逻辑用 cargo test,驱动用 Mock trait,硬件交互用 HIL 测试。Rust 的 trait 系统使得前两层测试在嵌入式中首次变得简单。

  7. 产品化清单:看门狗、低功耗、OTA、日志持久化、版本管理、读保护——这些在 C 中需要手动实现的东西,在 Rust 生态中都有成熟的 crate 支持。

下一章预告:第 14 章将深入性能与体积优化——如何让你的 Embassy 固件比同等功能的 C 代码更小?异步运行时的真实开销到底有多大?我们将用反汇编和实际测量数据说话。

Comment seems to stuck. Try to refresh?✨