4、调试寄存器与内存映射:CoreSight寄存器空间布局、ROM Table解析、调试寄存器访问时序
调试接口说白了就是芯片的「后门」。没有它,你只能看着黑盒子发呆。有了它,你可以随时暂停CPU、翻看内存、修改寄存器。CoreSight就是ARM给出的这套标准后门方案。
我刚开始接触CoreSight时,最头疼的就是那一大堆寄存器地址。后来发现,只要搞懂ROM Table,一切就豁然开朗了。
4.1 CoreSight寄存器空间布局
CoreSight架构把调试组件都挂在一个共享的调试总线上。每个组件都有自己的地址窗口,大小通常是4KB(0x1000)。为什么是4KB?因为ARMv8的页表粒度最小就是4KB,这样每个组件正好占一页,MMU管理起来很方便。
| 地址范围 | 组件类型 | 典型基址 |
|---|---|---|
| 0x8000_0000 - 0x800F_FFFF | ROM Table | 0x8000_0000 |
| 0x8010_0000 - 0x8010_0FFF | CTI (Cross Trigger Interface) | 0x8010_0000 |
| 0x8020_0000 - 0x8020_0FFF | ETM (Embedded Trace Macrocell) | 0x8020_0000 |
| 0x8030_0000 - 0x8030_0FFF | CTI (Cross Trigger Interface) | 0x8030_0000 |
| 0x8040_0000 - 0x8040_0FFF | ETB (Embedded Trace Buffer) | 0x8040_0000 |
| 0x8050_0000 - 0x8050_0FFF | TPIU (Trace Port Interface Unit) | 0x8050_0000 |
每个组件内部,寄存器偏移也是固定的。CoreSight规范定义了标准偏移,比如:
- 0x000:组件ID寄存器 (CIDR0-CIDR3)
- 0x010:设备架构寄存器 (DEVID)
- 0x014:设备类型寄存器 (DEVTYPE)
- 0xF00:外设ID寄存器 (PIDR0-PIDR7)
- 0xFB0:组件ID寄存器 (CIDR0-CIDR3) 的另一个副本
4.2 ROM Table解析
ROM Table是CoreSight调试系统的「黄页」。它告诉你:系统里有哪些调试组件?它们分别在哪?
每个CoreSight系统至少有一个ROM Table,通常放在调试地址空间的起始位置。ROM Table本身也是一个4KB的组件,它的寄存器用来描述其他组件的位置。
4.2.1 ROM Table寄存器结构
ROM Table的核心寄存器是 ROMENTRY,从偏移0x000开始,每个entry占4字节。第0个entry描述第一个组件,第1个entry描述第二个组件,以此类推。最后一个entry的值是0,表示结束。
// ROMENTRY寄存器格式
// bit [31:28] : 入口类型 (0=组件, 1=子ROM Table)
// bit [27:12] : 保留
// bit [11:0] : 相对于ROM Table基址的偏移量(单位:4KB)
// 示例:读取ROM Table的第一个entry
uint32_t rom_base = 0x8000_0000;
uint32_t entry0 = *(volatile uint32_t*)(rom_base + 0x000);
uint32_t entry1 = *(volatile uint32_t*)(rom_base + 0x004);
// entry0 = 0x0000_0010 → 组件在 rom_base + 0x10 * 4KB = 0x8001_0000
// entry1 = 0x0000_0020 → 组件在 rom_base + 0x20 * 4KB = 0x8002_0000
我曾经在一个项目中,发现ROM Table读出来的entry全是0。查了半天,原来是调试总线的时钟没开。CoreSight组件需要时钟才能响应访问,ROM Table也不例外。
4.2.2 组件ID与设备类型
找到组件地址后,怎么确认它是什么组件?读它的组件ID寄存器(CIDR)和外设ID寄存器(PIDR)。
| 寄存器 | 偏移 | 含义 | 典型值 |
|---|---|---|---|
| PIDR0 | 0xFE0 | 外设ID低8位 | 0x21 (ETM) |
| PIDR1 | 0xFE4 | 外设ID [15:8] + 部件号 [3:0] | 0xB9 |
| PIDR2 | 0xFE8 | 修订版本 + 部件号 [7:4] | 0x4A |
| PIDR3 | 0xFEC | 外设ID [23:16] | 0x00 |
| CIDR0 | 0xFF0 | 组件ID [7:0] | 0x0D |
| CIDR1 | 0xFF4 | 组件ID [15:8] | 0xB1 |
| CIDR2 | 0xFF8 | 组件ID [23:16] | 0x05 |
| CIDR3 | 0xFFC | 组件ID [31:24] | 0xB1 |
CIDR的值是固定的:0xB105_100D。如果读出来不是这个值,说明要么地址不对,要么组件没使能。
4.3 调试寄存器访问时序
调试寄存器的访问和普通内存访问不一样。它走的是调试总线(DAP总线),而不是系统总线。这意味着:
- 访问延迟更高:一次调试寄存器访问可能需要几十到几百个APB时钟周期
- 不支持缓存:调试寄存器都是non-cacheable、non-bufferable的
- 访问大小有限制:大多数调试寄存器只支持32位访问
4.3.1 典型访问时序
通过JTAG/SWD访问调试寄存器的典型流程是这样的:
- 选择AP:通过DP的APSEL寄存器选择要访问的Access Port
- 设置地址:写入目标寄存器的32位地址到AP的TAR寄存器
- 发起传输:写DRW(数据读/写)寄存器触发实际访问
- 等待完成:轮询DP的CTRL/STAT寄存器,直到READY位被置位
- 读取数据:从DRW寄存器读出结果
// 伪代码:通过SWD读取调试寄存器
uint32_t debug_read(uint32_t addr) {
// 1. 选择AP0
swd_write(DP_SELECT, 0x00000000);
// 2. 设置目标地址
swd_write(AP_TAR, addr);
// 3. 发起读请求
swd_write(AP_DRW, 0); // 写入任意值触发读
// 4. 等待传输完成
while (!(swd_read(DP_CTRL_STAT) & 0x00000020)); // 等待READY
// 5. 读取结果
return swd_read(AP_DRW);
}
你想想看,如果每次读一个寄存器都要走这5步,那效率确实不高。所以调试器通常会做「自动递增访问」——连续读取相邻地址时,只需要设置一次地址,后面的访问自动递增。
4.3.2 访问时序的坑
我在调试一个Cortex-A72的功耗管理固件时,遇到过一个问题:通过调试接口读某个寄存器,有时候返回0,有时候返回正确值。后来发现是时序问题——调试接口的时钟和CPU时钟不同步,读操作刚好落在寄存器更新的中间时刻。
解决办法?读两次,取一致的那次。或者读之前先写一个dummy值,等一个时钟周期再读。
- 读之前先写0到同一个地址(清空内部流水线)
- 插入3-5个NOP(给总线时间稳定)
- 连续读两次,比较结果是否一致
- 如果一致,取该值;如果不一致,重新读
4.4 知识结构总览
下面这张图把CoreSight寄存器空间、ROM Table解析和访问时序串在了一起。你可以把它当作调试系统的手绘地图。
这张图从左到右展示了完整的调试链路:先知道地址空间布局,然后用ROM Table解析出每个组件的位置,最后通过标准的5步时序去访问它们。
嗯,到这里,调试寄存器与内存映射的核心内容就讲完了。记住一句话:ROM Table是调试系统的入口,组件ID是验证手段,稳定时序是访问保障。这三样东西搞定了,CoreSight调试系统在你面前就是透明的。