Try   HackMD

Final Project: Extend Lab3 to comply with RV32IM and CSR

Status: In progress

Contributed by <Yu Jui Huang>

Real-time Update in Notion <Link>

1. RISC-V Information

1.1 RV32I Instruction Format

In the foundational RV32I Instruction Set Architecture (ISA), there exist four fundamental instruction formats denoted as R, I, S, and U. These formats are consistently 32 bits in length and necessitate alignment on a four-byte boundary within memory. If a branch or unconditional jump is executed and the target address is not aligned to a four-byte boundary, an instruction-address-misaligned exception is triggered. Notably, this exception is associated with the branch or jump instruction itself and not the target instruction. It's essential to emphasize that a conditional branch, when not taken, does not result in the generation of an instruction-address-misaligned exception.

The three formats are shown below:

  • R-Type (Register) Format:

    funt7 rs2 rs1 funct3 rd opcode
    funt7 rs2 rs1 funct3 rd 0110011
  • I-Type (Immediate) Format:

    imm[11:0] rs1 funct3 rd opcode
    imm[11:0] rs1 funct3 rd 0110011
  • S**-Type (Register) Format:**

    imm[11:5] rs2 rs1 funct3 rd opcode
    imm[11:5] rs2 rs1 funct3 rd 0110011

1.2 M standard extension

1.2.1 Integer Multiplication Instructions:

  • MUL rd, rs1, rs2: x[rd] = x[rs1] × x[rs2]

    Multiplies the values in registers rs1 and rs2 and stores the result in register rd.

  • MULH rd, rs1, rs2: x[rd] = (x[rs1] × x[rs2]) ≫XLEN (signed)

    Computes the high bits of the signed product of the values in registers rs1 and rs2 and stores the result in register rd.

  • MULHSU rd, rs1, rs2: x[rd] = (xrs1 × xrs2) ≫XLEN(s)

    Computes the high bits of the signed product of rs1 and the absolute value of rs2, storing the result in rd.

  • MULHU rd, rs1, rs2: x[rd] = (x[rs1] × x[rs2]) ≫XLEN (unsigned)

    Computes the high bits of the unsigned product of the values in registers rs1 and rs2 and stores the result in register rd.

1.2.2 Integer Division Instructions:

  • DIV rd, rs1, rs2: x[rd] = x[rs1] ÷(s) x[rs2]

    Divides the signed value in register rs1 by the signed value in register rs2 and stores the quotient in register rd.

  • DIVU rd, rs1, rs2: x[rd] = x[rs1] ÷(u) x[rs2]

    Divides the unsigned value in register rs1 by the unsigned value in register rs2, storing the quotient in register rd.

  • REM rd, rs1, rs2: x[rd] = x[rs1] % (s) x[rs2]

    Computes the remainder of the signed division of rs1 by rs2 and stores the result in rd.

  • REMU rd, rs1, rs2: x[rd] = x[rs1] % (u) x[rs2]
    Computes the remainder of the unsigned division of rs1 by rs2, storing the result in rd.

1.2.3 The Instruction Decoder of M Extension

funct7 rs2 rs1 funct3 rd opcode
(R Type) 31-25 24-20 19-15 14-12 11-7 6-0
MUL 0000001 - - 000 - 0110011
MULH 0000001 - - 001 - 0110011
MULHU 0000001 - - 011 0110011
MULHSU 0000001 - - 010 - 0110011
DIV 0000001 - - 100 - 0110011
DIVU 0000001 - - 101 - 0110011
REM 0000001 - - 110 - 0110011
REMU 0000001 - - 111 - 0110011

1.3 Control and Status Register(CSR) & Core Local Interrupts(CLINT) Concept

1.3.1 CSR

Introduction

Control and Status Registers (CSRs) are essential components in computer architecture that play a crucial role in managing and monitoring the behavior of a processor or a system.

Purpose

