[Computer composition and design] Chisel instruction fetching and instruction decoding design

This test is divided into three parts:

Table of Contents

Design decoding circuit

Design register file

Implement a 32-word instruction memory


Design decoding circuit

Input a 32-bit machine word, and complete the decoding of add, sub, lw, and sw instructions according to the textbook MIPS instruction format, and all other instructions will be decoded into nop instructions. The input signal is named Instr_word. The decoded output signals for the above four instructions are named add_op, sub_op, lw_op and sw_op. The other instructions are all decoded as nop;

Given the Chisel design code and simulation test waveform, observe that the input Instr_word is add R1, R2, R3; sub R0, R5, R6, lw R5,100(R2), sw R5,104(R2), JAL RA,100(R2) ), the corresponding output waveform

Decode.scala

import chisel3._

class Decoder extends Module {
  val io = IO(new Bundle {
    val Instr_word = Input(UInt(32.W))
    val add_op = Output(Bool())
    val sub_op = Output(Bool())
    val lw_op = Output(Bool())
    val sw_op = Output(Bool())
    val nop_op = Output(Bool())
  })
  //Define opcode
  val OPCODE_ADD = "b000000".U
  val OPCODE_SUB = "b000000".U
  val OPCODE_LW = "b100011".U
  val OPCODE_SW = "b101011".U
  //Define function code
  val FUNCT_ADD = "b100000".U
  val FUNCT_SUB = "b100010".U
  //Extract the opcode of the MIPS instruction
  val opcode = io.Instr_word(31, 26)
  //Extract the function code of the MIPS instruction
  val funct = io.Instr_word(5, 0)
  // decode
  io.add_op := opcode === OPCODE_ADD & amp; & amp; funct === FUNCT_ADD
  io.sub_op := opcode === OPCODE_SUB & amp; & amp; funct === FUNCT_SUB
  io.lw_op := opcode === OPCODE_LW
  io.sw_op := opcode === OPCODE_SW
  io.nop_op := !(io.add_op || io.sub_op || io.lw_op || io.sw_op)
}

object Decoder extends App {
  (new chisel3.stage.ChiselStage).emitVerilog(new Decoder())
}

DecoderTest.scala

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

class DecoderTest extends AnyFlatSpec with ChiselScalatestTester {
  behavior of "Decoder"
  it should "correctly decode instructions" in {
    test(new Decoder).withAnnotations(Seq(WriteVcdAnnotation)) { c =>
      //Test instructions
      val addInstruction = "b000000_00010_00011_00001_00000_100000".U
      val subInstruction = "b000000_00101_00110_00000_00000_100010".U
      val lwInstruction = "b100011_00010_00101_0000000001100100".U
      val swInstruction = "b101011_00010_00101_0000000001101000".U
      val jalInstruction = "b000011_00000_00000000000000000000".U
      // Set the input instruction and evaluate the decoder
      c.io.Instr_word.poke(addInstruction)
      c.clock.step()
      c.io.add_op.expect(true)
      c.io.sub_op.expect(false)
      c.io.lw_op.expect(false)
      c.io.sw_op.expect(false)
      c.io.nop_op.expect(false)

      c.io.Instr_word.poke(subInstruction)
      c.clock.step()
      c.io.add_op.expect(false)
      c.io.sub_op.expect(true)
      c.io.lw_op.expect(false)
      c.io.sw_op.expect(false)
      c.io.nop_op.expect(false)

      c.io.Instr_word.poke(lwInstruction)
      c.clock.step()
      c.io.add_op.expect(false)
      c.io.sub_op.expect(false)
      c.io.lw_op.expect(true)
      c.io.sw_op.expect(false)
      c.io.nop_op.expect(false)

      c.io.Instr_word.poke(swInstruction)
      c.clock.step()
      c.io.add_op.expect(false)
      c.io.sub_op.expect(false)
      c.io.lw_op.expect(false)
      c.io.sw_op.expect(true)
      c.io.nop_op.expect(false)

      c.io.Instr_word.poke(jalInstruction)
      c.clock.step()
      c.io.add_op.expect(false)
      c.io.sub_op.expect(false)
      c.io.lw_op.expect(false)
      c.io.sw_op.expect(false)
      c.io.nop_op.expect(true)
    }
  }
}

Design register file

There are a total of 32 32-bit registers, allowing two reads and one write, and register 0 has a fixed readout bit of 0. The four input signals are RS1, RS2, WB_data, and Reg_WB, and the register outputs RS1_out and RS2_out; the initial value stored inside the register is equal to the register number.

Given the Chisel design code and simulation test waveform, observe the output waveform of RS1=5, RS2=8, WB_data=0x1234, Reg_WB=1 and the values of the affected registers.

Register.scala

import chisel3._
import chisel3.util._

class RegisterFile extends Module {
  val io = IO(new Bundle {
    val RS1 = Input(UInt(5.W)) // RS1 input signal, used to select the register to be read
    val RS2 = Input(UInt(5.W)) // RS2 input signal, used to select the register to be read
    val WB_data = Input(UInt(32.W)) //Write data signal, used to write registers
    val Reg_WB = Input(UInt(5.W)) //Select the register to write data to
    val RS1_out = Output(UInt(32.W)) // RS1 output data
    val RS2_out = Output(UInt(32.W)) // RS2 output data
  })

