如何启用 semihosting?

解读

在国内嵌入式 Rust 岗位面试中,"semihosting" 常被用来快速验证候选人是否真正做过 裸机调试底层日志输出
面试官真正想确认的是:

  1. 你知道 semihosting 是 ARM 官方调试机制,不是 Rust 语法糖;
  2. 你能在 no_std + no_main 环境下,用 Rust 生态工具链 把调试信息打到 Host 端,而不是依赖串口;
  3. 你清楚 Cargo.toml、memory.x、runner、.cargo/config.toml 四条线的配合,且能解释 "为什么 Release 模式要关 semihosting"
  4. 你了解 cortex-m-semihostingrtt-target 的取舍,避免"只会抄模板"的嫌疑。

知识点

  • ARM semihosting 原理:BKPT 0xAB + 寄存器传参,由 OpenOCD/J-Link 在 Host 端代理 syscall
  • cortex-m-rt 启动流程与 __RESET_HANDLER 链接脚本
  • panic-handlerexit 语义:semihosting 的 SYS_EXIT 会 halt 整个 MCU,生产固件必须剔除
  • cargo-embed、probe-run、pyOCD 三种 runner 对 semihosting 的自动识别差异
  • scb::sysresetsemihosting::heprintln!同步/阻塞 行为对实时性的影响
  • 特征开关 cfg(debug_assertions)link-arg -lnosys 的配套用法,防止 Release 误链接 semihosting

答案

  1. 新建 cortex-m 裸机工程
    cargo generate --git https://github.com/rust-embedded/cortex-m-quickstart

  2. 在 Cargo.toml 引入官方包并 只在 dev 模式启用

    [target.thumbv7em-none-eabi.dependencies]
    cortex-m-semihosting = "0.5"
    panic-semihosting    = { version = "0.6", optional = true }
    
    [features]
    default = []
    debug   = ["panic-semihosting"]
    
  3. 配置 .cargo/config.toml 让 runner 识别 semihosting

    [target.thumbv7em-none-eabi]
    runner = "probe-run --chip STM32F407VG"   # 或 J-Link GDB Server
    rustflags = [
      "-C", "link-arg=-Tlink.x",
      "-C", "link-arg=-lnosys",               # Release 模式屏蔽 semihosting
    ]
    
    [build]
    target = "thumbv7em-none-eabi"
    
  4. 主程序 按条件初始化

    #![no_std]
    #![no_main]
    
    use cortex_m_rt::entry;
    use cortex_m_semihosting::hprintln;
    
    #[cfg(feature = "debug")]
    use panic_semihosting as _;
    #[cfg(not(feature = "debug"))]
    use panic_halt as _;
    
    #[entry]
    fn main() -> ! {
        hprintln!("Hello from MCU @ {:08X}", 0x0800_0000).unwrap();
        loop { cortex_m::asm::wfi(); }
    }
    
  5. 编译与运行
    cargo run --features debug # 开发阶段,OpenOCD 终端可见日志
    cargo build --release # 发布阶段,无 semihosting,体积最小

  6. 验证是否真正启用

    • OpenOCD 终端出现 "Hello from MCU @ 08000000"
    • 查看 map 文件,仅 debug elf 含 sh_* 符号,release 无

拓展思考

  • 生产固件如何无缝切换日志通道?
    自定义 log::Logger,dev 模式指向 hprintln!,release 指向 ITM stimulus port 0RTT,用 link-time feature 保证零开销。

  • semihosting 的性能惩罚到底多大?
    实测 STM32F407 72 MHz 下,单条 hprintln! 约 850 µs,而 ITM 仅 1.2 µs;因此 中断上下文严禁调用 semihosting

  • RISC-V 没有 BKPT 0xAB,怎么办?
    国内 GD32VF103 项目常用 OpenOCD 自定义 SBI call 模拟 semihosting,但 Rust 层需用 riscv-semihosting crate,并 把 _start 中的 tohost 地址写进 .data,否则 GDB 收不到数据。

  • 面试官常追问的坑:

    1. "为什么 Release 要加 -lnosys?"——防止 newlib nanowrite() 默认导向 semihosting,导致 板子跑飞
    2. "如何防止 SYS_EXIT 触发断点复位?"——在 panic_handler永不调用 semihosting::exit,而是 loop {}scb::sysreset()