The primary purpose of CSRs is to provide a mechanism for the processor to communicate and interact with the rest of the system. They act as a bridge between the processor's internal components and external devices or the operating system. CSRs are used for configuration, control, and monitoring of various aspects of the processor's operation.

In the RISC-V architecture, the machine-level interrupt handling is a critical aspect of system design. This involves the careful design of Control and Status Registers (CSRs) and the Core Local Interruptor (CLINT) to efficiently manage interrupts and exceptions at the machine level. This section explores the key considerations and design principles when creating a CSR and CLINT focused on machine-level operations.

CSR Register

  • Machine Status (mstatus) Register
    Use to record various control and status related to “Machine state”.
    It is a machine-level CSR that holds various status and control information about the machine mode.

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

The Machine-mode status register (mstatus) for RV32.

  • Machine Exception Program Counter(mepc) Register
    Use to ****hold the program counter value at the time of an exception or trap in machine mode.

  • Machine Cause(mcause) Register
    Use to hold a code or identifier that indicates the cause of an exception or interrupt.

  • “mtvec” Register
    Use to specify the base address of the trap vector table for machine-level interrupts.

  • Relative CSR Address extracted from Spec. Vol.II Page 8-10

    Number Privilege Name Description
    0x300 MRW mstatus
    0x305 MRW mtvec Machine trap-handler base address.
    0x341 MRW mepc Machine exception program counter.
    0x342 MRW mcause
    0xC00 MRW CycleL
    0XC80 MRW CycleH only in RV32

CSR Operation

  • CSRRW/CSRRWI:

    Read the value from a Control and Status Register (CSR) and write the contents of a general-purpose register into the CSR.

  • CSRRS/CSRRSI:

    Read the value from a CSR, perform a bitwise OR operation with the value in register rs1, and write the result back into the CSR.

  • CSRRC/CSRRCI:

    Read the value from a CSR, perform a bitwise AND operation with the complement (~) of the value in register rs1, and write the result back into the CSR.

  • MERT (Machine Exception Return):

    This instruction is used to return from an exception, updating the mstatus register:

    • Update MIE (Machine Interrupt Enable) with the value of MPIE.
    • Update the MPIE field's value to 1.

1.3.2 Core Local Interrupt (CLINT)

Purpose

CLINT provides a centralized facility for handling interrupts that are local to each processor core in a multi-core system.

Interrupt Handling:

  • Manages local interrupts, which are specific to a particular core.
  • Coordinates interrupt requests from various sources within the core.

Timer Facilities:

  • Includes programmable timers for each core.
  • Often used for implementing time-related functionalities, such as context switching and scheduling.

Key Components:

  1. Timer Registers:
    • The CLINT contains a timer that generates periodic interrupts.
    • This timer, often referred to as the mtime (machine time) or mtimecmp (machine time compare), can be configured to generate interrupts at specified intervals.
  2. Interrupt Registers:
    • msip (Machine Software Interrupt Pending): Indicates whether a software interrupt is pending for the core.
    • mtime and mtimecmp interrupts: Triggered when the machine time (mtime) exceeds the value in mtimecmp.
  3. Software Interrupts:
    • The CLINT also supports software-generated interrupts. Software running on the RISC-V processor can trigger these interrupts by writing to specific registers in the CLINT.
  4. Memory-Mapped Registers:
    • The CLINT exposes a set of memory-mapped registers that allow software and peripherals to interact with it. The most common registers include:
      • mtime: Holds the current machine time.
      • mtimecmp: Used to set the compare value for the timer interrupt.
  5. Interrupt Handling:
    • When a timer interrupt occurs or a software-generated interrupt is triggered, the CLINT signals the core by raising an interrupt. The processor then follows the standard interrupt handling procedure, which involves saving the current context and transferring control to the interrupt service routine (ISR).

