With that, the 8 bit computer project is essentially finished. As mentioned in the introduction, there are still a few minor issues to iron out, especially at very high clock speeds, and there are some optional features that I haven’t experimented with yet, but they don’t add much value to what this project is: A fully functional basic computer that, in my opinion, looks great and is a nice tool to show how computers work on a hardware level.
This project was designed and built before I started my university education in computer science. This helped me a lot in my first computer architecture courses, as I already knew a lot of the concepts. However, there are many things that I didn’t know prior to taking those courses and many things I’ve learned about computer architecture that I didn’t know when designing this computer. So looking back at it, there are quite a few things I would do differently if I were to start a project like this now. Still, I am proud of it and happy with how it turned out.
In the future, I may design a new computer, perhaps implementing some more advanced computer architecture features and avoiding some of the problems that I encountered and worked around in this one. Until then, I recommend everyone who’s interested to keep looking into this topic – there is much more to learn than what I’ve covered here!
With the electronics finished and working, the computer now needs to be told what to do. This is realised by implementing instructions that perform small individual tasks on the computer, like moving a value between registers or adding two numbers. These instructions can then be combined to create more complex programs.
Each of these instructions is associated with an opcode (operation code) which is used to identify the instruction in the program in memory. The computer reads an opcode from memory, executes the associated instruction, then reads the next opcode, executes that, and so on. This fetch-execute cycle continues until the program ends, i.e. a special instruction tells the computer to stop executing the program.
To the program, the instructions are atomic, which means they are the smallest steps the program can be divided into. However, in this computer implementation, these instructions are subdivided into even smaller microinstructions (as discussed in the “Modules, Part 5” chapter). Each of these microinstructions is exactly one clock cycle long and defines the hardware control signals that should be set for the execution of the desired behaviour. For example, to read a value from memory, first the target memory address needs to be moved to the memory address register, then the value at that memory location can be read. Some of these microinstructions are also used to implement the fetch-execute cycle (read next opcode and pass it to the control logic).
To manage the programming of these instructions, I wrote an app in C# where you can define the control signals that exist in the computer, use them to program instructions out of microinstructions/steps, and use those instructions to write programs that can run on the computer:
The instructions and programs can then be written to instruction ROM or computer RAM, respectively, using an Arduino (which is controlled from this same app).
With this, you can program the computer to do essentially anything that it’s physically capable of with the provided hardware. The program shown in the third screenshot above prints “Hello World!” to the LCD screen (bottom left). Here is a video of it running:
To avoid waiting too long for the message to appear, the program is running too quickly for the human eye (tens of clock cycles per second), so you can’t really see the individual instructions running – but it is rather spectactular to watch the control signals (white LEDs) going crazy! If you look closely, you can see the “HLT” LED at the right light up pink at the end – this is the “halt” signal that tells the computer to stop running when the program is done.
In the previous post I explained the schematics of the different modules of the computer. Based on those, I made a PCB (printed circuit board). You can click the images to zoom in.
As you can see, the board is quite cramped. I wanted to reduce the board size as much as possible to save costs and make it more portable. This made signal routing quite challenging, as the through-hole component pads (green) don’t leave a lot of room for the over 2000 wire connections required. I also fit it all into 2 layers (top and bottom), I didn’t want to use a 4-layer PCB as it’s significantly more expensive and I believed that it could be done with just 2. It seems that I was right.
The total board size is 31.5 x 16.1 cm (12.4 x 6.4 inches), which is a lot smaller than I initially expected, considering the breadboard prototype was about 40 x 40 cm (15.7 x 15.7 inches).
I wanted to keep the individual modules visually separated on the board to show how the computer is made of these somewhat independent pieces working together. I went through multiple layout iterations for each module to make it stand out as a separate entity but still fit in well with the other modules. Additionally I added some thick lines on the silkscreen as rough outlines between the modules and labeled them all. The LEDs are also arranged in nice bit strings in the correct reading order and labeled if necessary.
Here’s the raw PCB, fresh from the friendly neighborhood chinese factory:
And here are some impressions from the assembly process:
The assembly was surprisingly fast, it only took about a week. Most of it was rather repetitive, soldering thousands of component pins, but seeing it come together felt great anyway.
Find the introduction here. Previous parts: 1, 2, 3, 4.
Another one of Ben’s modules that’s been slightly expanded, the program counter is a basic tool that is required for program execution: The computer needs to keep track of its current position in the running program. That way it always knows where in memory the next instruction can be found. The program can also overwrite this counter with an arbitrary value, which has the effect of jumping to that point in the program.
The counter itself is implemented using four 74LS161 4-bit binary counter chips which are cascaded to create one 16-bit counter. As the data bus is only 8 bits wide, the counter input and output is divided into two virtual registers, P (higher 8 bits) and C (lower 8 bits). As usual, the output is managed using 74LS245 bus transceivers and there are some LED packs to display the counter value.
Instruction control logic
Now this module is where the magic happens: It tells all the other modules that we’ve seen what to do and when to do it. Ben’s version can be found here.
Let’s start on the left side: There are two 4-bit register chips that store the current instruction code, which is 7 bits long. Above that there’s a 4-bit counter that keeps track of the so-called microinstruction. Microinstructions are small steps that are exactly one clock cycle long and do a very basic task like moving a byte from one register to another. Multiple microinstructions are combined in sequence to execute an instruction which does something more complicated, like fetching a value from memory or adding two numbers. Afterwards a few extra microinstructions are executed to retrieve the next instruction from memory, this is called the “instruction fetch” cycle.
There are some LED packs to display the instruction and microinstruction values, and then these two values are fed into the next part of the circuit, which is combinational logic. Here it’s implemented using three ROM chips, similarly to the numerical output module (see part 4). This logic translates the instruction code and microinstruction step into the control signals that are fed to all other modules in the computer (input enables, output enables, other behaviour controls). The topmost chip’s outputs are fed into two 74HCT154 1-of-16 selectors (also called demultiplexers). This way the 8 bits from the ROM output can be expanded out into 16 input enable controls and 16 output enable controls, using the fact that only one input and one output need to be enabled at any point. All these control signals are displayed using some white LEDs and some of them are inverted if the corresponding modules require a different signal polarity.
To help the computer’s timing, the microinstruction counter increments on an inverted clock pulse (see the inverter left of the counter chip). This way the computer alternates between executing a microinstruction (rising clock edge) and setting the control signals for the next microinstruction (falling clock edge).
The details and implementation of instructions will be discussed in a separate post, for now, let’s look at the PCB that I made from this schematic.
Find the introduction here. Previous parts: 1, 2, 3.
User Input and LCD
These two modules are relatively simple additions that greatly improve the abilities of the computer. Let’s start with the LCD module (bottom).
The main part of it is, of course, the LCD panel itself. I used a typical 16×2 character LCD display with a blue LED backlight. It has an HD44780 controller (or, more likely, a chinese clone of it). That controller is ideal here, as it has an 8-bit parallel data interface that I can just directly connect to the main bus.
The only things left to connect were contrast (potentiometer on the left), the enable signal (enabled when the LCD is selected and a clock pulse occurs), the register select pin (just another control line) and power/ground. The positive power supply is buffered through a CMOS inverter (two transistors) and is briefly turned off when the computer is reset. That is the easiest way to make sure that the LCD is cleared and reset whenever the computer resets.
The user input module features the input switches themselves (S400, right) with pull-up resistors and a bus transceiver for output. Alternatively, input can be supplied from the Arduino (see RAM module in part 2) or from an external connector (J400, middle of the page).
To the left of the module you can see the input enable latch (made of two NAND gates). It’s a simple SR-latch (set-reset-latch) that gets set when the user input is enabled (control signal NO). This lights a pink LED that signals that it’s waiting for input and pauses the clock to give the user time to submit their input. The user then presses a button (S401) to reset the latch, resuming the clock and allowing the computer to read the input.
This module also exists in Ben’s computer and is very similarly designed. It’s a simple seven-segment display that can show an 8-bit value in different representations and interpretations.
On the left side there are three 4-bit register chips. Two of them form an 8-bit register to store the value to be displayed and the third one stores a 3-bit value that selects the display mode. Currently only modes 000 (decimal unsigned integer) and 001 (decimal signed integer) are implemented, but I could add things like hexadecimal or binary display options.
On the bottom is a fast 555 timer connected to a 74LS161 4-bit binary counter. This is used to quickly switch between the four displays. What this means is that really only one of the four digits is being displayed at once, but it quickly switches between them so that it looks like all of them are lit constantly. This is also called “multiplexing”.
In the middle you can then see the last part of the display driver: The combinational logic. The inputs are the value to be displayed, the number of the currently lit digit and the selected display mode. From that, you can determine which segments of the digit should be lit, which are the outputs of the combinational circuit. Instead of creating a circuit out of logic gates, it’s possible to use a ROM (read-only-memory) chip and store the correct outputs for each combination of inputs in the memory. Here I used an AT28C64B ROM chip.
Here we have two very useful modules. The first one is the arithmetic unit, which can do addition and subtraction (top half of the page). Its main components are two 74LS283 4-bit adder chips (U700, U701) that are chained together to create an 8 bit adder. After the adder chips, there’s an LED pack to display the sum and a bus transceiver to output the value to the rest of the computer.
For subtraction, the module forms the two’s complement of B, effectively negating it, and then adds that to A. The two’s complement is formed by inverting every bit of B using the XOR gates on the left and then adding 1 (done here using the carry input signal of the first adder chip). If you have no idea what any of that means, you should probably check out Ben’s videos on two’s complement and this module – the module is identical to his design.
Something I never even thought of before watching Ben’s series is that an “add” instruction using this module doesn’t actually tell the computer to do the addition – the addition is always done and the sum is always available and can be seen on the LEDs. The only thing that an “add” instruction does is tell the computer to output that sum value onto the bus and store it in a register. Now that seems quite obvious and normal to me, but it blew my mind back then. There are a few more things I learned about computers during this project that made me feel that way – which is why I consider this a very successful and valuable project.
On the lower half of this page you can see the comparator module. It allows the computer to compare two numbers by magnitude, which is quite important as it opens the way to conditional branching – an essential concept in programming. It is implemented using two 74LS85 4-bit comparators (U705, U706) chained together, creating an 8-bit comparator. Next to that is a 74LS153 dual 4-to-1 selector (U709), which provides the selection logic that allows both signed and unsigned numbers to be correctly compared. Then there is a 4-bit register to store the states of the less than, equal, greater than and carry flags from both modules on this page, as well as some output logic that allows individual flag values to be output onto the main bus.
Magnitude comparison of unsigned numbers is done quite simply: The comparator first checks the highest bit of the inputs and checks if they’re different. If they are, the input value that has a 1 in that place is definitely larger than the other input value – and so the corresponding output (A<B or A>B) is turned on. If the highest bits are the same, it checks the second highest bits the same way, then the third highest and so on, until it finds a bit difference between the two inputs. If no difference is found after all bits have been checked, the numbers are equal, which is signalled using the A=B output.
For signed numbers in two’s complement format it seems a bit more complicated at first glance – but it turns out that the comparison can be done using the same 8 bit unsigned comparator, you just have to swap the A<B and A>B outputs if one input is negative and the other one isn’t. That is what the selector chip U709 does if a signed comparison is requested. If don’t see why that trick works, try it yourself (on paper)!
The Bitwise Logic Module
While this page is larger than the previous one, it’s actually a lot simpler. It implements bitwise NOT, AND, OR and XOR operations that can be applied to the registers A and B, which can be quite useful for programming and conditional logic.
The components are straightforward: There are 8 logic gates of each kind named above, used to compute all four operations (NOT A, A AND B, A OR B, A XOR B). To the right there are four dual 4-to-1 selectors (74LS153, just like in the comparator above). They are used to select one of the four results that should be output to the bus. The selected value is then displayed on an LED pack and connected to a bus transceiver for output.
This module together with the arithmetic unit above form the ALU (Arithmetic Logic Unit) of the computer, which does most of the useful work in most programs.
This page contains a few independent, small modules. The largest one is the shift register, which occupies most of the left half of the page. A shift register can be used just like any other register, but it has an additional ability: it can shift the binary value inside it to the left or right. This can be quite useful in programming and calculations, as a shift to the left corresponds to a multiplication by 2 (and a right shift is a division by 2). I simply used a dedicated 8 bit shift register chip (74LS299, U300 on the left) to implement this.
Usually when shifting, the outermost bit that gets “shifted out” is simply discarded and the other side of the byte is filled with a zero. But sometimes it can be useful to keep that bit and push it back into the other side of the byte instead. That operation is called a “roll”, and it is implemented using the two AND gates next to the chip. That way the program can use the SRO (shift roll-over) control signal to enable this feature. In addition to the register chip, it also features a bus transceiver and LEDs as before, except that the bus tranceiver is used bidirectionally here, both for reading and writing data to/from the shift register.
The next part of this page on the right side are some utilities for the main bus. There is an LED pack to show what is currently on the bus and some pull-down resistors to hold the bus at all zeroes when no module is outputting data to it. Then there’s a bus transceiver and a transistor that the control logic can use to set the bus to the values 01, FE or FF (hexadecimal) instead of the default 00. That is useful for the quick implementation of some instructions.
Finally, this page houses the logic for the main reset signals of the computer (bottom left). The reset is triggered by the button S300. From that the circuit produces reset signals in both polarities (active high and active low) that are distributed to all memory-containing modules, resetting the computer’s state (but not clearing the RAM). It also handles the two smaller reset signals RM and RMSB, which can be triggered either by the main reset or by the control logic.
The RAM and Z register
Now we get to the largest page of the schematic. It contains the RAM (Random Access Memory), the memory address register, programming logic and the general purpose 16 bit register Z. The RAM module is relatively similar to Ben’s design, but expanded by a factor of 2048 – from 16 bytes to 32KB.
The RAM chip itself (HM62256) is the largest schematic symbol on the page (U200 in the middle right area). On its right side is the memory data bus which is used to transfer data (one byte at a time) to and from memory. It’s connected to the main bus through a bus transceiver (directly to the right of the RAM chip). On the left side is the 15 bit wide memory address bus, giving it a total of 32768 memory locations (=32KB of memory).
The RAM module is relatively complex because there are two separate ways that the RAM needs to be accessed: by the computer when running a program and by the user/arduino when programming the computer (writing the program into memory). Both of these supply an address and data. Therefore the module must select one of the given addresses (program address or externally supplied address) and the corresponding data lines, based on whether the computer is running or being programmed. This also happens in Ben’s design.
The user must select between “run” mode and “programming” mode using the switch S200, you can see it at the bottom right of the page. Based on that signal the address is selected using 4 selector chips (74LS157) that you can see on the left of the main RAM chip. The address for “run” mode is stored in the 15 bit memory address register (top middle) which is made of four 4 bit register chips. The data in “run” mode is provided by the main bus through the bus transceiver mentioned above. In “programming” mode, the address and data are provided by either manual switches (bottom middle) or by an Arduino through some shift registers (74HCT595, bottom left).
Now for the Z register. You can find it on the top left side of the page and it’s not part of the RAM module. It’s on the same page because it is functionally related to the RAM, often serving as an address buffer for memory access. It consists of four 4 bit register chips and two 8 bit bus transceivers connected to the main bus.
Last but not least, there are quite a few LED packs on this page: 16 LEDs showing the contents of the Z register, 15 LEDs showing the currently selected memory address (for “run” mode) and 8 LEDs showing the RAM contents at the currently selected memory address, as well as a green/red LED pair indicating “run” or “programming” mode respectively.
This is one of the few modules that I barely changed from Ben’s design. I only tweaked some of the passive component values and introduced a second signal which pauses the clock. Here’s a quick overview:
The three NE555 timer chips each do different things. U100 at the top works in astable mode, which means its output is constantly oscillating between low and high. This is used for the main automatically running clock of the system. The frequency of that clock signal can be controlled by the user using the variable resistor RV100, that way you can freely choose how fast you want the computer to run.
U101 runs in monostable mode, which means its output is off until it receives a trigger signal from the button S100, at which point the output turns on for a set period of time, then off again. This is used to allow the user to manually input individual clock pulses for debugging and demonstration purposes, while also filtering out switch bounce effects.
U102 runs in a bistable mode, which means its output essentially mirrors the state of the switch S101. The only reason it’s there (instead of just using the switch by itself) is to filter out switch bounce (see above). The user can then flip the switch to choose between the auto-running clock and the manual clock pulses (the latter of which effectively pauses the computer, allowing single steps to be made).
There are also two signals which, when either of them is high, will block any clock pulses from being generated. Those are the HLT (halt) signal that’s set when the computer has finished execution of its program, and NO_LATCH which is set when the computer is waiting for user input (see the User Input module in part 4).
Now let’s look at a very basic component that exists in pretty much any processor: Registers. They are very small, but very fast pieces of memory that are used to buffer data for the processor to work with. These four registers each store 8 bits (1 byte) of data and are used for specific things:
The A register is referred to as the “arithmetic accumulator” or just “accumulator”. It is used both as source and target of many operations. For example during an addition, it contains one of the summands and then gets overwritten with the sum.
The B register is used as the second operand for most operations. To complete the example above, the “add” instruction performs the addition A + B and stores the result back in A. To save some space and control signals, the B register can only be written, but not directly read by the program. (If you really need to read the B register, you can just add 0 to it and you’ll get the “result”, which is the value of B, in the A register.)
The V register is a general purpose register that the programmer can use as they please.
The F register is a hidden register, so it cannot be directly accessed by a program. It is instead used internally by many instructions as a “parking space” for a value that may not be directly available later, for example for swapping the contents of two registers.
Each register consists of two 4-bit register chips (74LS173), eight LEDs to show the register contents with an octuple resistor array to limit the LED currents, and a 74LS245 bus transceiver that allows the register to output its value onto the main bus (with the exception of the write-only register B, which is just missing the bus transceiver).
Here I’ll be documenting my second large electronics project, which was a lot of fun to work on. It’s not quite 100% finished as of writing this (some very minor issues and optional improvements left), but I decided I’d call it done and maybe get back to it later.
The idea for this project came from the awesome YouTube educator Ben Eater, who made a series about the basics of computer architecture where he built an 8 bit computer based on a very simple model/structure. I highly recommend that you watch the series, he does an amazing job at explaining every part of his design from the ground up and in my opinion it’s just fun to watch as well. My design is directly based on Ben’s, but I added new features on top of it and expanded/reworked some core sections. Therefore I’ll be referring to his videos when writing about the individual sections, particularly if I haven’t changed them a lot.
I initially tried to build the computer on prototyping breadboards just like Ben did, but some modules just ended up being too complicated and cramped, so I didn’t get it to work. Suboptimal breadboard quality and dense wiring caused connection problems all over the place, causing the behaviour to be very erratic, which stopped me from going any further with that design. So I sat down and spent a lot of time creating a full schematic of the computer and designing a PCB based on that. Let’s start with a summary of the specs and modules of the computer:
Adjustable clock (about 1Hz to 5kHz) with pause/single-step function
8 bit shared address + data bus (“main bus”)
32KB of static RAM
8 bit arithmetic unit (add/subtract)
8 bit logic unit (NOT, AND, OR, XOR)
8 bit comparator (with 4 bit flags register)
Two 8 bit and one 16 bit general purpose registers
8 bit bidirectional shift register with roll function
15/16 bit program counter
7 bit opcode length (up to 128 instructions) with up to 16 steps per instruction
8 bit user input (using switches for binary input or data from an external connector)
Seven segment display for numerical output with up to 8 output modes/formats
Character LCD for text output
Programmed manually with switches or using an Arduino Nano
And perhaps most importantly: 196 LEDs showing exactly what is going on in every part of the computer.
This may seem like a lot, and it is, but I’ll do my best to go through everything and explain it in the next few posts: