Rust in Everything
基于 probe-rs 0.32.0

MEM-AP 与 ROM table

主线位置:访问主线中段的上半截。Core::read_word_32(0x2000_0000) 到这里变成「设 CSW 的传输宽度、写 TAR、读 DRW」;本页还讲调试器怎么从 MEM-AP 的 BASE 出发,在 ROM table 里找到 SCS、FPB 这些调试组件。

flowchart TB
  api["Core::read_word_32"] --> armv7["Armv7m:委托给 memory"]
  armv7 --> adi["ADIMemoryInterface::read_32"]
  adi --> size["try_set_datasize:CSW.Size 变了才写"]
  size --> chunk["按 1 KiB 自增边界切块"]
  chunk --> tar["set_target_address:写 TAR"]
  tar --> drw["read_data:一个字读 DRW,多个字块读"]
  drw --> dap["DapAccess:SELECT 与探针"]

MEM-AP 的寄存器

MEM-AP 把对它寄存器的访问翻译成系统总线上的访问。probe-rs 用到的寄存器定义在 ap/registers.rs:

偏移寄存器用途
0x00CSW传输宽度(Size)、自增模式(AddrInc)、总线属性(特权、安全、可缓存)
0x04 / 0x08TAR / TAR2目标地址的低 / 高 32 位
0x0CDRW读它就是读 [TAR],写它就是写 [TAR];开了自增时每次访问后 TAR 前进一个传输宽度
0x10–0x1CBD0–BD3以 TAR 对齐到 16 字节的那 4 个字为窗口的直接访问
0xF0 / 0xF8BASE2 / BASEROM table(或调试组件)的基地址
0xF4CFG是否支持大地址(LA)、大数据(LD)扩展,大小端
0xFCIDRAP 的身份:设计者、类别(MEM-AP)、类型(AHB3、AXI 等)

前 4 个在 bank 0,后 4 个在 bank 0xF,所以在 CSW / TAR / DRW 和 IDR / CFG / BASE 之间来回,每次都要改 SELECT(上一页)。

ADI 规定 TAR 的自增只保证在一个 1 KiB 的块内有效,越过块边界后 TAR 的值由实现决定(可能回绕到块开头)。这一条决定了 probe-rs 读写内存的切块方式。

7 种 MEM-AP,一个枚举

不同的总线(AHB3、AHB5、APB、AXI)对应不同的 MEM-AP,CSW 里总线属性那几位的含义不一样。memory_aps! 宏生成一个 MemoryAp 枚举,有 7 个变体:AmbaAhb3、AmbaAhb5、AmbaAhb5Hprot、AmbaApb2Apb3、AmbaApb4Apb5、AmbaAxi3Axi4、AmbaAxi5。MemoryAp::new 读 IDR,按 TYPE 字段选变体;IDR 为 0 报 ApDoesNotExist,JTAG-AP 等非内存 AP 报 WrongApType。

每个变体在 new 里先读出 CSW 和 CFG,再写一个配好的 CSW。以 Cortex-M 上最常见的 AHB3 为例(amba_ahb3.rs:22):

// probe-rs/src/architecture/arm/ap/memory_ap/amba_ahb3.rs
        let csw = probe.read_raw_ap_register(&address, CSW::ADDRESS)?;
        let cfg = probe.read_raw_ap_register(&address, CFG::ADDRESS)?;
        let (csw, cfg) = (csw.try_into()?, cfg.try_into()?);

        let me = Self { address, csw, cfg };
        let mut csw = me.csw;
        csw.set_DbgSwEnable(true);
        csw.set_HNONSEC(!csw.SPIDEN());
        csw.set_MasterType(true);
        csw.set_Cacheable(true);
        csw.set_Privileged(true);
        csw.set_Data(true);
        csw.set_AddrInc(AddressIncrement::Single);
        probe.write_ap_register(&me, csw)?;

以特权、数据访问、可缓存的身份上总线,打开单次自增;HNONSEC 取 !SPIDEN:如果调试器没有安全调试权限,就以非安全身份访问。CSW 的值缓存在变体里,之后 try_set_datasize 只在传输宽度真的变了时才再写一次 CSW。

读一块内存

ADIMemoryInterface::read_32:

// probe-rs/src/architecture/arm/memory/adi_memory_interface.rs
        self.memory_ap
            .try_set_datasize(self.interface, DataSize::U32)?;

        while !data.is_empty() {
            let chunk_size = data.len().min(autoincr_max_bytes(address) / 4);

            // ...
            // autoincrement is limited to the 10 lowest bits, so write TAR every time.
            self.memory_ap.set_target_address(self.interface, address)?;
            self.memory_ap
                .read_data(self.interface, &mut data[..chunk_size])?;

            address = address
                .checked_add(chunk_size as u64 * 4)
                .ok_or(ArmError::OutOfBounds)?;
            data = &mut data[chunk_size..];
        }

