Building a MIPS single-cycle processor in Verilog (Part 1)

Lena
8 min readJan 27, 2023

--

Back in 2019, I built a MIPS single-cycle processor in Verilog, extended it into a pipeline, and ran it on an FPGA.

Here, I will be going through the things I did to make a single-cycle MIPS processor in Verilog HDL, perform tests on Intel Quartus Prime’s Modelsim, and implement a BNE instruction.

This is a 3-part series, where the 2nd part will talk about extending this MIPS processor into a 5-stage pipeline, and the 3rd part will talk about running the MIPS 5-stage pipeline on a DE10 Nano FPGA.

Part 1: Building a MIPS single-cycle processor in Verilog

Part 2: Building a MIPS 5-stage Pipeline processor in Verilog

Part 3: Running the MIPS 5-stage Pipeline processor on a DE10-Nano FPGA

Table of contents

  1. Some prerequisites
  2. Making a single-cycle MIPS processor in Verilog HDL
  3. Testing the single-cycle MIPS processor on Modelsim
  4. Implementing a BNE instruction on the single-cycle MIPS processor
  5. Testing the single-cycle MIPS processor with the BNE instruction on ModelSim

Some prerequisites

I’ve included some prerequisites for building a single-cycle MIPS processor in Verilog HDL in this section.

To get started with Intel Quartus Prime, please check out the following blog post,

To create FPGA projects using Verilog HDL, please check out the following blog post,

Digital Design and Computer Architecture by David Harris, Sarah L. Harris contains a walkthrough of building a single-cycle MIPS using Verilog HDL or VHDL.

Computer Organization and Design by David A. Patterson, John L. Hennessy talks in detail about the MIPS design and computer architecture.

Making a single-cycle MIPS processor in Verilog HDL

The MIPS design used in this blog post is based on the design found in Computer Organization and Design by David A. Patterson, John L. Hennessy.

The design of the Single-cycle MIPS processor that implements a MIPS subset can be seen below.

Image taken from Digital Design and Computer Architecture by David Harris, Sarah L. Harris

In this section, I’ll provide some example of the Verilog codes that were used in the MIPS single-cycle processor.

In my previous blog post, I went into detail about how to build an adder using Verilog HDL, at the very low-layer level, where I took into consideration each wire, gates, flip flops, etc.

In Verilog, you can program in a higher level, without having to consider each wire, gates, flip flops. This is especially useful when creating complex circuitry like a CPU.

For example, the MIPS processor contains an adder, so we’ll be using a 32-bit adder module. Two 32-bit operand inputs, a and b will be added together to output a 32-bit y. The adder module looks like the following,

module adder(input  logic [31:0] a, b,
output logic [31:0] y);
assign y = a + b;
endmodule

The ALU, which stands for the Arithmetic Logic Unit, is responsible for carrying out the arithmetic and logical operations. There are two 32-bit operand inputs, a and b, and a 3-bit ALU control signal input alucontrol that specifies which ALU operation is to be done. It outputs a 32-bit result. The ALU module looks like the following,

module alu(input  logic [31:0] a, b,
input logic [2:0] alucontrol,
output logic [31:0] result);
logic [31:0] condinvb, sum;
assign condinvb = alucontrol[2] ? ~b : b;
assign sum = a + condinvb + alucontrol[2];
always_comb
case (alucontrol[1:0])
2'b00: result = a & b;
2'b01: result = a | b;
2'b10: result = sum;
2'b11: result = sum[31];
endcase
endmodule

The instruction memory module looks like the following, where it reads a file called instr.dat (contains the instruction in hex), and puts it into RAM. This module takes a 6-bit input a, and outputs a 32-bit instruction rd.

module imem(input logic [5:0] a, output logic [31:0] rd);
logic [31:0] RAM[63:0];
initial
$readmemh("instr.dat", RAM);
assign rd = RAM[a]; // word aligned
endmodule

There are many more components to the MIPS processor. The book Digital Design and Computer Architecture covers in detail how to implement the Single-cycle MIPS processor using Verilog HDL or VHDL.

Testing the single-cycle MIPS processor on Modelsim

In order to see the contents of registers and memory, I used $display() to output the contents of the register and memory on Modelsim whenever a register or memory value is updated.

In the register file module, I put $display() to output the contents of the register upon update.

module regfile(input  logic        clk, 
input logic we3,
input logic [4:0] ra1, ra2, wa3,
input logic [31:0] wd3,
output logic [31:0] rd1, rd2);
...
...
rf[wa3] <= wd3;
case(wa3)
5'b10000: $display("content of $s0 = %h", wd3);
5'b10001: $display("content of $s1 = %h", wd3);
5'b10010: $display("content of $s2 = %h", wd3);
5'b10011: $display("content of $s3 = %h", wd3);
5'b10100: $display("content of $s4 = %h", wd3);
5'b01000: $display("content of $t0 = %h", wd3);
5'b01001: $display("content of $t1 = %h", wd3);
//default: $display("no");
endcase
end
end
...
endmodule

In the dmem module, I put $display() to output the address and the contents at that address upon memory update.

