Deep Dive: Bankswitching

So, earlier on, I explained that NESmaker projects use Mapper 30, which has 32 banks of 16KB data. The NES console itself can only handle up to 32KB – or 2 NESmaker banks – of program data though. So how can we get access to all the data we have available on the cartridge? Well, this is where bankswitching comes in handy.

Bankswitching: explanation and an analogy

The NES’ CPU has 32KB of memory, divided in two 16KB parts (an upper bank, and a lower bank). A NESmaker game loads one bank of data in each of these two parts of memory. The upper bank of memory is “fixed” and it’s contents can’t be changed as long as the game is running. The lower bank of memory is swappable, which means that, through codes in the game, the cartridge can replace the lower bank’s memory with a new bank at any given time.

Let’s consider the banks to be instruction pages of a manual. The right page (which we will call Page 32) of the manual stays the same all the time; the left page of the manual can be replaced with a different page, when the instruction manual tells you to. For example, somewhere on page 32, these instructions are shown:

  • Take page 1 and put it on the left side.
  • Remember the number that’s on line 16 of the left page.
  • Take page 2 and put it on the left side.
  • Follow the instructions on line 8 of the left page.

On line 16 of page 1, it says:

  • The number is 4.

Line 8 of page 2 says:

  • Multiply that number by 2.

This is essentially what bankswitching does. The instructions on page 32 might have told us to take page 3 instead of page 1 to put on the left side, which may hold a different number on line 16, which means the result would have also been different.

This is also why the right page (or upper bank) is always the same: swapping out a bank while executing code on that bank may (and almost certainly will) result in unexpected results, more often than not crashing the game. If you’re reading a book, and halfway the page you’re jumping to a different page and go on reading from halfway that page, the story won’t make sense (and your brain might crash as well).

Bankswitching implemented in NESmaker

Please excuse the lenghty introduction; bankswitching is not that complex of a concept, but it’s kinda hard to put in words. I hope it makes sense so far though!

So, on to the Deep Dive part of the article: how is bankswitching implemented in NESmaker? In practice, it uses a subroutine (doBankswitchY) and two macros (SwitchBank and ReturnBank). Let’s take a look at the SwitchBank macro first; here’s the code, with my added comments to make sense of it all. The comments will mention various CPU registers, although I assume you’re already familiar with those at this point.

MACRO SwitchBank arg0      ;; arg0 = what bank to switch to
	TYA                ;; Transfer the Y-register value to the Accumulator
	PHA                ;; Push the Accumulator's value onto the stack
	LDA currentBank    ;; Load the current bank number into the Accumulator
	STA prevBank       ;; Store it in the previous bank variable
	LDY arg0           ;; Load the new bank number into the Y-register
	JSR doBankswitchY  ;; Execute the bankswitch
	PLA                ;; Pull the last value from the stack into the Accumulator
	TAY                ;; Transfer the accumulator's value to the Y-register
	ENDM               ;; This ends the macro

This script saves the Y-value on the stack (so the Y-register can be reused for bankswitching, and its original value can be retrieved after executing the bankswitching script). Then, it saves the current bank number as the previous bank number, which makes sense, because the current bank will become the previous bank as soon as the new bank is switched in. This previous bank variable will be used in the ReturnBank macro, to be able to switch back dynamically. Subsequently, the macro loads the new bank number into the Y-register and then will jump to the bankswitching subroutine. When the subroutine is done, the original Y-register value will be restored from the stack.

The ReturnBank macro looks quite a lot like the SwitchBank macro. The main difference is that it doesn’t take an arbitrary bank number as an argument, but rather uses the previous bank number to switch. This is why we’d save the previous bank number in the SwitchBank macro. The code looks like this:

MACRO ReturnBank
	TYA                ;; Transfer the Y-register value to the Accumulator
	PHA                ;; Push the Accumulator's value onto the stack
	LDY prevBank       ;; Load the previous bank number
	JSR doBankswitchY  ;; Execute the bankswitch
	PLA                ;; Pull the last value from the stack into the Accumulator
	TAY                ;; Transfer the accumulator's value to the Y-register
	ENDM               ;; This ends the macro

You’ll encounter this stack-based save and restore method (TYA/PHA and PLA/TAY) a lot throught the NESmaker codebase, so it’s good to know what it does and why it’s there.

Finally, let’s take a look at the script that does the actual bankswitching: doBankswitchY. In my opinion, this script looks less intimidating than you might expect. I’ve removed the lines of code that were commented out in the original file.

doBankswitchY:        ;; Start subroutine here
	STY currentBank    ;; Save the Y-register in the current bank variable
bankswitchNoSave:
	LDA currentBank    ;; Load the current bank variable into the Accumulator
	AND #%00011111     ;; Only use the last five values ($00-$1f)
	ORA chrRamBank     ;; Do a logical OR with the chrRamBank variable
	STA $c000          ;; Save the Accumulator's value to memory address $c000
	RTS                ;; This ends the subroutine

That’s it. In a nutshell, it takes the bank number to switch to and writes that number to address $c000, which initiates the actual bank switch. It seems like this code was prepared to handle CHR RAM switching as well, but the chrRamBank variable seems to be always zero and never changed throughout the entire NESmaker codebase. In other words, you might shave off two bytes and two clock cycles by removing or commenting out the ORA chrRamBank line, as it seems to do nothing. Also, as far as I could see, the value of currentBank will never exceed $1f, so the AND #%00011111 line may be commented out as well, saving two more bytes and clock cycles. Which isn’t much, but in NES development, every byte counts.

So there you have it! Now you know what bankswitching is and how NESmaker does it. I hope this article takes away a little bit of the mystical aura that may surround swapping banks.