一、实验目的

  1. 熟悉 MIPS 处理器的常用指令集(10 条)

  2. 掌握单周期处理器数据通路和控制单元的设计方法

  3. 基于增量方式,实现单周期 MIPS 处理器;

  4. 基于测试用例对所设计的单周期 MIPS 处理器进行功能验证。

二、实验环境

  1. 操作系统:Windows 10 或 Ubuntu 16.04

  2. 开发环境:Xilinx Vivado 2018.2

  3. 硬件平台:远程 FPGA 硬件云平台

三、实验原理

处理器(CPU)本质上是一个复杂的数字时序电路。通常,时序电路由记忆部件(如寄存器、存储器等)和组合逻辑构成。记忆部件用于保存电路的工作状态,而组合逻辑则由逻辑门组成,提供电路的所有逻辑功能。在组合逻辑的作用下,电路从一个状态转化为另一个状态,这样的电路也称为“状态机”。因此,单周期 MIPS 处理器在概念上也可以被看作一个大规模的状态机,如下图所示。其中,组合逻辑依据当前记忆部件中的值(即电路的现态)对指令进行处理,这个处理过程将再次修改记忆部件的值,使电路达到新状态(即电路的次态)。

image-20240617163813747.png

在设计单周期 CPU 这样复杂的时序电路系统时,通常的方法是从包含记忆部件的硬件开始。这些元件包括存储器寄存器,寄存器又可分为程序计数器寄存器文件。然后,在这些存储元件之间增加组合逻辑基于当前状态计算新的状态。从指令存储器中读取指令,然后译码时访问寄存器文件获取操作数,再使用加载和存储指令从数据存储器中读取和写入数据。下面给出了具有 4 种状态元件(程序计数器 PC寄存器文件指令存储器数据存储器)的框图。

image-20240617163833476.png

四、实验内容

基于 SystemVerilog HDL 设计并实现单周期 MIPS 处理器——MiniMIPS32。

该处理器具有如下特点:

⚫ 32 位数据通路

⚫ 小端模式

⚫ 支持 10 条指令:lw、sw、lui、ori、addiu、addu、slt、beq、bne 和 j

⚫ 寄存器文件由 32 个 32 位寄存器组成,采用异步读/同步写工作模式

⚫ 采用哈佛结构(即分离的指令存储器和数据存储器),指令存储器由ROM构成,采用异步读工作模式;数据存储器由 RAM 构成,采用异步读/同步写工作模式。

顶层模块MiniMIPS32_SYS结构图如下:

image-20240617164020561.png

下表给出了给出了顶层模块 MiniMIPS32_SYS 的输入/输出端口。

image-20240617164130074.png

最终设计实现的单周期MIPS处理器能够运行所提供的6个测试用例 mem.S,i-type.S,r-type.S,branch.S,sort_sim.S 和 sort_board.S。其中,前 5 个只能用于功能仿真;最后一个可以上传到远程 FPGA 硬件云平台完成功能验证,如果测试通过则 LED 灯 led_g 被点亮为绿色,否则 LED 灯 led_r 被点亮为红色。

五、实验步骤

(一)MiniMIPS的总体设计

由于MiniMIPS32模块较为复杂,所以我划分为了6个子模块,对应不同的功能

由于CPU的执行指令过程为:取指、译码、执行、访存、写回(、更新PC),因此我划分为ControlUnitRegister_FileALUOpsRegWriteDataSelectAddressCalculationpc这六个子模块,功能如下:

指令获取:从指令存储器中获取当前指令,并将指令传递给 ControlUnit 进行译码。

译码ControlUnit 根据指令的操作码和功能码,生成相应的控制信号,指导各个模块的操作。

寄存器读写Register_File 根据控制信号和指令中的寄存器地址,进行寄存器的读写操作。

ALU 操作ALUOps 根据控制信号,对操作数进行相应的运算,并输出结果。

数据选择RegWriteDataSelect 根据控制信号,选择写回寄存器的数据和目标地址。

地址计算AddressCalculation 根据控制信号和指令,计算下一条指令的地址。

程序计数器更新pc 模块在时钟上升沿更新程序计数器的值。

1. ControlUnit.sv

