天天看點

吃透Chisel語言.40.Chisel實戰之單周期RISC-V處理器實作(下)——具體實作和最終測試Chisel實戰之單周期RISC-V處理器實作(下)——具體實作和最終測試

Chisel實戰之單周期RISC-V處理器實作(下)——具體實作和最終測試

上一篇文章中我們對本項目的需求進行了分析,并得到了初步的設計,這一篇文章我們就可以基于該設計來實作我們的單周期RISC-V處理器了。實作之後也必須用實際代碼來測試一下,至少也得能運作遞歸版本的斐波那契數列計算。完整項目代碼可以在本人的Github倉庫擷取:github-3rr0r/RV32ISC: A RISC-V RV32I ISA Single Cycle CPU。接下來我們直接進入正題!

實作思路

再次放上我們的設計圖:

吃透Chisel語言.40.Chisel實戰之單周期RISC-V處理器實作(下)——具體實作和最終測試Chisel實戰之單周期RISC-V處理器實作(下)——具體實作和最終測試

根據上篇文章的分析,我們設計的CPU中應該至少需要包含以下元件:

  1. 指令記憶體(

    MemInst

    ):接收一個32位的指令位址,讀取出指令;
  2. PC寄存器(

    PCReg

    ):為指令記憶體提供指令位址,每個時鐘周期位址都會+4,目前指令為跳轉時,下一條指令為跳轉目的位址,目前指令為分支指令且分支成功時,下一條指令為分支目标位址;
  3. 通用寄存器堆(

    Registers

    ):可讀可寫的寄存器,接收寄存器号,為運算單元提供操作數,接收運算結果或從資料記憶體讀取到的值;
  4. 資料記憶體(

    MemData

    ):根據加載/存儲位址,加載或存儲資料,加載或存儲依賴于譯碼器的譯碼;
  5. 指令譯碼器(

    Decoder

    ):對指令進行譯碼,解析得到立即數、操作碼、寄存器号等資訊;
  6. 運算單元(

    ALU

    ):根據操作數和操作碼進行運算,運算結果寫到寄存器,分支指令時将比較結果發送給PC,加載存儲指令時計算位址;
  7. 控制單元(

    Controller

    ):根據譯碼結果,給出對資料通路進行控制的相關信号;

子產品化思維必須要有,如果把一個複雜的系統全都塞到一個代碼檔案裡面,不管是編寫、測試還是調試、疊代,都會有巨大的麻煩在等着你。而子產品化的設計可以讓子產品之間互相獨立,可以分别編寫、測試各個子產品,修改時也不會影響其他子產品。

由于我們在設計時已經将系統劃分出各個子產品,是以分别實作為一個Chisel的

Module

,然後用一個頂層的

Module

将它們連接配接起來就可以了。

全局配置檔案

磨刀不誤砍柴工,在實作各個子產品之前,我們做一件事會對我們很有幫助,那就是建立一個全局配置檔案。

為什麼要這麼做呢?比如,我們實作的是32位的處理器,那麼32這個數字肯定在編寫過程中經常使用。我們當然可以在所有檔案中都使用32這個數字,但是擴充性就會有很大的問題。比如要把項目遷移到64位實作,我們隻能一個個數字去修改,而如果有全局配置檔案,我們就可以直接在裡面用一個對象來存放32這個數,在需要用32的地方使用這個對象,如果需要改成64,那麼直接修改該對象的值就行了。

上面的說法可能不太嚴謹,但适用場合很多,拿我們的項目來說,雖然隻是個單周期32位處理器,但也還是有其他東西是可以配置的,比如記憶體初始化的位址等。複雜的項目可能還可以用于配置Cache的參數、記憶體的端口數等。總之,這是個好的做法,我們應該形成這種思維。

那麼我們在

src/main/scala/config

檔案夾下建立一個

Configs.scala

檔案,内容暫時如下:

package config

import chisel3._

object Configs {
    val ADDR_WIDTH = 32 // 位址位寬
    val ADDR_BYTE_WIDTH = ADDR_WIDTH / 8    // 位址位寬按位元組算
    val DATA_WIDTH = 32 // 資料位寬
    val DATA_WIDTH_H = 16   // 半字資料位寬
    val DATA_WIDTH_B = 8    // 位元組資料位寬
}
           

注意,

config

檔案夾是自己建立的,使用

Configs.scala

中的對象時,将檔案夾名作為包名使用,導入所有參數即可:

import config.Configs._
           

後續如果有需要添加的配置,在此配置檔案中添加即可。

接下來我們開始各個子產品的實作,首先就是PC寄存器子產品。

PC寄存器的實作

PC寄存器的功能如下:

  1. 32位的指令位址輸出,為指令記憶體提供位址以擷取指令;
  2. 每個時鐘周期,PC寄存器中的指令位址都會自增4,以擷取下一個位址;
  3. 如果目前指令為跳轉指令,會接收到控制單元的信号

    ctrlJump

    ,接收計算結果(跳轉位址)作為下一個位址;
  4. 如果目前指令為分支指令,會接收到控制單元的信号

    ctrlBranch

    ,以及運算單元的分支結果,如果分支成功,接收計算結果(跳轉位址)作為下一個位址;
  5. 寄存器初始化時,輸出的指令位址為0;

是以我們可以這麼實作

PCReg

子產品(

src/main/scala/rv32isc/PCReg.scala

):

package rv32isc

import chisel3._
import chisel3.util._

import config.Configs._

// PCReg的子產品接口
class PCRegIO extends Bundle {
    val addrOut = Output(UInt(ADDR_WIDTH.W))      // 位址輸出
    val ctrlJump = Input(Bool())                // 目前指令是否為跳轉指令
    val ctrlBranch = Input(Bool())              // 目前指令是否為分支指令
    val resultBranch = Input(Bool())            // 分支結果是否為分支成功
    val addrTarget = Input(UInt(ADDR_WIDTH.W))    // 跳轉/分支的目的位址
}

// PCReg子產品
class PCReg extends Module {
    val io = IO(new PCRegIO())  // 輸入輸出接口

    val regPC = RegInit(UInt(ADDR_WIDTH.W), START_ADDR.U)   // PC寄存器,初始化時重置為START_ADDR

    when (io.ctrlJump || (io.ctrlBranch && io.resultBranch)) {  // 跳轉或分支成功時,更新為目的位址
        regPC := io.addrTarget
    } .otherwise {  // 否則自增4
        regPC := regPC + ADDR_BYTE_WIDTH.U
    }

    io.addrOut := regPC // 每個時鐘周期輸出目前PC寄存内的位址
}
           

注意,這裡我們用到了一個常量

START_ADDR

用于表示起始執行位址,應該在

Configs

對象中包含:

接着,我們可以建立

PCReg

子產品對應的測試(

src/test/scala/rv32isc/PCRegTest.scala

):

package rv32isc

import chisel3._
import chiseltest._
import chisel3.util._
import org.scalatest.flatspec.AnyFlatSpec

import config.Configs._

trait PCRegTestFunc {
    // 生成十個随機位址用于測試
    val target_list =
        Seq.fill(10)(scala.util.Random.nextInt().toLong & 0x00ffffffffL)

    def testFn(dut: PCReg): Unit = {
        // 初始化狀态
        dut.io.ctrlBranch.poke(false.B)
        dut.io.ctrlJump.poke(false.B)
        dut.io.resultBranch.poke(false.B)
        dut.io.addrTarget.poke(START_ADDR)
        dut.io.addrOut.expect(START_ADDR)
        
        var addr: Long = START_ADDR

        // 正常自增功能
        for (target <- target_list) {
            dut.io.addrTarget.poke(target.U)
            addr += ADDR_BYTE_WIDTH
            dut.clock.step()
            dut.io.addrOut.expect(addr.U)
        }

        // 跳轉功能測試
        dut.io.ctrlJump.poke(true.B)
        for (target <- target_list) {
            dut.io.addrTarget.poke(target.U)
            dut.clock.step()
            dut.io.addrOut.expect(target.U)
            addr = target
        }
        dut.io.ctrlJump.poke(false.B)

        // 分支功能測試
        // 分支指令,但分支不成功
        dut.io.ctrlBranch.poke(true.B)
        for (target <- target_list) {
            dut.io.addrTarget.poke(target.U)
            addr += ADDR_BYTE_WIDTH
            dut.clock.step()
            dut.io.addrOut.expect(addr.U)
        }

        // 分支指令,且分支成功
        dut.io.resultBranch.poke(true.B)
        for (target <- target_list) {
            dut.io.addrTarget.poke(target.U)
            addr += ADDR_BYTE_WIDTH
            dut.clock.step()
            dut.io.addrOut.expect(target.U)
            addr = target
        }
    }
}

class PCRegTest extends AnyFlatSpec with ChiselScalatestTester with PCRegTestFunc {
    "PCReg" should "pass" in {
        test(new PCReg) { dut =>
            testFn(dut)
        } 
    }
}
           

測試通過。

