当前位置: 首页 > article >正文

数字设计实验:RISC-V指令单周期CPU

文章目录

  • RISC-V指令单周期CPU设计
    • 一、实验目的
    • 二、实验内容
    • 三、实验设计
      • 1. CPU功能分解与设计
        • 1.1 单周期运行逻辑
        • 1.2 控制信号生成逻辑
      • 2. CPU主要部件设计分块思路
      • 3. 具体模块设计
        • 3.1 寄存器堆设计
        • 3.2 PC取指模块设计
        • 3.3 指令存储器(PM)设计
        • 3.4 数据存储器(DM)设计
        • 3.5 立即数扩展单元设计
        • 3.6 主控单元设计
        • 3.7 ALU信号控制单元设计
        • 3.8 算术逻辑单元(ALU)设计
        • 3.9 CPU顶层实现单元设计
    • 四、仿真过程及其结果分析
      • 1. `addi X1, X0, 0x8`指令实现验证
      • 2. `lw X2, 4(X1)`指令实现验证
      • 3. `add X3, X1, X2`指令实现验证
      • 4. `sub X4, X3, X1`指令实现验证
      • 5. `or X5, X1, X4`指令实现验证
      • 6. `ori X6, X5, 1`指令实现验证
      • 7. `sw X6, 0(X2)`指令实现验证
      • 8. `slt X7, X2, X4`指令实现验证
      • 9. `slti X8, X2, 8`指令实现验证
      • 10. ` beq X3, X5, - 12`指令实现验证
    • 五、实验体会

RISC-V指令单周期CPU设计

一、实验目的

​ 在这个项目中,需要完成一个能够实现指定RISC-V指令功能的简单单周期CPU的电路设计。并用verilog编程编写项目,通过vivado软件仿真验证所设计的CPU功能的正确性。

二、实验内容

​ 对于这个32位单周期CPU电路,有如下具体的要求:

  • 该CPU需要完成的指令有以下9个:add,sub,or,slt,addi,ori,slti,sw,lw。每条指令长度都为32bits。

  • 该CPU的部件资源包括:1)X0-X31共32个通用寄存器;2)特殊寄存器PC(program counter)指令暂存寄存器IR(instruction Register)。以上寄存器都是32bits字长。

  • 存储器包括256bytes的memory(地址为0~255,采用little endian方式存储数据或者指令)。其中地址0-127存放程序指令(PM)(最多32条指令),地址128-255存放数据(DM)。也可以使用两块128byte的独立存储器(本实验采用两块独立存储器方式)。

  • 最后的波形仿真应当采用功能仿真,且所有存储器件中的数据都应当被显示。

三、实验设计

1. CPU功能分解与设计

1.1 单周期运行逻辑

​ 本项目中采取如下的流程阶段实现CPU的一个完整周期的功能。

  1. 取指:PC 作为指令存储器的地址,在时钟上升沿读取指令并存储到 IR 寄存器中。同时,PC 自动指向下一条指令的地址(PC = PC + 4),除非遇到跳转或分支指令改变 PC 的值。

  2. 译码:控制单元根据 IR 寄存器中的指令操作码和功能码生成各种控制信号,同时从寄存器堆中读取指令所需的操作数(对于寄存器操作数指令)。

  3. 执行:根据控制信号,ALU 对操作数进行相应的运算(如加法、减法、逻辑运算等),或者数据存储器进行数据的读写操作。运算结果或读取的数据会被暂存起来,准备写入目的寄存器或用于后续的操作。

  4. 访存:将执行结果读取或者写入到存储器。在RISC中,CPU需要通过寄存器对内存操作,而寄存器与内存间的通信则需要LSU来完成。load store unit为加载存储单元,管理所有load, store操作。

  5. 写回:如果指令需要将结果写回寄存器(由 RegWr 信号控制),则在时钟上升沿将执行阶段的结果写入目的寄存器(由 rd 字段指定)。对于 lw 指令,从数据存储器读取的数据会由 MemtoReg 信号控制写入目的寄存器。

1.2 控制信号生成逻辑

​ 根据指令格式和操作码,使用组合逻辑电路生成控制信号。例如,对于 R 型指令(如 add、sub、slt 等),当 opcode 为 0110011 时,根据 funct3 和 funct7 的值进一步确定具体的操作,并生成相应的 ALUctr 信号。对于 I 型指令(如 addi、ori、slti、lw 等),当 opcode 为 0010011 时,根据 funct3 的值生成对应的控制信号,同时控制立即数扩展单元进行符号扩展或零扩展(根据指令要求)。对于 S 型指令(sw),当 opcode 为 0100011 时,生成控制信号控制数据存储器的写操作。对于 B 型指令(beq),当 opcode 为 1100011 时,根据比较结果和偏移量生成分支控制信号。

2. CPU主要部件设计分块思路

​ 本项目中分为以下几个模块设计CPU的不同部件:寄存器堆、PC取指模块、指令存储器(PM)、数据存储器(DM)、立即数扩展单元、主控单元、ALU信号控制单元、算术逻辑单元(ALU)、CPU顶层实现单元,按其分块设计的CPU电路图如图1所示。

在这里插入图片描述

  • 图1.软件生成的CPU结构电路图

3. 具体模块设计

3.1 寄存器堆设计

3.1.1 结构与功能说明

​ 本模块内部引入32个32位通用寄存器register_file,CPU在运行过程可以调用。该模块通过根据输入的rs1、rs2寄存器地址,读取其存储的数据,并在写使能端有效时将待写入的数据写入目的寄存器rd中。同时,若有从内存写回寄存器使能端有效,则将从内存中的数据写入目的寄存器rd中。

3.1.2 接口列表

​ 除了为了仿真而用于查看的样例寄存器值输出,以下为该模块主要接口列表:

