CS61C Summer 2018 Lab 6 - CPU: ALU and RegFile

Setup

Copy the lab files from the instructional servers to your lab account with

$ cp -r ~cs61c/labs/06/ ~/labs/06/

Alternatively, secure-copy (scp) them from the instructional servers to your own laptop with

$ scp -r cs61c-xxx@hive10.cs.berkeley.edu:~cs61c/labs/06/ ~/YOUR_FILEPATH

And if you want to secure-copy them back to your class account:

$ scp -r ~/YOUR_FILEPATH/06 cs61c-xxx@hive10.cs.berkeley.edu:~/YOUR_LABACCT_FILEPATH

Exercises

This week, we will start working on our CPU project, starting with the Artihmetic Logic Unit (ALU) and the Register File (RegFile)! In this project you will be using Logisim Evolution to implement a simple 32-bit processor, with an ISA that uses a subset of RISC-V instructions. This reduced ISA suppports up to 32 registers that hold 32 bits of data each, and a 32 bit memory address space. In this lab, you will make the RegFile and ALU which will be used in your final CPU.


IMPORTANT INFO - PLEASE READ

  • You are allowed to use any of Logisim's built-in blocks for all parts of this project.
  • LOGISM DISCLAIMER: From past experience, it is best practice to put your project files under version control (Bitbucket). We have had students lose their work due to Logism incorrectly formatting their files. So please, use Bitbucket and commit your work every so often so you always have a backup for yourself and just in case something goes wrong. Also, be sure to save frequently in case Logisim crashes.
  • Tests for a completed ALU and RegFile have been included with the lab starter code. You can run them with the command ./test.sh. See the Testing section for information on how to interpret your test output.
  • MAKE SURE TO CHECK YOUR CIRCUITS WITH THE GIVEN HARNESSES TO SEE IF THEY FIT! YOU WILL FAIL ALL OUR TESTS IF THEY DO NOT.
    (This also means that you should not be moving around given inputs and outputs in the circuits).
  • Because the files you are working on are not plain code and circuit schematics, they can't really be merged. DO NOT WORK ON THE SAME FILE IN TWO PLACES AND TRY TO MERGE THEM. YOU WILL NOT BE ABLE TO MERGE THEM AND YOU WILL BE SAD.

Logisim Notes

If you are having trouble with Logisim, RESTART IT and RELOAD your circuit! Don't waste your time chasing a bug that is not your fault. However, if restarting doesn't solve the problem, it is more likely that the bug is a flaw in your project. Please post to Piazza about any crazy bugs that you find and we will investigate.

Things to Look Out For

Exercise 1: Arithmetic Logic Unit (ALU)

Your first task is to create an ALU that supports all the operations needed by the instructions in our ISA (which is described in further detail in the next section). Please note that we treat overflow as RISC-V does with unsigned instructions, meaning that we ignore overflow.

We have provided a skeleton of an ALU for you in alu.circ. It has three inputs:

Input Name Bit Width Description
A 32 Data to use for Input A in the ALU operation.
B 32 Data to use for Input B in the ALU operation.
ALUSel 4 Selects what operation the ALU should perform
(see the list of operations with corresponding switch values below).

... and one output:

Output Name Bit Width Description
Result 32 Result of the ALU Operation.

And as previously promised, here is the list of operations that you need to implement (along with their associated ALUSel values). You are allowed and encouraged to use built-in Logisim blocks to implement the arithmetic operations.

Switch Value Instruction
0 add: Result = A + B
1 and: Result = A & B
2 or: Result = A | B
3 xor: Result = A^B
4 srl: Result = (unsigned) A >> B
5 sra: Result = (signed) A >> B
6 sll: Result = A << B
7 slt: Result = (A < B) ? 1 : 0 Signed
8 div: Result = (unsigned) A / B
9 rem: Result = A % B
10 mult: Result = A*B[31:0]
11 mulh: Result = A*B[63:32]
12 sub : Result = A - B
13 bsel: Result = B

NOTE: The multiplier circuit built into Logisim is signed (when operating on 32-bit numbers)! Use the built in block--you are NOT expected to implement multiply from scratch.

