8-Bit Computer (2021)

<< click to return to front page

<< click to return to content page

Project Photos:\text{Project Photos:}



Practicing:\text{Practicing:}

  1. Soldering, Circuit, and PCB Design
  1. Prototyping, Troubleshooting, and Verification Techniques (oscilloscope, multimeter, etc.)
  1. Learning about Microprocessor Architecture
  1. FPGA Testing

History + Outline:\text{History + Outline:}

From late 2020 to mid 2021, this 8-bit computer was my first introduction to computer architecture and digital logic. I was initially inspired by Ben Eater’s model, but here I’ve outlined several of the improvements and changes I’ve made to extend the architecture. I presented this project at the 2021 (provincial level) Greater Vancouver Science Fair, receiving Senior Bronze Divisional.


1. Baseline

I’m not going to go too in-depth here, since the baseline computer is derived from Ben Eater’s model, which you can find here: https://eater.net/8bit/. The first version of this CPU was made on an array of breadboards, which I later soldered onto circuit boards.

Alongside the logic itself, I also had to make an auxiliary EEPROM programmer to implement the control logic of the CPU. It’s not as sophisticated as Ben Eater’s shift register programmer, but it gets the job done.

An ATTINY85 (bottom left) was used to generate <1000 ns clock pulses to write to the AT28C16.

One regret I had in this stage was not using an FPGA or similar large IO device for the control logic. Debugging, writing, and reading EEPROM contents became increasingly challenging down the line, which ended up being one of the largest hurdles in this project.

My final build was as follows, and here I replaced the breadboards with perf-boards and chip sockets instead, soldering the components on for stability (a lot of the wiring here is on the underside!).

⚠️
Referring to my earlier disclaimer, this project was made before the idea of this website was conceived, so I don’t have many pictures of the physical product. Currently, the CPU and my modifications are in a storage unit; the next time I get my hands on it I’ll make sure to take some pictures and add them to the sections below.

2. Conditional Programs

Implementing conditional execution was my largest improvement and what I presented at the Greater Vancouver Science Fair (Provincial). Instead of a standalone modules like RAM or registers, this device synthesizes the RAM, ALU (arithmetic logic unit), and program counter together.

Introducing Jumps (long read ahead)

Before I outline how this module was made, I should explain what I mean by “jumping”. In fact, this proverbial “module” I’ve been referring to is, what I like to call, a jump splitter — you’re learn why that is here.

First a small review on what the program counter is. The program counter (PC) is simply a binary counter. When it increments, it sends its count to the RAM, and the RAM returns what instruction should be next executed, which is passed down to the control logic. Thus as the PC counts, the instructions of a program are completed.

Just how registers can load a value by toggling a control pin low, the PC can interrupt its counting, skip to a specified value on its inputs, and then resume counting. Let’s look at an example:

We set some binary digits on our PC’s input. If the counter is not to load, then it counts as regular. But when we instruct the counter to load, its input overwrites the current PC value on the next clock (just like a register). The key difference is that once we set the load signal off, the counter continues counting from the loaded value

So our program counter has jumped to a specified value! This sequence is classified by a jump instruction. Let’s look at the implications of this by considering some programs.

In a sequential program like this, the jump instruction holds little value to us. That’s because each operation is intended to be done after the previous, so jumping around steps is useless. Now let’s consider a conditional program instead.


This is slightly more intricate. If we go through the program sequentially, then we’ll end up executing the if-statement block anyways. You may start to see how the jump instruction can help us.


Here, I’ve partitioned the program specifically, such that each “chunk” of the program is in its own location of memory. The 4-bit number to the left of each instruction is a specific address in memory for this demo.

Since x+y is indeed less than z, our CPU is told to jump to memory address 1000. The PC does as such, and begins execution from 1000, where it will print “in range”. However, we’re not done yet. If we refer to the original python code, the print(z) operation was to execute regardless if x+y<z. Thus, we need to jump back to the end of the if statement, which is at 0011.

And here’s our final “program”! We see that if x+y<z is false, the program counter will just increment from 0010 to 0011, printing z without executing the if-statement block. If x+y<z is true, our counter must jump to a specified location in memory, execute print("in range"), and then jump back to 0011 to print z.

Implementing Jumps

So I’ve introduced the concept of jumping around in a program, and how we can use it to run conditional programs with if-statements. The question is, how do we create a circuit to do so for this architecture?

Let’s take a look at what we’re implementing here:

if (A condition B): jump to XXXX\text{if (A condition B): jump to XXXX}

We assume that our condition has been set up in advance. For example, if we want to do something if x < 4, then we’ve loaded x and 4 into their respective registers (A and B), and the ALU’s comparator has stored the relation between the two; for this we’ll use the following convention:

