S-SMP: Difference between revisions

From SNESdev Wiki
Jump to navigationJump to search
m (Rainwarrior moved page SPC700 to SPC-700: hyphen is easier to read)
(→‎See Also: Add link to S-DSP_registers page)
 
(35 intermediate revisions by 3 users not shown)
Line 1: Line 1:
The SPC700 is the CPU portion of the S-SMP processor used to run the sound and music of the SNES. It is an 8-bit CPU with capabilities similar to the 6502.
The '''SHVC-SOUND''' module of the SNES includes several components:<ref>Super Nintendo (NTSC) Revision 2 Motherboard schematic by Jonathon W. Donaldson</ref>
 
* '''S-SMP''': Sony sub CPU
** Sony SPC-700 processor, clocked at 1.024MHz (see [[SPC-700 instruction set]])
** 3 timers, two running at 8kHz and one at 64kHz
** [[MMIO registers#APUIOn|APUIO]] and [[S-SMP#CPUIOx|CPUIOx]] registers for communication between the 65816 S-CPU and the SPC700 code
** [[#IPL Boot ROM|IPL Boot ROM]]
* '''S-DSP''': Sony sound chip (see [[S-DSP registers]])
** Digital signal processor that generates the sound data
** 8 voice channels
** Decodes [[BRR samples]] at a variable sample rate using 4-point Gaussian interpolation
** FIR filtered echo buffer
** Manages the clock of the S-DSP, S-SMP and DAC
*** Clocked by a 24.576MHz ceramic resonator
*** Generates 1 stereo sample every 768 resonator cycles
*** Internally clocked at 3.072MHz
** Bus Arbiter for the Audio-RAM.  The Audio-RAM is time-shared between the S-SMP and S-DSP, with 1 S-SMP memory access for every 2nd S-DSP memory access.
* '''Audio-RAM''': 64KiB RAM from two 32K x 8bit PSRAM chips. (Also known as ARAM.)
* µPD6376 16-bit stereo DAC
** Digital to analog converter, outputting the analog audio signal from the DSP's stereo 16-bit 32000 Hz digital audio
* Stereo Amplifier
** Mixes DAC output with the [[Cartridge connector]] analog audio input and the [[EXT connector]] analog audio input
** The amplifier can be disabled by the S-SMP's /MUTE pin (bit 6 of the [[S-DSP_registers#FLG]] S-DSP register)
** The output of the stereo amplifier is mixed into a mono output for the RF modulator and EXT connector
 
 
The SPC-700 is an 8-bit CPU that is almost like an extended MOS-6502.
 
The SPC-700 driven by a 1.024 MHz clock, though the SPC-700 instruction timing is very similar to a 6502 in that each instruction takes at least 2 cycles. The exact clock rate is independent from the rest of the SNES, and may drift slightly with temperature. The nominal 32000 Hz sample rate is actually 32 clocks per sample.
 
Related reference:
* [[SPC-700 instruction set]]
* [[S-DSP registers]]
* [[DSP envelopes]]
* [[BRR samples]]
 
The term SPC is also used to describe a [[SPC file format]] (.SPC) for storing SNES music. The S-DSP is not to be confused with the [[DSP-1]] cartridge expansion hardware. The term S-SMP is often contrasted with [[S-CPU]] when describing communication with the SNES main CPU.


== Memory Layout ==
== Memory Layout ==
Line 22: Line 58:


== Registers ==
== Registers ==
Aside from the SPC-700 CPU registers (see: [[SPC-700 instruction set]]), there are a collection of memory-mapped registers in the last 16 bytes of the zero-page.


{| class="wikitable"
{| class="wikitable"
! Name !! Address !! Bits !! Type !! Notes
|+ S-SMP register summary
|-
|-
! Test !! $F0
! colspan=2 | Name
| style="text-align: right" | <tt style="white-space: nowrap">.I.E TRWH</tt>
! Address
| W8
! Bits
| Undocumented test register.
! Type
! Notes
|-
|-
! Control !! $F1
{{:APU register table/SMP}}
| style="text-align: right" | <tt style="white-space: nowrap">I.CC .210</tt>
| W8
| Enable IPL ROM (I), Clear data ports (C), timer enable (2,1,0).
|-
|-
! Register Address !! $F2
! colspan=6 | <small>([[APU register table/SMP|table source]])</small>
| style="text-align: right" | <tt style="white-space: nowrap">.AAA AAAA</tt>
| RW8
| Selects a DSP register address.
|-
! Register Data !! $F3
| style="text-align: right" | <tt style="white-space: nowrap">VVVV VVVV</tt>
| RW8
| Reads or writes data to the selected DSP address.
|-
! Port 0 !! $F4
| style="text-align: right" | <tt style="white-space: nowrap">VVVV VVVV</tt>
| RW8
| Reads or writes data to [[MMIO registers#APUIO0|APUIO0]].
|-
! Port 1 !! $F5
| style="text-align: right" | <tt style="white-space: nowrap">VVVV VVVV</tt>
| RW8
| Reads or writes data to [[MMIO registers#APUIO1|APUIO1]].
|-
! Port 2 !! $F6
| style="text-align: right" | <tt style="white-space: nowrap">VVVV VVVV</tt>
| RW8
| Reads or writes data to [[MMIO registers#APUIO2|APUIO2]].
|-
! Port 3 !! $F7
| style="text-align: right" | <tt style="white-space: nowrap">VVVV VVVV</tt>
| RW8
| Reads or writes data to [[MMIO registers#APUIO3|APUIO3]].
|-
! --- !! $F8
| style="text-align: right" | <tt style="white-space: nowrap">.... ....</tt>
| RW8
| Unused (normal RAM).
|-
! --- !! $F9
| style="text-align: right" | <tt style="white-space: nowrap">.... ....</tt>
| RW8
| Unused (normal RAM).
|-
! Timer 0 !! $FA
| style="text-align: right" | <tt style="white-space: nowrap">TTTT TTTT</tt>
| W8
| 8KHz timer 0 interval.
|-
! Timer 1 !! $FB
| style="text-align: right" | <tt style="white-space: nowrap">TTTT TTTT</tt>
| W8
| 8KHz timer 1 interval.
|-
! Timer 2 !! $FC
| style="text-align: right" | <tt style="white-space: nowrap">TTTT TTTT</tt>
| W8
| 64KHz timer 2 interval.
|-
! Counter 0 !! $FD
| style="text-align: right" | <tt style="white-space: nowrap">.... CCCC</tt>
| R8
| Timer 0 count-up.
|-
! Counter 1 !! $FE
| style="text-align: right" | <tt style="white-space: nowrap">.... CCCC</tt>
| R8
| Timer 1 count-up.
|-
! Counter 2 !! $FF
| style="text-align: right" | <tt style="white-space: nowrap">.... CCCC</tt>
| R8
| Timer 2 count-up.
|}
|}


Write-only registers will read back as $00.
Write-only registers will read back as $00.


===$F0 Test===
This undocumented register responds to writes only when the P flag is clear.


{{Anchor|TEST}}
=== TEST - Undocumented test register ($F0, write-only) ===
  7  bit  0
  7  bit  0
  .I.E TRWH
  ---- ----
IIEE TRWH
|||| ||||
|||| |||+- Halt timers
|||| ||+-- RAM writable
|||| |+--- Disable RAM reads
|||| +---- Enable timers
||++------ External wait state
++-------- Internal wait state
On power-on: TEST = $0A


* I (bit 6) - Internal wait state.
* E (bit 4) - External wait state.
* T (bit 3) - Timers enable.
* R (bit 2) - Ram disable.
* W (bit 1) - Ram writable.
* H (bit 0) - Timers disable.


===$F1 Control===
'''DO NOT write to this register!!!'''
* Setting any of the wait state bits will slow the clock speed of the SPC-700 processor.
* Changing the wait state bits can cause crashes on real hardware.<ref>ares source code, <tt>[https://github.com/ares-emulator/ares/tree/master/ares/ares/sfc/smp/timing.cpp ares/sfc/smp/timing.cpp]</tt>, by Near</ref>
* The <tt>TEST</tt> register can disable Audio-RAM reads and/or writes.
* The <tt>TEST</tt> register can disable the timers.


This provides a way for the SPC-700 to reset the read ports ($F4-F7) without the SNES CPU having to write them externally. It also starts and stops the 3 timers.


7  bit  0
This undocumented register responds to writes only when the P flag is clear.
I.CC .210


* 0 (bit 0) - Enables timer 0 when set. A transition from clear to set resets the internal interval counter to 0.
* 1 (bit 1) - Enables timer 1.
* 2 (bit 2) - Enables timer 2.
* C (bit 4) - If set will reset the value the SPC will read from ports 0 and 1 ($F4, $F5) to $00.
* C (bit 5) - If set will reset ports 2 and 3 ($F6, $F7).
* I (bit 7) - Will enable the IPL ROM if set.


At reset this register is initialized as if $80 was written to it.
{{Anchor|CONTROL}}


The function of bit 7 enabling the IPL ROM is not documented in the [[SNES Development Book]].
=== CONTROL - Control register ($F1, write-only) ===
bit 0
---- ----
I.CC .210
| ||  |||
| ||  ||+- Enable timer 0
| ||  |+-- Enable timer 1
| ||  +--- Enable timer 2
| |+------ Clear CPUIO read ports 0 & 1
| +------- Clear CPUIO read ports 2 & 3
+--------- IPL ROM enable
On power-on: CONTROL = $B0
On reset:    CONTROL = $B0


===$F2-F3 DSP===
Write $F2 to select a DSP register, then a value can be read or written to that DSP register via $F3.


* Writing $F2 with the high bit set will select a DSP register according to the lower 7 bits, but it will be read-only.
This provides a way for the SPC-700 to reset the read ports ($F4-F7) without the SNES CPU having to write them externally. It also starts and stops the 3 timers.
* The high bit of $F2 will always read back as 0.


===$F4-F7 Port 0-3===
* 0/1/2 (bits 0-2) - Enables each of the 3 timers.
These 4 ports allow communication with the SNES CPU. There are 8 stored values, each is a one-way communication written from one side, and readable only from the other side. Each port therefore has two separate one-way values, each seen from only either the SNES CPU or the SPC-700.
** A transition from clear to set (0 -> 1) will reset the timer's internal counter and <tt>TxOUT</tt> to 0.
* C (bit 4) - If set the <tt>CPUIO0</tt> and <tt>CPUIO1</tt> Data-from-CPU read registers ($F4, $F5) are reset to $00.
** <tt>CPUIOx</tt> Data-from-CPU is cleared on any <tt>CONTROL</tt> write with a clear bit set.
** <tt>CPUIOx</tt> is only cleared on <tt>CONTROL</tt> writes.  Future APUIO writes to the APU will not be changed.
** The 65816 S-CPU <tt>[[MMIO registers#APUIOn|APUIOn]]</tt> Data-from-APU registers will not be changed.
* C (bit 5) - If set the <tt>CPUIO2</tt> and <tt>CPUIO3</tt> Data-from-CPU read registers ($F6, $F7) are reset to $00.
* I (bit 7) - Enables the IPL ROM if set.
** This does not disable Audio-RAM writes to the IPL ROM memory addresses.


If a port is read on the same cycle it is written, an incorrect value will result. For this reason, common practice is to read a port in a loop until the value changes, and then read it once more to ensure the correct value is read. (A single port can be used this way to indicate that a message is ready, and the other 3 ports could be safely read only once with the assumption that the other CPU will not write to them once the ready indication was given.)


At reset these registers are initialized to $00.
At reset this register is initialized as if $80 was written to it.


===$FA-FC Timer 0-2===
The function of bit 7 enabling the IPL ROM is not documented in the [[SNES Development Manual]].
When enabled via $F1, the 3 timers will internally count at a rate of 8 KHz (timers 0,1) or 64 KHz (timer 2), and when this interval value has been exceeded, they will increment their external counter result ($FD-FF) and begin again.


===$FD-FF Counter 0-2===
The 4-bit result of the three timers counts up every time the interval is reached.


Reading these registers resets each counter to 0 immediately after the read. The upper 4 bits will always read as 0.
{{Anchor|DSPADDR}}


== DSP Registers ==
=== DSPADDR - DSP register address ($F2) ===
7  bit  0
---- ----
RAAA AAAA
|||| ||||
|+++-++++- S-DSP register address
+--------- Read only flag


A DSP register can be selected with '''$F2''', after which it can be read or written at '''$F3'''. Often it is useful to load the register address into A, and the value to send in Y, so that '''MOV $F2, YA''' can be used to do both in one 16-bit instruction.
Write $F2 to select a DSP register, then a value can be read or written to that DSP register via $F3.


The DSP register address space only has 7 bits. The high bit of '''$F2''', if set, will make the selected register read-only via '''$F3'''.
* Writing $F2 with the high bit set will select a DSP register according to the lower 7 bits, but it will be read-only.
* The high bit of $F2 will always read back as 0.


When initializing the DSP registers for the first time, take care not to accidentally enable echo writeback via '''[[#FLG|FLG]]''', because it will immediately begin overwriting values in RAM.
See: [[S-DSP_registers#Reading_and_writing_S-DSP_registers|Reading and writing S-DSP registers]] and [[S-DSP registers]]


=== Voices ===


There are 8 voices, numbered 0 to 7. Each voice X has 10 registers in the range $X0-$X9.
{{Anchor|DSPDATA}}
=== DSPDATA - DSP register data ($F3) ===
7  bit  0
---- ----
DDDD DDDD
|||| ||||
++++-++++- S-DSP register data
On write: if DSPADDR bit 7 clear: write to S-DSP address DSPADDR
On read: read from S-DSP register address DSPADDR


{| class="wikitable"
A DSP register can be selected with <tt>DSPADDR</tt> ($F2), after which it can be read or written at <tt>DSPDATA</tt> ($F3). Often it is useful to load the register address into A, and the value to send in Y, so that '''MOV $F2, YA''' can be used to do both in one 16-bit instruction.
! Name
! Address
! Bits
! Notes
|-
! VOL (L)
! $X0
| style="text-align: right" | <tt style="white-space: nowrap">SVVV VVVV</tt>
| Left channel volume, signed.
|-
! VOL (R)
! $X1
| style="text-align: right" | <tt style="white-space: nowrap">SVVV VVVV</tt>
| Right channel volume, signed.
|-
! P (L)
! $X2
| style="text-align: right" | <tt style="white-space: nowrap">LLLL LLLL</tt>
| Low 8 bits of sample pitch.
|-
! P (H)
! $X3
| style="text-align: right" | <tt style="white-space: nowrap">--HH HHHH</tt>
| High 6 bits of sample pitch.
|-
! SCRN
! $X4
| style="text-align: right" | <tt style="white-space: nowrap">SSSS SSSS</tt>
| Selects a sample source entry from the directory (see DIR below).
|-
! ADSR (1)
! $X5
| style="text-align: right" | <tt style="white-space: nowrap">EDDD AAAA</tt>
| ADSR enable (E), decay rate (D), attack rate (A).
|-
! ADSR (2)
! $X6
| style="text-align: right" | <tt style="white-space: nowrap">LLLR RRRR</tt>
| Sustain level (L), sustain rate (R).
|-
! GAIN
! $X7
| style="text-align: right" | <tt style="white-space: nowrap">0VVV VVVV</tt><br/> <tt style="white-space: nowrap">1MMV VVVV</tt>
| Mode (M), value (V).
|-
! ENVX
! $X8
| style="text-align: right" | <tt style="white-space: nowrap">0VVV VVVV</tt>
| Reads current 7-bit value of ADSR/GAIN envelope.
|-
! OUTX
! $X9
| style="text-align: right" | <tt style="white-space: nowrap">SVVV VVVV</tt>
| Reads signed 8-bit value of current sample wave multiplied by ENVX, before applying VOL.
|}


==== P ====
The DSP register address space only has 7 bits. The high bit of <tt>DSPADDR</tt> ($F2), if set, will make the selected register read-only via <tt>DSPDATA</tt> ($F3).


Sample pitch is a 14-bit value controlling the rate the BRR sound sample will be played back.
When initializing the DSP registers for the first time, take care not to accidentally enable echo writeback via <tt>[[S-DSP_registers#FLG|FLG]]</tt>, because it will immediately begin overwriting values in RAM.


Rate: '''P x 32,000 Hz / $1000'''
See: [[S-DSP_registers#Reading_and_writing_S-DSP_registers|Reading and writing S-DSP registers]] and [[S-DSP registers]]


A pitch of $1000 will play back the sample at the SNES native samplerate of 32,000 Hz.


The pitch can go as high as $3FFF, almost two octaves above $1000. Pitches above $1000 will be subject to some aliasing from samples that are skipped over.
{{Anchor|CPUIO}}


The pitch can go all the way down to 0, where it is halted. Pitches below $1000 will be lacking in higher frequencies, and there is not very much precision as the pitch value approaches 0.
=== CPUIOx - APU-to-Data register x ($F4 - $F7, write) ===
7  bit  0
---- ----
DDDD DDDD
|||| ||||
++++-++++- Data to CPU


==== SCRN ====


This points to an entry in the sample source directory (DIR).
=== CPUIOx - Data-from-CPU register x ($F4 - $F7, read) ===
Changing this will not immediately change the voice's sample without a key on (KON), but if a looping sample is playing it can be used to change the loop point without a key on.
7  bit  0
---- ----
DDDD DDDD
|||| ||||
++++-++++- Data from CPU


==== ADSR ====
These 4 ports allow communication with the SNES CPU. There are 8 stored values, each is a one-way communication written from one side, and readable only from the other side. Each port therefore has two separate one-way values, each seen from only either the SNES CPU or the SPC-700.


This controls an Attack-Decay-Sustain-Release envelope that automatically adjusts the sample's envelope volume (ENVX) over time.
If a port is read on the same cycle it is written, an incorrect value will result. For this reason, common practice is to read a port in a loop until the value changes, and then read it once more to ensure the correct value is read. (A single port can be used this way to indicate that a message is ready, and the other 3 ports could be safely read only once with the assumption that the other CPU will not write to them once the ready indication was given.)


* '''E''': 1 to enable ADSR envelope, otherwise 0 uses GAIN instead.
At reset these registers are initialized to $00.
* '''A''': Attack speed. $F for instant.
* '''D''': Decay speed, time to decay from full volume to the sustain level after the initial attack.
* '''L''': Sustain level.
* '''R''': Sustain release, speed of decay to 0 after note off.


==== GAIN ====


This register has 5 modes:
{{Anchor|TxTARGET}}
=== TxTARGET - Timer x target ($FA - $FC, write-only) ===
7  bit  0
---- ----
TTTT TTTT
|||| ||||
++++-++++- Timer target
On power-on: TxTARGET = 0


* <tt>0VVV VVVV</tt> sets ENVX directly.
When enabled via $F1, the 3 timers will internally count at a rate of 8 KHz (timers 0,1) or 64 KHz (timer 2), and when this interval value has been exceeded, they will increment their external counter result ($FD-FF) and begin again.
* <tt>110V VVVV</tt> Linear slide up to 100% volume with rate V.
* <tt>111V VVVV</tt> Bent-line (fast to 75%, then slower to 100%) slide up with rate V.
* <tt>100V VVVV</tt> Linear slide down to 0% volume with rate V.
* <tt>101V VVVV</tt> Exponential slide down to 0% volume with rate V.


=== Global ===


Other DSP registers apply globally, rather than to a specific voice.
{{Anchor|TxOUT}}
=== TxOUT - Timer x output ($FD - $FF, read-only) ===
7  bit  0
---- ----
0000 CCCC
      ||||
      ++++- Timer counter
On power-on: TxOUT = 0
On reset:    TxOUT = 0
On read:    TxOUT = 0


{| class="wikitable"
The 4-bit result of the three timers counts up every time the interval is reached.
! Name
! Address
! Bits
! Notes
|-
! MVOL (L)
! $0C
| style="text-align: right" | <tt style="white-space: nowrap">SVVV VVVV</tt>
| Left channel main volume, signed.
|-
! MVOL (R)
! $1C
| style="text-align: right" | <tt style="white-space: nowrap">SVVV VVVV</tt>
| Right channel main volume, signed.
|-
! EVOL (L)
! $2C
| style="text-align: right" | <tt style="white-space: nowrap">SVVV VVVV</tt>
| Left channel echo volume, signed.
|-
! EVOL (R)
! $3C
| style="text-align: right" | <tt style="white-space: nowrap">SVVV VVVV</tt>
| Right channel main volume, signed.
|-
! KON
! $4C
| style="text-align: right" | <tt style="white-space: nowrap">7654 3210</tt>
| Key on. Writing this with any bit set will start a new note for the corresponding voice.
|-
! KOF
! $5C
| style="text-align: right" | <tt style="white-space: nowrap">7654 3210</tt>
| Key off. Writing this with any bit set will put the corresponding voice into its release state.
|-
! FLG
! $6C
| style="text-align: right" | <tt style="white-space: nowrap">RMEN NNNN</tt>
| Flags: soft reset (R), mute all (M), echo disable (E), noise frequency (N).
|-
! ENDX
! $7C
| style="text-align: right" | <tt style="white-space: nowrap">7654 3210</tt>
| Read for end of sample flag for each channel.
|-
! EFB
! $0D
| style="text-align: right" | <tt style="white-space: nowrap">SVVV VVVV</tt>
| Echo feedback, signed.
|-
! -
! $1D
| style="text-align: right" | <tt style="white-space: nowrap">---- ----</tt>
| Unused.
|-
! PMON
! $2D
| style="text-align: right" | <tt style="white-space: nowrap">7654 321-</tt>
| Enables pitch modulation for each channel, controlled by OUTX of the next lower channel.
|-
! NON
! $3D
| style="text-align: right" | <tt style="white-space: nowrap">7654 3210</tt>
| For each channel, replaces the sample waveform with the noise generator output.
|-
! EON
! $4D
| style="text-align: right" | <tt style="white-space: nowrap">7654 3210</tt>
| For each channel, sends to the echo unit.
|-
! DIR
! $5D
| style="text-align: right" | <tt style="white-space: nowrap">DDDD DDDD</tt>
| Pointer to the sample source directory page at $DD00.
|-
! ESA
! $6D
| style="text-align: right" | <tt style="white-space: nowrap">EEEE EEEE</tt>
| Pointer to the start of the echo memory region at $EE00.
|-
! EDL
! $7D
| style="text-align: right" | <tt style="white-space: nowrap">---- DDDD</tt>
| Echo delay time (D).
|-
! C0
! $0F
| style="text-align: right" | <tt style="white-space: nowrap">SVVV VVVV</tt>
| Echo filter coefficient.
|-
! C1
! $1F
| style="text-align: right" | <tt style="white-space: nowrap">SVVV VVVV</tt>
| Echo filter coefficient.
|-
! C2
! $2F
| style="text-align: right" | <tt style="white-space: nowrap">SVVV VVVV</tt>
| Echo filter coefficient.
|-
! C3
! $3F
| style="text-align: right" | <tt style="white-space: nowrap">SVVV VVVV</tt>
| Echo filter coefficient.
|-
! C4
! $4F
| style="text-align: right" | <tt style="white-space: nowrap">SVVV VVVV</tt>
| Echo filter coefficient.
|-
! C5
! $5F
| style="text-align: right" | <tt style="white-space: nowrap">SVVV VVVV</tt>
| Echo filter coefficient.
|-
! C6
! $6F
| style="text-align: right" | <tt style="white-space: nowrap">SVVV VVVV</tt>
| Echo filter coefficient.
|-
! C7
! $7F
| style="text-align: right" | <tt style="white-space: nowrap">SVVV VVVV</tt>
| Echo filter coefficient.
|}


==== KON ====
Reading these registers resets each counter to 0 immediately after the read. The upper 4 bits will always read as 0.


This immediately starts the sample from its beginning, as designated by SCRN. If ADSR is enabled, its envelope will also start from the beginning.
==== FLG ====
* '''R''': soft reset prevents KON and mutes all voices.
* '''M''': mutes all voices.
* '''E''': set to 1 to disable echo, if 0 the echo unit will actively overwrite memory in the echo region (ESA). Do not write 0 to this bit unless ESA/EDL have been prepared.
* '''N''': sets the noise generator frequency.
At reset the high 3 bits of '''FLG''' are set.
==== ENDX ====
Each bit will read as set once a voice has finished playing a BRR sample block with the Source End flag set.
Writing any value to this register will reset all the bits to 0, otherwise they remain set until the voice receives a new note on (KON).
== Instruction Set ==
'''See: [[SPC-700 Instruction Set]]'''


== IPL Boot ROM ==
== IPL Boot ROM ==
Line 462: Line 274:
It is most common to place the data program at $0200, just above the stack page, and start execution from there, but this is not a requirement.
It is most common to place the data program at $0200, just above the stack page, and start execution from there, but this is not a requirement.


== BRR Samples ==
IPL loading takes about 520 master clocks per byte transferred. This allows about 650 bytes in a 60hz frame, if the CPU is dedicated to this activity.
 
Sound samples are stored in the [https://en.wikipedia.org/wiki/Bit_Rate_Reduction BRR] (bit-rate-reduction) data format.
 
BRR is composed of 16-sample blocks, each of which is stored in 9 bytes of data. This is a 1 byte control block, followed by 8 bytes containing 16 4-bit samples to be decoded (high nibble first).
 
{| class="wikitable"
! Offset !! Bits !! Notes
|-
! 0
| style="text-align: right" | <tt style="white-space: nowrap">SSSS FFLE</tt>
| Left-shift (S), decoding filter (F), loop (L), end (E).
|-
! 1
| style="text-align: right" | <tt style="white-space: nowrap">AAAA BBBB</tt>
| Samples 0 (A) and 1 (B).
|-
! 2
| style="text-align: right" | <tt style="white-space: nowrap">CCCC DDDD</tt>
| Samples 2 (C) and 3 (D).
|-
! 3
| style="text-align: right" | <tt style="white-space: nowrap">EEEE FFFF</tt>
| Samples 4 (E) and 5 (F).
|-
! 4
| style="text-align: right" | <tt style="white-space: nowrap">GGGG HHHH</tt>
| Samples 6 (G) and 7 (H).
|-
! 5
| style="text-align: right" | <tt style="white-space: nowrap">IIII JJJJ</tt>
| Samples 8 (I) and 9 (J).
|-
! 6
| style="text-align: right" | <tt style="white-space: nowrap">KKKK LLLL</tt>
| Samples 10 (K) and 11 (L).
|-
! 7
| style="text-align: right" | <tt style="white-space: nowrap">MMMM NNNN</tt>
| Samples 12 (M) and 13 (N).
|-
! 8
| style="text-align: right" | <tt style="white-space: nowrap">OOOO PPPP</tt>
| Samples 14 (O) and 15 (P).
|}
 
When a block with the end (E) flag set finishes, the channel will automatically stop unless the loop (L) flag is also set for this block. If the loop flag is set, the loop point will be read based on the channel's current '''SCRN''' and sample playback will continue from there. Because of the block structure, loop points must lie on 16-sample boundaries. The loop flag has no effect on blocks without the end flag set.
 
The left-shift (S) value specifies the overall magnitude of values. This is simply a left shift applied to each sample nibble before BRR filter decoding.
 
Each sample nibble is a signed 4-bit value in the range of -8 to +7. After being shifted, one of four BRR decoding filters (F) is applied, which may include the last 0-2 decoded samples in the result. Filter 0 will just use the shifted nibble directly.
 
Sound samples should normally start with a filter-0 block, to prevent leftover BRR samples from the previously playing sample from having an effect on decoding.
 
=== Filters ===
 
The four BRR filters form an [https://en.wikipedia.org/wiki/Adaptive_differential_pulse-code_modulation adaptive PCM] (ADPCM) encoding scheme, using the previous few samples to predict the next one with a low amount of input data. By choosing the best fitting filter (i.e. adapting) for each block of 16 samples, the result can more closely match a desired waveform.
 
Each filter adds the shifted nibble to two previously decoded samples, each multiplied by a coefficient:
 
{|class="wikitable"
! Filter !! Shifted Nibble !! Sample -1 !! Sample -2
|-
! 0
| 1 || 0 || 0
|-
! 1
| 1 || 15/16 || 0
|-
! 2
| 1 || 61/32 || 15/16
|-
! 3
| 1 || 115/64 || 13/16
|}
 
Filter 0 is the non-filter, which just uses the shifted nibble result directly. The previous samples are not used. Sound samples should normally begin with a filter 0 block to prevent leftover results from the previous sound affecting the new one.
 
Filter 1 includes some of the previously encoded sample.
 
Filters 2 and 3 include some of two previously encoded samples.
 
As the filter number increases, the high frequency bandwidth (sharpness) of the 4-bit sample encoding is traded for better low frequency (smoothness) encoding. Because most natural sounds have stronger low frequencies and weaker high frequencies, this allows the bandwidth allocation of each block to be adapted to better suit the frequency spectrum of the sound at that moment.
 
=== Gaussian Interpolation ===
 
Finally, after the BRR sample data is decoded, when a sample is played back, because the rate (P) is adjustable, an filter is used to smoothly interpolate values between the decoded samples. This is a gaussian interpolator which blends a combination of a neighbourhood of 4 adjacent samples according to a lookup table. This reduces high frequency aliasing across a range of playback rates.
 
Playback begins with the interpolator centred on the second sample. If your key-on does not begin with an ADSR attack envelope, you may wish to have three 0 samples at the beginning of your sound sample to ensure it begins at 0 and smoothly moves from there (avoiding clicks/pops).
 
Because of a bug in the DSP's gaussian interpolation filter, 3 maximum-negative values in a row can cause an overflow of the resulting sample, creating a loud pop. This can be intentionally exploited, but normally it is advised avoid this.


== See Also ==
== See Also ==


* [[SPC-700 instruction set]]
* [[S-DSP registers]]
* [[DSP envelopes]]
* [[BRR samples]]
* [[Booting the SPC700]] - Guide for setting up the SPC program after reset.
* [[Booting the SPC700]] - Guide for setting up the SPC program after reset.
* [[Tools]] - Lists tools for building SPC700 code.
* [[Tools]] - Lists tools for building SPC700 code.
Line 564: Line 290:
* [http://snesmusic.org/files/spc700_documentation.html SPC 700 Documentation] - Article by Gau.
* [http://snesmusic.org/files/spc700_documentation.html SPC 700 Documentation] - Article by Gau.
* [https://wiki.superfamicom.org/spc700-reference SPC700 Reference] - Superfamicom.org wiki article.
* [https://wiki.superfamicom.org/spc700-reference SPC700 Reference] - Superfamicom.org wiki article.
* [https://wiki.superfamicom.org/bit-rate-reduction-(brr) Bit Rate Reduction] - Superfamicom.org wiki article, documents BRR sample format.
* [http://problemkaputt.de/fullsnes.htm#snesapudspbrrsamples SNES APU DSP BRR Samples] - Fullsnes documentation of BRR sample format.
* [https://wiki.superfamicom.org/how-to-write-to-dsp-registers-without-any-spc-700-code How to Write to DSP Registers Without any SPC-700 Code] - Superfamicom.org wiki, demonstrating that the DSP can be written directly through the IPL.
* [https://wiki.superfamicom.org/how-to-write-to-dsp-registers-without-any-spc-700-code How to Write to DSP Registers Without any SPC-700 Code] - Superfamicom.org wiki, demonstrating that the DSP can be written directly through the IPL.
[[Category:Sound]]

Latest revision as of 22:20, 15 November 2024

The SHVC-SOUND module of the SNES includes several components:[1]

  • S-SMP: Sony sub CPU
  • S-DSP: Sony sound chip (see S-DSP registers)
    • Digital signal processor that generates the sound data
    • 8 voice channels
    • Decodes BRR samples at a variable sample rate using 4-point Gaussian interpolation
    • FIR filtered echo buffer
    • Manages the clock of the S-DSP, S-SMP and DAC
      • Clocked by a 24.576MHz ceramic resonator
      • Generates 1 stereo sample every 768 resonator cycles
      • Internally clocked at 3.072MHz
    • Bus Arbiter for the Audio-RAM. The Audio-RAM is time-shared between the S-SMP and S-DSP, with 1 S-SMP memory access for every 2nd S-DSP memory access.
  • Audio-RAM: 64KiB RAM from two 32K x 8bit PSRAM chips. (Also known as ARAM.)
  • µPD6376 16-bit stereo DAC
    • Digital to analog converter, outputting the analog audio signal from the DSP's stereo 16-bit 32000 Hz digital audio
  • Stereo Amplifier
    • Mixes DAC output with the Cartridge connector analog audio input and the EXT connector analog audio input
    • The amplifier can be disabled by the S-SMP's /MUTE pin (bit 6 of the S-DSP_registers#FLG S-DSP register)
    • The output of the stereo amplifier is mixed into a mono output for the RF modulator and EXT connector


The SPC-700 is an 8-bit CPU that is almost like an extended MOS-6502.

The SPC-700 driven by a 1.024 MHz clock, though the SPC-700 instruction timing is very similar to a 6502 in that each instruction takes at least 2 cycles. The exact clock rate is independent from the rest of the SNES, and may drift slightly with temperature. The nominal 32000 Hz sample rate is actually 32 clocks per sample.

Related reference:

The term SPC is also used to describe a SPC file format (.SPC) for storing SNES music. The S-DSP is not to be confused with the DSP-1 cartridge expansion hardware. The term S-SMP is often contrasted with S-CPU when describing communication with the SNES main CPU.

Memory Layout

64 kilobytes of RAM are mapped across the 16-bit memory space of the SPC-700. Some regions of this space are overlaid with special hardware functions.

Range Note
$0000-00EF Zero Page RAM
$00F0-00FF Sound CPU Registers
$0100-01FF Stack Page RAM
$0200-FFBF RAM
$FFC0-FFFF IPL ROM or RAM

The region at $FFC0-FFFF will normally read from the 64-byte IPL ROM, but the underlying RAM can always be written to, and the high bit of the Control register $F1 can be cleared to unmap the IPL ROM and allow read access to this RAM.

Registers

Aside from the SPC-700 CPU registers (see: SPC-700 instruction set), there are a collection of memory-mapped registers in the last 16 bytes of the zero-page.

S-SMP register summary
Name Address Bits Type Notes
TEST Test $F0 IIEE TRWH W8 Undocumented test register.
CONTROL Control $F1 I.CC .210 W8 Enable IPL ROM (I), Clear data ports (C), timer enable (2,1,0).
DSPADDR Register Address $F2 RAAA AAAA RW8 Selects a DSP register address.
DSPDATA Register Data $F3 DDDD DDDD RW8 Reads or writes data to the selected DSP address.
CPUIO0 Port 0 $F4 DDDD DDDD RW8 Reads or writes data to APUIO0.
CPUIO1 Port 1 $F5 DDDD DDDD RW8 Reads or writes data to APUIO1.
CPUIO2 Port 2 $F6 DDDD DDDD RW8 Reads or writes data to APUIO2.
CPUIO3 Port 3 $F7 DDDD DDDD RW8 Reads or writes data to APUIO3.
--- $F8 .... .... RW8 Unused (normal RAM).
--- $F9 .... .... RW8 Unused (normal RAM).
T0TARGET Timer 0 $FA TTTT TTTT W8 8KHz timer 0 interval.
T1TARGET Timer 1 $FB TTTT TTTT W8 8KHz timer 1 interval.
T2TARGET Timer 2 $FC TTTT TTTT W8 64KHz timer 2 interval.
T0OUT Counter 0 $FD 0000 CCCC R8 Timer 0 count-up.
T1OUT Counter 1 $FE 0000 CCCC R8 Timer 1 count-up.
T2OUT Counter 2 $FF 0000 CCCC R8 Timer 2 count-up.
(table source)

Write-only registers will read back as $00.


TEST - Undocumented test register ($F0, write-only)

7  bit  0
---- ----
IIEE TRWH
|||| ||||
|||| |||+- Halt timers
|||| ||+-- RAM writable
|||| |+--- Disable RAM reads
|||| +---- Enable timers
||++------ External wait state
++-------- Internal wait state

On power-on: TEST = $0A


DO NOT write to this register!!!

  • Setting any of the wait state bits will slow the clock speed of the SPC-700 processor.
  • Changing the wait state bits can cause crashes on real hardware.[2]
  • The TEST register can disable Audio-RAM reads and/or writes.
  • The TEST register can disable the timers.


This undocumented register responds to writes only when the P flag is clear.


CONTROL - Control register ($F1, write-only)

7  bit  0
---- ----
I.CC .210
| ||  |||
| ||  ||+- Enable timer 0
| ||  |+-- Enable timer 1
| ||  +--- Enable timer 2
| |+------ Clear CPUIO read ports 0 & 1
| +------- Clear CPUIO read ports 2 & 3
+--------- IPL ROM enable

On power-on: CONTROL = $B0
On reset:    CONTROL = $B0


This provides a way for the SPC-700 to reset the read ports ($F4-F7) without the SNES CPU having to write them externally. It also starts and stops the 3 timers.

  • 0/1/2 (bits 0-2) - Enables each of the 3 timers.
    • A transition from clear to set (0 -> 1) will reset the timer's internal counter and TxOUT to 0.
  • C (bit 4) - If set the CPUIO0 and CPUIO1 Data-from-CPU read registers ($F4, $F5) are reset to $00.
    • CPUIOx Data-from-CPU is cleared on any CONTROL write with a clear bit set.
    • CPUIOx is only cleared on CONTROL writes. Future APUIO writes to the APU will not be changed.
    • The 65816 S-CPU APUIOn Data-from-APU registers will not be changed.
  • C (bit 5) - If set the CPUIO2 and CPUIO3 Data-from-CPU read registers ($F6, $F7) are reset to $00.
  • I (bit 7) - Enables the IPL ROM if set.
    • This does not disable Audio-RAM writes to the IPL ROM memory addresses.


At reset this register is initialized as if $80 was written to it.

The function of bit 7 enabling the IPL ROM is not documented in the SNES Development Manual.


DSPADDR - DSP register address ($F2)

7  bit  0
---- ----
RAAA AAAA
|||| ||||
|+++-++++- S-DSP register address
+--------- Read only flag

Write $F2 to select a DSP register, then a value can be read or written to that DSP register via $F3.

  • Writing $F2 with the high bit set will select a DSP register according to the lower 7 bits, but it will be read-only.
  • The high bit of $F2 will always read back as 0.

See: Reading and writing S-DSP registers and S-DSP registers


DSPDATA - DSP register data ($F3)

7  bit  0
---- ----
DDDD DDDD
|||| ||||
++++-++++- S-DSP register data

On write: if DSPADDR bit 7 clear: write to S-DSP address DSPADDR

On read: read from S-DSP register address DSPADDR

A DSP register can be selected with DSPADDR ($F2), after which it can be read or written at DSPDATA ($F3). Often it is useful to load the register address into A, and the value to send in Y, so that MOV $F2, YA can be used to do both in one 16-bit instruction.

The DSP register address space only has 7 bits. The high bit of DSPADDR ($F2), if set, will make the selected register read-only via DSPDATA ($F3).

When initializing the DSP registers for the first time, take care not to accidentally enable echo writeback via FLG, because it will immediately begin overwriting values in RAM.

See: Reading and writing S-DSP registers and S-DSP registers


CPUIOx - APU-to-Data register x ($F4 - $F7, write)

7  bit  0
---- ----
DDDD DDDD
|||| ||||
++++-++++- Data to CPU


CPUIOx - Data-from-CPU register x ($F4 - $F7, read)

7  bit  0
---- ----
DDDD DDDD
|||| ||||
++++-++++- Data from CPU

These 4 ports allow communication with the SNES CPU. There are 8 stored values, each is a one-way communication written from one side, and readable only from the other side. Each port therefore has two separate one-way values, each seen from only either the SNES CPU or the SPC-700.

If a port is read on the same cycle it is written, an incorrect value will result. For this reason, common practice is to read a port in a loop until the value changes, and then read it once more to ensure the correct value is read. (A single port can be used this way to indicate that a message is ready, and the other 3 ports could be safely read only once with the assumption that the other CPU will not write to them once the ready indication was given.)

At reset these registers are initialized to $00.


TxTARGET - Timer x target ($FA - $FC, write-only)

7  bit  0
---- ----
TTTT TTTT
|||| ||||
++++-++++- Timer target

On power-on: TxTARGET = 0

When enabled via $F1, the 3 timers will internally count at a rate of 8 KHz (timers 0,1) or 64 KHz (timer 2), and when this interval value has been exceeded, they will increment their external counter result ($FD-FF) and begin again.


TxOUT - Timer x output ($FD - $FF, read-only)

7  bit  0
---- ----
0000 CCCC
     ||||
     ++++- Timer counter

On power-on: TxOUT = 0
On reset:    TxOUT = 0
On read:     TxOUT = 0

The 4-bit result of the three timers counts up every time the interval is reached.

Reading these registers resets each counter to 0 immediately after the read. The upper 4 bits will always read as 0.


IPL Boot ROM

See: Booting the SPC700

The IPL Boot ROM is a small built-in program responsible for initializing the SPC-700, and making it ready to transfer a program from the SNES CPU, then execute it. It normally resides at $FFC0, with its code beginning at this address, but if desired it can be unmapped using the $F1 control register, and replaced with the underlying RAM.

When the SNES is reset, the SPC-700 will also reset and begin executing the IPL.

IPL might stand for "Initial Program Load".

The high level process of the IPL is described below:

  1. Reset: Stack pointer = $EF. Zero-page from $00-$EF is set to $00. (Note: this leaves the top 16-bytes of the stack page unused by default.)
  2. Signal ready: Port 0 = $AA. Port 1 = $BB.
  3. Wait for signal: Loop until $CC is read from port 0.
  4. Read address: Read a 2 byte address from port 2 (low) and 3 (high).
  5. Acknowledge: Read value from port 0 and write it to port 0, confirming the signal was received.
  6. Begin: Read value from port 1, if 0 begin executing code at address read in step 4, otherwise begin reading data (step 7).
  7. Begin Transfer: Loop until read port 0 reads 0. Set 8-bit counter to 0, then proceed to step 8.
  8. Transfer Loop:
    • Read a byte from port 1 and write to the destination address.
    • Write the value read from port 0 to port 0 to acknowledge receipt of the byte.
    • Increment the destination address, and increment the 8-bit counter.
    • Wait until port 0 reads equal to the new counter (SNES will increment its own counter and send it to port 0 to signal the next byte), and repeat step 8, otherwise...
    • If port 0 reads greater than the new counter (i.e. SNES increments by 2 or more before writing port 0): write back port 0 to acknowledge, transfer ends and return to step 4.

On the SNES side, to load your program into the SPC:

  1. Wait for the ready signal: port 0 = $AA, port 1 = $BB (IPL 2).
  2. Write the first destination address to ports 2+3, write any non-zero value to port 1, then write $CC to port 0 to begin (IPL 3).
  3. Wait for port 0 to read back $CC (IPL 5).
  4. Set an 8-bit counter to $00. Write the first byte of data to port 1, then write the counter value ($00) to port 0 (IPL 7). Read port 0 until it is equal to the counter $00.
  5. For each byte of data: write the data to port 1, increment the 8-bit counter and write it to port 0, then read port 0 until is becomes equal to the counter. Repeat until finished. (IPL 8)
  6. After the last byte is written, write the next destination address to ports 2+3, write $00 to port 1, then increment your counter twice and write it to port 0. (IPL 4)
  7. The SPC-700 will begin executing code at the destination address now.

Most often only one data transfer is necessary, but by writing a non-zero value to port 1 in SNES step 6, we can instead initiate another transfer at the new address. When doing this, if your counter is 0 after incrementing twice, increment it a third time to be non-zero before writing it to port 0. This is because a value of 0 in port 0 will also signal the first byte of the transfer in the next step.

It is most common to place the data program at $0200, just above the stack page, and start execution from there, but this is not a requirement.

IPL loading takes about 520 master clocks per byte transferred. This allows about 650 bytes in a 60hz frame, if the CPU is dedicated to this activity.

See Also

Links

  1. Super Nintendo (NTSC) Revision 2 Motherboard schematic by Jonathon W. Donaldson
  2. ares source code, ares/sfc/smp/timing.cpp, by Near