Chisel芯片开发入门系列 -- 15. CPU芯片开发和解释5(一整套RISC-V指令集)

继续接上一篇,从04_lw到05_riscvtests目录。对比一下两个目录下的文件如下:

其中主要的改动是Core.scala。这个改动不再是+1的小改动,而是从Load/Store共2个指令扩展为一整套RISC-V指令集(30多个指令)的实现,其中当然继续包括Load/Store指令,还包括了加减乘除算术运算、左移右移等位运算、条件判断跳转的控制,可以想见这三类指令的实现几乎就是一个完备的计算机了,不管多高级的语言编写的多复杂的程序,最后落地到计算机硬件上大体应该就是算术移位和条件跳转了。

这里不再详细比较04和05的Core.scala差异,而是列出05_riscvtests目录下的Core.scala文件进行分析:

package riscvtests

import chisel3._
import chisel3.util._
import common.Instructions._
import common.Consts._

class Core extends Module {
  val io = IO(
    new Bundle {
      val imem = Flipped(new ImemPortIo())
      val dmem = Flipped(new DmemPortIo())
      val exit = Output(Bool())
      val gp   = Output(UInt(WORD_LEN.W))
    }
  )

  val regfile     = Mem(32,   UInt(WORD_LEN.W))
  val csr_regfile = Mem(4096, UInt(WORD_LEN.W))


  //**********************************
  // Instruction Fetch (IF) Stage

  val pc_reg = RegInit(START_ADDR)
  io.imem.addr := pc_reg
  val inst = io.imem.inst
  val pc_plus4 = pc_reg + 4.U(WORD_LEN.W)
  val br_target = Wire(UInt(WORD_LEN.W))
  val br_flg = Wire(Bool())
  val jmp_flg = (inst === JAL || inst === JALR)
  val alu_out = Wire(UInt(WORD_LEN.W))

  val pc_next = MuxCase(pc_plus4, Seq(
    br_flg  -> br_target,
    jmp_flg -> alu_out,
    (inst === ECALL) -> csr_regfile(0x305) // go to trap_vector
  ))
  pc_reg := pc_next


  //**********************************
  // Instruction Decode (ID) Stage

  val rs1_addr = inst(19, 15)
  val rs2_addr = inst(24, 20)
  val wb_addr  = inst(11, 7)
  val rs1_data = Mux((rs1_addr =/= 0.U(WORD_LEN.U)), regfile(rs1_addr), 0.U(WORD_LEN.W))
  val rs2_data = Mux((rs2_addr =/= 0.U(WORD_LEN.U)), regfile(rs2_addr), 0.U(WORD_LEN.W))

  val imm_i = inst(31, 20)
  val imm_i_sext = Cat(Fill(20, imm_i(11)), imm_i)
  val imm_s = Cat(inst(31, 25), inst(11, 7))
  val imm_s_sext = Cat(Fill(20, imm_s(11)), imm_s)
  val imm_b = Cat(inst(31), inst(7), inst(30, 25), inst(11, 8))
  val imm_b_sext = Cat(Fill(19, imm_b(11)), imm_b, 0.U(1.U))
  val imm_j = Cat(inst(31), inst(19, 12), inst(20), inst(30, 21))
  val imm_j_sext = Cat(Fill(11, imm_j(19)), imm_j, 0.U(1.U))
  val imm_u = inst(31,12)
  val imm_u_shifted = Cat(imm_u, Fill(12, 0.U))
  val imm_z = inst(19,15)
  val imm_z_uext = Cat(Fill(27, 0.U), imm_z)