Hints:

NOTE: Your ALU must be able to fit in the provided harness alu_harness.circ. Follow the same instructions as the register file regarding rearranging inputs and outputs of the ALU. In particular, you should ensure that your ALU is correctly loaded by a fresh copy of alu-harness.circ before you submit.

Testing

When you run ./test.sh, the ALU tests will produce output that contains the number of the test followed by the result produced by the ALU. The "number" of the test is treated as an address for the ROM units found in the harness. For the ALU, there are three ROM units, for Input A, Input B, and ALUSel. By checking the number of the test, you can see exactly which Input A, Input B, and ALUSel produced your output, and what the correct output was. The output produced by your circuit will be put in a file that is the name of the test followed by -student.out, and you can find it in the student_outputs folder. If you want to compare your output to the reference output, you can run a diff. For example, for the alu-add test, you would run diff tests/reference_output/alu-add-ref.out tests/student_output/alu-add-student.out.

Checkoff


Exercise 2: Register File (RegFile)

As you learned in class, RISC-V architecture has 32 registers. However, in this project, You will only implement 9 of them (specified below) to save you some repetitive work. This means your rs1, rs2, and rd signals will still be 5-bit, but we will only test you on the specified registers.

Your RegFile should be able to write to or read from these registers specified in a given RISC-V instruction without affecting any other registers. There is one notable exception: your RegFile should NOT write to x0, even if an instruction try. Remember that the zero register should ALWAYS have the value 0x0. You should NOT gate the clock at any point in your RegFile: the clock signal should ALWAYS connect directly to the clock input of the registers without passing through ANY combinational logic.

The registers and their corresponding numbers are listed below.

Register # Register Name
x0 zero
x1 ra
x2 sp
x5 t0
x6 t1
x7 t2
x8 s0
x9 s1
x10 a0

You are provided with the skeleton of a register file in regfile.circ. The register file circuit has six inputs:

Input Name Bit Width Description
Clock 1 Input providing the clock. This signal can be sent into subcircuits or attached directly to the clock inputs of memory units in Logisim, but should not otherwise be gated (i.e., do not invert it, do not "and" it with anything, etc.).
RegWEn 1 Determines whether data is written to the register file on the next rising edge of the clock.
Read Register 1 (rs1) 5 Determines which register's value is sent to the Read Data 1 output, see below.
Read Register 2 (rs2) 5 Determines which register's value is sent to the Read Data 2 output, see below.
Write Register (rd) 5 Determines which register to set to the value of Write Data on the next rising edge of the clock, assuming that RegWEn is a 1.
Write Data 32 Determines what data to write to the register identified by the Write Register input on the next rising edge of the clock, assuming that RegWEn is 1.

The register file also has the following outputs:

Output Name Bit Width Description
Read Data 1 32 Driven with the value of the register identified by the Read Register 1 input.
Read Data 2 32 Driven with the value of the register identified by the Read Register 2 input.
ra Value 32 Always driven with the value of ra (This is a DEBUG/TEST output.)
sp Value 32 Always driven with the value of sp (This is a DEBUG/TEST output.)
t0 Value 32 Always driven with the value of t0 (This is a DEBUG/TEST output.)
t1 Value 32 Always driven with the value of t1 (This is a DEBUG/TEST output.)
t2 Value 32 Always driven with the value of t2 (This is a DEBUG/TEST output.)
s0 Value 32 Always driven with the value of s0 (This is a DEBUG/TEST output.)
s1 Value 32 Always driven with the value of s1 (This is a DEBUG/TEST output.)
a0 Value 32 Always driven with the value of a0 (This is a DEBUG/TEST output.)

The DEBUG/TEST outputs are present because those registers are more special than the others - they are for testing and debugging purposes, and will be used in the tests! If you were implementing a real register file, you would omit those outputs. In our case, be sure they are included correctly--if they are not, you will not pass.

You can make any modifications to regfile.circ you want, but the outputs must obey the behavior specified above. In addition, your regfile.circ that you submit must fit into the regfile-harness.circ file we have provided for you. This means that you should take care not to move inputs or outputs. To verify changes you have made didn't break anything, you can open regfile-harness.circ and ensure there are no errors and that the circuit functions well. (The tests use a slightly modified copy of regfile-harness.circ.) Hints:

