loading...

SEGA Genesis: Building a ROM

This first article is focused on building a very simple ROM for the SEGA Genesis using assembly code. The ROM does a simple graphical effect, flickering the background color. Getting text on the screen is more complicated, so a proper 'Hello World' ROM will have to wait.

Genesis Hardware

SEGA Megadrive PAL Model 1

The SEGA Genesis had a Motorola 68000 CPU running at 7.6 MHz, a pretty fast processor for its time. It had very limited memory, only 64 KB main RAM and 64 KB Video RAM, half of what a decent home computer of the time would have, but that would be offset by the storage device being ROM cartridges with 512 KB - 8 MB, reducing any load time to almost zero.

The Genesis had a secondary CPU, a Zilog Z80 running at 3.58 MHz with 8 KB RAM. The Z80 was a very popular CPU in the late 70s and early 80s, used in the ZX Spectrum, the MSX computers, the Sega Master System, and in arcade games such as Pac-Man, Galaga, Dig Dug, Xevious, and others.

The Z80 was primarily used for controlling the audio chips, a Yamaha YM2612 FM chip which delivered 6 stereo FM channels and the Texas Instruments SN76489 Programmable Sound Generator (PSG), which has 3 square waves and a noise channel.

For this article, we're going to focus on the Motorola 68000.

The Motorola 68000

Motorola 68000

The Motorola 68000 is a 16/32-bit processor introduced in 1979. It was very popular in 80s computers, notably:

It was also a popular CPU for arcade games:

For the 1988 Genesis, the MC68000 was an obvious choice, it was already a mainstay in home computers, and SEGA already used it their System 16 arcade hardware, presumably making it much easier to port arcade games to the home market. In fact, the Genesis architecture is very similar to the System 16, which also features a Z80, a Yamaha FM chip, and a standard resolution of around 320x224.

Markey Jester's Motorola 68000 Beginner's Tutorial is useful for learning MC68000 assembly code, the game development language of choice for game programmers.

Tools

The only tool needed to create a ROM is an MC68000 assembler. The open source assembler vasm built with Motorola-style syntax works well for this purpose.

Currently, there isn't a Windows binary download of vasm, so it is necessary to build it from source. It is distributed with a Makefile, which can be built with Visual Studio's nmake tool. To build it, start the Visual Studio 'Developer Command Prompt' and enter the directory containing the vasm source, and then execute these commands:

makedir obj_win32
nmake -f Makefile.Win32 CPU=m68k SYNTAX=mot

This generates the executable vasmm68k_mot_win32.exe, which can be used to create a ROM file.

We can test the assembler with a simple program:

    move.w #7,d0

A single instruction that sets register D0 to the value 7.

Assemble it using this command:

vasmm68k_mot_win32 -Fbin test.asm -o test.bin

The result is a 4 byte file test.bin:

