As promised last time we start with the cartridge header. From the documentation we can extract the following types:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
type GBCFlag = 
    | DMG = 0x00uy           // Only the basic gameboy is supported by the game
    | GBCSupported = 0x80uy  // The game also supports some gameboy color features but works fine on the original gameboy
    | GBCOnly = 0xC0uy       // The game only works with gameboy color.

type RomSize = 
    | _32k = 0x00uy
    | _64k = 0x01uy
    | _128k = 0x02uy
    | _256k = 0x03uy
    | _512k = 0x04uy
    | _1m = 0x05uy
    | _2m = 0x06uy
    | _4m = 0x07uy
    | _8m = 0x08uy
    | _1m1k = 0x52uy
    | _1m2k = 0x53uy
    | _1m5k = 0x54uy

type RamSize = 
    | None = 0x00uy
    | _2k = 0x01uy
    | _8k = 0x02uy
    | _32k = 0x03uy
    | _128k = 0x04uy
    | _64k = 0x05uy

// Details for roms with memorybank controllers.
type MBCDetails = {
    RomSize: RomSize
    RamSize: RamSize
    HasBattery: bool
}

type CartridgeType =
    | Simple of HasRam:bool * HasBattery:bool
    | MBC1 of MBCDetails
    | MBC2 of RomSize * HasBattery:bool
    | MMM01 of MBCDetails
    | MBC3 of MBCDetails * HasTimer:bool
    | MBC5 of MBCDetails * HasRumble:bool
    | MBC6 of MBCDetails
    | MBC7 of MBCDetails
    
type Destination = 
    | Japanese = 0x00uy
    | NonJapanese = 0x01uy

type CartridgeHeader = {
    Title: string                // 0134-0143
    ManufacturerCode: string     // 013F-0142
    GBC: GBCFlag                 // 0143 
    LicenseeCode: uint16         // 0144-0145
    SGB: bool                    // 0146
    CartridgeType: CartridgeType // 0147-0149
    Destination: Destination     // 014A 
    Version: byte                // 014C 
}

As you may know the gameboy uses 16bit addresses to access everything. (Including ROM, RAM, Timers, Link cable, Sound and Graphic). So how could we address up to 8 mb with only 16 bit of addresses? Simple answer: we simply swap the ROM / RAM Bank which is bound to an address range. This is done by writing a specific value to an address. For example with an MBC 2 chip on your cartridge you could write the value 4 to the address 0x2000 to select the 4th rom bank.

The basic memory layout looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
0000-00FF Restart and Interrupt Vectors
0100-0103 Entry Point of the game.
0104-014F Cartridge Header
0150-3FFF Cartridge ROM (Bank 0)
4000-7FFF Cartridge ROM switchable banks 1 - ...
8000-9FFF Sprite Data & Background Maps (VRam)
A000-BFFF Cartridge RAM (if available)
C000-CFFF Internal RAM - Bank 0 (fixed) (WRam)
D000-DFFF Internal RAM - Bank 1-7 (switchable - CGB only)
E000-FDFF Echo RAM - Reserved, Do Not Use
FE00-FE9F OAM - Object Attribute Memory
FEA0-FEFF Unusable Memory
FF00-FF7F Hardware I/O Registers
FF80-FFFE Zero Page - 127 bytes
FFFF      Interrupt Enable Flag

This whole thing has a few implications for our linking process:

  1. We have to make the rom file as large as the header indicates
  2. With switchable rom / ram banks, multiple symbols can share the same address without being at the same location
  3. We need a possibility to get the bank number from a symbol.
  4. We need to calculate at least 2 checksums (header checksum + rom checksum)

So lets start with the serialization of the header.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
let HEADERSTART = 0x0104 // the location of the header
let GLOBALCHECK = 0x014E

// copyTo copies one byte from  the source to the destination array
// we are also substracting the HEADERSTART for easier mapping of the addresses
let private copyTo (destA :byte[]) (srcA: byte[]) offset srcIdx =
    if srcA <> null && srcIdx >= 0 && srcIdx < srcA.Length then
        destA.[(offset - HEADERSTART)+srcIdx] <- srcA.[srcIdx]
    else
        ()

// prepares a string to be serialized in the header
let private str len offset s : (int*int*byte[]) = 
    if System.String.IsNullOrEmpty s then
        (len, offset, null)
    else
        let s = s.ToUpperInvariant ()
        let len = min len s.Length
        let buf = Encoding.ASCII.GetBytes(s, 0, len)
        (len, offset, buf)

// prepares a byte value for the header
let private bv offset (b:byte) =
    let v = byte b
    (1, offset, [|v|])

// prepares a 16 bit value for the header
let private word offset v =
    let v1 = byte v
    let v2 = byte (v >>> 8)
    (2, offset, [|v1; v2|])

