- 虚拟化
- 图形学
- 桌面应用
【免费下载链接】melonDS
DS emulator, sorta
本文以 melonDS 仓库内嵌的 Teakra DSP 仿真子项目的官方设计文档 src/teakra/src/icu.md 为核心骨架,结合 icu.h、mmio.cpp 与 teakra.cpp 等源码实现,完整讲解中断控制器(Interrupt Control Unit, ICU)的 MMIO 寄存器布局、16 路 IRQ 信号的来源与路由、IRQ 到处理器中断的翻译流程,以及 3DS 游戏固件利用软件中断实现 DSP 线程调度的经典用法。读完本文,你将能够读懂 DSP 固件中任意一段中断初始化代码,并能定位 melonDS 中对应的仿真实现路径。
一、ICU 在 Teakra 中的角色
Teakra 是 melonDS 为模拟 3DS/DSi 的 DSP(数字信号处理器)而实现的独立仿真模块,位于 src/teakra 目录。ICU(Interrupt Control Unit)是该 DSP 内部的一个关键外设:它负责收集来自各个外设的16 路 IRQ 信号,按照固件配置将它们翻译为处理器可以响应的 4 路中断(int0、int1、int2 与 vectored interrupt)。
文档 icu.md 将 ICU 的功能总结为两层:
- 信号汇聚层:DMA、APBP、BTDMP、Timer 等外设在特定事件发生时拉高对应的 IRQ 线(详见本文第三节);
- 信号翻译层:ICU 依据
I0x/I1x/I2x/IVx寄存器把每路 IRQ 映射到某个处理器中断,并驱动处理器进入中断处理流程(详见本文第四节)。
在 teakra.cpp 中,ICU 实例与处理器实例通过回调绑定在一起,构成了完整的中断链路:
icu.SetInterruptHandler(std::bind(&Processor::SignalInterrupt, &processor, _1), std::bind(&Processor::SignalVectoredInterrupt, &processor, _1, _2));即:ICU 每触发一路普通中断,就会调用Processor::SignalInterrupt(interrupt_index);每触发一路向量中断,就会调用Processor::SignalVectoredInterrupt(address, context_switch)。
二、ICU 的 MMIO 布局
ICU 在 DSP 的 MMIO 空间中占用0x0200 ~ 0x0210的 12 个半字(16 位)寄存器,以及随后的 16 组向量寄存器。原文档给出如下位图(#为 4 位分组分隔符,每格为 1 个 bit):
+0x0200 |IPF|IPE|IPD|IPC|IPB|IPA|IP9|IP8|IP7|IP6|IP5|IP4|IP3|IP2|IP1|IP0| +0x0202 |IAF|IAE|IAD|IAC|IAB|IAA|IA9|IA8|IA7|IA6|IA5|IA4|IA3|IA2|IA1|IA0| +0x0204 |ITF|ITE|ITD|ITC|ITB|ITA|IT9|IT8|IT7|IT6|IT5|IT4|IT3|IT2|IT1|IT0| +0x0206 |I0F|I0E|I0D|I0C|I0B|I0A|I09|I08|I07|I06|I05|I04|I03|I02|I01|I00| +0x0208 |I1F|I1E|I1D|I1C|I1B|I1A|I19|I18|I17|I16|I15|I14|I13|I12|I11|I10| +0x020A |I2F|I2E|I2D|I2C|I2B|I2A|I29|I28|I27|I26|I25|I24|I23|I22|I21|I20| +0x020C |IVF|IVE|IVD|IVC|IVB|IVA|IV9|IV8|IV7|IV6|IV5|IV4|IV3|IV2|IV1|IV0| +0x020E |TPF|TPE|TPD|TPC|TPB|TPA|TP9|TP8|TP7|TP6|TP5|TP4|TP3|TP2|TP1|TP0| +0x0210 |PLF|PLE|PLD|PLC|PLB|PLA|PL9|PL8|PL7|PL6|PL5|PL4|PL3|PL2|PL1|PL0| N = 0..15 +0x0212+N*4 |VIC| |VADDR_H| +0x0214+N*4 | VADDR_L |2.1 基础寄存器(0x0200 ~ 0x0210)
| 偏移 | 名称 | 位字段 | 含义 |
|---|---|---|---|
| 0x0200 | IP0..IPF | 16 位只读 | IRQ pending flag:每路 IRQ 的挂起标志,固件通过读取它得知具体是哪一路 IRQ 触发了中断 |
| 0x0202 | IA0..IAF | 16 位只写 | IRQ acknowledge:向对应位写 1 清除挂起标志(1 to clears pending flag) |
| 0x0204 | IT0..ITF | 16 位只写 | 软件触发:写 1 手动触发对应 IRQ(software interrupt) |
| 0x0206 | I00..I0F | 16 位 | 将对应 IRQ 连接到处理器中断 0 |
| 0x0208 | I10..I1F | 16 位 | 将对应 IRQ 连接到处理器中断 1 |
| 0x020A | I20..I2F | 16 位 | 将对应 IRQ 连接到处理器中断 2 |
| 0x020C | IV0..IVF | 16 位 | 将对应 IRQ 连接到处理器向量中断(vectored interrupt) |
| 0x020E | TP0..TPF | 16 位 | IRQ 触发模式:0 = 脉冲(pulse),1 = 粘滞(sticky) |
| 0x0210 | PL0..PLF | 16 位 | IRQ 信号极性(polarity) |
需要特别说明两点:
- 原文档作者在
TPx一栏旁标注了“I might have got PLx and TPx swapped”(可能把 PLx 与 TPx 的意义写反了)。从源码看,这两个寄存器的仿真目前尚未实现——mmio.cpp 中0x20E与0x210两处均被注释掉(// polarity for each interrupt?、// source type for each interrupt?),因此它们属于“已命名但语义待考证”的保留字段。 IPx为只读寄存器:在 mmio.cpp 中,对0x200的写操作被绑定为NoSet("ICU::GetRequest")(写入时仅打印警告),读操作则调用ICU::GetRequest()。
2.2 向量寄存器组(0x0212+N4、0x0214+N4)
N = 0..15对应 16 路 IRQ,每组两个 16 位寄存器:
| 偏移 | 字段 | 含义 |
|---|---|---|
| 0x0212+N*4 | VADDR_H(bit15 为 VIC) | 向量中断处理程序的高 16 位地址;bit15VIC写 1 表示在该向量中断发生时启用上下文切换(context switch) |
| 0x0214+N*4 | VADDR_L | 向量中断处理程序的低 16 位地址 |
从 mmio.cpp 的实现可以看到,VIC实际被建模为icu.vector_context_switch[i],与vector_high[i]同存于一个 BitFieldCell 中:
impl->cells[0x212 + i * 4] = Cell::BitFieldCell({ BitFieldSlot::RefSlot(0, 2, icu.vector_high[i]), // VADDR_H BitFieldSlot::RefSlot(15, 1, icu.vector_context_switch[i]), // VIC }); impl->cells[0x214 + i * 4] = Cell::RefCell(icu.vector_low[i]); // VADDR_L而 icu.h 中把高低两半拼成 32 位向量地址:
u32 GetVector(u32 irq) const { return vector_low[irq] | ((u32)vector_high[irq] << 16); }三、16 路 IRQ 信号的来源
原文档指出:MMIO 中的所有位字段都对应着 16 路 IRQ 信号,其中一部分已确认与其它外设相连,另一部分则尚未探明。已知连接关系如下(*F即 bit15、*0即 bit0):
DMA --------* (IRQ 15) APBP ------------* (IRQ 14) ICU? ----------------* (IRQ 13) --------------------* (IRQ 12) BTDMP ------------------------* (IRQ 11) TIMER0----------------------------* (IRQ 10) TIMER1--------------------------------* (IRQ 9)这一连接表可以在 teakra.cpp 的构造函数中找到一一对应的证据:
timer[0].SetInterruptHandler([this]() { icu.TriggerSingle(0xA); }); // TIMER0 → IRQ 10 timer[1].SetInterruptHandler([this]() { icu.TriggerSingle(0x9); }); // TIMER1 → IRQ 9 apbp_from_cpu.SetDataHandler(0, [this]() { icu.TriggerSingle(0xE); }); // APBP → IRQ 14 apbp_from_cpu.SetDataHandler(1, [this]() { icu.TriggerSingle(0xE); }); apbp_from_cpu.SetDataHandler(2, [this]() { icu.TriggerSingle(0xE); }); apbp_from_cpu.SetSemaphoreHandler([this]() { icu.TriggerSingle(0xE); }); btdmp[0].SetInterruptHandler([this]() { icu.TriggerSingle(0xB); }); // BTDMP0 → IRQ 11 btdmp[1].SetInterruptHandler([this]() { icu.TriggerSingle(0xC); }); // BTDMP1 → IRQ 12 dma.SetInterruptHandler([this]() { icu.TriggerSingle(0xF); }); // DMA → IRQ 15由此可以确认的完整映射为:
| IRQ 编号 | 信号源 | 触发事件(可参考源码) |
|---|---|---|
| IRQ 9(0x9) | TIMER1 | 定时器事件,见 timer.cpp |
| IRQ 10(0xA) | TIMER0 | 定时器事件 |
| IRQ 11(0xB) | BTDMP0 | 音频数据收发中断,见 btdmp.cpp |
| IRQ 12(0xC) | BTDMP1 | 另一路音频数据收发中断 |
| IRQ 13(0xD) | ICU?(未确认) | 原文档猜测可能连接 ICU 自身或其它未知组件 |
| IRQ 14(0xE) | APBP | CPU↔DSP 数据通道就绪 / 信号量(semaphore)变化,见 apbp.md |
| IRQ 15(0xF) | DMA | DMA 传输完成,见 dma.cpp |
IRQ 0~8 以及文档图中第 4 列(IRQ 12 在图中未标注文字)的归属在原文档写作时尚未完全探明——不过 IRQ 12 从代码看即btdmp[1],而 IRQ 0 在第四节会看到它承担着 3DS 固件线程调度的职责。文档亦提醒:“There might be more undiscovered components that have associated IRQ”(可能还存在更多尚未发现的带 IRQ 的组件)。
四、IRQ-to-interrupt translator:中断翻译与处理器响应
4.1 ICU 侧的翻译逻辑
原文档对 ICU 核心职责的表述是:
The main job of ICU is to translate 16 IRQ signals to 4 processor interrupt signals (int0, int1, int2 and vint), specified by
I0x,I1x,I2xandIVxregisters.
其实现位于 icu.h 的ICU::Trigger:
void Trigger(u16 irq_bits) { IrqBits bits(irq_bits); request |= bits; // 1. 置起 IPx 挂起标志 for (u32 irq = 0; irq < 16; ++irq) { if (bits[irq]) { for (u32 interrupt = 0; interrupt < enabled.size(); ++interrupt) { if (enabled[interrupt][irq]) { // 2. 查 I0x/I1x/I2x 映射 on_interrupt(interrupt); // 3. 通知处理器对应中断 } } if (vectored_enabled[irq]) { // 4. 查 IVx 映射 on_vectored_interrupt(GetVector(irq), vector_context_switch[irq] != 0); } } } }对应到 MMIO 上:
request(即IPx)由GetRequest()读出;enabled[0]、enabled[1]、enabled[2]分别由写0x0206(I0x)、0x0208(I1x)、0x020A(I2x)的SetEnable(0/1/2, irq_bits)设置;vectored_enabled由写0x020C(IVx)的SetEnableVectored()设置;- 中断处理完毕后,固件向
0x0202(IAx)写 1,ICU::Acknowledge执行request &= ~IrqBits(irq_bits),按位清除挂起标志——这正是原文档所说的“setIAxregisters to clear the IRQ signal”。
4.2 处理器侧的中断响应流程
ICU 通过回调把中断送达处理器后,处理器在 interpreter.h 的取指循环中完成响应。其状态由 register.h 中的一组寄存器描述:
| 寄存器 | 含义 |
|---|---|
ip[0..2] | 中断 0/1/2 的 pending 位(对应 ICU 的 int0/int1/int2) |
ipv | 向量中断 pending 位 |
im[0..2] | 中断 0/1/2 的使能位(mask) |
imv | 向量中断使能位 |
ic[0..2] | 中断 0/1/2 是否启用上下文切换 |
ie | 中断使能总开关(master enable) |
流程如下:
- 每周期先检查 ICU 送来的 pending 信号:
SignalInterrupt(i)把interrupt_pending[i]置 true,随后在取指循环开头兑换为regs.ip[i] = 1;向量中断则兑换为regs.ipv = 1; - 每条指令执行结束后,若
regs.ie == 1(总开关打开)且不在rep(单指令重复)期间,则依次检查im[i] && ip[i]:命中后清除ip[i]、关闭ie、PushPC()保存返回地址,并把 PC 设为0x0006 + i * 8(即中断 0 的入口在0x0006,中断 1 在0x000E,中断 2 在0x0016); - 若
ic[i]置位,则调用ContextStore()(interpreter.h)保存当前寄存器上下文; - 若普通中断均未命中,再检查向量中断:
imv && ipv时跳转到vinterrupt_address,并按vinterrupt_context_switch决定是否做上下文切换; - 中断处理程序以
reti(interpreter.h)返回,恢复ie = 1。
因此,固件侧的标准中断处理流程正是原文档所描述的:进入中断处理程序后先读IPx确认具体的 IRQ 编号,处理完后再写IAx清除该 IRQ 的挂起标志。
五、软件中断与 3DS 游戏的线程调度
除了外设硬件触发外,处理器自身也可以通过写ITx寄存器手动产生软件中断(software interrupt)。由于ITx的写操作最终同样调用ICU::Trigger(见 mmio.cpp),软件中断与硬件中断在 ICU 之后的路径完全一致:置起IPx挂起标志 → 按I0x/I1x/I2x/IVx路由到处理器中断。
原文档特别点出了一个极具实战意义的用例:
A use case for this in many 3DS games is that IRQ 0 is used as the reschedule procedure for multithreading. When a thread wants to yield, it triggers IRQ 0, which switches thread context and resume another thread.
也就是说,IRQ 0 在许多 3DS 游戏的 DSP 固件中被用作多线程的重新调度(reschedule)原语:当当前线程想要让出 CPU 时,固件通过写IT0触发 IRQ 0,处理器随即保存当前线程上下文(配合ic[0]的上下文切换功能与ContextStore()的 Shadow 寄存器机制),切换并恢复另一个线程继续执行。
从仿真实现的角度看,这一机制能够工作,依赖的是 ICU 与处理器中断机制三部分的无缝配合:
IT0写 1 →ICU::Trigger(1)→on_interrupt(0)→Processor::SignalInterrupt(0);- 处理器把
ip[0]置 1,在im[0]、ie均使能时跳转到0x0006的中断 0 入口; - 上下文切换由
ContextStore()/ContextRestore()完成,它们操作 register.h 中的 Shadow 寄存器组(ShadowStore/ShadowSwap/ShadowRestore,见 register.h),与reti指令构成完整的“保存—切换—恢复—返回”闭环。
六、状态保存与复位
ICU 的可仿真状态通过两种接口管理,这也是 emulator 中可验证的确定性来源:
- Reset(icu.h):清零
request(IPx)、enabled[0..2](I0x/I1x/I2x)与vectored_enabled(IVx)。注意向量寄存器vector_low/vector_high/vector_context_switch与回调在 Reset 中不重置; - DoSavestate(icu.h):以
"TKiu"为节名,序列化request、enabled[0..2]、vectored_enabled五个 16 位变量,供 melonDS 存档/读档使用(整个 Teakra 的状态序列化入口见 teakra.cpp)。
对于希望进一步调试中断链路、或为 Teakra 贡献实现的读者,建议按以下顺序阅读仓库源码:
- 先读本文所依据的 icu.md 与 icu.h,建立寄存器级认知;
- 再到 mmio.cpp 核对每个 ICU 寄存器在 MMIO 上的读写绑定(注意
IPx只读、IAx/ITx只写的行为差异); - 最后在 interpreter.h 中跟踪中断从 pending 到跳转入口、上下文切换、
reti返回的完整生命周期。
七、已知未解之谜
忠实于原文档,以下几点仍属探索性结论,仿真与文档层面都尚未完全定论:
- TPx / PLx 语义:
0x020E(触发模式 pulse/sticky)与0x0210(信号极性)在当前 mmio.cpp 中尚未实现;文档作者自己也提示“可能把 PLx 和 TPx 写反了”; - IRQ 13 的归属:原文档的 IRQ 信号图将其标注为“ICU?”,并注明可能还有未发现的带 IRQ 组件;
- IRQ 0~8 的硬件来源:除 IRQ 0 被 3DS 固件用作软件调度中断外,其余低编号 IRQ 的外设连接尚未在文档中完整标注。
这些未解点恰好也是参与该开源仿真项目后续逆向与实现工作的切入点——阅读 CONTRIBUTING.md 与 README.md 可以了解项目的整体协作方式,而 icu.md 所在的 src/teakra 目录中还有 apbp.md、timer.md、dma.md 等成套的外设文档,共同构成了对这颗 DSP 芯片的完整逆向笔记。
- 虚拟化
- 图形学
- 桌面应用
【免费下载链接】melonDS
DS emulator, sorta
相关推荐
DeepSeek-V4.1-Flash多节点部署实战:convert.py权重切分与torchrun分布式推理全流程
DeepSeek V4.1 Flash多节点部署实战:convert.py权重切分与torchrun分布式推理全流程 DeepSeek V4.1 Flash 是
虚拟化图形学桌面应用Wger Workout Manager 安装指南:如何快速跑起一套自建的健身计划管理系统
Wger Workout Manager 安装指南:如何快速跑起一套自建的健身计划管理系统 Wger Workout Manager 是一个自由开源的健身计划与
虚拟化图形学桌面应用深入解析 Raspberry Pi 3 的 Linux 中断控制器:本地与全局 IRQ 芯片的协作机制
深入解析 Raspberry Pi 3 的 Linux 中断控制器:本地与全局 IRQ 芯片的协作机制 导读 本文以 raspberry pi os 开源项目第
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考