Rust in Everything
基于 probe-rs 0.32.0

调试序列与 vendor

主线位置:命令主线的「attach」一步,以及访问主线、调试主线上所有「芯片说了算」的地方。前三章讲的是规范里的通用做法,本页讲 probe-rs 怎么让每颗芯片在固定的几个点上改写这些做法,并把 Session::new 的 attach 顺序完整走一遍。

sequenceDiagram
  participant S as Session::new
  participant Q as 调试序列
  participant I as ArmCommunicationInterface
  participant T as 目标
  S->>I: select_debug_port
  I->>Q: debug_port_setup 和 debug_port_start
  Q->>T: 线复位 读 DPIDR 上电
  S->>Q: debug_device_unlock
  Q->>T: STM32 写 DBGMCU_CR 或 nRF 读 CTRL-AP
  S->>Q: 每个核 debug_core_start
  Q->>T: 写 DHCSR C_DEBUGEN
  S->>T: clear_all_hw_breakpoints 停核 清 FPB 放行

为什么需要序列

ADI 和 Armv7-M 规定了「通用」的连接、上电、复位方法,但真实芯片有各种例外:低功耗模式下调试时钟会被关掉,要先改厂商的调试控制寄存器;芯片被读保护锁住了,要先通过厂商自定义的 AP 擦除解锁;系统复位会把调试端口也复位掉,要重新连。CMSIS-Pack 的调试描述(debug description)为这类差异定义了一组标准的「序列」名字(DebugPortSetup、DebugDeviceUnlock、ResetSystem 等),厂商在 pack 里给出自己的实现。

probe-rs 把这组名字做成了一个 trait:ArmDebugSequence,每个方法都有默认实现(大多照 CMSIS 的默认行为写),厂商只覆盖需要的几个。方法文档里大都链到 Open-CMSIS-Pack 规范的对应条目,#[doc(alias = "...")] 写着 CMSIS 的名字。

18 个钩子

组方法对应 CMSIS 序列谁调
DPdebug_port_setupDebugPortSetupselect_dp,第一次连 DP 时
debug_port_connect无(probe-rs 自己的)debug_port_setup;multidrop 切换 DP 时
debug_port_startDebugPortStartselect_dp,DP 第一次被访问时
debug_port_stopDebugPortStop接口断开时
设备debug_device_unlockDebugDeviceUnlockSession::new,连上 DP 之后
debug_erase_sequenceDebugDeviceUnlock 里的擦除部分erase 的 chip erase
allowed_access_ports无枚举 AP 时的白名单
核on_attach无连核之前
debug_core_startDebugCoreStartSession::new,给每个核使能调试
debug_core_stopDebugCoreStopSession 被 drop 时
recover_support_startRecoverSupportStartstep / run 之前(默认空)
复位reset_hardware_assert / reset_hardware_deassertResetHardwareAssert / ResetHardwareDeassertconnect-under-reset
reset_catch_set / reset_catch_clearResetCatchSet / ResetCatchClearreset_and_halt、connect-under-reset
reset_systemResetSystemreset、reset_and_halt
其他trace_startTraceStart配置 SWO / TPIU
prepare_running_on_ram无固件下载到 RAM 运行时设 SP、PC、VTOR

18 个方法里,DP 一组拿到的是 &mut dyn DapProbe(直接面对探针,因为这时还没有可用的 DP),核一组拿到的是某个 MEM-AP 上的 ArmMemoryInterface,设备一组拿到的是整个 ArmDebugInterface。参数类型本身就说明了每个钩子能碰到哪一层。

不覆盖任何方法的 DefaultArmSequence 就是「规范里的通用做法」。Cortex-M 的默认实现都很短:debug_core_start 读 DHCSR,没开 C_DEBUGEN 就写上(cortex_m_core_start);reset_system 写 AIRCR 的 SYSRESETREQ,然后最多等 600 ms 让 DHCSR 的 S_RESET_ST 清掉(cortex_m_reset_system,注释说 600 ms 来自 PSOC 6 的文档)。

Vendor:按名字选序列,按 ROM table 认芯片

序列是按芯片选的,选的人是 Vendor:

// probe-rs/src/vendor/mod.rs
pub trait Vendor: Send + Sync + std::fmt::Display {
    /// Tries to create a debug sequence for the given chip.
    fn try_create_debug_sequence(&self, chip: &Chip) -> Option<DebugSequence>;

    /// Tries to identify an ARM chip. Returns `Some(target name)` on success.
    fn try_detect_arm_chip(
        &self,
        _registry: &Registry,
        _probe: &mut dyn ArmDebugInterface,
        _chip_info: ArmChipInfo,
    ) -> Result<Option<String>, Error> {
        Ok(None)
    }
    // ... RISC-V、Xtensa、从探针固件识别,同样默认返回 None
}

