To use the register file, ALU, and screen memory, along with some other components, to build a small, but complete CPU in Logisim.
Your CPU will use the imaginary "Pinky" instruction set architecture, which is a very simple derivative of ARM. All Pinky instructions are 16-bits wide. There are two formats for instructions, "R-type" and "I-type", which are described below:
The R-type instructions contain an opcode, and three register numbers:
R-type | |||
Opcode | Dest Register | Src1 Register | Src2 Register |
4 bits | 4 bits | 4 bits | 4 bits |
The I-type instructions contain an opcode, 1 register number, and then an 8-bit immediate value:
I-type | |||
Opcode | Register | Immediate | |
4 bits | 4 bits | 8 bits |
The opcode will tell you what type of instruction you have. The first 12 opcodes are for R-type instructions, while the last 4 are for I-type instructions. Each individual opcode is described in the sections below.
There are 12 R-type instructions as follows:
Opcode | Mnemonic | Effect |
0 | and | dest = src1 & src2 |
1 | orr | dest = src1 | src2 |
2 | eor | dest = src1 ^ src2 |
3 | not | dest = ~src1 |
4 | add | dest = src1 + src2 |
5 | sub | dest = src1 - src2 |
6 | mul | dest = src1 * src2 |
7 | slt | dest = (src1 < src2) ? 1 : 0 |
8 | lsft | dest = src1 << src2 |
9 | rsft | dest = src1 >> src2 |
10 | ldr | dest = M[src1] |
11 | str | M[src1] = src2 |
Unlike the ARM, Pinky uses explicit instructions for shifts. It also does not allow immediate values for any of these arithmetic instructions, or include an offset for memory instructions. Also unlike ARM, Pinky does not have a general cmp instruction. Instead, slt can be used to compare two registers.
There are 4 I-type instructions as follows:
Opcode | Mnemonic | Effect |
12 | mov | reg = immed |
13 | b | PC = PC + immed |
14 | bz | if (reg == 0) PC = PC + immed |
15 | bnz | if (reg != 0) PC = PC + immed |
The mov instruction simply loads the 8-bit immediate field into the specified register. The immediate should be sign extended so that it works with negative numbers. You can find a sign extender in Logisim.
The branch instructions modify the program counter (PC). Unlike ARM, the PC in Pinky is not one of our 16 registers. Instead, it is an internal register. The branch instructions add the immediate value (which may be positive or negative) to the PC. This allows us to branch backwards or forwards in the program.
There are five major processor stages that you are going to have to implement for this project. The first of these is the fetch stage which reads the instructions from memory. Your CPU will have a ROM for storing instructions (under the memory section). The data bits should be 16 (because each instruction is 16 bits), and the address bits should also be 16. This gives us 65K of instructions which is more than we would ever need.
Another part of fetching instructions is keeping track of which instruction to fetch next. This is done with the program counter (PC) which is a 16-bit register. The PC should be put into the address input of the code ROM which will then read the instruction at that location.
The fetch stage has an adder which adds some number to the PC each cycle. Normally this number will be 1, but fetch should have two inputs from other parts of the processor. One of these tells fetch it should do a branch, instead of moving on to the next instruction, and the other should give the value to add to the PC instead of 1.
The input to the decode stage is the 16-bit instruction we just fetched. The job of this stage is to rip out all of the information that we need from the instruction into a more useful form.
One part of this is splitting the instruction into the different fields (opcode, register numbers, immediate etc.) with splitters. You should have wires for the source registers and the immediate - even though only one will be valid at a time.
The immediate value should also be sign extended from 8 bits to 16. This immediate value will be passed along to the next stage.
You should also put in comparators for the opcode to create signals that indicate whether we are doing a load, whether we are doing a store, whether we are doing a branch instruction, whether this instruction writes the register file, and whether this is an I-type instruction. These signals will control the next stages of the processor as well.
In this stage, you should have the register file you created in part 1. Some inputs should come from the decode stage: the register numbers we are reading and writing, and whether we are writing a register at all.
Its write data should come from a later stage. In the case of a ldr instruction, this will be the memory stage. In the case of arithmetic instructions, the execute stage.
The output of the register file, the two registers we've read, should be passed to the execute stage.
The execute stage contains the ALU. It feeds the two source values and the opcode into the ALU inputs, and routes the result back to the register file.
In the case of an I-type instruction, the second argument to the ALU should be the immediate value instead of a register file value.
Our CPU has three distinct memory spaces. The first is for addresses 0x0000 through 0x7fff (all 16-bit addresses which start with a 0). These addresses refer to 32K of general purpose RAM. Reads and writes to theses addresses should be directed to a RAM component from the memory section with an address width of 15-bits, and a data width of 16 bits. To make wiring the RAM simpler, set the "Data Interface" to "Separate load and store ports".
The next section of memory is 0x8000 through 0x800f which are the next 16 addresses after the end of RAM. These addresses are mapped to our screen memory unit. This means that writes to this range do not go to RAM, instead they go to VRAM and change the screen instead. We cannot read from VRAM with ldr instructions, we can ignore this possibility.
The last section is only one address 0x8010 which is the button state register. Writes to this address do nothing, but reads from this address return the current button state. The button state is given as a single 16-bit number, but only bits 0-5 are ever on. Their meaning is as follows:
To create this, you can just create the six buttons, and then wire them into a splitter - with each button wired to the right slot - and use that value as the button state register.
For a store instruction, the data being stored is always the value of the second source register.
You should make one global clock which routes to the PC, register file, data RAM, and screen memory. This will keep all of the memory components in sync.
You should also create one "Clear" button which routes into all of those same memory components. This will make testing easier since you can clear all of the CPU state at one time. Essentially this resets the whole computer.
Connect the screen memory to an LED matrix from the input/output section. It should have 16 rows and 16 columns, and have "Rows" as its input format. You should attach this up the 16 screen memory outputs, so you can continually see what is on your screen.
The syntax of Pinky assembly files is quite similar to ARM. Comments
begin with the '@' character, and the instructions are named given the mnemonics
above. The assembler does allow register to register moves, despite the only
move instruction above being I-type. It does this by assembling mov r1, r2
as and r1, r2, r2
which amounts to the same thing.
You don't actually need to write any assembly programs for this assignment, but if you'd like to assemble your own programs, you can use the assembler online. If you come up with a cool program, submit it along with your CPU for (substantial) extra credit!
However, reading the assembly source will be helpful when debugging your CPU!
You should test your CPU with the following example programs:
Program | Assembly | Hex | Description | Output |
Fact | fact.asm | fact.hex | Outputs the factorials of the numbers 1 through 8. | fact.png |
Checkers | checkers.asm | checkers.hex | Draws a checker board pattern on the screen. | checkers.png |
Sum | sum.asm | sum.hex | Adds the numbers 0-100 and checks the result. | sum.png |
Pong | pong.asm | pong.hex | Playable version of the game Pong! | pong.gif |
Guy | guy.asm | guy.hex | A little guy who can jump on the screen | guy.gif |
Evan! | evan.asm | evan.hex | Program which repeats a message on screen | evan.gif |
To run these programs, you should click on your code ROM, and click the Contents attribute. This will pull up an editor. You can edit the programs your CPU will run here, or click "Load" and point it to a .hex file.
Once the program is in code ROM, the CPU should run the program, executing each instruction in turn!
Copyright © 2024 Ian Finlayson | Licensed under a Creative Commons BY-NC-SA 4.0 License.