Working draft, revision 2 — May 25, 2012, 17:00
The MSU1 is an expansion chip, not entirely unlike the SuperFX, SDD-1, CX4 or the DSP series used in games such as Yoshi’s Island, Star Ocean, Mega Man X 2 and most games with scenes in Mode 7. Consequently, it is a part of the game cartridge. A common misconception is that supporting the MSU1 in hardware would require modifying the main SNES unit, and that it therefore does not “count”. Unfortunately for those who would make this mistake and stick to it, that would disqualify the expansion chips listed above as well. Perhaps the misunderstanding occurs because in emulation, it is the emulator itself that needs to be modified to support the MSU1 – just like it’s the emulator itself that brings support for the SuperFX et al.
A lot of people don’t seem to get the point of the MSU1 expansion chip byuu designed. A common complaint is that SNES games with CD-quality background music doesn’t go together at all.
On one side, there was the Sega Mega Drive, also known as the Sega Genesis. This was, like the SNES, a cartridge-based game console. The Mega CD peripheral added increased storage size and CD-Audio capabilities to it, keeping the rest of the system mostly the same. A similar thing happened with the TurboGrafx-16/PC-Engine, which also received a CD-ROM attachment, to pretty much the same effect.
Did you know that Sony was contracted to produce a CD-ROM attachment for the SNES, having previously supplied the system’s sound hardware? It didn’t work out. But consider this: if Sony didn’t try to claim complete control over all titles on the SNES CD, the SNES would have a larger storage size and CD-Audio capabilities – just like the Genesis and PC-Engine had.
MSU1 basically delivers what Sony was supposed to.
Paraphrased from the author’s blog, “Logo Pending”.[1]
The MSU1 expansion chip offers up to four gigabytes of data storage in a single stream, which can be used to store, for example, full motion video data. More popularly, it offers CD-quality music playback, leaving the SNES’ own SPC700 sound chip free to do better sound effects and jingles. Another neat trick is to store program data on it, copy this data into the SNES’ main RAM and execute it from there.
As a practical example, the MSU1 can be used to run full motion video games such as The 7th Guest[2], which was at one point supposed to be released on the SNES CDROM (specifically, the attempt that would later become the Philips CD-i system – Sony wasn’t the only player here).
It can not be used as RAM. That is, it can’t be used to store four gigabytes of saved games. It also can’t be used to play more than one audio track at a time – while playing background music on the MSU1, sound effects and jingles must remain on the SPC700. On the other hand it could be turned around, playing (sequenced) background music on the SPC700 and a voice track on the MSU1 during a cutscene. Or you could cross-fade between the background music on the MSU1 and another piece of music on the SPC700 as your RPG character approaches a performer on the street.
The MSU1 registers range from $2000
to $2007
. Their functions are different between reading and writing.[3][5]
Address | Reading | Writing |
---|---|---|
$2000 |
Status port (MSU_STATUS ) |
Data seek port (MSU_SEEK ) |
$2001 |
Data read port (MSU_READ ) |
|
$2002 |
Identification (MSU_ID ) |
|
$2003 |
||
$2004 |
Audio track (MSU_TRACK ) |
|
$2005 |
||
$2006 |
Audio volume (MSU_VOLUME ) |
|
$2007 |
Audio state control (MSU_CONTROL ) |
An explanation of all the registers and their dual functions is given below.
$2000
, MSU_STATUS
)The status port has the following bit layout:
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|
Data busy | Audio busy | Audio repeat | Audio playing | Track missing | Revision |
The revision part will be equal to 1, unless a new version of the MSU1 specification is released, which may add more features. A common usage for the status port is to lock the SNES CPU after setting an audio track or specifying a seek target on the MSU1 Data file. This is a rather important step to include if you’re aiming to support the MSU1 in hardware. The “track missing” bit is set if, after seeking to a given audio track, that track is found to be unavailable. This allows selectively falling back to SPC700 sound.
$2001
, MSU_READ
)Reading from the data read port returns the next byte of data from the MSU1 Data file and increments the stream port’s address automatically. Reads have no effect if the “data busy” bit is set on the status port.
$2002
to $2007
, MSU_ID
)
Reading from $2002
to $2007
in order returns the string “S-MSU1
” in ASCII. This allows the program to detect availability and as such offer compatibility fallbacks.
$2000
to $2003
, MSU_SEEK
)
These four registers combine to one 32-bit address, with $2000
being the least significant byte, up to $2003
. Writing to $2003
will trigger the seek, setting the “data busy” bit on the status port until done.
$2004
and $2005
, MSU_TRACK
)
These two registers combine to one 16-bit address, with $2004
being the least significant byte. Writing to $2005
will trigger the track change, setting the “audio busy” bit on the status port until done. The currently playing track is stopped.
$2006
, MSU_VOLUME
)
Sets the audio playback volume on a linear scale. A value of 0
is 0% (completely silent), 127
is 50% and 255
is 100% (full volume).
$2007
, MSU_CONTROL
)The audio state port has the following bit layout:
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|
Unused | Repeat | Playing |
Writing to the audio state port has no effect if the “audio busy” bit is set on the status port. If the play bit is not set, the music is effectively paused. The upper six bits are reserved for future use – they must be set to zero to ensure proper operation.
An MSU1-enabled SNES ROM requires the following things:
gamename.sfc
gamename.msu
gamename-#.pcm
files with the audio tracksgamename.xml
program.rom
msu1.rom
track-#.pcm
filesmanifest.xml
The audio tracks are 44.1 kilohertz, 16-bit stereo uncompressed unsigned PCM files in little-endian order, left channel first, with a simple eight-byte header. The first four bytes spell out “MSU1
” in ASCII. This is followed by a 32-bit unsigned integer used as the loop point, measured in samples (a sample being four bytes) – if the repeat bit is set in the audio state register, this value is used to determine where to seek the audio track to.[4]
Creation of audio tracks can be done easily by using the wav2msu tool.
<?xml version="1.0" encoding="UTF-8"?> <cartridge region="NTSC"> <!-- This part depends on the ROM --> <rom> <map mode="linear" address="00-7f:8000-ffff"/> <map mode="linear" address="80-ff:8000-ffff"/> </rom> <ram size="0x2000"> <map mode="linear" address="20-3f:6000-7fff"/> <map mode="linear" address="a0-bf:6000-7fff"/> <map mode="linear" address="70-7f:0000-ffff"/> <map mode="linear" address="f0-ff:0000-ffff"/> </ram> <!-- This is the important bit --> <msu1> <map address="00-3f:2000-2007"/> <map address="80-bf:2000-2007"/> </msu1> </cartridge>
; WLA-DX .define MSU_STATUS $2000 .define MSU_READ $2001 .define MSU_ID $2002 .define MSU_SEEK $2000 .define MSU_TRACK $2004 .define MSU_VOLUME $2006 .define MSU_CONTROL $2007 ; xkas MSU_STATUS = $2000 MSU_READ = $2001 MSU_ID = $2002 MSU_SEEK = $2000 MSU_TRACK = $2004 MSU_VOLUME = $2006 MSU_CONTROL = $2007 //SNESSDK #define MSU1SEEK *(unsigned long*)0x2000 #define MSU1TRACK *(unsigned short*)0x2004 #define MSU1VOLUME *(unsigned char*)0x2006 #define MSU1PLAY *(unsigned char*)0x2007 #define MSU1STATUS *(unsigned char*)0x2000 #define MSU1DATA *(unsigned char*)0x2001 //A.N.: I don't even remember why I did it this way. #define MSU1IDENT0 *(char*)0x2002 #define MSU1IDENT1 *(char*)0x2003 #define MSU1IDENT2 *(char*)0x2004 #define MSU1IDENT3 *(char*)0x2005 #define MSU1IDENT4 *(char*)0x2006 #define MSU1IDENT5 *(char*)0x2007
; WLA-DX CheckForMSU: lda MSU_ID cmp #$53 ; 'S' bne NoMSU ; Stop checking if it's wrong lda MSU_ID+1 cmp #$2D ; '-' bne NoMSU lda MSU_ID+2 cmp #$4D ; 'M' bne NoMSU lda MSU_ID+3 cmp #$53 ; 'S' bne NoMSU lda MSU_ID+4 cmp #$55 ; 'U' bne NoMSU lda MSU_ID+5 cmp #$31 ; '1' beq NoMSU MSUFound: ; Do something with this fact here. rts NoMSU: ; Do something with this fact here. rts //SNESSDK int msu1available() { if( MSU1IDENT0 == 'S' && MSU1IDENT1 == '-' && MSU1IDENT2 == 'M' && MSU1IDENT3 == 'S' && MSU1IDENT4 == 'U' && MSU1IDENT5 == '1' ) return 1; return 0; }
; WLA-DX lda #$FF sta MSU_VOLUME ldx #$0001 ; Writing a 16-bit value will automatically stx MSU_TRACK ; set $2005 as well, so this is easy. lda #$01 ; Set audio state to play, no repeat. sta MSU_CONTROL ; The MSU1 will now start playing. ; Use lda #$03 to play a song repeatedly. //SNESSDK void msu1play(unsigned int track) { MSU1TRACK = track; /* TODO: edit SMO-ckery to load a message from somewhere not 0x0000 * and as such have a proper busy loop. Confirm if the below would * do. */ /* POSSIBILITY: for version 2 systems, let this function return bool. * If so, switch the results around and have true mean "it worked"? */ //while(MSU1STATUS) //{ /* busy loop */ } MSU1PLAY = 1; }
; WLA-DX ; Seeks to $0000:0410, just as an example. ldx #$0410 stx MSU_SEEK ldx #$0000 stx MSU_SEEK+2 - bit MSU_STATUS ; Wait for the Data Busy bit to clear. bmi - ; We can use "Branch if Minus" for cheap because the Data Busy ; bit happens to be the same as the sign bit in a Two's ; Complement number: Two's complement on Wikipedia //SNESSDK void msu1seek(unsigned long offset) { MSU1SEEK = offset; //while(MSU1STATUS) //{ /* busy loop */ } }
; Loads a tile map of $800 bytes in size to offset $4000. ; Assumes the data stream is already at the correct seek position. ldx #$4000 ; Set starting point stx VMADDL ldy #$0000 ; Initialize counter - lda MSU_DATA ; Grab tile byte sta VMDATAL iny lda MSU_DATA ; Grab attribute byte sta VMDATAH iny cpy #$0800 bne - ; Loading character data is basically the same, but without ; tile/attribute distinction. Note that the first few bits of ; the attribute byte are more tile indices, but this would only ; complicate the descriptions.
; WLA-DX ; Loads a single color palette strip (16 colors) to the second palette index. ; Assumes the data stream is already at the correct seek position. lda #$10 ; Set starting point - color #16 sta CGADD ldy #$0000 ; Initialize counter - lda MSU_DATA sta CGADD iny cpy #$20 ; Colors are 16-bit values, so we must copy 32 bytes, not 16. bne - //SNESSDK //TODO
???
Include code for a simple video player? I’m guessing it’d be like… load a frame count, then continuously load bitmap and palette data into the appropriate parts of VRAM until you’re out of frames?
More fun times: a SeekToID
routine that, given a table of pointers at the start of the data file, seeks to a specific entry?
On the MSU1. Retrieved from Logo Pending: http://helmet.kafuka.org/Pika/blog/?p=267
The 7th Guest. Retrieved from Snes Central: http://www.snescentral.com/article.php?id=0854
MSU1.Retrieved from byuu’s homepage: http://byuu.org/msu1/
MSU1 audio file format support.Retrieved September 5, 2010, from byuu’s message board: http://board.byuu.org/viewtopic.php?f=16&t=947
Road Blaster (beta) released.Retrieved May 25, 2012, from byuu’s message board: http://board.byuu.org/viewtopic.php?p=65149#p65149