Rust in Everything
基于 probe-rs 0.32.0

DP、AP 与 SELECT

主线位置:访问主线中段的下半截。上一层要「读 AP0 的 DRW」,本页讲 ArmCommunicationInterface 在把它交给探针之前要做什么:确保连上了那个 DP、DP 已上电、SELECT 指向了正确的 AP 和 bank。

flowchart TB
  req["read_raw_ap_register(AP0, 0x0C)"] --> sap["select_ap_and_ap_bank"]
  sap --> sdp["select_dp:第一次就跑 setup 和 start"]
  sdp --> cache{"SELECT 缓存和要的一样吗"}
  cache -->|"一样"| raw["probe.raw_read_register(ApRegister 0x0C)"]
  cache -->|"不一样"| wsel["写 DP SELECT,更新缓存"] --> raw

ADI 的模型

ARM 调试接口(Arm Debug Interface,ADI)把调试器看到的东西分两层:

  • DP(Debug Port):线这一端。SWD 或 JTAG 的每个包都是对 DP 或 AP 的一次寄存器访问。DP 自己有 DPIDR(只读,身份)、ABORT(只写,清错误)、CTRL/STAT(上电请求和错误标志)、SELECT(选 AP 和 bank)、RDBUFF(取 posted 读的结果)几个寄存器。一根 SWD 线上通常只有一个 DP;SWD v2 的 multidrop 允许多个 DP 共享一根线,靠 TARGETSEL 选中其中一个(RP2040 就是这样)。
  • AP(Access Port):DP 后面的若干个端口。最常见的是 MEM-AP,它把「读写 AP 寄存器」翻译成「读写一条系统总线上的地址」,下一页讲;也有厂商自定义的 AP,比如 nRF52 的 CTRL-AP。

SWD 请求里只有 A[3:2] 两位地址(位级探针),所以一个 AP 的 256 字节寄存器空间被分成 16 个 bank,每个 bank 4 个寄存器。SELECT 的高 8 位选 AP(APSEL),bit[7:4] 选 bank(APBANKSEL),bit[3:0] 选 DP 自己的 bank(DPBANKSEL,只影响 DP 地址 0x4)。ADIv6(DPv3)改成用 SELECT 加 SELECT1 拼出一个 64 位的 AP 基地址,AP 不再有编号。

probe-rs 用两个类型表示这两种地址(traits.rs):ApAddress::V1(u8) 是 ADIv5 的 AP 编号,ApAddress::V2(ApV2Address) 是 ADIv6 的基地址;再加上 DpAddress(Default 或 Multidrop(targetsel)),合成 FullyQualifiedApAddress。目标描述里每个核写着自己的 AP,比如 STM32F103C8 的核是 ap: !v1 0。

ArmDebugInterface 与 ArmCommunicationInterface

架构层对探针的要求是 ArmDebugInterface:

// probe-rs/src/architecture/arm/communication_interface.rs
pub trait ArmDebugInterface: DapAccess + SwdSequence + SwoAccess + Send {
    fn reinitialize(&mut self) -> Result<(), ArmError>;
    fn access_ports(&mut self, dp: DpAddress)
        -> Result<BTreeSet<FullyQualifiedApAddress>, ArmError>;
    fn close(self: Box<Self>) -> Probe;
    fn select_debug_port(&mut self, dp: DpAddress) -> Result<(), ArmError>;
    fn current_debug_port(&self) -> Option<DpAddress>;
    fn memory_interface(
        &mut self,
        access_port: &FullyQualifiedApAddress,
    ) -> Result<Box<dyn ArmMemoryInterface + '_>, ArmError>;
}

DapAccess 是「按 DP 地址和 AP 地址读写寄存器」;memory_interface 给出某个 MEM-AP 上的内存接口。ST-Link 这类探针自己实现这个 trait(DebugProbe 与驱动注册表),其余的都用 ArmCommunicationInterface:

// probe-rs/src/architecture/arm/communication_interface.rs
pub struct ArmCommunicationInterface {
    probe: Option<Box<dyn DapProbe>>,

    /// Currently selected debug port. For targets without multidrop,
    /// this will always be the single, default debug port in the system.
    ///
    /// If this is None, the interface is in an uninitialized state.
    current_dp: Option<DpAddress>,
    dps: HashMap<DpAddress, DpState>,
    use_overrun_detect: bool,
    sequence: Arc<dyn ArmDebugSequence>,
}

dps 给每个连过的 DP 存一份 DpState:DP 的版本,和一份 SELECT 的缓存(SelectCache::DPv1(SelectV1) 或 SelectCache::DPv3(SelectV3, Select1))。sequence 是芯片的调试序列,DP 的 setup、start、stop 都要问它。

select_dp:懒初始化

