Nerdy-Nights: CHR Bank Switching Question

Discuss technical or other issues relating to programming the Nintendo Entertainment System, Famicom, or compatible systems.

Moderator: Moderators

Post Reply
justin-rwx
Posts: 12
Joined: Sun Jan 10, 2021 1:12 pm

Nerdy-Nights: CHR Bank Switching Question

Post by justin-rwx » Thu Jan 28, 2021 12:01 am

Would someone be able to explain how bank switching is completed in the example below? The Nerdy-Nights tutorial uses the CNROM mapper.

1. How are the CHR bank numbers (which I assume are .bank 2 and .bank 3) tied to the Bankvalues at all? Where is the link?
2. What does storing the value $01 to a Bankvalues location do to trigger the program to 'bank switch'?

Image

Code: Select all

  .bank 2
  .org $0000
  .incbin "mario0.chr"   ;includes 8KB graphics file from SMB1
  
  .bank 3
  .org $0000
  .incbin "mario1.chr"   ;includes 8KB graphics file from SMB1, modified
The full code below

Code: Select all

  .inesprg 1   ; 1x 16KB PRG code
  .ineschr 2   ; 2x  8KB CHR data
  .inesmap 3   ; mapper 3 = CNROM, 8KB CHR bank swapping
  .inesmir 1   ; background mirroring
  

;;;;;;;;;;;;;;;

    
  .bank 0
  .org $C000 
RESET:
  SEI          ; disable IRQs
  CLD          ; disable decimal mode
  LDX #$40
  STX $4017    ; disable APU frame IRQ
  LDX #$FF
  TXS          ; Set up stack
  INX          ; now X = 0
  STX $2000    ; disable NMI
  STX $2001    ; disable rendering
  STX $4010    ; disable DMC IRQs

vblankwait1:       ; First wait for vblank to make sure PPU is ready
  BIT $2002
  BPL vblankwait1

clrmem:
  LDA #$00
  STA $0000, x
  STA $0100, x
  STA $0200, x
  STA $0400, x
  STA $0500, x
  STA $0600, x
  STA $0700, x
  LDA #$FE
  STA $0300, x
  INX
  BNE clrmem
   
vblankwait2:      ; Second wait for vblank, PPU is ready after this
  BIT $2002
  BPL vblankwait2


LoadPalettes:
  LDA $2002             ; read PPU status to reset the high/low latch
  LDA #$3F
  STA $2006             ; write the high byte of $3F00 address
  LDA #$00
  STA $2006             ; write the low byte of $3F00 address
  LDX #$00              ; start out at 0
LoadPalettesLoop:
  LDA palette, x        ; load data from address (palette + the value in x)
                          ; 1st time through loop it will load palette+0
                          ; 2nd time through loop it will load palette+1
                          ; 3rd time through loop it will load palette+2
                          ; etc
  STA $2007             ; write to PPU
  INX                   ; X = X + 1
  CPX #$20              ; Compare X to hex $10, decimal 16 - copying 16 bytes = 4 sprites
  BNE LoadPalettesLoop  ; Branch to LoadPalettesLoop if compare was Not Equal to zero
                        ; if compare was equal to 32, keep going down



LoadSprites:
  LDX #$00              ; start at 0
LoadSpritesLoop:
  LDA sprites, x        ; load data from address (sprites +  x)
  STA $0200, x          ; store into RAM address ($0200 + x)
  INX                   ; X = X + 1
  CPX #$20              ; Compare X to hex $20, decimal 32
  BNE LoadSpritesLoop   ; Branch to LoadSpritesLoop if compare was Not Equal to zero
                        ; if compare was equal to 32, keep going down
              
              

  LDA #%10000000   ; enable NMI, sprites from Pattern Table 1
  STA $2000

  LDA #%00010000   ; enable sprites
  STA $2001

Forever:
  JMP Forever     ;jump back to Forever, infinite loop
  
 

NMI:
  LDA #$00
  STA $2003       ; set the low byte (00) of the RAM address
  LDA #$02
  STA $4014       ; set the high byte (02) of the RAM address, start the transfer


LatchController:
  LDA #$01
  STA $4016
  LDA #$00
  STA $4016       ; tell both the controllers to latch buttons


ReadA: 
  LDA $4016       ; player 1 - A
  AND #%00000001  ; only look at bit 0
  BEQ ReadADone   ; branch to ReadADone if button is NOT pressed (0)
                  ; add instructions here to do something when button IS pressed (1)
  LDA $0203       ; load sprite X position
  CLC             ; make sure the carry flag is clear
  ADC #$01        ; A = A + 1
  STA $0203       ; save sprite X position
ReadADone:        ; handling this button is done
  

ReadB: 
  LDA $4016       ; player 1 - B
  AND #%00000001  ; only look at bit 0
  BEQ ReadBDone   ; branch to ReadBDone if button is NOT pressed (0)
                  ; add instructions here to do something when button IS pressed (1)
  LDA $0203       ; load sprite X position
  SEC             ; make sure carry flag is set
  SBC #$01        ; A = A - 1
  STA $0203       ; save sprite X position