  val csignals = ListLookup(inst,
               List(ALU_X    , OP1_RS1, OP2_RS2, MEN_X, REN_X, WB_X  , CSR_X),
    Array(
      LW    -> List(ALU_ADD  , OP1_RS1, OP2_IMI, MEN_X, REN_S, WB_MEM, CSR_X),
      SW    -> List(ALU_ADD  , OP1_RS1, OP2_IMS, MEN_S, REN_X, WB_X  , CSR_X),
      ADD   -> List(ALU_ADD  , OP1_RS1, OP2_RS2, MEN_X, REN_S, WB_ALU, CSR_X),
      ADDI  -> List(ALU_ADD  , OP1_RS1, OP2_IMI, MEN_X, REN_S, WB_ALU, CSR_X),
      SUB   -> List(ALU_SUB  , OP1_RS1, OP2_RS2, MEN_X, REN_S, WB_ALU, CSR_X),
      AND   -> List(ALU_AND  , OP1_RS1, OP2_RS2, MEN_X, REN_S, WB_ALU, CSR_X),
      OR    -> List(ALU_OR   , OP1_RS1, OP2_RS2, MEN_X, REN_S, WB_ALU, CSR_X),
      XOR   -> List(ALU_XOR  , OP1_RS1, OP2_RS2, MEN_X, REN_S, WB_ALU, CSR_X),
      ANDI  -> List(ALU_AND  , OP1_RS1, OP2_IMI, MEN_X, REN_S, WB_ALU, CSR_X),
      ORI   -> List(ALU_OR   , OP1_RS1, OP2_IMI, MEN_X, REN_S, WB_ALU, CSR_X),
      XORI  -> List(ALU_XOR  , OP1_RS1, OP2_IMI, MEN_X, REN_S, WB_ALU, CSR_X),
      SLL   -> List(ALU_SLL  , OP1_RS1, OP2_RS2, MEN_X, REN_S, WB_ALU, CSR_X),
      SRL   -> List(ALU_SRL  , OP1_RS1, OP2_RS2, MEN_X, REN_S, WB_ALU, CSR_X),
      SRA   -> List(ALU_SRA  , OP1_RS1, OP2_RS2, MEN_X, REN_S, WB_ALU, CSR_X),
      SLLI  -> List(ALU_SLL  , OP1_RS1, OP2_IMI, MEN_X, REN_S, WB_ALU, CSR_X),
      SRLI  -> List(ALU_SRL  , OP1_RS1, OP2_IMI, MEN_X, REN_S, WB_ALU, CSR_X),
      SRAI  -> List(ALU_SRA  , OP1_RS1, OP2_IMI, MEN_X, REN_S, WB_ALU, CSR_X),
      SLT   -> List(ALU_SLT  , OP1_RS1, OP2_RS2, MEN_X, REN_S, WB_ALU, CSR_X),
      SLTU  -> List(ALU_SLTU , OP1_RS1, OP2_RS2, MEN_X, REN_S, WB_ALU, CSR_X),
      SLTI  -> List(ALU_SLT  , OP1_RS1, OP2_IMI, MEN_X, REN_S, WB_ALU, CSR_X),
      SLTIU -> List(ALU_SLTU , OP1_RS1, OP2_IMI, MEN_X, REN_S, WB_ALU, CSR_X),
      BEQ   -> List(BR_BEQ   , OP1_RS1, OP2_RS2, MEN_X, REN_X, WB_X  , CSR_X),
      BNE   -> List(BR_BNE   , OP1_RS1, OP2_RS2, MEN_X, REN_X, WB_X  , CSR_X),
      BGE   -> List(BR_BGE   , OP1_RS1, OP2_RS2, MEN_X, REN_X, WB_X  , CSR_X),
      BGEU  -> List(BR_BGEU  , OP1_RS1, OP2_RS2, MEN_X, REN_X, WB_X  , CSR_X),
      BLT   -> List(BR_BLT   , OP1_RS1, OP2_RS2, MEN_X, REN_X, WB_X  , CSR_X),
      BLTU  -> List(BR_BLTU  , OP1_RS1, OP2_RS2, MEN_X, REN_X, WB_X  , CSR_X),
      JAL   -> List(ALU_ADD  , OP1_PC , OP2_IMJ, MEN_X, REN_S, WB_PC , CSR_X),
      JALR  -> List(ALU_JALR , OP1_RS1, OP2_IMI, MEN_X, REN_S, WB_PC , CSR_X),
      LUI   -> List(ALU_ADD  , OP1_X  , OP2_IMU, MEN_X, REN_S, WB_ALU, CSR_X),
      AUIPC -> List(ALU_ADD  , OP1_PC , OP2_IMU, MEN_X, REN_S, WB_ALU, CSR_X),
      CSRRW -> List(ALU_COPY1, OP1_RS1, OP2_X  , MEN_X, REN_S, WB_CSR, CSR_W),
      CSRRWI-> List(ALU_COPY1, OP1_IMZ, OP2_X  , MEN_X, REN_S, WB_CSR, CSR_W),
      CSRRS -> List(ALU_COPY1, OP1_RS1, OP2_X  , MEN_X, REN_S, WB_CSR, CSR_S),
      CSRRSI-> List(ALU_COPY1, OP1_IMZ, OP2_X  , MEN_X, REN_S, WB_CSR, CSR_S),
      CSRRC -> List(ALU_COPY1, OP1_RS1, OP2_X  , MEN_X, REN_S, WB_CSR, CSR_C),
      CSRRCI-> List(ALU_COPY1, OP1_IMZ, OP2_X  , MEN_X, REN_S, WB_CSR, CSR_C),
      ECALL -> List(ALU_X    , OP1_X  , OP2_X  , MEN_X, REN_X, WB_X  , CSR_E)
                )
        )
  val exe_fun :: op1_sel :: op2_sel :: mem_wen :: rf_wen :: wb_sel :: csr_cmd :: Nil = csignals

