CSE141L Lab 3: Single-cycle MIPS control

Due Jul 18 (6:00pm)


In Lab 3 you will extend the design you created in Lab 2 to implement the control unit for the Single-Cycle MIPS processor. As you probably realized throughout Lab 2, it is not very easy to test each individual module completely; however, with the addition of Lab 3 (the control unit), testing will be much more straightforward.

The processor you build in this lab will not support branches or jumps so you won't be able to run most programs, we will add support for these in a future lab, along with additional instructions. It will be enough to run some simple programs, such as "Hello World" that have been written completely without branches.

NOTE: You can complete lab 3 in groups of 2-3.

Please remember that you must conform to the class coding standards. They are available on the wiki: CS141L Verilog Coding Standards.

Getting Started

General Notes

Controlling the Processor

When building complex hardware like processors, its useful to separate the datapath and the control decisions. In the last lab, you built and wired up all of the datapath modules. In this lab we will connect up all of the control signals (mux select inputs, adder op-codes, etc.) to a new control module that will be responsible for making all of the decisions about what input muxes should use and what operation the ALU should perform. The control module will make these decisions based on the instruction fetched from the inst_mem module (and maybe a few other signals, later on).

To get started, open up your project from the last lab and make a list of all of the control signals you need to connect. This includes all of the mux selector inputs, the alu function code, register file write controls and data memory control signals.

Task 1: Create a new module control with a port for each of the control signals in your project. You will also need to have the instruction as an input to your control module. Update your schematic to show your control module with the ports labeled and all of the control signals connected to the appropriate modules.

Now that we have all of the signals connected to your controller, we can start designing the controller. Remember to follow the coding standards - you can find a link above if you need a reminder. Since this module has no clock and reset inputs this will be strickly combinational.

A processors control unit is responsible for decoding an instruction and setting up the data path (by changing control signals) to execute that instruction. For example, on a Load Word instruction, the control unit must change the mux to use the data_memory data output rather than the alu result as the register_file write data source. At the same time, it must assert the Write Enable signal to the register file. Of course, there are other signals that will need to be set for a Load Word, these are just a few examples. For each of the control signals in your datapath you must program the controller to output the correct values for each instruction your processor will execute.

In this lab your controller will need to decode the following instructions correctly: LW, SW, ADD, ADDI, SUB, AND, OR, NOR, XOR.
You can refer to your textbook for details on what each is supposed to do. Begin by writing your always @(*) block and checking the instruction input bits to detect which instruction is being executed. Remember that all of the control signals must have some value assigned, even though not all of them are important for every instruction. It is not acceptable to assign them x for "don't care".

Its OK to individually decode each possible instruction, but looking for common patterns in the instructions might help you decode them more easily.

Look in your text book for the opcodes and functio codes of the various instructions. The green card at the front is useful, as well as figure B.10.2 and the instrunction listing in the appendix at the back.

NOTE: Remember that our ALU and datapath are slightly different than what you'll find in the textbook so take care to, for example, provide the right ALU function codes. Here's a reminder of what that module looks like.

