If you want to use the Mega CD when booting from cartridge (mode 1) you need to use the Sub-CPU, either for crunching numbers or to get access to the rest of the Mega CD hardware. This will teach you how to run your own programs on the Sub-CPU.
This will not load the BIOS for you, it's a raw program that takes completely over the Mega CD side. This is still useful when you don't need the BIOS (either you don't need CD or back-up RAM access, or you can provide your own routines like MSU-MD does).
- ROM header
- Check if a Mega CD is present
- Resetting the Gate Array
- Loading a program into the Sub-CPU
- Communicating with the Sub-CPU side
- Minimal Sub-CPU program
To-do: explain how to use PRG-RAM and WORD-RAM (for now I just focused on the boot process itself).
Support for this in emulators and flashcarts is spotty at the moment, with many of them having their own unique procedure to detect mode 1. This is the official procedure and the idea is that it will become better supported in the future, and should not matter when used with a real Mega CD.
First of all, you should indicate Mega CD support in the ROM header. Add
C" character to the device
support field for this. For example, if it was this before:
dc.b 'J' dcb.b $1A0-*, ' '
replace it with this:
dc.b 'JC' dcb.b $1A0-*, ' '
Check if a Mega CD is present
Before doing anything you want to make sure that a Mega CD (or equivalent) is present.
First we should get ahold of some addresses that will be useful for us:
HwVersion: equ $A10001 ; Console region, version, etc. CdSubCtrl: equ $A12000 ; Sub-CPU reset/busreq, etc. CdMemCtrl: equ $A12002 ; Mega CD memory mode, bank, etc. CdBootRom: equ $400000 ; Main-CPU boot ROM CdPrgRam: equ $420000 ; PRG-RAM window CdWordRam: equ $600000 ; WORD-RAM window
There are two ways to check for the presence of the Mega CD:
- Check if bit 5 of
- Check for "
SEGA" signature in the boot ROM's header (
In theory, the former is the "proper" way to detect it, however this may not work with modern cartridges that include their own CD implementation since they aren't in the expansion slot. The latter is more reliable, but it requires CD firmware to be present (making it the only time you'll need it if you aren't using the BIOS).
If you want to cover your bases, testing for both is a good idea (if either is true, you can assume that a Mega CD or equivalent is present).
; Checks if Mega CD is present ; return d0 = 0 if no Mega CD ; 1 if Mega CD TestForCd: btst #5, (HwVersion) beq.s @Yep cmpi.l #'SEGA', (CdBootRoom+$100) beq.s @Yep @Nope: moveq #0, d0 rts @Yep: moveq #1, d0 rts
Resetting the Gate Array
Now we should reset the Mega CD's Gate Array (aka "the ASIC"). While it may work on first boot, we don't know what it could be doing if the Reset button is pressed, so this lets us put it back to a known state, as well as ensuring that the Sub-CPU is halted and such.
To reset the Gate Array we need to do the exact sequence of writes as in the routine below, anything else will not do. The Gate Array sees this particular sequence as a reset command.
InitCd: ; Write the Gate Array reset sequence so ; it's back to a known state move.w #$FF00, (CdMemCtrl) move.b #$03, (CdSubCtrl+1) move.b #$02, (CdSubCtrl+1) move.b #$00, (CdSubCtrl+1) ; Burn some cycles to give it time ; to reset, just in case moveq #$7F, d0 dbf d0, * rts
You only need to do this when booting, you shouldn't need to reset the whole thing again when you just want to replace the current program with another one.
Loading a program into the Sub-CPU
First we need to reset the Sub-CPU and then get ahold of the bus:
LoadSubCpu: lea (CdSubCtrl+1), a0 move.b #$00, (a0) ; Reset the Sub-CPU @Reset: move.b (a0), d0 and.b #$01, d0 cmp.b #$00, d0 bne.s @Reset move.b #$03, (a0) ; Request the Sub-CPU bus @BusReq: move.b (a0), d0 and.b #$03, d0 cmp.b #$03, d0 bne.s @BusReq
Now we can copy the program to PRG-RAM. The snippet below will do for programs up to 128KB in size, if you need larger you will need to look up on how to do PRG-RAM bank switching (I suggest to start only with this for now, anyway).
; a6 = pointer to program to load ; d7 = number of words to load (up to 128K) lea (CdPrgRam), a5 move.w #$0000, (CdMemCtrl) subq.w #1, d7 @Load: move.w (a6)+, (a5)+ dbf d7, @Load
As a final step, we reset the Sub-CPU again and then let it run. You absolutely need to reset it again or it won't work, despite the fact that we never gave it room to run before. I'm not sure why, but I confirmed that not resetting caused problems on real hardware.
; Reset Sub-CPU again move.b #$00, (a0) @Reset2: move.b (a0), d0 and.b #$01, d0 cmp.b #$00, d0 bne.s @Reset2 ; Let it run now move.b #$01, (a0) @StartUp: move.b (a0), d0 and.b #$01, d0 cmp.b #$01, d0 bne.s @StartUp rts
At this point the Sub-CPU program should be running.
Communicating with the Sub-CPU side
The most obvious way to communicate between the two 68000s is by using the communication ports (16-bit each). The Main-CPU and the Sub-CPU have eight ports each, both sides can read all ports but each CPU can only write to its own ports (they send data only in one direction).
The addresses for the ports from the Main-CPU side are as follows, for
the Sub-CPU side replace
CdCommMain1: $A12010 ; Main-CPU to Sub-CPU port #1 CdCommMain2: $A12012 ; Main-CPU to Sub-CPU port #2 CdCommMain3: $A12014 ; Main-CPU to Sub-CPU port #3 CdCommMain4: $A12016 ; Main-CPU to Sub-CPU port #4 CdCommMain5: $A12018 ; Main-CPU to Sub-CPU port #5 CdCommMain6: $A1201A ; Main-CPU to Sub-CPU port #6 CdCommMain7: $A1201C ; Main-CPU to Sub-CPU port #7 CdCommMain8: $A1201E ; Main-CPU to Sub-CPU port #8 CdCommSub1: $A12020 ; Sub-CPU to Main-CPU port #1 CdCommSub2: $A12022 ; Sub-CPU to Main-CPU port #2 CdCommSub3: $A12024 ; Sub-CPU to Main-CPU port #3 CdCommSub4: $A12026 ; Sub-CPU to Main-CPU port #4 CdCommSub5: $A12028 ; Sub-CPU to Main-CPU port #5 CdCommSub6: $A1202A ; Sub-CPU to Main-CPU port #6 CdCommSub7: $A1202C ; Sub-CPU to Main-CPU port #7 CdCommSub8: $A1202E ; Sub-CPU to Main-CPU port #8
If the Main-CPU needs the Sub-CPU to do something immediately, it can send an interrupt. Make sure that the Sub-CPU's IRQ2 vector points to an appropriate interrupt handler.
This interrupt is normally intended to let the Sub-CPU know when the vblank interrupt has happened, but if you don't need to know about vblank in the Sub-CPU side you can repurpose it for anything else.
To assert the interrupt, we write
$81 to the high byte of
CdSubCtrl, then wait until bit 0 reads back as 1 (to know
that the interrupt was accepted):
; issue interrupt from the Main-CPU move.b #$81, (CdSubCtrl) ; wait for Sub-CPU to accept it @WaitIrqAck: move.b (CdSubCtrl), d0 and.b #$01, d0 beq.s @WaitIrqAck
Minimal Sub-CPU program
Of course all of this is worthless unless you have a program to run on the Sub-CPU side in the first place. The code below is a minimal program that does nothing, it sets up the 68000 vectors and a few basic handlers:
EntryPointis where the Sub-CPU program begins.
Irq2handles the Main-CPU interrupt.
ErrorIntin case the Sub-CPU crashes.
This should be a good starting point to start working on your own program.
; All 68000 vectors dc.l $80000, EntryPoint, ErrorInt, ErrorInt dc.l ErrorInt, ErrorInt, ErrorInt, ErrorInt dc.l ErrorInt, ErrorInt, ErrorInt, ErrorInt dc.l ErrorInt, ErrorInt, ErrorInt, ErrorInt dc.l ErrorInt, ErrorInt, ErrorInt, ErrorInt dc.l ErrorInt, ErrorInt, ErrorInt, ErrorInt dc.l ErrorInt, ErrorInt, Irq2, ErrorInt dc.l ErrorInt, ErrorInt, ErrorInt, ErrorInt dc.l ErrorInt, ErrorInt, ErrorInt, ErrorInt dc.l ErrorInt, ErrorInt, ErrorInt, ErrorInt dc.l ErrorInt, ErrorInt, ErrorInt, ErrorInt dc.l ErrorInt, ErrorInt, ErrorInt, ErrorInt dc.l ErrorInt, ErrorInt, ErrorInt, ErrorInt dc.l ErrorInt, ErrorInt, ErrorInt, ErrorInt dc.l ErrorInt, ErrorInt, ErrorInt, ErrorInt dc.l ErrorInt, ErrorInt, ErrorInt, ErrorInt ; Where program starts EntryPoint: bra.s * ; Handler for Main-CPU interrupt (if you use that) Irq2: rte ; In case Sub-CPU crashes ErrorInt: bra.s *