My favourite videogames console of all time – the Sega Megadrive. I’ve been pretty excited about getting started on this machine for many years now, and has been the catalyst which finally kicked me into learning some assembly language.
Now, I’ve jumped the gun a bit, since I was originally planning to work on these platforms in chronological order, which means the Nintendo Entertainment System is going to wait in the queue for a while (don’t be fooled, the Sega Master System II was released AFTER the Megadrive, because Sega are nuts like that). I also don’t yet have any development hardware (I’m currently in negotiations with some sellers, though), so I’ll be starting out with a PC emulator with debugging features. The Sonic disassembly packages over at Sonic Retro contain a modified (fixed for Windows 7) version of SN Systems’ 68000 assembler, which was a low cost alternative to Sega’s tools at the time, and used by many Megadrive developers.
The point is, I’m just too excited to leave this console alone, and if anything will kickstart my motivation for this project with a flying leap it will be the Megadrive.
The Sega Megadrive technical specifications
A quick and naive list of the console’s basics, but it’s all I need to know to get started:
- CPU: Motorola 68000 at 7.61 mhz
- Slave CPU: Zilog z80
- Main memory: 64kb
- Video: Yamaha YM7101 VDP (Video Display Processor)
- Video memory: 64kb
- Audio: Yamaha YM2612 FM chip, Texas Instruments SN76489 chip
- Game media: Cartridge
- Programming language: 68k Assembler language
- Known development hardware: Official Sega Genesis dev unit, Cross Products MegaCD unit
As mentioned, I’ve managed to get hold of the SN Systems ASM68K assembler, a command line tool for MS-DOS. Since there was no official IDE or text editor included, nor can I find any clues as to what was commonly used at the time, I’ll be using Microsoft Visual Studio, simply because I’m familiar with its keyboard shortcuts.
Until I can get hold of some hardware, I’ll be making use of a PC emulator which has some debugging features. After some searching around, it seems the MAME emulator MESS does a good job, Gens with the KMod plugin is capable of debugging, and I’ve also had Regen recommended to me on the ASSEMblergames.com forums. I’m inclined to start with MESS since it uses the same debugging shortcut keys as Visual Studio.
Testing the Assembler
Since documentation seems scarce, I’ve used the Sonic the Hedgehog disassemblies from Sonic Retro as a guide. The package contains a batch file used to build the Sonic source, and the assembler bit simply boils down to:
Let’s test something out:
Loop: move.l #0xF, d0 ; Move 15 into register d0 move.l d0, d1 ; Move contents of register d0 into d1 jmp Loop ; Jump back up to 'Loop'
…and that assembles just fine:
SN 68k version 2.53 Assembly completed. 0 error(s) from 4 lines in 0.1 seconds
I won’t pretend that I just came up with that assembly snippet like it was natural, it’s been a while since I last touched some 68k assembly (on the Atari STe) and it was the result of an hour or so of trawling through documentation and example code to refresh my memory!
The Megadrive ROM header
Unfortunately, it’s not as simple as loading up the generated ROM into an emulator and hitting Debug. A Megadrive ROM needs a header, which contains some meta info about the ROM, and a block of CPU vectors used to initialise the 68000 before the code gets executed. The header takes up 512 bytes at the very top of the ROM, and looks a little something like this:
; ****************************************************************** ; Sega Megadrive ROM header ; ****************************************************************** dc.l 0x00FFE000 ; Initial stack pointer value dc.l EntryPoint ; Start of program dc.l Exception ; Bus error dc.l Exception ; Address error dc.l Exception ; Illegal instruction dc.l Exception ; Division by zero dc.l Exception ; CHK exception dc.l Exception ; TRAPV exception dc.l Exception ; Privilege violation dc.l Exception ; TRACE exception dc.l Exception ; Line-A emulator dc.l Exception ; Line-F emulator dc.l Exception ; Unused (reserved) dc.l Exception ; Unused (reserved) dc.l Exception ; Unused (reserved) dc.l Exception ; Unused (reserved) dc.l Exception ; Unused (reserved) dc.l Exception ; Unused (reserved) dc.l Exception ; Unused (reserved) dc.l Exception ; Unused (reserved) dc.l Exception ; Unused (reserved) dc.l Exception ; Unused (reserved) dc.l Exception ; Unused (reserved) dc.l Exception ; Unused (reserved) dc.l Exception ; Spurious exception dc.l Exception ; IRQ level 1 dc.l Exception ; IRQ level 2 dc.l Exception ; IRQ level 3 dc.l HBlankInterrupt ; IRQ level 4 (horizontal retrace interrupt) dc.l Exception ; IRQ level 5 dc.l VBlankInterrupt ; IRQ level 6 (vertical retrace interrupt) dc.l Exception ; IRQ level 7 dc.l Exception ; TRAP #00 exception dc.l Exception ; TRAP #01 exception dc.l Exception ; TRAP #02 exception dc.l Exception ; TRAP #03 exception dc.l Exception ; TRAP #04 exception dc.l Exception ; TRAP #05 exception dc.l Exception ; TRAP #06 exception dc.l Exception ; TRAP #07 exception dc.l Exception ; TRAP #08 exception dc.l Exception ; TRAP #09 exception dc.l Exception ; TRAP #10 exception dc.l Exception ; TRAP #11 exception dc.l Exception ; TRAP #12 exception dc.l Exception ; TRAP #13 exception dc.l Exception ; TRAP #14 exception dc.l Exception ; TRAP #15 exception dc.l Exception ; Unused (reserved) dc.l Exception ; Unused (reserved) dc.l Exception ; Unused (reserved) dc.l Exception ; Unused (reserved) dc.l Exception ; Unused (reserved) dc.l Exception ; Unused (reserved) dc.l Exception ; Unused (reserved) dc.l Exception ; Unused (reserved) dc.l Exception ; Unused (reserved) dc.l Exception ; Unused (reserved) dc.l Exception ; Unused (reserved) dc.l Exception ; Unused (reserved) dc.l Exception ; Unused (reserved) dc.l Exception ; Unused (reserved) dc.l Exception ; Unused (reserved) dc.l Exception ; Unused (reserved) dc.b "SEGA GENESIS " ; Console name dc.b "(C)SEGA 1992.SEP" ; Copyrght holder and release date dc.b "YOUR GAME HERE " ; Domestic name dc.b "YOUR GAME HERE " ; International name dc.b "GM XXXXXXXX-XX" ; Version number dc.w 0x0000 ; Checksum dc.b "J " ; I/O support dc.l 0x00000000 ; Start address of ROM dc.l __end ; End address of ROM dc.l 0x00FF0000 ; Start address of RAM dc.l 0x00FFFFFF ; End address of RAM dc.l 0x00000000 ; SRAM enabled dc.l 0x00000000 ; Unused dc.l 0x00000000 ; Start address of SRAM dc.l 0x00000000 ; End address of SRAM dc.l 0x00000000 ; Unused dc.l 0x00000000 ; Unused dc.b " " ; Notes (unused) dc.b "JUE " ; Country codes
Note that the assembler requires code and data to be tabbed one to the right, I’ll look into why this is necessary at a later date. Labels, however seem happy with no tabs.
The top section is a block of CPU vectors, read in when the system boots, and are used to initialise various registers and interrupt addresses. The first longword is the value of the stack pointer register when the system boots, although the rest of the registers must be initialised manually so I’m confused as to why this one must be explicitly set. The EntryPoint is the address of the first line of code that gets run, and the majority of the rest point to an exception routine to catch errors. Eventually I plan to write a proper exception handler for each type of problem, and print to screen some information which would help me diagnose the issue.
The HBlankInterrupt and VBlankInterrupt are routines that get called when the electron beam in the TV reaches the right hand side of the screen, and when the beam hits the bottom right before switching off and moving back to the top left. I guess modern LCD and plasma TVs don’t have this concept, but from the examples I’ve seen the timing for these interrupts being called is clock-accurate, so they’re perfect for implementing timers.
The second block is some information about the cartridge, hopefully the comments are self explanatory. The ROM/SRAM start and end addresses make sense to me since a cartridge and its savegame space (if any) can be of variable size, but I’ve yet to discover why the RAM start and end addresses need explicitly defining. The checksum is not read by the boot code itself and nothing is done with it, it’s only there for the programmer to implement a check if they wish.
All of the addresses can just be specified in hex, but the assembler allows for labels which makes things a great deal easier. EntryPoint, Exception, __end, HBlankInterrupt and VBlankInterrupt will need defining:
EntryPoint: Loop: move.l #0xF, d0 ; Move 15 into register d0 move.l d0, d1 ; Move contents of register d0 into d1 jmp Loop ; Jump back up to 'Loop' HBlankInterrupt: VBlankInterrupt: rte ; Return from Exception Exception: rte ; Return from Exception __end ; Very last line, end of ROM address
EntryPoint just loops around the little snippet I used to test out the assembler above. Both H/VBlankInterrupts and the Exception handler do nothing and return for now, I’ll experiment with those later. __end contains no code, it’s just a marker for the address of the last byte of the ROM. I’ve prefixed the label with underscores, simply to indicate that it’s not a subroutine and shouldn’t be called explicitly.
Ok, it should be ready to build and run!
Debugging the ROM
The ROM assembles with the ASM68k.EXE line demonstrated earlier. My chosen emulator, MESS, needs to be configured to enable the debugging features. After running MESS once, a mess.ini file is generated alongside the .exe, which contains a debug flag which can be set to 1. Now the ROM can be run using:
mess64.exe genesis -cart test.bin
MESS fires up, loads the ROM, and displays a debugging window. Unfortunately, I ran into a problem: the disassembly window shows garbage. The opcodes are mostly ‘ori’ and ‘illegal’, and I couldn’t make head or tail of my code:
After some digging around and tearing my hair out, the guys at ASSEMblergames.com pointed out that the first 15 bytes of my ROM didn’t belong there (I’m assuming the assembler added some sort of meta data to the start of the binary, perhaps for the SN debugger), and would need removing before it would work. After deleting those bytes using a hex editor (or assembling with the /p option), the ROM seems to work:
Much better, the opcodes are recognisable now. Time to test it out – MESS uses the same keyboard shortcuts for debugging as Visual Studio:
- F9 – Set/unset breakpoint
- F10 – Step over
- F11 – Step into
- SHIFT + F11 – Step out
So, after a single step (F10) the program counter moves straight to the address specified as the entry point in the header and executes it, and the value 0xF is moved into register r0. After a second step the contents of r0 (still 0xF) are moved into register r1, and after a third step the program counter is jumped back up to the first line again:
It’s not exactly Crysis, but it demonstrates that everything is in the right place and ready for the next part – initialising the Megadrive.