Hello world tutorial: Difference between revisions
mNo edit summary |
mNo edit summary |
||
(12 intermediate revisions by 2 users not shown) | |||
Line 1: | Line 1: | ||
[[File:Tut teaser.png|thumb|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. | |||
*Download the latest MAME | 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]]. | ||
**052-c1. | |||
**052-m1. | =Setting up a minimal development environment= | ||
**052-p1. | |||
**052-s1. | * 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. | * 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. | ||
[[File:Tut ssideki.png|thumb|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: | |||
<pre> | <pre> | ||
mame | mame -debug -nofilter -window ssideki | ||
pause | pause | ||
</pre> | </pre> | ||
[[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 | * 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, | * 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: | ||
<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. | c:\neogeo\asw\flip main.bin 052-p1.p1 | ||
c:\neogeo\asw\pad 052-p1. | c:\neogeo\asw\pad 052-p1.p1 524288 255 | ||
copy 052-p1. | copy 052-p1.p1 c:\mame\roms\ssideki\052-p1.p1 | ||
pause | |||
</pre> | </pre> | ||
And save it as "make.bat" in the " | 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 only the P ROM is replaced | 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 | * 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: | ||
<pre> | <pre> | ||
cpu 68000 | cpu 68000 | ||
Line 51: | Line 59: | ||
INCLUDE "regdefs.asm" | INCLUDE "regdefs.asm" | ||
</pre> | </pre> | ||
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 | 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 [[ | 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, | 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. | ||
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. | |||
== | ==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: | |||
<syntaxhighlight> | <syntaxhighlight> | ||
move.l #(40*32)-1, | ORG $300 | ||
move.w #FIXMAP,REG_VRAMADDR | 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 | move.w d0,REG_VRAMRW ; Write to VRAM | ||
nop ; Wait a bit... | |||
dbra | nop | ||
dbra d7,.clearfix ; Are we done ? No: jump back to .clearfix | |||
</syntaxhighlight> | </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). | |||
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 $ | |||
Now that the | 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. | |||
<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 | 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 . | |||
Have fun playing with values, colors, VRAM addresses, different font sets... | |||
[[Category:Code]] | [[Category:Code]] |
Latest revision as of 17:08, 31 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.
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.
- 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):
- 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 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:
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:
- 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 !
Run make.bat, then runmvs.bat .
Have fun playing with values, colors, VRAM addresses, different font sets...