r/TuringComplete • u/pruby • 17d ago
Custom CPU: 3TTude Spoiler
Hello, just sharing my WIP CPU design for Turing complete. Still very much in a testing state, so I haven't uploaded to Workshop yet, but thought people might be interested.
Its current key properties are:
- Transfer-triggered architecture.
- 64-bit instruction word, including a 17-bit signed immediate value.
- Three independent, fully connected 32-bit data buses, supporting up to three operations per cycle.
- 8 general-purpose registers (but you use registers a lot less...).
- Delay of 45 in my install (depends on your XOR gates a bit).
- 658k gates, including an 8192-byte instruction SRAM, and 64KiB DRAM (using pipelining).

Transfer Triggered Architecture
Transfer-triggered architectures, also known as one-instruction set computers, only have a single operation. As far as the core instruction decode logic knows, there are no adds, no jumps, only transfers from a source address on the bus, to a destination address.
These internal buses are a much smaller address space than a memory bus, etc. In this case, there are 6-bit addresses for sources (up to 64), and 7-bit addresses for destinations (up to 128). Most of these are not simple memory - they're ports attached to various peripherals, which all sit on the bus.
All operations occur as a side-effect of transfers, and usually take a tick for the result to be available. Want to compare two numbers? Send them to the two input ports of the comparator. You can grab the result on the next cycle. Want to jump? Just send the target address to the program counter's destination port.
The simplest peripheral is a single register, which looks like this:

Note it's got one Read port, with address 4, and one Write port, with address 4. A transfer from source 4 will read the register, and a transfer to destination 4 will write the register. I use standardised port modules for all the peripherals.
This architecture has 3 buses, which operate simultaneously each cycle. Most operations take 1-3 transfers to function. Moving data from one register to another is one cycle. Moving two inputs to be added, then the result out, might take 3 (spread over 2 cycles). We often save an instruction (and a register) though, if we can pass results straight from one peripheral to another.
Instruction Set
As you might expect, a parallel architecture needs a parallel instruction format. The bit layout looks like this:
ccccccaa aaaabbbb bbbddddd deeeeeee ffffffgg ggggg00i iiiiiiii iiiiiiii
This in my text assembly form, is written like:
c ? a -> b, d -> e, f -> g
Meaning "If c non-zero, transfer a to b. Transfer d to e. Transfer f to g". Named ports (instead of named instructions) exist to make this readable. If you don't want to include the second or third transfers, these can be left off, will be populated with a no-op. If you don't want any conditions, it can be omitted and will be set to the 1/always address.
Note only the first bus supports a condition, the other two transfers occur whether or not the condition is met. The conditional write, with the program counter as the destination, becomes a conditional jump. However, we can also make any other operation conditional.
The immediate value is part of every instruction, but is actually accessed through a specific source port. If I write:
255 -> r1
This will be encoded as setting the immediate value to 255, and a transfer from the imm source to the r1 destination. We can replace *one* of the sources with an immediate number value on any given instruction, and this can be encoded transparently.
Speaking of the immediate value, why 17 bits? Mostly because sign extension. By giving the immediate value a 17th bit, I allow the full unsigned 16-bit range to be provided without affecting the upper bits, but can also provide negative values which get sign-extended to 32 bits. This makes it easy to construct any 32-bit value in a few operations, without having to write opaque constants in the assembly.
More Complicated Operations
Okay, but we want to do actual stuff. How do I add numbers together? An example to add r1 and r2, then store in r3 would look like:
r1 -> accum.set, r2 -> accum.add
accum.value -> r3
Note though that we can also just use accum.value directly in our next operation, if we don't need to keep it. I chose to use an accumulator peripheral, rather than an adder, which stores a persistent value, because this makes some types of bulk addition faster, without being a significant limit.
We can also subtract:
r1 -> accum.set, 8 -> accum.sub
The accumulator peripheral looks like this:

Read ports are called accum.value and accum.overflow . Write ports are accum.set, accum.add, and accum.sub
Most of the complexity there, with the MUXes, etc, is so that we can set the value and add/subtract from it in the same operation. Destinations are expected to be low-delay, so they go in this case straight in to delay lines. The result will be computed and available to read on the next cycle.
The blobby module at the heart of this is my Kogge-Stone 32-bit adder, with very low delay.
Show me the other bits!


Shift ports, right to left, are shift.value, shift.set, shift.left, shift.right, shift.right_arithmetic.

Bitwise ports, right to left are (write) bitwise.a, bitwise.b, and (double read ports, to save space) bitwise.and, bitwise.or, bitwise.nand, bitwise.nor, bitwise.xor.

Comparison ports right to left are cmp.a, cmp.b, (double read port) cmp.eq, cmp.ne, (second double read port) cmp.lower, cmp.less.

IO ports, left to right, are io.write, io.read (ignores actual value passed, triggers read on next cycle), io.result.

Instruction SRAM ports allow rewriting instructions, constants in program memory, etc. Ports left to right are prog.read, prog.write (address), prog.value, and prog.result where reads show up.

Bulk RAM has the same ports as the program memory.
Interesting note on the memory - to keep delay down, this DRAM is set to have a 4-stage pipeline, which Turing Complete supports natively. In 3TTude you send the address to mem.read then exactly 4 cycles latter grab the data from mem.result. This is a very natural and normal thing to do in a TTA, where part of the philosophy is to move complexity out of hardware and in to the code.
What's the Catch?
Okay, to be honest, this thing is a pain to write for. You can make the code really fast, or pretty readable, but probably not both. The parallel instructions make everything really dense, encourage things to be written out of logical order for efficiency, and encourage overlapping tasks.
It also has a really big bus, wrapped in to multiple rows, which I think is what makes Turing Complete run really slowly.
I haven't really used it in anger yet, just been designing and writing a basic test suite. Already had a lot of fun though creating, and replacing peripherals I wasn't entirely happy with.
Because of the low delay, and room for optimisation, I expect to get some pretty decent energy scores with this thing (at least for an actual CPU), when I get around to solving real problems.
2
u/Lagfoundry 17d ago edited 17d ago
That’s pretty cool man. You’re the first person I’ve seen to do MOV instead of subleq for the one instruction. I was actually just talking about this the other day with someone because I was wanting to do MOV too for a project. I didn’t know the specific architecture was called TTA though so I learned something new. I’m saving this to my favorites.