Programming the MSU1

Working draft, revision 2 — May 25, 2012, 17:00

What the MSU1 is

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 little history

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]

What the MSU1 can do

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.

Register reference

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.

Status port ($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.

Data read port ($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.

Identification ($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.

Data seek ports ($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.

Audio track ($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.

Audio volume ($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).

Audio state control ($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.

Setup

An MSU1-enabled SNES ROM requires the following things:

As a regular ROM

As a cartridge folder

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 Map file

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

Cookbook

Register definitions

	; 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

Checking for MSU1 availability

	; 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;
	}

Playing a track

	; 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;
	}

Seek to a specific point in the data track

	; 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 */ }
	}

Load a tile map

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

Load a color palette

	; 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

Loading some data

???

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?

Sources

  1. Kawa. (2010). On the MSU1. Retrieved from Logo Pending: http://helmet.kafuka.org/Pika/blog/?p=267
  2. Evan G. (2007). The 7th Guest. Retrieved from Snes Central: http://www.snescentral.com/article.php?id=0854
  3. byuu. (n.d.). MSU1. Retrieved from byuu’s homepage: http://byuu.org/msu1/
  4. byuu. (n.d.). MSU1 audio file format support. Retrieved September 5, 2010, from byuu’s message board: http://board.byuu.org/viewtopic.php?f=16&t=947
  5. byuu. (n.d.). Road Blaster (beta) released. Retrieved May 25, 2012, from byuu’s message board: http://board.byuu.org/viewtopic.php?p=65149#p65149