Bank-switching for Beginners

This is an archive of a topic from NESdev BBS, taken in mid-October 2019 before a server upgrade.
View original topic
Bank-switching for Beginners
by on (#52740)
Before I dive into the murky void of bank-switching can I just ask a couple of fundamental questions;

1) (I think I know the answer to this already but) Can you bank-switch DPCM?
2) As well as bank-switching graphics, I presume you can bank switch code? If so, what's the protocol for switching PRG, calling routines and returning?
3) If I'm going to go down the bank-switching route, what's the most common bank-switchable mapper/ROM configuration? I thinking here from the point of view of PowerPak support and also the possibility of building carts/using socketed/EPROM boards etc.
Re: Bank-switching for Beginners
by on (#52741)
neilbaldwin wrote:
Before I dive into the murky void of bank-switching can I just ask a couple of fundamental questions;

1) (I think I know the answer to this already but) Can you bank-switch DPCM?

You can but IMO it's not useful. This is a subjective matter tough.
Quote:
2) As well as bank-switching graphics, I presume you can bank switch code? If so, what's the protocol for switching PRG, calling routines and returning?

Yes technically you can banswitch PRG-ROM, CHR-ROM, and more rarely PRG-RAM and CHR-RAM.
What you actually can do depends on the cartridge hardware you use.
I haven't developped anything with bankswitched PRG-ROM yet so I can't really help you about a protocol. I wrote something about switching the whole 32k in a user-friendly way, and another way to go is with last 16k fixed at $c000-$ffff, and first 16k switchable at $8000-$bfff.
Quote:
3) If I'm going to go down the bank-switching route, what's the most common bank-switchable mapper/ROM configuration? I thinking here from the point of view of PowerPak support and also the possibility of building carts/using socketed/EPROM boards etc.

I guess mapper 2 is the most common with it's 16k switchable / 16k fixed sheme. Most advanced mappers such as MMCs are usually used in a way that simulate this bankswitching sheme.
Re: Bank-switching for Beginners
by on (#52742)
neilbaldwin wrote:
Before I dive into the murky void of bank-switching can I just ask a couple of fundamental questions;

1) (I think I know the answer to this already but) Can you bank-switch DPCM?


Yes, but I think you have to have your samples at $C000, so you need a mapper setup that will let you swap that section out.

Quote:
2) As well as bank-switching graphics, I presume you can bank switch code? If so, what's the protocol for switching PRG, calling routines and returning?


Yes. You can swap out anything in PRG-ROM. Mappers typically let you swap out banks either 8k or 16k in size. One bank will be fixed (unswappable). The fixed bank is usually the last one, since the vectors are at $FFFA-FFFF. Most games I've looked at have a bankswapping subroutine in the fixed bank.

Quote:
3) If I'm going to go down the bank-switching route, what's the most common bank-switchable mapper/ROM configuration? I thinking here from the point of view of PowerPak support and also the possibility of building carts/using socketed/EPROM boards etc.


As Bregalad said, Mapper 2 (UxROM) is common. Sivak's game Battle Kid uses UOROM and it is going to be a cart release. UxROM will give you problems with swapping DPCM though since $C000-FFFF is the fixed bank. I think I read once that somebody (Memblers?) made an altered UNROM that had $8000-BFFF fixed and $C000-FFFF swappable though.

I've only worked with UxROM, so I can't comment on other mappers.

