This bit was difficult. When the Megadrive is turned on, you get a blank slate. Nothing is initialised for you – the RAM is full of garbage, the controller ports are dead, and the VDP is cold, alone and scared – you have to restore some sanity and set each piece up one by one. What makes it even more difficult, is that you get no visual feedback that it’s been done correctly until you’ve set up enough things to start displaying something on screen – and that takes a LOT of code.
I’ve found various tutorials and code samples showing how to initialise the Megadrive, to the point where we can begin doing some VDP work and get a few pixels showing. Unfortunately they were a little complex for me, I lost some hair trying to get it to work with my chosen assembler, a lot of things were left unexplained, and I’ve had to do some research to fill in the gaps. Now that I know how each step works I’ve since rewritten the code, breaking things down into smaller steps and commenting every line. Here’s each step explained:
1. Checking the Reset Button
The first thing to figure out is if we need to do anything at all. If the player pressed the reset button, then everything will already have been setup and we can just jump straight to the action again. From all the sample code I’ve seen, two separate reset indicators are checked – one is the physical button on the console, but I can’t find any information about the other one. Perhaps it has something to do with the expansion port, so that future addon hardware (the MegaCD or the 32X) can trigger a software reset. Anyway, here’s how to check:
EntryPoint: ; Entry point address set in ROM header tst.w 0x00A10008 ; Test mystery reset (expansion port reset?) bne Main ; Branch if Not Equal (to zero) - to Main tst.w 0x00A1000C ; Test reset button bne Main ; Branch if Not Equal (to zero) - to Main
If the results of the test are non-zero, then a soft reset has occurred and we can branch straight to Main, skipping all of this initialisation.
We test two addresses – they’re not addresses in main memory, but mapped to some specific hardware ports. Addresses starting from 0x00A00000 are not those of main RAM, but are the system I/O areas, which point to various ports or the memory of other coprocessors within the Megadrive. Most of the system I/O addresses can be found in a technical manual straight from Sega themselves, which can be found in the references at the bottom of this post.
2. Clearing the RAM
When the system is powered up, the RAM could be in any old state. Most good emulators clear it when loading a ROM, but this isn’t going to be of much help when I finally get hold of some development hardware and start scratching my head at the garbled mess on screen. We know the Megadrive’s RAM is 64kb in size, and technically we know where its address mappings begin and end since we’ve defined that in the ROM header, but it seems to be common practise to rely on the machine’s ability to wrap around the end of the physical addresses back to the beginning, and clear it from 0x00000000 backwards.
If we put 0x00000000 into an address register, and then use pre-decrement when writing a zero to that address, we’ll wrap around to the end of memory and clear the last byte:
move.l #0x00000000, d0 ; Place a 0 into d0, ready to copy to each longword of RAM move.l #0x00000000, a0 ; Starting from address 0x0, clearing backwards move.l #0x00003FFF, d1 ; Clearing 64k's worth of longwords (minus 1, for the loop to be correct) @Clear: move.l d0, -(a0) ; Decrement the address by 1 longword, before moving the zero from d0 to it dbra d1, @Clear ; Decrement d0, repeat until depleted
I’ve purposely written a whole longword to the d1 register, where just a word-sized MOVE would suffice for the byte count 0x3FFF. This is because I have no idea if the registers will have been cleared or not when the system was powered on. Better safe than crashy.
3. Writing the TMSS
The Trade Mark Security Signature – or TMSS – was a feature put in by Sega to combat unlicensed developers from releasing games for their system, which is a kind of killswitch for the VDP. It’s the pinnacle of security systems, a very sophisticated encryption key which is almost uncrackable. You write the string “SEGA” to 0x00A14000.
This was only implemented in the second hardware version of the Megadrive, so we need to test the system’s version number at mapped I/O address 0x00A10001 before proceeding. This points to a byte of read-only memory, possibly on another chip, which stores the version ID (bits 0-3), CPU clock/region (bit 6 on = 7.60mhz PAL, off = 7.67mhz NTSC), and domestic/overseas model (bit 7). We only need to test the bottom four bits (one nybble):
move.b 0x00A10001, d0 ; Move Megadrive hardware version to d0 andi.b #0x0F, d0 ; The version is stored in last four bits, so mask it with 0F beq @Skip ; If version is equal to 0, skip TMSS signature move.l #'SEGA', 0x00A14000 ; Move the string "SEGA" to 0xA14000 @Skip:
I’m unsure at what point the signature is checked and VDP killswitch activated, whether it’s by time or the first VDP command is sent. Either way, the VDP is now safe. There’s also a new opcode there – ANDI (immediate logic AND), which ANDs two values, storing the result in d0.
4. Initialising the Z80
Next, we can begin initialising each of the Megadrive’s coprocessors, starting with the Zilog Z80. The Z80 is the same 8-bit chip used in the Sega Master System, and in the Megadrive it acts as both a controller for the PSG and FM sound chips, and a backwards compatibility processor for playing Master System games (with an appropriate adapter for the cartridge). The Z80 has its own set of registers, and various command and data ports for sending it instructions and information, as do the other coprocessors. It also has 8kb of RAM to itself. To send it commands, or some data, we can simply MOVE values to mapped I/O addresses.
The Z80 needs a few things doing – first, we need to request access to its bus, so that it can listen to us. We request – or release – control of the bus by writing 0x0100 or 0 to its BUSREQ port, and then wait in a loop until we have control, by reading this same port. We also need to stop it running by holding it in a reset state – again by writing a 1 to one of its ports. Whilst we’re holding it in this state, we can freely write a program to its RAM. Finally, we release control of the bus and let go of the reset state, and it can then be left alone to act on the data.
move.w #0x0100, 0x00A11100 ; Request access to the Z80 bus, by writing 0x0100 into the BUSREQ port move.w #0x0100, 0x00A11200 ; Hold the Z80 in a reset state, by writing 0x0100 into the RESET port @Wait: btst #0x0, 0x00A11100 ; Test bit 0 of A11100 to see if the 68k has access to the Z80 bus yet bne @Wait ; If we don't yet have control, branch back up to Wait
Here’s a new opcode, BTST (bit test). It does the same as TST, but only compares the least significant bits.
Now the 68000 has access to the Z80’s bus, and the chip is held in a reset state, so we can write the program data to its memory. This is mapped from 0xA000000.
move.l #Z80Data, a0 ; Load address of data into a0 move.l #0x00A00000, a1 ; Copy Z80 RAM address to a1 move.l #0x29, d0 ; 42 bytes of init data (minus 1 for counter) @Copy: move.b (a0)+, (a1)+ ; Copy data, and increment the source/dest addresses dbra d0, @Copy move.w #0x0000, 0x00A11200 ; Release reset state move.w #0x0000, 0x00A11100 ; Release control of bus
Now the chip starts running again, and begins executing the program written to its memory. I keep glossing over this ‘program’ since I don’t yet have any clue as to what it does! I’ll get some documentation and dissect it bit by bit once I start doing some audio work.
Z80Data: dc.w 0xaf01, 0xd91f dc.w 0x1127, 0x0021 dc.w 0x2600, 0xf977 dc.w 0xedb0, 0xdde1 dc.w 0xfde1, 0xed47 dc.w 0xed4f, 0xd1e1 dc.w 0xf108, 0xd9c1 dc.w 0xd1e1, 0xf1f9 dc.w 0xf3ed, 0x5636 dc.w 0xe9e9, 0x8104 dc.w 0x8f01
5. Initialising the PSG
This one is the Programmable Sound Generator. It can generate square waves and white noise for procedurally creating sounds. As with the Z80 program, I have no idea what the sample data does yet, I’ll look into it at a later date. Copying data to the PSG is a lot simpler than the Z80, since we can just write the data straight to its RAM through an I/O address without requesting bus access:
move.l #PSGData, a0 ; Load address of PSG data into a0 move.l #0x03, d0 ; 4 bytes of data @Copy: move.b (a0)+, 0x00C00011 ; Copy data to PSG RAM dbra d0, @Copy PSGData: dc.w 0x9fbf, 0xdfff
6. Initialising the VDP
The VDP – or Visual Display Processor – is the most complex of the coprocessors. It’s a dedicated graphics chip for displaying sprites and patterns, and warrants its own chapter, which I’ll write up in the next post – getting something on screen.
The VDP has its own set of registers (24 of them), as well as 64kb of dedicated RAM. Communication with the VDP is via two ports – the control port and the data port, which are I/O addresses mapped to 0x00C00004 and 0x00C00000 respectively. The control port is used for setting registers, and supplying a VDP RAM address ready to send data through the data port. The VDP can only send and receive data in bytes or words, but we can make use of a feature which automatically increments the destination address for us, and it will treat a longword write as two separate word writes. More about this feature in the next post.
Each of the VDP’s registers are used to set its various graphics modes, plane addresses and scrolling settings, amongst other things. We initialise the VDP by setting all of these registers, using a word-size command sent to the control port:
- The top nybble is the command – 0x8XXX means set register value
- The next nybble is the register number – so 0x80XX = set register 0, 0x81XX = set register 1, etc
- The bottom byte is the data – so 0x82FF writes FF into register 2
To make things easier, we just keep one big table of all of the VDP’s register values, and copy the whole lot in one go:
move.l #VDPRegisters, a0 ; Load address of register table into a0 move.l #0x18, d0 ; 24 registers to write move.l #0x00008000, d1 ; 'Set register 0' command (and clear the rest of d1 ready) @Copy: move.b (a0)+, d1 ; Move register value to lower byte of d1 move.w d1, 0x00C00004 ; Write command and value to VDP control port add.w #0x0100, d1 ; Increment register # dbra d0, @Copy
Explanations (albeit short explanations) of the VDP registers can be found in chapter 4 of the SEGA2 doc (I’ve added a link to an HTML version in the references). Below is the minimum of things enabled to get started, but these registers will be revisited quite often as I work with more graphics features.
VDPRegisters: dc.b 0x20 ; 0: Horiz. interrupt on, plus bit 2 (unknown, but docs say it needs to be on) dc.b 0x74 ; 1: Vert. interrupt on, display on, DMA on, V28 mode (28 cells vertically), + bit 2 dc.b 0x30 ; 2: Pattern table for Scroll Plane A at 0xC000 (bits 3-5) dc.b 0x40 ; 3: Pattern table for Window Plane at 0x10000 (bits 1-5) dc.b 0x05 ; 4: Pattern table for Scroll Plane B at 0xA000 (bits 0-2) dc.b 0x70 ; 5: Sprite table at 0xE000 (bits 0-6) dc.b 0x00 ; 6: Unused dc.b 0x00 ; 7: Background colour - bits 0-3 = colour, bits 4-5 = palette dc.b 0x00 ; 8: Unused dc.b 0x00 ; 9: Unused dc.b 0x00 ; 10: Frequency of Horiz. interrupt in Rasters (number of lines travelled by the beam) dc.b 0x08 ; 11: External interrupts on, V/H scrolling on dc.b 0x81 ; 12: Shadows and highlights off, interlace off, H40 mode (40 cells horizontally) dc.b 0x34 ; 13: Horiz. scroll table at 0xD000 (bits 0-5) dc.b 0x00 ; 14: Unused dc.b 0x00 ; 15: Autoincrement off dc.b 0x01 ; 16: Vert. scroll 32, Horiz. scroll 64 dc.b 0x00 ; 17: Window Plane X pos 0 left (pos in bits 0-4, left/right in bit 7) dc.b 0x00 ; 18: Window Plane Y pos 0 up (pos in bits 0-4, up/down in bit 7) dc.b 0x00 ; 19: DMA length lo byte dc.b 0x00 ; 20: DMA length hi byte dc.b 0x00 ; 21: DMA source address lo byte dc.b 0x00 ; 22: DMA source address mid byte dc.b 0x00 ; 23: DMA source address hi byte, memory-to-VRAM mode (bits 6-7)
7. Initialising the Controller Ports
The controller ports are generic 9-pin I/O ports, and are not particularly tailored to any device. They have five mapped I/O address each – CTRL, DATA, TX, RX and S-CTRL:
- CTRL controls the I/O direction and enables/disables interrupts generated by the port
- DATA is used to send/receive data to or from the port (in bytes or words) when the port is in parallel mode
- TX and RX are used to send/receive data in serial mode
- S-CTRL is used to get/set the port’s current status, baud rate and serial/parallel mode.
The SEGA2 doc mentions three controller ports – Controller 1, Controller 2, and EXP. I’m guessing EXP is the 9-pin expansion port on the back of the version 1 Genesis, perhaps intended for basic non-joypad peripherals that didn’t require the full expansion port on the bottom of the unit.
; Set IN I/O direction, interrupts off, on all ports move.b #0x00, 0x000A10009 ; Controller port 1 CTRL move.b #0x00, 0x000A1000B ; Controller port 2 CTRL move.b #0x00, 0x000A1000D ; EXP port CTRL
8. Clearing the Registers and Tidying Up
Now everything should be initialised ready for some real work, but it would be best if the actual game code could start with a clean slate. Some rubbish is still in the registers, so let’s clear it:
move.l #0x00000000, a0 ; Move 0x0 to a0 movem.l (a0), d0-d7/a1-a7 ; Multiple move 0 to all registers
Here’s a very useful opcode – MOVEM (move multiple). It can move data to/from a list of registers or register ranges, for example d0,d3,d5 or a3-a5. A common use for it would be to backup/restore all of the registers to/from the stack, in a single instruction.
Next, the status register. The only thing I currently understand about the status register is that certain opcodes can leave the results of an operation in it, like a return value in C/C++. After some reading, it turns out that it can also store the stack pointer register used for interrupts (so that the JMP to an interrupt routine doesn’t trample over the real stack), enable or disable interrupts, and to enable or disable tracing (calls a routine after every opcode, useful for storing callstacks for an exception handler).
; Init status register (no trace, A7 is Interrupt Stack Pointer, no interrupts, clear condition code bits) move #0x2700, sr
And that’s it! The system is initialised, albeit in a very minimal state, ready to do some work. I’ll come back and amend the init code later if I need more functionality out of the machine. Now to jump to the main game code, which I’ve labelled as __main in a separate ASM file. I’ve also labelled the JMP itself as Main, so that we branch here if the reset button has been pressed and the initialisation is skipped:
Main: jmp __main ; Jump to the game code!