So far we’ve managed to create our first ROM, with just one instruction that sets the register d0. The next thing I would like to do is draw something on the screen, but before that we need some more info on how this old console produced those awesome graphics we all know and love.
I’ve found this version of “sega2.doc” that says it has corrected images.
Random stuff I noted while reading the document:
- I have to go back and forth. The text makes no sense the first time you read it.
- There are obviously some mistakes in the figures.
- Made it to page 67. It gets better after the first few pages (more explanations given).
- Maybe they just put reference material near the beginning and then started explaining so that developers could easily find info in the first pages of the book, after being familiarized with the console.
Ok, writing a pixel is out of the question at this point. It’s going to be ultra-complicated. The Mega Drive does not have the concept of a “frame buffer”. I think I found out a way to change the background color though. Moreover this might not require more knowledge of the 68K as I think it’s only going to involve writing very specific stuff to very specific memory locations.
This will depend on whether we can communicate with the VDP while it’s actively drawing. Otherwise we’ll need to find out a way to synchronize this communication with the VBlank, which is going to require us to set up an interrupt, but let’s hope for a quick win and try to just set the background color!
First things first: the Mega Drive has a graphics co-processor, a thing that resembles a GPU. This piece of hardware, called the VDP (Video Display Processor) is closely tied to the way CRT displays produced images: An electron beam was scanning the screen pixel by pixel, much like the way we read text: line by line. When all lines had been updated, the beam moved back to the first line.
We’re interested in the two events when the beam is shut down. The first one, which is shorter is called the HBlank (horizontal) and takes place when a scanline has been updated and the beam goes back to the beginning of the next scanline. The second one is the VBlank (vertical) and happens when the beam has finished scanning the entire screen and moves back up to the beginning of the first line.
The CPU is informed when those events occur with the “horizontal” and “vertical” interrupts. During these periods of time, the CPU stops its normal execution, jumps to a specific address and executes the interrupt handler routine, then resumes its normal execution.
All graphical entities (all layers and sprites) are composed of 8×8 pixel blocks named “cells” in sega2.doc. There are 4 graphics layers with different attributes that are maintained by the VDP: 2 scrolling layers, 1 “window” layer that does not support scrolling and the sprite layer that contains the sprites.
While the VDP is drawing on the screen (during the “active scan”), it produces the signal that the TV set consumes by examining the state of the current pixel. It examines all of the layers at the specific pixel, applies some functions to them (I noticed a “priority” setting for layers) and then outputs the correct pixel to the TV. It also performs some basic collision detection (!) that sets a flag if two sprites have non-transparent pixels that overlap in a given scanline.
The scrolling layers have scrolling offsets and they are larger than the screen so they can be scrolled. I don’t expect them to be too large, so they’ll have to be redrawn from time to time, when they scroll too much, but we’ll worry about that later.
Because the VDP is that busy during the drawing phase (the “active scan”) it will be best to update its settings during the interrupts (mostly during the VBlank). In the docs I read that “the CPU accesses are limited” during active scan (but not prohibited), so we might actually be able to do our background thingy without having to mess with the interrupts just yet.
Talking to the VDP
The way we communicate with the VDP is through its “ports”. Those are mapped to two predefined memory addresses: $C00000 (Data) and $C00004 (control). Writing data to these addresses is going to put the data to a FIFO that the VDP consumes. The FIFO can become full if we write too frequently and there’s a flag that informs us of it being full. That must be related to the CPU accesses being “limited” during the active scan. The VDP might consume the FIFO slower then.
So to write data to VDP memory we first put our command to the Control port and then put our data to the Data port. To read data, we give the command to Control and then read data from the Data port.
The same applies to setting VDP registers as well. The VDP has a number of 8-bit registers, which can be thought of as “render settings”.
Both ports are each mapped to two consecutive locations in memory. Writing to $C00000 will be identical to writing to $C00002 (Data port) and the same goes for $C00004 and $C00006 (Control). This will come in handy for the Control port, because it works in 2 modes:
- The first mode is a 16-bit write, that sets a VDP register.
- The second mode is 2 16-bit writes that set a word in the VDP RAM.
Instead of writing twice to “Control”, we can write one long-word (a move.l) to $C00004 which will be identical to writing first the high word and subsequently the low word, which in turn will be identical to performing the two writes to the same $C00004 address. Take a look at the diagrams at “??4 VDP PORT”, just below the port diagram. The little boxes are bits. Notice that in “WRITE1 : REGISTER SET”, the first (most significant) bits are “10”. Then in “WRITE2 : ADDRESS SET”, we have two writes, none of which starts with the bits “10”. That’s how the VDP distinguishes between the first (register) and second (RAM) port access.
To make things worse, notice that the “Access Mode” code bits are scattered in both writes! The least-significant bits of the access mode code (CD1 and CD0) are placed as the first 2 bits of write 1, and the rest of them (CD5 through CD2) are placed in write 2. This is to ensure that the first bits of write 1 can never be “10”. Indeed, none of the “Access Mode” codes ends in “10” as we can see from the table.
This is how messy it can get when dealing with hardware… I’m loving it!
Checklist for setting background color
So, here is what we need to do, in order to make the background blue:
- Access the palette (CRAM) and make the first colour of the first palette blue
- Access VDP register #7 (background colour) and set it to zero (palette 0, color 0)
- Access VDP register #1 and set the second bit to enable display
At precisely this point, we fast forward 10 months into the future, to July 2016. An awfully demanding work and university year was now over and I found myself project-less. The “megadrive” folder seemed appealing enough and I went through my notes. They quickly sucked me once again in this fun, pointless adventure.
We didn’t learn about the 68K in the microprocessors course this year, but fortunately, we studied Intel’s 8085, which I hear is almost identical to Mega Drive’s sound processor: Z80.
My first order of business was to confirm my speculation about the “sega” waveform being at the bottom of Sonic 1 ROM. So I loaded the ROM in Audacity as 8-bit unsigned PCM. Turns out I was right:
By trial and error I figured out the sample rate must have been around 16 KHz. Of course I took a listen of the whole thing just in case there was something interesting to be heard. More or less noise though.
Setting palette colors
Now, to continue where we left off: painting the background. The first thing we need to do is access palette memory (CRAM). Back to the sega2 document.
Skipping to the (rather cryptic) “CRAM ACCESS” part, I assume that to make the first color of the first palette blue, I need to do the following:
1 – Access VDP Data port
Since we’ll be using both VDP ports quite frequently, it makes sense to allocate a couple of our address registers to point to them. Say a6 and a7:
move.l #$C00000, a6 move.l #$C00004, a7
Edit: a7 was not a good choice as we’ll see later.
2 – Write the command for writing the palette entry, to the CONTROL port of the VDP:
This is achieved by two consecutive word writes to the control port (or one long-word write but let’s play safe for now). The bit pattern for the two writes is as follows:
1100 0000 0AAA AAAA 0000 0000 0000 0000
Where A is the address of the CRAM we want to write to. This is in bytes, and each palette entry is 2 bytes long. We get yet another warning about “interesting side effects” when odd addressing is attempted.
The CRAM is 128 bytes long, hence 7-bit addressing, and each palette entry is a word, so we have in total 64 colors to play with. Currently we are interested in the first color, so our address is 0000000. Converted to hex, here are the two words we need to write to the control port:
And here are the instructions to do it:
move.w #$C000, (a7) move.w #$0000, (a7)
3 – Write our color to the data port
The color is encoded in BGR-333, in one 16-bit word as follows:
I’m going to pick the bluest blue which will be written as:
Replacing the indifferent bits with zeroes:
0000 1110 0000 0000
And converting to hex, we get:
So, to write this value to VDP’s data port we need to:
move.w #$0E00, (a6)
Let’s try this and see what happens first. As I recall, the emulator we’re using can display the VDP RAM, so even if graphics output is not enabled, we might be able to see our palette entry updated. Here is the complete program:
ORG $200 START: ; first instruction of program * Mandatory (?) first instructions nop nop bra $200 * === Program code === move.l #$C00000, a6 ; Make a6 point to VDP's DATA port move.l #$C00004, a7 ; Make a7 point to VDP's CONTROL port move.w #$C000, (a7) ; Give the command to write to palette entry 0 move.w #$0000, (a7) move.w #$0E00, (a6) ; Write BLUE to the data port loop: jmp loop ; infinite loop SIMHALT ; halt simulator * Put variables and constants here END START ; last line of source
I have also added an infinite loop, so that the emulator won’t try to execute whatever random instructions happen to lie after our program.
As I’m doing this, I’m listening to nectarine demoscene radio, and the exact moment when I opened the VDP debugger inside the emulator, to check if the palette entry was updated, a “victory” chiptune came out of my media player. The kind of tune you’d expect to hear when you’ve completed a level in a game. That was a fantastic coincidence as it reflected exactly how I felt when I saw the little blue rectangle in the first palette position:
It’s not much, but it definitely is positive feedback that we’re moving to the right direction. Now if I can fill the screen with this awesome coder’s blue, I’ll be happy for the day.
Setting the background color
Next item in our checklist is: Access VDP register #7 (background color) and set it to zero (palette 0, color 0).
In sega2 document, skip to “REGISTER SET”, where the bit pattern for writing VDP registers is:
100R RRRR DDDD DDDD
The ‘R’ bits form a register index and the ‘D’ bits form a byte of data to write to the register.
Then skip to “BACKGROUND COLOR”, and you’ll find the bit pattern for writing to register #7 (background color):
We’re interested in the first color (palette 0, color 0), so we need to write zero to register #7. That takes care of the ‘D’ bits. The ‘R’ bits must be 00111, which is ‘7’ in binary. So here’s what we need to write to VDP’s control port:
1000 0111 0000 0000
and in hex:
So, let us add the following instruction to our little program:
move.w #$8700, (a7)
And… VDP register #7 goes to zero! I have checked with ‘1’ as well just in case it was zero by default when the emulator started.
Finally we need to “Access VDP register #1 and set the second bit to enable display”. The bit pattern for reg #1 is:
‘D’ is display on/off. I marked with ‘*’ other flags we’re not going to use right now and will set them to zero. So:
0100 0100 = $44
We follow the same procedure for setting r#01, so:
move.w #$8144, (a7)
And… it’s not working 😦 , even though the screen is filled with a dark, blue-ish color.
I was hoping for a quick win here, but the VDP has a ton of settings, and just turning on the display won’t do the trick. I’m suspicious of another setting I encountered while scanning the docs. Something about a shadow/highlight mode.
Hitting the docs again…
Actually, register #01 that we set previously is called “MODE SET REGISTER No. 2”, so it might be a good idea to set “No 1” first. This has to do with H interrupt and a “HV counter”, but apart from those two flags, it has a specific bit pattern that must be used:
Where ‘H’ is the flag to enable H interrupt and ‘C’ is the flag to enable “HV Counter”, whatever that is. Let’s put them to zero for now:
move.w #$8004, (a7)
Which probably marks the first point in time when a person was ecstatic to see a blue screen:
With that, I’m now ready to call it a day. Our little program has to be the shortest program that actually does something on a Mega Drive.
Edit: It’s unbelievable given the amount of hacks this little ROM contains, but it actually runs on a real Mega Drive!
Even though we’ve been lucky up to this point, I think it’s time to go through all the VDP settings and do a complete initialization so that the VDP is in a well-defined state before trying anything else. On the other hand, I’m pretty sure this is going to be ugly, involving endless repeats of similar assembly blocks of code.
So, it might be a better idea to improve our development environment a bit before that. Currently all we can do is compile a single assembly file. It would help if we could somehow include “library” files, so we could put the initialization code there. I don’t know yet, but Easy68K might be missing this feature, plus it has already bored me to death, having to click “assemble” every time. So maybe we’ll search for a command-line assembler that can handle multiple files next time.
Either way, it will probably be boring, but I’m not at all worried about that. Let future self take one for the team! Right now, I’m just going to feast my eyes once again on that beautiful blue screen.
|Previous Chapter||TOC||Next Chapter|