端口名称类型位宽说明
clkinput1输入的时钟信号
resetinput1输入的复位信号
reg_write_enableinput1寄存器的写使能信号
rd_addrinput5要更改的寄存器序号,就是目的寄存器的序号
wr_datainput32要更改寄存器的值
rs1_addrinput5输入的寄存器rs1的序号
rs1_dataoutput32从rs1中读出来的值
rs2_addrinput5输入的寄存器rs2的序号
rs2_dataoutput32从rs2中读出来的值
mem_to_reginput1从内存写回寄存器使能
mem_datainput32从内存中读取的数据

3.1.3 代码实现

Register_file.v

`timescale 1ns / 1ps
module Register_file(
    input wire clk,
    input wire reset, 
    input wire [4:0] rs1_addr,
    input wire [4:0] rs2_addr,
    input wire [4:0] rd_addr,
    input wire [31:0] wr_data,
    input wire [31:0] mem_data,
    input wire reg_write_enable,
    input wire mem_to_reg,
    output wire [31:0] rs1_data,
    output wire [31:0] rs2_data,
    //查看样例寄存器值
    output wire [7:0] X0,
    output wire [7:0] X1,
    output wire [7:0] X2,
    output wire [7:0] X3,
    output wire [7:0] X4,
    output wire [7:0] X5,
    output wire [7:0] X6,
    output wire [7:0] X7,
    output wire [7:0] X8
);

    reg [31:0] register_file[31:0];

    // 组合逻辑读取寄存器数据
    assign rs1_data = (rs1_addr == 5'b0)? 32'b0 : register_file[rs1_addr];
    assign rs2_data = (rs2_addr == 5'b0)? 32'b0 : register_file[rs2_addr];

    // 时序逻辑写寄存器数据
    integer i;
    always @(posedge clk or posedge reset) begin
        if (reset) begin
            // 复位时将所有寄存器清零
            for (i = 0; i < 32; i = i + 1)
                register_file[i] <= 32'b0;
        end else if (reg_write_enable && rd_addr!= 5'b0) begin
                if (mem_to_reg) begin  // 根据mem_to_reg信号判断是从内存读数据写回还是其他情况(如ALU结果写回)
                register_file[rd_addr] <= mem_data;
//                $display("Writing data from memory %h to register %d", mem_data, rd_addr);  // 添加显示语句查看写回情况
            end else begin
                register_file[rd_addr] <= wr_data;
//                $display("Writing data from ALU %h to register %d", wr_data, rd_addr);  // 添加显示语句查看写回情况
            end
        end
    end
    
    assign X0 = register_file[0][7:0];
    assign X1 = register_file[1][7:0];
    assign X2 = register_file[2][7:0];
    assign X3 = register_file[3][7:0];
    assign X4 = register_file[4][7:0];
    assign X5 = register_file[5][7:0];
    assign X6 = register_file[6][7:0];
    assign X7 = register_file[7][7:0];
    assign X8 = register_file[8][7:0];
    
endmodule

3.2 PC取指模块设计

3.2.1 结构与功能说明

​ 本模块用于实现CPU的PC更新功能,正常指令下进行自增4的操作以进入下一个指令,如果当前指令为beq指令,则根据指令中具体的立即数值来进行指令地址跳转的操作。

3.2.2 接口列表

​ 以下为该模块主要接口列表:

端口名称类型位宽说明
clkinput1输入的时钟信号
resetinput1输入的复位信号
branch_offsetinput32与beq指令相关的立即数扩展量
beq_takeninput1上步指令是否为beq指令
PCinput32下步指令地址

3.2.3 代码实现

PC_module.v

`timescale 1ns / 1ps
module PC_module(
    input wire clk,
    input wire reset,
    input wire [31:0] branch_offset,
    input wire beq_taken,
    output reg [31:0] PC
);

    always @(posedge clk or posedge reset) begin
        if (reset)
            PC <= 32'b0;
        else if (beq_taken)
            PC <= PC + branch_offset;
        else
            PC <= PC + 4;
    end

endmodule
3.3 指令存储器(PM)设计

3.3.1 结构与功能说明

​ 本模块中包含一个128字节大小的指令存储器(PM),CPU可根据输入的PC值(指令地址)从指令存储器中取出对应的指令值instruction

3.3.2 接口列表

​ 以下为该模块主要接口列表:

端口名称类型位宽说明
PCinput32当前PC值(指令地址)
instructionoutput32取出的指令值

3.3.3 代码实现

PM_module.v

`timescale 1ns / 1ps
module PM_module(
    input wire [31:0] PC,
    output wire [31:0] instruction
);

    reg [7:0] PM[127:0];

    // 根据PC读取指令
    assign instruction = {PM[PC[6:0]+3], PM[PC[6:0]+2], PM[PC[6:0]+1], PM[PC[6:0]]};

endmodule
3.4 数据存储器(DM)设计

3.4.1 结构与功能说明

​ 本模块包含一个128字节大小的数据存储器(DM),根据输入的读写操作起始内存地址以及读写使能端的有效情况,读出以该地址为小端的四字节内存中的数据值,或者将输入的数据写入以该地址为小端的四字节内存中。

3.4.2 接口列表

​ 除了为了仿真而用于查看的样例存储器值输出,以下为该模块主要接口列表:

端口名称类型位宽说明
clkinput1输入的时钟信号
addressinput32读写操作的起始内存地址
write_datainput32将要写入内存的数据值
mem_write_enableinput1写使能端
mem_read_enableinput1读使能端
read_dataoutput32从内存中读出的数据值

3.4.3 代码实现

DM_module.v

`timescale 1ns / 1ps
module DM_module(
    input wire clk,
    input wire [31:0] address,
    input wire [31:0] write_data,
    input wire mem_write_enable,
    input wire mem_read_enable,
    output reg [31:0] read_data,
    output wire [7:0] DM_4,  //查看地址为4的内存值
    output wire [7:0] DM_12  //查看地址为12的内存值
);

    reg [7:0] DM[127:0];

    // 读数据
    always @(*) begin
        if (mem_read_enable) begin
            read_data[7:0] <= DM[address[6:0]];
            read_data[15:8] <= DM[address[6:0] + 1];
            read_data[23:16] <= DM[address[6:0] + 2];
            read_data[31:24] <= DM[address[6:0] + 3];
