Rust in Everything
基于 probe-rs-tools 0.32.0

从 QEMU 换到真板子

主线位置:命令主线的入口。固件这一侧决定了 probe-rs 在轮询循环里看到什么:semihosting 停核,还是 RTT 里的 defmt 帧。本页把 embassy 卷的一个 QEMU 固件改成「真板子加 probe-rs」的版本,逐处说明为什么要改,每处对应 probe-rs 的哪段代码。
flowchart TB
  q["QEMU 版:b-l475e-iot01a"] --> r1["运行器:qemu-system-arm"]
  q --> h1["embassy-stm32 打补丁:PWR"]
  q --> t1["手写 SysTick 时间驱动"]
  q --> o1["hprintln:semihosting"]
  b["真板子版:STM32L475VG"] --> r2["运行器:probe-rs run"]
  b --> h2["原版 embassy-stm32"]
  b --> t2["time-driver-tim2"]
  b --> o2["defmt-rtt、panic-probe"]
  r2 --> pr["probe-rs:烧录、RTT、vector catch"]
  o2 --> pr

起点:embassy 卷的 QEMU 模板

embassy 卷的实验方法用 qemu-system-arm -M b-l475e-iot01a,也就是 B-L475E-IOT01A 开发板(STM32L475VG,Cortex-M4F)。为了在 QEMU 上跑,模板做了两处让步:embassy-stm32 打了一行补丁(QEMU 没给 PWR 建模,init 会死等 DBP 位),时间驱动是手写的 1 kHz SysTick 驱动(QEMU 没有 TIM)。输出用 cortex-m-semihosting 的 hprintln!,结束用 debug::exit,panic 用 panic-semihosting。

拿模板里的 ticker(两个任务各 tick 三次,main 等 600 ms 后退出)做例子。这块板子正好有板载的 ST-LINK,probe-rs 支持它(DebugProbe 与驱动注册表),所以「换到真板子」只是换运行器,不用换芯片。

改六处

QEMU 版真板子版为什么
1. 运行器qemu-system-arm -M b-l475e-iot01a -display none -serial stdio -semihosting-config enable=on,target=native -kernelprobe-rs run --chip STM32L475VGTxcargo 把 ELF 路径接在后面;probe-rs run 负责烧录、复位、轮询(一条 probe-rs run)
2. 链接参数-Tlink.x再加 -Tdefmt.x,[env] 里 DEFMT_LOG = "info"放 .defmt 段;日志级别在编译期过滤
3. HAL[patch.crates-io] 换成注释掉 PWR 等待的 embassy-stm32去掉 patch,用原版 =0.6.0补丁只是因为 QEMU 不模拟 PWR;真芯片上那行等待会正常结束
4. 时间驱动自写 SysTickDriver,embassy-time-driver、embassy-time-queue-utils 两个依赖,tick-hz-1_000embassy-stm32 开 time-driver-tim2,embassy-time 用 tick-hz-32_768,删掉自写驱动和 SysTick 中断真芯片有 TIM2;驱动的差别见 embassy 卷的时间驱动
5. 日志hprintln!(SYS_OPEN 加 SYS_WRITE)defmt::info! 加 defmt-rttsemihosting 每次调用停一次核、几十次调试端口访问;RTT 只是 RAM 里的环形缓冲区(defmt 与 semihosting)
6. panicpanic-semihosting(exit feature)panic-probe(print-defmt)panic 信息走 defmt,然后 HardFault,由 vector catch 停下(上一页)

改完的 Cargo.toml(只列依赖):

[dependencies]
embassy-stm32 = { version = "=0.6.0", features = ["stm32l475vg", "memory-x", "exti", "unstable-pac", "time-driver-tim2"] }
embassy-executor = { version = "=0.10.0", features = ["platform-cortex-m", "executor-thread"] }
embassy-time = { version = "=0.5.1", features = ["tick-hz-32_768"] }
cortex-m = { version = "0.7.7", features = ["critical-section-single-core"] }
cortex-m-rt = "0.7.5"
cortex-m-semihosting = "0.5"
defmt = "1"
defmt-rtt = "1"
panic-probe = { version = "1", features = ["print-defmt"] }

固件代码只剩任务本身,SysTick 驱动的三十来行全部删掉:

// 本站实验:ticker,真板子版
#[embassy_executor::task(pool_size = 2)]
async fn ticker(id: u32, period_ms: u64) {
  for n in 0..3 {
    Timer::after_millis(period_ms).await;
    defmt::info!("task {} tick {} at {} ms", id, n, Instant::now().as_millis());
  }
}

#[embassy_executor::main]
async fn main(spawner: Spawner) {
  defmt::info!("before init");
  let _p = embassy_stm32::init(Default::default());
  defmt::info!("init done");
  spawner.spawn(ticker(1, 100).unwrap());
  spawner.spawn(ticker(2, 150).unwrap());
  Timer::after_millis(600).await;
  defmt::info!("main done at {} ms", Instant::now().as_millis());
  debug::exit(debug::EXIT_SUCCESS);
}

编译结果

依赖全部来自本机缓存,cargo build --release --offline 通过(embassy-stm32 0.6.0、embassy-executor 0.10.0、embassy-time 0.5.1、defmt 1.2.0、defmt-rtt 1.3.1、panic-probe 1.0.0)。ELF 里 probe-rs 要找的东西都在:

Idx Name            Size     VMA      LMA      Type
  1 .vector_table   00000188 08000000 08000000 DATA
  2 .text           00003344 08000188 08000188 TEXT
  3 .rodata         00000cb4 080034cc 080034cc DATA
  4 .data           00000050 20000000 08004180 DATA
  6 .bss            00000170 20000050 20000050 BSS
  7 .uninit         00000400 200001c0 200001c0 BSS
  8 .defmt          0000000c 00000000 00000000

20000020 D _SEGGER_RTT
08000586 T TIM2
  • _SEGGER_RTT 在 .data 里,命令行从符号表拿到确切地址,不扫描(RTT 控制块与通道)。
  • .defmt 段 12 字节(每个格式串占 1 字节,地址就是索引),四条 defmt::info! 的格式串(before init、init done、task {} tick {} at {} ms、main done at {} ms)都在符号表里。
  • TIM2 是 embassy-stm32 时间驱动的中断处理函数,替换了 QEMU 版的 SysTick。

probe-rs run 这一步本机没有探针,停在 No connected probes were found.。在板子上跑出来的 defmt 日志、两个任务的时间戳、退出码待实测。

debug::exit 还能用吗

能,而且这是 QEMU 和 probe-rs 少有的相通之处。debug::exit(EXIT_SUCCESS) 是 semihosting 的 SYS_EXIT,原因 ADP_Stopped_ApplicationExit。QEMU 开了 -semihosting-config 时直接退出进程;probe-rs 的轮询循环在核停在 BKPT 0xAB 上时识别它,handle_halt 返回 SemihostingExit(Ok),run 打印「Firmware exited successfully」后以 0 退出。所以固件不用为两种环境写两套结束逻辑,probe-rs run 也能当测试脚本用:跑完退出码就是结果。

有一个前提:必须有调试器接着。板子单独上电、没有 probe-rs 时,BKPT 指令按 Armv7-M 架构手册的规定会升级成 HardFault,固件停在 HardFault 处理函数里。这一点在板子上待实测;要让同一个固件脱离调试器运行,就不要在正常路径上调 debug::exit。

同样的道理也适用于 hprintln!:probe-rs 认得 SYS_OPEN(":tt") 和 SYS_WRITE,QEMU 版的 hprintln! 不改也能在真板子上打印,只是每次都要停核(defmt 与 semihosting)。把它换成 defmt 是为了速度,不是为了能用。

两种环境能看到的东西

QEMU + semihosting真板子 + probe-rs
日志模拟器执行 BKPT 0xAB 时直接处理,几乎没有代价defmt 帧写进 RAM,probe-rs 每 100 ms 读一次
panicpanic-semihosting 打印后 SYS_EXITpanic-probe:HardFault,probe-rs 打印调用栈
调试-s -S 起 GDB 服务端,GDB 直连probe-rs gdb(只有硬件断点)或 VS Code 加 dap-server
时间SysTick 的 tick 或 -icount 指令数,不代表硬件时间TIM2,真实时间

两边没有现成的桥:probe-rs 源码里没有 GDB 远程协议的客户端,不能把 QEMU 当成一个目标来连。13 动手会写一个最小的客户端,把 QEMU 里跑着的固件交给 probe-rs-debug 展开调用栈。

小结

  • QEMU 模板的三处让步(PWR 补丁、SysTick 驱动、semihosting 输出)在真板子上都要撤掉:用原版 embassy-stm32、time-driver-tim2、defmt + defmt-rtt。
  • 运行器换成 probe-rs run --chip STM32L475VGTx,链接参数加 -Tdefmt.x,panic 换成 panic-probe;改完离线编译通过,ELF 里有 _SEGGER_RTT 和 .defmt。
  • debug::exit 在 probe-rs 下照样能结束 run 并给出退出码;脱离调试器时它会变成 HardFault。
  • 板子上的实际输出、计时和退出码待实测。

💬 关联讨论

发起讨论
加载中...