I was bored this afternoon and I decided to do some experiment on how to build a mapper 7 setup (also valuable on any mapper that allow full 32kb switching at the same time like MMC1 or MMC5)
The problem comes from the 32kb bankswitching. It can be quite troublesome, because as code is often executed from ROM, when writing to a register in order to do banswitching, this will immediately bring us in the new bank without having the time to jump somewhere safe, and this could be quite a hard task to handle.
Fortunately, if you put a string of bytes that is the same in all banks to do this, you can jsr to a bankswitching routine which is the exactly the same in all banks. Or you could have a routine in RAM that changes the adress of the ROM while it's not used, but you'd still have to come with one RESET; NMI and IRQ routine per bank.
I have no idea how commercial games handled this (I haven't even looked in them at all), but I guess it would be really great if you could jsr to a routine in another bank just as easily as if it were in the same bank (in the view of the main code). By tricking the stack a little this is possible to do, and I made it so it would be as much ROM efficient as possible.
So I have a tiny kernel of about 100 bytes which you will be able to put in all PRGROM banks, and that kernel handles interrupts and bankswitching.
If you wanted to do call a routine normally you'll do that :
And with that kernel, all you have to change if the routine is not in the same bank is :
In fact there is 2 versions of it, the normal (uncommented) version, and a slightly faster version (that is commented) that uses rts insead of jmp (indirect), but you'll have to add a -1 after the .dw
And I use now parenthesis for indirection as dish suggested so that it is compatible with 65816 syntax.
The kernel will automatically save the old bank, bankswitch the new bank, and resume the old before returning after the .db statement to continue the code normally. That way you can do many subroutine calls from all banks to all banks very easily. The only drawback is that it will take more stack space and more CPU time but you get nothing for noting. Also, it's impossible to pass arguments with the A or Y registers, only X could be used, and that applies for both input and output arguments which is a shame.
According to WLA doccumentation, it does supports multiple labels with the same name if and only if they are at the same adress. By wroting this I could verify this is not true, no matter what I try duplicate labels at the same adress are not supported. However, I evnetually did a trick to not have to get rid of labels and get everything manutally.
You could just copy/pase the code and add numbers after labels for each bank but this is ugly and if you want to change the kernel it will be troublesome to go changing in each bank.
So I just use local labels, and afterthat, I use .export directive so that they are available to the rest of the programm. I get warnings for exporting many times the same label, but not any errors so this comiles fine
In order to make the Longjsr as transparent as possible I'd liked to have a macro under WLA that would automatically call Longjsr if the caller is not in the same bank as the called, and just a normal jsr otherwise.
Unfortunately I can't get it to work, I keep getting errors. I have the following macro, if anyone has the solution what is wrong please tell me :
Let me know if you like the kernel and what could be improved.
The problem comes from the 32kb bankswitching. It can be quite troublesome, because as code is often executed from ROM, when writing to a register in order to do banswitching, this will immediately bring us in the new bank without having the time to jump somewhere safe, and this could be quite a hard task to handle.
Fortunately, if you put a string of bytes that is the same in all banks to do this, you can jsr to a bankswitching routine which is the exactly the same in all banks. Or you could have a routine in RAM that changes the adress of the ROM while it's not used, but you'd still have to come with one RESET; NMI and IRQ routine per bank.
I have no idea how commercial games handled this (I haven't even looked in them at all), but I guess it would be really great if you could jsr to a routine in another bank just as easily as if it were in the same bank (in the view of the main code). By tricking the stack a little this is possible to do, and I made it so it would be as much ROM efficient as possible.
So I have a tiny kernel of about 100 bytes which you will be able to put in all PRGROM banks, and that kernel handles interrupts and bankswitching.
Code:
_Table
.db $00, $01, $02, $03
.db $04, $05, $06, $07
_Reset
sei
cld
__ lda #:Start
sta _b+1.w ;Bankswitch start bank in
sta LastBank.w
jmp Start ;Go to actual rest code
_NMI
pha ;NMI interrup
txa
pha
tya
pha
lda LastBank.w
pha
lda #:NMI ;Save state and get bank for actual code
tay
sta _Table.w,Y
jmp NMI ;Jump to actual interrupt code
_IRQ ;Same as above
pha
txa
pha
tya
pha
lda LastBank.w
pha
lda #:IRQ
tay
sta _Table.w,Y
jmp IRQ
_endInt
pla ;Restore state
jsr _MapperWrite
pla
tay
pla
tax
pla
rti
_LongJSR ;This jsr to any routine in any bank easily
pla
sta PointerL
clc
adc #$03
tay
pla
sta PointerH ;Get pointer where return adress is
adc #$00 ;and add 3 to it
pha
tya
pha
lda LastBank.w
pha ;Push old bank number
lda #>(_MapperReturn-1)
pha
lda #<(_MapperReturn-1)
pha ;Return adress to restore old bank
/* ldy #$02 ;VERSION N°1
lda (Pointer),Y
pha
dey
lda (Pointer),Y
pha
ldy #$03
lda (Pointer),Y
tay
sta _Table.w,Y ;Bankswitch the bank we want
sta LastBank
rts */
ldy #$01 ;VERSION N°2
lda (Pointer),Y
sta JumpPtrL.w
iny
lda (Pointer),Y
sta JumpPtrH.w
iny
lda (Pointer),Y
tay
sta _Table.w,Y ;Could be sta (Pointer),Y but this will still
sta LastBank.w ;cause potential conflicts on real hardware !
jmp (JumpPtr)
_MapperReturn
pla ;When RTS, automatically return there
_MapperWrite
sta LastBank.w
tay
sta _Table.w,Y
rts
.db $00, $01, $02, $03
.db $04, $05, $06, $07
_Reset
sei
cld
__ lda #:Start
sta _b+1.w ;Bankswitch start bank in
sta LastBank.w
jmp Start ;Go to actual rest code
_NMI
pha ;NMI interrup
txa
pha
tya
pha
lda LastBank.w
pha
lda #:NMI ;Save state and get bank for actual code
tay
sta _Table.w,Y
jmp NMI ;Jump to actual interrupt code
_IRQ ;Same as above
pha
txa
pha
tya
pha
lda LastBank.w
pha
lda #:IRQ
tay
sta _Table.w,Y
jmp IRQ
_endInt
pla ;Restore state
jsr _MapperWrite
pla
tay
pla
tax
pla
rti
_LongJSR ;This jsr to any routine in any bank easily
pla
sta PointerL
clc
adc #$03
tay
pla
sta PointerH ;Get pointer where return adress is
adc #$00 ;and add 3 to it
pha
tya
pha
lda LastBank.w
pha ;Push old bank number
lda #>(_MapperReturn-1)
pha
lda #<(_MapperReturn-1)
pha ;Return adress to restore old bank
/* ldy #$02 ;VERSION N°1
lda (Pointer),Y
pha
dey
lda (Pointer),Y
pha
ldy #$03
lda (Pointer),Y
tay
sta _Table.w,Y ;Bankswitch the bank we want
sta LastBank
rts */
ldy #$01 ;VERSION N°2
lda (Pointer),Y
sta JumpPtrL.w
iny
lda (Pointer),Y
sta JumpPtrH.w
iny
lda (Pointer),Y
tay
sta _Table.w,Y ;Could be sta (Pointer),Y but this will still
sta LastBank.w ;cause potential conflicts on real hardware !
jmp (JumpPtr)
_MapperReturn
pla ;When RTS, automatically return there
_MapperWrite
sta LastBank.w
tay
sta _Table.w,Y
rts
If you wanted to do call a routine normally you'll do that :
Code:
jsr Routine
And with that kernel, all you have to change if the routine is not in the same bank is :
Code:
jsr Longjsr
.dw Routine
.db :Routine (the : is for getting bank number in WLA, I don't know how other assemblers handles this).
.dw Routine
.db :Routine (the : is for getting bank number in WLA, I don't know how other assemblers handles this).
In fact there is 2 versions of it, the normal (uncommented) version, and a slightly faster version (that is commented) that uses rts insead of jmp (indirect), but you'll have to add a -1 after the .dw
And I use now parenthesis for indirection as dish suggested so that it is compatible with 65816 syntax.
The kernel will automatically save the old bank, bankswitch the new bank, and resume the old before returning after the .db statement to continue the code normally. That way you can do many subroutine calls from all banks to all banks very easily. The only drawback is that it will take more stack space and more CPU time but you get nothing for noting. Also, it's impossible to pass arguments with the A or Y registers, only X could be used, and that applies for both input and output arguments which is a shame.
According to WLA doccumentation, it does supports multiple labels with the same name if and only if they are at the same adress. By wroting this I could verify this is not true, no matter what I try duplicate labels at the same adress are not supported. However, I evnetually did a trick to not have to get rid of labels and get everything manutally.
You could just copy/pase the code and add numbers after labels for each bank but this is ugly and if you want to change the kernel it will be troublesome to go changing in each bank.
So I just use local labels, and afterthat, I use .export directive so that they are available to the rest of the programm. I get warnings for exporting many times the same label, but not any errors so this comiles fine
In order to make the Longjsr as transparent as possible I'd liked to have a macro under WLA that would automatically call Longjsr if the caller is not in the same bank as the called, and just a normal jsr otherwise.
Unfortunately I can't get it to work, I keep getting errors. I have the following macro, if anyone has the solution what is wrong please tell me :
Quote:
.macro jsl args label
__ ;This is a dummy label
.ifneq :label :_b ;Check if the argument has the same bank as the dummy label
jsr LongJSR
.dw label ;If not, use the LongJSR routine from the kernel
.db :label
.else
jsr Label ;If so, just jsr normally
.endif
.endm
__ ;This is a dummy label
.ifneq :label :_b ;Check if the argument has the same bank as the dummy label
jsr LongJSR
.dw label ;If not, use the LongJSR routine from the kernel
.db :label
.else
jsr Label ;If so, just jsr normally
.endif
.endm
Let me know if you like the kernel and what could be improved.