功能描述

  • 控制单元模块根据指令操作码 (op) 和功能码 (funct),生成各种控制信号,以指导处理器的不同部分如何操作。

  • 该模块输入操作码和功能码,并输出多个控制信号。

输入信号

  • op:指令的操作码。

  • funct:指令的功能码(仅对 R 型指令有用)。

  • RD1RD2:来自寄存器文件的两个操作数。

  • daddr:数据存储器地址。

输出信号

  • we:数据存储器写使能信号。

  • we3:寄存器文件写使能信号。

  • pcflag:程序计数器更新标志。

  • regdst:寄存器目标选择信号。

  • mem_to_reg:内存到寄存器数据选择信号。

  • j_flag:跳转指令标志。

  • alusrc:ALU 第二操作数选择信号。

  • AluControl:ALU 控制信号。

作用

  • 根据指令类型和具体功能,生成相应的控制信号,确保处理器各个部分按照预期的方式操作。例如,lwsw 指令需要访问数据存储器,ControlUnit 生成的信号会指示数据存储器进行读或写操作。

2. Register_File.sv

功能描述

  • 寄存器文件模块实现了一个包含 32 个 32 位寄存器的存储结构。提供对寄存器的读写功能。

输入信号

  • sys_clk:系统时钟信号。

  • WE3:写使能信号。

  • sys_rst_n:系统复位信号(低电平有效)。

  • A1A2A3:要读取或写入的寄存器地址。

  • WD3:要写入寄存器的数据。

输出信号

  • RD1RD2:从寄存器读取的数据。

作用

  • 在时钟上升沿或复位时,更新寄存器值。根据输入地址和控制信号,实现对寄存器的读写操作。

3. ALUOps.sv

功能描述

  • ALU 操作模块实现算术逻辑单元 (ALU) 的功能。根据控制信号执行不同的运算,如加法、减法、与、或等。

输入信号

  • alu1:ALU 的第一个操作数。

  • alu2:ALU 的第二个操作数。

  • AluControl:控制信号,决定 ALU 要执行的具体操作。

输出信号

  • aluresult:ALU 运算结果。

作用

  • 根据 AluControl 信号,对 alu1alu2 进行相应的运算,并输出结果。例如,如果 AluControl 信号指示加法操作,ALU 就会对 alu1alu2 进行加法运算,并将结果输出到 aluresult

4. RegWriteDataSelect.sv

功能描述

  • 该模块负责选择写回寄存器的数据和目标寄存器地址。

输入信号

  • regdst:寄存器目标选择信号,决定写回寄存器地址是 rd 还是 rt

  • mem_to_reg:内存到寄存器数据选择信号,决定写回的数据是来自 ALU 还是数据存储器。

  • rtrd:指令中的寄存器地址字段。

  • aluresult:ALU 运算结果。

  • dout:从数据存储器读取的数据。

输出信号

  • A3:要写入的寄存器地址。

  • WD3:要写入寄存器的数据。

作用

  • 根据 regdst 信号,决定要写入的寄存器地址是 rd 还是 rt。根据 mem_to_reg 信号,决定写入寄存器的数据是 ALU 结果还是从内存读取的数据。

5. AddressCalculation.sv

功能描述

  • 地址计算模块负责计算下一条指令的程序计数器 (PC) 值。

输入信号

  • pcflag:程序计数器更新标志。

  • j_flag:跳转指令标志。

  • next_addr:当前的程序计数器值。

  • extend:符号扩展后的立即数。

  • instr_index:跳转指令的目标地址。

输出信号

  • current_addr:计算后的下一条指令地址。

作用

  • 根据 pcflagj_flag 信号,决定程序计数器的更新方式。处理顺序执行、条件分支和无条件跳转等情况,计算出下一条指令的地址。

6. pc.sv

功能描述

  • 程序计数器 (PC) 模块实现程序计数器的功能。

输入信号

  • cpu_clk:系统时钟信号。

  • cpu_rst_n:系统复位信号(低电平有效)。

  • current_addr:当前的程序计数器值。

输出信号

  • next_addr:更新后的程序计数器值。

作用

  • 在时钟上升沿,根据复位信号和输入的 current_addr,更新程序计数器的值。确保处理器按照正确的指令地址执行。

