melonDS 中 Teakra XpertTeak DSP 的 CRU(Code Replacement Unit)外设解析:MMIO 寄存器布局与程序替换机制

原创2026-10-03 16:48:37476 阅读
文章标签:虚拟化JIT编译图形学

melonDS 中 Teakra XpertTeak DSP 的 CRU(Code Replacement Unit)外设解析:MMIO 寄存器布局与程序替换机制

本文基于 melonDS 仓库内 Teakra 源码树 中 XpertTeak DSP 外设文档,深入剖析 CRU(Code Replacement Unit,代码替换单元)在 DSP MMIO 空间中的寄存器布局、字段语义与地址路由机制,并结合 MMIO 总览、内存接口实现 以及 melonDS 的 DSP LLE 集成代码 验证其底层工作原理。读完本文,你将掌握 CRU 的完整寄存器地图、如何将外设相对偏移换算为绝对 MMIO 地址,以及如何在模拟器源码中追踪该外设的读写路径与当前实现状态。

一、CRU 在 XpertTeak 外设体系中的定位

XpertTeak 是 DSi/3DS 所搭载的 Teak 家族 DSP(详见 Teakra 项目说明),由一个 Teak 系处理器核心及一系列外设组成。根据 MMIO 总览文档,处理器与外设(以及间接的外部硬件,如 ARM CPU)之间的通信全部经由 MMIO 完成:MMIO 区域占用 16 位地址空间中的 0x0800 个字,默认起始地址为 0x8000,且起始位置可通过 MIU(内存接口单元)中的 MMIOBASE 字段重新配置。

每个外设对应 MMIO 中的一个子区域,文档中给出的分配如下(地址均为相对 MMIO 区域起点的偏移):

偏移 外设 说明
+0x0000 ? 未明确
+0x0004 JAM —
+0x0010 GLUE —
+0x0020 Timer 定时器
+0x0050 SIO 串行 IO
+0x0060 OCEM 片上仿真模块(调试)
+0x0080 PMU 电源管理单元
+0x00C0 APBP 与 CPU 通信的高级外设总线端口
+0x00E0 AHBM 访问外部内存的高级高性能总线主机
+0x0100 MIU 内存接口单元
+0x0140 CRU 代码替换单元
+0x0180 DMA 直接内存访问引擎
+0x0200 ICU 中断控制单元
+0x0280 BTDMP 音频输入/输出端口

从这张表可以看到,CRU 位于 MIU(+0x0100)与 DMA(+0x0180)之间,独占 +0x0140 起的一段子区域。CRU 的全称是 Code Replacement Unit(代码替换单元),其核心作用由 CRU 文档 末尾的注释点明:OFFSET_L、OFFSET_H 保存的是"要被替换的程序地址"(address of the program to replace)。也就是说,CRU 是一组可以让调试器或固件指定"程序空间中某个地址应当被替换"的寄存器文件,属于典型的调试/仿真辅助外设,与同一地址段附近的 OCEM(片上仿真模块) 职责相邻(OCEM 负责程序流追踪、程序地址断点、单步等调试能力)。

二、CRU 的 MMIO 布局逐字段解析

CRU 文档 给出的 MMIO 布局如下(原文档内容完整继承):

(N = 0..14)
+-----------#---+---+---+---#---+---+---+---#---+---+---+---#---+---+---+---#
|+0x0140+N*4|                            OFFSET_L                           |
+-----------#---+---+---+---#---+---+---+---#---+---+---+---#---+---+---+---#
|+0x0142+N*4|   |                                   |           ?           |
+-----------#---+---+---+---#---+---+---+---#---+---+---+---#---+---+---+---#
|+0x017C    |                            OFFSET_L                           |
+-----------#---+---+---+---#---+---+---+---#---+---+---+---#---+---+---+---#
|+0x017E    |       |   |                           |           ?           |
+-----------#---+---+---+---#---+---+---+---#---+---+---+---#---+---+---+---#

OFFSET_L, OFFSET_H: address of the program to replace

1. 两组寄存器块的结构差异

  • 数组块(N = 0..14):这是 15 组结构相同、以 4 字为步进排布的寄存器组。每组包含两个寄存器:
    • +0x0140 + N*4:OFFSET_L——程序地址的低字(低位字);
    • +0x0142 + N*4:OFFSET_H——程序地址的高字(高位字),其中高字寄存器的位域并不完全明确:图中可见开头有若干保留位、中间是一段较宽的字段、末尾还有一个标为 ? 的未知字段。
  • 单块(+0x017C / +0x017E):在数组之后,还有一对独立的 OFFSET_L / OFFSET_H 寄存器,其布局与数组中的每一组相同(+0x017E 同样含保留位与 ? 未知字段)。由于文档未给出进一步说明,它可能是"当前生效"的替换槽位或另一用途的独立替换地址寄存器——这一点只能从布局推测,原文档并未确认。

