MIPS Computer


This was a class project for my Digital Design course that was the culmination of a semester’s worth of labs. The “computer” is a single-cycle, simplified MIPS processor designed with Verilog in Xilinx Vivado and uplaoded to a Xilinx Pynq-Z2 board’s FPGA. The processor is capable of running assembly code. The multiplication of two numbers is demonstrated in the video above; the display shows the contents of Register 2, which eventually contains the final answer.

The basic organization of the processor is shown in the block diagram below.

Block diagram of MIPS processor

In layman’s terms, the processor takes two numbers, does some math on them, remembers them, and repeats with different numbers and different math.

To get more technical, the schematic of the processor in Vivado is below. It may help to open the image in a new tab and zoom in. Each component of the processor is broken down in the following section.

Full schematic of MIPS processor

Debounce circuit

Debounce component in schematic

The pushbutton is used to advance the clock by one cycle. The debounce is used to prevent unintentional advances from prolonged presses or the sporadic make-break pattern of the button contacts.

module debounce (
   input wire clk_in,
   input wire rst_in,
   input wire sig_in,
   output reg sig_debounced_out);

   parameter NDELAY = 25000000;
   parameter NBITS = 26;

   reg [NBITS-1:0] count0;
   reg [NBITS-1:0] count1;
   reg xnew0;
   reg xnew1;
   reg toggle0;
   reg toggle1;

   wire RST_in;
   wire cnt_done0;
   wire cnt_done1;
   assign RST_in = rst_in;
   //assign CLK_out = clk_in;

   ////////// Button 0 Processes //////////
   // Count 1 process
   assign cnt_done0 = (count0 == NDELAY);
   always @(posedge clk_in or posedge RST_in) begin
      if (RST_in) count0 <= 0;
      else if (sig_in != xnew0) count0 <= 0;
      else count0 <= count0 + 1;
   end

   // xnew1 process
   always @(posedge clk_in or posedge RST_in) begin
      if (RST_in) xnew0 <= sig_in;
      else if (sig_in != xnew0) xnew0 <= sig_in;
      else xnew0 <= xnew0;
   end

   // Toggle1 process
   always @(posedge clk_in or posedge RST_in) begin
      if (RST_in) toggle0 <= 0;
      else if (xnew0) toggle0 <= 1;
      else if (cnt_done0) toggle0 <= 0;
      else toggle0 <= toggle0;
   end

   // sig_debounced_out process
   always @(posedge clk_in or posedge RST_in) begin
      if (RST_in) sig_debounced_out <= sig_in;
      else if (cnt_done0 & toggle0 & sig_in) sig_debounced_out <= 1;
      else sig_debounced_out <= 0;
   end

   ////////// End Button 0 Processes //////////

endmodule

Program Counter (PC) and Instruction Memory

Program counter and instruction memory

The program counter keeps track of which instruction the processor is working on. After completion of an instruction, it either iterates by one instruction, or branches to a different instruction as specified by the offset input. The program counter passes its value into the instruction memory, which outputs the instruction at that address. This instruction eventually goes into the instruction decoder.

The instruction memory is an IP module in Vivado. The PC code is shown below.

module pc_logic(
    input clk,
    input rst,
    input signed [7:0] offset,
    input take_branch,
    output reg [7:0] pc);

    always@(posedge clk or posedge rst) begin
        if(rst) pc <= 8'h00;
        else if (take_branch) pc <= pc+offset;
        else pc <= pc+1;
    end
endmodule

Instruction decoder

Instruction decoder schematic

The instruction decoder is a critical component. It takes the instruction, analyzes what type it is (R-type, I-type, J-type), and controls the data path in the processor. For example, if two numbers need to be added, it directs those numbers into the ALU (arithmetic logic unit, covered later) where it tells the ALU to add; it then directs the result either into memory or back into the register.

