Feed on
Posts
Comments

From our previous part, the PCBs arrived and I started testing games. There were a few that didn’t work and was able to get some to work by adding some MBC1 support via a few little detection hacks. This part, we’ll look at how to add Multi-game support plus use GBDK to create our game selection screen loader.


(sneak peak of loader screen)

Multi-game support is pretty useful if you have a lot of little games that are 32-256KB but don’t want to have a cart just for each of them. They might also be games that don’t need to save because it can be very easy to load the wrong game and it could potentially overwrite the SRAM.

The way multi-game support works is once you select a game, it send either the bank or the memory location of the game to the custom MBC via an address that wouldn’t likely be written to, the ROM bank is changed, the Gameboy is soft reset and then the game loads.

reg [6:0] romBankMulti;
...
romBankMulti <= 7'd2;
...

// *** ROM Functions ***
if (inputAddress <= 4'd7 && (!inputRD || !inputWR)) begin
	if (inputAddress <= 4'd3) begin // 0x0000-3FFF, Bank 0 always
		highAddress <= romBankMulti << 1;
	end
	else begin
		highAddress <= ((romBank + romBankMulti) << 1);
	end
end

Just to test the concept, we can append one 32KB game to another 32KB game, flash it to the cart and then add a rom bank adjustment variable for multi-carts. If we change the variable to be 2, it will select bank 2 of the flash chip when the Gameboy asks for bank 0. When it asks for bank 1, we simply add the romBankMulti variable to the rom bank the Gameboy asked for; we are always adjusting the rom bank to be 2 banks higher than what it asks for in this case.

Now we just need to make that rom bank adjustment variable configurable so what’s the best address for us to send the bank command to our CPLD? We also need to consider that users may not use the multi-game loader so it shouldn’t conflict with normal cart operation.

The best address seems to be 0x6000 to 0x7FFF, as long as we don’t conflict with the MBC1 ROM/RAM mode detection, we won’t be using bank 0 anyway as that’s where the loader will be and we just won’t allow bank 1 to be used.

MBC3 carts also use this address for the RTC but I’m hoping that since there is no RTC that perhaps it won’t try to write anything useful. But what if for some reason, MBC3 wrote to the RTC anyway, it could accidentally select a bank to switch to whilst in the game. What we could do is have a 2 stage bank loader, firstly we send a special byte to an address to unlock the multi-game support and then another byte at another address for the bank to switch to.

// First stage - Check address 0x7000-7FFF for data byte 0x65 
if (multiStage == 2'd0 && inputData == 7'h65 && inputAddress == 4'd7) begin
	multiStage <= 2'd1;
end

// Second stage - Check address 0x6000-6FFF for the bank to switch to
if (multiStage == 2'd1 && inputAddress == 4'd6) begin
	romBankMulti <= inputData;
	multiStage <= 2'd2; // Turn off any more multi-game bank changing 
	romBank <= 7'b1;
	mbc1Detect303FOn <= 1'b0;
end

Firstly we have our 2 bit variable for the stage we are at, when at stage 0, we listen for the byte 0x65 at address 0x7000-7FFF which then changes the stage to 1. We then listen for the address 0x6000-6FFF and read the data as our multi-game bank variable. We change the stage variable to be 2 so that we won’t trigger the stage 0/1 sections again and for good measure we reset the romBank to 1 and turn off MBC1 0x3000-3FFF support (as later on I found that GBDK creates MBC1 ROMs and one of the first things it does is switch to bank 0 which turns on the 0x3000-3FFF detection code I have).

But wait, now we have a new problem, when we write to the flash cart, we get stuck at around 0x7000-7FFF because the special byte 0x65 would almost certainly be in ROM bank 1 or higher within games, so it’s switching the bank to somewhere else. Now we need to detect if a write is coming from a flash cart or from the Gameboy.

// Detect flash cart writing (AM29F016B)
if ((inputAddress == 4'd0 || inputAddress == 4'd1) && inputData == 7'hAA && !inputWR && inputRD && inputCE) begin
	detectedWriting <= 1'b1;
end

For the AM29F016B flash chip, it writes to the address 0x555 with byte 0xAA at the start, lucky for us, that’s the RAM enable address which normally won’t ever see 0xAA by a Gameboy, so we can simply detect for 0xAA (I’m only checking for 7 bits so it’s really 0x2A for me).

// Multi-game cart support, check if MBC1 bank 0 requested, that we haven't detected writing
// and multi-game isn't already active
if (mbc1Detect303FOn && !detectedWriting && multiStage <= 2'd1 && !inputWR && inputRD && inputCE) begin
	// First stage - Check address 0x7000-7FFF for data byte 0x65 
	...

Now we can add a bit more protection to our multi-game bank switching, firstly if MBC1 0x3000-3FFF is on, then we know it’s likely an MBC1 ROM as mentioned before, we can check if writing has been detecting, if it has we won’t allow for multi-game bank switching and lastly if the stage is either 0 or 1, plus the usual WR low, RD high and CS high.

Another problem emerged, the cart wouldn’t work in GBxCart RW, I couldn’t read back the ROM. I started playing around and found that the cart needed to be reset (reset pin low for a few milliseconds) and then it would always work fine. I’m guessing since I have a 10K resistor going from VCC to RST on GBxCart RW and with the new extra logic I added to the CPLD, there may not be enough time to initialise the CPLD and before it can detect the reset line is low it’s already too late. Writing to the flash cart now works without any issues!

Multi-game loader using GBDK

Now it’s time for making our Multi-game loader using GBDK. I hadn’t used GBDK before and started playing around with it for a few days, trying out the simple examples, having a sense of how the basics works, printing text, key press detection, moving the printf location and lastly how to link ASM files to the C code it uses so we could potentially do things on our own, luckily there was an example but it still took a little bit to work my way through it.

The loader needs to be written in such a way so that’s it’s dynamic, so users don’t need to re-compile with GBDK to add their ROMs to it. When playing around with GBDK, it seems to create a 32KB file but only uses about 0x2100 of it in my case. What if we could have the loader read data back from it’s own ROM file at say 0x3000? I could add the game titles, the bank numbers and the number of games in that location as the rest of the file is just all 0xFFs (blank/not used).

WORD readlocation(WORD);

Adding ASM to your C file in GBDK is relatively simple, first you specify the function in the C file and it expects a 16 bit number (WORD).

.globl _readlocation
	_readlocation: 

	LDA	HL,2(SP)

	LD	E,(HL) 
	INC	HL

	LD	D,(HL)
	INC	HL
	
	; Load byte location to load in HL
	LD	H,D
	LD	L,E
	
	; Read location data
	LD	E,(HL)
	LD	D,#0
	
	RET

In the ASM file, you use the same function name but with an underscore at the start. The first few commands are from the example, when GBDK switches over to ASM it transfers the number we sent to the stack so here it loads those values back to DE from the stack. DE can be broken up so D is the high 8 bits and E is the low 8 bits.

We load the two values DE back to HL and by using the brackets around (HL) to tell it to read that location and put the 8 bit data it read back into E. We load D with 0 as we only want to read the 8 bits LSB (E) and return which returns the result DE back to our C code.

UBYTE totalGames = 0;
UWORD readCharLocation = 0x2FFF;
...
totalGames = readlocation(readCharLocation);

We can now access it like above, give it the address to read and store the result back.

I think I’ll store the total number of games in 0x2FFF to align everything nicely to 0x3000 which will store the bank number of a game and the next 15 bytes will be the game title, 0x3010 will be the bank number of the next game and so on.

if (totalGames != 0xFF) {
	readCharLocation = 0x3001;

	UBYTE g = 0;
	for (g = 0; g < totalGames; g++) {
		
		if (g >= 1) {
			printf(" ");
		}
		
		// Read file names
		UBYTE x = 0;
		for (x = 0; x < 15; x++) {
			letter = readlocation(readCharLocation);
			readCharLocation++;
			printf("%c", (UWORD) letter);
		}
		readCharLocation ++; // Skip the bank select byte
		printf("\n");
	}
}
else {
	printf("No games found");
}

After a bit of playing around, we can check if games were detected and list them out one by one and have an arrow to show which one is selected.

 .globl _multibankswitch
	_multibankswitch:
	...
	
	; Load 0x7000 with A to unlock multi-game mode
	LD	A,#0x65
	LD	(0x7000),A
	
	; Load 0x6000 with the bank to switch to
	LD	A,E
	LD	(0x6000),A
	
	; Soft reset Gameboy
	JP 0x100
	
	RET

The last thing remaining is the code to unlock the multi-game cart mode and specify the bank, it starts off the same as the other ASM code so we’ll skip that part. We can just load any value we like to register A by using hash before the number (#0x65) and then we output the 0x65 to the address location 0x7000. Next we grab the lower 8 bits (E) of the bank value we received (because we won’t ever need more than 8 bits in my case), load that to register A and then put that at 0x6000. Lastly after playing around with where we should jump to for a soft reset, it appears 0x100 is the best place (BGB also starts there when you reload a ROM file).

if (joypad() & J_A) {
	readCharLocation = 0x3000 + (arrowLocation * 16);
	bankSelect = readlocation(readCharLocation);
	multibankswitch(bankSelect);
}

Once the A button is pressed, we read the multi-game bank number from the location corresponding to the game selected and load that value to our bank switching code.

However, we’re not actually done, when we initiate the last bank switch command, it will switch banks straight away and not actually give us enough time to soft reset the Gameboy. When testing it, it would just hang (later on I found that it could have been a problem with the game I used “Tennis”, it doesn’t work with the working solution either).

One solution might be to add another stage which would activate the bank switch in the CPLD when the ROM was read next or another method could be to run the commands from bank 1, switch bank 0 out and then switch bank 1 out too.

In the middle of this I was helping a user who was trying to write to a clone Multi-game cart, it seemed to stop around 0x7002, interesting, that’s the same sort of problem I had so I had a ROM dump of the cart before they tried flashing it.

For that cart, it looks like it divides a 24 bit number into 3x 8 bit chunks and sends each chunk to a different location, 0x7000, 0x7001 and 0x7002, looks like they can read all the address lines which is handy. In the above example, they point to the ROM address 2,154,640 which is just over 2MB by writing 0x20, 0xE0, 0x90 (not shown in picture above) and this number changes when loading the other games too. After that they load bank 1 to the lower 8 bits of MBC5, zero out the high bit then jump to 0x100.

20 e0 90	yellow
40 c0 90	gold
80 e0 90	blue
c0 c0 90	silver

00 c0 91	crystal
40 e0 91	pinball
80 e0 91	red
c0 c0 91	card

Edit 10 Sep 18: Upon further analysis, it appears that it isn’t a 24 bit number, the first and second number looks to somehow make the bank number and the last number stays the same for each 4 games, maybe some sort of 6 or 8MB boundary that separates them, kind of odd.

So it looks like the sort of thing I’m doing, they just have a few no operations (nops) along the way just in case, so strange that it works for them. However when looking at it again, the most surprising thing is that they are executing these commands in HRAM!

The area they are writing to is the High RAM which from the Gameboy Memory Map is supposed to be used for stack space or quick RAM access. It makes sense now, you load the commands you want in the RAM, jump to the RAM and start executing from it and then you are free to change the banks as you wish and the soft reset will still be executed. Once a soft reset occurs, I believe it should clear out the most of the memory locations.

The issue now is, we can’t actually write our commands to HRAM like we can for the ROM, what we have to do is get the actual ASM instructions byte codes for our existing commands and write those bytes to the HRAM so they will be parsed as our commands.

Above is the actual ASM instructions for the commands we issued, so we now have to write 0x3E then 0x65 somewhere in the HRAM which would correspond to ld a,65 when it executes.

; ******************************
; Load HRAM with commands to run
; ******************************

; Load A with 0x65
LD	a,#0x3E
LD	(0xFFAC),a
LD	a,#0x65
LD	(0xFFAD),a

; Load 0x7000 with A to unlock multi-game mode
LD	a,#0xEA
LD	(0xFFAE),a
LD	a,#0x00
LD	(0xFFAF),a
LD	a,#0x70
LD	(0xFFB0),a

...
; Jump to HRAM and execute the above
JP 	0xFFAC

Here’s a few commands to show you how they look like, after we write the commands to the HRAM we then jump to the start of the HRAM with our code and it starts executing them, nice!

When running in BGB as you can see it has the right commands as before, except now we’re executing from the HRAM. I tested appending the Tetris game and it all worked!

Although some games don’t work, they either don’t refresh the screen or have a white screen, seems like 80% of games work. Those that don’t work, do work normally when the game is flashed as a single game to the cart, for example, like Tennis.


I looked into it a bit more and the Gameboy Manual provides the default register and I/O states so I went ahead and implement this in my code, unfortunately it didn’t make any difference but I’ll leave it in for good measure; the clone multi-cart game doesn’t seem to reset the registers or I/O (but they could be doing it elsewhere).

Edit 10 Sep: I was playing around with BGB’s “Load ROM without reset” function and eventually found that I wasn’t resetting the 0xFFFF (IE) register to 0x00 (was 0x09), now the Tennis game works! I have a list of non-working multi-game cart games and only a handful of them work now.

Edit 15 Sep: It appears the loader only works for Gameboy (non-colour) games so I’ve made another loader just for Gameboy Colour games.

Download insideGadgets_Multi-Game_Loader_v1.1_GB or insideGadgets_Multi-Game_Loader_v1.1_GBC

Multi-Game Cart Maker

Now it’s time to make a simple program which will do the calculating of banks, reading of game title, appending the games and provide us a single output ROM file.

// Calculate file sizes
long fileSizeTotal = 32768; // Loader size
for (uint8_t g = 1; g < argc; g++) {
	FILE *romReadFile = fopen(argv[g], "rb");
	fseek (romReadFile, 0, SEEK_END);
	long fileSize = ftell (romReadFile);
	fileSizeTotal += fileSize;
	fclose(romReadFile);
}

// Check if too large
...

// Copy the base rom file to the output file
system("copy base.gb output.gb > /nul");

Firstly we just calculate the file sizes of all the ROMs that were dragged and dropped (it has to be done all at once), check if the output rom file will be too large and then copy the base ROM file to our output file.

// Write how many games there are
uint8_t gamesCount = argc;
buffer[0] = gamesCount - 1;
fwrite(buffer, 1, 1, romWriteFile);

I’ve manually cut off everything after the 0x2FFE location in the base rom file so that we can just append the games count and game titles to the output file.

// Read ROM game title
uint16_t startLocation = 0x0134;
uint8_t gameTitle[16];
...

// File size
...

// Parse game title
...

// Write ROM bank up to
buffer[0] = romBank;
fwrite(buffer, 1, 1, romWriteFile);

// Write the game title
fwrite(gameTitle, 1, 15, romWriteFile);

// Calculate ROM banks used
romBank += fileSize / 16384;

We just open up each game, read and write the game title, write the rom bank we are up (we start off with bank 2), calculate the rom banks for the game and add that to the rom bank we are up to. A 32KB game has 2 rom banks, so after that game, we’re up to rom bank 4 for the next game.

// Write FF's to make 0x7FFF
uint16_t remainingFFs = (0x4FFF - ((gamesCount - 1) * 16)) / 16;
for (uint8_t x = 0; x < 16; x++) {
	buffer[x] = 0xFF;
}
for (uint16_t x = 0; x <= remainingFFs; x++) {
	fwrite(buffer, 1, 16, romWriteFile);
}
fclose(romWriteFile);

// Append the ROM files
for (uint8_t g = 1; g < gamesCount; g++) {
	char appendCommand[255];
	strncpy(appendCommand, "copy /b output.gb+\"", 21);
	strncat(appendCommand, argv[g], 200);
	strncat(appendCommand, "\" output.gb", 12);
	strncat(appendCommand, " > /nul", 8);
	
	//printf("%s\n", appendCommand);
	
	system(appendCommand);
}

Once everything has been appended to the output rom file, we can calculate the remaining 0xFF’s to add into that file to make it end at 0x7FFF and then we append all the game roms to that output file.

Download insideGadgets_Multi-Game_Cart_Maker_v1.0

Here’s a video of the end result, works good.

There are a few features which could be added, some of them would be interesting to play around with:
– Save game manager on the loader (could allow you to save or load your save game to a section on the flash chip)
– A feature to prevent you from loading a different game if you have a save in another game
– Most games only use 8-32KB of SRAM, could allow for switching of ram banks so you can play multiple games that use saves at any time without having to backup your save before hand
– A GUI version of the Maker, drag and drop, sort games, change game titles, etc
– The loader could send another special command to the CPLD to tell it what MBC type the game is

Parts:
Part 1: CPLD as the MBC and adding Flash as our ROM
Part 2: Adding the SRAM
Part 3: PCBs arrived, Adding some MBC1 support and troubleshooting a few games
Part 4: Adding Multi-game support
Part 5: Using 32KB FRAM and Adding MBC1 2MB ROM Support

4 Responses to “Building a 2MB MBC5 Gameboy Cart – Part 4: Adding Multi-game support”

  1. boh says:

    Hi Alex,

    I stumbled across your website whilst looking for a menu program source that i could edit for testing my home made 512KB flash cart based on the mmm01 mapper, I used the game Momotarou collection DMG-AM3J as a donor.
    I’ve made some chages so that the cart can detect on the fly what is on it (banking in all the banks & searching for the nintendo logo) and rebuilding the gametable in WRAM. (toggled by pressing select)
    I also made changes to the bank switching routine (for my mmm01 mapper), also I’ve used memcpy to copy the bankswitch code into HRAM.
    Unfortunately, I can’t seem to get the mmm01 mapper to bank in anything unless its in mapped mode (the game is mapped to bank 0 &1), so it’s not working out for me yet. Hard coding the menu like you’ve done works fine. The other problem I have is getting the SRAM enabled. I’ve emulated the IO register writes, but still no luck.
    Next is to modify your menu to let me set register bits on the fly before mapping in.
    Would you be interested in the changes so far?

    • Alex says:

      Hi Boh,

      I’m not too familiar with the MMM01 mapper but it appears that once it’s mapped, you can’t unmap it (this might also mean you can’t change the mapping). “The only way to unmap the mapper after mapping is to reset it.” – I found this info here: https://wiki.tauwasser.eu/view/MMM01

      For the SRAM enable, have you also checked in BGB to see how it looks? And also perhaps check a MMM01 mapper game to check your register writes are similar.

      You should the GBDev discord as there are a few users there that might be able to help too: https://discord.gg/Bn5ekfH (I’m only on there every now and then)

  2. boh says:

    Hi Alex,

    Thank you for the information, I had already seen Tauwasser’s wiki and have contacted him. I managed to get the SRAM working for now,seems like the order of writing to the registers is incorrect in the code of the source game cartridge (Momotarou collection 2). I’ve built another cart this time transplanting 32KB sram and 2Mbyte, however another problem with going beyond bank 0x20. Anyway, as I am still using bung transferer (printer port) & an old win 98 laptop with LPT port, development & flash times are slow.
    I may buy one of your GBxCartRW units to see if i can speed up development time. I still have a few bung cartridges to hand, so I would like to try and see if I can get those working through your GBxCartRW as well? Also, I built a few carts based on mbc1 and am29f040b as per http://reinerziegler.de.mirrors.gg8.se/readplus.htm . Seeing as you already support am29f010b, in theory they should work?

    Regarding your multigame /cart menu, I remember from DGBMAX menu program, it detects the type of Gameboy (regular/cgb/sgb) and can adapt palettes accordingly. I suspect you could do the same and unify your multi game loader menu into a single source.

    • Alex says:

      Yep GBxCart RW should work fine with the AM29F040B with MBC1 since it’s 512KB, if it was more, we would just need to do a little modification to the code. You could also modify the console interface of GBxCart to try changing banks directly, read a bit of the rom to check for a change or anything else, so compiling and testing would much quicker.

      It doesn’t seem like too many users were interested in the multi-game cart option. I was testing a chinese clone cart and noticed that it just rebooted the GB when selecting a game, done in hardware! So I went ahead and duplicated that, just set the reset line low for a little bit and you don’t have to worry about any detecting anything other than the MBC type (and sometimes ram size) 🙂

Leave a Reply to Alex