(二)lw 和 sw(访存类指令)

1.ControllerUnit.sv

ControlUnit 模块中添加 lwsw 指令的控制逻辑。

module ControlUnit(
    input  logic [5:0]  op,
    input  logic [5:0]  funct,
    input  logic [31:0] RD1,
    input  logic [31:0] RD2,
    input  logic [31:0] daddr,
    output logic        we,
    output logic        we3,
    output logic        pcflag,
    output logic        regdst,
    output logic        mem_to_reg,
    output logic        j_flag,
    output logic [1:0]  alusrc,
    output logic [1:0]  AluControl
);
    always_comb begin
        we = 0;
        we3 = 0;
        pcflag = 0;
        regdst = 0;
        mem_to_reg = 0;
        j_flag = 0;
        alusrc = 2'b00;
        AluControl = 2'b00;
​
        case(op)
            6'b100011: begin // lw
                we = 0;
                we3 = 1;
                pcflag = 0;
                regdst = 0;
                alusrc = 2'b01;
                mem_to_reg = 1;
                j_flag = 0;
                AluControl = 2'b00;
            end
            6'b101011: begin // sw
                we = (daddr[31:16] != 16'h8000 && daddr[31:16] != 16'h8004);
                we3 = 0;
                pcflag = 0;
                regdst = 0;
                alusrc = 2'b01;
                mem_to_reg = 0;
                j_flag = 0;
                AluControl = 2'b00;
            end
            default: pcflag = 0;
        endcase
    end
endmodule

lw 指令:控制信号 we 设为 0(不写数据存储器),we3 设为 1(写寄存器文件),alusrc 设为 2'b01(选择符号扩展后的立即数作为 ALU 的第二个操作数),mem_to_reg 设为 1(选择从数据存储器读取的数据写回寄存器),AluControl 设为 2'b00(执行加法操作)。

sw 指令:控制信号 we 根据地址判断是否写数据存储器(避免与特定地址冲突),we3 设为 0(不写寄存器文件),alusrc 设为 2'b01mem_to_reg 设为 0AluControl 设为 2'b00

2.Register_File.sv

寄存器文件模块,读取和写入寄存器。

在时钟上升沿或复位时,更新寄存器文件的值。

根据 WE3 信号,决定是否将 WD3 写入到地址 A3 对应的寄存器。

读取地址 A1A2 对应的寄存器值,并分别输出到 RD1RD2

module Register_File(
    input               sys_clk,
    input               WE3,
    input               sys_rst_n,
    input        [4:0]  A1,
    input        [4:0]  A2,
    input        [4:0]  A3,
    input        [31:0] WD3,
    output logic [31:0] RD1,
    output logic [31:0] RD2
);
    logic [31:0] r [31:0];
​
    always_ff @(posedge sys_clk) begin
        if (!sys_rst_n) begin
            for (int i = 0; i < 32; i++) begin
                r[i] <= 32'd0;
            end
        end
        else if (WE3) begin
            r[A3] <= WD3;
        end
    end
​
    assign RD1 = r[A1];
    assign RD2 = r[A2];
endmodule

3.ALUOps.sv

实现加法操作,根据 AluControl 信号,执行加法操作。对于 lwsw 指令,ALU 需要执行加法操作,将基址寄存器的值与立即数相加,得到内存访问地址。

module ALUOps(
    input  logic [31:0] alu1,
    input  logic [31:0] alu2,
    input  logic [1:0]  AluControl,
    output logic [31:0] aluresult
);
    always_comb begin
        case (AluControl)
            2'b00: aluresult = alu1 + alu2; // 加法操作
            default: aluresult = 32'b0;
        endcase
    end
endmodule

4.RegWriteDataSelect.sv

选择写回寄存器的数据和目标寄存器地址。具体来讲

根据 regdst 信号,决定写回的寄存器地址是 rd 还是 rt

根据 mem_to_reg 信号,决定写回的数据是来自内存还是 ALU 运算结果。

module RegWriteDataSelect(
    input  logic        regdst,
    input  logic        mem_to_reg,
    input  logic [4:0]  rt,
    input  logic [4:0]  rd,
    input  logic [31:0] aluresult,
    input  logic [31:0] dout,
    output logic [4:0]  A3,
    output logic [31:0] WD3
);
    assign A3 = regdst ? rd : rt;
    assign WD3 = mem_to_reg ? {dout[7:0], dout[15:8], dout[23:16], dout[31:24]} : aluresult;