2. 一个值得注意的地址算术观察

把两个块放在一起看,它们恰好严丝合缝地填满了 CRU 子区域:

  • 15 组寄存器按 N*4 字步进排列:0x140 到 0x17B(第 15 组占 0x178/0x17A),共 60 个字;
  • 随后 0x17C/0x17E 是最后一对寄存器;
  • 整个 CRU 子区域从 0x140 到 0x17F 共 64 个字(0x40),下一位 0x180 正好是 DMA 子区域的起点。

这说明 CRU 的寄存器文件与 MMIO 总览 中子区域边界(+0x0140 起、+0x0180 止)完全吻合,也印证了文档中"外设文档里的寄存器地址都是相对 MMIO 区域起点的偏移"这一约定。

3. 偶数地址规则

MMIO 总览 明确指出:"MMIO 寄存器通常只使用偶数地址,这似乎是硬件层面的优化(MIU 中存在一个可强制启用该规则的寄存器)"。CRU 的寄存器全部落在偶数偏移(0x140、0x142、0x17C、0x17E),完全符合这一约定;与之形成对照的是 OCEM 文档特别注明"这是唯一使用奇数地址寄存器的 MMIO 区域"。理解这一点有助于在阅读位图时避免把相邻奇数地址误当作独立寄存器。

4. 字段语义总结

寄存器 字段 语义
OFFSET_L 16 位 待替换程序地址的低位字
OFFSET_H 高位字(含保留位与未知 ? 字段) 待替换程序地址的高位字/页信息
+0x017E 中的 ? 未知 文档未确认其含义

三、MMIO 地址空间与路由机制:CRU 寄存器如何被读写

要真正"用得上"上述寄存器地址,需要理解 MMIO 地址在模拟器中的换算路径。这一部分可由仓库源码直接印证。

1. 地址空间与 MMIO 边界

在 内存接口头文件 中,MemoryInterfaceUnit 定义了:

u16 mmio_base = 0x8000;
static constexpr u16 MMIOSize = 0x0800;

bool InMMIO(u16 addr) const {
    return addr >= mmio_base && addr < mmio_base + MMIOSize;
}
u16 ToMMIO(u16 addr) const {
    ASSERT(z_page == 0);
    return (addr - mmio_base) & (MMIOSize - 1);
}

这与 MIU 文档 中 MMIOBASE("MMIO base address, with resolution of 512-word")的描述相互印证:MMIO 基址可通过 MIU 寄存器配置,默认是 0x8000。因此 CRU 的绝对地址 = MMIO 基址 + 文档中的相对偏移,例如在默认基址下,OFFSET_L[0] 的绝对地址是 0x8000 + 0x140 = 0x8140。

2. 数据读写路径与镜像

内存接口实现 展示了两条通往 MMIO 的路径:

  • 数据空间路径:DataRead/DataWrite 会先调用 InMMIO 判断地址是否落在 MMIO 区间;若是则调用 ToMMIO 换算偏移后交给 MMIORegion::Read/Write;否则 ConvertDataAddress 将 16 位数据地址换算为共享内存中的真实地址(分页模式见 MIU 文档)。
  • MMIO 专用路径:MMIORead/MMIOWrite 面向 ARM9 侧直接访问 DSP IO 端口,代码注释引用了 GBATEK 的说法,指出这类访问是镜像的——mmio->Read(address & (MMIOSize - 1)),即高位被掩码截断后仍能命中同一寄存器(memory_interface.cpp)。

3. 寄存器单元的模拟实现

MMIO 实现 用 std::array<Cell, 0x800> 表示整个 0x0800 字寄存器文件,每个 Cell 持有 set/get 回调:

class MMIORegion::Impl {
public:
    std::array<Cell, 0x800> cells{};
    ...
};

对于未显式注册的单元,默认 Cell 会打印 MMIO: cell %04X set/get 调试信息。这意味着我们可以在运行时通过日志观察到对未实现外设寄存器的每次访问——这是验证 CRU 行为最直接的手段(见下文)。

四、CRU 在 Teakra 模拟器中的实现状态与程序取指路径

1. 当前实现状态:寄存器未显式接线

通读 mmio.cpp 的构造函数可以发现:Timer、APBP、AHBM、MIU、DMA、ICU、BTDMP 等外设的寄存器都被逐一绑定到了对应实现对象,但 +0x0140..+0x017F(即整个 CRU 子区域)没有任何 impl->cells[...] 注册语句。从源码结构可以推断:CRU 寄存器目前落入了默认 Cell(带调试打印、仅存储值),尚未接入任何"程序替换"的仿真逻辑。这也与 CRU 文档本身的简短程度一致——该外设在仓库中属于已记录布局、未实现语义的状态。

