SEGA Genesis: Video Display Processor
The graphics of the SEGA Genesis is handled by the Video Display Processor (VDP) chip. It has it's own 64 KB of dedicated Video RAM (VRAM), containing all graphics data including tiles, sprites, color palettes, and scrolling tables. This memory is separate from the 64 KB of main memory directly accessible from the Motorola 68000 CPU.
The VDP is controlled through memory-mapped I/O, specifically these addresses:
$C00004 VDP Control Port
$C00000 VDP Data Port
All access to the VDP is provided through writing to these addresses. In this post, I will explain how to set the VDP registers and write data to VRAM.
In the following, we'll use these constants instead of the raw addresses:
vdp_control = $C00004
vdp_data = $C00000
Now, this post is going to be pretty dry, but hey, at least there are a few Golden Axe screenshots.
VDP Registers
The VDP has a set of registers which, along with the contents of VRAM, determine its behaviour. The VDP registers are set up by writing a control word to the VDP Control Port ($C00004
).
Let's try an example:
Set the address of Plane A (tilemap) to the VRAM address
$C000
.
This is done by setting VDP register $02
, 'Plane A Name Table Location' to $C000
.
VDP registers are set using this bit pattern:
Bits: [10?R RRRR DDDD DDDD]
- ? is ignored (just set to 0)
- R is VDP register select ($00-$1F). It has a 5-bit
register number, with the bits distributed like this:
[...4 3210 .... ....]
- D is data, an 8-bit number:
[.... .... 7654 3210]
R
is just going to be $02
, but D
is set using this bit pattern:
Bits: [?XAA A???]
- ? is ignored, set to 0
- A is the upper 3 bits of a 16-bit address
- X is used if 128 KB mode is enabled (we'll set it to 0)
Since we only specify the upper 3 bits of the VRAM address, it has to be a multiple of $2000
. So, for the address $C000
, we fill out the bit pattern. First, we set 128 KB mode off:
[.0.. ....]
The address $C000
written in binary is:
1100 0000 0000 0000
The upper 3 bits are then 110
. We put them into the bit pattern:
[.011 0...]
And fill out the ignored bits with 0s:
[0011 0000]
And now we have our 'D' value, we can fill out the full VDP register bit pattern. We start out with these bits, required for setting VDP registers:
[10.. .... .... ....]
Then, we set our register number ($02), which as a 5-bit binary number is 00010:
[10.0 0010 .... ....]
Then, the data, which was 0011 0000
:
[10.0 0010 0011 0000]
And the '?' is set to zero, and we get our final binary number:
[1000 0010 0011 0000]
We can convert it to hexadecimal:
[1000 1011 0011 0000] 8 B 3 0
So, we end up with $8B30
. To set the VDP register $02
to the value address $C000
, we write this word to the VDP Control Port address $C00004
:
move.w #$8B30,vdp_control ; Plane A address = $C000
Doesn't look like much now, does it? Now that we know how to set the VDP registers, I take a look at writing to VRAM, and then we can start going nuts with the VDP!
Accessing VRAM
All access to the VRAM from the MC68000 is done through the VDP. To set up a read or write, we should go through the following steps:
- First, we select our access type: if we're reading or writing.
- Then we select which part of VRAM we'll access: color palettes, vertical scrolling data, or general graphics memory.
- Access type and VRAM part together forms 'Operation Type'.
- Of course, we also need the VRAM address we want to access.
- Operation Type and VRAM address are packed into a 32-bit integer and written to the VDP Control Port.
- Then, we can start accessing VRAM from the VDP Data Port.
Let's try this:
Writing the 16-bit word
$0007
to VRAM address$C000
.
Building on the previous example, this would set the first tile index in the Plane A tile map to index 7. More on this in a later post.
We still use the VDP Control Port, but instead of writing a 16-bit value, we now need a 32-bit command word, which is structured like this:
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 ....]
Those bits are distributed in a pretty crazy way, we'll have to be careful to not make any mistakes.
Let's start with the Operation Type. It takes the following values:
B = 000000: VRAM read (normal VRAM)
B = 000001: VRAM write
B = 001000: CRAM read (color palette RAM)
B = 000011: CRAM write
B = 000100: VSRAM read (vertical scroll RAM)
B = 000101: VSRAM write
We're not writing to color palette RAM or vertical scroll RAM, so we'll pick the normal 'VRAM write': 000001
. We distribute it according to the pattern shown earlier:
[01.. .... .... .... .... .... 0000 ....]
We then take our address $C000
, which in binary is 1100000000000000
, and, distribute it according to the bit pattern:
[0100 0000 0000 0000 .... .... 0000 ..11]
And fill up with zeroes according to the pattern:
[0100 0000 0000 0000 0000 0000 0000 0011]
We're done with the command word. In hexadecimal it's:
[0100 0000 0000 0000 0000 0000 0000 0011]
4 0 0 0 0 0 0 3
We can now write it to the VDP Control Port to set up our VRAM write:
move.l #$40000003,vdp_control ; VRAM write to address $C000
Now that the VRAM write operation is set up, we can write our data to the VDP Data Port:
move.w #$0007,vdp_data ; Write 7 to $C000
Here's a macro that simplifies this process as long as the address is constant:
m_setup_vram_write macro addr ; \1
command = $40000000
addr0 = (((\1)&$3FFF)<<16)
addr1 = (((\1)&$C000)>>14)
move.l #(command|addr0|addr1),vdp_control
endm
It can be called like this:
m_setup_vram_write $C000 ; setup VRAM write to $C000
Auto-increment
To be able to write a loop that copies a sequence of data to VRAM, we can set up the auto-increment register in the VDP. When writing to the VRAM Data Port, the address written to is automatically incremented with this value. Thus, a single VRAM write command written to the VDP Control Port can be followed by several VDP Data Port writes.
Let's make a VDP register set command for setting the auto-increment register $0F
:
Bits: [10?R RRRR DDDD DDDD] - ? is ignored (just set to 0) - R is VDP register select ($00-$1F). It has a 5-bit register number, with the bits distributed like this: [...4 3210 .... ....] - D is data, an 8-bit number: [.... .... 7654 3210]
We start with the standard bits for setting VDP registers:
[10.. .... .... ....]
Auto-increment was VDP register $0F
, 01111
in binary:
[10.0 1111 .... ....]
The data is simply 8 bits signifying the auto-increment value, so if we use auto-increment = 4 as an example:
[10.0 1111 0000 0100]
We get this:
[1000 1111 0000 0100] 8 F 0 4
Written in assembly like this:
move.w #$8F04,vdp_control
So, to use everything in one snippet of code, here is an example of writing 256 32-bit longwords of data to VRAM address $C000
:
move.w #$8F04,vdp_control ; VDP autoincrement = 4
move.l #$40000003,vdp_control ; VRAM write to $C000
lea my_data,a0
move.w #256,d0 ; 256 iterations
.loop
move.l (a0)+,vdp_data ; write *A0 and increment
dbra.w d0,.loop
my_data:
dc.w $FEDE, $ABE0, ... ; 256 words of data
Debugging the VDP
While working on some scrolling code, I encountered a VDP bug that was tricky to track down.
I was trying to set the VDP register $0D
(HScroll) to the value $34
, and had accidentally written:
move.w #$8D43,vdp_control ; set VDP reg. $0D = $43
See the problem?
It should have been this:
move.w #$8D34,vdp_control ; set VDP reg. $0D = $34
Debugging this using mednafen or MAME was difficult, and I ended up installing yet another Genesis emulator, Regen. It has a VDP debug view that was very informative, as it shows the values of all VDP registers. Now I could easily see that register $0D
(shown as #13
) was set to $34
instead of $43
.
In the next post, I'll take a look at the basic functionality of the VDP: characters and playfields.