//            $display("Reading data from memory at address %h, read_data = %h", address, read_data);  // 添加显示语句便于调试查看读数据情况
        end 
    end
    
//    always @(*) begin
//        $display("adress ", address);
//        $display("DM[adr] ", DM[address]);
//        $display("read_data ", read_data);
//    end
    
    // 写数据
    always @(posedge clk) begin
//        $display("memwrt", mem_write_enable);
        if (mem_write_enable) begin
            DM[address[6:0]] <= write_data[7:0];
            DM[address[6:0]+1] <= write_data[15:8];
            DM[address[6:0]+2] <= write_data[23:16];
            DM[address[6:0]+3] <= write_data[31:24];
//            $display("Writing data %h to memory at address %h", write_data, address);  // 添加显示语句便于调试查看写数据情况
        end
    end
    
    assign DM_4 = DM[4];    
    assign DM_12 = DM[12];
    
endmodule
3.5 立即数扩展单元设计

3.5.1 结构与功能说明

​ 本模块用于实现对输入指令中的立即数进行扩展的功能。

3.5.2 接口列表

​ 以下为该模块主要接口列表:

端口名称类型位宽说明
instructioninput32输入当前指令值
imm_extendedoutput32输出指令中的立即数扩展

3.5.3 代码实现

ImmExtend.v

`timescale 1ns / 1ps
module ImmExtend(
    input wire [31:0] instruction,
    output reg [31:0] imm_extended
);

    always @(*) begin
        case (instruction[6:0])
            7'b0010011:  // addi, ori, slti
                imm_extended = {{20{instruction[31]}}, instruction[31:20]};
            7'b0000011:  // lw
                imm_extended = {{20{instruction[31]}}, instruction[31:20]};
            7'b0100011:  // sw
                imm_extended = {{20{instruction[31]}}, instruction[31:25], instruction[11:7]};
            7'b1100011:  // beq
                imm_extended = {{20{instruction[31]}}, instruction[7], instruction[30:25], instruction[11:8], 1'b0};
            default:
                imm_extended = 32'b0;
        endcase
    end

endmodule

3.6 主控单元设计

3.6.1 结构与功能说明

​ 本模块实现了对输入指令进行解析的功能。根据输入的指令,生成各种控制信号。

3.6.2 接口列表

​ 以下为该模块主要接口列表:

端口名称类型位宽说明
instructioninput32输入当前的指令值
branchoutput1根据指令值解析是否为beq指令的信号
memreadoutput1控制是否进行存储器读操作的信号
ALUopoutput2传递给ALU控制单元的操作码,用于决定ALU执行何种运算
memtoregoutput1控制是否加载内存值写入寄存器
memwriteoutput1控制是否进行存储器写操作的信号
ALUsrcoutput1控制ALU的操作数来源(是寄存器还是立即数)
regwriteoutput1控制是否将结果写回寄存器堆的信号

3.6.3 代码实现

Control.v

`timescale 1ns / 1ps
module control(
    input wire [6:0] instruction,  // 输入的指令,取其低7位用于判断指令类型
    output reg branch,  // 控制是否进行分支操作的信号
    output reg memread,  // 控制是否进行存储器读操作的信号
    output reg [1:0] ALUop,  // 传递给ALU控制单元的操作码,用于决定ALU执行何种运算
    output reg memtoreg,  //控制是否加载内存值写入寄存器
    output reg memwrite,  // 控制是否进行存储器写操作的信号
    output reg ALUsrc,  // 控制ALU的操作数来源(是寄存器还是立即数)
    output reg regwrite  // 控制是否将结果写回寄存器堆的信号
);

    always @(*) begin
        case (instruction[6:0])
            7'b0110011: begin  // 常规指令 add,sub,or,slt
                branch <= 1'b0;  // 不进行分支操作
                memread <= 1'b0;  // 不进行存储器读操作
                memtoreg <= 1'b0;  // 按特定方式(此处具体由整体设计决定)写回寄存器堆
                ALUop <= 2'b11;  // 设置ALU操作码,指示ALU执行对应操作
                memwrite <= 1'b0;  // 不进行存储器写操作
                ALUsrc <= 1'b0;  // ALU操作数来源选择(这里是从寄存器获取)
                regwrite <= 1'b1;  // 将运算结果写回寄存器堆
            end
            7'b0010011: begin  // 立即数相关指令 addi,ori,slti
                branch <= 1'b0;
                memread <= 1'b0;
                memtoreg <= 1'b0;
                ALUop <= 2'b10;
                memwrite <= 1'b0;
                ALUsrc <= 1'b1;  // ALU操作数来源选择立即数
                regwrite <= 1'b1;
            end
            7'b0000011: begin  // lw
                branch <= 1'b0;
                memread <= 1'b1;  // 需要进行存储器读操作
                memtoreg <= 1'b1;  // 按对应方式写回寄存器堆
                ALUop <= 2'b10;
                memwrite <= 1'b0;
                ALUsrc <= 1'b1;
                regwrite <= 1'b1;
            end
            7'b0100011: begin  // sw
                branch <= 1'b0;
                memread <= 1'b0;
                memtoreg <= 1'b0;
                ALUop <= 2'b10;
                memwrite <= 1'b1;  // 需要进行存储器写操作
                ALUsrc <= 1'b1;
                regwrite <= 1'b0;  // 不需要写回寄存器堆(因为是存储操作)
            end
            7'b1100011: begin  // beq
                branch <= 1'b1;  // 进行分支操作
                memread <= 1'b0;
                memtoreg <= 1'b0;
                ALUop <= 2'b01;
                memwrite <= 1'b0;
                ALUsrc <= 1'b0;
                regwrite <= 1'b0;
            end
            default: begin  // 对于其他未明确匹配的指令类型
                branch <= 1'b0;
                memread <= 1'b0;
                memtoreg <= 1'b0;
                ALUop <= 2'b00;
                memwrite <= 1'b0;
                ALUsrc <= 1'b0;
                regwrite <= 1'b0;
            end
        endcase
    end
    