The Core Local Interrupter (CLINT) is a part of the memory-mapped I/O (MMIO) space in RISC-V processors. It provides a mechanism for handling timer interrupts and software-generated interrupts at the core level. CLINT is typically used in systems with multiple RISC-V cores, and each core has its own instance of the CLINT.

Operation:

  1. Interrupt Generation:
    • External devices, software, or timers can generate interrupts specific to a core.
    • Timer interrupts are generated when the machine time (mtime) surpasses the value set in mtimecmp.
  2. Interrupt Handling:
    • When an interrupt occurs, the core suspends its current execution and transfers control to the corresponding interrupt service routine (ISR).
    • The ISR is a specific piece of code designed to handle the interrupt and perform necessary actions.
  3. Timer Functionality:
    • The programmable timer facilities assist in managing time-related events.
    • Context switches, task scheduling, and other time-sensitive operations can be synchronized using CLINT's timer capabilities.
  4. Global Interrupt Controller:
    • Interacting with a global interrupt controller that manages interrupts across all cores.

2. Recap Lab3: Single cycle CPU

lab 3 Link: Assignment3: single-cycle RISC-V CPU - HackMD

Github Link: DarrenHuang0411/NCKU_CA_2023 at Assignment-3 (github.com)

3. Single cycle CPU comply with RV32IM and CSR

3.1 Single cycle CPU Diagram

Single_cycle_with_CSR

Refer to https://yatcpu.sysu.tech/tutorial/interrupt-and-exception/

3.2 Revise the version including CSR

3.2.1 Instruction Fetch

Compared to Assignment 3, the Instruction Fetch needs to be enhanced to handle interrupt signals received in CSR. This involves the introduction of two key conditions: interrupt_assert and interrupt_handler_address.

IF

  • interrupt_assert:
    This parameter signifies the assertion of an interrupt signal. When set, it indicates that an interrupt has occurred and needs attention. Proper handling of this signal is crucial for interrupt-driven processes.
  • interrupt_handler_address:
    This parameter denotes the address where the interrupt handler is located. Upon the assertion of an interrupt, the program counter is redirected to this address, initiating the execution of the corresponding interrupt handler routine. It plays a vital role in managing the flow of execution during interrupt scenarios.
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 interrupt_assert             = Input(Bool())
    val interrupt_handler_address    = Input(UInt(Parameters.AddrWidth))

    val instruction_address = Output(UInt(Parameters.AddrWidth))
    val instruction         = Output(UInt(Parameters.InstructionWidth))
  })
  val pc = RegInit(ProgramCounter.EntryAddress)

when(io.instruction_valid) {
    io.instruction := io.instruction_read_data
    // Final project(InstructionFetch) begin
    when(io.interrupt_assert){
      pc := io.interrupt_handler_address
    }
    .elsewhen(io.jump_flag_id){
    	pc := io.jump_address_id
    }
    .otherwise{
    	pc := pc + 4.U	
    }
    // lab3(InstructionFetch) end
  }.otherwise {
    pc             := pc
    io.instruction := 0x00000013.U
  }
  io.instruction_address := pc

3.2.2 Instruction Decoder

ID

  • For M-extension

    ​​​​
    
  • For CSR Control

    io.ex_csr_address:

    • CSR-related instructions assigns the bits [31:20] of the input instruction (io.instruction) to the ex_csr_address signal.

    io.ex_csr_write_enable:

    • Write operation to a CSR should is enabled based on the opcode and funct3 field of the instruction.
    • The condition (opcode === Instructions.csr) checks if the opcode corresponds to the CSR instruction, indicating a CSR operation is being performed.
    • The second part of the condition checks the funct3 field against various CSR-related instruction types (csrrw, csrrs, csrrc, csrrwi, csrrsi, csrrci).
      If the funct3 matches any of these values, the condition evaluates to true.
    • If both conditions are true, io.ex_csr_write_enable is set to true, indicating that the CSR write operation is enabled.
    ​​​​val ex_csr_write_enable    = Output(Bool())
    ​​​​val ex_csr_address         = Output(UInt(Parameters.CSRRegisterAddrWidth))
    
    ​​​​io.ex_csr_address := io.instruction(31, 20)
    ​​​​
    ​​​​io.ex_csr_write_enable := (opcode === Instructions.csr) &&
    ​​​​  (funct3 === InstructionsTypeCSR.csrrw || funct3 === InstructionsTypeCSR.csrrs ||
    ​​​​  funct3 === InstructionsTypeCSR.csrrc || funct3 === InstructionsTypeCSR.csrwri ||
    ​​​​  funct3 === InstructionsTypeCSR.csrrsi || funct3 === InstructionsTypeCSR.csrrci
    ​​​​  )
    

