Hello Sega Mega Drive

After a long absence, I’m back with a long overdue post about the Sega Mega Drive. The code itself was completed over 4 years ago, so my memory of it is a bit hazy. I have however been writing new MegaDrive code recently so this is the perfect time to finally write this post.

The Sega Mega Drive (MD) is based on the Motorola 68000 CPU and its graphics chip (VDP) is a more advanced version of the VDP in the Sega Master System. The main CPU have 64K of memory, and the VDP as an extra 64K internal RAM for storing graphics, sprite positions, color palettes and more.

The Code

The code in this section is not the complete code, and it is moved around a bit compared to the original to make it more coherent when explaining. With that said, let’s dive into it.

This first section starts with defining some constants required later in the code. The VDP is the Video Display Processor, and it is access through two different addresses. Address $c00004 is used for writing control commands to the VDP. When you want write data to the VDP memory, you first write a command to the control port to select what address you want to write to. The actual data is then written to the DATA port at $c00000. More on this later.

VDP_BASE	equ	$c00000
VDP_DATA	equ	VDP_BASE
VDP_CTRL	equ	VDP_BASE+4

The entry point of a normal MD cartridge is located at address $200 (512 in decimal terms). The first 512 bytes of the cartridge contains vectors the CPU use for knowing what code to execute at specific times. For example, at address 4 in the ROM the initial address to execute is stored. It this example that would be $200, since that is where we tell the assembler to locate our code with the org $200 command.

So what does the code at $200 do? When the original Mega Drive was released, some game companies release unlicensed game which made Sega a bit mad. Therefore they made a slight modification to the following hardware revision to require the game write the string ‘SEGA’ to a special address when the console is booted, or the VDP will not display anything on screen. This rendered the old unlicensed game unplayable. But all games released after that required the developers to add this little snippet of code (or something similar) at the start of the game.

What we do is get the console version number from address $a10001. If the version is more than 0, we write ‘SEGA’ to the undocumented hardware register $a14000. This will unlock the VDP to work as expected. Finally we jump to the setup_vdp subroutine.

    org $200
start:
    move.b	$a10001,d0		; Version
    andi.b	#$0f,d0			; is low byte zero?
    beq.s	.softreset		; yes, skip unlocking of VDP
    move.l  #'SEGA',$a14000
.softreset
    bsr.s	setup_vdp

When the VDP is initialised we return here to upload the graphics and write the tile map to the VDP. The first instruction sets CPU register a0 to $C00000, the address for the VDP data port, which means writing to 4(a0) will write to $C00004, the VDP control port. Except for choosing what address to write to in the VDP, telling the VDP to change the write address after each write is the most common thing you need to do. When writing contiguous data to the VDP RAM, you need to write a 2 to VDP register 15. Why a 2 you might ask? It’s because we write two bytes at a time to the VPD, so the address should be updated by 2. Writing to a register in the VDP is done by setting the high byte of a word to the register number, the low byte to the value you want to set the register to, and then set the highest bit of the word before you write it to the VDP control port. Easy! That’s where the #$8f02 is coming from.

The rest of the code that follows is a loop to clear the VDP RAM. Since the VDP RAM is $10000 bytes long, and we write 4 bytes per move we loop $4000 times. Writing $40000000 to VDP control port tells the VDP to write to address 0 of its main RAM.

    lea	VDP_BASE,a0
    move.w	#$8f02,4(a0)
    move.w	#$4000-1,d7
    moveq	#0,d0
    move.l	#$40000000,4(a0)
.clear_vram:
    move.l	d0,(a0)
    dbra.w	d7,.clear_vram

Let’s write some color data to the internal color RAM of the VDP. I know I said the VDP has 64K internal RAM, but that is not entirely true. It also has some special memory dedicated to color and scrolling. To tell the VDP we want to write to address 0 of the color RAM, write $C0000000 to the control port. Now we can write the actual color data.

The MD have 4 different palettes of 16 color each. Every tile can use of of the palettes to select what colors to use. For every color there are two bytes to store the RGB values. Three bits are used for each of the RGB elements. The binary representation of the bytes (big endian) would be %0000bbb0ggg0rrr0.

Writing #$00000eee to color RAM address 0 means the first entry is be black, and the second color entry is white.

    move.l	#$c0000000,4(a0)
    move.l	#$00000eee,(a0)

Now that the VDP RAM is empty, and palette is setup we can copy all the graphics data to the VDP. tile_set is the location in the ROM where the tile graphics is located, and we want to write it to address 0 of the VDP. The copy loop just writes data to the VDP RAM until it reaches the end of the tile data.

    lea	tile_set,a1
    move.l	#$40000000,4(a0)
.copy_loop:
    move.w	(a1)+,(a0)
    cmpa.l	#tile_set_end,a1
    blt.s	.copy_loop

