Jump to content

Genesis Cartridge Port (for real this time)

Posted by Tursi , 16 December 2014 · 940 views

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.c...ls.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:
(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. ;) 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
		' Launch appropriate bank here...
		trampoline opt
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! ;)
	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
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
	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	
	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
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 ;) ).

Attached File  TFMenu.zip (61.4KB)
downloads: 42

  • --- Ω --- likes this

April 2019

141516171819 20

Recent Entries

Recent Comments