跳转至

单周期 CPU 数据通路设计

1838 个字 337 行代码 预计阅读时间 10 分钟

Abstract

计算机系统 Ⅰ lab5-1 实验报告(2022.05.20 ~ 2022.06.10)

Warning

仅供学习参考,请勿抄袭

实验内容

  • 完成 32 位单周期 CPU 数据通路设计
    • 实现 lw sw bne beq jal lui addi add slt slti 指令
  • 根据提供的测试文件进行仿真测试
  • 根据提供的测试环境进行上班验证
  • bonus:指令扩展
    • 实现 andi ori and or sll
    • 实现 xori slli srli srl
    • 实现 auipc sltu jalr

数据通路设计

明确已给代码

CPU 相关部分

和单周期 CPU 本身有关的核心代码有

Control.v

通过 Control.edf 使用,在仿真时使用 Control_sim.v 文件中的 Control。功能是 CPU 的控制部分,其 Control 模块的输入输出部分:

input   [6:0]   op_code;
input   [2:0]   funct3;
input           funct7_5;
output  [1:0]   pc_src;
output          reg_write;
output          alu_src_b;
output  [3:0]   alu_op;
output  [1:0]   mem_to_reg;
output          mem_write;
output          branch;
output          b_type;
输入为 RISC-V 32 位指令的三个部分,op_code 为低 7 位的操作码,funct3 为 inst[14:12] 的功能部分,funct7_5 为 funct7 的第五位也就是 inst[30]

输出为译码得到的一些结果:

  • pc_src:pc 来源,00 来自 pc+401 来自 JALR10 来自 JAL
  • reg_write:是否写入寄存器组
  • alu_src_b:ALUsrc,0 表示 ALU 的第二个操作数来自寄存器,1 表示来自立即数
  • alu_op:ALU 进行的操作符,在 AluOp.vh 中定义
  • mem_to_reg:写回寄存器的数据来源,00 写回数据来自 ALU01 来自立即数、10 来自 pc+411 来自 data memory
  • mem_write:是否写入 data memory0 表示读,1 表示写
  • branch:指令是否是 branch 分支操作,1 表示是,0 不是
  • b_type:分支操作类型,0 表示 bne1 表示 beq
SCPU.v

单周期 CPU 结构文件,用于连接 CPU 的控制部分和数据通路部分,SCPU 模块输入输出部分:

input           clk,
input           rst,
input   [31:0]  inst,
input   [31:0]  data_in,  // data from data memory
input   [4:0]   debug_reg_addr,
output  [31:0]  addr_out, // data memory address
output  [31:0]  data_out, // data to data memory
output  [31:0]  pc_out,   // connect to instruction memory
output          mem_write,
output  [31:0]  debug_reg
输入有单周期 CPU 时钟 clk,复位信号 rst,指令 inst,从 data memory 获取的数据 data_in,用于上板时 debug 输出寄存器值的寄存器地址 debug_reg_addr。

输出有要操作 data memory 的地址 addr_out,要传给 data memory 的数据 data_out,接入 instruction memory pc 指针值 pc_out,是否写入 data memory mem_write,以及得到的 debug 要显示的寄存器值 debug_reg

模块中间将 inst 拆分连接到了 Control 模块,再将 Control 模块的输出连接到 Datapath 的输入,Datapath 的输出作为 SCPU 的输出。

Core.v

SCPU data memoryip 核中的 RAM)和 instruction memoryip 核中的 ROM)连接,并处理 SCPU 的时钟和 data memory 的时钟,再提供一些用于 debug 的输入输出

其它文件

  • Top.‌sv

    用于上板验证的顶层模块,其中将 Core 连接到了 IO_Manager 中来为 FPGA 板子提供输入输出的控制。

  • IO_Manager.sv

    FPGA 板子提供输入输出的模块,其中处理了开发版上的按键、开关,以及七段管的输出。

  • Core_tb.sv

    Core 模块的 test bench,进行仿真测试的时候将这个文件设为需要仿真的文件。

  • AluOp.vh

    里面为 ALU 要进行的运算提供了变量名,实现 ALU 时要按照里面的规定来执行运算。

明确目标

