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:
| 偏移 | 寄存器 | 用途 |
|---|---|---|
0x00 | CSW | 传输宽度(Size)、自增模式(AddrInc)、总线属性(特权、安全、可缓存) |
0x04 / 0x08 | TAR / TAR2 | 目标地址的低 / 高 32 位 |
0x0C | DRW | 读它就是读 [TAR],写它就是写 [TAR];开了自增时每次访问后 TAR 前进一个传输宽度 |
0x10–0x1C | BD0–BD3 | 以 TAR 对齐到 16 字节的那 4 个字为窗口的直接访问 |
0xF0 / 0xF8 | BASE2 / BASE | ROM table(或调试组件)的基地址 |
0xF4 | CFG | 是否支持大地址(LA)、大数据(LD)扩展,大小端 |
0xFC | IDR | AP 的身份:设计者、类别(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。
读一块内存
// 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 用它找调试组件,也用它的设计者和部件号自动识别芯片。