Wednesday, February 11, 2026

A deepdive into the JP8000 and TC170C140 ESP

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

0x0424: store pitch to iram1[0x65]
0x0454: Osc 1: Store updated value to iram1[0x05]
0x0455: Osc 2: Multiply pitch and detune, 14bit precision, and add
               pitch + previous value for osc.
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++) 
               + previous value
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)
               >> 5 + previous value
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++) * 
               -102) >> 5 + previous value
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++) * 
               44) >> 3 + previous value
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++) *
               -45) >> 3 + previous value
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 

DSPs are called asic in the code
 
3 addresses are copied, e.g. the output of 3 voices.
0x80 is in voice group 1, 0x82 and 0x84 in voice group 2 when starting up
 
DSP1.GRAM[0x80 + k] => DSP2.GRAM[k] 
  
6 addresses are copied, the output of the 3 first is equal to the input from DSP2 
0x86 and 0x8a  is in voice group 1, 0x88 in voice group 2 when starting up  
DSP2.GRAM[0x80 + k] => DSP3.GRAM[k]
 
Not sure what is copied here: 
DSP2.GRAM[0xa0] => DSP3.GRAM[0x20]
DSP2.GRAM[0xa2] => DSP3.GRAM[0x22] 
 
8 addresses are copied, likely the 8 voices. Not sure if the signal at this point contains envelopes and filter 
DSP3.GRAM[0x80 + k] => DSP3.GRAM[k]
 

Communication between DSP and DAC 

called postSample in the code, reads GRAM 0xe8 and 0xec from DSP3
 
To hear output of the other DSPs, change to 0x80 + k on DSP1/2/3. 
 
 

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 

 

Fields, left to right: address in program memory, hex representation of instruction, [] = instruction split into fields, o=op code, m=memory address, s=shift bits, c=coefficient. Then store-to-ram operation if any. After the first |: op code and op code name, then the original disassembly-comment. Finally, my own description of the code. For sat(A) etc, there's an additional @ [0xXXXX], that's the address in program memory where the value of A was calculated.

Multi-line comment to make it easier to see exactly how kMulCoef is calculated


Opcodes w. description

The instructions are as described earlier in this post. This tells how to interpret each of them. 

List of all opcodes

 


 

Normal opcodes  

All codes except for 0x20 to 0x34 follow these rules:
 
shift is always set by shiftbits 
mulInputB is always coefficient
mulInputA has a default mapping, but may also be set more explicitly
result is written to either accumulator A or B
result either clears the current value of (A=) or adds to (A+=) the accumulator at the end
 
Each operation ends by doing 
acc = mulInputA * mulInputB >> shift (or +=)
 
Defaults for mulInputA
 
mem


10x1016
20x4001024
30x1000065536
40x4000004194304
default
iram[mempos]
 
Shiftbits 
 
the value of shiftbits selects the number of places to right-shift the result: 
0: 7
1: 6
2: 5
3: 3
 
Illustration of the three interp functions:
 
The the three variations of interp are a bit hard to understand. Here is a plot of each one:
 
interpStorePos

interpStoreNeg

interp

 

0x30 - special

Coefficient decides operation:
 
Coefficient format:
B = source for mulInputB - mulcoeffs, eram eller mulcoeffs 5??
F = Flips B amplitude around a midpoint between 0 and 1 (1-x) 
I = invert B 
L = If 1: Load mulInputA, if "weird"*: detect sign. 
    Save to iram[mempos]. 
    If 0: uses default mulInputA
A = Accumulator 
C = !Clear

Coeff: BBBFILAC 

* weird: Coeff =xxx111xx
 

0x34 - special

Mem decides operation:
 
mem & 0xF0 = 0xA0: 
 
4 LSB: MMMA, M = mulcoeff index, A = Accumulator 
Stores saturated accumulator to mulCoeffs 

mem > 0xC0:
 
Mem format: 
C: Clear
A: Accumulator
P: Operation
 
11CAPPPP 
 
Commands reads coefficient to jump etc
 
0x0: Jump on 0
0x1: Jump on < 0
0x2: Jump on > 0
0x3: Jump
0x4: Set INT pins
0x6: DMAC - double precision multiplication
0x7: Sets ERAM address
0xa: Write saturated accumulator to readback reg, read by uC
0xb: Write to ERAM
0xc:
0xd:
0xe:
0xf: Read from ERAM to IRAM[mem|0xF0] and to mulInputA

 