3.2.3 Execute

Inputs:

  • funct3: The instruction that specifies the CSR operation (e.g., csrrw, csrrs, etc.).
  • io.reg1_data: The data from register 1 (specified by rs1 in the instruction).
  • io.exe_read_data_csr: The data read from the CSR specified by the instruction.
  • io.immediate: An immediate value from the instruction (if applicable).

MuxLookup Function:

  • The MuxLookup function is used to perform a conditional selection based on the value of funct3. It acts like a multiplexer (Mux), selecting a value based on the given conditions.

Conditions and Values:

  • For each CSR instruction type (csrrw, csrrs, etc.), there is a corresponding condition in the IndexedSeq. The selected value is determined based on the type of CSR instruction.
    • InstructionsTypeCSR.csrrw:
      Write the data from register 1 (io.reg1_data) to the CSR.
    • InstructionsTypeCSR.csrrs:
      Write the result of a bitwise OR between the data read from the CSR and the data from register 1.
    • InstructionsTypeCSR.csrrc:
      Write the result of a bitwise AND between the complement of the data from register 1 and the data read from the CSR.
    • InstructionsTypeCSR.csrrwi:
      Write the immediate value to the CSR.
    • InstructionsTypeCSR.csrrsi:
      Write the result of a bitwise OR between the data read from the CSR and the immediate value.
    • InstructionsTypeCSR.csrrci:
      Write the result of a bitwise AND between the complement of the immediate value and the data read from the CSR.

Result:

  • The selected value based on the CSR instruction type is assigned to io.csr_write_data_exe. This data will be used for writing to the specified CSR in the subsequent stages of the processor pipeline.
io.csr_write_data_exe := MuxLookup(
    funct3,
    0.U,
    IndexedSeq(
      InstructionsTypeCSR.csrrw  -> io.reg1_data,
      InstructionsTypeCSR.csrrs  -> (io.exe_read_data_csr | io.reg1_data),
      InstructionsTypeCSR.csrrc  -> (io.exe_read_data_csr & (~io.reg1_data)),
      InstructionsTypeCSR.csrrwi -> io.immediate,
      InstructionsTypeCSR.csrrsi -> (io.exe_read_data_csr | io.immediate),
      InstructionsTypeCSR.csrrci -> (io.exe_read_data_csr & (~io.immediate))
    )
  )

3.2.4 Write Back (WB)

  • Usage of csr_read_data in InstructionDecode:
    • The csr_read_data signal is utilized in the InstructionDecode module as one of the potential sources for writing data to the registers (regs_write_data).
    • The MuxLookup construct selects the appropriate data source based on the value of io.regs_write_source.
      If io.regs_write_source corresponds to RegWriteSource.CSR, the value of io.csr_read_data is chosen as the regs_write_data.
package riscv.core

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

class WriteBack extends Module {
  val io = IO(new Bundle() {
...
  //final project
    val csr_read_data       = Input(UInt(Parameters.DataWidth))
  //final end
  })

