//////////////////////////////////////////////////////// //// NSF PLAYBACK CARTRIDGE GUIDE ////////////////////// //// by nullsleep ////////////////////////////////////// //// version 1.0 /////////// product of 8bitpeoples //// //////////////////////////// Research & Development //// //////////////////////////////////////////////////////// ******************************************************************************* ** OBJECTIVE ****************************************************************** ******************************************************************************* This document is aimed at providing all the information necessary to playback NSF files on an actual NES, rather than through the use of an emulator. First, the assembly code for a simple NSF player engine will be covered and explained. Following this, there will be an overview outlining the light hardware hacking aspect involved. Specifically, the modification of an existing NES cartridge in order to accomodate the NSF playback code. Thanks: Bananmos, Memblers, Andy Gunn, Cory Arcangel, Brad Taylor, Kevin Horton ******************************************************************************* ** SOFTWARE: NSF PLAYBACK CODE ************************************************ ******************************************************************************* The code below was written for x816 assembler, which can be found with a quick web search or simply by going to nesdev.parodius.com and downloading it there. If you are more comfortable using a different assembler, you should not have much trouble converting the code over to fit its requirements. This first group of settings is required by x816 assembler: ; *** X816 SETTINGS *** .mem 8 ; 8-bit memory mode .index 8 ; 8-bit index mode .opt on ; address optimize Next the code is initialized to start at the load address MINUS $80. The load address can be found in the NSF header at offset 0008 and is 2 bytes long. The reason why $80 is subtracted from the load address is because the NSF header itself is 128 (or $80 in hex) bytes long. If $80 was not subtracted the music data would actually be loaded at an address 128 bytes after the correct load address. So for example, if the load address specified at offset 0008 in the NSF header was 8080, then it would be appropriate to .org $8000 thereby putting the actual music data at $8080. Once the code is initialized to start at the correct address, simply include the NSF file. Note that since x816 is a DOS program, the filename for the NSF should be a maximum of 8 characters (not including the file extension). .org $---- ; replace dashes with load address MINUS $80 .incbin "chip.nsf" ; include NSF tune Initializing the NES is the next step, this portion of code defines what will happen when the NES is reset and is simply standard practice. The decimal flag is cleared (cld) and interrupts are disabled (sei). The next two instructions disable vblank interrupts, specifically, the most significant bit of $2000 is used to either enable(1) or disable(0) these interrupts. Address $2000 is the first of two PPU (picture processing unit) control registers, mostly used when dealing with graphics, however its being used here since vblank interrupts will be providingthe correct timing for our music playback. Reset_Routine: cld ; clear decimal flag sei ; disable interrupts lda #%00000000 ; disable vblank interrupts by clearing sta $2000 ; the most significant bit of $2000 The next few lines of code give the PPU (pixel processing unit) of the NES some time to initialize. Although this code segment may not be required when using an emulator, it is integral to getting the replay routine running correctly on the real hardware. Additionally, if graphics are to be displayed, waiting for vblank becomes even more important. This is because vblank is the period of time in between frames, and this is the only time during which graphics should be loaded to be displayed. Without this precaution, graphics on the screen may become corrupted. ; *** WAIT 2 VBLANKS *** WaitV1: lda $2002 ; give the PPU a little time to initialize bpl WaitV1 ; by waiting for a vblank WaitV2: lda $2002 ; wait for a second vblank to be safe bpl WaitV2 ; and now the PPU should be initialized The following chunk of code is responsible for clearing all the sound registers of the NES. Generally, this may not actually be necessary because it should already be taken care of by the code in the NSF itself. However, it cannot really do any harm, so look at it as good practice. Registers $4000-$4003 are associated with the first pulse wave channel, $4004-$4007 with the second pulse wave channel, $4008-$400B with the triangle wave channel, and $400C-$400F with the noise channel. The Clear_Sound loop below simply steps through each of these registers and sets them to $00. First the accumulator and x are loaded with $00, then the accumulator value ($00) is stored in address $4000 at the offset of the x value (currently $00), then inx increments x, and cpx #$0F compares the x value to the value $0F, finally the bne statement branches back to the Clear_Sound label if these values are not equal. Once $4000-$400F have been cleared program execution continues downward. The remaining registers from $4010 to $4013 are all associated with the delta modulation channel (DMC) and are initialized according to values found in the NSF specification by Kevin Horton. Check Brad Taylor's DMC documentation for details on what each of these registers control. ; *** CLEAR SOUND REGISTERS *** lda #$00 ; clear all the sound registers by setting ldx #$00 ; everything to 0 in the Clear_Sound loop Clear_Sound: sta $4000,x ; store accumulator at $4000 offset by x inx ; increment x cpx #$0F ; compare x to $0F bne Clear_Sound ; branch back to Clear_Sound if x != $0F lda #$10 ; load accumulator with $10 sta $4010 ; store accumulator in $4010 lda #$00 ; load accumulator with 0 sta $4011 ; clear these 3 registers that are sta $4012 ; associated with the delta modulation sta $4013 ; channel of the NES The following instructions are responsible for enabling the sound channels of the NES. Address $4015 acts as a channel enable register when being written to, with the bit 0 corresponding to the first square channel, bit 1 the next square channel, bit 2 the triangle wave channel, bit 3 the noise channel, and bit 4 is used to control DMC operation. The remaining bits 5 through 7 are unused (in terms of $4015 as a write register). So, setting the 4 least significant bits to 1, thereby enables all sound channels of the NES except the delta modulation channel. The DMC is disabled to avoid the possibility of sample playback before the song begins playing. ; *** ENABLE SOUND CHANNELS *** lda #%00001111 ; enable all sound channels except sta $4015 ; the delta modulation channel Anytime a write to $4017 occurs, the frame counter and clock divider are reset. The following lines of code are necessary for synchronizing the sound playback routine to the internal timing of the NES sound hardware. ; *** RESET FRAME COUNTER AND CLOCK DIVIDER *** lda #$C0 ; synchronize the sound playback routine sta $4017 ; to the internal timing of the NES The next three lines of code carry out some of the final steps of the playback setup. First, the accumulator is loaded with the song number to be played back. The numbering starts at 00, so for a single song NSF file, this first line should be set to lda #$00. Next set x to $00 for NTSC or $01 for PAL. Finally, jsr to the init address. This is found at offset 000A in the NSF header and is 2 bytes long. Keep in mind that because it is 2 bytes, when looking at it in a hex editor, the low byte will be followed by the high byte. So for example, if the 2 bytes at offset 000A in the hex editor are 16 8A, the 16-bit address that this represents for jsr to jump to is $8A16. ; *** SET SONG # & PAL/NTSC SETTING *** lda #$-- ; replace dashes with song number ldx #$-- ; replace with $00 for NTSC or $01 for PAL jsr $---- ; replace dashes with init address Next, vblank interrupts are enabled by setting the most significant bit of address $2000 and program execution drops into an infinite jmp loop. The rest of the program code will be located in the NMI routine. ; *** ENABLE VBLANK NMI *** lda #%10000000 ; enable vblank interrupts by setting the sta $2000 ; most significant bit of $2000 Loop: jmp Loop ; loop loop loop loop ... The NMI routine is where program execution jumps to whenever a non-maskable interrupt occurs. By enabling the vblank interrupts above, this routine will be called at either 60Hz (NTSC) or 50Hz (PAL), making it useful for maintaining the correct timing in playing back the NSF. First $2002 is read, this is the PPU status register, and reading it resets the vblank flag. Next $2000, the first PPU control register is cleared, and then the most significant bit of it is again set to enable vblank interrupts. Now the NSF playback is achieved by setting jsr to jump to the play address found at offset 000C in the NSF header. Remember that, like the load and init addresses, this is a 16-bit address and so, when looking at it in the hex editor the low byte will be followed by the high byte. Finding a value of 84 80 in the editor means jsr should point to $8084. Finally, rti returns from the interrupt routine. NMI_Routine: lda $2002 ; read $2002 to reset the vblank flag lda #%00000000 ; clear the first PPU control register sta $2000 ; writing 0 to it lda #%10000000 ; reenable vblank interrupts by setting sta $2000 ; the most significant bit of $2000 jsr $---- ; replace dashes with play address rti ; return from interrupt routine The remainder of the code is simply necessary for all NES programs. The IRQ routine contains a single statement, rti, because it is not necessary to have anything occur during an IRQ since we are using the NMI routine for playback. Finally, we pad the code out to location $FFFA and setup the interrupt vectors. IRQ_Routine: rti ; return from interrupt routine .pad $FFFA .dw NMI_Routine ; setup the NMI vector at $FFFA .dw Reset_Routine ; setup the Reset vector at $FFFC .dw IRQ_Routine ; setup the IRQ vector at $FFFE That finishes the software section of this NSF playback guide. While the amount of information above might be overwhelming if you have never done any assembly programming (or NES programming) before, it is really not very complex code at all. If you are having trouble understanding certain parts of it, grab some additional documents from nesdev.parodius.com and give it another read. I hope that this guide gives a nice start for some beginners but keep in mind that the real fun in NES development is the joy of discovering things for yourself. ******************************************************************************* ** HARDWARE: MODIFYING THE CARTRIDGE ****************************************** ******************************************************************************* The hardware modification aspect of creating the NSF playback cartridge is very straightforward. However, it does require a bit of equipment that the average person may not just have laying around. The required equipment is as follows: NES game cartridge (mapper 0) Screwdriver (may require special Nintendo screw bit) Small Cutters (for clipping the pins on the PRG ROM) Soldering Iron & Solder De-Soldering Braid (or other implement for De-Soldering) 28-pin EPROM socket (low-profile ZIF socket recommended) 28-pin EPROM (32k or larger recommended) 28-pin EPROM programmer EPROM eraser (you will probably want one of these too) First off, the NES cartridge you modify should be a mapper 0 cartridge. This is basically the simplest cartridge format, it contains no memory mapping hardware and is essentially just 2 ROM chips stuck to a board. The game cartridge that I would recommend is Super Mario Bros. (NOT the Super Mario Bros./Duck Hunt dual game cartridges) since it tends to be easy to find and available cheap. Some other possibilities are Arkanoid, Elevator Action, Excitebike, Ice Hockey, and Joust among others. The rest of the equipment on the list should be readily available from most electronics component suppliers. 1. Open the Cartridge Begin by opening up the game cartridge. Inside there will be a small circuit board, much smaller than the game casing itself. It should look something like what you see below: ________________________________________ | | |' __ '| |' | | |``````````````| |``````````````| | | | | | CHR ROM | | PRG ROM | | | | | |______________| |______________| | | '--' `````````````` `````````````` | |_ _| | | ||||||||||||||||||||||||||||||||||||||||||| 2. Remove the PRG ROM The CHR ROM on the left contains graphics data and the PRG ROM on the right contains all the program code. It is the PRG ROM that will need to be removed and replaced with an EPROM. Begin by clipping all the pins of the PRG ROM and removing the chip. Next use the de-soldering braid and the soldering iron to soak up the solder around the remaining lengths of pin sticking through the board. Once most of the solder around the pins has been removed, it should be possible to push/pull/knock out the pins so there are holes in the board that are clear and ready to receive the socket. 3. Solder the Socket Place the pins of the socket into the place left by the PRG ROM. Next, solder the socket into place. This is basically the extent of modification necessary to the cartridge. Mapper 0 cartridges do not require any pin reconfigurations, since the mapping for the removed ROM and replacement EPROM is identical. 4. Write Data to the EPROM The final step is to use the EPROM programmer to first blank check the EPROM and then write to it with the compiled program code. If everything was done properly, the EPROM can then be placed into the socket, the cartridge inserted into the NES, and the lovely sounds of the NSF will fill the air! - EOF ------------------------------------------------------------------------- ../love_always/nullsleep ../www.8bitpeoples.com ../www.nullsleep.com ../www.2A03.org If you find any errors in this document or have any suggestions or requests for additions, please contact me: nullsleep@8bitpeoples.com Related Links: http://nesdev.parodius.com ; a wealth of NES docs http://www.2A03.org ; NES scene music archive http://www.vorc.org ; relevant vgm & chiptune news http://research.8bitpeoples.com ; NES and other 8bit love