写在前面

不知道什么原因, 也可能是因为博主能力有限, 很少找到 ZJU 计算机系统课的资料笔记, 而且由于该课程的 TA 生产力过高 (赞美 TA), 实验部分常常是一年一换, 因此想找到适合学习的资料更为难上加难

比如去年的实验内容应该是以 RV32I 为板子写 CPU, 今年就变成了 RV64I, 同时很多细节都不一样 (怒)

再此留下一份单周期 CPU 的实验报告, 鉴于一些别的原因, 源码暂不开源, 权当抛砖引玉


数据通路设计

根据设计要求, 数据通路图如下

datapath256774a14723cab2.jpg

由于我们设计的是单周期 CPU, 除内存模块, 寄存器模块 Reg[] 和 PC 外, 均可按照组合电路的设计模式设计

Memory

根据数据通路图, 数据走线不可避免地要与内存交流, 因此首先要分析 Memory 接口的结构

由于设计的是单周期 CPU, 所有的操作一定可以在一个时钟周期内完成, 不需要考虑很多, 只需要保持和 Memory 的数据信道的常开即可

需要考虑的是发送 / 接受的读写数据包格式 RrequestBit, RreplyBit, WrequestBit, WreplyBit; 这些格式在之后 core.sv 文件编写的时候需要考虑

RegFile

在 RISCV 架构中, 寄存器组一共有 32 个寄存器(x0 - x31), 其中 x0 始终是 0

always @(posedge clk or posedge rst) begin
if(rst) begin // 重置信号
for(i = 1; i < 32; i++) begin
register[i] <= 0; // 所有寄存器置 0
end
end else begin
if(we) begin // 如果处于写使能状态
if(write_addr != 0) begin
register[write_addr] <= write_data;
// 对非 x0 的寄存器写入
end
end
end
end

assign read_data_1 = register[read_addr_1];
assign read_data_2 = register[read_addr_2];
// 读取寄存器内容

ALU

ALU(算术逻辑单元) 是执行算术计算的模块, 根据 RISCV 架构, ALU 需要支持以下操作

typedef enum logic [3:0] {
ALU_ADD, ALU_SUB, ALU_AND, ALU_OR,
ALU_XOR, ALU_SLT, ALU_SLTU, ALU_SLL,
ALU_SRL, ALU_SRA, ALU_ADDW, ALU_SUBW,
ALU_SLLW, ALU_SRLW, ALU_SRAW, ALU_DEFAULT
} alu_op_enum;

根据各操作符的特点编写 ALU.sv

always_comb begin
case (alu_op)
ALU_ADD: res = a + b;
ALU_SUB: res = a - b;
ALU_AND: res = a & b;
ALU_OR: res = a | b;
ALU_XOR: res = a ^ b;
ALU_SLT: res = $signed(a) < $signed(b) ? 1 : 0;
ALU_SLTU: begin
if (a < b) res = 1;
else res = 0;
end
ALU_SLL: res = (a << b[5: 0]);
ALU_SRL: res = (a >> b[5: 0]);
ALU_SRA: res = ($signed(a) >>> b[5: 0]);
ALU_ADDW: res = {{32{a[31]}}, a[31:0] + b[31:0]};
ALU_SUBW: res = {{32{a[31]}}, a[31:0] - b[31:0]};
ALU_SLLW: res = {{32{a[31]}}, a[31:0] << b[4:0]};
ALU_SRLW: res = {{32{a[31]}}, a[31:0] >> b[4:0]};
ALU_SRAW: res = {{32{a[31]}}, $signed(a[31:0]) >>> b[4:0]};
ALU_DEFAULT: res = 0;
endcase
end

ALU_asel 和 ALU_bsel

ALU_asel 和 ALU_bsel 分别是 ALU 两个操作数的选择器

typedef enum logic [1:0] {
ASEL0, ASEL_REG, ASEL_PC, ASEL3
} alu_asel_op_enum;

typedef enum logic [1:0] {
BSEL0, BSEL_REG, BSEL_IMM, BSEL3
} alu_bsel_op_enum;

ALU_asel 的选择范围是寄存器, PC; 而 ALU_bsel 的选择范围是寄存器, 立即数

再根据数据通路图中, 两者的端口情况编写 ALU_asel 和 ALU_bsel