  io.regs_write_data := MuxLookup(
    io.regs_write_source,
    io.alu_result,
    IndexedSeq(
      RegWriteSource.Memory                 -> io.memory_read_data,
    //final project (object in InstructionDecode)
      RegWriteSource.CSR                    -> io.csr_read_data,
    //final end
      RegWriteSource.NextInstructionAddress -> (io.instruction_address + 4.U)
    )
  )
}

3.2.5 CSR (New)

package riscv.core

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

// CSRRegister get
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 read_address_id   = Input(UInt(Parameters.CSRRegisterAddrWidth)) 
    val write_address_id  = Input(UInt(Parameters.CSRRegisterAddrWidth))
    val write_enable_id   = Input(Bool())
    val write_data_exe    = Input(UInt(Parameters.DataWidth))

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

    val clint_csr_bundle  = Flipped(new CSRDirectAccessBundle)

    val debug_reg_read_address  = Input(UInt(Parameters.CSRRegisterAddrWidth))
    val debug_reg_read_data = Output(UInt(Parameters.DataWidth))

  })

//initialization
  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)

// connect to preset register
  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

  // If the pipeline and the CLINT are going to read and write the CSR at the same time, let the pipeline write first.
  // This is implemented in a single cycle by passing (write_data_exe) to clint and writing the data from the CLINT to the CSR.
  io.exe_csrrd_csr := MuxLookup(io.read_address_id, 0.U)(regLUT)

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

  io.clint_csr_bundle.mstatus   := mstatus
  io.clint_csr_bundle.mtvec     := mtvec
  io.clint_csr_bundle.mcause    := mcause
  io.clint_csr_bundle.mepc      := mepc

  when(io.clint_csr_bundle.direct_write_enable) {
    mstatus := io.clint_csr_bundle.mstatus_write_data
    mepc    := io.clint_csr_bundle.mepc_write_data
    mcause  := io.clint_csr_bundle.mcause_write_data
  }
  .elsewhen(io.write_enable_id) {
    when(io.write_address_id === CSRRegister.MSTATUS) {
      mstatus := io.write_data_exe
    }
    .elsewhen(io.write_address_id === CSRRegister.MEPC) {
      mepc := io.write_data_exe
    }
    .elsewhen(io.write_address_id === CSRRegister.MCAUSE) {
      mcause := io.write_data_exe
    }                                                                     
  }

  when(io.write_enable_id) {
    when(io.write_address_id === CSRRegister.MIE) {
      mie := io.write_data_exe
    }
    .elsewhen(io.write_address_id === CSRRegister.MTVEC){
      mtvec := io.write_data_exe
    }
    .elsewhen(io.write_address_id === CSRRegister.MSCRATCH) {
      mscratch := io.write_data_exe
    }
  }
}

3.2.6 CLINT (New)

How to operation

interrupt_enable:

Extracts the MIE (Machine Interrupt Enable) bit from the mstatus CSR.

Trap Handling:

Checks if there is a pending interrupt (Interrupt_Flag is not None) and if interrupts are enabled.

If true, updates relevant CSR values (mstatus, mepc, mcause) based on the type of interrupt.

Sets direct_write_enable to true, asserts interrupt_assert, and sets interrupt_handler_address to mtvec from the CSR.

MRET Handling:

Checks if the current instruction is an MRET instruction.

If true, updates mstatus, mepc, and mcause using values from the CSR.

Sets direct_write_enable to true, asserts interrupt_assert, and sets interrupt_handler_address to mepc from the CSR.**Default Behavior:**If no interrupt or MRET, preserves the current CSR values.

Sets direct_write_enable to false, interrupt_assert to false, and interrupt_handler_address to 0.

package riscv.core

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

object InterruptStatus{
    val None = 0x0.U(8.W)
    val Timer0 = 0x1.U(8.W)
    val Ret = 0xF.U(8.W)
} 

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

object InterruptState{
    val Idle = 0x0.U
    val SyncAssert = 0x1.U
    val AsyncAssert = 0x2.U
    val MRET = 0x3.U
}

