Computing with Switches
A working adder made of ordinary switches and relays, checked by a Python script before building, the same adder on an Arduino and in Logisim – and a look at everything a modern processor already has built in: floating point, text standards, encryption and more, with seven examples.
Open in the interactive Digital Garden
| What You Need |
|---|
| Curiosity – the text needs no knowledge of electronics; the examples are there for those who want to build or try |
| For the switch adder: three DPDT toggle switches, one or two 5 V relays, LEDs, resistors, a breadboard and a 5 V USB power supply (full list in Example 2) |
| For the simulations: Python 3 on any computer, optionally an Arduino Uno and the free Logisim Evolution |
| For the processor examples: a Linux computer or WSL on Windows with OpenSSL (installed almost everywhere) |
| Background: the NAND adder in From Source Code to Silicon and the layers in Communication with the Machines |
Jump to Examples
In From Source Code to Silicon I added two numbers in Python using nothing but NAND gates. That is still a program pretending to be hardware. In this note I go one step further down: an adder made of real switches that you flip with your fingers, a few wires and two LEDs – no chip, no code, nothing hidden. Then I go the other way, up into a modern processor, and look at how much of what computers do every day is no longer written by programmers at all, because it has been agreed on once and built into the silicon.
The surprising part is how close the first step is to everyday life. The two switches that turn the light in a staircase on and off from the top and the bottom – the light is on when exactly one of them is "up" – are a logic gate called XOR. Half of an adder hangs on the wall of almost every house.
Safety first: everything in this note runs on 5 volts from a USB charger. Never experiment with switches connected to the mains – the house wiring is only the idea, not the material.
A Bit of History: From Relays to Instruction Sets
In 1937 a student at MIT, Claude Shannon, wrote in his master's thesis (published in 1938) that circuits of switches and relays follow the rules of Boolean algebra: switches in series behave like AND, switches in parallel like OR. With that, designing a calculating machine became a matter of logic rather than trial and error. In the same year, at Bell Labs, George Stibitz built a relay adder for binary numbers at home – later called the "Model K", K for the kitchen table it was assembled on. In September 1940 he demonstrated his Complex Number Calculator at Dartmouth College with a teletype that sent the problems over telegraph lines to the machine in New York – the first remote use of a computing machine.
In Berlin, Konrad Zuse presented the Z3 on 12 May 1941: about 2,600 relays, binary floating-point numbers with 22 bits and a clock of 5 to 10 steps per second. It was destroyed in an air raid in December 1943. The Harvard Mark I (1944) was electromechanical too – 765,000 parts and about three additions per second. ENIAC (1945) replaced the moving contacts with about 18,000 vacuum tubes and reached about 5,000 additions per second. Then the switches shrank: the transistor (1947), the integrated circuit (1958–1959) and the microprocessor (Intel 4004, 1971). The idea did not change – a modern processor is still a huge number of switches that switch other switches.
What did change is what the switches are wired to do. The first chips could only add, subtract and move bytes; everything else was software. Over the years the most common jobs moved into the hardware, one after another: floating-point arithmetic (the Intel 8087 coprocessor, and the IEEE 754 standard from 1985), parallel arithmetic on many numbers at once (MMX in 1997, SSE in 1999, AVX in 2011), checksums and text search (SSE4.2, 2008), encryption (AES-NI, 2010), random numbers (RDRAND, 2012) and finally matrix multiplication for AI (AMX, 2023).
A short timeline:
| 1937 – Shannon: switching circuits are Boolean algebra; Stibitz's relay adder on the kitchen table |
| 1940 – the Complex Number Calculator is used remotely over telegraph lines |
| 1941 – Zuse's Z3: about 2,600 relays, binary floating point |
| 1944 – Harvard Mark I; 1945 – ENIAC with about 18,000 vacuum tubes |
| 1947 – the transistor; 1958–1959 – the integrated circuit; 1971 – the microprocessor |
| 1963 – ASCII; 1985 – IEEE 754 floating point; 1991 – Unicode; 1992 – UTF-8 |
| 1997–2013 – MMX, SSE, SSE4.2, AVX and AVX2: arithmetic on many numbers at once |
| 2005 – hardware virtualization (VT-x); 2010 – AES-NI; 2012 – RDRAND |
| 2023 – AMX: matrix multiplication inside a server processor |
Some resources for history:
- A Symbolic Analysis of Relay and Switching Circuits – Shannon's thesis (Wikipedia)
- Z3 (Wikipedia) and Harvard Mark I (Wikipedia)
- x86 instruction listings – when each group of instructions arrived (Wikipedia)
- Nand to Tetris – build a whole computer from gates, free course
How a Switch Calculates
A switch is either closed or open – 1 or 0. That is all a bit is. The trick is in how switches are connected. A DPDT switch ("double pole, double throw") is simply two switches under one lever: each half (a pole) has a middle contact that is connected either to the "up" contact or to the "down" contact. I call these contacts C (common), NO and NC, the names used for relays: NO is connected when the switch is in position 1, NC when it is in position 0.
Four ways of connecting switches:
| In series – AND: the LED lights only if switch A and switch B are closed. This is the carry of an adder: 1 + 1 gives a carry. |
| In parallel – OR: the LED lights if A or B (or both) is closed. |
| Staircase – XOR: two changeover contacts joined by two crossed wires (the "travellers"). The LED lights when the switches are in different positions. This is the sum digit: 0 + 1 = 1, 1 + 1 = 0 (with a carry). |
| Relay – a switch moved by electricity: a coil pulls the contacts over when current flows through it. Now the result of one circuit can flip a switch in another. This is the step that makes chains possible – the transistor does exactly the same, only without moving parts, a billion times smaller and a million times faster. |
With two DPDT switches – one pole of each wired as a staircase, the other in series – you get a half adder: it adds two bits and shows the sum and the carry. To add a third bit, the carry from the previous column, the result of A XOR B has to work a switch by itself. That is the job of one relay. Three switches and one relay make a full adder, one column of written addition. Every further column needs two more relays: one for its own XOR and one that is moved by the carry coming in from the right. Count it up and you see why Zuse needed thousands of relays for 22-bit numbers.
Some open-source tools for playing with logic:
- Logisim Evolution – draw gates and click on them (Example 4)
- Digital – a similar simulator by Helmut Neemann, good for larger circuits and for exporting to Verilog
- ngspice – simulates the actual voltages and currents, also of relays and transistors
- KiCad – draw the schematic and, if you like, a printed circuit board for the adder
- Nand to Tetris – a free course that builds a whole computer, from NAND gates to a game
What Is Already Built into the Processor
The adder from switches is the oldest piece of standard hardware: every processor has one, and nobody writes addition in software. Over 50 years many other tasks followed the same path. When a job is common enough and the way of doing it is agreed on, it moves into the silicon, where it runs ten to a hundred times faster and uses less energy. Example 5 shows which of these your own processor has, and Example 6 measures the difference for encryption.
Built into the processor (x86 names; ARM and RISC-V have their own equivalents):
| Whole numbers: addition, subtraction, multiplication, division, comparison. Negative numbers are stored as two's complement – -1 is "all ones" – so the same adder works for negative numbers too. |
Real numbers: floating-point arithmetic by the IEEE 754 standard (1985, revised 2008 and 2019). Its main designer, William Kahan, also worked on Intel's 8087. Because of it, 0.1 + 0.2 gives the same slightly wrong answer in Python, C, JavaScript and Excel on every computer. |
| Many numbers at once (SIMD): MMX (1997), SSE (1999), AVX (2011), AVX2 (2013), AVX-512 – one instruction adds or multiplies 4, 8 or 16 numbers together. Used for images, sound, video and AI. |
| Text and checksums: SSE4.2 (first in Nehalem processors, 2008) can compare strings and compute the CRC-32C checksum used by file systems such as ext4 and Btrfs. |
| Encryption: AES-NI (proposed in 2008, first in Westmere processors in 2010) does the AES cipher behind HTTPS, VPNs and disk encryption. SHA extensions (announced in 2013) compute SHA-1 and SHA-256 hashes. |
| Random numbers: RDRAND (Intel 2012, AMD 2015) gives random numbers from a noise source inside the chip. Because nobody can check it from the outside, Linux mixes it with other sources rather than trusting it alone – Thompson's question from From Source Code to Silicon in a new form. |
| Virtualization: VT-x (Intel, 2005) and AMD-V (2006) let one computer run several operating systems safely – the basis of every cloud and of the virtual machines in Constructing a Cloud. |
| Matrices for AI: AMX (Sapphire Rapids, January 2023) multiplies small matrices of 8-bit and 16-bit numbers directly in the processor core. |
| Next to the processor: most chips with built-in graphics also have fixed blocks for decoding and encoding common video formats, which is why a film plays without making the fan spin. |
Just as interesting is what is not in the processor. The processor does not know what a letter is, what a file is or what a picture is. These are agreements – standards – kept in software that almost everyone shares. They are "standardized" just as firmly as the adder, only one layer higher.
Standardized, but in shared software and conventions:
| Writing: ASCII (1963) gave numbers to 128 characters – enough for English. Unicode (1991) gives a number to every character of every script, including č, š and ž, and UTF-8 (1992, Ken Thompson and Rob Pike) stores them as bytes. At the time of this writing, about 99% of websites use UTF-8. To the processor my surname is just ten bytes (Example 7); the letters on the screen are drawn by fonts, which are software too. |
Basic functions: the C library (libc) and its maths library give every program the same printf, sqrt and sin. POSIX (1988) is the agreement on how programs talk to Unix-like systems, so that the same program runs on Linux, macOS and BSD. |
| Compression: zlib (1995) and its Deflate format sit inside ZIP files, PNG pictures and compressed web pages. |
| Secure connections: OpenSSL (1998) and a few similar libraries implement TLS – the "s" in https – and they are the programs that use AES-NI. |
| Calculation: BLAS (1979) is the agreed set of vector and matrix operations under NumPy, MATLAB, R and Julia; vendors write fast versions of it for their own processors. |
| Sound and video: FFmpeg (2000) reads and writes almost every audio and video format and is inside countless players and websites. |
| Programs themselves: the ELF file format on Linux (PE on Windows) and the calling convention – which register holds which argument – are agreements, so a program compiled by one compiler can use a library compiled by another. |
The pattern is always the same. First many people solve a problem in many ways; then one way wins and becomes a standard; then, if it is used often enough, it becomes an instruction in the processor. The switch adder is the first rung of that ladder, AES-NI and AMX the latest.
Some thoughts to consider:
- The Pentium FDIV bug (1994) – a few missing entries in a table inside the chip made some divisions wrong, and Intel replaced the processors. A mistake in software can be patched; a mistake in silicon stays.
- Meltdown and Spectre (2018) – features that made processors faster turned out to leak data between programs. They were mitigated with microcode, operating system and compiler changes, partly at the cost of speed.
- RDRAND and the question of trust – a random number generator nobody can inspect, and why Linux does not rely on it alone.
- Ken Thompson – Reflections on Trusting Trust – the same question one layer higher, discussed in From Source Code to Silicon.
Examples: From Switches to Your Own Processor
The examples go from the bottom up. I ran the Python examples with Python 3 and the processor examples on Linux (x86-64) with OpenSSL 3.0; the outputs are from those runs. I tested the Arduino sketch on a PC with a small stand-in for the Arduino library, not on a board. The physical build and the Logisim steps I could not test here, which is exactly why Example 1 comes first: it checks the wiring before you build.
Example 1 – Check the wiring before you solder
The wiring of both adders is written as a list of wires, one pair of contact names per wire. The script tries every position of the switches, follows the wires from +5 V and tells you which LEDs would light. If you change one wire – for example, you forget to cross the two travellers – it reports an error instead of letting you search for it with a multimeter. Save it as check_wiring.py.
"""check_wiring.py - check a switch/relay wiring list before you solder it.
Every switch (and the relay) is DPDT: two poles, each pole has a
common contact (C), a normally-open contact (NO) and a normally-closed
contact (NC). Switch "up" (1) joins C to NO, "down" (0) joins C to NC.
The script tries every switch position, works out which points are
connected to +5V and tells you which LEDs light.
"""
from itertools import product
# Wires: each pair is one piece of wire between two contacts.
HALF_ADDER = [
("+5V", "A1.C"), # pole 1: staircase = XOR
("A1.NO", "B1.NC"), ("A1.NC", "B1.NO"), # crossed travellers
("B1.C", "LED_SUM"),
("+5V", "A2.C"), # pole 2: series = AND
("A2.NO", "B2.C"),
("B2.NO", "LED_CARRY"),
]
FULL_ADDER = [
# relay X is switched on when A XOR B (staircase A/B, pole 1)
("+5V", "A1.C"),
("A1.NO", "B1.NC"), ("A1.NC", "B1.NO"),
("B1.C", "X_COIL"),
# SUM = X XOR Cin (staircase X/Cin, pole 1)
("+5V", "X1.C"),
("X1.NO", "Cin1.NC"), ("X1.NC", "Cin1.NO"),
("Cin1.C", "LED_SUM"),
# CARRY = if X then Cin else A (relay pole 2 chooses)
("+5V", "X2.C"),
("X2.NO", "Cin2.NO"), ("Cin2.C", "LED_CARRY"),
("X2.NC", "A2.NO"), ("A2.C", "LED_CARRY"),
]
def lit_points(wires, switches):
"""Return the set of points connected to +5V for given switch positions."""
links = list(wires)
for name, up in switches.items():
for pole in ("1", "2"):
c = name + pole + ".C"
links.append((c, name + pole + (".NO" if up else ".NC")))
powered, changed = {"+5V"}, True
while changed:
changed = False
for a, b in links:
if (a in powered) != (b in powered):
powered |= {a, b}
changed = True
return powered
def run(wires, inputs, relay=None):
print(" ".join(f"{n:>3}" for n in inputs), " SUM CARRY ok?")
all_ok = True
for bits in product((0, 1), repeat=len(inputs)):
sw = dict(zip(inputs, bits))
if relay:
sw[relay] = 0 # relay starts released
for _ in range(3): # let it settle
sw[relay] = int("X_COIL" in lit_points(wires, sw))
p = lit_points(wires, sw)
s, c = int("LED_SUM" in p), int("LED_CARRY" in p)
total = sum(bits)
ok = (s, c) == (total % 2, total // 2)
all_ok &= ok
print(" ".join(f"{b:>3}" for b in bits), f"{s:>4} {c:>5} ",
"yes" if ok else "WRONG")
print("all correct" if all_ok else "WIRING ERROR")
return all_ok
if __name__ == "__main__":
print("Half adder (2 switches):")
run(HALF_ADDER, ["A", "B"])
print()
print("Full adder (3 switches + relay X):")
run(FULL_ADDER, ["A", "B", "Cin"], relay="X")
Output:
Half adder (2 switches):
A B SUM CARRY ok?
0 0 0 0 yes
0 1 1 0 yes
1 0 1 0 yes
1 1 0 1 yes
all correct
Full adder (3 switches + relay X):
A B Cin SUM CARRY ok?
0 0 0 0 0 yes
0 0 1 1 0 yes
0 1 0 1 0 yes
0 1 1 0 1 yes
1 0 0 1 0 yes
1 0 1 0 1 yes
1 1 0 0 1 yes
1 1 1 1 1 yes
all correct
To see the check work, swap the travellers in the half adder to ("A1.NO", "B1.NO"), ("A1.NC", "B1.NC"): every line of the half adder becomes WRONG, because uncrossed travellers make the opposite of XOR. The relay is handled by the "settle" loop: the script first finds out whether current reaches the relay coil, moves the relay accordingly and only then looks at the LEDs – the same thing that happens in a real relay in a few milliseconds.
Example 2 – Building it with switches and relays
This is the part I like most: the adder you can touch. All parts are cheap and available in any electronics shop or online; the types are examples, any similar ones will do.
- 3 × DPDT ON-ON toggle switch (for example the common MTS-202 type with 6 pins)
- 2 × 5 V DPDT relay (for example a small 8-pin signal relay such as HK19F-DC5V-SHG); the full adder needs one, the two-bit adder two
- 2 × diode 1N4148 or 1N4007 – one across each relay coil
- 3 × LED and 3 × 330 Ω resistor (one for each LED; about 9 mA each at 5 V)
- A breadboard and jumper wires
- A 5 V USB charger with a USB breakout board (or the 5 V and GND pins of an Arduino)
- Optional: a multimeter, to find out which pin of a switch or relay is which
Wiring the half adder (two switches, no relay – the HALF_ADDER list in Example 1):
- Pole 1 of both switches is the staircase: +5 V to the middle contact of A.
- The two outer contacts of A go crosswise to the outer contacts of B: the "1" contact of A to the "0" contact of B, and the "0" contact of A to the "1" contact of B.
- The middle contact of B, pole 1, goes to the SUM LED.
- Pole 2 is the series circuit: +5 V to the middle of A, the "1" contact of A to the middle of B, the "1" contact of B to the CARRY LED.
- Each LED goes through its resistor to GND (the shorter leg of the LED towards GND).
- Flip the switches: 0 + 0 – both dark; 1 + 0 and 0 + 1 – SUM; 1 + 1 – only CARRY, which is binary 10, two.
Wiring the full adder (three switches and relay X – the FULL_ADDER list):
- The staircase of A and B (pole 1) now goes to the coil of relay X instead of an LED; the other end of the coil goes to GND.
- The diode goes directly across the coil, the end with the stripe (cathode) towards the side that gets +5 V. When the coil switches off, it produces a short spike of high voltage, and the diode absorbs it.
- Pole 1 of the relay and pole 1 of switch Cin form a second staircase: +5 V to the middle of X, crossed wires to Cin, the middle of Cin to the SUM LED.
- Pole 2 of the relay decides the carry: +5 V to its middle contact. Its "1" contact goes to the "1" contact of Cin, pole 2, and the middle of Cin, pole 2, goes to the CARRY LED. Its "0" contact goes to the "1" contact of A, pole 2, and the middle of A, pole 2, also goes to the CARRY LED.
- In words: if X is on (A and B differ), the carry is whatever Cin is; if X is off (A and B are the same), the carry is A – both are 1 or both are 0.
Important:
- Only 5 V. Even if you use wall switches from the hardware store for the staircase, never connect anything in this note to the mains.
- Each relay coil draws tens of milliamps, depending on the type. A USB charger handles that easily, but check the datasheet of your relay for the coil current and for the order of the pins, which differs between makes.
- Before you connect the power, run the wiring list of what you actually built through Example 1.
Two bits. To add two-bit numbers (0 to 3), use the half adder for the right column, but let its carry drive a second relay K instead of (or as well as) the LED. The left column is a full adder in which relay K takes the place of switch Cin. The script checks all 16 combinations; save it next to check_wiring.py:
"""two_bit.py - two-bit adder: 4 switches and 2 relays (uses check_wiring.py)."""
from itertools import product
from check_wiring import lit_points
TWO_BIT = [
# bit 0: half adder from switches A0 and B0
("+5V", "A01.C"), ("A01.NO", "B01.NC"), ("A01.NC", "B01.NO"),
("B01.C", "LED_S0"),
("+5V", "A02.C"), ("A02.NO", "B02.C"),
("B02.NO", "K_COIL"), # the carry now drives relay K
# bit 1: relay X is on when A1 XOR B1
("+5V", "A11.C"), ("A11.NO", "B11.NC"), ("A11.NC", "B11.NO"),
("B11.C", "X_COIL"),
# S1 = X XOR K
("+5V", "X1.C"), ("X1.NO", "K1.NC"), ("X1.NC", "K1.NO"),
("K1.C", "LED_S1"),
# C1 = if X then K else A1
("+5V", "X2.C"),
("X2.NO", "K2.NO"), ("K2.C", "LED_C1"),
("X2.NC", "A12.NO"), ("A12.C", "LED_C1"),
]
ok_all = True
print(" a + b = LEDs C1 S1 S0")
for a1, a0, b1, b0 in product((0, 1), repeat=4):
sw = {"A0": a0, "B0": b0, "A1": a1, "B1": b1, "X": 0, "K": 0}
for _ in range(4): # let the relays settle
p = lit_points(TWO_BIT, sw)
sw["X"], sw["K"] = int("X_COIL" in p), int("K_COIL" in p)
p = lit_points(TWO_BIT, sw)
leds = [int(n in p) for n in ("LED_C1", "LED_S1", "LED_S0")]
a, b = 2 * a1 + a0, 2 * b1 + b0
got = leds[0] * 4 + leds[1] * 2 + leds[2]
ok_all &= got == a + b
mark = "" if got == a + b else " WRONG"
print(f" {a} + {b} = {got} {leds[0]} {leds[1]} {leds[2]}{mark}")
print("all 16 correct" if ok_all else "WIRING ERROR")
Output:
a + b = LEDs C1 S1 S0
0 + 0 = 0 0 0 0
0 + 1 = 1 0 0 1
0 + 2 = 2 0 1 0
0 + 3 = 3 0 1 1
1 + 0 = 1 0 0 1
1 + 1 = 2 0 1 0
1 + 2 = 3 0 1 1
1 + 3 = 4 1 0 0
2 + 0 = 2 0 1 0
2 + 1 = 3 0 1 1
2 + 2 = 4 1 0 0
2 + 3 = 5 1 0 1
3 + 0 = 3 0 1 1
3 + 1 = 4 1 0 0
3 + 2 = 5 1 0 1
3 + 3 = 6 1 1 0
all 16 correct
Four switches and two relays add 3 + 3 = 6, binary 110. Each extra column adds one more switch for each number and two relays – the same pattern Stibitz and Zuse followed.
Example 3 – The same adder on an Arduino
The second way: the switches stay real, but the logic becomes a program. Three ordinary switches (simple on-off ones are enough here) connect pins 2, 3 and 4 to GND; the Arduino's internal pull-up resistors keep the pins HIGH while a switch is open, so a closed switch reads as LOW, which the program turns into 1. The LEDs with their resistors go on pins 8 and 9. The serial monitor (9600 baud) prints each sum as it changes.
// full_adder.ino - three switches in, two LEDs out.
// Switches go from pins 2, 3, 4 to GND (internal pull-up, so
// a closed switch reads LOW = 1). LEDs with 330 ohm resistors
// on pins 8 (SUM) and 9 (CARRY).
const int PIN_A = 2, PIN_B = 3, PIN_CIN = 4;
const int PIN_SUM = 8, PIN_CARRY = 9;
int readSwitch(int pin) {
return digitalRead(pin) == LOW ? 1 : 0; // closed = 1
}
void setup() {
pinMode(PIN_A, INPUT_PULLUP);
pinMode(PIN_B, INPUT_PULLUP);
pinMode(PIN_CIN, INPUT_PULLUP);
pinMode(PIN_SUM, OUTPUT);
pinMode(PIN_CARRY, OUTPUT);
Serial.begin(9600);
}
int last = -1;
void loop() {
int a = readSwitch(PIN_A), b = readSwitch(PIN_B), cin = readSwitch(PIN_CIN);
// The same logic the relays do, written with C operators:
int sum = a ^ b ^ cin; // XOR
int carry = (a & b) | (cin & (a ^ b)); // AND, OR
digitalWrite(PIN_SUM, sum ? HIGH : LOW);
digitalWrite(PIN_CARRY, carry ? HIGH : LOW);
int state = a * 4 + b * 2 + cin;
if (state != last) { // print only on change
Serial.print(a); Serial.print(" + "); Serial.print(b);
Serial.print(" + "); Serial.print(cin); Serial.print(" = ");
Serial.print(carry); Serial.println(sum); // two-bit binary result
last = state;
}
delay(20); // simple debounce
}
The two lines with ^, & and | are the whole adder – the processor of the Arduino does them with its own built-in adder, and in a larger program you would simply write a + b. I tested the logic on a PC by compiling the sketch with a small stand-in for the Arduino library and feeding it all eight combinations:
0 + 0 + 0 = 00
0 + 0 + 1 = 01
0 + 1 + 0 = 01
0 + 1 + 1 = 10
1 + 0 + 0 = 01
1 + 0 + 1 = 10
1 + 1 + 0 = 10
1 + 1 + 1 = 11
all 8 combinations correct
Example 4 – Drawing it in Logisim Evolution
The third way is a simulator where you draw gates and wires and click on the inputs. Logisim Evolution is free and open source (GPL 3), written in Java, and runs on Windows, macOS and Linux (the installers already include the Java it needs); it continues the original Logisim by Carl Burch. I could not test these steps here, so the menu names may differ slightly in your version.
Steps:
| 1. Download the installer for your system from the project's releases page on GitHub and start the program. |
| 2. Place three input pins (A, B, Cin) and two output pins (SUM, CARRY) from the Wiring group, and two XOR, two AND and one OR gate from the Gates group. |
| 3. Wire it: SUM = (A XOR B) XOR Cin; CARRY = (A AND B) OR (Cin AND (A XOR B)) – the same formulas as in the Arduino sketch. |
| 4. With the hand (poke) tool, click the inputs and watch the outputs change – the same table as in Example 1. |
| 5. Use Project → Analyze Circuit to let Logisim write the truth table for you – or type in a truth table and let it build the gates. |
| 6. Make the adder a subcircuit and place four copies in a row, connecting each carry to the next Cin: a 4-bit adder. Then compare it with the ready-made adder in the Arithmetic group – the standard part, just like in a real processor. |
Example 5 – Ask your processor what it has built in
On Linux every feature of the processor is listed as a short "flag". This command picks out the ones from the tables above:
grep -o -w -E 'aes|avx2|avx512f|sha_ni|sse4_2|rdrand|vmx|svm' /proc/cpuinfo | sort -u
On the cloud server I used, the answer was aes avx2 avx512f rdrand sha_ni sse4_2 – the virtualization flag (vmx for Intel, svm for AMD) was missing because that server is itself a virtual machine. On Windows the free Coreinfo from Microsoft's Sysinternals shows the same information in a command prompt.
Example 6 – Encryption with and without AES-NI
OpenSSL has a built-in speed test. The first command encrypts with AES as usual; in the second, the variable OPENSSL_ia32cap tells OpenSSL to pretend that the processor does not have AES-NI (and the related multiplication instruction), so it has to do the same work with ordinary instructions:
openssl speed -elapsed -seconds 2 -bytes 16384 -evp aes-128-gcm
OPENSSL_ia32cap="~0x200000200000000" openssl speed -elapsed -seconds 2 -bytes 16384 -evp aes-128-gcm
The last line of each (the numbers are thousands of bytes per second):
AES-128-GCM 12972343.30k
AES-128-GCM 212557.82k
About 13 gigabytes per second with the instructions, about 0.2 without – roughly 60 times faster on my run. That is why a laptop can encrypt its whole disk without anyone noticing, and why the server in Constructing a Cloud can serve https on a few watts. Your numbers will differ, but the ratio tells the story.
Example 7 – Standards you can see in the bytes
Four agreements, each in two or three lines: how negative numbers are stored, why 0.1 + 0.2 is not exactly 0.3, what my surname looks like to the computer, and a checksum that gives the same number on every computer in the world.
"""standards.py - things every computer agrees on, shown byte by byte."""
import struct
import zlib
# 1. Whole numbers: two's complement. -1 is "all ones".
for n in (5, -1, -5):
print(f"{n:>3} as one byte: {n & 0xFF:08b}")
# 2. Real numbers: IEEE 754. 0.1 cannot be stored exactly.
print("0.1 + 0.2 =", 0.1 + 0.2)
bits = struct.unpack(">Q", struct.pack(">d", 0.1))[0]
b = f"{bits:064b}"
print("0.1 as 64 bits:", b[0], b[1:12], b[12:]) # sign exponent fraction
# 3. Text: Unicode code points, stored as UTF-8 bytes.
word = "Pleteršek"
print(word, "->", [hex(ord(ch)) for ch in "ršk"])
print("UTF-8 bytes:", word.encode("utf-8").hex(" "))
print("letters:", len(word), " bytes:", len(word.encode("utf-8")))
# 4. Checksums: CRC-32, the same number in ZIP, PNG and Ethernet.
print("CRC-32 of 'Hello':", hex(zlib.crc32(b"Hello")))
Output:
5 as one byte: 00000101
-1 as one byte: 11111111
-5 as one byte: 11111011
0.1 + 0.2 = 0.30000000000000004
0.1 as 64 bits: 0 01111111011 1001100110011001100110011001100110011001100110011010
Pleteršek -> ['0x72', '0x161', '0x6b']
UTF-8 bytes: 50 6c 65 74 65 72 c5 a1 65 6b
letters: 9 bytes: 10
CRC-32 of 'Hello': 0xf7d18982
The letter "š" is Unicode number 0x161 and takes two bytes in UTF-8 (c5 a1), so the nine letters of "Pleteršek" are ten bytes. The 64 bits of 0.1 are split into a sign, an exponent and a fraction exactly as IEEE 754 prescribes – and the fraction ends in a rounded ...1010, which is where the tiny error comes from. The CRC-32 value is the one ZIP and PNG files use; note that the CRC32 instruction from SSE4.2 computes a different variant, CRC-32C, so the two give different numbers for the same text.
For all examples
- Use only 5 V from a USB charger or an Arduino. The switches in this note never touch the mains.
- An Arduino pin can light an LED through a resistor, but it cannot drive a relay coil directly – that needs a transistor or a ready-made relay module.
- Measure the switches and relays with a multimeter before wiring: not every switch has its "1" contact on the same side.
- The processor examples (5 to 7) only read information and change nothing on your computer.
A Few More Thoughts
There is something satisfying about an adder you can hold in your hand. Three switches, a relay that clicks, two LEDs – and 1 + 1 + 1 = 11 in binary appears in front of you, without a single line of code. Everything a processor does starts here; the rest is the same idea repeated and shrunk until billions of switches fit on a fingernail.
The other half of the story is that nobody builds computers from scratch any more, and nobody needs to. Addition, floating point, text, compression and encryption were each solved once, agreed on and then built into chips and shared libraries that everyone uses. That is a huge gift – and, as in From Source Code to Silicon, a question of trust: the more is standardized and hidden in silicon, the more we rely on it working as described. A few switches on a breadboard are a good reminder that underneath it all is still something simple enough to understand completely.