//    always @(*) begin
//        $display("ALU_src ", ALUsrc);
//    end
endmodule
3.7 ALU信号控制单元设计

3.7.1 结构与功能说明

​ 本模块实现了将指令值解析成更具体的ALU运算的功能。根据先前得到的ALUop信号和原指令值,产生ALU控制信号control_signal

3.7.2 接口列表

​ 以下为该模块主要接口列表:

端口名称类型位宽说明
ALUopinput1在主控单元产生的控制信号
instructioninput32原指令值
control_signalinput4输出ALU控制信号

3.7.3 代码实现

ALU_control.v

`timescale 1ns / 1ps
module ALU_control(
    input wire [1:0] ALUop,
    input wire [31:0] instruction,
    output reg [3:0] control_signal
);

    always @(ALUop or instruction) begin
        case (ALUop)
            2'b00:
                control_signal = 4'b0000; 
            2'b01:
                control_signal = 4'b1000;  // beq
            2'b10:
                case (instruction[14:12])  // funct3 
                    3'b010:
                        if (instruction[4] == 0)
                            control_signal = 4'b0010;  // lw, sw
                        else
                            control_signal = 4'b0110;  // slti
                    3'b000:
                        control_signal = 4'b0000;  // addi
                    3'b110:
                        control_signal = 4'b0011;  // ori
                    default:
                        control_signal = 4'b1111;  // 默认合理值
                endcase
            2'b11:
                case (instruction[14:12])  // funct3 
                    3'b000:
                        if (instruction[30] == 0)
                            control_signal = 4'b0000;  // add
                        else
                            control_signal = 4'b0001;  // sub
                    3'b110:
                        control_signal = 4'b0011;  // or
                    3'b011:
                        control_signal = 4'b0110;  // slt
                    default:
                        control_signal = 4'b1111;  // 默认合理值
                endcase
        endcase
    end

//    always @(*) begin
//        $display("At time %t", $time);
//        $display("control_signal", control_signal);
//    end
    
endmodule
3.8 算术逻辑单元(ALU)设计

3.8.1 结构与功能说明

​ 本模块实现了ALU的核心运算功能。即根据输入的ALU控制信号,选择对应的运算种类进行运算,并输出运算结果、lw/sw指令的内存地址和分支控制信号ALU_zero。

下面是ALU的主要功能说明:

  • 算术运算:ALU执行所有基本的算术运算,包括addaddisub

  • 逻辑运算:除了算术运算外,ALU还能执行逻辑运算,本项目中只需要实现orori

  • 比较运算:ALU可以比较两个数值的大小,支持等于、不等于、大于、小于等比较操作。比较结果通常用于程序流程控制,如循环和条件跳转,本项目中,有sltslti判断小于指令和beq的相等时分支指令都基于此实现。

  • 内存访问和处理:在一些设计中,ALU也参与内存数据的传输操作,如数据之间的复制和修改。虽然这些功能可能更多地由其他CPU组件处理,但ALU在这方面仍然发挥作用(如计算swlw指令相关的内存地址)。

3.8.2 接口列表

​ 以下为该模块主要接口列表:

端口名称类型位宽说明
ALU_srcinput1控制第二个运算值是来自立即数还是寄存器
contrl_signalinput4ALU的控制信号
read_data1input32输入ALU的1号数据
read_data2input32输入ALU的2号数据
immediateinput32输入ALU的立即数
ALU_resultoutput32运算结果
mem_adroutput32由ALU计算出来的lw/sw指令的内存地址
ALU_zerooutput1判断是否为0的信号,用于决定beq指令是否分支

3.8.3 代码实现

ALU.v

`timescale 1ns / 1ps
module ALU(
    input wire ALU_src,
    input wire [3:0] control_signal,
    input wire [31:0] read_data1,
    input wire [31:0] read_data2,
    input wire [31:0] immediate,
    output reg [31:0] ALU_result,
    output reg [31:0] mem_adr,
    output reg ALU_zero
);

    reg [31:0] ALU_data2;

//    always @(*) begin
//        $display("ALU_src ", ALU_src);
//    end

//    always @(*) begin
//        $display("control ", control_signal);
//        $display("read_data1 ", read_data1);
//        $display("ALU_data2 ", ALU_data2);
//        $display("ALU_result ", ALU_result);
//    end    

    always @(*) begin
        if (ALU_src)
            ALU_data2 = immediate;
        else
            ALU_data2 = read_data2;
    end

    always @(read_data1 or ALU_data2 or control_signal) begin
        case (control_signal)
            4'b0000:  ALU_result <= read_data1 + ALU_data2;    // add, addi
            4'b0001:  ALU_result <= read_data1 - ALU_data2;    // sub
            4'b0011:  ALU_result <= read_data1 | ALU_data2;   // ori or
            4'b0110:  ALU_result <= (read_data1 < ALU_data2)? 32'd1 : 32'd0; // slt slti
            4'b0010: begin  // 针对lw和sw指令,计算访存地址并存入mem_adr
                mem_adr <= read_data1 + ALU_data2;    
                ALU_result <= read_data1 + ALU_data2;  // 可以根据需求决定是否同时更新ALU_result,这里假设同时更新
            end
            default:begin
                ALU_result = 32'd0;
                mem_adr = 32'b0;
            end
        endcase
    end

    always @(*) begin
        case (control_signal)
            4'b1000:
                ALU_zero = (read_data1 == ALU_data2)? 1'b1 : 1'b0;    // beq
            default:ALU_zero = 1'b0;
        endcase
    end