`include "core_struct.vh"
module ALU_Asel (
input CorePack::data_t a,
input CorePack::alu_asel_op_enum alu_asel_op,
input logic [63: 0] pc,
output CorePack::data_t alu_a
);
import CorePack::*;

always_comb begin
case (alu_asel_op)
ASEL0: alu_a = 0;
ASEL_REG: alu_a = a;
ASEL_PC: alu_a = pc;
ASEL3: alu_a = 0;
endcase
end
endmodule
`include "core_struct.vh"
module ALU_Bsel (
input CorePack::data_t b,
input CorePack::alu_asel_op_enum alu_bsel_op,
input logic [63: 0] imm,
output CorePack::data_t alu_b
);
import CorePack::*;

always_comb begin
case (alu_bsel_op)
ASEL0: alu_b = 0;
ASEL_REG: alu_b = b;
ASEL_PC: alu_b = imm;
ASEL3: alu_b = 0;
endcase
end
endmodule

Imm Gen

立即数生成器通过 immgen_op 来截取 inst 的不同部分, 再经过符号扩展生成一个立即数

typedef enum logic [2:0] {
IMM0, I_IMM, S_IMM, B_IMM,
U_IMM, UJ_IMM, IMM6, IMM7
} imm_op_enum;
  • IMM0: 生成一个 0 立即数
  • I_IMM: 生成一个 I-type 立即数, 区间在 [31: 20], 需要进行符号扩展
  • S_IMM: 生成一个 S-type 立即数, 区间在 [31: 25, 11: 7], 需要进行符号扩展
  • B_IMM: 生成一个 B-type 立即数, 区间在 [31, 7, 30: 25, 11: 8], 在最低位补一个 0(保证为偶数), 需要进行符号扩展
  • U_IMM: 生成一个 U-type 立即数, 区间在 [31: 12], 在高 32 位进行符号扩展, 低 12 位补 0
  • UJ_IMM: 生成一个 UJ-type 立即数, 区间在 [31, 19: 12, 20, 30: 21], 在最低位补一个 0(保证为偶数), 需要进行符号扩展
  • IMM6 和 IMM7 无意义

Cmp

比较器模块用于进行两个操作数的比较, 比较运算符列表如下

typedef enum logic [2:0] {
CMP_NO, CMP_EQ, CMP_NE, CMP_LT,
CMP_GE, CMP_LTU, CMP_GEU, CMP7
} cmp_op_enum;

对应的代码逻辑如下

    case (cmp_op)
CMP_NO: cmp_res = 0;
CMP_EQ: cmp_res = (a == b);
CMP_NE: cmp_res = (a != b);
CMP_LT: begin
if (a[DATA_WIDTH - 1] == 0 && b[DATA_WIDTH - 1] == 1)
cmp_res = 0;
else if (a[DATA_WIDTH - 1] == 1 && b[DATA_WIDTH - 1] == 0)
cmp_res = 1;
else if (a[DATA_WIDTH - 1] == b[DATA_WIDTH - 1])begin
if (a[DATA_WIDTH - 2: 0] < b[DATA_WIDTH - 2: 0])
cmp_res = 1;
else cmp_res = 0;
end
else cmp_res = 0;
end
CMP_GE: begin
if (a[DATA_WIDTH - 1] == 0 && b[DATA_WIDTH - 1] == 1)
cmp_res = 1;
else if (a[DATA_WIDTH - 1] == 1 && b[DATA_WIDTH - 1] == 0)
cmp_res = 0;
else if (a[DATA_WIDTH - 1] == b[DATA_WIDTH - 1])begin
if (a[DATA_WIDTH - 2: 0] >= b[DATA_WIDTH - 2: 0])
cmp_res = 1;
else cmp_res = 0;
end
else cmp_res = 0;
end
CMP_LTU: cmp_res = (a < b);
CMP_GEU: cmp_res = (a >= b);
default: cmp_res = 0;
endcase
end

Instruction Selection

指令选择器的作用是选择从内存中读取到的 64bit 的数据的高 / 低 32bit 作为指令(根据 PC 进行截断)