完成数据通路设计的目标有以下几个:

  • 编写 ALU 模块,完成 AluOp.vh 文件中规定的运算
  • 编写寄存器 Regs 模块,存储 31 32 位寄存器(第 0 个是 0,处理寄存器的读写操作
  • 编写一些 Mux 模块用于数据通路中的多路选择
  • 编写 ImmGen 模块,用来从指令中解析出立即数
  • 编写 Datapath 模块,连接起数据通路
  • SCPU 模块中正确连接 Control Datapath(已经做好)
  • Core 模块中连接 SCPU RAM ROM

模块编写与连接

ALU

输入是两个 32 位数 a b 和进行的操作 alu_op,输出是结果和 zero 标志,case alu_op 然后分别实现就可以:

`timescale 1ns / 1ps

module ALU (
    input       [31:0]  a,
    input       [31:0]  b,
    input       [3:0]   alu_op,
    output reg  [31:0]  res,
    output              zero
);
    `include "AluOp.vh"
    always @(*) begin
        case (alu_op)
            ADD: res <= a + b;
            SUB: res <= a - b;
            SLL: res <= a << b;
            SLT: begin
                if (a[31] == 0 && b[31] == 1) res <= 0;
                else if (a[31] == 1 && b[31] == 0) res <= 1;
                else if (a[31] == b[31]) begin
                    if (a[30:0] < b[30:0]) res <= 1;
                    else res <= 0;
                end
            end
            SLTU: begin
                if (a < b) res <= 1;
                else res <= 0;
            end
            XOR: res <= a ^ b;
            SRL: res <= a >> b;
            SRA: res <= a >>> b;
            OR:  res <= a | b;
            AND: res <= a & b;
            default: res = 0;
        endcase
    end
    assign zero = (a-b) ? 1'b0 : 1'b1;
endmodule

寄存器 Regs

lab pdf 文件中已经给出了寄存器组部分的设计,只需要添加一下对于 debug_reg_addr debug_reg 的输入输出即可:

`timescale 1ns / 1ps

module Regs (
    input           clk,
    input           rst,
    input           we,
    input   [4:0]   read_addr_1,
    input   [4:0]   read_addr_2,
    input   [4:0]   write_addr,
    input   [31:0]  write_data,
    input   [4:0]   debug_reg_addr,
    output  [31:0]  read_data_1,
    output  [31:0]  read_data_2,
    output  [31:0]  debug_reg
);
    integer i;
    reg [31:0] register [1:31]; // x1 - x31, x0 keeps zero

    assign read_data_1 = (read_addr_1 == 0) ? 0 : register[read_addr_1]; // read
    assign read_data_2 = (read_addr_2 == 0) ? 0 : register[read_addr_2]; // read
    assign debug_reg = (debug_reg_addr == 0) ? 0 : register[debug_reg_addr];

    always @(posedge clk or posedge rst) begin
        if (rst == 1) for (i = 1; i < 32; i = i + 1) register[i] <= 0; // reset
        else if (we == 1 && write_addr != 0) register[write_addr] <= write_data;
    end
endmodule

多路选择器 Mux

首先实现普通的 32 2 路选择器 Mux2x32 32 4 路选择器 Mux4x32

`timescale 1ns / 1ps

module Mux2x32 (
    input   [31:0]  I0,
    input   [31:0]  I1,
    input           s,
    output  [31:0]  o
);
    assign o = s ? I1 : I0;
endmodule

module Mux4x32 (
    input   [31:0]  I0,
    input   [31:0]  I1,
    input   [31:0]  I2,
    input   [31:0]  I3,
    input   [1:0]   s,
    output  [31:0]  o
);
    reg [31:0] out;
    always @(*) begin
        case (s)
            2'b00: out <= I0;
            2'b01: out <= I1;
            2'b10: out <= I2;
            2'b11: out <= I3;
        endcase
    end
    assign o = out;
endmodule
然后再实现一个用于处理最后 PC 结果的特殊选择器(即需要判断一下是否有分支以及分支是否成立):
module MuxPC (
    input   [31:0]  I0,         // pc+4
    input   [31:0]  I1,         // JALR 的地址
    input   [31:0]  I2,         // JAL 的地址
    input   [31:0]  I3,         // branch 的地址
    input   [1:0]   s,          // pc_src
    input           branch,     // 是否分支
    input           b_type,     // 0 bne, 1 beq
    input   [31:0]  alu_res,    // alu 的结果,用来判断分支是否成立
    output  [31:0]  o           // 结果的 pc
);
    reg [31:0] out;
    always @(*) begin
        if (branch) begin // 有分支操作
            if (b_type) begin // beq
                if (alu_res == 32'b0)   out <= I3; // 分支
                else                    out <= I0;
            end
            else begin // bne
                if (alu_res == 32'b0)   out <= I0;
                else                    out <= I3; // 分支
            end
        end
        else begin
            case (s)
                2'b00: out <= I0;
                2'b01: out <= I1;
                2'b10: out <= I2;
                2'b11: out <= I3;
            endcase
        end
    end
    assign o = out;
endmodule

解析立即数 ImmGen

先通过一个 Opcodes.vh 文件定义一些要用的指令名和 opcode 的对应以及指令类型的枚举:

//`ifndef OPCODES_H
//`define OPCODES_H
parameter   LW      = 7'b0000011,
            SW      = 7'b0100011,
            ADDI    = 7'b0010011,
            BNE     = 7'b1100011,
            BEQ     = 7'b1100011,
            JAL     = 7'b1101111,
            LUI     = 7'b0110111,
            ADD     = 7'b0110011,
            SLT     = 7'b0110011,
            SLTI    = 7'b0010011,
            ANDI    = 7'b0010011,
            ORI     = 7'b0010011,
            AND     = 7'b0110011,
            OR      = 7'b0110011,
            SLL     = 7'b0110011,
            XORI    = 7'b0010011,
            SLLI    = 7'b0010011,
            SRLI    = 7'b0010011,
            SRL     = 7'b0110011,
            AUIPC   = 7'b0010111,
            SLTU    = 7'b0110011,
            JALR    = 7'b1100111;

parameter   R   = 3'b000,
            I   = 3'b001,
            S   = 3'b010,
            B   = 3'b011,
            U   = 3'b100,
            J   = 3'b101;
//`endif
然后再编写一个 ImmGen 模块来识别 opcode、分类指令、获取立即数,对于几种指令,获取的立即数分别是:
  • R 型:20 个与 inst[31] 相同的符号位扩展比特和 inst[31:20]
  • I 型:同 R
  • S 型:20 个与 inst[31] 相同的符号位扩展比特和 inst[31:25] + inst[11:7]
  • B 型:19 个与 inst[31] 相同的符号位扩展比特和 inst[31] + inst[7] + inst[30:25] + inst[11:8],最后一位补 0(相当于预先进行了分支时的 imm << 1 操作)
  • U 型:inst[31:12] 后面补 12 0
  • J 型:11 个与 inst[31] 相同的符号位扩展比特和 inst[31] + inst[19:12] + inst[20] + inst[30:21],最后一位补 0

根据这些规则以及指令和类型的对应可以写出 ImmGen 类:

`timescale 1ns / 1ps

module ImmGen(
    input   [31:0]  inst,
    output  [31:0]  imm
);
    `include "Opcodes.vh"
    reg [3:0] type;
    reg [31:0] out;
    assign imm = out;
    always @(*) begin
        case (inst[6:0])
            LW:     type <= I;
            SW:     type <= S;
            ADDI:   type <= I;
            BNE:    type <= B;
            BEQ:    type <= B;
            JAL:    type <= J;
            LUI:    type <= U;
            ADD:    type <= R;
            SLT:    type <= R;
            SLTI:   type <= I;
            ANDI:   type <= I;
            ORI:    type <= I;
            AND:    type <= R;
            OR:     type <= R;
            SLL:    type <= R;
            XORI:   type <= I;
            SLLI:   type <= R;
            SRLI:   type <= R;
            SRL:    type <= R;
            AUIPC:  type <= U;
            SLTU:   type <= R;
            JALR:   type <= I;
        endcase
        case (type)
            R: out <= {{20{inst[31]}}, inst[31:20]};
            I: out <= {{20{inst[31]}}, inst[31:20]};
            S: out <= {{20{inst[31]}}, inst[31:25], inst[11:7]};
            B: out <= {{19{inst[31]}}, inst[31], inst[7], inst[30:25], inst[11:8], 1'b0};
            U: out <= {inst[31:12], 12'b0};
            J: out <= {{11{inst[31]}}, inst[31], inst[19:12], inst[20], inst[30:21], 1'b0};
        endcase
    end
endmodule

数据通路 Datapath

根据前面写好的模块以及分析,还有 pdf 给出的数据通路设计图就可以写出数据通路部分的代码:

`timescale 1ns / 1ps

module Datapath(
    input           clk,
    input           rst,
    input   [1:0]   pc_src,     // pc 来源
    input           reg_write,  // 是否写入寄存器
    input           alu_src_b,  // ALUsrc
    input           branch,     // 是否为 branch 类型的指令
    input           b_type,     // branch 类型
    input   [3:0]   alu_op,     // ALU operation
    input   [1:0]   mem_to_reg, // MemtoReg
    input   [31:0]  inst_in,    // 当前指令
    input   [31:0]  data_in,    // data from data memory
    output  [31:0]  addr_out,   // data memory address
    output  [31:0]  data_out,   // data to data memory
    output  [31:0]  pc_out,     // connect to instruction memory
    input   [4:0]   debug_reg_addr,
    output  [31:0]  debug_reg
);
    reg     [31:0]  pc;
    wire    [31:0]  pc_next;
    wire    [31:0]  write_data, read_data_1, read_data_2;
    wire    [31:0]  mux_out_1, alu_result;
    wire            alu_zero;
    wire    [31:0]  imm;
    wire    [31:0]  jmp_addr;

    assign pc_out = pc;
    assign addr_out = alu_result;
    assign data_out = read_data_2;

    always @(posedge clk or posedge rst) begin
        if (rst) begin
            pc <= 32'b0;        // 对 pc 复位
        end
        else begin
            pc <= pc_next;      // 设置 pc 的新值
        end
    end

    Regs regs (     // 连接寄存器组
        .clk(clk),
        .rst(rst),
        .we(reg_write),
        .read_addr_1(inst_in[19:15]),
        .read_addr_2(inst_in[24:20]),
        .write_addr(inst_in[11:7]),
        .write_data(write_data),
        .read_data_1(read_data_1),
        .read_data_2(read_data_2),
        .debug_reg_addr(debug_reg_addr),
        .debug_reg(debug_reg)
    );

    ImmGen immgen ( // 连接 ImmGen,从 inst_in 中获得立即数 imm
        .inst(inst_in),
        .imm(imm)
    );

    Mux2x32 mux2x32 (   // 选择 ALU 第二个输入
        .I0(read_data_2),
        .I1(imm),
        .s(alu_src_b),
        .o(mux_out_1)
    );

    ALU alu (   // 进行 ALU 运算,第一个输入是寄存器,第二个是选择器的结果
        .a(read_data_1),
        .b(mux_out_1),
        .alu_op(alu_op),
        .res(alu_result),
        .zero(alu_zero)
    );

    Mux4x32 mux4x32 (   // 选择写入 data memory 的数据
        .I0(alu_result),
        .I1(imm),
        .I2(pc + 4),
        .I3(data_in),
        .s(mem_to_reg),
        .o(write_data)
    );

    assign jmp_addr = pc + imm; // 得到跳转指令的跳转目标

    MuxPC mux_pc (  // 选择 pc 的新值
        .I0(pc + 4),    // 直接加 4
        .I1(jmp_addr),  // JALR 跳转地址
        .I2(jmp_addr),  // JAL 跳转地址
        .I3(jmp_addr),  // branch 跳转地址
        .s(pc_src),
        .branch(branch),
        .b_type(b_type),
        .alu_res(alu_result),
        .o(pc_next)     // pc 的新值
    );
endmodule
这样就运用了这些模块,将其连接在了一起形成了数据通路。

连接 RAM ROM

直接将提供的 Ram.xci Rom.xci 载入 vivado 就可以生成 ip 核,然后再 Core.v 文件中根据提示连接:

Rom rom_unit (
    .a(pc_out/4),   // 地址输入
    .spo(inst)      // 读数据输出
);

Ram ram_unit (
    .clka(mem_clk),         // 时钟
    .wea(mem_write),        // 是否写数据
    .addra(addr_out),       // 地址输入
    .dina(core_data_out),   // 写数据输入
    .douta(core_data_in)    // 读数据输出
);
Rom 的输入需要将 pc 除 4 因为 Rom 读取的时候每 32 位当作一个元素,而不是每个字节一个元素。

Ram 的时钟要是 mem_clk 而不是 cpu_clk,不然会使 data memory 的输入输出被使用时错开一个周期,剩下的 mem_write、addr_out、core_data_out、core_data_in 都是 Core 模块中写好的从 SCPU 中获得的输出。

最后在 Core 末尾用于 debug 的部分根据提示来输出想要得到的寄存器的值:

assign chip_debug_out3 = debug_reg;

这样整个数据通路的设计和整体的连接就完成了。

仿真测试

vivado 中载入 ip 核的时候直接选择带有要求实现的全部十个指令 lw sw bne beq jal lui addi add slt slti coe 参数文件 lab10_all.coe,与之对应的汇编文件就是 lab10_all.asm

然后选择 Core_tb.sv 为目标进行仿真测试,再通过调整得到全部中间变量(寄存器组值、ALU 输入输出、Mux 输入输出等)得到仿真波形:

以及 pc 和寄存器的波形数据:

通过仔细分析每一条汇编指令与所有值的变化、寄存器的变化可以看出 CPU 按照给出的指令正确运行了。

上板验证

vivado disable Control_sim.v 文件(即使用 Control.v Control.edf)然后通过 Top.sv 中顶层模块生成比特流,写入 FPGA 开发版即可进行上板验证。

通过单步运行,观察 pc、inst、寄存器值可以确定上板也没有任何问题。

Bonus

指令扩展部分放到下一个 lab 中跟随控制部分一起实现,写在下一次 lab 的实验报告中。


最后更新: 2022年11月29日 15:52:39
创建日期: 2022年11月29日 15:52:39
回到页面顶部