loading...
All Genesis articles | Back to top

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

68000 Exception Table

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

2 x 32 KB RAM chips (source: allaboutcircuits.com)

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

Megadrive 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

Video Display Processor

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

References

xAD at nightfallcrew

SN76489 Programmable Sound Generator