So the ALU has told us the relation between A and B, but we need to compare this ALU output with what the programmer has specified. So in memory, our program must store 01, 11, or 10 to check if it matches with the ALU’s statement.

step(8) = <INSTRUCTION (4)> <DATA (4)>\text{step(8) = <INSTRUCTION (4)> <DATA (4)>}

In our CPU, each step is 8-bits long. As per the CPU’s architecture, the 4 MSBs (most significant bits) represent the instruction. Letting 00 become an arbitrary prefix, we set 0001, 0011, and 0010 to represent if A<B, if A=B, and if A>B respectively. Notice how the last two bits of these instruction sets align with the ALU’s output.

The 4 LSBs (least significant bits), which hold instruction data, are set to the jump address where the if-statement block is located in memory.


That was a lot to unpack, so let’s go through a quick example. Suppose register A holds x = 3, and register B holds 4. Thus the ALU outputs 01.

Now say that our programmer wanted to jump to memory location 1011 if x < 4. This step would be “compiled” down into machine language as such:

00(01)  1011\text{00(01) }\space\text{1011}

Notice the 01 in parenthesis since we were checking a < relation.

In this case, the programmer is “asking” for (01) and the ALU is indeed outputting 01. This means x < 4 is true! So we would need to jump to 1011.

Suppose in memory we instead wrote 00(10) 1011. In this case, (10) is not the same as 01 — the programmer is asking to jump if x > 4, but x is actually less than 4. Thus we would not jump and simply increment to the next step.


We’ve now greatly simplified our task by breaking it down into three steps:

  1. If the instruction is other 0001, 0011, or 0010, extract the 1st and 2nd bit (2 LSB) from the step to obtain either 01, 11, or 10 respectively
  1. Assuming the comparison has been set up beforehand, obtain the ALU value (either 01, 11, or 10)
  1. If the ALU output aligns with our extracted bits, jump to the specified address in the step (4 LSB).

Now that we’ve broken down this if-statement step, it’s very simple to create a circuit for it:

We saw earlier that regardless of if we hold some value on the PC’s inputs, the value will only load once the load pin is enabled (for the 74LS168 counter, toggled low).

Convince yourself that the jump signal is only held low assuming the following conditions are met;

  1. The 2 MSB’s of the instruction is equal to 00
  1. The 2 LSB’s of the instruction is equal to the ALU’s comparison

Tying this jump signal to our program counter will complete our implementation, with the 4 LSB’s of our step connected to the jump inputs. This circuit is deceptively simple, but it took a lot to get here. The crux of the problem lies with being able to partition memory to allow for this (this is what your compiler does).


One Last Example

Let’s resolve any confusion with one last example:

x = 0
while (x < 10):
	x += 1
print(x)

While staying true to Ben Eater’s opcode and instructions, we can come up with the following to run on our 8-bit CPU:

  1. The program counter begins at 0000 and then 0001. Since the CPU has no intrinsic way of loading an 8 bit value to the registers, we instead pass down 4-bit pointers to other spots in memory, which hold 8 bit values. You can see this by the highlights, and how “load register A with x” actually means “load register A with whatever is in memory 1110”, which in this case = 0. The same applies for loading register B with 10.
  1. Next, we use our newly created expression “IF A < 10 JUMP TO 0110” or 00010110.
  1. Since A (zero) < 10, our program counter would jump to 0110, where the next step becomes to increment A. Since the registers are not built in with any increments, we use a series of elementary instructions to complete it:
    1. First, register B (which once held 10, is overwritten with “FULL”, which equals 11111111)
    1. Subtracting B from A (by two’s complement) means that A will be incremented by 1
    1. Lastly, return the ALU’s output to A
  1. The final step is to set a default jump back to 0001 and set B to 10 again for our comparison.

Let’s take a deep breath before moving on — we just added conditional logic to this CPU! Here’s the final block diagram, including our newly minted JUMP? block.

Again, I’ll make sure to add a picture of the module I made ASAP.

3. Further Development

There’s a lot more that I’ve looked into and created for this computer, but this post has gotten lengthy enough. I was going through some old plans and found some notes on how to implement a 256 byte RAM instead of the current 16 byte RAM. Perhaps this little sketch may give you some ideas.

Some other changes I made which I want to note but can’t delve into are:

  1. 16x2 Character LCD Implementation
  1. Reducing Instruction Fetch Cycle by 66% (this was done by creating a dedicated instruction bus)

I hope you enjoyed reading, and learned something new about how computers really work. Perhaps the idea of what a “program” really is has changed, like it did for me. In fact, pursuing this whole project made me more appreciative of how much our processors do that we don’t get to see — for example, the implementation of recursion! For now, any increasingly-complex work will be on standalone CPU’s (stares at Z80/8008), or on FPGA’s (which I hope to post soon!).

Best,

Ebrahim