ReadBDone:        ; handling this button is done


ReadSelect: 
  LDA $4016       ; player 1 - select
  AND #%00000001  ; only look at bit 0
  BEQ ReadSelectDone   ; branch to ReadSelectDone if button is NOT pressed (0)
                  ; add instructions here to do something when button IS pressed (1)

  LDA #$00
  JSR Bankswitch  ; change to graphics bank 0

ReadSelectDone:   ; handling this button is done


ReadStart: 
  LDA $4016       ; player 1 - start
  AND #%00000001  ; only look at bit 0
  BEQ ReadStartDone   ; branch to ReadStartDone if button is NOT pressed (0)
                  ; add instructions here to do something when button IS pressed (1)

  LDA #$01
  JSR Bankswitch  ; change to graphics bank 1

ReadStartDone:   ; handling this button is done


  
  RTI             ; return from interrupt
 
 
 
 
 
 
 
Bankswitch:
  TAX                    ;;copy A into X
  STA Bankvalues, X      ;;new bank to use
  RTS

Bankvalues:
  .db $00, $01, $02, $03 ;;bank numbers


 
 
;;;;;;;;;;;;;;  
  
  
  
  .bank 1
  .org $E000
palette:
  .db $0F,$31,$32,$33,$34,$35,$36,$37,$38,$39,$3A,$3B,$3C,$3D,$3E,$0F
  .db $0F,$1C,$15,$14,$31,$02,$38,$3C,$0F,$1C,$15,$14,$31,$02,$38,$3C

sprites:
     ;vert tile attr horiz
  .db $80, $32, $00, $80   ;sprite 0
  .db $80, $33, $00, $88   ;sprite 1
  .db $88, $34, $00, $80   ;sprite 2
  .db $88, $35, $00, $88   ;sprite 3

  .org $FFFA     ;first of the three vectors starts here
  .dw NMI        ;when an NMI happens (once per frame if enabled) the 
                   ;processor will jump to the label NMI:
  .dw RESET      ;when the processor first turns on or is reset, it will jump
                   ;to the label RESET:
  .dw 0          ;external interrupt IRQ is not used in this tutorial
  
  
;;;;;;;;;;;;;;  
  
  
  .bank 2
  .org $0000
  .incbin "mario0.chr"   ;includes 8KB graphics file from SMB1
  
  .bank 3
  .org $0000
  .incbin "mario1.chr"   ;includes 8KB graphics file from SMB1, modified

User avatar
aa-dav
Posts: 154
Joined: Tue Apr 14, 2020 9:45 pm
Location: Russia

Re: Nerdy-Nights: CHR Bank Switching Question

Post by aa-dav » Thu Jan 28, 2021 1:38 am

justin-rwx wrote:
Thu Jan 28, 2021 12:01 am

1. How are the CHR bank numbers (which I assume are .bank 2 and .bank 3) tied to the Bankvalues at all? Where is the link?
It is https://wiki.nesdev.com/w/index.php/Bus_conflict.
In short: simple mappers doesn't prevent ROM from doing it's work while CPU read/writes address. So ROM (even in write operation - for simplicity again) writes to data bus byte from address it sees on the address bus. So data bus gets byte from ROM and byte from you and simpliest way to make things work is to provide they will be the same. Just write byte 03 to the location with byte 03.
You should check if mapper has bus conflict in the wiki.

User avatar
tokumaru
Posts: 12003
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: Nerdy-Nights: CHR Bank Switching Question

Post by tokumaru » Thu Jan 28, 2021 6:02 am

justin-rwx wrote:
Thu Jan 28, 2021 12:01 am
1. How are the CHR bank numbers (which I assume are .bank 2 and .bank 3) tied to the Bankvalues at all? Where is the link?
There's no link. The .bank directive is a requirement of NESASM, it's the way it controls the sizes of the binaries it generates. NESASM "banks" are always 8KB, which's not always true on the NES, which has mappers commonly using bank sizes of 16KB and 32KB in addition to 8KB. You may still use NESASM's banking features (such as the function that retrieves bank numbers from labels), but you may need to divide bank numbers by 2 or 4 if you're working with mappers that use 16KB or 32KB banks, respectively.
2. What does storing the value $01 to a Bankvalues location do to trigger the program to 'bank switch'?
It's not the value in the table that triggers the bankswitch, it's the act of trying to write to ROM that does it. But since some mappers do not disable the ROM during these writes, you have to do the mapper writes to ROM locations that contain the same value that you're writing, otherwise there will be two chips (the CPU and the ROM) outputting different values into the data bus, resulting in the mapper picking up the wrong bank number. If both chips are outputting the save value, there are no conflicts.

User avatar
Quietust
Posts: 1687
Joined: Sun Sep 19, 2004 10:59 pm
Contact:

Re: Nerdy-Nights: CHR Bank Switching Question

Post by Quietust » Thu Jan 28, 2021 6:10 am