Offset(h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000  30 3C 00 07                                      0<..

Generated by HxD

The Code

This ROM is a very simple example that flashes the background color and does nothing else. The main loop looks like this:

__main
    move.w  #0,d0                  ; d0 = 0
    move.w  #$8F00,vdp_control     ; Graphics init code
                                   ; (disable auto-increment)
    move.l  #$C0000003,vdp_control ; Set up write to color 0
loop
    move.w  d0,vdp_data            ; Write color 0 value
    add.w   #1,d0                  ; Modify color

    move.w  #100,d1                ; Busy-wait a little while
.wait                              ; by counting down from 100
    dbra    d1,.wait

    jmp     loop                   ; Goto loop

Note that vdp_control and vdp_data are constant addresses, used for memory-mapped I/O between the MC68000 and the VDP graphics processor. The color cycling is implemented by incrementing the D0 register and writing that value to the COLOR 0 memory of the VDP. This 16-bit value is interpreted as a 9-bit color value, selecting a color from 512 possible colors. The 16-bit value is interpreted like this:

Bit #: FEDCBA9876543210
Color: ....BBB.GGG.RRR.

So that's it - incrementing this 16-bit value will change the color and it will eventually wrap around to 0, starting over with the color black.

Now, to make a functional ROM out of this program, we need the following extra stuff:

The following is the assembly code for a very minimal functional ROM containing the above main loop. The details of the initialization code is explained in a later article.

; Minimal SEGA Genesis ROM by the Nameless Algorithm 2018-07-02
; - http://namelessalgorithm.com/
;
; ROM HEADER
; -------------------------------------------------------------
rom_header:
    dc.l   $00FFFFFE        ; Initial stack pointer value
    dc.l   EntryPoint       ; Start of program
    dc.l   ignore_handler   ; Bus error
    dc.l   ignore_handler   ; Address error
    dc.l   ignore_handler   ; Illegal instruction
    dc.l   ignore_handler   ; Division by zero
    dc.l   ignore_handler   ; CHK exception
    dc.l   ignore_handler   ; TRAPV exception
    dc.l   ignore_handler   ; Privilege violation
    dc.l   ignore_handler   ; TRACE exception
    dc.l   ignore_handler   ; Line-A emulator
    dc.l   ignore_handler   ; Line-F emulator
    dc.l   ignore_handler   ; Unused (reserved)
    dc.l   ignore_handler   ; Unused (reserved)
    dc.l   ignore_handler   ; Unused (reserved)
    dc.l   ignore_handler   ; Unused (reserved)
    dc.l   ignore_handler   ; Unused (reserved)
    dc.l   ignore_handler   ; Unused (reserved)
    dc.l   ignore_handler   ; Unused (reserved)
    dc.l   ignore_handler   ; Unused (reserved)
    dc.l   ignore_handler   ; Unused (reserved)
    dc.l   ignore_handler   ; Unused (reserved)
    dc.l   ignore_handler   ; Unused (reserved)
    dc.l   ignore_handler   ; Unused (reserved)
    dc.l   ignore_handler   ; Spurious exception
    dc.l   ignore_handler   ; IRQ level 1
    dc.l   ignore_handler   ; IRQ level 2
    dc.l   ignore_handler   ; IRQ level 3
    dc.l   ignore_handler   ; IRQ level 4 (horiz. retrace int.)
    dc.l   ignore_handler   ; IRQ level 5
    dc.l   ignore_handler   ; IRQ level 6 (vert. retrace int.)
    dc.l   ignore_handler   ; IRQ level 7
    dc.l   ignore_handler   ; TRAP #00 exception
    dc.l   ignore_handler   ; TRAP #01 exception
    dc.l   ignore_handler   ; TRAP #02 exception
    dc.l   ignore_handler   ; TRAP #03 exception
    dc.l   ignore_handler   ; TRAP #04 exception
    dc.l   ignore_handler   ; TRAP #05 exception
    dc.l   ignore_handler   ; TRAP #06 exception
    dc.l   ignore_handler   ; TRAP #07 exception
    dc.l   ignore_handler   ; TRAP #08 exception
    dc.l   ignore_handler   ; TRAP #09 exception
    dc.l   ignore_handler   ; TRAP #10 exception
    dc.l   ignore_handler   ; TRAP #11 exception
    dc.l   ignore_handler   ; TRAP #12 exception
    dc.l   ignore_handler   ; TRAP #13 exception
    dc.l   ignore_handler   ; TRAP #14 exception
    dc.l   ignore_handler   ; TRAP #15 exception
    dc.l   ignore_handler   ; Unused (reserved)
    dc.l   ignore_handler   ; Unused (reserved)
    dc.l   ignore_handler   ; Unused (reserved)
    dc.l   ignore_handler   ; Unused (reserved)
    dc.l   ignore_handler   ; Unused (reserved)
    dc.l   ignore_handler   ; Unused (reserved)
    dc.l   ignore_handler   ; Unused (reserved)
    dc.l   ignore_handler   ; Unused (reserved)
    dc.l   ignore_handler   ; Unused (reserved)
    dc.l   ignore_handler   ; Unused (reserved)
    dc.l   ignore_handler   ; Unused (reserved)
    dc.l   ignore_handler   ; Unused (reserved)
    dc.l   ignore_handler   ; Unused (reserved)
    dc.l   ignore_handler   ; Unused (reserved)
    dc.l   ignore_handler   ; Unused (reserved)
    dc.l   ignore_handler   ; Unused (reserved)
    
    dc.b "SEGA GENESIS    " ; Console name
    dc.b "(C) NAMELESS    " ; Copyrght holder and release date
    dc.b "VERY MINIMAL GENESIS CODE BY NAMELESS ALGORITHM   "
                                                 ; Domest. name
    dc.b "VERY MINIMAL GENESIS CODE BY NAMELESS ALGORITHM   "
                                                 ; Intern. name
    dc.b "2018-07-02    "   ; 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
    dc.b "JUE             "                     ; Country codes
        


; CONSTANTS
; -------------------------------------------------------------
vdp_control     = $C00004 ; Memory mapped I/O
vdp_data        = $C00000 ;



; INIT
; -------------------------------------------------------------
EntryPoint:               ; Entry point addr. set in ROM header
    move    #$2700,sr     ; disable interrupts


; Skip clear RAM - we don't use it at all in this example

; TMSS
    move.b  $00A10001,d0  ; Move Megadrive hardware ver. to d0
    andi.b  #$0F,d0       ; Version is stored in last four bits
                          ;   so mask it with 0F
    beq     @Skip         ; If version = 0, skip TMSS signature
    move.l  #'SEGA',$00A14000 ; Move string "SEGA" to $A14000
@Skip:

; Z80
    move.w  #$0100,$00A11100 ; Request access to the Z80 bus
    move.w  #$0100,$00A11200 ; Hold the Z80 in a reset state
@Wait:
    btst    #$0,$00A11101    ; Check if we have access to
    bne     @Wait            ;   the Z80 bus yet
    move.l  #$00A00000,a1    ; Copy Z80 RAM address to a1
    move.l  #$00C30000,(a1)  ; Copy data and increment the
                             ;   source/dest addresses
 
    move.w  #$0000,$00A11200 ; Release reset state
    move.w  #$0000,$00A11100 ; Release control of bus
 
; Initialize PSG to silence
    ;move.l  #$9fbfdfff,$00C00011  ; silence

; Initialising the VDP
    move.l  #VDPRegisters,a0 ; Load address of register table
    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         ; Copy register value to d1
    move.w  d1,$00C00004     ; Write command and value to
                             ; VDP control port
    add.w   #$0100,d1        ; Increment register #
    dbra    d0,@CopyVDP

; Ignore I/O ports - we don't use them
    
; Status register
    move    #$2700,sr



; MAIN PROGRAM
; -------------------------------------------------------------
__main
    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  #100,d1
.wait
    dbra    d1,.wait
    jmp     loop



; EXCEPTION AND INTERRUPT HANDLERS
; -------------------------------------------------------------
    align 2 ; word-align code

ignore_handler
    rte ; return from exception (seems to restore PC)



; VDP REGISTER INITIALIZATION
; -------------------------------------------------------------
; Code by Matt Philips
; - https://bigevilcorporation.co.uk/2012/03/09
;                          /sega-megadrive-3-awaking-the-beast/
; - Explanations (albeit short explanations) of the VDP
;   registers can be found in chapter 4 of the SEGA2 doc 

    align 2 ; word-align code

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 - bit 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)
__end:

