This document discusses how to initialize the Sega MegaDrive (known as Sega Genesis in the US). As I'm myself still a beginner at programming the MegaDrive there are several aspects which I don't yet fully understand or even may have wrong, so if you have any corrections or can explain things I don't know yet please write me (of course you will be given credit).
The source in this document uses the syntax for the GNU assembler. I've also written a document on how to set up the GNU binutils and GNU gcc for use as cross-compiler.
I assume that you have at least some knowledge about assembler, no matter for which processor. The most important and complete document about the Motorola 68000 is of course from Motorola itself, titled Motorola M68000 Family Programmer's Reference Manual. Just google for 68kpm.pdf to find it.
The complete source code for the file that is discussed in this document can be found here (init.S) and here (variables.inc).
Credits
This document is written by Marc Haisenko, the original URL is https://darkdust.net/writings/megadrive/initializing .
The init.S code is a modified version of CDoty's sega.asm/sega.s. I have stripped off unnecessary code and reorganized it a bit to make it more readable. But still much of the code from init.S is in fact from him and I only modified it. (insert CDoty's homepage and e-mail address)
I'd also like to thank Fonzie, Metalix, KanedaFr, Wayne Kerr, phil and Mask of Destiny from The Sega Programming Network's forum for giving me hints and help.
M-374 LX sent me corrections and additional informations about the Z80 initialization.
IntroductionThere are many documents that describe the Sega MegaDrive ROM format, aka the .bin format. They explain the header in detail but leave out one important piece of information: what do the very first 256 byte mean ? Those are addresses in your ROM pointing to interrupt service routines, that is routines that will be called when an interrupt or exception occurs. I'll first show you the source for the first 256 bytes in the ROM and explain it afterwards.
1: .include "variables.inc"
2:
3: | Sega Genesis ROM header
4:
5: .long 0x00FFE000 | Initial stack pointer value
6: .long 0x00000200 | Start of our program in ROM
7: .long Interrupt | Bus error
8: .long Interrupt | Address error
9: .long Interrupt | Illegal instruction
10: .long Interrupt | Division by zero
11: .long Interrupt | CHK exception
12: .long Interrupt | TRAPV exception
13: .long Interrupt | Privilege violation
14: .long Interrupt | TRACE exception
15: .long Interrupt | Line-A emulator
16: .long Interrupt | Line-F emulator
17: .long Interrupt | Unused (reserved)
18: .long Interrupt | Unused (reserved)
19: .long Interrupt | Unused (reserved)
20: .long Interrupt | Unused (reserved)
21: .long Interrupt | Unused (reserved)
22: .long Interrupt | Unused (reserved)
23: .long Interrupt | Unused (reserved)
24: .long Interrupt | Unused (reserved)
25: .long Interrupt | Unused (reserved)
26: .long Interrupt | Unused (reserved)
27: .long Interrupt | Unused (reserved)
28: .long Interrupt | Unused (reserved)
29: .long Interrupt | Spurious exception
30: .long Interrupt | IRQ level 1
31: .long Interrupt | IRQ level 2
32: .long Interrupt | IRQ level 3
33: .long HBlankInterrupt | IRQ level 4 (horizontal retrace interrupt)
34: .long Interrupt | IRQ level 5
35: .long VBlankInterrupt | IRQ level 6 (vertical retrace interrupt)
36: .long Interrupt | IRQ level 7
37: .long Interrupt | TRAP #00 exception
38: .long Interrupt | TRAP #01 exception
39: .long Interrupt | TRAP #02 exception
40: .long Interrupt | TRAP #03 exception
41: .long Interrupt | TRAP #04 exception
42: .long Interrupt | TRAP #05 exception
43: .long Interrupt | TRAP #06 exception
44: .long Interrupt | TRAP #07 exception
45: .long Interrupt | TRAP #08 exception
46: .long Interrupt | TRAP #09 exception
47: .long Interrupt | TRAP #10 exception
48: .long Interrupt | TRAP #11 exception
49: .long Interrupt | TRAP #12 exception
50: .long Interrupt | TRAP #13 exception
51: .long Interrupt | TRAP #14 exception
52: .long Interrupt | TRAP #15 exception
53: .long Interrupt | Unused (reserved)
54: .long Interrupt | Unused (reserved)
55: .long Interrupt | Unused (reserved)
56: .long Interrupt | Unused (reserved)
57: .long Interrupt | Unused (reserved)
58: .long Interrupt | Unused (reserved)
59: .long Interrupt | Unused (reserved)
60: .long Interrupt | Unused (reserved)
61: .long Interrupt | Unused (reserved)
62: .long Interrupt | Unused (reserved)
63: .long Interrupt | Unused (reserved)
64: .long Interrupt | Unused (reserved)
65: .long Interrupt | Unused (reserved)
66: .long Interrupt | Unused (reserved)
67: .long Interrupt | Unused (reserved)
68: .long Interrupt | Unused (reserved)
Line 1 includes a file where we define all address of our variables. Line 3 just containes a comment.
The very first long, which is in line 5, is the value to which the stack pointer is set upon hardware reset (power on). Since we'll overwrite it very soon and won't use it until then you can put in whichever value you like.
The second long (line 6) is the address where our code starts, the entry point. After the MegaDrive has powered up it will jump to this address and then we are in control. In most cases the entry point (that is, the start of our initializing routine) is just after the ROM header, and because it is 512 bytes in size and we start counting at 0 this makes the first address after the ROM header 0x00000200.
All following longs (lines 7 - 68) are addresses for service routines. They get called when the corresponding exception or interrupt request happens. We don't handle any of these except for IRQ level 4 and 6, so we've got the name of a routine called Interrupt in each of them which I'll explain in the next paragraph. The linker will replace the Interrupt's with the address of that symbol (label) which is defined in line 208.
The only two interrupts that are really of interrest to us are the IRQ level 4 and 6 which get triggered by the Video Display Processor on horizontal and vertical retrace. A retrace is when the electron beam of your TV set has reached the right side, is switched off and moves to the left side of the next line (horizontal retrace) or when the electron beam has reached the lower right corner, is switched off and moves to the upper left again (vertical retrace). These are very important events as especially the vertical retrace gives us time to redraw our scene (move sprites etc.) without any flickering.
So we reference the routines HBlankInterrupt (line 211) and VBlankInterrupt (line 215) which get called when the corresponding interrupts happen. Note that handling of the IRQ's is by default switched off, you have to enable them by setting the bits 8 - 10 in the SR (service register).
ROM headerNext is the ROM header which is used to give the MegaDrive some meta informations. It looks like this:
69:
70: | Sega string and copyright
71: .ascii "SEGA MEGA DRIVE (C)MARC 2004.SEP"
72: | Domestic name
73: .ascii "MARCS TEST CODE "
74: | Overseas name
75: .ascii "MARCS TEST CODE "
76: | GM (game), product code and serial
77: .ascii "GM 12345678-01"
78: | Checksum will be here
79: .byte 0x81, 0xB4
80: | Which devices are supported ?
81: .ascii "JD "
82: | ROM start address
83: .byte 0x00, 0x00, 0x00, 0x00
84: | ROM end address will be here
85: .byte 0x00, 0x02, 0x00, 0x00
86: | Some magic values, I don't know what these mean
87: .byte 0x00, 0xFF, 0x00, 0x00
88: .byte 0x00, 0xFF, 0xFF, 0xFF
89: | We don't have a modem, so we fill this with spaces
90: .ascii " "
91: | Unused
92: .ascii " "
93: .ascii " "
94: | Country
95: .ascii "JUE "
There are several good documents explaining this header in details, for example in the Genesis Technical Overview from Sega, better known as sega2.doc, or genesis-rom.txt (just google for them). I'll just explain these in short.
- Line 70: The first thing must be the string "SEGA MEGA DRIVE " or "SEGA GENESIS " (padded to 16 bytes). Then follows "(C)" and four bytes that identify the producer. A date is next, in the form "YYYY.MMM" (where MMM is a three letter abbreviation for the month name in uppercase).
- Line 73: The "domestic" name, this was intended to hold the japanese name of the game. 48 bytes long.
- Line 75: The "overseas" name, this was intended to hold the name used in other countries (i.e. USA). 48 bytes long.
- Line 77: First either "GM" for game or "AI" for educational, followed by a space. The a serial in the form "XXXXXXXX-XX".
- Line 79: Contains a checksum for the ROM. It's not necessary for a ROM to have the correct checksum in here, the MegaDrive does not seem to require it to be correct, so insert whatever you like (2 bytes). The formula for calculating the checksum is very simple and described in the genesis-rom.txt if you like to insert the correct one.
- Line 81: Specifies which devices this ROM supports. I won't explain it here.
- Line 83: Start address of the ROM, normally 0x000000. Four bytes (one long).
- Line 85: End address of the ROM. This is calculated simply by creating the ROM, decrementing the size of the resulting ROM file by one and converting this number into hexadecimal. Four bytes (one long). E.g. if your ROM file is 4341 bytes in size you decrement that by one (4340) and convert it into hexadecimal, resulting in 0x0010F4 as end address.
- Lines 87 and 88 are fixed. They are related to the RAM in the MegaDrive and additional RAM on the module, AFAIK.
- Line 90 would contain some data if the ROM would support a modem. Just blanks otherwise.
- Lines 92 and 93 are called Memo and you can insert here whatever you like.
- Line 95: Contains the country codes for which this ROM is released. Three bytes, just insert "JUE" for Japan, USA and Europe which will make the ROM run on every MegaDrive/Genesis. The rest is padded with spaces to fill up the remaining bytes to make the header 256 bytes long.
At this point I must first explain a little feature of the GNU assembler that I make use of a lot: the GNU assembler allows the use of "temporary" labels which get numbers instead of names. Some people might argue that names are more informative than numbers, and it is a valid argument. However I prefer the temporary labels nevertheless because they make it clear that this is not the beginning of a code block called from elsewhere but only code that will be jumped to from within a short range. In branches you then refer to the temporary label number followed by a direction, "f" for forward or "b" for backward, telling the assembler whether to look backwards for next temporary label with the given number or forwards.
So lets start with the real code, which is starting at ROM offset 0x200 (512) and begins the initialization:
96:
97:
98: |-- Our code starts here (ROM offset: 0x00000200, see line 4)
99:
100: tst.l 0x00A10008 | Test on an undocumented (?) IO register ?
101: bne 1f | Branch to the next temp. label 1 if not zero
102: tst.w 0x00A1000C | Test port C control register
103:
104: 1: bne SkipSetup | Branch to SkipSetup if not equal
This code checks whether the MegaDrive had a soft reset (by pressing the reset button on the MegaDrive) or whether it is powering up (power switch). I don't think it's a good idea to skip the setup as we simply don't know in which state the MegaDrive has been reset, but the original code from CDoty did this and so do we ;-)
105:
106: |||| Initialize some registers values
107:
108: lea Table, %a5 | Load address of Table into A5 | A5 = (address of Table)
109: movem.w (%a5)+, %d5-%d7 | The content located at the address stored in | D5 = 0x8000 A5 += 2
110: | A5 is moved into D5. Then A5 gets incremented | D6 = 0x3FFF A5 += 2
111: | by two (because we've read a word which is two| D7 = 0x0100 A5 += 2
112: | bytes long) and the content of the new loca-
113: | tion is moved into D6, and again for D7
114: movem.l (%a5)+, %a0-%a4 | The next four longwords (four bytes) are read | A0 = 0x00A00000 A5 += 4
115: | into A0 - A4, incrementing A5 after each | A1 = 0x00A11100 A5 += 4
116: | operation by four | A2 = 0x00A11200 A5 += 4
117: | A3 = 0x00C00000 A5 += 4
118: | A4 = 0x00C00004 A5 += 4
These three instructions demonstrate a very cool feature of the M68000: it can read values from RAM and fill them into several registers with just one instruction ! Line 108 first gets the address of a data table which we'll see later and which is used quite a lot in CDoty's original code. I've reduced the use of this table to make it more readable.
After we've read the address of the table into register A5 we read three words from the table and store them in the registers D5, D6 and D7. The value in D5, 0x8000, will be used when setting up the VDP. The value in D6, 0x3FFF will be used when clearing the RAM. D7's value, 0x0100, will again be used when setting up the VDP.
Line 114 reads five long words and stores them in the registers A0, A1, A2, A3 and A4. 0x00A00000 in A0 is the start of the IO region and also contains the version number of the MegaDrive. The addresses in A1 and A2, 0x00A11100 and 0x00A11200, are the Z80 BUSREQ and Z80 RESET and are needed when programming the Z80 processor which handles the sound. The address in A3, 0x00C00000, is the VDP data port and the address in A4, 0x00C00004, is the VDP control port.
Next comes a weird thing: except for the very first MegaDrive version, all MegaDrives require that the string "SEGA" gets written at 0x00A14000. I think Sega required this to make it harder for non-licensed manufacturers to write games for the MegaDrive. After all, console makers don't make money by selling consoles, they make money by selling licenses to game makers.
119: |||| Check version number
120:
121: | Version from the SEGA Technical Manual:
122:
123: move.b 0xA10001, %d0 | Read MegaDrive hardware version | D0 =(0x00A10001)
124: andi.b #0x0F, %d0 | The version is stored in last four bytes | D0 = 0x0000xxxx
125: beq 1f | If they are all zero we've got one the very
126: | first MegaDrives which didn't feature the
127: | protection
128: move.l #0x53454741, 0xA14000 | Move the string "SEGA" at 0xA14000
First we're reading the MegaDrive version from the address 0x00A10001 in line 123. What line 124 really does is set the ZERO flag in the M68000's status register if D0's last byte is zero. So if the version number is zero (the very first MegaDrive version) we skip the protection and forward to the temporary label 1 (at line 130). If the version number is not zero we write the string "SEGA" to address 0x00A14000.
Clearing the RAMAs we'd like the MegaDrive to be a well-defined state when we start our main method later on we're first setting up the stack pointer and then clearing the RAM:
129:
130: 1: clr.l %d0 | Move 0 into D0 | D0 = 0x00000000
131: movea.l %d0, %a6 | Move address from D0 into A6 (that is, clear | A6 = 0x00000000
132: | A6)
133: move %a6, %sp | Setup Stack Pointer (A7) | A7 = 0x00000000
134:
135: 2: move.l %d0, -(%a6) | Decrement A6 by four and and write 0x00000000 | D0 -> (A6) A6 -= 4
136: | into (A6)
137: dbra %d6, 2b | If D6 is not zero then decrement D6 and jump
138: | back to 1 (clear user RAM: 0xFFE00000 onward)
139: | D6 = 0x00000000
140: | A6 = 0xFFE00000 ?
Lines 130 to 133 simply set the stack pointer (A7) and A6 to 0. The working RAM address range of the MegaDrive is from 0xFFE00000 to 0xFFFFFFFF. The M68000 has no push or pop instruction like the x86 family, it has something way cooler: predecrement and postincrement. These increase the address value in the register from which was just read after reading from the memory pointed to by the register or first decrease the address value and the read the memory. The address gets increased/decreased by the number of bytes accessed, so if you request byte access the address gets increased/decreased by one, if you request word access it gets increased/decreased by two and if you request long word access by four.
Let's say we "push" a long word onto the stack: the first time we do this the address in A7 (0x00000000) gets decreased by 4, ending up as being 0xFFFFFFFC. Then four bytes are written at 0xFFFFFFFC, 0xFFFFFFFD, 0xFFFFFFFE and 0xFFFFFFFF.
The really cool thing about this feature is that every address register can be used as stack pointer. Lines 135 and 137 use this to clear the RAM: the value in D0, which happens to be just 0, gets "pushed" onto the stack managed by A6 in Line 135. Line 137 then first checks whether the value in D6 is zero and if not it decreases D6 by one and branches to line 135 again. The value of D6 has been set in line 109 and is 0x00003FFF. As we're always having one additional step through the loop we're looping 0x3FFF + 1 = 0x4000 = 16384 times. In each step we're writing 4 bytes. 4 bytes times 16384 loop iterations gives us 65536 cleared bytes or simply put: we clear all 64kB of work RAM.
Initializing the co-processors and cleaning upApart from the M68000 there are three additional processors in the MegaDrive: a Z80 which is responsible for the sound, the Programmable Sound Generator and the Video Display Processor.
Because we've already set up the stack pointer we can now use subroutines to increase readability and discuss the subroutines later on:
141:
142: jsr InitZ80 | Initialize the Z80 / sound
143: jsr InitPSG | Initialize the PSG
144: jsr InitVDP | Initialize the VDP
The last piece of hardware we need to initialize are the joypads plus the external data port. We need to write 0x40 into the control registers of the joypads and the external data port because otherwise we'll not get the correct button states on the real hardware (emulators don't seem to care).
145:
146: move.b #0x40, %d0 | Set last byte of D0 to 0x40
147: move.b %d0, 0x000A10009| We have to write 0x40 into the control ...
148: move.b %d0, 0x000A1000B| ... register of the joystick (data) ports ...
149: move.b %d0, 0x000A1000D| ... or else we might have problems reading ...
150: | ... the joypads on the real hardware
To finish initializing we have clean up and bring the MegaDrive in a well-defined state:
151:
152:
153: movem.l (%a6), %d0-%d7/%a0-%a6 | Clear all registers except A7 | D0 = 0x00000000
154: | The registers get cleared because we read from| D1 = 0x00000000
155: | the area which we've set to all-zero in the | D2 = 0x00000000
156: | "Initialize memory" section | D3 = 0x00000000
157: | D4 = 0x00000000
158: | D5 = 0x00000000
159: | D6 = 0x00000000
160: | D7 = 0x00000000
161: | A0 = 0x00000000
162: | A1 = 0x00000000
163: | A2 = 0x00000000
164: | A3 = 0x00000000
165: | A4 = 0x00000000
166: | A5 = 0x00000000
167: | A6 = 0x00000000
168:
169: move #0x2700, %sr | Move 0x2700 into Status Register, which now
170: | has these set: no trace, A7 is Interupt Stack
171: | Pointer, no interrupts, clear condition code bits
Line 153 is quite clever: from our RAM initializing routine we still have A6 pointing at the start of the work RAM. And since the whole work RAM is now filled with zero's we can read from it and fill the zeros into all registers, thus clearing all registers with just one instruction.
Then we set the status register in line 169. Note that the 7 (binary 0111) masks the three interrupts that the VDP triggers and they will therefor not get fired. So to enable all three interrupts you have to write 0x2000 into the SR.
This leaves only one thing to do: jump to the main routine.
172:
173:
174: SkipSetup:
175: jmp __main | Initializing done, jump to main method
The __main routine is in the next howto and will show how to get something on screen. But the init.S is not yet complete, we still have some data and subroutines to explain:
The data tableCDoty used a big data table to store everything: initial register values, VDP register values, Z80 sound program and lots of other stuff. It was quite hard to read and interpret, so I've stripped all unnecessary values, moved the VDP register value into a separate table and reformated what's now left:
176:
177:
178:
179: |==============================================================================
180: Table:
181: .word 0x8000 | D5 (needed for initializing the VDP)
182: .word 0x3FFF | D6 (needed for initializing the RAM)
183: .word 0x0100 | D7 (needed for initializing the VDP)
184: .long 0x00A00000 | A0 (version port)
185: .long 0x00A11100 | A1 (Z80 BUSREQ)
186: .long 0x00A11200 | A2 (Z80 RESET)
187: .long 0x00C00000 | A3 (VDP data port)
188: .long 0x00C00004 | A4 (VDP control port)
189:
190: .word 0xaf01, 0xd91f | The following stuff is for the Z80
191: .word 0x1127, 0x0021
192: .word 0x2600, 0xf977
193: .word 0xedb0, 0xdde1
194: .word 0xfde1, 0xed47
195: .word 0xed4f, 0xd1e1
196: .word 0xf108, 0xd9c1
197: .word 0xd1e1, 0xf1f9
198: .word 0xf3ed, 0x5636
199: .word 0xe9e9, 0x8104
200: .word 0x8f01
201:
202: .word 0x9fbf, 0xdfff | PSG values: set channels 0, 1, 2 and 3
203: | to silence.
The data in lines 181 to 188 is used in the Start of init code section for initializing the registers. Lines 190 to 200 will be used by the InitZ80 routine and line 202 by the InitPSG routine.
Interrupt routinesThe interrupt routines get called by the M68000 when an interrupt occurs. We have three routines: a generic routine, one handling the interrupt that the VDP triggers on horizontal retrace and one for the vertical retrace.
Just as a reminder: we've told the MegaDrive the addresses of these routines with the very first 256 bytes of the ROM, as noted in the Interrupt service routines addresses section.
204:
205:
206: |==============================================================================
207: | Interrupt routines
208: |==============================================================================
209: Interrupt:
210: rte
211:
212: HBlankInterrupt:
213: add.l #1, (VarHsync)
214: rte
215:
216: VBlankInterrupt:
217: add.l #1, (VarVsync)
218: rte
The Interrupt routine does nothing but return from the interrupt (also called exception in M68000 terminology). We just hope nothing bad happens. This is of course not a good thing but I just don't know what else to do here. One could of course add a routine that prints the register values on screen and then loops forever (like a Blue Screen Of Death).
Both the HBlankInterrupt and the VBlankInterrupt routines just increment a variable (whose memory address is defined in the variables.inc file which got included in line 1). Thus we don't have to worry about register values getting changed/having to restore them. In our main routine we can then simply read the value of one of these variables and loop until they change and know that the corresponding interrupt just happened. We'll then only be about two instructions late which is okay, normally.
InitZ80InitZ80 initializes the Z80 CPU and gives it a program to execute. I didn't understand what's exactly happening here, but "M-374 LX" shed some light one it. I've updated the comments accordingly.
219:
220:
221: |==============================================================================
222: | Initialize the Z80 / load sound program
223: |==============================================================================
224: InitZ80:
225: move.w %d7, (%a1) | Write 0x0100 into Z80 BUSREQ. This stops the | D7 -> (0x00A11100)
226: | Z80 so that the 68000 can access its bus.
227: move.w %d7, (%a2) | Write 0x0100 into Z80 RESET to reset the Z80. | D7 -> (0x00A11200)
228:
229: 1:
230: btst %d0, (%a1) | Check value of the bit 0 at Z80 BUSREQ
231: bne 1b | Jump back to 1:, waiting for the Z80 until
232: | it's ready.
233:
234: moveq #0x25, %d2 | Write 0x25 into D2 | D2 = 0x00000025
235:
236: 2:
237: move.b (%a5)+, (%a0)+ | Move byte at (A5) into (A0) and increment both| A0 += 1 A5 += 1 * 38 ?
238: dbra %d2, 2b | Decrement D2 and loop to 2: until D2 == -1. | D2 -= 1
239:
240: | D2 = 0xFFFFFFFF
241: | A0 = 0x00A00026
242:
243: move.w %d0, (%a2) | Write 0x0000 into Z80 RESET, clear the reset. | D0 -> (0x00A11200)
244: move.w %d0, (%a1) | Write 0x0000 into Z80 BUSREQ, give bus access | D0 -> (0x00A11100)
245: | back to the Z80.
246: move.w %d7, (%a2) | Write 0x0100 into Z80 RESET, resetting it. | D7 -> (0x00A11200)
247: rts | Jump back to caller
InitPSG
Thanks to "M-374 LX" I can finally explain the PSG initialization: the values 0x9F, 0xBF, 0xDF and 0xFF are written to the PSG (it's mapped at address 0x00C00011 and also a few other locations). These values set the sound channels 0-3 to silence.
248:
249: |==============================================================================
250: | Initialize the Programmable Sound Generator
251: | Sets all four channels to silence.
252: | See http://www.smspower.org/Development/SN76489 for PSG details.
253: |==============================================================================
254: InitPSG:
255: moveq #0x03, %d1 | Move 0x03 into D1 to loop 4 times | D5 = 0x00000003
256:
257: 1: move.b (%a5)+, 0x0011(%a3) | Write content at (A5) into 0x00C00011,| A5 += 1 * 4
258: | which is the PSG (Programmable Sound Generator)
259: | Writes 9F, BF, DF and FF which sets the the
260: | channels 0, 1, 2 and 3 to silence.
261: dbra %d1, 1b | If D1 is not 0 then decrement D5 and jump | D5 -= 1
262: | back to 1
263:
264: | D5 = 0x00000000
265: move.w %d0, (%a2) | Write 0x0000 into Z80 RESET ?
266: rts | Jump back to caller
InitVDP
This routine is very important and initializes the most important piece of hardware in the MegaDrive, next to the M68000: the Video Display Processor. If you set a wrong value you propably won't see anything at all on screen. Because the VDP is quite complex I can't explain it in detail here, for details you should read the Sega Technical Overview (aka sega2.doc) or some other document discussing the VDP. I try to explain the most important things here, but won't explain the sometimes very odd bit patterns used. You have to look those up elsewhere.
The VDP is controlled by two ports, the data port at address 0x00C00000 and the control port at address 0x00C00004. The control port is used for two different things: set a VDP register or set the address to which the data port is pointing.
To set a VDP register you have to write a word into the control port. The upper byte of this word is 0x80 + register number and the lower byte is the value to which the register should be set.
The VDP has its own memory region, which is numbered 0x0000 to 0xFFFF and thus 64kB big. It will hold the pattern informations and the plane regions which describe which patterns to display in the planes and which color palette to use for each pattern.
267:
268: |==============================================================================
269: | Initialize VDP registers
270: |==============================================================================
271: InitVDP:
272: moveq #18, %d0 | 24 registers, but we set only 19
273: lea VDPRegs, %a0 | start address of register values
274:
275: 1: move.b (%a0)+, %d5 | load lower byte (register value)
276: move.w %d5, (%a4) | write register
277: add.w %d7, %d5 | next register
278: dbra %d0, 1b | loop
279:
280: rts | Jump back to caller
We set the number of registers we'd like to change in line 272. The VDP has 24 registers but we're only setting 19 of them as setting the last 5 registers to zero causes the MegaDrive to freeze (although most emulators have no problem).
We then load the address of the VDP register table into A0 and begin to write the register values. In line 275 we read one byte from the table, write it in the lowest byte of D5 (which was set to 0x8000 in line 109) and increase A0 by one. So if the first byte in the table is 0x74 then D5 now reads 0x8074 and A0 points to the next byte. Line 276 then writes the word into the VDP control port and thus sets the register. We then add D7 (which was set to 0x0100 in line 109) to D5, advancing to the next register.
In line 278 D0 gets checked whether it is -1, if it's not it gets decreased by one and jumps back to the temporary label 1. If D0 was zero then line 280 gets executed and we jump back to the caller (we then end up in line 146).
VDP registersFirst I'll show you the register values we're using and explain them afterwards:
281:
282: |==============================================================================
283: | Register values for the VDP
284: |==============================================================================
285: VDPRegs:.byte 0x04 | Reg. 0: Enable Hint, HV counter stop
286: .byte 0x74 | Reg. 1: Enable display, enable Vint, enable DMA, V28 mode (PAL & NTSC)
287: .byte 0x30 | Reg. 2: Plane A is at 0xC000
288: .byte 0x40 | Reg. 3: Window is at 0x10000 (disable)
289: .byte 0x07 | Reg. 4: Plane B is at 0xE000
290: .byte 0x6A | Reg. 5: Sprite attribute table is at 0xD400
291: .byte 0x00 | Reg. 6: always zero
292: .byte 0x00 | Reg. 7: Background color: palette 0, color 0
293: .byte 0x00 | Reg. 8: always zero
294: .byte 0x00 | Reg. 9: always zero
295: .byte 0x00 | Reg. 10: Hint timing
296: .byte 0x08 | Reg. 11: Enable Eint, full scroll
297: .byte 0x81 | Reg. 12: Disable Shadow/Highlight, no interlace, 40 cell mode
298: .byte 0x34 | Reg. 13: Hscroll is at 0xD000
299: .byte 0x00 | Reg. 14: always zero
300: .byte 0x00 | Reg. 15: no autoincrement
301: .byte 0x01 | Reg. 16: Scroll 32V and 64H
302: .byte 0x00 | Reg. 17: Set window X position/size to 0
303: .byte 0x00 | Reg. 18: Set window Y position/size to 0
304: .byte 0x00 | Reg. 19: DMA counter low
305: .byte 0x00 | Reg. 20: DMA counter high
306: .byte 0x00 | Reg. 21: DMA source address low
307: .byte 0x00 | Reg. 22: DMA source address mid
308: .byte 0x00 | Reg. 23: DMA source address high, DMA mode ?
- Register 0: Enables the horizontal retrace interrupt but stops the HV counter (I've no idea what it's good for).
- Register 1: Enables the display (so we can see something on screen :-), enables the vertical retrace interrupt, enables direct memory access (a faster method of accessing the VDP memory, but I don't yet know how to make use of it) and finally sets the V28 mode. This means that the screen will be 28 cells high, with each cell being 8 pixels high this gives a vertical resolution of 224 pixels. This mode can be used both for PAL and for NTSC. PAL also supports a mode with 30 cells which would give a vertical resolution of 240 pixels but we want the ROM to be able to execute on every MegaDrive.
- Register 2: This sets the address of plane A to 0xC000.
- Register 3: Sets the address of the window plane to 0x10000 which is outside the VDP memory area, thus disabling it.
- Register 4: Sets the address of plane B to 0xE000.
- Register 5: Sprite attrible table is at 0xD400. I don't yet know about using sprites so I can't explain this one right now.
- Register 6: This is always zero.
- Register 7: Specifies which color from which palette to use as background color. We're using color #0 from palette #0.
- Register 8: This is always zero.
- Register 9: This is always zero.
- Register 10: As far as I've understood this specifies how often the horizontal retrace interrupt fires. If it is set to 0 then the horizontal retrace interrupt gets fired every single line, if set to 1 the interrupt fires every two lines etc. Could be useful sometimes to set this to 7, thus having the horizontal retrace to fire every 8 lines or every cell line.
- Register 11: Enables the external interrupt (I don't know what this is used for). Also sets both the vertical and horizontal scroll modes to full scroll.
- Register 12: Disables the shadow/highlight mode, no interlacing and use a screen width of 40 cells. With a pattern width of 8 pixels this gives a horizontal resolution of 320 pixels.
- Register 13: Set the horizontal scroll information table address to 0xD000.
- Register 14: This is always zero.
- Register 15: No autoincrement. This register is propably the one which gets set the most as it's very useful (even necessary). As noted above, the data port points to an address in the VDP RAM (or color RAM or vertical scroll RAM). After every access, no matter whether it was read or write access, the address the data port points to is advanced by the number in this register. We'll make use of this feature a lot !
- Register 16: Set the size of the scroll planes to 32 cells vertical and 64 cells horizontal.
- Register 17: This is related to the X position and width of the window plane.
- Register 18: This is related to the Y position and height of the window plane. By setting both register 17 and 18 to zero we disable the window plane.
Now that we've initialized the MegaDrive we have it in a well-defined state and go one step further and actually get something on screen. This is described in The first VDP steps.