aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile12
-rw-r--r--README.md11
-rw-r--r--basics/README.md1
-rw-r--r--basics/persist.s4
-rw-r--r--basics/unrom.s171
-rw-r--r--config/unrom.cfg58
6 files changed, 256 insertions, 1 deletions
diff --git a/Makefile b/Makefile
index c18c65d..c8ae5e7 100644
--- a/Makefile
+++ b/Makefile
@@ -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)
diff --git a/README.md b/README.md
index 49eaa04..e33b829 100644
--- a/README.md
+++ b/README.md
@@ -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;
+}