Howto:Use The Sega CD In Mode 1
The Sega CD has a not so commonly known feature named "Mode 1," in which code is ran from the cartridge instead of the CD. but it still has the ability to utilize the Sega CD's added features such as CD audio, PCM, and much more. The mode is very sparsely documented, and quite a bit of research from various parties has gone into describing this mode. Therefore, this how to article will serve as a reference on how to use the Sega CD in Mode 1. Throughout this how to, we'll be making a very simple Mega Drive program that plays the first program on the disc in the Sega CD, without any interface to the VDP of any kind.
Contents |
Hardware Basics
Before we can begin to use the Sega CD in Mode 1, we must first understand the additional restrictions and benefits the Sega CD gives one in Mode 1, as well as the added hardware available for use. With a Sega CD connected, the program has access to:
- A CD drive for data and audio
- An 8-channel stereo PCM chip
- Video Scaling and Rotation chip (ASIC)
- A 12.5 MHz Motorola 68000 with 512KB of Program RAM and 256 KB of Word RAM.
The memory map of the Sub-CPU is very different than the one of the Mega Drive's Main CPU. This memory map depends slightly upon what Word RAM mode has been chosen, but that will be pointed out below.
| Start address | End address | Description |
|---|---|---|
| $000000 | $07FFFF | 4 Megabit Program RAM |
| $080000 | $0BFFFF | Word RAM (2M Mode) |
| $0C0000 | $0D0000 | Word RAM (1M Mode) |
| $0E0000 | $FDFFFF | Reserved/Prohibited |
| $FE0000 | $FE3FFF | Backup RAM (Odd bytes only) |
| $FE4000 | $FEFFFF | System Reserved |
| $FF0000 | $FF3FFF | PCM Sound Source (Odd bytes only) |
| $FF4000 | $FF7FFF | System Reserved |
| $FF8000 | $FF81FF | Sub-CPU Registers |
| $FF8200 | $FFFFFF | System Reserved |
The Main CPU has no access to any of the Sega CD hardware besides some communications registers, as well as the Word and Program RAM. All Sega CD hardware access needs to be done through the Sub-CPU, and therefore a program is loaded into it from the Main CPU, after a BIOS has been loaded.
Initializing the Mega Drive
Before we have the Mega Drive in a state usable for initializing the Sega CD, we first must initialize our Mega Drive. For doing this, we use the code from this how to article. After writing our Mega Drive initialization code, we will be left with the following code:
StartOfRom:Vectors: dc.l $FFFE00, EntryPoint, ErrorTrap, ErrorTrap
dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
dc.l HBlank, ErrorTrap, VBlank, ErrorTrap
dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap
Console: dc.b 'SEGA MEGA DRIVE ' ; Hardware system ID
Date: dc.b '(C)XXXX YEAR.MON' ; Release date
Title_Local: dc.b 'SEGA CD DEMO PROGRAM ' ; Domestic name (Note: has to be 48 bytes long, pad it out with spaces if you do not have 48 characters in your title.
Title_Int: dc.b 'SEGA CD DEMO PROGRAM ' ; International name (Note: has to be 48 bytes long)
Serial: dc.b 'GM 10101010-00' ; Serial/version number
Checksum: dc.w 0
dc.b 'J ' ; I/O support (Has to be 16 bytes long, so pad it out with spaces.)
RomStartLoc: dc.l StartOfRom ; ROM start
RomEndLoc: dc.l EndOfRom-1 ; ROM end
RamStartLoc: dc.l $FF0000 ; RAM start
RamEndLoc: dc.l $FFFFFF ; RAM end
SRAMSupport: dc.l $5241F820 ; change to $5241F820 (NOT $5241E020) to create SRAM. 'RA' and $F8 also work.
dc.l $200000 ; SRAM start
dc.l $200200 ; SRAM end (Gives us $200 ($100 useable) bytes of SRAM)
Notes: dc.b ' ' ; Anything can be put in this space, but it has to be 52 bytes.
Region: dc.b 'JUE ' ; Region (J=Japan, U=USA, E=Europe)
EntryPoint:tst.l ($A10008).l ; Test port A control
bne.s PortA_Ok ; If so, magically branch
tst.w ($A1000C).l ; Test port C control
PortA_Ok:bne.w PortC_Okmove.b ($A10001).l,d0 ; Get hardware version
andi.b #$F,d0 ; Compare
beq.s SkipSecurity ; If the console has no TMSS, skip the security stuff.
move.l #'SEGA',($A14000).l ; Make the TMSS happy.
SkipSecurity:moveq #0,d0 ; Clear d0.
move.l #$C0000000,($C00004).l ; Set VDP to CRAM write
move.w #$3F,d7 ; Clear the entire CRAM.
VDP_ClrCRAM:move.w d0,($C00000).l ; Write 0 to the data port.
dbf d7,VDP_ClrCRAM ; Clear the CRAM
lea ($FFFF0000).l, a0 ; Load start of RAM into a0.
move.w #$3FFF, d0 ; Clear $3FFF longwords.
moveq #0, d1 ; Clear d1.
@clrRamLoop:move.l d1, (a0)+ ; Clear a long of RAM.
dbf d0, @clrRamLoop ; Continue clearing RAM if there's anything left.
PortC_Ok:bsr.w Init_Z80 ; Initialize the Z80
move #$2300, sr ; Re-enable interrupts.
bra.s MainProgramHInt:rte
VBlank:movem d0-a6, -(sp) ; Back up registers to stack
bsr.w SCD_U_SubCPUInt2 ; Generate an Level 2 interrupt on Sub-CPU
movem (sp)+, d0-a6 ; Restore registers.
rte
MainProgram:
Initialize the Sega CD
Now that our Mega Drive is initialized, it is time to initialize the Sega CD. Doing this requires a few things, such as setting the correct memory modes, resetting the Sub-CPU and taking it's bus, decompressing a BIOS and Sub-CPU code, and then allowing it to run again. All of this is accomplished by the following code (put directly after the MainProgram label above):
move #$2700, sr
cmp.w #"BR", $400180 ; Check to see if we have a Sega CD attached.
bne.w MainLoop ; If not, branch.
movem.l d0-a6, -(sp) ; Back up regs to stack
lea $A12000, a6 ; Load gate array start to a6
moveq #0, d0 ; Clear d0
move.b 1(a6), d0 ; Load reset reg to d0.
bset #1, d0 ; Request Sub-CPU Bus
bclr #0, d0 ; Reset Sub-CPU
move.b d0, 1(a6) ; Restore reset reg.
moveq #$7F, d0
@nopLoop:nopnopdbf d0, @nopLooplea ($A12002).l,a3 ; Word RAM Control to a3
move.w #$0009, (a3) ; Write mem mode
moveq #0, d0 ; Clear d0
cmp.w #"EG", $41586E ; Western BIOS at $415800
bne.s @checkRegularBIOS ; If not, branch.
lea $415800, a0 ; BIOS location to a0
bra.s @decompressSubCPU ; Branch to Sub-CPU Shenanigans.
@checkRegularBIOS:cmp.w #"EG", $41606E ; Regular BIOS at $416000
bne.s @checkWondermegaBIOS ; If not, branch.
lea $416000, a0 ; BIOS location to a0
bra.s @decompressSubCPU ; Branch to Sub-CPU Shenanigans.
@checkWondermegaBIOS:cmp.w #"ON", $41606E ; WonderMega BIOS at $416000
bne.s @checkLaserActiveBIOS ; If not, branch.
lea $416000, a0 ; BIOS location to a0
bra.s @decompressSubCPU ; Branch to Sub-CPU Shenanigans.
@checkLaserActiveBIOS:cmp.w #"EG", $41AD6E ; LaserActive BIOS at $41AD00
bne.w @UnknownBIOSError ; If not, branch. (Unknown BIOS)
lea $41AD00, a0 ; BIOS location to a0
@decompressSubCPU:lea $420000, a1 ; Location to decompress to to a1
bsr.w KosDec ; Decompress
lea SubCPU_Code, a0 ; Sub-CPU code to a0
lea $426000, a1 ; Location to load to
moveq #0, d0 ; Clear d0
move.w #SubCPU_End-SubCPU_Code, d0 ; Move length to d0
@loadSubCPULoop:move.b (a0)+, (a1)+ ; Write a byte of Sub-CPU code
dbf d0, @loadSubCPULoop ; Keep looping until all is written.
move.b #$2A,($A12002).l ; Set 2M mode, and some unknown shit
lea ($A12001).l,a3 ; Reset reg to a3
moveq #1,d0 ; Move 1 to d0
@resetLoop:move.b d0,(a3) ; Write d0 to reset reg
cmp.b (a3),d0 ; Is the reset reg set to our value?
bne.s @resetLoop ; Keep looping until it is
nopnopnopnopnopnopnopnopnopnopbsr.w SCD_U_SubCPUInt2 ; Generate an Level 2 interrupt on Sub-CPU
nopnopnopnopnopnopnopnopnopnopbsr.w SCD_U_SubCPUInt2 ; Generate an Level 2 interrupt on Sub-CPU
move.b #$FF, $A12010 ; Clear the function number.
bra.w MainLoop ; Jump to main loop.
@UnknownBIOSError:bra.s @UnknownBIOSError ; Keep looping
; ===========================================================================; SCD Utility method to cause a Level 2 interrupt on the Sub-CPU.SCD_U_SubCPUInt2:moveq #0, d0 ; Clear d0
move.w $A12000, d0 ; Move RESET reg to d0
bset #8, d0 ; Set the L2 Int flag
move.w d0, $A12000 ; Restore the thinger
rts
; ===========================================================================SubCPU_Code:incbin "SubCPU.bin"
SubCPU_End:dc.w $0000
even
; ---------------------------------------------------------------------------; Kosinski decompression algorithm; ---------------------------------------------------------------------------; ||||||||||||||| S U B R O U T I N E |||||||||||||||||||||||||||||||||||||||KosDec:var_2 = -2
var_1 = -1
subq.l #2,sp
move.b (a0)+,2+var_1(sp)
move.b (a0)+,(sp)
move.w (sp),d5
moveq #$F,d4
loc_18A8:lsr.w #1,d5
move sr,d6dbf d4,loc_18BAmove.b (a0)+,2+var_1(sp)
move.b (a0)+,(sp)
move.w (sp),d5
moveq #$F,d4
loc_18BA:move d6,ccrbcc.s loc_18C2move.b (a0)+,(a1)+
bra.s loc_18A8; ===========================================================================loc_18C2: ; XREF: KosDec
moveq #0,d3
lsr.w #1,d5
move sr,d6dbf d4,loc_18D6move.b (a0)+,2+var_1(sp)
move.b (a0)+,(sp)
move.w (sp),d5
moveq #$F,d4
loc_18D6:move d6,ccrbcs.s loc_1906lsr.w #1,d5
dbf d4,loc_18EAmove.b (a0)+,2+var_1(sp)
move.b (a0)+,(sp)
move.w (sp),d5
moveq #$F,d4
loc_18EA:roxl.w #1,d3
lsr.w #1,d5
dbf d4,loc_18FCmove.b (a0)+,2+var_1(sp)
move.b (a0)+,(sp)
move.w (sp),d5
moveq #$F,d4
loc_18FC:roxl.w #1,d3
addq.w #1,d3
moveq #-1,d2
move.b (a0)+,d2
bra.s loc_191C; ===========================================================================loc_1906: ; XREF: loc_18C2
move.b (a0)+,d0
move.b (a0)+,d1
moveq #-1,d2
move.b d1,d2
lsl.w #5,d2
move.b d0,d2
andi.w #7,d1
beq.s loc_1928move.b d1,d3
addq.w #1,d3
loc_191C:move.b (a1,d2.w),d0
move.b d0,(a1)+
dbf d3,loc_191Cbra.s loc_18A8; ===========================================================================loc_1928: ; XREF: loc_1906
move.b (a0)+,d1
beq.s loc_1938cmpi.b #1,d1
beq.w loc_18A8move.b d1,d3
bra.s loc_191C; ===========================================================================loc_1938: ; XREF: loc_1928
addq.l #2,sp
rts
; End of function KosDec
Whoa - that's quite a lot of code. Reading the comments will help greatly, as I have taken care to annotate my code as best as I can. The KosDec routine is a routine to decompress the Sub-CPU BIOS from the Sega CD BIOS EPROM, called Kosinski. It was a commonly used compression for Mega Drive games' code, as well as other things. The SubCPU_Code label includes a binary file called SubCPU.bin which is our Sub-CPU code, which will run on the Sega CD and allow the main CPU to play music from the CD, as well as a few other things. Note that Program RAM is accessed in 4 different banks of 128 KB, so if your Sub-CPU code exceeds that size, you need to load it in 128KB chunks and switch the bank, but that's out of the scope of this article. If your code is over 128KB anyways, you should really be using compression and not a simple byte loading loop.
Do note that as the Sub-CPU BIOS address is hard coded, it seems to fail to work on some specific versions of the Sega CD BIOS. See the below list for alternate addresses of the Sub-CPU BIOS code in some special versions of the BIOS:
(Note: A lot of these are based from primarily Emulator tests, though some have been properly tested on Hardware and have matched.) Multi-Mega (Europe) (v2.21X) [b] ........ $016000 CDX (USA) (v2.21X) ...................... $016000 Mega-CD 2 (Europe) (v2.00) .............. $016000 Mega-CD 2 (Europe) (v2.00W) ............. $016000 Mega-CD 2 (Japan) (v2.00C) .............. $016000 Sega CD 2 (USA) (v2.00) ................. $016000 Sega CD 2 (USA) (v2.00W) ................ $016000 Sega CD 2 (USA) (v2.11X) ................ $016000 Mega-CD (Asia) (v1.00S) ................. $016000 Mega-CD (Europe) (v1.00) ................ $015800 Mega-CD (Japan) (1.00l) ................. $016000 Mega-CD (Japan) (1.00S) ................. $016000 Mega-CD (Japan) (v1.00P) ................ $016000 Sega CD (USA) (v1.00) ................... $015800 Sega CD (USA) (v1.10) ................... $015800 WonderMega (Japan) (v1.00) (Sega) ....... $016000 WonderMega M2 (Japan) (v2.00) ........... $016000 X'Eye (USA) (v2.00) ..................... $016000 LaserActive (Japan) (v1.02) ............. $01AD00/$00D500? LaserActive (USA) (v1.02) ............... $01AD00/$00D500? LaserActive (USA) (v1.04) ............... $01AD00/$00D500?
Sub-CPU Code
As you probably noticed above, we're including a binary file that's our assembled Sub-CPU code. The Sub-CPU code is the code that runs on the Sega CD's Motorola 68000 and interacts with the Sega CD hardware. The Sega CD provides a few registers for the Sub-CPU and the Mega Drive's Main CPU to communicate, and to set things such as commands.
This code is loaded directly after the Sega CD BIOS, and the BIOS will call into it once the hardware has been initialized. All Sub-CPU code needs a header, exactly like this:
org $6000
SPHeader:dc.b 'MAIN-SUBCPU', 0
dc.w $0001, $0000 ; Version 0001, normal type
dc.l $00000000 ; No Link module
dc.l SPEnd-SPHeader ; Size
dc.l SPHeaderOffsets-SPHeader ; Jump table pointer
dc.l $00000000 ; Some Work RAM info
SPHeaderOffsets:dc.w SPInit-SPHeaderOffsets
dc.w SPMain-SPHeaderOffsets
dc.w SPInt2-SPHeaderOffsets
dc.w SPNull-SPHeaderOffsets
dc.w $0000
SPInit points to a function that initializes the state of the Sega CD and Sub-CPU code. We will set the hardware's faders to 100% volume. SPMain is the main code that is ran after the initialization routine has ran. SPInt2 is the routine that is ran once a Level 2 interrupt is received, which is caused by our MD code every VBlank. This interrupt is required to keep the Sub-CPU going. Then there is SPNull, which I believe has to do with the Sega CD timer the Sub-CPU can use.
Our initialization code looks like this:
; Called on a level 2 interrupt.SPInt2:rts
SPInit:move.b #'D', $FF800F ; Clear busy flag.
moveq #0, d1 ; Clear d1.
move.w #$0400, d1 ; Set volume to max.
move.w #$0085, d0 ; Set the fader.
jsr $5F22 ; Call into the BIOS.
move.w #$8400, d1 ; Set master volume to max.
move.w #$0085, d0 ; Set the fader.
jsr $5F22 ; Call into the BIOS.
lea DriveInit_Table(pc), a0 ; Load the table to a0.
move.w #$0010, d0 ; Call BIOS function to read the ToC.
jsr $5F22 ; Call into the BIOS.
move.w #$0089, d0 ; Stop the drive.
jsr $5F22 ; Call into the BIOS.
move.b #'D', $FF800F ; Clear busy flag.
rts
It sets the fader and master volume up to max, and then uses the rts to return control to the BIOS, which then calls our main code. It also declares our Level 2 interrupt handler - in this case, we aren't doing anything in it, so it is a simple rts. Our Main loop, or SPMain routine, will be responsible for calling the appropriate functions for reading the CD's Table of Contents (which contains pointers to where tracks are located on the disc) and then playing the first track on the disc. Note that the functions above shouldn't really be needed, so they can be omitted if the need be.
SPMain:MainLoop:move.b #'D', $FF800F ; Clear busy flag.
cmp.b #0, $FF8010 ; Do we have a pending command?
beq.s MainLoop ; If not, keep checking.
move.b #'B', $FF800F ; Set busy flag.
moveq #0, d0 ; Clear d0.
move.b $FF8010, d0 ; Write the function ID to d0.
cmp.b #$0, d0 ; Read ToC
beq.w Func00cmp.b #$1, d0 ; Stop CD audio
beq.w Func01cmp.b #$2, d0 ; Play track in word arg.
beq.w Func02cmp.b #$3, d0 ; Pause playback.
beq.w Func03cmp.b #$4, d0 ; Continue pause.
beq.w Func04bra.w MainLoop ; Keep looping the main loop.
; ===============================================================================DriveInit_Table:dc.w $01FF
even
Func00: ; Read drive TOC
move.w #$0002, d0 ; Stop all CD playback.
jsr $5F22 ; Call into the BIOS.
lea DriveInit_Table(pc), a0 ; Load the table to a0.
move.w #$0010, d0 ; Call BIOS function to read the ToC.
jsr $5F22 ; Call into the BIOS.
move.b #'D', $FF800F ; Clear busy flag.
moveq #$1F, d0
@loop:
nopnopnopnopnopdbf d0, @loop
bra.w MainLoop ; Jump back to main loop.
; ===============================================================================Func01: ; Stop music
move.w #$0002, d0 ; Stop all CD playback.
jsr $5F22 ; Call into the BIOS.
move.b #'D', $FF800F ; Clear busy flag.
moveq #$1F, d0
@loop:
nopnopnopnopnopdbf d0, @loop
bra.w MainLoop ; Jump back to main loop.
; ===============================================================================TrackPlay_Table:dc.w $0004
even
Func02:move.w #$0089, d0 ; Stop the drive from reading any data.
jsr $5F22 ; Call into the BIOS.
move.w #$0002, d0 ; Stop current CD track playback.
jsr $5F22 ; Call into the BIOS.
lea TrackPlay_Table(pc), a0 ; Load table address to a0.
move.w $FF8012, (a0) ; Write track number from word parameter
move.w #$0013, d0 ; Set to play track repeatedly.
jsr $5F22 ; Call into the BIOS.
move.b #'D', $FF800F ; Clear busy flag.
moveq #$1F, d0
@loop:
nopnopnopnopnopdbf d0, @loop
bra.w MainLoop ; Jump back to main loop.
; ===============================================================================Func03:move.w #$0003, d0 ; Pause the drive.
jsr $5F22 ; Call into the BIOS.
move.b #'D', $FF800F ; Clear busy flag.
moveq #$1F, d0
@loop:
nopnopnopnopnopdbf d0, @loop
bra.w MainLoop ; Jump back to main loop.
; ===============================================================================Func04:move.w #$0004, d0 ; Unpause the drive.
jsr $5F22 ; Call into the BIOS.
move.b #'D', $FF800F ; Clear busy flag.
moveq #$1F, d0
@loop:
nopnopnopnopnopdbf d0, @loop
bra.w MainLoop ; Jump back to main loop.
; ===============================================================================SPNull:rts
; ===============================================================================SPEnd:END
The code above first defines two pointers to words. The first tells the BIOS to read the Table of Contents for all of the tracks, while the second tells the BIOS what track to play from the CD. The first BIOS call makes use of the first word, and reads the Table of Contents from the disc. As explained earlier, it contains the location of the tracks on the disc. The second call makes sure that the CD drive is not in data reading mode, which the Table of Contents call uses to read the ToC. The last call then makes use of the second word and plays the track over and over again, thus looping it. To finish it off, our main code then gets to an infinite loop.
Conclusion
This is just a small example of what the Sega CD is capable of - there are much more BIOS functions that aren't explained here, but are in Sega's official Sega CD documentation, which you should take a look at if you're at all interested in making more interesting things with the Sega CD. The code here doesn't use any sort of handshaking nor does it have facilities for the Main CPU to interact with Sega CD hardware, as this is left as an exercise to the reader to figure out.