From 43e75276dfb805a9a405c1ff2b14ca69022ee568 Mon Sep 17 00:00:00 2001 From: Karol Stasiak Date: Mon, 18 Mar 2019 23:15:05 +0100 Subject: [PATCH] Preliminary and experimental Game Boy support --- .gitignore | 1 + examples/README.md | 4 + examples/gb/gbtest.mfk | 250 ++++++++++++++++++++++++++++++++++++ include/gb_hardware.mfk | 171 ++++++++++++++++++++++++ include/gb_header_small.mfk | 22 ++++ include/gb_joy.mfk | 47 +++++++ include/gb_small.ini | 34 +++++ 7 files changed, 529 insertions(+) create mode 100644 examples/gb/gbtest.mfk create mode 100644 include/gb_hardware.mfk create mode 100644 include/gb_header_small.mfk create mode 100644 include/gb_joy.mfk create mode 100644 include/gb_small.ini diff --git a/.gitignore b/.gitignore index 551c892e..5f059d8c 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ examples/lunix/ *.tap *.d88 *.com +*.gb HELLO HELLOCPC diff --git a/examples/README.md b/examples/README.md index fe63dc0d..c37f10ec 100644 --- a/examples/README.md +++ b/examples/README.md @@ -39,6 +39,10 @@ how to create a program made of multiple files loaded on demand * [MMC4 example](nes/nestest_mmc4.mfk) – the same thing as above, but uses a MMC4 mapper just to test bankswitching +## Game Boy examples + +* [GB test example](gb/gbtest.mfk) – a partial port of the NES example, with a rudimentary experimental text output implementation + ## Atari 2600 examples * [Colors](vcs/colors.mfk) – simple static rasterbars diff --git a/examples/gb/gbtest.mfk b/examples/gb/gbtest.mfk new file mode 100644 index 00000000..95dd71b0 --- /dev/null +++ b/examples/gb/gbtest.mfk @@ -0,0 +1,250 @@ +// compile with -t gb_small + +import gb_hardware +import gb_joy +import stdio + +array oam_buffer[$a0] align(256) + +sbyte sprite_direction +byte old_a + +void on_vblank(){ + flush_putchar_buffer() + sprite_dma(oam_buffer.addr.hi) + oam_buffer[1] += sprite_direction + react_to_input() + if oam_buffer[1] == 160 { sprite_direction = -1 } + if oam_buffer[1] == 8 { sprite_direction = 1 } +} +void on_lcdc(){ + +} +void on_timer(){ + +} +void on_serial(){ + +} +void on_joypad(){ + +} + +void react_to_input() { + read_joy() + if old_a == 0 && input_a != 0 { // A button + sprite_direction = 0 - sprite_direction + low_c() + } + old_a = input_a + if input_dy < 0 { // Up button + if oam_buffer[0] > 16 { oam_buffer[0] -= 1} + } + if input_dy > 0 { // Down button + if oam_buffer[0] < 152 { oam_buffer[0] += 1} + } +} + +void low_c() { +} + +void high_c() { +} + + +void main() { + byte i + for i,0,paralleluntil,$a0 { + oam_buffer[i] = 0 + } + reg_obj0_palette = 0q2222 + load_charset() + init_putchar() + reg_lcd_ctrl = lcd_tile_8000 | lcd_bg_on | lcd_obj_on | lcd_win_off | lcd_map_9800 + enable_lcd() + enable_irq() + putstrz("HELLO WORLD"z) + new_line() + sprite_direction = 1 + oam_buffer[0] = 50 + oam_buffer[1] = 8 + oam_buffer[2] = '@' + oam_buffer[3] = 0 + while(true){} +} + +void load_charset() { + pointer s + pointer d + s = charset.addr + d = $8200 + while s < charset.addr + $300 { + d[0] = s[0] + d += 1 + d[0] = s[0] + d += 1 + s += 1 + } +} + +array charset = [ + $00,$00,$00,$00,$00,$00,$00,$00, + $18,$18,$18,$18,$00,$00,$18,$00, + $66,$66,$66,$00,$00,$00,$00,$00, + $66,$66,$FF,$66,$FF,$66,$66,$00, + $18,$3E,$60,$3C,$06,$7C,$18,$00, + $62,$66,$0C,$18,$30,$66,$46,$00, + $3C,$66,$3C,$38,$67,$66,$3F,$00, + $06,$0C,$18,$00,$00,$00,$00,$00, + $0C,$18,$30,$30,$30,$18,$0C,$00, + $30,$18,$0C,$0C,$0C,$18,$30,$00, + $00,$66,$3C,$FF,$3C,$66,$00,$00, + $00,$18,$18,$7E,$18,$18,$00,$00, + $00,$00,$00,$00,$00,$18,$18,$30, + $00,$00,$00,$7E,$00,$00,$00,$00, + $00,$00,$00,$00,$00,$18,$18,$00, + $00,$03,$06,$0C,$18,$30,$60,$00, + $3C,$66,$6E,$76,$66,$66,$3C,$00, + $18,$18,$38,$18,$18,$18,$7E,$00, + $3C,$66,$06,$0C,$30,$60,$7E,$00, + $3C,$66,$06,$1C,$06,$66,$3C,$00, + $06,$0E,$1E,$66,$7F,$06,$06,$00, + $7E,$60,$7C,$06,$06,$66,$3C,$00, + $3C,$66,$60,$7C,$66,$66,$3C,$00, + $7E,$66,$0C,$18,$18,$18,$18,$00, + $3C,$66,$66,$3C,$66,$66,$3C,$00, + $3C,$66,$66,$3E,$06,$66,$3C,$00, + $00,$00,$18,$00,$00,$18,$00,$00, + $00,$00,$18,$00,$00,$18,$18,$30, + $0E,$18,$30,$60,$30,$18,$0E,$00, + $00,$00,$7E,$00,$7E,$00,$00,$00, + $70,$18,$0C,$06,$0C,$18,$70,$00, + $3C,$66,$06,$0C,$18,$00,$18,$00, + $3C,$66,$6E,$6E,$60,$62,$3C,$00, + $18,$3C,$66,$7E,$66,$66,$66,$00, + $7C,$66,$66,$7C,$66,$66,$7C,$00, + $3C,$66,$60,$60,$60,$66,$3C,$00, + $78,$6C,$66,$66,$66,$6C,$78,$00, + $7E,$60,$60,$78,$60,$60,$7E,$00, + $7E,$60,$60,$78,$60,$60,$60,$00, + $3C,$66,$60,$6E,$66,$66,$3C,$00, + $66,$66,$66,$7E,$66,$66,$66,$00, + $3C,$18,$18,$18,$18,$18,$3C,$00, + $1E,$0C,$0C,$0C,$0C,$6C,$38,$00, + $66,$6C,$78,$70,$78,$6C,$66,$00, + $60,$60,$60,$60,$60,$60,$7E,$00, + $63,$77,$7F,$6B,$63,$63,$63,$00, + $66,$76,$7E,$7E,$6E,$66,$66,$00, + $3C,$66,$66,$66,$66,$66,$3C,$00, + $7C,$66,$66,$7C,$60,$60,$60,$00, + $3C,$66,$66,$66,$66,$3C,$0E,$00, + $7C,$66,$66,$7C,$78,$6C,$66,$00, + $3C,$66,$60,$3C,$06,$66,$3C,$00, + $7E,$18,$18,$18,$18,$18,$18,$00, + $66,$66,$66,$66,$66,$66,$3C,$00, + $66,$66,$66,$66,$66,$3C,$18,$00, + $63,$63,$63,$6B,$7F,$77,$63,$00, + $66,$66,$3C,$18,$3C,$66,$66,$00, + $66,$66,$66,$3C,$18,$18,$18,$00, + $7E,$06,$0C,$18,$30,$60,$7E,$00, + $3C,$30,$30,$30,$30,$30,$3C,$00, + $0C,$12,$30,$7C,$30,$62,$FC,$00, + $3C,$0C,$0C,$0C,$0C,$0C,$3C,$00, + $00,$18,$3C,$7E,$18,$18,$18,$18, + $00,$10,$30,$7F,$7F,$30,$10,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF +] + + + + +// the following is an experimental implementation of text output for Game Boy +// if it matures, it might get mainlined into the standard distribution + +array putchar_buffer[256] +byte putchar_buffer_start +byte putchar_buffer_size +byte putchar_column +byte putchar_row +volatile byte putchar_lock + +void init_putchar() { + putchar_buffer_start = 0 + putchar_buffer_size = 0 + putchar_column = 0 + putchar_row = 0 + putchar_lock = 0 +} + +asm void putchar(byte e) { + ld a,(putchar_buffer_size) + cp $ff + jr nz, __putchar_do + halt + jr putchar + __putchar_do: + ld b,a + ld a,1 + ld (putchar_lock),a + ld a,(putchar_buffer_start) + add a,b + ld c,a + ld b,0 + ld hl,putchar_buffer.addr + add hl,bc + ld (hl),e + ld hl,putchar_buffer_size.addr + inc (hl) + xor a + ld (putchar_lock),a + ret +} + + +// TODO: ??? +noinline pointer calculate_cursor() { + pointer p + p = putchar_column + p <<= 5 + p += putchar_row + p += $9800 + return p +} + +void new_line() { + putchar(13) + putchar(10) +} + +void flush_putchar_buffer() { + pointer dest + pointer src + if putchar_lock != 0 { return } + if putchar_buffer_size == 0 { return } + src = putchar_buffer.addr + putchar_buffer_start + dest = calculate_cursor() + byte i + byte c + for i,0,paralleluntil,putchar_buffer_size { + c = src[0] + src += 1 + if c == 13 { + putchar_column = 0 + dest.lo &= $E0 + } else if c == 10 { + putchar_row += 1 + dest += 32 + } else { + dest[0] = c + dest += 1 + putchar_column += 1 + if putchar_column == 20 { + putchar_column = 0 + putchar_row += 1 + dest.lo &= $E0 + dest += 32 + } + } + } + putchar_buffer_start += putchar_buffer_size + putchar_buffer_size = 0 +} diff --git a/include/gb_hardware.mfk b/include/gb_hardware.mfk new file mode 100644 index 00000000..d7aa4319 --- /dev/null +++ b/include/gb_hardware.mfk @@ -0,0 +1,171 @@ +#if not(GAMEBOY) +#warn gb_hardware module should be only used on NES/Famicom targets +#endif + +#pragma zilog_syntax + +// TODO: optimize? +array __vectors @ $40 = [ + $c3, __on_vblank.addr.lo, __on_vblank.addr.hi, 0,0,0,0,0, + $c3, __on_lcdc.addr.lo, __on_lcdc.addr.hi, 0,0,0,0,0, + $c3, __on_timer.addr.lo, __on_timer.addr.hi, 0,0,0,0,0, + $c3, __on_serial.addr.lo, __on_serial.addr.hi, 0,0,0,0,0, + $c3, __on_joypad.addr.lo, __on_joypad.addr.hi, 0,0,0,0,0 +] + + +asm void __start() @ $150 { + di + + ld sp, $DFFF + xor a + ldh (reg_irq_flag), a + ? ld a,ief_vblank + ldh (reg_irq_enable), a + + + ? call disable_lcd + ? call clear_ram + + // copy the DMA routine to hi RAM + ld hl, __sprite_dma_template + ld de, __sprite_dma_actual + ld c, __sprite_dma_actual.length + __copy_sprite_dma_template: + ld a,(hli) + ld (de),a + inc de + dec c + jr nz, __copy_sprite_dma_template + + + ? jp main + ? jp __start +} + +asm void clear_ram() { + ? xor a + ? ld hl,$c000 + ? ld bc,$1ff0 + ? call memset + ? ld hl,$8000 + ? ld bc,$2000 + ? call memset + ? ld hl,$fe00 + ? ld bc,$a0 + ? call memset + ? ld hl,$ff80 + ? ld bc,$7f + ? call memset + ? ret +} + +inline asm void memset(word hl, word bc) { + ? inc c + ? inc b + ? jr __memzero_start + __memzero_loop: + ld (hli), a + __memzero_start: + dec c + jr nz, __memzero_loop + dec b + jr nz, __memzero_loop + ? ret +} + +interrupt void __on_vblank(){ + on_vblank() +} +interrupt void __on_lcdc(){ + on_lcdc() +} +interrupt void __on_timer(){ + on_timer() +} +interrupt void __on_serial(){ + on_serial() +} +interrupt void __on_joypad(){ + on_joypad() +} + +array oam[$a0] @$fe00 + +volatile byte reg_joypad @$ff00 +volatile byte reg_irq_flag @$ff0f +volatile byte reg_lcd_ctrl @$ff40 +volatile byte reg_lcd_status @$ff41 +volatile byte reg_scroll_y @$ff42 +volatile byte reg_scroll_x @$ff43 +volatile byte reg_dma @$ff46 +volatile byte reg_bg_palette @$ff47 +volatile byte reg_obj0_palette @$ff48 +volatile byte reg_obj1_palette @$ff49 +volatile byte reg_window_y @$ff4A +volatile byte reg_window_x @$ff4B +volatile byte reg_vram_bank @$ff4f +volatile byte reg_irq_enable @$ffff + +const byte lcd_on = $80 +const byte lcd_off = 0 +const byte lcd_win_9c00 = $40 +const byte lcd_win_9800 = 0 +const byte lcd_win_on = $20 +const byte lcd_win_off = 0 +const byte lcd_tile_8000 = $10 +const byte lcd_tile_8800 = 0 +const byte lcd_map_9c00 = 8 +const byte lcd_map_9800 = 0 +const byte lcd_obj_16 = 4 +const byte lcd_obj_8 = 0 +const byte lcd_obj_on = 2 +const byte lcd_obj_off = 0 +const byte lcd_bg_on = 1 +const byte lcd_bg_off = 1 + +const byte ief_vblank = 1 + +segment(hiram) +array __sprite_dma_actual[8] + +asm void __sprite_dma_template() { + ldh (reg_dma),a + ld a, $28 +__sprite_dma_wait: + dec a + jr nz, __sprite_dma_wait + ret + __sprite_dma_end: +} + +asm void sprite_dma(byte a) { + jp __sprite_dma_actual +} + +asm byte wait_for_vblank() { + ld hl, reg_irq_flag + __wait_for_vblank_loop: + ld a, ief_vblank + halt + and (hl) + jr z, __wait_for_vblank_loop + ? ld a, $ff ^ ief_vblank + ? and (hl) + ? ret +} + +asm void disable_lcd() { + ? call wait_for_vblank + ldh a,(reg_lcd_ctrl) + res 7,a + ldh (reg_lcd_ctrl),a + ? ret +} +asm void enable_lcd() { + ? call wait_for_vblank + ldh a,(reg_lcd_ctrl) + set 7,a + ldh (reg_lcd_ctrl),a + ? ret +} \ No newline at end of file diff --git a/include/gb_header_small.mfk b/include/gb_header_small.mfk new file mode 100644 index 00000000..62fb0752 --- /dev/null +++ b/include/gb_header_small.mfk @@ -0,0 +1,22 @@ +#if not(GAMEBOY) +#warn gb_header_small module should be only used on NES/Famicom targets +#endif + +#pragma zilog_syntax + +array __header @ $100 = [ + $00, $C3, $50, $01, $CE, $ED, $66, $66, $CC, $0D, $00, $0B, $03, $73, $00, $83, + $00, $0C, $00, $0D, $00, $08, $11, $1F, $88, $89, $00, $0E, $DC, $CC, $6E, $E6, + $DD, $DD, $D9, $99, $BB, $BB, $67, $63, $6E, $0E, $EC, $CC, $DD, $DC, $99, $9F, + $BB, $B9, $33, $3E, + "PROGRAM" ascii, 0,0,0,0,0,0,0,0, + 0,0, // no new license code + 0, // no SGB support + 0, // ROM only + 0, // 32 kB + 0, // no RAM + 0, // Japan + 0, // unlicensed + 0,0, // checksum, will patch later + 0,0 // global checksum, will patch later +] \ No newline at end of file diff --git a/include/gb_joy.mfk b/include/gb_joy.mfk new file mode 100644 index 00000000..163cd1bd --- /dev/null +++ b/include/gb_joy.mfk @@ -0,0 +1,47 @@ +#if not(GAMEBOY) +#warn gb_joy module should be only used on NES/Famicom targets +#endif + +import gb_hardware + +#pragma zilog_syntax + +// standard joystick driver for Game Boy + +import joy + +alias input_a = input_btn +byte input_b +byte input_select +byte input_start + +void read_joy() { + byte tmp + reg_joypad = $20 + asm { + ld a,(reg_joypad) + ld a,(reg_joypad) + ld a,(reg_joypad) + ld a,(reg_joypad) + } + tmp = reg_joypad + input_dx = 0 + input_dy = 0 + if tmp & 1 == 0 { input_dx += 1 } + if tmp & 2 == 0 { input_dx -= 1 } + if tmp & 4 == 0 { input_dy -= 1 } + if tmp & 8 == 0 { input_dy += 1 } + reg_joypad = $10 + asm { + ld a,(reg_joypad) + ld a,(reg_joypad) + ld a,(reg_joypad) + ld a,(reg_joypad) + } + byte tmp + tmp = reg_joypad ^ $ff + input_a = (tmp & 1) >> 0 + input_b = (tmp & 2) >> 1 + input_select = (tmp & 4) >> 2 + input_start = (tmp & 8) >> 3 +} \ No newline at end of file diff --git a/include/gb_small.ini b/include/gb_small.ini new file mode 100644 index 00000000..8d9bb012 --- /dev/null +++ b/include/gb_small.ini @@ -0,0 +1,34 @@ +[compilation] +arch=lr35902 +encoding=ascii +screen_encoding=ascii +modules=default_panic,stdlib,gb_hardware,gb_header_small + + +[allocation] + +segments=default,rom,hiram +default_code_segment=rom + +segment_default_start=$c000 +segment_default_end=$dfff + +segment_rom_start=$0150 +segment_rom_end=$7fff +segment_hiram_start=$ff80 +segment_hiram_end=$fffe + +[define] +GAMEBOY=1 +WIDESCREEN=0 +KEYBOARD=0 +JOYSTICKS=1 +HAS_BITMAP_MODE=0 + +[output] +style=single +format=rom:0:$7fff +gb_checksum=true +extension=gb + +