Atari Space Duel Self-Test ROM Signatures
Many coin-operated arcade video games have a self-test mode, which lets their owners determine if anything is wrong with the hardware. This usually includes testing the video (displaying a crosshatch, color bars or some other test pattern), controls (either a visual or audio display when the switches are closed), audio, RAM, and ROM. The nature of self-test is somewhat unreliable, because depending on what’s wrong, the hardware might be too broken to run the self-test program at all. But it is a good first step that allows diagnosing many common problems.
If you want to patch a game’s ROM, for example to improve a poorly-implemented free play mode, this can present a problem. Depending on the hardware, the game might play fine, but report errors in its test mode, or it might refuse to run at all. If you patch the ROMs, it’s imporant to do it in a way that lets the game run — and doesn’t make it report errors.
Approaches to solving this problem vary. The simplest is to checksum the ROMs, display the checksum on-screen, and include a printout of that screen in the operator’s manual. While this approach is dead simple, it’s problematic: You won’t always have the printed manual with you when you service the machine (or you might not have a copy at all). Also, if the game program gets revised, a new printing of the manual is required — and you might end up comparing different revisions of the manual and the ROM code.
The most common approach I’ve encountered is for the last two bytes of every ROM to hold a 16-bit checksum of its contents (minus those two bytes). This is an improvement on the easier approach, but still has problems. If the ROM itself is faulty, how do you know the checksum it holds is correct?
With Space Duel (at least, perhaps other titles as well), Atari chose a different approach. First, let’s look at how the program ROMs are laid out. This is a snippet of the Space Duel MAME driver:
/* Vector ROM */ ROM_LOAD( "136006-106.r7", 0x2800, 0x0800, CRC(691122fe) SHA1(f53be76a49dba319050ca7767de3441521910e83) ) ROM_LOAD( "136006-107.np7", 0x3000, 0x1000, CRC(d8dd0461) SHA1(58060b20b2511d30d2ec06479d21840bdd0b53c6) )
/* Program ROM */ ROM_LOAD( "136006-201.r1", 0x4000, 0x1000, CRC(f4037b6e) SHA1(9bacb64d257edd31f53db878477604f50681d78f) ) ROM_LOAD( "136006-102.np1", 0x5000, 0x1000, CRC(4c451e8a) SHA1(c05c52bb08acccb60950a15f05c960c3bc163d3e) ) ROM_LOAD( "136006-103.m1", 0x6000, 0x1000, CRC(ee72da63) SHA1(d36d62cdf7fe76ee9cdbfc2e76ac5d90f22986ba) ) ROM_LOAD( "136006-104.kl1", 0x7000, 0x1000, CRC(e41b38a3) SHA1(9e8773e78d65d74db824cfd7108e7038f26757db) ) ROM_LOAD( "136006-105.j1", 0x8000, 0x1000, CRC(5652710f) SHA1(b15891d22a47ac3448d2ced40c04d0ab80606c7d) )
In both cases, the MAME convention is for the ROM filenames to be the manufacturer’s ROM identifier (if any), and the extension is its postition on the game PCB. 136006 is Atari’s internal code for Space Duel; -1 means the first revision of that specific ROM; 01, 02, 03 etc are how Atari represents the ROM position; .r1 means that ROM is installed at location R1 on the PCB.
The next two values are the location in memory and size. All the program ROMs are 4kib, and the Vector ROM at R7 is 2kib. This holds the instructions for the custom CPU that controls the vector graphics; the rest run on the main 6502.
Here’s the core of the algorithm, which is just XORing bytes together. $0007 holds the 16-bit base address, and the Y register indexes into it.
825F: 51 07 eor ($07), y 8261: C8 iny 8262: D0 FB bne $825f
It repeats this 16 times to cover the whole 4kib ROM:
825A: A9 10 lda #$10 825C: 85 0A sta $0a ;;; etc 8269: C6 0A dec $0a 826B: D0 F2 bne $825f
And has another loop around that (six times) to repeat for each ROM, with X as that loop’s index.
The computed value gets stored for each ROM, starting at $00F1, and incrementing for each:
826D: 95 F1 sta $f1, x 826F: E8 inx 8270: F0 18 beq $828a
And when you bring up self-test, if a ROM has failed, it shows two digits — for example "4 D1" — which means ROM at position 4 had checkdum $D1, instead of the correct checksum. Per the manual, position 4 is M1, which is $6000-$6FFF.
Further experimentation showed a couple interesting things:
- The final byte(s) didn’t seem to correspond to any checksums.
- A good ROM gets $00 stored near the top of the zeropage ($F1+X). Any value other than 00 means the ROM checksum failed.
- Changing the last two bytes changed the checksum.
That last point is intriuging! If the checksum tests the whole ROM, as it seems to, then the ROM can’t hold its own checksum. So how does selftest know if it’s good or not?
I wrote a Python program that XORs a ROM’s contents together, and ran it on my patched ROM. It showed:
136002-125.n4 - 0b 136006-102.np1 - 02 136006-103.m1 - 31 136006-104.kl1 - 04 136006-105.j1 - d1 136006-106.r7 - ff 136006-107.np7 - 00 136006-201.r1 - 01
The two ROMs I’d modified were M1 and J1. The XOR from my program matched the self-test error display of the game.
Then, I noticed a pattern. Do you see it?
How about now?
136006-107.np7 - 00 136006-201.r1 - 01 136006-102.np1 - 02 136006-103.m1 - 31 136006-104.kl1 - 04 136006-105.j1 - d1
The checksums are in ascending order of address mapping. N4 is a PROM which isn’t mapped into CPU address space, so it can be ignored. Likewise, R7 is used by the vector machine, so it gets $FF. Everything else is in the same order as it lands in memory.
Here’s the unmodified ROM set:
roms/original/spacduel/136006-107.np7 - 00 roms/original/spacduel/136006-201.r1 - 01 roms/original/spacduel/136006-102.np1 - 02 roms/original/spacduel/136006-103.m1 - 03 roms/original/spacduel/136006-104.kl1 - 04 roms/original/spacduel/136006-105.j1 - 05 roms/original/spacduel/136006-106.r7 - ff
The same proved true for different revisions of the program ROMs. The checksum is the value of the X register used to loop over the address space.
This blew my mind. This is a really smart approach. If you have 16-bit checksums stored at the end of the ROM, it tells you that the ROM itself is good. But if you accidentally swap two ROMs when working on the board, selftest can pass, even though the game doesn’t run. Atari’s approach specifically tests for the ROMs being correct and installed correctly.
Armed with that knowledge, I solved for $D1 ^ x = 03, stuck that in a spare byte in the ROM, and loaded it up again. Success! My patched ROMs now pass selftest.