PC寄存器将指令位址給了指令記憶體,指令記憶體給出這一周期要執行的指令,那麼接下來我們就實作指令記憶體。

指令記憶體的實作

指令記憶體的工作邏輯特别簡單:

  1. 接收一個32位的指令位址;
  2. 根據指令位址輸出對應的指令;

Mem

生成的記憶體為異步讀、同步寫的記憶體,得到位址後輸出指令不含時序,為組合電路,是以實作起來也特别簡單:

package rv32isc

import chisel3._
import chisel3.util._

import config.Configs._

class MemInstIO extends Bundle {
    val addr = Input(UInt(ADDR_WIDTH.W))    // 指令位址
    val inst = Output(UInt(INST_WIDTH.W))   // 指令輸出
}

class MemInst extends Module {
    val io = IO(new MemInstIO())    // 輸入輸出接口

    // 指令記憶體,能存放MEM_INST_SIZE條INST_WIDTH位的指令
    val mem = Mem(MEM_INST_SIZE, UInt(INST_WIDTH.W))

    io.inst := mem.read(io.addr >> INST_BYTE_WIDTH_LOG.U)    // 讀取對應位置的指令并輸出
}
           

同樣,這裡我們使用了兩個常量需要添加到

Configs

對象裡:

val INST_WIDTH = 32 // 指令位寬
val INST_BYTE_WIDTH = INST_WIDTH / 8 // 指令位寬按位元組算
val INST_BYTE_WIDTH_LOG = ceil(log(INST_BYTE_WIDTH) / log(2)).toInt // 指令位址對齊的偏移量
val MEM_INST_SIZE = 1024 // 指令記憶體大小
           

我這裡着重解釋一下

mem.read(io.addr >> INST_BYTE_WIDTH_LOG.U)

和常量

INST_BYTE_WIDTH_LOG = ceil(log(INST_BYTE_WIDTH) / log(2)).toInt

:由于指令的寬度為32位,是以每次讀取指令時指令位址都要對齊到4位元組。而我們的指令記憶體的每個資料項都是四位元組的,是以應該用輸入的指令位址的高30位通路,即右移兩位。

不過,在測試上會稍微麻煩一點,我們需要先向記憶體中寫入一些模拟的指令填滿記憶體,在記憶體初始化的時候用

loadMemoryFromFile

寫入

mem

。我們在

MemInst

類中添加初始化相關代碼:

注意:

  1. loadMemoryFromFile

    需要導入包:

    import chisel3.util.experimental.loadMemoryFromFile

  2. MemoryLoadFileType.Hex

    需要導入包:

    import firrtl.annotations.MemoryLoadFileType

  3. src/test/scala/rv32isc/MemInst.hex

    檔案為在測試代碼中生成的随機文本檔案,共1024行,每一行都是一串8字元的随機的16進制數,用于模拟32位的指令,具體生成代碼見

    src/test/scala/rv32isc/MemInstTest.scala

  4. loadMemoryFromFile

    隻會在測試的時候執行;

下面我們就可以寫測試代碼了:

package rv32isc

import chisel3._
import chiseltest._
import chisel3.util._
import org.scalatest.flatspec.AnyFlatSpec

import java.io.PrintWriter
import java.io.File

import config.Configs._

trait MemInstTestFunc {
    // 生成MEM_INST_SIZE條随機指令進行測試
    val inst_list =
        Seq.fill(MEM_INST_SIZE)(scala.util.Random.nextInt().toLong & 0x00ffffffffL)

    // 為随機指令生成hex文本檔案
    def genMemInstHex(): Unit = {
        val memFile = new File(
          System.getProperty("user.dir") + "/src/test/scala/rv32isc/MemInst.hex"
        )
        memFile.createNewFile()
        val memPrintWriter = new PrintWriter(memFile)
        for (i <- 0 to MEM_INST_SIZE - 1) {
            memPrintWriter.println(inst_list(i).toHexString)
        }
        memPrintWriter.close()
    }

    def testFn(dut: MemInst): Unit = {
        // 依次讀取所有的指令,與inst_list進行比對
        for (i <- 0 to MEM_INST_SIZE - 1) {
            dut.io.addr.poke((i * INST_BYTE_WIDTH).U)   // 作為位址,應該左移兩位,即乘以4
            dut.io.inst.expect(inst_list(i).U)
        }
    }
}

class MemInstTest extends AnyFlatSpec with ChiselScalatestTester with MemInstTestFunc {
    "MemInst" should "pass" in {
        // 先生成随機hex檔案,再進行測試
        genMemInstHex()
        test(new MemInst) { dut =>
            testFn(dut)
        } 
    }
}
           

測試通過。

得到指令之後,我們就可以對指令進行譯碼,是以我們下一步要實作譯碼單元。

譯碼單元的實作

要對指令進行譯碼,首先我們需要對指令格式有清晰地了解,再次放上指令格式的圖:

吃透Chisel語言.40.Chisel實戰之單周期RISC-V處理器實作(下)——具體實作和最終測試Chisel實戰之單周期RISC-V處理器實作(下)——具體實作和最終測試

我們用

inst

表示指令,那麼很顯然有以下幾個規律:

  1. inst[6:0]

    都是

    opcode

  2. rs1

    rs2

    rd

    分别固定在

    inst[19:15]

    inst[24:20]

    inst[11:7]

    上;
  3. 功能碼

    funct3

    funct7

    分别位于

    inst[14:12]

    inst[31:25]

  4. 立即數的分布比較複雜;

我們的思路可以這麼來:

  1. opcode

    funct3

    funct7

    的解析結果交給控制子產品(

    Controller

    ),由控制子產品控制

    ALU

    的行為;
  2. rs1

    rs2

    rd

    直接交給寄存器堆(

    Registers

    ),由控制子產品控制是否寫寄存器,而寄存器時鐘讀寄存并交給

    ALU

  3. 譯碼單元自己計算立即數

    imm

    ,根據指令格式分類選擇輸出正确的立即數給

    ALU

接下來我們分析RV32I的指令中,可以如何根據

opcode

對應到指令格式類型上:

  1. U類型隻有兩個條指令

    LUI

    AUIPC

    inst[6:2]

    分别為

    b01101

    b00101

  2. J類型隻有一個

    JAL

    inst[6:2]

    b11011

  3. I類型有

    JALR

    、LOAD類指令、立即數算術邏輯類指令,

    inst[6:2]

    b11001

    b00000

    b00100

    三種;
  4. B類型都是條件分支類指令,

    inst[6:2]

    b11000

  5. S類型都是STORE類指令,

    inst[6:2]

    b01000

  6. R類型都是算術邏輯類指令,

    inst[6:2]

    b01100

我們再分析指令在

ALU

上的行為:

  1. 算術運算:加法、減法
  2. 邏輯運算:與、或、異或
  3. 位運算:邏輯左移、邏輯右移、算術右移
  4. 比較運算:等于、不等于、小于、大于等于
  5. 無:空操作

可以用四位二進制數對上面的行為進行編碼(空操作歸為算術運算):

操作 類型 信号編碼

NOP

算術運算

00_00

加法 算術運算

00_01

減法 算術運算

00_10

邏輯運算

01_00

邏輯運算

01_01

異或 邏輯運算

01_11

邏輯左移 位運算

10_00

邏輯右移 位運算

10_01

算術右移 位運算

10_11

等于 比較運算

11_00

不等于 比較運算

11_01

小于 比較運算

11_10

大于等于 比較運算

11_11

通過對

opcode

funct3

funct7

可以得到上面的編碼,在此不做贅述,後面看代碼就行。

對于這種寫死,我們不能每次都照着二進制數去寫,有一個好方法就是專門用一個檔案來存放這種寫死,我們這裡就建立一個

src/main/scala/utils/HardCodes.scala

檔案,内容如下:

package utils

import chisel3._

object OP_TYPES {
    val OP_TYPES_WIDTH = 4
    val OP_NOP = "b0000".U
    val OP_ADD = "b0001".U
    val OP_SUB = "b0010".U
    val OP_AND = "b0100".U
    val OP_OR = "b0101".U
    val OP_XOR = "b0111".U
    val OP_SLL = "b1000".U
    val OP_SRL = "b1001".U
    val OP_SRA = "b1011".U
    val OP_EQ = "b1100".U
    val OP_NEQ = "b1101".U
    val OP_LT = "b1110".U
    val OP_GE = "b1111".U
}
           

另外,我們還注意到幾條末尾是

U

的指令,它們要求将操作數作為無符号數來進行運算,是以

ALU

應該還有一個輸入用于訓示

ALU

進行無符号運算還是有符号運算,用一位信号

ctrlSigned

就行。

還有其他的是否分支、是否跳轉、是否加載、是否存儲、

rs1

是否為PC、

rs2

是否為立即數等,都比較簡單,分析方法類似,直接放上代碼吧。下面就是

src/main/scala/rv32isc/Decoder.scala

的具體實作:

package rv32isc