by on (#52744)
Thanks for the information, much appreciated.

OK, scenario time...

So, I definitely want to "reserve" $C000-> for DPCM. And I've got $8000-$FFFF as fixed. I need space for the tracker editor and also for the playback routine but both won't fit in the space $8000-$BFFF.

What's my next move? :)

Presumably I could have the music player in a switchable bank and do the bank switching somewhere in my NMI, call the music refresh routine, switch the bank back, JMP back to where I left off? If so, can the music refresh code, which resides in the switched bank, still see the SRAM area ($6000-$7FFF)?

by on (#52746)
Quote:
Presumably I could have the music player in a switchable bank and do the bank switching somewhere in my NMI, call the music refresh routine, switch the bank back, JMP back to where I left off?

Well I guess the "standard" way to do is to banswitch it in and call it from NMI (who is typically fixed bank), but yeah you have to back-up the old bank and restore it after the sound code returns. I don't know if that's what you described.
Re: Bank-switching for Beginners
by on (#52747)
neilbaldwin wrote:
If so, what's the protocol for switching PRG, calling routines and returning?

If the mapper you use has a fixed bank, you typically place all the bankswitching routines in it. If the mapper switches the whole 32KB space at once, it's good practice to replicate a small piece of code across all banks, containing the reset code and the bankswitching routines, so that they appear to be always mapped at the same location, no matter what bank is active.
Re: Bank-switching for Beginners
by on (#52754)
MetalSlime wrote:
I think I read once that somebody (Memblers?) made an altered UNROM that had $8000-BFFF fixed and $C000-FFFF swappable though.

You're thinking of the Crazy Climber board, which is an UNROM or UOROM board modded to replace the quad OR gate (74HC32) with a quad AND gate (74HC08). This fixes $8000-$BFFF to bank 0 and allows swapping $C000-$FFFF. You lose six bytes at the end of each bank for vectors, but that's no problem because DPCM ignores the last 15 bytes of each sample anyway. MMC1 can be configured to work this way as well (control register set to bank mode 1, bank location 0). And the upshot: RetroZone sells boards compatible with UOROM and MMC1 software, so you don't have to worry about finding a highly uncommon* donor cart.

If you want to run the music once per vblank, you could have the NMI handler just set a flag that your main code spins on. Final Fantasy does this, as does LJ65.

The small piece of replicated code that tokumaru mentions is often called a "trampoline".


* Can't really say "rare" without making people think of A*ROM.

by on (#52767)
:(

Can someone knock up a bit of code to show me how to set up for MMC1 bank switching using fixed $C000-FFFF and 2 switchable banks @ $8000?

I think I've got as far as putting the banks in the right place.... :S

by on (#52769)
MMC1 has one switchable PRG bank.

But fixed $C000 is the scenario you get when you reset the mapper anyway.

by on (#52770)
neilbaldwin wrote:
:(

Can someone knock up a bit of code to show me how to set up for MMC1 bank switching using fixed $C000-FFFF and 2 switchable banks @ $8000?

I think I've got as far as putting the banks in the right place.... :S

Someone asked if I could include this type of thing in NESICIDE and I had always planned on providing something like a "New Project Wizard" that includes "Mapper Interfacing". I had gotten as far as putting the mechanics together to be able to provide the wizard but none of the content, which is what you are after. I know someone will probably reply to this before I get that done [with a well-formed, ready-to-assemble ROM shell], but your post has reinvigorated my interest in getting something like that done inside my tool. It will be very similar to the MS Visual Studio "// TODO: Insert your specialized command handler code here" type of functional shell with inserted guidance comments.

by on (#52774)
Sorry Neil, we're stealing your thread, it will be split I guess (split me). I would like to help you with the MMC1 but I never tried yet, only MMC3/VRC6/VCR7.

@Nesicide:
Regarding your editor, if it could be more like visual studio 2008, with an MDI interface with tabbed documents, syntax highlighting, mouse over help, intellisense for known symbols by scope, support for ca65 (at the least) and other assemblers: I would be using it right away.

An editor that help beginner is great and all but even advanced users wants one with more options. I wanted to make one (like many project on my todo list) but gave up since I have no more free time these days. I just came from work and it's already midnight... So forget hobbies..

Then if you could debug, put break points in your code: even better. But if the format is all binary like the current build and not text, being a full time developer during the day, I would skip it.

My comments on the subject. I really would like to see that editor available some day. Remote debugging with your own source code/comments/symbols.. That would be nice.

by on (#52776)
NESICIDE wrote:
neilbaldwin wrote:
:(

Can someone knock up a bit of code to show me how to set up for MMC1 bank switching using fixed $C000-FFFF and 2 switchable banks @ $8000?

I think I've got as far as putting the banks in the right place.... :S

Someone asked if I could include this type of thing in NESICIDE and I had always planned on providing something like a "New Project Wizard" that includes "Mapper Interfacing". I had gotten as far as putting the mechanics together to be able to provide the wizard but none of the content, which is what you are after. I know someone will probably reply to this before I get that done [with a well-formed, ready-to-assemble ROM shell], but your post has reinvigorated my interest in getting something like that done inside my tool. It will be very similar to the MS Visual Studio "// TODO: Insert your specialized command handler code here" type of functional shell with inserted guidance comments.


Good idea.

Seriously, someone throw something into the ring. I can't remember the last time I spent such an utterly fruitless time trying to get something working on the NES. I've got better things to do (yes, that's me admitting defeat :) ) and I'd like to know I can get this working before diving in with a massive chunk of the tracker editor.

In the meantime I'm going to strip down my reset/NMI code so I can show you my non-working code.

by on (#52777)
Right, this is what I've got. It seems to be working now (perhaps the stripping down tidied it up a bit). Am I on the right lines?;

Code:
; iNES header

; iNES identifier
.byte "NES",$1a

; Number of PRG-ROM blocks
.byte $03

; Number of CHR-ROM blocks
.byte $01

; ROM control bytes: Horizontal mirroring, no SRAM
; or trainer, Mapper #0
;.byte $00, $00
.byte %00010000, %00000000         ;MMC1 with SRAM

; Filler
.byte $00,$00,$00,$00,$00,$00,$00,$00   
      
SCREEN   EQU $2000
PPU0   EQU $2000
PPU1   EQU $2001
PPU_STATUS   EQU $2002

   ;Zero page variables
   ENUM $0000
tempvec   db 0,0
tmp0   db 0
tmp1   db 0
tmp2   db 0
tmp3   db 0
tmp4   db 0
tmp5   db 0
tmp6   db 0
tmp7   db 0
   ENDE

;--------------------------------------------------------------------------------
; BANK 00
;--------------------------------------------------------------------------------
   ORG $0000
   BASE $8000
bank00   inc $FE
   rts
   PAD $BFFA
   DW vblank, reset, irq
;--------------------------------------------------------------------------------
; BANK 01
;--------------------------------------------------------------------------------

   BASE $8000
bank01   inc $FF
   rts
   PAD $BFFA
   DW vblank, reset, irq

;--------------------------------------------------------------------------------
; FIXED BANK
;--------------------------------------------------------------------------------
      
   ORG $C000
reset   sei

   jsr vblankwait
   jsr resetMMC1
   lda #%01100   ;Set bank layout
   jsr setMMC1r0
   lda #$00
   jsr setPRGBank
      
   ;clear RAM
   lda #$00
   ldx #$00
_reset_0   sta $0000,x
   sta $0100,x
   sta $0200,x
   sta $0300,x
   sta $0400,x
   sta $0500,x
   sta $0600,x
   sta $0700,x
   inx
   bne _reset_0

   ;reset Stack
   ldx #$FF
   txs

   lda #$00
   sta PPU0
   sta PPU1

   ;set PPU registers
   lda #%10001000
   sta PPU0
   lda #%00011010
   sta PPU1

main_loop   jmp main_loop
   
vblankwait   bit $2002
   bpl vblankwait
   rts
   
vblankendwait   bit $2002
   bmi vblankendwait
   rts

vblank   ;rti
   pha
   txa
   pha
   tya
   pha
   bit $2002

   jsr screen_off

   lda #>SCREEN
   sta $2006
   lda #<SCREEN
   sta $2006
   lda $Fe      ;write content of $FE and $FE to screen
   sta $2007
   lda $FF
   sta $2007
   
   lda #%00011110
   jsr screen_on
   
   lda #$00      ;call routine in bank 00 to inc $FE
   jsr setPRGBank
   jsr bank00

   lda #$01      ;call routine in bank 01 to inc $FF
   jsr setPRGBank
   jsr bank01
   
   pla
   tay
   pla
   tax
   pla
   rti

irq   rti

setPRGBank   
   sta $E000
   lsr a
   sta $E000
   lsr a
   sta $E000
   lsr a
   sta $E000
   lsr a
   sta $E000
   rts
   
screen_on   sta $2001
   ldx #$00
   stx $2005
   stx $2005
   stx $2006
   stx $2006
   rts
   
screen_off   lda #%00010000
   sta $2001
   sta $2005
   sta $2005
   sta $2006
   sta $2006
   rts
         
resetMMC1   ldx #$7F
@b   inx
   bne @a
   rts
@a   stx $8000
   bne @b
   rts
   
setMMC1r0   sta $9fff
   lsr a
   sta $9fff
   lsr a
   sta $9fff
   lsr a
   sta $9fff
   lsr a
   sta $9fff
   rts


   ORG $FFFA
   DW vblank, reset, irq
   
;--------------------------------------------------------------------------------
; CHR ROM
;--------------------------------------------------------------------------------
   BASE $0000
   incbin "set2.chr"
   ;incbin "set2.chr"
   ALIGN $2000
   
;--------------------------------------------------------------------------------
; END OF ROM
;--------------------------------------------------------------------------------

            

by on (#52780)
Well neil I guess you pretty much got it, in order to work with the MMC1 you have to write routines that does write 5 bits to it's registers and just call them.

For resetting the MMC1, you typically just do it once at reset by doing inc to somewhere in ROM that contains a value greater than $80 (such as $fffb, the high byte of your reset vector). I don't know what you were trying to do here, but it looks overly complicated. Just do it one in the reset code and you should be allright.

The problem is what happens if you try to write to a register NMI, if you were unlucky enough to have your main code interrupted when it was doing a register write. The most common solution is to set a flag each time you write to a MMC1 register, and skip the sound code in NMI if this flag is set. Another solution is to disable NMIs when writing to the MMC1, and enable it back when the write is done (which can ocasionally make VBlank time slightly shorter).
The "infamous" solution is posted here but I guess it works only if you use the standard 16k variable, 16k fixed bankwitching (which is the most common).

by on (#52784)
Megaman 2 has an infamous bug where the MMC1 bankswitch routine can be interrupted by the NMI, if there are lots of objects on the screen causing lag. Some tool assisted speedruns expoit the bug.

by on (#52788)
Bregalad wrote:
Well neil I guess you pretty much got it, in order to work with the MMC1 you have to write routines that does write 5 bits to it's registers and just call them.

For resetting the MMC1, you typically just do it once at reset by doing inc to somewhere in ROM that contains a value greater than $80 (such as $fffb, the high byte of your reset vector). I don't know what you were trying to do here, but it looks overly complicated. Just do it one in the reset code and you should be allright.

The problem is what happens if you try to write to a register NMI, if you were unlucky enough to have your main code interrupted when it was doing a register write. The most common solution is to set a flag each time you write to a MMC1 register, and skip the sound code in NMI if this flag is set. Another solution is to disable NMIs when writing to the MMC1, and enable it back when the write is done (which can ocasionally make VBlank time slightly shorter).
The "infamous" solution is posted here but I guess it works only if you use the standard 16k variable, 16k fixed bankwitching (which is the most common).


LOL @ myself. I read in one of the NES MMCx wikis that to reset the MMC1 you write $80 to $FF to the register. That's what my reset loop is doing, in a rather clumsy way. Oh dear :D

I was just thinking about the NMI issue, couldn't you increment a flag in the NMI, read it at the start of your bank switching routine and also at the end and if it's changed, write the bank switch again?

Kind of like;

Code:

NMI:
  inc NMI_counter
  rti

SET_BANK:
  ldx NMI_counter
  sta $E000
  lsr a
  etc
  cpx NMI_counter
  bne SET_BANK
  rts

by on (#52789)
Quote:
I was just thinking about the NMI issue, couldn't you increment a flag in the NMI, read it at the start of your bank switching routine and also at the end and if it's changed, write the bank switch again?

Oh my this is clever, much better than what I did. You'll have to reset the mapper in NMI before writing tough (implying this method can't be used if you use a non-standard bankswitching sheme), and like my complex solution, you can't use the same bankswitching routines inside and outside NMI.

So in summary when you use MMC1 :
- If your NMI timing is VERY tight and if you use the standard banswitching method, do what Neil sugested (and forgot about my complex approach)
- If it is acceptable to have a slightly shorter NMI, disable NMI (via $2000) between the MMC1 writes and enable it back after that.

by on (#52795)
This NMI handler will not interfere with bankswitching:
Code:
.proc nmi_handler
  inc retraces
  rti
.endproc

Then the main loop can spin on that flag to see when to blast VRAM updates.

by on (#52796)
tepples wrote:
This NMI handler will not interfere with bankswitching:
Code:
.proc nmi_handler
  inc retraces
  rti
.endproc


True.... :)