The data track of a SCD game uses plain 2048 byte sectors. If the image is not in that format, you need to get it into that format, even if all you do is write the image to a CDR and then dump the CDR using 2048 byte sectors.
Standard ISO9660 is used (although it doesn't NEED to be), and as you know, the first 16 sectors aren't used. The SCD uses these first 16 sectors for a boot program - it has a "standard" header followed by the region lock binary blob, which is just code that shows the "licensed by" message. It's like this
| SEGA CD Loader code
| by Chilly Willy
| First part of cdrom header
|
| Main-CPU IP, comprising sectors 0 and 1. Sector 0 is loaded automatically,
| while sector 1 is from the header.
.text
| Standard MegaCD Volume Header at 0x0
.ascii "SEGADISCSYSTEM "
.asciz "CDBOOTLOADR"
.word 0x0100
.word 0x0001
.asciz "SEGACD BOOT"
.word 0x0001
.word 0x0000
.long 0x00000800 /* main 68000 initial program cd offset */
.long 0x00000800 /* main 68000 initial program cd size */
.long 0x00000000 /* main 68000 initial program entry offset */
.long 0x00000000 /* main 68000 initial program Work RAM size */
.long 0x00001000 /* sub 68000 initial program cd offset */
.long 0x00007000 /* sub 68000 initial program cd size */
.long 0x00000000 /* sub 68000 initial program entry offset */
.long 0x00000000 /* sub 68000 initial program Work RAM size */
.ascii "07292012 " /* date - MMDDYYYY */
.space 160, 0x20
| Standard MegaDrive ROM header at 0x100
.ascii "SEGA CD Loader "
.ascii "(C)2012 "
.ascii "CD Boot Loader "
.ascii "CD Boot Loader "
.ascii "GM MK-0000 -00 "
.ascii "J6 " /* controller info */
.ascii " "
.ascii " " /* modem info */
.ascii " "
.ascii "JUE " /* region info */
| Standard MegaCD startup code at 0x200
|
| This data and the code following is copied to the Main-CPU Work RAM at
| 0xFF0000 and run.
.global _start
_start:
.ifdef REGION_US
.incbin "ipl/us.bin", 0, 0x584
.endif
.ifdef REGION_EU
.incbin "ipl/eu.bin", 0, 0x56E
.endif
.ifdef REGION_JP
.incbin "ipl/jp.bin", 0, 0x156
.endif
| fall into startup code for Main-CPU
At that point, the code is all up to the programmer - he can do literally ANYTHING as long as he loads the game code and runs it. In the case of my cdboot loader, I look through the root directory for a particular file, load it to 0x8000 and run it. That part of the loader looks like this
| fall into startup code for Main-CPU
lea 0xA10000.l,a5
move.b #0,0x200E(a5) /* clear main comm port */
| wait for MD init handshake from CD
0:
cmpi.b #'I,0x200F(a5)
bne.b 0b
bset #1,0x2003(a5) /* give Sub-CPU Word RAM */
move.b #'B,0x200E(a5) /* main comm port - do boot */
| wait for MD code handshake from CD
1:
cmpi.b #'M,0x200F(a5)
bne.b 1b
2:
btst #0,0x2003(a5) /* Main-CPU has Word RAM? */
beq.b 2b
move.w #0x2700,sr
lea 0xFFFD00,a0
movea.l a0,sp
jmp 0x200000.l
.org 0x1000
| Second part of cdrom header
|
| Sub-CPU IP, comprising sectors 2 to 15 from the header.
| Global Variable offsets
.equ VBLANK_HANDLER, 0
.equ VBLANK_PARAM, 4
.equ INIT_CD, 8
.equ READ_CD, 12
.equ SET_CWD, 16
.equ FIRST_DIR_SEC, 20
.equ NEXT_DIR_SEC, 24
.equ FIND_DIR_ENTRY, 28
.equ NEXT_DIR_ENTRY, 32
.equ LOAD_FILE, 36
.equ DISC_TYPE, 40
.equ DIR_ENTRY, 42
.equ CWD_OFFSET, 44
.equ CWD_LENGTH, 48
.equ CURR_OFFSET, 52
.equ CURR_LENGTH, 56
.equ ROOT_OFFSET, 60
.equ ROOT_LENGTH, 64
.equ DENTRY_OFFSET, 68
.equ DENTRY_LENGTH, 72
.equ DENTRY_FLAGS, 76
.equ DENTRY_NAME, 78
.equ TEMP_NAME, 78+256
.equ SIZE_GLOBALVARS,78+256+256
| Disc Read Buffer
.equ DISC_BUFFER, 0x6800
| Program Load Buffer
.equ LOAD_BUFFER, 0x8000
| ISO directory offsets (big-endian where applicable)
.equ RECORD_LENGTH, 0
.equ EXTENT, 6
.equ FILE_LENGTH, 14
.equ FILE_FLAGS, 25
.equ FILE_NAME_LEN, 32
.equ FILE_NAME, 33
| Primary Volume Descriptor offset
.equ PVD_ROOT, 0x9C
| CDFS Error codes
.equ ERR_READ_FAILED, -2
.equ ERR_NO_PVD, -3
.equ ERR_NO_MORE_ENTRIES,-4
.equ ERR_BAD_ENTRY, -5
.equ ERR_NAME_NOT_FOUND, -6
.equ ERR_NO_DISC, -7
| Standard MegaCD Sub-CPU Program Header at 0x1000 (loaded to 0x6000)
SPHeader:
.asciz "MAIN-SUBCPU"
.word 0x0001,0x0000
.long 0x00000000
.long 0x00000000
.long SPHeaderOffsets-SPHeader
.long 0x00000000
SPHeaderOffsets:
.word SPInit-SPHeaderOffsets
.word SPMain-SPHeaderOffsets
.word SPInt2-SPHeaderOffsets
.word SPNull-SPHeaderOffsets
.word 0x0000
| Sub-CPU Program Initialization (VBlank not enabled yet)
SPInit:
lea GlobalVars(pc),a0
move.l #0,VBLANK_HANDLER(a0) /* clear VBlank handler vector */
lea InitCD(pc),a1
move.l a1,INIT_CD(a0) /* set InitCD vector */
lea ReadCD(pc),a1
move.l a1,READ_CD(a0) /* set ReadCD vector */
lea SetCWD(pc),a1
move.l a1,SET_CWD(a0) /* set SetCWD vector */
lea FirstDirSector(pc),a1
move.l a1,FIRST_DIR_SEC(a0) /* set FirstDirSector vector */
lea NextDirSector(pc),a1
move.l a1,NEXT_DIR_SEC(a0) /* set NextDirSector vector */
lea FindDirEntry(pc),a1
move.l a1,FIND_DIR_ENTRY(a0) /* set FindDirEntry vector */
lea NextDirEntry(pc),a1
move.l a1,NEXT_DIR_ENTRY(a0) /* set NextDirEntry vector */
lea LoadFile(pc),a1
move.l a1,LOAD_FILE(a0) /* set LoadFile vector */
andi.b #0xE2,0x8003.w /* Priority Mode = off, 2M mode */
move.b #'I,0x800F.w /* send MD init handshake to MD */
rts
| Sub-CPU Program Main Entry Point (VBlank now enabled)
SPMain:
lea GlobalVars(pc),a6
lea iso_pvd_magic(pc),a5
bsr InitCD
| wait for boot handshake from main
0:
cmpi.b #'B,0x800E.w
bne.b 0b
move.b #0,0x800F.w /* clear sub comm port */
| read boot file from cd
lea root_dirname(pc),a0
bsr SetCWD /* set current working directory to root */
lea boot_name(pc),a0
lea LOAD_BUFFER.l,a1
bsr LoadFile
bne.b exit /* no Sub-CPU boot file */
1:
lea LOAD_BUFFER.l,a0 /* char *start(void) */
jsr (a0)
| app exited - if we return, the BIOS will call SPMain again
|
| my own idea - return NULL if done, else return a pointer to the filename
| to load and run, thus allowing apps to launch other apps
move.w #0x2700,sr /* disallow interrupts */
lea GlobalVars(pc),a6
clr.l VBLANK_HANDLER(a6)
clr.l VBLANK_PARAM(a6)
move.w #0x2000,sr /* allow interrupts */
tst.l d0
beq exit
move.l d0,-(sp)
andi.b #0xE2,0x8003.w /* Priority Mode = off, 2M mode */
move.b #'I,0x800F.w /* send init - switch Word RAM to Sub-CPU */
2:
cmpi.b #'I,0x800E.w
bne.b 2b /* wait for command ACK */
move.b #0,0x800F.w /* ACK handshake */
lea iso_pvd_magic(pc),a5
bsr InitCD
lea root_dirname(pc),a0
bsr SetCWD /* set current working directory to root */
movea.l (sp)+,a0
lea LOAD_BUFFER.l,a1
bsr LoadFile
beq.b 1b /* load okay */
exit:
rts
| Sub-CPU Program VBlank (INT02) Service Handler
SPInt2:
lea GlobalVars(pc),a0
tst.l VBLANK_HANDLER(a0)
bne.b 0f
rts
0:
move.l VBLANK_PARAM(a0),d0
move.l d0,-(sp)
movea.l VBLANK_HANDLER(a0),a0
jsr (a0)
addq.l #4,sp
rts
| Sub-CPU program Reserved Function - we use it get the loader global vars pointer
SPNull:
lea GlobalVars(pc),a0
move.l a0,d0
rts
|----------------------------------------------------------------------|
| File System Support Code |
|----------------------------------------------------------------------|
| Initialize CD - pass PVD Magic to look for in a5 and GlobalVars in a6
InitCD:
lea drive_init_parms(pc),a0
move.w #0x0010,d0 /* DRVINIT */
jsr 0x5F22.w /* call CDBIOS function */
move.w #0,d1 /* Mode 1 (CD-ROM with full error correction) */
move.w #0x0096,d0 /* CDCSETMODE */
jsr 0x5F22.w /* call CDBIOS function */
moveq #-1,d0
move.l d0,ROOT_OFFSET(a6)
move.l d0,ROOT_LENGTH(a6)
move.l d0,CURR_OFFSET(a6)
move.l d0,CURR_LENGTH(a6)
move.w d0,DISC_TYPE(a6) /* no disc/not recognized */
| find Primary Volume Descriptor
moveq #16,d2 /* starting sector when searching for PVD */
0:
lea DISC_BUFFER.w,a0 /* buffer */
move.l d2,d0 /* sector */
moveq #1,d1 /* # sectors */
move.w d2,-(sp)
bsr ReadCD
move.w (sp)+,d2
tst.l d0
bmi.b 9f /* error */
lea DISC_BUFFER.w,a0
movea.l a5,a1 /* PVD magic */
cmpm.l (a0)+,(a1)+
bne.b 1f /* next sector */
cmpm.l (a0)+,(a1)+
bne.b 1f /* next sector */
/* found PVD */
move.l DISC_BUFFER+PVD_ROOT+EXTENT.w,ROOT_OFFSET(a6)
move.l DISC_BUFFER+PVD_ROOT+FILE_LENGTH.w,ROOT_LENGTH(a6)
move.w #0,DISC_TYPE(a6) /* found PVD */
moveq #0,d0
rts
1:
addq.w #1,d2
cmpi.w #32,d2
bne.b 0b /* check next sector */
| No PVD found
moveq #ERR_NO_PVD,d0
9:
rts
| Set directory entry variables to next entry of directory in disc buffer
NextDirEntry:
lea DISC_BUFFER.w,a0
move.w DIR_ENTRY(a6),d2
cmpi.w #2048,d2
blo.b 1f
moveq #ERR_NO_MORE_ENTRIES,d0
rts
1:
tst.b (a0,d2.w) /* record length */
bne.b 2f
moveq #ERR_NO_MORE_ENTRIES,d0
rts
2:
lea (a0,d2.w),a1 /* entry */
moveq #0,d0
move.b (a1),d0 /* record length */
add.w d0,d2
move.w d2,DIR_ENTRY(a6) /* next entry */
cmpi.w #2048,d2
bls.b 3f
moveq #ERR_NO_MORE_ENTRIES,d0 /* entries should NEVER cross a sector boundary */
rts
3:
tst.b FILE_NAME_LEN(a1)
bne.b 4f
moveq #ERR_BAD_ENTRY,d0
rts
4:
move.b FILE_NAME_LEN(a1),d0
subq.w #1,d0
lea FILE_NAME(a1),a2
lea DENTRY_NAME(a6),a3
5:
move.b (a2)+,(a3)+
dbeq d0,5b
move.b #0,(a3) /* make sure is null-terminated */
lea DENTRY_NAME(a6),a2
/* check for special case 0 */
cmpi.b #0,(a2)
bne.b 9f
move.l #0x2E000000,(a2) /* "." */
bra.b 10f
9:
/* check for special case 1 */
cmpi.b #1,(a2)
bne.b 10f
move.l #0x2E2E0000,(a2) /* ".." */
10:
cmpi.b #0x3B,(a2)+ /* look for ";" */
beq.b 11f
tst.b (a2)
bne 10b
bra.b 12f
11:
move.b #0,-(a2) /* apply Rockridge correction to name */
12:
move.l EXTENT(a1),DENTRY_OFFSET(a6)
move.l FILE_LENGTH(a1),DENTRY_LENGTH(a6)
move.b FILE_FLAGS(a1),DENTRY_FLAGS(a6)
moveq #0,d0
rts
| Find entry in directory in the disc buffer using name in a0
FindDirEntry:
move.w DIR_ENTRY(a6),d0
cmpi.w #2048,d0
blo.b 1f
0:
moveq #ERR_NAME_NOT_FOUND,d0
rts
1:
move.l a0,-(sp)
bsr NextDirEntry
movea.l (sp)+,a0
bmi.b FindDirEntry
| got an entry, check the name
lea DENTRY_NAME(a6),a1
movea.l a0,a2
2:
cmpm.b (a1)+,(a2)+
bne.b FindDirEntry
tst.b -1(a1)
bne.b 2b
/* dentry holds match */
moveq #0,d0
rts
| Read first sector in CWD
FirstDirSector:
move.l CWD_OFFSET(a6),d0
cmp.l CURR_OFFSET(a6),d0
beq.b 0f /* already loaded, just reset length */
moveq #1,d1
lea DISC_BUFFER.w,a0 /* buffer */
bsr ReadCD
bmi.b 1f
/* disc buffer holds first sector of dir */
move.l CWD_OFFSET(a6),CURR_OFFSET(a6)
0:
clr.l CURR_LENGTH(a6)
clr.w DIR_ENTRY(a6)
moveq #0,d0
1:
rts
| Read next sector in CWD
NextDirSector:
addq.l #1,CURR_OFFSET(a6)
addi.l #2048,CURR_LENGTH(a6)
move.l CWD_LENGTH(a6),d0
cmp.l CURR_LENGTH(a6),d0
bhi.b 0f
moveq #ERR_NO_MORE_ENTRIES,d0
rts
0:
move.l CURR_OFFSET(a6),d0
moveq #1,d1
lea DISC_BUFFER.w,a0 /* buffer */
bsr ReadCD
bmi 1f
/* disc buffer holds next sector of dir */
clr.w DIR_ENTRY(a6)
moveq #0,d0
1:
rts
| Set current working directory using path at a0
SetCWD:
cmpi.b #0x2F,(a0) /* check for leading "/" */
bne.b 0f /* relative to cwd */
/* start at root dir */
addq.l #1,a0 /* skip over "/" */
move.l ROOT_OFFSET(a6),CWD_OFFSET(a6)
move.l ROOT_LENGTH(a6),CWD_LENGTH(a6)
0:
move.l a0,-(sp)
bsr FirstDirSector /* disc buffer holds first sector of dir */
movea.l (sp)+,a0
bmi.b 2f
/* check if done */
tst.b (a0)
bne.b 3f
1:
moveq #0,d0
2:
rts
3:
addq.l #1,a0 /* skip over "/" */
tst.b (a0)
beq.b 1b /* was trailing "/" */
/* copy next part of path to temp */
lea TEMP_NAME(a6),a1
4:
move.b (a0)+,(a1)+
beq.b 5f
cmpi.b #0x2F,-1(a0) /* check for "/" */
bne.b 4b
5:
clr.b -1(a1) /* null terminate string in temp */
subq.l #1,a0
6:
/* check current directory sector for entry */
move.l a0,-(sp)
lea TEMP_NAME(a6),a0
bsr FindDirEntry
movea.l (sp)+,a0
bmi.b 7f
/* found this part of path */
move.l DENTRY_OFFSET(a6),CWD_OFFSET(a6)
move.l DENTRY_LENGTH(a6),CWD_LENGTH(a6)
bra.b 0b /* read first sector of dir and check if done */
7:
/* not found, try next sector */
move.l a0,-(sp)
bsr NextDirSector
movea.l (sp)+,a0
beq.b 6b
moveq #ERR_NAME_NOT_FOUND,d0
rts
| Load file in CWD with name at a0 to memory at a1
LoadFile:
movem.l a0-a1,-(sp)
bsr FirstDirSector
movem.l (sp)+,a0-a1
0:
/* check current directory sector for entry */
movem.l a0-a1,-(sp)
bsr FindDirEntry
movem.l (sp)+,a0-a1
bmi.b 1f
/* found file */
move.l DENTRY_OFFSET(a6),d0
move.l DENTRY_LENGTH(a6),d1
addi.l #2047,d1
moveq #11,d2
lsr.l d2,d1 /* # sectors */
movea.l a1,a0
bra ReadCD
1:
/* not found, try next sector */
movem.l a0-a1,-(sp)
bsr NextDirSector
movem.l (sp)+,a0-a1
beq.b 0b
moveq #ERR_NAME_NOT_FOUND,d0
rts
| Read d1 sectors starting at d0 into buffer in a0 (using Sub-CPU)
ReadCD:
movem.l d0-d1/a0-a1,-(sp)
0:
move.w #0x0089,d0 /* CDCSTOP */
jsr 0x5F22.w /* call CDBIOS function */
movea.l sp,a0 /* ptr to 32 bit sector start and 32 bit sector count */
move.w #0x0020,d0 /* ROMREADN */
jsr 0x5F22.w /* call CDBIOS function */
1:
move.w #0x008A,d0 /* CDCSTAT */
jsr 0x5F22.w /* call CDBIOS function */
bcs.b 1b /* no sectors in CD buffer */
/* set CDC Mode destination device to Sub-CPU */
andi.w #0xF8FF,0x8004.w
ori.w #0x0300,0x8004.w
2:
move.w #0x008B,d0 /* CDCREAD */
jsr 0x5F22.w /* call CDBIOS function */
bcs.b 2b /* not ready to xfer data */
movea.l 8(sp),a0 /* data buffer */
lea 12(sp),a1 /* header address */
move.w #0x008C,d0 /* CDCTRN */
jsr 0x5F22.w /* call CDBIOS function */
bcs.b 0b /* failed, retry */
move.w #0x008D,d0 /* CDCACK */
jsr 0x5F22.w /* call CDBIOS function */
addq.l #1,(sp) /* next sector */
addi.l #2048,8(sp) /* inc buffer ptr */
subq.l #1,4(sp) /* dec sector count */
bne.b 1b
lea 16(sp),sp /* cleanup stack */
rts
|----------------------------------------------------------------------|
| Global Variables |
|----------------------------------------------------------------------|
.align 2
root_dirname:
.asciz "/"
.align 2
boot_name:
.asciz "APP.BIN"
.align 2
iso_pvd_magic:
.asciz "\1CD001\1"
.align 2
drive_init_parms:
.byte 0x01, 0xFF /* first track (1), last track (all) */
.align 4
GlobalVars:
.space SIZE_GLOBALVARS
.org 0x8000
Notice how I pad out the loader to 0x8000 - that's 16 sectors. When I burn a CD, the binary from the above code is written to the first 16 sectors as a boot block (mkisofs allows for custom boot blocks). Then the actual SCD game code goes in the file to load - in the above case, it's "APP.BIN" but can be changed to anything I want. My boot loader goes above and beyond by providing the program a global structure with key routines in the loader available for call from the game so that things like reading ISO9660 files don't need to be duplicated in the game.
And that's how SCD games start up - the BIOS loads the first couple sectors to get the header, loads the rest of the boot loader, checks the security blob, then runs the code, which eventually falls into the user code after the security blob which then loads the game code somehow.