The DS cartridge interface: endless fun
Depending on your definition of fun, of course.

This is a bit of a pace change from all the recent OpenGL stuff: this is going to be some hardware infodumping slash juicy technical post. It all started with this bug report: Bug: Surviving High School (DSiWare) not booting.

Basically, this DSi game gets stuck on a black screen. This bug report piqued my interest, and oh boy, I had no idea what kind of rabbit hole it would be.


I first started by doing what I do in order to troubleshoot emulation bugs: track where each CPU is hanging around at, dump RAM, throw it into IDAPro. This gives me an idea what the game is doing, and hopefully provides a lead on the bug. Another possibility is to try modifying the cache timing constants to probe for a timing bug, but this made no difference here.

In this situation, the ARM9 was running an idle loop, but the ARM7 was stuck in an endless loop. Backtracking from this, I found that the game was reading the cartridge's chip ID, comparing it against the value stored at 0x02FFFC00, and panicking because the two were different. This code isn't atypical for a DS game...

But... wait?!

This game is a DSiWare title! There is no cartridge here.

No idea why it's reading the cartridge chip ID. Maybe the game was intended to be released in physical form? But in this case, the chip ID stored at 0x02FFFC00 is zero. When no cartridge is inserted, melonDS returns 0xFFFFFFFF instead of zero, hence one part of the bug.

Changing melonDS to return zero when no cartridge is inserted would fix the bug, but only when there's indeed no cartridge. If there's one, it will fail. This is because when booting a DSiWare title, the DSi menu forcefully powers off the cartridge if there's one inserted. melonDS doesn't emulate this bit.


This was the start of a longer journey, which ended up becoming its own branch, again. I don't like the way the cartridge code in melonDS has evolved over time.

On one hand, it was built quickly to answer simple needs in simpler times, and things were kind of just piled together. The object-oriented implementation of different cartridge types was a good idea, but they were kind of jammed into one code file, which turned into a bit of a mess.

With DSP HLE, I started experimenting with using subdirectories for specific areas of the emulator, rather than just dumping all the code files into one directory. I thought that doing the same for the cartridge code, after splitting it into separate files, was a good idea. Other areas of the emulator could use this kind of reorganization too, but one thing at a time.

On the other hand, the cartridge interface implementation in melonDS is technically wrong. This is where the fun begins.


In the DS, the cartridge interface is the piece of hardware that sits between the CPU and the cartridge. It can operate in two modes: ROM and SPI. ROM mode uses all the data lines to transfer one byte per cycle, and is typically used to access the ROM. SPI mode uses two data lines as a SPI interface, and is typically used to access the save memory.

The cartridge interface's registers are exposed to both ARM9 and ARM7, but only one CPU may use it at a given time - which CPU gets access is selected by bit 11 in EXMEMCNT. This is a rather important detail - if you happen to remember the old times, we've had a rather evil bug related to this.

I assumed that the cart interface was one set of registers, one hardware block, and that it was accessible to either one or the other CPU. But that's not how this works, actually.

Each CPU has its own register set, and its own cart interface hardware block. Technically, both sides are always functional, and can run transfers at the same time. The EXMEMCNT access right setting merely controls which one is connected to the actual cartridge - the other one will just be reading 0xFF bytes.

Another detail that is wrong is timings.

The way communication works in ROM mode is that you send a 8-byte command to the cartridge, and optionally send or receive data. For example, to retrieve the cart's chip ID, you send a command with the first byte set to 0xB8, then receive one data word: the chip ID.

Since the interface transfers one byte per cycle, it takes 8 cycle to transmit the full command, and 4 cycles per data word transferred. Since the cart's ROM chip may need extra time to fulfill the command, It is also possible to configure delays before the data transfer, and between each 512 bytes of data. The delays are specified in cycles. The duration of a cycle can be either 5 or 8 system cycles (ie 33.51 MHz).

Cartridge transfer delays in melonDS were modelled based on this. However, the model is missing one simple fact, and this ends up making cart transfers slower than they should be.

