Try   HackMD

Single-cycle RISC-V core

李尚宸

GitHub

  • Pass ca2023-lab3 funtional test
  • Make it with RV32IM instruction set
  • Build riscv-arch-test environment
  • Rewrite at least 3 RISC-V programs in quizs to run on your improved processor.

Experiment environment

  • Ubuntu Linux 22.04

Prerequisites

Install the dependent packages

$ sudo apt install build-essential verilator gtkwave

Install Temurin JDK 11 and sbt

$ curl -s "https://get.sdkman.io" | bash
$ source "$HOME/.sdkman/bin/sdkman-init.sh"
$ sdk install java 11.0.21-tem 
$ sdk install sbt

You don't have to copy and paste Chisel code. Instead, discuss and reflect what you have done. Seek for compliance and improvements.

sbt test result

...
[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 18 seconds, 613 milliseconds.
[info] Total number of tests run: 9
[info] Suites: completed 7, aborted 0
[info] Tests: succeeded 9, failed 0, canceled 0, ignored 0, pending 0   
[info] All tests passed.
[success] Total time: 26 s, completed Jan 16, 2025, 9:41:13 AM

InstructionFetchTest

class InstructionFetchTest extends AnyFlatSpec with ChiselScalatestTester {
  behavior.of("InstructionFetch of Single Cycle CPU")
  it should "fetch instruction" in {
    test(new InstructionFetch).withAnnotations(TestAnnotations.annos) { c =>
      val entry = 0x1000
      var pre   = entry
      var cur   = pre
      c.io.instruction_valid.poke(true.B)
      var x = 0
      for (x <- 0 to 100) {
        Random.nextInt(2) match {
          case 0 => // no jump
            cur = pre + 4
            c.io.jump_flag_id.poke(false.B)
            c.clock.step()
            c.io.instruction_address.expect(cur)
            pre = pre + 4
          case 1 => // jump
            c.io.jump_flag_id.poke(true.B)
            c.io.jump_address_id.poke(entry)
            c.clock.step()
            c.io.instruction_address.expect(entry)
            pre = entry
        }
      }
    }
  }
}

In the InstructionFetchTest, the goal is to verify the module's ability to correctly set the PC(0x1000) by using a multiplexer to choose between PC + 4.U and jump_address_id, with the selection determined randomly using Random.nextInt(2).

  • Original entry address: 0x1010

    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 →

  • When io_jump_flag_id set to 0, entry address = PC + 4.U = 0x1014

    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 →

  • When io_jump_flag_id set to 1, entry address = 0x1000

    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 →

InstructionDecoderTest

class InstructionDecoderTest extends AnyFlatSpec with ChiselScalatestTester {
  behavior.of("InstructionDecoder of Single Cycle CPU")
  it should "produce correct control signal" in {
    test(new InstructionDecode).withAnnotations(TestAnnotations.annos) { c =>
      c.io.instruction.poke(0x00a02223L.U) // S-type
      c.io.ex_aluop1_source.expect(ALUOp1Source.Register)
      c.io.ex_aluop2_source.expect(ALUOp2Source.Immediate)
      c.io.regs_reg1_read_address.expect(0.U)
      c.io.regs_reg2_read_address.expect(10.U)
      c.clock.step()

      c.io.instruction.poke(0x000022b7L.U) // lui
      c.io.regs_reg1_read_address.expect(0.U)
      c.io.ex_aluop1_source.expect(ALUOp1Source.Register)
      c.io.ex_aluop2_source.expect(ALUOp2Source.Immediate)
      c.clock.step()

      c.io.instruction.poke(0x002081b3L.U) // add
      c.io.ex_aluop1_source.expect(ALUOp1Source.Register)
      c.io.ex_aluop2_source.expect(ALUOp2Source.Register)
      c.clock.step()
    }
  }
}

In the InstructionDecoderTest, the task is to set io_memory_read_enable and io_memory_write_enable depending on the opcode.

For the first instruction sw x10, 4(x0):

  • io_memory_read_enable should be 0
  • io_memory_write_enable should be 1
    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 →

For the second instruction lui x5, 2:

  • io_memory_read_enable should be 0
  • io_memory_write_enable should be 0
    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 →

FOr the third instruction add x3, x1, x2:

  • io_memory_read_enable should be 0
  • io_memory_write_enable should be 0
    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 →

ExecuteTest

class ExecuteTest extends AnyFlatSpec with ChiselScalatestTester {
  behavior.of("Execution of Single Cycle CPU")
  it should "execute correctly" in {
    test(new Execute).withAnnotations(TestAnnotations.annos) { c =>
      c.io.instruction.poke(0x001101b3L.U) // x3 =  x2 + x1

      var x = 0
      for (x <- 0 to 100) {
        val op1    = scala.util.Random.nextInt(429496729)
        val op2    = scala.util.Random.nextInt(429496729)
        val result = op1 + op2
        val addr   = scala.util.Random.nextInt(32)

        c.io.reg1_data.poke(op1.U)
        c.io.reg2_data.poke(op2.U)

        c.clock.step()
        c.io.mem_alu_result.expect(result.U)
        c.io.if_jump_flag.expect(0.U)
      }

      // beq test
      c.io.instruction.poke(0x00208163L.U) // pc + 2 if x1 === x2
      c.io.instruction_address.poke(2.U)
      c.io.immediate.poke(2.U)
      c.io.aluop1_source.poke(1.U)
      c.io.aluop2_source.poke(1.U)
      c.clock.step()

      // equ
      c.io.reg1_data.poke(9.U)
      c.io.reg2_data.poke(9.U)
      c.clock.step()
      c.io.if_jump_flag.expect(1.U)
      c.io.if_jump_address.expect(4.U)

      // not equ
      c.io.reg1_data.poke(9.U)
      c.io.reg2_data.poke(19.U)
      c.clock.step()
      c.io.if_jump_flag.expect(0.U)
      c.io.if_jump_address.expect(4.U)
    }
  }
}

In the ExecuteTest, the task is to connect alu_func and set alu.io.op1 and alu.io.op2 based on their respective sources.

  • alu.io.op1 should be io_op1
  • alu.io.op2 should be io_op2

For the first instruction add x3, x1, x2:

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 →

For the second instruction, beq x1, x2, 2:

  • When x1 != x2, io_if_jump_flag should be 0

    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 →

  • When x1 == x2, io_if_jump_flag should be 1

    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 →

Extend ca2023-lab3 to support the RV32IM instruction set

The M standard extension is for integer multiplication and division instructions. RV32IM is the RV32I instruction set with the M standard extension, which includes the mul, mulh, mulhsu, mulhum, div, divu, rem, and remu instructions.

Code can be found in src/main/scala/riscv/core

ALU.scala

Add the corresponding operations for each M instruction respectively.

ALUControl.scala

The diagrams below show the instruction codes for RV32I and RV32M. Both share the same opcode (0110011 for R-type instructions), but RV32M is identified by funct7 = 0x01, while RV32I uses other funct7 values 0x00.

  • RV32I:
    image
  • RV32M:
    image

We can distinguish between the two using funct7, as shown in the following code:

...
    is(InstructionTypes.RM) {
      when(io.funct7 === "b0000000".U) {
        io.alu_funct := MuxLookup(
          io.funct3,
          ALUFunctions.zero,
          IndexedSeq(
            InstructionsTypeR.add_sub -> Mux(io.funct7(5), ALUFunctions.sub, ALUFunctions.add),
            InstructionsTypeR.sll     -> ALUFunctions.sll,
            InstructionsTypeR.slt     -> ALUFunctions.slt,
            InstructionsTypeR.sltu    -> ALUFunctions.sltu,
            InstructionsTypeR.xor     -> ALUFunctions.xor,
            InstructionsTypeR.or      -> ALUFunctions.or,
            InstructionsTypeR.and     -> ALUFunctions.and,
            InstructionsTypeR.sr      -> Mux(io.funct7(5), ALUFunctions.sra, ALUFunctions.srl)
          )
        )
      }.elsewhen(io.funct7 === "b0000001".U) {
        io.alu_funct := MuxLookup(
          io.funct3,
          ALUFunctions.zero,
          IndexedSeq(
            InstructionsTypeM.mul     -> ALUFunctions.mul,
            InstructionsTypeM.mulh    -> ALUFunctions.mulh,
            InstructionsTypeM.mulhsu  -> ALUFunctions.mulhsu,
            InstructionsTypeM.mulhum  -> ALUFunctions.mulhum,
            InstructionsTypeM.div     -> ALUFunctions.div,
            InstructionsTypeM.divu    -> ALUFunctions.divu,
            InstructionsTypeM.rem     -> ALUFunctions.rem,
            InstructionsTypeM.remu    -> ALUFunctions.remu
          )
        )
      }.otherwise {
        io.alu_funct := ALUFunctions.zero
      }
    }

Build riscv-arch-test environment

Reference: RISCOF Official Documentation

The riscv-arch-test is an official RISC-V Architectural Test Framework used to verify compliance with the RISC-V Instruction Set Architecture (ISA) specifications. It ensures that a processor implementation correctly implements the required features of the RISC-V ISA.

Install Python

$ sudo apt-get install python3.6
$ pip3 install --upgrade pip

Install RISCOF

$ pip3 install git+https://github.com/riscv/riscof.git

Installation of dependencies for riscv-arch-test

$ git clone https://github.com/riscv-non-isa/riscv-arch-test
$ cd riscv-arch-test
$ cd riscv-ctg
$ pip3 install --editable .
$ cd riscv-isac
$ pip3 install --editable .

Install RISCV-GNU Toolchain

The RISC-V GNU Toolchain is a complete set of tools based on the GNU Project, customized for the RISC-V architecture. It provides essential tools for compiling, linking, and debugging applications targeting RISC-V processors.

The official website provides the following installation steps. However, I encountered an error while executing the command:

$ sudo apt-get install autoconf automake autotools-dev curl python3 libmpc-dev \
      libmpfr-dev libgmp-dev gawk build-essential bison flex texinfo gperf libtool \
      patchutils bc zlib1g-dev libexpat-dev
$ git clone --recursive https://github.com/riscv/riscv-gnu-toolchain
$ git clone --recursive https://github.com/riscv/riscv-opcodes.git
$ cd riscv-gnu-toolchain
$ ./configure --prefix=/path/to/install --with-arch=rv32gc --with-abi=ilp32d # for 32-bit toolchain
$ [sudo] make # sudo is required depending on the path chosen in the previous setup

To resolve this issue, I opted to download the prebuilt RISC-V GNU toolchain.

Then add the path /path/to/install to $PATH in the .bashrc/cshrc.

Installing RISC-V reference models: Spike and SAIL

Install SPIKE (riscv-isa-sim)

$ sudo apt-get install device-tree-compiler
$ git clone https://github.com/riscv-software-src/riscv-isa-sim.git
$ cd riscv-isa-sim
$ mkdir build
$ cd build
$ ../configure --prefix=/path/to/install
$ make
$ [sudo] make install

Install SAIL (SAIL C-emulator)

$ sudo apt-get install libgmp-dev pkg-config zlib1g-dev curl
$ curl --location https://github.com/rems-project/sail/releases/download/0.18-linux-binary/sail.tar.gz | [sudo] tar xvz --directory=/path/to/install --strip-components=1
$ git clone https://github.com/riscv/sail-riscv.git
$ cd sail-riscv
$ ARCH=RV32 make
$ ARCH=RV64 make

Rewrite 3 RISCV programs in quizs to run on mycpu

I rewrote three RISC-V programs to test the correctness of the M extension. The following are my implementations.

Quiz 2 Problem A

This program multiplies two numbers using the mul instruction in RISC-V and stores the result

.global _start

_start:
    li a1, 9                 # Load multiplier value
    li a3, 7                 # Load multiplicand value

    mul t0, a1, a3           # Perform multiplication (t0 = a1 * a3)

loop:
    j loop                    # Infinite loop to halt execution

Quiz 2 Problem D

This program implements Ancient Egyptian Multiplication using the M extension for multiplication and division.

.global _start
_start:
    li t0, 13             # Load the first number (num1) into t0
    li t1, 7              # Load the second number (num2) into t1
    li t2, 0              # Initialize the result (t2) to 0

loop:
    andi t3, t0, 1        # Check if the least significant bit of t0 is 1 (i.e., if t0 is odd)
    beq t3, x0, skip_add  # If t0 is even, skip the addition step

    # If t0 is odd, add t1 (num2) to the current result in t2
    add t2, t2, t1        # Accumulate the value of t1 in t2

skip_add:
    li t3, 2              # Load immediate value 2 into t3
    mul t1, t1, t3        # Multiply t1 by 2 (t1 = t1 * 2)
    div t0, t0, t3        # Divide t0 by 2 (t0 = t0 / 2)

    bnez t0, loop         # If t0 is not zero, continue the loop

end:
    nop                   # Placeholder for any further operations

Quiz 4 Problem A

This program calculates the square of an integer (a0 = n) and returns the result in a0.

.global _start
_start:
    li a0, 13             # Load the number (13) into register a0

    # Square the number directly in a0
    mul a0, a0, a0        # a0 = a0 * a0 (square the number using M extension)

end:
    nop                    # Placeholder for any further operations

Modify makefile to enable M Extension

+ CROSS_COMPILE ?= riscv32-unknown-elf-
- CROSS_COMPILE ?= riscv-none-elf-

+ ASFLAGS = -march=rv32im_zicsr -mabi=ilp32
- ASFLAGS = -march=rv32i_zicsr -mabi=ilp32
+ CFLAGS = -O0 -Wall -march=rv32im_zicsr -mabi=ilp32
- CFLAGS = -O0 -Wall -march=rv32i_zicsr -mabi=ilp32
LDFLAGS = --oformat=elf32-littleriscv

AS := $(CROSS_COMPILE)as
CC := $(CROSS_COMPILE)gcc
LD := $(CROSS_COMPILE)ld
OBJCOPY := $(CROSS_COMPILE)objcopy

%.o: %.S
	$(AS) -R $(ASFLAGS) -o $@ $<
%.elf: %.S
	$(AS) -R $(ASFLAGS) -o $(@:.elf=.o) $<
	$(CROSS_COMPILE)ld -o $@ -T link.lds $(LDFLAGS) $(@:.elf=.o)
%.elf: %.c init.o
	$(CC) $(CFLAGS) -c -o $(@:.elf=.o) $<
	$(CROSS_COMPILE)ld -o $@ -T link.lds $(LDFLAGS) $(@:.elf=.o) init.o

%.asmbin: %.elf
	$(OBJCOPY) -O binary -j .text -j .data $< $@

BINS = \
	fibonacci.asmbin \
	hello.asmbin \
	mmio.asmbin \
	quicksort.asmbin \
	sb.asmbin \
+	quiz2_problem_a.asmbin \
+	quiz2_problem_d.asmbin \
+	quiz4_problem_a.asmbin

# Clear the .DEFAULT_GOAL special variable, so that the following turns
# to the first target after .DEFAULT_GOAL is not set.
.DEFAULT_GOAL :=

all: $(BINS)

update: $(BINS)
	cp -f $(BINS) ../src/main/resources

clean:
	$(RM) *.o *.elf *.asmbin

Scala test class

class MultiplyTest extends AnyFlatSpec with ChiselScalatestTester {
  behavior.of("Arithmetic Operations using M Extension")
  it should "perform multiplication correctly" in {
    test(new TestTopModule("quiz2_problem_a.asmbin")).withAnnotations(TestAnnotations.annos) { c =>
      for (i <- 1 to 50) {
        c.clock.step(1000)
        c.io.mem_debug_read_address.poke((i * 4).U) // Avoid timeout
      }

      c.io.regs_debug_read_address.poke(5.U) // Address of "t0"
      c.clock.step()
      c.io.regs_debug_read_data.expect(63.U) // Verify multiplication result
    }
  }
}

class EgyptianMultiplicationTest extends AnyFlatSpec with ChiselScalatestTester {
  behavior.of("Arithmetic Operations using M Extension")
  it should "implement Ancient Egyptian multiplication correctly" in {
    test(new TestTopModule("quiz2_problem_d.asmbin")).withAnnotations(TestAnnotations.annos) { c =>
      for (i <- 1 to 50) {
        c.clock.step(1000)
        c.io.mem_debug_read_address.poke((i * 4).U) // Avoid timeout
      }

      c.io.regs_debug_read_address.poke(7.U) // Address of "t2"
      c.clock.step()
      c.io.regs_debug_read_data.expect(91.U) // Verify multiplication result
    }
  }
}

class SquareTest extends AnyFlatSpec with ChiselScalatestTester {
  behavior.of("Arithmetic Operations using M Extension")
  it should "compute the square of a number correctly" in {
    test(new TestTopModule("quiz4_problem_a.asmbin")).withAnnotations(TestAnnotations.annos) { c =>
      for (i <- 1 to 50) {
        c.clock.step(1000)
        c.io.mem_debug_read_address.poke((i * 4).U) // Avoid timeout
      }

      c.io.regs_debug_read_address.poke(10.U) // Address of "a0"
      c.clock.step()
      c.io.regs_debug_read_data.expect(169.U) // Verify square calculation result
    }
  }
}

sbt test result

image