This post has been postponed for a very long time. I started writing it almost 3 years ago, but due to a bug in the code I never got to finish it. When I was done with the Atari 8bit post, I started looking at this code again to find out what was wrong. The issue was found and fixed so now I present you with Hello World for Game Boy.
The Gameboy CPU is based on the Z80 but have some small but significant changes to the instruction set. Some instructions are removed while other have been added. Most removed instructions have to do with I/O ports, but since the Game Boy have memory mapped I/O, they were not needed anyway. The added instructions makes more sense with the memory mapped I/O, you’ll see why soon enough.
This time I leave out the header and setup code from the blog post. If you want to try the code yourself, grab the source from bitbucket instead of trying to copy it from this post.
During the boot sequence the built-in ROM code will verify the cartridge header with some checksums and display the nintendo logo before jumping to address $100. The only code at that address is a jump to $150, where we start to go through the code.
The first instruction is di, to disable interrupts before we switch off the LCD display.
.orga $150 main: di -: ldh a,($44) sub 144 jp nz,- xor a ldh ($40),a
According to this page, you should never disable the LCD outside the Vertical Blank period, because that could physically break the Game Boy hardware. So the loop above waits for the VBL before switching of the LCD.
The ldh instruction is one of the special instructions in the Game Boy. What it does is add $ff00 to the number between the parenthesis. So ldh a,($44) will fetch the value from address $ff44 and store it in register a. As it happens, the I/O ports are mapped to the address range $ff00-$ff7e, so this new instruction makes perfect sense. At address $ff44 we can find the current line that is currently sent to the LCD. The line 144 is the start of the VBL period, so subtract 144 from the register value, and jump to the nearest label named - (minus sign) above the current instruction if the result was not zero.
The following block of code will clear the memory used by the tile map that decides what is drawn on screen. Since our code is entered when the nintendo logo is displayed, this little loop will clear it.
ld hl,$9bff -: xor a ldd (hl),a ld a,h sub $97 jp nz,-
The tile map can be found in the range $9800-$9bff, and we use another of the special instructions to clear it. ldd works like a normal ld instruction, except it also decreases the value of hl after the load. So ld (hl),a will first copy the content of register a to where hl register pair points to, which will be $9bff the first iteration, and then decrease hl to $9bfe. To decide when all the memory is cleared, I grab the high byte of the pointer from h, subtracts $97 and check if the result is zero. If it is, that means the hl register just went from $9800 to $97ff, and the whole tile map has been cleared.
The next step is to copy our tile data to the tile data RAM. In this example, $8000 is the address for the tile data, so we load the hl register with that, and the we setup de register pair with the source address, which is at label _gfx _defined later in the code. As you may know, the original Game Boy is capable of displaying 4 different colours, but since I’m a lazy being I only made the graphics with one colour. Tiles are 8 by 8 pixels, using 2 bits per pixel giving 16 bytes per tile. The source data is only 1 bit per pixel; 8 bytes per tile and since we have 7 tiles, the first being an empty tile, we setup register b with the number of bytes to copy and use it as a counter.
When the counter is setup, we start the loop by fetching data from the source, then using the ldi instruction to store it to the destination twice. ldi is similar to ldd except that it increases hl instead of decrease after the load is done. This way our 8 bytes per tile source data will be written twice to the destination filling both bitplanes every line with the same source data. Then we increase the source pointer, and decrease the counter and to the looping magic until the counter reaches 0.
ld hl,$8000 ld de,gfx ld b,8*7 -: ld a,(de) ldi (hl),a ldi (hl),a inc de dec b jp nz,-
Now that the tile data is written, we can start using it by writing tile indices to the tile map memory. As before, we load the tile map address, set up b as a counter but this time we also load a with a 1. This is because we uploaded an empty tile as tile 0. Now we loop to write 1 to 6 to the first 6 entries in the tile map.
ld hl,$9800 ld b,6 ld a,1 -: ldi (hl),a inc a dec b jp nz,-
Almost done now. The only thing that is left is to enable the LCD again and loop forever. The highest bit we set is to enable the LCD, bit 4 is to select address $8000 to be the tile data address, and bit 0 is set to enable the background graphics, which is what we have been setting up to display our hello world text. The other bits in the LCD controller register are used to enable sprites and chose base address for tile map amongst other things.
When the LCD is enabled again, we halt the CPU to save batteries and jump back to halt again if it would ever wake up from the halt instruction.
ld a,%10010001 ldh ($40),a -: halt jp - ; endless loop
Only the graphics data left. I’m starting to feel that this section is more or less implicit. This time I compacted it to hexadecimal syntax, with on tile per data row to reduce the space it took in the source code.
gfx: .db $00,$00,$00,$00,$00,$00,$00,$00 .db $00,$44,$44,$45,$7d,$45,$44,$00 .db $00,$05,$e5,$15,$f5,$05,$f5,$00 .db $00,$01,$31,$49,$49,$49,$30,$00 .db $00,$10,$13,$14,$54,$54,$a3,$00 .db $00,$01,$19,$a5,$a1,$a1,$21,$00 .db $00,$0a,$3a,$4a,$4a,$48,$3a,$00
You might be curious of what bug delayed this post for almost three years. Well, it turned out I had missed the fact that you can’t write to the video RAM while it’s accessed by the LCD controller, so my tiles were only partially copied and the graphics were garbled.
Next up, I will probably write a post on the Mega Drive as the code was completed over a year ago. Until then, keep on coding!