Practical 5 Single Cycle Processor

Objectives

This section is not a list of tasks for you to do. It is a list of skills you will have or things you will know after you complete the practical.

Following completion of this practical you should be able to:

Guidelines

Time estimate This practical will take approximately 5-8 hours per student, depending on your familiarity with Verilog and the single-cycle RISC-V architecture covered in class.

Preliminary Tasks

Follow this sequence of instructions to complete the practical.

Obtain your RISC-V-single-cycle-processor git repo

First, get your team name from your professor (it is probably on moodle or CATME). Once you have been assigned team names, you can create your repository via github classroom.

When you follow the link to get your repo you need to enter your team name exactly as your professor provided it.

Only the first person in your group to set up the repo needs to do this, later people can select the correct team from the list of teams.

Get your repo at this link. Don't click that link without reading the sentences above.

Create your own ModelSim Project (.mpf) file.

Because ModelSim uses absolute pathing in its settings, each .mpf project is intended to be used on one single machine. This means you cannot easily run ModelSim using another team member’s .mpf file. Instead, each team member should have their own unique .mpf file (e.g., username.mpf).

With unique project files, each team member can contribute towards the practical without annoying path issues.

You each project file will reference the same verilog source files (*.v), so you will not maintain copies of the verilog files.

Have each team member do the following:

  1. Create their own ModelSim Project .mpf file,
  2. Check the .mpf file contains the two lines msgmode=both and displaymsgmode=both,
  3. Add, commit, and push the .mpf file to the git repository. (Tip: do a pull before you add/commit/push to avoid merges)

Build a basic RISC-V Processor

Repository Overview

Before diving in, let’s take a quick overview of the files in your repository:

1 Implement R-type Instructions

You will begin by implementing a processor that only implements R-Type RISC-V instructions.

Trace R-Types on the datapath

On the first page of your practical worksheet, there's a datapath drawing.

  1. (Q) Using the RTL from class, trace the wires used for the add instruction.
  2. Label each traced wire with a name. For example, consider the wire coming out of the right side of Instruction Memory; you could label this wire inst to be consistent with the RTL.
  3. As you trace through blocks of logic (register file, muxes, etc) circle the names of any control bits necessary for the add instruction.

Implement Control

We've provided you with the start of a verilog control unit, Control.v and a test bench tb_Control.v. It doesn't currently do much, but has all the inputs and outputs you'll need.

Review your traced datapath and make note of what each of the 7 control signals should be for add (and other R-type instructions). For ALUOp, this is a 2-bit signal. Review the comments documented in ALUCtl.v to determine what ALUOp should be for R types.

  1. Open up your repo in VS Code (or your favorite text editor)
  2. Edit Control.v
  3. In the always @(opcode) block of the control unit, look for the case block for R_OPCODE. Between the begin and end, add value assignments for all the control signals you will need to set to make add happen.
    • You can see Figure 4.26 in your textbook (should be page 281) to verify which signals and which values matter for "R-format" instructions.
    • HINT: ALUOp is a two-bit bus, unlike in the texbook table that treats each bit separately; do not set each bit separately, instead assign a two-bit binary value.
    • Again, reference the comments in ALUCtl.v to determine what the two-bit ALUOp should be.
    • Note: you should also include every "write" control signal and explicitly disable them (set to zero) for things you don't want to be written (for example, memWrite).

Test Control

Once you've coded control for R-types, you need to make sure it emits the right signals!

  1. In VS Code, open the tb_Control.v file and review the test we provide for R-type instructions.

    • Note that it calls the test_control task we created with the expected control signals. This is similar to how you will create additional tests later.
    • While you do not need to edit that file yet, read and try to understand what it currently does.
  2. Open ModelSim, and your uniquely-named project file.

  3. Add all the verilog files in the repo to your ModelSim project.

  4. Compile all the verilog files, fixing any errors in Control.v and tb_Control.v. Don't worry about errors in the other modules for now.

  5. Start simulation for the tb_Control module and run -all. The tests should pass; if they don't, check your work.

  6. You could add another test, but since all R-format instructions have the same opcode, there's no need.

  7. When you get control working, do a git pull, add, commit, and push to save your work.

  8. While this test may seem redundant since you just implemented the exact same thing in Control.v, it is still very important to do this check to catch errors:

    • Have one team member implement Control.v, and another team member implement tb_Control.v. This way you have redundancy built into this verification process.
    • As you implement the other instruction types and eventually other logic modules, the team member implementing these modules will be fatigued and be prone to mistakes.
    • Having a testbench to check the validity of a module before integrating it into the larger Processor.v module will help prevent elusive bugs. Even if you do get a bug, you can run these smaller testbenches again to pinpoint where the bug is.

