RISC-V 基础

指令集划分

名称类别说明
RV32I基础指令整数指令,包含算术、分支、逻辑、访存指令,有32个32位寄存器,能寻址32位地址空间
RV32E基础指令与RV32I一样,只不过只能使用前16个32位寄存器
RV64I基础指令整数指令,包含算术、分支、逻辑、访存指令,有32个64位寄存器,能寻址64位地址空间
RV128I基础指令整数指令,包含算术、分支、逻辑、访存指令,有32个128位寄存器,能寻址128位地址空间
M扩展指令包含乘法、除法、求模取余指令
F扩展指令单精度(32bit)浮点指令
D扩展指令双精度(32bit)浮点指令,必须要同时支持F扩展指令
Q扩展指令四倍精度浮点指令
A扩展指令存储器原子操作指令,比如比较并交换,读-改-写等指令
C扩展指令压缩指令,指令长度为16位,主要用于改善程序大小
P扩展指令单指令多数据(Packed-SIMD)指令
B扩展指令位操作指令
H扩展指令支持 Hypervisor 管理指令
J扩展指令动态翻译语言的指令
L扩展指令十进制浮点指令
N扩展指令用户中断指令
G通用指令包含 I、M、A、F、D指令

RV32I 基础指令集

RV32I

RV32I 通用寄存器

寄存器ABI 名字描述Saver
x0zero0值寄存器,硬编码为0,写入数据忽略,读取永远为0-
x1ra返回地址Caller
x2sp栈指针Callee
x3gp全局指针-
x4tp线程指针-
x5t0临时寄存器或者备用的链接寄存器Caller
x6-x7t1-t2临时寄存器Caller
x8s0/fp需要保存的寄存器或者帧指针寄存器Callee
x9s1需要保存的寄存器,保存原进程中的关键数据,避免在函数调用过程中被破坏Callee
x10-x11a0-a1函数参数/返回值Caller
x12-x17a2-a7函数参数Caller
x18-x27s2-s11需要保存的寄存器Callee
x28-x31t3-t6临时寄存器Caller

函数调用时保留的寄存器

被调用函数一般不会使用这些寄存器,即便使用也会提前保存好原值,可以信任。这些寄存器有:sp, gp, tp 和 s0-s11 寄存器。

函数调用时不保存的寄存器

有可能被调用函数使用更改,需要caller在调用前对自己用到的寄存器进行保存。这些寄存器有:参数与返回地址寄存器 a0-a7,返回地址寄存器 ra,临时寄存器 t0-t6

RV32I 基础指令格式

RV32I 指令编码格式

  • 源寄存器和目标寄存器都设计固定在所有 RISC-V 指令同样的位置上,指令译码相对简单,所以指令在 CPU 流水线中执行时,可以先开始访问寄存器,然后再完成指令解码。
  • 所有立即数的符号位总是在指令的最高位。这么做的好处是,有可能成为关键路径的立即数符号扩展可以在指令解码前进行,有利于 CPU 流水线的时序优化。

寄存器-寄存器的算术指令

RV32I 寄存器-寄存器的算术指令

指令汇编格式

加法
add rd, rs1, rs2
减法
sub rd, rs1, rs2
逻辑与
and rd, rs1, rs2
逻辑或
or rd, rs1, rs2
逻辑异或
xor rd, rs1, rs2
有符号小于比较
slt rd, rs1, rs2
无符号小于比较
sltu rd, rs1, rs2
逻辑左移
sll rd, rs1, rs2
逻辑右移
srl rd, rs1, rs2
算数右移
sra rd, rs1, rs2

立即数的算术指令

RV32I 立即数的算术指令

注意,在立即数算术指令中,没有减法运算。

指令汇编格式

立即数加法
addi rd, rs1, imm[11:0]
立即数逻辑与
andi rd, rs1, imm[11:0]
立即数逻辑或
ori rd, rs1, imm[11:0]
立即数逻辑异或
xori rd, rs1, imm[11:0]
立即数有符号小于比较
slti rd, rs1, imm[11:0]
立即数无符号小于比较
sltiu rd, rs1, imm[11:0]
立即数逻辑左移
slli rd, rs1, shamt[4:0]
立即数逻辑右移
srli rd, rs1, shamt[4:0]
立即数算数右移
srai rd, rs1, shamt[4:0]

Load/Store 指令

RV32I Load/Store 指令

Load 和 Store 的寻址模式只能是符号扩展 12 位的立即数,加上基地址寄存器得到访问的存储器地址。因为没有了复杂的内存寻址方式,这让 CPU 流水线可以对数据冲突提前做出判断,并通过流水线各级之间的转送加以处理,而不需要插入空操作(NOP),极大提高了代码的执行效率。

注意,Load指令属于 I 型指令,而 Store 指令属于 S 型指令。

指令汇编格式

字加载
lw rd, offset[11:0](rs1)
半字加载
lh rd, offset[11:0](rs1)
无符号半字加载
lhu rd, offset[11:0](rs1)
字节加载
lb rd, offset[11:0](rs1)
无符号字节加载
lbu rd, offset[11:0](rs1)
字存储
sw rs2, offset[11:0](rs1)
半字存储
sh rs2, offset[11:0](rs1)
字节存储
sb rs2, offset[11:0](rs1)

有条件分支跳转指令

有条件分支跳转指令

指令汇编格式

相等跳转
beq rs1, rs2, label
不等跳转
bne rs1, rs2, label
小于跳转
blt rs1, rs2, label
无符号小于跳转
bltu rs1, rs2, label
大于等于跳转
bge rs1, rs2, label
无符号大于等于跳转
bgeu rs1, rs2, label