// get the rom size from the cartridge type
let private getRomSizeType ct =
    match ct with 
    | Simple _       -> RomSize._32k
    | MBC1  det      -> det.RomSize
    | MBC2  (rs, _)  -> rs
    | MMM01 det      -> det.RomSize
    | MBC3  (det, _) -> det.RomSize
    | MBC5  (det, _) -> det.RomSize
    | MBC6  det      -> det.RomSize
    | MBC7  det      -> det.RomSize

// get the ram size from the cartridge type
let private getRamSize ct =
    match ct with 
    | Simple (false,_) -> RamSize.None
    | Simple (true, _) -> RamSize._8k
    | MBC1  det        -> det.RamSize
    | MBC2  _          -> RamSize.None
    | MMM01 det        -> det.RamSize
    | MBC3  (det, _)   -> det.RamSize
    | MBC5  (det, _)   -> det.RamSize
    | MBC6  det        -> det.RamSize
    | MBC7  det        -> det.RamSize

// get the byte value for the header which indicates the MBC type.
let private getMBCMarker ct =
    match ct with
    | Simple (false, _) -> 0x00uy
    | MBC1 {HasBattery = false; RamSize = RamSize.None } -> 0x01uy
    | MBC1 {HasBattery = false } -> 0x02uy
    | MBC1 _ -> 0x03uy
    | MBC2 (_, false) -> 0x05uy
    | MBC2 _ -> 0x06uy
    | Simple (true, false) -> 0x08uy
    | Simple (true, true) -> 0x09uy
    | MMM01 {RamSize = RamSize.None} -> 0x0Buy
    | MMM01 {HasBattery = false} -> 0x0Cuy
    | MMM01 _ -> 0x0Duy
    | MBC3 ({HasBattery=true; RamSize = RamSize.None}, true) -> 0x0Fuy
    | MBC3 ({HasBattery=true}, true) -> 0x10uy
    | MBC3 ({RamSize = RamSize.None},_) -> 0x11uy
    | MBC3 ({HasBattery=false},_) -> 0x12uy
    | MBC3 _ -> 0x13uy
    | MBC5 ({RamSize = RamSize.None}, false) -> 0x19uy
    | MBC5 ({HasBattery = false}, false) -> 0x1Auy
    | MBC5 ({HasBattery = true}, false) -> 0x1Buy
    | MBC5 ({RamSize = RamSize.None}, true) -> 0x1Cuy
    | MBC5 ({HasBattery = false}, true) -> 0x1Duy
    | MBC5 ({HasBattery = true}, true) -> 0x1Euy
    | MBC6 _ -> 0x20uy
    | MBC7 _ -> 0x22uy

// create a byte array with the information from the cartridge type (rom size, ram size, MBC type...)
let private cartridgeTypeToBytes (ct: CartridgeType) : byte[] =
    let res = [| 
        getMBCMarker ct; 
        byte (getRomSizeType ct); 
        byte (getRamSize ct) 
    |]
    res

// calculate the header checksum.
let private calcCheckSum (buf: byte[]) : byte =
    seq {0x0134 .. 0x014C}
    |> Seq.map (fun x -> x-HEADERSTART)
    |> Seq.map (fun idx -> int (buf.[idx]))
    |> Seq.fold (fun state v -> state - v - 1) 0
    |> byte

// convert the header to a byte array.
let headerToBytes (h:CartridgeHeader) : byte[] =
    // allocate the resulting array
    let result = Array.zeroCreate<byte>(0x4A)

    let copyTo (cnt, offset, data) =
        Seq.init cnt (fun x -> x)
        |> Seq.iter (copyTo result data offset)

    copyTo (48,    0x0104, LOGO)                                    // Nintendo logo
    copyTo (str 16 0x0134  h.Title)                                 // Game title
    copyTo (str  4 0x013F  h.ManufacturerCode)                      // New Manufacturer Code
    copyTo (bv     0x0143  (byte h.GBC))                            // GBC Flag
    copyTo (word   0x0144  h.LicenseeCode)                          // Licensee Code
    if h.SGB then
        copyTo (bv 0x0146  0x03uy)                                  // Super-Gameboy Flag
    copyTo (3,     0x0147, cartridgeTypeToBytes h.CartridgeType)    // 3 bytes cartridge type
    copyTo (bv     0x014A  (byte h.Destination))                    // Destination
    copyTo (bv     0x014B  0x33uy)                                  // Old Licensee Code 
    copyTo (bv     0x014C  h.Version)                               // Game version
    copyTo (bv     0x014D (calcCheckSum result))                    // header checksum
    result

the global checksum can only be calculated once the whole rom is done, so we will do this at the end of the linking process.

That is enough for this time.