endmodule

5.AddressCalculation.sv

计算下一条指令的程序计数器 (PC) 值。处理顺序执行和条件分支指令,计算下一条指令的地址。

对于 lwsw 指令,程序计数器顺序递增。

module AddressCalculation(
    input  logic        pcflag,
    input  logic        j_flag,
    input  logic [31:0] next_addr,
    input  logic [31:0] extend,
    input  logic [25:0] instr_index,
    output logic [31:0] current_addr
);
    always_comb begin
        if (!j_flag) begin
            if (!pcflag)
                current_addr = next_addr + 4;
            else
                current_addr = next_addr + 4 + (extend << 2);
        end else begin
            logic [31:0] mid = next_addr + 4;
            current_addr = {mid[31:28], instr_index, 2'b00};
        end
    end
endmodule

6.pc.sv

程序计数器模块,在时钟上升沿,根据复位信号和输入的 current_addr,更新程序计数器的值。

module pc(
    input logic cpu_clk,
    input logic cpu_rst_n,
    input logic [31:0] current_addr,
    output logic [31:0] next_addr
);
​
    always_ff @(posedge cpu_clk) begin
        if (!cpu_rst_n)
            next_addr <= 32'd0;
        else
            next_addr <= current_addr;
    end
endmodule

7.顶层模块

将各个子模块联系起来构成完整的系统

从指令存储器获取当前指令,传递给 ControlUnit 进行译码,生成控制信号。

根据控制信号,Register_File 模块读取或写入寄存器。

ALUOps 模块执行相应的运算,并输出结果。

RegWriteDataSelect 模块选择写回寄存器的数据和目标地址。

AddressCalculation 模块计算下一条指令的地址。

pc 模块在时钟上升沿更新程序计数器的值。

`include "defines.sv"
​
module MiniMIPS32(
    input  logic        cpu_clk,
    input  logic        cpu_rst_n,
    output logic [31:0] iaddr,
    input  logic [31:0] inst,
    output logic [31:0] daddr, 
    output logic        we,   
    output logic [31:0] din,     
    input  logic [31:0] dout    
);
​
    logic [31:0] Instruction;
    logic [5:0] op, funct;
    logic [4:0] rs, rt, rd;
    logic [15:0] imm;
    logic [25:0] instr_index;
​
    logic [31:0] current_addr;
    logic [31:0] next_addr;  
    logic [31:0] aluresult;
    logic [31:0] extend;
    logic [31:0] extend_0;
    logic [31:0] alu1;
    logic [31:0] alu2;
    logic [31:0] RD1;
    logic [31:0] RD2;
    logic [31:0] WD3;
    logic [4:0]  A3;
​
    logic we3, pcflag, regdst, mem_to_reg, j_flag;
    logic [1:0] alusrc, AluControl;
​
    assign Instruction = {inst[7:0], inst[15:8], inst[23:16], inst[31:24]};
    assign op = Instruction[31:26];
    assign rs = Instruction[25:21];
    assign rt = Instruction[20:16];
    assign rd = Instruction[15:11];
    assign imm = Instruction[15:0];
    assign funct = Instruction[5:0];
    assign instr_index = Instruction[25:0];
​
    ControlUnit cu(
        .op(op),
        .funct(funct),
        .RD1(RD1),
        .RD2(RD2),
        .daddr(daddr),
        .we(we),
        .we3(we3),
        .pcflag(pcflag),
        .regdst(regdst),
        .mem_to_reg(mem_to_reg),
        .j_flag(j_flag),
        .alusrc(alusrc),
        .AluControl(AluControl)
    );
​
    Register_File rf(
        .sys_clk(cpu_clk),
        .WE3(we3),
        .sys_rst_n(cpu_rst_n),
        .A1(rs),
        .A2(rt),
        .RD1(RD1), 
        .RD2(RD2),
        .A3(A3),
        .WD3(WD3)
    );
​
    always_comb begin
        extend = {{16{imm[15]}}, imm};
        extend_0 = {16'b0, imm};
        alu1 = RD1;
        alu2 = (alusrc == 2'b00) ? RD2 : (alusrc == 2'b01) ? extend : extend_0;
    end
​
    ALUOps alu_ops(
        .alu1(alu1),
        .alu2(alu2),
        .AluControl(AluControl),
        .aluresult(aluresult)
    );
​
    RegWriteDataSelect reg_write_data_select(
        .regdst(regdst),
        .mem_to_reg(mem_to_reg),
        .rt(rt),
        .rd(rd),
        .aluresult(aluresult),
        .dout(dout),
        .A3(A3),
        .WD3(WD3)
    );
​
    AddressCalculation addr_calc(
        .pcflag(pcflag),
        .j_flag(j_flag),
        .next_addr(next_addr),
        .extend(extend),
        .instr_index(instr_index),
        .current_addr(current_addr)
    );
​
    assign daddr = aluresult;
    assign {din[7:0], din[15:8], din[23:16], din[31:24]} = RD2;
​
    pc pc_count(
        .cpu_clk(cpu_clk),
        .cpu_rst_n(cpu_rst_n),
        .current_addr(current_addr),
        .next_addr(next_addr)
    );
​
    assign iaddr = next_addr;
​
endmodule

(二)lui、ori 和 addiu(I-型指令)

1.ControllerUnit.sv

..........
6'b101011: begin // sw
    we = (daddr[31:16] != 16'h8000 && daddr[31:16] != 16'h8004);
    we3 = 0;
    pcflag = 0;
    regdst = 0;
    alusrc = 2'b01;
    mem_to_reg = 0;
    j_flag = 0;
    AluControl = 2'b00;
end
6'b001111: begin // lui
    we = 0;
    we3 = 1;
    pcflag = 0;
    regdst = 0;
    alusrc = 2'b10;
    mem_to_reg = 0;
    j_flag = 0;
    AluControl = 2'b10;
end
6'b001101: begin // ori
    we = 0;
    we3 = 1;
    pcflag = 0;
    regdst = 0;
    alusrc = 2'b10;
    mem_to_reg = 0;
    j_flag = 0;
    AluControl = 2'b01;
end
6'b001001: begin // addiu
    we = 0;
    we3 = 1;
    pcflag = 0;
    regdst = 0;
    alusrc = 2'b01;
    mem_to_reg = 0;
    j_flag = 0;
    AluControl = 2'b00;
end
..........

2.ALUOps.sv

..........
always_comb begin
    case (AluControl)
        2'b00: aluresult = alu1 + alu2; // 加法操作
        2'b01: aluresult = alu1 | alu2; // ORI 操作
        2'b10: aluresult = alu2 << 16;  // LUI 操作
        default: aluresult = 32'b0;
    endcase
end
..........

3.顶层模块

为了使 lui 指令工作,需要确保立即数在 lui 指令中的扩展方式正确。以下代码片段应在顶层模块的 always_comb 块中:

..........
always_comb begin
    extend = {{16{imm[15]}}, imm};
    extend_0 = {imm, 16'b0}; // 对于LUI指令,高16位是立即数,低16位为0
    alu1 = RD1;
    alu2 = (alusrc == 2'b00) ? RD2 : (alusrc == 2'b01) ? extend : extend_0;
end
..........

以上增加的代码片段在 ControlUnit.svALUOps.sv 中添加了对 luioriaddiu 指令的控制逻辑和操作,实现了对这三条 I-型指令的支持。在顶层模块中添加了立即数的扩展方式以支持 lui 指令。

(三)addu 和 slt(R-型指令)

1.ControllerUnit.sv

..........
6'b001001: begin // addiu
    we = 0;
    we3 = 1;
    pcflag = 0;
    regdst = 0;
    alusrc = 2'b01;
    mem_to_reg = 0;
    j_flag = 0;
    AluControl = 2'b00;
end
6'b000000: begin // R-type
    if(funct == 6'b100001) begin // addu
        we = 0;
        we3 = 1;
        pcflag = 0;
        regdst = 1;
        alusrc = 2'b00;
        mem_to_reg = 0;
        j_flag = 0;
        AluControl = 2'b00;
    end
    else if(funct == 6'b101010) begin // slt
        we = 0;
        we3 = 1;
        pcflag = 0;
        regdst = 1;
        alusrc = 2'b00;
        mem_to_reg = 0;
        j_flag = 0;
        AluControl = 2'b11;
    end
end
..........

2.ALUOps.sv

..........
always_comb begin
    case (AluControl)
        2'b00: aluresult = alu1 + alu2; // 加法操作
        2'b01: aluresult = alu1 | alu2; // ORI 操作
        2'b10: aluresult = alu2 << 16;  // LUI 操作
        2'b11: aluresult = (alu1 < alu2) ? 32'b1 : 32'b0; // SLT 操作
        default: aluresult = 32'b0;
    endcase
end
..........

3.顶层模块

顶层模块中,已包含了立即数扩展的代码,现需确保 funct 字段的操作也被正确处理:

..........
assign funct = Instruction[5:0];
..........

(四)beq、bne 和 j(转移指令)

1.ControllerUnit.sv

..........
6'b000000: begin // R-type
    if(funct == 6'b100001) begin // addu
        we = 0;
        we3 = 1;
        pcflag = 0;
        regdst = 1;
        alusrc = 2'b00;
        mem_to_reg = 0;
        j_flag = 0;
        AluControl = 2'b00;
    end
    else if(funct == 6'b101010) begin // slt
        we = 0;
        we3 = 1;
        pcflag = 0;
        regdst = 1;
        alusrc = 2'b00;
        mem_to_reg = 0;
        j_flag = 0;
        AluControl = 2'b11;
    end
end
6'b000100: begin // beq
    we = 0;
    we3 = 0;
    pcflag = (RD1 == RD2);
    regdst = 0;
    alusrc = 2'b00;
    mem_to_reg = 0;
    j_flag = 0;
    AluControl = 2'b00;
end
6'b000101: begin // bne
    we = 0;
    we3 = 0;
    pcflag = (RD1 != RD2);
    regdst = 0;
    alusrc = 2'b00;
    mem_to_reg = 0;
    j_flag = 0;
    AluControl = 2'b00;
end
6'b000010: begin // j
    we = 0;
    we3 = 0;
    pcflag = 0;
    regdst = 0;
    alusrc = 2'b00;
    mem_to_reg = 0;
    j_flag = 1;
    AluControl = 2'b00;
end
..........

2.AddressCalculation.sv

AddressCalculation 模块中已处理条件分支和无条件跳转的逻辑,无需增加新的代码,只需确保现有代码正确计算跳转地址:

..........
always_comb begin
    if (!j_flag) begin
        if (!pcflag)
            current_addr = next_addr + 4;
        else
            current_addr = next_addr + 4 + (extend << 2);
    end else begin
        logic [31:0] mid = next_addr + 4;
        current_addr = {mid[31:28], instr_index, 2'b00};
    end
end
..........

3.顶层模块

顶层模块中,已包含了对 instr_index 的处理代码,现需确保 pcflagj_flag 信号被正确使用:

..........
assign instr_index = Instruction[25:0];
..........
always_comb begin
    extend = {{16{imm[15]}}, imm};
    extend_0 = {imm, 16'b0}; // 对于LUI指令,高16位是立即数,低16位为0
    alu1 = RD1;
    alu2 = (alusrc == 2'b00) ? RD2 : (alusrc == 2'b01) ? extend : extend_0;
    pc_src = pcflag;
    next_pc = (j_flag) ? {next_pc[31:28], instr_index, 2'b00} : (pc_src ? (next_pc + 4 + (extend << 2)) : (next_pc + 4));
end
..........

(六)实验结果

仿真前加载相应的指令和数据,即分别加载XXX_inst.coe和XXX_data.coe

image-20240617190516561.png

我对仿真测试的代码进行了略微的修改,以便于测试重置后是否能够再次正常运行,修改部分如下

initial begin
    // Initialize Inputs
    sys_clk = 0;
    sys_rst_n = 0;
    #400
    sys_rst_n = 1;     
​
​
    //#15000 $stop;
    #12000
    sys_rst_n = 0;
    #400
    sys_rst_n = 1;
end

仿真结果:

image-20240617190725392.png

远程平台验证结果:

image-20240617190758172.png