SNES Development
SPC700 Reference

SPC-700

System Overview

CPU

8-bit SPC700, runs at ~1Mhz …with the effective speed being half (each instruction takes a minimum of 2 cycles)

Memory

64KB (NOT 32KB), shared with everything.

Communication

4 8-bit I/O ports to transfer data to/from the SNES.

Sound

8x channels of ADPCM compressed sample goodness. The DSP also has special hardware for echo effects, white noise, and pitch modulation.

Timers

Two 8-bit 8KHz timers + one more 8-bit 64KHz timer, all have 4-bit count-up values.

SPC Memory Map and Registers

RangeDescription
0000 - 00EFPage 0
00F0 - 00FFRegisters
0100 - 01FFPage 1
0200 - FFBFMemory
FFC0 - FFFFMemory (read / write)
FFC0 - FFFFMemory (write only)*
FFC0 - FFFF64 byte IPL ROM (read only)*

* Only if X bit is set in the undocumented register.

Pages 0/1 are known as the “Zero Pages”. Accesses to zero page memory can be done with faster (and smaller) cpu instructions (one byte addresses). Which zero page to be operated on can be switched with a processor flag. Page 1 is mainly used for stack space.

Registers

MnemonicDescriptionControl
F0Undocumented?/W
F1Control Register /W
F2DSP Register AddressR/W
F3DSP Register DataR/W
F4Port 0R/W
F5Port 1R/W
F6Port 2R/W
F7Port 3R/W
F8Regular MemoryR/W
F9Regular MemoryR/W
FATimer-0 /W
FBTimer-1 /W
FCTimer-2 /W
FDCounter-0R/
FECounter-1R/
FFCounter-2R/

Control Register

       7     6     5     4     3     2     1     0
    +-----+-----+-----+-----+-----+-----+-----+-----+
 F1 |  -  |  -  | PC32| PC10|  -  | ST2 | ST1 | ST0 | Control, Write only!
    +-----+-----+-----+-----+-----+-----+-----+-----+

PC32: "PORT CLEAR" Writing '1' here will reset input from ports 2 & 3. (reset to zero)
PC10: "PORT CLEAR" Writing '1' here will reset input from ports 0 & 1.
STx:  Writing '1' here will activate timer X, writing '0' disables the timer.

There will probably be some conflict if the snes writes data at the same time the SPC initiates a port clear function.

Some emulators don’t emulate this, BSNES does though.

Writing to the control register will ALWAYS reset active timers. Control is not readable. Writing ‘1’ to any of the timer bits will start / restart the timer, even if it’s already active, writing ‘0’ will stop the timer. Even when using bit operations, the other timers will still be affected.

Communication Ports

Basically there are actually 8-bytes of data passed around. The SNES uses registers 2140 - 2143. The SPC uses registers F4 - F7.

SNES SIDE               SPC SIDE

(write)                    (write)
-----> $2140 ------------,    .-------- $F4 <-----
-----> $2141 ----------, |    | .------ $F5 <-----
-----> $2142 --------, | |    | | .---- $F6 <-----
-----> $2143 ------, | | |    | | | .-- $F7 <-----
                   | | | |    | | | |
<----- $2140 <-----|-|-|-|----' | | |
<----- $2141 <-----|-|-|-|------' | |
<----- $2142 <-----|-|-|-|--------' |
<----- $2143 <-----|-|-|-|----------'
 (read)            | | | |          (read)
                   | | | `------------> $F4 ------>
                   | | `--------------> $F5 ------>
                   | `----------------> $F6 ------>
                   `------------------> $F7 ------>

Hardware Quirk! When writing in 16-bit values to 2140 / 2141, a noise pulse (or some technical thingy) may write data to 2143 too! Always write to 2140 / 2141 with 8-bit writes if this is undesired.

Another Warning! When a read from a port conflicts with a write to the port from the opposing CPU, the data may be garbled. Always re-fetch values that may be affected by this conflict!

Here is a fraction of code that handles the above conflict:

;-------------------------------------------------------------------
comms_process:
;-------------------------------------------------------------------

    mov a, comms_v          ; COMMS_V is the last data written to port0
    cmp a, spc_port0        ; if the value in port0 is different, this means the SNES has sent new data.
    beq cp_exit             ; if its the same, then there are no messages.

; receive message
    mov a, spc_port1        ; PORT1 contains the message type
    mov comms_v, spc_port0  ; PORT0 should contain valid data now, since a few cycles have passed.
    cmp a, #20h             ; determine message type... etc...

    --PROCESS MESSAGE--

    mov spc_port0, comms_v  ; mimic the value of port0 to let the SPC know we're finished,
                            ; if the value is incorrect then the SNES will get caught in an infinite loop.

Here is an unstable example that may crash:

;-------------------------------------------------------------------
comms_process:
;-------------------------------------------------------------------

    mov a, spc_port0        ; check port0 for different data
    cmp a, comms_v          ; comms_v is last value
    beq cp_exit             ; if its the same, then there are no messages.

; receive message
    mov comms_v, a          ; save validation <- THE DATA MAY BE GARBLED!
    mov a, spc_port1

    --PROCESS MESSAGE--

    mov spc_port0, comms_v  ; mimic the value of port0 to let the blah blah blah. This
                            ; data might be incorrect! It may hang-up the SNES side.

DSP Access

The DSP is accessed through 2 registers. One register points to a DSP register index. The other allows reading from / writing to the DSP register specified.

         7     6     5     4     3     2     1     0
      +-----+-----+-----+-----+-----+-----+-----+-----+
$F2   |  x  |  x  |  x  |  x  |  x  |  x  |  x  |  x  | DSP-Address, Read / Write
      +-----+-----+-----+-----+-----+-----+-----+-----+

x: Pointer to DSP register.

         7     6     5     4     3     2     1     0
      +-----+-----+-----+-----+-----+-----+-----+-----+
$F3   |  x  |  x  |  x  |  x  |  x  |  x  |  x  |  x  | DSP-Data, Read / Write
      +-----+-----+-----+-----+-----+-----+-----+-----+

x: Data to be written / data being read.

To write a value to the DSP:

  1. Write address of DSP register to F2.
  2. Write desired value to F3.

And to read a value:

  1. Write address of DSP register to F2.
  2. Read the value from F3.

F2 / F3 can be written simultaneously with a 16-bit transfer:

; Setting DSP register $00 to $25
; and $01 to $30

    mov a, #$00
    mov y, #$25
    movw    $F2, ya
    inc a
    mov y, #$30
    movw    $F2, ya

; theres a few possibilities to write data to the DSP...
; spc has some mem->mem / mem->imm instructions too.
    inc $F2     ; set register $02 to $99
    mov $F3, #$99

Timers

The SPC contains 3 timers that can be used for general purpose. Timers 0,1 count up at 8KHz, Timer 2 has a higher precision of 64KHz.

         7     6     5     4     3     2     1     0
      +-----+-----+-----+-----+-----+-----+-----+-----+
$FA+X |  t  |  t  |  t  |  t  |  t  |  t  |  t  |  t  | Timer-X, Write only!
      +-----+-----+-----+-----+-----+-----+-----+-----+

t: Timing value.

         7     6     5     4     3     2     1     0
      +-----+-----+-----+-----+-----+-----+-----+-----+
$FD+X |  -  |  -  |  -  |  -  |  c  |  c  |  c  |  c  | Counter-X, Read only!
      +-----+-----+-----+-----+-----+-----+-----+-----+

c: Count-up value.

The timers consist of an 8-bit internal counter and a 4-bit up-counter. When the timer is off, the internal counter can be programmed through registers Timer-X registers. When the timer is activated by a ‘1’ bit in the Control register, then the timer starts counting up (at 8KHz/64Khz) until it reaches the amount specified in Timer-X. When this happens the internal counter is reset and Counter-X is incremented. When you read Counter-X, the value in it will be reset to zero. Take care not to let the up-counter overflow! It’s only 4-bits wide, and if it overflows the value will be lost.

Psuedo Code - Setting Timer 0 to tick at 15ms:

  1. …Make sure the timer is off.
  2. Write 120 (0x78) to FA (15/(1000/8000) = 15*8 = 120)
  3. Write ‘1’ to the control register bit (F1)
  4. Timer is started..
  5. Read FD periodically to check if ticks have passed.

Real Code:

; An example to wait 15ms.

    mov timer0,  #$78           ; 15ms timing value
    mov control, #$01           ; this will destroy all other timers

    ; timer is started

wait_for_tick:
        mov a, counter0         ; read counter
        beq     wait_for_tick   ; loop if 0

    ; tick has passed, process data

CPU Register Set

The CPU has 6 registers in it: A, X, Y, SP, PC, PSW

All are 8-bit except for the PC (Program Counter), which points at a 16-bit program execution address.

The A (Accumulator) register is compatible with most CPU instructions.

The X/Y (Index) registers are mostly used for counters/memory addressing. For some instructions, the Y register can be paired with A (“YA”) for 16-bit operations.

The SP (Stack Pointer) register points to an area of memory in Page1 (0100 - 01FF). Stack operations affect SP, the IPL ROM initializes SP to the top of Page1.

The PSW (Program Status Word) register contains various bits that effect operation of the CPU.

CPU Program Status Word

Flags stored in PSW Register

MnemonicDescription
NNegative
VOverflow
PDirect page
BBreak
HHalf Carry
IInterrupt enabled (unused)
ZZero
CCarry

Negative Flag (N): The MSB of an operation’s result is copied to the Negative flag.