Running the ROM

Color Cycling

Building the ROM is as simple as assembling a raw binary file:

vasmm68k_mot_win32 -Fbin minimal.asm -o roms\minimal.gen 

Which results in this ROM file:

Offset(h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000  00 FF FF FE 00 00 02 04 00 00 02 9C 00 00 02 9C  .ÿÿþ.......œ...œ
00000010  00 00 02 9C 00 00 02 9C 00 00 02 9C 00 00 02 9C  ...œ...œ...œ...œ
00000020  00 00 02 9C 00 00 02 9C 00 00 02 9C 00 00 02 9C  ...œ...œ...œ...œ
00000030  00 00 02 9C 00 00 02 9C 00 00 02 9C 00 00 02 9C  ...œ...œ...œ...œ
00000040  00 00 02 9C 00 00 02 9C 00 00 02 9C 00 00 02 9C  ...œ...œ...œ...œ
00000050  00 00 02 9C 00 00 02 9C 00 00 02 9C 00 00 02 9C  ...œ...œ...œ...œ
00000060  00 00 02 9C 00 00 02 9C 00 00 02 9C 00 00 02 9C  ...œ...œ...œ...œ
00000070  00 00 02 9C 00 00 02 9C 00 00 02 9C 00 00 02 9C  ...œ...œ...œ...œ
00000080  00 00 02 9C 00 00 02 9C 00 00 02 9C 00 00 02 9C  ...œ...œ...œ...œ
00000090  00 00 02 9C 00 00 02 9C 00 00 02 9C 00 00 02 9C  ...œ...œ...œ...œ
000000A0  00 00 02 9C 00 00 02 9C 00 00 02 9C 00 00 02 9C  ...œ...œ...œ...œ
000000B0  00 00 02 9C 00 00 02 9C 00 00 02 9C 00 00 02 9C  ...œ...œ...œ...œ
000000C0  00 00 02 9C 00 00 02 9C 00 00 02 9C 00 00 02 9C  ...œ...œ...œ...œ
000000D0  00 00 02 9C 00 00 02 9C 00 00 02 9C 00 00 02 9C  ...œ...œ...œ...œ
000000E0  00 00 02 9C 00 00 02 9C 00 00 02 9C 00 00 02 9C  ...œ...œ...œ...œ
000000F0  00 00 02 9C 00 00 02 9C 00 00 02 9C 00 00 02 9C  ...œ...œ...œ...œ
00000100  53 45 47 41 20 47 45 4E 45 53 49 53 20 20 20 20  SEGA GENESIS    
00000110  28 43 29 20 4E 41 4D 45 4C 45 53 53 20 20 20 20  (C) NAMELESS    
00000120  56 45 52 59 20 4D 49 4E 49 4D 41 4C 20 47 45 4E  VERY MINIMAL GEN
00000130  45 53 49 53 20 43 4F 44 45 20 42 59 20 4E 41 4D  ESIS CODE BY NAM
00000140  45 4C 45 53 53 20 41 4C 47 4F 52 49 54 48 4D 20  ELESS ALGORITHM 
00000150  20 20 56 45 52 59 20 4D 49 4E 49 4D 41 4C 20 47    VERY MINIMAL G
00000160  45 4E 45 53 49 53 20 43 4F 44 45 20 42 59 20 4E  ENESIS CODE BY N
00000170  41 4D 45 4C 45 53 53 20 41 4C 47 4F 52 49 54 48  AMELESS ALGORITH
00000180  4D 20 20 20 32 30 31 38 2D 30 37 2D 30 32 20 20  M   2018-07-02  
00000190  20 20 00 00 4A 20 20 20 20 20 20 20 20 20 20 20    ..J           
000001A0  20 20 20 20 00 00 00 00 00 00 02 B8 00 FF 00 00      .......¸.ÿ..
000001B0  00 FF FF FF 00 00 00 00 00 00 00 00 00 00 00 00  .ÿÿÿ............
000001C0  00 00 00 00 00 00 00 00 00 00 00 00 20 20 20 20  ............    
000001D0  20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20                  
000001E0  20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20                  
000001F0  20 20 20 20 4A 55 45 20 20 20 20 20 20 20 20 20      JUE         
00000200  20 20 20 20 46 FC 27 00 10 39 00 A1 00 01 02 00      Fü'..9.¡....
00000210  00 0F 67 0A 23 FC 53 45 47 41 00 A1 40 00 33 FC  ..g.#üSEGA.¡@.3ü
00000220  01 00 00 A1 11 00 33 FC 01 00 00 A1 12 00 08 39  ...¡..3ü...¡...9
00000230  00 00 00 A1 11 01 66 F6 22 7C 00 A0 00 00 22 BC  ...¡..fö"|. .."¼
00000240  00 C3 00 00 33 FC 00 00 00 A1 12 00 33 FC 00 00  .Ã..3ü...¡..3ü..
00000250  00 A1 11 00 41 FA 00 4A 70 18 22 3C 00 00 80 00  .¡..Aú.Jp."<..€.
00000260  12 18 33 C1 00 C0 00 04 D2 7C 01 00 51 C8 FF F2  ..3Á.À..Ò|..QÈÿò
00000270  46 FC 27 00 30 3C 00 00 33 FC 8F 00 00 C0 00 04  Fü'.0<..3ü...À..
00000280  23 FC C0 00 00 03 00 C0 00 04 33 C0 00 C0 00 00  #üÀ....À..3À.À..
00000290  52 40 32 3C 00 64 51 C9 FF FE 60 EE 4E 73 00 00  R@2<.dQÉÿþ`îNs..
000002A0  14 74 30 00 07 78 00 00 00 00 FF 00 81 3F 00 02  .t0..x....ÿ..?..
000002B0  01 00 00 FF FF 00 00 80                          ...ÿÿ..€

Generated by HxD

It can be run using the emulator mednafen:

mednafen minimal.gen 
Running on a real SEGA Genesis

Testing the ROM on a real SEGA Genesis is also possible using the Mega EverDrive X5, a cartridge that enables running any SEGA Genesis or even SEGA Master System ROM on a real SEGA Genesis console.

Debugging

mednafen can also be used for basic debugging. Starting mednafen from the command-line with the debugger enabled and paused on the first instruction can be done like this:

    mednafen -debugger.autostepmode 1 roms\test1.gen 

A few important mednafen debug commands:

R                      Run
S                      Step
Up/Down, Page Up/Down  Navigate code
Space                  Set breakpoint
Alt+1                  CPU debugger view
Alt+3                  Memory view

See Mednafen Debugger Documentation.

If you want to inspect certain problems such as stack pointer corruption, you could instead try using the MAME debugger, which is more full-featured and shows all the registers.

MAME Genesis debug command-line (pauses on first instruction):

mame64 genesis -debug -window -cart roms\test1.gen

A few MAME debugger hotkeys:

F10     Step over
F11     Step into
Ctrl-M  Show memory

And commands:

go [ADDR]  Run until reaching ADDR
help       The most important command of all

Finally, for debugging the VDP graphics processor, it is recommended to use the Regen emulator. It is not stable, but it has a window that displays the VDP registers and video RAM contents, which is very useful for debugging graphics.

References

Tanglewood

For the SEGA Genesis specifics, Matt Phillips's blog bigevilcorporation.co.uk was very helpful. He's working on a new Genesis game, Tanglewood, which looks like a very cool project.

SGI and Sun workstations:

Tools:

Emulators:

Hardware

Tutorials:

General SEGA Genesis info:

Motorola 68000: