Using the Sub-CPU in mode 1

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).

Iwis says

To-do: explain how to use PRG-RAM and WORD-RAM (for now I just focused on the boot process itself).

ROM header

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 the "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:

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 $A120xx with $FF80xx:

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:

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   *