---
# System prepended metadata

title: 'Assignment 3: My RISC-V CPU'

---

# Assignment 3: My RISC-V CPU
#### contributed by <`bbchen`>
[Full code](https://github.com/CHENSHIBO0000/ca2025-mycpu.git)

# [Lab3](https://hackmd.io/@sysprog/B1Qxu2UkZx#RISC-V-CPU-with-MMIO-Peripherals-and-Trap-Handling)

## Single-cycle CPU

### Overview of Single-Cycle CPU Implementation

This single-cycle RISC-V CPU are building supports a basic subset of the RV32I instruction set, including:

1. Arithmetic and Logic Operations.
2. Memory Access Instructions.
3. Branch and Jump Instructions.

---
To execute each instruction, the CPU goes through five main phases:

1. `Instruction Fetch` – Retrieve the instruction from memory.
2. `Instruction Decode` – Interpret the instruction and read the required register values.
3. `Execute` – Perform the necessary computation using the ALU.
4. `Memory Access` – For load/store instructions, read or write data from/to memory.
5. `Write-back` – Store the computed or loaded result back into the register file (except for store instructions).

---

### 1. Instruction Fetch

This is the first stage of the processor, responsible for:
- Maintaining the program counter (PC) register
- Providing current PC to instruction memory
- Handling control flow changes from Execute stage


And PC update logic:
- Sequential: PC = PC + 4 (when no jump/branch)
- Control flow: PC = jump_address_id (when jump_flag_id asserted by Execute stage)

---

:::spoiler IF
```scala
package riscv.core

import chisel3._
import riscv.Parameters

// Program counter reset value
object ProgramCounter {
  val EntryAddress = Parameters.EntryAddress
}
class InstructionFetch extends Module {
  val io = IO(new Bundle {
    val jump_flag_id          = Input(Bool())
    val jump_address_id       = Input(UInt(Parameters.AddrWidth))
    val instruction_read_data = Input(UInt(Parameters.DataWidth))
    val instruction_valid     = Input(Bool())

    val instruction_address = Output(UInt(Parameters.AddrWidth))
    val instruction         = Output(UInt(Parameters.InstructionWidth))
  })

  // Program counter register (PC)
  val pc = RegInit(ProgramCounter.EntryAddress)

  when(io.instruction_valid) {
    io.instruction := io.instruction_read_data

    pc := Mux(io.jump_flag_id, io.jump_address_id, pc + 4.U)

  }.otherwise {
    pc             := pc
    io.instruction := 0x00000013.U // NOP: prevents illegal instruction execution
  }
  io.instruction_address := pc
}
```
:::


I complete PC update logic using muitiplexer.

```scale
    pc := Mux(io.jump_flag_id, io.jump_address_id, pc + 4.U)
```

---

### 2. Instruction Decode

This stage is response for :
 - Extract instruction fields (`opcode`, `rd`, `rs1`, `rs2`, `funct3`, `funct7`)
 - Generate control signals for `Execute`, `Memory`, and `WriteBack` stages
 - Decode and sign-extend `immediate` values based on instruction format
 - Determine ALU operand sources (register vs PC, register vs immediate)
 - Identify register file read/write operations
 - Configure memory access (read/write enable signals)


:::spoiler ID
```scala
package riscv.core

import chisel3._
import chisel3.util._
import chisel3.ChiselEnum
import riscv.Parameters

// RV32I opcode groupings
object InstructionTypes {
  val Load    = "b0000011".U(7.W)
  val OpImm   = "b0010011".U(7.W)
  val Store   = "b0100011".U(7.W)
  val Op      = "b0110011".U(7.W)
  val Lui     = "b0110111".U(7.W)
  val Auipc   = "b0010111".U(7.W)
  val Jal     = "b1101111".U(7.W)
  val Jalr    = "b1100111".U(7.W)
  val Branch  = "b1100011".U(7.W)
  val MiscMem = "b0001111".U(7.W)
  val System  = "b1110011".U(7.W)
}

// Convenience aliases for specific opcodes
object Instructions {
  val jal   = InstructionTypes.Jal
  val jalr  = InstructionTypes.Jalr
  val lui   = InstructionTypes.Lui
  val auipc = InstructionTypes.Auipc
}

// Funct3 encodings for load instructions
object InstructionsTypeL {
  val lb  = "b000".U(3.W)
  val lh  = "b001".U(3.W)
  val lw  = "b010".U(3.W)
  val lbu = "b100".U(3.W)
  val lhu = "b101".U(3.W)
}

// Funct3 encodings for store instructions
object InstructionsTypeS {
  val sb = "b000".U(3.W)
  val sh = "b001".U(3.W)
  val sw = "b010".U(3.W)
}

// Funct3 encodings for OP-IMM instructions
object InstructionsTypeI {
  val addi  = "b000".U(3.W)
  val slli  = "b001".U(3.W)
  val slti  = "b010".U(3.W)
  val sltiu = "b011".U(3.W)
  val xori  = "b100".U(3.W)
  val sri   = "b101".U(3.W)
  val ori   = "b110".U(3.W)
  val andi  = "b111".U(3.W)
}

// Funct3 encodings for OP instructions
object InstructionsTypeR {
  val add_sub = "b000".U(3.W)
  val sll     = "b001".U(3.W)
  val slt     = "b010".U(3.W)
  val sltu    = "b011".U(3.W)
  val xor     = "b100".U(3.W)
  val sr      = "b101".U(3.W)
  val or      = "b110".U(3.W)
  val and     = "b111".U(3.W)
}

// Funct3 encodings for branch instructions
object InstructionsTypeB {
  val beq  = "b000".U(3.W)
  val bne  = "b001".U(3.W)
  val blt  = "b100".U(3.W)
  val bge  = "b101".U(3.W)
  val bltu = "b110".U(3.W)
  val bgeu = "b111".U(3.W)
}

object ALUOp1Source {
  val Register           = 0.U(1.W)
  val InstructionAddress = 1.U(1.W)
}

object ALUOp2Source {
  val Register  = 0.U(1.W)
  val Immediate = 1.U(1.W)
}

object RegWriteSource {
  val ALUResult              = 0.U(2.W)
  val Memory                 = 1.U(2.W)
  val NextInstructionAddress = 2.U(2.W)
}

object ImmediateKind extends ChiselEnum {
  val None, I, S, B, U, J = Value
}


class InstructionDecode extends Module {
  val io = IO(new Bundle {
    val instruction = Input(UInt(Parameters.InstructionWidth))

    val regs_reg1_read_address = Output(UInt(Parameters.PhysicalRegisterAddrWidth))
    val regs_reg2_read_address = Output(UInt(Parameters.PhysicalRegisterAddrWidth))
    val ex_immediate           = Output(UInt(Parameters.DataBits.W))
    val ex_aluop1_source       = Output(UInt(1.W))
    val ex_aluop2_source       = Output(UInt(1.W))
    val memory_read_enable     = Output(Bool())
    val memory_write_enable    = Output(Bool())
    val wb_reg_write_source    = Output(UInt(2.W))
    val reg_write_enable       = Output(Bool())
    val reg_write_address      = Output(UInt(Parameters.PhysicalRegisterAddrWidth))
  })

  val instruction = io.instruction
  val opcode      = instruction(6, 0)
  val rs1         = instruction(19, 15)
  val rs2         = instruction(24, 20)
  val rd          = instruction(11, 7)

  val isLoad   = opcode === InstructionTypes.Load
  val isStore  = opcode === InstructionTypes.Store
  val isOpImm  = opcode === InstructionTypes.OpImm
  val isOp     = opcode === InstructionTypes.Op
  val isLui    = opcode === InstructionTypes.Lui
  val isAuipc  = opcode === InstructionTypes.Auipc
  val isJal    = opcode === InstructionTypes.Jal
  val isJalr   = opcode === InstructionTypes.Jalr
  val isBranch = opcode === InstructionTypes.Branch
  val usesRs1  = isLoad || isStore || isOpImm || isOp || isBranch || isJalr
  val usesRs2  = isStore || isOp || isBranch
  val regWrite = isLoad || isOpImm || isOp || isLui || isAuipc || isJal || isJalr


  val wbSource = WireDefault(RegWriteSource.ALUResult)

  when(isLoad) {
    wbSource := RegWriteSource.Memory
  }

  .elsewhen(isJal || isJalr) {
    wbSource := RegWriteSource.NextInstructionAddress
  }


  val aluOp1Sel = WireDefault(ALUOp1Source.Register)

  when(isBranch || isAuipc || isJal) {
    aluOp1Sel := ALUOp1Source.InstructionAddress
  }


  val needsImmediate = isLoad || isStore || isOpImm || isBranch || isLui || isAuipc || isJal || isJalr
  val aluOp2Sel      = WireDefault(ALUOp2Source.Register)

  when(needsImmediate) {
    aluOp2Sel := ALUOp2Source.Immediate
  }

  val immKind = WireDefault(ImmediateKind.None)
  when(isLoad || isOpImm || isJalr) {
    immKind := ImmediateKind.I
  }
  when(isStore) {
    immKind := ImmediateKind.S
  }
  when(isBranch) {
    immKind := ImmediateKind.B
  }
  when(isLui || isAuipc) {
    immKind := ImmediateKind.U
  }
  when(isJal) {
    immKind := ImmediateKind.J
  }

  io.regs_reg1_read_address := Mux(usesRs1, rs1, 0.U)
  io.regs_reg2_read_address := Mux(usesRs2, rs2, 0.U)
  io.ex_aluop1_source       := aluOp1Sel
  io.ex_aluop2_source       := aluOp2Sel
  io.memory_read_enable     := isLoad
  io.memory_write_enable    := isStore
  io.wb_reg_write_source    := wbSource
  io.reg_write_enable       := regWrite
  io.reg_write_address      := rd


  val immI = Cat(
    Fill(Parameters.DataBits - 12, instruction(31)),  // Sign extension: replicate bit 31 twenty times
    instruction(31, 20)                                // Immediate: bits [31:20]
  )

  val immS = Cat(
    Fill(Parameters.DataBits - 12, instruction(31)),  // Sign extension
    instruction(31, 25),                                  // High 7 bits
    instruction(11, 7)                                   // Low 5 bits
  )


  val immB = Cat(
    Fill(Parameters.DataBits - 13, instruction(31)), // Sign extension
    instruction(31),                                  // bit [12]
    instruction(7),                                  // bit [11]
    instruction(30, 25),                               // bits [10:5]
    instruction(11, 8),                                // bits [4:1]
    0.U(1.W)                                          // bit [0] = 0 (alignment)
  )


  val immU = Cat(instruction(31, 12), 0.U(12.W))


  val immJ = Cat(
    Fill(Parameters.DataBits - 21, instruction(31)), // Sign extension
    instruction(31),                                  // bit [20]
    instruction(19, 12),                             // bits [19:12]
    instruction(20),                                  // bit [11]
    instruction(30, 21),                             // bits [10:1]
    0.U(1.W)                                          // bit [0] = 0 (alignment)
  )

  val immediate = MuxLookup(immKind.asUInt, 0.U(Parameters.DataBits.W))(
    Seq(
      ImmediateKind.I.asUInt -> immI,
      ImmediateKind.S.asUInt -> immS,
      ImmediateKind.B.asUInt -> immB,
      ImmediateKind.U.asUInt -> immU,
      ImmediateKind.J.asUInt -> immJ
    )
  )
  io.ex_immediate := immediate
}

```
:::


---

### 3. Execution

 Execute stage's responsibilities:
  - Select ALU operands from register data, PC, or immediate values
  - Perform arithmetic and logical operations via ALU submodule
  - Evaluate branch conditions for all six RV32I branch types
  - Calculate branch and jump target addresses
  - Generate control signals for instruction fetch (jump flag and address)

:::spoiler EX
```scala
package riscv.core

import chisel3._
import chisel3.util.Cat
import chisel3.util.MuxLookup
import riscv.Parameters


class Execute extends Module {
  val io = IO(new Bundle {
    val instruction         = Input(UInt(Parameters.InstructionWidth))
    val instruction_address = Input(UInt(Parameters.AddrWidth))
    val reg1_data           = Input(UInt(Parameters.DataWidth))
    val reg2_data           = Input(UInt(Parameters.DataWidth))
    val immediate           = Input(UInt(Parameters.DataWidth))
    val aluop1_source       = Input(UInt(1.W))
    val aluop2_source       = Input(UInt(1.W))

    val mem_alu_result  = Output(UInt(Parameters.DataWidth))
    val if_jump_flag    = Output(Bool())
    val if_jump_address = Output(UInt(Parameters.DataWidth))
  })

  // Decode instruction fields
  val opcode = io.instruction(6, 0)
  val funct3 = io.instruction(14, 12)
  val funct7 = io.instruction(31, 25)

  // Instantiate ALU and control logic
  val alu      = Module(new ALU)
  val alu_ctrl = Module(new ALUControl)

  alu_ctrl.io.opcode := opcode
  alu_ctrl.io.funct3 := funct3
  alu_ctrl.io.funct7 := funct7

  // Select ALU operands based on instruction type
  alu.io.func := alu_ctrl.io.alu_funct
  val aluOp1 = Mux(io.aluop1_source === ALUOp1Source.InstructionAddress, io.instruction_address, io.reg1_data)
  val aluOp2 = Mux(io.aluop2_source === ALUOp2Source.Immediate, io.immediate, io.reg2_data)
  alu.io.op1 := aluOp1
  alu.io.op2 := aluOp2

  io.mem_alu_result := alu.io.result


  val branchCondition = MuxLookup(funct3, false.B)(
    Seq(
      // TODO: Implement six branch conditions
      // Hint: Compare two register data values based on branch type
      InstructionsTypeB.beq  -> (io.reg1_data === io.reg2_data),
      InstructionsTypeB.bne  -> (io.reg1_data =/= io.reg2_data),

      // Signed comparison (need conversion to signed type)
      InstructionsTypeB.blt  -> (io.reg1_data.asSInt < io.reg2_data.asSInt),
      InstructionsTypeB.bge  -> (io.reg1_data.asSInt >= io.reg2_data.asSInt),

      // Unsigned comparison
      InstructionsTypeB.bltu -> (io.reg1_data < io.reg2_data),
      InstructionsTypeB.bgeu -> (io.reg1_data >= io.reg2_data)
    )
  )
  val isBranch = opcode === InstructionTypes.Branch
  val isJal    = opcode === Instructions.jal
  val isJalr   = opcode === Instructions.jalr


  val branchTarget = io.instruction_address + io.immediate

  val jalTarget    = branchTarget  // JAL and Branch use same calculation method

  // JALR address calculation:
  //   1. Add register value and immediate
  //   2. Clear LSB (2-byte alignment)
  val jalrSum      = io.reg1_data + io.immediate

  val jalrTarget   = Cat(jalrSum(31, 1), 0.U(1.W))
  
  val branchTaken = isBranch && branchCondition
  io.if_jump_flag := branchTaken || isJal || isJalr
  io.if_jump_address := Mux(
    isJalr,
    jalrTarget,
    Mux(isJal, jalTarget, branchTarget)
  )
}


```
:::

---

### 4. Memory Access

This stage implements RV32I memory access operations:
 - Load operations (`LB`, `LH`, `LW`, `LBU`, `LHU`): extract and sign/zero-extend data
 - Store operations (`SB`, `SH`, `SW`): write with byte-level strobes


:::spoiler Memory access
```scala
package riscv.core

import chisel3._
import chisel3.util._
import peripheral.RAMBundle
import riscv.Parameters


class MemoryAccess extends Module {
  val io = IO(new Bundle() {
    val alu_result          = Input(UInt(Parameters.DataWidth))
    val reg2_data           = Input(UInt(Parameters.DataWidth))
    val memory_read_enable  = Input(Bool())
    val memory_write_enable = Input(Bool())
    val funct3              = Input(UInt(3.W))

    val wb_memory_read_data = Output(UInt(Parameters.DataWidth))

    val memory_bundle = Flipped(new RAMBundle)
  })
  val mem_address_index = io.alu_result(log2Up(Parameters.WordSize) - 1, 0).asUInt

  io.memory_bundle.write_enable := false.B
  io.memory_bundle.write_data   := 0.U
  io.memory_bundle.address      := io.alu_result
  io.memory_bundle.write_strobe := VecInit(Seq.fill(Parameters.WordSize)(false.B))
  io.wb_memory_read_data        := 0.U

  when(io.memory_read_enable) {
    // Optimized load logic: extract bytes/halfwords based on address alignment
    val data  = io.memory_bundle.read_data
    val bytes = Wire(Vec(Parameters.WordSize, UInt(Parameters.ByteWidth)))
    for (i <- 0 until Parameters.WordSize) {
      bytes(i) := data((i + 1) * Parameters.ByteBits - 1, i * Parameters.ByteBits)
    }
    // Select byte based on lower 2 address bits (mem_address_index)
    val byte = bytes(mem_address_index)
    // Select halfword based on bit 1 of address (word-aligned halfwords)
    val half = Mux(mem_address_index(1), Cat(bytes(3), bytes(2)), Cat(bytes(1), bytes(0)))

    io.wb_memory_read_data := MuxLookup(io.funct3, 0.U)(
      Seq(

        InstructionsTypeL.lb  -> Cat(Fill(24, data(7)), data(7, 0)),


        InstructionsTypeL.lbu -> Cat(0.U(24.W), byte),


        InstructionsTypeL.lh  -> Cat(Fill(16, half(15)), half(15, 0)),


        InstructionsTypeL.lhu -> Cat(0.U(16.W), half),


        InstructionsTypeL.lw  -> data
      )
    )
 
  }.elsewhen(io.memory_write_enable) {
    io.memory_bundle.write_enable := true.B
    io.memory_bundle.address      := io.alu_result

    val data = io.reg2_data

    val strobeInit   = VecInit(Seq.fill(Parameters.WordSize)(false.B))
    val defaultData  = 0.U(Parameters.DataWidth)
    val writeStrobes = WireInit(strobeInit)
    val writeData    = WireDefault(defaultData)

    switch(io.funct3) {
      is(InstructionsTypeS.sb) {

        writeStrobes(mem_address_index) := true.B
        writeData := data(7, 0) << (mem_address_index << 3)
      }
      is(InstructionsTypeS.sh) {

        when(mem_address_index(1) === 0.U) {

          writeStrobes(0) := true.B
          writeStrobes(1) := true.B
          writeData := data(15, 0)
        }.otherwise {

          writeStrobes(2) := true.B
          writeStrobes(3) := true.B
          writeData := data(15, 0) << 16
        }
      }
      is(InstructionsTypeS.sw) {

        writeStrobes := VecInit(Seq.fill(Parameters.WordSize)(true.B))
        writeData    := data
      }
    }
    io.memory_bundle.write_data   := writeData
    io.memory_bundle.write_strobe := writeStrobes
  }
}
```
:::

---

### 5. Write-back


Write Back stage selects final result to write to register file

This is the final stage of the processor, responsible for multiplexing the appropriate data source to be written back to the register file:
- ALU result (default): Arithmetic/logical operation output
- Memory data: Load instruction result
- Next instruction address (PC+4): Return address for JAL/JALR instructions


:::spoiler WB
```scala
package riscv.core

import chisel3._
import chisel3.util._
import riscv.Parameters

class WriteBack extends Module {
  val io = IO(new Bundle() {
    val instruction_address = Input(UInt(Parameters.AddrWidth))
    val alu_result          = Input(UInt(Parameters.DataWidth))
    val memory_read_data    = Input(UInt(Parameters.DataWidth))
    val regs_write_source   = Input(UInt(2.W))
    val regs_write_data     = Output(UInt(Parameters.DataWidth))
  })


  io.regs_write_data := MuxLookup(io.regs_write_source, io.alu_result)(
    Seq(
      RegWriteSource.Memory                 -> io.memory_read_data,
      RegWriteSource.NextInstructionAddress -> (io.instruction_address + 4.U)
    )
  )
}
```
:::

---
### Functional Test

Using Chiseltest to run functional tests for the CPU design. Chiseltest provides a lightweight and expressive framework for verifying Chisel-based RTL.

As an example, a simple C program is that computes `Fibonacci(10)` and writes the result to memory address 4. 

During testing, the Chiseltest framework checks this memory location to confirm that the CPU correctly executed the program and stored the expected value.

```
make test
```
![image](https://hackmd.io/_uploads/rJcU3QpZbx.png)

All functional and integration tests for the single-cycle CPU passed successfully, confirming that each submodule operates as intended.

The CPU correctly executed a variety of workloads, from basic RV32I operations to recursive Fibonacci computation and even the Quicksort algorithm. These results demonstrate that the overall design is stable, compliant with RISC-V requirements, and capable of running nontrivial programs reliably.

---

### RISCOF Compliance Testing


RISCOF (RISC-V Compatibility Framework) is the official framework used to verify whether a RISC-V processor implementation conforms to the RISC-V ISA specification. It works by running a collection of official RISC-V architectural tests (riscv-arch-test) on both the user-designed CPU (the DUT) and a trusted golden reference model. Each test program produces a memory region called a signature, which records the results of the test’s execution. RISCOF compares the DUT’s signature with the golden signature, and a test is considered passed only if both match exactly.

![image](https://hackmd.io/_uploads/HyUZ07aZWe.png)

![image](https://hackmd.io/_uploads/r1X4AmTWZx.png)

The RISCOF compliance report shows that my CPU implementation successfully passed all `41` RV32I architectural tests with 0 failures. This indicates that the functional behavior of the processor fully matches the RISC-V ISA specification for the RV32I subset.

Therefore, based on the complete `RISCOF` pass rate, the CPU can be considered a functionally correct and ISA-compliant RV32I processor.

---

## MMIO Peripherals and Trap Handling

### Control and Status Register (CSR)

The module manages machine-level status, interrupt control, trap handling, and provides debug visibility for verification. It supports atomic CSR operations, ensures consistent trap entry semantics, and interfaces directly with the CLINT for interrupt-driven updates.

---

The CSR module implements the following features:
- A separate 4096-entry CSR register file mapped to the RISC-V CSR address space
- Read-only enforcement for constant or information-only CSRs
- Atomic Read-Modify-Write (RMW) semantics for CSRRS/CSRRC instructions in a single cycle
- CLINT interface for interrupt-driven CSR updates
- Debug read port for simulation and verification
- Full support for machine-mode CSRs required by trap and interrupt handling



:::spoiler CSR
```scala
package riscv.core

import chisel3._
import chisel3.util._
import riscv.Parameters

// RISC-V Machine-mode CSR addresses (Privileged Spec Vol.II)
object CSRRegister {
  val MSTATUS  = 0x300.U(Parameters.CSRRegisterAddrWidth)
  val MIE      = 0x304.U(Parameters.CSRRegisterAddrWidth)
  val MTVEC    = 0x305.U(Parameters.CSRRegisterAddrWidth)
  val MSCRATCH = 0x340.U(Parameters.CSRRegisterAddrWidth)
  val MEPC     = 0x341.U(Parameters.CSRRegisterAddrWidth)
  val MCAUSE   = 0x342.U(Parameters.CSRRegisterAddrWidth)
  val CycleL   = 0xc00.U(Parameters.CSRRegisterAddrWidth)
  val CycleH   = 0xc80.U(Parameters.CSRRegisterAddrWidth)
}

class CSR extends Module {
  val io = IO(new Bundle {
    val reg_read_address_id    = Input(UInt(Parameters.CSRRegisterAddrWidth))
    val reg_write_enable_id    = Input(Bool())
    val reg_write_address_id   = Input(UInt(Parameters.CSRRegisterAddrWidth))
    val reg_write_data_ex      = Input(UInt(Parameters.DataWidth))
    val debug_reg_read_address = Input(UInt(Parameters.CSRRegisterAddrWidth))

    val debug_reg_read_data = Output(UInt(Parameters.DataWidth))
    val reg_read_data       = Output(UInt(Parameters.DataWidth))

    val clint_access_bundle = Flipped(new CSRDirectAccessBundle)
  })

  val mstatus  = RegInit(UInt(Parameters.DataWidth), 0.U)
  val mie      = RegInit(UInt(Parameters.DataWidth), 0.U)
  val mtvec    = RegInit(UInt(Parameters.DataWidth), 0.U)
  val mscratch = RegInit(UInt(Parameters.DataWidth), 0.U)
  val mepc     = RegInit(UInt(Parameters.DataWidth), 0.U)
  val mcause   = RegInit(UInt(Parameters.DataWidth), 0.U)
  val cycles   = RegInit(UInt(64.W), 0.U)
 
  val regLUT =
    IndexedSeq(
      CSRRegister.MSTATUS  -> mstatus,
      CSRRegister.MIE      -> mie,
      CSRRegister.MTVEC    -> mtvec,
      CSRRegister.MSCRATCH -> mscratch,
      CSRRegister.MEPC     -> mepc,
      CSRRegister.MCAUSE   -> mcause,

      CSRRegister.CycleL   -> cycles(31, 0),
      CSRRegister.CycleH   -> cycles(63, 32),
    )
  cycles := cycles + 1.U


  io.reg_read_data       := MuxLookup(io.reg_read_address_id, 0.U)(regLUT)
  io.debug_reg_read_data := MuxLookup(io.debug_reg_read_address, 0.U)(regLUT)

  io.clint_access_bundle.mstatus := mstatus
  io.clint_access_bundle.mtvec   := mtvec
  io.clint_access_bundle.mcause  := mcause
  io.clint_access_bundle.mepc    := mepc
  io.clint_access_bundle.mie     := mie


  // - mcause: Record trap cause
  when(io.clint_access_bundle.direct_write_enable) {

    mstatus := io.clint_access_bundle.mstatus_write_data
    mepc := io.clint_access_bundle.mepc_write_data
    mcause := io.clint_access_bundle.mcause_write_data
  }.elsewhen(io.reg_write_enable_id) {

    when(io.reg_write_address_id === CSRRegister.MSTATUS) {
      mstatus := io.reg_write_data_ex
    }.elsewhen(io.reg_write_address_id === CSRRegister.MEPC) {
      mepc := io.reg_write_data_ex
    }.elsewhen(io.reg_write_address_id === CSRRegister.MCAUSE) {
      mcause := io.reg_write_data_ex
    }
  }


  when(io.reg_write_enable_id) {
    when(io.reg_write_address_id === CSRRegister.MIE) {
      mie := io.reg_write_data_ex
    }.elsewhen(io.reg_write_address_id === CSRRegister.MTVEC) {
      mtvec := io.reg_write_data_ex
    }.elsewhen(io.reg_write_address_id === CSRRegister.MSCRATCH) {
      mscratch := io.reg_write_data_ex
    }
  }

}
```
:::

---

### Core-Local Interrupt Controller (CLINT)

CLINT manages interrupt entry/exit and CSR state transitions.

CLINT's responsibilities:
- Handle external interrupts from peripherals (timer, UART)
- Manage trap entry for exceptions (ECALL, EBREAK)
- Execute MRET (machine return) for trap exit
- Update CSR state atomically during trap handling

---

State Transitions:
- Interrupt entry: Save PC to `mepc`, set `mcause`, disable interrupts (`MIE=0`), jump to `mtvec`.
- Exception entry: Same as interrupt, but `mcause[31]` = 0.
- MRET: Restore `PC` from `mepc`, re-enable interrupts (`MIE=MPIE`), set `MPIE=1`.

CSR Updates (`mstatus`):
- Trap entry: `MPIE←MIE`, `MIE←0` (save and disable interrupts)
- `MRET`: `MIE←MPIE`, `MPIE←1` (restore interrupts and set MPIE)

:::spoiler CLINT
```scala
package riscv.core

import chisel3._
import chisel3.util.Cat
import chisel3.util.MuxLookup
import riscv.Parameters

// Interrupt cause codes for mcause register
object InterruptCode {
  val None   = 0x0.U(8.W)
  val Timer0 = 0x1.U(8.W)
  val Ret    = 0xff.U(8.W)
}

object InterruptEntry {
  val Timer0 = 0x4.U(8.W)
}

class CSRDirectAccessBundle extends Bundle {
  val mstatus = Input(UInt(Parameters.DataWidth))
  val mepc    = Input(UInt(Parameters.DataWidth))
  val mcause  = Input(UInt(Parameters.DataWidth))
  val mtvec   = Input(UInt(Parameters.DataWidth))
  val mie     = Input(UInt(Parameters.DataWidth))

  val mstatus_write_data = Output(UInt(Parameters.DataWidth))
  val mepc_write_data    = Output(UInt(Parameters.DataWidth))
  val mcause_write_data  = Output(UInt(Parameters.DataWidth))

  val direct_write_enable = Output(Bool())
}


class CLINT extends Module {
  val io = IO(new Bundle {
    // Interrupt signals from peripherals
    val interrupt_flag = Input(UInt(Parameters.InterruptFlagWidth))

    val instruction         = Input(UInt(Parameters.InstructionWidth))
    val instruction_address = Input(UInt(Parameters.AddrWidth))

    val jump_flag    = Input(Bool())
    val jump_address = Input(UInt(Parameters.AddrWidth))

    val interrupt_handler_address = Output(UInt(Parameters.AddrWidth))
    val interrupt_assert          = Output(Bool())

    val csr_bundle = new CSRDirectAccessBundle
  })
  val interrupt_enable_global   = io.csr_bundle.mstatus(3) // MIE bit (global enable)
  val interrupt_enable_timer    = io.csr_bundle.mie(7)     // MTIE bit (timer enable)
  val interrupt_enable_external = io.csr_bundle.mie(11)    // MEIE bit (external enable)

  val instruction_address = Mux(
    io.jump_flag,
    io.jump_address,
    io.instruction_address + 4.U,
  )
  val mpie = io.csr_bundle.mstatus(7)
  val mie  = io.csr_bundle.mstatus(3)

  // Check individual interrupt source enable based on interrupt type
  val interrupt_source_enabled = Mux(
    io.interrupt_flag === InterruptCode.Timer0,
    interrupt_enable_timer,
    interrupt_enable_external
  )

  when(io.interrupt_flag =/= InterruptCode.None && interrupt_enable_global && interrupt_source_enabled) { // interrupt
    io.interrupt_assert          := true.B
    io.interrupt_handler_address := io.csr_bundle.mtvec

    io.csr_bundle.mstatus_write_data :=
      Cat(
        io.csr_bundle.mstatus(31, 13),
        3.U(2.W), // mpp ← 0b11 (Machine mode)
        io.csr_bundle.mstatus(10, 8),
        mie, // mpie ← mie (save current interrupt enable)
        io.csr_bundle.mstatus(6, 4),
        0.U(1.W), // mie ← 0 (disable interrupts)
        io.csr_bundle.mstatus(2, 0)
      )
    io.csr_bundle.mepc_write_data := instruction_address
    io.csr_bundle.mcause_write_data :=
      Cat(
        1.U,
        MuxLookup(
          io.interrupt_flag,
          11.U(31.W) // machine external interrupt
        )(
          IndexedSeq(
            InterruptCode.Timer0 -> 7.U(31.W),
          )
        )
      )
    io.csr_bundle.direct_write_enable := true.B
  }.elsewhen(io.instruction === InstructionsEnv.ebreak || io.instruction === InstructionsEnv.ecall) { // exception
    io.interrupt_assert          := true.B
    io.interrupt_handler_address := io.csr_bundle.mtvec
    io.csr_bundle.mstatus_write_data :=
      Cat(
        io.csr_bundle.mstatus(31, 13),
        3.U(2.W), // mpp ← 0b11 (Machine mode)
        io.csr_bundle.mstatus(10, 8),
        mie, // mpie
        io.csr_bundle.mstatus(6, 4),
        0.U(1.W), // mie
        io.csr_bundle.mstatus(2, 0)
      )
    io.csr_bundle.mepc_write_data := instruction_address
    io.csr_bundle.mcause_write_data := Cat(
      0.U,
      MuxLookup(io.instruction, 0.U)(
        IndexedSeq(
          InstructionsEnv.ebreak -> 3.U(31.W),
          InstructionsEnv.ecall  -> 11.U(31.W),
        )
      )
    )
    io.csr_bundle.direct_write_enable := true.B

  }.elsewhen(io.instruction === InstructionsRet.mret) { // ret
    io.interrupt_assert          := true.B
    io.interrupt_handler_address := io.csr_bundle.mepc

    io.csr_bundle.mstatus_write_data :=
      Cat(
        io.csr_bundle.mstatus(31, 13),
        3.U(2.W), // mpp ← 0b11 (Machine mode)
        io.csr_bundle.mstatus(10, 8),
        1.U(1.W), // mpie ← 1 (reset MPIE)
        io.csr_bundle.mstatus(6, 4),
        mpie, // mie ← mpie (restore interrupt enable)
        io.csr_bundle.mstatus(2, 0)
      )
    io.csr_bundle.mepc_write_data     := io.csr_bundle.mepc
    io.csr_bundle.mcause_write_data   := io.csr_bundle.mcause
    io.csr_bundle.direct_write_enable := true.B
  }.otherwise {
    io.interrupt_assert               := false.B
    io.interrupt_handler_address      := io.csr_bundle.mtvec
    io.csr_bundle.mstatus_write_data  := io.csr_bundle.mstatus
    io.csr_bundle.mepc_write_data     := io.csr_bundle.mepc
    io.csr_bundle.mcause_write_data   := io.csr_bundle.mcause
    io.csr_bundle.direct_write_enable := false.B
  }

}
```
:::

---


### Memory-Mapped Peripherals

Processor uses high-order address bits to select between devices:

- `deviceSelect = 0`: Main memory
- `deviceSelect = 1`: Timer peripheral
- `deviceSelect = 2`: UART peripheral
- `deviceSelect = 3`: VGA peripheral

1. Timer peripheral : A memory-mapped timer peripheral provides periodic interrupt generation capabilities.
2. UART peripheral : Implements 8-N-1 serial transmission (8 data bits, no parity, 1 stop bit) with ready/valid handshaking protocol.
3. VGA peripheral : A memory-mapped VGA display peripheral for visual output with 640×480@72Hz timing and indexed color support.

---

### Functional and RISCOF Compliance Test


All functional tests for the `CPU`, `CSR module`, `CLINT interrupt handling`, `UART subsystem`, `memory operations`, and `execution pipeline` successfully passed. 

A total of 9 test suites were executed, including byte-access tests, CSR write-back validation, machine-mode interrupt flow, UART TX/RX behavior, timer register tests, recursive Fibonacci execution, and a full quicksort program.

![image](https://hackmd.io/_uploads/ry3uLdRbZx.png)


These results confirm that the CPU core correctly implements memory access, ALU execution, CSR semantics, trap handling (interrupt + exception), timer operations, and I/O communication. The successful completion of higher-level programs (Fibonacci, Quicksort) further demonstrates the correctness and stability of the pipeline and control logic.

---

The RISCOF compliance framework was executed using the rv32emu reference model and validated against the `RV32I + Zicsr` ISA specification. 

The device under `mycpu` successfully passed all compliance checks, confirming that the CPU correctly implements the RISC-V base integer instructions, CSR operations, and required machine-mode architectural behavior.

![image](https://hackmd.io/_uploads/ByBBwORb-x.png)

---

### Nyancat
```
make demo
```

![image](https://hackmd.io/_uploads/SykWCb4fWx.png)

---

## Pipelined RISC-V CPU

Since I have already completed a single-cycle RV32I processor, the next step of the project is to extend it into several `pipelined versions`. Building on the same instruction memory, register file, and peripheral framework from previous labs, these pipeline designs progressively introduce more advanced techniques to remove performance bottlenecks while preserving correct architectural behavior.

---

| Implementation | Stages | Highlight |
| --- | --- | --- |
| `ThreeStage` | IF → ID → EX/MEM/WB (folded) | Minimal pipeline that introduces control-flow redirection and CLINT interaction with a single execute stage. |
| `FiveStageStall` | IF → ID → EX → MEM → WB | Classic five-stage design that resolves `data hazards` with interlocks (`bubbles`) and performs `branch resolution in EX`. |
| `FiveStageForward` | IF → ID → EX → MEM → WB | Adds `bypass` paths from MEM/WB back to EX to reduce stalls caused by `RAW hazards`. |
| `FiveStageFinal` | IF → ID → EX → MEM → WB | Combines `forwarding`, refined `flush` logic, and the optimized CLINT/CSR handshake that matches the interrupt-capable 

---

### Hazard

#### 1. Structural Hazards

In myCPU project, we used `Harvard-style memory system`. 

- Memory have independent ports for instruction fetch and data. 
- The register file supports two reads and one write per cycle. 

--> `Structural hazards` are intentionally avoided.

#### 2. Data Hazards

Read-After-Write (RAW):
- `Three-stage` and `five-stage` stall cores insert `bubbles` when an instruction consumes a value still in flight. 
- `Forwarding` and `final` cores extend forwarding technique to feed results from `MEM` or `WB` back into `EX`.

#### 3. Control Hazards

`Branches` and `jumps` must redirect the instruction fetch stage.


#### 4. Hazard Unit

- `Control.scala` (stall and flush decisions).
- `Forwarding.scala` : Resolves data hazards with dual-stage forwarding.
- `ID2EX.scala` and `EX2MEM.scala` (registering control signals so that the hazard unit can observe the pipeline state).

---

### IF2ID

IF/ID Pipeline Register's responsibilities : 

- Hold the instruction between `IF` → `ID`
- Hold the instruction address (`PC`) for control-flow resolution
- Carry interrupt/trap-related flags into later stages
- Support pipeline stalls by freezing contents
- Support pipeline flushes by inserting safe NOP instructions

---

And this module has two control signals : 
 - `stall`: Freeze register contents (hold current instruction when ID/EX busy)
 - `flush`: Clear register contents to NOP (discard wrong-path instructions)

---

For instruction register:
- `Normal` : Pass instruction from IF
- `Stall` : Keep previous instruction
- `Flush` : Output `NOP` (InstructionsNop.nop = 0x00000013)


:::spoiler IF2ID
```scala
class IF2ID extends Module {
  val io = IO(new Bundle {
    val stall               = Input(Bool())
    val flush               = Input(Bool())
    val instruction         = Input(UInt(Parameters.InstructionWidth))
    val instruction_address = Input(UInt(Parameters.AddrWidth))
    val interrupt_flag      = Input(UInt(Parameters.InterruptFlagWidth))

    val output_instruction         = Output(UInt(Parameters.DataWidth))
    val output_instruction_address = Output(UInt(Parameters.AddrWidth))
    val output_interrupt_flag      = Output(UInt(Parameters.InterruptFlagWidth))
  })

  val instruction = Module(new PipelineRegister(defaultValue = 0x00000013.U))
  instruction.io.in     := io.instruction
  instruction.io.stall  := io.stall
  instruction.io.flush  := io.flush
  io.output_instruction := instruction.io.out


  val instruction_address = Module(new PipelineRegister(defaultValue = ProgramCounter.EntryAddress))
  instruction_address.io.in     := io.instruction_address
  instruction_address.io.stall  := io.stall
  instruction_address.io.flush  := io.flush
  io.output_instruction_address := instruction_address.io.out


  val interrupt_flag = Module(new PipelineRegister(Parameters.InterruptFlagBits))
  interrupt_flag.io.in     := io.interrupt_flag
  interrupt_flag.io.stall  := io.stall
  interrupt_flag.io.flush  := io.flush
  io.output_interrupt_flag := interrupt_flag.io.out
}
```
:::

### Forwarding Unit

The forwarding unit defines the source of forwarded data:
- `NoForward`: Use register file value (no forwarding needed)
- `ForwardFromMEM`: Forward from EX/MEM pipeline register (1 cycle old)
- `ForwardFromWB`: Forward from MEM/WB pipeline register (2 cycles old)

```scala
object ForwardingType {
  val NoForward      = 0.U(2.W)
  val ForwardFromMEM = 1.U(2.W)
  val ForwardFromWB  = 2.U(2.W)
}
```

---

The enhancements over basic forwarding : 

- `ID stage forwarding`: Enables early branch comparison in decode stage
- `Dual-stage support`: Simultaneous forwarding to both ID and EX stages
- `Reduced branch penalty`: Branch decisions made 1 cycle earlier

---

The forwarding unit determines where the forwarded data will be delivered:

- `ID` Stage (for branch comparison)
    - `EX/MEM` → `ID` : Forward for immediate branch operand resolution
    - `MEM/WB` → `ID` : Forward for 2-cycle old branch operands
- `EX` Stage (for ALU operations):
    - `EX/MEM` → `EX` : Forward ALU result from previous instruction
    - `MEM/WB` → `EX` : Forward memory or writeback value

---

Due to early branch resolution with `ID forwarding`, the `branch penalty` is reduced from 2 cycles to 1 cycle. 

For example : 
```assembly
ADD    x1, x2, x3
BEQ    x1, x4, label
NOP
```

`ADD x1, x2, x3` produces its result in the `EX` stage, but the value will not be written back to the register file until the `WB` stage.

The next instruction:

`BEQ x1, x4, label` needs the value of `x1` during the `ID` stage, because branches compare their operands early to decide whether the pipeline must redirect control flow.

This creates a `RAW data hazard`.


If the processor cannot forward operands into the ID stage, BEQ must wait until ADD completes WB.

>[!Note]Timing illustration of Without ID forwarding
>```
> ADD: IF  ID  EX  MEM  WB
> BEQ:     IF  xx  xx   ID  EX  MEM  WB   ← 2 stalls
>```

This requires `2 stall cycles` before BEQ can safely read x1.

---

>[!Note]Timing illustration of With ID forwarding
>```
> ADD: IF  ID  EX  MEM  WB
> BEQ:     IF  xx  ID   EX  MEM  WB      ← only 1 stall
>```

Forwarding from EX/MEM supplies the correct value before WB.

However, because the two events occur in the same cycle, the pipeline typically inserts `one bubble` to align the timing so that BEQ’s ID stage sees a stable forwarded value.

---

#### Data Forwarding to EX Stage

- Forwarding Conditions : 

    For a source register `rs1` (same applies to `rs2`):

    Forwarding is required if:

    1. The later stage will write a register
    (`reg_write_enable_mem` or `reg_write_enable_wb`)
    2. The destination register matches the source register
    (`rd_mem` === `rs1_ex`, or `rd_wb` === `rs1_ex`)
    3. The destination is not `x0`
    (`rd ≠ 0`), because `x0` always reads as zero

- Forwarding Priority :

    - Priority 1 — Forward from EX/MEM
        - Most recent result (1-cycle hazard)
        - Used when the MEM-stage instruction writes the needed register
    - Priority 2 — Forward from MEM/WB
        - Older result (2-cycle hazard)
        - Used only when EX/MEM does not match
    - Otherwise — No Forward
        - Safe to use register file output directly


:::spoiler EX-Stage Forwarding Logic
```scala

// rs1 Forwarding Logic
when(io.reg_write_enable_mem &&
     (io.rd_mem === io.rs1_ex) &&
     (io.rd_mem =/= 0.U)) {

  io.reg1_forward_ex := ForwardingType.ForwardFromMEM

}.elsewhen(io.reg_write_enable_wb &&
           (io.rd_wb === io.rs1_ex) &&
           (io.rd_wb =/= 0.U)) {

  io.reg1_forward_ex := ForwardingType.ForwardFromWB

}.otherwise {

  io.reg1_forward_ex := ForwardingType.NoForward
}


// rs2 Forwarding Logic
when(io.reg_write_enable_mem &&
     (io.rd_mem === io.rs2_ex) &&
     (io.rd_mem =/= 0.U)) {

  io.reg2_forward_ex := ForwardingType.ForwardFromMEM

}.elsewhen(io.reg_write_enable_wb &&
           (io.rd_wb === io.rs2_ex) &&
           (io.rd_wb =/= 0.U)) {

  io.reg2_forward_ex := ForwardingType.ForwardFromWB

}.otherwise {

  io.reg2_forward_ex := ForwardingType.NoForward
}
```
:::


---

#### Data Forwarding to ID Stage

- Forwarding Conditions : 

    For each branch operand (`rs1_id` or `rs2_id`), forwarding is needed if:

    1. A later pipeline stage will write a register
        - `io.reg_write_enable_mem` or `io.reg_write_enable_wb`
    2. The destination register matches the source register used in ID
        - `io.rd_mem === io.rs1_id` / `io.rs2_id`
        - `io.rd_wb === io.rs1_id` / `io.rs2_id`
    3. The destination is not `x0`
        - `rd ≠ 0`, since `x0` is always 0 and does not need forwarding
        
- Forwarding Priority :
    - The priority is the same as EX-stage forwarding.


:::spoiler ID-Stage Forwarding Logic
```scala
// rs1 ID-Stage Forwarding Logic
when(io.reg_write_enable_mem &&
     (io.rd_mem === io.rs1_id) &&
     (io.rd_mem =/= 0.U)) {

  // Forward from EX/MEM → ID for branch rs1
  io.reg1_forward_id := ForwardingType.ForwardFromMEM

}.elsewhen(io.reg_write_enable_wb &&
           (io.rd_wb === io.rs1_id) &&
           (io.rd_wb =/= 0.U)) {

  // Forward from MEM/WB → ID if no newer MEM-stage match
  io.reg1_forward_id := ForwardingType.ForwardFromWB

}.otherwise {

  // No hazard: use register file output for rs1
  io.reg1_forward_id := ForwardingType.NoForward
}

// rs2 ID-Stage Forwarding Logic
when(io.reg_write_enable_mem &&
     (io.rd_mem === io.rs2_id) &&
     (io.rd_mem =/= 0.U)) {

  // Forward from EX/MEM → ID for second branch operand
  io.reg2_forward_id := ForwardingType.ForwardFromMEM

}.elsewhen(io.reg_write_enable_wb &&
           (io.rd_wb === io.rs2_id) &&
           (io.rd_wb =/= 0.U)) {

  // Forward from MEM/WB → ID
  io.reg2_forward_id := ForwardingType.ForwardFromWB

}.otherwise {

  // No hazard: use register file output for rs2
  io.reg2_forward_id := ForwardingType.NoForward
}
```
:::

---

### Control Unit

The design provides the most advanced hazard detection, enabling early branch resolution in the `ID` stage and incorporating full forwarding support.

- Key Enhancement : 
     - `Early branch resolution`: Branches resolved in `ID` stage (not EX)
     - `ID-stage forwarding`: Enables immediate branch operand comparison
     - `Complex hazard detection`: Handles jump dependencies and multi-stage loads

---

The control unit implements centralized pipeline hazard detection for pipelined CPU. It handles both data and control hazards by coordinating `stalls` and `flushes` across the PC, IF, and ID stages. 

For data hazards, it detects `load-use` and `jump` dependencies on registers whose values are not yet available, inserting a bubble by stalling PC/IF and flushing the ID/EX register. 

For control hazards, it supports `early branch` and `jump resolution` in the ID stage: when a branch or jump is taken (`jump_flag`), only the IF stage is flushed, reducing the branch penalty to a single cycle.

---

To handle these situations, the Control unit drives four main outputs:
- `pc_stall` — freezes the PC to prevent fetching the next instruction
- `if_stall` — holds the IF/ID pipeline register
- `id_flush` — clears ID/EX, inserting a bubble
- `if_flush` — clears IF/ID to discard wrong-path instructions

---

The `when` block detects all data hazards that prevent the ID-stage instruction from safely executing.
If any such hazard is detected, the pipeline must `stall` and insert a `bubble` to ensure correctness.

If no data hazard exists but a branch/jump is taken (`jump_flag = true`), the pipeline performs an `IF flush` to discard the wrong-path instruction.


1. Condition 1 — EX-stage Dependency (1-cycle hazard)

    Triggered when:

    - The `ID-stage` instruction is a `jump` OR
    - The `EX-stage` instruction is a `load`

    AND:
    - The `EX-stag`e instruction writes a register (`rd_ex ≠ x0`)
    - That register is used as `rs1` or `rs2` of the ID-stage instruction

    Meaning:
    The instruction in ID needs a value that the EX-stage instruction has not finished producing.
    
    Even with forwarding, EX → ID forwarding is not possible for jumps and some load-use hazards.

2. Condition 2 — MEM-stage Load with Jump Dependency (2-cycle hazard)

    Triggered when:

    - `ID-stage` instruction is a `jump`, AND
    - `MEM-stage` instruction is a `load`, AND
    - The load destination (`rd_mem`) matches `rs1_id` or `rs2_id`, AND
    - The destination is not `x0`

    Meaning:
    Even with forwarding, the load value in MEM stage takes one extra cycle before it can be forwarded back to ID for jump target calculation.

    If above conditions either is detected:
    ```scala
        io.id_flush := true.B
        io.pc_stall := true.B
        io.if_stall := true.B
    ```
    The pipeline performs a stall + bubble insertion.
 
 3. Control Hazard Handling (`jump_flag`)

    If the `ID` stage determines that a branch or `jump` is `taken`, the pipeline must discard the next sequential instruction.

    ```scala
        io.id_flush := true.B
    ```
    Only IF is flushed because:

    The correct `jump target PC` is already known in `ID`.

    ID-stage early resolution avoids the need for flushing ID/EX

---

The hazard detection logic distinguishes between EX-stage and MEM-stage dependencies that affect ID-stage jumps or load-use cases. 

If a dependency makes the operand unavailable in time, the pipeline stalls and inserts a bubble. Otherwise, if a branch or jump is taken, only the IF stage is flushed thanks to early resolution in ID, achieving a one-cycle branch penalty. 

This combination ensures correctness with minimal performance overhead.

:::spoiler Control.scala
```scala
class Control extends Module {
  val io = IO(new Bundle {
    val jump_flag              = Input(Bool())                                     // id.io.if_jump_flag
    val jump_instruction_id    = Input(Bool())                                     // id.io.ctrl_jump_instruction           //
    val rs1_id                 = Input(UInt(Parameters.PhysicalRegisterAddrWidth)) // id.io.regs_reg1_read_address
    val rs2_id                 = Input(UInt(Parameters.PhysicalRegisterAddrWidth)) // id.io.regs_reg2_read_address

    val memory_read_enable_ex  = Input(Bool())                                     // id2ex.io.output_memory_read_enable
    val rd_ex                  = Input(UInt(Parameters.PhysicalRegisterAddrWidth)) // id2ex.io.output_regs_write_address

    val memory_read_enable_mem = Input(Bool())                                     // ex2mem.io.output_memory_read_enable   //
    val rd_mem                 = Input(UInt(Parameters.PhysicalRegisterAddrWidth)) // ex2mem.io.output_regs_write_address   //

    val if_flush = Output(Bool())
    val id_flush = Output(Bool())
    val pc_stall = Output(Bool())
    val if_stall = Output(Bool())
  })

  io.if_flush := false.B
  io.id_flush := false.B
  io.pc_stall := false.B
  io.if_stall := false.B

  when(

    ((io.jump_instruction_id || io.memory_read_enable_ex) && // Either:
      (io.rd_ex =/= 0.U) &&                                 // Destination is not x0
      (io.rd_ex === io.rs1_id || io.rd_ex === io.rs2_id)) // Destination matches ID source

      || 
        (io.jump_instruction_id &&                              // Jump instruction in ID
          io.memory_read_enable_mem &&                          // Load instruction in MEM
          (io.rd_mem =/= 0.U) &&                                  // Load destination not x0
          (io.rd_mem === io.rs1_id || io.rd_mem === io.rs2_id)) // Load dest matches jump source

  ) {

    io.id_flush := true.B
    io.pc_stall := true.B
    io.if_stall := true.B

  }.elsewhen(io.jump_flag) {

    io.if_flush := true.B
  }

}
```
:::

---

### Hazard Detection Summary and Analysis

#### Q1 : Why do we need to stall for load-use hazards?

>[!Note] Ans
>Because the value loaded from memory is not available until the `MEM/WB` stage, and forwarding cannot provide it early enough for the next instruction in ID. Without stalling, the instruction in ID would read an incorrect register value.

---

#### Q2 : What is the difference between "stall" and "flush" operations?

>[!Note] Ans
>A `stall` freezes the `PC` and `IF/ID` registers, keeping the same instruction for another cycle. A `flush` overwrites a pipeline register with a `NOP` to discard an incorrect or unwanted instruction. 
>
>`Stall` delays execution; `flush` removes wrong-path instructions.
___

#### Q3 : Why does jump instruction with register dependency need stall?

>[!Note] Ans
>The `jump` target address depends on a register value that is not yet ready when the jump reaches the `ID` stage. The CPU must wait for the producer instruction (EX or MEM stage) to compute the correct value before calculating the jump target.

___

#### Q4 : In this design, why is branch penalty only 1 cycle instead of 2?

>[!Note] Ans
>`Branches` and `jumps` are resolved in the `ID` stage instead of the `EX` stage. 
>This early resolution means only the instruction in `IF` needs to be flushed, reducing the penalty from two discarded instructions to just one.

---

#### Q5 : What would happen if we removed the hazard detection logic entirely?
>[!Note] Ans
>The pipeline would execute instructions using incorrect data, leading to wrong branch decisions, incorrect jump targets, and corrupted ALU results.
>
>Control flow would break, and the CPU would no longer execute programs correctly.
___

#### Q6 : Complete the stall condition summary
>[!Note] Ans
>Stall is needed when:
>1. EX-stage dependency :
>A `jump` or `ID-stage` instruction depends on a value produced by `EX`, or the EX-stage instruction is a load whose destination matches `rs1_id` or `rs2_id`.
>2. MEM-stage dependency:
>A `jump` instruction in `ID` depends on a `load result` still in the `MEM` stage, and the loaded register matches `rs1_id` or `rs2_id`.
>
>Flush is needed when:
>1. Branch or jump is taken (jump_flag):
>The `IF-stage` instruction is on the wrong path and must be discarded.

---

### Functional Test

```ㄘ
[info] PipelineProgramTest:
[info] Three-stage Pipelined CPU
[info] - should calculate recursively fibonacci(10)
[info] - should quicksort 10 numbers
[info] - should store and load single byte
[info] - should solve data and control hazards
[info] - should handle all hazard types comprehensively
[info] - should handle machine-mode traps
[info] Five-stage Pipelined CPU with Stalling
[info] - should calculate recursively fibonacci(10)
[info] - should quicksort 10 numbers
[info] - should store and load single byte
[info] - should solve data and control hazards
[info] - should handle all hazard types comprehensively
[info] - should handle machine-mode traps
[info] Five-stage Pipelined CPU with Forwarding
[info] - should calculate recursively fibonacci(10)
[info] - should quicksort 10 numbers
[info] - should store and load single byte
[info] - should solve data and control hazards
[info] - should handle all hazard types comprehensively
[info] - should handle machine-mode traps
[info] Five-stage Pipelined CPU with Reduced Branch Delay
[info] - should calculate recursively fibonacci(10)
[info] - should quicksort 10 numbers
[info] - should store and load single byte
[info] - should solve data and control hazards
[info] - should handle all hazard types comprehensively
[info] - should handle machine-mode traps
[info] PipelineUartTest:
[info] Three-stage Pipelined CPU UART Comprehensive Test
[info] - should pass all TX and RX tests
[info] Five-stage Pipelined CPU with Stalling UART Comprehensive Test
[info] - should pass all TX and RX tests
[info] Five-stage Pipelined CPU with Forwarding UART Comprehensive Test
[info] - should pass all TX and RX tests
[info] Five-stage Pipelined CPU with Reduced Branch Delay UART Comprehensive Test
[info] - should pass all TX and RX tests
[info] PipelineRegisterTest:
[info] Pipeline Register
[info] - should be able to stall and flush
[info] Run completed in 1 minute, 32 seconds.
[info] Total number of tests run: 29
[info] Suites: completed 3, aborted 0
[info] Tests: succeeded 29, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
```
---

All pipeline implementations—including the `three-stage` CPU, the `five-stage stalling` design, the `five-stage design with forwarding`, and the `five-stage final design` successfully passed every functional and hazard-related test. 

The tests includes `recursive Fibonacci` computation, `quicksort` execution, `byte-level memory operations`, comprehensive `data and control hazard resolution`, and correct handling of `machine-mode traps`.

In addition, all `UART transmit/receive tests` passed across all pipeline variants, confirming correct peripheral integration. The pipeline register behavior was also verified to properly support `stall` and `flush ` operations.

---

### RISCOF Compliance Testing

![image](https://hackmd.io/_uploads/ByvJReVMWg.png)

Pepelined version of mycpu successfully passed all compliance checks, confirming that the CPU design correctly implements the `RISC-V` base `integer instructions`, `CSR` operations, and required machine-mode architectural behavior.

# [Adapting Homework 2 RISC-V Assembly](https://hackmd.io/@bbchen/arch2025-homework2)


I successfully modified the handwritten `RISC-V assembly` from `Homework 2` to make it compatible with the pipelined CPU.

After adapting the `UF8 encode/decode` routines to respect the pipeline’s hazard behavior and placing the updated assembly in the `csrc` directory as required, the program now executes correctly on the pipelined processor.

The test confirms that both encoding and decoding operations produce the expected results across all input values, demonstrating full functional correctness under the pipelined execution model.

---

### C version

Before integrating the handwritten RISC-V assembly routines, I first validated the `UF8 encode/decode` logic using a pure C implementation. 

I compiled the C-only version into `main.asmbin` and executed it on the pipelined CPU to ensure that the baseline algorithm behaved correctly independent of any assembly-level hazards.

The `C version` successfully completed all encode–decode checks and wrote the expected result code to the debug memory location, confirming that the UF8 algorithm itself was correct and that the test harness, memory-mapped interface, and pipeline environment were functioning properly.

:::spoiler c version
```c
typedef unsigned int  uint32_t;
typedef unsigned char uint8_t;
typedef uint8_t       uf8;
The C version successfully completed all encode–decode checks and wrote the expected result code to the debug memory location, confirming that the UF8 algorithm itself was correct and that the test harness, memory-mapped interface, and pipeline environment were functioning properly.
/* ============= Decode Encode  ============= */

/* CLZ: count leading zeros for 32-bit unsigned int */
static unsigned clz(uint32_t x)
{
    int n = 32;
    int c = 16;

    do {
        uint32_t y = x >> c;
        if (y) {
            n -= c;
            x = y;
        }
        c >>= 1;
    } while (c);

    return n - x;
}

/* Decode uf8 to uint32_t */
uint32_t uf8_decode(uf8 fl)
{
    uint32_t mantissa = (uint32_t)(fl & 0x0f);
    uint8_t  exponent = (uint8_t)(fl >> 4);
    uint32_t offset   = (0x7FFFu >> (15 - exponent)) << 4;
    return (mantissa << exponent) + offset;
}

// /* Encode uint32_t to uf8 */
uf8 uf8_encode(uint32_t value)
{
    /* Use CLZ for fast exponent calculation */
    if (value < 16u)
        return (uf8)value;

    /* Find appropriate exponent using CLZ hint */
    int     lz  = (int)clz(value);
    int     msb = 31 - lz;
    uint8_t exponent = 0;
    uint32_t overflow = 0;

    if (msb >= 5) {
        /* Estimate exponent - the formula is empirical */
        exponent = (uint8_t)(msb - 4);
        if (exponent > 15u)
            exponent = 15u;

        /* Calculate overflow for estimated exponent */
        for (uint8_t e = 0; e < exponent; e++)
            overflow = (overflow << 1) + 16u;

        /* Adjust if estimate was off */
        while (exponent > 0u && value < overflow) {
            overflow = (overflow - 16u) >> 1;
            exponent--;
        }
    }

    /* Find exact exponent */
    while (exponent < 15u) {
        uint32_t next_overflow = (overflow << 1) + 16u;
        if (value < next_overflow)
            break;
        overflow = next_overflow;
        exponent++;
    }

    uint8_t mantissa = (uint8_t)((value - overflow) >> exponent);
    return (uf8)((exponent << 4) | mantissa);
}

//test
int main(void)
{

    *(int *)(4) = 1;

    for (int i = 5; i < 256; i++) {
        uint32_t d = uf8_decode_asm((uf8)i);

        uf8 e = uf8_encode_asm(d);

        if ( (e != (uf8)i) ) {
            *(int *)(4) = i;
        }
    }
}
```
:::

---

Modify `PipelineProgeamTest.scala` : 
```scala
    it should "perform uf8 encode and decode" in {
      runProgram("main.asmbin", cfg) { c =>
        for (i <- 1 to 100) {
          c.clock.step(1000)
          c.io.mem_debug_read_address.poke((i * 4).U)
        }
        c.io.mem_debug_read_address.poke(4.U)
        c.clock.step()
        c.io.mem_debug_read_data.expect(1.U)
      }
    }
```

---

Successfully completed all encode–decode checks.

For a more detailed explanation of the testing methodology, refer to [Assignment 2](https://hackmd.io/@bbchen/arch2025-homework2).

```info
[info] PipelineProgramTest:
[info] Three-stage Pipelined CPU
[info] - should perform uf8 encode and decode
[info] Five-stage Pipelined CPU with Stalling
[info] - should perform uf8 encode and decode
[info] Five-stage Pipelined CPU with Forwarding
[info] - should perform uf8 encode and decode
[info] Five-stage Pipelined CPU with Reduced Branch Delay
[info] - should perform uf8 encode and decode
[info] Run completed in 55 seconds, 560 milliseconds.
[info] All tests passed.
```

---

### RISC-V assembly

Next, I adapt the handwritten `RISC-V assembly` code so that it operates correctly under the pipelined RISC-V CPU.

:::spoiler RISC-V assembly
```assembly
.section .text
# .align  2

/* --------- uf8_decode --------- */
.globl  uf8_decode_asm
.type   uf8_decode_asm, @function

uf8_decode_asm:

    addi    sp, sp, -4
    sw      ra, 0(sp)

    andi t0, a0, 0x0f #t0 = mantissa
    srli t1, a0, 4    #t1 = exponent
    
    # offset = ((1 << exponent) - 1) << 4
    addi t2, zero, 1
    sll t2, t2, t1
    addi t2, t2, -1
    slli t2, t2, 4 #t2 = offset
    
    sll a0,t0, t1
    add a0,a0, t2

    lw      ra, 0(sp)              # restore return addr
    addi    sp, sp, 4

    ret

/* --------- uf8_encode --------- */
.globl  uf8_encode_asm
.type   uf8_encode_asm, @function

uf8_encode_asm:

    li   t0, 16
    slt  t0, a0 ,t0
    bne  t0, zero, return_value
    
    addi  sp, sp, -36
    sw    s7, 32(sp)
    sw    s6, 28(sp)
    sw    s0, 24(sp)
    sw    s1, 20(sp)
    sw    s2, 16(sp)
    sw    s3, 12(sp)
    sw    s4, 8(sp)
    sw    s5, 4(sp)
    sw    ra, 0(sp)


    
    add  s0, a0, zero #s0 = value
    
    #Find appropriate exponent using CLZ hint
    jal  ra, clz_asm
    add  s1, a0, x0  #s1 = lz
    li   t0, 31
    sub  s2, t0, s1    #s2 = msb
    
    
    add s3, zero, zero #s3 = exponent
    add s4, zero, zero #s4 = overflow


    li  t0, 5
    blt t0, s2, 1f

    addi s3, s2, -4

    li  t0, 15
    ble s3, t0, 2f
    li  s3, 15

# Calculate overflow for estimated exponent
2:
    li  s5, 0
3:
    bge  s5, s3, 4f
    slli s4, s4, 1
    addi s4, s4, 16
    addi s5, s5, 1
    j    3b

# Adjust if estimate was off 
4:
    slt t0, x0, s3
    slt t1, s0, s4
    and t0, t0, t1

    beq t0, x0, 1f

    addi  s4, s4, -16
    srli  s4, s4, 1
    addi  s3, s3, -1

    j     4b 


#Find exact exponent
1:
    addi t0, x0, 15

    bge  s3, t0, encode_done
    slli s6, s4, 1
    addi s6, s6, 16

    blt  s0, s6, encode_done
    mv   s4, s6
    addi s3, s3, 1
    j    1b

encode_done:
    sub  s7, s0, s4
    srl  s7, s7, s3

    slli  a0, s3, 4
    or    a0, a0, s7
    
    lw    ra, 0(sp)
    lw    s5, 4(sp)
    lw    s4, 8(sp)
    lw    s3, 12(sp)
    lw    s2, 16(sp)
    lw    s1, 20(sp)
    lw    s0, 24(sp)
    lw    s6, 28(sp)
    lw    s7, 32(sp)
    addi  sp, sp, 36
    
return_value:
    
    ret

/* --------- clz (count leading zeros, 0..32) --------- */
.globl  clz_asm
.type   clz_asm, @function

clz_asm:
    addi t0, zero, 32 #t0 = n
    addi t1, zero, 16 #t1 = c
    add t3, a0, zero #t3 = x
    
clz_Loop:
    srl t2, t3, t1 #t2 = y
    beq t2, zero, clz_y_0
    sub t0, t0, t1
    add t3, t2, zero #t3 = x
    
clz_y_0:
    srli t1,t1,1 # c >> 1
    
    bne  t1, x0, clz_Loop
    sub  a0,t0,t3
    ret

```
:::

---

I also updated the `Makefile` to link the handwritten `UF8 assembly module`. 
I added a dedicated build rule for `main.elf` so that it includes `uf8.o` along with `main.o`:

```makefile
main.elf: main.o init.o uf8.o
	$(LD) -o $@ -T link.lds $(LDFLAGS) $^
```

---


### Functional Test

I updated the test logic in `main.c` to perform a full cross-check between the `C` reference implementation and the `handwritten assembly` routines for UF8 encoding and decoding.

The program now iterates through all 256 possible UF8 values and verifies three conditions:

1. The `C` implementation of `uf8_encode()` must generate the original UF8 byte.
2. The assembly implementation `uf8_encode_asm()` must match the behavior of the C version.
3. The assembly decoder `uf8_decode_asm()` must produce the same 32-bit result as the C decoder.

---

A result code is written to address 0x4 to indicate which check fails:

- `3` → all tests passed
- `2` → C encoder mismatch
- `1` → assembly encoder mismatch
- `0` → assembly decoder mismatch



```c
int main(void)
{

    *(int *)(4) = 3;

    for (int i = 0; i < 256; i++) {
        uint32_t d = uf8_decode((uf8)i);
        uint32_t d_asm = uf8_decode_asm((uf8)i);
        uf8 e = uf8_encode(d);
        uf8 e_asm = uf8_encode_asm(d_asm);
        if ( (e != (uf8)i) ) {
            *(int *)(4) = 2;
            break;
        }else if(e_asm != (uf8)i) {
            *(int *)(4) = 1;
            break;
        }else if(d_asm != d) {
            *(int *)(4) = 0;
            break;
        }
    }
```


The `UF8` program ran correctly on the pipelined CPU, and all encode/decode checks passed, confirming full functional correctness of the modified assembly implementation.

```info
[info] PipelineProgramTest:
[info] Three-stage Pipelined CPU
[info] - should perform uf8 encode and decode
[info] Five-stage Pipelined CPU with Stalling
[info] - should perform uf8 encode and decode
[info] Five-stage Pipelined CPU with Forwarding
[info] - should perform uf8 encode and decode
[info] Five-stage Pipelined CPU with Reduced Branch Delay
[info] - should perform uf8 encode and decode
[info] All tests passed.
```

>[!warning]
>At first, I encountered linking errors because both the C implementation and the handwritten assembly implementation were named uf8 (e.g., `uf8.c` and `uf8.S`).
>
>Since they produced object files with the same name (uf8.o), the linker could not distinguish between the two, causing one file to overwrite the other during compilation.
>
>As a result, the final executable was missing the correct symbols, leading to unresolved references when the program attempted to call the assembly routines.