object CSRState {
  val Idle      = 0x0.U
  val Traping   = 0x1.U
  val Mret = 0x2.U
}

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 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 {
        val Interrupt_Flag = Input(UInt(Parameters.InterruptFlagWidth))

        val Instruction    = Input(UInt(Parameters.InstructionWidth))
        val IF_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
    })

    // mstatus(3) ==> MIE 
    val interrupt_enable = io.csr_bundle.mstatus(3)

    // Trap: choose Sync/Async
    val instruction_address = Mux(io.jump_flag, io.jump_address, io.IF_Instruction_Address + 4.U)

    when(io.Interrupt_Flag =/= InterruptStatus.None && interrupt_enable){
        io.csr_bundle.mstatus_write_data    := io.csr_bundle.mstatus(31, 4) ## 0.U(1.W) ## io.csr_bundle.mstatus(2, 0)
        io.csr_bundle.mepc_write_data       := io.IF_Instruction_Address + 4.U
        io.csr_bundle.mcause_write_data     := Mux(io.Interrupt_Flag(0), 0x80000007L.U, 0x8000000BL.U)

        io.csr_bundle.direct_write_enable   := true.B
        io.interrupt_assert                 := true.B
        io.interrupt_handler_address        := io.csr_bundle.mtvec
    }
    .elsewhen(io.Instruction === InstructionsRet.mret){
        io.csr_bundle.mstatus_write_data    := io.csr_bundle.mstatus(31, 4) ## io.csr_bundle.mstatus(7) ## 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
        io.interrupt_assert                 := true.B
        io.interrupt_handler_address        := io.csr_bundle.mepc
    }
    .otherwise{
        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
        io.interrupt_assert                 := false.B
        io.interrupt_handler_address        := 0.U
    }

}
  • The CLINT module monitors interrupt flags, handles traps and MRET instructions, and updates relevant CSRs accordingly.
  • It provides information about the interrupt handler address and whether an interrupt is currently asserted.
  • The direct_write_enable signal indicates whether a direct write to the CSRs is required based on the executed instruction.
  • This design allows for interrupt handling and CSR updates in a RISC-V processor.

3.2.7 Timer (New)

I/O Port Definition:

  • Timer_interrupt_Flag: An output indicating whether a timer interrupt is asserted.
  • debug_limit: An output providing the current value of the timer limit for debugging purposes.
  • debug_enabled: An output indicating whether the timer is currently enabled for debugging.

Register Declarations:

  • Three registers (count, limit, and enabled) are declared to maintain the state of the timer.
    • count: Maintains the current count of the timer.
    • limit: Represents the limit at which the timer resets.
    • enabled: Indicates whether the timer is currently enabled.

Debug Outputs:

  • debug_limit and debug_enabled are set to the values of limit and enabled, respectively. These are provided for debugging purposes.

Timer Logic:

  • The timer logic is enclosed within the when and otherwise blocks.
  • When the timer is enabled (enabled is true), the count is incremented. If the count reaches the limit, the count is reset to 0, and the Timer_interrupt_Flag is asserted.
  • If the timer is not enabled, Timer_interrupt_Flag is set to false.
package peripheral

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

class Timer extends Module {
  val io = IO(new Bundle {
    val bundle = new RAMBundle

    val Timer_interrupt_Flag = Output(Bool())

    val debug_limit = Output(UInt(Parameters.DataWidth))
    val debug_enabled = Output(Bool())
  })

  val count = RegInit(0.U(32.W))
  val limit = RegInit(100000000.U(32.W))
  val enabled = RegInit(true.B)

  io.debug_limit    := limit
  io.debug_enabled  := enabled

  //final 
  //finish the read-write for count,limit,enabled. And produce appropriate Timer_interrupt_Flag
  when(enabled){
    count :=  count + 1.U
    when(count === limit){
      count := 0.U
      io.Timer_interrupt_Flag := true.B
    }
    .otherwise{
      io.Timer_interrupt_Flag := false.B
    }
  }
  .otherwise{
    io.Timer_interrupt_Flag   := false.B
  }

}