endmodule
3.9 CPU顶层实现单元设计

3.9.1 结构与功能说明

​ 这个模块是设计的顶层文件,相当于C语言中的main函数。需要按照先前的逻辑电路图来编写。将前面编写的8个模块进行实例化,并且将各个模块的输出与需要其作为输入的模块端口进行连接,从而完成整个CPU的功能实现编写。

3.9.2 接口列表

​ 以下为该模块主要接口列表:

端口名称类型位宽说明
clkinput1从外部输入的时钟信号
resetinput1从外部输入的复位信号

3.9.3 代码实现

CPU.v

`timescale 1ns / 1ps
module CPU(
    input wire clk,
    input wire reset,
    output wire [31:0] PC,
    output wire [31:0] instruction,
    output wire [31:0] rs1_data,
    output wire [31:0] rs2_data,
    output wire [31:0] imm_extended,
    output wire [31:0] ALU_result,
    output wire [31:0] mem_adr,
    //  内存值和寄存器值查看(由于测试用例都是单字节数据,因此简化观察,用8位来输出)
    output wire [7:0] DM_4,
    output wire [7:0] DM_12,
    output wire [7:0] X0,
    output wire [7:0] X1,
    output wire [7:0] X2,
    output wire [7:0] X3,
    output wire [7:0] X4,
    output wire [7:0] X5,
    output wire [7:0] X6,
    output wire [7:0] X7,
    output wire [7:0] X8
);

//    wire [31:0] PC;
//    wire [31:0] instruction;
//    wire [31:0] rs1_data;
//    wire [31:0] rs2_data;
//    wire [31:0] imm_extended;
//    wire [31:0] ALU_result;
    wire [31:0] rd;
    wire ALU_zero;
    wire branch;
    wire memread;
    wire memtoreg;
    wire [1:0] ALUop;
    wire [3:0 ]  control_signal ;
    wire memwrite;
    wire ALUsrc;
    wire regwrite;

//    always @(*) begin
//        $display("ALU_src ", ALUsrc);
//    end
    
    // 实例化各个模块
    PC_module pc_module(
       .clk(clk),
       .reset(reset),
       .branch_offset(imm_extended),  // 根据指令情况传递合适的偏移量,这里假设是立即数扩展后的结果用于分支偏移
       .beq_taken(branch & ALU_zero),  // beq指令执行条件为branch信号有效且ALU结果为零
       .PC(PC)
    );

    PM_module pm_module(
        .PC(PC),
        .instruction(instruction)
    );

    Register_file register_file(
        .clk(clk),
        .reset(reset),
        .rs1_addr(instruction[19:15]),
        .rs2_addr(instruction[24:20]),
        .rd_addr(instruction[11:7]),
        .wr_data(ALU_result), 
        .mem_data(rd),
        .reg_write_enable(regwrite),
        .mem_to_reg(memtoreg),
        .rs1_data(rs1_data),
        .rs2_data(rs2_data),
        .X0(X0),
        .X1(X1),
        .X2(X2),
        .X3(X3),
        .X4(X4),
        .X5(X5),
        .X6(X6),
        .X7(X7),
        .X8(X8)
    );

    ImmExtend imm_extend(
        .instruction(instruction),
        .imm_extended(imm_extended)
    );

    control ctrl(
        .instruction(instruction),
        .branch(branch),
        .memread(memread),
        .memtoreg(memtoreg),
        .ALUop(ALUop),
        .memwrite(memwrite),
        .ALUsrc(ALUsrc),
        .regwrite(regwrite)
    );

    ALU_control alu_ctrl(
        .ALUop(ALUop),
        .instruction(instruction),
        .control_signal(control_signal)
    );

    ALU alu(
        .ALU_src(ALUsrc),
        .control_signal(control_signal),  // 此处control_signal需要来自ALUcontrol模块
        .read_data1(rs1_data),
        .read_data2(rs2_data),
        .immediate(imm_extended),
        .ALU_result(ALU_result),
        .ALU_zero(ALU_zero),
        .mem_adr(mem_adr)
    );

    DM_module dm_module(
        .clk(clk),
        .address(mem_adr),
        .write_data(rs2_data),
        .mem_write_enable(memwrite),
        .mem_read_enable(memread),
        .read_data(rd),
        .DM_4(DM_4),
        .DM_12(DM_12)
    );
    
endmodule

四、仿真过程及其结果分析

现有如下指令存储器和数据存储器数据测试用例:

PM初始值:

PM地址指令序号指令(小端排序合并后)对应指令说明
3~00x00800093addi X1, X0, 0x8寄存器 X0 机器代码:值恒为 0,X1 = 8
7~40x0040a103lw X2, 4(X1)X2 = 4
11~80x002081b3add X3, X1, X2X3 = 12 (0xc)
15~120x40118233sub X4, X3, X1X4 = 4
19~160x0040e2b3or X5, X1, X4X5 = 0xc
23~200x0012e313ori X6, X5, 1X6 = 0xd
27~240x00612023sw X6, 0(X2)dm(0x4) = 0xd
31~280x004123b3slt X7, X2, X4X7 = 0
35~320x00812413slti X8, X2, 8X8 = 1
39~360xfe518ae3beq X3, X5, - 12PC = PC - 12

DM初始值:

DM地址(小端模式)指令代码
00x00
10x01
20x02
30x03
40x04
50x05
60x06
70x07
80x08
90x00
100x00
110x00
120x04
130x00
140x00
150x00

根据此写一个仿真平台CPU_tb:

CPU_tb.v

`timescale 1ns / 1ps
module CPU_tb;

    // 输入信号
    reg clk;
    reg reset;

    // 输出信号
    wire [31:0] PC;
    wire [31:0] instruction;
    wire [31:0] rs1_data;
    wire [31:0] rs2_data;
    wire [31:0] imm_extended;
    wire [31:0] ALU_result;
    wire [31:0] mem_adr;

    // 样例寄存器和内存值
    wire [7:0] DM_4;
    wire [7:0] DM_12;
    wire [7:0] X0;
    wire [7:0] X1;
    wire [7:0] X2;
    wire [7:0] X3;
    wire [7:0] X4;
    wire [7:0] X5;
    wire [7:0] X6;
    wire [7:0] X7;
    wire [7:0] X8;
    
    // 数据存储器和指令存储器模块实例化
    reg [7:0] PM [0:127]; // 44条指令
    reg [7:0] DM [0:127]; // 16个数据存储单元

    // CPU模块实例
    CPU cpu (
        .clk(clk),
        .reset(reset),
        .PC(PC),
        .instruction(instruction),
        .rs1_data(rs1_data),
        .rs2_data(rs2_data),
        .imm_extended(imm_extended),
        .ALU_result(ALU_result),
        .mem_adr(mem_adr),
        .DM_4(DM_4),
        .DM_12(DM_12),
        .X0(X0),
        .X1(X1),
        .X2(X2),
        .X3(X3),
        .X4(X4),
        .X5(X5),
        .X6(X6),
        .X7(X7),
        .X8(X8)
    );

    // 时钟生成
    always begin
        #5 clk = ~clk;  // 时钟周期为10ns
    end

    // 初始化过程
    initial begin
        // 初始化时钟和复位
        clk = 1;
        reset = 1;

        // 初始化指令存储器(PM) - 按照小端格式初始化
        PM[0] = 8'h93; PM[1] = 8'h00; PM[2] = 8'h80; PM[3] = 8'h00; // 0x93008000
        PM[4] = 8'h03; PM[5] = 8'hA1; PM[6] = 8'h40; PM[7] = 8'h00; // 0x03A14000
        PM[8] = 8'hB3; PM[9] = 8'h81; PM[10] = 8'h20; PM[11] = 8'h00; // 0xB3182000
        PM[12] = 8'h33; PM[13] = 8'h82; PM[14] = 8'h11; PM[15] = 8'h40; // 0x33211840
        PM[16] = 8'hB3; PM[17] = 8'hE2; PM[18] = 8'h40; PM[19] = 8'h00; // 0xB23E4000
        PM[20] = 8'h13; PM[21] = 8'hE3; PM[22] = 8'h12; PM[23] = 8'h00; // 0x13E31200
        PM[24] = 8'h23; PM[25] = 8'h20; PM[26] = 8'h61; PM[27] = 8'h00; // 0x23012000
        PM[28] = 8'hB3; PM[29] = 8'h23; PM[30] = 8'h41; PM[31] = 8'h00; // 0x3B234100
        PM[32] = 8'h13; PM[33] = 8'h24; PM[34] = 8'h81; PM[35] = 8'h00; // 0x13248100
        PM[36] = 8'hE3; PM[37] = 8'h8A; PM[38] = 8'h51; PM[39] = 8'hFE; // 0xE38A51FE
        PM[40] = 8'h00; PM[41] = 8'h00; PM[42] = 8'h00; PM[43] = 8'h00; // 0x00000000 (No-op)

        // 初始化数据存储器(DM) - 小端排序数据
        DM[0] = 8'h00;
        DM[1] = 8'h01;
        DM[2] = 8'h02;
        DM[3] = 8'h03;
        DM[4] = 8'h04;
        DM[5] = 8'h05;
        DM[6] = 8'h06;
        DM[7] = 8'h07;
        DM[8] = 8'h08;
        DM[9] = 8'h00;
        DM[10] = 8'h00;
        DM[11] = 8'h00;
        DM[12] = 8'h04;
        DM[13] = 8'h00;
        DM[14] = 8'h00;
        DM[15] = 8'h00;

        // 将指令存储器和数据存储器初始化值传递给CPU模块
        cpu.pm_module.PM[0] = PM[0];
        cpu.pm_module.PM[1] = PM[1];
        cpu.pm_module.PM[2] = PM[2];
        cpu.pm_module.PM[3] = PM[3];
        cpu.pm_module.PM[4] = PM[4];
        cpu.pm_module.PM[5] = PM[5];
        cpu.pm_module.PM[6] = PM[6];
        cpu.pm_module.PM[7] = PM[7];
        cpu.pm_module.PM[8] = PM[8];
        cpu.pm_module.PM[9] = PM[9];
        cpu.pm_module.PM[10] = PM[10];
        cpu.pm_module.PM[11] = PM[11];
        cpu.pm_module.PM[12] = PM[12];
        cpu.pm_module.PM[13] = PM[13];
        cpu.pm_module.PM[14] = PM[14];
        cpu.pm_module.PM[15] = PM[15];
        cpu.pm_module.PM[16] = PM[16];
        cpu.pm_module.PM[17] = PM[17];
        cpu.pm_module.PM[18] = PM[18];
        cpu.pm_module.PM[19] = PM[19];
        cpu.pm_module.PM[20] = PM[20];
        cpu.pm_module.PM[21] = PM[21];
        cpu.pm_module.PM[22] = PM[22];
        cpu.pm_module.PM[23] = PM[23];
        cpu.pm_module.PM[24] = PM[24];
        cpu.pm_module.PM[25] = PM[25];
        cpu.pm_module.PM[26] = PM[26];
        cpu.pm_module.PM[27] = PM[27];
        cpu.pm_module.PM[28] = PM[28];
        cpu.pm_module.PM[29] = PM[29];
        cpu.pm_module.PM[30] = PM[30];
        cpu.pm_module.PM[31] = PM[31];
        cpu.pm_module.PM[32] = PM[32];
        cpu.pm_module.PM[33] = PM[33];
        cpu.pm_module.PM[34] = PM[34];
        cpu.pm_module.PM[35] = PM[35];
        cpu.pm_module.PM[36] = PM[36];
        cpu.pm_module.PM[37] = PM[37];
        cpu.pm_module.PM[38] = PM[38];
        cpu.pm_module.PM[39] = PM[39];
        cpu.pm_module.PM[40] = PM[40];
        cpu.pm_module.PM[41] = PM[41];
        cpu.pm_module.PM[42] = PM[42];
        cpu.pm_module.PM[43] = PM[43];

        cpu.dm_module.DM[0] = DM[0];
        cpu.dm_module.DM[1] = DM[1];
        cpu.dm_module.DM[2] = DM[2];
        cpu.dm_module.DM[3] = DM[3];
        cpu.dm_module.DM[4] = DM[4];
        cpu.dm_module.DM[5] = DM[5];
        cpu.dm_module.DM[6] = DM[6];
        cpu.dm_module.DM[7] = DM[7];
        cpu.dm_module.DM[8] = DM[8];
        cpu.dm_module.DM[9] = DM[9];
        cpu.dm_module.DM[10] = DM[10];
        cpu.dm_module.DM[11] = DM[11];
        cpu.dm_module.DM[12] = DM[12];
        cpu.dm_module.DM[13] = DM[13];
        cpu.dm_module.DM[14] = DM[14];
        cpu.dm_module.DM[15] = DM[15];

        // 激活复位信号
        #5 reset = 0;

        // 仿真运行几个时钟周期
        #100;  // 运行200ns,足够执行指令

        // 结束仿真
        #20;
        $finish;
    end

