Difference between revisions of "Hello world tutorial"

From NeoGeo Development Wiki
Jump to: navigation, search
m
m
 
(14 intermediate revisions by 3 users not shown)
Line 1: Line 1:
This tutorial will guide you through all the necessary steps to make a NeoGeo binary which displays the text "Hello world !". The language we will be using is 68k asm, but good knowledge of it is not needed.
+
[[File:Tut teaser.png|thumb|The result.]]
  
=Setting up a minimalist development environment=
+
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.
  
*Download the latest MAME emulator binaries from [[http://mamedev.org/release.html this page]]. Extract the zip contents to a folder such as "C:/mame/".
+
The programming language used is [[68k]] assembly. Good knowledge of it is not needed as all instructions will be detailed.
*You will need the NeoGeo MVS [[BIOSes|BIOS]] files to run NeoGeo ROMs. Find it with google and make sure you have the '''000-lo.lo''' (64KiB), '''sfix.sfix''' (128KiB), '''sm1.sm1''' (128KiB), '''sp-s2.sp1''' (128KiB) and '''sfix.sfix''' (128KiB) files in the "c:/mame/roms/neogeo/" folder.
+
 
*Use google again to find and download a '''Super Sidekicks''' romset (ssideki.zip), extract it to "c:/mame/roms/ssideki/". Make sure that the following files are present:
+
The source files can be downloaded here: [[File:Helloworld.zip]].
**052-c1.bin and 052-c2.bin ([[C ROM]] pair, sprite graphics)
+
 
**052-m1.bin (Z80 code, we won't mess with that)
+
=Setting up a minimal development environment=
**052-p1.bin (68k code, that's what we'll make)
+
 
**052-s1.bin ([[S ROM]]: fix graphics)
+
* 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/".
**052-v1.bin ([[V ROM]]: sound)
+
* 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.
If they are named incorrectly, please renamed them as MAME wants the files to be named exactly that way.
+
[[File:Tut ssideki.png|thumb|All Super Sidekicks ROM files.]]
*You will need a batch file to start MAME with the right parameters. Create a new text file and add these lines:
+
* 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:
  
 
<pre>
 
<pre>
mame ssideki -debug -nofilter -video d3d -waitvsync -window
+
mame -debug -nofilter -window ssideki
 
pause
 
pause
 
</pre>
 
</pre>
  
Run this batch file, 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. If the romset is bad, the missing and/or invalid files will be shown in the DOS prompt (bad checksum, missing files...). When everything works, close the debugger and the emulation window.
+
[[File:Tut mame.png|thumb|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 [[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 source 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/".
*You will need to add a batch file in this folder to run ASW to assemble your source file. Make a new text file with the following contents:
+
* 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:
  
 
<pre>
 
<pre>
 
@echo off
 
@echo off
c:\neogeo\asw\asw main -L -quiet
+
c:\neogeo\asw\bin\asw main -L -quiet
c:\neogeo\asw\p2bin main -r $000000-$01FFFF
+
c:\neogeo\asw\bin\p2bin main -r $000000-$01FFFF
c:\neogeo\asw\flip main.bin 052-p1.bin
+
c:\neogeo\asw\flip main.bin 052-p1.p1
c:\neogeo\asw\pad 052-p1.bin 524288 255
+
c:\neogeo\asw\pad 052-p1.p1 524288 255
copy 052-p1.bin c:\mame\roms\ssideki\052-p1.bin
+
copy 052-p1.p1 c:\mame\roms\ssideki\052-p1.p1
 +
pause
 
</pre>
 
</pre>
  
And save it as "make.bat" in the "c:/neogeo/helloworld/" folder.
+
And save it as "make.bat" in the "C:/neogeo/helloworld/" folder. Don't run it for now.
  
'''Asw''' assembles your code, '''p2bin''' makes the [[P ROM]] with the right size (128KiB), '''flip.exe''' inverts bytes by 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 ROM size) and it's then copied in place of the original Super Sidekicks 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 original P ROM in MAME's directory.
  
Be aware that only the P ROM is replaced, all graphics, musics and sounds are still the Super Sidekicks ones.
+
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 44: Line 52:
 
=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 pretty meaningless and hard-to-remember addresses. 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.
+
* 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.
*Start a new text file and add the following:
+
* Create a new text file and add the following lines:
 
<pre>
 
<pre>
 
     cpu 68000
 
     cpu 68000
Line 51: Line 59:
 
     INCLUDE "regdefs.asm"
 
     INCLUDE "regdefs.asm"
 
</pre>
 
</pre>
This indicates ASW what CPU we're writing code for ([[68k|Motorola 68000]]), that the code will be run in '''supervisor''' mode (allows us to use special instructions), and to include our previous "regdefs.asm" file. Save this file as "main.asm".
+
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==
 +
 
 +
For the "cartridge" to be recognized by the system ROM and launched, header data must be present. Copy the [[68k_program_header|minimal header]] to a new "header.asm" file.
 +
 
 +
Add the following line to main.asm:
 +
 
 +
<pre>
 +
    INCLUDE "header.asm"
 +
</pre>
 +
 
 +
This will also set up the [[68k interrupts|v-blank interrupt]] vector to a label called '''VBLANK'''.
  
 
==A bit of thinking==
 
==A bit of thinking==
  
To display the "Hello world !" text, there are basically two options: using sprites (one sprite for each letter, for example), or using the [[fix layer]]. To avoid having the need to edit the sprite ROMs and create an alphabet, we will use the alphabet that is already stored in the fix ROM (052-s1.bin).
+
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)
  
If you look at the contents of a typical S ROM (like on [[SFIX|this page]]), you will see that the first bank ($100) of tiles contains a 8x8 pixels alphabet, which is often customized for the game. The Super Sidekicks one uses color gradients, so we will use the 8x16 pixels alphabet instead, which is simpler (2 colors), and which is found in the next two banks (tiles $100 to $2FF).
+
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).
  
As shown in the SFIX tileset reference, the bank 1 (tiles $100 to $1FF) contains the upper tiles (the top of the letters), and the bank 2 (tiles $200 to $2FF) contains the lower tiles (the bottom of the letters).
+
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).
  
 
By mapping those tiles correctly on the '''fix map''', text can be displayed.
 
By mapping those tiles correctly on the '''fix map''', text can be displayed.
  
Know that there is a special BIOS call ([[MESS_OUT]]) which purpose is to simplify text writing to the fix. For the purpose of this tutorial, we'll do it the hard way, without using the BIOS.
+
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 number to the fix map, we need to make sure it's empty.
+
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 the tile $FF (which is filled with color 1).
 
A loop is used to fill in the 40*32 map:
 
  
<pre>
+
In main.asm, add the following lines:
move.l  #(40*32)-1,d0
+
 
move.w  #FIXMAP,REG_VRAMADDR
+
<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    SYS_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 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.
 +
 
 +
==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 a jump (see header.asm). 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    SYS_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.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
 +
    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
</pre>
+
    dbra    d7,.clearfix          ; Are we done ? No: jump back to .clearfix  
 +
</syntaxhighlight>
 +
 
 +
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.
  
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.
+
<syntaxhighlight>
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 $20).
+
    move.w  #BLACK,PALETTES        ; Transparency, color 0 of palette 0 must always be black anyways
The byte write to REG_DIPSW kicks the [[watchdog]], this has to be done regularly otherwise the Neogeo '''will reset'''.
+
    move.w  #WHITE,PALETTES+2      ; Text color
 +
    move.w  #BLUE,PALETTES+4      ; Background
 +
</syntaxhighlight>
  
Now that the fix is cleared, the palette needs to be set up. We'll use 2 colors of the palette zero.
+
Each color entry is a word, so +2 each time.
The first color is the background, the second is the text.
 
  
<pre>
+
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.
move.w  #BLACK,PALETTES
 
move.w  #WHITE,PALETTES+2
 
</pre>
 
  
 
==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.
 +
 
 +
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+(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
 +
    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>
 +
 
 +
[[File:Tut_oneline.png|thumb|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:
 +
 
 +
<syntaxhighlight>
 +
    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
 +
    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 (+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:
 +
 
 +
<syntaxhighlight>
 +
.loop:
 +
    bra .loop
 +
</syntaxhighlight>
 +
 
 +
Or else the program counter will continue out of space and bad things will happen.
 +
 
 +
And the text needs to be defined:
 +
 
 +
<syntaxhighlight>
 +
Text:
 +
    dc.b "ARUZE SUXX !!", 0
 +
</syntaxhighlight>
 +
 
 +
=Run it !=
 +
 
 +
[[File:Tut_final.png|thumb|And there you go :)]]
 +
 
 +
Run make.bat, then runmvs.bat .
  
*To be continued...
+
Have fun playing with values, colors, VRAM addresses, different font sets...
  
 
[[Category:Code]]
 
[[Category:Code]]

Latest revision as of 17:08, 31 May 2017

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

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

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.

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

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 a jump (see header.asm). 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     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:

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

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
    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:
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
    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 (+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:

.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:

Text:
    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...