Overflow Flag (V): I don’t think I ever had to use this bit, It tells you if an overflow occured in the previous arithmetic operation.

Direct Page Flag (P): This bit controls the direct-page (dp) operations. The dp operations only have 1 address byte. If the P flag is cleared then the effective address will be 0000h+address. Otherwise it will be 0100h+address.

Half Carry Flag (H): The half-carry flag is used in arithmetic operations. It can be used to determine if the DAA/DAS instruction needs to be executed after a BCD addition/subtraction operation. It cannot be directly set by any instruction, however, it is cleared with CLRV.

Carry Flag (C): The carry flag is mainly used in arithmetic operations. It can be used to control program flow after addition/subtraction. Its also used by a few other functions, like shift commands. It can be directly set by instructions SETC and CLRC.

Break Flag (B): This flag is unused. The only instruction that touches it is BRK.

Interrupt Enable (I): Interrupts are not supported. This flag is unused.

Opcode Matrices (by jwdonal)

SPC700 Opcode Matrix by Type (Native Mnemonics)

SPC700 Opcode Matrix by Type (65816-Style Mnemonics)

SPC700 Opcode Matrix by Addressing Mode (Native Mnemonics)

SPC700 Opcode Matrix by Addressing Mode (65816-Style Mnemonics)

SPC700 Opcode Matrix by Bits (Native Mnemonics)

SPC700 Opcode Matrix by Bits (65816-Style Mnemonics)

SPC700 Addressing Modes Effective Address Regions

Instruction Chart