Implement the Datapath for R-types

In this part you will use your control unit and design a datapath around it to execute R-type instructions from memory.

  1. Examine the Processor.v file. We're providing you with a processor module that has a few component instances in it, but they're not hooked up. Compare this set of instances with the components you traced on the worksheet.
  2. Add new instances of any missing components you will need to make your processor execute only R-types. Just make them, don't connect them to anything. Here are some hints and suggestions:
    • We are providing you with working implementations of ALU.v and ALUCtl.v. They should already be in your git repository.
    • We've also included copies of the DP_Memory.v, and Register.v from Practical 4.
    • There are lots of suggestions and tips in the comments in the files provided, make use of these.
    • Instruction and data memory will be the same component. We're using both halves of the DP_Memory module. See the comments in Processor.v for details.
    • Don't make multiplexers. You can make them with raw verilog later.
    • There's no register file in your git repo so you will have to make your own!
      • SUGGESTION: Since you created and tested this in the previous practical, one member of your team should copy their implementation into your repo and use them for this practical.
      • Don't forget to add this file to your ModelSim project after you've made it.
  3. Create wires to connect the components. Create an instance of each wire you labeled on the practical worksheet's datapath; use the same name you wrote on the worksheet.
    • Some of your wires will need to be 32 bits: wire [31:0] myWideWire;
    • Put the wire declarations inside the Processor module before you declare the other major components. This will ensure they're ready to use by any component that needs it.
  4. Attach the wires to your component's input or output pins
    • This is as simple as writing the wire name between the parentheses next to the input or output where you want to attach it. In this example, the output of instance A is connected to the input of instance B:
        wire [4:0] myWire;
        Thingy A(
            .InputPin1(),
            .OutputPin(myWire)  // <-- attach one end here
        );
        Thingy B(
            .InputPin1(myWire),  // <-- attach the other end here
            .OutputPin()
        );
    • STRONG SUGGESTION: Because you are not implementing immediates, memory, or branch instructions yet, ignore the three muxes used by the ALU (from ImmGen) branches (From the adder) and data memory (output of memory) for now. Assume the wire goes straight through the mux. For example, you might have a wire B that goes directly from the register file into the second input on the ALU. You can add these muxes later.
Tip: name and create wires when connecting components

This is so important, we're saying it twice.

For ease of wiring, we recommend defining wires as you named them when tracing your datapath diagram. For example, consider the instruction:

This could be a 32-bit wire inst connected to Mem and Control:

    wire [31:0] inst;

    DP_Memory Mem (
        // instruction memory ports
        .addr_a(),      // instruction address (TODO)
        .we_a(0),       // no writing to instructions, so we’ll ground this
        .data_a(32’b0), // no writing to instructions, so we’ll ground this
        .clk_a () ,     // clock for reading instructions (TODO)
        .q_a(inst) ,    // q_a output is always an instruction
        ...
    );

    Control controlUnit (
        .opcode( inst[6:0] ), // these are the opcode bits of the instruction
        .ALUSrc() ,
        ...
    ) ;
A note about Memory and Addresses

When you hook up memory you will need to adjust the address.

RISC-V uses byte addressing, but the provided memory modules use word addressing. Because our words are 4 bytes long, every byte address is 4 times too big. For example, the second instruction in memory will be at byte address 0x0004 but word address 0x0001.

We can simply shift the byte address right by 2 to divide by 4 and convert it to a word number.

Hint: you do not need a dedicated bit shifter, nor an assign statement to achieve this. You can strategically wire a subset of PC_output[xx:yy] to addr_a. If this is confusing, consult your instructor.

