写在前面
不知道什么原因, 也可能是因为博主能力有限, 很少找到 ZJU 计算机系统课的资料笔记, 而且由于该课程的 TA 生产力过高 (赞美 TA), 实验部分常常是一年一换, 因此想找到适合学习的资料更为难上加难
比如去年的实验内容应该是以 RV32I 为板子写 CPU, 今年就变成了 RV64I, 同时很多细节都不一样 (怒)
再此留下一份单周期 CPU 的实验报告, 鉴于一些别的原因, 源码暂不开源, 权当抛砖引玉
数据通路设计
根据设计要求, 数据通路图如下
由于我们设计的是单周期 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 ; end end else begin if (we) begin if (write_addr != 0 ) begin register[write_addr] <= write_data; 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)
接下来将上面涉及的所有模块连起来
端口
模块定义部分
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 ;
指令切割
根据 RISCV ISA Standard 和数据通路图, 可以做出如下切割, 将操作码, 两个源寄存器和目标寄存器的地址分离出来
assign opcode = inst[6 :0 ];assign rs1 = inst[19 :15 ];assign rs2 = inst[24 :20 ];assign rd = inst[11 :7 ];
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; alu_bsel = BSEL_IMM; alu_op = ALU_ADD; immgen_op = I_IMM; wb_sel = WB_SEL_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; wb_sel = WB_SEL_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; 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; 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; wb_sel = WB_SEL_ALU; case (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; alu_bsel = BSEL_IMM; alu_op = ALU_ADD; immgen_op = B_IMM; wb_sel = WB_SEL_ALU; case (funct3) 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; alu_op = ALU_ADD; alu_asel = ASEL_PC; alu_bsel = BSEL_IMM; immgen_op = UJ_IMM;
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; alu_op = ALU_ADD; alu_asel = ASEL_REG; alu_bsel = BSEL_IMM; immgen_op = I_IMM; end
LUI_OPCODE
U-type 指令, 生成立即数, 加载到 rd 的高 20 位, 低位全部置 0
lui rd, imm
we_reg = 1'b1 ; immgen_op = U_IMM; alu_bsel = BSEL_IMM; alu_op = ALU_ADD; wb_sel = WB_SEL_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;