Atari XE Multicart
Posted on by Idorobots
Cranking out another post took me way longer than I had anticipated when I rebooted this blog, but I got sidetracked by another quick hardware project that turned out to be way more involved and equally more fun than I originally thought...
I've designed an Atari 400/800/XL/XE standard cartridge compatible board that can hold up to 127 different standard 8KB and 16KB game ROMs at the same time. It's memory chip agnostic and features software game selection & a few neat hacks.
The hardware
The PCB design went through several iteration and that's part of the reason why this little project took so much time - the initial version supported only 8KB game ROMs, while the latest version supports both standard cartridge types, a plethora of different memory chips and fits neatly in a Z7 case, which apparently is still produced here in Poland. Here's a schematic:
The 74HTC373
latch is used as register that can be written to by accessing the Cartridge Control Block within the Atari causing the CCTL
line to go low and in turn latch EN
able line to go high. Storing a value there causes a toggle of some of the address lines going to the main memory chip and, if applicable, the RD4
line telling the Atari that a 16KB ROM is present. A special care was needed to support 16KB ROMs - the A12
address line of the memory chip is the result of OR
'ing one of the latch outputs and the S4
(low memory slot selection) line. This is done by the 74HTC08
quad-AND
gate, as both of these lines are negated. The S4
line, which is used only for 16KB ROMs, basically overrides the A12
address line taking precedence over the latch output, which is OK, since 16KB ROMs occupy both memory slots and shouldn't be using that output anyway. It's worth to mention, that it is mandatory to align 16KB ROMs in the memory chip to a 16KB boundary (A12
equal to 0
) so both 8KB halves of the ROM are mapped to the correct memory slots in the Atari address space. The main memory chip footprint is configured so that any JEDEC compatible EEPROM/Flash can be used ranging from 27C64
compatibles up to 27C080
, which can hold a whoopin' 1MB of classic Atari games.
The design is deceptively similar to a XEGS cartridge in that it uses a 74HTC373
latch and some glue logic, but in terms of functionality it's far from it. For starters, XE Multicart only supports standard ROMs which it selectively maps to the cartridge slots of the Atari memory space. Furthermore, it switches both cartridge memory slots on and off when appropriate, while XEGS cartridges always map the last ROM bank to the second slot. Basically, XE Multicart emulates standard 8KB and 16KB cartridges with a single shot, software game selection. No other cartridge types are supported at the moment.
For the next iteration, I might want to get rid of the 74HTC08
chip, as only half of it is used by the design. Both of these AND
gates could be implemented with MOSFETs, just like the inverter on the CCTL
line:
The software
The software is very simple - it only presents a list of available games and processes user input to make the selection:
Once a suitable selection is made, a value matching it is written to the Cartridge Control Block (CCTL
), which is mapped to the 74HTC373
latch described above, causing a game bank switch in the memory chip. This bank switch causes a problem, however. The very game selection code that is being executed by the console is being swapped out in favor of a game ROM before we actually jump into it. The rather obvious thing to do is to store all the needed code elsewhere, so we don't loose any of it during the switch. The software just needs to back itself up to a known empty place in RAM, perform a cleanup and then jump its way back to the stored copy... Somehow...
Cue the hacks!
#define CCTL ((unsigned char *) 0xD500)
#define DOSVEC ((unsigned int *) 0x0A)
#define RESET 0xFFFC
#define BOOTSTRAP_SIZE 0x400
#define BOOTSTRAP_AREA (0x8000 - BOOTSTRAP_SIZE)
unsigned char selection_mask;
unsigned int DOSVEC_save;
int main(void) {
// ...
selection_mask = ...;
// Backup bootstrap() into the RAM not to loose it later.
memcpy((void *) BOOTSTRAP_AREA, (const void *) bootstrap, BOOTSTRAP_SIZE);
// Spoof the return address so we jump back into bootstrap().
DOSVEC_save = *DOSVEC;
*DOSVEC = BOOTSTRAP_AREA;
// Do the cleanup.
exit(1);
}
void __fastcall__ bootstrap(void) {
// Restore the DOS vector.
*DOSVEC = DOSVEC_save;
// Swap the game.
*CCTL = selection_mask;
// Reset the console.
__asm__ ("jmp (%w)", RESET);
}
That's precisely what I did - a little bootstrap code is straight up memcpy
'ied into another location with no regard for position dependence whatsoever. Crude but effective. Once that has been done, the OS return address (DOSVEC
) is backed up and overwritten to point to the newly copied bootstrap code and the CC65 runtime is cleaned up simply by invoking exit()
. After that, the runtime jumps into our spoofed DOSVEC
and the bank switch is performed along a console reset, which starts the game.
Unfortunately, this isn't enough as a so-called warm-reset is executed - the OS doesn't perform most of the setup that it normally does, only the bare minimum - and that, coupled with how CC65 runtime works, messes some games up. Fortunately enough, a hackaround for this is rather simple - just mess up the OS sanity checks by faking broken RAM where appropriate:
#define SANITYCHECK ((unsigned char *) 0x33D)
int main(void) {
*SANITYCHECK = 0x23; // Ensure cold-start is performed during reset.
// ...
}
¯\_(ツ)_/¯