剖析 melonDS 中 Teakra(XpertTeak DSP)的 MMIO 子系统:寄存器布局、Cell 抽象与地址镜像机制
剖析 melonDS 中 Teakra(XpertTeak DSP)的 MMIO 子系统:寄存器布局、Cell 抽象与地址镜像机制
导读
本文以 src/teakra/src/mmio.md 为核心,系统讲解 melonDS 内嵌的 Teakra(即 DSi/3DS 所使用的 XpertTeak DSP)中内存映射 I/O(MMIO)子系统的完整设计:MMIO 区域如何从 16 位数据地址空间中划分出来、各外围设备如何按子区域挂接寄存器、以及 mmio.cpp 中 MMIORegion/Cell 抽象是如何把寄存器读写翻译成外设行为的。读完本文,你将掌握 Teakra MMIO 的地址布局、镜像规则、基址重定位机制,并能对照源码逐区块读懂 Timer、APBP、AHBM、MIU、DMA、ICU、BTDMP 等外设的寄存器定义。
一、MMIO 是什么:DSP 与外围设备之间的唯一窗口
在 Teakra 架构中,处理器核心与各类外设(以及间接地外部硬件,如 DSi/3DS 的 ARM CPU)之间的通信全部通过 MMIO 完成。这一点在 mmio.md 中开宗明义:
The core processor communicates with peripherals (and indirectly external hardware such as CPU) via MMIO.
MMIO 区域的规模与位置非常特殊:
- 区域大小:占据 16 位地址空间中一块 0x0800 字(word) 的区域,即 2048 个 16-bit 存储单元;
- 默认基址:初始位于 0x8000;
- 基址可重定位:MMIO 区域的位置可以通过 MIU(Memory Interface Unit,内存接口单元)中的寄存器配置——而 MIU 自身又是通过 MMIO 访问的外设,形成了"用 MMIO 配置 MMIO"的自指关系(详见本文第六节);
- 偶数地址规则:MMIO 寄存器通常只使用偶数地址。mmio.md 推测这是硬件层面的一种优化,因为 MIU 中存在一个可强制该规则的寄存器(对应 miu.md 中
ZSP位的描述:"1 to enable single access mode, where the Z data memory space's Core/DMA addresses use only the Z-even address bus")。
从软件仿真的角度理解:MMIO 本质上是在 DSP 的数据内存地址空间中"截取"了一块专用区间,凡是落入该区间的数据读写都会被路由到 MMIORegion,而不是访问普通数据内存。这一判定逻辑实现在 memory_interface.h 中:
bool InMMIO(u16 addr) const {
return addr >= mmio_base && addr < mmio_base + MMIOSize;
}
u16 ToMMIO(u16 addr) const {
ASSERT(z_page == 0);
// according to GBATek ("DSi Teak I/O Ports (on ARM9 Side)"), these are mirrored
return (addr - mmio_base) & (MMIOSize - 1);
}
其中 MMIOSize = 0x0800 与 mmio_base = 0x8000 均定义于 memory_interface.h。ToMMIO 中的 & (MMIOSize - 1) 意味着:实际读写发生时,偏移会被"回卷"到 0x800 字窗口内,形成镜像效果(详见第五节)。
二、外围设备子区域映射表(完整继承自 mmio.md)
mmio.md 给出了每个外设对应的 MMIO 子区域清单。需要特别注意:各外设文档(timer.md、sio.md、apbp.md 等)中给出的寄存器地址,都是相对于 MMIO 区域起始地址的偏移量(offset),而非绝对地址。实际绝对地址 = MMIO 基址 + 偏移,例如默认基址下 Timer0 配置寄存器位于 0x8020。
| 偏移(offset) | 外设 | 说明 |
|---|---|---|
+0x0000 |
(未知) | 未确定用途,源码中也未实现 |
+0x0004 |
JAM | — |
+0x0010 |
GLUE | 胶合逻辑 |
+0x0020 |
Timer | 定时器(含 Timer0/Timer1 两组) |
+0x0050 |
SIO | 串行 IO |
+0x0060 |
OCEM | 片上仿真模块(On-chip Emulation Module) |
+0x0080 |
PMU | 电源管理单元(Power Management Unit) |
+0x00C0 |
APBP | 高级外设总线端口(Advanced Peripheral Bus Port?) |
+0x00E0 |
AHBM | 高级高性能总线主机(Advanced High Performance Bus Master) |
+0x0100 |
MIU | 内存接口单元(Memory Interface Unit) |
+0x0140 |
CRU | 代码替换单元(Code Replacement Unit) |
+0x0180 |
DMA | 直接内存访问 |
+0x0200 |
ICU | 中断控制单元(Interrupt Control Unit) |
+0x0280 |
BTDMP | 缓冲时分复用端口(Buffered Time Division Multiplexing Port) |
值得注意的是,上述区间并不连续覆盖 0x800 字的全部空间:各子区域之间存在大量间隙(例如 0x0000 之后直接跳到 0x0004,Timer 区 0x0020–0x003F 之后直到 0x0050 才是 SIO)。这些间隙在源码中以"默认 Cell"或完全未初始化的存储单元存在。
三、源码实现:MMIORegion 与 Cell 抽象
3.1 类的接口
mmio.h 定义了 MMIORegion 类,其构造函数一次性接收所有外设对象的引用,将各外设的寄存器读写行为绑定到对应的 MMIO 单元上:
class MMIORegion {
public:
MMIORegion(MemoryInterfaceUnit& miu, ICU& icu, Apbp& apbp_from_cpu, Apbp& apbp_from_dsp,
std::array<Timer, 2>& timer, Dma& dma, Ahbm& ahbm, std::array<Btdmp, 2>& btdmp);
~MMIORegion();
u16 Read(u16 addr); // not const because it can be a FIFO register
void Write(u16 addr, u16 value);
private:
class Impl;
std::unique_ptr<Impl> impl;
};
注意 Read 的注释:它不是 const 方法,因为部分寄存器是 FIFO 寄存器(如 APBP 的命令/应答通道、BTDMP 的收发端口),读操作会产生副作用(消费数据、清除"数据就绪"标志等)。
3.2 底层存储:0x800 个 Cell
mmio.cpp 中 MMIORegion::Impl 持有 std::array<Cell, 0x800>,即与 MMIO 区域大小一一对应的 2048 个存储单元,并在构造时为每个 Cell 记录其索引(index),用于调试日志输出。
3.3 Cell 的五种形态
Cell(mmio.cpp)封装了一对读写回调:set(value) 与 get()。所有外设寄存器行为最终都归结为对某个 Cell 的 set/get 绑定。具体有五种构造形态:
| 形态 | 工厂函数 | 行为 |
|---|---|---|
| 默认 Cell | Cell() |
持有独立 u16 存储,读写时打印 MMIO: cell %04X set/get 调试日志 |
| 常量 Cell | Cell::ConstCell(u16) |
只能读回固定常量,写入时产生 NoSet 警告(用于芯片检测寄存器) |
| 引用 Cell | Cell::RefCell(u16&) |
直接读写外部变量(用于计数器、地址、配置位等状态量) |
| 镜像 Cell | Cell::MirrorCell(Cell*) |
读写转发到另一个 Cell(用于 CPU 侧/ DSP 侧共享寄存器) |
| 位域 Cell | Cell::BitFieldCell(slots) |
按位域拆分 16 位值,每位域各自绑定 set/get 回调 |
位域 Cell 是理解整个 MMIO 的关键:写入时把 16 位值按 pos/length 拆成若干位段分发给对应回调,读取时再把各位域回调的结果拼回 16 位。BitFieldSlot::RefSlot 则提供"直接绑定一个 u16 变量"的便捷形式(mmio.cpp)。
此外还有两个"哑回调" mmio.cpp:NoSet 在被写入时打印 Warning: NoSet on ...,NoGet 在被读取时打印 Warning: NoGet on ... 并返回 0。它们用于标记"硬件上只读/只写"的寄存器。
3.4 读写入口
u16 MMIORegion::Read(u16 addr) { return impl->cells[addr].get(); }
void MMIORegion::Write(u16 addr, u16 value) { impl->cells[addr].set(value); }
(mmio.cpp)——注意这里的 addr 已经是经过 ToMMIO/镜像掩码转换后的、0x0000–0x07FF 范围内的相对偏移。这再次印证:上节映射表中的偏移就是 cells 数组的下标。
四、地址判定与镜像:MMIO 如何从数据总线中被识别
Teakra 的 16 位数据地址空间既承载普通数据内存,也承载 MMIO。区分逻辑在 memory_interface.cpp 的 DataRead/DataWrite 中:
u16 MemoryInterface::DataRead(u16 address, bool bypass_mmio) {
if (memory_interface_unit.InMMIO(address) && !bypass_mmio) {
ASSERT(mmio != nullptr);
return mmio->Read(memory_interface_unit.ToMMIO(address));
}
u32 converted = memory_interface_unit.ConvertDataAddress(address);
return shared_memory.ReadWord(converted);
}
要点:
- 当地址落在
[mmio_base, mmio_base + 0x800)区间内,且未显式bypass_mmio时,读写被路由到 MMIO; bypass_mmio参数允许某些内部访问(如 DMA 的 MMIO 空间直传)绕过该判定;ToMMIO把绝对地址减去基址并按 0x800 取模,得到 Cell 数组下标;- 落在 MMIO 区间之外的数据地址,则经
ConvertDataAddress换算成实际物理内存地址(页模式映射到数据内存,见 memory_interface.h)。
4.1 ARM 侧的镜像访问
MMIORead/MMIOWrite(memory_interface.cpp)是另一条路径,供 CPU(ARM9)侧访问 DSP 的 I/O 端口使用。注释引用了 GBATek 的"DSi Teak I/O Ports (on ARM9 Side)":ARM 侧访问这些端口时,地址也按 MMIOSize - 1 掩码镜像,因此只要落在对应窗口内,地址的高位被忽略:
u16 MemoryInterface::MMIORead(u16 address) {
ASSERT(mmio != nullptr);
// according to GBATek ("DSi Teak I/O Ports (on ARM9 Side)"), these are mirrored
return mmio->Read(address & (MemoryInterfaceUnit::MMIOSize - 1));
}
这种镜像设计意味着:DSP 侧的"相对偏移"与 ARM 侧看到的地址窗口共享同一套 0x800 字的寄存器空间,两侧通过同一批 Cell 交互。
五、配置 MMIO 基址:MIU 与 MMIOBASE
MMIO 区域的基址由 MIU 的 MMIOBASE 字段控制。miu.md 给出了完整定义:
MMIOBASE: MMIO base address, with resolution of 512-word.
即 MMIOBASE 的粒度是 512 字(地址需按 512 字对齐)。源码侧,memory_interface.h 将 mmio_base 的复位值设为 0x8000,与 mmio.md 的"initially starting at 0x8000"完全一致。
在 mmio.cpp 中,MMIOBASE 被绑定到偏移 +0x11E:
impl->cells[0x11E] = Cell::RefCell(miu.mmio_base); // MIU_MMIOBASE
也就是说,DSP 固件可以在运行时通过写入 MMIOBASE 寄存器(绝对地址 0x811E)移动整个 MMIO 窗口的位置,这正解释了 mmio.md 中"MMIO 区域的位置可通过 MIU 配置(……是的,通过 MMIO)"这句话——移动基址的寄存器本身就在 MMIO 里。
MIU 区块其余已实现的寄存器还包括(mmio.cpp):
| 偏移 | 寄存器 | 绑定 |
|---|---|---|
+0x10E |
MIU_XPAGE | miu.x_page |
+0x110 |
MIU_YPAGE | miu.y_page |
+0x112 |
MIU_ZPAGE | miu.z_page |
+0x114 |
MIU_PAGE0CFG | x_size[0](位 0–5)/ y_size[0](位 8–13) |
+0x116 |
MIU_PAGE1CFG | x_size[1] / y_size[1] |
+0x11A |
页面控制 | 含 PAGEMODE(位 6,绑定 miu.page_mode),PP/TESTP/INTP/ZSINGLEP 位未绑定 |
miu.md 还说明:XPAGE0CFG 在页模式 0 下表示 X 内存第 0 页的大小、在页模式 1 下表示所有页的大小;ZPAGE 在页模式 0 下是绝对数据内存页、在页模式 1 下是 Z 内存页。这些语义与 ConvertDataAddress 中 page_mode 的分支逻辑一一对应(memory_interface.h)。
六、核心外设寄存器逐区块详解
以下按 mmio.md 的子区域划分,结合各外设文档与 mmio.cpp 的绑定代码,逐一展开。所有地址均为 MMIO 相对偏移。
6.1 Timer:+0x0020(Timer0)与 +0x0030(Timer1)
timer.md 给出了 Timer0 的完整寄存器布局(Timer1 布局相同、偏移整体 +0x10):
| 偏移 | 寄存器 | 位域/含义 |
|---|---|---|
+0x00 |
TIMERx_CFG | TS(位 0–1,时钟分频 1/2/4/16)、CM(位 2–4,计数模式)、TP(位 6,输出极性)、CT(位 7,清除输出)、PC(位 8,暂停)、MU(位 9,更新计数寄存器)、RES(位 10,重启)、BP(位 11,断点请求)、CS(位 12,时钟源)、GP/TM(位 13–15) |
+0x02 |
TIMERx_EW | 写 1 触发事件计数递减 / 看门狗重载 |
+0x04 |
START_COUNT_L | 计数器重启值低 16 位 |
+0x06 |
START_COUNT_H | 计数器重启值高 16 位 |
+0x08 |
COUNTER_L | 当前计数值低 16 位 |
+0x0A |
COUNTER_H | 当前计数值高 16 位 |
+0x0C |
PWM_COUNTER_L | PWM 计数器重启值低 16 位 |
+0x0E |
PWM_COUNTER_H | PWM 计数器重启值高 16 位 |
其中 CM 计数模式包括:0 单次计数、1 自动重启、2 自由运行、3 事件计数、4/5/6 三种看门狗模式。TS 分频:0 为 /1、1 为 /2、2 为 /4、3 为 /16。
mmio.cpp 的绑定代码与之一一对应,两个定时器按 i * 0x10 步进:
for (unsigned i = 0; i < 2; ++i) {
impl->cells[0x20 + i * 0x10] = Cell::BitFieldCell({
BitFieldSlot::RefSlot(0, 2, timer[i].scale), // TS
BitFieldSlot::RefSlot(2, 3, timer[i].count_mode), // CM
BitFieldSlot{6, 1, {}, {}}, // TP
BitFieldSlot{7, 1, {}, {}}, // CT
BitFieldSlot::RefSlot(8, 1, timer[i].pause), // PC
BitFieldSlot::RefSlot(9, 1, timer[i].update_mmio), // MU
BitFieldSlot{10, 1, &timer, i { if (v) timer[i].Restart(); }, []() { return 0; }}, // RES
...
});
impl->cells[0x22 + i * 0x10].set = &timer, i { if (v) timer[i].TickEvent(); }; // EW
impl->cells[0x24 + i * 0x10] = Cell::RefCell(timer[i].start_low); // SCL
impl->cells[0x26 + i * 0x10] = Cell::RefCell(timer[i].start_high); // SCH
impl->cells[0x28 + i * 0x10] = Cell::RefCell(timer[i].counter_low); // CCL
impl->cells[0x2A + i * 0x10] = Cell::RefCell(timer[i].counter_high); // CCH
impl->cells[0x2C + i * 0x10] = Cell(); // SPWMCL
impl->cells[0x2E + i * 0x10] = Cell(); // SPWMCH
}
可以看到,写 RES(重启)位会触发 timer[i].Restart(),写 EW 会触发 TickEvent()——这正是"寄存器写操作触发硬件动作"的典型体现。
6.2 APBP:+0x00C0 —— CPU 与 DSP 通信的中枢
apbp.md 说明 APBP 主要包含 3 对对称数据通道和 1 对 16 位对称信号量通道:
- 数据通道(REPLY0..2 / CMD0..2,偏移 +0x00C0/+0x00C2/+0x00C4/+0x00C6/+0x00C8/+0x00CA):一方写入数据通道即置位"数据就绪"位并(若允许)向另一方触发中断;另一方读取通道寄存器即自动清除就绪位;新数据覆盖旧数据时会再次触发中断。
- 信号量通道(SET_SEMAPHORE / MASK_SEMAPHORE / ACK_SEMAPHORE / GET_SEMAPHORE,偏移 +0x00CC/+0x00CE/+0x00D0/+0x00D2):SET 与 ACK 改变信号量值,
S位即时反映(GET_SEMAPHORE & ~MASK_SEMAPHORE)是否非零,S的 0→1 跳变触发对方中断。 - 配置/状态寄存器:+0x00D4(
END大小端、CI0..CI2中断屏蔽)、+0x00D6(R0..R2/C0..C2数据就绪、S)、+0x00D8(CPU 侧DSP_PSTS的镜像,含RTU/WTU/PRS/RFL/RNE/WFL/WEM)。
mmio.cpp 的实现非常直白:命令/应答通道把 Cell 绑定到 apbp_from_dsp 的 SendData/PeekData 与 apbp_from_cpu 的 RecvData;信号量寄存器则绑定 SetSemaphore/GetSemaphore/MaskSemaphore/ClearSemaphore。其中 +0x00D8 正是"CPU 侧寄存器 DSP_PSTS 的镜像"(源码注释原文:This register is a mirror of CPU side register DSP_PSTS),对应 apbp.md 的 S' 等位域。
6.3 AHBM:+0x00E0 —— 访问外部内存(DSi/3DS 主内存)的桥
ahbm.md 描述了三个 6 字步进的通道配置块:
| 偏移(N=0,1,2) | 寄存器 | 说明 |
|---|---|---|
+0x00E0 |
状态 | RNE:突发队列非空即置 1;应用需等待所有位为 0 再连接 DMA |
+0x00E2+N*6 |
通道配置 | BURST(0:x1 / 1:x4 / 2:x8)、TYPE(0:8 位 / 1:16 位 / 2:32 位)、R 位 |
+0x00E4+N*6 |
方向 | W(0:从外部内存读 / 1:写)、E(应用恒置 1) |
+0x00E6+N*6 |
DMA 连接 | D0..D7:置 1 表示连接到 DMA 通道 0..7 |
mmio.cpp 中,+0x0E0 的 get 绑定 Ahbm::GetBusyFlag(写被 NoSet 拒绝);三个通道配置分别绑定 Set/GetBurstSize(位 1–2)、Set/GetUnitSize(位 4–5)、Set/GetDirection(位 8)、Set/GetDmaChannel(位 0–7)。
6.4 MIU:+0x0100 —— 内存接口单元
详见第五节。除 MMIOBASE 与页配置外,miu.md 还定义了 Z 区等待状态(ZxWS)、X/Y 区等待状态、断点中断时的程序页跳转(INP)、观测模式(OBS/OBSMOD)、总线极性位(PRD/PZS/PXS/PXT/PZT/PZW/PZR)等。注意 miu.md 特别注明:该寄存器定义"提取自 Lauterbach 的 Teak 调试器,部分未经测试"——在源码中这些未测试寄存器大多以注释形式保留(mmio.cpp 中被注释掉的 WSCFG/OBSCFG/POLARITY 等)。
6.5 DMA:+0x0180 —— 三维阵列搬运引擎
dma.md 定义了完整布局:
- 通道使能/状态区:
+0x0184的C0..C7(通道使能)、+0x018C的E0..E7(传输结束标志,源码实现为读返回0xFFFF的常量 Cell,见 mmio.cpp)、+0x018E/+0x0190的S0..S7槽位; - 活动通道绑定区:
+0x01BE的CHANNEL选择当前寄存器组绑定的通道,+0x01C0..+0x01DE依次为SRC_ADDR_LOW/HIGH、DST_ADDR_LOW/HIGH、SIZE0/1/2、SRC_STEP0/1/2、DST_STEP0/1/2、空间与模式寄存器(SRC_SPACE/DST_SPACE:0=DSP 主内存、1=MMIO、7=AHBM/外部内存;DWM位:0=16 位字、1=32 位双字)。
dma.md 用一个三维数组复制示例说明 SIZE/STEP 语义(SRC_ADDR=0, SIZE0=3, SIZE1=5, SIZE2=2, 步进 2/1/7),并指出 SIZEx=0 与 1 效果相同;双字模式下 SIZE0 按"1 个 32 位元素计 2"折算,且地址自动对齐到 32 位边界;外部内存(如 3DS FCRAM)为 8 位寻址,因此 STEP=1 在 FCRAM 上步进 8 位、在 DSP 内存上步进 16 位。
mmio.cpp 将上述寄存器逐一绑定到 Dma 的 EnableChannel、ActivateChannel、SetAddrSrcLow/High、SetSize0/1/2、SetSrcStep0/1/2、SetDstStep0/1/2、SetSrcSpace/SetDstSpace/SetDwordMode、SetY/SetZ 等方法。
6.6 ICU:+0x0200 —— 16 路 IRQ 到 4 路核心中断的翻译器
icu.md 的布局:
| 偏移 | 寄存器 | 说明 |
|---|---|---|
+0x0200 |
IP0..IPF | IRQ 挂起标志 |
+0x0202 |
IA0..IAF | IRQ 应答,写 1 清除挂起 |
+0x0204 |
IT0..ITF | 软件触发 IRQ |
+0x0206/08/0A |
I00..I2F | 将各 IRQ 连接到核心中断 0/1/2 |
+0x020C |
IV0..IVF | 连接到核心向量中断 |
+0x020E/0210 |
TPx/PLx | 触发模式(脉冲/粘滞)、极性 |
+0x0212+N*4 |
VADDR_H + VIC | 向量中断处理地址高位 + 上下文切换使能 |
+0x0214+N*4 |
VADDR_L | 向量中断处理地址低位 |
ICU 的职责是把 16 路 IRQ 信号翻译成 4 路处理器中断信号(int0/int1/int2/vint)。已知的 IRQ 信号连线(icu.md 附图):DMA→IRQ0、APBP→IRQ1、ICU?→IRQ2、BTDMP→IRQ3、TIMER0→IRQ4、TIMER1→IRQ5。icu.md 还特别提到一个 3DS 游戏中的典型用例:IRQ0 被用作多线程的调度重试机制——线程让出 CPU 时触发软件中断 IRQ0,借此切换线程上下文。
mmio.cpp 中,+0x200 绑定 ICU::GetRequest,+0x202 绑定 Acknowledge,+0x204 绑定 Trigger,+0x206/208/20A 分别绑定三个中断使能组 SetEnable(0/1/2),+0x20C 绑定 SetEnableVectored;16 组向量寄存器(+0x212+N*4 的高位向量 + 上下文切换位、+0x214+N*4 的低位向量)按 4 字步进展开。
6.7 BTDMP:+0x0280 —— 音频输入/输出端口
teakra/src/README.md 将 BTDMP 描述为"audio input/output ports",即 DSi/3DS 上 DSP 音频流的收发通道。两组端口(i=0,1)按 0x80 步进,mmio.cpp 的绑定包括:
- 接收侧:
+0x282+i*0x80(ReceiveClockConfig)、+0x29E(ReceiveEnable)、+0x2C0(ReceiveFull/ReceiveEmpty 状态位)、+0x2C4(Receive,读即取走一个样本)、+0x2C8(ReceiveFlush); - 发送侧:
+0x2A2(TransmitClockConfig)、+0x2BE(TransmitEnable)、+0x2C2(TransmitFull/TransmitEmpty)、+0x2C6(Send,写即推入一个样本)、+0x2CA(TransmitFlush)。
接收/发送状态位都是"只读 + 动态计算"的位域 Cell(get 绑定 GetReceiveFull/GetReceiveEmpty 等),体现了 FIFO 型寄存器的特性。
6.8 特殊的杂项 Cell
除上述外设外,mmio.cpp 还注册了一个芯片检测寄存器:
impl->cells[0x01A] = Cell::ConstCell(0xC902); // chip detect
即偏移 +0x01A 恒读回 0xC902(写入会被 NoSet 拒绝),可用于固件确认 DSP 型号/版本。
七、未实现区与"预留"寄存器
从 mmio.cpp 可以看到,0x800 个 Cell 中只有少量被显式赋值。未被赋值的 Cell 走默认构造:读写都会打印 MMIO: cell %04X set/get 日志并读写内部哑存储。这意味着在模拟器当前状态下:
- mmio.md 映射表中
+0x0000(未知)、JAM(+0x0004)、GLUE(+0x0010)、SIO(+0x0050)、OCEM(+0x0060)、PMU(+0x0080)、CRU(+0x0140)等外设尚未在源码中实现具体寄存器行为; - 已注释保留的 MIU 寄存器(
MIU_WSCFG0/1、MIU_Z0..Z3WSCFG、MIU_OFFPAGECFG、MIU_DLCFG、MIU_OBSCFG、MIU_POLARITY)与 DMA/ICU 中标注?的位域,属于"文档有记载但模拟器未实现"的部分(miu.md 同样标注"Some of them are untested")。
八、与 melonDS 的集成:Savestate
MMIO 本身不直接参与存档,但它所绑定的外设状态需要随存档保存。以 MIU 为例,memory_interface.cpp 的 MemoryInterfaceUnit::DoSavestate 使用 melonDS 的 Savestate 框架、以 "TKmi" 区块保存 x_page/y_page/z_page/x_size/y_size/page_mode/mmio_base 等字段:
void MemoryInterfaceUnit::DoSavestate(melonDS::Savestate *file) {
file->Section("TKmi");
file->Var16(&x_page);
...
file->Var16(&mmio_base);
}
由于 mmio_base 也随存档持久化,加载存档后 MMIO 区域的位置会恢复到保存时的配置,这保证了运行时重定位 MMIO 基址的固件在存档/读档循环中行为一致。
九、调试与扩展建议
结合源码与文档,在 melonDS 中排查 DSP/外设交互问题时可以参考以下线索:
- NoSet / NoGet 警告:若日志中出现
Warning: NoSet/NoGet on ...,说明访问了硬件上只读/只写(或源码中未实现对应方向)的寄存器。例如对 chip detect 寄存器写入、对 AHBM 忙标志写入都会触发该警告; - 默认 Cell 的日志:访问未实现寄存器会打印
MMIO: cell %04X set/get,可据此定位固件触碰了哪些尚未实现的偏移(如 SIO/OCEM/PMU/CRU 区块); - 地址换算:文档中的偏移必须加上当前
mmio_base(默认 0x8000)才是 DSP 侧绝对地址;ARM 侧访问则按 0x800 掩码镜像,两者的 Cell 下标相同; - 镜像判定:
InMMIO要求addr >= mmio_base && addr < mmio_base + MMIOSize,而ToMMIO断言z_page == 0——即在页模式/分页配置下访问 MMIO 前需确保 Z 页为 0,这是硬件手册层面的约束,代码中通过ASSERT强制。
十、总结
Teakra 的 MMIO 是一个"小而完整"的外设总线窗口:0x800 字的空间、默认 0x8000 的基址、可由 MIU 在 512 字粒度上重定位、偶数地址规则、ARM 侧按窗口掩码镜像,再加上 mmio.cpp 中统一抽象的 Cell(常量/引用/镜像/位域/默认五种形态),构成了 DSP 与 Timer、APBP、AHBM、MIU、DMA、ICU、BTDMP 等全部外设之间的唯一通信桥梁。理解这套映射,就等于拿到了读懂 DSi/3DS DSP 固件中所有 I/O 操作的地图;而 mmio.md 与 miu.md、apbp.md、dma.md、icu.md、timer.md、ahbm.md 等配套文档,以及 mmio.cpp 的绑定代码,正是逐位确认这些寄存器语义的权威依据。