module inst_decoder(
    input [15:0] instruction,
    output [3:0] opcode,
    output reg [1:0] rs_addr,
    output reg [1:0] rt_addr,
    output reg [1:0] rd_addr,
    output reg [7:0] immediate,
    output reg RegDst,
    output reg RegWrite,
    output reg ALUSrc1,
    output reg ALUSrc2,
    output reg [2:0] ALUOp,
    output reg MemWrite,
    output reg MemToReg
);
    assign opcode = instruction[15:12];

    always@(opcode) begin
        case (opcode)
            4'b0000: begin //lw i-type
                RegDst = 0; RegWrite = 1; ALUSrc1 = 0; ALUSrc2 = 1;
                ALUOp = 3'b000; MemWrite = 0; MemToReg = 1;
                rs_addr = instruction[11:10]; rt_addr = instruction[9:8]; rd_addr = 2'b00; immediate = instruction[7:0];
            end
            4'b0001: begin //lw i-type
                RegDst = 0; RegWrite = 0; ALUSrc1 = 0; ALUSrc2 = 1;
                ALUOp = 3'b000; MemWrite = 1; MemToReg = 0;
                rs_addr = instruction[11:10]; rt_addr = instruction[9:8]; rd_addr = 2'b00; immediate = instruction[7:0];
            end
            4'b0010: begin //add r-type
                RegDst = 1; RegWrite = 1; ALUSrc1 = 0; ALUSrc2 = 0;
                ALUOp = 3'b000; MemWrite = 0; MemToReg = 0;
                rs_addr = instruction[11:10]; rt_addr = instruction[9:8]; rd_addr = instruction[7:6]; immediate = 8'h00;
            end
            4'b0011: begin //addi i-type
                RegDst = 0; RegWrite = 1; ALUSrc1 = 0; ALUSrc2 = 1;
                ALUOp = 3'b000; MemWrite = 0; MemToReg = 0;
                rs_addr = instruction[11:10]; rt_addr = instruction[9:8]; rd_addr = 2'b00; immediate = instruction[7:0];
            end
            4'b0100: begin //inv r-type
                RegDst = 1; RegWrite = 1; ALUSrc1 = 0; ALUSrc2 = 0;
                ALUOp = 3'b001; MemWrite = 0; MemToReg = 0;
                rs_addr = 2'b00; rt_addr = instruction[9:8]; rd_addr = instruction[7:6]; immediate = 8'h00;
            end
            4'b0101: begin //and r-type
                RegDst = 1; RegWrite = 1; ALUSrc1 = 0; ALUSrc2 = 0;
                ALUOp = 3'b010; MemWrite = 0; MemToReg = 0;
                rs_addr = instruction[11:10]; rt_addr = instruction[9:8]; rd_addr = instruction[7:6]; immediate = 8'h00;
            end
            4'b0110: begin //andi i-type
                RegDst = 0; RegWrite = 1; ALUSrc1 = 0; ALUSrc2 = 1;
                ALUOp = 3'b010; MemWrite = 0; MemToReg = 0;
                rs_addr = instruction[11:10]; rt_addr = instruction[9:8]; rd_addr = 2'b00; immediate = instruction[7:0];
            end
            4'b0111: begin //or r-type
                RegDst = 1; RegWrite = 1; ALUSrc1 = 0; ALUSrc2 = 0;
                ALUOp = 3'b011; MemWrite = 0; MemToReg = 0;
                rs_addr = instruction[11:10]; rt_addr = instruction[9:8]; rd_addr = instruction[7:6]; immediate = 8'h00;
            end
            4'b1000: begin //ori i-type
                RegDst = 0; RegWrite = 1; ALUSrc1 = 0; ALUSrc2 = 1;
                ALUOp = 3'b011; MemWrite = 0; MemToReg = 0;
                rs_addr = instruction[11:10]; rt_addr = instruction[9:8]; rd_addr = 2'b00; immediate = instruction[7:0];
            end
            4'b1001: begin //sra i-type
                RegDst = 0; RegWrite = 1; ALUSrc1 = 0; ALUSrc2 = 1;
                ALUOp = 3'b100; MemWrite = 0; MemToReg = 0;
                rs_addr = instruction[11:10]; rt_addr = instruction[9:8]; rd_addr = 2'b00; immediate = instruction[7:0];
            end
            4'b1010: begin //sll i-type
                RegDst = 0; RegWrite = 1; ALUSrc1 = 0; ALUSrc2 = 1;
                ALUOp = 3'b101; MemWrite = 0; MemToReg = 0;
                rs_addr = instruction[11:10]; rt_addr = instruction[9:8]; rd_addr = 2'b00; immediate = instruction[7:0];
            end
            4'b1011: begin //beq j-type
                RegDst = 0; RegWrite = 0; ALUSrc1 = 0; ALUSrc2 = 0;
                ALUOp = 3'b110; MemWrite = 0; MemToReg = 0;
                rs_addr = instruction[11:10]; rt_addr = instruction[9:8]; rd_addr = 2'b00; immediate = instruction[7:0];
            end
            4'b1100: begin //bne jtype
                RegDst = 0; RegWrite = 0; ALUSrc1 = 0; ALUSrc2 = 0;
                ALUOp = 3'b111; MemWrite = 0; MemToReg = 0;
                rs_addr = instruction[11:10]; rt_addr = instruction[9:8]; rd_addr = 2'b00; immediate = instruction[7:0];
            end
            4'b1101: begin //clr r-type
                RegDst = 1; RegWrite = 1; ALUSrc1 = 1; ALUSrc2 = 0;
                ALUOp = 3'b010; MemWrite = 0; MemToReg = 0;
                rs_addr = 2'b00; rt_addr = instruction[9:8]; rd_addr = instruction[7:6]; immediate = 8'h00;
            end
            default: begin
                RegDst = 0; RegWrite = 0; ALUSrc1 = 0; ALUSrc2 = 0;
                ALUOp = 3'b000; MemWrite = 0; MemToReg = 0;
                rs_addr = 2'b00; rt_addr = 2'b00; rd_addr = 2'b00; immediate = 8'h00;
            end
        endcase
    end
