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 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.