With that, the 8 Bit Computer project was essentially finished.
This was my second large electronics project, and it was a lot of fun to work on. It’s difficult to consider it “completely done” (some minor issues and optional improvements remain), but I decided I’d call it done and maybe get back to it later – though that seems to be a recurring theme with all hobby projects, at least in my experience. Also, none of these imperfections change 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 basic level.
This project was designed and mostly built before I started my university education in computer science. The experience from it helped me a lot in my first computer architecture courses, as I already knew a lot of the concepts. However, there were 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 binary program. 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 programmer, 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 spectacular 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 posts, I explained the schematics of the different modules of the computer. Based on those, I made a large 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 (at this size), and I believed that it could be done with just 2. It seems that I was right. As expected, given this was my first dense circuit board design ever, it does contain some beginner mistakes – for example, not using any ground planes, and making power/ground traces thinner than ideal. But it’s fine, I learned from it, gained experience with PCB design, and the board did end up working anyway.
The total board size ended up being 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 drew some thick lines on the silkscreen as rough outlines between the modules and labelled them all. The LEDs are also arranged in nice bit strings in the correct reading order, and labelled if necessary.
Here’s the raw PCB, fresh from the friendly neighbourhood 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.
Now that the PCB was assembled, there was still one thing missing for the computer to work: Programming the instruction behaviour. This is what we will look at next.
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 (“page”, higher 8 bits) and C (“counter”, 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. These two values are then 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 bus input enable controls and 16 bus output enable controls, using the fact that at most one input and one output need to be enabled at any point. All of 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 on the 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, but for now, let’s look at the PCB that I made from this schematic.
These two modules are relatively simple additions that greatly improve the practical 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.
Numerical Output
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.
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 index 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 important 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 many 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 for me.
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 bits 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 small independent 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 transceiver 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, contains some utilities for the main bus. There is an LED pack to show what is currently on the bus, as well as 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, which 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 lot – 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 the 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, which you can see 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 either by 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.
Let’s start going through the modules of the computer.
The Clock
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, allowing them to freely choose how fast they 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 store 8 bits (1 byte) of data each and are used for specific things:
The A register is referred to as the “arithmetic accumulator”, or just “accumulator”. It is used both as the source and the 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).
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 was 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. Most of this design adaptation was done in early 2018, when I was still in high school. Of course, this means that the design is not quite ideal in hindsight – but considering my lack of experience and knowledge back then, I think I did quite well.
Initially, I 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. Mediocre breadboard quality and dense wiring caused connection problems all over the place, resulting in very erratic behaviour, which stopped me from going any further with that design. So in early 2019, I sat down and spent a lot of time creating a full schematic of the computer, and designing a PCB based on that. Here’s 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.
In the spirit of Ben’s original series, I will be documenting this project in a relatively high level of detail and trying to explain every part of it, though I will refrain from repeating the explanations given by Ben for the general function of the computer – he probably explains it much better than I could anyway. Let’s get started:
To provide the best experiences, we use technologies like cookies to store and/or access device information. Consenting to these technologies will allow us to process data such as browsing behavior or unique IDs on this site. Not consenting or withdrawing consent, may adversely affect certain features and functions.
Functional
Always active
The technical storage or access is strictly necessary for the legitimate purpose of enabling the use of a specific service explicitly requested by the subscriber or user, or for the sole purpose of carrying out the transmission of a communication over an electronic communications network.
Preferences
The technical storage or access is necessary for the legitimate purpose of storing preferences that are not requested by the subscriber or user.
Statistics
The technical storage or access that is used exclusively for statistical purposes.The technical storage or access that is used exclusively for anonymous statistical purposes. Without a subpoena, voluntary compliance on the part of your Internet Service Provider, or additional records from a third party, information stored or retrieved for this purpose alone cannot usually be used to identify you.
Marketing
The technical storage or access is required to create user profiles to send advertising, or to track the user on a website or across several websites for similar marketing purposes.