endmodule

Register File

Register file and write mux

The register file contains 4 registers; each register stores a value that is meant to be operated on soon. It accepts two addresses to decide which registers to pass to its outputs. It also accepts an address on which register to write to and data to write to that specified register. It accepts a control bit from the decoder on whether or not to write in that cycle.

module reg_file (
    input rst,
    input clk,
	input wr_en,
    input [1:0] rd0_addr,
    input [1:0] rd1_addr,
    input [1:0] wr_addr,
	input [8:0] wr_data,
    output [8:0] rd0_data,
    output [8:0] rd1_data
    );

	reg [8:0] mem[3:0];
	integer i;

	always @ (posedge rst, posedge clk)
    if (rst) begin
		mem[0] <= 9'b0_0000_0000;
		mem[1] <= 9'b0_0000_0000;
		mem[2] <= 9'b0_0000_0000;
		mem[3] <= 9'b0_0000_0000;
		end
	else if (wr_en) begin
		mem[wr_addr] <= wr_data;
		end

	assign rd0_data = mem[rd0_addr];
	assign rd1_data = mem[rd1_addr];

endmodule

Arithmetic Logic Unit (ALU)

Arithmetic logic unit

The ALU does the math in the processor. It can add, subtract, compare numbers, perform NOT, AND, and OR operations, and perform arithmetic and logical shifts. It accepts two data inputs and outputs data and a take_branch bit that goes back to the PC.

module eightbit_alu(
    input signed [7:0] a,
    input signed [7:0] b,
    input [2:0] s,
    output signed [7:0] f,
    output ovf,
    output take_branch
    );

    reg signed [7:0] f;
    reg ovf;
    reg take_branch;


    always@(a or b or s)
        case(s)
            3'b000: begin
                   f = a + b;
                   ovf=((a[7]&b[7]&~f[7])|(~a[7]&~b[7]&f[7]));
                   take_branch=0;
                   end
            3'b001: begin
                   f=~b;
                   ovf=0;
                   take_branch=0;
                   end
            3'b010: begin
                   f=a&b;
                   ovf=0;
                   take_branch=0;
                   end
            3'b011: begin
                   f=a|b;
                   ovf=0;
                   take_branch=0;
                   end
            3'b100: begin
                   f=a>>>1;
                   ovf=0;
                   take_branch=0;
                   end
            3'b101: begin
                   f=a<<1;
                   ovf=0;
                   take_branch=0;
                   end
            3'b110: begin
                   f=8'b00000000;
                   ovf=0;
                   take_branch=(a==b);
                   end
            3'b111: begin
                   f=8'b00000000;
                   ovf=0;
                   take_branch=(a!=b);
                   end
            default: begin
                   f=8'b00000000;
                   ovf=0;
                   take_branch=0;
                   end
        endcase
endmodule

ALU Multiplexers

ALU multiplexers

This module contains 3 multiplexers. The first two decide what data to pass into the ALU. For the first, either register data or a zero register; the second, either register data or an immediate input. The third decides whether the data going back to the register comes directly from the ALU or comes from the data memory.