The alu module:
module alu(
	input [5:0] Func_in,
	input [31:0] A_in,
	input [31:0] B_in,
	output [31:0] O_out,
	output Branch_out,
	output Jump_out
Because our ALU has different inputs and outputs, here's a list of operations it can perform and what value of Func_i each corresponds to. You won't need information for branches and jumps for this lab, but you will for the lab 4. They are included here for completeness.
Func_in Operation O_out value Branch_out Jump_out
100000 ADD A+B 0 0
100001 ADD A+B 0 0
100010 SUB A-B 0 0
100011 SUB A-B 0 0
100100 AND A AND B 0 0
100101 OR A OR B 0 0
100110 XOR A XOR B 0 0
100111 NOR A NOR B 0 0
101000 Set-Less-Than Signed (signed(A) < signed(B)) 0 0
101001 Set-Less-Than Unsigned (A < B) 0 0
111000 Branch Less Than Zero A (A < 0) 0
111001 Branch Greater Than or Equal to Zero A (A >= 0) 0
111010 Jump A 0 1
111011 Jump A 0 1
111100 Branch Equal A (A == B) 0
111101 Branch Not Equal A (A != B) 0
111110 Branch Less Than or Equal to Zero A (A <= 0) 0
111111 Branch Greater Than Zero A (A > 0) 0

Task 2: Finish coding up your control module.

Task 3: Create a test bench for your control module. you need to show us that your control module decoding at least one of each type of instruction it needs to support. You should check that all of the outputs are correct for each of the input instructions.

Testing the whole processor

When you are satisfied that your control unit is behaving correctly, its time to move on to testing the whole processor.

Let's start with a simple test program. This will use store a few values into registers 11, 12, and 13 in the processor, while testing all of the instructions you are supposed to have implemented. You can find this application here: Lab3 Test. In this zip file you will find several files:

To "load" this program into your processor we'll change some parameters on the provided init_rom and data_memory modules (they are in your datapath). First, unzip the files somewhere where there are no spaces in the file name or any of the directory names leading to the file and take note of the full path to the files. In the inst_rom module add the following parameter: INIT_PROGRAM. You should set the value of this parameter to the full path for the lab3-test.inst_rom.memh file. For example, your intantiation of the insturction ROM should now look something like this:

inst_rom #(
) myInstructionRom (

Similarly, set the INIT_PROGRAM0, INIT_PROGRAM1, etc. parameters on your instantiation of the data_memory module. INIT_PROGRAM0 should be lab3-test.data_ram0.memh, etc.

lab3-test.dis contains a listing of the MIPS instructions that make up this program. With the assembly listing and modelsim you can verify that everything is behaving correctly.

lab3-test.spim.s contains the source file for this application (also in MIPS assembly), this is the pre-assembly version and should be loadable into SPIM so you can see what the correct behavior of the application is in SPIM and compare that to your own processor. See here: 141 SPIM Tutorial for some instructions on downloading and using SPIM.

This test applications performs loads, stores, and arithmetic operations to set specific values into registers 11, 12 and 13 of your processor. Because we don't have branches or the LUI instruction yet, we have to play a few tricks to set all of the values. You'll notice that this application creates a table of values in memory (starting at address 0x10000000) with each word having one bit set. It later uses adds, subtracts, and logic operators to load the bits it needs into the various registers. If your processor is working and correctly implements all of the instructions listed above, your should get 3 human "readable" words in the hexadecimal values in registers 11, 12 and 13. (Remember to set the Radix to hexadecimal in modelsim or you won't see the values).

Now we are set to simulate the whole processor. Use the test bench (testbench.v). In addition to generating a clock and reset for your processor, this test bench also includes some code to print out when your processor writes to the serial port. We'll use that for the next program.

Double check that your program counter resets to 32'h003FFFFC at the start of your simulation or the applications won't work correctly. You can check if this value is correct by looking at the simulation and inspecting the PC register's output value while the reset signal is still high.

To help you debug your processor, here's one way that we work through bugs in the design. By comparing the MIPS assembly of the test program to your processor's simulation waveforms, you can decide if you proecssor is executing the code correctly.

If you open up the source listing you can see that the first instruction is at address 0x00400000 and is an addi instrunction:

00400000 <__start>:
  400000:       200a4000        addi    t2,zero,16384
You can simulate your design and, by looking at the output value of your Program Counter module, find the instruction at address 0x00400000. You should see that the instruction output from the inst_rom matches the second column from the dissasembly (lab3-test.dis). The value should be 0x200a4000. This is the encoded addi instruction. Now you can look at the trace the instruction into your control unit and into the register file where it should be reading from registers zero (0) and t2 (10). You should be using the immediate value (16384 or 0x4000) as an input to your ALU. By looking at your control signals and the inputs to the various modules you can check that your datapath is behaving correctly. Verify that at the end of the cycle, you end up writing the value 0x4000 into register 10.

Once you have checked the first instruction and fixed any errors, you can move on to the next one and repeat the process.

Task 4: Simulate your processor and make sure everything works as expected. Simulate your design for 5us. Include a waveform of the simulation showing the value of all the registers at time 5us. Show a waveform with all of the wires in processor.v as well. Run the whole synthesis and place and route flow and include the Fmax (and minimum period in nanoseconds), Total logic elements, and the Total registers statistics from your design. (Just like we did in lab 1.)

Lets try another simple test program. This will use the serial output on your processor to write a message to you. You can find this application here: No-Branch Hello World. In this zip file you will find several files:

Point your processor at these files as we did for the program above. *.instrom.memh goes to the inst_rom and the others to the data_memory.

This program uses the serial port interface of your processor to print characters out to the console. Beacuse we don't have branches, this program wouldn't behave correctly on real hardware (as opposed to in modelsim), because the serial port has a limited buffer, or temporary storage space, for new bytes to be written into. If you run this program in SPIM, you'll see that only the first character gets printed.

Here's a description of the serial port interface that is included in your data_memory module:

The serial port is a memory-mapped IO device, meaning that it is accessed by your processor through loads and stores. There are four registers in the serial port interface:

Address Register Name Description
0xFFFF 0000 Read Ready This register has value 0 when no data is available to be read, and 1 when there is data available.
0xFFFF 0004 Read Data This register will have one byte of data when Read Ready has a value of 1, reading from this register allows the next byte to be received.
0xFFFF 0008 Write Ready This register will have a value of 1 when a byte can be written to the Write Data register, otherwise it has value 0 to indicate that the buffer is full.
0xFFFF 000C Write Data Writing a byte to this register when Write Ready has a value of 1 will cause the byte to be sent over the serial link.

So to write a character to the serial port we have to perform a store to the serial port:

#assuming $10 has value 0xFFFF0000
#and $11 contains the byte we want to write:
sw $11, 12($10)		#write to the Write Data register

Really, we need to check the Write Ready register before we write to the data port, but without loops this is difficult, so this application doesn't do the checks. If we had loops, the code would look like this:

#assuming $10 has value 0xFFFF0000
#and $11 contains the byte we want to write:

lw	$12, 8($10)	#read Write Ready
beq	$0, $12, loop	#loop if Write Ready eq 0
sw $11, 12($10)		#write to the Write Data register

Try running this program on your processor and in SPIM and see the difference in behavior - our processor's serial port simulates a very fast serial link so that the Write Data register is always writable - SPIM requires 3000 to 4000 cycles between writes to the Write Data register.

Task 5:Simulate for 1us and put the output from the messages panel (usually at the bottom of the modelsim window). Also include a waveform that shows all of the wires in processor.v.

Interview Questions

  • Simulate for 1 us and show us the output from the messages panel (usually at the bottom of the modelsim window) using the application No-Branch Hello World.

  • Show us the waveform that contains all of the wires in processor.v after simulating the processor for 1us

  • Repeat the above, but simulate for 5us.

  • Tell us the Fmax, total logic elements, and total register statistics for your design.

Due: July 18th (6:00pm)