endmodule

在vivado软件上进行behavioral simulation仿真,得到图2所示的仿真波形图。

在这里插入图片描述

  • 图2.仿真全过程总波形图

下面对每条指令的实现验证进行逐一分解说明:

1. addi X1, X0, 0x8指令实现验证

​ 如图3所示,此时PC值为0,instruction值为0x00800093,解析后为addi X1, X0, 0x8指令,寄存器X0的值0读进rs1_data中,立即数0x8读进imm_extended中,ALU运算结果为8,结果将要写入寄存器X1中,代表的运算为X1=X0+8=0+8=8,发现在下一步操作前X1的值变成了8,说明这条指令正确执行了。

在这里插入图片描述

  • 图3.addi X1, X0, 0x8指令波形图

2. lw X2, 4(X1)指令实现验证

​ 如图4所示,此时PC值为4,instruction值为0x0004a103,解析后为lw X2, 4(X1)指令,寄存器X1的值8读进rs1_data中,立即数0x4读进imm_extended中,内存地址mem_adr运算结果为12,代表的运算为4(X1)=4+8=12,而图中DM[12]的值是4,所以这条指令应该把4赋值给X2,发现在下一步操作前X2值变成了4,说明这条指令正确执行了。

在这里插入图片描述

  • 图4.lw X2, 4(X1)指令波形图