  val op1_data = MuxCase(0.U(WORD_LEN.W), Seq(
    (op1_sel === OP1_RS1) -> rs1_data,
    (op1_sel === OP1_PC)  -> pc_reg,
    (op1_sel === OP1_IMZ) -> imm_z_uext
  ))

  val op2_data = MuxCase(0.U(WORD_LEN.W), Seq(
    (op2_sel === OP2_RS2) -> rs2_data,
    (op2_sel === OP2_IMI) -> imm_i_sext,
    (op2_sel === OP2_IMS) -> imm_s_sext,
    (op2_sel === OP2_IMJ) -> imm_j_sext,
    (op2_sel === OP2_IMU) -> imm_u_shifted
  ))


  //**********************************
  // Execute (EX) Stage

  alu_out := MuxCase(0.U(WORD_LEN.W), Seq(
    (exe_fun === ALU_ADD)   -> (op1_data + op2_data),
    (exe_fun === ALU_SUB)   -> (op1_data - op2_data),
    (exe_fun === ALU_AND)   -> (op1_data & op2_data),
    (exe_fun === ALU_OR)    -> (op1_data | op2_data),
    (exe_fun === ALU_XOR)   -> (op1_data ^ op2_data),
    (exe_fun === ALU_SLL)   -> (op1_data << op2_data(4, 0))(31, 0),
    (exe_fun === ALU_SRL)   -> (op1_data >> op2_data(4, 0)).asUInt,
    (exe_fun === ALU_SRA)   -> (op1_data.asSInt >> op2_data(4, 0)).asUInt,
    (exe_fun === ALU_SLT)   -> (op1_data.asSInt < op2_data.asSInt).asUInt,
    (exe_fun === ALU_SLTU)  -> (op1_data < op2_data).asUInt,
    (exe_fun === ALU_JALR)  -> ((op1_data + op2_data) & ~1.U(WORD_LEN.W)),
    (exe_fun === ALU_COPY1) -> op1_data
  ))

  // branch
  br_target := pc_reg + imm_b_sext
  br_flg := MuxCase(false.B, Seq(
    (exe_fun === BR_BEQ)  ->  (op1_data === op2_data),
    (exe_fun === BR_BNE)  -> !(op1_data === op2_data),
    (exe_fun === BR_BLT)  ->  (op1_data.asSInt < op2_data.asSInt),
    (exe_fun === BR_BGE)  -> !(op1_data.asSInt < op2_data.asSInt),
    (exe_fun === BR_BLTU) ->  (op1_data < op2_data),
    (exe_fun === BR_BGEU) -> !(op1_data < op2_data)
  ))


  //**********************************
  // Memory Access Stage

  io.dmem.addr  := alu_out
  io.dmem.wen   := mem_wen
  io.dmem.wdata := rs2_data

  // CSR
  val csr_addr = Mux(csr_cmd === CSR_E, 0x342.U(CSR_ADDR_LEN.W), inst(31,20))
  val csr_rdata = csr_regfile(csr_addr)
  val csr_wdata = MuxCase(0.U(WORD_LEN.W), Seq(
    (csr_cmd === CSR_W) -> op1_data,
    (csr_cmd === CSR_S) -> (csr_rdata | op1_data),
    (csr_cmd === CSR_C) -> (csr_rdata & ~op1_data),
    (csr_cmd === CSR_E) -> 11.U(WORD_LEN.W)
  ))

  when(csr_cmd > 0.U){
    csr_regfile(csr_addr) := csr_wdata
  }

  //**********************************
  // Writeback (WB) Stage

  val wb_data = MuxCase(alu_out, Seq(
    (wb_sel === WB_MEM) -> io.dmem.rdata,
    (wb_sel === WB_PC)  -> pc_plus4,
    (wb_sel === WB_CSR) -> csr_rdata
  ))

  when(rf_wen === REN_S) {
    regfile(wb_addr) := wb_data
  }


