Home

Awesome

MDK: Mega Drive Software Development Kit

Origin

This project was born out of my wanting to write my own C support code for Mega Drive when I was working on a particular game. I had developed it in another SDK, but between a mismatch in code style, old code not aging well, and some functionally completely breaking when I updated to a newer version, I wanted to feel a little more in control of my project. So, I started fresh, and tried to compartmentalize all of my code that interacted with the MD hardware, keeping it divorced from the game logic where sensible. This project continues to receive updates as I add features or change it during the development of that game.

IT IS NOT YET STABLE! The API may change at any time, until that game is done.

Goals

This project aims to supply a set of minimal support code for use in making software for Sega Mega Drive, Genesis, System C, and System C2 platforms using the C programming language. Current solutions exist on both ends of the complexity spectrum, with straight 68000 assembly programming on one and SGDK on the other. This project aims to deliver something a little more in the middle in terms of weight and simplicity.

This project supports Linux, Windows, and MacOS-based development. For Windows, I strongly suggest using Windows Subsytem for Linux.

Rather than provide a library to link against, this project is intended to act as a skeleton that is the basis of another project. MD support functions may be compiled and used. The suggested use case is to make an mdk submodule for a project.

Sample code and Makefiles exist in the samples/ directory. A makefile is provided that will recursively look for source files in the src/ directory. It links using the provided linker script, which puts ROM from $000000 to $3FFFFF and RAM at $FF0000-$FFFFFF.

Setup

Environment Prerequisites

The Sega Mega Drive / Genesis is powered by the Motorola 68000 processor. In this era, it is expected that development is gone through cross-assembly and cross-compilation from a more powerful host computer. To that end, we need to install a toolchain.

Building the Cross Compiler

A Makefile has already been made which will clone the GCC 12 source, configure it appropriately for M68000, and kick off a build. Run the following commands:

    $ git clone git@github.com:andwn/m68k-gcc-toolchain
    $ cd m68k-gcc-toolchain
    $ make
    $ sudo make install

This will install the toolchain into /opt/toolchains/m68k-elf/.

Afterwards, you may remove the build artifacts and GCC sources cloned earlier.

    $ rm -rf ./m68k-gcc-toolchain

Starting a Project

If you are starting fresh, I recommend copying the contents of blank-project into your root and using that:

    $ cp -r mdk/blank-project my-project

Then, add MDK as a submodule:

    $ cd my-project
    $ git submodule add git@github.com:mikejmoffitt/mdk mdk
    $ git add mdk && git commit -m "Added MDK submodule."

The default Makefile searches in the src directory recursively for any .c files or .s files, and compiles or assembles them accordingly. In addition, files placed in res are included in binary form. Their usage is detailed further below. Edit the Makefile to change the name from blank-project to that if your project.

At this stage, you should be able to build your project, though it will not do anything interesting.

    $ make

It's not required to do so, but you may want to edit mdk/header.inc to include your project metadata as well.

You can periodically update MDK with the following command:

    $ git submodule update --remote --merge
    $ git add mdk && git commit -m "Updated MDK submodule."

Extra Dependencies

If you have edited your Makefile to include additional generated files (script parsing, data generation, etc), you should declare any external build dependencies with EXTERNAL_DEPS, and make the build system aware of externally generated artifacts with EXTERNAL_ARTIFACTS (so they can be cleaned when the clean target is run).

Utils

MDK relies on a few host-side utilities, so you will want to include utils/ in your project.

SDK Usage

Paths and Sources

MDK presents two directories in your source path: md, which contains the core Mega Drive-related functionality, and util, which has useful auxiliary functions that aren't considered core functions.

Different components are in md, and their header files will provide useful structures, enums, and documentation. However, md/megadrive.h exists to declare a function which initializes all of these components, and if included you may avoid referencing each component separately.

Interaction

The C runtime startup code clears RAM, sets up initial data for static variables, sets up the stack, and jumps to main(). That's all that is done out of the C programmer's sight. However, I'd recommend making the very first line in main() a call to megadrive_init():

    megadrive_init();

This sets the Mega Drive to sane defaults, and initializes various helper systems for the peripherals. Following that, an infinite loop like this is sufficient:

    while(1)
    {
        // Your logic goes here!
        megadrive_finish();
    }

Samples

Check out the samples/ directory for some small example projects that show usage of the SDK.

Platforms and Unused Code

MDK presently supports the following platforms:

The default platform is Mega Drive, and the symbol MDK_TARGET_MD is defined. You may specify another platform by setting the following in the Makefile:

    TARGET_PLATFORM := MDK_TARGET_C2

Multiple platforms means that some SDK code may not be relevant to your platform. If the code is not relevant, it is not called, or the branch to determine whether to call it is extremly cheap (i.e. a single tst.w) As for code size, GCC does a good job deleting unused functions and variables, so code size will not suffer. As for memory usage, unnecessary variables are gated by #ifdef guards, and will not waste RAM.

Components

