一条 probe-rs run 从命令到日志
主线位置:命令主线全程。从
cargo run调起probe-rs run --chip ... fw.elf,到终端上出现第一行日志。每一步只讲到能看清全貌的深度,细节在后面各部分。
sequenceDiagram
participant C as CLI 客户端
participant S as RPC 服务端
participant P as 探针
participant T as 目标芯片
C->>C: 注册插件 解析参数
C->>S: probe/select
S->>P: Lister 枚举 USB
C->>S: probe/attach
S->>P: open 选协议 attach
P->>T: 线复位 读 DPIDR 上电
S->>T: 调试序列 使能 C_DEBUGEN
C->>S: flash/build 和 flash/flash
S->>T: flash 算法 擦除 编程
C->>S: monitor
loop 每 100 ms
S->>T: 读 DHCSR 读 RTT 缓冲区
S-->>C: RttEvent 原始字节
C->>C: defmt 解码 打印
end
cargo runner
README 推荐的配置是在 .cargo/config.toml 里写:
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
runner = "probe-rs run --chip nRF52840_xxAA"
cargo 编译完,会把 ELF 的路径追加在 runner 后面执行,于是 cargo run 等于 probe-rs run --chip nRF52840_xxAA target/thumbv7em-none-eabihf/debug/app。如果固件依赖了 probe-rs-meta 并用 probe_rs_meta::chip!("...") 把芯片名写进了 ELF,--chip 可以省掉;两者都给而且不一致时,attach_probe 直接报 elf_chip does not match probe_chip。
第一步:main 和本地 RPC
main 做四件事:注册 ESP 和 Linux 插件(见源码地图);读配置文件 .probe-rs.toml 等;检查自己是不是以 cargo-flash / cargo-embed 的名字被调用;解析命令行,然后把真正的工作交给 run_app。
run_app 是理解整个命令行的关键:
// probe-rs-tools/src/bin/probe-rs/main.rs
async fn run_app<R>(
_connection_params: Option<(String, Option<String>)>,
cb: impl AsyncFnOnce(RpcClient) -> Result<R>,
) -> Result<R> {
#[cfg(feature = "remote")]
if let Some((host, token)) = _connection_params {
// Run the command remotely.
let client = rpc::client::connect(&host, token).await?;
return cb(client).await;
}
// Create a local server to run commands against.
let (mut local_server, tx, rx) = RpcApp::create_server(16, rpc::functions::ProbeAccess::All);
let handle = tokio::spawn(async move { local_server.run().await });
// Run the command locally.
let client = RpcClient::new_local_from_wire(tx, rx);
let result = cb(client).await;
// ...
即使探针就插在本机上,命令也不直接调库,而是在同一个进程里起一个 RPC 服务端(一个 tokio 任务),客户端和服务端之间用两条内存通道传 postcard 编码的消息。开了 remote feature 时,同一个客户端改连 websocket 或 unix socket 上的远端 probe-rs serve,命令的代码一行不改。服务端有哪些「远程调用」,全写在 rpc/functions.rs 的 endpoints! 表里:probe/select、probe/attach、flash/build、flash/flash、monitor、memory/read32 等 30 来个。这层 RPC 的设计在 CLI 的 RPC 架构,取舍在设计取舍。
第二步:选探针
Cmd::run 先看 ELF 里有没有 .embedded_test 段(有就进测试模式),读 ELF 元数据,然后调 cli::attach_probe。它第一件事是选探针:
// probe-rs-tools/src/bin/probe-rs/util/cli.rs
let probe = match select_probe(client, probe_options.probe.map(Into::into)).await {
Ok(probe) => probe,
Err(error) => {
print_setup_hints_if_relevant(client).await;
return Err(error);
}
};
select_probe 发一个 probe/select 请求。服务端的 select_probe 用 Lister 让每个注册的驱动枚举自己的 USB 设备(见 DebugProbe 与驱动注册表):零个就返回 NoProbesFound,一个就选它,多个就把列表交回客户端,让用户在终端里输一个序号。
第三步:attach
选好探针后,客户端发 probe/attach。服务端的 attach 打开探针、按 --protocol、--speed 配置,再调 Probe::attach_with_registry,最后把 Session 存进服务端的上下文,只把一个 key 交回客户端;之后客户端每次操作目标,都拿这个 key 发请求。
attach_with_registry 直接进 Session::new(session.rs:180):按芯片名从注册表里找到目标描述;ARM 目标走 attach_arm_debug_interface,依次做线复位、读 DPIDR、给调试端口上电、跑芯片的 debug_device_unlock 序列、给每个核写 C_DEBUGEN;最后清掉所有硬件断点。这一串在调试序列与 vendor里用模拟目标逐条记录,一共 142 次 DP / AP 访问。
第四步:烧录
cli::flash 先发 flash/build 让服务端读 ELF、建 FlashLoader,再发 flash/flash。服务端在目标的 RAM 里装入 CMSIS flash 算法,擦除、编程、可选地校验;进度通过 flash/progress 这个 topic 推回客户端画进度条。在 cli::flash 之前,Cmd::run 已经建好了 RTT 客户端(它要先从 ELF 里找 _SEGGER_RTT 的地址),烧录时把它一并交给服务端:如果控制块所在的地址本身就在这次要下载的数据里(固件直接下载到 RAM 运行),就不能在启动前清掉控制块(configure_from_loader)。细节在 06 烧录、07 RTT。
第五步:轮询
最后是 cli::monitor,它发一个 monitor 请求。服务端把它放进一个单独的任务,跑 RunLoop::run_until:先按参数打开 vector catch(默认在 HardFault 和复位时停下),让核跑起来,然后循环调 poll_once:
// probe-rs-tools/src/bin/probe-rs/rpc/utils/run_loop.rs
let mut next_poll = Duration::from_millis(100);
// check for halt first, poll rtt after.
// this is important so we do one last poll after halt, so we flush all messages
// the core printed before halting, such as a panic message.
let return_reason = match core.status()? {
probe_rs::CoreStatus::Halted(reason) => match predicate(reason, &mut core) {
Ok(Some(r)) => Some(Ok(ReturnReason::Predicate(r))),
Err(e) => Some(Err(e)),
Ok(None) => {
// Re-poll immediately if the core was halted, to speed up reading strings
// from semihosting. The core is not expected to be halted for other reasons.
next_poll = Duration::ZERO;
core.run()?;
None
}
},
// ...
};
let poller_result = poller.poll(&mut core);
每一轮先读一次 DHCSR 看核停了没有。停了就交给 predicate:semihosting 调用在这里被处理掉,然后马上让核继续跑;真正的异常(比如 HardFault)让循环结束、打印调用栈。没停,就让 RttPoller 读一遍所有 up channel(monitor.rs:265):读到数据就把下次轮询的间隔设成 0,连续读空;读不到就是 100 ms。do_run_until 里的注释说明了为什么不轮询得更勤:「If the polling frequency is too high, the USB connection to the probe can become unstable.」
读到的 RTT 数据是原始字节,以 RttEvent::Output 的形式推回客户端;defmt 帧的解码在客户端的 handle_monitor_event 里做,用的是本机那份 ELF 里的 defmt 字符串表。所以远程模式下,目标端的 probe-rs 不需要 ELF 的调试信息就能转发日志。
实验:没有探针时,命令停在哪一步
本机没有探针。list 正常返回:
$ probe-rs list
No debug probes were found.
run、download、info 都有 --dry-run 参数,但加上它也一样失败,即使传了一个真实的 ELF:
$ probe-rs run --dry-run --chip STM32F103C8 <项目>/fixtures/nRF52833_xxAA_hardfault_from_usagefault.elf
Error: No connected probes were found.
$ probe-rs download --dry-run --chip STM32F103C8 <项目>/fixtures/nRF52833_xxAA_hardfault_from_usagefault.elf
Error: No connected probes were found.
原因在第二步和第三步的顺序上。--dry-run 的处理在服务端的 attach_probe:
// probe-rs-tools/src/bin/probe-rs/util/common_options.rs
pub fn attach_probe(&self, lister: &Lister) -> Result<Probe, OperationError> {
let mut probe = if self.0.dry_run {
Probe::from_specific_probe(Box::new(FakeProbe::with_mocked_core()))
} else {
但走 RPC 的命令在发 probe/attach 之前,先在客户端发了 probe/select,这一步不看 dry_run,列表为空就返回 NoProbesFound,显示成「No connected probes were found.」。probe-rs gdb 不走 RPC:Cli::run 只借用客户端里的目标注册表,然后直接调 simple_attach,后者直接调上面的 attach_probe,所以 --dry-run 真的换上了 FakeProbe:
$ probe-rs gdb --dry-run --chip STM32F103C8 --gdb-connection-string 127.0.0.1:13581
Read 0xe0042004 = 0
Write 0xe0042004 = 0x00000007
Read DHCSR: 0xe000edf0 = 0x10000
Write DHCSR = 0xa05f0001
...
Firing up GDB stub for Armv7m cores at [127.0.0.1:13581]
这些 Read / Write 行是 FakeProbe 的模拟核打印的,前两行是 STM32 的调试序列在写 DBGMCU_CR,后面是给核使能调试。本卷后面会反复用这个办法,配合 lldb 观察每条调试命令变成了哪些寄存器读写;FakeProbe 能做什么、做不了什么见 FakeProbe。「--dry-run 对 run 不起作用」在常见坑里作为一个坑再讲。
小结
probe-rs run是 cargo runner:cargo 把 ELF 路径追加在 runner 命令后面;芯片名可以来自--chip,也可以来自固件用probe-rs-meta写进 ELF 的段。- 命令行在本进程里起一个 postcard-rpc 服务端,本地和远程用同一套客户端代码;
Session留在服务端,客户端只拿到一个 key。 - 顺序是:
probe/select→probe/attach(Session::new)→flash/build、flash/flash→monitor;轮询每 100 ms 一次,先查 halt 再读 RTT,有数据时连续读;defmt 在客户端解码。 --dry-run只在gdb上有效:run、download、info在 RPC 的probe/select这一步就因为没有探针失败了。