无条件跳转指令

注意,直接跳转是 J 型指令,而相对跳转是 I 型指令。

直接跳转指令

无条件直接跳转

JAL 指令的执行过程:

  1. 首先,把 20 位的立即数做符号扩展,并左移一位,产生一个 32 位的符号数
  2. 然后,将该 32 位符号数和 PC 相加来产生目标地址(这样 JAL 可以作为短跳转指令,跳转至 PC±1MB 的地址范围内)
  3. 同时,JAL 会把紧随其后的那条指令的地址,存入目标寄存器中。这样,如果目标寄存器是0,则 JAL 就等同于 goto 指令;如果目标寄存器不为零,JAL 可以实现函数调用的功能

相对跳转指令

无条件相对跳转

JALR 指令会把 12 位立即数和源寄存器相加,并把相加的结果末位清零,作为新的跳转地址。和 JAL 指令一样,JALR 也会把紧随其后的那条指令的地址,存入目标寄存器中。

指令汇编格式

无条件直接跳转
jal rd, label # 将 PC+4 的值保存到 rd 寄存器中,然后设置 PC = PC + offset

伪指令 j 实际上就是jal指令的变体,此时 rd 会被设置为 x0,表示丢弃返回地址

无条件相对跳转
jalr rd, rs1, imm # 将 PC+4 保存到 rd 寄存器中,然后设置 PC = rs1  + imm

跳转到任意 32 位绝对地址处

lui x1, <hi20bits>
jalr ra, x1, <lo12bits>

相对PC地址32位偏移量的相对跳转

auipc x1, <hi20bits>
jalr x0, x1, <lo12bits>

U(Upper immediate)型指令

U型指令

指令汇编格式

lui 指令 (Load Upper Immediate)
lui rd, imm # 将 20 位的立即数左移12位,低 12 位补零,并写回寄存器 rd 中

配合 addi 指令(设置低 12 比特)可实现讲寄存器设置为任意 32 比特的立即数,例如:

lui x10, 0x87654     # x10 = 0x87654000
addi x10, x10, 0x321 # x10 = 0x87654321

但是,当这个 12 位的立即数为负数(即最高比特位是1)时,得到的结果是高 20 位减 1 再和低 12 位拼接,比如:

lui x10, 0xDEADB     # x10 = 0xDEADB000
addi x10, x10, 0xEEF # x10 = 0xDEADBEEF

解决这个问题的一种方法是,如果低 12 位的立即数的符号位是 1 ,那就预先给高 20 位的数加 1。li 伪指令可以替我们处理好这种特殊情况。

auipc 指令 (Add Upper Immediate to PC)
auipc rd, imm # 将 20 位的立即数左移12位,低 12 位补零,将得到的 32 位数与 pc 的值相加,最后写回寄存器 rd 中

具体应用有:

Label: auipc x10, 0 # 将 Label 的地址保存在 x10 寄存器中

指令编码空间的可扩展性

指令编码空间的扩展

  • custom-0、custom-1 用于 RV32 的自定义指令集扩展
  • custom-2、custom-3 预留给 RV128,也可以用于 RV32、RV64 的用户自定义指令集扩展

CSR 寄存器指令

除了内存地址空间和通用寄存器地址空间外,RISC-V 中还定义了一个独立的控制与状态寄存器(CSR)地址空间。

独立的 12 位地址编码空间

CSR寄存器访问指令的编码

专用的 CSR 指令

CSR 指令

其他指令

  • 系统调用 ecall 指令
  • 调试时用于将控制转移到调试环境的 ebreak 指令

常用汇编伪指令

赋值指令
mv rd, rs # 等效于 addi rd, rs, x0
加载立即数
li rd, 13 # 等效于 addi rd, x0, 13
函数调用和返回
jal my_foo # 函数调用
ret # 函数返回,等效于 jr ra,等效于 jalr x0, ra, 0

单核 CPU 组成结构

ALU

数据通路是处理器中执行处理器所需操作的硬件部分,就像是处理器的四肢。

控制器是对数据通路要做什么操作进行行为调度的硬件结构,就像是处理器的大脑。

流水线技术

五级流水线

五级流水线

流水线在不同阶段使用的资源

流水线资源使用

为了确保硬件共享的时候,前一阶段的数据不被丢失,需要在流水线之间插入“阶段寄存器”来保存中间值和控制信号。

数据通路

数据通路

  1. 取指阶段(Instruction Fetch):将指令从存储器中读取出来,PC 寄存器告诉当前指令在存储器中的位置。读取一条指令后,PC 寄存器会根据指令的长度自动递增,或者改写成指定的地址。
  2. 译码阶段(Instruction Decode):将存储器中取出的指令进行翻译,识别出指令的类别以及所需的各种操作数。
  3. 执行阶段(Instruction Execute):对指令进行真正的运算,期间最关键的模块是算术逻辑单元(ALU)。
  4. 访存阶段(Memory Access):存储器访问指令将数据从存储器中读出,或写入存储器。
  5. 写回阶段(Write Back):将指令执行的结果写回通用寄存器。

简易 CPU 内部组件框图

RV32I CPU 5级流水线设计框图

pre_if 模块设计

根据当前的指令和 PC 寄存器,预测下一条指令的地址。为了实现程序分支跳转的功能,就需要设计一个预读取模块,不管指令是否跳转(这个结果会在指令执行阶段结束才能知道),都提前把跳转之后的下一条指令从存储器中读取出来,以备流水线的下一个阶段使用,这能提到 CPU 的执行效率。