`include "core_struct.vh"
module InstSelector (
input logic [63: 0] ro_rdata,
input logic [63: 0] pc,
output logic [31: 0] inst
);

always_comb begin
inst = (pc[2] == 1'b0) ? ro_rdata[31: 0] : ro_rdata[63: 32];
end

endmodule

Data Package

为了保证数据在内存中的对齐性, 要通过 Data Package 模块对数据进行打包

根据数据宽度的不同(字节Byte, 半字Halfword, 字word, 双字Doubleword), 和 ALU 的计算结果 alu_res 的低 3bit 对寄存器 dataR2 的数据进行移位

typedef enum logic [2:0] {
MEM_NO, MEM_D, MEM_W, MEM_H,
MEM_B, MEM_UB, MEM_UH, MEM_UW
} mem_op_enum;

Data Mask Generation

为了保证数据的有效性, 要通过 Data Mask Generation 模块对数据进行截取

根据数据宽度的不同(字节Byte, 半字Halfword, 字word, 双字Doubleword), 和 ALU 的计算结果 alu_res 的低 3bit 进行掩码的生成

typedef enum logic [2:0] {
MEM_NO, MEM_D, MEM_W, MEM_H,
MEM_B, MEM_UB, MEM_UH, MEM_UW
} mem_op_enum;

Data Truncation

数据调整模块对从内存中读取的数据, 按照数据位宽进行截断, 扩展和其他操作, 以扩展到 64bit

主要分为有符号数和无符号数

  • 对于有符号数, 按照给定的数据宽度(字节Byte, 半字Halfword, 字word, 双字Doubleword), 从对应的高 / 低相应长度的位置截取数据并作符号扩展
  • 对于无符号数, 截取后直接补零, 也就是作无符号扩展

WbSel

WbSel(写回选择) 模块用于根据给定的写回操作类型(wb_sel 的值), 对 ALU 结果, 内存读取结果, PC 内容进行选择

typedef enum logic [1:0] {
WB_SEL0, WB_SEL_ALU, WB_SEL_MEM, WB_SEL_PC
} wb_sel_op_enum;
  • 对于 ALU 结果, 根据当前的 opcode, 对 ALU 结果进行扩展
    • 只有在 opcode 为寄存器写入 REGW 操作时, 需要进行无符号扩展
  • 对于内存读取结果, 直接回写
  • 对于 PC, 回写 PC + 4 以执行下一条指令

PC 和 NPC

PC(程序计数器)和下一个 NPC(程序计数器)用于计算指令地址的跳转

PC 是一个简单的时序模块, 在每个时钟信号上升沿修改为 NPC 的值

`include "core_struct.vh"
module PC (
input logic clk,
input logic rst,
input logic [63: 0] pc_in,
output logic [63: 0] pc_out
);
always_ff @( posedge clk or posedge rst ) begin : blockName
if(rst) pc_out <= 0;
else pc_out <= pc_in;
end
endmodule

NPC 用于计算 PC 的下一个值, 通过 NPC 选择器 npc_sel 进行选择

  • 当 npc_sel 取 0 时, NPC = PC + 4, 即自动跳转到 4Byte 后的地址
  • 当 npc_sel 取 1 时, 表示根据 ALU 结果计算 NPC 的值, 此时需要根据操作码 opcode 进行选择
    • 当 opcode 取 JALR(间接跳转) 时, 将最低位清 0 后赋值给 NPC, 以确保 NPC 始终是一个偶数
    • 否则, 直接将 ALU 的结果赋值给 NPC