import chisel3._
import chisel3.util._

import config.Configs._
import utils.OP_TYPES._
import utils.LS_TYPES._
import utils._

class DecoderIO extends Bundle {
    val inst = Input(UInt(INST_WIDTH.W))
    val bundleCtrl = new BundleControl()
    val bundleReg = new BundleReg()
    val imm = Output(UInt(DATA_WIDTH.W))
}

class Decoder extends Module {
    val io = IO(new DecoderIO())

    // 三個寄存器号
    io.bundleReg.rs1 := io.inst(19, 15)
    io.bundleReg.rs2 := io.inst(24, 20)
    io.bundleReg.rd := io.inst(11, 7)

    // 五種立即數
    val imm_i = Cat(Fill(20, io.inst(31)), io.inst(31, 20))
    val imm_s = Cat(Fill(20, io.inst(31)), io.inst(31, 25), io.inst(11, 7))
    val imm_b = Cat(Fill(20, io.inst(31)), io.inst(7), io.inst(30, 25), io.inst(11, 8), 0.U(1.W))
    val imm_u = Cat(io.inst(31, 12), Fill(12, 0.U))
    val imm_j = Cat(Fill(12, io.inst(31)), io.inst(31), io.inst(19, 12), io.inst(20), io.inst(30, 21), Fill(1, 0.U))
    // 和用于移位的shamt
    val imm_shamt = Cat(Fill(27, 0.U), io.inst(24, 20))

    // 用于立即數輸出
    val imm = WireDefault(0.U(32.W))

    // 用于控制信号
    val ctrlJump = WireDefault(false.B)
    val ctrlBranch = WireDefault(false.B)
    val ctrlRegWrite = WireDefault(true.B)
    val ctrlLoad = WireDefault(false.B)
    val ctrlStore = WireDefault(false.B)
    val ctrlALUSrc = WireDefault(false.B)
    val ctrlJAL = WireDefault(false.B)
    val ctrlOP = WireDefault(0.U(OP_TYPES_WIDTH.W))
    val ctrlSigned = WireDefault(true.B)

    // 根據opcode對控制信号指派
    switch (io.inst(6, 2)) {
        // U: LUI, AUIPC
        is ("b01101".U, "b00101".U) {
            ctrlALUSrc := true.B
            ctrlOP := OP_ADD
            imm := imm_u
        }
        // J: JAL
        is ("b11011".U) {
            ctrlALUSrc := true.B
            ctrlJump := true.B
            ctrlOP := OP_ADD
            ctrlJAL := true.B
            imm := imm_j
        }
        // I: JALR, 
        // I: LB, LH, LW, LBU, LHU
        // I: ADDI, SLTI, SLTIU, XORI, ORI, ANDI, SLLI, SRLI, SRAI
        is ("b11001".U, "b00000".U, "b00100".U) {
            ctrlALUSrc := true.B
            // JALR
            when (io.inst(6, 2) === "b11001".U) {
                ctrlJump := true.B
                ctrlOP := OP_ADD
                imm := imm_i
            }
            // LOAD
            .elsewhen (io.inst(6, 2) === "b00000".U) {
                ctrlLoad := true.B
                ctrlOP := OP_ADD
                imm := imm_i
                when (io.inst(14, 12) === "b100".U | io.inst(14, 12) === "b101".U) {
                    ctrlSigned := false.B
                }
            }
            // AL
            .elsewhen (io.inst(6, 2) === "b00100".U && (io.inst(14, 12) === "b001".U || io.inst(14, 12) === "b101".U)) {
                imm := imm_shamt
                switch (Cat(io.inst(30), io.inst(14, 12))) {
                    // SLLI
                    is ("b0001".U) {
                        ctrlOP := OP_SLL
                    }
                    // SRLI
                    is ("b0101".U) {
                        ctrlOP := OP_SRL
                    }
                    // SRAI
                    is ("b1101".U) {
                        ctrlOP := OP_SRA
                    }
                }
            } .otherwise {
                imm := imm_i
                switch (io.inst(14, 12)) {
                    // ADDI
                    is ("b000".U) {
                        ctrlOP := OP_ADD
                    }
                    // SLTI
                    is ("b010".U) {
                        ctrlOP := OP_LT
                    }
                    // SLTIU
                    is ("b011".U) {
                        ctrlOP := OP_LT
                        ctrlSigned := false.B
                    }
                    // XORI
                    is ("b100".U) {
                        ctrlOP := OP_XOR
                    }
                    // ORI
                    is ("b110".U) {
                        ctrlOP := OP_OR
                    }
                    // ANDI
                    is ("b111".U) {
                        ctrlOP := OP_AND
                    }
                }
            }
        }
        // B: BEQ, BNE, BLT, BGE, BLTU, BGEU
        is ("b11000".U) {
            ctrlALUSrc := false.B
            ctrlBranch := true.B
            ctrlRegWrite := false.B
            imm := imm_b
            switch (io.inst(14, 12)) {
                // BEQ
                is ("b000".U) {
                    ctrlOP := OP_EQ
                }
                // BNE
                is ("b001".U) {
                    ctrlOP := OP_NEQ
                }
                // BLT
                is ("b100".U) {
                    ctrlOP := OP_LT
                }
                // BGE
                is ("b101".U) {
                    ctrlOP := OP_GE
                }
                // BLTU
                is ("b110".U) {
                    ctrlOP := OP_LT
                    ctrlSigned := false.B
                }
                // BGEU
                is ("b111".U) {
                    ctrlOP := OP_GE
                    ctrlSigned := false.B
                }
            }
        }
        // S: SB, SH, SW
        is ("b01000".U) {
            ctrlALUSrc := true.B
            ctrlStore := true.B
            ctrlRegWrite := false.B
            ctrlOP := OP_ADD
            imm := imm_s
            when (io.inst(14, 12) === "b000".U) {
                ctrlLSType := LS_B
            }
            when (io.inst(14, 12) === "b001".U) {
                ctrlLSType := LS_H
            }
        }
        // R: ADD, SUB, SLL, SLT, SLTU, XOR, SRL, SRA, OR, AND
        is ("b01100".U) {
            switch (io.inst(14, 12)) {
                // ADD, SUB
                is ("b000".U) {
                    when (io.inst(30)) {
                        ctrlOP := OP_SUB
                    } .otherwise {
                        ctrlOP := OP_ADD
                    }
                }
                // SLL
                is ("b001".U) {
                    ctrlOP := OP_SLL
                }
                // SLT
                is ("b010".U) {
                    ctrlOP := OP_LT
                }
                // SLTU
                is ("b011".U) {
                    ctrlOP := OP_LT
                    ctrlOP := false.B
                }
                // XOR
                is ("b100".U) {
                    ctrlOP := OP_XOR
                }
                // SRL, SRA
                is ("b101".U) {
                    when (io.inst(30)) {
                        ctrlOP := OP_SRA
                    } .otherwise {
                        ctrlOP := OP_SRL
                    }
                }
                // OR
                is ("b110".U) {
                    ctrlOP := OP_OR
                }
                // AND
                is ("b111".U) {
                    ctrlOP := OP_AND
                }
            }
        }
    }

    // 連接配接控制信号和立即數
    io.bundleCtrl.ctrlALUSrc := ctrlALUSrc
    io.bundleCtrl.ctrlBranch := ctrlBranch
    io.bundleCtrl.ctrlJAL := ctrlJAL
    io.bundleCtrl.ctrlJump := ctrlJump
    io.bundleCtrl.ctrlLoad := ctrlLoad
    io.bundleCtrl.ctrlOP := ctrlOP
    io.bundleCtrl.ctrlRegWrite := ctrlRegWrite
    io.bundleCtrl.ctrlSigned := ctrlSigned
    io.bundleCtrl.ctrlStore := ctrlStore
    io.imm := imm
}
           

其中,我們使用了兩個Bundle,由于這個Bundle可以在子產品之間複用,是以我們将其放到一個單獨的Bundle檔案中供使用(

src/main/scala/utils/Bundles.scala

):

package utils

import chisel3._

import config.Configs._
import utils.OP_TYPES._

// 用于連接配接控制子產品的Bundle
class BundleControl extends Bundle {
    val ctrlJump = Output(Bool())
    val ctrlBranch = Output(Bool())
    val ctrlRegWrite = Output(Bool())
    val ctrlLoad = Output(Bool())
    val ctrlStore = Output(Bool())
    val ctrlALUSrc = Output(Bool())
    val ctrlJAL = Output(Bool())
    val ctrlOP = Output(UInt(OP_TYPES_WIDTH.W))
    val ctrlSigned = Output(Bool())
}

// 用于連接配接寄存器子產品的Bundle
class BundleReg extends Bundle {
    val rs1 = Output(UInt(REG_NUMS_LOG.W))
    val rs2 = Output(UInt(REG_NUMS_LOG.W))
    val rd = Output(UInt(REG_NUMS_LOG.W))
}
           

