With the menu for my Thunder Force Multicart nearly ready to go, I needed to understand what sort of switching scheme was legal on the Genesis.
Research didn't actually help much.. even the pinout of the cartridge port is just the same pinout copied everywhere with little to no explanation of the pins. My original plan was to do it the same way as on the TI - just treat writes to ROM as a request to switch banks. Unfortunately I couldn't even determine for certain if that was safe. Likewise, attempts to determine with the debugger if any of the games write to ROM accidentally were thwarted when the debugger treated most accesses as a write.
I stumbled upon schematics here: http://emu-docs.org/?page=Genesis
and a description of internal signals here: http://code.google.com/p/genplus-gx/downloads/detail?name=gen_signals.doc&can=2&q=
With these, I was able to trace out and write notes on the actual pins of the cartridge port - all but one pin were explained by the time I was done:
GENESIS CARTRIDGE PORT PINOUT (from schematics - Tursi)A01 gndA02 Vcc1 - +5vA03 A08A04 A11A05 A07A06 A12A07 A06A08 A13A09 A05A10 A14A11 A04A12 A15A13 A03A14 A16A15 A02A16 A17A17 A01A18 gndA19 D07A20 D00A21 D08A22 D06A23 D01A24 D09A25 D05A26 D02A27 D10A28 D04A29 D03A30 D11A31 Vcc1 - +5v, filter cap to neighboring groundA32 gndB01 SL1 - Left Audio (input? Likely. COULD be output too.) - Ties into the middle of an analog mixing circuit between YM2612 and CXA1034B02 !MRES (!HRES) - Master/Hard reset, resets BIOS ROM and allB03 SR1 - Right Audio (? see SL1)B04 A09B05 A10B06 A18B07 A19B08 A20B09 A21B10 A22B11 A23B12 !YS - Video output control bit - 0=transparent pixel, 1=opaque pixelB13 !VSYNC -Vertical sync outputB14 !HSYNC -Horizontal sync outputB15 EDCLK - External Dot Clock? MCLK/5 during HSYNC, MCLK/4 otherwise. Generated by Bus arbiter and used by VDP as pixel clock reference in 40-cell mode only.B16 !CAS0 - (!C_OE) - Common output enable, asserted by the VDP for reads $000000-$DFFFFF B17 !CE0 - Chip Enable for Cart - asserted for RW from $000000-$3FFFFF is !CART is asserted, or $400000-$7FFFFF when it is not (ie: a RAM cart).B18 !AS - 68000 Valid Address Strobe - indicates that the address bus is validB19 VCLK - Master 68k clock (MCLK/7) generated by the VDP (7MHz then?)B20 !DTAK - (!DTACK) - 68000 Data Acknowledge (68k hangs if this or !BERR doesn't assert) Generated by the VDP or Bus Arbiter for valid Genesis addresses B21 !CAS2 - ??? - generated (probably) by the Bus Arbiter, no documentation on what for. Runs only to cart and side ports.B22 D15B23 D14B24 D13B25 D12B26 !ASEL - (!LO_MEM) - Asserted for reads to low memory ($000000-$7FFFFF)B27 !VRES - (!RST) - Output! 68000 reset, output from bus arbiter when !SRES or !WRES are assertedB28 !LWR - Lower byte write strobe, asserted by VDP when !R/W and !LDS are assertedB29 !UWR - Upper byte write strobe, asserted by VDP when !R/W and !UDS are assertedB30 !M3 - (SEL0) - System mode select input. 0 = normal, 1 = SMS mode. Not clear if there is a pullup/pulldown, but doesn't seem needed.B31 !TIME - I/O select output, asserted when $A13000-$A130FF is accessed (used for cartridge-based hardware/bank switching/etc)B32 !CART - 4.7k pull up to VCC - tie to ground to indicate cart present
After reading around a while, it was specifically that I/O select, called !TIME on the schematic, that I was after. This provides an alternate I/O specific memory space, pre-decoded, that I should be able to use. Tying into a 74LS174 (as in a schematic for the Radica clone I noted) should allow me to easily set the upper address lines of a 4MB EPROM.
So with a scheme in mind, I needed to add just a bit of code to the menu to support it. Since I want four 1MB banks, I need just two bits to select them. Since I'm wary of how the 68k might access the cartridge port, and since I have lots of space, I will space my meaningful addresses by 4. The goal then, is that accessing these addresses should select these banks:
$A13000 - Bank 0 - $000000$A13004 - Bank 1 - $100000$A13008 - Bank 2 - $200000$A1300C - Bank 3 - $300000
Since I'll only be tying two pins off, that pattern will actually repeat through the whole 256 byte space listed above.
Next I needed to update the menu code, mainly because I don't have the 174s yet to test the hardware. So first, a handler for the Start button in the original code:
if old AND &h080 then ' start for old = 1 to 3 gosub blackit sleep 15 gosub lightit sleep 15 next old cls ' Launch appropriate bank here... trampoline opt endif
So this is pretty hacky, but it does the job. It sets up a loop to repeat 3 times - calls a new subroutine called 'blackit' which makes the current selection black. Then it delays for 15/60ths of a second. Then it calls 'lightit' which we previously had, to light it up in color again. Another sleep, and it repeats.
After the 3 flashes, it clears the screen with cls (this is why the code earlier set the text plane to the same as the graphics plane, even though it doesn't draw text. CLS works on the configured text plane). And it calls another new function called trampoline. We'll come back to that.
The function 'blackit' is just like 'greyit' and 'lightit', except it sets all the colors in a palette row to black. We could have done this with a loop, but in this case I just use palDat, which you'll recall is set to all black since it wasn't being used. Convenient!
blackit: valt select case opt case 0 palettes palDat,1,0,16 exit select case 1 palettes palDat,2,0,16 exit select case 2 palettes palDat,3,0,16 exit select end select return
Nothing interesting there, so lets talk about 'trampoline'. What the hell is a trampoline?
In programming, a trampoline is a function you call that sets up to make it possible to call another function. It's usually used in bank switch cases, just like this. Allow me to explain.
This system presents a pretty naïve bank switch - the entire memory space is switched out when the address is toggled. This INCLUDES the code that is currently running to perform the switch, if it's in ROM. Generally, this means that instead of executing the rest of the switch, you're now in the middle of someone else's random code -- crash. Now, there are a couple of ways to deal with this.
One way is to make sure the switching code is in exactly the same location in every bank. This way, when the bank switch happens, you're still executing the same code, and each bank can do what it needs to do after the switch. I actually looked into this, and it /might/ have been possible - empty blocks exist in all three Thunder Force games and there seemed to be some overlap, but I didn't know if there would be enough.
The other way is to copy a small amount of code into memory that does NOT switch, perform the switch there, then jump back. Usually, this is RAM. And that's a trampoline function.
There is another way to deal with this - to switch only part of the ROM at a time - many systems work like this, but it requires slightly more circuitry to do it.
So, I created a simple assembly function - it has three jobs. First, it copies the trampoline function to RAM, and jumps to it. Then, it calculates the address of the switch to toggle, and toggles it. Finally, since each bank is a full cartridge in this case, we simply load and execute the reset vector to start it up.
The cartridge then runs in its native space, and as long as it never touches that I/O space, it never knows that a switch took place.
' this function copies itself into RAM at $FFFF8000 and then jumps to it' the passed in parameter is which game to run (0,1,2) which is used to' set the correct bank - note: never returnsdeclare asm sub trampoline(d2.w) move.l #@1,d0 ; start address movea.l d0,a0 move.l #@[email protected],d1 ; number of bytes lsr.l #$2,d1 ; divide by 4 for words addq #1,d1 ; and account for potential partial words (probably unneeded but wont hurt) movea.l #$ffff8000,a1 ; target address movea.l a1,a2 ; save the [email protected]: move.l (a0)+,(a1)+ ; copy 32-bits dbf d1,@3 ; loop until done jmp (a2) ; execute the code from RAM; the following code runs from RAM, d2.w has the program number @1: lsl.w #2,d2 ; multiply by four for offset moveq #0,d1 ; prepare to zero move.w d2,d1 ; now we know its safe add.l #$a13000,d1 ; add the address of the IO space movea.l d1,a0 ; this is the address we will poke move.w d1,(a0) ; do the poke (value irrelevant) moveq #$0,d0 ; cart should be active, so prepare to jump movea.l d0,a0 ; read from reset vector movea.l (a0)+,sp ; set stack pointer movea.l (a0)+,a0 ; get boot address jmp (a0) ; and go do it nop ; [email protected]: nopEnd Sub
So the first part of the function just copies the code between @1 and @2 to RAM at the fixed address of $8000. Actually it copies too much but I didn't really care. It then jumps directly to that code.
After the jump, the argument is still in d2. First we multiply that by 4 for the offset I mentioned, then we just add the base of the IO space. The write to that address causes the bank switch (the data written is irrelevant).
Now that the correct bank is mapped in, we can just read in the reset vector into stack and an address register, and jump right to it.
Next article when I have the hardware to actually test with (although if I get time I might hack an emulator to do it first ).