  val registers = RegInit(VecInit((0 until 32).map(_.U(32.W)))) // 32 32-bit registers, the initial value is equal to the register number
  registers(io.Reg_WB) := io.WB_data // Write data to register
  io.RS1_out := Mux(io.RS1 === 0.U, 0.U, registers(io.RS1)) // RS1 outputs data, register 0 is fixed to read bit 0
  io.RS2_out := Mux(io.RS2 === 0.U, 0.U, registers(io.RS2)) // RS2 output data, register 0 is fixed to read bit 0
}

object RegisterFile extends App {
  (new chisel3.stage.ChiselStage).emitVerilog(new RegisterFile())
}

RegisterTest.scala

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

class RegisterFileTest extends AnyFlatSpec with ChiselScalatestTester {
  behavior of "RegisterFile"
  it should "correctly update and read registers" in {
    test(new RegisterFile).withAnnotations(Seq(WriteVcdAnnotation)) { c =>
      //Set input signal
      c.io.RS1.poke(5.U)
      c.io.RS2.poke(8.U)
      c.io.WB_data.poke(0x1234.U)
      c.io.Reg_WB.poke(1.U)

      c.clock.step()
      c.io.RS1_out.expect(5.U)
      c.io.RS2_out.expect(8.U)
    }
  }
}

Implementing a 32-word instruction memory

Store 4 instructions add R1, R2, R3 from address 0; sub R0, R5, R6, lw R5,100(R2), sw R5,104(R2). Then combine the instruction memory, register file, decoding circuit, and combine it with the PC update circuit (the initial value of PC is 0), WB_data and Reg_WB signal generation circuit, and finally allow the circuit to fetch and decode instructions one by one (without completing the instruction execution).

The Chisel design code and simulation test waveforms are given, the execution process waveforms of the four instructions are observed, and their meanings are recorded and explained.

InstructionMemory.scala

import chisel3._

class InstructionMemory extends Module {
  val io = IO(new Bundle {
    val address = Input(UInt(5.W)) // 32 words, 5-bit address required
    val instruction = Output(UInt(32.W))
  })
  //Create a 32-word instruction memory
  val mem = Mem(32, UInt(32.W))
  //Initialize memory and store MIPS instructions
  mem.write(0.U, "b000000_00010_00011_00001_00000_100000".U) // add R1, R2, R3
  mem.write(1.U, "b000000_00101_00110_00000_00000_100010".U) // sub R0, R5, R6
  mem.write(2.U, "b100011_00010_00101_0000000001100100".U) // lw R5, 100(R2)
  mem.write(3.U, "b101011_00010_00101_0000000001101000".U) // sw R5, 104(R2)
  //Read instructions from memory
  io.instruction := mem.read(io.address)
}

Circuit.scala

import chisel3._
import chisel3.util._

class Circuit extends Module {
  val io = IO(new Bundle {
    //Register input and output
    val WB_data = Input(UInt(32.W)) //Write data signal, used to write registers
    val Reg_WB = Input(UInt(5.W)) //Select the register to write data to
    val RS1_out = Output(UInt(32.W))
    val RS2_out = Output(UInt(32.W))
    // decode
    val add_op = Output(Bool())
    val sub_op = Output(Bool())
    val lw_op = Output(Bool())
    val sw_op = Output(Bool())
    val nop_op = Output(Bool())
  })

  val instructionMemory = Module(new InstructionMemory)
  val registerFile = Module(new RegisterFile)
  val decoder = Module(new Decoder)
  val pc = RegInit(0.U(5.W))
  // Get the corresponding instruction from the instruction register based on the value of pc
  instructionMemory.io.address := pc
  decoder.io.Instr_word := instructionMemory.io.instruction
  registerFile.io.RS1 := instructionMemory.io.instruction(25, 21)
  registerFile.io.RS2 := instructionMemory.io.instruction(20, 16)
  registerFile.io.WB_data := (0.U(32.W))
  registerFile.io.Reg_WB := (0.U(5.W))
  // update output
  io.RS1_out := registerFile.io.RS1_out
  io.RS2_out := registerFile.io.RS2_out
  io.add_op := decoder.io.add_op
  io.sub_op := decoder.io.sub_op
  io.lw_op := decoder.io.lw_op
  io.sw_op := decoder.io.sw_op
  io.nop_op := decoder.io.nop_op
  //Update PC
  pc := pc + 1.U
}

object Circuit extends App {
  (new chisel3.stage.ChiselStage).emitVerilog(new Circuit())
}

CircuitTest.scala

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

class CircuitTest extends AnyFlatSpec with ChiselScalatestTester {
  behavior of "Circuit"
  it should "correct circuit" in {
    test(new Circuit).withAnnotations(Seq(WriteVcdAnnotation)) { c =>
      c.clock.step()
      c.clock.step()
      c.clock.step()
      c.clock.step()
    }
  }
}