Despite numerous technical challenges, especially during our initial experience with WSL and simulation tools, we implemented key instructions such as JAL and JR and added support for the .data section. We also developed and tested Verilog testbenches to verify functionality. The process was at times difficult, but it significantly deepened our understanding of computer architecture and hardware simulation.
We would like to sincerely thank our course instructor, Dr. Mohammad Abdul Qayum, for the in-class demonstrations, which greatly helped our comprehension and guided our implementation.
This project implements a MIPS CPU design in Verilog and includes testing across four progressive project phases.
- Project 0: Verify ModelSim and WSL setup
- Project 1: Test JAL and JR instruction functionality
- Project 2: Implement and verify MIN, MAX, MEAN operations
- Project 3: Add support for .data section handling
- ModelSim (Intel FPGA Starter Edition 2020.1)
- Ubuntu WSL (Ubuntu 22.04.5 LTS)
- MipsAssembler (for generating binary files)
- MIPSVerilogWOJAL (base Verilog codebase)
- UpgradedMIPS32Assembler (for .data section support)
- Open Ubuntu WSL
- Mount your Windows drive:
cd /mnt/your_location
- Install required gcc and g++ compilers:
sudo apt install gcc --fix-missing
sudo apt install g++ --fix-missing
- Compile the MIPS assembler:
g++ finalassembler.cpp -o myassemblername
- Create a binary file from the WSL terminal for our .s file:
./myassemblername inttest2.s test.bin
nano test.bin
- Create a new project
- Add all .v files from MIPSVerilogWOJAL (copy files to the project rather than referencing)
- Add memfile.txt to the project (copy files to the project rather than referencing)
- Compile all files (right-click on any file and select "Compile All")
- Start simulation (Simulate Menu → Start Simulation)
- In the start dialog, select
work
, then your testbench (MIPS_SCP_tb.v
), and click OK - Right-click on
MIPS_SCP_tb
in the Instance panel and add wave - Start simulation by selecting "Run All" from the toolbar
- Stop the simulation after a few seconds
- Locate the Memory List panel above the terminal
- Select the memory addresses you want to inspect
- Right-click on memory data and go to Properties to change display format to decimal for easier interpretation
To implement the JAL and JR instructions in our MIPS CPU, we added two control signals, Jal and Jr to distinguish these operations. JAL (Opcode 0x03) requires writing the return address (PC+4) to register $ra, so we set RegWrite and used a mux (jalmux) to route PC+4 to the write back stage. JR (Opcode 0x00, Func 0x08) needs the next program counter (PC) to come from a register, so we introduced jumpmux2 to select between a normal jump and register-based jump based on the Jr signal. These changes required extending the control logic to detect and assign proper values to Jal and Jr, and modifying the datapath to support conditional PC updates and write-back routing. The main challenge was ensuring correct control signal combinations so that JAL could simultaneously jump and store a link, while JR could dynamically fetch the next PC from a register. These changes impacted the control unit, PC update logic, register file, and the multiplexers used to control the data flow.
Here we implemented JAL and JR in control.v and datapath.v verilog file. We also write a new testbench for MIPS_SCP_tb.v to show the output. Here is our jaljrtest.s and MIPS_SCP_tb.v file:
//MIPS_SCP_tb.v
`timescale 1ns/1ns
`define V0_PATH uut.datapathcomp.RF.register[2]
`define A0_PATH uut.datapathcomp.RF.register[4]
`define A1_PATH uut.datapathcomp.RF.register[5]
module MIPS_SCP_tb;
// clock & reset
reg clk = 0;
reg reset = 1;
always #5 clk = ~clk;
// DUT
MIPS_SCP uut ( .clk(clk), .reset(reset) );
wire [31:0] v0_tb = `V0_PATH;
wire [31:0] a0_tb = `A0_PATH;
wire [31:0] a1_tb = `A1_PATH;
initial begin
$readmemb("memfile.txt", uut.imem.Imem);
#10;
reset = 0;
#100;
$display("$a0 = %0d (0x%08h), position -> 4", a0_tb, a0_tb);
$display("$a1 = %0d (0x%08h), position -> 5", a1_tb, a1_tb);
$display("$v0 = %0d (0x%08h), position -> 2", v0_tb, v0_tb);
end
endmodule
./myassembler jaljrtest.s jaljrtest.bin
nano jaljrtest.bin
- Simulate the new project using the same methods as Project 0.
Here we have written a .s code to find the minimum, maximum, and mean(average) among some numbers. We also write a new testbench for MIPS_SCP_tb.v to show the output. Our mmm.s code:
# MIPS program to find min, max, sum, and average of 2, 1, 3
# Results:
# $s0 = min
# $s1 = max
# $s2 = sum
# $v0 = average
.text
main:
li $t0, 2 # num1
li $t1, 1 # num2
li $t2, 3 # num3
# Min
move $s0, $t0
slt $t3, $t1, $s0
beq $t3, $zero, check_t2_min
move $s0, $t1
check_t2_min:
slt $t3, $t2, $s0
beq $t3, $zero, done_min
move $s0, $t2
done_min:
# Max
move $s1, $t0
slt $t3, $s1, $t1
beq $t3, $zero, check_t2_max
move $s1, $t1
check_t2_max:
li $t0, 2 # num1
li $t1, 1 # num2
li $t2, 3 # num3
slt $t3, $s1, $t2
beq $t3, $zero, done_max
move $s1, $t2
done_max:
li $t0, 2 # num1
li $t1, 1 # num2
li $t2, 3 # num3
# Sum
add $s2, $t0, $t1
add $s2, $s2, $t2
move $a0, $s2
li $a1, 3
jal divide
li $v0, 10
syscall
divide:
move $v1, $a0
li $v0, 0
div_loop:
sub $v1, $v1, $a1
slt $a2, $v1, $zero
bne $a2, $zero, end_div
addi $v0, $v0, 1
j div_loop
end_div:
jr $ra
Our MIPS_SCP_tb.v code:
//MIPS_SCP_tb.v file
`timescale 1ns/1ps
`define RF_PATH uut.datapathcomp.RF.register
`define S0_PATH `RF_PATH[16]
`define S1_PATH `RF_PATH[17]
`define S2_PATH `RF_PATH[18]
`define V0_PATH `RF_PATH[2]
`define T0_PATH `RF_PATH[ 8]
`define T1_PATH `RF_PATH[ 9]
`define T2_PATH `RF_PATH[10]
module MIPS_SCP_tb;
reg clk = 0;
reg reset = 1;
always #5 clk = ~clk;
MIPS_SCP uut ( .clk(clk), .reset(reset) );
wire [31:0] s0_tb = `S0_PATH;
wire [31:0] s1_tb = `S1_PATH;
wire [31:0] s2_tb = `S2_PATH;
wire [31:0] v0_tb = `V0_PATH;
wire [31:0] t0_tb = `T0_PATH;
wire [31:0] t1_tb = `T1_PATH;
wire [31:0] t2_tb = `T2_PATH;
integer i;
localparam RUN_TIME = 800;
initial begin
$readmemb("memfile.txt", uut.imem.Imem);
#10 reset = 0;
#(RUN_TIME);
$display("$t0 (num1) = %0d", t0_tb);
$display("$t1 (num2) = %0d", t1_tb);
$display("$t2 (num3) = %0d", t2_tb);
$display("$s0 (minimum) = %0d", s0_tb);
$display("$s1 (maximum) = %0d", s1_tb);
$display("$s2 (sum) = %0d", s2_tb);
$display("$v0 (average) = %0d", v0_tb);
end
`ifdef VCD
initial begin
$dumpfile("mips_scptest.vcd");
$dumpvars(0, MIPS_SCP_tb);
end
`endif
endmodule
- Create a binary file from the WSL terminal for our .s file:
./myassembler mmm.s mmm.bin
nano mmm.bin
- Simulate the new project using the same methods as Project 0.
Here we added initial $readmemb("data.txt", Dmem) in ram.v verilog file to add support for .data. We also write a new testbench for MIPS_SCP_tb.v to show the output. Here is our hellomips.s and MIPS_SCP_tb.v file:
.data
message: .asciiz "hello word 123"
.text
main:
li $v0, 4
la $a0, message
syscall
li $v0, 10
syscall
`timescale 1ns/1ps
`define IM_ARRAY_PATH uut.imem.Imem
`define DM_ARRAY_PATH uut.dmem.Dmem
`define RF_PATH uut.datapathcomp.RF.register
module MIPS_SCP_tb;
reg clk = 0;
reg reset = 1;
always #5 clk = ~clk;
MIPS_SCP uut ( .clk(clk), .reset(reset) );
reg [31:0] prev_v0 = 0;
reg [31:0] v0, a0;
initial begin
$readmemb("memfile.txt", `IM_ARRAY_PATH); // instructions
$readmemb("data.txt", `DM_ARRAY_PATH); // data segment
#12 reset = 0;
end
always @(negedge clk) begin
v0 = `RF_PATH[2]; // $v0
a0 = `RF_PATH[4]; // $a0
if (v0 !== prev_v0) begin
case (v0)
4 : begin
$display("\nSyscall 4 – print string:");
print_string(a0);
end
10 : begin
$finish;
end
endcase
end
prev_v0 <= v0; //for next edge
end
task automatic print_string (input [31:0] byte_addr_start);
reg [31:0] word;
reg [7:0] char;
reg [31:0] byte_addr;
integer idx;
begin
idx = 0;
forever begin
byte_addr = byte_addr_start + idx;
word = `DM_ARRAY_PATH[byte_addr[31:2]];
case (byte_addr[1:0]) // big endian
2'b00: char = word[31:24];
2'b01: char = word[23:16];
2'b10: char = word[15:8];
2'b11: char = word[7:0];
endcase
if (char == 8'h00) begin
$display("");
disable print_string;
end
else begin
$write("%c", char);
idx = idx + 1;
end
end
end
endtask
endmodule
Our previous assembler will not work, since we are working with both data and text. So, we need to upgrade our assembler by downloading from here: UpgradedMIPS32Assembler
- After downloading, go to the WSL and run these commands to make a clean, safe, and new CMake cache (You might need to install CMake in your WSL).
rm -rf build
mkdir build
cd build
cmake ..
make
- Now create a binary file from the WSL terminal for our .s file: It will create two binary files -> one for data and another for a text file.
./build/MipsAssembler hellomips.s hello.out hello.log
- View the binary file using any text editor:
-
Transfer content from data.bin to data.txt and from text.bin to memfile.txt, and add these files to your project in ModelSim.
-
Simulate the new project using the same methods as Project 0. We can see our testbench is working perfectly, and our data in Dmem, text in Imem is working perfectly.