This post sums up everything I've learned about the Roland/Toshiba TC170C140 ESP. Some of it comes from the Usual Suspects' presentation at 39C3, some from studying and running the JE-8086 code, and a little bit from the JP8000 service manual
Hardware
The TC170C140 ESP, from now on referred to as 'the DSP', has the following specs:
Two cores, each with:
- 24 x 8bit multiplier *
- Two accumulators
- Programs of 768 instructions
- 256 words of 24 bit internal RAM
- 3 pipeline slots for IRAM write **
One of the cores can communicate with external DRAM, referred to as ERAM. It looks like this is core 2.
Separate program memory areas per core
PR0: 24 bit, starts at 0x0000 in emulator. Used by core 1
PR1: 28 bit, starts at 0x0400 in emulator. Used by core 2
* The DSP does multiply high, so in practice multiplication can be thought of as multiplying two 24bit numbers but only using the 8MSBs of the second number, then keeping the upper 24 bits of the result.
** Not sure what they mean by this, perhaps the accumulators as they are pipelines with length 3
Memory
IRAM - Internal RAM
- 256 words of 24 bit internal RAM, addressed as delay line/circular buffer
GRAM - Probably global RAM
- 256 words of 24 bit shared internal RAM (GRAM), also used for audio I/O and communications between the DSPs, at least in the emulator
- Shared between cores
IRAM/GRAM is not accessible from the host interface, but in the emulator at least, values are copied between DSPs directly from GRAM to GRAM.
ERAM - External Ram (DRAM)
- Only one core can access external RAM, from the program memory formats it looks like it's core 2
Accumulators
- both circular buffers with 3 steps
Instruction/bytecode format:
E = ERAM control, only found on core 2
P = op
M = mem
S = shiftbits
C = coefficient, a signed 8-bit integer
x = unused?
Bit format (28 bits) - usually only 23 are used:
EEEE EPPPPPMM MMMMMMSS CCCCCCCC
- In the DSP code,
op = (instr >> 16) & 0x7C
e.g.
0b0PPPPP00
so all op codes are shown as * 4
Each instruction performs a multiply and accumulate (MAC) with variable shift right (3,5,6,7 bits):
acc += (mulInputA * mulInputB) >> shift
or
acc = (mulInputA * mulInputB) >> shift
Special instruction:
0x372800 -> sends result to CP // WHAT IS CP
Other instructions:
- Store accA or accB to IRAM
- Store saturated or unsaturated (unsigned)
- Read/write GRAM
- Multiplication per variable
- Double precision multiplication ("DMAC")
- Rectify clamp, interpolate helpers
- Control Flow
- Jump always/zero/pos/neg*
- Skip the following instructions based on condition
* Jump is never used in oscillator code/DSP 1 it seems, but perhaps in others?
The chip is in general extremely similar to LSP (See: "Sound Chip, whisper me your secrets", gulaschprogrammiernacht 23)
- same instruction format, memory addressing, external memory control, coefficient size
MAC
Every instruction ends with a multiply and accumulate (MAC):
acc += factor1 * factor2 >> shift
The multiplication is 24 x 8 bits
To add a number without multiplying, one of the factors must be reduced to 1 by the shift operation.
acc += factor * 1
To not change anything, the second factor can be set to 0
acc += factor * 0
This is used for most of the memory write functions.
The MAC result can either overwrite or be added to either of the accumulators.
The multiplier is usually used as a 24 x 24 bit multiply high where the second factor only has 7 bit precision (1 sign + 7 value bits + 16 0s).
Can do 14bit precision with an additional instruction (DMAC).
Can do up-to-24bit multiply-by-coefficient by running a special multCoeff instruction, then up to thee DMAC instructions with their separate 8 bit coefficients.
IRAM
IRAM is circular and the pointer to head is decremented for every full run of the program memory
Within a single run you can read and write to the same position
Between runs you can get the previous version by reading the previous address
Accumulator
ACC is also a circular buffer, but a bit weird. For set and add the value from the previous instruction will be used directly, and the value is stored at the current head position
Reading saturated or raw however, reads the previous value at the same position as head, which is the result of MAC three instructions back.
Also note, that if the accumulator is not changed by the instruction, the previous value is copied. After three steps, all the positions in the acc are the same, so in reality the value you read is the most recent write three OR MORE steps back. This also means that two additional changes to the accumulator may have happened since the value you read, you just don't have access to them yet - or, at least not the one two steps back, the previous one is available when doing MAC.
The pipeline is advanced for every instruction
GRAM
Communication between DSPs are through GRAM, output is found at x and inserted into y
Parameter changes
The code runs 88200 times per second, so it would be inefficient to always write parameters to IRAM. Instead, parameter changes happen directly by changing the instruction in program memory and are written to IRAM if needed.
Each instruction can only take 8 data bits, so larger coefficients are set (and multiplied into one) by additional instructions.
How the JP8000 uses the DSP
Algorithm changes
Since program memory is limited, only the program for the current type of oscillator is kept in the DSP. Whenever the user switches oscillator type (super saw, feedback oscillator etc), a whole code block is changed.
Things that change the code in DSP 1:
0x006b to 0x00b9 - Oscillator 2 algorithm // Core 1
0x043c to 0x049e - Oscillator 1 algorithm // Core 2
0x0005 to 0x0008 - Ring modulator
0x0068 to 0x00b2 - Sync // surrounds osc 2 code
Input parameters, these are written as program code changes.
0x0044: Oscillator balance
0x0400: X-mod depth
0x0404: Unknown coefficient 1
0x040e: Unknown coefficient 2
0x0420: Unknown coefficient 3
0x0425: Unknown coefficient 4
0x0430: Unknown coefficient 5
0x041b: Pitch, stored in IRAM pos 0x65
0x043f: Detune
0x0442: Mix
PS: LFOs and other pitch modifiers are done in the MCU and set as parameters after combination with the pitch.
Other interesting addresses
0x0454: Osc 1: Store updated value to iram1[0x05]
0x0455: Osc 2: Multiply pitch and detune, 14bit precision, and add
0x0459: Osc 3: Multiply pitch and detune, 14bit precision
0x045b: Osc 2: Store updated value to iram1[0x07]
0x045c: Osc 3: Load pitch, add -1 * pitch * detune (from 0x0459++)
0x045f: Osc 4: Multiply pitch and detune, 14bit precision
0x0461: Osc 3: Store updated value to iram1[0x09]
0x0462: Osc 4: Load pitch, add (pitch * detune (from 0x045f++) * 120)
0x0465: Osc 5: Multiply pitch and detune, 14bit precision
0x0467: Osc 4: Store updated value to iram1[0x0b]
0x0468: Osc 5: Load pitch, add (pitch * detune (from 0x0465++) *
0x046b: Osc 6: Multiply pitch and detune, 14bit precision
0x046d: Osc 5: Store updated value to iram1[0x0d]
0x046e: Osc 6: Load pitch, add (pitch * detune (from 0x046b++) *
0x0471: Osc 7: Multiply pitch and detune, 14bit precision
0x0473: Osc 6: Store updated value to iram1[0x0f]
0x0474: Osc 7: Load pitch, add (pitch * detune (from 0x0471++) *
0x0479: Osc 7: Store updated value to iram1[0x11]
0x047a: Set B = (Osc 7 * (mix >> 16)) >> 7
0x047b: B += (Osc 5 * (mix >> 16)) >> 7
0x047c: B += (Osc 3 * (mix >> 16)) >> 7
0x047d: B += (Osc 1 * 25) >> 7
0x047e: B += (Osc 2 * (mix >> 16)) >> 7
0x047f: B += (Osc 4 * (mix >> 16)) >> 7
0x0480: B += (Osc 6 * (mix >> 16)) >> 7
0x047a: Set B = (Osc 7 * (mix >> 16)) >> 7
Communication between DSPs
Communication between DSP and DAC
Where does the code run
On DSP1, and perhaps on the others as well:
Core 1 is oscillator 2
Core 2 is oscillator 1
DSP 1 generates 3 voices
DSP 2 generates 3 voices and passes the three from DSP1 straight through to the output
DSP 3 generates the remaining 2 voices. Has 8 outputs, not sure if filter and envelopes are here or in DSP 4.
DSP 4 is likely FX and output summing.
Pitfalls when trying to understand the code
Acc is a circular buffer, so the value read by sat/raw is not the previous value, it is the last time the accumulator changed three or more steps back.
IRAM is circular, so reading and writing to the same cell in different iterations happens at consecutive addresses. Important when searching for the source of a value that is read, it may be at address+1.
The DSP is 24bit, so all variables are 24bit and overflow as such. The multiplication intermediate result however, before shifting, is 32bit.
The DSP is fixed point, so think of numbers as decimal numbers having a range +/-1.0 even if they're represented as ints.
The multiplication is usually thought of as a multiply high 24 x 24 bits, but with bit 15:0 of the second factor set to 0 - it has 7 bit precision.
How I debugged the code
I approached the code in two different ways: Disassembling/reading the machine code, step debugging and looking at the output wave files
I used the jeTestConsole app as a an entry point - it boots the JE8086 in headless mode and outputs the display text to the terminal. I extended it with keyboard event handling to be able to change stuff on the fly.
On startup of the script, I set the je8086 to a known state. It boots up as a normal JP8000, so by pressing EXIT and WRITE at the same time it enters manual mode:
device.getJe8086().setButton(devices::kSwitch_Exit, true); device.getJe8086().setButton(devices::kSwitch_Write, true);
The device boots up in split keyboard mode. While I tried to change this, for some reason it prevented detune/mix param values from arriving at the correct positions in the DSP. I have not investigated this further and I don't know if it was true when using midi to set the params (I exposed a setFader method similar to the setButton). Changing keyboard mode was however useful when figuring out what voices are calculated in what DSP.
I then switched to a known midi note so I could look at the pitch parameter.
The rest of the control was done interactively - setting detune and mix to known values, dumping memory etc.
Things I did to make debugging easier
- Added asicId, coreId and the current value of the program counter, including the address offset for core 2, to the esp code. That made it possible to set conditional breakpoints in a single core on a single DSP, and easier to visually match the pc to a print out of the program memory.
- Added auto-calculation of high-res coefficients, iram contents before and after execution to make it easier to read when running the debugger.
- Added debug output to writeuC, the function where MCU writes to DSP. There are three modes of writing to the program memory:
- Mode 0x54: Raw Program Memory Write (program changes)
- Mode 0x55: Coefficient Update (parameter values)
- Mode 0x56: ERAM Control Bits Update
- Added way to dump/print disassembled pcmem on demand. The dump function is already included in both je8086devices.h and the DSP code.
- Extended the disassembler.
- Per-line comments, so running dump repeatedly added my own comments at the right lines
- More accurate description of what happens per instruction type, including multi-line comments if more than one thing happens.
- Human readable coefficient values as comments and directly in descriptions of instructions
- Hiding lines with opcode 0
- Adding blank lines when the accumulator was cleared, to automatically group instructions that belong together.
- Auto-calculation of address of the last write to an accumulator when reading sat/raw, as these are often way back in the program memory.
- Added a way to get midi commands to the je8086
- Added a keyboard handler that could change parameters like detune, mix, pitch etc through midi parameterChange, as well as execute dump.
- For interactive debugging I switched from the JIT to interpreter version of the ESP, as it's way easier to read the full code than the JIT version. This change is done on line 149 in je8086devices.h
Example of disassembled output
| Multi-line comment to make it easier to see exactly how kMulCoef is calculated |
Opcodes w. description
List of all opcodes
Normal opcodes
0x30 - special
0x34 - special
0x6: DMAC - double precision multiplication
0xa: Write saturated accumulator to readback reg, read by uC
0xc:











No comments:
Post a Comment