  //**********************************
  // Debug
  io.gp   := regfile(3)
  io.exit := (pc_reg === 0x44.U(WORD_LEN.W))
  printf(p"io.pc      : 0x${Hexadecimal(pc_reg)}\n")
  printf(p"inst       : 0x${Hexadecimal(inst)}\n")
  printf(p"gp         : ${regfile(3)}\n")
  printf(p"rs1_addr   : $rs1_addr\n")
  printf(p"rs2_addr   : $rs2_addr\n")
  printf(p"wb_addr    : $wb_addr\n")
  printf(p"rs1_data   : 0x${Hexadecimal(rs1_data)}\n")
  printf(p"rs2_data   : 0x${Hexadecimal(rs2_data)}\n")
  printf(p"wb_data    : 0x${Hexadecimal(wb_data)}\n")
  printf(p"dmem.addr  : ${io.dmem.addr}\n")
  printf(p"dmem.rdata : ${io.dmem.rdata}\n")
  printf("---------\n")
}

这里有一个最引人注目的函数调用是val csignals = ListLookup(inst, List(xxx), Array(xxx))!就是将此前取指(fetch)到的指令(inst),在RISC-V整套指令集(位于Array所定义的字典)中进行查找匹配,将匹配的结果保存在csignals变量中。如果没有匹配到字典中的任何一个,则使用ListLookup函数中的第二个参数List(xxx)作为返回结果。Array(xxx)作为一个字典,每一个元素是一个用key->value表达的字典,位于->左边的是key,位于->右边的是value,在本例子中value又是一个复合体,这个复合体和第二个参数List(xxx)格式完全一样,是一个含有7个字段的List。对于取指fetch到的指令inst进行字典查找的过程,就是使用inst和字典中的每一个key值进行匹配的过程。

这个ListLookup函数是chisel的一个API函数,可以在chisel-lang官网查阅API的说明如下,其中的第二个参数是字典查找没有匹配时返回的默认值。

随后,使用val x :: y :: z :: w = csignals将csignals中的7个成员分别赋值给x, y, z等变量,其中的x实际为exe_func!

第二个引人注目的地方,是alu_out = MuxCase(0, Seq(xxx))这一个函数调用,对于算术运算(加减乘除)、逻辑运算(与或非)、移位运算(左移右移)都是在这里完成!显然这是实现“计算机”的“计算能力”的最直接的体现代码体现。这个MuxCase函数的第二个参数,又又又是一个字典,Seq和Array看起来都是字典!但是这个MuxCase函数没有像ListLookup函数中携带的第一个参数(inst)那样,字典的Key(也就是->的左侧)不是一个匹配inst的值、而是一个(xxx === XXX)的判断条件。MuxCase的第一个参数,表示的是没有匹配到Seq字典中的任何情况时作为函数的默认值返回给alu_out,这一点又和ListLookup函数的第二个参数相似!

这个MuxCase函数是chisel的一个API函数,可以在chisel-lang官网查阅API的说明如下,其中的第一个参数是字典查找没有匹配时返回的默认值。此前的系列文章中有用到Mux函数,本文上面的代码也有Mux函数的使用,可以对照Mux和MuxCase来更好地理解它们!

继续以alu_out这一变量为线索梳理代码,并结合常识来理解加法运算指令的结果如何写到内存中。之所以要结合常识来理解,是因为单纯的代码来看会很快陷入疑问中。看一下alu_out使用之处(总共5处):

32行:val alu_out = Wire(UInt(WORD_LEN.W))

36行:val pc_next = MuxCase(xxx, Seq(jmp_flg -> alu_out))

127行:alu_out = MuxCase(0, Seq(xxx))

157行:io.dmem.addr  := alu_out

178行:val wb_data = MuxCase(alu_out, Seq(xxx))

这其中的第36行和第157行中,alu_out都是被当做内存地址(或内存地址的偏移量)来使用的!而我们常规理解的算术运算、逻辑运算和位运算中使用ALU的地方以及第127行的alu_out值的获得,都是内存地址的值而不是内存地址。这就是上面说的“陷入疑问”的问题了。

事实上,在本系列前面的文章还没有设计到ADD/ADDI等算术运算指令、只涉及Load/Store指令时,就使用了alu_out这样一个变量名表示内存地址的!

另外在本例子第127行的Array(xxx)的字典中,LW和SW指令与ADD和ADDI指令对应的操作码(也就是存放到exe_func变量中的值)一样,都是ALU_ADD,也就是说Load/Store指令的执行过程,也需要使用类似ALU的加法运算。这个加法运算不是使用ALU硬件单元,而是仅仅借用了ALU_ADD这样一个符号,以及127行处的加法代码,实现内存地址与内存地址偏移量的加法!