module alu_regfile(
    input ALUSrc1,
    input ALUSrc2,
    input MemToReg,
    input [8:0] ReadData1,
    input [8:0] ReadData2,
    input [7:0] Instr_i,
    input [8:0] DataMemOut,
    input [8:0] alu_result,
    output reg [7:0] input1,
    output reg [7:0] input2,
    output reg [8:0] WriteData
);
    reg [7:0] zero_register = 8'h00;

    always @ (ALUSrc1, zero_register, ReadData1) begin
        if (ALUSrc1) input1 =  zero_register;
        else input1 = ReadData1;
    end

   always @ (ALUSrc2, ReadData2, Instr_i) begin
        if (ALUSrc2) input2 = Instr_i;
        else input2 = ReadData2;
    end

    always @ (DataMemOut, alu_result, MemToReg) begin
        if (MemToReg) WriteData = DataMemOut;
        else WriteData = alu_result;
    end

endmodule

Data Memory

Data memory

This memory is intended for longer term storage of data. It accepts an 8-bit input for the address and outputs the data at that address. This data is written back to the regfile. It is a Vivado IP module.

Adaptor display and Virtual Input/Output (VIO)

Adaptor display and VIO

The adaptor display is a module that displays the value in register 2 on a 7-segment display attached to the board. It was university provided.

The VIO is a debugging tool that shows the value of critical datapaths in the processor. As the processor is running entirely from instructions, it has no outputs.


Code Execution

The processor was given a program that multiplies two numbers: 5 and -5, with an expected output of -25. The documented assembly code is shown below. It repeatedly adds the absolute value of the first number to the absolute value of the second, and performs logic to determine the final sign.

clr $0 #clear all reg to 0 00 00
clr $1 #01
clr $2 #02
clr $3 #03
ori $0, $0, 0x05 #loads 5 to reg 0 (A) 04
ori $1, $1, 0xfb #loads -5 to reg 1 (B) 05
sw $0, 0x1($2) #stores A to mem1 06
sw $1, 0x2($2) #stores B to mem2 07
andi $2, $0, 0x80 #stores msb (sign bit) of A followed by 7 0's 08
andi $3, $1, 0x80 #stores msb (sign bit) of B value followed by 7 0's 09
beq $2, $3, posresult #if signs are different, set final answer as negative 0a
clr $3 #prepare to load reg 3 0b
clr $2 #clears 2
ori $3, $3, 0x01 #loads reg 3 with indicator of negative answer 0c
sw $3, 0x3($2) #stores +/- indicator to mem3
beq $3, $3, finalanswerneg # skips part that makes indicator for positive (unconditional jump) 0d
posresult:
clr $3 #prepare to load reg 3 0e
ori $3, $3, 0x00 #loads reg 3 with indicator of positive  0f
clr $0 10
sw $3, 0x3($0) #stores final answer +/- flag to mem3 11
clr $3 12
lw $0, 0x1($3) #reloads A to reg0 13
finalanswerneg:
clr $3 14
clr $2 #clears 2
andi $2, $0, 0x80 #stores msb (sign bit) of A followed by 7 0's 08
beq $2, $3, Aispos #if a is already pos, skip 2's comp to make pos 15
inv $0, $0 #2's comp A step 1 16
addi $0, $0, 0x01 #2's comp A step 2 17
Aispos:
clr $2 #prepare to load reg 2 18
andi $2, $1, 0x80 #stores msb (sign bit) of B followed by 7 0's 19
beq $2, $3, Bispos #if b is already pos, skip 2's comp to make pos 1a
inv $1, $1 #2's comp B step 1 1b
addi $1, $1, 0x01 #2's comp B step 2 1c
Bispos:
clr $2 #clears reg 2 to store answer 1d
beq $0, $3, doneadding #skip if A is 0 1e
beq $1, $3, doneadding #skip if B is 0 1f
addagain:
add $2, $2, $0 #adds A to final answer will eventually add A to $2 B times 20 20
addi $1, $1, 0xFF #adds -1 to B 22
beq $1, $3, doneadding #if B is 0, magnitude of result is in $2 21
beq $0, $0, addagain #unconditional jump to loop 23
doneadding:
clr $1 24
lw $3, 0x3($1) #reloads +/- flag from mem3 to reg 3 25
beq $3, $1, answerispos #makes answer negative if it should be 26
inv $2, $2 #2s comp answer step 1 27
addi $2, $2, 0x01 #2s comp answer step 2 28
answerispos:
ori $2, $2, 0x00 #done, answer inreg 2 29

These two videos show the execution of the code on the actual board. The first is a video of the board itself and the second shows the VIO monitor.