I made a computer – here’s how it works! This is going to be too technical an overview for those without any background knowledge. But for those who made their way through Ben Eater’s full video series this should be a good 1-stop summary, and for those with some background knowledge about computing this should hopefully be pretty tractable.
The computer is made out of a collection of discrete Modules, which I will go through and describe. For reference, here is how all the modules are laid out on the board, and how they can exchange signals.
This is basically a set of shared wires for each of the 8 bits we will be moving around the computer. When we want to move a number, we tell one module to output to the bus, and another to accept an input from the bus. LEDs on the top of the bus tell us what’s on it.
If we’re moving numbers around, we need somewhere to put them. For temporary storage, we have two Registers. Each register consists of:
- A set of 8 flip-flops, which we can enable in order to read in a value from the Bus.
- An 8-bit buffer which we can enable in order to flow the value in the above flip-flops back out to the Bus.
- 8 LEDs to tell us what’s in the Register.
Reading off numbers from a set of 8 LEDs is a little tedious, so let’s put in a digital Display. This is essentially another Register which can load in values from the Bus. On these 7-segment displays, we need to convert from a binary number to a list of which segments on the display need to light up. Integrated packages which do this are super common, but we do it the hard way by programing an EEPROM (non-volatile memory) with a giant lookup table that takes in an 8 bit number and outputs a list of display segments to light up.
However, this EEPROM only has enough pins to output one number at a time. So, we also build up a small counter using a fast clock and two chained flip-flops to count from 1-4 very quickly. This is tied to the power-enable pin for each of the displays, and so essentially we cycle through each of the 4 digits for a given number faster than the eye can see to form the illusion of a steady display.
Arithmetic Logic Unit
It would nice to do some math with these numbers. We can sum the values in A and B with an Arithmetic Logic Unit, which on normal computers implements a full range of basic math operations but on ours only adds them together. However, with the magic of twos complement number representation, we can also subtract numbers! Inverting the number in B and adding 1 will get us the number for –B. The ALU consists of:
- A pair of chained 4-bit adders, which constantly sum the A and B Registers.
- An 8-bit buffer (same as the one in the registers) which allows us to output the value in the ALU to the bus.
- 8 LEDs to display the current sum of A and B.
- 8 XOR gates to invert B for subtraction.
- A “carry” bit which goes high if A+B > 255
We’re going to write programs with this computer, so let’s give it some memory to read and write from. This happens with the RAM module. The RAM reads in the first 4 bits from the Bus as the memory address to access, so we have 2^4 = 16 lines of memory. Each address holds an 8-bit number, which represents either a) a 4-bit command ID and a 4-bit argument if passed on to the CPU, or b) simply an 8-bit number if passed on elsewhere. It will be up to us to make sure that our programs don’t try to read numbers as commands or vice versa. Much like the rest of the computer we put in another register and buffer to output the value to the bus, and include some more LEDs to show what’s going on.
The RAM has two modes: programming mode and run mode, which is controlled via a pushbutton on the breadboards connected to the write switches on the chips.
In programming mode, there are dip switches to manually set high or low voltage for the 4-bit memory address, and to set an 8-bit value that will go to that address location. Programming this computer entails powering it on, and putting an 8-bit number into each of the 16 memory locations but hand. This is pretty tedious! A worthwhile upgrade in the future would be to hook up an Arduino and have a list of programs that you could quickly load into RAM automatically.
In Run mode, the dip switches are disabled, and we need to bring in the memory address to access from the Bus. This leads us to the…
Where in memory do we access next? The Program Counter is a 4-bit register that we use to keep track of this. At the start of each command step, the RAM will use the number in the counter to determine which memory address to try and execute. This initializes at 0, and increments by 1 after each step so the RAM will then read from the next sequential memory address, and in this way we loop through the entire 16 lines of memory. We will also allow the counter to read in values, which will let us jump to arbitrary memory locations, and in this way we can loop through specific ranges of memory. Another word for the number in this module is a Pointer.
It’s time to give the computer a sense of time. The Clock is a simple 555-timer set to pulse high at a steady rate. A potentiometer changes the time constant of this timer and lets us go between ≈0.1 and 200 Hz. There is also a push-button that disables the automatic clock and lets us manually advance the clock one pulse at a time. Additionally, the clock has an enable pin which we can control while running, which will let us halt the computer when our program is done.
Now for the brains of the operation. Any command we want to execute will require several smaller steps, called microinstructions. For example, to move the number in B to A through the Bus requires us to:
- Set the output pin on the B register to high.
- Set the input pin on the A register to high.
These are both microinstructions, and other commands may require several sets of these microinstructions to happen in order.
The CPU is just a giant lookup table of the sets of microinstructions required to execute all the commands we could have. We pre-program two EEPROMs with these microinstructions, and command lines feed out from there to most of the chips on the board.
There are a few more small bits on the computer not covered by the above. The first is a reset button, which clears out all the stored register values and starts the program over again. The second is a series of chained OR chips that sums up the individual bits in the ALU and lets us determine whether or not A+B = 0.
Anatomy of a Command Signal – “Load A”
Say that we have a number in RAM address 15, and we want to put it in the A register. We’ll call this command “Load A”, and let’s make it command #1 (0001). This command takes an argument, namely the memory address it should load from. In this case, 15 (1111). So if we want the computer to execute the command LoadA(15), this is the same as passing the 8-bit number (0001 1111) from RAM into the CPU. If we want to execute this on the first command step, we should put this number into RAM address 0.
Note that this value (0001 1111) is also equal to the decimal number 31. We have to be careful to not execute memory locations with values that represent numbers rather than commands!
Let’s break down the microinstruction steps that will have to be executed in order for us to run this LoadA command. The first two steps load the command at the RAM location specified by the Program Counter into the CPU. This is executed “blind” by the CPU every command step. On the 3rd clock cycle of each command, the CPU has loaded in the command from RAM, and starts to execute the microinstructions defined for each subsequent cycle.
- Output the Program Counter to the Bus.
Input the Bus value into the RAM module to set the memory address.
- Output the value of the RAM at that memory
address to the Bus.
Input the Bus value to the CPU.
Increment the Program Counter in preparation for the next command.
- Output the last 4 buts of the CPU register to
Input the Bus value into the RAM module to set the memory address from which we’ll get the number to load into A.
- Output the value of the RAM at that memory
address to the Bus.
Input the Bus value into the A Register.
In a series of steps similar to these, we build up a library of general commands which we can use to do different tasks. We can move numbers in and out of memory, add and subtract them, display results, jump to different locations in memory to run next, and halt the program. We can also implement two conditional jumps, namely to jump if the sum of two numbers overflows (i.e. is bigger than 255), or if the sum of two numbers is equal to 0. With these conditional jumps, this computer is Turing complete!
That’s it for the computer architecture and the structure of how it works! From here the only thing left to do is chain together commands to make programs to run on the machine. For that, check out my post on programs and some more commands for this machine, which contains more videos of this thing actually working!