Bare Bones in Logisim
This tutorial serves as an overview of how to create an implementation of a Bare Bones ISA based CPU using Logisim logic simulator. This is not a Logisim tutorial in and of itself, and will not go into how to use it.
In order to follow this tutorial it is expected of you to have an understanding of Boolean Algebra, combinational logic, sequential logic, as well as, understanding of binary and hexadecimal numeric systems. Since this tutorial will make use of Logisim, you also need to know how to use it. Reading the built-in tutorial should be enough.
We will construct our computer according to Von Neumann architecture. A single memory space for both instructions and data. In accordance to Bare Bones ISA, we will also have ROM chip and IO device (Teletype) that is mapped into the memory space. Our CPU will follow simple two bus architecture. That means that all parts of the CPU that operate on data will send said data over one Data Bus, while address manipulating parts of the CPU use Address Bus. There is one more bus, Control Bus, that is used by Control Unit to control different parts of the CPU.
The CPU has 16 general purpose registers, Instruction Pointer, 2 ALU (A and B) registers, Temporary (TMP) register, as well as, 2 registers to store currently executing instruction (OP-H and OP-L). All registers are 8-bit.
The computer can address 256 bytes of memory which is divided into RAM, ROM and, IO. They are mapped as follows:
We will brake the computer into 4 sub-circuits:
- main - this is where the computer will live. It contains CPU sub-circuit, TTY, ROM and, RAM;
- CPU - this is the heart of our computer, and the thing we want to build. It contains ALU and Control Unit (CU) sub-circuits;
- ALU - the workhorse of the CPU, actually computes all the data;
- CU - the conductor of this orchestra, contains all the control logic.
Since we brake down the computer like that, we can focus more on one part at a time while we're building it. So let's begin!
Arithmetic Logic Unit
ALU is the beating heart of the CPU, and where all the computation actually happen. In accordance to Bare Bones ISA our simple ALU will only provide one function: arithmetic addition.
The ALU has 3 inputs:
- A - the 1st operand;
- B - the 2nd operand;
- ADD - Selects arithmetic addition as an operation.
And 1 output:
- OUT - the result of A <op> B
We don't care about carry in or out.
Register File and Other Registers
Now that we can add data, we would like to store it. For that, as mentioned above, we have 16 general-purpose registers. They are implemented as a register file, to select the register you want to work with you send a 4-bit number that represent the index of the register in the file. The register file is connected to Control Bus that controls if it is possible to read or write data, and to which bus (address, data).
Since our registers are not directly wired (multiplexed) to the ALU we create ALU-A and ALU-B registers to serve as a temporary place for data to reside for ALU to use as inputs.
We also add a TMP registers to aid with swapping data (NOTE: This could be optimized away with ALU registers).
In order to keep track where in the program we are we also need IP (Instruction Pointer) register. It's not just a simple register, it's a counter, since we want it to increment every time we fetch an instruction.
And the last thing we need is 2 registers to hold currently executing instructions for the Control Unit to decode and execute. These are OP-H (High byte of the instruction) and OP-L (Low byte).
Control Unit and Control Bus
The Control Unit for this CPU is a hardwired one, as opposed to Microcoded Control Unit. The logic is pretty simple, instructions can take multiple phases (clock cycles) to complete. So we have a 8-bit shift register that feeds back into itself, creating sort of a Ring Counter. This gives us 8 phases in which the instruction can be. The first two are the same for all instructions, it's the fetch phase of the instruction. It takes two cycles, because the instruction word is 16-bit, so we load the instruction in parts. As described above, we have two registers that hold this data.
On the left of the circuit is where the decoding happens. We simply use 4-way AND gates and negate the inputs to represent 0s in the opcode.
The actual execution logic is described in the "cells" where phase and opcode lines intersect. This is checked by a simple AND gate. Then, the desired bits are set on the control bus to activate the required components. The instruction then can either progress to next phase, or reset the phase shift register, restarting the whole cycle.
The Control Bus itself is just a 32-bit bus, bits of which are routed to all the parts of CPU, like registers (enable, load), IP (enable, load, count), ALU (ADD) and other.
|0||Enable Write (to Memory from Data Bus)|
|1||Enable Load (from Memory to Data Bus)|
|6||IP Enable Write|
|8||IP Load (to Address Bus)|
|9||(General Purpose) Registers Enable Write|
|10||(General Purpose) Registers Enable Load (to selected bus)|
|11||(General Purpose) Registers Bus Select (0 - Data, 1 - Address)|
|15:12||(General Purpose) Register Index|
|16||ALU Load Output|
|20:17||ALU Function Select (ADD = 0, 1-15 - Reserved)|
|21||ALU-A Enable Write|
|22||ALU-B Enable Write|
|23||OP-H Enable Write|
|24||OP-L Enable Write|
|25||TMP Enable Write|
|26||TMP Enable Load|
Each instruction's phase output mostly constant bits to control bus. They put the CPU in the required state in order to process the current phase of the instruction. Some instructions (if not most) are working with registers and/or ALU, so some data from the instruction word itself is spliced into the control word. Most notably Register Index and ALU operation.
On power up (Logisim's start of the simulation) the CPU starts in what is, basically, a halted state. Some logic is required to properly set it up for the Fetch-Decode-Execute Loop. In order for the CPU to start executing the instructions we need to set up next things:
1. IP needs to point to the starting address (0xC0 (the ROM) as per Bare Bones ISA); 2. Control Unit Phase should be set to 0 (the shift register should emit 1 for bit 0); 3. Clock signal should be connected to the rest of the CPU; 4. We also check if CPU is reset when the clock is high (this was leading to some unexpected behaviour).
In order to achieve this we set some D Flip-Flops that detect the first start of the CPU and upon reset trigger the logic for all those things.
Memory and IO
The only thing that is left now is to setup our memory. We need to map RAM, ROM and, IO into single memory space. For that we use a Decoder (for ranges) and a Comparator (for single address) (NOTE: It is better to use AND gate like we used in the Control Unit Decode stage, comparator adds too much logic, and it is arithmetic not bitwise).
And that's that, this is the whole computer!
Now we want to see it in action. For that let's write a simple program that reads a byte from memory, increments it and, stores it:
LDI R0, 0xCE # Load the address of the predefined byte into R0 LDI R1, 0x80 # Load the address of the TTY into R1 LDA R2, R0 # Load the predefined byte into R2 LDI R3, 1 # Load the constant 1 into R3 ADD R2, R2, R3 # Increment R2 STA R1, R2 # Store the value from R2 into TTY HLT # Halt DB 0x41 # Predefined byte 0x41='A'
Now let's hand-assemble it into Logisim ROM image file and store it as ROM.img:
v2.0 raw 40 CE # LDI R0, 0xCE 41 80 # LDI R1, 0x80 22 00 # LDA R2, R0 43 01 # LDI R3, 1 52 23 # ADD R2, R2, R3 31 20 # STA R1, R2 10 00 # HLT 41 00 # DB 0x41
Now load it into the ROM, reset the CPU and start the clock, it should output:
And that's that! You've made a CPU, albeit a very simple one!
But what's next? Well, you can extend the ISA. I reserved some bits in the Control Bus so you can more easily add functionality without rewiring the whole bus. The easiest things to add are ALU operations. All you need to do is to add them to ALU and decode the ALU Function 4 bits of the Control Bus. After that try to add jumping to make your CPU Turing Complete.
It also may be a good idea to create your own ISA. This one may be limiting, for example, having only 256 bytes to work with is tough. Is going full 16-bits is a good idea? 32? 64? 128?! That is for you to decide. The purpose of this tutorial was to introduce you to the concepts that are required to create a CPU.
If you are going to design your own ISA, please check ISA Design Considerations.
This is it! Have fun! And happy building!
If you built your CPU and want to compare it to this implementation, or just want to play around with it you can download it from redgek's website: https://codebite.xyz/other/cpudev/barebones/cpudev-bare_bones_ISA-impl.circ