SEGA Genesis: Building a ROM
In this article, the initialization tables and code in a SEGA Genesis ROM will be described in detail.
Interrupt Table and ROM Header
The first 256 B of a SEGA Genesis ROM is taken up by an initialization table consisting of a list of addresses, specified as 32-bit long words. The first address points to an address in RAM, and holds the initial address for the stack. The next address is the start of the program, the main entry point of the ROM. After the entry point is a list of addresses for handlers that handle errors and interrupts.
The beginning of the initialization table looks like this:
dc.l $00FFFFFE ; Initial stack pointer value
dc.l EntryPoint ; Start of program
dc.l except_unknown ; Bus error
dc.l except_unknown ; Address error
dc.l except_unknown ; Illegal instruction
dc.l except_unknown ; Division by zero
dc.l except_unknown ; CHK exception
dc.l except_unknown ; TRAPV exception
dc.l except_unknown ; Privilege violation
dc.l except_unknown ; TRACE exception
dc.l except_unknown ; Line-A emulator
dc.l except_unknown ; Line-F emulator
dc.l except_unknown ; Unused (reserved)
...
dc.l except_unknown ; IRQ level 1
dc.l except_unknown ; IRQ level 2
dc.l except_unknown ; IRQ level 3
dc.l hblank_interrupt ; IRQ level 4 (horizontal retrace interrupt)
dc.l except_unknown ; IRQ level 5
dc.l vblank_interrupt ; IRQ level 6 (vertical retrace interrupt)
...
The exception handler is empty in our example, as is our hblank and vblank interrupt handlers:
except_unknown
rte ; return from exception (seems to restore PC)
hblank_interrupt
rte
vblank_interrupt
rte
After the initialization table, the ROM header follows, which contains metadata such as the title of the software, version number, checksum, and some memory mapping info. It is also 256 B, and note the use of strings padded with spaces, as the metadata entries all must start at particular offsets:
dc.b "SEGA GENESIS " ; Console name
dc.b "(C)NAMELESS 2018" ; Copyrght holder and release date
dc.b "MINIMAL GENESIS CODE BY NAMELESS ALGORITHM " ; Domest. name
dc.b "MINIMAL GENESIS CODE BY NAMELESS ALGORITHM " ; Intern. name
dc.b "2018-07-01 " ; Version number
dc.w $0000 ; Checksum
dc.b "J " ; I/O support
dc.l $00000000 ; Start address of ROM
dc.l __end ; End address of ROM
dc.l $00FF0000 ; Start address of RAM
dc.l $00FFFFFF ; End address of RAM
dc.l $00000000 ; SRAM enabled
dc.l $00000000 ; Unused
dc.l $00000000 ; Start address of SRAM
dc.l $00000000 ; End address of SRAM
dc.l $00000000 ; Unused
dc.l $00000000 ; Unused
dc.b " " ; Notes (unused)
dc.b "JUE " ; Country codes
First Steps
Note: All the following is based on code and explanation from Matt Phillips over at Big Evil Corporation, edited by Lionel Sanderson, and further modified by the Nameless Algorithm.
The memory and registers of the Genesis is not guaranteed to be in a particular state when the machine is turned on, so we need to initialize everything to ensure deterministic behaviour in all situations. The initialization code takes care of this.
First, we disable interrupts, so the initialization is guaranteed to finish:
move #$2700,sr ; disable interrupts
The reset button on the Genesis normally works as a soft reset, so we test for that being pressed, which will skip the initialization:
; Check Reset Button
EntryPoint: ; Entry point address set in ROM header
tst.w $00A10008 ; Test mystery reset (expansion port reset?)
bne __main ; Branch if Not Equal (to zero) - to Main
tst.w $00A1000C ; Test reset button
bne __main ; Branch if Not Equal (to zero) - to Main
Next, we clear the full 64 KB of RAM:
move.l #$00000000,d0 ; Place a 0 into d0, ready to copy to each longword
; of RAM
move.l #$00000000,a0 ; Starting from address $0,clearing backwards
move.l #$00003FFF,d1 ; Clearing 64k's worth of longwords (minus 1,for
; the loop to be correct)
@Clear:
move.l d0,-(a0) ; Decrement the address by 1 longword,before moving
; the zero from d0 to it
dbra d1,@Clear ; Decrement d0,repeat until depleted
Next, we have the Trade Mark Security Signature (TMSS), which was a feature to stop unlicensed developers from creating games for the system. It is described somewhat sarcastically in Matt's code:
The Trade Mark Security Signature – or TMSS – was a feature put in by Sega to combat unlicensed developers from releasing games for their system, which is a kind of killswitch for the VDP. It’s the pinnacle of security systems, a very sophisticated encryption key which is almost uncrackable. You write the string "SEGA" to $00A14000.
It looks like this:
move.b $00A10001,d0 ; Move Megadrive hardware version to d0
andi.b #$0F,d0 ; The version is stored in last four bits,
; so mask it with 0F
beq @Skip ; If version is equal to 0,skip TMSS signature
move.l #'SEGA',$00A14000 ; Move the string "SEGA" to $A14000
Initializing the Z80
Next, we initialize the Z80 coprocessor with some dummy code. The Z80 has a different instruction set than the MC68000, so it is assembled separately. Here is our dummy Z80 code:
loop:
nop
jp loop
Using vasm compiled for Z80 assembly, we generate a binary file:
vasmZ80 -Fbin z80.asm -o z80.bin
Which outputs z80.bin
, a tiny 4 B file containing only:
00 C3 00 00
The file is included directly in our MC68000 assemble code:
Z80Code:
incbin "z80.bin"
Z80CodeEnd:
And now we can copy the code to the Z80's own memory:
move.w #$0100,$00A11100 ; Request access to the Z80 bus, by writing $0100
; into the BUSREQ port
move.w #$0100,$00A11200 ; Hold the Z80 in a reset state, by writing $0100
; into the RESET port
@Wait:
btst #$0,$00A11101 ; Test bit 0 of A11100 to see if the 68k has
; access to the Z80 bus yet
bne @Wait ; If we don't yet have control,branch back up to Wait
; Now the 68000 has access to the Z80’s bus, and the chip is
; held in a reset state,so we can write the program data to
; its memory. This is mapped from $A000000.
move.l #Z80Code,a0 ; Load address of data into a0
move.l #$00A00000,a1 ; Copy Z80 RAM address to a1
move.l #(Z80CodeEnd-Z80Code-1),d0
@CopyZ80:
move.b (a0)+,(a1)+ ; Copy data,and increment the source/dest addresses
dbra d0,@CopyZ80
move.w #$0000,$00A11200 ; Release reset state
move.w #$0000,$00A11100 ; Release control of bus
Initializing the VDP
The Video Display Processor should also be initialized to a well-defined state.
We start out by defining two useful constants:
vdp_control = $C00004 ; Memory mapped I/O
vdp_data = $C00000 ;
The VDP has a set of registers, 24 B in total, that define the state of the VDP. To initialize it, we write all the registers:
; Initialising the VDP
move.l #vdp_init,a0 ; Load address of register table into a0
move.l #24,d0 ; 24 registers to write
move.l #$00008000,d1 ; 'Set register 0' command
.copy_register:
move.b (a0)+,d1 ; Move register value to lower byte of d1
move.w d1,vdp_control ; Write command and value to VDP control port
add.w #$0100,d1 ; Increment register #
dbra d0,.copy_register
Here is the register table:
; VDP REGISTER INITIALIZATION
; -------------------------------------------------------------
vdp_init:
VDPReg0: dc.b $14 ; 0: H interrupt on, palettes on
VDPReg1: dc.b $74 ; 1: V interrupt on, display on, DMA on, Genesis mode on
VDPReg2: dc.b $30 ; 2: Pattern table for Scroll Plane A at VRAM $C000
; (bits 3-5 = bits 13-15)
VDPReg3: dc.b $00 ; 3: Pattern table for Window Plane at VRAM $0000
; (disabled) (bits 1-5 = bits 11-15)
VDPReg4: dc.b $07 ; 4: Pattern table for Scroll Plane B at VRAM $E000
; (bits 0-2 = bits 11-15)
VDPReg5: dc.b $78 ; 5: Sprite table at VRAM $F000 (bits 0-6 = bits 9-15)
VDPReg6: dc.b $00 ; 6: Unused
VDPReg7: dc.b $00 ; 7: Background colour - bits 0-3 = colour,
; bits 4-5 = palette
VDPReg8: dc.b $00 ; 8: Unused
VDPReg9: dc.b $00 ; 9: Unused
VDPRegA: dc.b $FF ; 10: Frequency of Horiz. interrupt in Rasters
; (number of lines travelled by the beam)
VDPRegB: dc.b $00 ; 11: External interrupts off, V scroll fullscreen,
; H scroll fullscreen
VDPRegC: dc.b $81 ; 12: Shadows and highlights off, interlace off,
; H40 mode (320 x 224 screen res)
VDPRegD: dc.b $3F ; 13: Horiz. scroll table at VRAM $FC00 (bits 0-5)
VDPRegE: dc.b $00 ; 14: Unused
VDPRegF: dc.b $02 ; 15: Autoincrement 2 bytes
VDPReg10: dc.b $01 ; 16: Vert. scroll 32, Horiz. scroll 64
VDPReg11: dc.b $00 ; 17: Window Plane X pos 0 left
; (pos in bits 0-4, left/right in bit 7)
VDPReg12: dc.b $00 ; 18: Window Plane Y pos 0 up
; (pos in bits 0-4, up/down in bit 7)
VDPReg13: dc.b $FF ; 19: DMA length lo byte
VDPReg14: dc.b $FF ; 20: DMA length hi byte
VDPReg15: dc.b $00 ; 21: DMA source address lo byte
VDPReg16: dc.b $00 ; 22: DMA source address mid byte
VDPReg17: dc.b $80 ; 23: DMA source address hi byte,
; memory-to-VRAM mode (bits 6-7)
Palette data is initialized in Color RAM:
load_palette:
move.w #$8F02,vdp_control ; Set VDP autoincrement to 2 words/write
move.l #$C0000003,vdp_control ; Set up VDP to write to CRAM address $0000
move.w #$0000,vdp_data ; black (BGR)
move.w #$000f,vdp_data ; red
move.w #$0f00,vdp_data ; blue
move.w #$00f0,vdp_data ; green
Final Steps
FIXME: how about the YM2612? Should it not be initialized?
In addition to the Yamaha YM2612 FM sound chip, the Genesis contains a Programmable Sound Generator (PSG), which is integrated in the VDP. We initialize the PSG to be silent like this:
; Initialising the PSG (programmable sound generator)
move.l #PSGData,a0 ; Load address of PSG data into a0
move.l #$03,d0 ; 4 bytes of data
@CopyPSG:
move.b (a0)+,$00C00011 ; Copy data to PSG RAM
dbra d0,@CopyPSG
PSGData:
dc.w $9fbf, $dfff ; silence
; Initialising the Controller Ports
; The controller ports are generic 9-pin I/O ports,and are not particularly
; tailored to any device.
; They have five mapped I/O address each – CTRL,DATA,TX,RX and S-CTRL:
;
; CTRL controls the I/O direction and enables/disables interrupts generated
; by the port
; DATA is used to send/receive data to or from the port (in bytes or words)
; when the port is in parallel mode
; TX and RX are used to send/receive data in serial mode
; S-CTRL is used to get/set the port’s current status,baud rate and
; serial/parallel mode.
; Set IN I/O direction,interrupts off,on all ports
move.b #$00,$000A10009 ; Controller port 1 CTRL
move.b #$00,$000A1000B ; Controller port 2 CTRL
move.b #$00,$000A1000D ; EXP port CTRL
Finally, we reset the status register:
; Init status register (no trace,A7 is Interrupt Stack Pointer,no interrupts,clear condition code bits)
move #$2700,sr
And we're ready to go!
Full Source
; ==============================================================================
; MAIN game source code
;
;
; Sources
; -------
; Video Display Processor (VDP)
; VDP notes
; https://emudocs.org/Genesis/Graphics/genvdp.txt
; VDP Overview
; http://md.squee.co/VDP
; Sprites
; https://wiki.megadrive.org/index.php?title=VDP_Sprites
; Hello world
; https://bigevilcorporation.co.uk/2012/03/23/sega-megadrive-4-hello-world/
; ROM header and initialization
; https://en.wikibooks.org/wiki/Genesis_Programming#ROM_header
; http://darkdust.net/writings/megadrive/initializing
; http://darkdust.net/writings/megadrive/firststeps
; Memory Map
; https://emu-docs.org/Genesis/gen-hw.txt
; Controller Input
; http://md.squee.co/Howto:Read_Control_Pads
; MC68000 Assembler Code
; Instruction Tutorial
; http://mrjester.hapisan.com/04_MC68/Index.html
; Jump tables (From sonic.asm)
; https://github.com/sonicretro/s1disasm
; ==============================================================================
; ROM HEADER
; ------------------------------------------------------------------------------
rom_header:
dc.l $00FFFFFE ; Initial stack pointer value
dc.l EntryPoint ; Start of program
dc.l except_unknown ; Bus error
dc.l except_unknown ; Address error
dc.l except_unknown ; Illegal instruction
dc.l except_unknown ; Division by zero
dc.l except_unknown ; CHK exception
dc.l except_unknown ; TRAPV exception
dc.l except_unknown ; Privilege violation
dc.l except_unknown ; TRACE exception
dc.l except_unknown ; Line-A emulator
dc.l except_unknown ; Line-F emulator
dc.l except_unknown ; Unused (reserved)
dc.l except_unknown ; Unused (reserved)
dc.l except_unknown ; Unused (reserved)
dc.l except_unknown ; Unused (reserved)
dc.l except_unknown ; Unused (reserved)
dc.l except_unknown ; Unused (reserved)
dc.l except_unknown ; Unused (reserved)
dc.l except_unknown ; Unused (reserved)
dc.l except_unknown ; Unused (reserved)
dc.l except_unknown ; Unused (reserved)
dc.l except_unknown ; Unused (reserved)
dc.l except_unknown ; Unused (reserved)
dc.l except_unknown ; Spurious exception
dc.l except_unknown ; IRQ level 1
dc.l except_unknown ; IRQ level 2
dc.l except_unknown ; IRQ level 3
dc.l hblank_interrupt ; IRQ level 4 (horizontal retrace interrupt)
dc.l except_unknown ; IRQ level 5
dc.l vblank_interrupt ; IRQ level 6 (vertical retrace interrupt)
dc.l except_unknown ; IRQ level 7
dc.l except_unknown ; TRAP #00 exception
dc.l except_unknown ; TRAP #01 exception
dc.l except_unknown ; TRAP #02 exception
dc.l except_unknown ; TRAP #03 exception
dc.l except_unknown ; TRAP #04 exception
dc.l except_unknown ; TRAP #05 exception
dc.l except_unknown ; TRAP #06 exception
dc.l except_unknown ; TRAP #07 exception
dc.l except_unknown ; TRAP #08 exception
dc.l except_unknown ; TRAP #09 exception
dc.l except_unknown ; TRAP #10 exception
dc.l except_unknown ; TRAP #11 exception
dc.l except_unknown ; TRAP #12 exception
dc.l except_unknown ; TRAP #13 exception
dc.l except_unknown ; TRAP #14 exception
dc.l except_unknown ; TRAP #15 exception
dc.l except_unknown ; Unused (reserved)
dc.l except_unknown ; Unused (reserved)
dc.l except_unknown ; Unused (reserved)
dc.l except_unknown ; Unused (reserved)
dc.l except_unknown ; Unused (reserved)
dc.l except_unknown ; Unused (reserved)
dc.l except_unknown ; Unused (reserved)
dc.l except_unknown ; Unused (reserved)
dc.l except_unknown ; Unused (reserved)
dc.l except_unknown ; Unused (reserved)
dc.l except_unknown ; Unused (reserved)
dc.l except_unknown ; Unused (reserved)
dc.l except_unknown ; Unused (reserved)
dc.l except_unknown ; Unused (reserved)
dc.l except_unknown ; Unused (reserved)
dc.l except_unknown ; Unused (reserved)
dc.b "SEGA GENESIS " ; Console name
dc.b "(C) NAMELESS " ; Copyrght holder and release date
dc.b "MINIMAL GENESIS CODE BY NAMELESS ALGORITHM " ; Domest. name
dc.b "MINIMAL GENESIS CODE BY NAMELESS ALGORITHM " ; Intern. name
dc.b "2018-07-01 " ; Version number
dc.w $0000 ; Checksum
dc.b "J " ; I/O support
dc.l $00000000 ; Start address of ROM
dc.l __end ; End address of ROM
dc.l $00FF0000 ; Start address of RAM
dc.l $00FFFFFF ; End address of RAM
dc.l $00000000 ; SRAM enabled
dc.l $00000000 ; Unused
dc.l $00000000 ; Start address of SRAM
dc.l $00000000 ; End address of SRAM
dc.l $00000000 ; Unused
dc.l $00000000 ; Unused
dc.b " " ; Notes (unused)
dc.b "JUE " ; Country codes
; CONSTANTS
; ------------------------------------------------------------------------------
vdp_control = $C00004 ; Memory mapped I/O
vdp_data = $C00000 ;
; MEMORY MAP
; ------------------------------------------------------------------------------
col = $E00000 ; 1B
; MAIN PROGRAM
; ------------------------------------------------------------------------------
__init
jmp init_system
__main
; Load palette data into CRAM
load_palette:
move.w #$8F02,vdp_control ; Set VDP autoincrement to 2 words/write
move.l #$C0000003,vdp_control ; Set up VDP to write to CRAM address $0000
move.w #$0000,vdp_data ; black (BGR)
move.w #$000f,vdp_data ; red
move.w #$0f00,vdp_data ; blue
move.w #$00f0,vdp_data ; green
; Load font data into VRAM
load_font:
move.w #$8F02,vdp_control ; Set VDP autoincrement to 2 words/write
move.l #$40000000,vdp_control ; Set up VDP to write to VRAM address $0000
move.l #$00000000,vdp_data ; Move data to VDP data port, increment source addr
move.l #$00222200,vdp_data
move.l #$02222220,vdp_data
move.l #$02211220,vdp_data
move.l #$02211220,vdp_data
move.l #$02222220,vdp_data
move.l #$00222200,vdp_data
move.l #$00000000,vdp_data
; Bits: [BBAA AAAA AAAA AAAA 0000 0000 BBBB 00AA]
; - 0 is always just 0
; - A is destination address, in this order:
; [..DC BA98 7654 3210 .... .... .... ..FE]
; - B is Operation type, in this order:
; [10.. .... .... .... .... .... 5432 ....]
move.w #$C000,d3 ; tilemap base address
move.l #$40000000,d4 ; d4 : command (VRAM write)
clr.l d5 ; clear high word of d3
move.w d3,d5 ; d3 = offset
and.w #%1100000000000000,d5 ; 2 most sign. bits of offset...
lsr.w #7,d5 ; ...shifted 14 bits right
lsr.w #7,d5 ;
or.l d5,d4 ; d4 |= d3
clr.l d5 ; clear high word of d3
move.w d3,d5
and.w #%0011111111111111,d5 ;
lsl.l #8,d5 ;
lsl.l #8,d5 ;
or.l d5,d4 ; d4 |= d3
move.w #$8F02,vdp_control ; Set VDP autoincrement to 2 bytes
move.l d4,vdp_control
move.w #$6E7,d0
.loop
move.w #1,vdp_data ; write character index 1 to VDP
dbra d0,.loop
.done
move.w #0,d0
move.w #$8F00,vdp_control ; Set VDP autoincrement to 2 words/write
move.l #$C0000003,vdp_control ; Set up VDP to write to CRAM address $0000
loop
move.w d0,vdp_data ; black (BGR)
add.w #1,d0
move.w #50000,d1
.wait
dbra d1,.wait
jmp loop
;-------------------------------------------------------------------------------
; SEGA Megadrive initialization
; Original version:
; Lionel Sanderson,26-06-16
; SOURCE: https://bigevilcorporation.co.uk/2012/03/09/sega-megadrive-3-awaking-the-beast/
;
; Updated version:
; Nameless Algorithm, 2018-07-01
; http://namelessalgorithm.com/genesis/blog/2017-06-11-genesis/
; - Added interrupt disable in the beginning
; - Removed buggy register initialization code
; - Modified to vasm motorola syntax
;-------------------------------------------------------------------------------
init_system:
move #$2700,sr ; disable interrupts
; Check Reset Button
EntryPoint: ; Entry point address set in ROM header
tst.w $00A10008 ; Test mystery reset (expansion port reset?)
bne __main ; Branch if Not Equal (to zero) - to Main
tst.w $00A1000C ; Test reset button
bne __main ; Branch if Not Equal (to zero) - to Main
; Clear RAM
move.l #$00000000,d0 ; Place a 0 into d0, ready to copy to each longword
; of RAM
move.l #$00000000,a0 ; Starting from address $0,clearing backwards
move.l #$00003FFF,d1 ; Clearing 64k's worth of longwords (minus 1,for
; the loop to be correct)
@Clear:
move.l d0,-(a0) ; Decrement the address by 1 longword,before moving
; the zero from d0 to it
dbra d1,@Clear ; Decrement d0,repeat until depleted
; Write the TMSS:
; The Trade Mark Security Signature – or TMSS – was a feature put in by Sega to
; combat unlicensed developers from releasing games for their system, which is
; a kind of killswitch for the VDP. It’s the pinnacle of security systems, a
; very sophisticated encryption key which is almost uncrackable.
; You write the string “SEGA” to $00A14000.
move.b $00A10001,d0 ; Move Megadrive hardware version to d0
andi.b #$0F,d0 ; The version is stored in last four bits,
; so mask it with 0F
beq @Skip ; If version is equal to 0,skip TMSS signature
move.l #'SEGA',$00A14000 ; Move the string "SEGA" to $A14000
@Skip:
; Initialize Z80
move.w #$0100,$00A11100 ; Request access to the Z80 bus, by writing $0100
; into the BUSREQ port
move.w #$0100,$00A11200 ; Hold the Z80 in a reset state, by writing $0100
; into the RESET port
@Wait:
btst #$0,$00A11101 ; Test bit 0 of A11100 to see if the 68k has
; access to the Z80 bus yet
bne @Wait ; If we don't yet have control,branch back up to Wait
; Now the 68000 has access to the Z80’s bus, and the chip is
; held in a reset state,so we can write the program data to its
; memory. This is mapped from $A000000.
move.l #Z80Code,a0 ; Load address of data into a0
move.l #$00A00000,a1 ; Copy Z80 RAM address to a1
move.l #(Z80CodeEnd-Z80Code-1),d0
@CopyZ80:
move.b (a0)+,(a1)+ ; Copy data,and increment the source/dest addresses
dbra d0,@CopyZ80
move.w #$0000,$00A11200 ; Release reset state
move.w #$0000,$00A11100 ; Release control of bus
; Initialising the PSG (programmable sound generator)
move.l #PSGData,a0 ; Load address of PSG data into a0
move.l #$03,d0 ; 4 bytes of data
@CopyPSG:
move.b (a0)+,$00C00011 ; Copy data to PSG RAM
dbra d0,@CopyPSG
; Initialising the VDP
move.l #VDPRegisters,a0 ; Load address of register table into a0
move.l #$18,d0 ; 24 registers to write
move.l #$00008000,d1 ; 'Set register 0' command
; (and clear the rest of d1 ready)
@CopyVDP:
move.b (a0)+,d1 ; Move register value to lower byte of d1
move.w d1,$00C00004 ; Write command and value to VDP control port
add.w #$0100,d1 ; Increment register #
dbra d0,@CopyVDP
; Initialising the Controller Ports
; The controller ports are generic 9-pin I/O ports,and are not particularly
; tailored to any device.
; They have five mapped I/O address each – CTRL,DATA,TX,RX and S-CTRL:
;
; CTRL controls the I/O direction and enables/disables interrupts generated
; by the port
; DATA is used to send/receive data to or from the port (in bytes or words)
; when the port is in parallel mode
; TX and RX are used to send/receive data in serial mode
; S-CTRL is used to get/set the port’s current status,baud rate and
; serial/parallel mode.
; Set IN I/O direction,interrupts off,on all ports
move.b #$00,$000A10009 ; Controller port 1 CTRL
move.b #$00,$000A1000B ; Controller port 2 CTRL
move.b #$00,$000A1000D ; EXP port CTRL
; Tidy Up
move.l #$00000000,a0 ; Move $0 to a0
; movem.l (a0),d0-d7/a1-a7 ; Multiple move 0 to all registers
; ^ Nameless Algorithm:
; This seems like a mistake. The instruction:
;
; MOVEM <ea>,list ; Source -> Listed Registers
;
; Copies the contents of 'ea' to the listed registers. In the example
; code, A0 is set to 0, which, according to sega2.doc points to the start
; of the ROM, which contains random stuff.
; Specifically, the stack pointer was overwritten, causing subroutines to
; return to bad locations.
; Status register
; Init status register (no trace, A7 is Interrupt Stack Pointer,no interrupts,clear condition code bits)
move #$2700,sr
jmp __main
; EXCEPTION AND INTERRUPT HANDLERS
; ----------------------------------------------------------------------------
align 2 ; word-align code
except_unknown
rte ; return from exception (seems to restore PC)
hblank_interrupt
move.w #$8F00,vdp_control ; Set VDP autoincrement to 2 words/write
move.l #$C0000003,vdp_control ; Set up VDP to write to CRAM address $0000
move.w col,d0
add.w #1,d0
move.w d0,vdp_data ; black (BGR)
move.w d0,col
rte
vblank_interrupt
rte ; return from exception (seems to restore PC)
align 2 ; word-align code
; loop:
; nop
; jp loop
Z80Code:
incbin "z80.bin"
Z80CodeEnd:
align 2 ; word-align code
__end:
; VDP REGISTER INITIALIZATION
; -------------------------------------------------------------
VDPRegisters:
VDPReg0: dc.b $14 ; 0: H interrupt on, palettes on
VDPReg1: dc.b $74 ; 1: V interrupt on, display on, DMA on, Genesis mode on
VDPReg2: dc.b $30 ; 2: Pattern table for Scroll Plane A at VRAM $C000
; (bits 3-5 = bits 13-15)
VDPReg3: dc.b $00 ; 3: Pattern table for Window Plane at VRAM $0000
; (disabled) (bits 1-5 = bits 11-15)
VDPReg4: dc.b $07 ; 4: Pattern table for Scroll Plane B at VRAM $E000
; (bits 0-2 = bits 11-15)
VDPReg5: dc.b $78 ; 5: Sprite table at VRAM $F000 (bits 0-6 = bits 9-15)
VDPReg6: dc.b $00 ; 6: Unused
VDPReg7: dc.b $00 ; 7: Background colour - bits 0-3 = colour,
; bits 4-5 = palette
VDPReg8: dc.b $00 ; 8: Unused
VDPReg9: dc.b $00 ; 9: Unused
VDPRegA: dc.b $FF ; 10: Frequency of Horiz. interrupt in Rasters
; (number of lines travelled by the beam)
VDPRegB: dc.b $00 ; 11: External interrupts off, V scroll fullscreen,
; H scroll fullscreen
VDPRegC: dc.b $81 ; 12: Shadows and highlights off, interlace off,
; H40 mode (320 x 224 screen res)
VDPRegD: dc.b $3F ; 13: Horiz. scroll table at VRAM $FC00 (bits 0-5)
VDPRegE: dc.b $00 ; 14: Unused
VDPRegF: dc.b $02 ; 15: Autoincrement 2 bytes
VDPReg10: dc.b $01 ; 16: Vert. scroll 32, Horiz. scroll 64
VDPReg11: dc.b $00 ; 17: Window Plane X pos 0 left
; (pos in bits 0-4, left/right in bit 7)
VDPReg12: dc.b $00 ; 18: Window Plane Y pos 0 up
; (pos in bits 0-4, up/down in bit 7)
VDPReg13: dc.b $FF ; 19: DMA length lo byte
VDPReg14: dc.b $FF ; 20: DMA length hi byte
VDPReg15: dc.b $00 ; 21: DMA source address lo byte
VDPReg16: dc.b $00 ; 22: DMA source address mid byte
VDPReg17: dc.b $80 ; 23: DMA source address hi byte,
; memory-to-VRAM mode (bits 6-7)
; PROGRAMMABLE SOUND GENERATOR
; ------------------------------------------------------------------------------
PSGData:
dc.w $9fbf, $dfff ; silence