From fcf4af6c55d73a9dd8629c3c7eb24686d4935fa1 Mon Sep 17 00:00:00 2001 From: Karol Stasiak Date: Sat, 17 Mar 2018 17:09:30 +0100 Subject: [PATCH] NES mapper experiments --- doc/api/famicom-programming-guide.md | 20 +- doc/api/target-platforms.md | 4 +- examples/README.md | 2 + examples/nes/nestest.mfk | 1 + examples/nes/nestest_mmc4.mfk | 269 +++++++++++++++++++++++++++ include/nes_mmc4.ini | 64 +++++++ include/nes_mmc4.mfk | 32 ++++ include/nes_small.ini | 7 +- 8 files changed, 391 insertions(+), 8 deletions(-) create mode 100644 examples/nes/nestest_mmc4.mfk create mode 100644 include/nes_mmc4.ini create mode 100644 include/nes_mmc4.mfk diff --git a/doc/api/famicom-programming-guide.md b/doc/api/famicom-programming-guide.md index 994988bd..4e9d89ea 100644 --- a/doc/api/famicom-programming-guide.md +++ b/doc/api/famicom-programming-guide.md @@ -33,21 +33,33 @@ The minimal Famicom program thus looks like this: To use a mapper of your choice, create a new `.ini` file with the definitions you need. The most important ones are `[output]format` and `[allocation]segments`. +Currently, its a bit inconvenient to create programs using mappers that change the bank containing the interrupt vectors. +Therefore, it's recommended to stick to mappers that have a fixed bank at the end of the address space. + +Mappers that should be fine: NROM (0), CNROM (1), UxROM(2), MMC2 (9), MMC3 (4), MMC4 (10), MMC6 (4). + +Mappers that can have arbitrary bank at the end and are therefore not recommended: MMC1 (1), MMC5 (5). + You should define at least three segments: * `default` – from $200 to $7FF, it will represent the physical RAM of the console. -* `chrrom` (sample name) – from $0000 to $3FFF, it will represent the CHRROM (if you need more). +* `chrrom` (sample name) – from $0000 to $1FFF, it will represent the CHRROM +(if you need more, you can make it bigger, up to $ffff, or even add another segment of CHRROM). +Put there only arrays with pattern tables. Don't read from them directly, it won't work. -* `prgrom` (sample name) – from either $8000 or $C000 to $FFFF, it will contain the code of your program. +* `prgrom` (sample name) – it will contain the code of your program and read-only data. +Each segment should be defined in a range it is going to be switched into. You should set the `default_code_segment` to the segment that contains the $FFxx addresses. If your mapper supports it, you can add more CHRROM or PRGROM segments, just specify them correctly in the `[output]format` tag. -The `[output]format` tag should contain a valid iNES header of the mapper of your choice and then all the segments in proper order. +The `[output]format` tag should contain a valid iNES or NES 2.0 header of the mapper of your choice +and then all the segments in proper order (first PRGROM, then CHRROM). +See [the MMC4 example](../../include/nes_mmc4.ini) to see how it can be done. -See [the NesDev wiki](https://wiki.nesdev.com/w/index.php/INES) for more info about the iNES file format. +See [the NesDev wiki](https://wiki.nesdev.com/w/index.php/NES_2.0) for more info about the NES 2.0 file format. diff --git a/doc/api/target-platforms.md b/doc/api/target-platforms.md index 42742f5f..216a3ebb 100644 --- a/doc/api/target-platforms.md +++ b/doc/api/target-platforms.md @@ -27,7 +27,9 @@ The following platforms are currently supported: * `pet` – Commodore PET -* `nes_small` – a tiny 32K PRGROM + 8K CHRROM Famicom/NES program +* `nes_small` – a tiny 32K PRGROM + 8K CHRROM Famicom/NES program, using iNES mapper 0 (NROM) + +* `nes_mcc4` – a 128K PRGROM + 128K CHRROM + extra 8KRAM Famicom/NES program, using iNES mapper 10 (MMC4) For more complex programs, you need to create your own "platform" definition. Read [the NES programming guide](./famicom-programming-guide.md) for more info. diff --git a/examples/README.md b/examples/README.md index 7254fb02..2957b204 100644 --- a/examples/README.md +++ b/examples/README.md @@ -28,6 +28,8 @@ * [NES 101 tutorial example](nes/nestest.mfk) – a port of the tutorial example from the NES 101 tutorial by Michael Martin +* [MMC4 example](nes/nestest_mmc4.mfk) – the same thing as above, but uses a MMC4 mapper just to test bankswitching + ## Apple II examples * [Bell](apple2/bell.mfk) – a program that goes \*ding!\* diff --git a/examples/nes/nestest.mfk b/examples/nes/nestest.mfk index 0369d3a0..3338572d 100644 --- a/examples/nes/nestest.mfk +++ b/examples/nes/nestest.mfk @@ -1,4 +1,5 @@ // Based upon the example code from NES 101 tutorial by Michael Martin +// compile with -t nes_small void main() { init_graphics() diff --git a/examples/nes/nestest_mmc4.mfk b/examples/nes/nestest_mmc4.mfk new file mode 100644 index 00000000..80946510 --- /dev/null +++ b/examples/nes/nestest_mmc4.mfk @@ -0,0 +1,269 @@ +// Based upon the example code from NES 101 tutorial by Michael Martin +// compile with -t nes_mmc4 + +void main() { + init_graphics() + init_input() + init_sound() + ppu_ctrl = %10001000 + ppu_mask = %00011110 + while(true){} +} + +void nmi() { + set_prg_bank(1) + scroll_screen() + set_prg_bank(2) + update_sprite() + set_prg_bank(3) + react_to_input() +} + +void irq() { + +} + +array oam_buffer [256] @$200 +byte scroll +byte a +segment(ram) sbyte dx + +void init_graphics() { + set_chr_bank0($06) + set_chr_bank1($1A) + init_sprites() + set_prg_bank(6) + load_palette() + load_name_tables() + init_scrolling() +} + +void init_input() { + a = 0 +} + +void init_sound() { + apu_status = 1 + apu_pulse1_sweep = 0 + apu_frame_counter = $40 +} + +void init_sprites() { + byte i + for i,255,downto,0 { + oam_buffer[i] = 0 + } + oam_buffer[0] = $70 + oam_buffer[1] = 1 + oam_buffer[3] = 1 + dx = 1 +} + +segment(prgrom6) +noinline void load_palette() { + byte i + ppu_set_addr($3f00) + for i,0,until,palette.length { + ppu_write_data(palette[i]) + } +} + +segment(prgrom6) +noinline void load_name_tables() { + pointer p + byte page_count + byte b + + read_ppu_status() + p = bg.addr + ppu_set_addr($2400) + for page_count,0,until,4 { + for b,0,until,255 { + ppu_write_data(p[b]) + } + p.hi += 1 + } + for page_count,0,until,4 { + for b,0,until,255 { + ppu_write_data(0) + } + } +} + +void init_scrolling() { + scroll = 240 +} + +segment(prgrom2) +noinline void update_sprite() { + ppu_oam_dma_write(oam_buffer.addr.hi) + if oam_buffer[3] == 0 { + dx = 1 + high_c() + } else if oam_buffer[3] == 255-8 { + dx = -1 + high_c() + } + oam_buffer[3] += dx +} + +segment(prgrom3) +noinline void react_to_input() { + strobe_joypad() + if read_joypad1() & 1 != 0 { // A button + reverse_dx() + } + read_joypad1() // B button + read_joypad1() // Select button + read_joypad1() // Start button + if read_joypad1() & 1 != 0 { // Up button + if oam_buffer[0] > 7 { oam_buffer[0] -= 1} + } + if read_joypad1() & 1 != 0 { // Down button + if oam_buffer[0] < 223 { oam_buffer[0] += 1} + } +} + +segment(prgrom3) +void reverse_dx() { + dx = 0 - dx + low_c() +} + +segment(prgrom3) +void low_c() { + apu_pulse1_ctrl = $84 + apu_pulse1_period = $9AA +} + +segment(prgrom2) +void high_c() { + apu_pulse1_ctrl = $86 + apu_pulse1_period = $869 +} + +segment(prgrom1) +noinline void scroll_screen() { + ppu_write_data(0) + if scroll != 0 { + scroll -= 1 + ppu_set_scroll(0, scroll) + } +} + +array bg = [ + " " ascii, + "12345678901234567890123456789012" ascii, + " " ascii, + " " ascii, + " PRESENTING NES 101 " ascii, + " A GUIDE FOR OTHERWISE " ascii, + " EXPERIENCED PROGRAMMERS " ascii, + " " ascii, + " TUTORIAL FILE BY " ascii, + " MICHAEL MARTIN " ascii, + " " ascii, + " " ascii, + " " ascii, + " PRESS UP AND DOWN TO SHIFT " ascii, + " THE SPRITE " ascii, + " " ascii, + " PRESS A TO REVERSE DIRECTION" ascii, + " " ascii, + " " ascii, + " " ascii, + " " ascii, + " " ascii, + " " ascii, + "CHARACTER SET HIJACKED FROM " ascii, + "COMMODORE BUSINESS MACHINES " ascii, + " (C64'S CHARACTER ROM)" ascii, + " " ascii, + "READY. " ascii, + " " ascii, + " " ascii, + $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00, + $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00, + $00,$00,$00,$00,$00,$00,$00,$00,$F0,$F0,$F0,$F0,$F0,$F0,$F0,$F0, + $FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$0F,$0F,$0F,$0F,$0F,$0F,$0F,$0F +] + + +array palette = [ + $0E,$00,$0E,$19,$00,$00,$00,$00,$00,$00,$00,$00,$01,$00,$01,$21, + $0E,$20,$22,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 +] + +segment(chrrom0) +array charset @$6200 = [ + $00,$00,$00,$00,$00,$00,$00,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $18,$18,$18,$18,$00,$00,$18,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $66,$66,$66,$00,$00,$00,$00,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $66,$66,$FF,$66,$FF,$66,$66,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $18,$3E,$60,$3C,$06,$7C,$18,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $62,$66,$0C,$18,$30,$66,$46,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $3C,$66,$3C,$38,$67,$66,$3F,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $06,$0C,$18,$00,$00,$00,$00,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $0C,$18,$30,$30,$30,$18,$0C,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $30,$18,$0C,$0C,$0C,$18,$30,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $00,$66,$3C,$FF,$3C,$66,$00,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $00,$18,$18,$7E,$18,$18,$00,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $00,$00,$00,$00,$00,$18,$18,$30,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $00,$00,$00,$7E,$00,$00,$00,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $00,$00,$00,$00,$00,$18,$18,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $00,$03,$06,$0C,$18,$30,$60,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $3C,$66,$6E,$76,$66,$66,$3C,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $18,$18,$38,$18,$18,$18,$7E,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $3C,$66,$06,$0C,$30,$60,$7E,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $3C,$66,$06,$1C,$06,$66,$3C,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $06,$0E,$1E,$66,$7F,$06,$06,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $7E,$60,$7C,$06,$06,$66,$3C,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $3C,$66,$60,$7C,$66,$66,$3C,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $7E,$66,$0C,$18,$18,$18,$18,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $3C,$66,$66,$3C,$66,$66,$3C,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $3C,$66,$66,$3E,$06,$66,$3C,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $00,$00,$18,$00,$00,$18,$00,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $00,$00,$18,$00,$00,$18,$18,$30,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $0E,$18,$30,$60,$30,$18,$0E,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $00,$00,$7E,$00,$7E,$00,$00,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $70,$18,$0C,$06,$0C,$18,$70,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $3C,$66,$06,$0C,$18,$00,$18,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $3C,$66,$6E,$6E,$60,$62,$3C,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $18,$3C,$66,$7E,$66,$66,$66,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $7C,$66,$66,$7C,$66,$66,$7C,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $3C,$66,$60,$60,$60,$66,$3C,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $78,$6C,$66,$66,$66,$6C,$78,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $7E,$60,$60,$78,$60,$60,$7E,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $7E,$60,$60,$78,$60,$60,$60,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $3C,$66,$60,$6E,$66,$66,$3C,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $66,$66,$66,$7E,$66,$66,$66,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $3C,$18,$18,$18,$18,$18,$3C,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $1E,$0C,$0C,$0C,$0C,$6C,$38,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $66,$6C,$78,$70,$78,$6C,$66,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $60,$60,$60,$60,$60,$60,$7E,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $63,$77,$7F,$6B,$63,$63,$63,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $66,$76,$7E,$7E,$6E,$66,$66,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $3C,$66,$66,$66,$66,$66,$3C,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $7C,$66,$66,$7C,$60,$60,$60,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $3C,$66,$66,$66,$66,$3C,$0E,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $7C,$66,$66,$7C,$78,$6C,$66,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $3C,$66,$60,$3C,$06,$66,$3C,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $7E,$18,$18,$18,$18,$18,$18,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $66,$66,$66,$66,$66,$66,$3C,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $66,$66,$66,$66,$66,$3C,$18,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $63,$63,$63,$6B,$7F,$77,$63,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $66,$66,$3C,$18,$3C,$66,$66,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $66,$66,$66,$3C,$18,$18,$18,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $7E,$06,$0C,$18,$30,$60,$7E,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $3C,$30,$30,$30,$30,$30,$3C,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $0C,$12,$30,$7C,$30,$62,$FC,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $3C,$0C,$0C,$0C,$0C,$0C,$3C,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $00,$18,$3C,$7E,$18,$18,$18,$18,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF, + $00,$10,$30,$7F,$7F,$30,$10,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF +] + +segment(chrrom1) +array sprites @$A000 = [ + $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00, + $18,$24,$66,$99,$99,$66,$24,$18,$00,$18,$18,$66,$66,$18,$18,$00 +] \ No newline at end of file diff --git a/include/nes_mmc4.ini b/include/nes_mmc4.ini new file mode 100644 index 00000000..1d2a52a8 --- /dev/null +++ b/include/nes_mmc4.ini @@ -0,0 +1,64 @@ +; a NES MMC4 cartridge, uses iNES mapper 10 +; 8×16 kB PRGROM and 32×4kB CHRROM +; $c000–$ffff is assumed to be fixed to prgrom7, $8000-$bfff can be switched +; uses horizontal mirroring: +; - to use vertical mirroring, change byte #6 of the header from $A0 to $A1 +; uses extra 8K of RAM at $6000–$7fff that is not battery-backed: +; - to disable it, change byte #10 of the header from $07 to 0 and remove the ram segment +; - to make it battery-backed, change byte #10 of the header from $07 to $77 +; output file size: 262160 bytes + +[compilation] +arch=ricoh +modules=nes_hardware,nes_routines,default_panic,nes_mmc4 +ro_arrays=true + +[allocation] +zp_pointers=all + +segments=default,ram,prgrom0,prgrom1,prgrom2,prgrom3,prgrom4,prgrom5,prgrom6,prgrom7,chrrom0,chrrom1 +default_code_segment=prgrom7 + +segment_default_start=$200 +segment_default_end=$7ff + +segment_ram_start=$6000 +segment_ram_end=$7fff + +segment_prgrom7_start=$c000 +segment_prgrom7_end=$ffff + +segment_prgrom0_start=$8000 +segment_prgrom0_end=$bfff + +segment_prgrom1_start=$8000 +segment_prgrom1_end=$bfff + +segment_prgrom2_start=$8000 +segment_prgrom2_end=$bfff + +segment_prgrom3_start=$8000 +segment_prgrom3_end=$bfff + +segment_prgrom4_start=$8000 +segment_prgrom4_end=$bfff + +segment_prgrom5_start=$8000 +segment_prgrom5_end=$bfff + +segment_prgrom6_start=$8000 +segment_prgrom6_end=$bfff + +segment_chrrom0_start=$0000 +segment_chrrom0_end=$ffff + +segment_chrrom1_start=$0000 +segment_chrrom1_end=$ffff + +[output] +style=single +format=$4E,$45,$53,$1A, 8,16,$A0,8, 0,0,$07,0, 2,0,0,0, prgrom0:$8000:$bfff,prgrom1:$8000:$bfff,prgrom2:$8000:$bfff,prgrom3:$8000:$bfff,prgrom4:$8000:$bfff,prgrom5:$8000:$bfff,prgrom6:$8000:$bfff,prgrom7:$c000:$ffff,chrrom0:$0000:$ffff,chrrom1:$0000:$ffff + +extension=nes + + diff --git a/include/nes_mmc4.mfk b/include/nes_mmc4.mfk new file mode 100644 index 00000000..8df04216 --- /dev/null +++ b/include/nes_mmc4.mfk @@ -0,0 +1,32 @@ +asm inline void set_prg_bank(byte a) { + STA $A000 + ? RTS +} + +// calling with with e.g. $1E will set $0000-$0fff to $E000-$EFFF in chrrom1 +asm inline void set_chr_bank0(byte a) { + STA $B000 + STA $C000 + ? RTS +} + +// calling with with e.g. $1E will set $1000-$1fff to $E000-$EFFF in chrrom1 +asm inline void set_chr_bank1(byte a) { + STA $D000 + STA $E000 + ? RTS +} + +// TODO: FD/FE latches + +asm inline void set_vertical_mirroring() { + LDA #0 + STA $F000 + ? RTS +} +asm inline void set_horizontal_mirroring() { + LDA #1 + STA $F000 + ? RTS +} + diff --git a/include/nes_small.ini b/include/nes_small.ini index b0385498..f5331bc5 100644 --- a/include/nes_small.ini +++ b/include/nes_small.ini @@ -1,7 +1,8 @@ ; a very simple NES cartridge format ; uses mapper 0 and no bankswitching, so it's only good for very simple games -; assumes CHRROM is at chrrom:$0000-$3fff and PRGROM is at prgrom:$8000-$ffff +; assumes CHRROM is at chrrom:$0000-$1fff and PRGROM is at prgrom:$8000-$ffff ; uses horizontal mirroring; to use vertical mirroring, change byte #6 of the header from 0 to 1 +; output file size: 40976 bytes [compilation] arch=ricoh @@ -21,11 +22,11 @@ segment_prgrom_start=$8000 segment_prgrom_end=$ffff segment_chrrom_start=$0000 -segment_chrrom_end=$3fff +segment_chrrom_end=$1fff [output] style=single -format=$4E,$45,$53,$1A, 2,2,0,0, 0,0,0,0, 0,0,0,0, prgrom:$8000:$ffff, chrrom:$0000:$3fff +format=$4E,$45,$53,$1A, 2,1,0,0, 0,0,0,0, 0,0,0,0, prgrom:$8000:$ffff, chrrom:$0000:$1fff extension=nes