建 Target 时,config/target.rs:114 挨个问 16 个 vendor(插件注册的排在最前面),第一个返回 Some 的说了算;都不认识就用该架构的默认序列。选法很朴素,就是比芯片名前缀。ST 的 try_create_debug_sequence:STM32F0、L0、G0 用 Stm32Armv6;F1、F2、F3、F4、F7、G4、L1、L4、WB、WL 用 Stm32Armv7;H7 系列、H5 / L5 / U3 / U5 / WBA、N6 各有各的。Nordic 的同理,nRF52 开头的用 Nrf52。

没给芯片名时(--chip 和 ELF 元数据都没有),auto_determine_target 先连上 DP,读最外层 ROM table 的设计者和部件号(MEM-AP 与 ROM table),再问每个 vendor 的 try_detect_arm_chip。Nordic 的实现只在设计者是 Nordic 时才往下走,再读芯片的 FICR 或 CONFIGID 比对注册表里每个家族的识别规则;注释坦白「FIXME: This is a bit shaky but good enough for now.」所有 vendor 都不认识时,退回按 ROM table 的部件号查注册表。

例一:STM32 的 DBGMCU_CR

STM32 在睡眠、停止、待机模式下会关掉调试相关的时钟,调试器就连不上了。DBGMCU_CR(0xE0042004)的低三位 DBG_SLEEP、DBG_STOP、DBG_STANDBY 让这些模式下调试保持可用。Stm32Armv7 在 debug_device_unlock 里把三位置 1,同时记下原值;在 debug_core_stop 里写回原值:

// probe-rs/src/vendor/st/sequences/stm32_armv7.rs
    fn debug_device_unlock(
        &self,
        interface: &mut dyn ArmDebugInterface,
        default_ap: &FullyQualifiedApAddress,
        _permissions: &crate::Permissions,
    ) -> Result<(), ArmError> {
        let mut memory = interface.memory_interface(default_ap)?;
        let mut cr = dbgmcu::Control::read(&mut *memory)?;
        self.saved_cr_value.lock().unwrap().replace(cr.0);

        cr.enable_standby_debug(true);
        cr.enable_sleep_debug(true);
        cr.enable_stop_debug(true);
        cr.write(&mut *memory)?;

        Ok(())
    }

这就是 probe-rs gdb --dry-run --chip STM32F103C8 打印的头两行 Read 0xe0042004 = 0、Write 0xe0042004 = 0x00000007,和退出时的最后一行 Write 0xe0042004 = 0x00000000(FakeProbe)。它借用的是「解锁」这个钩子,做的事和解锁无关,只因为这是「连上之后、碰核之前」唯一的时机。

例二:nRF52 的 CTRL-AP

nRF52 除了 AP 0(AHB-AP)还有一个 Nordic 自定义的 CTRL-AP,编号 1。它的 APPROTECTSTATUS(偏移 0x0C)为 0 表示芯片开了读保护(APPROTECT),此时 AHB-AP 访问不到内存,只能通过 CTRL-AP 的 ERASEALL 整片擦除来解锁。Nrf52::debug_device_unlock:

// probe-rs/src/vendor/nordicsemi/sequences/nrf52.rs
        let ctrl_ap = &FullyQualifiedApAddress::v1_with_default_dp(1);

        tracing::info!("Checking if core is unlocked");
        if self.is_core_unlocked(iface, ctrl_ap)? {
            tracing::info!("Core is already unlocked");
            return Ok(());
        }

        tracing::warn!("Core is locked. Erase procedure will be started to unlock it.");
        permissions
            .erase_all()
            .map_err(|MissingPermissions(desc)| ArmError::MissingPermissions(desc))?;

        // Reset
        iface.write_raw_ap_register(ctrl_ap, RESET, 1)?;
        iface.write_raw_ap_register(ctrl_ap, RESET, 0)?;

        // Start erase
        iface.write_raw_ap_register(ctrl_ap, ERASEALL, 1)?;

        // Wait for erase done
        while iface.read_raw_ap_register(ctrl_ap, ERASEALLSTATUS)? != 0 {}
        // ... 再复位一次,确认已解锁
        Err(ArmError::ReAttachRequired)

三点:擦除会抹掉整片 flash,所以要用户显式给 Permissions::allow_erase_all()(命令行的 --allow-erase-all),没给就报 MissingPermissions;等擦除完成的 while 没有超时;解锁成功后返回 ReAttachRequired,Session::new 收到它会调 reattach_arm_interface 重连一次。第一步 is_core_unlocked 读的就是 AP 1 的地址 12,所以 FakeProbe 上 nRF52 一 attach 就 panic;用 expect_operation 登记一个非 0 的应答就能过(FakeProbe)。锁住的芯片上解锁的实际日志待实测。