For each component, I recommend reading the notes in the header files. I will summarize the components here.

Platform

megadrive_finish() should be called at the end of one frame of execution*, and will do the following things:

You can see the body of megadrive_finish() in md/megadrive.h. As with almost everything in this project, you aren't required to use it. Feel free to copy the parts you like, or call them elsewhere, if you really want to.

* Small note on execution timing and vblank: On a system with fixed specifications like the Mega Drive, it is not only normal but also recommended that one "tick" of game engine execution occur synchronously with the refresh of the screen. As a result, it's typical that one frame of logic runs, and then waits for the end of the frame. I am not doing anything from stopping you from running your game based on timer reads from the OPN or something, if you really want to.

Interrupt Handlers

irq.s is where the interrupt handlers live. The vblank ISR does three things: signals the start of vblank for a synchronization function, polls and caches controller data, and processes the DMA queue with any remaining time.

The hblank and controller th pin ISRs have been left empty. If you wish to do anything with them, you will want to place a handler in irq.s, or at least call out to a C function of your choosing from here.

VDP Control

vdp.h contains VDP control functions. Most registers do not need to be manipulated directly, as separate manipulation functions exist with some level of friendliness on top. Registers written are cached, so that they may be "read back" even though the VDP does not actually support this feature. This is useful for restoring temporarily changed register values.

DMA control

dma.h contains DMA related functions. On the Mega Drive, VRAM, CRAM, and VSRAM are all written to indirectly through the VDP, as they are not memory mapped. For transferring large chunks of data, DMA is the only efficient way to do this. However, timing a DMA can be complicated, as it not only ties up the CPU bus, but faces VRAM contention during the active display region.

DMA transfers are registered with calls to the DMA queue functions, and that list is processed during vertical blank (from md_dma_process()). A single high-priority slot exists for the sake of the sprite table, which will be checked and transferred before any others.

If you really want a DMA to occur immediately, you may call md_dma_process() right after queueing a transfer.

Palettes

pal.h has functions for uploading an entire palette, or setting individual colors. It operates on a 64-entry palette cache in work RAM, and keeps track of whether a palette "line" (16-color grouping native to the video system) has changed. Accordingly, it will queue a transfer with DMA to be done during vertical blank.

IO

io.h is used to get controller data. It supports 3 and 6 button controllers, and SMS controllers with a caveat.

A six-button pad is detected with a simple herusitic, and a special bit is set in the returned data (MD_PAD_6B). You may test this bit to determine if a six-button pad is in use.

If no pad is plugged in, MD_PAD_UNPLUGGED is set in the controller data. If an SMS controller is in use, this bit may get set as a side effect. It is safe to ignore this bit.

Sprites

spr.h houses sprite placement functions. The sprite table is exposed under a global symbol g_sprite_table, but sprite placement is easy using the md_spr_put() inline function. Sprites are placed into a buffer in main memory with this function. Once a frame's calculation is finished, md_spr_finish() will terminate the sprite list properly, and schedule a DMA transfer of the sprite table to VRAM.

md_spr_mask_line_full will insert a "magic sprite" that masks off any other sprites appearing on the specified scanline.

md_spr_finish() is called by megadrive_finish(), so if you are using that, you do not need to worry about it.

System

sys.h is for controlling various top level system functions. Interrupts may be disabled and enabled, as well as Z80 bus manipulation and initialization. The status register can be checked too, to retrieve the system region, revision, refresh rate, etc.

Interrupts and Exceptions

irq.h presents md_irq_register() by which a callback can be associated with interrupts and excepptions.

    void irq_callback(void);
    { ... }

    md_irq_register(MD_IRQ_VBLANK, irq_callback);

They are called safely from an interrupt context, with register clobber protection.

The vertical blank ISR clears a flag related to frame timing, and then calls the callback if it is there. Registering a callback will not interfere with normal operation.

Sound

opn.h and psg.h contain functions for talking to the FM and PSG sound hardware, respectively. You may use these to create your own sound driver, but that task is typically relegated to the Z80. You might find the PSG channel pitch to be a better debugging tool than it appears at first glance.

Including Binary Data

A tool called bin2s is included with this project, and is a build target for the host machine. It takes several binary files and generates an assembly language (.s) file containing their data, with symbols named after their name and location.

If a file structure is created like this:

res/
    foo.bin
    bar/baz.bin

the following uint8_t arrays will be defined:

extern const uint8_t res_foo_bin[];
extern const uint8_t res_bar_baz_bin[];

The following sizes will also be defined:

extern const uint32_t res_foo_bin_size;
extern const uint32_t res_bar_baz_bin_size;

If you wish to generate files using other tools that will then be placed in res/, you can declare them as external dependencies.

Hardware Testing

Bryan Topp's Megaloader is included, allowing for the flash make target to push the game binary over to a Mega Everdrive connected via USB. This is for testing your program on hardware.

TODO

This project is far from complete. In no particular order, there are some tasks I'd like to get taken care of: