Ideal MMC1/3 mapper writes code

This is an archive of a topic from NESdev BBS, taken in mid-October 2019 before a server upgrade.
View original topic
Ideal MMC1/3 mapper writes code
by on (#31124)
As the problem of writing to mappers can be somewhat a pain for some people arround, as an interrupts can cause a mapper write procedure to be interupted, and then the interrupt would want to write to the mapper on its own, making the write of the main programm fail.
Disabling interupt can be a solution, but this will delay the interupt and in some case you want to avoid this. I guess I'll post some code that will allow solutions to fix this. Those allow all interupt and main code to do any mapper write all the time, if used proprely. Of course any personal variant of those will work.

MMC1 writes needs only 2 zero-page variables, while MMC3 needs only one zero-page variable.

MMC1 : Main programm MMC1 write routine. Enter with A=Data to write and Y=High byte to adress you'd want to write to ($80-$9f for reg0, $a0-$bf for reg1, etc...). X is unnafected.
Code:
MMC1Write_Main
   sta MMC1WriteData
   sty MMC1WriteAdress
   ldy #$05
   inc MMC1WriteFlag
-  sta [MMC1WriteAdress-1],Y   ;Note that low byte doesn't matter. Last write always write to the good page since Y is zero
   lsr A
   dey
   bne -                                ;Those who want faster performence can unroll this loop
   dec MMC1WriteFlag
   rts


MMC1 : Code to write to the MMC1 in Reset, IRQ and/or NMI code (enter with A=Data to write). I use a fixed adress for faster performance, but it's also possible to implement it the fashion as it's above. You'd typically make as many copies of it as many MMC1 registers you actually write to during interups that should be treated fastly.
Code:
MMC1Write_Int
   inc Dummy  ;Make a PRG write with bit 7 set, to reset write counter
   sta $f000
   lsr A
   sta $f000
   lsr A
   sta $f000
   lsr A
   sta $f000
   lsr A
   sta $f000
   rts
Dummy
   .db $80


Code to be always called before returning (IRQ and NMIs). Also NMI shouldn't enable IRQs back if used.
Code:
   lda MMC1WriteFlag
   beq +
   dec MMC1WriteFlag        ;Reset the flag
   lda MMC1WriteData
   jsr MMC1Write_Main+4    ;Don't store adress and data, but load them instead
   pla   ;Restore Y (useless)
   pla
   tax   ;Restore X
   pla   ;Restore A (useless)
   plp   ;Restore P (exept Z and N flags which are affected, but who cares)
   pla   ;Discard return adress of MMC1Writa_Main (as the write is done)
   pla
   rts
+  pla
   tay
   pla
   tax
   pla
   rti

If anyone ever wants to enable back IRQs during NMI routine, and allow up to 3 layer of writes, but I'm pretty sure nobody will really wants to do this since NMI is supposed to execute fastly.

MMC3 write main programm routine, enter with A=Data and X=Adress :
Code:
MMC3Write_Main
   stx MMC3AdressLatch
   stx $8000
   sta $8001
   rts

MMC3 Routine to write to MMC3 during an interupt (NMI, IRQ) :
Code:
MMC3Write_Int
   stx $8000
   sta $8001
   lda MMC3AdressLatch
   sta $8000
   rts

Again, it only alows 2 layers as it, but it would be possible to use the same concept to make 3 layers.

by on (#31126)
Caution: Resetting the MMC1, such as through "inc Dummy", changes the PRG bankswitching mode to fixed-$C000. If a program uses fixed-$8000 or 32 KiB bankswitching, it won't be able to use this technique as easily.

by on (#31132)
MMC1:

One game I looked at had an interesting method for handling register writes. The idea was to delay, but not inhibit, the NMI code from running until the register access was done. Something like:
Code:
SafeBankSwitch:
        inc MMC1Lock
        sta $E000
        lsr a
        sta $E000
        lsr a
        sta $E000
        lsr a
        sta $E000
        lsr a
        sta $E000
        dec MMC1Lock
        beq +
        dec MMC1Lock
        jmp SubNMI
+       rts

NMI:    asl MMC1Lock
        bne +
        jsr SubNMI
+       rti

SubNMI: pha
        txa
        pha
        tya
        pha
        ...
        pla
        tay
        pla
        tax
        pla
        rts

This method does add some latency, however, so it might not be ideal if you do a lot of PPU stuff in your NMI code. However, the worse case for latency will only occur if the main program bankswitches at the time of NMI, which will usually mean the code has not finished a frame of animation, which in turn would mean the NMI would not have anything to send to the PPU. Even in the best case, however, you still lose several CPU cycles at the start of NMI.

MMC3:

Your code is fine, except you must write to MMC3AddressLatch BEFORE you write to $8000 in the bankswitch code. If you go the other way, and an interrupt occurs after the $8000 write but before the latch write, the register won't be properly configured when the interrupt returns. (Always think about what happens if your code is interrupted after each instruction. A number of bugs I've seen in commercial games could've been avoided if the developers followed this rule.)

by on (#31134)
Oh, god you both of you are right. Thanks for correcting me (I fixed it). Should be all right now, exept for those doing unusual PRG bankswitching on MMC1 (in this case delaying the interrupt is the only solution).

Also, I've trought about a programm who counts the writes and restore them exactly as they were, but it's impossible since an interrupt can always be caused between the write and the count change, regardless where you place it.

dvdmth, I knew about this method, but when you think about it, wouldn't write a value with bit 7 clear to $2000 and write it's normal value back after the write is done have the EXACT same effect as the code you posted ? The NMI will be delayed, but still will happen.

by on (#31137)
Bregalad wrote:
wouldn't write a value with bit 7 clear to $2000 and write it's normal value back after the write is done have the EXACT same effect as the code you posted ? The NMI will be delayed, but still will happen.

I just tested this carefully and it works. Nice!
Code:
nmi:    bit $2002               ; clear VBL flag
        ; print low byte of PC from stack
        ; ...
        rti

.align 256 ; force low byte of PC to 0
mmc_write:
        lda #0                  ; 00 Disable NMI
        sta PPUCTRL             ; 02
        nop                     ; 05 Do uninterruptable code
        nop                     ; 06
        nop                     ; 07
        nop                     ; 08
        lda #PPUCTRL_NMI        ; 09 Re-enable NMI
        sta PPUCTRL             ; 0B
        nop                     ; 0E These are just to see where NMI occurs
        nop                     ; 0F
        rts                     ; 10

When run on NES with NMI occurring one clock later on each line, these are the low bytes of the PC on entry to NMI handler. The NMI never interrupts the critical sequence of four NOPs, and is never suppressed (that would generate a blank line in the test).

00
00
02
02
05
05
05
05
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
10