Session::new 的 attach 顺序

把序列放回调用顺序里。ARM 目标走 attach_arm_debug_interface:

步普通 attachconnect-under-reset 多出的
0reset_hardware_assert:拉低 nRESET
1probe.attach_to_unspecified():探针自己的连接(CMSIS-DAP 发 DAP_Connect)
2try_into_arm_debug_interface,select_debug_port:debug_port_setup、debug_port_start
3debug_device_unlock;返回 ReAttachRequired 时重连
4每个核 reset_catch_set(置 VC_CORERESET),然后 reset_hardware_deassert:放开 nRESET,读引脚等它真的变高,最多 1 秒
5每个核 debug_core_start
6建 Session等每个核停在复位向量上,reset_catch_clear
7clear_all_hw_breakpoints:停下所有核,清空 FPB 比较器,再放行原本在跑的核同左

connect-under-reset 的意义在于:如果固件一上电就把 SWD 引脚改作他用、或者进了深睡眠,正常 attach 就连不上;在复位期间连接,固件还没来得及跑。reset_hardware_deassert 超时时,Session::new 会打一条警告,提示复位引脚大概没接好。

实验:142 次访问

模拟目标上普通 attach STM32F103C8,日志按 MEM-AP 的重建(每次 memory_interface 都有的那 8 次访问)切开,正好对应上表的步骤:

步DP / AP 访问内存一层做了什么
2 select_debug_port3 个 SWJ 序列 + 15线复位、读 DPIDR、上电、开溢出检测(见 DP、AP 与 SELECT)
3 debug_device_unlock13读 DBGMCU_CR 得 0,写 7(多出的 1 次是 CSW 改传输宽度:模拟目标的 CSW 复位值是字节宽度)
5 debug_core_start12读 DHCSR 0x00010000,写 0xa05f0001
7 停核30第一次拿 Core:读 DHCSR、清 DFSR、读 MVFR0;核在跑,于是 halt 并读 PC
7 清 FPB34读 FP_CTRL 得 6 个比较器,逐个读出(都已为空,不用写)
7 放行38run:先单步,再清 C_MASKINTS、C_STEP
合计142
== attach STM32F103C8
  ...
  R DBGMCU_CR  0x00000000
  W DBGMCU_CR  0x00000007
  R DHCSR      0x00010000
  W DHCSR      0xa05f0001
  R DHCSR      0x00010001
  W DFSR       0x0000001f
  R MVFR0      0x00000000
  R DHCSR      0x00010001
  W DHCSR      0xa05f0003
  ...
## attach: raw transfers=142 est. DAP commands=62

(上面只列了内存一层的访问,每次访问前的 SELECT、TAR 写省略。)其中有 40 次(5 次 MEM-AP 重建 × 8)花在反复重建 MEM-AP 上,这是 MEM-AP 与 ROM table 讲过的每次 memory_interface 的固定开销。真实芯片上 attach 的耗时、connect-under-reset 的时序待实测。

Session 被 drop 时反向收尾:每个核 debug_core_stop(STM32 在这里写回 DBGMCU_CR),接口断开时 debug_port_stop 清掉上电请求并等两个 ACK 位落下。模拟目标日志的最后三行就是:

  W DBGMCU_CR  0x00000000
  W DP SELECT       =  0x00000000
  W DP CTRL/STAT    =  0x00000000
  R DP CTRL/STAT    -> 0x00000000

小结

  • ArmDebugSequence 把 CMSIS-Pack 调试描述里的标准序列做成 18 个带默认实现的钩子,按参数类型分别面对探针、MEM-AP 或整个调试接口。
  • Vendor 按芯片名前缀选序列,插件注册的排在最前;不给芯片名时先读 ROM table 的设计者和部件号,再交给各 vendor 识别,Nordic 还要读 FICR。
  • STM32 借 debug_device_unlock 打开 DBGMCU_CR 的低功耗调试位、在 debug_core_stop 恢复;nRF52 在同一个钩子里读 CTRL-AP,锁住时需要 allow_erase_all 才擦除解锁,并要求重连。
  • attach 顺序:探针连接 → DP setup / start → 设备解锁 → 每个核 debug_core_start → 清硬件断点;connect-under-reset 在前后加上拉低 / 放开 nRESET 和 reset catch。模拟目标上普通 attach 是 142 次 DP / AP 访问,近三成花在重建 MEM-AP 上。

💬 关联讨论

发起讨论
加载中...