這一部分其實硬寫測試并不明智,這裡還是放上一個随意的測試代碼(

src/test/scala/rv32isc/DecoderTest.scala

):

package rv32isc

import chisel3._
import chiseltest._
import chisel3.util._
import org.scalatest.flatspec.AnyFlatSpec

import config.Configs._

trait DecoderTestFunc {
    def testFn(dut: Decoder): Unit = {
        // LUI x1, 0x1111
        // AUIPC x2, 0x2222
        // JAL x3, L0
        // L0:
        // JALR x4, x3, 4
        // L3:
        // BEQ x5, x6, L1
        // L1:
        // BNE x1, x2, L2
        // L2:
        // BLT x1, x2, L4
        // L4:
        // BGE x1, x2, L5
        // L5:
        // BLTU x1, x2, L6
        // L6:
        // BGEU x1, x2, L7
        // L7:
        // LB x1, 0x4, x2
        // LH x1, 0x8, x2
        // LW x1, 0x4, x2
        // LBU x1, 0x8, x2
        // LHU x1, 0xc, x2
        // SB x1, 0x4, x1
        // SH x1, 0x4, x1
        // SW x1, 0x4, x1
        // ADDI x1, x1, 0x4
        // SLTI x1, x1, 0x4
        // SLTIU x1, x1, 0x4
        // XORI x1, x1, 0x4
        // ORI x1, x1, 0x4
        // ANDI x1, x1, 0x4
        // SLLI x1, x1, 0x4
        // SRLI x1, x1, 0x4
        // SRAI x1, x1, 0x4
        // ADD x1, x1, x2
        // SUB x1, x1, x2
        // SLL x1, x1, x2
        // SLT x1, x1, x2
        // SLTU x1, x1, x2
        // XOR x1, x1, x2
        // SRL x1, x1, x2
        // SRA x1, x1, x2
        // OR x1, x1, x2
        // AND x1, x1, x2

        val inst_list = Seq(
            "h011110b7".U,
            "h02222117".U,
            "h004001ef".U,
            "h00418267".U,
            "h00628263".U,
            "h00209263".U,
            "h0020c263".U,
            "h0020d263".U,
            "h0020e263".U,
            "h0020f263".U,
            "h00410083".U,
            "h00811083".U,
            "h00412083".U,
            "h00814083".U,
            "h00c15083".U,
            "h00108223".U,
            "h00109223".U,
            "h0010a223".U,
            "h00408093".U,
            "h0040a093".U,
            "h0040b093".U,
            "h0040c093".U,
            "h0040e093".U,
            "h0040f093".U,
            "h00409093".U,
            "h0040d093".U,
            "h4040d093".U,
            "h002080b3".U,
            "h402080b3".U,
            "h002090b3".U,
            "h0020a0b3".U,
            "h0020b0b3".U,
            "h0020c0b3".U,
            "h0020d0b3".U,
            "h4020d0b3".U,
            "h0020e0b3".U,
            "h0020f0b3".U
        )

        def test_Decoder(dut: Decoder): Unit = {
            for (inst <- inst_list) {
                dut.io.inst.poke(inst)
                println(dut.io.bundleReg.rs1.peekInt())
                println(dut.io.bundleReg.rs2.peekInt())
                println(dut.io.imm.peek())
                println(dut.io.bundleReg.rd.peekInt())
                println(dut.io.bundleCtrl.peek())
            }
        }
    }
}

class DecoderTest
    extends AnyFlatSpec
    with ChiselScalatestTester
    with DecoderTestFunc {
    "Decoder" should "pass" in {
        test(new Decoder) { dut =>
            testFn(dut)
        }
    }
}

           

測試通過。

解碼單元的信号輸出一部分給到寄存器,用于讀取寄存器中的資料,下面我們就實作寄存器。

寄存器組的實作

寄存器的實作比較簡單,我們前面的文章中也有相應的例子,這裡就不分析了,直接放上代碼(

src/main/scala/rv32isc/Registers.scala

):

package rv32isc

import chisel3._
import chisel3.util._

import config.Configs._
import utils._

class RegistersIO extends Bundle {
    val ctrlRegWrite = Input(Bool())
    val dataWrite = Input(UInt(DATA_WIDTH.W))
    val bundleReg = Output(Flipped(new BundleReg))
    val dataRead1 = Output(UInt(DATA_WIDTH.W))
    val dataRead2 = Output(UInt(DATA_WIDTH.W))
}

class Registers extends Module {
    val io = IO(new RegistersIO())

    // 寄存器組,REG_NUMS個,位寬DATA_WIDTH
    val regs = Reg(Vec(REG_NUMS, UInt(DATA_WIDTH.W)))

    // 寄存器号為0時讀到0
    when (io.bundleReg.rs1 === 0.U) {
        io.dataRead1 := 0.U
    }
    when (io.bundleReg.rs2 === 0.U) {
        io.dataRead2 := 0.U
    }
    // 否則給出資料
    io.dataRead1 := regs(io.bundleReg.rs1)
    io.dataRead2 := regs(io.bundleReg.rs2)
    // 給出寫信号,且rd不為0時寫寄存器
    when (io.ctrlRegWrite && io.bundleReg.rd =/= 0.U) {
        regs(io.bundleReg.rd) := io.dataWrite
    }
}
           

然後是測試代碼:

package rv32isc

import chisel3._
import chiseltest._
import chisel3.util._
import org.scalatest.flatspec.AnyFlatSpec

import config.Configs._

trait RegistersTestFunc {
    // 随機填入資料
    val oprand_list = Seq.fill(REG_NUMS)(scala.util.Random.nextInt().toLong & 0x00ffffffffL)

    def testRegs(dut: Registers): Unit = {
        // 初始化狀态
        for (i <- 0 to REG_NUMS - 1) {
            dut.io.bundleReg.rs1.poke(i.U)
            dut.io.dataRead1.expect(0.U)
            dut.io.bundleReg.rs2.poke(i.U)
            dut.io.dataRead2.expect(0.U)
        }
        // 寫入
        for (i <- 0 to REG_NUMS - 1) {
            dut.io.ctrlRegWrite.poke(true.B)
            dut.io.bundleReg.rd.poke(i.U)
            dut.io.dataWrite.poke(oprand_list(i))
            dut.clock.step()
        }
        // 讀取
        for (i <- 0 to REG_NUMS - 1) {
            dut.io.bundleReg.rs1.poke(i.U)
            if (i == 0) {
                dut.io.dataRead1.expect(0.U)
            } else {
                dut.io.dataRead1.expect(oprand_list(i))
            }
            
            dut.io.bundleReg.rs2.poke(i.U)
            if (i == 0) {
                dut.io.dataRead2.expect(0.U)
            } else {
                dut.io.dataRead2.expect(oprand_list(i))
            }
        }
        // 不能寫時嘗試寫0
        for (i <- 0 to REG_NUMS - 1) {
            dut.io.ctrlRegWrite.poke(false.B)
            dut.io.bundleReg.rd.poke(i.U)
            dut.io.dataWrite.poke(0)
            dut.clock.step()
        }
        // 再次讀
        for (i <- 0 to REG_NUMS - 1) {
            dut.io.bundleReg.rs1.poke(i.U)
            if (i == 0) {
                dut.io.dataRead1.expect(0.U)
            } else {
                dut.io.dataRead1.expect(oprand_list(i))
            }
            
            dut.io.bundleReg.rs2.poke(i.U)
            if (i == 0) {
                dut.io.dataRead2.expect(0.U)
            } else {
                dut.io.dataRead2.expect(oprand_list(i))
            }
        }
    }
}

class RegistersTest extends AnyFlatSpec with ChiselScalatestTester with RegistersTestFunc {
    "Registers" should "pass" in {
        test(new Registers) { dut =>
            testRegs(dut)
        }
    }
}
           

測試通過。

資料也有了,控制信号也有了,下面我們就來實作最關鍵的ALU部分吧。

ALU子產品的實作

雖然說ALU子產品很關鍵,但是由于前面打下了較好的基礎,是以在實作

Alu

子產品時輕松了很多。現在控制單元到

Alu

有四個控制信号,是以我們還是用Bundle的方式,在

src/main/scala/utils/Bundles.scala

中加入:

class BundleAluControl extends Bundle {
    val ctrlALUSrc = Input(Bool())
    val ctrlJAL = Input(Bool())
    val ctrlOP = Input(UInt(OP_TYPES_WIDTH.W))
    val ctrlSigned = Input(Bool())
    val ctrlBranch = Input(Bool())
}
           

需要注意到的是,

JAL

指令需要PC寄存器寄存器的值作為操作數1,是以需要增加一個輸入接口。然後就可以輕松實作

Alu

了,具體代碼(

src/main/scala/rv32isc/Alu.scala

)如下:

package rv32isc

import chisel3._
import chisel3.util._

import config.Configs._
import utils.OP_TYPES._
import utils._

