Neo-Geo Assembly Programming for the Absolute Beginner

Written by freem at the tail end of 2015, spilling into the beginning of 2016
release candidate version 1b (2016/02/02 18:57 Central USA Time/GMT-6)


After spending over a year and a half working with this wonderful platform, I was requested to make a "from scratch" guide to Neo-Geo programming. This guide does not use any libraries (e.g. the NeoBitz kit, DATLib, or freemlib for Neo-Geo). If the idea of doing everything in assembly language scares you, there's a few alternatives out there for coding Neo-Geo games in C. (All of them either spawn from or require parts of the original kit by Fabrice Martinez, Jeff Kurtz, et al.)

If the white-on-black color scheme annoys you, please switch to the "Bright" color scheme.

WIP Notes

This guide still assumes a lot of information. These gaps will (hopefully) be covered in the future.

Things I can think of off the top of my head:

If there is anything missing from this list that you think should be mentioned, please let me know via e-mail (ajk187 on gmail) or on IRC: #neogeodev

The actual code is being worked on as well.

Table of Contents

  1. Introduction
    1. Terminology/Legend
  2. The Neo-Geo
    1. Specifications
    2. Limitations
  3. The Motorola 68000
    1. Data Sizes
    2. Registers
    3. User and Supervisor Modes
    4. Interrupts
    5. The Instruction Set
  4. Tools and Utilities
    1. Assemblers
    2. Linkers
    3. Graphics Tools
    4. Sound Tools
    5. Other Tools
  5. Tools and Workspace Setup
    1. Tools
    2. Workspace
  6. Neo-Geo Program Structure
    1. 68000 Vectors
    2. Neo-Geo Header
    3. Software Dips
    4. Security Code
    5. Required Routines
  7. Neo-Geo Registers and System ROM
    1. Neo-Geo Hardware Registers
    2. The System ROM
  8. Graphics System Details
    1. Registers
    2. VRAM
    3. Fix Layer
    4. Sprites
  9. Creating a simple "Hello World" program
    1. Example Project Contents
    2. The Vectors and Neo-Geo Header
    3. Required Routines
    4. Interrupts
    5. Code Structure
    6. Displaying Items on the Fix Layer
    7. The "Hello World" String (Manual)
    8. The "Hello World" String (Routine)
    9. Build Process
    10. Running the Demo in MAME
  10. Further Reading


This guide is meant to be a beginner's first port of call for learning how to program for the Neo-Geo. Some sort of previous programming experience would be good to have, even if it's not assembly language.

I can't possibly teach you everything there is to know about the Neo-Geo, sadly. That's outside the scope of this document for many reasons, not the least of which is that I dislike writing Z80 sound code. :D
Plus, introducing too many things this early will likely scare you off.

At this time, the guide assumes you know how to use your computer and how to set up various things (e.g. adding programs to and/or changing the PATH).


This guide uses a bit of programming jargon that you might not be familiar with.

Binary Numbers

In this guide, binary numbers (0 or 1 for each bit) are displayed with a % prefix (e.g. %00001111, which is $0F (decimal 15)).

Hexadecimal Numbers ("hex")

In this guide, hex numbers ($0-$F, representing decimal numbers 0-15) are displayed with a $ prefix (e.g. $10FD80).

Binary Coded Decimal ("BCD")

Binary coded decimal involves using hex values $0-$9 to represent decimal numbers. For example, the decimal number 20 would be encoded as hex $20.


Generally, processors don't agree on how data is stored internally. There are two competing formats, big endian (also referred to as "network byte order" in other docs) and little endian.

In little endian, the most significant byte is the first to be written, so a hex value of $12345678 is stored as $78 $56 $34 $12.

Luckily, the Neo-Geo doesn't use little endian; it uses big endian instead. A hex value of $12345678 will be stored in big endian as $12 $34 $56 $78.

The Neo-Geo

