从 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 -kernel | probe-rs run --chip STM32L475VGTx | cargo 把 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_000 | embassy-stm32 开 time-driver-tim2,embassy-time 用 tick-hz-32_768,删掉自写驱动和 SysTick 中断 | 真芯片有 TIM2;驱动的差别见 embassy 卷的时间驱动 |
| 5. 日志 | hprintln!(SYS_OPEN 加 SYS_WRITE) | defmt::info! 加 defmt-rtt | semihosting 每次调用停一次核、几十次调试端口访问;RTT 只是 RAM 里的环形缓冲区(defmt 与 semihosting) |
| 6. panic | panic-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 读一次 |
| panic | panic-semihosting 打印后 SYS_EXIT | panic-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。- 板子上的实际输出、计时和退出码待实测。