3.2.8 CPU


 //final project(csr i/o)
  val csr_regs   = Module(new CSR)
  val clint      = Module(new CLINT)
  //final end
....
	//final project(clint interrupt assert)
  IF.io.interrupt_assert          := clint.io.interrupt_assert
  IF.io.interrupt_handler_address := clint.io.interrupt_handler_address
  //final end
....
	//final project csr()
  csr_regs.io.read_address_id   := id.io.ex_csr_address
  csr_regs.io.write_address_id  := id.io.ex_csr_address
  csr_regs.io.write_enable_id   := id.io.ex_csr_write_enable
  csr_regs.io.write_data_exe    := ex.io.csr_write_data_exe
  //csr_regs.io.exe_csrrd_csr     :=
  csr_regs.io.clint_csr_bundle  <> clint.io.csr_bundle

  //final end
  //final project clint
  clint.io.Interrupt_Flag         := io.Interrupt_Flag
  clint.io.IF_Instruction_Address := IF.io.instruction_address

  clint.io.Instruction            := IF.io.instruction
  clint.io.jump_address           := ex.io.if_jump_address
  clint.io.jump_flag              := ex.io.if_jump_flag

  //final end

3.2.9 CPUBundle

The CPUBundle plays a crucial role in establishing a communication interface between the central processing unit (CPU) and peripheral devices, with a primary focus on memory interactions. This interface serves as the conduit for the exchange of data, allowing the CPU to interact seamlessly with various external components.

  • Communication Interface:

    • The CPUBundle serves as the bridge that facilitates the communication flow between the CPU and peripheral devices, especially memory modules. It defines the signals and connections required for data transfer, enabling efficient interaction between the CPU and external components.
  • Data Exchange:

    • Within the CPUBundle, various signals are defined to support bidirectional data exchange. These signals may include inputs and outputs for addresses, data, control signals, and other relevant information. The well-defined structure of the bundle ensures a standardized and organized approach to communication.
  • Peripheral Devices:

    • The term "peripheral devices" encompasses a broad range of external components, such as memory units, storage devices, and other interfaces. The CPUBundle accommodates the specific needs of these peripheral devices, allowing the CPU to read from and write to external memory and interact with other connected devices.
    ​​​​package riscv
    ​​​​
    ​​​​import chisel3._
    ​​​​import peripheral.RAMBundle
    ​​​​
    ​​​​// CPUBundle serves as the communication interface for data exchange between
    ​​​​// the CPU and peripheral devices, such as memory.
    ​​​​class CPUBundle extends Bundle {
    ​​​​  val instruction_address = Output(UInt(Parameters.AddrWidth))
    ​​​​  val instruction = Input(UInt(Parameters.DataWidth))
    ​​​​  val instruction_valid = Input(Bool())
    ​​​​  val Interrupt_Flag = Input(UInt(Parameters.InterruptFlagWidth))
    ​​​​  val memory_bundle = Flipped(new RAMBundle)
    ​​​​  val deviceSelect = Output(UInt(Parameters.SlaveDeviceCountBits.W))
    ​​​​  val regs_debug_read_address = Input(UInt(Parameters.PhysicalRegisterAddrWidth))
    ​​​​  val regs_debug_read_data = Output(UInt(Parameters.DataWidth))
    ​​​​  val csr_regs_debug_read_address = Input(UInt(Parameters.CSRRegisterAddrWidth))
    ​​​​  val csr_regs_debug_read_data = Output(UInt(Parameters.DataWidth))
    ​​​​}
    

4. Validation

Environment: Linux Ubuntu 22.04

Tool: GTKwave

The first to test =⇒ Single cycle CPU with CSR

we new folder "Final_Project" including the testing data “"

