This looks a lot like the Mapper 113 implementation we use, just with CHR-RAM.
Personally, for full-32K bank switching I prefer to code individual NROMs with the bankswitching/jumping code in a fixed memory location, then glue them all together with an external utility (which I have written).
I just have a 16 o 32 bytes of RAM to share info between roms. The crt0 doesn't zero such area, and it is out of the C-controlled memory heap, so nothing can touch it but my code.
If you need more info about this I can post more details about my methods.
EDIT
cfg fileThis is the cfg file I use for every NROM in the project. I will compile each NROM separately, then extract the binaries and glue them together with a proper header using a custom utility. I'm attaching my utility plus sources. Can be modified for AOROM quite easily, I believe. It compiles with freeBASIC.
Code:
MEMORY {
ZP: start = $00, size = $100, type = rw, define = yes;
HEADER: start = $0, size = $10, file = %O ,fill = yes;
PRG: start = $8000, size = $7fc0, file = %O ,fill = yes, define = yes;
RJM: start = $ffc0, size = $3a, file = %O, fill = yes;
VECTORS: start = $7ffa, size = $6, file = %O, fill = yes;
CHR: start = $0000, size = $2000, file = %O, fill = yes;
# standard 2K SRAM (-zeropage)
# $0100 famitone, palette, cpu stack
# $0200 oam buffer
# $0300 shared table ($0F bytes)
# $0310..$800 ca65 stack
RAM: start = $0310, size = $04F0, define = yes;
}
SEGMENTS {
HEADER: load = HEADER, type = ro;
STARTUP: load = PRG, type = ro, define = yes;
LOWCODE: load = PRG, type = ro, optional = yes;
INIT: load = PRG, type = ro, define = yes, optional = yes;
CODE: load = PRG, type = ro, define = yes;
RODATA: load = PRG, type = ro, define = yes;
DATA: load = PRG, run = RAM, type = rw, define = yes;
VECTORS: load = VECTORS, type = rw;
ROMCHGR: load = RJM, type = rw;
CHARS: load = CHR, type = rw;
BSS: load = RAM, type = bss, define = yes;
HEAP: load = RAM, type = bss, optional = yes;
ZEROPAGE: load = ZP, type = zp;
}
FEATURES {
CONDES: segment = INIT,
type = constructor,
label = __CONSTRUCTOR_TABLE__,
count = __CONSTRUCTOR_COUNT__;
CONDES: segment = RODATA,
type = destructor,
label = __DESTRUCTOR_TABLE__,
count = __DESTRUCTOR_COUNT__;
CONDES: type = interruptor,
segment = RODATA,
label = __INTERRUPTOR_TABLE__,
count = __INTERRUPTOR_COUNT__;
}
SYMBOLS {
__STACKSIZE__: type = weak, value = $04F0; # 5 pages stack
}
Notice there's a custom segment called RJM at the end of the 32K ROM space, just before the vectors. That's where I place my bankswitching routine. This guarantees that the code is exactly at the same place in each rom, so when the bankswitch is performed the routine can keep executing.
Notice also that I've moved the cc65 stack $10 bytes upwards. Now, $300-$30F is reserved, untouched RAM, which I can use to communicate between roms. I also use this space to detect a RESET. In mapper 113, a power on pages in PRG-ROM 0, but a reset doesn't, so I have to handle this in software. A simple CRC check does the trick. More about this later.
crt0.sThis is the neslib crt0.s file with a couple of changes. First of all, I need to avoid it from clearing $300-$30F at startup, so I comment out this line:
Code:
[...]
clearRAM:
txa
@1:
sta $000,x
sta $100,x
sta $200,x
; sta $300,x ; <-- comment out
sta $400,x
sta $500,x
sta $600,x
sta $700,x
inx
bne @1
[...]
Next thing to do is adding the bankswitching code to the RJM segment, right before segment "VECTORS":
Code:
.segment "ROMCHGR"
_change_rom:
lda #0
sta PPU_MASK
sta PPU_CTRL
lda $0300
sta $4100
jmp start
_change_reg:
lda $0300
sta $4100
rts
.segment "VECTORS"
[...]
For a mapper with bus conflicts such as a plan GNROM the code would look like...
Code:
.segment "ROMCHGR"
_change_rom:
lda #0
sta PPU_MASK
sta PPU_CTRL
ldx $0300
lda bus_conflict_tbl, x
sta bus_conflict_tbl, x
jmp start
_change_reg:
ldx $0300
lda bus_conflict_tbl, x
sta bus_conflict_tbl, x
rts
bus_conflict_tbl:
.byte $00, $01, $02, $03 ; 1st PRG Bank $00-$03
.byte $10, $11, $12, $13 ; 2nd PRG Bank $04-$07
.byte $20, $21, $22, $23 ; 3rd PRG Bank $08-$0b
.byte $30, $31, $32, $33 ; 4th PRG Bank $0c-$0f
.endif
.segment "VECTORS"
[...]
Both pieces of code will use the byte at RAM address $0300 as the register byte. The meaning of such byte varies depending on the mapper. For GNROM it is xxPPxxCC; for Mapper 113 it is MCPPPCCC. A call to _change_rom will page in then jump to the "start" vector, useful for changing PRG-ROM and CHR-ROM in one go. _change_reg will just update the register, useful if you just need to change mirroring (in Mapper 113) or the CHR-ROM (changing the PRG-ROM bits will most likely end in a nice crash).
Using this stuffI've written some functions I use for bankswitching, reset handling, and stuff like that:
Code:
// MT MK2 NES v0.3
// Copyleft 2016 by The Mojon Twins
// Communication pool utilites.
// Needs
// #define COMM_POOL ((unsigned char*)0x0300)
// #define COMM_CHECKSUM COMM_POOK [0xf]
void m113_comm_pool_checksum_calculate (void) {
rda = 0; for (gpit = 1; gpit < 15; gpit ++) rda += COMM_POOL [gpit];
}
void m113_safe_change_chr_bank (unsigned char chr) {
rda = COMM_REG & 0xb8; // MCPPPCCC -> 10111000
if (chr & 8) { rda |= 0x40; chr &= 0x7; }
rda |= chr;
COMM_REG = rda;
__asm__ ("jmp _change_reg");
}
// You can comment out these from "head" ROM if space is tight:
unsigned char m113_comm_pool_checksum_check (void) {
m113_comm_pool_checksum_calculate (); // Written to rda
return (COMM_CHECKSUM == rda);
}
void m113_handle_reset (void) {
if (m113_comm_pool_checksum_check ()) {
COMM_CHECKSUM = 0xff; // Bad
return;
}
COMM_GAME_SELECT = 0x00;
COMM_REG = 0x80; // MCPPPCCC
__asm__ ("jmp _change_rom");
}
// You can comment out these from "child" ROMs if space is tight:
void m113_comm_pool_init (void) {
for (gpit = 1; gpit < 15; gpit ++) COMM_POOL [gpit] = gpit;
}
void m113_comm_pool_checksum_write (void) {
m113_comm_pool_checksum_calculate (); // Written to rda
COMM_CHECKSUM = rda;
}
void m113_rom_pair_jump (unsigned char prg, unsigned char chr) {
rda = 0x00; if (chr & 8) { rda |= 0x40; chr &= 0x7; }
rda |= (prg << 3);
rda |= chr;
COMM_REG = rda;
__asm__ ("jmp _change_rom");
}
Just place a call to m113_handle_reset at the beginning of your main in each module but the main one (ROM0), and a call to m113_comm_pool_init (); at the beginning of ROM0.
To jump to another ROM, just write what you need in COMM_REG [0x01-0x0f] ($301-$30F) and then:
Code:
m113_comm_pool_checksum_write ();
m113_rom_pair_jump (menu_prg [rdb], menu_chr [rdb]);
Works great. I've created a multicart with severan games in each rom and a complex game with different engines in each ROM using this method and it works.