`include "core_struct.vh"
module NPC (
input logic [63: 0] pc_in,
input logic [63: 0] alu,
input logic npc_sel,
input logic [6: 0] opcode,
output logic [63: 0] pc_out
);
import CorePack::*;
always_comb begin
if (npc_sel) begin
if (opcode == JALR_OPCODE) begin
pc_out = alu & 64'hFFFFFFFFFFFFFFFE;
end
else begin
pc_out = alu;
end
end
else begin
pc_out = pc_in + 4;
end
end
endmodule

Core(DataPath)

接下来将上面涉及的所有模块连起来

  1. 端口

模块定义部分

module Core (
input clk,
input rst,

Mem_ift.Master imem_ift,
Mem_ift.Master dmem_ift,

output cosim_valid,
output CorePack::CoreInfo cosim_core_info
);

主要关注内存数据交换端口 imem_ift 和 dmem_ift, 两者分别是指令内存接口和数据内存接口, 根据前文提到的内存接口结构分析, 他们的类型是接口(interface), 包含了读写通道成员数据包 r_request_bits 和 w_request_bits

这两个接口也要根据数据通路图接入 Core 模块

assign dmem_ift.r_request_bits.raddr = alu_res;
assign dmem_ift.r_request_valid = re_mem;

assign dmem_ift.w_request_bits.waddr = alu_res;
assign dmem_ift.w_request_valid = we_mem;

assign imem_ift.r_request_bits.raddr = pc;
assign imem_ift.r_request_valid = 1'b1;
  1. 指令切割

根据 RISCV ISA Standard 和数据通路图, 可以做出如下切割, 将操作码, 两个源寄存器和目标寄存器的地址分离出来

assign opcode = inst[6:0];
assign rs1 = inst[19:15];
assign rs2 = inst[24:20];
assign rd = inst[11:7];
  1. npc_sel

NPC 选择器是根据 is_b 和 is_j 信号计算出来的

只有在既不是 is_j, 也不是 is_b, 且 br_taken(分支跳转) 的情况下, npc_sel 才会置 0

assign npc_sel = is_j || (br_taken && is_b);

Controller 设计

对于设计好的单周期 CPU 来说, 除开访存和写回两个步骤要涉及一定的时序, 其他部分只需要应用组合电路的思想就够了

主控模块 Controller 也不例外, 说白了这个模块的作用就是一个大型的译码器, 作用是解码指令, 发出信号, 指挥 DataPath 进行工作

也就是说, 主控控制的是 DataPath 上的某些开关的开闭, 开关开好了, 这个周期内的数据计算就可以认为是完成了

端口

控制通路模块的输出是一个 22 位的向量,从最高位到最低为依次定义如下

也就是说, 主控的功能就是根据 inst 给这些变量赋值

指令切割

首先对指令进行切割, 分理出操作码 opcode, 功能码 funct3, funct7

opcode_t opcode;
funct3_t funct3;
funct7_t funct7;

assign opcode = inst[6: 0];
assign funct3 = inst[14: 12];
assign funct7 = inst[31: 25];

初始化

将所有寄存器状态置 0, 或者置为无意义状态

we_reg = 0;
we_mem = 0;
re_mem = 0;
is_b = 0;
is_j = 0;
immgen_op = IMM0;
alu_op = ALU_DEFAULT;
cmp_op = CMP_NO;
alu_asel = ASEL0;
alu_bsel = BSEL0;
wb_sel = WB_SEL0;
mem_op = MEM_NO;

操作码 opcode 支持范围

根据 core_struct.vh 的 hint, 需要支持的 opcode 如下

typedef logic [6:0] opcode_t;
parameter LOAD_OPCODE = 7'b0000011;
parameter IMM_OPCODE = 7'b0010011;
parameter AUIPC_OPCODE = 7'b0010111;
parameter IMMW_OPCODE = 7'b0011011;
parameter STORE_OPCODE = 7'b0100011;
parameter REG_OPCODE = 7'b0110011;
parameter LUI_OPCODE = 7'b0110111;
parameter REGW_OPCODE = 7'b0111011;
parameter BRANCH_OPCODE = 7'b1100011;
parameter JALR_OPCODE = 7'b1100111;
parameter JAL_OPCODE = 7'b1101111;

LOAD_OPCODE

load 指令的作用是通过 rs1 计算得到一个地址, 从内存中加载一个数据, 回写到 rd

ld rd, offset(rs1)

re_mem = 1;
we_reg = 1;
alu_asel = ASEL_REG; // 对应 rs1
alu_bsel = BSEL_IMM; // 对应 offset
alu_op = ALU_ADD; // rs1 偏移 offset, ALU 做加法
immgen_op = I_IMM; // 生成 I 型立即数
wb_sel = WB_SEL_MEM; // 回写 MEM 的数据

根据 funct3, 可以确定内存操作码以适应不同长度的数据, 如 ld, lb, lh 等等(图片不完整)

case (funct3)
LB_FUNCT3: mem_op = MEM_B;
LH_FUNCT3: mem_op = MEM_H;
LW_FUNCT3: mem_op = MEM_W;
LD_FUNCT3: mem_op = MEM_D;
LBU_FUNCT3: mem_op = MEM_UB;
LHU_FUNCT3: mem_op = MEM_UH;
LWU_FUNCT3: mem_op = MEM_UW;
default: mem_op = MEM_NO;
endcase

REG_OPCODE

R-type(寄存器操作指令), 通过读取 rs1 和 rs2 的数据, 计算得到的结果回写到 rd

add rd, rs1, rs2

we_reg = 1'b1;          // 寄存器组写使能
alu_asel = ASEL_REG;
alu_bsel = BSEL_REG; // ALU 两个操作数均是寄存器
wb_sel = WB_SEL_ALU; // 回写 ALU 的计算结果

根据 funct3 和 funct7(仅 add / sub, srl / sra), 可以确定具体的运算

case (funct3)
ADD_FUNCT3: alu_op = (funct7 == 7'b0100000) ?
ALU_SUB : ALU_ADD; // 减法 / 加法
SLT_FUNCT3: alu_op = ALU_SLT; // 小于
SLTU_FUNCT3: alu_op = ALU_SLTU; // 无符号数小于
XOR_FUNCT3: alu_op = ALU_XOR; // 异或
OR_FUNCT3: alu_op = ALU_OR; // 或
AND_FUNCT3: alu_op = ALU_AND; // 与
SLL_FUNCT3: alu_op = ALU_SLL; // 逻辑左移
SRL_FUNCT3: alu_op = (funct7 == 7'b0100000) ?
ALU_SRA : ALU_SRL; // 算数 / 逻辑右移
default: alu_op = ALU_DEFAULT;
endcase

REGW_OPCODE

R-type 指令, 通过 rs1 和 rs2 读取有符号数, 作为操作数计算得到的结果, 截取低 32 位后写回 rd 的低 32 位

addw rd, rs1, rs2

REGW_OPCODE: begin
we_reg = 1'b1; // 寄存器组写使能
alu_asel = ASEL_REG;
alu_bsel = BSEL_REG; // 操作数均是寄存器
wb_sel = WB_SEL_ALU; // 回写 ALU 的结果
end

因此 WbSel 模块中, 当 wb_sel == WB_SEL_ALU 时, 要额外判断 opcode 是不是 REGW_OPCODE

IMM_OPCODE

I-type 指令, 将立即数与寄存器 rs1 计算得到结果, 写回 rd

addi rd, rs1, imm

we_reg = 1'b1;
alu_asel = ASEL_REG;
alu_bsel = BSEL_IMM; // 来源是立即数
immgen_op = I_IMM; // 产生 I-type 立即数
wb_sel = WB_SEL_ALU;

根据 funct3 可以选择具体的运算符

case(funct3)
ADD_FUNCT3: alu_op = ALU_ADD;
SLT_FUNCT3: alu_op = ALU_SLT;
SLTU_FUNCT3: alu_op = ALU_SLTU;
XOR_FUNCT3: alu_op = ALU_XOR;
OR_FUNCT3: alu_op = ALU_OR;
AND_FUNCT3: alu_op = ALU_AND;
SLL_FUNCT3: alu_op = ALU_SLL;
SRL_FUNCT3: alu_op = (funct7 == 7'b0000000) ?
ALU_SRL : ALU_SRA;
default: alu_op = ALU_DEFAULT;
endcase

IMMW_OPCODE

通过 rs1 和 rs2 读取有符号数, 作为操作数计算得到的结果写回 rd

we_reg = 1'b1;
alu_asel = ASEL_REG;
alu_bsel = BSEL_IMM;
immgen_op = I_IMM;
wb_sel = WB_SEL_ALU;

case (funct3)
ADDW_FUNCT3: alu_op = ALU_ADDW;
SLLW_FUNCT3: alu_op = ALU_SLLW;
SRLW_FUNCT3: alu_op = (funct7 == 7'b0000000) ?
ALU_SRLW : ALU_SRAW;
default: alu_op = ALU_DEFAULT;
endcase

STORE_OPCODE

S-type 指令, 通过立即数和 rs1 相加得到一个地址, 再将 rs2 的值写入这个地址(内存)

sd rs2, imm(rs1)

we_mem = 1;             // 内存写使能
alu_asel = ASEL_REG;
alu_bsel = BSEL_IMM;
alu_op = ALU_ADD; // 只有加法
immgen_op = S_IMM; // 生成 S-type 立即数
wb_sel = WB_SEL_ALU; // 回写 ALU 结果

case (funct3) // funct3 确定回写数据的位宽
SB_FUNCT3: mem_op = MEM_B;
SH_FUNCT3: mem_op = MEM_H;
SW_FUNCT3: mem_op = MEM_W;
SD_FUNCT3: mem_op = MEM_D;
default: mem_op = MEM_NO;
endcase

BRANCH_OPCODE

B-type 指令, 比较 rs1 和 rs2 的值, 假如返回值为真, 则跳转到 PC + imm 处, 否则顺序执行 PC + 4

blt rs1, rs2

对于 B-type 指令来说, 核心就不是 ALU 了, 而是 Cmp

在主控部分只涉及前面一段话的一小部分的实现(PC + imm), 至于如何比较, 是通过 is_b 将信号传递给 Cmp(同时还有比较操作码 cmp_opcode ), 再由 Cmp 输出 br_taken 信号给 NPC, 让 NPC 选择跳转到 PC + 4 还是 ALU 的运算结果 (PC + imm)

is_b = 1;               // 比较器使能
alu_asel = ASEL_PC; // 操作数 PC
alu_bsel = BSEL_IMM; // 操作数立即数
alu_op = ALU_ADD; // 加法
immgen_op = B_IMM; // 生成 B-type 立即数
wb_sel = WB_SEL_ALU; // 回写 ALU 计算结果

case (funct3) // 根据 funct3 确定 cmp_op
BEQ_FUNCT3: cmp_op = CMP_EQ;
BNE_FUNCT3: cmp_op = CMP_NE;
BLT_FUNCT3: cmp_op = CMP_LT;
BGE_FUNCT3: cmp_op = CMP_GE;
BLTU_FUNCT3: cmp_op = CMP_LTU;
BGEU_FUNCT3: cmp_op = CMP_GEU;
default: cmp_op = CMP_NO;
endcase

JAL_OPCODE

UJ-type 指令, 生成立即数, 跳转到 imm 作为地址的内存位置, 并回写原本的下一条指令的地址到 rd

也就是执行了 rd = PC + 4; PC = PC + imm;

jar rd, imm

is_j = 1;           // 跳转使能
we_reg = 1; // 寄存器写使能
wb_sel = WB_SEL_PC; // 回写 PC(实际上回写了 PC + 4)
alu_op = ALU_ADD; // 加法

alu_asel = ASEL_PC;
alu_bsel = BSEL_IMM;
immgen_op = UJ_IMM; // 生成 UJ-type 立即数

JALR_OPCODE

I-type 指令, 生成立即数, 跳转到 rs1 + imm 处, 并回写原本的下一条指令的地址到 rd

也就是执行了 rd = PC + 4; PC = rs1 + imm;

jalr rd, rs1, imm

JAL_OPCODE, JALR_OPCODE: begin
is_j = 1; // 跳转使能
we_reg = 1; // 寄存器写使能
wb_sel = WB_SEL_PC; // 回写 PC(实际上回写了 PC + 4)
alu_op = ALU_ADD; // 加法
alu_asel = ASEL_REG;
alu_bsel = BSEL_IMM;
immgen_op = I_IMM; // 生成 I-type 立即数
end

LUI_OPCODE

U-type 指令, 生成立即数, 加载到 rd 的高 20 位, 低位全部置 0

lui rd, imm

we_reg = 1'b1;          // 寄存器写使能
immgen_op = U_IMM; // 生成 U-type 立即数
alu_bsel = BSEL_IMM;
alu_op = ALU_ADD;
wb_sel = WB_SEL_ALU; // 回写 ALU 结果

AUIPC_OPCODE

U-type 指令, 生成立即数, 左移成 32 位, 再加上当前 PC 的值, 储存到 rd 的高 32 位

auipc rd, imm

we_reg = 1'b1;
alu_asel = ASEL_PC;
alu_bsel = BSEL_IMM;
immgen_op = U_IMM;
alu_op = ALU_ADD;
wb_sel = WB_SEL_ALU;