Jakly figured out that there is buffering involved. Basically, when a data word is transferred from the cartridge, it is placed in a buffer where it may be read out (at 0x04100010), and the DRQ signal is raised to notify the CPU (or DMA). However, the buffer can actually store two words worth of data, so when one data word is available, the interface can start transferring another word, rather than having to wait for the CPU to read out the data.

This buffering scheme is definitely worth implementing. In melonDS, the lack of buffering ends up adding about 400 cycles per 512 bytes of data transferred.

I remember that Rabbids Go Home suffers from a related issue. Basically, the game runs several ROM transfers and measures how long they take. The total needs to fall within a certain range, or the anti-piracy kicks in and the game freezes. So implementing buffering into melonDS might fix this.

I've also been finding out that write transfers (when transferring data to the cartridge) suffer from a couple hardware bugs, too. For example, under certain specific circumstances, the cart interface may accidentally send out one data word despite the buffer being empty. Some of this is worth documenting, but not necessarily worth emulating, atleast for now.


Then you have the DSi, which has two cartridge interfaces.

Yes, you read this right.

Basically, the DSi was planned to have two cartridge slots. In the end, Nintendo didn't like it, so it was scrapped.

The functionality is still part of the retail DSi. The second cartridge interface is completely functional. It even appears that the retail SoC has the required signals for a second cart slot, but they aren't connected to anything on the motherboard.

So how does this all work?

The second cart interface has the same register layout as the first one. The registers may be found at 0x040021A0 instead of 0x040001A0 (and 0x04102010 instead of 0x04100010 for the transfer buffer). There are also new IRQ lines for it (IRQ 26 and 27, instead of 19 and 20 respectively), a new DMA trigger line, and a separate CPU access setting in EXMEMCNT (bit 10). This cart interface can't be used with old DMA, but NDMA has an adequate trigger mode for it (0x05).

Register SCFG_MC was added to control the cartridge slots. Each cartridge may be turned on or off in software. This is used to reset the cartridge - initializing a DSi cartridge requires a reset, but the original interface provided no way to do that in software. SCFG_MC also serves to turn off the cartridge when loading DSiWare, for example.

An empty cartridge slot is also forced off by hardware. The nonexistant second slot is considered to always be empty, so it is always off.

It's also worth noting that the on/off status applies to the cartridge itself. The cart interface is functional regardless, but it will read 0x00 bytes if the cart is off.

SCFG_MC also provides another setting to swap the two cartridge interfaces. The point would be that games could be loaded from either cart slot without needing to know which slot is in use, and it would also guarantee backwards compatibility with DS games. This setting merely swaps the address at which each cart interface appears, as well as the IRQ lines and all.


So all in all, this requires some foundational changes to the way cartridge stuff is emulated in melonDS. As well as a lot of hardware testing to figure out every detail, answer every question that is raised along the way, and so on.

But, I hope, the improved accuracy will be worth it.
Arisotura says:
Mar 3rd 2026
I kind of want to try, but since the SoC is a BGA package, you would need to design some interposer PCB with a breakout for the required cart slot signals
Toya_9 says:
Mar 4th 2026
UPDATE: Ended up trying compute renderer and that works an absolute treat so thank yall so much for thatšŸ™šŸ¼ for now I’m playing with the setting that mutes when speeding up, but I’ll keep my fingers crossed and pray that hopefully in a future patch there’s a work around/fix for that audio bug. Thank yall so much again :)
Chris Jones says:
Mar 7th 2026
i just had an idea what if you had a little cartridge for the gba slot that swapped the protocols for online and local wireless in my experience with playing multiplayer online with a real ds it always connects easier if they are put through the same network so if you could make a gba cartridge that swapped the protocols and made local read as online that could work just have it go to a wifi menu connect to an open hotspot doesn't need actual wifi and can just connect find a way to make that happen if thats possible please
Post a comment
Name:
DO NOT TOUCH