从常识上来说,ADD/ADDI/SUB/SUBI等算术运算、逻辑运算、移位运算的结果,最终是要通过Store写到数据内存中,这是通过两个指令(ADD和SW指令)实现的。在SW指令中,要获得Store的内存地址,需要使用加法将内存地址(位于寄存器组regfile中的某个寄存器的值,其中的寄存器是SW指令字中的5bit的wb_addr字段指定的) 加上位于SW指令字中的偏移量(12bit带符号的偏移量)。在SW指令中,还涉及到某个存储数据的寄存器(也就是算术运算的结果),这个应该就是第178行的赋值了!单纯的算术运算指令ADD/ADDI,在Store指令之前已经完成了!

假设我们运行的指令序列是ADD和SW,那么:

第一个ADD指令执行的时候,第127行->第157行->第178行依次执行,其中第127行计算的alu_out就是ADD指令的计算结果。第157行会将alu_out赋值给io.dmem.addr,但这个不会被使用。178行会将alu_out变量的值赋给wb_data(因为ADD执行下wb_sel变量的值等于WB_ALU)。第185行还会将这个wb_data存放到regfile(wb_addr)寄存器中,留给下一个指令SW使用!

第二个SW执行执行的时候,第127行->第157行->第178行依次执行,其中的第127行会计算内存地址的偏移(也就是wb_addr相关的地址计算),并保存到alu_out中。第157行会将alu_out赋值给io.dmem.addr。第159行会将rs2_data赋给io.dmem.wdata(这个rs2_data就是上一步ADD指令中的wb_data,靠编译器将代码编译为指令的时候对应上)。第158行对io.dmem.wen赋值MEM_S(Array字典中的SW这一行的MEM_S列)。到此SW其实已经就执行完了,io.dmem的三个信号wen, addr, wdata都准备好了,后端的Memory模块会在时钟驱动下将信号上的wdata写到信号addr所指定的内存中!

至此,通过代码并结合{一个ADD指令+一个SW指令}的梳理,大体上理解了整个Core.scala的运行过程。这个ADD指令换为其他算术指令、逻辑运算指令、移位运算指令,也是完全一样的。梳理一下所涉及的指令:

红色框内的LW/SW是Load/Store指令,是RISC系列CPU与CISC系列CPU(x86等)的关键区别。

绿色框内的是条件跳转(BRanch)指令。

位于红色框和绿色框之间的,是运算指令,包括算术运算(加减乘除)、逻辑运算(与或非异或)、移位运算(左移右移)。一般来说这些运算的操作数都在寄存器上,但有一部分以是直接操作数,也就是指令字中的部分bit作为运算操作的操作数!这些指令都带有一个I字符,比如ADDI或者ORI。这些计算类指令的其他字段都是完全一样的,仅这个OP2_IMI稍微特殊一些,用蓝色标识出来,对应的op2_data的构造会区分。

位于绿色框之后的,是JAL/JALR等指令,以及CSRxxx的系列执行,以及ECALL的执行。这其中又以CSR系列指令最值得关注!因为CSR(控制和状态寄存器)是CPU指令集的一个关键概念,用于多个指令连贯执行时候的状态记录,类似上面举例子的{ADD指令 + SW指令}的组合,没有用到CSR指令,但需要编译器在编译高级语言程序 到 底层的RISC-V CPU的时候,分配和使用regfile中的某一个寄存器并在ADD指令和SW指令中对应上!这个CSR要留待后面分析,这里先备忘一下,TBD。

至此,已经看到了RISC-V的一整套指令集共38条指令(第67-104行),以及加减乘除运算的实际执行过程!这个实际计算过程的完整闭环,还需要Load/Store这两个指令将计算结果从寄存器写到内存中!这中间涉及到先序步骤ADD等运算结果保存到寄存器中,以及后序步骤SW将寄存器中的运算结果“搬运”到内存中,这两者的衔接,需要编译器的支持,包括从寄存器组regfile中分配哪个寄存器给先序后序的指令使用。更复杂的情况,大概就会涉及到CSR系列指令的使用!

另外,从05和04的Top.scala对比来看,05的Top模块增加了了一个io.gp = Core.io.gp,在上面Core.scare代码中的io.gp := regfile(3)明确了这个模块的输出信号,实际上就是把regfile(3)这个寄存器值赋值到输出信号上。这个是一个Global Pointer(全局指针),主要用途是快速访问程序中的全局变量和静态数据。但在目前的这个代码里暂时还没有使用。

最后总结一下:本文介绍的是一整套RISC-V的指令集的CPU芯片开发Demo代码,这一整套RISC-V指令集有38条指令,包括Load/Store、运算指令(算术-逻辑-移位)、条件跳转(BRanch)等常见的,还包括一个CSR的概念及配套的CSR系列指令。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ChipCamp

谢谢支持和鼓励!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值