I recently found my old GameBoy Colour. I loved this thing and still do. Fits in my pocket,
when the batteries run out you lose all of your progress because why would I remember to
save anything, and you can only view the screen with a light bright enough for use in
federal torture methods. Lets parse and view some aspects of a GameBoy cartridge header.
For any actual/in-depth information, this link is incredible.
>
gbdev.io - The Cartridge Header
Just a quick first note, I'll be writing this in C++ but really leaning heavily into the C
aspect for my sanity. This implementation will be quick and dirty, as this project progresses
things can be cleaned up but until then I really don't care as long as it works.
First up, I define the locations of some important data in the header like so:
#define CART_HEADER_START_ADDRESS 0x100
#define CART_ENTRY_START_ADDRESS 0x100
#define CART_NINTENDO_LOGO_START_ADDRESS 0x104
#define CART_TITLE_START_ADDRESS 0x134
#define CART_MANUFACTURER_CODE_START_ADDRESS 0x13F // NOTE(nathan): In older cartridges, these
#define CART_CGB_FLAG_ADDRESS 0x143 // NOTE : values are included in the Title
#define CART_NEW_LICENSEE_CODE_HIGH_ADDRESS 0x144
#define CART_NEW_LICENSEE_CODE_LOW_ADDRESS 0x145
#define CART_SGB_FLAG_ADDRESS 0x146
#define CART_CARTRIDGE_TYPE_FLAG_ADDRESS 0x147
#define CART_ROM_SIZE_ADDRESS 0x148
#define CART_RAM_SIZE_ADDRESS 0x149
#define CART_DESTINATION_CODE_ADDRESS 0x14A
#define CART_OLD_LICENSEE_CODE_ADDRESS 0x14B
#define CART_ROM_VERSION_MASK_ADDRESS 0x14C
#define CART_HEADER_CHECKSUM_ADDRESS 0x14D // NOTE(nathan): 8-bit checksum computed from 0x134 - 0x14C
#define CART_GLOBAL_CHECKSUM_START_ADDRESS 0x14E // NOTE(nathan): 16-bit BE checksum of the sum of all ROM bytes (excl. itself)
Great. Now that we know where things live we need to be able to convert them into something meaningful to us
humans. We will do this by storing some of the possible values the header values could be referencing.
I'll start off with the easiest ones because why not knock them out of the way. I store
the CGB mode flag, SGB mode flag, and the official Nintendo logo directly:
#define CART_CGB_FLAG_CGB_MODE 0xC0
#define CART_CGB_FLAG_NON_CGB_MODE 0x80
#define CART_SGB_FLAG_SGB_MODE 0x03
#define CART_SGB_FLAG_NON_SGB_MODE 0x00
const u8 CART_OFFICIAL_NINTENDO_LOGO[48] = {
0xCE, 0xED, 0x66, 0x66, 0xCC, 0x0D, 0x00, 0x0B,
0x03, 0x73, 0x00, 0x83, 0x00, 0x0C, 0x00, 0x0D,
0x00, 0x08, 0x11, 0x1F, 0x88, 0x89, 0x00, 0x0E,
0xDC, 0xCC, 0x6E, 0xE6, 0xDD, 0xDD, 0xD9, 0x99,
0xBB, 0xBB, 0x67, 0x63, 0x6E, 0x0E, 0xEC, 0xCC,
0xDD, 0xDC, 0x99, 0x9F, 0xBB, 0xB9, 0x33, 0x3E
};
The destination code flag, ROM size, and RAM size are a little bigger but still on the easy side:
const char* CART_DESTINATION_CODES[2] = {
"JAPAN",
"OVERSEAS"
};
#define CART_ROM_SIZE_KIB(NumBanks) (32 * (1UL << (NumBanks)))
const char* CART_ROM_SIZES[9] = {
"32 KiB (2 BANKS)",
"64 KiB (4 BANKS)",
"128 KiB (8 BANKS)",
"256 KiB (16 BANKS)",
"512 KiB (32 BANKS)",
"1 MiB (64 BANKS)",
"2 MiB (128 BANKS)",
"4 MiB (256 BANKS)",
"8 MiB (512 BANKS)"
};
const char* CART_RAM_SIZES[6] = {
"NO RAM",
"UNUSED",
"8 KiB (1 * 8KiB)",
"32 KiB (4 * 8KiB)",
"128 KiB (16 * 8KiB)",
"64 KiB (8 * 8KiB)"
};
These values were actually really nice. They all started at 0 and they were sequential, which
sadly is not the case for some of the values to come. I've defined a little macro to easily
convert the ROM size value stored in the header to a value in KiB, super easy.
Next up are a little more annoying, just because there's more typing to do. I store the possible
values for the cartridge type and the old licencee names in sparse arrays. The new licensee names are
even more annoying becuase they aren't defined sequentially, instead they are stored as 2 ASCII
characters that correpsond to a publisher's name. For this I implemented a simple function that
uses a switch-case to return the appropriate string value:
const char* CART_CARTRIDGE_TYPES[256] = {
"ROM ONLY",
"MBC1",
"MBC1+RAM",
"MBC1+RAM+BATTERY",
NULL,
"MBC2",
"MBC2+BATTERY",
NULL,
"ROM+RAM", // NOTE(nathan): Unused in licensed cartridges
"ROM+RAM+BATTERY", // NOTE(nathan): Unused in licensed cartridges
NULL,
"MMM01",
"MMM01+RAM",
"MMM01+RAM+BATTERY",
NULL,
"MBC3+TIMER+BATTERY",
"MBC3+TIMER+RAM+BATTERY",
"MBC3",
"MBC3+RAM",
"MBC3+RAM+BATTERY",
NULL, NULL, NULL, NULL, NULL,
"MBC5",
"MBC5+RAM",
"MBC5+RAM+BATTERY",
"MBC5+RUMBLE",
"MBC5+RUMBLE+RAM",
"MBC5+RUMBLE+RAM+BATTERY",
NULL,
"MBC6",
NULL,
"MBC7+SENSOR+RUMBLE+RAM+BATTERY",
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL,
"POCKET CAMERA",
"BANDAI TAMA5",
"HuC3",
"HuC1+RAM+BATTERY"
};
const char* CART_OLD_LICENSEES[256] = {
"None",
"Nintendo",
NULL, NULL, NULL, NULL, NULL, NULL,
"Capcom",
"HOT-B",
"Jaleco",
"Coconuts Japan",
"Elite Systems",
NULL, NULL, NULL, NULL, NULL, NULL,
"EA (Electronic Arts)",
NULL, NULL, NULL, NULL,
"Hudson Soft",
"ITC Entertainment",
"Yanoman",
NULL, NULL,
"Japan Clary",
NULL,
"Virgin Games Ltd.3",
NULL, NULL, NULL, NULL,
"PCM Complete",
"San-X",
NULL, NULL,
"Kemco",
"SETA Corporation",
NULL, NULL, NULL, NULL, NULL, NULL,
"Infogrames5",
"Nintendo",
"Bandai",
"(SEE OLD LICENSEE CODE)",
"Konami",
"HectorSoft",
NULL, NULL,
"Capcom",
"Banpresto",
NULL, NULL,
".Entertainment i",
NULL,
"Gremlin",
NULL, NULL,
"Ubi Soft1",
"tlus",
NULL,
"Malibu Interactive",
NULL,
"Angel",
"Spectrum Holoby",
NULL,
"Irem",
"Virgin Games Ltd.3",
NULL, NULL,
"Malibu Interactive",
NULL,
"U.S. Gold",
"Absolute",
"Acclaim Entertainment",
"Activision",
"Sammy USA Corporation",
"GameTek",
"Park Place",
"LJN",
"Matchbox",
NULL,
"Milton Bradley Company",
"Mindscape",
"Romstar",
"Naxat Soft13",
"Tradewest",
NULL, NULL,
"Titus Interactive",
"Virgin Games Ltd.3",
NULL, NULL, NULL, NULL, NULL,
"Ocean Software",
NULL,
"EA (Electronic Arts)",
NULL, NULL, NULL, NULL,
"Elite Systems",
"Electro Brain",
"Infogrames5",
"Interplay Entertainment",
"Broderbund",
"Sculptured Software6",
NULL,
"The Sales Curve Limited7",
NULL, NULL,
"THQ",
"Accolade",
"Triffix Entertainment",
NULL,
"Microprose",
NULL, NULL,
"Kemco",
"Misawa Entertainment",
NULL, NULL,
"Lozc",
NULL, NULL,
"Tokuma Shoten",
NULL, NULL, NULL, NULL,
"Bullet-Proof Software2",
"Vic Tokai",
NULL,
"Ape",
"I'Max",
NULL,
"Chunsoft Co.8",
"Video System",
"Tsubaraya Productions",
NULL,
"Varie",
"Yonezawa/S'Pal",
"Kemco",
NULL,
"Arc",
"Nihon Bussan",
"Tecmo",
"Imagineer",
"Banpresto",
NULL,
"Nova",
NULL,
"Hori Electric",
"Bandai",
NULL,
"Konami",
NULL,
"Kawada",
"Takara",
NULL,
"Technos Japan",
"Broderbund",
NULL,
"Toei Animation",
"Toho",
NULL,
"Namco",
"Acclaim Entertainment",
"ASCII Corporation or Nexsoft",
"Bandai",
NULL,
"Square Enix",
NULL,
"HAL Laboratory",
"SNK",
NULL,
"Pony Canyon",
"Culture Brain",
"Sunsoft",
NULL,
"Sony Imagesoft",
NULL,
"Sammy Corporation",
"Taito",
NULL,
"Kemco",
"Square",
"Tokuma Shoten",
"Data East",
"Tonkinhouse",
NULL,
"Koei",
"UFL",
"Ultra",
"Vap",
"Use Corporation",
"Meldac",
"Pony Canyon",
"Angel",
"Taito",
"Sofel",
"Quest",
"Sigma Enterprises",
"ASK Kodansha Co.",
NULL,
"Naxat Soft13",
"Copya System",
NULL,
"Banpresto",
"Tomy",
"LJN",
NULL,
"NCS",
"Human",
"Altron",
"Jaleco",
"Towa Chiki",
"Yutaka",
"Varie",
NULL,
"Epcoh",
NULL,
"Athena",
"Asmik Ace Entertainment",
"Natsume",
"King Records",
"Atlus",
"Epic/Sony Records",
NULL,
"IGS",
NULL,
"A Wave",
NULL, NULL,
"Extreme Entertainment",
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL,
"LJN"
};
internal const char*
CartNewLicenseeName(u16 NewLicenseeCode)
{
switch (NewLicenseeCode) {
case 0x3030: return "None";
case 0x3031: return "Nintendo Research & Development 1";
case 0x3038: return "Capcom";
case 0x3133: return "EA (Electronic Arts)";
case 0x3138: return "Hudson Soft";
case 0x3139: return "B-AI";
case 0x3230: return "KSS";
case 0x3232: return "Planning Office WADA";
case 0x3234: return "PCM Complete";
case 0x3235: return "San-X";
case 0x3238: return "Kemco";
case 0x3239: return "SETA Corporation";
case 0x3330: return "Viacom";
case 0x3331: return "Nintendo";
case 0x3332: return "Bandai";
case 0x3333: return "Ocean Software/Acclaim Entertainment";
case 0x3334: return "Konami";
case 0x3335: return "HectorSoft";
case 0x3337: return "Taito";
case 0x3338: return "Hudson Soft";
case 0x3339: return "Banpresto";
case 0x3431: return "Ubi Soft";
case 0x3432: return "Atlus";
case 0x3434: return "Malibu Interactive";
case 0x3436: return "Angel";
case 0x3437: return "Bullet-Proof Software";
case 0x3439: return "Irem";
case 0x3530: return "Absolute";
case 0x3531: return "Acclaim Entertainment";
case 0x3532: return "Activision";
case 0x3533: return "Sammy USA Corporation";
case 0x3534: return "Konami";
case 0x3535: return "Hi Tech Expressions";
case 0x3536: return "LJN";
case 0x3537: return "Matchbox";
case 0x3538: return "Mattel";
case 0x3539: return "Milton Bradley Company";
case 0x3630: return "Titus Interactive";
case 0x3631: return "Virgin Games Ltd.";
case 0x3634: return "Lucasfilm Games";
case 0x3637: return "Ocean Software";
case 0x3639: return "EA (Electronic Arts)";
case 0x3730: return "Infogrames";
case 0x3731: return "Interplay Entertainment";
case 0x3732: return "Broderbund";
case 0x3733: return "Sculptured Software";
case 0x3735: return "The Sales Curve Limited";
case 0x3738: return "THQ";
case 0x3739: return "Accolade";
case 0x3830: return "Misawa Entertainment";
case 0x3833: return "lozc";
case 0x3836: return "Tokuma Shoten";
case 0x3837: return "Tsukuda Original";
case 0x3931: return "Chunsoft Co.";
case 0x3932: return "Video System";
case 0x3933: return "Ocean Software/Acclaim Entertainment";
case 0x3935: return "Varie";
case 0x3936: return "Yonezawa/s'pal";
case 0x3937: return "Kaneko";
case 0x3939: return "Pack-In-Video";
case 0x3948: return "Bottom Up";
case 0x4134: return "Konami (Yu-Gi-Oh!)";
case 0x424C: return "MTO";
case 0x444B: return "Kodansha";
default: return "Unknown New Licensee";
}
}
Once again nothing really ground-breaking or difficult, just some simple look-up tables to make
things fast and easy. Just out of interest I'll also steal the implementation to validate the
header checksum from the GameBoy boot ROM:
internal u8
ValidateROMHeaderChecksum(u8* ROM)
{
u8 Result = 0;
for (u16 Address = 0x134; Address <= 0x14C; ++Address)
Result = Result - ROM[Address] - 1;
return Result;
}
Finally, I think that should just about do it for storing the information I care about.
Now I define a stuct to store it all in the same place and pass it around to other
functions if I so choose later on.
struct CartridgeHeader {
u8* NintendoLogo;
char* Title;
char* ManufacturerCode;
u8 ColourFlag;
u16 NewLicenseeCode;
u8 SuperFlag;
u8 Type;
u8 ROMSize;
u8 RAMSize;
u8 DestinationCode;
u8 OldLicenseeCode;
u8 ROMVersionNumber;
u8 Checksum;
u16 GlobalChecksum;
};
I never had many games for the GBC. Me and my 2 brothers each had our own GameBoy, but somehow
managed to survive sharing a single actual game cartridge and another 8-in-1 cartridge that
I have never been able to find (even just as a ROM file) to this day. That amazing game was
Harry Potter and the Chamber of Secrets. What a game. I loved it, I was terrible at it, but
I loved it. Looking back its unbelievable how much content they managed to squeeze on to
such a small cartridge. The soundtrack was amazing, chiptune Harry Potter never sounded so
good. Anyway, that's the ROM I'll be testing my program with. My program will only read the
ROM, store the header and then dump the info. You'll notice another struct in the code which
I haven't mentioned or shown `Cartridge`. It doesnt do anything other than open the file,
read its contents and store it. The file handle/pointer (depending on your OS) is closed
immediately after reading the file in, but the memory storing the contents is never freed
and that's on purpose. For a program as simple as this memory leaks are a nothing-burger
and as the great Raymond Chen has said, "The house is on fire and the alarms are going off,
why are you cleaning up?".
Anyway, the code flow goes pretty simply: read the ROM file and store, extract header data
and store, print header data, exit. Easy to follow and easy to fix if troubles arise. I say
that because very little error handling is being done, this is a toy project after all.
int
main(void)
{
Cartridge Cart = {};
Cart = LoadCartridge("Roms/Harry Potter and the Chamber of Secrets (USA, Europe) (En,Fr,De,Es,It,Pt,Nl,Sv,Da).gbc");
if (Cart.RomSize == 0)
return 1;
CartridgeHeader Header = {};
Header.Title = (char*) Cart.Rom + CART_TITLE_START_ADDRESS;
Header.ManufacturerCode = (char*) Cart.Rom + CART_MANUFACTURER_CODE_START_ADDRESS;
Header.ColourFlag = Cart.Rom[CART_CGB_FLAG_ADDRESS];
Header.SuperFlag = Cart.Rom[CART_SGB_FLAG_ADDRESS];
Header.Type = Cart.Rom[CART_CARTRIDGE_TYPE_FLAG_ADDRESS];
Header.NewLicenseeCode = (Cart.Rom[CART_NEW_LICENSEE_CODE_HIGH_ADDRESS] << 8) | (Cart.Rom[CART_NEW_LICENSEE_CODE_LOW_ADDRESS]);
Header.Type = Cart.Rom[CART_CARTRIDGE_TYPE_FLAG_ADDRESS];
Header.ROMSize = Cart.Rom[CART_ROM_SIZE_ADDRESS];
Header.RAMSize = Cart.Rom[CART_RAM_SIZE_ADDRESS];
Header.DestinationCode = Cart.Rom[CART_DESTINATION_CODE_ADDRESS];
Header.OldLicenseeCode = Cart.Rom[CART_OLD_LICENSEE_CODE_ADDRESS];
Header.ROMVersionNumber = Cart.Rom[CART_ROM_VERSION_MASK_ADDRESS];
Header.Checksum = Cart.Rom[CART_HEADER_CHECKSUM_ADDRESS];
Header.GlobalChecksum = *(u16*) Cart.Rom + CART_GLOBAL_CHECKSUM_START_ADDRESS;
printf("*************************************************\n");
printf("** ROM HEADER INFO *\n");
printf("*************************************************\n");
printf("** Title : %s\n", Header.Title);
printf("** Manufacturer Code : %s\n", Header.ManufacturerCode);
printf("** CGB Mode Flag : 0x%X\n", Header.ColourFlag);
printf("** New Licensee Code : %s\n", CartNewLicenseeName(Header.NewLicenseeCode));
printf("** SGB Mode Flag : 0x%X\n", Header.SuperFlag);
printf("** Cartridge Type : %s\n", CART_CARTRIDGE_TYPES[Header.Type]);
printf("** ROM Size : %s\n", CART_ROM_SIZES[Header.ROMSize]);
printf("** ROM Size Raw : %lu KiB\n", CART_ROM_SIZE_KIB(Header.ROMSize));
printf("** RAM Size : %s\n", CART_RAM_SIZES[Header.RAMSize]);
printf("** Destination : %s\n", CART_DESTINATION_CODES[Header.DestinationCode]);
printf("** Old Licensee Code : %s\n", CART_OLD_LICENSEES[Header.OldLicenseeCode]);
printf("** Version : 0x%X\n", Header.ROMVersionNumber);
printf("** Checksum : 0x%X\n", Header.Checksum);
printf("** Global Checksum : 0x%X\n", Header.GlobalChecksum);
printf("*************************************************\n");
u8 HeaderComputedChecksum = ValidateROMHeaderChecksum(Cart.Rom);
if (HeaderComputedChecksum == Header.Checksum)
printf("** Header Checksum : SUCCESS *\n");
else
printf("** Header Checksum : FAILURE (0x%X != 0x%X)\n", Header.Checksum, HeaderComputedChecksum);
printf("*************************************************\n");
return 0;
}
And from all of this, we get a nice little output frame of our cartridge ROM header:
*************************************************
** ROM HEADER INFO *
*************************************************
** Title : HPCOSECRETSBH6E�69
** Manufacturer Code : BH6E�69
** CGB Mode Flag : 0xC0
** New Licensee Code : EA (Electronic Arts)
** SGB Mode Flag : 0x0
** Cartridge Type : MBC5+RAM+BATTERY
** ROM Size : 4 MiB (256 BANKS)
** ROM Size Raw : 4096 KiB
** RAM Size : 8 KiB (1 * 8KiB)
** Destination : OVERSEAS
** Old Licensee Code : (SEE OLD LICENSEE CODE)
** Version : 0x0
** Checksum : 0x18
** Global Checksum : 0xCF2F
*************************************************
** Header Checksum : SUCCESS *
*************************************************
You'll notice the Title and Manufacturer codes seem to be reading more data than
they should be, but everything else looks pretty much exactly how I want it to.
I'll leave those problems as a breadcrumb for a future investigation. In fact now that
I'm looking at it, its most likely because I'm just callously casting the ROM data
to a char*. I'll need to factor in that the Title should only be 11 characters long
and I shouldn't assume they will always be NULL terminated. An easy fix for later.
There you have it. A nice simple program that dumps some information from
a GameBoy Colour ROM header. Not really useful (yet), but I did spend some time
finding some random ROMs and seeing their info. There are also a bunch of things
that should/could change about the program such as how I store the Cartridge and
CartridgeHeader separately which annoys me but I only have myself to blame and
these will prove to have simple solutions. In the future I'll learn more about
what these values exactly mean to a GameBoy console, and how I can go about
using them to run some games.
P.S. This code has been tested and known to be working with both clang and cl