Assembler ExampleOperationHEXFlags SetBytesCycles
ADC (X), (Y) (X) = (X)+(Y)+C 99NV–H-ZC15
ADC A, #i A = A+i+C 88NV–H-ZC22
ADC A, (X) A = A+(X)+C 86NV–H-ZC13
ADC A, [d]+Y A = A+([d]+Y)+C 97NV–H-ZC26
ADC A, [d+X] A = A+([d+X])+C 87NV–H-ZC26
ADC A, d A = A+(d)+C 84NV–H-ZC23
ADC A, d+X A = A+(d+X)+C 94NV–H-ZC24
ADC A, !a A = A+(a)+C 85NV–H-ZC34
ADC A, !a+X A = A+(a+X)+C 95NV–H-ZC35
ADC A, !a+Y A = A+(a+Y)+C 96NV–H-ZC35
ADC dd, ds (dd) = (dd)+(d)+C 89NV–H-ZC36
ADC d, #i (d) = (d)+i+C 98NV–H-ZC35
ADDW YA, d YA = YA + (d), H on high byte 7ANV–H-ZC25
AND (X), (Y) (X) = (X) & (Y) 39N—–Z-15
AND A, #i A = A & i 28N—–Z-22
AND A, (X) A = A & (X) 26N—–Z-13
AND A, [d]+Y A = A & ([d]+Y) 37N—–Z-26
AND A, [d+X] A = A & ([d+X]) 27N—–Z-26
AND A, d A = A & (d) 24N—–Z-23
AND A, d+X A = A & (d+X) 34N—–Z-24
AND A, !a A = A & (a) 25N—–Z-34
AND A, !a+X A = A & (a+X) 35N—–Z-35
AND A, !a+Y A = A & (a+Y) 36N—–Z-35
AND dd, ds (dd) = (dd) & (ds) 29N—–Z-36
AND d, #i (d) = (d) & i 38N—–Z-35
AND1 C, /m.b C = C & ~(m.b) 6A——-C34
AND1 C, m.b C = C & (m.b) 4A——-C34
ASL A Left shift A: high->C, 0->low 1CN—–ZC12
ASL d Left shift (d) as above 0BN—–ZC24
ASL d+X Left shift (d+X) as above 1BN—–ZC25
ASL !a Left shift (a) as above 0CN—–ZC35
BBC d.0, r PC+=r if d.0 == 0 13——–35/7
BBC d.1, r PC+=r if d.1 == 0 33——–35/7
BBC d.2, r PC+=r if d.2 == 0 53——–35/7
BBC d.3, r PC+=r if d.3 == 0 73——–35/7
BBC d.4, r PC+=r if d.4 == 0 93——–35/7
BBC d.5, r PC+=r if d.5 == 0 B3——–35/7
BBC d.6, r PC+=r if d.6 == 0 D3——–35/7
BBC d.7, r PC+=r if d.7 == 0 F3——–35/7
BBS d.0, r PC+=r if d.0 == 1 03——–35/7
BBS d.1, r PC+=r if d.1 == 1 23——–35/7
BBS d.2, r PC+=r if d.2 == 1 43——–35/7
BBS d.3, r PC+=r if d.3 == 1 63——–35/7
BBS d.4, r PC+=r if d.4 == 1 83——–35/7
BBS d.5, r PC+=r if d.5 == 1 A3——–35/7
BBS d.6, r PC+=r if d.6 == 1 C3——–35/7
BBS d.7, r PC+=r if d.7 == 1 E3——–35/7
BCC r PC+=r if C == 0 90——–22/4
BCS r PC+=r if C == 1 B0——–22/4
BEQ r PC+=r if Z == 1 F0——–22/4
BMI r PC+=r if N == 1 30——–22/4
BNE r PC+=r if Z == 0 D0——–22/4
BPL r PC+=r if N == 0 10——–22/4
BVC r PC+=r if V == 0 50——–22/4
BVS r PC+=r if V == 1 70——–22/4
BRA r PC+=r 2F——–24
BRK Push PC and Flags, PC = [$FFDE] 0F—1-0–18
CALL !a (SP--)=PCh, (SP--)=PCl, PC=a 3F——–38
CBNE d+X, r CMP A, (d+X) then BNE DE——–36/8
CBNE d, r CMP A, (d) then BNE 2E——–35/7
CLR1 d.0 d.0 = 0 12——–24
CLR1 d.1 d.1 = 0 32——–24
CLR1 d.2 d.2 = 0 52——–24
CLR1 d.3 d.3 = 0 72——–24
CLR1 d.4 d.4 = 0 92——–24
CLR1 d.5 d.5 = 0 B2——–24
CLR1 d.6 d.6 = 0 D2——–24
CLR1 d.7 d.7 = 0 F2——–24
CLRC C = 0 60——-012
CLRP P = 0 20–0—–12
CLRV V = 0, H = 0 E0-0–0—12
CMP (X), (Y) (X) - (Y) 79N—–ZC15
CMP A, #i A - i 68N—–ZC22
CMP A, (X) A - (X) 66N—–ZC13
CMP A, [d]+Y A - ([d]+Y) 77N—–ZC26
CMP A, [d+X] A - ([d+X]) 67N—–ZC26
CMP A, d A - (d) 64N—–ZC23
CMP A, d+X A - (d+X) 74N—–ZC24
CMP A, !a A - (a) 65N—–ZC34
CMP A, !a+X A - (a+X) 75N—–ZC35
CMP A, !a+Y A - (a+Y) 76N—–ZC35
CMP X, #i X - i C8N—–ZC22
CMP X, d X - (d) 3EN—–ZC23
CMP X, !a X - (a) 1EN—–ZC34
CMP Y, #i Y - i ADN—–ZC22
CMP Y, d Y - (d) 7EN—–ZC23
CMP Y, !a Y - (a) 5EN—–ZC34
CMP dd, ds (dd) - (ds) 69N—–ZC36
CMP d, #i (d) - i 78N—–ZC35
CMPW YA, d YA - (d) 5AN—–ZC24
DAA A decimal adjust for addition DFN—–ZC13
DAS A decimal adjust for subtraction BEN—–ZC13
DBNZ Y, r Y-- then JNZ FE——–24/6
DBNZ d, r (d)-- then JNZ 6E——–35/7
DEC A A-- 9CN—–Z-12
DEC X X-- 1DN—–Z-12
DEC Y Y-- DCN—–Z-12
DEC d (d)-- 8BN—–Z-24
DEC d+X (d+X)-- 9BN—–Z-25
DEC !a (a)-- 8CN—–Z-35
DECW d Word (d)-- 1AN—–Z-26
DI I = 0 C0—–0–13
DIV YA, X A=YA/X, Y=mod(YA,X) 9ENV–H-Z-112
EI I = 1 A0—–1–13
EOR (X), (Y) (X) = (X) EOR (Y) 59N—–Z-15
EOR A, #i A = A EOR i 48N—–Z-22
EOR A, (X) A = A EOR (X) 46N—–Z-13
EOR A, [d]+Y A = A EOR ([d]+Y) 57N—–Z-26
EOR A, [d+X] A = A EOR ([d+X]) 47N—–Z-26
EOR A, d A = A EOR (d) 44N—–Z-23
EOR A, d+X A = A EOR (d+X) 54N—–Z-24
EOR A, !a A = A EOR (a) 45N—–Z-34
EOR A, !a+X A = A EOR (a+X) 55N—–Z-35
EOR A, !a+Y A = A EOR (a+Y) 56N—–Z-35
EOR dd, ds (dd) = (dd) EOR (ds) 49N—–Z-36
EOR d, #i (d) = (d) EOR i 58N—–Z-35
EOR1 C, m.b C = C EOR (m.b) 8A——-C35
INC A A++ BCN—–Z-12
INC X X++ 3DN—–Z-12
INC Y Y++ FCN—–Z-12
INC d (d)++ ABN—–Z-24
INC d+X (d+X)++ BBN—–Z-25
INC !a (a)++ ACN—–Z-35
INCW d Word (d)++ 3AN—–Z-26
JMP [!a+X] PC = [a+X] 1F——–36
JMP !a PC = a 5F——–33
LSR A Right shift A: 0->high, low->C 5CN—–ZC12
LSR d Right shift (d) as above 4BN—–ZC24
LSR d+X Right shift (d+X) as above 5BN—–ZC25
LSR !a Right shift (a) as above 4CN—–ZC35
MOV (X)+, A (X++) = A (no read) AF——–14
MOV (X), A (X) = A (read) C6——–14
MOV [d]+Y, A ([d]+Y) = A (read) D7——–27
MOV [d+X], A ([d+X]) = A (read) C7——–27
MOV A, #i A = i E8N—–Z-22
MOV A, (X) A = (X) E6N—–Z-13
MOV A, (X)+ A = (X++) BFN—–Z-14
MOV A, [d]+Y A = ([d]+Y) F7N—–Z-26
MOV A, [d+X] A = ([d+X]) E7N—–Z-26
MOV A, X A = X 7DN—–Z-12
MOV A, Y A = Y DDN—–Z-12
MOV A, d A = (d) E4N—–Z-23
MOV A, d+X A = (d+X) F4N—–Z-24
MOV A, !a A = (a) E5N—–Z-34
MOV A, !a+X A = (a+X) F5N—–Z-35
MOV A, !a+Y A = (a+Y) F6N—–Z-35
MOV SP, X SP = X BD——–12
MOV X, #i X = i CDN—–Z-22
MOV X, A X = A 5DN—–Z-12
MOV X, SP X = SP 9DN—–Z-12
MOV X, d X = (d) F8N—–Z-23
MOV X, d+Y X = (d+Y) F9N—–Z-24
MOV X, !a X = (a) E9N—–Z-34
MOV Y, #i Y = i 8DN—–Z-22
MOV Y, A Y = A FDN—–Z-12
MOV Y, d Y = (d) EBN—–Z-23
MOV Y, d+X Y = (d+X) FBN—–Z-24
MOV Y, !a Y = (a) ECN—–Z-34
MOV dd, ds (dd) = (ds) (no read) FA——–35
MOV d+X, A (d+X) = A (read) D4——–25
MOV d+X, Y (d+X) = Y (read) DB——–25
MOV d+Y, X (d+Y) = X (read) D9——–25
MOV d, #i (d) = i (read) 8F——–35
MOV d, A (d) = A (read) C4——–24
MOV d, X (d) = X (read) D8——–24
MOV d, Y (d) = Y (read) CB——–24
MOV !a+X, A (a+X) = A (read) D5——–36
MOV !a+Y, A (a+Y) = A (read) D6——–36
MOV !a, A (a) = A (read) C5——–35
MOV !a, X (a) = X (read) C9——–35
MOV !a, Y (a) = Y (read) CC——–35
MOV1 C, m.b C = (m.b) AA——-C34
MOV1 m.b, C (m.b) = C CA——–36
MOVW YA, d YA = word (d) BAN—–Z-25
MOVW d, YA word (d) = YA (read low only) DA——–25
MUL YA YA = Y * A, NZ on Y only CFN—–Z-19
NOP do nothing 00——–12
NOT1 m.b m.b = ~m.b EA——–35
NOTC C = !C ED——-C13
OR (X), (Y) (X) = (X) | (Y)19N—–Z-15
OR A, #i A = A | i08N—–Z-22
OR A, (X) A = A | (X)06N—–Z-13
OR A, [d]+Y A = A | ([d]+Y)17N—–Z-26
OR A, [d+X] A = A | ([d+X])07N—–Z-26
OR A, d A = A | (d)04N—–Z-23
OR A, d+X A = A | (d+X)14N—–Z-24
OR A, !a A = A | (a)05N—–Z-34
OR A, !a+X A = A | (a+X)15N—–Z-35
OR A, !a+Y A = A | (a+Y)16N—–Z-35
OR dd, ds (dd) = (dd) | (ds)09N—–Z-36
OR d, #i (d) = (d) | i18N—–Z-35
OR1 C, /m.b C = C | ~(m.b)2A——-C35
OR1 C, m.b C = C | (m.b)0A——-C35
PCALL u CALL $FF00+u 4F——–26
POP A A = (++SP) AE——–14
POP PSW Flags = (++SP) 8ENVPBHIZC14
POP X X = (++SP) CE——–14
POP Y Y = (++SP) EE——–14
PUSH A (SP--) = A 2D——–14
PUSH PSW (SP--) = Flags 0D——–14
PUSH X (SP--) = X 4D——–14
PUSH Y (SP--) = Y 6D——–14
RET Pop PC 6F——–15
RETI Pop Flags, PC 7FNVPBHIZC16
ROL A Left shift A: low=C, C=high 3CN—–ZC12
ROL d Left shift (d) as above 2BN—–ZC24
ROL d+X Left shift (d+X) as above 3BN—–ZC25
ROL !a Left shift (a) as above 2CN—–ZC35
ROR A Right shift A: high=C, C=low 7CN—–ZC12
ROR d Right shift (d) as above 6BN—–ZC24
ROR d+X Right shift (d+X) as above 7BN—–ZC25
ROR !a Right shift (a) as above 6CN—–ZC35
SBC (X), (Y) (X) = (X)-(Y)-!C B9NV–H-ZC15
SBC A, #i A = A-i-!C A8NV–H-ZC22
SBC A, (X) A = A-(X)-!C A6NV–H-ZC13
SBC A, [d]+Y A = A-([d]+Y)-!C B7NV–H-ZC26
SBC A, [d+X] A = A-([d+X])-!C A7NV–H-ZC26
SBC A, d A = A-(d)-!C A4NV–H-ZC23
SBC A, d+X A = A-(d+X)-!C B4NV–H-ZC24
SBC A, !a A = A-(a)-!C A5NV–H-ZC34
SBC A, !a+X A = A-(a+X)-!C B5NV–H-ZC35
SBC A, !a+Y A = A-(a+Y)-!C B6NV–H-ZC35
SBC dd, ds (dd) = (dd)-(ds)-!C A9NV–H-ZC36
SBC d, #i (d) = (d)-i-!C B8NV–H-ZC35
SET1 d.0 d.0 = 1 02——–24
SET1 d.1 d.1 = 1 22——–24
SET1 d.2 d.2 = 1 42——–24
SET1 d.3 d.3 = 1 62——–24
SET1 d.4 d.4 = 1 82——–24
SET1 d.5 d.5 = 1 A2——–24
SET1 d.6 d.6 = 1 C2——–24
SET1 d.7 d.7 = 1 E2——–24
SETC C = 1 80——-112
SETP P = 1 40–1—–12
SLEEP Halts the processor EF——–1?
STOP Halts the processor FF——–1?
SUBW YA, d YA = YA - (d), H on high byte 9ANV–H-ZC25
TCALL 0 CALL [$FFDE] 01——–18
TCALL 1 CALL [$FFDC] 11——–18
TCALL 2 CALL [$FFDA] 21——–18
TCALL 3 CALL [$FFD8] 31——–18
TCALL 4 CALL [$FFD6] 41——–18
TCALL 5 CALL [$FFD4] 51——–18
TCALL 6 CALL [$FFD2] 61——–18
TCALL 7 CALL [$FFD0] 71——–18
TCALL 8 CALL [$FFCE] 81——–18
TCALL 9 CALL [$FFCC] 91——–18
TCALL 10 CALL [$FFCA] A1——–18
TCALL 11 CALL [$FFC8] B1——–18
TCALL 12 CALL [$FFC6] C1——–18
TCALL 13 CALL [$FFC4] D1——–18
TCALL 14 CALL [$FFC2] E1——–18
TCALL 15 CALL [$FFC0] F1——–18
TCLR1 !a (a) = (a)&~A, ZN as for A-(a) 4EN—–Z-36
TSET1 !a (a) = (a)|A, ZN as for A-(a)0EN—–Z-36
XCN A A = (A>>4) | (A<<4)9FN—–Z-15

CPU Instruction Set

Registers:
A   A register
X   X register
Y   Y register
PSW PSW register
YA  YA paired 16-bit register
PC  Program Counter
SP  Stack Pointer

Data types:
imm 8-bit immediate data
dp  8-bit direct page offset (direct page is either 0000h or 0100h depending on P flag)
abs 16-bit absolute address
rel 8-bit offset relative to PC
mem bit operation address, (13-bits)
bit bit location (3-bits)

Memory:
(addr)       8-bit memory value at addr
word:(addr)  16-bit memory value at addr
abs:(addr)   16-bit memory value at addr
I hope you get the point. :\

For opcode affected instructions:
    MSB
x   [ x | x | x | 0 |     opcode    ]

y   [ y | y | y | 1 |     opcode    ]

Flag settings:
Uppercase flag: Flag is set according to result of operation
Lowercase 'c' : Flag is complemented
       -      : Flag is not affected
       0      : Flag is reset
       1      : Flag is set

Operations:

<-  Transfer data (in direction)
->  Transfer data
+=  Add values and affect operand
+   Add values and do not affect operand
    Same for all arithmetic/logical operations
-   Subtraction
|   Logical OR
^   Exclusive Logical OR
&   Logical AND
>>  Shift right 1 bit
<<  Shift left 1 bit
++  Increment Data
--  Decrement Data
*   Multiplication
/   Division
%   Modulus (remainder)

8-bit Data Transmission (Read)