class AluIO extends Bundle {
    val bundleAluControl = new BundleAluControl()
    val dataRead1 = Input(UInt(DATA_WIDTH.W))
    val dataRead2 = Input(UInt(DATA_WIDTH.W))
    val imm = Input(UInt(DATA_WIDTH.W))
    val pc = Input(UInt(ADDR_WIDTH.W))
    val resultBranch = Output(Bool())
    val resultAlu = Output(UInt(DATA_WIDTH.W))
}

class Alu extends Module {
    val io = IO(new AluIO())

    // 用于輸出比較結果和計算結果
    val resultBranch = WireDefault(false.B)
    val resultAlu = WireDefault(0.U(DATA_WIDTH.W))

    // 用于得到操作數
    val oprand1 = WireDefault(0.U(DATA_WIDTH.W))
    val oprand2 = WireDefault(0.U(DATA_WIDTH.W))

    oprand1 := Mux(io.bundleAluControl.ctrlJAL, io.pc, io.dataRead1)
    oprand2 := Mux(io.bundleAluControl.ctrlALUSrc, io.imm, io.dataRead2)

    // 根據bundleAluControl中的信号進行選擇
    switch(io.bundleAluControl.ctrlOP) {
        is(OP_NOP) { // 啥也不幹
            resultAlu := 0.U
            resultBranch := false.B
        }
        is(OP_ADD) {
            resultAlu := oprand1 +& oprand2
        }
        is(OP_SUB) {
            resultAlu := oprand1 -& oprand2
        }
        is(OP_AND) {
            resultAlu := oprand1 & oprand2
        }
        is(OP_OR) {
            resultAlu := oprand1 | oprand2
        }
        is(OP_XOR) {
            resultAlu := oprand1 ^ oprand2
        }
        is(OP_SLL) {
            resultAlu := oprand1 << oprand2(4, 0)
        }
        is(OP_SRL) {
            resultAlu := oprand1 >> oprand2(4, 0)
        }
        is(OP_SRA) { // 需要注意算術右移的寫法
            resultAlu := (oprand1.asSInt >> oprand2(4, 0)).asUInt
        }
        is(OP_EQ) {
            resultBranch := oprand1.asSInt === oprand2.asSInt
            resultAlu := io.pc +& io.imm
        }
        is(OP_NEQ) {
            resultBranch := oprand1.asSInt =/= oprand2.asSInt
            resultAlu := io.pc +& io.imm
        }
        is(OP_LT) { // 區分有符号比較和無符号比較、分支和SLT
            when(io.bundleAluControl.ctrlBranch) {
                when(io.bundleAluControl.ctrlSigned) {
                    resultBranch := oprand1.asSInt < oprand2.asSInt
                }.otherwise {
                    resultBranch := oprand1 < oprand2
                }
                resultAlu := io.pc +& io.imm
            }.otherwise {
                when(io.bundleAluControl.ctrlSigned) {
                    resultAlu := oprand1.asSInt < oprand2.asSInt
                }.otherwise {
                    resultAlu := oprand1 < oprand2
                }
            }
        }
        is(OP_GE) { // 區分有符号比較和無符号比較
            when(io.bundleAluControl.ctrlSigned) {
                resultBranch := oprand1.asSInt >= oprand2.asSInt
            }.otherwise {
                resultBranch := oprand1 >= oprand2
            }
            resultAlu := io.pc +& io.imm
        }
    }

    io.resultAlu := resultAlu
    io.resultBranch := resultBranch
}
           

測試代碼如下(因為趕時間,這裡寫的是有問題的,大家可以自行認真編寫):

package rv32isc

import chisel3._
import chiseltest._
import chisel3.util._
import org.scalatest.flatspec.AnyFlatSpec

import config.Configs._
import utils.OP_TYPES._

trait AluTestFunc {
    // 測試所有功能
    val OP_TYPES_LIST = Seq(
        OP_NOP,
        OP_ADD,
        OP_SUB,
        OP_AND,
        OP_OR,
        OP_XOR,
        OP_SLL,
        OP_SRL,
        OP_SRA,
        OP_EQ,
        OP_NEQ,
        OP_LT,
        // OP_GE
    )

    // 随機的操作數
    val oprand_list =
        Seq.fill(10)(scala.util.Random.nextInt().toLong & 0x00ffffffffL)

    // 用于比對的正确結果
    def alu(a: Long, b: Long, op: UInt, sign: Boolean): (Long, Boolean) = {
        op match {
            case OP_NOP => (0, false)
            case OP_ADD => (a + b, false)
            case OP_SUB => (a - b, false)
            case OP_AND => (a & b, false)
            case OP_OR  => (a | b, false)
            case OP_XOR => (a ^ b, false)
            case OP_SLL => (a << (b & 0x000000001f), false)
            case OP_SRL => (a >>> (b & 0x000000001f), false)
            case OP_SRA => (a.toInt >> (b & 0x000000001f).toInt, false)
            case OP_EQ  => (0, a == b)
            case OP_NEQ => (0, a != b)
            case OP_LT => {
                if (sign) {
                    (0, (a << 32) < (b << 32))
                } else {
                    (0, a < b)
                }
            }
            case OP_GE => {
                if (sign) {
                    (0, (a << 32) >= (b << 32))
                } else {
                    (0, a >= b)
                }
            }
            case _ => (0, false)
        }
    }

    def testOne(dut: Alu, a: Long, b: Long, op: UInt, sign: Boolean): Unit = {
        // 正常測試
        dut.io.bundleAluControl.ctrlALUSrc.poke(false.B)
        dut.io.bundleAluControl.ctrlJAL.poke(false.B)
        dut.io.bundleAluControl.ctrlOP.poke(op)
        dut.io.bundleAluControl.ctrlSigned.poke(sign)
        dut.io.pc.poke(0.U)
        dut.io.imm.poke(0.U)
        dut.io.dataRead1.poke(a.U)
        dut.io.dataRead2.poke(b.U)
        val (resultAlu, resultBranch) = alu(a, b, op, sign)
        dut.io.resultAlu.expect((resultAlu.toLong & 0x00ffffffffL).U)
        dut.io.resultBranch.expect(resultBranch.B)
        // JAL+IMM
        dut.io.bundleAluControl.ctrlALUSrc.poke(true.B)
        dut.io.bundleAluControl.ctrlJAL.poke(true.B)
        dut.io.pc.poke(a.U)
        dut.io.imm.poke(b.U)
        dut.io.dataRead1.poke(0.U)
        dut.io.dataRead2.poke(0.U)
        val (resultAluJAL, resultBranchJAL) = alu(a, b, op, sign)
        dut.io.resultAlu.expect((resultAluJAL.toLong & 0x00ffffffffL).U)
        dut.io.resultBranch.expect(resultBranchJAL.B)
        // IMM
        dut.io.bundleAluControl.ctrlALUSrc.poke(true.B)
        dut.io.bundleAluControl.ctrlJAL.poke(false.B)
        dut.io.pc.poke(0.U)
        dut.io.imm.poke(b.U)
        dut.io.dataRead1.poke(a.U)
        dut.io.dataRead2.poke(0.U)
        val (resultAluIMM, resultBranchIMM) = alu(a, b, op, sign)
        dut.io.resultAlu.expect((resultAluIMM.toLong & 0x00ffffffffL).U)
        dut.io.resultBranch.expect(resultBranchIMM.B)
    }

    // 周遊功能和操作數進行測試
    def testFn(dut: Alu): Unit = {
        for (a <- oprand_list) {
            for (b <- oprand_list) {
                for (op <- OP_TYPES_LIST) {
                    testOne(dut, a, b, op, true)
                    testOne(dut, a, b, op, false)
                }
            }
        }
    }
}

class AluTest extends AnyFlatSpec with ChiselScalatestTester with AluTestFunc {
    "ALU" should "pass" in {
        test(new Alu) { dut =>
            testFn(dut)
        }
    }
}

           

測試通過。

資料記憶體的實作

Alu的計算結果可以直接給寄存器,也可以先給資料記憶體。因為LOAD、STORE類指令需要使用ALU計算得到的記憶體位址,是以我們完全可以把計算結果給資料記憶體,如果是LOAD或STORE指令,那就以此為位址讀寫資料,否則将結果直接發送給寄存器。

另外一點在之前的設計中忽略了的是,LOAD、STORE類指令需要區分字、半字和位元組,是以我們需要在譯碼階段多給控制單元一個信号,來訓示操作數。并且,LOAD類指令也區分有符号無符号,是以也需要提供這個信号。

還有,STORE的資料源從哪裡來?是

rs2

,可是我們計算位址用的是

imm

作為操作數2,是以,這裡我們還需要把

rs2

裡面的值給出到資料記憶體。

最後資料記憶體的實作如下:

package rv32isc

import chisel3._
import chisel3.util._

import config.Configs._
import utils.OP_TYPES._
import utils.LS_TYPES._
import utils._

class MemDataIO extends Bundle {
    val bundleMemDataControl = new BundleMemDataControl()
    val resultALU = Input(UInt(DATA_WIDTH.W))
    val dataStore = Input(UInt(DATA_WIDTH.W))
    val result = Output(UInt(DATA_WIDTH.W))
}

class MemData extends Module {
    val io = IO(new MemDataIO)

    // 資料記憶體
    val mem = Mem(MEM_DATA_SIZE, UInt(DATA_WIDTH.W))

    // 用于輸出的結果
    val result = WireDefault(0.U(DATA_WIDTH.W))

    // 從記憶體中讀取的數
    val dataLoad = WireDefault(0.U(DATA_WIDTH.W))

    // 不論是STORE還是LOAD,都需要用到這個讀數
    dataLoad := mem.read(io.resultALU >> DATA_BYTE_WIDTH_LOG.U)

    // STORE指令
    when(io.bundleMemDataControl.ctrlStore) {
        when(io.bundleMemDataControl.ctrlLSType === LS_W) { // 修改全部4位元組
            mem.write(io.resultALU >> DATA_BYTE_WIDTH_LOG.U, io.dataStore)
        }.elsewhen(io.bundleMemDataControl.ctrlLSType === LS_H) {   // 修改低2位元組
            mem.write(io.resultALU >> DATA_BYTE_WIDTH_LOG.U, Cat(dataLoad(31, 16), io.dataStore(15, 0)))
        }.otherwise {   // 修改最低一個位元組
            mem.write(io.resultALU >> DATA_BYTE_WIDTH_LOG.U, Cat(dataLoad(31, 8), io.dataStore(7, 0)))
        }
    }
    // LOAD指令
    when (io.bundleMemDataControl.ctrlLoad) {
        when(io.bundleMemDataControl.ctrlLSType === LS_W) {
            result := dataLoad
        }.elsewhen(io.bundleMemDataControl.ctrlLSType === LS_H) {
            when (io.bundleMemDataControl.ctrlSigned) {
                result := Cat(Fill(16, dataLoad(15)), dataLoad(15, 0))
            } .otherwise {
                result := Cat(Fill(16, 0.U), dataLoad(15, 0))
            }
        }.otherwise {
            when (io.bundleMemDataControl.ctrlSigned) {
                result := Cat(Fill(24, dataLoad(7)), dataLoad(7, 0))
            } .otherwise {
                result := Cat(Fill(24, 0.U), dataLoad(7, 0))
            }
        } 
    // 非LOAD指令
    } .otherwise {
        result := io.resultALU
    }
    
    // 輸出
    io.result := result
}
           

測試代碼如下:

package rv32isc

import chisel3._
import chiseltest._
import chisel3.util._
import org.scalatest.flatspec.AnyFlatSpec

import config.Configs._
import utils.OP_TYPES._
import utils.LS_TYPES._

trait MemDataTestFunc {
    // 生成MEM_DATA_SIZE條随機資料進行測試
    val data_list =
        Seq.fill(MEM_DATA_SIZE)(
            scala.util.Random.nextInt().toLong & 0x00ffffffffL
        )

    def testFn(dut: MemData): Unit = {
        // 初始化狀态
        dut.clock.setTimeout(0)
        dut.io.bundleMemDataControl.ctrlLoad.poke(false.B)
        dut.io.bundleMemDataControl.ctrlStore.poke(false.B)
        dut.io.bundleMemDataControl.ctrlLSType.poke(LS_W)
        dut.io.bundleMemDataControl.ctrlSigned.poke(false.B)
        dut.io.dataStore.poke(0.U(DATA_WIDTH.W))
        dut.io.resultALU.poke(0.U(DATA_WIDTH.W))
        // 非LS指令
        for (i <- 0 to MEM_DATA_SIZE - 1) {
            dut.io.resultALU.poke((i * DATA_BYTE_WIDTH).U)
            dut.clock.step(1)
            dut.io.result.expect((i * DATA_BYTE_WIDTH).U)
        }
        // SW指令
        dut.io.bundleMemDataControl.ctrlStore.poke(true.B)
        for (i <- 0 to MEM_DATA_SIZE - 1) {
            dut.io.dataStore.poke(data_list(i))
            dut.io.resultALU.poke((i * DATA_BYTE_WIDTH).U) // 作為位址,應該左移兩位,即乘以4
            dut.io.result.expect((i * DATA_BYTE_WIDTH).U)
            dut.clock.step(1)
        }
        // LW指令
        dut.io.bundleMemDataControl.ctrlStore.poke(false.B)
        dut.io.bundleMemDataControl.ctrlLoad.poke(true.B)
        for (i <- 0 to MEM_DATA_SIZE - 1) {
            dut.io.dataStore.poke(0.U)
            dut.io.resultALU.poke((i * DATA_BYTE_WIDTH).U) // 作為位址,應該左移兩位,即乘以4
            dut.io.result.expect(data_list(i).U)
        }
        // SH清零低16比特
        dut.io.bundleMemDataControl.ctrlStore.poke(true.B)
        dut.io.bundleMemDataControl.ctrlLoad.poke(false.B)
        dut.io.bundleMemDataControl.ctrlLSType.poke(LS_H)
        for (i <- 0 to MEM_DATA_SIZE - 1) {
            dut.io.dataStore.poke(0)
            dut.io.resultALU.poke((i * DATA_BYTE_WIDTH).U) // 作為位址,應該左移兩位,即乘以4
            dut.io.result.expect((i * DATA_BYTE_WIDTH).U)
            dut.clock.step(1)
        }
        // LHU指令讀低16比特
        dut.io.bundleMemDataControl.ctrlStore.poke(false.B)
        dut.io.bundleMemDataControl.ctrlLoad.poke(true.B)
        for (i <- 0 to MEM_DATA_SIZE - 1) {
            dut.io.dataStore.poke(0.U)
            dut.io.resultALU.poke((i * DATA_BYTE_WIDTH).U) // 作為位址,應該左移兩位,即乘以4
            dut.io.result.expect(0.U)
        }
        // LH指令讀低16比特
        dut.io.bundleMemDataControl.ctrlSigned.poke(true.B)
        for (i <- 0 to MEM_DATA_SIZE - 1) {
            dut.io.dataStore.poke(0.U)
            dut.io.resultALU.poke((i * DATA_BYTE_WIDTH).U) // 作為位址,應該左移兩位,即乘以4
            dut.io.result.expect(0.U)
        }
        dut.io.bundleMemDataControl.ctrlSigned.poke(false.B)
        // SB存儲低8比特
        dut.io.bundleMemDataControl.ctrlStore.poke(true.B)
        dut.io.bundleMemDataControl.ctrlLoad.poke(false.B)
        dut.io.bundleMemDataControl.ctrlLSType.poke(LS_B)
        for (i <- 0 to MEM_DATA_SIZE - 1) {
            dut.io.dataStore.poke(data_list(i))
            dut.io.resultALU.poke((i * DATA_BYTE_WIDTH).U) // 作為位址,應該左移兩位,即乘以4
            dut.io.result.expect((i * DATA_BYTE_WIDTH).U)
            dut.clock.step(1)
        }
        // LBU指令讀低8比特
        dut.io.bundleMemDataControl.ctrlStore.poke(false.B)
        dut.io.bundleMemDataControl.ctrlLoad.poke(true.B)
        for (i <- 0 to MEM_DATA_SIZE - 1) {
            dut.io.dataStore.poke(0.U)
            dut.io.resultALU.poke((i * DATA_BYTE_WIDTH).U) // 作為位址,應該左移兩位,即乘以4
            dut.io.result.expect((data_list(i).toLong & 0x00000000ffL).U)
        }
        // LB指令讀低8比特
        dut.io.bundleMemDataControl.ctrlSigned.poke(true.B)
        for (i <- 0 to MEM_DATA_SIZE - 1) {
            dut.io.dataStore.poke(0.U)
            dut.io.resultALU.poke((i * DATA_BYTE_WIDTH).U) // 作為位址,應該左移兩位,即乘以4
            if ((data_list(i).toLong & 0x0000000080L) == 0) {
                dut.io.result.expect((data_list(i).toLong & 0x00000000ffL).U)
            } else {
                dut.io.result.expect(((data_list(i).toLong & 0x00000000ffL) | 0x00ffffff00L).U)
            }
        }
    }
}

class MemDataTest
    extends AnyFlatSpec
    with ChiselScalatestTester
    with MemDataTestFunc {
    "MemData" should "pass" in {
        test(new MemData) { dut =>
            testFn(dut)
        }
    }
}
           

測試通過。

控制單元的實作

最後控制單元,但在控制單元之前,我們需要注意到

JAL

JALR

這兩條指令的特殊性,它們存放的寄存器的值是目前的指令的位址+4,是以我們需要對寄存器堆先做一些修改,一個方面是要從控制子產品給一個

ctrlJump

信号,另一方面是要選擇寫入的資料是

PC+4

還是資料記憶體傳回的計算結果/加載的資料。修改後如下:

