DebugProbe 与驱动注册表
主线位置:访问主线的最底层,命令主线的「选探针」「attach」两步。本页讲 probe-rs 怎么从一堆 USB 设备里找到探针、打开它,以及不同探针在哪一层接入 ARM 调试架构。
flowchart TB lister["Lister::list / open"] --> drivers["DRIVERS:9 个 ProbeFactory,插件可追加"] drivers --> open["ProbeFactory::open(selector)"] open --> probe["Probe:Box dyn DebugProbe 加 attached 标志"] probe --> getif["try_get_arm_debug_interface"] getif --> aci["ArmCommunicationInterface:CMSIS-DAP、Glasgow、J-Link、FTDI 等"] getif --> own["驱动自己的 ArmDebugInterface:ST-Link、SiFli、BMP 的 ADIv5 加速"]
驱动注册表
所有内置驱动登记在 probe.rs:47 的一张全局表里:
// probe-rs/src/probe.rs
static DRIVERS: LazyLock<RwLock<Vec<&'static dyn ProbeFactory>>> = LazyLock::new(|| {
let probes: Vec<&'static dyn ProbeFactory> = vec![
&blackmagic::BlackMagicProbeFactory,
&cmsisdap::CmsisDapFactory,
&ftdi::FtdiProbeFactory,
&stlink::StLinkFactory,
&jlink::JLinkFactory,
&wlink::WchLinkFactory,
&sifliuart::SifliUartFactory,
&glasgow::GlasgowFactory,
&ch347usbjtag::Ch347UsbJtagFactory,
];
RwLock::new(probes)
});
插件通过 register_probe_factory 往末尾追加(见源码地图):ESP 插件加 EspUsbJtagFactory,Linux 插件加 LinuxGpiodFactory 和 LinuxSpidevSwdFactory。
每个驱动只需要实现 ProbeFactory 的两个方法:
// probe-rs/src/probe.rs
pub trait ProbeFactory: std::any::Any + std::fmt::Display + std::fmt::Debug + Sync {
fn open(&self, selector: &DebugProbeSelector) -> Result<Box<dyn DebugProbe>, DebugProbeError>;
fn list_probes(&self) -> Vec<list::ProbeListItem>;
fn list_probes_filtered(
&self,
selector: Option<&DebugProbeSelector>,
) -> Vec<list::ProbeListItem> {
// ... 默认实现:list_probes() 再按 selector 过滤
}
}
DebugProbeSelector 就是命令行 --probe VID:PID[:序列号] 解析出来的东西:USB 的 vendor id、product id,可选的接口号和序列号。
Lister:问每一个驱动
Lister 默认包着一个 AllProbesLister。列举时它把每个驱动的 list_probes_filtered 结果拼起来;打开时按表的顺序挨个调 open,返回第一个成功的:
// probe-rs/src/probe/list.rs
fn open(&self, selector: &DebugProbeSelector) -> Result<Probe, DebugProbeError> {
let mut open_error = None;
let mut fallback_error = ProbeCreationError::NotFound;
for probe_ctor in DRIVERS.read().iter() {
match probe_ctor.open(selector) {
Ok(link) => return Ok(Probe::from_specific_probe(link)),
Err(DebugProbeError::ProbeCouldNotBeCreated(ProbeCreationError::NotFound)) => {}
Err(DebugProbeError::ProbeCouldNotBeCreated(ProbeCreationError::CouldNotOpen)) => {
fallback_error = ProbeCreationError::CouldNotOpen;
}
Err(e) => open_error = Some(e),
};
}
Err(open_error.unwrap_or(DebugProbeError::ProbeCouldNotBeCreated(fallback_error)))
}
错误的优先级有讲究:「不是我的设备」(NotFound)静默跳过;「是我的但打不开」(CouldNotOpen,通常是权限)只在没有更具体的错误时报;驱动报出的其他错误优先返回。0.32 起 Linux 上没有 udev 规则的探针也会被列出来并标成 inaccessible(#4128),命令行据此决定要不要打印设置提示。
同一个 USB 设备可能被多个驱动认领。比如一个设备同时有 CMSIS-DAP v1 和 v2 两个接口,CMSIS-DAP 驱动自己先试 v2;表里的顺序决定了不同驱动之间的先后。Lister::with_lister 允许换一个自定义的 ProbeLister,测试里可以用它塞进假的探针。
Probe 与 DebugProbe
Probe 很薄:一个 Box<dyn DebugProbe> 加一个 attached 标志。attached 之前可以改协议、改速度;之后再改就返回 DebugProbeError::Attached。
DebugProbe 的方法分三组:
| 组 | 方法 | 有默认实现吗 |
|---|---|---|
| 探针本身 | get_name、speed_khz、set_speed、attach、detach、select_protocol、active_protocol、target_reset、target_reset_assert、target_reset_deassert、into_probe | 没有,11 个都必须实现 |
| 按架构取接口 | has_arm_interface / try_get_arm_debug_interface、has_riscv_interface / try_get_riscv_interface_builder、has_xtensa_interface / try_get_xtensa_interface | 有:默认「不支持」 |
| 可选能力 | try_as_jtag_probe、try_as_dap_probe、get_swo_interface(_mut)、get_target_voltage | 有:默认 None |
try_get_arm_debug_interface 的签名值得看一眼:
// probe-rs/src/probe.rs
fn try_get_arm_debug_interface<'probe>(
self: Box<Self>,
_sequence: Arc<dyn ArmDebugSequence>,
) -> Result<Box<dyn ArmDebugInterface + 'probe>, (Box<dyn DebugProbe>, ArmError)> {
它按值拿走探针(self: Box<Self>),成功时返回一个 ARM 调试接口,接口内部持有探针;失败时把探针原样还回来。这样 Session 的自动识别可以先试 ARM、失败了拿回探针再试 RISC-V,所有权一直清楚。传进来的 ArmDebugSequence 是芯片的调试序列,调试序列与 vendor 讲。
每个驱动接在哪一层
ARM 调试要做的事从低到高大致是:在线上收发 SWD / JTAG 位 → 组成一次 DP / AP 寄存器访问(「DAP 事务」)→ 组成一次内存访问(写 TAR、读 DRW)。不同探针的固件做到不同的高度,probe-rs 在对应的那一层接上:
| 驱动 | 固件做到哪一层 | probe-rs 实现的接口 | ARM / RISC-V / Xtensa |
|---|---|---|---|
| CMSIS-DAP | DAP 事务(DAP_Transfer 等命令) | RawDapAccess,再交给 ArmCommunicationInterface | 是 / 是 / 是(JTAG) |
| Glasgow | DAP 事务(只支持 SWD) | RawDapAccess | 是 / 否 / 否 |
| linuxspidevswd(插件) | 借 SPI 控制器收发 | RawDapAccess | 是 / 否 / 否 |
| ST-Link | 内存访问(ST-Link 的读写内存命令) | 自己的 StlinkArmDebug | 是 / 否 / 否 |
| SiFli UART | 内存访问(经 UART) | 自己的 SifliUartArmDebug | 是 / 否 / 否 |
| Black Magic Probe | 固件支持 ADIv5 加速时做到内存访问;否则只做位级 IO | 加速时用自己的 BlackMagicProbeArmDebug,否则走 polyfill | 是 / 是 / 是 |
| J-Link | 位级 IO | RawSwdIo + RawJtagIo,DAP 事务由 polyfill 拼 | 是 / 只在支持 JTAG 时 / 同左 |
| FTDI | 位级 IO(MPSSE) | RawSwdIo + RawJtagIo | 是 / 是 / 是 |
| CH347 | 位级 IO | RawSwdIo + RawJtagIo | 是 / 是 / 是 |
| linuxgpiod(插件) | 主机直接翻转 GPIO | RawSwdIo + RawJtagIo | 是 / 否 / 否 |
| WCH-Link | RISC-V 调试(WCH 自己的协议) | 自己的 JtagAccess 与 RISC-V DTM | 否 / 是 / 否 |
| ESP USB-JTAG(插件) | 位级 JTAG | RawJtagIo | 否 / 是 / 是 |
两点从这张表里能直接读出来:
- 走
ArmCommunicationInterface的探针共享全部 DP / AP 逻辑(SELECT 缓存、上电、调试序列的 DP 部分),驱动只负责把一次寄存器访问送出去。CMSIS-DAP 和 J-Link 的差别只在create的第三个参数:CMSIS-DAP 传false,J-Link、FTDI 等位级探针传true,打开 CTRL/STAT 的溢出检测(ORUNDETECT)。select_dp里的注释说:「For "bit-banging" probes, such as JLink or FTDI, we rely on it for good, stable communication.」 - ST-Link 不经过这一层。它的
try_get_arm_debug_interface把序列参数命名成_sequence,没有用;DP 的线复位、上电由 ST-Link 固件自己做,调试序列里 DP 一级的钩子(debug_port_setup、debug_port_start)对它不起作用。芯片级的钩子(解锁、debug_core_start)由Session直接调,仍然生效。
实验:在库外实现一个探针
既然 DebugProbe、RawDapAccess 和 ArmCommunicationInterface::create 都是公开的,就可以在库外写一个「探针」。本卷的软件模拟目标就是这样接进去的(依赖 probe-rs =0.32.0,2 空格缩进是本站代码):
// 本站实验:DapSim 背后是一个模拟的 SW-DP、AHB-AP、内存和 Cortex-M 调试寄存器
impl DebugProbe for DapSim {
fn get_name(&self) -> &str { "dap-sim" }
fn speed_khz(&self) -> u32 { 1000 }
fn set_speed(&mut self, speed_khz: u32) -> Result<u32, DebugProbeError> { Ok(speed_khz) }
fn attach(&mut self) -> Result<(), DebugProbeError> { Ok(()) }
fn active_protocol(&self) -> Option<WireProtocol> { Some(WireProtocol::Swd) }
// ... detach、target_reset* 、select_protocol、into_probe 同样只返回 Ok
fn has_arm_interface(&self) -> bool { true }
fn try_get_arm_debug_interface<'probe>(
self: Box<Self>,
sequence: Arc<dyn ArmDebugSequence>,
) -> Result<Box<dyn ArmDebugInterface + 'probe>, (Box<dyn DebugProbe>, ArmError)> {
Ok(ArmCommunicationInterface::create(self, sequence, true))
}
}
impl DapProbe for DapSim {}
impl RawDapAccess for DapSim {
fn raw_read_register(&mut self, address: RegisterAddress) -> Result<u32, ArmError> {
// 按 address 分 DP / AP,交给模拟目标,同时记一行日志
}
// ... raw_write_register、swj_sequence、swj_pins 等
}
let probe = Probe::new(DapSim::new(6));
let mut session = probe.attach("STM32F103C8", Permissions::default())?;
attach 成功,返回一个真正的 Session。之后 session.core(0)?.read_word_32(...)、halt、set_hw_breakpoint 都会穿过 probe-rs 自己的代码,最后变成对 DapSim 的 raw_read_register / raw_write_register 调用。attach 一次的统计:
## attach: raw transfers=142 est. DAP commands=62
142 次 DP / AP 访问逐条在调试序列与 vendor里拆开;「DAP commands」是按 CMSIS-DAP 驱动的批处理规则推算的命令条数,见 CMSIS-DAP 驱动。只要再实现 RawSwdIo 而不是 RawDapAccess,同一个模拟目标就能接在位级那一层,见下一页。
Probe::new 不经过 DRIVERS 表,所以这个假探针不会出现在 probe-rs list 里;要让命令行能选到它,得实现 ProbeFactory 并通过插件注册。
小结
- 内置的 9 个
ProbeFactory在DRIVERS表里,插件追加;Lister按表的顺序挨个open,返回第一个成功的,NotFound静默跳过。 Probe是Box<dyn DebugProbe>加attached标志;DebugProbe有 11 个必需方法,按架构取接口的方法按值拿走探针、失败时还回来。- CMSIS-DAP、Glasgow 在 DAP 事务一层接入,J-Link、FTDI、CH347、linuxgpiod 只提供位级 IO,二者都走
ArmCommunicationInterface;ST-Link、SiFli 和带加速的 BMP 自己实现 ARM 调试接口,ST-Link 不使用 DP 一级的调试序列。 - 这些 trait 和
ArmCommunicationInterface::create都是公开的:本卷用它在库外接上一个软件模拟目标,attach 一次是 142 次 DP / AP 访问(模拟目标)。