Parameter values

The tables below show the values send to the DSP when setting a MIDI CC or NOTE value. They are also found in my previous post, in a more copy-paste friendly format.
 

Detune 

 
First column is the midi value, the second is what the uC probably works on while the last, ESP, is what is sent to the DSP. 


 

Mix

First column is the midi value, the second what is sent to the DSP. 
 




Pitch

First column is MIDI value, second is what is sent to the DSP. The third, "actual", is the result of the pitch formula in my post about the correct version of the supersaw code. The last column is the difference between the two, showing the pitch error. For the lowest notes it is 1/1647, well below what we're able to hear. It increases as we go up the scale, but that's not a big problem, the relative difference is probably not increasing
 

Sunday, February 8, 2026

JP80x0 midi to coefficient mapping tables

Pitch

MidiPitch
01555
11647
21745
31849
41959
52075
62198
72329
82468
92615
102770
112935
123110
133295
143491
153698
163918
174151
184397
194658
204936
215230
225541
235871
246220
256590
266982
277396
287836
298302
308794
319317
329873
3310460
3411082
3511742
3612441
3713181
3813965
3914792
4015672
4116604
4217589
4318635
4419747
4520921
4622164
4723485
4824883
4926362
5027930
5129585
5231345
5333209
5435179
5537270
5639494
5741842
5844329
5946970
6049767
6152725
6255860
6359171
6462691
6566418
6670358
6774541
6878988
6983684
7088659
7193941
7299534
73105450
74111720
75118342
76125382
77132836
78140716
79149082
80157976
81167368
82177318
83187882
84199068
85210900
86223440
87236684
88250764
89265672
90281432
91298164
92315952
93334736
94354636
95375764
96398136
97421800
98446880
99473368
100501528
101531344
102562864
103596328
104631904
105669472
106709272
107751528
108796272
109843600
110893760
111946736
1121003056
1131062688
1141125728
1151192656
1161263808
1171338944
 

Detune:

MidiuC
01
11
22
32
43
53
64
74
85
95
106
116
127
137
148
158
169
179
1810
1910
2011
2111
2212
2312
2413
2513
2614
2714
2815
2915
3016
3116
3217
3317
3418
3518
3619
3719
3820
3920
4021
4121
4222
4322
4423
4523
4624
4724
4825
4925
5026
5126
5227
5327
5428
5528
5629
5729
5830
5930
6031
6131
6232
6332
6433
6534
6635
6736
6837
6938
7039
7140
7241
7342
7443
7544
7645
7746
7847
7948
8049
8151
8253
8355
8457
8559
8661
8763
8865
8967
9069
9171
9273
9375
9477
9579
9681
9783
9885
9987
10089
10191
10293
10395
10497
10599
106101
107103
108105
109107
110109
111111
112113
113115
114117
115119
116121
117123
118125
119127
120129
121137
122145
123153
124169
125193
126225
127321
 

Mix

MidiMix
0102400
1118784
2135168
3151552
4167936
5184320
6200704
7217088
8233472
9249856
10266240
11282624
12299008
13315392
14331776
15348160
16364544
17380928
18397312
19413696
20430080
21446464
22462848
23479232
24495616
25512000
26528384
27544768
28561152
29577536
30593920
31610304
32626688
33643072
34659456
35675840
36692224
37708608
38724992
39741376
40757760
41774144
42790528
43806912
44823296
45839680
46856064
47872448
48888832
49905216
50921600
51937984
52954368
53970752
54987136
551003520
561019904
571036288
581052672
591069056
601085440
611101824
621118208
631134592
641150976
651167360
661183744
671200128
681216512
691232896
701249280
711265664
721282048
731298432
741314816
751331200
761347584
771363968
781380352
791396736
801413120
811429504
821445888
831462272
841478656
851495040
861511424
871527808
881544192
891560576
901576960
911593344
921609728
931626112
941642496
951658880
961675264
971691648
981708032
991724416
1001740800
1011757184
1021773568
1031789952
1041806336
1051822720
1061839104
1071855488
1081871872
1091888256
1101904640
1111921024
1121937408
1131953792
1141970176
1151986560
1162002944
1172019328
1182035712
1192052096
1202068480
1212084864
1222101248
1232117632
1242134016
1252150400
1262166784
1272183168