The Neo-Geo was SNK (and ADK's) solution for arcade operators to switch out games easily. It ended up being quite popular, being released in 1989, with the last official game being released in 2004. In addition to the arcade machines (called "MVS" or "Multi Video System"), there was a home version produced. Later, a couple of CD versions of the system were released as well.


For the time, the Neo-Geo provided a lot of power.


The cart and CD systems have different sets of limitations. Unless you're trying to make the most detailed game ever, you probably won't run into them.


Both system types run into these limits:

Cart Systems

Cart systems aren't limited by much, since pretty much everything is loaded from ROM. Cartridges also have the advantage of being able to use custom hardware. (Some examples are the link jacks and extra processor on League Bowling and Riding Hero, the CPLD on Metal Slug X, and the NEO-CMC, among others.)

CD Systems

CD systems are limited in a number of ways, since most data is loaded into RAM.

Getting around these limitations requires loading different files off of the disc. When switching a large amount of data, a loading screen should be used.

More technical information is found in later sections, closer to the program example.

The Motorola 68000

At the heart of the Neo-Geo is the Motorola 68000 processor (also manufactured by third parties). The same processor (though sometimes a later revision) powers the Sega Genesis/Mega Drive, the Commodore Amiga (non-PPC versions), and many arcade games of the 1980s and 1990s. You might see the processor being referred to as "m68k" or "680x0" in various literature.

Though the 68000 is a 16-bit processor (defined by the external bus size), it is more than capable of handling 32-bit values.

Data Sizes

On the 68000, the size of the data type you use plays an important role in the program's layout. The original 68000 is not capable of accessing words and longwords on "odd" addresses, so e.g. a word read (two bytes) from $102061 will cause an error. This is a very important thing to note, as your code may compile fine, but running it will cause a reset.

Name Size
byte 1 byte (8 bits)
word 2 bytes (16 bits)
longword/long 4 bytes (32 bits)
quadword/quad 8 bytes (64 bits, split across two registers)


The 68000 family gives you access to 8 data registers, 7 address registers, the stack pointer, and a few others. All registers are 32-bit.

Register Name Type
d0-d7 Data
a0-a6 Address
a7/sp Stack Pointer (User)
pc Program Counter
ccr Condition Code Register
sr Status Register

User and Supervisor Modes

The 68000 has two modes of operation: User mode and Supervisor mode. The difference between the two modes is the instructions that are available for use. For Neo-Geo development, you're going to be using Supervisor mode, since the System ROM uses privileged instructions. It's easier to just set it and forget it, compared to using User mode and having to remember to switch back to supervisor before calling a system ROM routine, etc.


The 68K family is capable of servicing hardware interrupts. On the Neo-Geo, these are used for horizontal and vertical blanking. (The CD systems also seem to use the third interrupt for file loading?)

The interrupts are explained in more detail in the example section.

The Instruction Set

The 68000's instruction set is much too large to cover here, so you are suggested to pick up some extra documentation. The primary reference for the 680x0 family is the Motorola M68000 Family Programmer's Reference Manual, which is available for download from NXP's site. (See Further Reading.)

Other good sources are Internet source engines and possibly your local library. There's a lot of literature available for the 68000 family, but remember that you're only dealing with the base 68000 (and not any of the successors) without a FPU.

MarkeyJester's 68k Tutorial is a good resource for beginners.

Tools and Utilities

In order to create the data necessary for a Neo-Geo game/utility/whatever, you're going to need some tools.


The assembler handles the job of taking your source code and spitting out the relevant data in binary format. There are many more assemblers out there than what is listed here.


vasm is a cross-platform assembler that supports a number of architectures, including 68000 and Z80. This tutorial uses vasm for assembly.

GNU assembler ("gas")

The GNU assembler ("as", "gas") is another option. Its default syntax for M68K leaves a bit to be desired, but there are options for getting around it.

The Macroassembler AS

AS is another cross-platform assembler that targets a number of architectures.


While a linker isn't exactly required, it helps a lot with bigger projects.


vlink is the recommended linker to use with vasm. It is the linker that would be used if this project was more than a simple "Hello World".

GNU linker ("ld")

The GNU linker can also be used, if you have a toolchain that supports it.

Graphics Tools

Many tools for graphics exist, including converters, viewers, and editors. A lot of these tools are Windows-only.


NGFX is a tool by blastar for viewing and editing various Neo-Geo graphic formats. A public version has not yet been released.

DATLib graphics tools

A few graphics tools are included with DATLib. This is probably your best bet for easily creating Neo-Geo format graphics (until NGFX is released, anyways).


YY-CHR is a graphics editor for various game formats. While the original YY-CHR can run in Wine, the later YY-CHR.NET version does not.


NeoFixFormat is a YY-CHR.NET plugin that allows you to edit Neo-Geo fix tiles.


Converts 4BPP Sega Master System/Game Gear/Wonderswan Color graphics to a format the Neo-Geo can understand (after some manipulation; see ROMWak below).

Sound Tools

If there's a weak point in the current Neo-Geo homebrew development scene, it's sound. Luckily, 2015 was the year of a few good sound-related tool releases.

Neo Sound Builder

Jeff Kurtz's Neo Sound Builder enables you to create ADPCM-A samples and handles placement and addresses for you.

ADPCM-A Encoder

Command line ADPCM-A encoder by freem.

ADPCM-B Encoder

Command line ADPCM-B encoder by ValleyBell and Fred/FRONT.

Other Tools

There are a few other tools that are extremely helpful to have around when developing for the Neo-Geo.

GNU make

GNU Make is just one way of creating a system to build your project. Its lack of arithmetic commands (which would be useful for creating the C ROMs) may make you want to choose another option.


Originally created by Jeff Kurtz, ROMwak allows you to perform many simple binary manipulations (e.g. byteswapping, padding).


mkisofs is a part of the cdrtools suite. It creates an .iso image, which you can then burn and use on your Neo-Geo CD.


chdman is used to create .chd files from CD images. This is useful for when you want to test your Neo-Geo CD program on MAME before burning it to a disc.

Tools and Workspace Setup

Before we can begin creating the Hello World project, the tools will need to be installed and runnable.


You can use whatever tools you're most comfortable with, but this guide will be using vasm (68k target, Motorola syntax) as the assembler. Since the Hello World program is small, we don't really need a linker. The graphics are already supplied, so you don't have to mess with those if you don't want to.

The source code and binary for the simplest possible sound driver are also included. You do not need to have vasm (z80 target, oldstyle syntax) installed unless you want to rebuild the sound driver from source.


Setting up a project workspace is a matter of personal preference. For the most part, I like keeping items separated in directories, based on need.

My typical project workspace looks like this:

You are free to either use or ignore this; modify it to your liking, and so on.

Neo-Geo Program Structure

68000 Vectors

As with most (if not all) 68k binaries, the first 256 ($100) bytes define various addresses for the machine to use. This is typically called the "vector" section. The vectors are slightly different depending on if you're targeting cart or CD systems. The values marked "(your choice)" are up to you. A typical define for these is $C00426, which will reset the system.

All values here are longwords (4 bytes each). (Only three bytes are shown for clarity, as the topmost byte will be $00 in all cases.)

Cart Systems

Address Name Description Suggested Default
$00 Initial Supervisor Stack Pointer Starting location of the supervisor stack. $10F300
$04 Initial Program Counter The first address the program runs on boot. $C00402
$08 Bus Error Used by the development BIOS to launch the built-in monitor. $C00408
$0C Address Error $C0040E
$10 Illegal Instruction Run upon executing the ILLEGAL opcode. $C00414
$14 Divide by 0 Computers can't divide by zero. (your choice)
$18 CHK Instruction Run upon executing the CHK opcode. (your choice)
$1C TRAPV Instruction Run upon executing the TRAPV opcode. (your choice)
$20 Privilege Violation Occurs when using privileged instructions (e.g. those meant for supervisor mode) in user mode. $C0041A
$24 Trace (Runs when the T bit of the stack register is set. ??) $C00420
$28 Line 1010 Emulator Allows you to trap opcodes that start wth the bit pattern %1010. (your choice)
$2C Line 1111 Emulator Allows you to trap opcodes that start wth the bit pattern %1111. (your choice)
$30-$3B Reserved (not used for any specific purpose) $C00426
$3C Uninitialized Interrupt Vector $C0042C
$40-$5F Reserved (not used for any specific purpose) $C00426
$60 Spurious Interrupt $C00432
$64 Level 1 Interrupt VBlank IRQ (your choice)
$68 Level 2 Interrupt HBlank IRQ (your choice)
$6C Level 3 Interrupt Typically unused (your choice)
$70 Level 4 Interrupt Typically unused (your choice)
$74 Level 5 Interrupt Typically unused (your choice)
$78 Level 6 Interrupt Typically unused (your choice)
$7C Level 7 Interrupt Typically unused (your choice)
$80-$BC Traps Typically unused. Puzzle Bobble seems to use some of them. (your choice)
$C0-$FF Reserved (not used for any specific purpose) (your choice)

CD Systems

Address Name Description Suggested Default
$00 Initial Supervisor Stack Pointer Starting location of the supervisor stack. $10F300
$04 Initial Program Counter The first address the program runs on boot. $C00402
$08 Bus Error Used by the development BIOS to launch the built-in monitor. $C00408
$0C Address Error $C0040E
$10 Illegal Instruction Run upon executing the ILLEGAL opcode. $C00414
$14 Divide by 0 Computers can't divide by zero. (your choice)
$18 CHK Instruction Run upon executing the CHK opcode. (your choice)
$1C TRAPV Instruction Run upon executing the TRAPV opcode. (your choice)
$20 Privilege Violation Occurs when using privileged instructions (e.g. those meant for supervisor mode) in user mode. $C0041A
$24 Trace (Runs when the T bit of the stack register is set. ??) $C00420
$28 Line 1010 Emulator Allows you to trap opcodes that start wth the bit pattern %1010. (your choice)
$2C Line 1111 Emulator Allows you to trap opcodes that start wth the bit pattern %1111. (your choice)
$30-$3B Reserved (not used for any specific purpose) $C00426
$3C Uninitialized Interrupt Vector $C0042C
$40 (CD-specific call?)   $C00522
$44 (CD-specific call?)   $C00528
$48 (CD-specific call?)   $C0052E
$4C (CD-specific call?)   $C00534
$50 (CD-specific call?)   $C0053A
$54 (CD-specific call?)   $C004F2
$58 (CD-specific call?)   $C004EC
$5C (CD-specific call?)   $C004E6
$60 Spurious Interrupt $C004E0
$64 Level 1 Interrupt HBlank IRQ (your choice)
$68 Level 2 Interrupt VBlank IRQ (your choice)
$6C Level 3 Interrupt Used on CD systems for ???. (your choice)
$70 Level 4 Interrupt Typically unused (your choice)
$74 Level 5 Interrupt Typically unused (your choice)
$78 Level 6 Interrupt Typically unused (your choice)
$7C Level 7 Interrupt Typically unused (your choice)
$80-$BC Traps Typically unused. Puzzle Bobble seems to use some of them. (your choice)
$C0-$FF Reserved (not used for any specific purpose) (your choice)

Neo-Geo Header

After the vectors, there's a set of values that identify the binary as Neo-Geo compatible.

Address Name Length Description
$0100 "NEO-GEO" string 7 bytes  
$0107 System Version 1 byte 0 on cart systems... 1 or 2 on Neo-Geo CD?
$0108 NGM/NGH Number 2 bytes The game's identifying number, used for memory card saves and MVS bookkeeping.
$10A Program Size 4 bytes The size of the program (in bytes).
$10E Backup RAM Pointer 4 bytes Points to a location in user RAM, used on MVS for saving backup data. (The first two bytes are used for debug dipswitches.)
$112 Game Save Size 2 bytes Size of the game's save size (in bytes).
$114 Eyecatch Flag 1 byte Determines how/if the BIOS plays the eyecatcher sequence. (0=handled by BIOS; 1=handled by game; 2=don't show)
$115 Eyecatch Sprite Bank 1 byte Defines the upper 8 bits of the tile number for the eyecatcher, if handled by the BIOS.
$116 Japanese Soft Dip address 4 bytes Pointer to the Japanese Soft Dips.
$11A USA Soft Dip address 4 bytes Pointer to the USA Soft Dips.
$11E European Soft Dip address 4 bytes Pointer to the European Soft Dips.
$122 Jump to USER routine 6 bytes jmp USER
$128 Jump to PLAYER_START routine 6 bytes jmp PLAYER_START
$12E Jump to DEMO_END routine 6 bytes jmp DEMO_END
$134 Jump to COIN_SOUND routine 6 bytes jmp COIN_SOUND
$13A-$181 (Typically unused?) XX bytes  
$182 Security code location 4 bytes Pointer to security code data

Software Dips

The software dipswitches are accessed via the operator's menu of the MVS. (They can also be accessed with the development BIOS; more on that later.)

Offset Name Size Description
$00 Game Name 16 bytes The game's name.
$10 Timed Option 1 2 bytes Timed option, BCD values. Maximum is $2959 (29 minutes, 59 seconds). Use $FFFF to disable.
$12 Timed Option 2 2 bytes Timed option, BCD values. Maximum is $2959 (29 minutes, 59 seconds). Use $FFFF to disable.
$14 Counter Option 1 1 byte A value between 00 and 99; $FFFF to disable.
$15 Counter Option 2 1 byte A value between 00 and 99, including "WITHOUT" and "INFINITE" options; $FFFF to disable.
$16 Option Mapping 10 bytes Map the default value and number of choices for each option. Use $00 for unused slots.
$20 Option Names and Choices (variable) Each string is 12 bytes long.

Example Software Dipswitch Setting

	dc.b "Example Softdips" ; Game name (16 bytes)
	dc.w $FFFF ; Timed option 1 (disabled)
	dc.w $FFFF ; Timed option 2 (disabled)
	dc.b $FF   ; Counter Option 1 (disabled)
	dc.b $FF   ; Counter Option 2 (disabled)

	; --Option Mapping list-- ;
	dc.b $02 ; Option 1: Default choice = 0, Num. choices = 2
	; Fill the rest with $00
	dc.b $00,$00,$00,$00,$00,$00,$00,$00,$00

	; --Option Defines-- ;
	; [Option 1]: Demo Sound (On/Off)
	;    "12characters"
	dc.b "Demo Sound  " ; Option Title
	dc.b "On          " ; Choice 1
	dc.b "Off         " ; Choice 2

Security Code

In addition to the check for the "NEO-GEO" string, the system ROM checks for a chunk of code. This code is typically pointed to at location $000182, though some games are known to just put the security code at that address.

The data you'll need to include is as follows (sourced from the Neo-Geo Development Wiki):

	dc.l $76004A6D,$0A146600,$003C206D,$0A043E2D
	dc.l $0A0813C0,$00300001,$32100C01,$00FF671A
	dc.l $30280002,$B02D0ACE,$66103028,$0004B02D
	dc.l $0ACF6606,$B22D0AD0,$67085088,$51CFFFD4
	dc.l $36074E75,$206D0A04,$3E2D0A08,$3210E049
	dc.l $0C0100FF,$671A3010,$B02D0ACE,$66123028
	dc.l $0002E048,$B02D0ACF,$6606B22D,$0AD06708
	dc.l $588851CF,$FFD83607
	dc.w $4E75

Required Routines

The Neo-Geo header requires the definition of four routines in your code. The routines are explained further in the example code.

USER Routine

Handles the user request value and performs one of four actions.


Called when one of the start buttons is pressed (both), or if time runs out on the title screen (MVS).

DEMO_END Routine

Called by the system ROM when the Select button is pressed (MVS mode).


The simplest of the four required routines. All COIN_SOUND has to do is play the coin sound (send the sound value to the Z80).

Neo-Geo Registers and System ROM

In order to communicate with the Neo-Geo, you'll need to know what addresses to read from and write to. This goes for both hardware registers and RAM locations used by the system ROM.

Neo-Geo Hardware Registers

Communication with the Neo-Geo hardware is primarily done in the $300000-$3FFFFF range. CD systems have additional registers (starting at $FF0000?). Only a few registers will be documented here. If you want to learn more, please see the Memory mapped registers page on the Neo-Geo Development Wiki.

Note: The graphics registers are in a separate section below.

Address Short Name Description Read Value Write Value
$300000 REG_P1CNT Player 1 Controller Data
(active low; e.g. button pressed=0)
7 6 5 4 3 2 1 0
D C B A Right Left Down Up
$320000 REG_SOUND Sound Register Read reply from Z80 Send command to Z80
$300001 REG_DIPSW Dipswitches and watchdog
7 6 5 4 3 2 1 0
Freeze Free Play Multiplayer Enable Multiplayer ID number 0: normal controls
1: mahjong panel
0: 1 coin chute
1: 2 coin chutes
Service Mode
Kick Watchdog
$320001 REG_STATUS_A Status and Coin
7 6 5 4 3 2 1 0
D4990 data bit D4990 time pulse 0: 4 slot
1: 6 slot
Coin 4 Coin 3 Service Button Coin 2 Coin 1
$340000 REG_P2CNT Player 2 Controller Data
(active low; e.g. button pressed=0)
7 6 5 4 3 2 1 0
D C B A Right Left Down Up
$380000 REG_STATUS_B Start/Select buttons, etc.
(active low; e.g. button pressed=0)
7 6 5 4 3 2 1 0
0: Home
1: MVS
Memory card
write protect
00: Memory card inserted Select P2 Start P2 Select P1 Start P1
$3A0001 REG_NOSHADOW Normal video output. (invalid?) byte
$3A0011 REG_SHADOW Darken video output. (invalid?) byte
$3A0003 REG_SWAPBIOS Use System ROM vector table. (invalid?) byte
$3A0013 REG_SWAPROM Use game's vector table. (invalid?) byte
$3A0005 REG_CARDUNLOCK1 Card unlock 1/2. (invalid?) byte
$3A0017 REG_CARDUNLOCK2 Card unlock 2/2. (invalid?) byte
$3A0015 REG_CARDLOCK1 Card lock 1/2. (invalid?) byte
$3A0007 REG_CARDLOCK2 Card lock 2/2. (invalid?) byte
$3A0009 REG_CARD_REGSEL Enables memory card register select mode. (invalid?) byte
$3A0019 REG_CARD_NORMAL Disables memory card register select mode. (invalid?) byte
$3A000B REG_BIOSFIX Use System ROM SFIX tiles and SM1/sound driver. (invalid?) byte
$3A001B REG_GAMEFIX Use game's Fix tiles and M1/sound driver. (invalid?) byte
$3A000D REG_SRAMLOCK Write-protect MVS backup RAM. (invalid?) byte
$3A001D REG_SRAMUNLOCK Un-protect MVS backup RAM. (invalid?) byte
$3A000F REG_PALBANK1 Use palette bank 1. (invalid?) byte
$3A001F REG_PALBANK0 Use palette bank 0. (invalid?) byte

The Watchdog

Special attention needs to be given to the watchdog. If it is not written to every ~0.13 seconds, the system will reset. In order to kick the watchdog, write any byte to the range $300000-$31FFFF. Typically, REG_DIPSW is the address written to.

The System ROM

The system ROM on the Neo-Geo uses a number of addresses in RAM. Some are for internal use only, while others are meant to be used by the programmer. The system ROM reserves the region from $10F300 to $10FFFF for its own purposes. CD systems use some RAM locations differently from cart systems.

These addresses are only meant for use with the official SNK System ROM; third party system ROM replacements may or may not use these addresses. Also, this is not a comprehensive list. The Neo-Geo Development Wiki has a page with various BIOS RAM locations if you wish to know more.

System Calls

The system calls are outlined on the Neo-Geo Development Wiki. Not all system calls are included here.

Address Name Description
$C00438 SYSTEM_INT1 System ROM VBlank routine.
$C0043E SYSTEM_INT2 Cart system VBlank routine. CD just rtses.
$C00444 SYSTEM_RETURN Gives control back to the system ROM. jmp'ed to at the end of the USER subroutine.
$C0044A SYSTEM_IO Handles system input/output. Updates the system ROM's RAM values, among other things.
$C004C2 FIX_CLEAR Clears the fix layer using tile $FF and draws a column of $20 tiles on each side of the screen.
$C004C8 LSP_1st Initialize sprite hardware.

RAM Locations

The system ROM uses a lot of RAM locations. This list is nowhere near complete, as a complete understanding of the RAM isn't required at the beginning.

(todo: there are quite a few more locations to add here; determine which ones are ok)

Address Short Name Length Description
$10FD80 BIOS_SYSTEM_MODE 1 byte Determines which VBlank routine should run. $00=system ROM; $80=game
$10FD82 BIOS_MVS_FLAG 1 byte Software-defined identifier. $00=home system, $01=MVS.
$10FD83 BIOS_COUNTRY_CODE 1 byte Software-defined system region. $00=Japan, $01=USA, $02=Europe/Export
$10FD96 BIOS_P1CURRENT 1 byte Player 1's inputs from current frame. (0=not pressed, 1=pressed)
$10FD97 BIOS_P1CHANGE 1 byte Player 1 active-edge input. (0=not pressed, 1=pressed)
$10FD98 BIOS_P1REPEAT 1 byte Auto-repeat flags.
$10FD9A BIOS_P2CURRENT 1 byte Player 2's inputs from current frame. (0=not pressed, 1=pressed)
$10FD9B BIOS_P2CHANGE 1 byte Player 2 active-edge input. (0=not pressed, 1=pressed)
$10FD9C BIOS_P2REPEAT 1 byte Auto-repeat flags.
$10FDAC BIOS_STATCURNT 1 byte Start and Select inputs for current frame. (Select bits are 0 on MVS)
$10FDAD BIOS_STATCHANGE 1 byte Start and Select active-edge inputs. (Select bits are 0 on MVS)
$10FDAE BIOS_USER_REQUEST 1 byte Command for USER.
$10FDAF BIOS_USER_MODE 1 byte Current game status. (0:init/boot, 1:title/demo, 2:game)
$10FDB0 BIOS_CREDIT1_DEC 1 byte (each) Set the number of credits to decrement here, before calling CREDIT_CHECK and CREDIT_DOWN. $10FDB0-$10FDB3 for Credits 1-4.
$10FDB4 BIOS_START_FLAG 1 byte Player(s) starting the game on a PLAYER_START call.
$10FDB6 BIOS_PLAYER1_MODE 1 byte (each) Status Values: 0=No play, 1=Playing, 2=Continue display, 3=Game Over. $10FD86-$10FD89 for Players 1-4.
$10FE80 BIOS_DEVMODE 1 byte Determines if the system is in developer mode or not.

Graphics System Details

In order to learn how to get graphics on the screen, you're going to need to know a little bit about the internals of the graphics system.


All access to the graphics hardware is done through memory-mapped registers. These registers lie in the $3C000x section. Most writes to the registers are meant to be words, though the IRQ acknowledgement register is an exception.

Address Name Description Read Write
$3C0000 LSPC_ADDR Used for setting the VRAM address to write to. Read VRAM data (address unchanged) Set VRAM address
$3C0002 LSPC_DATA Used to write data to VRAM. Read VRAM data (address unchanged) Write VRAM data
$3C0004 LSPC_INCR Controls how many bytes should be added to the VRAM address after a write. Read current value Set new value (signed)
$3C0006 LSPC_MODE LSPC status.
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
Raster line counter (unused?) 0=60Hz, 1=50Hz (LSPC2 only) Auto-animation counter
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
Auto-animation speed (frames) Timer interrupt mode Timer interrupt enable Disable auto-animation (unknown)
$3C0008 LSPC_TIMER_HI Timer control 1/2 (Invalid) Timer value, most significant bits
$3C000A LSPC_TIMER_LO Timer control 2/2 (Invalid) Timer value, least significant bits
$3C000C LSPC_IRQ_ACK IRQ acknowledgement register. Uses a byte instead of a word. (Invalid)
7 6 5 4 3 2 1 0
(Unknown) Ack. VBlank Ack. HBlank Ack. IRQ3
$3C000E LSPC_TIMER_STOP NTSC/PAL timer behavior (Invalid) If bit 0 is 1, timer counter is stopped for 32 raster lines in PAL mode.


The VRAM in the Neo-Geo contains a map for the Fix data, and sections for sprite control. Not all VRAM addresses are mentioned here, as they are out of scope for a beginner's tutorial.

All values in VRAM are word length (two bytes), despite the addresses only incrementing by 1.

Address Range Name Description
$0000-$6FFF Sprite Control Block 1 Contains the tilemap, palette, auto-animation values, and flip values.
$7000-$74FF Fix Layer Map Data contained on the Fix layer.
$8000-$81FF Sprite Control Block 2 Handles horizontal and vertical shrinking values.
$8200-$83FF Sprite Control Block 3 Controls the Y position, sprite size, and if this sprite is attached to the previous one.
$8400-$85FF Sprite Control Block 4 X position

Fix Layer

The Fix Layer is a tile-based map that appears over all sprites. Therefore, it's typically useful for HUDs (and not much else). Tiles are mapped from top to bottom, left to right.

Data in the Fix Layer is mapped as follows:

Palette Tile Index
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0


Sprites are used for everything else that needs to be displayed. A full sprite definition is split into four parts, making up the "Sprite Control Block".

SCB1: Tilemap

SCB1 defines the tilemap for each sprite. Each sprite's entry in the SCB1 block takes up 2 words × 32 tiles.

Even Words
Tile Index ($0xxxx)
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Odd Words
Palette Tile Index ($x0000) Auto-Anim (3) Auto-Anim (2) Vertical Flip Horizontal Flip
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

SCB2: Shrinking

Contrary to popular belief, the Neo-Geo cannot zoom sprites. However, it can shrink them.

N/A Horizontal Vertical
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

SCB3: Y Position and More

The most complex of the sprite values, SCB3 controls the sprite's Y position, size, and if it's attached to the previous sprite (when "sticky bit" = 1). The Sprite Size value ranges from 0-33; a value of 33 makes the sprite 32 tiles high with looped borders (when shrinking).

Y Position (436-Y) "Sticky Bit" Sprite Size
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

SCB4: X Position

SCB4 controls the X position. Unlike the Y position, it is mapped properly (e.g. a value of 0 will put the sprite at the leftmost part of the screen).

X Position N/A
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

Creating a simple "Hello World" program

With all of this out of the way, we can finally start work on the Hello World program itself. Since it's easier to create the cart version, that will be the first target.

At this time, you should download the Hello World project so you can follow along.

Example Project Contents

The contents of the Hello World project are laid out as follows:

The Vectors and Neo-Geo Header

The first two things that need to be in the program ROM are the 68K vectors and the Neo-Geo header, both mentioned above.

Required Routines

Four routines were mentioned above in the Neo-Geo header section. In order to ensure proper system operation, these routines need to perform some tasks.


The USER routine needs to respond to a command sent by the system ROM (stored in BIOS_USER_REQUEST). USER is also the first port of call for any Neo-Geo program, so a few things are initialized as well.

	move.b d0,REG_DIPSW ; kick watchdog
	lea $10F300,sp ; set stack pointer to $10F300 (BIOS_WORKRAM)
	move.w #0,LSPC_MODE ; Disable auto-animation and timer interrupts, set auto-anim speed to 0 frames
	move.w #7,LSPC_IRQ_ACK ; acknowledge all IRQs

	move.w #$2000,sr ; Enable VBlank interrupt, go Supervisor

	; Handle user request
	moveq #0,d0 ; clear all bits of d0
	move.b BIOS_USER_REQUEST,d0 ; put user request value into d0
	lsl.b #2,d0 ; shift value left twice to get offset into tbl_UserRequestCommands
	lea tbl_UserRequestCommands,a0 ; load address of user commands
	movea.l (a0,d0),a0 ; get address from table and offset
	jsr (a0) ; jump to subroutine (typically ends with a jmp SYSTEM_RETURN)
	jmp SYSTEM_RETURN ; this is here just in case it doesn't...?

; tbl_UserRequestCommands
; Table that contains the addresses for each BIOS_USER_REQUEST command.

	dc.l User_Initialize ; Command 0 (Initialize)
	dc.l SYSTEM_RETURN ; Command 1 (Custom Eyecatch, unused)
	dc.l User_Main ; Command 2 (Demo Game/Game)
	dc.l User_Main ; Command 3 (Title Display)

This routine is meant to initialize the backup RAM area (defined at $10E). This command is only called once on the MVS, when booting the game for the first time. (Other details are still todo...)

	; this would be the place to initialize high scores and other such data in
	; the backup RAM area (see cart/cd header, "pointer to backup RAM block").

	; in this demo we don't do anything, so just jump to SYSTEM_RETURN.
Custom Eyecatch

The "custom eyecatch" request allows you to change the standard Neo-Geo boot screen, but only on the AES. The value of $114/Eyecatch Flag must be 1 for this routine to be run. For now, it just jumps back to SYSTEM_RETURN.

Demo Game, Game

Games typically use this as their shared startup code. A lot of Neo-Geo System ROM variables will need to be juggled here in a normal game. For this demo, we're going to keep things simple and not worry about them. This command is described in more detail in the Code Structure section.

Title Display

This is only called when forced start is enabled on MVS. The game's title screen is displayed. If the select timer is enabled, you should print out the number of seconds ($10FDDA/SELECT_TIMER, in BCD) on the screen.

When time runs out, the game is started. There's no need to end with SYSTEM_RETURN. Everything else is the same as in command 2, so we just point command 3 to the same routine as command 2.


When a player presses start or the compulsion timer expires on the title screen, PLAYER_START is called. We don't bother handling it in this example...

	; In this demo, we don't handle the Start button, or coins, for that matter.


DEMO_END doesn't need to do much (it mainly saves MVS backup RAM values), but it should end with an rts instruction.

	rts ; we're not doing anything in this routine, so just exit.


The only thing COIN_SOUND needs to do is send a Z80 command to play a coin noise. In this example, we don't even do that; we just exit.

	rts ; we're not doing anything in this routine, so just exit.


Every Neo-Geo game is expected to handle the horizontal and vertical blanking interrupts.

Horizontal Blank (HBLANK)

The horizontal blank will not be used in this demo, but we will need to answer its interrupt in case it's run. This is done by writing 2 (%00000010; acknowledge HBlank) to LSPC_IRQ_ACK. Also, kick the watchdog, because defensive programming is a good idea with a device that will reset the system.

	move.w #2,LSPC_IRQ_ACK ; acknowledge interrupt #2 (HBlank)
	move.b d0,REG_DIPSW    ; kick watchdog (prevent reset)

Vertical Blank (VBLANK)

The VBlank, on the other hand, is one of the most important parts of any Neo-Geo program, as it's run once per frame. A few things are expected to happen in the VBlank.

  1. First, the game needs to give control back to the system ROM if it's requested. This is done by checking the topmost bit of BIOS_SYSTEM_MODE. If the topmost bit is not set, the game should jump to the system ROM's VBlank handler, SYSTEM_INT1.
  2. If the topmost bit is set, we can run the game's vblank. The first thing to do here is to save the status of all the registers. This is because the VBlank interrupt can happen at any time, interrupting any code.
  3. Next, the value 4 (%0000100; acknowledge VBlank) is written to LSPC_IRQ_ACK.
  4. For precautionary measures, you should kick the watchdog to prevent the system from resetting itself. Write any byte to REG_DIPSW to kick the watchdog.
  5. At this point, you can run whatever code you need in your VBlank.
  6. Once your code is done, you should call SYSTEM_IO.
  7. If you use a flag to determine when you're in VBlank, you should clear it here.
  8. Finally, restore the registers and return (via rte).
	btst #7,BIOS_SYSTEM_MODE ; check if the System ROM wants to run its VBlank routine.
	bne.s .VBlank_game ; if not, run our VBlank.
	jmp SYSTEM_INT1 ; jump to system ROM's VBlank routine

	movem.l d0-d7/a0-a6,-(sp) ; save registers to the stack
	move.w #4,LSPC_IRQ_ACK ; acknowledge the VBlank interrupt
	move.b d0,REG_DIPSW ; kick the watchdog

	; [Things to perform in VBlank]
	; VBlank is where you should be doing sprite data updates (SCB writes) and
	; palette RAM updates. However, this demo doesn't use either...yet.

	; SNK also wants you to call SYSTEM_IO every 1/60th of a second. (probably 1/50th on PAL)
	; This is pretty important, otherwise the RAM locations for input variables
	; don't get updated (unless you do it yourself), among other things.
	; Note: Other libraries may call this for you, so be aware when using them.

	move.b #0,flag_VBlank ; clear vblank flag so WaitVBlank knows to stop
	movem.l (sp)+,d0-d7/a0-a6 ; restore registers from the stack

A common routine to find in Neo-Geo games is a loop that waits for the VBlank interrupt to run. The flag_VBlank variable is an example of that. It gets set to a nonzero value in a routine called WaitVBlank, which looks something like this:

	move.b #1,flag_VBlank ; set vblank flag to 1.
	tst.b flag_VBlank ; check if vblank flag is 0.
	bne.s .WaitVBlank_loop ; if not zero, loop until it is.


IRQ 3 is only handled on CD systems. I'm not fully sure what it does yet, but like HBlank, we'll need to acknowledge the interrupt.

	move.w #1,LSPC_IRQ_ACK ; acknowledge interrupt 3
	move.b d0,REG_DIPSW ; kick watchdog

Code Structure

With the required routines out of the way, we can begin to worry about structuring our code. The end goal is to display "Hello World" on the screen, and the easiest way to do that is with the Fix Layer. Before we jump into writing the text, however, there are a few things we need to take care of.

Palette Setup

In order for anything to display properly, you're going to need to set up the palette RAM. The palette RAM spans the address range $400000-$401FFF, and can be bankswitched (see Neo-Geo Hardware Registers). Each entry in the palette is a word (two bytes), and has a special 6BPP format that can be a pain to work with. For this tutorial, just pretend that the values are $0RGB, where R is red, G is green, and B is blue. Each component ranges from $0 to $F.

Two positions in the palette RAM are important: the Reference Color, and the Background Color. The Reference Color is at $400000, and must be set to $8000 for proper display. The Background Color is at $401FFE, and can be set to whatever value you'd like.

For this demo, there's only one other palette location that matters: $400002. This is the entry that defines the color of the text displayed on the screen. By default, it's set to $0FFF, but you can change it to see how it works.

Fix Layer and Sprite Setup

The system ROM provides routines to set up the Fix layer and the Sprites. FIX_CLEAR clears the screen (aside from the left and right borders), while LSP_1st sets up the sprites.

Example Code

	; In a real Neo-Geo program, this would be handled differently, but this is
	; a demo, so it's going to be simpler than expected.

	; --perform initialization, part 1--
	; (Palette)
	; The reference color (address $400000) must be set to the darkest possible
	; color, which is $8000. (Black with the 'dark bit' set.)
	move.w #$8000,PALETTE_REFERENCE

	; Set the background color ($401FFE) to a regular black ($0000).

	; Finally, we need to set a color for the text to display. Set the first
	; palette entry in the first palette row ($40xxxx) to white ($0FFF).
	move.w #$0FFF,PALETTES+2

	; (Fix Layer)
	; The System ROM provides a command to set up the Fix layer, and it should
	; typically be called at boot.
	jsr FIX_CLEAR ; jump to the FIX_CLEAR subroutine

	; (Sprites)
	; Sprites will need to be initialized as well. The System ROM provides a
	; routine for this purpose as well.
	jsr LSP_1st ; jump to the LSP_1st subroutine

	; --perform initialization, part 2--
	jsr DrawHello_Manual  ; draw "Hello World" string manually
	jsr DrawHello_Routine ; draw "Hello World" string with a routine

	; this demo doesn't need to do anything, so infinite loop here.
	jmp MainLoop

Displaying Items on the Fix Layer

Writing to the Fix Layer is relatively straightforward. First, you'll want to set the increment to 32 ($20), so the text displays horizontally. Next, you'll want to write the target address to the LSPC address register. Finally, write the combined palette and tile index to the VRAM.

The following code writes tile $041 (typically an "A" in the default Fix ROM) with palette 0 to $7026:

	move.w #$20,LSPC_INCR ; set LSPC increment to +1 horizontal
	move.w #$7026,LSPC_ADDR ; set LSPC address to $7026
	move.w #$0041,LSPC_DATA ; write "A" from page 0 with palette 0

The "Hello World" String (Manual)

In order to introduce the basics of handling the VRAM, we'll manually write the "Hello World" string to the Fix layer. This involves sending multiple words to the LSPC data register.

	move.w #$20,LSPC_INCR ; set LSPC increment to +1 horizontal
	move.w #$7066,LSPC_ADDR ; set LSPC address to $7066
	move.w #$0348,LSPC_DATA ; write "H" from page 3 with palette 0
	move.w #$0365,LSPC_DATA ; write "e" from page 3 with palette 0
	move.w #$036C,LSPC_DATA ; write "l" from page 3 with palette 0
	move.w #$036C,LSPC_DATA ; write "l" from page 3 with palette 0
	move.w #$036F,LSPC_DATA ; write "o" from page 3 with palette 0
	move.w #$0320,LSPC_DATA ; write " " from page 3 with palette 0
	move.w #$0357,LSPC_DATA ; write "W" from page 3 with palette 0
	move.w #$036F,LSPC_DATA ; write "o" from page 3 with palette 0
	move.w #$0372,LSPC_DATA ; write "r" from page 3 with palette 0
	move.w #$036C,LSPC_DATA ; write "l" from page 3 with palette 0
	move.w #$0364,LSPC_DATA ; write "d" from page 3 with palette 0

The "Hello World" String (Routine)

Manually writing the values to the Fix layer's VRAM is alright for small changes, but it gets old if you want to print multiple strings. By setting a few ground rules, we can print string data to the Fix layer using a subroutine.

Much discussion could be had about these rules, but now is not the time. You're free to not follow them in your own productions, but for the sake of this example, using it will be easier than not.

; str_HelloWorld
; $FF-terminated hello world string.

str_HelloWorld: dc.b "Hello World",$FF

; fix_PrintString
; Prints a string on the Fix layer.

; (Params)
; d0 - [word] Fix layer address
; d1 - [byte] Palette number and page number
; a0 - [long] Pointer to string to write

	move.w #$20,LSPC_INCR ; set LSPC increment to $20/32 (horizontal writing)
	move.w d0,LSPC_ADDR ; set up LSPC address from param in d0
	moveq #0,d0 ; clear d0 so we can use it in the loop without issue.
	asl.w #8,d1 ; move byte from param in d1 to upper half of word

	move.b (a0)+,d0 ; get character from string and increment pointer position
	cmpi.b #$FF,d0 ; check if this character is $FF (the terminator)
	beq.s .fix_PrintString_End ; if so, we're done with the string; exit the routine.

	; normal execution:
	or.w d1,d0 ; OR with the palette and page number
	move.w d0,LSPC_DATA ; write tile to fix layer
	bra.s .fix_PrintString_Loop ; loop back for another character

	rts ; exit routine

; example usage:

	move.w #$708D,d0 ; fix layer address $708D
	moveq #$03,d1 ; Palette 0, Page 3
	; (moveq is used to clear out any garbage from the top bits, since it will be shifted later.)
	lea str_HelloWorld,a0 ; load pointer to string into a0
	jsr fix_PrintString ; jump to the print string subroutine

Build Process

The project is set up to be built using a Makefile with multiple targets. Required tools include vasm (targeting 68000 w/Motorola syntax) and GNU Make. vasm (targeting Z80 w/oldstyle syntax) is required to build the sound ROM.


prg Target

The prg target builds the program ROM (hello-p1.p1) for cart systems.

cdprg Target

The prg target builds the program ROM (HELLO.PRG) for CD systems.

cart Target

The cart target builds the entire program for cart systems.

cd Target

The cd target produces a Neo-Geo CD version of the program, including an .ISO image.

chd Target

The chd target produces a .chd file for use with MAME. It implies making the cd target.

Running the Demo in MAME

Thanks to somewhat recent developments, it's now possible to test Neo-Geo homebrew without requiring a recompile of MAME. After building the proper target (either cart or chd), you'll need to let MAME know a little bit about the ROMs/CD image. This involves editing files in the hash directory.

Example hashes for the cart and CHD targets are included in the "hash" directory of the project distribution, so all you need to do is copy the data over to the proper xml file.

Running the Cart Version

In order to test the cart versions (MVS/arcade, AES/home), you'll need to copy the files in the _cart directory to a new directory called hellotut in your MAME ROMs directory. From there, you can run one of the two commands below, assuming you're using the 64-bit versoin of MAME:

Running the CD Version

After creating the CHD target, copying the CHD file to the roms directory, and editing the neocd.xml to include the new entry, you can test the tutorial with this command:

mame64 neocdz HelloTut

A note about the Neo-Geo CD hashes: The CHD itself stores the proper hash values inside of the file (as opposed to being a hash of the entire file). In order to get the proper values for the xml, you're going to need to run the program once with the wrong hashes. MAME will tell you the proper values; edit those into neocd.xml and you'll be good to go.

Further Reading

This tutorial has only scratched the surface of developing for the Neo-Geo with the 68K. There's a lot of material out there worth reading for when you want to go further.