2. 程序替换的语义落点:取指路径

CRU 要替换的是"程序地址",而处理器的程序空间访问集中在取指与 movp/movd 等指令上。以解释器取指为例(interpreter.h):

u16 opcode = mem.ProgramRead((regs.pc++) | (regs.prpage << 18));

程序地址由 16 位 PC 与程序页(prpage,左移 18 位)拼接而成;ProgramRead 最终落在 memory_interface.cpp:

u16 MemoryInterface::ProgramRead(u32 address) const {
    return shared_memory.ReadWord(address);
}

结合这两处实现可以推断:所谓"被替换的程序地址"如果生效,其高位(OFFSET_H)很可能对应页/高位地址信息,而低位(OFFSET_L)对应页内偏移——即 OFFSET_L + OFFSET_H 共同构成一个完整的程序空间地址。这一推断尚未得到文档或实现的直接确认,仅基于取指地址构成方式。

3. 与 OCEM 调试外设的关系

从功能定位上看,CRU 与 OCEM(PAB1~3 程序地址断点、PFT 程序流追踪、单步使能等)同属调试/仿真辅助设施:OCEM 负责"拦截/观察"程序的执行,而 CRU 负责"替换"程序地址处的代码。两者在真实硬件上常配合用于调试器注入断点指令或补丁代码。这一关联属于合理推断——仓库文档并未给出 OCEM 与 CRU 的显式交互说明。

五、在 melonDS 中如何接触 CRU

1. Teakra LLE 的接入

melonDS 通过 Teakra 库 实现 DSi DSP 的低级仿真(LLE)。在 DSi_DSP.cpp 的 StartDSPLLE 中:

  • 创建 Teakra::Teakra 实例作为 DSPCore,其 GetID() 返回 0x7EAC0000(teakra.h),melonDS 据此在 SetDSPProgram 中识别固件/ucode 的 DSP 类型(DSi_DSP.cpp);
  • 通过 SetSharedMemoryCallback / SetAHBMCallback 将 DSP 的共享内存读写与 AHB 总线访问桥接到 DSi 的 NWRAM 与 ARM9 内存空间(对应 DSPRead16/DSPWrite16 及 ARM9Read/Write 系列,见 DSi_DSP.cpp)。

2. 从 ARM9 侧观察 CRU 寄存器

Teakra 对外暴露了 ProgramRead、DataRead、MMIORead/MMIOWrite 等 API(teakra.h)。从 ARM9 侧观察 CRU 时,可以预期其访问路径为:ARM9 → MMIORead(绝对地址) → 按 MMIOSize - 1 镜像掩码 → mmio->Read(相对偏移)。由于 CRU 寄存器未显式实现,读写会触发默认 Cell 的调试打印(MMIO: cell 0140 set/get 之类),这是目前模拟器中观察 CRU 活动的最直接手段。

3. 动手验证建议

  • 在 Teakra 的 mmio.cpp 中,Cell 的默认构造已包含 std::printf("MMIO: cell %04X set/get\n", index),开启调试日志后运行任意使用 DSP 的 DSi 软件(如 "Nintendo DSi Sound",见 Teakra 项目说明),观察是否有 0x140~0x17F 范围内的访问日志;
  • 对照 CRU 文档 与 MMIO 总览 的偏移约定,自行计算默认基址(0x8000)下的绝对地址,在 MMIORead/MMIOWrite 上设断点即可截获具体访问;
  • 如需在模拟器中补全 CRU 语义,可仿照 mmio.cpp 中其他外设的 Cell::BitFieldCell / Cell::RefCell 接线方式(如 MIU 寄存器),把 OFFSET_L/OFFSET_H 绑定到程序替换逻辑——注意仓库为只读,仅作理解与本地实验参考。

六、总结

CRU 是 XpertTeak DSP 中负责"程序替换"的外设,其 MMIO 布局由 15 组 OFFSET_L/OFFSET_H 寄存器数组(+0x0140+N*4,N=0..14)与一组独立寄存器(+0x017C/+0x017E)构成,寄存器偏移均为相对 MMIO 起点的字数偏移,默认基址为 0x8000。在 melonDS 的 Teakra LLE 实现中,CRU 子区域尚未显式接线,读写会落入带调试日志的默认单元;其最终生效需要与解释器的取指路径(ProgramRead)以及 OCEM 调试外设协同。理解 CRU 的关键,在于把 CRU 文档 的位图与 MMIO 总览、内存接口实现 的地址路由代码对照阅读。

进一步阅读:整个 DSP 外设体系可参考 Teakra 源码文档索引(处理器、寄存器、AHBM、APBP、DMA、ICU、定时器、MIU 等);在 melonDS 中的完整集成入口见 DSi_DSP.cpp。

登录后查看全文
melonDS