When we initialised the VDP (read more about it in the next section) we told the VDP to read the tile map for Scroll A (the MD have to separate planes, called Scroll A and Scroll B, to allow for parallax scrolling) from address $2000 in the VDP RAM. More info on what value to write to the control port to select address can be found here.

The tile map consists of a number of words describing what tile data to use, what palette to use, and other display properties. The lowest 11 bits contains the tile number, which is the only data we are interested in in this code. Our ‘Hello world!’ graphics consists of 6 tiles, which should be displayed in order to make any sense, so we write 1 through 6, combined into 32 bit writes to the address the Scroll A plane is located in the VDP RAM.

When the tile map is written, we are all done so we can stop doing anything useful by halting the CPU until there is an interrupt, and the just halt it again until the end of times. Or the power is cut.

    move.l	#$60000000,d0	; $40000000  + VRAM address to plane A
    move.l	d0,4(a0)	; Select VRAM address write

    move.l	#$00010002,(a0)	; Tiles 1 and 2
    move.l	#$00030004,(a0)	; tiles 3 and 4
    move.l	#$00050006,(a0)	; tiles 5 and 6
stay:
    stop	#$2400
    bra.s	stay

VDP init and data

The VDP have a set of registers that controls how it displays the graphics. This loop reads the configuration from the data defined at memory vdp_regs and writes it to the VDP control port. Simple enough, but the important thing are the values we write to the registers.

setup_vdp:
    lea	VDP_CTRL,a1
    lea	vdp_regs,a0
    move.w	#((vdp_regs_end-vdp_regs)/2)-1,d2
.copy_loop:
    move.w	(a0)+,(a1)
    dbra.w	d2,.copy_loop
    rts

The VDP register data contain all necessary configuration of the VDP. To have anything at all display, register 1 (mode register 2) is the most important, since it contains a bit to enable the display altogether. Registers 2, 4, and 6 selects at what address in VDP ram it should fetch graphics data for rendering, so those are also a bit more important than some others.

For a full description of each register you should, read more at the wiki at megadrive.org.

vdp_regs:
    dc.w	$8004		; mode register 1
    dc.w	$8144		; mode register 2 - Display enable bit set ($40) + VBI ($20)
    dc.w	$8208		; plane a table location - VRAM:$2000
    dc.w	$8318		; window table location -  VRAM:$3000
    dc.w	$8406		; plane b table location - VRAM:$4000
    dc.w	$8500		; sprite table location (reg 5) 2*$200 = $400
    dc.w	$8600		; sprite pattern generator base addr.
    dc.w	$8700		; backgroud colour,  (reg 7)
    dc.w	$8800		; 0
    dc.w	$8900		; 0
    dc.w	$8b00		; Mode register 3
    dc.w	$8c00		; mode register 4
    dc.w	$8d05		; HBL_scroll data location. ($1400)
    dc.w	$8e00		; 0
    dc.w	$8f02		; auto-increment value
    dc.w	$9000		; plane size
    dc.w	$9100		; window plane h-pos
    dc.w	$9200		; window place v-pos
vdp_regs_end:

Finally comes the tile data. The tiles consists of 8 by 8 pixels of 16 color indices. That means each byte holds color data for 2 pixels, and in total 32 bytes is required for every tile.

The data below might look a bit strange since it consists of only ones and zeros. It almost seems like binary data, but it is hexadecimal. As we only use palette entry 0 and 1, it wouldn’t make sense to have any other values in the data.

tile_set:
    ds.l	8	; first tile is empty.

    dc.l    $00000000
    dc.l    $01000100
    dc.l    $01000100
    dc.l    $01000101
    dc.l    $01111101
    dc.l    $01000101
    dc.l    $01000100
    dc.l    $00000000

    dc.l    $00000000
    dc.l    $00000101
    dc.l    $11100101
    dc.l    $00010101
    dc.l    $11110101
    dc.l    $00000101
    dc.l    $11110101
    dc.l    $00000000

    dc.l    $00000000
    dc.l    $00000001
    dc.l    $00110001
    dc.l    $01001001
    dc.l    $01001001
    dc.l    $01001001
    dc.l    $00110000
    dc.l    $00000000

    dc.l    $00000000
    dc.l    $00010000
    dc.l    $00010011
    dc.l    $00010100
    dc.l    $01010100
    dc.l    $01010100
    dc.l    $10100011
    dc.l    $00000000

    dc.l    $00000000
    dc.l    $00000001
    dc.l    $00011001
    dc.l    $10100101
    dc.l    $10100001
    dc.l    $10100001
    dc.l    $00100001
    dc.l    $00000000

    dc.l    $00000000
    dc.l    $00001010
    dc.l    $00111010
    dc.l    $01001010
    dc.l    $01001010
    dc.l    $01001000
    dc.l    $00111010
    dc.l    $00000000
tile_set_end:

That is is. We are done. Finally. Hopefully it will be less than 3 year until next entry.