when(io.ctrlRegWrite && io.bundleReg.rd =/= 0.U) {
    when(io.ctrlJump) {
        regs(io.bundleReg.rd) := io.pc + INST_BYTE_WIDTH.U
    }.otherwise {
        regs(io.bundleReg.rd) := io.dataWrite
    }
}
           

那我們就可以寫控制單元了,都是些連線,沒什麼技術含量:

package rv32isc

import chisel3._
import chisel3.util._

import utils._

class ControllerIO extends Bundle {
    val bundleControlIn = Flipped(new BundleControl()) // 來自譯碼器
    val bundleAluControl = Flipped(new BundleAluControl())  // 到ALU
    val bundleMemDataControl = Flipped(new BundleMemDataControl())  // 到資料記憶體
    val bundleControlOut = new BundleControl()  // 到其他
}

class Controller extends Module {
    val io = IO(new ControllerIO)

    // alu
    io.bundleAluControl.ctrlALUSrc := io.bundleControlIn.ctrlALUSrc
    io.bundleAluControl.ctrlJAL := io.bundleControlIn.ctrlJAL
    io.bundleAluControl.ctrlOP := io.bundleControlIn.ctrlOP
    io.bundleAluControl.ctrlSigned := io.bundleControlIn.ctrlSigned
    io.bundleAluControl.ctrlBranch := io.bundleControlIn.ctrlBranch

    // 記憶體單元
    io.bundleMemDataControl.ctrlLSType := io.bundleControlIn.ctrlALUSrc
    io.bundleMemDataControl.ctrlLoad := io.bundleControlIn.ctrlLoad
    io.bundleMemDataControl.ctrlSigned := io.bundleControlIn.ctrlSigned
    io.bundleMemDataControl.ctrlStore := io.bundleControlIn.ctrlStore
    
    // 其他
    io.bundleControlOut <> io.bundleControlIn
}
           

因為隻有連線,就不測試了,我們直接進入最後一步,把各子產品連接配接成一個處理器。

把各子產品連接配接成一個處理器!

這一步仍然沒有什麼技術含量,把各個子產品連接配接到一起就好了,一定要注意連線不要錯連、漏連:

package rv32isc

import chisel3._
import chisel3.util._

import config.Configs._
import utils._

// Top的子產品接口,用于測試
class TopIO extends Bundle {
    val addr = Output(UInt(ADDR_WIDTH.W))
    val inst = Output(UInt(INST_WIDTH.W))
    val bundleCtrl = new BundleControl()
    val resultALU = Output(UInt(DATA_WIDTH.W))
    val rs1 = Output(UInt(DATA_WIDTH.W))
    val rs2 = Output(UInt(DATA_WIDTH.W))
    val imm = Output(UInt(DATA_WIDTH.W))
    val resultBranch = Output(Bool())
    val result = Output(UInt(DATA_WIDTH.W))
}

class Top extends Module {
    val io = IO(new TopIO())

    val pcReg = Module(new PCReg())
    val memInst = Module(new MemInst())
    val decoder = Module(new Decoder())
    val registers = Module(new Registers())
    val alu = Module(new Alu())
    val memData = Module(new MemData())
    val controller = Module(new Controller())

    // PCReg in
    pcReg.io.resultBranch <> alu.io.resultBranch
    pcReg.io.addrTarget <> memData.io.result
    pcReg.io.ctrlBranch <> controller.io.bundleControlOut.ctrlBranch
    pcReg.io.ctrlJump <> controller.io.bundleControlOut.ctrlJump
    
    // MemInst in
    memInst.io.addr <> pcReg.io.addrOut

    // Decoder in
    decoder.io.inst <> memInst.io.inst

    // Registers in
    registers.io.bundleReg <> decoder.io.bundleReg
    registers.io.ctrlRegWrite <> controller.io.bundleControlOut.ctrlRegWrite
    registers.io.ctrlJump <> controller.io.bundleControlOut.ctrlJump
    registers.io.dataWrite <> memData.io.result
    registers.io.pc <> pcReg.io.addrOut

    // ALU in
    alu.io.bundleAluControl <> controller.io.bundleAluControl
    alu.io.dataRead1 <> registers.io.dataRead1
    alu.io.dataRead2 <> registers.io.dataRead2
    alu.io.imm <> decoder.io.imm
    alu.io.pc <> pcReg.io.addrOut
    
    // MemData in
    memData.io.bundleMemDataControl <> controller.io.bundleMemDataControl
    memData.io.dataStore <> registers.io.dataRead2
    memData.io.resultALU <> alu.io.resultAlu

    // Controller in
    controller.io.bundleControlIn <> decoder.io.bundleCtrl
    
    // top
    io.addr <> pcReg.io.addrOut
    io.bundleCtrl <> decoder.io.bundleCtrl
    io.inst <> memInst.io.inst
    io.result <> memData.io.result
    io.resultALU <> alu.io.resultAlu
    io.resultBranch <> alu.io.resultBranch
    io.imm <> decoder.io.imm
    io.rs1 <> registers.io.dataRead1
    io.rs2 <> registers.io.dataRead2
}

object main extends App {
    println(getVerilogString(new Top()))
}
           

通過

sbt run

運作,可以得到最終的Verilog代碼,限于篇幅,這裡就不放上來了,至少可以說明,編譯生成Verilog代碼是看起來沒問題的。

但是具體的處理器的功能驗證還需要對

Top

子產品進行測試,下面就着重說一下。

CPU的整體測試

既然是CPU,那就必須得能跑程式,也就是說我們至少能做到這樣的事:

寫一段C程式,然後用RISC-V的工具鍊編譯出二進制代碼,我們的CPU可以運作這樣的代碼。

我們可以通過

loadMemoryFromFile

向指令記憶體的記憶體中加載十六進制文本格式的代碼,是以我們首先需要從C源檔案生成這樣可以加載到記憶體的代碼。假設你已經裝好了rv32i的工具鍊,源檔案為

test.c

,那麼生成過程如下:

  1. 生成彙編:
    riscv32-unknown-elf-gcc -march=rv32i -S test.c
               
  2. 生成目标檔案:
    riscv32-unknown-elf-as -march=rv32i test.s -o test.o
               
  3. 轉換出二進制檔案:
    riscv32-unknown-elf-objcopy -O binary test.o test.bin
               
  4. 自己寫一個腳本,将二進制檔案轉換為十六進制文本檔案,以Python為例:
    import sys
    
    if __name__ == "__main__":
        f_bin = open(str(sys.argv[1]), "rb")
        f_hex = open(str(sys.argv[2]), "w")
        while True:
            buf = f_bin.read(4)
            buf_len = len(buf)
            if buf_len > 0:
                s_hex = ''
                s_hex += hex(buf[3])[2:].zfill(2) + hex(buf[2])[2:].zfill(2) + hex(buf[1])[2:].zfill(2) + hex(buf[0])[2:].zfill(2)
                f_hex.write(s_hex + '\n')
            else:
                break
               
    然後運作:
    python3 bin2hex.py test.bin test.hex
               

我們可以簡單寫個斐波那契數列計算的C程式:

int test(int n)
{
    if (n == 1 || n == 2) // 數列前兩項
    {
        return 1;
    }
    else // 從第三項開始
    {
        return test(n - 1) + test(n - 2);
    }
    return 0;
}
int main()
{
    int n = 10;
    int ret = test(n); // 計算斐波那契數列
    return ret;
}
           

根據上面的步驟,可以生成如下的内容:

fe010113
00112e23
00812c23
00912a23
02010413
fea42623
fec42703
00100793
00f70863
fec42703
00200793
00f71663
00100793
0380006f
fec42783
fff78793
00078513
00000097
000080e7
00050493
fec42783
ffe78793
00078513
00000097
000080e7
00050793
00f487b3
00078513
01c12083
01812403
01412483
02010113
00008067
fe010113
00112e23
00812c23
02010413
00a00793
fef42623
fec42503
00000097
000080e7
fea42423
fe842783
00078513
01c12083
01812403
02010113
00008067
           

但是不是沒法看?都是十六進制,也不知道跟彙編指令怎麼對應。沒關系,一句話,讓代碼更好懂:

riscv32-unknown-elf-objdump -d test.o > test.dump
           

生成的檔案如下:

test.o:     file format elf32-littleriscv


Disassembly of section .text:

00000000 <test>:
   0:	fe010113          	addi	sp,sp,-32
   4:	00112e23          	sw	ra,28(sp)
   8:	00812c23          	sw	s0,24(sp)
   c:	00912a23          	sw	s1,20(sp)
  10:	02010413          	addi	s0,sp,32
  14:	fea42623          	sw	a0,-20(s0)
  18:	fec42703          	lw	a4,-20(s0)
  1c:	00100793          	li	a5,1
  20:	00f70863          	beq	a4,a5,30 <.L2>
  24:	fec42703          	lw	a4,-20(s0)
  28:	00200793          	li	a5,2
  2c:	00f71663          	bne	a4,a5,38 <.L3>