3. add X3, X1, X2指令实现验证

​ 如图5所示,此时PC值为8,instruction值为0x002081b3,解析后为add X3, X1, X2指令,寄存器X1的值8读进rs1_data中,寄存器X2的值4读进rs2_data中,ALU运算结果为12,结果将要写入寄存器X3中,代表的运算为X3=X1+X2=8+4=12,发现在下一步操作前X3的值变成了12,说明这条指令正确执行了。

在这里插入图片描述

  • 图5.add X3, X1, X2指令波形图

4. sub X4, X3, X1指令实现验证

​ 如图6所示,此时PC值为12,instruction值为0x40118233,解析后为sub X4, X3, X1指令,寄存器X3的值12读进rs1_data中,寄存器X1的值8读进rs2_data中,ALU运算结果为4,结果将要写入寄存器X4中,代表的运算为X4=X3-X1=12-8=4,发现在下一步操作前X4的值变成了4,说明这条指令正确执行了。

在这里插入图片描述

  • 图6sub X4, X3, X1指令波形图

5. or X5, X1, X4指令实现验证

​ 如图7所示,此时PC值为16,instruction值为0x0040e2b3,解析后为or X5, X1, X4指令,寄存器X1的值8读进rs1_data中,寄存器X4的值4读进rs2_data中,ALU运算结果为12,结果将要写入寄存器X5中,代表的运算为X5=X1|X4=8|4=12,发现在下一步操作前X5的值变成了12,说明这条指令正确执行了。

在这里插入图片描述

  • 图7.or X5, X1, X4指令波形图

6. ori X6, X5, 1指令实现验证

​ 如图8所示,此时PC值为20,instruction值为0x0012e313,解析后为ori X6, X5, 1指令,寄存器X5的值12读进rs1_data中,立即数0x1读进imm_extended中,ALU运算结果为13,结果将要写入寄存器X6中,代表的运算为X6=X5|1=12|1=13,发现在下一步操作中的X6的值变成了13,说明这条指令正确执行了。

在这里插入图片描述

  • 图8.ori X6, X5, 1指令波形图

7. sw X6, 0(X2)指令实现验证

​ 如图9所示,此时PC值为24,instruction值为0x00612023,解析后为sw X6, 0(X2)指令,寄存器X2的值4读进rs1_data中,立即数0x0读进imm_extended中,内存地址mem_adr运算结果为4,代表的运算为0(X2)=0+4=4,这条指令应该把X6的值13写进内存DM[4]中,发现在下一步操作前内存DM[4]值变成了13,说明这条指令正确执行了。

