Rust in Everything
基于 probe-rs 0.32.0

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-DAPDAP 事务(DAP_Transfer 等命令)RawDapAccess,再交给 ArmCommunicationInterface是 / 是 / 是(JTAG)
GlasgowDAP 事务(只支持 SWD)RawDapAccess是 / 否 / 否
linuxspidevswd(插件)借 SPI 控制器收发RawDapAccess是 / 否 / 否
ST-Link内存访问(ST-Link 的读写内存命令)自己的 StlinkArmDebug是 / 否 / 否
SiFli UART内存访问(经 UART)自己的 SifliUartArmDebug是 / 否 / 否
Black Magic Probe固件支持 ADIv5 加速时做到内存访问;否则只做位级 IO加速时用自己的 BlackMagicProbeArmDebug,否则走 polyfill是 / 是 / 是
J-Link位级 IORawSwdIo + RawJtagIo,DAP 事务由 polyfill 拼是 / 只在支持 JTAG 时 / 同左
FTDI位级 IO(MPSSE)RawSwdIo + RawJtagIo是 / 是 / 是
CH347位级 IORawSwdIo + RawJtagIo是 / 是 / 是
linuxgpiod(插件)主机直接翻转 GPIORawSwdIo + RawJtagIo是 / 否 / 否
WCH-LinkRISC-V 调试(WCH 自己的协议)自己的 JtagAccess 与 RISC-V DTM否 / 是 / 否
ESP USB-JTAG(插件)位级 JTAGRawJtagIo否 / 是 / 是

两点从这张表里能直接读出来:

  • 走 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 访问(模拟目标)。

💬 关联讨论

发起讨论
加载中...