Sega Megadrive – 3: Awaking the Beast

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, DATATX, 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!

Matt.

Source

References

19 thoughts on “Sega Megadrive – 3: Awaking the Beast

  1. On line 2 when clearing the RAM, shouldn’t there be a # in front of 0x00000000?
    As it is now, aren’t you loading the contents of 0x00000000 into a0, and couldn’t that result
    in some garbage value being loaded into a0?

    Thanks!

    • Yes, well spotted! That will just fill the memory with garbage (unless address 0x0 contains zero, which must have been the case for all my tests). I’ll correct it.

  2. Found some more.
    Line 2 at the VDP init, you are missing 0x before 18, also, what is the point of loading d0 with 24 and then overwriting it with 0x8000 right away? I’m just a little confused on that last part I guess. 😛

    Thanks!

  3. Hey, it is I again, sorry for spamming your blog with comments. 😛
    You are loading register 0 of the VDP wrong I believe, it should be 0x14, which would be 20 in decimal. 😉

    Thanks!

    • The registers get messed around with regularly, and I’ve corrected them several times depending on the requirements of each demo. For this particular article, it doesn’t really matter since we’re not planning on drawing anything, and they get changed in the next post. Of course, if the comment next to the value doesn’t match, it might throw someone off. I’ll take a look when I get home from work 🙂

  4. Please, describe opcode MOVEM in the section “Clearing the Registers”…
    In the second line, we are putting the contents A0 to d0-d7/a1-a7 , rather than the value in memory addressing by A0?
    In A0 we have value #0, but in (A0) contain value of ROM #0 address – initial SP value.

    • Hi,

      Yes, that looks like a bug. The aim is to copy the value #0 from somewhere into every register, and it looks like (a0) contains whatever was leftover from when it was last used. I’ll correct it when I get home.

      Cheers!

  5. This line ‘…you have to restore some sanity and set each piece up one by one.’ just made me understand why you have to jump through all these hoops to get something up and running on the Megadrive. I think of all the posts I’ve read on your site this one has made the biggest difference.

    It makes sense when you think about it, that the Sega team just fire up the 68K for you when you turn on the machine and then leave it up to the developers to decide what other items they need to work with. It’s unlikely but possible I guess that you might make a program that doesn’t need the VDP, perhaps it is used only for number crunching and then outputs its results in some other way, perhaps saving to SRAM on the cart? Like I said, unlikely but possible.

    I’m going to get the initialisation routine ready up to (and including) the ‘Hello World’ example via Easy68K and then I’ll share the final product as well. Hopefully it will come in useful to others.

    • Learning this step was the big hitter for me, too. It helped me understand that computing is just about moving bytes from one location to another. Every operation you do, every piece of hardware you control, you just move bytes to and from specific addresses or registers. It’s so simple and so complicated.

      An example of a program which won’t need the VDP is one to run a MIDI-to-gamepad sound interface. Something like this: https://www.youtube.com/watch?v=zMbOZI829JI

  6. Looks like you forgot the minus one in this line (init VDP):
    move.l #0x18, d0 ; 24 registers to write
    It should be #0x17 as zero counts too. Doesn’t seem like an important issue, just helping make the code cleaner.

    Thank you for the great articles!
    I hope you write more when have free time.

    • Cheers! I’ve checked my most recent code and it seems I cleaned this up a while ago, but I do keep finding a few surprises now and again.

      Finding free time has been difficult recently, but it’s always on my mind.

  7. Can’t download source file, so my comment based on article code.
    I think that this code is more appropriate for clearing registers:
    move.l #0x00FF0000, a0 ; Take first address of RAM. It defenetly contains zero bacause we cleared all RAM earlier
    movem.l (a0), d0-d7/a1-a6 ; Clear all registers except a7 because it contains stack pointer
    move.l #0x00000000, a0 ; Finally set a0 value to 0

    Thank you for your articles. They helps me a lot. Great job!

  8. Hi! Thanks for your tutorial – I’m a beginner and I’m having a bit of trouble.
    The line when we initialize the first piece of Z80 data redirects to an exception for some reason. I’m really not sure why – it’s not an address or bus exception or anything. The interrupt stack pointer is 0x00FFDFFA. Any chance I might be able to get some help?

Leave a comment