EECS 31L: Introduction to Digital Logic Laboratory (Spring 2025)
Lab 3: Processor Components
(Revision: v1.1)
Due: May 11, 2025 (11:59 PM)
Revision v1.1: Code 3 should be inside the InstMem module definition.
Through this course, we will design a single-cycle RISC-V processor. We have already designed some of the modules needed in a single cycle processor. In this lab, we will design a few other essential components in a processor. We will build a high-level module to connect the lower-level modules in the next lab.
1 Single Cycle Processor
The building block of a single cycle processor is shown in Figure 1. The instruction execution starts by using the program counter (PC) register to supply the instruction address to the instruction memory. After the instruction is fetched, the register operands used by an instruction are specified by specific bit fields of that instruction. Once the register operands have been fetched from the register file, they can be operated on to compute a memory address (for a load or store instruction), to compute an arithmetic result (for an integer arithmetic-logical instruction), or an equality check (for a branch). If the instruction is an arithmetic-logical instruction, the result from the ALU must be written to a register. If the operation is a load or store, the ALU result is used as a memory address to either store a value from the registers or load a value from memory into the registers. The result from the ALU or memory is written back into the register file.
The blue lines interconnecting the functional units represent buses, which consist of multiple signals. The arrows are used to guide the reader in knowing how information flows. Since signal lines may cross, we explicitly show when crossing lines are connected by the presence of a dot where the lines cross.
Some of the inputs (RegWrite, ALUSrc, ALUCC, MemRead, MemWrite, MemtoReg) are control signals which are derived by a module named “Control Unit”. You will design the control unit later in Lab 5.
Figure 2 shows the list of instructions our single cycle processor supports.
1.1 Lower Level Modules
As you can see in Figure 1, there is a top module (Datapath) and nine lower-level modules (FlipFlop, Adder, InstMem, RegFile, ImmGen, Mux (two instantiations), ALU, and data mem). In this lab we will design four lower-level modules including FlipFlop, InstMem, RegFile, and ImmGen. For each of these modules, you need to create a new source file and run some testing scenarios to validate their functionality.
1.1.1 Flip-Flop
Flip-flops are the simplest memory elements. D flip-flop stores the value of its data input signal in its internal memory. It has three inputs and one output. The inputs are the data value to be stored (d), a clock signal (clk) that indicates when the flip-flop should read the value on the d input and store it, and a reset signal (reset) that indicates when the output should set back to the initialized value (usually 0). The output
Figure 1: The building block of a single cycle processor.
Figure 2: RISC-V Instruction Set.
is simply the value of the internal state (q). When the clock input (clk) changes from deasserted to asserted (rising edge of the clk), the q output stores the value of the d input. The value of the output does not change till the next rising edge of the clock. Figure 3 shows the operation of a D flip-flop with a rising-edge trigger.
Here we want to design a flip-flop with an 8-bit input “d” and a synchronous reset. Note that synchronous reset means reset is sampled with respect to the clock clk. In other words, when reset is enabled, it will not be effective till the next active clock edge (in our case, it is the rising edge).
Figure 3: The D Flip flop with a rising-edge trigger.
Use the following code for the module definition of the D flip-flop.
Code 1: D Flip-flop
`timescale 1 ns / 1 ps
// Module definition
module FlipFlop ( clk , reset , d , q );
// Define input and output signals
// Define the D Flip flop module 's behaviour
endmodule // FlipFlop
In our single cycle processor, this flip-flop holds the program counter (PC), which is a register that holds the address of the current instruction. We will send this address to the instruction memory to read the current instruction.
Test your module: Complete the design for a D flip-flop and also write a testbench to test the following scenario:
• In the testbench define the clock signal with a clock period of 20 ns.
• Activate the reset signal for the first 100 ns and the output signal have to be set to zero (at the rising edge of the clock) regardless of the input values.
• After 100 ns the reset signal is inactive and the output should reflect the input values. Note that the output may change only at the rising edge of the clock.
The simulation result for a similar testing scenario is shown in Figure 4:
1.1.2 Adder
We need an adder to increment the PC to get the address of the next instruction. For the adder, you can simply use “+” operator.
Figure 4: The simulation result for D Flip Flop.
1.1.3 Instruction Memory
Instruction memory has one input which is the address line (called addr with 8 bits), and one output which is the instruction we read from the instruction memory (instruction, 32 bits). With the 8 bits address which comes from the PC, we can address 28 bytes (this memory is byte addressable). An instruction has 4 bytes (32 bits). So we can store 28 /4 = 64 instructions in our instruction memory. Hence, our instruction memory capacity is 64 × 32 bits.
Figure 5: Instruction memory block diagram
In the instruction memory, we need internal storage to store the instructions. We need to define a 2D array to store 64 instructions each with 4 bytes (32 bits). Then, we need to actually store some instructions (which are 32 bits data) in the internal storage.
Use the following code for the module definition of the instruction memory.
Code 2: Instruction Memory
`timescale 1 ns / 1 ps
// Module definition
module InstMem (
input [7:0] addr ,
output wire [31:0] instruction
);
// Define the Instruction memory module behavior.
endmodule // InstMem
You need to save the following instruction codes in your Instruction memory for further usage (inside the InstMem module definition). The comments show the meaning of each instruction and the expected execution result.
Code 3: Instruction Memory
memory [0] = 32 'h00007033 ; // and x0 , x0 , x0 32 'h00000000
memory [1] = 32 'h00100093 ; // addi x1 , x0 , 1 32 'h00000001
memory [2] = 32 'h00200113 ; // addi x2 , x0 , 2 32 'h00000002
memory [3] = 32 'h00308193 ; // addi x3 , x1 , 3 32 'h00000004
memory [4] = 32 'h00408213 ; // addi x4 , x1 , 4 32 'h00000005
memory [5] = 32 'h00510293 ; // addi x5 , x2 , 5 32 'h00000007
memory [6] = 32 'h00610313 ; // addi x6 , x2 , 6 32 'h00000008
memory [7] = 32 'h00718393 ; // addi x7 , x3 , 7 32 'h0000000B
memory [8] = 32 'h00208433 ; // add x8 , x1 , x2 32 'h00000003
memory [9] = 32 'h404404b3 ; // sub x9 , x8 , x4 32 'hfffffffe
memory [10] = 32 'h00317533 ; // and x10 , x2 , x3 32 'h00000000
memory [11] = 32 'h0041e5b3 ; // or x11 , x3 , x4 32 'h00000005
memory [12] = 32 'h0041a633 ; // if x3 is less than x4 , then x12 = 1
// 32 'h00000001
memory [13] = 32 'h007346b3 ; // nor x13 , x6 , x7 32 'hfffffff4
memory [14] = 32 'h4d34f713 ; // andi x14 , x9 , "4 D3" 32 'h000004d2
memory [15] = 32 'h8d35e793 ; // ori x15 , x11 , "8 D3" 32 'hfffff8d7
memory [16] = 32 'h4d26a813 ; // if x13 is less than 32 'h000004d2 , then x16 = 1
// 32 'h00000000
memory [17] = 32 'h4d244893 ; // nori x17 , x8 , "4 D2" 32 'hfffffb2c
Test your module: Try to set the higher 6 bits of “addr” signal to different numbers between 0-17 (and lower 2 bits addr[1:0] = 2’b00), and see if the output is equal to the corresponding instruction code. Here are two example test cases:
• addr[7:2]: 16 (decimal) the expected output instruction: 32’h4d26a813
• addr[7:2]: 3 (decimal) the expected output instruction: 32’h00308193
Important note: This instruction memory is byte addressable, hence you need to use bits [7:2] of addr as the instruction index (0-17). Figure 6 shows the simulation result for the second test case:
1.1.4 Register File
One structure that is central to our datapath is a register file. A register file consists of a set of registers that can be read and written by supplying a register number to be accessed. For reading from a register file,
Figure 6: The simulation result for instruction memory
we only need the register number. For writing to a register we need three inputs: a register number, the data to write, and a clock that controls the writing into the register. Our register file has two read ports and one write port. The block diagram of the register file is shown in Figure 7.
Figure 7: Register File
We have two 5-bit inputs, the register read addresses (rg rd addr1, rg rd addr2). With 5 bits, we can address 25 = 32 registers. To read from the register file, we give the register read addresses as inputs and the register values should be available at the two output lines rg rd data1 and rg rd data2 (these are 32 bits), regardless of the clock or reset. To write to the register file if reset input is zero and write enable signal (rg wrt en) is ‘1’ (ON), in the rising edge of the clock we will write the data from input line rg wrt data to the register number rg wrt addr.
Unlike what we had for flip-flop, here we want to have an asynchronous reset. It means that regardless of the clk, whenever we have reset signal, we should reset the register file (set all registers to 32’h00000000). Here is the module definition of the register file. Same as instruction memory, we need some internal storage for the register file.
Code 4: Register File
`timescale 1 ns / 1 ps
// Module definition
module RegFile (
clk , reset , rg_wrt_en ,
rg_wrt_addr ,
rg_rd_addr1 ,
rg_rd_addr2 ,
rg_wrt_data ,
rg_rd_data1 ,
rg_rd_data2
);
// Define the input and output signals
// Define the Register File module behavior.
endmodule // RegFile
Test your module:
• Generate a clock with a clock period of 100 ns.
• Set the reset signal as “1”. It means that the reset signal is getting active and set all registers to zero.
• Set the rg wrt en as “1” meaning that it is allowed to write into a selected register.
• Set the rg wrt addr to for example “10010” meaning that you have chosen the register number 18 to write into it.
• Set the rg wrt data to an optional number you would like to write into the selected register.
An example result of the simulation is shown in Figure 8.
Follow the same steps above again, but set reset signal as “0”. It means the reset is getting inactive. The results of the simulation is shown in Figure 9. If you expand the register file signal in the waveform window, you will see the data is written into the selected register.
1.1.5 Immediate Generator
Immediate Generator (ImmGen) receives the 32-bits instruction and based on the opcode, decides which bits of the instruction should be interpreted as the 12-bit immediate. Then, it sign extends the 12-bits imme- diate into 32 bits. The opcode part of the instruction is 7 bit (bit 0 to bit 6). Sign extension means to extend the sign bit. As an example, if the 12-bits immediate are 12’h900 then the output of this module should be 32’hfffff900. Or if the 12-bits immediate are 12’h700, then the output will be 32’h00000700.
Figure 10 shows the block diagram. The Verilog implementation of ImmGen is provided for you and you can use it in later labs. Test the provided ImmGen module to verify its correctness (optional, no need to include in your report).
Figure 8: The simulation results with reset as 0
Code 5: Immediate Generator
module ImmGen ( InstCode , ImmOut );
// The input and output signals
input
[31:0] InstCode ;
output reg [31:0] ImmOut ;
// The ImmGen modules behaviour
always @ ( InstCode )
begin
case ( InstCode [6:0])
// I- type - Load
7'b0000011 :
ImmOut = { InstCode [31]? {20{1 'b1 }}:20 'b0 , InstCode [31:20]};
// I- type - non - Load
7'b0010011 :
Figure 9: The simulation results for writing into Register File
Figure 10: The Immediate Generator block diagram.
ImmOut = { InstCode [31]? {20{1 'b1 }}:20 'b0 , InstCode [31:20]};
// S- type - Store
7'b0100011 :
ImmOut = { InstCode [31]? {20{1 'b1 }}:20 'b0 , InstCode [31:25] ,
InstCode [11:7]};
// U- type
7'b0010111 :
ImmOut = { InstCode [31:12] , 12 'b0 };
default :
ImmOut = {32 'b0 };
endcase
end
endmodule
1.1.6 ALU
We completed the ALU design in Lab 2.
1.1.7 Result MUX
The MUX on the output of the Data Memory will decide whether the writing data (to the register file) should come from the ALU (ALU Result) or from the Data Memory (DataMem read). You designed a 2-to-1 MUX module in Lab 1.
2 Assignment Deliverables
New Testbench Requirements:
To make the grading more efficient, please add the following initial block in your testbench module:
Code 6: Testbench Initial Block
initial begin
$dumpfile (" test . vcd ");
$dumpvars ;
end
Submission Requirements:
Your submission should be in a *.zip file and should be submitted to GradeScope. The ZIP file should include the following items:
• Source Code: design and testbench files. (FlipFlop.v, InstMem.v, RegFile.v, and their testbenches)
• PDF Report: A report in the PDF format including the simulation results.
Compress all files (*.v files + report) into one ZIP file named “lab3 UCInetID firstname lastname.zip” (note: UCInetID is your email user name and it is alphanumeric string), e.g., “lab3 sitaoh sitao huang.zip”, and upload this ZIP file to GradeScope before deadline.
Note 1: To make the grading process more efficient, please use the exact same module names, port names, and file names as specified in this manual. You may lose points if you use different names. Use the code skeletons given in the lab manual. The module part of your code (module name, module declaration, port names, and port declaration) should not be changed.
Note 2: Start working on the lab as early as possible.
Note 3: It is fine to discuss the lab with others, but please write the code by yourself.