module dmem(input  logic        clk, we,
input logic [31:0] a, wd,
output logic [31:0] rd);
logic [31:0] RAM[63:0];
assign rd = RAM[a[31:2]]; // word aligned
always_ff @(posedge clk)
if (we) begin
RAM[a[31:2]] <= wd;
$display("address %h now has data %h", a[31:0], wd);
end
endmodule

I prepared the following assembly instructions to test the MIPS single-cycle processor,

ADDI $s1 $zero 0x0001 //a is $s1, has 1
ADDI $s2 $zero 0x0002 //b is $s2, has 2
ADDI $s3 $zero 0x0003 //c is $s3, has 3
ADDI $s4 $zero 0x0004 //d is $s4, has 4
ADD $t0 $s1 $s2 //put a+b in $t0
ADD $t1 $s3 $s4 //put c+d in $t1
SUB $s0 $t1 $t0 //put (c+d)-(a+b) in $s0
SW $s1 0x0($zero)
SW $s2 0x4($zero)
SW $s3 0x8($zero)
SW $s4 0xC($zero) //this is 12($zero) in decimal
SW $s0 0x10($zero) //this is 16($zero) in decimal
LW $s1 0x8($zero)

As instr.dat required the instructions to be in hex, I converted the above assembly into the following and put it inside instr.dat.

20110001
20120002
20130003
20140004
02324020
02744820
01288022
AC110000
AC120004
AC130008
AC14000C
AC100010
8C110008

The following shows the output on Modelsim after running,

As shown in the output, the register and memory contents are correct and show that the single-cycle MIPS processor is working as expected.

Implementing a BNE instruction on the single-cycle MIPS processor

In order to implement a BNE (Branch Not Equal) instruction to the Single-cycle MIPS processor, I modified the circuitry as shown below,

I added a new wire called BeqBne, which uses the 27:26 bit of the instruction to control a new multiplexor called muxBEQBNE. This muxBEQBNE takes in an input zero and NotZero from the ALU and outputs it as zeroNzero.

module muxBEQBNE(input logic BeqBne, 
input logic zero,
input logic notzero,
output logic zeroNzero);
assign zeroNzero = BeqBne ? notzero : zero;
//always_ff @(zeroNzero) $display("BeqBne=%b, zero=%b, notzero=%b, zeroNzero=%b", BeqBne, zero, notzero, zeroNzero);
endmodule

The OP code of BEQ is 000100, while the OP code of BNE is 000101.

module maindec(input  logic [5:0] op,
output logic memtoreg, memwrite,
output logic branch, alusrc,
output logic regdst, regwrite,
output logic jump,
output logic [1:0] aluop);
...
...
always_comb
case(op)
6'b000000: controls <= 9'b110000010; // RTYPE
6'b100011: controls <= 9'b101001000; // LW
6'b101011: controls <= 9'b001010000; // SW
6'b000100: begin
controls <= 9'b000100001; // BEQ
$display("BEQ");
end
6'b000101: begin
controls <= 9'b000100001; // BNE has same controls as BEQ
$display("BNE");
end
6'b001000: controls <= 9'b101000000; // ADDI
6'b000010: controls <= 9'b000000100; // J
default: controls <= 9'bxxxxxxxxx; // illegal op
endcase
endmodule

The full Verilog code can be found in my Github repo.

Testing the single-cycle MIPS processor with the BNE instruction on ModelSim

To test the single-cycle MIPS processor with the BNE instruction, I prepared the following assembly instructions,

    addi $s0 $zero 0x1  
addi $s1 $zero 0x2
bne $s0 $s1 0x1 //taken
addi $s3 $zero 0x5 //skipped
addi $s4 $zero 0x4 //come here from bne, $s4 should be 4
addi $t0 $zero 0x2
beq $t0 $s1 0x2 //taken
addi $s2 $zero 0x8 //skipped
add $s1 $s0 $s1 //skipped
addi $t1 $zero 0x9 //come here from beq, $t1 should be 9
bne $s1 $t0 0x1 //not taken
add $s1 $s1 $s0 //$s1 should be 3
beq $s1 $s0 0x1 //not taken
add $s1 $s1 $s0 //$s1 should be 4

Then converted into hex,

    20100001
20110002
16110001
20130005
20140004
20080002
11110002
20120008
02118820
20090009
16280001
02308820
12300001
02308820

Then I ran the tests on ModelSim to confirm that the outputs are as expected.

This post focused on what I did for implementing the single-cycle MIPS processor on Verilog, as well as implementing the BNE instruction.

In the next blog post, I will be talking about how I extended this single-cycle MIPS processor into a 5-stage pipeline.

Part 1: Building a MIPS single-cycle processor in Verilog

Part 2: Building a MIPS 5-stage Pipeline processor in Verilog

Part 3: Running the MIPS 5-stage Pipeline processor on a DE10-Nano FPGA

--

--

Lena
Lena

Written by Lena

I'm a Cybersecurity Analyst! My passions include hacking, investigations, writing, and drawing! Contact: lambdamamba@proton.me, Website: LambdaMamba.com

No responses yet