# Single-cycle RISC-V core > 李尚宸 [GitHub](https://github.com/TreeLand1101/Single-cycle-RISC-V-core) - [x] Pass [ca2023-lab3](https://github.com/sysprog21/ca2023-lab3) funtional test - [x] Make it with RV32IM instruction set - [x] Build [riscv-arch-test](https://github.com/riscv-non-isa/riscv-arch-test) environment - [x] 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 ``` :::danger 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 ```scala 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` ![螢幕擷取畫面 2025-01-18 140802](https://hackmd.io/_uploads/Sk-NEToP1l.png) * When `io_jump_flag_id` set to 0, entry address = `PC + 4.U` = `0x1014` ![螢幕擷取畫面 2025-01-18 140822](https://hackmd.io/_uploads/SkcEEpsDyg.png) * When `io_jump_flag_id` set to 1, entry address = `0x1000` ![螢幕擷取畫面 2025-01-18 140958](https://hackmd.io/_uploads/rkxHNTsw1l.png) ### InstructionDecoderTest ```scala 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 ![螢幕擷取畫面 2025-01-18 144735](https://hackmd.io/_uploads/SJvDEaoPkx.png) For the second instruction `lui x5, 2`: * `io_memory_read_enable` should be 0 * `io_memory_write_enable` should be 0 ![螢幕擷取畫面 2025-01-18 144845](https://hackmd.io/_uploads/ry0PVpjwyx.png) FOr the third instruction `add x3, x1, x2`: * `io_memory_read_enable` should be 0 * `io_memory_write_enable` should be 0 ![螢幕擷取畫面 2025-01-18 144910](https://hackmd.io/_uploads/SJXONTiwyx.png) ### ExecuteTest ```scala 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`: ![螢幕擷取畫面 2025-01-18 145934](https://hackmd.io/_uploads/BJLcE6ov1e.png) For the second instruction, `beq x1, x2, 2`: * When `x1 != x2`, `io_if_jump_flag` should be 0 ![螢幕擷取畫面 2025-01-18 150822](https://hackmd.io/_uploads/BkWhV6oPyg.png) * When `x1 == x2`, `io_if_jump_flag` should be 1 ![螢幕擷取畫面 2025-01-18 150842](https://hackmd.io/_uploads/ByH246oDyx.png) ## 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](https://hackmd.io/_uploads/B1HuSpswye.png) * RV32M: ![image](https://hackmd.io/_uploads/r1strTiPyg.png) We can distinguish between the two using `funct7`, as shown in the following code: ```scala ... 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](https://riscof.readthedocs.io/en/stable/arch-tests.html) The [riscv-arch-test](https://github.com/riscv-non-isa/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](https://github.com/riscv-collab/riscv-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](https://github.com/riscv-collab/riscv-gnu-toolchain/releases/tag/2024.11.22). 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](https://hackmd.io/@sysprog/HyoeCOcr1e#Quiz2-ProblemA) 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](https://hackmd.io/@sysprog/arch2024-quiz2-sol#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](https://hackmd.io/@sysprog/arch2024-quiz4-sol#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 ```diff + 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](https://hackmd.io/_uploads/SytKXL1_ye.png)