Additionally, the provided memory only has *10 bit (word) addresses (meaning 2^12 byte addresses), this is because of the limitations on the amount of memory on the FPGA we are simulating.

Feed in the least significant ten bits of your address (after left shifting) to account for this (we're essentially cutting off the top of memory when we do this). As you debug keep in mind that if you look at the values in memory the addresses will be 4 times smaller than the address you would "expect".

  1. Connect the reset input on Processor to all components that have a reset (most likely the PC, and RegFile). When you "reset" the processor, you want to clear out all the registers and make sure the PC goes back to the beginning of your code.

  2. We recommend you connect components from left-to-right in your datapath diagram, checking each step to see if it worked before moving on:

    • Make the PC increment every clock cycle. To increment the PC by 4 every clock cycle, we can skip defining a separate adder module and simply define a new wire newPC and assign its value to be PC_output + 4. Note that PC_output is not defined for you.
    • Connect PC and memory to read instructions Use the waveform to see if the right instructions are coming out of memory. You can look at the memory-R.txt file to see the instructions in memory.
    • Connect the instruction bus (output of memory) to the register file and control unit. Use the waveform to see if the right registers get read.
    • Connect the register file's outputs to the ALU
    • Connect the ALU output back to the register file
    • Connect control outputs to the register file and ALU

    It is ok to start this list, then move to the next section ("Test with a few R-Types") to test your progress as you finish implementing the R-types.

  3. Be sure your Processor compiles in ModelSim before moving on. Make sure you've added all the files used in your Processor or the tests to the ModelSim project.

Test with a few R-types

  1. Examine memory-R.txt. This is the first set of instructions you will test on your processor. It has an add and a sub that have been assembled and put into memory in this order.

  2. In the tb_Processor_R.v file, you can see a start at a test bench for your processor. It loads memory-R.txt into memory, then resets your processor, then allows the clock to cycle and checks the changes to the register file after each cycle.

    • Modify the test to properly inspect the contents of your register file. Follow the instructions in the code to tell the test bench how to look at your regsters' values.
  3. In ModelSim, run the tb_Processor_R tests!

    • If your implementation is not complete, it's ok, the tests will still do useful things.

      Tip: How to test a partial implementation

      To run a test with a partially-working implementation, compile in ModelSim and then simulate tb_Processor. (Make sure the compile succeeds!)

      Populate the waveform with values that are important to check. Remember, you can add values from individual module components onto the waveform via sim → tb Processor → UUT → component name.

      This is what your waveform may look like after connecting the PC, Control, Memory and Register File.

      Consider saving your waveform file (tb_Processor_R_waves.do) as you test.

    • If you think your implementation is complete but the tests don't pass, consider building a more detailed waveform with control and all your labeled wires to inspect what it's doing.

    • If you build a waveform, be sure to save it! We recommend calling it something similar to the test bench where it is useful (tb_Processor_R_waves.do for example).

  4. Once the instructions in the R-type tests are succeeding, assemble a few more R-type instructions and add them to the memory file, then add tests to the verilog test bench.

    • HINT: you can use your assembler from practicals 1/2, or RARS to quickly assemble instructions.
  5. Once you've got R-types working, save your progress in git!

2 Implement I, Memory, and SB type instructions

From here, repeat the same steps for I, S, and SB types. Note that for I-types, skip jalr; we will do that on the next practical. In addition, lw (an I-type) should be implemented with sw (the S-type).

For each of the remaining instructions for this practical (I-Types, memory instructions, and SB types):

The following sections contain hints, requirements, and tips for each type.

I-TYPES (not lw or sw)

Datapath:

Tip: Ways to create a Mux

Here are three different ways to create a mux:

Method 1: conditional connection

First, you can "conditionally connect" a wire to the input. For example, this code connects wire A to the input when "ACONTROL" is 1, and otherwise it connects B:

wire [4:0] A;
wire [4:0] B;
Thingy UNIT(
    .InputPin1( ACONTROL ? A : B ),
    .OutputPin()
);

Method 2: input and output wires for the mux

Another way is to create a third wire from the output of the mux, lets call it "choice", then conditionally assign that wire:

wire [4:0] A;
wire [4:0] B;
wire [4:0] choice;
assign choice = ACONTROL ? A : B;
Thingy UNIT(
    .InputPin1( choice ),
    .OutputPin()
);

Method 3: using an always block and if statement

A final way is to use an always block to recompute choice when the inputs change. Note that choice is a reg type here:


wire [4:0] A;
wire [4:0] B;
reg [4:0] choice;

always @(A,B,ACONTROL) begin
    if (ACONTROL === 1) choice 

Testbench:

MEMORY TYPES (lw and sw)

You may have noticed that now there is a critical series of more than two clocked things that must happen in sequence during one clock cycle:

The processor cannot put the value into the register file until it has been loaded from memory. And it cannot do any of this until it has read the instruction (a). The clock only has two edges, rising and falling edge, so we need a strategy to handle this!

If your register file has async reads, this makes it much easier; we can write the data into the register file while the next instruction is getting read from memory. Both (a) and (c) can happen on the rising edge, and the memory read (b) can happen between the other two.

Datapath:

Testbench:

Tip: manually pre-loading data into memory

When you write tests for lw and sw, you may need to manually set values in memory. You can do so with the same approach as how the provided testbench manually sets register values when a task begins.

For instance, if I want to set the 50th word in memory to the value 1729, then I can write UUT.Mem.ram[50] = 1729; in tb_Processor.

Refer to Practical 4’s tb_Memory for a testbench that reads what gets written into memory:

    // wait for clock to rise
    @( posedge CLK );
    #1; // wait a bit more for propagation
    VU.SET_TEST_NAME("Single Port Memory WRITE Test");
    // COOL TIP: we can reach into the sp_ram to read 'ram' values !!!
    VU.ASSERT(sp_ram.ram[0] === sp_data) ;
    VU.SET_TEST_NAME("Single Port Memory WRITE Test");
    VU.ASSERT_INT_EQUAL(sp_q, sp_data);

If you want to pre-load memory before a test, you can reach in right before you let the clock cycle.

SB TYPES (beq ONLY, FIRST)

Datapath:

Testbench:

SB TYPES (All Remaining)

Once you’ve ensured your processor supports beq, you can now expand out to the remaining branch variations (bne, bge, blt). Here, instead of and-ing the control unit's branch with ALU’s zero, you need the full branch detection unit.

The decision table for the branch detection unit from lecture is replicated below:

Instruction zero result[31] Branch?
beq 0 x no
1 x yes
bne 0 x yes
1 x no
bge 0 0 yes
0 1 no
1 x yes
blt 0 0 no
0 1 yes
1 x no

Datapath:

When deciding where to put this Branch Control unit, you can either:

Testbench:

Working Ahead

You can read ahead to Practical 6 and start planning how to support U types, UJ types, and plan your own instruction.

Submission and Grading

Functional Requirements

At the end of the practical you should have done these things:

Git Requirements

Remember, Do not add and commit every single file ModelSim creates. Only add, commit, and push .v, .do, and .mpf files.

In addition to the list below, you should regularly commit and push whenever you fix a bug, work to a stopping point, or make any incremental updates. At minimum, you must have at least 5 commits in your repo for this practical:

Commit and push via git either using VSCode’s built-in source control or with the git bash terminal. Be sure to copy your final commit ID number for the final question on the worksheet. This ID number can be found on the commit history tab on your Github repository.

Worksheet Requirement

All the practicals for CSSE232 have these general requirements:

General Requirements for all Practicals

  1. The solution fits the need
  2. Aspects of performance are discussed
  3. The solution is tested for correctness
  4. The submission shows iteration and documentation

Some practicals will hit some of these requirements more than others. But you should always be thinking about them.

(Q) Complete the practical worksheet. Specifically, complete the question on page 9 about how you tested your work and the last few questions that ask you to reflect on the efficiency of this design and your implementation experiences.

Final Checklist

Grading Breakdown

Practical 5 Rubric items Possible Points Weight
Worksheet 76 50%
Code 80 50%
Total out of 100%