在这里插入图片描述

  • 图9.sw X6, 0(X2)指令波形图

8. slt X7, X2, X4指令实现验证

​ 如图10所示,此时PC值为28,instruction值为0x004123b3,解析后为slt X7, X2, X4指令,寄存器X2的值4读进rs1_data中,寄存器X4的值4读进rs2_data中,ALU运算结果为0,结果将要写入寄存器X7中,代表的运算为X7=X2<X4=4<4=0,发现在下一步操作前X7的值变成了0,说明这条指令正确执行了。

在这里插入图片描述

  • 图10.slt X7, X2, X4指令波形图

9. slti X8, X2, 8指令实现验证

​ 如图11所示,此时PC值为32,instruction值为0x00812413,解析后为slti X8, X2, 8指令,寄存器X2的值4读进rs1_data中,立即数0x8读进imm_extended中,ALU运算结果为1,结果将要写入寄存器X8中,代表的运算为X8=X2<8=4<8=1,发现在下一步操作中的X8的值变成了1,说明这条指令正确执行了。

在这里插入图片描述

  • 图11.slti X8, X2, 8指令波形图

10. beq X3, X5, - 12指令实现验证

​ 如图12所示,此时PC值为36,instruction值为0xfe518ae3,解析后为 beq X3, X5, - 12指令,寄存器X3的值12读进rs1_data中,寄存器X5的值12读进rs2_data中,两值相等,立即数-12读进imm_extended中,则需要跳转到PC-12的指令地址,发现在下一步中的PC的值变成了前面执行过的24,instruction值跳转成前面执行过的0x00612023,并且继续一条一条执行下去之至仿真结束,说明这条指令正确执行了。

在这里插入图片描述

  • 图12. beq X3, X5, - 12指令波形图

五、实验体会

​ 在本次单周期 CPU 设计实验中,我深入地探索了计算机底层硬件的工作原理,从电路设计到代码编写,再到仿真验证,整个过程充满挑战但收获颇丰。

​ 在实验设计阶段,我将 CPU 功能进行细致分解,从取指、译码、执行、访存到写回,每一步都需要精心设计与严谨逻辑。这让我对 CPU 的工作流程有了极为清晰的认识,理解了各个部件如何协同运作以完成复杂的指令任务。设计各个模块时,如寄存器堆、PC 取指模块、存储器模块等,不仅要考虑其独立功能的实现,还需关注模块间的接口与交互,这锻炼了我的系统设计与整合能力。

​ 编写 Verilog 代码实现各个模块是一项艰巨任务。在代码编写过程中,我需要精确地运用硬件描述语言来描述每个模块的功能与行为。例如,在控制信号生成模块中,要依据不同指令的操作码与功能码准确地生成相应控制信号,这要求我对指令格式和操作码有深入理解。通过不断调试与修改代码,我逐渐掌握了 Verilog 语言在硬件设计中的应用技巧,也提升了自己的代码编写能力与逻辑思维能力。

​ 仿真验证环节至关重要。通过编写测试用例并在 Vivado 软件中进行仿真,我能够直观地观察到 CPU 内部各个信号的变化以及指令的执行过程。当看到每条指令都能按照预期正确执行,各个寄存器和存储器的值都准确无误时,我获得了极大的成就感。在这个过程中,我学会了如何分析仿真波形,从波形中找出潜在问题并追溯到代码中的错误根源,这极大地提高了我的问题排查与解决能力。

​ 然而,实验过程并非一帆风顺。我在控制信号的生成与传递方面遇到了不少问题,由于不同模块间的信号依赖关系复杂,一旦某个控制信号出现错误,就会导致整个 CPU 功能出错。任何一个小的失误都可能引发连锁反应,导致整个系统无法跑出正常的仿真结果。

​ 通过本次实验,我不仅在专业知识和技能上取得了显著进步,更培养了自己的耐心、细心和解决问题的能力。虽然本项目只需要完成仿真验证,但我依然深刻明白硬件设计是一个严谨而细致的过程,需要对每个细节都精益求精。这次实验也让我对计算机底层世界有了更深刻、更直观的认识与理解。


http://www.kler.cn/a/457016.html

相关文章:

  • 蓝牙|软件 Qualcomm S7 Sound Platform开发系列之初级入门指南
  • 计算机网络基础知识(7)中科大郑铨老师笔记
  • 【漫话机器学习系列】028.CP
  • Unity SpriteAtlasManager.atlasRequested趟坑
  • hive-sql 连续登录五天的用户
  • 操作系统论文导读(八):Schedulability analysis of sporadic tasks with multiple criticality specifications——具有多个
  • 简单的skywalking探针加载原理学习
  • apifox
  • Vulnhub靶场morpheus获得shell攻略
  • spring url匹配
  • WordPress Elementor Page Builder 插件存在任意文件读取漏洞(CVE-2024-9935)
  • python编译为可执行文件
  • 读书笔记-《乡下人的悲歌》
  • 【Rust自学】7.4. use关键字 Pt.2 :重导入与换国内镜像源教程
  • vite 多环境变量配置
  • 安装 PostgreSQL 数据库的教程
  • 新品:SA628F39大功率全双工音频传输模块
  • systemverilog语法:assertion summary
  • 前端node.js
  • SpringBoot + vue 管理系统
  • 未来具身智能的触觉革命!TactEdge传感器让机器人具备精细触觉感知,实现织物缺陷检测、灵巧操作控制
  • SQL中的窗口函数
  • 【HarmonyOS之旅】ArkTS语法(一)
  • PDF书籍《手写调用链监控APM系统-Java版》第3章 配置文件系统的建立
  • 机器人C++开源库The Robotics Library (RL)使用手册(二)
  • 前端开发中的常用工具函数解析与应用