This is the third of a series of blog posts on my Nintendo GameBoy demo. The first summarised the machine; covered part of the tiled graphics system and introduced my concept for implementing full frame buffers on the unit. The second discussed the Game Boy's Sharp SM83 CPU as it's quite intriguing, and in some ways quite different to its Z80 and Intel 8080 relatives and very different to the 6502 used in the Nintendo NES, Atari VCS 2600 and many 1970s/early 1980s 8-bit computers.
This one explores the frame buffer proof-of-concept and defines the font I'll be using for text output.
Recap
The GameBoy uses tiled graphics, where the 160x144 pixels screen is divided into a 20x18 2-D tile map of bytes that reference one of 256, 8x8 pixel tiles. This would appear to prevent a full pixel mapped image to be displayed, because there are 360 tile map locations and only 256 tiles they can reference.
However, it's possible to switch the set of tiles used by the Game Boy to provide access to another 128 tiles (0..127) with the higher numbered tiles (128..255) being the same as in the first set. In addition, the set of tiles can be switched on the fly by generating an interrupt just at the end of the first set. This means we can use a total of 384 tiles, more than enough to uniquely cover the screen.
Goals
My goals are fairly simple. I want to be able to recreate the kinds of text and graphics primitives available on early 8-bit home computers. These are:
- Being able to display a character at any character location. Because I want to support at least a 40x18 display, all my characters will be on a 4x8 matrix.
- Being able to clear the screen, by clearing the frame buffer (you'll see this isn't as trivial as I thought).
- Being able to scroll the screen by a single line and clear the bottom line.
- Being able to plot points and draw lines anywhere on the screen (this was surprisingly easy).
I also wanted to be able to perform all these operations using the full set of 4 colours (greyscales) for both foreground and background colours. With my Sinclair background I refer to these as Ink (foreground) and Paper (background).
Development Process
Generating the Full Frame Buffer
This was fairly easy. I started with the default Hello demo you can find on the https://gbdev.io/rgbds-live/ web page. That demo simply sets up a static image on the background tile map using 69 tiles.
The code is remarkably simple, it just turns off the audio; waits for Blank; turns the LCD off; copies the tile data (first), then the data for the Tile Map; then turns the LCD on and initialises the main display register in the first (blank) frame. The whole code (but not data) fits within a single editor window.
To support a simple frame buffer; all the tiles needed to be in order and I needed a demo image. The easiest way to do this when bringing up a new system IMHO, is to fill the frame buffer with successive values: 0..255. Then you can see them all as binary patterns. I made the first set of tiles black binary patterns and the second set of 128 tiles light grey patterns. The only other change I made was to use the first set of tiles rather than the second set (in memory order), so my code to turn on the LCD looked like:
; Turn the LCD on, use 1st tile block.ld a, LCDC_ON | LCDC_BG_ON | LCDC_BLOCKSldh [rLCDC], a
So, initially I could see that it was using the same set of tiles twice, because they were all dark. Then I'd know the interrupt worked because the remaining 360-256=104 tiles would look light. As a general principle that works for both this kind of game development and embedded programming, incremental changes are far more preferable, because errors are caught quickly and early.
SECTION "StatVec", ROM0[INT_HANDLER_STAT]jp LcdStat ;This needs to go near the start of the ROM;.. then after the ds $150 - @, 0 header line..LcdStat: ;switch between 96 and 144 and switch tile set.push afldh a,[rLCDC] ;get the old LCD tile mode.xor LCDC_BLOCKS ;swap the tile set.ldh [rLCDC],aldh a,[rLYC];get the old scan line number.xor ((256/20)*8)^144 ;(See explanation below)ldh [rLYC],apop afreti;And later to turn on interrupts..ld a,(256/20)*8 ;this calculates the right scan (scan 96)ldh [rLYC],ald a,STAT_LYCldh [rSTAT],ald a,IE_STAT ;initially only use the STAT interrupt.ldh [rIE],aei
Xors can be used to swap between any two values, not merely to turn a bit on and off, because of the identity: b^a^b=a. So, if a=numberX and we want to swap with numberY then xor numberX^numberY will generate numberX^numberX^numberY=numberY the first time around and numberY^numberX^numberY=numberX the second time around. This saves on an if.. else.. control flow. The GameBoy then generates the display:
You can see that where the light-green patterns begin they're the same patterns as in the top-left, but they aren't the same tiles, because the tiles contain the colours too.
Adding a 4x8 Font
This is actually a fairly small change to the firmware, but took quite a bit of work. I wanted to use a 4x8 font, so that I could display 40x18 characters on the screen instead of 20x18 (a 4x6 font could achieve 40x24, but they're harder to read and it'd involve crossing tile boundaries). The font looked like this:
(It's the Tasword 2 font, except I inverted the © sign). To save space, in the ROM I also just wanted to store them as 1bpp images and combine two characters per 8x8 pixels.
GameBoy tiles are 8x8, but 2 bits per pixel. The pixels aren't paired, instead there's 8 light-grey pixels followed by 8 dark-grey pixels (and setting the same bits on both bytes gives black).This means I would need to unpack my font bytes, but I don't need to expand 4-bits into 8-bits. So, for black on white text that's easy, I just repeat both bytes for every scan on the tile.
GenTiles10:
ld a,[de] ;de^font.
inc de
ldi [hl],a ;hl^frame buffer (i.e. tiles)
ldi [hl],a ;copy twice
dec bc ;bc was set to the number of bytes in the font
ld a,b
or c
jr nz,GenTiles10
I've ended up using font like this (or a 6x6 font) several times, and I always end up having to convert it to hex, so I thought I'd just publish it here:
CharSet:
db 0x00, 0x02, 0x02, 0x02, 0x02, 0x00, 0x02, 0x00 ; !
db 0x00, 0x50, 0x52, 0x07, 0x02, 0x07, 0x02, 0x00 ;"#
db 0x00, 0x25, 0x71, 0x42, 0x72, 0x14, 0x75, 0x20 ;$%
db 0x00, 0x21, 0x52, 0x20, 0x60, 0x50, 0x60, 0x00 ;&'
db 0x00, 0x14, 0x22, 0x22, 0x22, 0x22, 0x14, 0x00 ;()
db 0x00, 0x00, 0x52, 0x22, 0x77, 0x22, 0x52, 0x00 ;*+
db 0x00, 0x00, 0x00, 0x00, 0x07, 0x20, 0x20, 0x40 ;,-
db 0x00, 0x01, 0x01, 0x02, 0x02, 0x64, 0x64, 0x00 ;.
db 0x00, 0x22, 0x56, 0x52, 0x52, 0x52, 0x27, 0x00 ;01
db 0x00, 0x22, 0x55, 0x12, 0x21, 0x45, 0x72, 0x00 ;23
db 0x00, 0x57, 0x54, 0x76, 0x11, 0x15, 0x12, 0x00 ;45
db 0x00, 0x37, 0x41, 0x61, 0x52, 0x54, 0x24, 0x00 ;67
db 0x00, 0x22, 0x55, 0x25, 0x53, 0x55, 0x22, 0x00 ;89
db 0x00, 0x00, 0x02, 0x20, 0x02, 0x22, 0x04, 0x00 ;:;
db 0x00, 0x00, 0x10, 0x27, 0x40, 0x27, 0x10, 0x00 ;<=
db 0x00, 0x02, 0x45, 0x21, 0x12, 0x20, 0x42, 0x00 ;>?
db 0x00, 0x62, 0x95, 0xb7, 0xb5, 0x85, 0x65, 0x00 ;@A
db 0x00, 0x62, 0x55, 0x64, 0x54, 0x55, 0x62, 0x00 ;BC
db 0x00, 0x67, 0x54, 0x56, 0x54, 0x54, 0x67, 0x00 ;DE
db 0x00, 0x72, 0x45, 0x74, 0x47, 0x45, 0x42, 0x00 ;FG
db 0x00, 0x57, 0x52, 0x72, 0x52, 0x52, 0x57, 0x00 ;HI
db 0x00, 0x35, 0x15, 0x16, 0x15, 0x55, 0x25, 0x00 ;JK
db 0x00, 0x45, 0x47, 0x47, 0x45, 0x45, 0x75, 0x00 ;LM
db 0x00, 0x52, 0x55, 0x75, 0x75, 0x55, 0x52, 0x00 ;NO
db 0x00, 0x62, 0x55, 0x55, 0x67, 0x47, 0x43, 0x00 ;PQ
db 0x00, 0x62, 0x55, 0x52, 0x61, 0x55, 0x52, 0x00 ;RS
db 0x00, 0x75, 0x25, 0x25, 0x25, 0x25, 0x22, 0x00 ;TU
db 0x00, 0x55, 0x55, 0x55, 0x57, 0x27, 0x25, 0x00 ;VW
db 0x00, 0x55, 0x55, 0x25, 0x52, 0x52, 0x52, 0x00 ;XY
db 0x00, 0x77, 0x14, 0x24, 0x24, 0x44, 0x77, 0x00 ;Z[
db 0x00, 0x47, 0x41, 0x21, 0x21, 0x11, 0x17, 0x00 ;\]
db 0x00, 0x20, 0x50, 0x00, 0x00, 0x00, 0x07, 0x00 ;^_
db 0x00, 0x20, 0x56, 0x41, 0x63, 0x45, 0x73, 0x00 ;£a
db 0x00, 0x40, 0x42, 0x65, 0x54, 0x55, 0x62, 0x00 ;bc
db 0x00, 0x10, 0x12, 0x35, 0x56, 0x54, 0x23, 0x00 ;de
db 0x00, 0x20, 0x52, 0x45, 0x65, 0x43, 0x45, 0x02 ;fg
db 0x00, 0x42, 0x40, 0x66, 0x52, 0x52, 0x57, 0x00 ;hi
db 0x00, 0x14, 0x04, 0x35, 0x16, 0x15, 0x55, 0x20 ;jk
db 0x00, 0x40, 0x45, 0x47, 0x47, 0x55, 0x25, 0x00 ;lm
db 0x00, 0x00, 0x62, 0x55, 0x55, 0x55, 0x52, 0x00 ;no
db 0x00, 0x00, 0x63, 0x55, 0x55, 0x63, 0x41, 0x41 ;pq
db 0x00, 0x00, 0x63, 0x54, 0x42, 0x41, 0x46, 0x00 ;rs
db 0x00, 0x40, 0x75, 0x45, 0x45, 0x55, 0x22, 0x00 ;tu
db 0x00, 0x00, 0x55, 0x55, 0x57, 0x27, 0x25, 0x00 ;vw
db 0x00, 0x00, 0x55, 0x55, 0x23, 0x51, 0x55, 0x02 ;xy
db 0x00, 0x00, 0x71, 0x12, 0x26, 0x42, 0x71, 0x00 ;z{
db 0x00, 0x20, 0x24, 0x22, 0x23, 0x22, 0x24, 0x00 ;|}
db 0x00, 0x06, 0xaf, 0x59, 0x0b, 0x09, 0x0f, 0x06 ;~©
As you can tell, every 8 bytes contains a pair of characters. It's just the printable 96 characters, so they take 384 bytes in total. The display then shows...
Conclusion
Starting with a basic Tile-mapped display demo, I added a line match interrupt which was triggered on scans 96 and 144, inverting the tile set each time. I set the TileMap to point to successive tiles (mod 256). The real GameBoy Tile Map is 32x32, so in reality I had to skip 12 bytes in the TileMap after each 20 byte row. I hand-converted a 4x8 font I had lying around: two characters will fit in each half of an 8x8 bitmap. That maps nicely onto the GameBoy Tiles (I just repeat each byte), but for real text display I'll have to properly mask off the correct half of each character and combine it with the existing background.
That's the subject for the next post in this series.