Hello world tutorial: Difference between revisions

From NeoGeo Development Wiki
Jump to navigation Jump to search
mNo edit summary
No edit summary
Line 1: Line 1:
This tutorial will guide you through all the necessary steps to make a NeoGeo romset which displays the text "Hello world !". It is written for Windows but all the necessary tools have equivalents on MacOS and Linux.
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, but good knowledge of it is not needed as all instructions will be detailed.
The programming language used is [[68k]] assembly. Good knowledge of it is not needed as all instructions will be detailed.


=Setting up a minimal development environment=
=Setting up a minimal development environment=


* Download the latest [[emulators|MAME]] binaries from [[http://mamedev.org/release.html mamedev.org]]. Extract the zip contents to a folder such as "C:/MAME/".
* Download the latest [[emulators|MAME]] binaries from [[http://mamedev.org/release.html 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 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]]). They will have to be copied to the "C:/MAME/roms/neogeo/" folder for them to be recognized by MAME. Like original games these ROMs are copyrighted, so you won't find them here.
* 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.
* 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 make sure they are named correctly (MAME is picky about file names and extensions):
* 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-c1.c1 and 052-c2.c2 ([[C ROM]] pair, sprite graphics)
** 052-m1.m1 (Z80 code, we won't mess with that)
** 052-m1.m1 (Z80 code, we won't mess with that)
Line 20: Line 20:
</pre>
</pre>


Save it with the .bat extension 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 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 (bad checksum, missing files...). When the game runs correctly, close the debugger and the emulation window.
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 [[http://john.ccac.rwth-aachen.de:8000/ftp/as/precompiled/i386-unknown-win32/aswcurr.zip the macro assembler AS (ASW)]] and [[Flip_pad.zip|this zip file]]. There is no installation required, just extract those files to a folder like "C:/neogeo/asw/".
* Download [[http://john.ccac.rwth-aachen.de:8000/ftp/as/precompiled/i386-unknown-win32/aswcurr.zip 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/".
* 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. Create a new text file with the following contents:
* Write a batch file in this folder to make ASW assemble your code and generator the [[P ROM]]. Create a new text file with the following contents:


<pre>
<pre>
Line 37: Line 37:
And save it as "make.bat" in the "C:/neogeo/helloworld/" folder.
And save it as "make.bat" in the "C:/neogeo/helloworld/" folder.


'''ASW''' assembles your code, '''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" (255) bytes up to 512KiB to match the original ssideki P ROM size and make MAME happy, and it's finally copied in place of the original Super Sidekicks P ROM.
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 origina P ROM in MAME's directory.


Be aware that only the P ROM file is replaced, all graphics, musics and sounds will still be those from Super Sidekicks.
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.
Now that you have everything to assemble and run the ROM, you can start coding.
Line 45: Line 45:
=The code=
=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 <pre>$300001</pre> in your code, you'll be able to write <pre>REG_DIPSW</pre> instead and ASW will take care of translating that to $300001.
* 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:
* Create a new text file and add the following lines:
<pre>
<pre>
Line 52: Line 52:
     INCLUDE "regdefs.asm"
     INCLUDE "regdefs.asm"
</pre>
</pre>
These aren't assembly instruction but directives that only ASW will understand. '''cpu 68000''' indicates what CPU we're writing code for ([[68k|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/".
These aren't assembly instructions but directives that only ASW will understand. '''cpu 68000''' indicates what CPU we're writing code for ([[68k|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/".


==Formalities==
==Formalities==
Line 66: Line 66:
* Use the [[fix layer]] (one tile per letter)
* 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 one that is already stored in the ssideki's fix ROM (052-s1.bin).
To avoid the need to edit the sprite ROMs and draw a font, we will use the fix font that is already stored in the ssideki's S ROM (052-s1.bin).


If you look at the contents of a typical S ROM (like on [[SFIX_ROM|this page]]), you will see that the first bank ($100) of tiles contains a 8x8 pixels character set, which is often customized for each game. The Super Sidekicks one uses 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).
If you look at the contents of a typical S ROM (like on [[SFIX_ROM|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).
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).
Line 76: Line 76:
Note that there is a [[Category:BIOS_calls|system ROM call]] 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.
Note that there is a [[Category:BIOS_calls|system ROM call]] 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.


==Cleaning everything up==
==V-blank handler==


Before writing tile numbers to the fix map, we want to make sure it's cleared.
First of all, we need to define a v-blank interrupt handler. The header file indicates that a label called VBLANK must be used.
For this, we will fill it with tile $FF (which is filled with color 1).
 
A loop is used to fill in the 40*32 fix map:
In main.asm, add the following lines:
 
<syntaxhighlight>
    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    BIOSF_SYSTEM_INT1      ; Yes: jump to system ROM
.getvbl:
    move.w  #4,REG_IRQACK          ; Acknowledge v-blank interrupt
    move.b  d0,REG_DIPSW            ; Kick watchdog
    rte                            ; Return from interrupt
</syntaxhighlight>
 
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 BIOS_SYSTEM_MODE.
* '''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 MOVEM instruction). We're only reading D0 in our case, so we don't need to preserve anything.
 
==Initialization==
 
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 another jump:


<syntaxhighlight>
<syntaxhighlight>
move.l  #(40*32)-1,d0
    jmp USER
move.w  #FIXMAP,REG_VRAMADDR
</syntaxhighlight>
 
So we need to define the [[USER subroutine]].
 
In main.asm, add the following lines:
 
<syntaxhighlight>
JT_USER:
    dc.l    StartupInit            ; Jump table for the different things the system ROM can ask for
    dc.l    EyeCatcher
    dc.l    Game
    dc.l    Title
 
USER:
    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    BIOSF_SYSTEM_RETURN    ; Tell the system ROM that we're done
</syntaxhighlight>
 
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:
 
<syntaxhighlight>
StartupInit:
EyeCatcher:
Title:
    rts
</syntaxhighlight>
 
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.
 
<syntaxhighlight>
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
</syntaxhighlight>
 
'''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.
 
<syntaxhighlight>
    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
.clear_ram:
    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
</syntaxhighlight>
 
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.
 
<syntaxhighlight>
    move.l  #512-1,d7              ; Clear all 512 sprites
    move.w  #SCB3,REG_VRAMADDR    ; Height attributes are in VRAM at Sprite Control Bank 3
    clr.w    d0
    nop
.clearspr:
    move.w  d0,REG_VRAMRW          ; Write to VRAM
    nop                            ; Wait a bit...
    nop
    dbra    d7,.clearspr          ; Are we done ? No: jump back to .clearspr
</syntaxhighlight>
 
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:
 
<syntaxhighlight>
    move.l  #(40*32)-1,d7          ; Clear the whole map
    move.w  #FIXMAP,REG_VRAMADDR
    move.w  #$0120,d0              ; Use tile $FF
.clearfix:
.clearfix:
move.w  #$00FF,REG_VRAMRW
    move.w  d0,REG_VRAMRW         ; Write to VRAM
move.b  d0,REG_DIPSW
    nop                            ; Wait a bit...
dbra    d0,.clearfix
    nop
    dbra    d7,.clearfix          ; Are we done ? No: jump back to .clearfix  
</syntaxhighlight>
</syntaxhighlight>


Register D0 is used as the loop counter, which will be decremented each iteration. The count must be 1 less because DBRA branches again when D0=0.
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).
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 $00FF (palette 0, tile $0FF).
 
The byte write to REG_DIPSW kicks the [[watchdog]], this has to be done regularly otherwise the NeoGeo '''will reset'''.
The tile $120 in Super Sidekicks is filled with color 2.


Now that the fix is cleared, the palette needs to be set up. We'll use the first 2 colors of palette 0.
==Setting up colors==
The first color is the background, the second is the text.
 
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.


<syntaxhighlight>
<syntaxhighlight>
move.w  #BLACK,PALETTES
move.w  #BLACK,PALETTES           ; Transparency, color 0 of palette 0 must always be black anyways
move.w  #WHITE,PALETTES+2
move.w  #WHITE,PALETTES+2         ; Text color
move.w  #BLUE,PALETTES+4          ; Background
</syntaxhighlight>
</syntaxhighlight>
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==
==Writing the text==


The text can now be mapped to the fix map.
The text can finally be written to the fix map. Let's define the text:
 
<syntaxhighlight>
Text:
    dc.b "ARUZE SUXX !!", 0
</syntaxhighlight>
 
Note that the last 0 byte '''must''' be specified. ASW doesn't add it automatically after a quoted string.
 
Like palette access, VRAM access doesn't have to be done in the v-blank interval. We don't have to wait for it.
 
<syntaxhighlight>
    lea      Text,a0                ; Load the text's address in A0
    move.w  #FIXMAP+(10*32)+8,REG_VRAMADDR  ; Set the text position (address in fix map)
    move.w  #$0100,d0              ; Upper tiles are in S ROM bank 1
    nop
    move.w  #32,REG_VRAMMOD        ; Set the VRAM address auto-increment value
.writeupper:
    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
.doneupper:
</syntaxhighlight>
 
* '''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, (10*32) specifies the column since the fix map is 32 tiles high, and +8 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:
 
<syntaxhighlight>
    lea      Text,a0                ; Load the text's address in A0
    move.w  #FIXMAP+(10*32)+9,REG_VRAMADDR  ; Set the text position (address in fix map)
    move.w  #$0200,d0              ; Lower tiles are in S ROM bank 2
    nop
    move.w  #32,REG_VRAMMOD        ; Set the VRAM address auto-increment value
.writelower:
    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
.donelower:
</syntaxhighlight>
 
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 (+9 instead of +8), 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.
 
Our program is now complete, it just needs to halt in an infinite loop:
 
<syntaxhighlight>
.loop:
    bra .loop
</syntaxhighlight>
 
Or else the program counter will continue out of space and bad things will happen.
 
=Run it !=


*To be continued...
Run make.bat, then runmvs.bat and there you go :)


[[Category:Code]]
[[Category:Code]]

Revision as of 12:32, 28 May 2017

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.

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.
  • 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
pause

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 generator the P ROM. Create a new text file with the following contents:
@echo off
c:\neogeo\asw\asw main -L -quiet
c:\neogeo\asw\p2bin main -r $000000-$01FFFF
c:\neogeo\asw\flip main.bin 052-p1.bin
c:\neogeo\asw\pad 052-p1.bin 524288 255
copy 052-p1.bin c:\mame\roms\ssideki\052-p1.bin

And save it as "make.bat" in the "C:/neogeo/helloworld/" folder.

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 origina 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/".

Formalities

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.

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 the 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     BIOSF_SYSTEM_INT1       ; Yes: jump to system ROM
.getvbl:
    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 BIOS_SYSTEM_MODE.
  • 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 MOVEM instruction). We're only reading D0 in our case, so we don't need to preserve anything.

Initialization

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 another jump:

    jmp USER

So we need to define the USER subroutine.

In main.asm, add the following lines:

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

USER:
    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     BIOSF_SYSTEM_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:

StartupInit:
EyeCatcher:
Title:
    rts

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
.clear_ram:
    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.l   #512-1,d7              ; Clear all 512 sprites
    move.w   #SCB3,REG_VRAMADDR     ; Height attributes are in VRAM at Sprite Control Bank 3
    clr.w    d0
    nop
.clearspr:
    move.w   d0,REG_VRAMRW          ; Write to VRAM
    nop                             ; Wait a bit...
    nop
    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   #FIXMAP,REG_VRAMADDR
    move.w   #$0120,d0              ; Use tile $FF
.clearfix:
    move.w   d0,REG_VRAMRW          ; Write to VRAM
    nop                             ; Wait a bit...
    nop
    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. Let's define the text:

Text:
    dc.b "ARUZE SUXX !!", 0

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

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+(10*32)+8,REG_VRAMADDR   ; Set the text position (address in fix map)
    move.w   #$0100,d0              ; Upper tiles are in S ROM bank 1
    nop
    move.w   #32,REG_VRAMMOD        ; Set the VRAM address auto-increment value
.writeupper:
    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
.doneupper:
  • 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, (10*32) specifies the column since the fix map is 32 tiles high, and +8 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+(10*32)+9,REG_VRAMADDR   ; Set the text position (address in fix map)
    move.w   #$0200,d0              ; Lower tiles are in S ROM bank 2
    nop
    move.w   #32,REG_VRAMMOD        ; Set the VRAM address auto-increment value
.writelower:
    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
.donelower:

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 (+9 instead of +8), 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.

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

.loop:
    bra .loop

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

Run it !

Run make.bat, then runmvs.bat and there you go :)