Testing

When you run ./test.sh, the RegFile tests will produce output that contains the number of the test followed by the results in the registers, from left to right, followed by read data 1 and read data 2. The "number" of the test is treated as an address for the ROM units found in the harness. For the RegFile, there is only one ROM unit, which has a bit width of 16. These 16 bits are split into the RegWEn signal, along with rs1, rs2, and rd.The output produced by your circuit will be put in a file that is the name of the test followed by -student.out, and you can find it in the student_outputs folder. If you want to compare your output to the reference output, you can run a diff. For example, for the regfile-zero test, you would run diff tests/reference_output/regfile-zero-ref.out tests/student_output/regfile-zero-student.out.

Checkoff

  • Run ./test.sh and show your TA the results of the 4 RegFile tests.

You have now completed everything that needs to be checked off for this lab, but please read and consider completing the optional Exercise 3 below, as it will help you a lot with the CPU project (which will be released very soon, on Thursday 7/12).

Exercise 3: CPU Project Guide

We wanted to include this to guide you through the CPU project when it is released. Please use this guide to your advantage for getting started.

We know that trying to build a CPU with a blank slate might be intimidating, so we want to guide you through how to think about this project by implementing a simple R-type instruction, add.

Recall the five stages of the CPU pipeline:

  1. Instruction Fetch
  2. Instruction Decode
  3. Execute
  4. Memory
  5. Write Back

This guide will help you work through each of these stages, as it pertains to the add instruction. Each section will contain questions for you to think through, pointers to important details, and references to lecture material - but won't tell you exactly how to implement it. You may need to read and understand each question before going to the next one, and you can see the answers by clicking on the question. During your implementation, feel free to place things in subcircuits as you see fit.

Stage 1: Instruction Fetch

The main thing we are concerned about in this stage is: how do we get the current instruction? From lecture, we know that instructions are stored in the instruction memory, and each of these instructions can be accessed through an address.

  1. Which file in the project holds your instruction memory? How does it connect to your cpu.circ file?

    The instruction memory is the ROM module in run.circ. It provides an input into your CPU named "instruction" and takes an output named "fetch_addr".

  2. In your CPU, how would changing the address you output to fetch_address affect the instruction input? The instruction that run.circ outputs to your CPU should be the instruction at address fetch_address in instruction memory.
  3. How do you know what the fetch_address should be? (Hint: it is also known as PC) fetch_address is the address of the current instruction being executed, so it is saved in the PC register. For this project, it's fine for the PC to start at 0, and that is the default value for registers.
  4. For this project, does your PC hold an address of a byte or a word? If you look in run.circ, you will see that the address coming from your CPU will go through a splitter before entering the word-addressed memory module. This means that your PC should hold a byte address.
  5. For basic programs without any jumps or branches, how would the PC change from line to line? The PC must increment by 1 instruction in order to go to the next instruction, as the address held by the PC register represents what instruction to execute.
  6. We have provided the PC register in the cpu.circ file. Please implement the PC's behavior for simple programs - ignoring jumps and branches. You will have to add in the latter two in the project, but for now we are only concerned with being able to run strings of add instructions. Where should the output of the PC register go? Remember to connect the clock!

Stage 2: Instruction Decode

Now that we have our instruction coming from the instruction input, we have break it down in the Instruction Decode step, according to the RISCV Instruction Format you have learned.

  1. What type of instruction is add? What are the different bit fields and which bits are needed for each? R type. The fields are:
    • funct7 [31-25]
    • rs2 [24-20]
    • rs1 [19-15]
    • funct3 [14-12]
    • rd [11-7]
    • opcode [6-0]
  2. In Logisim, what tool would you use to split out different groups of bits? Splitter!
  3. Please implement the instruction field decode stage using the instruction input. You should use tunnels to label and group the bits.
  4. Now we need to get the data from the corresponding registers, using the register file. Which instruction fields should be connected to the register file? Which inputs of the register file should it connect to? Instruction fields rs1 and rs2 will need to connect to read register 1 and 2.
  5. Please implement reading from the register file. You will have to bring in your register file from part 1 of the project. Remember to connect the clock!

