Hello world tutorial

From NeoGeo Development Wiki
Jump to: navigation, search
The result.

This tutorial will guide you through all the necessary steps to produce a burnable NeoGeo romset which displays the text "Hello world !" (or something close). It is written for Windows but all the necessary tools have equivalents for MacOS and Linux.

The programming language used is 68k assembly. Good knowledge of it is not needed as all instructions will be detailed.

The source files can be downloaded here: File:Helloworld.zip.

Setting up a minimal development environment

  • Download the latest MAME binaries from [mamedev.org]. Extract the zip contents to a folder such as "C:/MAME/".
  • To run NeoGeo software with MAME, you will need the dumps of the various embedded ROMs which are part of the system. You can find them as a pack called neogeo.zip (various forms, more or less complete). The files needed are 000-lo.lo (64KiB, the LO ROM), sm1.sm1 (128KiB, the M1 ROM for sound), sp-s2.sp1 (128KiB, the system ROM) and sfix.sfix (128KiB, the SFIX ROM). Copy those 4 files to the "C:/MAME/roms/neogeo/" folder (create roms/ and neogeo/ if needed). Like original games these ROMs are copyrighted, so you won't find them here.
All Super Sidekicks ROM files.
  • As a base, find and download a Super Sidekicks romset (ssideki.zip) and extract it to "C:/MAME/roms/ssideki/". Make sure that the following files are present and that they are named correctly (MAME is picky about file names and extensions):
    • 052-c1.c1 and 052-c2.c2 (C ROM pair, sprite graphics)
    • 052-m1.m1 (Z80 code, we won't mess with that)
    • 052-p1.p1 (68k code, that's what we'll replace)
    • 052-s1.s1 (S ROM: fix graphics, we'll use it)
    • 052-v1.v1 (V ROM: sound, won't touch that either)
  • Write a short batch file to start MAME easily with the right parameters. Create a new text file and add these lines:
mame -debug -nofilter -window ssideki
Super Sidekicks running in MAME with debugger enabled.

Save it with as runmvs.bat in "C:/MAME/" and run it. MAME should open the emulation window and its debugger. Don't be scared, just highlight the debugger window and press F5 (run) to see if Super Sidekicks starts in the main window. If the romset is bad, the missing and/or invalid files will be shown in the command line prompt. When the game runs correctly, close the debugger and the emulation window.

  • Download [the macro assembler AS (ASW)] and File:Flip pad.zip. There is no installation required, just extract the files to a folder like "C:/neogeo/asw/".
  • For the purpose of this tutorial, Notepad can be used as a code editor. Make a folder for this project such as "C:/neogeo/helloworld/".
  • Write a batch file in this folder to make ASW assemble your code and generate the P ROM. Create a new text file with the following contents:
@echo off
c:\neogeo\asw\bin\asw main -L -quiet
c:\neogeo\asw\bin\p2bin main -r $000000-$01FFFF
c:\neogeo\asw\flip main.bin 052-p1.p1
c:\neogeo\asw\pad 052-p1.p1 524288 255
copy 052-p1.p1 c:\mame\roms\ssideki\052-p1.p1

And save it as "make.bat" in the "C:/neogeo/helloworld/" folder. Don't run it for now.

Here's what happens: ASW assembles your source file, p2bin makes the P ROM with the right size ($1FFFF = 128KiB), flip.exe flips bytes in pairs (byteswapping) because P ROMs have to be that way, pad.exe fills up the P ROM with "$FF" bytes up to 512KiB to match the original ssideki P ROM size to make MAME happy, and it's finally copied in place of the original P ROM in MAME's directory.

Be aware that in this example, only the P ROM is replaced. All graphics, musics and sounds will still be those from Super Sidekicks.

Now that you have everything to assemble and run the ROM, you can start coding.

The code

  • Go to the 68k ASM defines page, copy everything, paste it in a new text file and save it as "regdefs.asm" in "c:/neogeo/helloworld/". This allows you to use register names in place of their addresses (easier to remember). For example, instead of writing $300001 in your code, you'll be able to write REG_DIPSW instead and ASW will take care of translating that to $300001 thanks to this new file.
  • Create a new text file and add the following lines:
    cpu 68000
    supmode on
    INCLUDE "regdefs.asm"

These aren't assembly instructions but directives that only ASW will understand. cpu 68000 indicates what CPU we're writing code for (Motorola 68000), supmode on indicates that the code will run in supervisor mode (allows us to use special instructions, the NeoGeo always runs in supervisor mode), and INCLUDE tells ASW to take a look in our previous "regdefs.asm" file. Save this file as "main.asm" in "c:/neogeo/helloworld/".


For the "cartridge" to be recognized by the system ROM and launched, header data must be present. Copy the minimal header to a new "header.asm" file.

Add the following line to main.asm:

    INCLUDE "header.asm"

This will also set up the v-blank interrupt vector to a label called VBLANK.

A bit of thinking

To display the "Hello world !" text, there are basically two methods:

  • Use sprites (one sprite for each letter, for example)
  • Use the fix layer (one tile per letter)

To avoid the need to edit the sprite ROMs and draw a font, we will use the fix font that is already stored in ssideki's S ROM (052-s1.bin).

If you look at the contents of a typical S ROM (like on this page), you will see that the first bank of tiles (256 tiles) contains a 8x8 pixels character set, which is often customized for each game. The Super Sidekicks one is stylized with color gradients, so we will use the 8x16 pixels alphabet instead, which is simpler (2 colors). It is found in the next two tile banks (tiles $100 to $2FF).

As shown in the SFIX tileset reference, bank 1 (tiles $100 to $1FF) contains the upper tiles (the top of the letters), and bank 2 (tiles $200 to $2FF) contains the lower tiles (the bottom of the letters).

By mapping those tiles correctly on the fix map, text can be displayed.

Note that there is a named MESS_OUT which purpose is to simplify text writing to the fix layer. For the purpose of this tutorial we'll do it the hard way, without using this call.

V-blank handler

First of all, we need to define a v-blank interrupt handler. The header file indicates that a label called VBLANK must be used.

In main.asm, add the following lines:

    ORG $300
VBLANK:                             ; Label defined in header.asm
    btst    #7,BIOS_SYSTEM_MODE     ; Check if the system ROM wants to take care of the interrupt
    bne     .getvbl                 ; No: jump to .getvbl
    jmp     SYS_INT1                ; Yes: jump to system ROM
    move.w  #4,REG_IRQACK           ; Acknowledge v-blank interrupt
    move.b  d0,REG_DIPSW            ; Kick watchdog
    rte                             ; Return from interrupt

The 'ORG $300 line is also an ASW directive. It tells ASW to put the following code at address $300 in our ROM. This avoids overwriting the header (which is between $0 and $200).

SNK required games to check the BIOS_SYSTEM_MODE variable to see if control has to be given to the system ROM, or if the game itself can use it.

  • btst #7,BIOS_SYSTEM_MODE (Bit TeST) checks bit 7 of the BIOS_SYSTEM_MODE variable.
  • bne .getvbl (Branch if Not Equal) jumps to .getvbl if the previously checked bit wasn't 0.
  • jmp BIOSF_SYSTEM_INT1 (JuMP) can be guessed quite easily.
  • move.w #4,REG_IRQACK writes 4 as a word (16 bits) in REG_IRQACK.
  • move.b d0,REG_DIPSW writes the D0 register's value as a byte in REG_DIPSW. This prevents the watchdog reset.

Our v-blank handler is the simplest there could be. Don't forget that interrupts can happen at any time during normal program flow, if registers have to be used in the handler routine, care must be taken to save and restore them (see the MOVEM instruction). We're only reading D0 in our case, so we don't need to preserve anything.


The system ROM doesn't just jump to the game's code once and for all. It jumps to code at $122, which is also defined in the header.asm file.

At $122, there is just enough room for a jump (see header.asm). So we need to define the USER subroutine.

In main.asm, add the following lines:

    dc.l    StartupInit             ; Jump table for the different things the system ROM can ask for
    dc.l    EyeCatcher
    dc.l    Game
    dc.l    Title

    move.b  d0,REG_DIPSW            ; Kick watchdog
    clr.l   d0                      ; Clear register D0
    move.b  BIOS_USER_REQUEST,d0    ; Put BIOS_USER_REQUEST (byte) in D0
    lsl.b   #2,d0                   ; D0 <<= 2
    lea     JT_USER,a0              ; Put the address of JT_USER in A0
    movea.l (a0,d0),a0              ; Read from jump table
    jsr     (a0)                    ; Jump to label
    jmp     SYS_RETURN              ; Tell the system ROM that we're done

When the system ROM calls the USER subroutine, it indicates what needs to be done with the BIOS_USER_REQUEST variable. There are 4 possible values, so we specify 4 labels as longword (32 bits) addresses (dc.l is yet another ASW directive).

The code for the USER subroutine starts after the USER label. First, the watchdog is kicked, just in case.

  • clr.l d0 (CLeaR) clears the whole D0 register (32 bits).
  • move.b BIOS_USER_REQUEST,d0 loads the byte variable BIOS_USER_REQUEST in D0. Since D0 was cleared, we made sure that D0 is now 000000XX.
  • lsl.b #2,d0 (Logical Shift Left) shifts D0 left 2 bits, multiplying D0 by 4 (our jump table entries are 32 bits = 4 bytes long).
  • lea JT_User,a0 (Load Effective Address) simply puts the JT_USER label's address in A0.
  • movea.l (a0,d0),a0 loads the longword value at (A0+D0) in A0, effectively reading the jump table at the index specified in BIOS_USER_REQUEST.
  • jsr (a0) jumps to the freshly loaded label address.

We now have our USER subroutine defined... And 4 new labels to define. Luckily for us, since we're not making a real game, we can only use one to simply run our code. We'll say that StartupInit, EyeCatcher and Title don't do anything:


This way, all 3 labels will resolve to the same address and point to a single RTS instruction (ReTurn from Subroutine). Normally, these subroutines are used to initialize backup RAM and to allow the system ROM to control the attract mode. We don't need that in our case.

The 4th label Game is the one we'll use to run our code.

Game:                               ; Label defined in the jump table
    lea     $10F300,sp              ; Init stack pointer
    move.b  d0,REG_DIPSW            ; Kick watchdog
    move.w  #$0000,REG_LSPCMODE     ; Make sur the pixel timer is disabled

    move.w  #7,REG_IRQACK           ; Clear all interrupts
    move.w  #$2000,sr               ; Enable interrupts

SP is a special 68k register (Stack Pointer). It should already be set to $10F300 by the system ROM, but we make sure it is. $10F300 is right at the end of user RAM. The stack will grow downwards. SR is also a special 68k register (System Register). Writing $2000 to it enables all interrupt levels.

Now that interrupts are ready to run, let's clear the user RAM, just for good measure.

    move.l  #($F300/32)-1,d7        ; We'll clear $F300 bytes of user RAM by writing 8 longwords (32 bytes) at a time
    lea     RAMSTART,a0             ; Start at the beginning of user RAM
    moveq.l #0,d0                   ; Clear it with 0's
    move.l  d0,(a0)+                ; Write the 8 longwords, incrementing A0 each time
    move.l  d0,(a0)+
    move.l  d0,(a0)+
    move.l  d0,(a0)+
    move.l  d0,(a0)+
    move.l  d0,(a0)+
    move.l  d0,(a0)+
    move.l  d0,(a0)+
    dbra    d7,.clear_ram           ; Are we done ? No: jump back to .clear_ram

User RAM (work RAM available for the game) starts at RAMSTART and is $F300 bytes long. The clearing loop could just write a 0 byte at each address, but why not use the concept of "unrolled loop", for speed ? Instead of writing one byte at a time, we can write 32 bytes for example. This makes clearing faster since there will be 32 times less iterations of the loop (that DBRA instruction takes some time).

  • move.l #($F300/32)-1,d7 sets the iterations count. ASW is able to evaluate "($F300/32)-1" to $797.
  • move.l d0,(a0)+ writes D0 as a longword at the address pointed to by A0. A0 is then incremented by 4 (longword = 4 bytes).
  • dbra d7,.clear_ram decrements D7 and jumps to .clear_ram if it isn't zero. The count must be 1 less because DBRA branches one last time when D7 = 0.

Using D7 for the iteration count isn't required, it's just a habit.

Cleaning up the display

Before writing tile numbers to the fix map, we want to make sure the display is cleared. There are system ROM calls which clear the fix map and sprites, but again, for this tutorial we'll do it the hard way.

Clearing sprites can simply be done by setting all the sprites height attributes to 0. That way, they will be ignored for rendering.

    move.w   #SCB3,REG_VRAMADDR     ; Height attributes are in VRAM at Sprite Control Bank 3
    clr.w    d0
    move.w   #1,REG_VRAMMOD         ; Set the VRAM address auto-increment value
    move.l   #512-1,d7              ; Clear all 512 sprites
    move.w   d0,REG_VRAMRW          ; Write to VRAM
    nop                             ; Wait a bit...
    dbra     d7,.clearspr           ; Are we done ? No: jump back to .clearspr

The NeoGeo should only be able to "see" the first 381 possible sprites, but clearing everything up to 512 isn't harmful. The NOP instructions are required because VRAM access can't be done at full speed. This is very important.

Then, the fix map is cleared:

    move.l   #(40*32)-1,d7          ; Clear the whole map
    move.w   #$0120,d0              ; Use tile $FF
    move.w   d0,REG_VRAMRW          ; Write to VRAM
    nop                             ; Wait a bit...
    dbra     d7,.clearfix           ; Are we done ? No: jump back to .clearfix

Keep in mind that the VRAM is accessed only by words, and that the fix map also holds the palette number for each tile. So we need to write $0120 (palette 0, tile $120).

The tile $120 in Super Sidekicks is filled with color 2.

Setting up colors

Now that the display is cleared, the colors need to be set up. We'll only use the first 3 color entries of palette 0. Color 0 is always transparent, color 1 is the text, color 2 is the background.

    move.w   #BLACK,PALETTES        ; Transparency, color 0 of palette 0 must always be black anyways
    move.w   #WHITE,PALETTES+2      ; Text color
    move.w   #BLUE,PALETTES+4       ; Background

Each color entry is a word, so +2 each time.

Note that palette access can be done at any time, but short glitches can appear if done during active display. It's a good idea to only access the palette RAM during blanking.

Writing the text

The text can finally be written to the fix map.

Like palette access, VRAM access doesn't have to be done in the v-blank interval. We don't have to wait for it.

    lea      Text,a0                ; Load the text's address in A0
    move.w   #FIXMAP+(15*32)+13,REG_VRAMADDR   ; Set the text position (address in fix map)
    move.w   #$0100,d0              ; Upper tiles are in S ROM bank 1
    move.w   #32,REG_VRAMMOD        ; Set the VRAM address auto-increment value
    move.b   (a0)+,d0               ; Load D0's lower byte
    tst.b    d0                     ; See if that was a 00 byte
    beq      .doneupper             ; It was: end of text, jump to .doneupper
    move.w   d0,REG_VRAMRW          ; Write to VRAM
    nop                             ; Wait a bit...
    bra      .writeupper            ; Jump back to .writeupper
When only one line of tiles is written.
  • move.w #FIXMAP+(10*32)+8,REG_VRAMADDR uses ASW's expression evaluator again to make things simpler. FIXMAP is the start of the fix map, (15*32) specifies the column since the fix map is 32 tiles high, and +13 specified the line. Don't forget that the fix map is "scanned" top to bottom, and left to right.
  • move.w #32,REG_VRAMMOD tells LSPC to increment the VRAM address by 32 each time we write to it. This allows us to write the text column by column.
  • move.w #$0100,d0 sets D0 to $0100. The upper byte is $01 (upper tiles) and won't change.
  • tst.b d0 checks if D0 is zero. If so, the end of the text is reached and the next BEQ instruction will jump out of the loop.

Note that there's only one NOP in the writing loop. Since there are more instructions between two writes, there's no need to wait that much now.

The top row of tiles is written, now let's do the bottom one:

    lea      Text,a0                ; Load the text's address in A0
    move.w   #FIXMAP+(15*32)+14,REG_VRAMADDR   ; Set the text position (address in fix map)
    move.w   #$0200,d0              ; Lower tiles are in S ROM bank 2
    move.w   #32,REG_VRAMMOD        ; Set the VRAM address auto-increment value
    move.b   (a0)+,d0               ; Load D0's lower byte
    tst.b    d0                     ; See if that was a 00 byte
    beq      .donelower             ; It was: end of text, jump to .doneupper
    move.w   d0,REG_VRAMRW          ; Write to VRAM
    nop                             ; Wait a bit...
    bra      .writelower            ; Jump back to .writeupper

What's the difference ? Pay attention to the initial VRAM address and the value of D0. We're now starting to write one line below (+14 instead of +13), and we're now using S ROM bank 2 (lower tiles).

Note that setting REG_VRAMMOD again isn't required. The value didn't change since last time it was set.

Also note that due to the similarity of both loops, they could be made as a single subroutine with the VRAM address and tile bank as parameters.

Note that the last 0 byte must be specified. ASW doesn't add it automatically after a quoted string.

Our program is almost complete, it just needs to halt in an infinite loop:

    bra .loop

Or else the program counter will continue out of space and bad things will happen.

And the text needs to be defined:

    dc.b "ARUZE SUXX !!", 0

Run it !

And there you go :)

Run make.bat, then runmvs.bat .

Have fun playing with values, colors, VRAM addresses, different font sets...