autoincr_max_bytes 算出从当前地址到下一个 1 KiB 边界还剩多少字节,每块开头写一次 TAR。read_data 对只有一个字的块用普通的 AP 读,多个字用块读(read_ap_register_repeated,到了 CMSIS-DAP 就是 DAP_TransferBlock)。

read_8 / read_16 用对应的传输宽度,但 DRW 永远是 32 位:读出来的字节落在它在 32 位字里的「字节通道」上,代码按 地址 % 4 移位取出来(read_8)。所以按字节读 512 个字节,就是 512 次 DRW 访问;而 MemoryInterface::read 在 MEM-AP 不支持 64 位传输时会改走 32 位读,前后多读几个字节再裁掉(memory/mod.rs:154)。

写的路径对称。0.32 删掉了 ADIMemoryInterface 自己的 read 实现,改用 trait 的默认实现,因为旧实现在起始地址不对齐时会读到范围之外(CHANGELOG #3932)。

实验:一次读、一次跨界读

模拟目标上,session.core(0)?.read_word_32(0x2000_0000):

== read_word_32(0x20000000)
  W DP SELECT       =  0x000000f0
  R AP0 IDR         -> 0x24770011
  W DP SELECT       =  0x00000000
  R AP0 CSW         -> 0xeb000052
  W DP SELECT       =  0x000000f0
  R AP0 CFG         -> 0x00000000
  W DP SELECT       =  0x00000000
  W AP0 CSW         =  0xeb000052
  W AP0 TAR         =  0x20000000
  R AP0 DRW         -> 0xcafef00d   [read 0x20000000]
## read_word_32(0x20000000): 0xcafef00d; raw transfers=10 est. DAP commands=4

真正的读只有最后两行:写 TAR、读 DRW。CSW 的传输宽度已经是 32 位,try_set_datasize 没有再写。前 8 行是 MemoryAp::new:读 IDR、CSW、CFG,写 CSW,中间 4 次 SELECT 在 bank 0 和 bank 0xF 之间切换。

这 8 次访问出现在每一次 session.core(i) 里。原因在 attach_arm:每次拿 Core 都调 arm_interface.memory_interface(...),后者每次都新建一个 ADIMemoryInterface,也就新建一个 MemoryAp。Session::core 的文档鼓励「不要长期持有 Core,用完就还」,而命令行的轮询循环 poll_once 每轮都要 session.core(self.core_id),于是每 100 ms 的那一次 DHCSR 读之前,都先有这 8 次访问。在 CMSIS-DAP 上这是 3 次额外的 USB 往返(CMSIS-DAP 驱动),实际耗时待实测。

另一处可以在日志里看到的是 TAR 没有缓存:读 DHCSR 再写 DHCSR,两次访问前各写一遍同样的 TAR(Cortex-M 内核的日志里随处可见)。

跨 1 KiB 边界读 4 个字,read_32(0x200003f8, 4 words)(省略开头同样的 8 次):

  W AP0 TAR         =  0x200003f8
  -- block read, 2 words (DAP_TransferBlock)
  R AP0 DRW         -> 0x11111111   [read 0x200003f8]
  R AP0 DRW         -> 0x22222222   [read 0x200003fc]
  W AP0 TAR         =  0x20000400
  -- block read, 2 words (DAP_TransferBlock)
  R AP0 DRW         -> 0x33333333   [read 0x20000400]
  R AP0 DRW         -> 0x44444444   [read 0x20000404]

0x200003f8 到 0x20000400 只剩 8 个字节,第一块 2 个字;到了 0x20000400 重新写 TAR,第二块 2 个字。模拟目标的 TAR 自增是在 1 KiB 内回绕的,如果 probe-rs 不切块,第三个字会读回 0x20000000 的内容;读回的 4 个值都对,说明切块生效。

ROM table

MEM-AP 的 BASE 指向一张 ROM table:一个调试组件的目录。base_address 读 BASE(ADIv5 格式时再读 BASE2 拼高 32 位),低 12 位清零就是表的地址。

每个调试组件(包括 ROM table 自己)都占一个 4 KiB 的块,块的最后 48 字节是身份信息:

  • CIDR0–3(0xFF0–0xFFC):前导码 0x0D、0x?0、0x05、0xB1,第二个字节的高 4 位是组件类别(1 是 ROM table,9 是 CoreSight 组件,0xE 是通用 IP 组件)。component_class 只对前导码不对的情况打一条 info,然后继续,注释说「For now this works」。
  • PIDR0–7(0xFD0–0xFEC):设计者的 JEP106 编码、部件号、版本。peripheral_id 还读 0xFCC 的 DEVTYPE 和 0xFBC 的 DEVARCH。

Component::try_parse 读这两组,类别是 ROM table 就进 RomTable::try_parse:从表头开始每 4 字节一个条目,遇到全 0 结束;条目的 bit 0 表示「存在」,bit[31:12] 是相对表地址的有符号偏移(RomTableEntryRaw::new)。每个存在的条目再递归 Component::try_parse,嵌套的 ROM table 就这样展平。最后 determine_part 用(设计者、部件号、DEVTYPE、DEVARCH)查一张表,认出「Cortex-M3 SCS」「Cortex-M4 ROM」这类已知组件。

ROM table 在 probe-rs 里有两个用处:session.get_arm_components 找 DWT、ITM、TPIU 这些 trace 组件;芯片自动识别时,read_chip_info_from_rom_table 取最外层 ROM table 的设计者和部件号,交给各 vendor 和注册表去匹配芯片(调试序列与 vendor)。源码里的注释提醒,有的厂商不按规范给部件号,「Consider this not unique when working with targets!」

实验:遍历一张 ROM table

模拟目标里放了一张 Cortex-M3 式的表:表在 0xE00FF000,4 个条目指向 SCS(0xE000E000)、DWT(0xE0001000)、FPB(0xE0002000)、ITM(0xE0000000),条目值分别是 0xFFF0F003、0xFFF02003、0xFFF03003、0xFFF01003,每个组件的 CIDR / PIDR 按 ARM 的 JEP106 编码填好。调 session.get_arm_components(DpAddress::Default):

  R AP0 IDR         -> 0x24770011
  R AP1 IDR         -> 0x00000000
  ...
  R AP0 BASE        -> 0xe00ff003
  R AP0 DRW         -> 0x0000000d   [read 0xe00ffff0 ROM table]
  R AP0 DRW         -> 0x00000010   [read 0xe00ffff4 ROM table]
  R AP0 DRW         -> 0x00000005   [read 0xe00ffff8 ROM table]
  R AP0 DRW         -> 0x000000b1   [read 0xe00ffffc ROM table]
  ...
  R AP0 DRW         -> 0xfff0f003   [read 0xe00ff000 ROM table]
  R AP0 DRW         -> 0xfff02003   [read 0xe00ff004 ROM table]
  R AP0 DRW         -> 0xfff03003   [read 0xe00ff008 ROM table]
  R AP0 DRW         -> 0xfff01003   [read 0xe00ff00c ROM table]
  R AP0 DRW         -> 0x00000000   [read 0xe00ff010 ROM table]
  ...
## get_arm_components (ROM table): 0xe00ff000 part 0x4c3, 0xe000e000 Cortex-M3 SCS: SCS (System Control Space),
   0xe0001000 Cortex-M3 DWT: DWT (Data Watchpoint and Trace), 0xe0002000 Cortex-M3 FBP: FBP (Flash Patch and Breakpoint),
   0xe0000000 Cortex-M3 ITM: ITM (Instrumentation Trace Module); raw transfers=121 est. DAP commands=52

条目 0xFFF0F003 的 bit[31:12] 是 0xFFF0F,当 20 位有符号数是 -0xF1,乘以 4 KiB 就是 -0xF1000,0xE00FF000 - 0xF1000 = 0xE000E000,正是 SCS。四个组件都被认出来了;表本身的部件号 0x4C3 不在 determine_part 的表里,只显示成 part 0x4c3。整个遍历 121 次 DP / AP 访问:每个组件读 4 个 CIDR、8 个 PIDR、DEVTYPE、DEVARCH,再加 5 个条目和 MEM-AP 的初始化。真实芯片上的 ROM table 内容、嵌套层数待实测。

小结

  • MEM-AP 用 CSW 定宽度和总线属性、TAR 给地址、DRW 读写数据;CSW / TAR / DRW 在 bank 0,IDR / CFG / BASE 在 bank 0xF。
  • 7 种 MEM-AP 共用 MemoryAp 枚举,new 时读 IDR、CSW、CFG 并写一个配好的 CSW;CSW 有缓存,TAR 没有,每块访问前都重写。
  • 读写按 1 KiB 自增边界切块;8 位、16 位访问靠字节通道移位;模拟目标上跨界读的结果验证了切块。
  • 每次 session.core(i) 都重建 MemoryAp,多出 8 次 DP / AP 访问;命令行的轮询每 100 ms 拿一次 Core,这笔开销每轮都有。
  • ROM table 从 BASE 开始,CIDR 定类别、PIDR 定身份,条目是相对偏移;probe-rs 用它找调试组件,也用它的设计者和部件号自动识别芯片。

💬 关联讨论

发起讨论
加载中...