Stage 3: Execute

The Execute stage, also known as the ALU stage, is where the computation of most instructions is performed. This is also where we will introduce the idea of using a Control Module.

  1. For the add instruction, what should be your inputs in to the ALU? Read Data 1 and 2 should go into ports A and B of the ALU.
  2. In the ALU, what is the purpose of ALU_Sel? It chooses which operation the ALU should perform.
  3. Although it is possible for now to just put a constant as the ALUSel, why would this be infeasible as you need to implement more instructions? With more instructions, the input to the ALU might need to change, so you would need to have some sort of circuit that changes ALUSel depending on the instruction being executed.
  4. Please create a new subcircuit for the Control Module. This module will need to take in as inputs the opcode and funct, using these to output a value for the ALU Switch, depending on what the current instruction is. There are a few ways of doing this. As you implement more instructions, this circuit will have to expand and become more complex.
  5. Please bring in your ALU from part 1 of the project and connect the ALU inputs correctly. Do you need to connect the clock? Why or why not?

Stage 4: Memory

The memory stage is where the memory can be written to using store instructions and read from using load instructions. Because the add instruction does not use memory, we will not spend too much time here.

  1. Please bring in the MEM module that we provided. At this point, we cannot connect most of the inputs, as we don't know where they should come from. However, you can still connect the clock.

Stage 5: Write back

The write back stage is where the results of the operation is saved back to the registers. Although not all instructions will write back to the register file (can you think of some which do not?), the add instruction does.

  1. Looking at the entire ISA, what are some of the instructions that will write back to a register? Where in the datapath would it get the values? add is an example that will take the output from the ALU and write it back. lhw will take the output from MEM and write it to a register. There are more not specified here.
  2. Let's create the write back phase so that it is able to write both ALU and MEM outputs to the Register File. Later, when you implement branching/jumping, you may need to add more to this mux. However, at the moment, we need to choose between the ALU and MEM outputs, as only one wire can end up being an input to the register file. Bring a wire from both the ALU and MEM, and connect it to a MUX.
  3. What should you use as the Select input to the MUX? What does the input depend on? This input should be able to choose between the two MUX inputs, ALU and MEM, which means that its value depends on which instruction is executing. This suggests that the input should originate from the Control Module, as the Control Module is responsible for figuring out which instruction is executing (using the opcode or funct).
  4. We now come to the second, and arguably more important, role of Control Modules - determining what values to output to the CPU, in order to control the path of execution. These values are called Control Signals.

    One example of this is the control signal that connects to the MUX from the previous step, which is commonly called WBSel. WBSel determines which value to write back to the register file.

    You can find more control signals on the lecture slides, and you'll need to define some yourself. If you find yourself needing a MUX, it's very likely that you'll need to define a control signal for it.

  5. There are a few ways of implementing the Control so that it can translate the opcode/functs to the corresponding instruction and then set the control signals correctly. One way to do so is outlined in lecture, using a ROM. The other method uses "AND" logic and "OR" logic to accomplish both tasks. If you are unfamiliar with it, review the slide now and try implementing it for the add instruction and the MemToReg control signal.
  6. Now that we have the inputs to the MUX sorted out, we need to wire the output. Where should the output connect to? Because the output is the data that you want to write into the Register File, it should connect to the Write Data input on the Register File.
  7. There are two more inputs on the Register File which are important for writing data: RegWEn and Write Register. One of these will come from the Instruction Decode stage and the other one will be a new control signal that you need to design. Please finish off the Write Back stage by these inputs on the Register File correctly.

If you have done all of the following steps correctly, you should have a processor that works for add instructions. For the rest of the project, you will be implementing more instructions in much of the same ways - connecting outputs to inputs, adding MUXes and other Logisim components, and defining new control signals. Hopefully, this will be an easier task now that you have a basic skeleton to work off of. Remember, the lecture slides have a lot of information already, and will help you to continue building on the circuits you have now. Good luck!