diff options
| author | Miquel Sabaté Solà <msabate@suse.com> | 2023-11-07 14:14:55 +0100 |
|---|---|---|
| committer | Miquel Sabaté Solà <mikisabate@gmail.com> | 2025-02-04 21:02:07 +0100 |
| commit | cb854722b9890c171f7d0600812a7e9a3a2a25c9 (patch) | |
| tree | 1d9bce62db538471b1c98970115999b8ff93b43e | |
| parent | 988332816972638adfa8000dd7e1a474c5fea424 (diff) | |
| download | code.nes-cb854722b9890c171f7d0600812a7e9a3a2a25c9.tar.gz code.nes-cb854722b9890c171f7d0600812a7e9a3a2a25c9.zip | |
basics: Add an example on UNROM
Signed-off-by: Miquel Sabaté Solà <msabate@suse.com>
| -rw-r--r-- | Makefile | 12 | ||||
| -rw-r--r-- | README.md | 11 | ||||
| -rw-r--r-- | basics/README.md | 1 | ||||
| -rw-r--r-- | basics/persist.s | 4 | ||||
| -rw-r--r-- | basics/unrom.s | 171 | ||||
| -rw-r--r-- | config/unrom.cfg | 58 |
6 files changed, 256 insertions, 1 deletions
@@ -1,4 +1,6 @@ CC65 ?= cl65 +CA65 ?= ca65 +LD65 ?= ld65 CCOPTS ?= --verbose --target nes .PHONY: all @@ -7,6 +9,8 @@ all: clean deps build .PHONY: clean clean: @rm -rf out + @find . -type f -name "*.o" -delete + @find . -type f -name "*.nes" -delete @mkdir -p out/basics .PHONY: deps @@ -17,11 +21,17 @@ deps: build: basics space .PHONY: basics -basics: +basics: unrom $(CC65) $(CCOPTS) basics/sprite.s -o out/basics/sprite.nes $(CC65) $(CCOPTS) basics/input.s -o out/basics/input.nes $(CC65) $(CCOPTS) basics/persist.s -o out/basics/persist.nes +.PHONY: unrom +unrom: + $(CA65) $(CCOPTS) basics/unrom.s -o basics/unrom.o + $(LD65) basics/unrom.o -C config/unrom.cfg -o out/basics/unrom.nes + @rm -f basics/unrom.o + .PHONY: space space: @cd space && CC65=$(CC65) CCOPTS="$(CCOPTS)" $(MAKE) @@ -20,6 +20,17 @@ NES memory. A safe bet is to go with either [Mesen](https://github.com/SourMesen/Mesen2/), which provide tools like RAM watchers or a full debugger. +Notice that I will use some terms without actually introducing them. That is, I +expect you to go over the [NES Dev +wiki](https://www.nesdev.org/wiki/Nesdev_Wiki) for glossary or for full +documentation on the stuff being shown here. For example, if I am writing an +example code using the UNROM or the MMC1 chips, I assume that you will go over +the [NES Dev wiki](https://www.nesdev.org/wiki/Nesdev_Wiki) for details on what +these chips actually are or how they were used. That is, if you find that on a +bunch of comments I write stuff like "memory mapper", "MMC3 chip", "OAM" or +stuff like that, just go to the [NES Dev +wiki](https://www.nesdev.org/wiki/Nesdev_Wiki) to get a better picture. + The examples are distributed like this: - `basics`: simple examples which cover basic stuff for NES development. These diff --git a/basics/README.md b/basics/README.md index 7571509..3e4c3a9 100644 --- a/basics/README.md +++ b/basics/README.md @@ -13,3 +13,4 @@ Anyhow, these programs are as follows: to get a detailed explanation on each section. - `input.s`: how to read the input from one controller. - `persist.s`: using the MMC1 chip in order to persist data. +- `unrom.s`: bank switching using the UNROM chip. diff --git a/basics/persist.s b/basics/persist.s index 392797f..430654f 100644 --- a/basics/persist.s +++ b/basics/persist.s @@ -11,6 +11,10 @@ ;; Last but not least, all of this is done thanks to the MMC1 chip. Thus, this ;; file is also a minimalistic example on how to configure it, even if it ;; doesn't take full advantage of it (e.g. we are not doing any bank switching). +;; Because of this, note that we are not using a proper linker configuration for +;; it (I'm using cc65's default, which is tailored for NROM chips). Thus, bank +;; switching will actually never work under this setup. Bank switching is +;; covered through other examples like `basics/unrom.s`. .segment "HEADER" .byte 'N', 'E', 'S', $1A diff --git a/basics/unrom.s b/basics/unrom.s new file mode 100644 index 0000000..c6e9fd8 --- /dev/null +++ b/basics/unrom.s @@ -0,0 +1,171 @@ +;;; +;; This is an example on how you can perform bank switching. In order to do this +;; I am using the UNROM chip (iNES mapper 2), which is one of the first chips to +;; ever support this. Because of this, it's also one of the simplest, which is +;; quite convenient in order to learn this technique. +;; +;; The program will do the following: +;; - Switch to bank 0. +;; - Call a function from bank 0 that will set $10 to #2. +;; - Switch to bank 1. +;; - Call a function from bank 1 that will set $11 to #3. +;; - Sum the values from $10 and $11 and store them in $12. +;; +;; At this point there are two things you can do in order to check that the +;; whole thing worked: +;; +;; 1. The values on $10, $11 and $12 are as expected (2, 3, and 5 +;; respectively). +;; 2. The last bank we switched was 1, which has a fill value on linker +;; configuration (see `config/unrom.cfg`) of `$f9`. Thus, check that except +;; from the code on `hello_bank1`, the rest of the values on $8000-$BFFF +;; are set to `$f9` (i.e. by using FCEUX's Debug -> Hex editor). + +.segment "HEADER" + .byte 'N', 'E', 'S', $1A + .byte $08 ; 128KB of PRG-ROM (8 x 16KB) + .byte $00 ; No CHR-ROM. Games using this chip used RAM instead. + + ;; On the 6th byte we have to set the lower nibble of the mapper (#%0010 for + ;; UNROM). + .byte $20 + + ;; Forcing iNES 2.0 format, which will help us for the next bytes. + .byte $08 + + ;; And now iNES 2.0-specific thingies. + .byte $00 ; No submapper + .byte $00 ; PRG ROM not 4 MiB or larger + .byte $00 ; No PRG RAM + .byte $07 ; 8192 (64 * 2^7) bytes CHR RAM, no battery + .byte $00 ; NTSC; use $01 for PAL + .byte $00 ; No special PPU + +.segment "VECTORS" + .addr nmi, reset, irq + +;;; +;; Note that we now define code on bank 0 and bank 1. These names are not magic: +;; they are defined on the linker configuration being used (see +;; `config/unrom.cfg`). All swappable banks are called `BANK#`. The bank that is +;; not swappable is simply called `FIXED`, and that's the bank where most of the +;; code from this example will reside. + +.segment "BANK0" + +hello_bank0: + lda #2 + sta $10 + rts + +.segment "BANK1" + +hello_bank1: + lda #3 + sta $11 + rts + +;;; +;; The rest of the banks are simply not used by this example. + +.segment "BANK2" +.segment "BANK3" +.segment "BANK4" +.segment "BANK5" +.segment "BANK6" + +;;; +;; And the following code is "fixed". That is, it is stored at $C000-$FFFF, +;; which is not swappable. You can check this by using FCEUX's hex editor, for +;; example. + +.segment "FIXED" + +;; This table contains the actual sauce of bank switching. In general, the UNROM +;; does bank switching by writing to $8000-$BFFF, but the value we are writing +;; there *must* match the value located at the destination address in ROM +;; (otherwise we get a bus conflict). Writing to this table already ensures that +;; this never happens because the indexing will match the actual value. +banktable: + .byte $00, $01, $02, $03, $04, $05, $06 + +;; Variable containing the bank we are currently in. It's useful to keep track +;; of the bank so the NMI handler can restore it if it does some bank switching +;; of its own. +m_current_bank = $00 + +;; Perform a bankswitch by using the value on the `y` register. Note that you +;; can use the `bankswitch_nosave` variant, which is useful if you just want to +;; perform a temporary bankswitch (e.g. on NMI code). +bankswitch: + sty m_current_bank +bankswitch_nosave: + tya + sta banktable, y + rts + +;; Unused +nmi: +irq: + rti + +;; Check `basics/sprite.s` for a deeper look on the logic below. I have only +;; added code after configuration/reset is done. +reset: + sei + cld + ldx #$40 + stx $4017 + + ldx #$ff + txs + + inx + stx $2000 + stx $2001 + stx $4010 + +@vblankwait1: + bit $2002 + bpl @vblankwait1 + + ldx #0 + lda #0 +@ram_reset_loop: + sta $000, x + sta $100, x + sta $200, x + sta $300, x + sta $400, x + sta $500, x + sta $600, x + sta $700, x + inx + bne @ram_reset_loop + +@vblankwait2: + bit $2002 + bpl @vblankwait2 + + ;;; + ;; NOTE: configuration/reset is done, the code below is our actual program :D + + ;; Switch to bank 0 and call the function from there. + ldy #0 + jsr bankswitch + jsr hello_bank0 + + ;; The same but on bank 1. + ldy #1 + jsr bankswitch + jsr hello_bank1 + + ;; And now we can add the numbers that were saved by both banks. + lda $10 + clc + adc $11 + sta $12 + + ;; Loop forever, there's nothing to be done here. +@loop: + jmp @loop diff --git a/config/unrom.cfg b/config/unrom.cfg new file mode 100644 index 0000000..5b18707 --- /dev/null +++ b/config/unrom.cfg @@ -0,0 +1,58 @@ +## +# This is a very minimalistic linker configuration for UNROM chips. This +# configuration assumes the standard UNROM configuration, with two regions +# defined that split $8000-$FFFF, where the first half is swappable. For the +# bank switching there are 7 banks, which coupled with the fixed region it sums +# up 128KB of total PRG-ROM. +# +# NOTE: it assumes that only assembly is being used, and thus a lot of magic +# required for C programs is missing. +# NOTE: it is following the example of games such as Castlevania or Megaman. +# Thus, there is no CHR segment because it's all done through RAM. + +MEMORY { + # iNES header. + HEADER: start = $0, size = $10, fill = yes; + + # Program RAM. Available if a battery-backed RAM was requested. + WRAM: start = $6000, size = $2000, define = yes; + + # Swappable ROM addresses. Note the filled values. This is done so it's also + # clear by inspecting the memory which bank we are on. This can come in + # handy when inspecting things with an hex editor. + PRG0: start = $8000, size = $4000, fill = yes, fillval = $f8, define = yes; + PRG1: start = $8000, size = $4000, fill = yes, fillval = $f9, define = yes; + PRG2: start = $8000, size = $4000, fill = yes, fillval = $fa, define = yes; + PRG3: start = $8000, size = $4000, fill = yes, fillval = $fb, define = yes; + PRG4: start = $8000, size = $4000, fill = yes, fillval = $fc, define = yes; + PRG5: start = $8000, size = $4000, fill = yes, fillval = $fd, define = yes; + PRG6: start = $8000, size = $4000, fill = yes, fillval = $fe, define = yes; + + # Fixed ROM address. + PRG: start = $C000, size = $4000, fill = yes, fillval = $ff, define = yes; +} + +SEGMENTS { + # iNES header. + HEADER: load = HEADER, type = ro; + + # This is the fixed bank, that spans $C000-$FFFF. Make sure to put the reset + # code and basic stuff that you don't want ever to be gone here. This will + # include stuff like the reset code, bank switching utilities, etc. + FIXED: load = PRG, type = ro, define = yes; + + ## + # Swappable banks. + + BANK0: load = PRG0, type = ro, define = yes; + BANK1: load = PRG1, type = ro, define = yes; + BANK2: load = PRG2, type = ro, define = yes; + BANK3: load = PRG3, type = ro, define = yes; + BANK4: load = PRG4, type = ro, define = yes; + BANK5: load = PRG5, type = ro, define = yes; + BANK6: load = PRG6, type = ro, define = yes; + + # Last but not least, the vectors must be the last thing and they are + # expecting a very special place on the fixed bank. + VECTORS: load = PRG, start = $fffa, type = ro; +} |