00000030 <.L2>:
  30:	00100793          	li	a5,1
  34:	0380006f          	j	6c <.L4>

00000038 <.L3>:
  38:	fec42783          	lw	a5,-20(s0)
  3c:	fff78793          	addi	a5,a5,-1
  40:	00078513          	mv	a0,a5
  44:	00000097          	auipc	ra,0x0
  48:	000080e7          	jalr	ra # 44 <.L3+0xc>
  4c:	00050493          	mv	s1,a0
  50:	fec42783          	lw	a5,-20(s0)
  54:	ffe78793          	addi	a5,a5,-2
  58:	00078513          	mv	a0,a5
  5c:	00000097          	auipc	ra,0x0
  60:	000080e7          	jalr	ra # 5c <.L3+0x24>
  64:	00050793          	mv	a5,a0
  68:	00f487b3          	add	a5,s1,a5

0000006c <.L4>:
  6c:	00078513          	mv	a0,a5
  70:	01c12083          	lw	ra,28(sp)
  74:	01812403          	lw	s0,24(sp)
  78:	01412483          	lw	s1,20(sp)
  7c:	02010113          	addi	sp,sp,32
  80:	00008067          	ret

00000084 <main>:
  84:	fe010113          	addi	sp,sp,-32
  88:	00112e23          	sw	ra,28(sp)
  8c:	00812c23          	sw	s0,24(sp)
  90:	02010413          	addi	s0,sp,32
  94:	00a00793          	li	a5,10
  98:	fef42623          	sw	a5,-20(s0)
  9c:	fec42503          	lw	a0,-20(s0)
  a0:	00000097          	auipc	ra,0x0
  a4:	000080e7          	jalr	ra # a0 <main+0x1c>
  a8:	fea42423          	sw	a0,-24(s0)
  ac:	fe842783          	lw	a5,-24(s0)
  b0:	00078513          	mv	a0,a5
  b4:	01c12083          	lw	ra,28(sp)
  b8:	01812403          	lw	s0,24(sp)
  bc:	02010113          	addi	sp,sp,32
  c0:	00008067          	ret
           

這樣我們就有十六進制指令和彙編指令的對應關系了,下面我們看代碼。

一個程式在執行的時候是從

main

函數進入的,在這段程式中,程式入口就是

00000084 <main>

。對應的,要想正确執行這段程式,我們需要讓我們的CPU從

0x00000084

開始執行。這時候,用

Configs.scala

存放全局變量的好處就展現出來了,我們隻需要将:

修改為:

就行了。

有程式入口就行了嘛?當然不是。我們看第一條語句:

addi	sp,sp,-32
           

這對

sp

寄存器減了32,這個

sp

就是棧指針,程式進行函數調用都需要保護現場,将一些資料壓棧,友善調用傳回的時候恢複現場。然而這個棧應該是提前配置設定好空間的,棧底的位址比棧頂的位址要大。

sp

寄存器其實對應

x2

寄存器,是以在我們的處理器中初始值是

,那麼減

32

之後就成負數了,我們

1024

大小的資料記憶體在索引資料時就會有問題。是以,我們要手動完成棧空間配置設定這個工作,隻需要在程式入口處添加一條指令就行:

addi	sp,sp,1024 # 對應十六進制指令40010113
           

這裡的1024作為位址是位元組,相當于我們在1024*4大小的資料記憶體上配置設定了1024位元組的空間,即可存放256個32位資料的棧,如果覺得不夠,可以放4條該指令,剛好完全用完資料記憶體。

另外,程式的結束處是個傳回指令,如果讓它傳回,它就會傳回到調用

main

函數的地方,但當時棧是空的,是以會傳回到位址0處開始執行,陷入無限循環。是以,我們還需要将最後一條指令替換為

00000000

,用于提示測試子產品程式已經結束了。

于是,修改之後的十六進制指令序列如下:

fe010113
00112e23
00812c23
00912a23
02010413
fea42623
fec42703
00100793
00f70863
fec42703
00200793
00f71663
00100793
0380006f
fec42783
fff78793
00078513
00000097
000080e7
00050493
fec42783
ffe78793
00078513
00000097
000080e7
00050793
00f487b3
00078513
01c12083
01812403
01412483
02010113
00008067
40010113
40010113
40010113
40010113
fe010113
00112e23
00812c23
02010413
00a00793
fef42623
fec42503
00000097
000080e7
fea42423
fe842783
00078513
01c12083
01812403
02010113
00000000
           

這些工作一般由loader完成,我們這裡臨時手動完成就行,不用編寫程式加載器。

下面我們就可以将代碼放到

MemInst.hex

檔案中,然後編寫測試程式。測試代碼如下:

package rv32isc

import chisel3._
import chiseltest._
import chisel3.util._
import org.scalatest.flatspec.AnyFlatSpec

import java.io.PrintWriter
import java.io.File

import config.Configs._

trait TopTestFunc {

    def testFn(dut: Top): Unit = {
        dut.clock.setTimeout(0)
        while (dut.io.inst.peekInt() != 0) {	// 運作到程式結束處停止運作
                println("PC", dut.io.addr.peekInt().toLong.toHexString)
                println("INST", dut.io.inst.peekInt().toLong.toHexString)
            if (dut.io.addr.peekInt() == 0xb8) {	// 調用傳回的下一條指令對應的位址是0xa8,加上4條添加的指令,0xa8+0x10=0xb8,此時的rs2就是計算結果
                println("RES", dut.io.result.peekInt())
                println("RESALU", dut.io.resultALU.peekInt())
                println("RESBRANCH", dut.io.resultBranch.peek())
                println("RESJUMP", dut.io.bundleCtrl.ctrlJump.peek())
                println("SRCCCCC", dut.io.bundleCtrl.ctrlALUSrc.peek())
                println("STORE", dut.io.bundleCtrl.ctrlStore.peek())
                println("LOAD", dut.io.bundleCtrl.ctrlLoad.peek())
                println("RESJAL", dut.io.bundleCtrl.ctrlJAL.peek())
                println("OP:\t", dut.io.bundleCtrl.ctrlOP.peek())
                println("isBranch:\t", dut.io.bundleCtrl.ctrlBranch.peek())
                println("IMM:\t", dut.io.imm.peekInt())
                println("RS1:\t", dut.io.rs1.peekInt())
                println("RS2:\t", dut.io.rs2.peekInt())
                println("PC", dut.io.addr.peekInt().toLong.toHexString)
                println("INST", dut.io.inst.peekInt().toLong.toHexString)
                println("++++++++++++++++++++")
            }
            dut.clock.step(1)
        }
    }
}

class TopTest extends AnyFlatSpec with ChiselScalatestTester with TopTestFunc {
    "Top" should "pass" in {
        test(new Top) { dut =>
            testFn(dut)
        }
    }
}
           

運作測試,最後一部分輸出如下:

(PC,b8)
(INST,fea42423)
(RES,4040)
(RESALU,4040)
(RESBRANCH,Bool(false))
(RESJUMP,Bool(false))
(SRCCCCC,Bool(true))
(STORE,Bool(true))
(LOAD,Bool(false))
(RESJAL,Bool(false))
(OP:	,UInt<4>(1))
(isBranch:	,Bool(false))
(IMM:	,4294967272)
(RS1:	,4064)
(RS2:	,55)
(PC,b8)
(INST,fea42423)
++++++++++++++++++++
(PC,bc)
(INST,fe842783)
(PC,c0)
(INST,78513)
(PC,c4)
(INST,1c12083)
(PC,c8)
(INST,1812403)
(PC,cc)
(INST,2010113)
           

可以看到,第16行顯示

rs2

值為55,确實是第十項斐波那契數,測試通過。

說明和結語

文中的代碼并非最終版本的代碼,一些在調試過程中的修改未展現在文中。

完整項目代碼可以在本人的Github倉庫擷取:github-3rr0r/RV32ISC: A RISC-V RV32I ISA Single Cycle CPU。

由于寫得很倉促,也沒有使用什麼複雜的Chisel文法,很多好用的特性也沒用上,甚至很多地方風格跟屎山一樣,又懶得改,是以希望有興趣的讀者可以幫忙維護一下這個倉庫。雖然最後測試通過了,但并不嚴謹,沒有覆寫所有指令和邊界情況,如果有不對的地方歡迎大家提出修改意見或直接git commit。

雖然隻是個單周期的CPU,但編寫起來并沒有看起來那麼順利,有些腦抽寫出來的錯誤邏輯找了很久。不過好在用的是Chisel,有更加直覺的調試方法,如果用波形圖可能就沒那麼順利了。

本系列的Chisel實戰部分到這裡就完結了,本系列後續可能看情況更新一些Chisel的高階内容,但不承諾一定會有。

下一步的計劃是開辟一個新的專欄,還是實作一個RISC-V處理器,但是會更全面、更深入。會包括一些現代處理器的基本特性,比如流水線、亂序、多發射、分支預測、Cache等等,還有外設啥的,屆時歡迎大家關注。

繼續閱讀