InstructionOperandOpcodeBytesCyclesFlags (NVPBHIZC)Operation
MOVA, #imm E822N-----Z-A <- imm
MOVA, (X) E613N-----Z-A <- (X)
MOVA, (X)+ BF14N-----Z-A <- (X), X is incremented afterward
MOVA, dp E423N-----Z-A <- (dp)
MOVA, dp+X F424N-----Z-A <- (dp+X)
MOVA, !abs E534N-----Z-A <- (abs)
MOVA, !abs+XF535N-----Z-A <- (abs+X)
MOVA, !abs+YF635N-----Z-A <- (abs+Y)
MOVA, [dp+X]E726N-----Z-A <- (abs:(abs+X))
MOVA, [dp]+YF726N-----Z-A <- (abs:(abs)+Y)
MOVX, #imm CD22N-----Z-X <- imm
MOVX, dp F823N-----Z-X <- (dp)
MOVX, dp+Y F924N-----Z-X <- (dp+Y)
MOVX, !abs E934N-----Z-X <- (abs)
MOVY, #imm 8D22N-----Z-Y <- imm
MOVY, dp EB23N-----Z-Y <- (dp)
MOVY, dp+X FB24N-----Z-Y <- (dp+X)
MOVY, !abs EC34N-----Z-Y <- (abs)

8-bit Data Transmission (Write)

InstructionOperandOpcodeBytesCyclesFlags (NVPBHIZC)Operation
MOV(X), A C614--------A -> (X)
MOV(X)+, A AF14--------A -> (X), X is incremented
MOVdp, A C424--------A -> (dp)
MOVdp+X, A D425--------A -> (dp+X)
MOV!abs, A C535--------A -> (abs)
MOV!abs+X, AD536--------A -> (abs+X)
MOV!abs+Y, AD636--------A -> (abs+Y)
MOV[dp+X], AC727--------A -> (abs:(dp+X))
MOV[dp]+Y, AD727--------A -> (abs:(dp)+Y)
MOVdp, X D824--------X -> (dp)
MOVdp+Y, X D925--------X -> (dp+Y)
MOV!abs, X C935--------X -> (abs)
MOVdp, Y CB24--------Y -> (dp)
MOVdp+X, Y DB25--------Y -> (dp+X)
MOV!abs, Y CC35--------Y -> (abs)

8-bit Data Transmission (Reg->Reg, Mem->Mem)

InstructionOperandOpcodeBytesCyclesFlags (NVPBHIZC)Operation
MOVA, X 7D12N-----Z-A <- X
MOVA, Y DD12N-----Z-A <- Y
MOVX, A 5D12N-----Z-A -> X
MOVY, A FD12N-----Z-A -> Y
MOVX, SP 9D12N-----Z-X <- SP
MOVSP, X BD12--------X -> SP
MOVdp, dp FA35--------(dp) <- (dp)
MOVdp, #imm8F35--------(dp) <- imm

8-bit Arithmetic

InstructionOperandOpcodeBytesCyclesFlags (NVPBHIZC)Operation
ADCA, #imm 8822NV--H-ZCA += imm + C
ADCA, (X) 8613NV--H-ZCA += (X) + C
ADCA, dp 8423NV--H-ZCA += (dp) + C
ADCA, dp+X 9424NV--H-ZCA += (dp+X) + C
ADCA, !abs 8534NV--H-ZCA += (abs) + C
ADCA, !abs+X9535NV--H-ZCA += (abs+X) + C
ADCA, !abs+Y9635NV--H-ZCA += (abs+Y) + C
ADCA, [dp+X]8726NV--H-ZCA += (abs:(dp+X)) + C
ADCA, [dp]+Y9726NV--H-ZCA += (abs:(dp)+Y) + C
ADC(X),(Y) 9915NV--H-ZC(X) += (Y) + C
ADCdp, dp 8936NV--H-ZC(dp) += (dp) + C
ADCdp, #imm 9835NV--H-ZC(dp) += imm + C
SBCA, #imm A822NV--H-ZCA -= imm + !C
SBCA, (X) A613NV--H-ZCA -= (X) + !C
SBCA, dp A423NV--H-ZCA -= (dp) + !C
SBCA, dp+X B424NV--H-ZCA -= (dp+X) + !C
SBCA, !abs A534NV--H-ZCA -= (abs) + !C
SBCA, !abs+XB535NV--H-ZCA -= (abs+X) + !C
SBCA, !abs+YB635NV--H-ZCA -= (abs+Y) + !C
SBCA, [dp+X]A726NV--H-ZCA -= (abs:(dp+X)) + !C
SBCA, [dp]+YB726NV--H-ZCA -= (abs:(dp)+Y) + !C
SBC(X), (Y) B915NV--H-ZC(X) -= (Y) + !C
SBCdp, dp A936NV--H-ZC(dp) -= (dp) + !C
SBCdp, #imm B835NV--H-ZC(dp) -= imm + !C
CMPA, #imm 6822N-----ZCA - imm
CMPA, (X) 6613N-----ZCA - (X)
CMPA, dp 6423N-----ZCA - (dp)
CMPA, dp+X 7424N-----ZCA - (dp+X)
CMPA, !abs 6534N-----ZCA - (abs)
CMPA, !abs+X7535N-----ZCA - (abs+X)
CMPA, !abs+Y7635N-----ZCA - (abs+Y)
CMPA, [dp+X]6726N-----ZCA - (abs:(dp+X))
CMPA, [dp]+Y7726N-----ZCA - (abs:(dp)+Y)
CMP(X), (Y) 7915N-----ZC(X) - (Y)
CMPdp, dp 6936N-----ZC(dp) - (dp)
CMPdp, #imm 7835N-----ZC(dp) - imm
CMPX, #imm C822N-----ZCX - imm
CMPX, dp 3E23N-----ZCX - (dp)
CMPX, !abs 1E34N-----ZCX - (abs)
CMPY, #imm AD22N-----ZCY - imm
CMPY, dp 7E23N-----ZCY - (dp)
CMPY, !abs 5E34N-----ZCY - (abs)

8-bit Logical Operations

InstructionOperandOpcodeBytesCyclesFlags (NVPBHIZC)Operation
ANDA, #imm 2822N-----Z-A &= imm
ANDA, (X) 2613N-----Z-A &= (X)
ANDA, dp 2423N-----Z-A &= (dp)
ANDA, dp+X 3424N-----Z-A &= (dp+X)
ANDA, !abs 2534N-----Z-A &= (abs)
ANDA, !abs+X3535N-----Z-A &= (abs+X)
ANDA, !abs+Y3635N-----Z-A &= (abs+Y)
ANDA, [dp+X]2726N-----Z-A &= (abs:(dp+X))
ANDA, [dp]+Y3726N-----Z-A &= (abs:(dp)+Y)
AND(X), (Y) 3915N-----Z-(X) &= (Y)
ANDdp, dp 2936N-----Z-(dp) &= (dp)
ANDdp, #imm 3835N-----Z-(dp) &= imm
OR A, #imm 0822N-----Z-A | imm
OR A, (X) 0613N-----Z-A | (X)
OR A, dp 0423N-----Z-A | (dp)
OR A, dp+X 1424N-----Z-A | (dp+X)
OR A, !abs 0534N-----Z-A | (abs)
OR A, !abs+X1535N-----Z-A | (abs+X)
OR A, !abs+Y1635N-----Z-A | (abs+Y)
OR A, [dp+X]0726N-----Z-A | (abs:(dp+X))
OR A, [dp]+Y1726N-----Z-A | (abs:(dp)+Y)
OR (X), (Y) 1915N-----Z-(X) | (Y)
OR dp, dp 0936N-----Z-(dp) | (dp)
OR dp, #imm 1835N-----Z-(dp) | imm
EORA, #imm 4822N-----Z-A ^= imm
EORA, (X) 4613N-----Z-A ^= (X)
EORA, dp 4423N-----Z-A ^= (dp)
EORA, dp+X 5424N-----Z-A ^= (dp+X)
EORA, !abs 4534N-----Z-A ^= (abs)
EORA, !abs+X5535N-----Z-A ^= (abs+X)
EORA, !abs+Y5635N-----Z-A ^= (abs+Y)
EORA, [dp+X]4726N-----Z-A ^= (abs:(dp+X))
EORA, [dp]+Y5726N-----Z-A ^= (abs:(dp)+Y))
EOR(X), (Y) 5915N-----Z-(X) ^= (Y)
EORdp, dp 4936N-----Z-(dp) ^= (dp)
EORdp, #imm 5835N-----Z-(dp) ^= imm

8-bit Increment / Decrement Operations

InstructionOperandOpcodeBytesCyclesFlags (NVPBHIZC)Operation
INCA BC12N-----Z-++A
INCdp AB24N-----Z-++(dp)
INCdp+XBB25N-----Z-++(dp+X)
INC!absAC35N-----Z-++(abs)
INCX 3D12N-----Z-++X
INCY FC12N-----Z-++Y
DECA 9C12N-----Z---A
DECdp 8B24N-----Z---(dp)
DECdp+X9B25N-----Z---(dp+X)
DEC!abs8C35N-----Z---(abs)
DECX 1D12N-----Z---X
DECY DC12N-----Z---Y

8-bit Shift / Rotation Operations

