After my thoughts about Amstrad CPC cartridges, I want to show how easy is bank switching in both CPC plus and Dandanator cartridges.
When the CPC boots from cartridge, our code will start executing at address 0x0000
and we have the first 16K
bank mapped on that address (lower ROM, in case you want to check CPC documentation).
The strategy that I’m proposing is the following:
Address | Size | Type | Function |
---|---|---|---|
0x0000 | 16K | ROM | Code |
0x4000 | 16K | RAM | R/W Data |
0x8000 | 16K | RAM | R/W Data |
0xC000 | 16K | RAM/ROM | Video memory/Mapped cart bank |
You may or may not use a back-buffer with hardware (e.g. in 0x8000
), but considering that we can have all our read only data in ROM, 16K
for just code is probably enough.
Thanks to the properties of the memory mapping on the CPC, when we map a bank of ROM into lower ROM (0x0000
) or higher ROM (0xC000
), reads will go to ROM and writes will go to RAM; and mapped ROM is only seen by the CPU. This is important because the gate array can only see RAM when drawing the screen.
This is great, because we can map a bank to higher ROM on 0xC000
and write to 0xC000
, even having video memory active at that address, and we will be fine because those writes will go to RAM –and is RAM what will be displayed on screen, not the mapped ROM–.
So the idea is mapping on higher ROM any of the banks as we need them, and we can use that data with RAM from 0x4000
to 0xffff
, leaving on lower ROM the bank 0 with our code.
In the case of the CPC plus cartridges, there’s nothing to do, but in the Dandanator we need to configure the cartridge to support this behaviour. We can do it like this:
; SDCC syntax
; the byte pointed by iy will be overwritten
di
ld a, #0x8a
.db 0xfd, 0xfd
ld 0 (iy), a
ei
We will run this once at the start of the game.
Then we need a bank set function that takes the bank number we want to map in higher ROM. Let’s look at the CPC plus cartridge:
; SDCC syntax
; defined as __z88dk_fastcall in C - parameter in l
di
ld a, l
; cart slots 0 - 31 start at 128
or #128
ld c, a
ld b, #0xdf
out (c),c
ld bc, #0x7f80
out (c),c
ei
And the equivalent code in the Dandanator:
; SDCC syntax
; defined as __z88dk_fastcall in C - parameter in l
; the byte pointed by iy will be overwritten
di
ld c, l
.db 0xfd, 0xfd
ld 0 (iy), c
ei
Finally we need a function to unmap the higher ROM and have regular RAM on 0xC000
. In CPC plus cartridges is done with:
; SDCC syntax
di
ld bc, #0x7f88
out (c),c
ei
And in the Dandanator:
; SDCC syntax
; the byte pointed by iy will be overwritten
di
ld c, #32
.db 0xfd, 0xfd
ld 0 (iy), c
ei
It is as simple as that!
Of course that the Dandanator has more functionality than what I’ve shown here, but if we hide these differences behind some preprocessor directives (e.g. ifdef
), we can write the code once and generate both types of cartridges without any other changes –tip: look at my CPR tools to generate cartridges; using raw and padding flags gives you a Dandanator–.
And that’s all! This post is result of my own research and I don’t consider myself an expert, so if you find any issues with the text, please let me know.
Update: I added a comment in the code to remember that the opcode used by the Dandanator to process commands will overwrite a byte, so be sure that iy
is pointing to a safe memory address.