module pre_if (
    input [31:0] instr,
    input [31:0] pc,

    output [31:0] pre_pc
);

    wire is_bxx = (instr[6:0] == `OPCODE_BRANCH); // 条件跳转指令的操作码
    wire is_jal = (instr[6:0] == `OPCODE_JAL) ;   // 无条件跳转指令的操作码

    // B型指令的立即数拼接
    wire [31:0] bimm  = {{20{instr[31]}}, instr[7], instr[30:25], instr[11:8], 1'b0};
    // J型指令的立即数拼接
    wire [31:0] jimm  = {{12{instr[31]}}, instr[19:12], instr[20], instr[30:21], 1'b0};

    // 指令地址的偏移量
    // 这里实际上做了一个简单的分支预测
    wire [31:0] adder = is_jal ? jimm : (is_bxx & bimm[31]) ? bimm : 4;
    // 根据当前 PC 和指令的偏移量相加,得到预测的 PC 值
    assign pre_pc = pc + adder;

endmodule

if_id 模块设计

预读取模块读出的指令并不是全部都能发送给后续的模块执行的,比如条件分支指令在执行后发现跳转条件不成立,这时预读取的指令就是无效的,需要对流水线进行冲刷(flush),把无效的指令都清除掉。

module if_id (
  input         clk,
  input         reset,
  input  [31:0] in_instr,
  input  [31:0] in_pc,
  input         flush,
  input         valid,
  output [31:0] out_instr,
  output [31:0] out_pc,
  output        out_noflush
);

  reg [31:0] reg_instr;
  reg [31:0] reg_pc;
  reg        reg_noflush;

  assign out_instr = reg_instr;
  assign out_pc = reg_pc;
  assign out_noflush = reg_noflush;

  //指令传递
  always @(posedge clk or posedge reset) begin
    if (reset) begin
      reg_instr <= 32'h0;
    end else if (flush) begin
      reg_instr <= 32'h0;
    end else if (valid) begin
      reg_instr <= in_instr;
    end
  end

  //PC值转递
  always @(posedge clk or posedge reset) begin
    if (reset) begin
      reg_pc <= 32'h0;
    end else if (flush) begin
      reg_pc <= 32'h0;
    end else if (valid) begin
      reg_pc <= in_pc;
    end
  end

  //流水线冲刷标志位
  always @(posedge clk or posedge reset) begin
    if (reset) begin
      reg_noflush <= 1'h0;
    end else if (flush) begin
      reg_noflush <= 1'h0;
    end else if (valid) begin
      reg_noflush <= 1'h1;
    end

  end

endmodule

decode 模块设计

尽管指令格式不同,但是指令译码模块翻译指令的工作机制是统一的。首先会翻译出指令中携带的寄存器索引、立即数等信息,接着处理可能存在的数据冒险,再由译码数据通路负责把译码后的指令信息,发送给对应的执行单元去执行。

译码的过程:先识别指令的操作码(永远是低7位),根据操作码对应的代码标识,产生分支信号 branch、跳转信号 jump、读存储器信号 mem_read ......

module decode (
  input   [31:0] instr,

  output  [4:0] rs1_addr,
  output  [4:0] rs2_addr,
  output  [4:0] rd_addr,
  output  [2:0] funct3,
  output  [6:0] funct7,
  output        branch,
  output [1:0]  jump,
  output        mem_read,
  output        mem_write,
  output        reg_write,
  output        to_reg,
  output [1:0]  result_sel,
  output        alu_src,
  output        pc_add,
  output [6:0]  types,
  output [1:0]  alu_ctrlop,
  output        valid_inst,
  output [31:0] imm
);

localparam DEC_INVALID = 21'b0;

reg [20:0] dec_array;

//---------- decode rs1、rs2 -----------------
assign rs1_addr = instr[19:15];
assign rs2_addr = instr[24:20];

//---------- decode rd -----------------------
assign rd_addr = instr[11:7];

//---------- decode funct3、funct7 -----------
assign funct7 = instr[31:25];
assign funct3 = instr[14:12];

// ----------------------------- decode signals ---------------------------------

//                        20     19-18  17       16        15        14     13-12      11      10     9--------3  2---1      0
//                        branch jump   memRead  memWrite  regWrite  toReg  resultSel  aluSrc  pcAdd     RISBUJZ  aluctrlop  validInst
localparam DEC_LUI     = {1'b0,  2'b00, 1'b0,    1'b0,     1'b1,     1'b0,  2'b01,     1'b0,   1'b0,  7'b0000100, 2'b00,     1'b1};
localparam DEC_AUIPC   = {1'b0,  2'b00, 1'b0,    1'b0,     1'b1,     1'b0,  2'b00,     1'b1,   1'b1,  7'b0000100, 2'b00,     1'b1};
localparam DEC_JAL     = {1'b0,  2'b00, 1'b0,    1'b0,     1'b1,     1'b0,  2'b10,     1'b0,   1'b0,  7'b0000010, 2'b00,     1'b1};
localparam DEC_JALR    = {1'b0,  2'b11, 1'b0,    1'b0,     1'b1,     1'b0,  2'b10,     1'b1,   1'b0,  7'b0100000, 2'b00,     1'b1};
localparam DEC_BRANCH  = {1'b1,  2'b00, 1'b0,    1'b0,     1'b0,     1'b0,  2'b00,     1'b0,   1'b0,  7'b0001000, 2'b10,     1'b1};
localparam DEC_LOAD    = {1'b0,  2'b00, 1'b1,    1'b0,     1'b1,     1'b1,  2'b00,     1'b1,   1'b0,  7'b0100000, 2'b00,     1'b1};
localparam DEC_STORE   = {1'b0,  2'b00, 1'b0,    1'b1,     1'b0,     1'b0,  2'b00,     1'b1,   1'b0,  7'b0010000, 2'b00,     1'b1};
localparam DEC_ALUI    = {1'b0,  2'b00, 1'b0,    1'b0,     1'b1,     1'b0,  2'b00,     1'b1,   1'b0,  7'b0100000, 2'b01,     1'b1};
localparam DEC_ALUR    = {1'b0,  2'b00, 1'b0,    1'b0,     1'b1,     1'b0,  2'b00,     1'b0,   1'b0,  7'b1000000, 2'b01,     1'b1};

assign  {branch, jump, mem_read, mem_write, reg_write, to_reg, result_sel, alu_src, pc_add, types, alu_ctrlop, valid_inst} = dec_array;


always @(*) begin
  //$write("%x", instr);
  case(instr[6:0])
    `OPCODE_LUI    :   dec_array <= DEC_LUI;
    `OPCODE_AUIPC  :   dec_array <= DEC_AUIPC;
    `OPCODE_JAL    :   dec_array <= DEC_JAL;
    `OPCODE_JALR   :   dec_array <= DEC_JALR;
    `OPCODE_BRANCH :   dec_array <= DEC_BRANCH;
    `OPCODE_LOAD   :   dec_array <= DEC_LOAD;
    `OPCODE_STORE  :   dec_array <= DEC_STORE;
    `OPCODE_ALUI   :   dec_array <= DEC_ALUI;
    `OPCODE_ALUR   :   dec_array <= DEC_ALUR;
    default        :  begin
                 dec_array <= DEC_INVALID;
               //  $display("~~~decode error~~~%x", instr);
    end
  endcase
end

// -------------------- IMM -------------------------

wire [31:0] Iimm = {{21{instr[31]}}, instr[30:20]};
wire [31:0] Simm = {{21{instr[31]}}, instr[30:25], instr[11:7]};
wire [31:0] Bimm = {{20{instr[31]}}, instr[7], instr[30:25], instr[11:8], 1'b0};
wire [31:0] Uimm = {instr[31:12], 12'b0};
wire [31:0] Jimm = {{12{instr[31]}}, instr[19:12], instr[20], instr[30:21], 1'b0};

assign imm = {32{types[5]}} & Iimm
           | {32{types[4]}} & Simm
           | {32{types[3]}} & Bimm
           | {32{types[2]}} & Uimm
           | {32{types[1]}} & Jimm;

endmodule

前面译码模块得到的指令信号可以分为两大类,一类是指令的操作码经过译码后产生的指令控制信号,另一类是从指令源码中提取出来的数据信息,如立即数、寄存器索引、功能码等。为了能对流水线更好地实施控制,我们把译码后的数据和控制信号分开处理。

译码控制模块

当指令发生冲突时,需要对流水线进行冲刷,译码阶段的指令信息也需要清除。

module id_ex_ctrl (
  input        clk,
  input        reset,
  input        in_ex_ctrl_itype,
  input  [1:0] in_ex_ctrl_alu_ctrlop,
  input  [1:0] in_ex_ctrl_result_sel,
  input        in_ex_ctrl_alu_src,
  input        in_ex_ctrl_pc_add,
  input        in_ex_ctrl_branch,
  input  [1:0] in_ex_ctrl_jump,
  input        in_mem_ctrl_mem_read,
  input        in_mem_ctrl_mem_write,
  input  [1:0] in_mem_ctrl_mask_mode,
  input        in_mem_ctrl_sext,
  input        in_wb_ctrl_to_reg,
  input        in_wb_ctrl_reg_write,
  input        in_noflush,
  input        flush,
  input        valid,
  output       out_ex_ctrl_itype,
  output [1:0] out_ex_ctrl_alu_ctrlop,
  output [1:0] out_ex_ctrl_result_sel,
  output       out_ex_ctrl_alu_src,
  output       out_ex_ctrl_pc_add,
  output       out_ex_ctrl_branch,
  output [1:0] out_ex_ctrl_jump,
  output       out_mem_ctrl_mem_read,
  output       out_mem_ctrl_mem_write,
  output [1:0] out_mem_ctrl_mask_mode,
  output       out_mem_ctrl_sext,
  output       out_wb_ctrl_to_reg,
  output       out_wb_ctrl_reg_write,
  output       out_noflush
);

  reg  reg_ex_ctrl_itype;
  reg [1:0] reg_ex_ctrl_alu_ctrlop;
  reg [1:0] reg_ex_ctrl_result_sel;
  reg  reg_ex_ctrl_alu_src;
  reg  reg_ex_ctrl_pc_add;
  reg  reg_ex_ctrl_branch;
  reg [1:0] reg_ex_ctrl_jump;
  reg  reg_mem_ctrl_mem_read;
  reg  reg_mem_ctrl_mem_write;
  reg [1:0] reg_mem_ctrl_mask_mode;
  reg  reg_mem_ctrl_sext;
  reg  reg_wb_ctrl_to_reg;
  reg  reg_wb_ctrl_reg_write;
  reg  reg_noflush;

  assign out_ex_ctrl_itype = reg_ex_ctrl_itype;
  assign out_ex_ctrl_alu_ctrlop = reg_ex_ctrl_alu_ctrlop;
  assign out_ex_ctrl_result_sel = reg_ex_ctrl_result_sel;
  assign out_ex_ctrl_alu_src = reg_ex_ctrl_alu_src;
  assign out_ex_ctrl_pc_add = reg_ex_ctrl_pc_add;
  assign out_ex_ctrl_branch = reg_ex_ctrl_branch;
  assign out_ex_ctrl_jump = reg_ex_ctrl_jump;
  assign out_mem_ctrl_mem_read = reg_mem_ctrl_mem_read;
  assign out_mem_ctrl_mem_write = reg_mem_ctrl_mem_write;
  assign out_mem_ctrl_mask_mode = reg_mem_ctrl_mask_mode;
  assign out_mem_ctrl_sext = reg_mem_ctrl_sext;
  assign out_wb_ctrl_to_reg = reg_wb_ctrl_to_reg;
  assign out_wb_ctrl_reg_write = reg_wb_ctrl_reg_write;
  assign out_noflush = reg_noflush;

  always @(posedge clk or posedge reset) begin
    if (reset) begin
      reg_ex_ctrl_itype <= 1'h0;
    end else if (flush) begin
      reg_ex_ctrl_itype <= 1'h0;
    end else if (valid) begin
      reg_ex_ctrl_itype <= in_ex_ctrl_itype;
    end
  end

  always @(posedge clk or posedge reset) begin
    if (reset) begin
      reg_ex_ctrl_alu_ctrlop <= 2'h0;
    end else if (flush) begin
      reg_ex_ctrl_alu_ctrlop <= 2'h0;
    end else if (valid) begin
      reg_ex_ctrl_alu_ctrlop <= in_ex_ctrl_alu_ctrlop;
    end
  end

  always @(posedge clk or posedge reset) begin
    if (reset) begin
      reg_ex_ctrl_result_sel <= 2'h0;
    end else if (flush) begin
      reg_ex_ctrl_result_sel <= 2'h0;
    end else if (valid) begin
      reg_ex_ctrl_result_sel <= in_ex_ctrl_result_sel;
    end
  end

  always @(posedge clk or posedge reset) begin
    if (reset) begin
      reg_ex_ctrl_alu_src <= 1'h0;
    end else if (flush) begin
      reg_ex_ctrl_alu_src <= 1'h0;
    end else if (valid) begin
      reg_ex_ctrl_alu_src <= in_ex_ctrl_alu_src;
    end
  end

  always @(posedge clk or posedge reset) begin
    if (reset) begin
      reg_ex_ctrl_pc_add <= 1'h0;
    end else if (flush) begin
      reg_ex_ctrl_pc_add <= 1'h0;
    end else if (valid) begin
      reg_ex_ctrl_pc_add <= in_ex_ctrl_pc_add;
    end
  end

  always @(posedge clk or posedge reset) begin
    if (reset) begin
      reg_ex_ctrl_branch <= 1'h0;
    end else if (flush) begin
      reg_ex_ctrl_branch <= 1'h0;
    end else if (valid) begin
      reg_ex_ctrl_branch <= in_ex_ctrl_branch;
    end
  end

  always @(posedge clk or posedge reset) begin
    if (reset) begin
      reg_ex_ctrl_jump <= 2'h0;
    end else if (flush) begin
      reg_ex_ctrl_jump <= 2'h0;
    end else if (valid) begin
      reg_ex_ctrl_jump <= in_ex_ctrl_jump;
    end
  end

  always @(posedge clk or posedge reset) begin
    if (reset) begin
      reg_mem_ctrl_mem_read <= 1'h0;
    end else if (flush) begin
      reg_mem_ctrl_mem_read <= 1'h0;
    end else if (valid) begin
      reg_mem_ctrl_mem_read <= in_mem_ctrl_mem_read;
    end
  end

  always @(posedge clk or posedge reset) begin
    if (reset) begin
      reg_mem_ctrl_mem_write <= 1'h0;
    end else if (flush) begin
      reg_mem_ctrl_mem_write <= 1'h0;
    end else if (valid) begin
      reg_mem_ctrl_mem_write <= in_mem_ctrl_mem_write;
    end
  end

  always @(posedge clk or posedge reset) begin
    if (reset) begin
      reg_mem_ctrl_mask_mode <= 2'h0;
    end else if (flush) begin
      reg_mem_ctrl_mask_mode <= 2'h0;
    end else if (valid) begin
      reg_mem_ctrl_mask_mode <= in_mem_ctrl_mask_mode;
    end
  end

  always @(posedge clk or posedge reset) begin
    if (reset) begin
      reg_mem_ctrl_sext <= 1'h0;
    end else if (flush) begin
      reg_mem_ctrl_sext <= 1'h0;
    end else if (valid) begin
      reg_mem_ctrl_sext <= in_mem_ctrl_sext;
    end
  end

  always @(posedge clk or posedge reset) begin
    if (reset) begin
      reg_wb_ctrl_to_reg <= 1'h0;
    end else if (flush) begin
      reg_wb_ctrl_to_reg <= 1'h0;
    end else if (valid) begin
      reg_wb_ctrl_to_reg <= in_wb_ctrl_to_reg;
    end
  end

  always @(posedge clk or posedge reset) begin
    if (reset) begin
      reg_wb_ctrl_reg_write <= 1'h0;
    end else if (flush) begin
      reg_wb_ctrl_reg_write <= 1'h0;
    end else if (valid) begin
      reg_wb_ctrl_reg_write <= in_wb_ctrl_reg_write;
    end
  end

  always @(posedge clk or posedge reset) begin
    if (reset) begin
      reg_noflush <= 1'h0;
    end else if (flush) begin
      reg_noflush <= 1'h0;
    end else if (valid) begin
      reg_noflush <= in_noflush;
    end
  end


endmodule

译码数据通路模块

译码数据通路会根据 CPU 相关控制模块产生的流水线冲刷控制信号,决定要不要把这些数据发送给后续模块。

module id_ex (
  input         clk,
  input         reset,
  input  [4:0]  in_rd_addr,
  input  [6:0]  in_funct7,
  input  [2:0]  in_funct3,
  input  [31:0] in_imm,
  input  [31:0] in_rs2_data,
  input  [31:0] in_rs1_data,
  input  [31:0] in_pc,
  input  [4:0]  in_rs1_addr,
  input  [4:0]  in_rs2_addr,
  input         flush,
  input         valid,
  output [4:0]  out_rd_addr,
  output [6:0]  out_funct7,
  output [2:0]  out_funct3,
  output [31:0] out_imm,
  output [31:0] out_rs2_data,
  output [31:0] out_rs1_data,
  output [31:0] out_pc,
  output [4:0]  out_rs1_addr,
  output [4:0]  out_rs2_addr
);
  reg [4:0] reg_rd_addr;
  reg [6:0] reg_funct7;
  reg [2:0] reg_funct3;
  reg [31:0] reg_imm;
  reg [31:0] reg_rs2_data;
  reg [31:0] reg_rs1_data;
  reg [31:0] reg_pc;
  reg [4:0] reg_rs1_addr;
  reg [4:0] reg_rs2_addr;

  assign out_rd_addr = reg_rd_addr;
  assign out_funct7 = reg_funct7;
  assign out_funct3 = reg_funct3;
  assign out_imm = reg_imm;
  assign out_rs2_data = reg_rs2_data;
  assign out_rs1_data = reg_rs1_data;
  assign out_pc = reg_pc;
  assign out_rs1_addr = reg_rs1_addr;
  assign out_rs2_addr = reg_rs2_addr;

  always @(posedge clk or posedge reset) begin
    if (reset) begin
      reg_rd_addr <= 5'h0;
    end else if (flush) begin
      reg_rd_addr <= 5'h0;
    end else if (valid) begin
      reg_rd_addr <= in_rd_addr;
    end
  end

  always @(posedge clk or posedge reset) begin
    if (reset) begin
      reg_funct7 <= 7'h0;
    end else if (flush) begin
      reg_funct7 <= 7'h0;
    end else if (valid) begin
      reg_funct7 <= in_funct7;
    end
  end

  always @(posedge clk or posedge reset) begin
    if (reset) begin
      reg_funct3 <= 3'h0;
    end else if (flush) begin
      reg_funct3 <= 3'h0;
    end else if (valid) begin
      reg_funct3 <= in_funct3;
    end
  end

  always @(posedge clk or posedge reset) begin
    if (reset) begin
      reg_imm <= 32'h0;
    end else if (flush) begin
      reg_imm <= 32'h0;
    end else if (valid) begin
      reg_imm <= in_imm;
    end
  end

  always @(posedge clk or posedge reset) begin
    if (reset) begin
      reg_rs2_data <= 32'h0;
    end else if (flush) begin
      reg_rs2_data <= 32'h0;
    end else if (valid) begin
      reg_rs2_data <= in_rs2_data;
    end
  end

  always @(posedge clk or posedge reset) begin
    if (reset) begin
      reg_rs1_data <= 32'h0;
    end else if (flush) begin
      reg_rs1_data <= 32'h0;
    end else if (valid) begin
      reg_rs1_data <= in_rs1_data;
    end
  end

  always @(posedge clk or posedge reset) begin
    if (reset) begin
      reg_pc <= 32'h0;
    end else if (flush) begin
      reg_pc <= 32'h0;
    end else if (valid) begin
      reg_pc <= in_pc;
    end
  end

  always @(posedge clk or posedge reset) begin
    if (reset) begin
      reg_rs1_addr <= 5'h0;
    end else if (flush) begin
      reg_rs1_addr <= 5'h0;
    end else if (valid) begin
      reg_rs1_addr <= in_rs1_addr;
    end
  end

  always @(posedge clk or posedge reset) begin
    if (reset) begin
      reg_rs2_addr <= 5'h0;
    end else if (flush) begin
      reg_rs2_addr <= 5'h0;
    end else if (valid) begin
      reg_rs2_addr <= in_rs2_addr;
    end
  end

endmodule

执行控制模块

在指令执行阶段,存储访问指令用 ALU 进行地址计算,条件分支跳转指令用 ALU 进行条件比较,算术逻辑指令用 ALU 进行逻辑运算。

module alu_ctrl (
    input [2:0]  funct3,
    input [6:0]  funct7,
    input [1:0]  aluCtrlOp,
    input        itype,
    output reg [3:0] aluOp
);
    always @(*) begin
      case(aluCtrlOp)
        2'b00:  aluOp <= `ALU_OP_ADD;           // Load or Store
        2'b01:  begin
          if(itype & funct3[1:0] != 2'b01)
            aluOp <= {1'b0, funct3};
          else
            aluOp <= {funct7[5], funct3};   // normal ALUI/ALUR
        end
        2'b10:  begin
         // $display("~~~aluCtrl bxx~~~%d", funct3);
          case(funct3)                    // bxx
            `BEQ_FUNCT3:  aluOp <= `ALU_OP_EQ;
            `BNE_FUNCT3:  aluOp <= `ALU_OP_NEQ;
            `BLT_FUNCT3:  aluOp <= `ALU_OP_SLT;
            `BGE_FUNCT3:  aluOp <= `ALU_OP_GE;
            `BLTU_FUNCT3: aluOp <= `ALU_OP_SLTU;
            `BGEU_FUNCT3: aluOp <= `ALU_OP_GEU;
            default:      aluOp <= `ALU_OP_XXX;
          endcase
          end
        default: aluOp <= `ALU_OP_XXX;
      endcase
    end
endmodule

通用寄存器模块

module gen_regs (
    input  clk,
    input  reset,
    input  wen,
    input  [4:0] regRAddr1, regRAddr2, regWAddr,
    input  [31:0] regWData,
    output [31:0] regRData1,
    output [31:0] regRData2
);
    integer ii;
    reg [31:0] regs[31:0];

    // write registers
    always @(posedge clk or posedge reset) begin
        if(reset) begin
            for(ii=0; ii<32; ii=ii+1)
                regs[ii] <= 32'b0;
        end
        else if(wen & (|regWAddr))
                regs[regWAddr] <= regWData;
    end

    // read registers
    assign regRData1 = wen & (regWAddr == regRAddr1) ? regWData
                    : ((regRAddr1 != 5'b0) ? regs[regRAddr1] : 32'b0);
    assign regRData2 = wen & (regWAddr == regRAddr2) ? regWData
                    : ((regRAddr2 != 5'b0) ? regs[regRAddr2] : 32'b0);

endmodule

写寄存器是边沿触发的,在一个时钟周期内写入的存储器数据,需要在写一个时钟周期才能把写入的数据读取出来。为了提高读写效率,在对同一个寄存器进行读写时,如果写使能 wen 有效,就直接把写入寄存器的数据送给读数据接口。

ALU 模块

module alu (
  input  [31:0] alu_data1_i,
  input  [31:0] alu_data2_i,
  input  [ 3:0] alu_op_i,
  output [31:0] alu_result_o
);

  reg  [31:0] result;

  // alu_op_i 的第3位和第1位为1时,做减法运算,这是为减法指令或者比较大小而准备的
  wire [31:0] sum    = alu_data1_i + ((alu_op_i[3] | alu_op_i[1]) ? -alu_data2_i : alu_data2_i);
  // 根据前面两个操作数相减的结果判断两个操作数是否相等
  wire        neq    = |sum;
  // 比较两个操作数的大小:
  // 如果操作数的符号位相同,则根据两个操作数相减的差值的符号位去判断
  // 如果操作数的符号位不同,先根据alu_op_i 的最低位判断是否是无符号数比较运算
  wire        cmp    = (alu_data1_i[31] == alu_data2_i[31]) ? sum[31]
                     : alu_op_i[0] ? alu_data2_i[31] : alu_data1_i[31];
  wire [ 4:0] shamt  = alu_data2_i[4:0];
  // 判断是左移还是右移,如果是左移,就先对源操作数做镜像处理
  wire [31:0] shin   = alu_op_i[2] ? alu_data1_i : reverse(alu_data1_i);
  // 判断是算术右移还是逻辑右移,如果是算术右移,需要在最高位补一个符号位
  wire [32:0] shift  = {alu_op_i[3] & shin[31], shin};
  // $signed() 函数会在右移操作前先把操作数的符号位扩位成跟结果相同的位宽
  wire [32:0] shiftt = ($signed(shift) >>> shamt);
  wire [31:0] shiftr = shiftt[31:0];
  // 左移的结果是右移后的结果再进行镜像处理
  wire [31:0] shiftl = reverse(shiftr);

  always @(*) begin
    case(alu_op_i)
      `ALU_OP_ADD:    result <= sum;
      `ALU_OP_SUB:    result <= sum;
      `ALU_OP_SLL:    result <= shiftl;
      `ALU_OP_SLT:    result <= cmp;
      `ALU_OP_SLTU:   result <= cmp;
      `ALU_OP_XOR:    result <= (alu_data1_i ^ alu_data2_i);
      `ALU_OP_SRL:    result <= shiftr;
      `ALU_OP_SRA:    result <= shiftr;
      `ALU_OP_OR:     result <= (alu_data1_i | alu_data2_i);
      `ALU_OP_AND:    result <= (alu_data1_i & alu_data2_i);

      `ALU_OP_EQ:     result <= {31'b0, ~neq};
      `ALU_OP_NEQ:    result <= {31'b0, neq};
      `ALU_OP_GE:     result <= {31'b0, ~cmp};
      `ALU_OP_GEU:    result <= {31'b0, ~cmp};
      default:        begin
                      result <= 32'b0;
                      //$display("*** alu error ! ***%x", alu_op_i);
        end
    endcase
  end

  function [31:0] reverse;
    input [31:0] in;
    integer i;
    for(i=0; i<32; i=i+1) begin
      reverse[i] = in[31-i];
    end
  endfunction

  assign alu_result_o = result;

endmodule
  • 左移运算复用了右移运算的电路,方便实现

完整的数据通路

完整的数据通路

  • 译码阶段,会将指令的功能码和操作码发送给控制器,来产生相应的控制信号
  • 立即数扩展信号:ImmSel
  • ALU 功能选择信号:ALUSel

控制器的设计

控制器的设计

R 型指令数据通路

R型指令数据通路

  • ALUSel 会根据指令的 funct3来取不同的值

I 型指令数据通路

I型指令数据通路

Load 指令数据通路

Load指令数据通路

Store 指令数据通路

Store指令数据通路

  • 立即数来自inst[31:25][11:7],这个和Load不同
  • Store指令没有写回阶段

B 型指令数据通路

B指令数据通路

  • 访存写回阶段

jalr 指令数据通路

jalr指令数据通路

  • PC+4 的值会保存到rd

jal 指令数据通路

jal指令数据通路

Cache

Cache

Cache 的结构

Cache的结构

  • 块(block):两级存储器层次结构中存储器信息交换的最小单元
  • 命中(hit):如果处理器需要的数据存放在高层存储器中的某个块中,称为一次命中
  • 缺失(miss):如果在高层存储器中没有找到所需的数据,这次数据请求称为一次缺失
    • 缺失代价(miss penalty):将相应的块从底层存储器替换到高层存储器的时间+将该信息块传送给处理器的时间

Cache 直接映射

直接映射:一种 cache 结构,其中每个存储器地址仅仅对应到 cache 中的一个位置

映射方法:(块地址)mod(cache 中的块数)

标记:表中的一个字段,包含了地址信息,这些地址信息可以用来判断cache中的字是否就是所请求的字

有效位:表中的一个字段,用来标识一个块是否包含有一个有效数据

Cache直接映射

Cache直接映射示例

缺点:利用率低,命中率低

Cache 全相联映射

全相联映射:一个块可以被放置在 cache 中的任何位置

Cache全相联映射

Cache全相联映射示例

缺点:硬件开销大(有多少cache块就配有相等数量的比较器)

Cache 组相联映射

在组相联映射中,每个块可被放置的位置数是固定的,每个块有 n 个位置可放的 cache 被称为 n 路组相联 Cache

Cache组相联映射

Cache组相联映射示例

四路组相联 Cache:

  • 4 个比较器
  • 1 个四选一多路选择器

Cache 的设计

  • 要考虑的维度
    • Cache 的容量
    • 块大小
    • 组织方式(Direct,Fully Associative,Set Associative)
    • 替换算法(FIFO,LRU)
    • 写策略(write-through, write-back)

虚拟地址

虚拟存储器管理

分段管理

分段管理

分段管理:将一个程序按照逻辑单元分成多个程序段,每一个段使用自己单独的虚拟地址空间。

  • 逻辑上相互独立
  • 容易实现共享和保护
  • 非常容易产生碎片(段长是不确定的)

分页管理

分页管理

  • 如果页表项为4字节,那么整张页表会占据4MB大小的内存空间

两级分页管理

两级分页管理

  • 4KB的页目录+4KB的页表

快速地址转换 TLB

块表

块表(Translation-Lookaside Buffer):用于记录最近使用地址的映射信息的高速缓存,从而可以避免每次都要访问页表

使用 TLB 进行地址转换

TLB的位置

TLB实现地址转换的原理

TLB虚实地址转换

特权级别

一个 RISC-V 的硬件线程在任一时刻只能运行在某一个特权级上,这个特权级由 CSR 指定和配置。

名称级别缩写编码说明
用户应用程序特权级0U00运行应用程序,同样也适用于嵌入式系统
管理员特权级1S01主要用于支持现代操作系统,如Linux
虚拟机监视特权级2H10支持虚拟机监视器
机器特权级3M11对内存、I/O和一些必要的底层功能(启动和系统配置)有着完全的控制权

标准寄存器列表

Machine Mode

名称地址属性备注
mvendorid0xF11RO商业供应商编号寄存器
marchid0xF12RO架构编号寄存器
mimpid0xF13RO硬件实现编号寄存器
mhartid0xF14ROHart编号寄存器 (Hart: Hardware Thread)
mstatus0x300RW异常处理状态寄存器
misa0x301RO指令集架构寄存器
mie0x304RW局部中断屏蔽控制寄存器
mtvec0x305RW异常入口基地址寄存器
mtvt0x307RW中断向量表的基地址,至少为 64byte 对齐
mscratch0x340RW暂存寄存器,比如进入异常处理模式后,将应用程序的用户的 sp 寄存器临时保存到这个寄存器中
mepc0x341RW异常PC寄存器
mcause0x342RW异常原因寄存器
mtval0x343RW异常值寄存器,保存进入异常之前出错指令的编码值或者存储器访问的地址值
mip0x344RW中断等待寄存器
mnxti0x345RW读操作返回值是下一个中断的handler地址,写回操作会更新中断使能的状态
mintstatus0x346RO用于保存当前中断 Level
mscratchcsw0x348RW用于在特权模式变化时交换mscratch与目的寄存器的值
mscratchcswl0x349RW用于在中断Level变化时交换mscratch与目的寄存器的值
mcycle0xB00RW周期计数器的低32位
mcycleh0xB80RW周期计数器的高32位
minstret0xB02RW完成指令计数器的低32位,该寄存器用于衡量处理器的性能
minstrech0xB82RW完成指令计数器的高32位

User Mode

名称地址属性备注
cycle0xC00ROmcycle寄存器的只读副本
time0xC01ROmtime寄存器的只读副本
instret0xC02ROminstret寄存器的只读副本
cycleh0xC80ROmcycleh寄存器的只读副本
timeh0xC81ROmtimeh寄存器的只读副本
instreth0xC82ROminstreth寄存器的只读副本

RISC-V 的中断

进入中断

退出中断

中断和异常相关的寄存器

异常相关的CSR寄存器

异常相关的CSR寄存器具体定义

mstatus

  • MIE:为1表示中断的全局开关打开,中断能够被正常响应
  • FS:维护浮点单元的状态。上电默认为0,表示Off,为了能够正常使用浮点单元,软件需要使用 CSR 写指令将 FS 的值改写为非 0 值以打开浮点单元的功能。操作系统在进行上下文切换的时候,需要通过该值来判断是否需要对浮点单元进行上下文的保存
  • XS:维护用户自定义的扩展指令单元状态,类似与 FS

mtvec

mtvec寄存器

异常代码

异常代码

中断返回

中断返回

中断屏蔽与中断等待

中断屏蔽和等待相关额寄存器

中断优先级

中断优先级

单指令数据通路的中断响应与退出

中断响应

中断退出