InstructionOperandOpcodeBytesCyclesFlags (NVPBHIZC)Operation
ASLA 1C12N-----ZCC << A << 0
ASLdp 0B24N-----ZCC << (dp) << 0
ASLdp+X1B25N-----ZCC << (dp+X) << 0
ASL!abs0C35N-----ZCC << (abs) << 0
LSRA 5C12N-----ZC0 >> A >> C
LSRdp 4B24N-----ZC0 >> (dp) >> C
LSRdp+X5B25N-----ZC0 >> (dp+X) >> C
LSR!abs4C35N-----ZC0 >> (abs) >> C
ROLA 3C12N-----ZCC << A << C :the last carry value is shifted
ROLdp 2B24N-----ZCC << (dp) << C :into A, not the one you just shifted out!
ROLdp+X3B25N-----ZCC << (dp+X) << C :
ROL!abs2C35N-----ZCC << (abs) << C :
RORA 7C12N-----ZCC >> A >> C :same with these
RORdp 6B24N-----ZCC >> (dp) >> C :
RORdp+X7B25N-----ZCC >> (dp+X) >> C :
ROR!abs6C35N-----ZCC >> (abs) >> C :
XCNA 9F15N-----Z-Swaps the nibbles in A (A = (A>>4) l (A<<4))

16-bit Data Transmission Operations

InstructionOperandOpcodeBytesCyclesFlags (NVPBHIZC)Operation
MOVWYA, dpBA25N-----Z-YA <- word:(dp)
MOVWdp, YADA25--------YA -> word:(dp) :same cycles as writing 1 byte!

16-bit Arithmetic Operations

InstructionOperandOpcodeBytesCyclesFlags (NVPBHIZC)Operation
INCWdp 3A26N-----Z-++word:(dp)
DECWdp 1A26N-----Z---word:(dp)
ADDWYA, dp7A25NV--H-ZCYA += word:(dp)
SUBWYA, dp9A25NV--H-ZCYA -= word:(dp)
CMPWYA, dp5A24N-----ZCYA - word:(dp)

Multiplication / Division Operations

InstructionOperandOpcodeBytesCyclesFlags (NVPBHIZC)Operation
MULYA CF19N-----Z-YA <- Y*A
DIVYA,X9E112NV--H-Z-Y <- YA % X and A <- YA / X

Decimal Compensation Operations

InstructionOperandOpcodeBytesCyclesFlags (NVPBHIZC)Operation
DAAADF13N-----ZCDecimal adjust for addition
DASABE13N-----ZCDecimal adjust for subtraction

Program Flow Operations

InstructionOperandOpcodeBytesCyclesFlags (NVPBHIZC)Operation
BRA rel 2F24--------Branch (always) : branch always is slower than jump,
BEQ rel F022/4--------Branch if Equal (Z=1) : but branches uses relative addressing
BNE rel D022/4--------Branch if Not Equal (Z=0) : (2 bytes instead of 3)
BCS rel B022/4--------Branch if Carry Set
BCC rel 9022/4--------Branch if Carry Cleared
BVS rel 7022/4--------Branch if V=1
BVC rel 5022/4--------Branch if V=0
BMI rel 3022/4--------Branch if Negative (N=1)
BPL rel 1022/4--------Branch if Positive (N=0)
BBS dp,bit,relX335/7--------Branch if memory bit set
BBC dp,bit,relY335/7--------Branch if memory bit cleared
CBNEdp, rel 2E35/7--------Branch if A != (dp)
CBNEdp+X,rel DE36/8--------Branch if A != (dp+X)
DBNZdp,rel 6E35/7----------(dp) and branch if not zero
DBNZY,rel FE24/6----------Y and branch if not zero
JMP !abs 5F33--------PC <- abs : allows to jump anywhere in the memory space
JMP [!abs+X] 1F36--------PC <- abs:(abs+X)

Subroutine Operations

InstructionOperandOpcodeBytesCyclesFlags (NVPBHIZC)Operation
CALL !abs 3F38--------Subroutine call :pushes PC to stack and begins execution from abs
PCALLupage4F26--------Upage call (???)
TCALL N118--------Table call (??:)
BRK 0F18---1-0--Software interrupt (???)
RET 6F15--------Return from subroutine (PC is popped)
RETI 7F16RESTOREDReturn from interrupt (PC and PSW are popped)

Stack Operations

InstructionOperandOpcodeBytesCyclesFlags (NVPBHIZC)Operation
PUSHA 2D14--------Push A to stack
PUSHX 4D14--------Push X to stack
PUSHY 6D14--------Push Y to stack
PUSHPSW0D14--------Push PSW to stack
POP A AE14--------Pop A from stack
POP X CE14--------Pop X from stack
POP Y EE14--------Pop Y from stack
POP PSW8E14RESTOREDPop PSW from stack :can be used to set PSW bits

Bit Operations

InstructionOperandOpcodeBytesCyclesFlags (NVPBHIZC)Operation
SET1 dp, bit X224--------Set bit in direct page : note that with the TASM table provided, these
CLR1 dp, bit Y224--------Clear bit in direct page : instructions are done with "SETx/CLRx dp" where x is the bit#
TSET1!abs 0E36N-----Z-Test and set bits with A (???)
TCLR1!abs 4E36N-----Z-Test and clear bits with A (???)
AND1 C,mem,bit 4A34-------CC &= mem:bit :to use these instructions
AND1 C,/mem,bit6A34-------CC &= ~mem:bit :with the TASM table
OR1 C,mem,bit 0A35-------CC l= mem:bit :the syntax is a bit wierd
OR1 C,/mem,bit2A35-------CC l= ~mem:bit : "for MOV1 mem,bit,C" it is:
EOR1 C,mem,bit 8A35-------CC ^= mem:bit : MOV1 (mem + (bit << 13)),C
NOT1 mem,bit EA35--------Complement mem:bit
MOV1 C,mem,bit AA34-------CC <- mem:bit
MOV1 mem,bit,C CA36--------C -> mem:bit

PSW Operations

InstructionOperandOpcodeBytesCyclesFlags (NVPBHIZC)Operation
CLRC6012-------0Clear Carry
SETC8012-------1Set Carry
NOTCED13-------cComplement Carry
CLRVE012-0--0---Clear V and H
CLRP2012--0-----Clear DP page to 0
SETP4012--1-----Set DP page to 1
EI A013------1-Enable Interrupts (but interrupts are not supported)
DI C013------0-Disable Interrupts (but interrupts are not supported)

Other Commands

InstructionOperandOpcodeBytesCyclesFlags (NVPBHIZC)Operation
NOP 0012--------Delay
SLEEPEF13--------Standby SLEEP mode
STOP FF13--------Standby STOP mode

Special Notes

(X), (Y) are direct page addresses too! Assume addition of 100h if the P flag is set.

DIV only works if the resulting quotient is < 200h; the output registers contain garbage if the quotient exceeds this.

Bit Rate Reduction (BRR)

BRR is the compression format used by the DSP. It’s a series of 9 byte blocks containing a header and 8 bytes of sample data. It looks like this:

BRR BLOCK
      8     7     6     5     4     3     2     1     0
   +-----+-----+-----+-----+-----+-----+-----+-----+-----+
   | E F | C D | A B | 8 9 | 6 7 | 4 5 | 2 3 | 0 1 | HEAD|
   +-----+-----+-----+-----+-----+-----+-----+-----+-----+
                                               ^
                                  the lower sample is in the high nibble

BRR HEAD
      7     6     5     4     3     2     1     0
   +-----+-----+-----+-----+-----+-----+-----+-----+
   |         RANGE         |   FILTER  |LOOP | END |
   +-----+-----+-----+-----+-----+-----+-----+-----+

RANGE: This is a shift value for the 4-bit data.

FILTER: This selects a filter for the decompression.

LOOP: This flag is set if the sample loops (commercial games set this for all blocks in looped samples).

END: When the decoder reads this bit, it will stop the sample (or restart from loop point) and set a bit in ENDX.

Example Decompression Routine:

NOTICE! This is only an approximation of the decompression routine performed by the DSP, look at some BRR compression source-code (like DMV47’s) for an accurate example!

Sample Decompression Routine:
1. Read 4 bits from source data. (SAMPLE)
2. Shift the bits left by RANGE.
3. Get filter coefficients a&b from table below.
4. Add `LAST_SAMPLE1 * a` to SAMPLE.
5. Add `LAST_SAMPLE2 * b` to SAMPLE.
6. LAST_SAMPLE2 = LAST_SAMPLE1.
7. LAST_SAMPLE1 = SAMPLE.
8. Output SAMPLE.

Filter  Coef A   Coef B
  0 0    0
  1 0.9375   0
  2 1.90625  -0.9375
  3 1.796875 -0.8125

DSP Register Map