4.1 CPU Validation

4.1.1 Testing Step

The

$ cd Final_Project/
$ sbt test

4.1.2 Testing Result

  • Pass

    ​​​​FibonacciTest:
    ​​​​[info] Single Cycle CPU with CSR and CLINT
    ​​​​[info] - should calculate recursively fibonacci(10)
    ​​​​[info] QuicksortTest:
    ​​​​[info] Single Cycle CPU with CSR and CLINT
    ​​​​[info] - should quicksort 10 numbers
    ​​​​[info] InstructionDecoderTest:
    ​​​​[info] InstructionDecoder of Single Cycle CPU
    ​​​​[info] - should produce correct control signal
    ​​​​...
    ​​​​[info] InstructionFetchTest:
    ​​​​[info] InstructionFetch of Single Cycle CPU
    ​​​​[info] - should fetch instruction
    ​​​​[info] ByteAccessTest:
    ​​​​[info] Single Cycle CPU with CSR and CLINT
    ​​​​[info] - should store and load single byte
    ​​​​[info] ExecuteTest:
    ​​​​[info] Execution of Single Cycle CPU
    ​​​​[info] - should execute correctly
    ​​​​[info] RegisterFileTest:
    ​​​​[info] Register File of Single Cycle CPU
    ​​​​[info] - should read the written content
    ​​​​[info] - should x0 always be zero
    ​​​​[info] - should read the writing content
    ​​​​[info] Run completed in 46 seconds, 932 milliseconds.
    ​​​​[info] Total number of tests run: 10
    ​​​​[info] Suites: completed 8, aborted 0
    ​​​​[info] Tests: succeeded 9, failed 1, canceled 0, ignored 0, pending 0
    ​​​​[info] *** 1 TEST FAILED ***
    ​​​​[error] Failed tests:
    ​​​​[error] 	riscv.singlecycle.SimpleTrapTest
    ​​​​[error] (Test / test) sbt.TestsFailedException: Tests unsuccessful
    ​​​​[error] Total time: 60 s, completed Jan 13, 2024, 6:17:58 PM
    
  • Wrong (Need to find)

[info] SimpleTrapTest:
[info] Single Cycle CPU with CSR and CLINT
[info] - should jump to trap handler and then return *** FAILED ***
[info]   java.lang.NullPointerException:
[info]   at ... ()
[info]   at peripheral.InstructionROM.readAsmBinary(InstructionROM.scala:42)
[info]   at peripheral.InstructionROM.<init>(InstructionROM.scala:25)
[info]   at riscv.singlecycle.TestTopModule.$anonfun$instruction_rom$2(CPUTest.scala:30)
[info]   at chisel3.Module$.do_apply(Module.scala:53)
[info]   at riscv.singlecycle.TestTopModule.$anonfun$instruction_rom$1(CPUTest.scala:30)
[info]   at chisel3.internal.plugin.package$.autoNameRecursively(package.scala:33)
[info]   at riscv.singlecycle.TestTopModule.<init>(CPUTest.scala:30)
[info]   at riscv.singlecycle.SimpleTrapTest.$anonfun$new$42(CPUTest.scala:125)
[info]   at ... ()
[info]   ...

4.2 GTKWave

  • FibonacciTest (10):

    The expect result is 55

    Untitled 1

5. Reference

  1. RISC-V 指令集架構介紹 - M Standard Extension
  2. 中斷和異常 - YatCPU 實驗文件 (sysu.tech)
  3. Specifications – RISC-V International (riscv.org)
  4. riscv-privileged-20211203.pdf - Google Drive
  5. 從零開始的RISC-V SoC架構設計與Linux核心運行 - 硬體篇 - HackMD
  6. RISC V::中斷與異常處理 異常篇 - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天 (ithome.com.tw)
  7. RV32M, RV64M Instructions — riscv-isa-pages documentation (msyksphinz-self.github.io)