Episode: 4614
Title: Dauug|18: Faster Than a '286, but Inspectable Like a Soroban
Source: https://hub.hackerpublicradio.org/ccdn.php?filename=/eps/hpr4614/hpr4614.mp3
Transcribed: 2026-07-31 16:15:11 (official HPR transcript)

---

This is Hacker Public Radio Episode 4614, for 2026-04-09
Today's show is entitled, "Dauug|18: Faster Than a ’286, but Inspectable Like a Soroban"
The host is Marc W. Abel and the duration is 00:37:06
The flag is Clean, and the license is CC-BY
The summary is "What would MacGyver build when he can't trust complex VLSI? I don't know, so here's what I'd build."
Welcome back, Hacker Public Radio listeners!
My name is Marc Abel, and I'm online at dauug.org, spelled D A U U G dot O R G.
Today we'll take a first look at an 18-bit controller being designed by The Dauug House
that doesn't use any complex VLSI.
It doesn't have a microprocessor or anything like one—no FPGA, PLD, ASIC, or even DRAM.
Now why would someone want a controller that doesn't use complex VLSI today?
Because complex VLSI has a long history of containing defects that hacking and malware
can exploit.
Even dynamic RAM has been shown to not work as designed when adversaries are present.
And even if we open-source our microprocessors and whatnot, we have no tools as makers that
can audit delivered chips against their open-source specs.
So from the standpoint of maybe someone who needs to automate some aspect of critical
infrastructure, or maybe someone who lives under an authoritarian regime, it can be valuable
to have modern electronics that operate transparently,
don't use any black-box components, and can be inspected by their owners clear down to
the logic-gate level.
I've thought about this need for about 20 years in hope of finding a way forward.
I was encouraged by modern surface mount techniques.
Individual transistors can be packaged so small that 25,000 can fit in a teaspoon.
Robots that can place and solder them require skill to set up and use, but they
aren't expensive.
And because a 32-bit microprocessor already existed that only had 25,000 transistors
(it was the original ARM CPU), I was sure right off that maker-constructable computers that
remain inspectable at the logic-gate level even after delivery could be made practical.
So I kept thinking.
My discrete transistor idea had several weaknesses, but I think it was looking at recently-built
relay computers—
there are hobbyists that like to build these—that exposed the most serious drawback, and
that's primary storage.
If a computer has even a million bits of random-access memory, it would take on the order
of a million components to build that memory.
Now a million transistors is way more than 25 thousand, and we'd need about six transistors
per bit to build a reliable memory.
So I could probably build a processor from discrete components, but the memory supporting
it would have to be VLSI, probably in the form of static random access memory, or SRAM.
But what about the security consequences of using VLSI SRAM?
Could SRAM introduce concealed security defects that hacking and malware could exploit?
Whether or not to trust SRAM is a judgment call.
We need to consider whether SRAM supply chain attacks are viable enough for someone to mount
one, what transparently functioning alternatives to SRAM could be, and how far we're willing
to go to have transparently functioning computers in the first place.
Exploring these questions is outside the scope of this episode, but I'll say a few
words in SRAM's favor.
As circuits, static random-access memory components are not proprietary.
In fact they're the exact opposite of proprietary in that their behavior and pinouts are standardized.
And although the chips are very large scale integration, this largeness is in cell count
rather than complexity.
An SRAM is the same 6 transistor flip-flop over and over again, millions of them, and
they're all isolated from each other.
In fact, if you read the datasheet for an SRAM IC alongside a datasheet for an AND gate,
you'll find they're not much different in length.
Even if a sophisticated adversary is warned that I plan to build a computer out of static
RAM, it will be hard to attack the machine by manipulating the SRAM supply chain.
The attacker would need to know what I was building, where in the circuit each RAM they
supply would go, when and where I would obtain the components, and how to know whether their
chip went into my computer or someone else's.
An attacker would also typically need access to the tainted components at their point of
use.
I can't tell you that an attacker this sophisticated won't come around, but I suggest that until
transparently functioning SRAM computers are in such widespread use that the number of
targets is large, attacks through the SRAM supply chain will be too expensive compared
to simpler offensives that cost less.
In light of SRAM's characteristics, I decided I would still be interested in building a
transparently functioning computer even if it uses VLSI for primary storage—as long
as it isn't complex VLSI that could easily conceal exploitable defects.
But with SRAM chips now permitted in the bill of materials, a question arises as to whether
SRAM is permissible for storage only, or if it could be used for computing as well.
I estimate that more likely than not, SRAM is actually safer for use in computing logic
than it is for storage.
If SRAM could be used as a logic gate, data passing through that part isn't at rest, it's
just a few bits at a time, not enough bits to identify structure and meaning in.
When I use SRAM to implement CPU logic thus far, no SRAM ever sees more than 6 bits
of data at a time.
That's not even a byte.
Also, consider where an attacker might buffer data for exfiltration.
When an SRAM is used for primary storage, there's lots of room to store and aggregate
information.
But SRAM used as a logic gate doesn't store data—
it only holds firmware.
So let's consider how SRAM can be used to design transparently functioning computers.
In Hacker Public Radio episode 4333, I described a minicomputer with preemptive multitasking,
paged virtual memory, no complex VLSI, and a refreshingly capable instruction set,
and I named this architecture Dauug|36 after its 36-bit word size.
Here again, the spelling is D A U U G.
The CPU, memory subsystem, firmware, and real-time operating system kernel work in
simulation today, but the architecture still needs I/O and initialization that call for
significant logic outside the CPU.
So after a couple false starts designing the missing logic from basic gates and flip-flops,
I decided the way forward is to build in a very small controller that has no complex
VLSI.
I named this smaller architecture Dauug|18 because it can't do as much as Dauug|36, and happens
to use 18-bit words.
Like Dauug|36, Dauug|18 is built principally from two kinds of components.
The simpler kind is basic glue logic from the Advanced Ultra-Low Voltage CMOS family
from Texas Instruments.
These are a modern take on the 7400 series of basic gates—AND, NAND, OR, NOR, XOR,
buffer, inverter, and D flip-flop, and they're insanely fast.
These ICs come in a variety of packages, but to simplify soldering I only use the packages
that have leads.
Most of these chips only operate on a single bit at a time, but the buffers and flip-flops
are available in widths up to 16 bits, which let us direct the flow of data within the
CPU.
There are places in Dauug|18 where buffers need to be installed facing both directions because
data sometimes flows either way.
Conveniently TI sells buffers with an extra pin to specify their direction.
You'd think these are called something natural like "bidirectional buffers," but they're actually
called "bus transceivers."
The name sounds like they do something complex, but instead they're just buffers that pass
bits in either direction.
The other kind of component in Dauug|18 is synchronous static RAM.
The word "synchronous" here means that the SRAMs' input is clocked through D flip-flops.
For read operations, the outputs will be ready five and a half nanoseconds after the clock rises.
I'm going to share specifics about the CPU's bill of materials and topology here because
they directly impact Dauug|18's capabilities and how it's programmed.
This being a radio program, I'll do my best to not lose you in these details.
But if you'd like a visual aid, I've uploaded a PDF with this show, and if you can't find it there,
you can visit 18.dauug.org (that's 1 8 dot D A U U G dot O R G), and you'll find a
PDF link near the bottom of the page.
The Dauug|18 processor contains just six SRAMs.
Each SRAM is 18 bits wide and has 262,144 words of storage,
so their address space is also 18 bits.
It's noteworthy that this capacity is more or less the smallest synchronous RAM on the
market, and we want to use the smallest chips we can because of their lower cost
and smaller firmware size.
Of these six SRAMs, one is data memory.
You more or less know what that does, and one of the architecture's limitations is that
it doesn't support any more data memory than the 256 kibiwords that it comes with.
Fortunately, code goes in a separate SRAM, which means code and data live in separate
18-bit address spaces.
Code is inefficiently stored because Dauug|18 doesn't have a control decoder and doesn't
have any concept of opcodes.
Dauug|36 has both, but Dauug|18 has neither because it's a simpler processor.
Instead, Dauug|18's control signals for each instruction are written directly into words
of the code memory.
Of the code SRAM's 18-bit width, six bits are used to tell the arithmetic logic unit
what it's doing, and the remaining 12 bits are all that are left to control the rest
of the CPU.
So that's two SRAMs out of six: the data memory and the code memory.
The third RAM is called "literal memory" and is wired in parallel lockstep with the
code memory.
This third RAM stores 18 bits of information for every instruction that needs literal data
attached.
So if you need to read from data memory location 1000, the code memory has the control signals
to produce the read, and the literal memory has the address to read from, which in this
case is 1000.
So in effect, Dauug|18 instructions are 36 bits wide, with 18 of those bits being optional
literal data, 12 bits being control signals, and 6 bits telling the arithmetic logic unit
what its function is.
The code and literal data are stored on different chips because the literal output sometimes
has to be turned off to accommodate other signals, while the control output must never
be turned off.
We've knocked out three SRAMs: data memory, code memory control signals, and literal data
attached to these control signals.
There are only three SRAMs left, and they implement the arithmetic logic unit's lowest
six bits, middle six bits, and highest six bits.
So how exactly do we use SRAM to compute?
The answer is we don't actually use SRAM to compute.
Instead, we use SRAM to look up answers we computed previously.
Here's how this works.
Recall that the smallest synchronous SRAMs on the market have 18 address bits, and consider
a sample problem like C = A + B. Since A is on the left side of the expression A
+ B, we call A the left operand, and B the right operand.
If we limit the left operand to 6 bits, it can be an integer between 0 and 63 inclusive,
and we can do the same with the right operand so that it's also an integer between 0 and
63.
And for the moment, let's say that our result of adding A + B wraps around so that C
will fall between 0 and 63.
So to make SRAM into a gate that can compute any 6-bit sum, we'll need a truth table with
a 12-bit address space (that's 4096 rows) where each row is 6 bits wide.
So it's easy for an SRAM to behave like it's adding 6-bit numbers for us; we just have
to store all the sums in advance.
So how do we get the other operations we need, like subtraction, bitwise AND, exclusive OR,
short multiplication, and so on?
Well, our SRAM has 18 address bits, and two 6-bit operands only come to 12 bits, so we
have 6 bits left.
These 6 are our room to say what we want done,
so we can put 64 operations on our menu so to speak.
Maybe operation 0 is addition, operation 1 is subtraction, operation 2 is bitwise AND,
and so on.
We're constrained to limit the number of operations we support to 64 at most, but that's
plenty for our purpose.
You've probably figured out that what I'm describing is something called a bit-sliced
ALU.
Rather than try to compute on 18-bit operands directly, which would take almost 70 billion
words of memory per operation, we split the processor's 18-bit words into 6-bit chunks,
where we only need about 4 thousand words of memory per operation.
If you're having trouble confirming the numbers here, remember that both the left and right
operands need address space, so the difference is between needing 2**36 rows
and 2**12 rows for each operation.
A constraint with bit-sliced arithmetic logic units is that each slice is isolated during
processing.
During addition, carries from bit 5 into bit 6 and bit 11 into bit 12 are lost.
We don't have this problem for bitwise logic such as NOT, NAND, and exclusive NOR, but
tasks such as addition, subtraction, shifts, and rotates need to look across entire words
to obtain their final answer.
How can this constraint be overcome with Dauug|18?
The answer involves three architectural features.
All three are simple, but this is a radio show, so let's try together to build a picture
in our minds.
The first architectural feature for overcoming isolation between bit slices is that each
Dauug|18 ALU operation produces two results instead of one result.
I'll explain in more detail in a moment, but for right now take it on faith that when
an SRAM looks up the 6-bit sum of A and B, it produces two different answers.
The second architectural feature is that wiring exists to scramble the order of the bits in
an 18-bit word.
I use this term wiring very literally.
It's not logic,
it's really just copper.
The third architectural feature is that, unsurprisingly, you can pass any given problem through the
arithmetic logic unit more than once, meaning you can compute the answer for a hard problem
using more than one step.
So let's consider how these three features work together to solve multi-step problems.
Dauug|18 takes a question like C = A + B, looks up small hints about the answer for
each slice separately, shuffles those hints between the slices in some magic way to produce
a new problem that is closer to the final answer, and then feeds the new problem through
the same arithmetic logic unit.
That's the big picture.
Let's go through these features a second time and explain what they mean more fully.
The first feature was that ALU computations produce two results instead of one.
We actually get that for free.
Each SRAM, remember it's the smallest size we can buy, has 18 address bits we use for
input, and 18 data bits we use for results.
But the result computed by each SRAM is only 6 bits, meaning we have 12 bits left over
in every row to use however we need for supplementary results.
So when we compute the 6-bit sum of 10 + 10, the answer is 20 and won't carry into
a 7th bit.
So that SRAM's main output, I actually call it the left output, is 20.
But that SRAM also has a 6-bit right output, and one of those bits is whether or not a
carry occurred, and that bit is zero.
Now suppose we compute the 6-bit sum of 60 + 3.
The answer is 63, which is six ones that go to that SRAM's left output.
Does a carry occur?
Not immediately,
so the carry in the right output is zero.
But there might need to be a carry if the SRAM with the next-lower place value outputs
a carry.
If there's no carry from the right, 63 just stays 63,
but if there's a carry coming in, that 63 wraps around to 0.
That's called carry propagation.
To handle propagation, the SRAM's right output has a bit named propagate alongside its companion
bit named carry.
The propagate bit will be 1 if and only if the local sum is exactly 63, while the carry
bit will be 1 if and only if the local sum is more than 63.
So each 6-bit addition on an SRAM produces two results.
The left result is the 6-bit sum, and the right result has a carry bit and a propagate bit.
We just need to get the carry and propagate bits over to any SRAMs that have greater place
value, and then account for them during a future instruction.
This is where our second architectural feature comes in, specifically, wires that move bits
of an 18-bit word outside of their immediate 6-bit slices.
Recall that each of the three SRAMs in the arithmetic logic unit have a left and right operand.
When given the appropriate control signal within a CPU instruction, a bus transceiver
will feed the ALU's left output (that's the sum) to become the left operand of the next
instruction.
Simultaneously, the ALU's right output (that's the carry and propagate information) are fed
through buffers to become the right operand of the next instruction.
The right operand is actually fed by two buffers, with only one of the buffers active at a time.
One buffer copies the right result straight back to become the next right operand with
no changes.
The other buffer transposes the order of the bits so the 6-bit result from each slice is
distributed two bits at a time into the three SRAMs for the next operation.
The upshot is that after the clock cycle where each 6-bit subword is added separately, on
the next clock every SRAM in the ALU sees the 6-bit sum it just generated as its left
operand, and carry and propagation information for all three subwords as the right operand.
The ALU SRAMs use this information to add any carries that are needed within each subword.
This second pass through the ALU is the third and final architecture feature Dauug|18 uses
to overcome subword isolation.
You may pick up that when an SRAM generates a carry bit in its right output, it has to
supply three copies of the carry bit,
so it can be distributed to all three SRAMs on the
next clock cycle.
For the same reason, it outputs three copies of its propagate bit, so all six bits in the
right result of addition operations have a purposeful use.
As I mentioned, addition is not the only operation to benefit from allowing the ALU to shuffle
a few bits between subwords.
You've probably guessed that subtraction uses the same mechanism, and it also turns out
that shifts and rotates of any number of bits can likewise be done in constant time.
Another operation this bit shuffling enables is population count, sometimes called just
popcount, or sometimes more formally Hamming weight.
Popcount computes the number of 1 bits in a given 18-bit word, and the result can be
as small as 0, when the input word is all zeros, and as large as 18, when the input word is
all ones.
It only takes three clock cycles for Dauug|18's ALU to compute a popcount, although the surrounding
CPU will use a few more clocks to fetch the operand from memory and store the result back.
Another operation that uses few clock cycles in this ALU design is computing hash values
for associative arrays.
This operation is closely related to another task this ALU does in a few clocks, which
is generate pseudorandom numbers that have good statistical properties.
Yet another is arbitrary permutations of the bits in an 18-bit word.
These three operations are efficient because the ALU effectively forms a substitution-permutation
network.
In the substitution step, the three SRAMs replace 6-bit subwords of the left operand
controlled by 6-bit keys in the right operand.
This substitution takes one clock cycle.
In the permutation step, which takes no clock cycle, each subword's output is split three
ways among the subwords of a future right operand.
If you're interested in the general theory of SRAM arithmetic logic units, including
how multiplication and division work without additional hardware, the Dauug|36 documentation
at 36.dauug.org goes into much detail.
Be sure to check out the download section.
That's 3 6 dot D A U U G dot O R G.
And if there's something you can't find or need explained better, I hope you'll drop
me a line.
Since the Dauug|18 virtual machine is pre-alpha and hasn't released any files as of March
2026, I'm probably the only person who's tried to write code for the architecture.
I'm happy to share it's not an easy CPU to write assembly code for.
You might think well, assembly code isn't easy in general, or maybe I'm not good at
writing assembly code.
You're probably right both times, but this is different.
I've written thousands of lines of Dauug|36 assembly code, and I've found Dauug|36 a joy to write
code for.
But Dauug|18 is more contorted to code due to its economy of components and bottlenecks
in data handling.
Here are some of the issues one faces coding for Dauug|18.
First off, the instruction pointer doesn't count linearly.
There's no end-user benefit to ordering the code addresses 0, 1, 2, and so on, and implementing
a linear order would require a counter that doesn't exist as an off-the-shelf component
with the speed and transparency of operation we need.
Dauug|36 manages to count linearly, but it uses a 27-bit incrementer comprising 70 gates that
only work because Dauug|36 can absorb two clock cycles of latency while counting.
In contrast, Dauug|18 has no extra clock cycles for incrementing a counter between instructions.
Both CPUs have a 40 MHz clock, but Dauug|18 pulls 40 MIPS while Dauug|36 only gets 10 MIPS.
Fortunately, there's no technical problem if an instruction pointer counts in a nonlinear
order, as long as it's deterministic and has a long enough period.
The only hardware needed, other than 18 flip-flops that hold the current address, is one XOR gate.
The circuit is called a Galois linear feedback shift register, abbreviated LFSR, and LFSRs
have been used for more than 50 years as a lightweight alternative to linear instruction
counters.
Another issue coding for Dauug|18 is it has no return address stack.
What you ideally want to do is write your program so no subroutines are reentrant.
Then each subroutine can allocate one word of data memory for its return address, and
a caller can save the return address before branching to the subroutine.
When it's time to return, the subroutine can fetch the return address and branch directly
to it.
For subroutines that must be reentrant, you'll need to brute force some kind of return address
stack in software.
Likewise there is no hardware-based stack pointer, so reentrant code (if there is any)
has to do extra work spilling local variables.
This brings up the further limitation that Dauug|18 really has only one address mode for
data memory;
specifically, the memory location being read or written is whatever address happens to
be at node 1 of the circuit board.
Node 1 can be driven by literal data in an instruction, or the most recent data memory
read, or the most recent ALU output, but no further adjustments are made to the address.
If you need an offset from a base pointer, for example, you're going to need to use the
ALU to compute that offset, and I'll point out that this can spill whatever ALU result
you were looking to store in the first place.
If you're familiar with the riddle where a farmer needs to move a wolf, goat, and cabbage
across a river one at a time, that's what Dauug|18 memory handling is like in practice.
It would be different if Dauug|18 had any registers other than the clocked outputs that are inherently
part of the SRAMs that comprise the CPU.
Unfortunately, no. Dauug|18 doesn't have any registers,
that's another limitation that the programmer is called on to overcome.
Another issue is that although most ALU operations are binary in the sense that they combine
two operands, as opposed to unary where only one operand is used, there is no direct access
to supply the ALU with its right operand.
The only place where the right operand electrically reaches the ALU comes from the ALU's own right
output.
This isn't the end of the world, but it adds clock cycles and consumes code memory.
For instance, if you want to add two numbers, you have to first pass the right operand through
the ALU from the left side to the right side, which adds a clock cycle to an already-tricky
addition sequence.
Another issue with Dauug|18 is that it has no freestanding controller for input or output,
nor is it designed to have I/O ports that are wider than perhaps a couple of bits.
On top of all that, all I/O would have to be polled because Dauug|18 has no interrupts
to facilitate transfers.
Another limitation in programming and using Dauug|18 is lack of any mechanism that can programmatically
change code memory.
You can't just launch a program from a command prompt, not unless the program you want already
resides in memory.
The reason this can't work is Dauug|18 doesn't have any microcoding or control decoder, so
the code memory is needed during every clock to control the CPU.
No cycles are left when the code memory is free and can be updated.
One challenge Dauug|18 assembly language programmers will face, and right now there are no other
languages implemented, is the architecture doesn't have any opcodes.
Instead, the language describes a collection of control signals that operate the processor.
The present assembler has rudimental macro support that can somewhat approximate opcodes,
but the strange flows that result from not having any registers make for complex instructions
and an appreciable learning curve.
Dauug|18 is certainly Turing complete and can overcome the aforementioned limitations
if given enough attention and clock cycles, but only within the umbrella of the architecture's
18-bit address space.
Once you're out of code or data memory, neither will expand, and although you could dive into
the tarpit of swapped external pages, the architecture's near-total lack of I/O support
make swapping even less realistic than it was on historic architectures.
The problem is even more acute for code memory, which stores programs inefficiently and can't
be changed at all by the CPU.
So with all these constraints imposed on programming and use, is Dauug|18 a lost cause?
I certainly don't think so.
In fact, Dauug|18 is on track to be the world's most advanced transparently functioning computer
when its first prototype is built.
Here is a machine that's entire operation is determined by open-source firmware and
a builder's soldering.
No "black box" parts at all, yet a footprint the size of a postcard.
Its 18-bit address space might sound small if you're very young, but I was once an MS-DOS
power user, and 256K is more than most people realize, especially when an architecture has
18-bit bytes instead of 8-bit bytes.
And remember that code for your programs doesn't count against the 256K limit for data.
And in terms of my need—a coprocessor to load firmware and handle I/O for Dauug|36, Dauug|18
checks my boxes about perfectly.
It's also hard for me to ignore Dauug|18's potential as an educational tool that implements an
entire architecture succinctly, transparently, and uniquely.
I've stressed on this show that Dauug|18 is missing a lot in terms of functionality, but it's
also missing a lot of headaches.
There's no dynamic RAM at all, so there are no RowHammer-like vulnerabilities.
There's no VLSI complex logic except in peripherals you choose to attach
that could hide exploitable defects.
There's no writable CPU persistent state;
the only state that survives a power cycle is its open-source firmware.
There's no electrical path for the CPU to modify its code memory, strongly reducing
an attacker's options for injecting code.
There's no CPU microcode to poison, and the CPU's only firmware is a completely disconnected
set of one-clock-cycle ALU operations that occur only in the order specified by a running
program.
Even this firmware can't be modified without physical access to the machine.
Dauug|18's clock is generated by a crystal oscillator directly, not a micro-electromechanical system
with a programmable frequency synthesizer.
MEMS oscillators are very popular now, but they create opportunities for age-selected
and frequency-selected attacks that simple crystal oscillators aren't vulnerable to.
Because the Dauug|18 firmware uses a substitution-permutation network to produce pseudorandom
numbers and associative array hashes, there are S-boxes in the firmware, but unlike some
of history's notorious ciphers where S-boxes were manipulated, Dauug|18's S-boxes have no
unexplained constants.
Instead, they're extracted from the square root of two using 29 lines of Python, and no
imported modules whatsoever. Even from the Python standard library.
No one has any unfair or unlevel knowledge of how Dauug|18's S-boxes work.
All Dauug|18 components are hand-solderable and can be probed after assembly to confirm that
all connections match the open-source netlist.
There is no secret functionality, no vendor lock-in, no encrypted or closed-source firmware,
no license fees to build, use or modify the machine, no purpose-of-use limitations,
no proprietary components to be discontinued without substitutes, no planned obsolescence,
and no right-to-repair infringements.
You can learn more about Dauug|18 and Dauug|36, including how to support both projects, at
dauug.org, spelled D A U U G dot O R G.
My name is Marc Abel, spelled MARC : ABEL.
Thank you for listening.
You have been listening to the Hacker Public Radio podcast, at hackerpublicradio.org.
Today's show was contributed by a HPR listener like yourself.
If you ever thought of recording a podcast, then visit the HPR site to find out how easy it really is.
Hosting for HPR has been kindly provided by anhonesthost.com, the Internet Archive, rsync.net, and the HPR Community Content Delivery Network.
Unless otherwise stated, today's show is released under a Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.