所有访问的第一步都是 select_dp。它做两件事:

  1. 换 DP。current_dp 不是要的那个时:如果此前没连过任何 DP,跑完整的 debug_port_setup(线复位、JTAG 转 SWD、必要时从 dormant 状态唤醒、读 DPIDR);如果只是在 multidrop 的几个 DP 之间切换,先试快速的 debug_port_connect(线复位加 TARGETSEL),失败了再退回完整的 setup。0.32 把快速切换失败时的那条日志从 warn 降成了 debug(#4133),因为 RP2040 这类芯片上这是预期中的失败。
  2. 初始化 DP 状态。dps 里还没有这个 DP 时,跑 debug_port_start(清错误、给调试域和系统域上电),然后按需要打开 CTRL/STAT 的溢出检测,读 DPIDR 记下版本;DPv3 的 SELECT 缓存换成 ADIv6 的形式。
// probe-rs/src/architecture/arm/communication_interface.rs,select_dp 里
            let idr: DebugPortId = self.read_dp_register::<DPIDR>(dp)?.into();
            tracing::info!(
                "Debug Port version: {} MinDP: {:?}",
                idr.version,
                idr.min_dp_support
            );
            // ...
            state.debug_port_version = idr.version;
            if idr.version == DebugPortVersion::DPv3 {
                state.current_select = SelectCache::DPv3(SelectV3(0), Select1(0));
            }

也就是说,连接是懒的:ArmCommunicationInterface::create 只是把探针和序列存起来,第一次真正访问时才连。Session::new 会显式调一次 select_debug_port,控制这件事发生在 attach 的哪一步(调试序列与 vendor)。

0.30 去掉的 typestate

0.29 里这个类型是 ArmCommunicationInterface<S: ArmDebugState>,状态参数有 Uninitialized 和 Initialized 两种;探针先给出一个 UninitializedArmProbe,调 initialize 才得到可用的接口。0.30 的 #3382 把它改成现在的样子:状态从类型参数挪进了 current_dp: Option<DpAddress>,「未初始化」变成运行期的 None。

取舍很直接。typestate 让「对未初始化的接口读 AP」在编译期就过不去,代价是每个持有接口的地方都要写清楚状态参数,而接口要被装进 Box<dyn ArmDebugInterface>、在 Session 里长期存放、还要支持 reinitialize(某些芯片的复位会把 DP 也复位掉,序列需要原地重连);这些在 typestate 下都别扭。改成运行期状态之后,「重连」就是把 current_dp 和 dps 清掉再 select_dp 一次:

// probe-rs/src/architecture/arm/communication_interface.rs
    fn reinitialize(&mut self) -> Result<(), ArmError> {
        let current_dp = self.current_dp;

        // Simulate the drop / close of the initialized communication interface.
        self.disconnect();

        // This should be set to None by the disconnect call above.
        assert!(self.current_dp.is_none());
        assert!(self.dps.is_empty());

        // Reconnect to the DP again
        if let Some(dp) = current_dp {
            self.select_dp(dp)?;
        }

        Ok(())
    }

同一批改动还把 trait 名 ArmProbeInterface 改成了 ArmDebugInterface(#3425)。

SELECT 缓存

访问 AP 寄存器前,select_ap_and_ap_bank 先算出这次需要的 SELECT 值,和缓存比较,不一样才写:

// probe-rs/src/architecture/arm/communication_interface.rs
        let previous_select = dp_state.current_select;
        match (ap.ap(), &mut dp_state.current_select) {
            (ApAddress::V1(port), SelectCache::DPv1(s)) => {
                let ap_register_address = (ap_register_address & 0xFF) as u8;
                let ap_bank = ap_register_address >> 4;
                s.set_ap_sel(*port);
                s.set_ap_bank_sel(ap_bank);
            }
            (ApAddress::V2(base), SelectCache::DPv3(s, s1)) => {
                let address = base.0.unwrap_or(0) + ap_register_address;
                s.set_addr(((address >> 4) & 0xFFFF_FFFF) as u32);
                s1.set_addr((address >> 32) as u32);
            }
            // ... 两种不匹配的组合,见下文
        }

        if previous_select != dp_state.current_select {
            tracing::debug!("Changing SELECT to {:x?}", dp_state.current_select);

            match dp_state.current_select {
                SelectCache::DPv1(select) => {
                    self.write_dp_register(ap.dp(), select)?;
                }
                SelectCache::DPv3(select, select1) => {
                    self.write_dp_register(ap.dp(), select)?;
                    self.write_dp_register(ap.dp(), select1)?;
                }
            }
        }

然后 read_raw_ap_register 只把地址的低 8 位交给探针:RegisterAddress::ApRegister((address & 0xFF) as u8),探针再从里面取 A[3:2]。DP 寄存器走另一个函数 select_dp_and_dp_bank,只有地址 0x0 和 0x4 可能需要切 DPBANKSEL。

地址类型和 DP 版本对不上的两种组合,0.32 都给了明确的处理:DPv3 的目标描述里写了 ADIv5 风格的 AP 0,就当作基地址 0 用并打一条警告(#4032);选了一个用 APv2 地址的芯片(比如 RP2350),实际连上的却是只有 ADIv5 SELECT 的 DP,返回错误而不是走到 unreachable!(#4129),错误信息提示「This usually means the wrong chip was selected for the attached target.」

缓存有一个前提:只有 probe-rs 自己写 SELECT。如果有别的东西改了目标的 SELECT(比如目标复位把 DP 也复位了),缓存就和实际不符,后续访问会落到错误的 AP 或 bank 上;这正是 reinitialize 要清空 dps 的原因。

实验:attach 时 DP 一侧发生了什么

模拟目标(本部分的说明)上 attach STM32F103C8,开头 18 行全是 DP 一侧的:

  SWJ_Sequence 51 bits 0x7ffffffffffff
  SWJ_Sequence 16 bits 0xe79e
  SWJ_Sequence 54 bits 0x7ffffffffffff
  R DP DPIDR        -> 0x1ba01477
  W DP ABORT        =  0x0000001e
  W DP SELECT       =  0x00000000
  R DP CTRL/STAT    -> 0x00000000
  W DP SELECT       =  0x00000000
  R DP DPIDR        -> 0x1ba01477
  R DP CTRL/STAT    -> 0x00000000
  W DP ABORT        =  0x0000001f
  W DP CTRL/STAT    =  0x50000f00
  R DP CTRL/STAT    -> 0xf0000f00
  W DP CTRL/STAT    =  0x50000f00
  R DP CTRL/STAT    -> 0xf0000f00
  R DP CTRL/STAT    -> 0xf0000f00
  W DP CTRL/STAT    =  0xf0000f01
  R DP DPIDR        -> 0x1ba01477

逐段对应到代码:

行来自做什么
1–3debug_port_setup → debug_port_connect51 个周期的 1(线复位);16 位的 0xE79E(JTAG 转 SWD);再一次线复位,这次后面跟 3 个 0(空闲)
4–7debug_port_connect读 DPIDR 让 SWD 接口生效;写 ABORT 0x1e 清四个粘滞错误;SELECT 归零;读一次 CTRL/STAT
8–15debug_port_startSELECT 归零,再读 DPIDR、CTRL/STAT;写 ABORT 0x1f(多了 DAPABORT);CDBGPWRUPREQ、CSYSPWRUPREQ 置 1(0x50000f00,bit[11:8] 的 0xf 是 byte lane 掩码),轮询到两个 ACK 位(0xf0000f00);按源码注释再写一遍、再读一遍确认
16–17select_dpuse_overrun_detect 是 true(模拟目标按位级探针的方式创建),读改写 CTRL/STAT,打开 ORUNDETECT(bit 0)
18select_dp读 DPIDR,0x1BA01477:bit[15:12] 版本 1(DPv1),bit[11:1] 设计者 0x23B(ARM 的 JEP106 编码)

第 19 行开始访问 AP:W DP SELECT = 0x000000f0,APSEL 0、APBANKSEL 0xF,为的是读 bank 0xF 里的 IDR(0xFC)。之后每次在 bank 0(CSW、TAR、DRW)和 bank 0xF(IDR、CFG、BASE)之间来回,都会多一个 SELECT 写;同一个 bank 里连续访问则一个也不多。比如建一个 MEM-AP 接口的 8 次访问里有 4 次是 SELECT(下一页)。

最后,access_ports 怎么知道有几个 AP:ADIv5 下 valid_access_ports 从 AP 0 往上逐个读 IDR,用 map_while,遇到第一个 IDR 为 0 的就停。模拟目标只有 AP 0,遍历 ROM table 时日志里就是 R AP0 IDR -> 0x24770011 之后紧跟 R AP1 IDR -> 0x00000000,到此为止。nRF52 的 CTRL-AP 是 AP 1,AP 编号连续,不受影响;编号中间有空洞的芯片,空洞之后的 AP 不会被枚举到,这是 map_while 的行为,实际是否有这样的芯片未核实。

小结

  • ADI 两层:DP 管线和上电,AP 挂在 DP 后面;SWD 只有 A[3:2] 两位地址,AP 寄存器靠 SELECT 的 APSEL 和 APBANKSEL 分 bank,ADIv6 改用 SELECT + SELECT1 拼出的 64 位基地址。
  • ArmCommunicationInterface 为所有 DAP 级探针实现 ArmDebugInterface;select_dp 懒连接:第一次访问时跑序列的 debug_port_setup 和 debug_port_start,打开溢出检测,读 DPIDR 定版本。
  • 0.30 去掉了 typestate,状态改由 current_dp: Option<DpAddress> 表示,换来可以装箱、长期持有、原地 reinitialize。
  • 每个 DP 一份 SELECT 缓存,值变了才写;地址类型和 DP 版本不匹配时 0.32 给出明确的警告或错误。
  • 模拟目标上,attach 的 DP 部分是 3 个 SWJ 序列加 15 次 DP 访问;AP 枚举遇到第一个 IDR 为 0 的 AP 就停。

💬 关联讨论

发起讨论
加载中...