SPC-700 Reference
An SPC-700 document that's not a decade old!
A bit of this manual may be a straight copy, but I'm trying to keep an original perspective.
You will find blocks of text with different background colors:
Green blocks will contain structure specifications, tables,
or other technical info.
|
Blue blocks will contain psuedo code, or instructions how
to do something.
|
Red blocks will contain warning messages. Things that should
be kept in mind while programming.
|
Purple blocks will contain information that I am not sure
is correct.
|
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 |
$0000->$00EF Page 0
$00F0->$00FF Registers
$0100->$01FF Page 1
$0200->$FFBF Memory
If X bit is set in the Undoc register:
$FFC0->$FFFF Memory (write only)
$FFC0->$FFFF 64 byte IPL ROM (read only)
Otherwise:
$FFC0->$FFFF Memory (read/write)
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:
$F0 Undocumented ?/W
$F1 Control Register /W
$F2 DSP Register Address R/W
$F3 DSP Register Data R/W
$F4 Port 0 R/W
$F5 Port 1 R/W
$F6 Port 2 R/W
$F7 Port 3 R/W
$F8 Regular memory R/W
$F9 Regular memory R/W
$FA Timer-0 /W
$FB Timer-1 /W
$FC Timer-2 /W
$FD Counter-0 R/
$FE Counter-1 R/
$FF Counter-2 R/
Anomie has written something that shows some use of this 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. |
I was a little confused about the I/O ports at first... (because I'm a
noob) but they're not too hard to understand. 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.
|
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
|
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 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, #$FA ; 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
|
There are two assemblers I know of.
WLA-DX
This is the assembler I use for SNES code. I also used it to write some
NES code (s3m->nsf), and a bit of Gameboy code. It hasn't failed
me... until I tried to use it for SPC-700 code, it assembled "mov a, dp"
with dp as a relative offset.... I gave up and went back to TASM for
spc assembly.
TASM - Table Driven Assembler
This is a pretty cool assembler that uses instruction tables during
assembly. I used GAU's instruction sheet when developing the XMSNES
driver (there were a few errors in it, but not too bad). I revised it a
bit for my new driver though.
GAU's tutorial
Revised SPC-700 sheet
Sure it's a lot of fun programming the SPC... BUT NOT DEBUGGING IT! There are almost no good debuggers for the SPC!
Here are the emulators I used:
SPCTool
Pros:
- SPC Debugger
- DSP Editor, very useful
- Profiler
- Accurate playback
Cons:
- I find the debugger a bit hard to use (maybe I just need to get used to it more)
- No SNES! It only emulates the SPC.
Snes9x/ZSNES/BSNES:
Pros:
Cons:
- No debugger!
- BSNES has a hard time running full speed on my machine
Snes9x debug version
Pros:
- SNES<->SPC emulation
- SNES Debugger
- Can log activity of SNES/SPC/DSP
- Hex viewer
Cons:
- A bit outdated
- No direct SPC debugging. (I hope you enjoy looking through gigantic log files and using "inf: bra inf" everywhere)
SNESAmp
Pros:
- Alpha-II in winamp :)
- Listen to SPCs easily.
Cons:
- This is not a debugger whatsoever.
Hardware Testing
The problem with emulation is that it's quite impossible to recreate the SNES environment perfectly. I got a flashcart from TOTOTEK
for hardware testing. This is the worst part of development, I spent
days trying to fix problems that appeared on the hardware. (days of
writing data to the SPC ports as a means of debugging (including
rewriting the cart over 100 times)). The problems experienced when
hardware testing can sometimes be really, really hard to fix.
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 (0100h -> 01FFh). 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.
Program Status Word
7 6 5 4 3 2 1 0
+-----+-----+-----+-----+-----+-----+-----+-----+
| N | V | P | B | H | I | Z | C |
+-----+-----+-----+-----+-----+-----+-----+-----+
|
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)
I never used this flag either. I'm not really sure what it does.
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.
??? 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.
How to read this chart
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)
INSTR OPERAND OPCODE BYTES CYCLES NVPBHIZC Operation
MOV A, #imm E8 2 2 N-----Z- A <- imm
MOV A, (X) E6 1 3 N-----Z- A <- (X)
MOV A, (X)+ BF 1 4 N-----Z- A <- (X), X is incremented afterward
MOV A, dp E4 2 3 N-----Z- A <- (dp)
MOV A, dp+X F4 2 4 N-----Z- A <- (dp+X)
MOV A, !abs E5 3 4 N-----Z- A <- (abs)
MOV A, !abs+X F5 3 5 N-----Z- A <- (abs+X)
MOV A, !abs+Y F6 3 5 N-----Z- A <- (abs+Y)
MOV A, [dp+X] E7 2 6 N-----Z- A <- (abs:(abs+X))
MOV A, [dp]+Y F7 2 6 N-----Z- A <- (abs:(abs)+Y)
MOV X, #imm CD 2 2 N-----Z- X <- imm
MOV X, dp F8 2 3 N-----Z- X <- (dp)
MOV X, dp+Y F9 2 4 N-----Z- X <- (dp+Y)
MOV X, !abs E9 3 4 N-----Z- X <- (abs)
MOV Y, #imm 8D 2 2 N-----Z- Y <- imm
MOV Y, dp EB 2 3 N-----Z- Y <- (dp)
MOV Y, dp+X FB 2 4 N-----Z- Y <- (dp+X)
MOV Y, !abs EC 3 4 N-----Z- Y <- (abs)
8-bit Data Transmission (Write)
INSTR OPERAND OPCODE BYTES CYCLES NVPBHIZC Operation
MOV (X), A C6 1 4 -------- A -> (X)
MOV (X)+, A AF 1 4 -------- A -> (X), X is incremented
MOV dp, A C4 2 4 -------- A -> (dp)
MOV dp+X, A D4 2 5 -------- A -> (dp+X)
MOV !abs, A C5 3 5 -------- A -> (abs)
MOV !abs+X, A D5 3 6 -------- A -> (abs+X)
MOV !abs+Y, A D6 3 6 -------- A -> (abs+Y)
MOV [dp+X], A C7 2 7 -------- A -> (abs:(dp+X))
MOV [dp]+Y, A D7 2 7 -------- A -> (abs:(dp)+Y)
MOV dp, X D8 2 4 -------- X -> (dp)
MOV dp+Y, X D9 2 5 -------- X -> (dp+Y)
MOV !abs, X C9 3 5 -------- X -> (abs)
MOV dp, Y CB 2 4 -------- Y -> (dp)
MOV dp+X, Y BB 2 5 -------- Y -> (dp+X)
MOV !abs, Y CC 3 5 -------- Y -> (abs)
8-bit Data Transmission (Reg->Reg, Mem->Mem )
INSTR OPERAND OPCODE BYTES CYCLES NVPBHIZC Operation
MOV A, X 7D 1 2 N-----Z- A <- X
MOV A, Y DD 1 2 N-----Z- A <- Y
MOV X, A 5D 1 2 N-----Z- A -> X
MOV Y, A FD 1 2 N-----Z- A -> Y
MOV X, SP 9D 1 2 N-----Z- X <- SP
MOV SP, X BD 1 2 -------- X -> SP
MOV dp, dp FA 3 5 -------- (dp) <- (dp)
MOV dp, #imm 8F 3 5 -------- (dp) <- imm
8-bit Arithmetic
INSTR OPERAND OPCODE BYTES CYCLES NVPBHIZC Operation
ADC A, #imm 88 2 2 NV--H-ZC A += imm + C
ADC A, (X) 86 1 3 NV--H-ZC A += (X) + C
ADC A, dp 84 2 3 NV--H-ZC A += (dp) + C
ADC A, dp+X 94 2 4 NV--H-ZC A += (dp+X) + C
ADC A, !abs 85 3 4 NV--H-ZC A += (abs) + C
ADC A, !abs+X 95 3 5 NV--H-ZC A += (abs+X) + C
ADC A, !abs+Y 96 3 5 NV--H-ZC A += (abs+Y) + C
ADC A, [dp+X] 87 2 6 NV--H-ZC A += (abs:(dp+X)) + C
ADC A, [dp]+Y 97 2 6 NV--H-ZC A += (abs:(dp)+Y) + C
ADC (X),(Y) 99 1 5 NV--H-ZC (X) += (Y) + C
ADC dp, dp 89 3 6 NV--H-ZC (dp) += (dp) + C
ADC dp, #imm 98 3 5 NV--H-ZC (dp) += imm + C
SBC A, #imm A8 2 2 NV--H-ZC A -= imm + !C
SBC A, (X) A6 1 3 NV--H-ZC A -= (X) + !C
SBC A, dp A4 2 3 NV--H-ZC A -= (dp) + !C
SBC A, dp+X B4 2 4 NV--H-ZC A -= (dp+X) + !C
SBC A, !abs A5 3 4 NV--H-ZC A -= (abs) + !C
SBC A, !abs+X B5 3 5 NV--H-ZC A -= (abs+X) + !C
SBC A, !abs+Y B6 3 5 NV--H-ZC A -= (abs+Y) + !C
SBC A, [dp+X] A7 2 6 NV--H-ZC A -= (abs:(dp+X)) + !C
SBC A, [dp]+Y B7 2 6 NV--H-ZC A -= (abs:(dp)+Y) + !C
SBC (X), (Y) B9 1 5 NV--H-ZC (X) -= (Y) + !C
SBC dp, dp A9 3 6 NV--H-ZC (dp) -= (dp) + !C
SBC dp, #imm B8 3 5 NV--H-ZC (dp) -= imm + !C
CMP A, #imm 68 2 2 NV----ZC A - imm
CMP A, (X) 66 1 3 NV----ZC A - (X)
CMP A, dp 64 2 3 NV----ZC A - (dp)
CMP A, dp+X 74 2 4 NV----ZC A - (dp+X)
CMP A, !abs 65 3 4 NV----ZC A - (abs)
CMP A, !abs+X 75 3 5 NV----ZC A - (abs+X)
CMP A, !abs+Y 76 3 5 NV----ZC A - (abs+Y)
CMP A, [dp+X] 67 2 6 NV----ZC A - (abs:(dp+X))
CMP A, [dp]+Y 77 2 6 NV----ZC A - (abs:(dp)+Y)
CMP (X), (Y) 79 1 5 NV----ZC (X) - (Y)
CMP dp, dp 69 3 6 NV----ZC (dp) - (dp)
CMP dp, #imm 78 3 5 NV----ZC (dp) - imm
CMP X, #imm C8 2 2 NV----ZC X - imm
CMP X, dp 3E 2 3 NV----ZC X - (dp)
CMP X, !abs 1E 3 4 NV----ZC X - (abs)
CMP Y, #imm AD 2 2 NV----ZC Y - imm
CMP Y, dp 7E 2 3 NV----ZC Y - (dp)
CMP Y, !abs 5E 3 4 NV----ZC Y - (abs)
8-bit Logical Operations
INSTR OPERAND OPCODE BYTES CYCLES NVPBHIZC Operation
AND A, #imm 28 2 2 N-----Z- A &= imm
AND A, (X) 26 1 3 N-----Z- A &= (X)
AND A, dp 24 2 3 N-----Z- A &= (dp)
AND A, dp+X 34 2 4 N-----Z- A &= (dp+X)
AND A, !abs 25 3 4 N-----Z- A &= (abs)
AND A, !abs+X 35 3 5 N-----Z- A &= (abs+X)
AND A, !abs+Y 36 3 5 N-----Z- A &= (abs+Y)
AND A, [dp+X] 27 2 6 N-----Z- A &= (abs:(dp+X))
AND A, [dp]+Y 37 2 6 N-----Z- A &= (abs:(dp)+Y)
AND (X), (Y) 39 1 5 N-----Z- (X) &= (Y)
AND dp, dp 29 3 6 N-----Z- (dp) &= (dp)
AND dp, #imm 38 3 5 N-----Z- (dp) &= imm
OR A, #imm 08 2 2 N-----Z- A |= imm
OR A, (X) 06 1 3 N-----Z- A |= (X)
OR A, dp 04 2 3 N-----Z- A |= (dp)
OR A, dp+X 14 2 4 N-----Z- A |= (dp+X)
OR A, !abs 05 3 4 N-----Z- A |= (abs)
OR A, !abs+X 15 3 5 N-----Z- A |= (abs+X)
OR A, !abs+Y 16 3 5 N-----Z- A |= (abs+Y)
OR A, [dp+X] 07 2 6 N-----Z- A |= (abs:(dp+X))
OR A, [dp]+Y 17 2 6 N-----Z- A |= (abs:(dp)+Y)
OR (X), (Y) 19 1 5 N-----Z- (X) |= (Y)
OR dp, dp 09 3 6 N-----Z- (dp) |= (dp)
OR dp, #imm 18 3 5 N-----Z- (dp) |= imm
EOR A, #imm 48 2 2 N-----Z- A ^= imm
EOR A, (X) 46 1 3 N-----Z- A ^= (X)
EOR A, dp 44 2 3 N-----Z- A ^= (dp)
EOR A, dp+X 54 2 4 N-----Z- A ^= (dp+X)
EOR A, !abs 45 3 4 N-----Z- A ^= (abs)
EOR A, !abs+X 55 3 5 N-----Z- A ^= (abs+X)
EOR A, !abs+Y 56 3 5 N-----Z- A ^= (abs+Y)
EOR A, [dp+X] 47 2 6 N-----Z- A ^= (abs:(dp+X))
EOR A, [dp]+Y 57 2 6 N-----Z- A ^= (abs:(dp)+Y))
EOR (X), (Y) 59 1 5 N-----Z- (X) ^= (Y)
EOR dp, dp 49 3 6 N-----Z- (dp) ^= (dp)
EOR dp, #imm 58 3 5 N-----Z- (dp) ^= imm
8-bit Increment/Decrement Operations
INSTR OPERAND OPCODE BYTES CYCLES NVPBHIZC Operation
INC A BC 1 2 N-----Z- ++A
INC dp AB 2 4 N-----Z- ++(dp)
INC dp+X BB 2 5 N-----Z- ++(dp+X)
INC !abs AC 3 5 N-----Z- ++(abs)
INC X 3D 1 2 N-----Z- ++X
INC Y FC 1 2 N-----Z- ++Y
DEC A 9C 1 2 N-----Z- --A
DEC dp 8B 2 4 N-----Z- --(dp)
DEC dp+X 9B 2 5 N-----Z- --(dp+X)
DEC !abs 8C 3 5 N-----Z- --(abs)
DEC X 1D 1 2 N-----Z- --X
DEC Y DC 1 2 N-----Z- --Y
8-bit Shift/Rotation Operations
INSTR OPERAND OPCODE BYTES CYCLES NVPBHIZC Operation
ASL A 1C 1 2 N-----ZC C << A << 0
ASL dp 0B 2 4 N-----ZC C << (dp) << 0
ASL dp+X 1B 2 5 N-----ZC C << (dp+X) << 0
ASL !abs 0C 3 5 N-----ZC C << (abs) << 0
LSR A 5C 1 2 N-----ZC 0 >> A >> C
LSR dp 4B 2 4 N-----ZC 0 >> (dp) >> C
LSR dp+X 5B 2 5 N-----ZC 0 >> (dp+X) >> C
LSR !abs 4C 3 5 N-----ZC 0 >> (abs) >> C
ROL A 3C 1 2 N-----ZC C << A << C :the last carry value is shifted
ROL dp 2B 2 4 N-----ZC C << (dp) << C :into A, not the one you just shifted out!
ROL dp+X 3B 2 5 N-----ZC C << (dp+X) << C :
ROL !abs 2C 3 5 N-----ZC C << (abs) << C :
ROR A 7C 1 2 N-----ZC C >> A >> C :same with these
ROR dp 6B 2 4 N-----ZC C >> (dp) >> C :
ROR dp+X 7B 2 5 N-----ZC C >> (dp+X) >> C :
ROR !abs 6C 3 5 N-----ZC C >> (abs) >> C :
XCN A 9F 1 5 N-----Z- Swaps the nibbles in A (A = (A>>4) | (A<<4))
16-bit Data Transmission Operations
INSTR OPERAND OPCODE BYTES CYCLES NVPBHIZC Operation
MOVW YA, dp BA 2 5 N-----Z- YA <- word:(dp)
MOVW dp, YA DA 2 4 -------- YA -> word:(dp) :same cycles as writing 1 byte!
16-bit Arithmetic Operations
INSTR OPERAND OPCODE BYTES CYCLES NVPBHIZC Operation
INCW dp 3A 2 6 N-----Z- ++word:(dp)
DECW dp 1A 2 6 N-----Z- --word:(dp)
ADDW YA, dp 7A 2 5 NV--H-ZC YA += word:(dp)
SUBW YA, dp 9A 2 5 NV--H-ZC YA -= word:(dp)
CMPW YA, dp 5A 2 4 N-----ZC YA - word:(dp)
Multiplication/Division Operations
INSTR OPERAND OPCODE BYTES CYCLES NVPBHIZC Operation
MUL YA CF 1 9 N-----Z- YA <- Y*A
DIV YA,X 9E 1 12 NV--H-Z- Y <- YA % X and A <- YA / X
Decimal Compensation Operations
INSTR OPERAND OPCODE BYTES CYCLES NVPBHIZC Operation
DAA A DF 1 3 N-----ZC decimal adjust for addition
DAS A BE 1 3 N-----ZC decimal adjust for subtraction
Program Flow Operations
INSTR OPERAND OPCODE BYTES CYCLES NVPBHIZC Operation
BRA rel 2F 2 4 -------- Branch (always) : branch always is slower than jump,
BEQ rel F0 2 2/4 -------- Branch if Equal (Z=1) : but branches uses relative addressing
BNE rel D0 2 2/4 -------- Branch if Not Equal (Z=0) : (2 bytes instead of 3)
BCS rel B0 2 2/4 -------- Branch if Carry Set
BCC rel 90 2 2/4 -------- Branch if Carry Cleared
BVS rel 70 2 2/4 -------- Branch if V=1
BVC rel 50 2 2/4 -------- Branch if V=0
BMI rel 30 2 2/4 -------- Branch if Negative (N=1)
BPL rel 10 2 2/4 -------- Branch if Positive (N=0)
BBS dp,bit,rel x3 3 5/7 -------- Branch if memory bit set
BBC dp,bit,rel y3 3 5/7 -------- Branch if memory bit cleared
CBNE dp, rel 2E 3 5/7 -------- Branch if A != (dp)
CBNE dp+X,rel DE 3 6/8 -------- Branch if A != (dp+X)
DBNZ dp,rel 6E 3 5/7 -------- --(dp) and branch if not zero
DBNZ Y,rel FE 2 4/6 -------- --Y and branch if not zero
JMP !abs 5F 3 3 -------- PC <- abs : allows to jump anywhere in the memory space
JMP [!abs+X] 1F 3 6 -------- PC <- abs:(abs+X)
Subroutine Operations
INSTR OPERAND OPCODE BYTES CYCLES NVPBHIZC Operation
CALL !abs 3F 3 8 -------- Subroutine call :pushes PC to stack and begins execution from abs
PCALL upage 4F 2 6 -------- Upage call (???)
TCALL n n1 1 8 -------- Table call (??:)
BRK 0F 1 8 ---1-0-- Software interrupt (???)
RET 6F 1 5 -------- Return from subroutine (PC is popped)
RETI 7F 1 6 RESTORED Return from interrupt (PC and PSW are popped)
Stack Operations
INSTR OPERAND OPCODE BYTES CYCLES NVPBHIZC Operation
PUSH A 2D 1 4 -------- Push A to stack
PUSH X 4D 1 4 -------- Push X to stack
PUSH Y 6D 1 4 -------- Push Y to stack
PUSH PSW 0D 1 4 -------- Push PSW to stack
POP A AE 1 4 -------- Pop A from stack
POP X CE 1 4 -------- Pop X from stack
POP Y EE 1 4 -------- Pop Y from stack
POP PSW 8E 1 4 RESTORED Pop PSW from stack :can be used to set PSW bits
Bit Operations
INSTR OPERAND OPCODE BYTES CYCLES NVPBHIZC Operation
SET1 dp, bit x2 2 4 -------- Set bit in direct page : note that with the TASM table provided, these
CLR1 dp, bit y2 2 4 -------- Clear bit in direct page : instructions are done with "SETx/CLRx dp" where
: where x is the bit#
TSET1 !abs 0E 3 6 N-----Z- Test and set bits with A (???)
TCLR1 !abs 4E 3 6 N-----Z- Test and clear bits with A (???)
AND1 C,mem,bit 4A 3 4 -------C C &= mem:bit :to use these instructions
AND1 C,/mem,bit 6A 3 4 -------C C &= ~mem:bit :with the TASM table
OR1 C,mem,bit 0A 3 5 -------C C |= mem:bit :the syntax is a bit wierd
OR1 C,/mem,bit 2A 3 5 -------C C |= ~mem:bit : "for MOV1 mem,bit,C" it is:
EOR1 C,mem,bit 8A 3 5 -------C C ^= mem:bit : MOV1 (mem+(bit<<13)),C
NOT1 mem,bit EA 3 5 -------- Complement mem:bit
MOV1 C,mem,bit AA 3 4 -------C C <- mem:bit
MOV1 mem,bit,C CA 3 6 -------- C -> mem:bit
PSW Operations
INSTR OPERAND OPCODE BYTES CYCLES NVPBHIZC Operation
CLRC 60 1 2 -------0 Clear Carry
SETC 80 1 2 -------1 Set Carry
NOTC ED 1 3 -------c Complement Carry
CLRV E0 1 2 -0--0--- Clear V and H
CLRP 20 1 2 --0----- Clear DP page to 0
SETP 40 1 2 --1----- Set DP page to 1
EI A0 1 3 ------1- Enable Interrupts (but interrupts are not supported)
DI C0 1 3 ------0- Disable Interrupts (but interrupts are not supported)
Other Commands
INSTR OPERAND OPCODE BYTES CYCLES NVPBHIZC Operation
NOP 00 1 2 -------- Delay
SLEEP EF 1 3 -------- standby SLEEP mode
STOP FF 1 3 -------- standby STOP mode (a good way to crash the program? :)
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.
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
|
- prefix indiciates that the register will be written to by the DSP during activity.
Addr | Register | Description
For each voice (x=voice#):
x0 | VOL (L) | Left channel volume.
x1 | VOL (R) | Right channel volume.
x2 | P (L) | Lower 8 bits of pitch.
x3 | P (H) | Higher 8-bits of pitch.
x4 | SRCN | Source number (0-255). (references the source directory)
x5 | ADSR (1) | If bit7 is set, ADSR is enabled. If cleared GAIN is used.
x6 | ADSR (2) | These two registers control the ADSR envelope.
x7 | GAIN | 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.
---------------------------------------------------------------------
0C | MVOL (L) | Main Volume (left output)
1C | MVOL (R) | Main Volume (right output)
2C | EVOL (L) | Echo Volume (left output)
3C | EVOL (R) | Echo Volume (right output)
4C | KON | Key On (1 bit for each voice)
5C | KOF | Key Off (1 bit for each voice)
6C | FLG | DSP Flags. (used for MUTE,ECHO,RESET,NOISE CLOCK)
7C | -ENDX | 1 bit for each voice.
---------------------------------------------------------------------
0D | EFB | Echo Feedback
1D | --- | Not used
2D | PMON | Pitch modulation
3D | NON | Noise enable
4D | EON | Echo enable
5D | DIR | Offset of source directory (DIR*100h = memory offset)
6D | ESA | Echo buffer start offset (ESA*100h = memory offset)
7D | EDL | Echo delay, 4-bits, higher values require more memory.
---------------------------------------------------------------------
xF | COEF | 8-tap FIR Filter coefficients
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.
|
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
+-----+-----+-----+-----+-----+-----+-----+-----+
$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!)
|
7 6 5 4 3 2 1 0
+-----+-----+-----+-----+-----+-----+-----+-----+
$x4 | Source Number |
+-----+-----+-----+-----+-----+-----+-----+-----+
|
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
+-----+-----+-----+-----+-----+-----+-----+-----+
$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! (I will need to learn when I
code the envelopes in my new driver)
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")
|
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 PARAMTERS
TIME FROM 0->1 TIME FROM 1->0
PARAM INCREASE INCREASE DECREASE DECREASE
VALUE LINEAR BENTLINE LINEAR EXPONENTIAL
00 INFINITE INFINITE INFINITE INFINITE
01 4.1s 7.2s 4.1s 38s
02 3.1s 5.4s 3.1s 28s
03 2.6s 4.6s 2.6s 24s
04 2.0s 3.5s 2.0s 19s
05 1.5s 2.6s 1.5s 14s
06 1.3s 2.3s 1.3s 12s
07 1.0s 1.8s 1.0s 9.4s
08 770ms 1.3s 770ms 7.1s
09 640ms 1.1s 640ms 5.9s
0A 510ms 900ms 510ms 4.7s
0B 380ms 670ms 380ms 3.5s
0C 320ms 560ms 320ms 2.9s
0D 260ms 450ms 260ms 2.4s
0E 190ms 340ms 190ms 1.8s
0F 160ms 280ms 160ms 1.5s
10 130ms 220ms 130ms 1.2s
11 96ms 170ms 96ms 880ms
12 80ms 140ms 80ms 740ms
13 64ms 110ms 64ms 590ms
14 48ms 84ms 48ms 440ms
15 40ms 70ms 40ms 370ms
16 32ms 56ms 32ms 290ms
17 24ms 42ms 24ms 220ms
18 20ms 35ms 20ms 180ms
19 16ms 28ms 16ms 150ms
1A 12ms 21ms 12ms 110ms
1B 10ms 18ms 10ms 92ms
1C 8ms 14ms 8ms 74ms
1D 6ms 11ms 6ms 55ms
1E 4ms 7ms 4ms 37ms
1F 2ms 3.5ms 2ms 18ms
|
ENVX
7 6 5 4 3 2 1 0
+-----+-----+-----+-----+-----+-----+-----+-----+
$x8 | 0 | VALUE |
+-----+-----+-----+-----+-----+-----+-----+-----+
7-bit unsigned value
|
ENVX gets written to by the DSP. It contains the present ADSR/GAIN envelope value.
OUTX
7 6 5 4 3 2 1 0
+-----+-----+-----+-----+-----+-----+-----+-----+
$x9 | sign| VALUE |
+-----+-----+-----+-----+-----+-----+-----+-----+
8-bit signed value
|
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.
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 |
+-----+-----+-----+-----+-----+-----+-----+-----+
|
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???
|
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|
+-----+-----+-----+-----+-----+-----+-----+-----+
|
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).
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.
VALUE | FREQ | VALUE | FREQ | VALUE | FREQ | VALUE | FREQ |
00 | 0 Hz
| 08 | 83 Hz
| 10 | 500 Hz
| 18 | 3.2 KHz |
01 | 16 KHz
| 09 | 100 Hz
| 11 | 667 Hz
| 19 | 4.0 KHz |
02 | 21 Hz
| 0A | 125 Hz
| 12 | 800 Hz
| 1A | 5.3 KHz |
03 | 25 Hz
| 0B | 167 Hz
| 13 | 1.0 KHz
| 1B | 6.4 KHz |
04 | 31 Hz
| 0C | 200 Hz
| 14 | 1.3 KHz
| 1C | 8.0 KHz |
05 | 42 Hz
| 0D | 250 Hz
| 15 | 1.6 KHz
| 1D | 10.7 KHz |
06 | 50 Hz
| 0E | 333 Hz
| 16 | 2.0 KHz
| 1E | 16 KHz |
07 | 63 Hz
| 0F | 400 Hz
| 17 | 2.7 KHz
| 1F | 32 KHz |
This register is written to during DSP activity.
ENDX
7 6 5 4 3 2 1 0
+-----+-----+-----+-----+-----+-----+-----+-----+
$7C |VOIC7|VOIC6|VOIC5|VOIC4|VOIC3|VOIC2|VOIC1|VOIC0|
+-----+-----+-----+-----+-----+-----+-----+-----+
|
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.
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 |
+-----+-----+-----+-----+-----+-----+-----+-----+
|
Pitch modulation, I have no idea how it works, but here is a guess..
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.
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).
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).
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 Offset*100h.
The source directory contains sample start and loop point offsets. Its a simple array of 16-bit values.
Basic structure:
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
...................................etc
This can continue for up to 256 samples. (SRCN can only reference 256 samples)
|
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.
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. The memory region used will be [ESA] -> [ESA + EDL*800h -1]. If EDL is zero, 4 bytes of memory at [ESA] -> [ESA+3] will still be used.
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.
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. (d*16ms time) (d*2KB 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).
|
Why the big red delay?? 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 d*16ms.
|
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. ;)
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.
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...(almost a copy :)
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
|
In English:
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...
|
LONG LIVE THE SNES AND ITS COPROCESSOR!
This has been fun... Special thanks to all the snesdev community for their information and help!
If you have found any errors, please tell me! I typed this up pretty quickly and didn't do much proofreading.
Questions, Comments, Mistakes, Anything:
EMAIL: mukunda51 [AT] hotmail [DOT] com
IRC: eKid on EFNet - #snesdev
Last updated: 9:22 PM 11/15/2007 |