- prefix indiciates that the register will be written to by the DSP during activity. For each voice (x=voice#):

AddressRegisterDescription
x0VOL (L) Left channel volume.
x1VOL (R) Right channel volume.
x2P (L) Lower 8 bits of pitch.
x3P (H) Higher 8-bits of pitch.
x4SRCN Source number (0-255). (references the source directory)
x5ADSR (1)If bit7 is set, ADSR is enabled. If cleared GAIN is used.
x6ADSR (2)These two registers control the ADSR envelope.
x7GAIN This register provides function for software envelopes.
x8-ENVX The DSP writes the current value of the envelope to this register. (read it)
x9-OUTX The DSP writes the current waveform value after envelope multiplication and before volume multiplication.
0CMVOL (L)Main Volume (left output)
1CMVOL (R)Main Volume (right output)
2CEVOL (L)Echo Volume (left output)
3CEVOL (R)Echo Volume (right output)
4CKON Key On (1 bit for each voice)
5CKOF Key Off (1 bit for each voice)
6CFLG DSP Flags. (used for MUTE,ECHO,RESET,NOISE CLOCK)
7C-ENDX 1 bit for each voice.
0DEFB Echo Feedback
1D--- Not used
2DPMON Pitch modulation
3DNON Noise enable
4DEON Echo enable
5DDIR Offset of source directory (DIR*100h = memory offset)
6DESA Echo buffer start offset (ESA*100h = memory offset)
7DEDL Echo delay, 4-bits, higher values require more memory.
xFCOEF 8-tap FIR Filter coefficients

DSP Voice Register: VOL

There are some undocumented quirks (emulated though) that I experienced when using these registers (while writing XMSNES). Sometimes the value I write gets ignored.. (or something), I’m not really sure what the exact problem is.

         7     6     5     4     3     2     1     0
      +-----+-----+-----+-----+-----+-----+-----+-----+
$x0   | sign|              Volume Left                |
      +-----+-----+-----+-----+-----+-----+-----+-----+

         7     6     5     4     3     2     1     0
      +-----+-----+-----+-----+-----+-----+-----+-----+
$x1   | sign|              Volume Right               |
      +-----+-----+-----+-----+-----+-----+-----+-----+

Volume, 8-bit signed value.

DSP Voice Register: P

         7     6     5     4     3     2     1     0
      +-----+-----+-----+-----+-----+-----+-----+-----+
$x2   |               Lower 8-bits of Pitch           |
      +-----+-----+-----+-----+-----+-----+-----+-----+

         7     6     5     4     3     2     1     0
      +-----+-----+-----+-----+-----+-----+-----+-----+
$x3   |  -  |  -  |   Higher 6-bits of Pitch          |
      +-----+-----+-----+-----+-----+-----+-----+-----+

Pitch is a 14-bit value.

The pitch can be calculated with this formula:

Let desired DSP pitch be P.

Pitch->Hz
               P
HZ = 32000 * ------
              2^12

(HZ = P * 7.8125)

Hz->Pitch
             HZ
P = 2^12 * -------
            32000

(P = HZ / 7.8125)

The highest pitch will reproduce the sound at approx. 2 octaves higher (~128KHz sample rate). The lowest pitch is basically not limited (but you will lose accuracy at lower pitches).

A few pitch->interval relations...

    400h     800h   1000h     2000h   3FFFh
-----|--------|-------|---------|-------|-----
   -2oct   -1oct   original   +1oct   +2oct
                    sound
                (best quality!)

DSP Voice Register: SRCN

Source number is a reference to the “Source Directory” (see DIR). The DSP will use the sample with this index from the directory. I’m not sure what happens when you change the SRCN when the channel is active, but it probably doesn’t have any effect until KON is set.


         7     6     5     4     3     2     1     0
      +-----+-----+-----+-----+-----+-----+-----+-----+
$x4   |                 Source Number                 |
      +-----+-----+-----+-----+-----+-----+-----+-----+

DSP Voice Register: ADSR

         7     6     5     4     3     2     1     0
      +-----+-----+-----+-----+-----+-----+-----+-----+
$x5   |ENABL|        DR       |          AR           |
      +-----+-----+-----+-----+-----+-----+-----+-----+

         7     6     5     4     3     2     1     0
      +-----+-----+-----+-----+-----+-----+-----+-----+
$x6   |     SL          |          SR                 |
      +-----+-----+-----+-----+-----+-----+-----+-----+

The ENABL bit determines which envelope mode to use. If this bit is set then ADSR is used, otherwise GAIN is operative.

My knowledge about DSP stuff is a bit low. Some enlightenment on how the ADSR works would be greatly appreciated!

Table 2.2 ADSR Parameters

AR  TIME FROM 0->1    DR  TIME FROM 1->SL    SL  RATIO    SR  TIME FROM 0->1
00  4.1 sec           00  1.2 sec            00  1/8      00  Infinite
01  2.5 sec           01  740 msec           01  2/8      01   38 sec
02  1.5 sec           02  440 msec           02  3/8      02   28 sec
03  1.0 sec           03  290 msec           03  4/8      03   24 sec
04  640 msec          04  180 msec           04  5/8      04   19 sec
05  380 msec          05  110 msec           05  6/8      05   14 sec
06  260 msec          06   74 msec           06  7/8      06   12 sec
07  160 msec          07   37 msec           07  8/8      07  9.4 sec
08   96 msec                                              08  7.1 sec
09   64 msec                                              09  5.9 sec
0A   40 msec                                              0A  4.7 sec
0B   24 msec                                              0B  3.5 sec
0C   16 msec                                              0C  2.9 sec
0D   10 msec                                              0D  2.4 sec
0E    6 msec                                              0E  1.8 sec
0F    0 msec                                              0F  1.5 sec
                                                          10  1.2 sec
                                                          11  880 msec
                                                          12  740 msec
     |                                                    13  590 msec
     |                                                    14  440 msec
   1 |--------                                            15  370 msec
     |       /\                                           16  290 msec
     |      /| \                                          17  220 msec
     |     / |  \                                         18  180 msec
     |    /  |   \                                        19  150 msec
   SL|---/---|-----\__                                    1A  110 msec
     |  /    |    |   \___                                1B   92 msec
     | /     |    |       \_________________              1C   74 msec
     |/AR    | DR |           SR            \   t         1D   55 msec
     |-------------------------------------------         1E   37 msec
     0                                      |             1F   18 msec

     key on                                Key off
       (cool ascii picture taken from "APU MANUAL IN TEXT BY LEDI")

DSP Voice Register: GAIN

GAIN can be used to implement custom envelopes in your program. There are 5 modes GAIN uses.

DIRECT

         7     6     5     4     3     2     1     0
      +-----+-----+-----+-----+-----+-----+-----+-----+
$x7   |  0  |               PARAMETER                 |
      +-----+-----+-----+-----+-----+-----+-----+-----+

INCREASE (LINEAR)
         7     6     5     4     3     2     1     0
      +-----+-----+-----+-----+-----+-----+-----+-----+
$x7   |  1  |  1  |  0  |          PARAMETER          |
      +-----+-----+-----+-----+-----+-----+-----+-----+

INCREASE (BENT LINE)

         7     6     5     4     3     2     1     0
      +-----+-----+-----+-----+-----+-----+-----+-----+
$x7   |  1  |  1  |  1  |          PARAMETER          |
      +-----+-----+-----+-----+-----+-----+-----+-----+

DECREASE (LINEAR)

         7     6     5     4     3     2     1     0
      +-----+-----+-----+-----+-----+-----+-----+-----+
$x7   |  1  |  0  |  0  |          PARAMETER          |
      +-----+-----+-----+-----+-----+-----+-----+-----+

DECREASE (EXPONENTIAL)

         7     6     5     4     3     2     1     0
      +-----+-----+-----+-----+-----+-----+-----+-----+
$x7   |  1  |  0  |  1  |          PARAMETER          |
      +-----+-----+-----+-----+-----+-----+-----+-----+

Direct: The value of GAIN is set to PARAMETER.

Increase (Linear): GAIN slides to 1 with additions of 1/64.

Increase (Bent Line): GAIN slides up with additions of 1/64 until it reaches 3/4, then it slides up to 1 with additions of 1/256.

Decrease (Linear): GAIN slides down to 0 with subtractions of 1/64.

Decrease (Exponential): GAIN slides down exponentially by getting multiplied by 255/256.

Table 2.3 Gain Parameters (Increate 0 -> 1 / Decrease 1 -> 0):

Parameter ValueIncrease LinearIncrease BentlineDecrease LinearDecrease Exponential
00INFINITEINFINITEINFINITEINFINITE
014.1s7.2s4.1s38s
023.1s5.4s3.1s28s
032.6s4.6s2.6s24s
042.0s3.5s2.0s19s
051.5s2.6s1.5s14s
061.3s2.3s1.3s12s
071.0s1.8s1.0s9.4s
08770ms1.3s770ms7.1s
09640ms1.1s640ms5.9s
0A510ms900ms510ms4.7s
0B380ms670ms380ms3.5s
0C320ms560ms320ms2.9s
0D260ms450ms260ms2.4s
0E190ms340ms190ms1.8s
0F160ms280ms160ms1.5s
10130ms220ms130ms1.2s
1196ms170ms96ms880ms
1280ms140ms80ms740ms
1364ms110ms64ms590ms
1448ms84ms48ms440ms
1540ms70ms40ms370ms
1632ms56ms32ms290ms
1724ms42ms24ms220ms
1820ms35ms20ms180ms
1916ms28ms16ms150ms
1A12ms21ms12ms110ms
1B10ms18ms10ms92ms
1C8ms14ms8ms74ms
1D6ms11ms6ms55ms
1E4ms7ms4ms37ms
1F2ms3.5ms2ms18ms

DSP Voice Register: ENVX

ENVX gets written to by the DSP. It contains the present ADSR/GAIN envelope value.

ENVX
         7     6     5     4     3     2     1     0
      +-----+-----+-----+-----+-----+-----+-----+-----+
$x8   |  0  |                 VALUE                   |
      +-----+-----+-----+-----+-----+-----+-----+-----+

7-bit unsigned value

DSP Voice Register: OUTX

OUTX is written to by the DSP. It contains the present wave height multiplied by the ADSR/GAIN envelope value. It isn’t multiplied by the voice volume though.

OUTX
         7     6     5     4     3     2     1     0
      +-----+-----+-----+-----+-----+-----+-----+-----+
$x9   | sign|                 VALUE                   |
      +-----+-----+-----+-----+-----+-----+-----+-----+

8-bit signed value

DSP Register: MVOL/EVOL

Main/Echo volume! 8-bit signed values. Regular sound is scaled by the main volume. Echoed sound is scaled by the echo volume.

I also had a problem with writing to these registers, sometimes my writes would get ignored, or zeroed?

         7     6     5     4     3     2     1     0
      +-----+-----+-----+-----+-----+-----+-----+-----+
$0C   | sign|         Left Output Main Volume         |
      +-----+-----+-----+-----+-----+-----+-----+-----+

         7     6     5     4     3     2     1     0
      +-----+-----+-----+-----+-----+-----+-----+-----+
$1C   | sign|        Right Output Main Volume         |
      +-----+-----+-----+-----+-----+-----+-----+-----+

         7     6     5     4     3     2     1     0
      +-----+-----+-----+-----+-----+-----+-----+-----+
$2C   | sign|         Left Output Echo Volume         |
      +-----+-----+-----+-----+-----+-----+-----+-----+

         7     6     5     4     3     2     1     0
      +-----+-----+-----+-----+-----+-----+-----+-----+
$3C   | sign|        Right Output Echo Volume         |
      +-----+-----+-----+-----+-----+-----+-----+-----+

DSP Registers: KON/KOF

Writing bits to KON will start/restart the voice specified. Writing bits to KOF will cause the voice to fade out. The fade is done with subtraction of 1/256 values and takes about 8msec.

It is said that you should not write to KON/KOF in succession, you have to wait a little while (a few NOPs).

Key-On
         7     6     5     4     3     2     1     0
      +-----+-----+-----+-----+-----+-----+-----+-----+
$4C   |VOIC7|VOIC6|VOIC5|VOIC4|VOIC3|VOIC2|VOIC1|VOIC0|
      +-----+-----+-----+-----+-----+-----+-----+-----+
Key-Off
         7     6     5     4     3     2     1     0
      +-----+-----+-----+-----+-----+-----+-----+-----+
$5C   |VOIC7|VOIC6|VOIC5|VOIC4|VOIC3|VOIC2|VOIC1|VOIC0|
      +-----+-----+-----+-----+-----+-----+-----+-----+

DSP Register: FLG

Flags
         7     6     5     4     3     2     1     0
      +-----+-----+-----+-----+-----+-----+-----+-----+
$6C   |RESET|MUTE |~ECEN|         NOISE CLOCK         |
      +-----+-----+-----+-----+-----+-----+-----+-----+

RESET: Soft reset. Writing a ‘1’ here will set all voices in a state of “Key-On suspension” (???). MUTE is also set. A soft-reset gets triggered upon power-on.

MUTE: Mutes all channel output.

ECEN: ~Echo enable. A ‘0’ here enables echo data to be written into external memory (the memory your program/data is in!). Be careful when enabling it, it’s quite easy to crash your program with the echo hardware!

NOISE CLOCK: Designates the frequency for the white noise.

ValueFrequency
000 Hz
0116 Hz
0221 Hz
0325 Hz
0431 Hz
0542 Hz
0650 Hz
0763 Hz
0883 Hz
09100 Hz
0A125 Hz
0B167 Hz
0C200 Hz
0D250 Hz
0E333 Hz
0F400 Hz
10500 Hz
11667 Hz
12800 Hz
131.0 KHz
141.3 KHz
151.6 KHz
162.0 KHz
172.7 KHz
183.2 KHz
194.0 KHz
1A5.3 KHz
1B6.4 KHz
1C8.0 KHz
1D10.7 KHz
1E16 KHz
1F32 KHz

DSP Register: ENDX

This register is written to during DSP activity.

Each voice gets 1 bit. If the bit is set then it means the BRR decoder has reached the last compressed block in the sample.

ENDX
         7     6     5     4     3     2     1     0
      +-----+-----+-----+-----+-----+-----+-----+-----+
$7C   |VOIC7|VOIC6|VOIC5|VOIC4|VOIC3|VOIC2|VOIC1|VOIC0|
      +-----+-----+-----+-----+-----+-----+-----+-----+

DSP Register: EFB

Writing to this register sets the Echo Feedback. It’s an 8-bit signed value. Some more information on how the feedback works would be nice.

EFB
         7     6     5     4     3     2     1     0
      +-----+-----+-----+-----+-----+-----+-----+-----+
$0D   | sign|             Echo Feedback               |
      +-----+-----+-----+-----+-----+-----+-----+-----+

DSP Register: PMON

PMON
         7     6     5     4     3     2     1     0
      +-----+-----+-----+-----+-----+-----+-----+-----+
$2D   |VOIC7|VOIC6|VOIC5|VOIC4|VOIC3|VOIC2|VOIC1|  -  |
      +-----+-----+-----+-----+-----+-----+-----+-----+

Pitch modulation multiplies the current pitch of the channel by OUTX of the previous channel. (P (modulated) = P[X] * (1 + OUTX[X-1])

So a sine wave in the previous channel would cause some vibrato on the modulated channel. Note that OUTX is before volume multiplication, so you can have a silent channel for modulating.

DSP Register: NON

Noise enable.

NON
         7     6     5     4     3     2     1     0
      +-----+-----+-----+-----+-----+-----+-----+-----+
$3D   |VOIC7|VOIC6|VOIC5|VOIC4|VOIC3|VOIC2|VOIC1|VOIC0|
      +-----+-----+-----+-----+-----+-----+-----+-----+

When the noise enable bit is specified for a certain channel, white noise is issued instead of sample data. The frequency of the white noise is set in the FLG register. The white noise still requires a (dummy) sample to determine the length of sound (or unlimited sound if the sample loops).

DSP Register: EON

Echo enable.

EON
         7     6     5     4     3     2     1     0
      +-----+-----+-----+-----+-----+-----+-----+-----+
$4D   |VOIC7|VOIC6|VOIC5|VOIC4|VOIC3|VOIC2|VOIC1|VOIC0|
      +-----+-----+-----+-----+-----+-----+-----+-----+

This register enables echo effects for the specified channel(s).

DSP Register: DIR

Source Directory Offset.

DIR
         7     6     5     4     3     2     1     0
      +-----+-----+-----+-----+-----+-----+-----+-----+
$5D   |                  Offset value                 |
      +-----+-----+-----+-----+-----+-----+-----+-----+

This register points to the source(sample) directory in external RAM. The pointer is calculated by Offset100h.

The source directory contains sample start and loop point offsets. Its a simple array of 16-bit values.

SAMPLE DIRECTORY

OFFSET  SIZE    DESC
dir+0   16-BIT  SAMPLE-0 START
dir+2   16-BIT  SAMPLE-0 LOOP START
dir+4   16-BIT  SAMPLE-1 START
dir+6   16-BIT  SAMPLE-1 LOOP START
dir+8   16-BIT  SAMPLE-2 START
...
This can continue for up to 256 samples. (SRCN can only reference 256 samples)

DSP Register: ESA

Echo data start address.

ESA
         7     6     5     4     3     2     1     0
      +-----+-----+-----+-----+-----+-----+-----+-----+
$6D   |                  Offset value                 |
      +-----+-----+-----+-----+-----+-----+-----+-----+

This register points to an area of memory to be used by the echo buffer. Like DIR its value is multiplied by 100h.

DSP Register: EDL

Echo delay size.

EDL
         7     6     5     4     3     2     1     0
      +-----+-----+-----+-----+-----+-----+-----+-----+
$7D   |  -  |  -  |  -  |  -  |      Echo Delay       |
      +-----+-----+-----+-----+-----+-----+-----+-----+

EDL specifies the delay between the main sound and the echoed sound. The delay is calculated as EDL * 16ms.

Increased amounts of delay require more memory. The amount of memory required is EDL * 2KBytes (MAX $7800 bytes). The memory region used will be [ESA*100h] -> [ESA*100h + EDL*800h -1]. If EDL is zero, 4 bytes of memory at [ESA*100h] -> [ESA*100h + 3] will still be used.

DSP Register: COEF

Echo FIR filter coefficients.

COEF
         7     6     5     4     3     2     1     0
      +-----+-----+-----+-----+-----+-----+-----+-----+
$xF   | sign|         Filter Coefficient X            |
      +-----+-----+-----+-----+-----+-----+-----+-----+

The 8-bytes at $0F,$1F,$2F,$3F,$4F,$5F,$6F,$7F are used by the 8-tap FIR filter. I really have no idea how FIR filters work… but the filter is applied to the echo output.

DSP Echo Function

Ah, the spotlight of SPC music, the echoing! A short review of the registers used:

EVOL: This is the volume multiplier of the echo output. Note that even if the Main volume is 0, the echo volume can still be heard. It is independant from the main volume.

FLG: Flags register. The “Echo Enable” bit is contained in here.

EFB: Echo Feedback. This is an 8-bit signed value used for feedback multiplication.

EON: Echo Channel Enable. One bit per channel to indicate that it should be echoed.

ESA: Echo Start Address. A pointer to the area of memory designated for the echoing buffer.

EDL: Echo Delay. 4-bit value that designates both the delay time for the echo, and the size of the echoing buffer. (d16ms time) (d2KB memory)

COEF: Echo FIR Filter Coefficients. Again, I have no idea how the filter works. A setting of 127,0,0,0,0,0,0,0 will result in the original sound.

To enable echoing:

Initialization, order of operation is not of concern
*. Set ESA to memory region to be used.
*. Set EDL to desired echo delay. You will require 2KB*EDL of space for the echoing buffer.
*. Set the filter coefficients. A setting of 127,0,0,0,0,0,0,0 won't affect the sound output.
*. Set Echo Feedback.
*. Enable echo for desired channels (EON).

Startup:
1. Delay 240ms ***see below***.
2. Set EVOL to desired volume.
3. Write '0' to the ECEN bit in the FLG register.
4. Echo is enabled.
5. Freely change all parameters (except ESA,EDL).

*** This is because the echo hardware doesn’t actually read the buffer designation values until it reaches the END of the old buffer! (the old buffer sizes could be uninitialized garbage). There is no way to determine the write position. 240ms is the max delay time, so it is the safe value to use. Another method is to read EDL (d) before you set it, and delay d16ms.

Another special note: Echo writing DOES wrap around the memory edge, I had my program crash when the ESA was at the very end of memory (and I didn’t delay properly). It wrote to the end… and then wrapped around to munch all my DP memory all the way to the program code.

PS: Lower your headphone volume when playing with the EFB register.

Precautions

Don’t use an excessive sound data compression ratio.
Samples with low sample rates may cause distortion. And they don’t convert to BRR very well.

Take care that the mixed output doesn’t exceed the maximum waveform limit.
If the mixed waveform (all 8-channels together) overflows, some distortion noise will be produced.

Check for monaural output!
This will mess up your cool ‘surreal’ effect when your setting one side of the channel volume to negative.

Sustain continuous sample flow.
Sampled data that isn’t continuous will cause ‘clicking’ sounds to appear. A very obvious example is when you disable a channel by cutting the volume. A quick fade-out should be used instead.

IPL ROM

The IPL ROM is a 64 byte image that is built into the SPC to handle data transfers from the SNES. This code is ran at power-on/reset. Here is the code for the IPL ROM:

; Commented SPC-700 IPL ROM
; by eKid

; Original dissasembly from SID-SPC written by Alfatech/Triad

; This code assembles with TASM

;-------------------------------------------------------------------------------
; DEFINITIONS
;-------------------------------------------------------------------------------

WriteAdr    =$00    ; Write address during transfers
Port0       =$F4    ; I/O ports
Port1       =$F5
Port2       =$F6
Port3       =$F7

.ORG $FFC0
;-------------------------------------------------------------------------------
Start:
;-------------------------------------------------------------------------------

; set stack pointer to $EF
;   "why EF? page1 has 256 memory bytes!"
;   because the value in X is reused in the page0 clear (saves 2 bytes)
;   the higher 16 bytes of page0 contain hardware registers.

    mov x, #$EF
    mov sp, x

; clear zero-page memory

    mov a, #$00
clrpg0: mov (x), a
    dec x
    bne clrpg0

; indicate ready signal, write 0BBAAh to ports 0/1

    mov Port0, #$AA
    mov Port1, #$BB

; idle until the SNES sends the transfer signal to port0 ($CC)
; and then process data

wait1:  cmp $F4, #$CC
    bne wait1

    bra ProcessData

;-------------------------------------------------------------------------------
TransferData:
;-------------------------------------------------------------------------------

; wait until Port0 gets zero written to it

wait2:  mov y, Port0
    bne wait2

; this is the main transfer loop

transfer_bytes:
    cmp y, Port0    ; check for data
    bne check_end

    mov a, Port1    ; read byte of data
    mov Port0, y    ; reply to SNES (snes can write new data now)
    mov [WriteAdr]+Y, A ; write data to memory
    inc y       ; increment index
    bne transfer_bytes  ; loop

; index overflowed, increment high byte of WriteAdr
    inc WriteAdr+1

check_end:

; if y - port0 < 0 then the transfer is complete (SNES added 2 or more)

    bpl transfer_bytes

; confirm this! we may have checked with invalid data
; also, this is used when the "inc WriteAdr+1" path is taken
;         (when transferring to $8000 or higher)

    cmp y, Port0
    bpl transfer_bytes

; transfer is finished, process data again

;-------------------------------------------------------------------------------
ProcessData:
;-------------------------------------------------------------------------------

; read word from ports 2/3
; word may be data write address,
; or program entry point (depending on port0)

    movw    ya, Port2
    movw    WriteAdr, ya
    movw    ya, $F4
    mov Port0, a    ; reply to SNES with PT0 data
    mov a, y
    mov x, a

; if port1 wasn't zero, then start the transfer

    bne TransferData

; otherwise...
; jump to program entry point
; X is zero in this case, so this
; is an effective "movw pc, WriteAdr"
    jmp [WriteAdr+X]

;-------------------------------------------------------------------------------
ResetVector:
;-------------------------------------------------------------------------------
    di
    stop

; When program flow is passed to the user code, the Accumulator
; and X/Y index registers are zero, and the SP is initialized to $EF.
; Also, page0 memory is cleared. (EXCEPT for the word at $00)

.end

Uploading Software

Here is the upload protocol:

SPC SIDE     |   SNES SIDE
---------------------------------------------------
PORT0=0AAh  >>>                                    | Stage 1, "is the spc ready to receive data??"
PORT1=0BBh  >>>  Confirm 0AABBh                    |
---------------------------------------------------
            <<<  PORT1 = NOT0                      | Stage 2, initialize transfer
            <<<  PORT2/3 = TRANSFER ADDRESS LO/HI  |
            <<<  PORT0 = 0CCh                      |
PORT0=PT0   >>>  (confirm this)                    | PT0 is data received in port0 on the SPC side
---------------------------------------------------| the spc will mimic stuff you put in port0
                DATA TRANSFER START                |
---------------------------------------------------|
            <<<  PORT1 = FIRST BYTE OF DATA        | Stage 3, start transfer
            <<<  PORT0 = 00h                       |
PORT0=PT0   >>>  (confirm this)                    |
---------------------------------------------------
            <<<  PORT1 = DATA                      | Stage 4, transfer data
            <<<  PORT0 = LAST PORT0 DATA +1        |
PORT0=PT0   >>>  (confirm this)                    |
--------------------------------------------------------
Sending the next block...                               |
            <<<  PORT1 = NOT 0                          | Stage 5, next block
            <<<  PORT2/3 = TRANSFER ADDRESS             |
            <<<  PORT0 = PREVIOUS PORT0 DATA + 2 to 127 | NOTE: this value cannot be zero, add another 1 or so if it is
PORT0=PT0   >>>  (confirm this)                         |
                 jump to DATA TRANSFER START            |
------------------------------------------------------------
After the last block...                                     |  Stage 6, start program
            <<<  PORT1 = 0                                  |
            <<<  PORT2/3 = PROGRAM ENTRY POINT              |
            <<<  PORT0 = PREVIOUS PORT0 DATA + 2 to 127     |
PORT0=PT0   >>>  (confirm this)                             |
                 (assume the code is running successfully)  |
------------------------------------------------------------

Warning! If this routine is interrupted, the SPC may time-out! (or something) Your program will then get stuck in an infinite loop of waiting for the SPC reply. Inhibit interrupts during the transfer protocol.

Have a look at my booting procedure (does not support multiple blocks):

;--------------------------------------------
snakBoot:
;--------------------------------------------

; wait for ready signal

-   ldx REG_APUI00
    cpx #0BBAAh
    bne -

; start transfer:
; port1   = !0
; port2/3 = transfer address ( 0200h )
; port0   = 0CCh

    lda #1
    sta REG_APUI01
    inc a
    stz REG_APUI02
    sta REG_APUI03

    lda #0CCh
    sta REG_APUI00

; wait for SPC

-   cmp REG_APUI00
    bne -

; ready to transfer...

    lda.w   snakd_binary    ; read first byte
    xba
    lda #0
    ldx #1
    bra sb_start

; transfer bytes

sb_send:
    xba

    lda.w   snakd_binary,x  ; read byte
    inx
    xba
-   cmp REG_APUI00
    bne -

    ina         ; add 1 to counter

sb_start:
    rep #20h
    sta REG_APUI00  ; write to port1
    sep #20h

    cpx #snakd_size ; all bytes transferred?
    bcc sb_send     ; no, then send next byte

; all bytes transferred

-   cmp REG_APUI00  ; sync
    bne -

    ina         ; add 2 or so...
    ina

    stz REG_APUI01  ; port1=0
    ldx #0200h      ; write program entry point
    stx REG_APUI02
    sta REG_APUI00  ; write validation

-   cmp REG_APUI00  ; final sync
    bne -

; snakdriver is installed

    rtl
  1. Wait for the word in ports0/1 to read BBAA
  2. Write a nonzero value in port1 and the transfer address in ports 2/3.
  3. Write 0CCh to port0.
  4. Wait for the SPC to mimic the data it receives in port0 (0CCh in this case). The loader only uses port0 for replies.
  5. Start a counter at 00h, write the first byte of data to port1, and write the counter to port0.
  6. Wait for the SPC to reply with 00h (yes, it mimics).
  7. Write the next data byte to port1, increment the counter, write the counter to port0.
  8. Wait for the SPC to reply with the counter value.
  9. Goto 7 if there are more bytes to be transferred.
  10. Write 0 to port1, the program entry point to port2/3, and then finally… add 2 (or more) to the counter and write to port0.
  11. (Optional) Wait for the SPC to mimic that last byte…

Based on SPCTECH by eKid (EFNet - #snesdev) and Anomie’s docs.