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=Genesisand 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 gnd
A02 Vcc1 - +5v
A03 A08
A04 A11
A05 A07
A06 A12
A07 A06
A08 A13
A09 A05
A10 A14
A11 A04
A12 A15
A13 A03
A14 A16
A15 A02
A16 A17
A17 A01
A18 gnd
A19 D07
A20 D00
A21 D08
A22 D06
A23 D01
A24 D09
A25 D05
A26 D02
A27 D10
A28 D04
A29 D03
A30 D11
A31 Vcc1 - +5v, filter cap to neighboring ground
A32 gnd
B01 SL1 - Left Audio (input? Likely. COULD be output too.) - Ties into the middle of an analog mixing
circuit between YM2612 and CXA1034
B02 !MRES (!HRES) - Master/Hard reset, resets BIOS ROM and all
B03 SR1 - Right Audio (? see SL1)
B04 A09
B05 A10
B06 A18
B07 A19
B08 A20
B09 A21
B10 A22
B11 A23
B12 !YS - Video output control bit - 0=transparent pixel, 1=opaque pixel
B13 !VSYNC -Vertical sync output
B14 !HSYNC -Horizontal sync output
B15 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 valid
B19 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 D15
B23 D14
B24 D13
B25 D12
B26 !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 asserted
B28 !LWR - Lower byte write strobe, asserted by VDP when !R/W and !LDS are asserted
B29 !UWR - Upper byte write strobe, asserted by VDP when !R/W and !UDS are asserted
B30 !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.
%20-%20AtariAge%20Forums_files/icon_wink.gif)
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 returns
declare asm sub trampoline(d2.w)
move.l #@1,d0 ; start address
movea.l d0,a0
move.l #@2-@1,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 address
@3:
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 ; padding
@2:
nop
End 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 ;)
).