Tokumaru beat me to the punch, though I spent long enough typing this message that I'm going to post it anyways.
justin-rwx wrote:
Thu Jan 28, 2021 12:01 am
1. How are the CHR bank numbers (which I assume are .bank 2 and .bank 3) tied to the Bankvalues at all? Where is the link?
Since NESASM uses the same set of .bank numbers for both PRG and CHR, they aren't going to match - you need to make sure all of the PRG banks are at the beginning and all of the CHR banks are at the end.

Beyond that, writing a value of 0 will select the first CHR bank, writing a value of 1 will select the second bank, 2 selects the third bank, and 3 selects the fourth bank (i.e. the bank numbers are zero-based).
justin-rwx wrote:
Thu Jan 28, 2021 12:01 am
2. What does storing the value $01 to a Bankvalues location do to trigger the program to 'bank switch'?
On the cartridge is a 74HC161 chip which acts as a simple 4-bit register, and that chip is connected to the CPU bus in such a way that performing a write to anywhere within $8000-$FFFF will cause the chip to store the bottom 2 bits of the data byte that was written. The corresponding output pins on that chip are then connected to the upper address lines on the CHR ROM, causing the PPU to see a different set of 8KB of data when it tries to read the pattern tables.

As aa-dev and tokumaru have mentioned, this particular mapper does not disable the PRG ROM chip during the write, so you need to write the bank number to a location in memory that contains that same value. Other mappers contain extra circuitry to protect against this (e.g. the NES-ANROM board does this using an additional 74HC02 chip) but are slightly more expensive (which is important if you're producing hundreds of thousands of cartridges, as Nintendo did).
Quietust, QMT Productions
P.S. If you don't get this note, let me know and I'll write you another.

justin-rwx
Posts: 12
Joined: Sun Jan 10, 2021 1:12 pm

Re: Nerdy-Nights: CHR Bank Switching Question

Post by justin-rwx » Thu Jan 28, 2021 10:50 am

All,

Thanks for all of the great responses.

So fundamentals time - the Bankvalues code below is read-only memory?

Code: Select all

Bankvalues:
  .db $00, $01, $02, $03 ;;bank numbers
Am I correct to assume that at any point during the program code I could write a value between $00 and $03 to any memory location that contains the same value, and it will bank switch to the bank of that value? (Assuming CNROM)

What about if I set a variable to contain the hex value $01, and later in the code I write a hex value $01 to that variable. Will a CHR bank switching occur? If so, why use the Bankvalues .db at all, and why not just use something like in the code below. Is it the same effect?

Code: Select all

CHR_BANK1 = $01
.
.
.
LDA = #$01
STA = CHR_BANK1

User avatar
tokumaru
Posts: 12003
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: Nerdy-Nights: CHR Bank Switching Question

Post by tokumaru » Thu Jan 28, 2021 12:00 pm

justin-rwx wrote:
Thu Jan 28, 2021 10:50 am
the Bankvalues code below is read-only memory?
Yup.
Am I correct to assume that at any point during the program code I could write a value between $00 and $03 to any memory location that contains the same value, and it will bank switch to the bank of that value? (Assuming CNROM)
Yes. We normally use a table and an index register when the bank number is defined at run-time:

Code: Select all

	lda BankNumber
	tax
	sta Bankvalues, x
But when the bank number is known at assembly-time you can actually do this:

Code: Select all

	lda #$02
	sta *-1
This is ca65 syntax, where * represents the current PC address (other assemblers might have different ways to get the PC value), and since the instructions are stored in ROM, you can simply "store" the value you just loaded over the last byte of the previous instruction (PC - 1).

What about if I set a variable to contain the hex value $01, and later in the code I write a hex value $01 to that variable. Will a CHR bank switching occur? If so, why use the Bankvalues .db at all, and why not just use something like in the code below. Is it the same effect?
CHR_BANK1 = $01
Here you're saying that the symbol/label CHR_BANK1 represents the constant value/address $01. This value cannot be changed.
LDA = #$01
STA = CHR_BANK1
This is not proper 6502 syntax (what are those "="?) but if you did LDA #$01 and then STA CHR_BANK1, you'd be storing value $01 into address $01, because that's the address you assigned to CHR_BANK1 before. If that's what you want, that's fine. You can then use and index register like I mentioned above to switch a bank based on this variable:

Code: Select all

	lda CHR_BANK1
	tax
	sta Bankvalues, x

tepples
Posts: 22288
Joined: Sun Sep 19, 2004 11:12 pm
Location: NE Indiana, USA (NTSC)
Contact:

Re: Nerdy-Nights: CHR Bank Switching Question

Post by tepples » Thu Jan 28, 2021 3:55 pm

In general: Writes to RAM ($0000-$07FF), PPU registers ($2000-$27FF), and APU registers ($4000-$401F) do not cause the cartridge to perform a bank switch. Only writes to the ROM area ($8000-$FFFF) cause any sort of bank switching.

(This assumes the most common mappers for homebrew productions: CNROM, UNROM, AOROM, BNROM, MMC1, and MMC3. Less common mappers can show exceptions to this rule, as they snoop PPU or APU activity.)

Post Reply