# Study Minimax (Compressed-First, Microcoded RISC-V core) > Term Project of Computer Archiecture 2023. Contributed by < [`yptang5488`](https://github.com/yptang5488) (P76111131) >, < [`GliAmanti`](https://github.com/GliAmanti) (P76111351) > :::info **Goal in the this term porject :** - Describe the techniques employed for running RVC extensions directly on Minimax. - Run the rv32i and rv32c tests in riscv-arch-test and do some experiments. - Prepare FuseSOC tool to simulate the execution of Minimax and analyze them. - Record the difficulties we encountered and how to solve them. ::: ## Introduction ### What is RVC? > RISC-V's compressed instruction (RVC) extension is intended as an add-on to the regular, 32-bit instruction set, not a replacement or competitor. Its designers designed RVC instructions to be expanded into regular 32-bit RV32I equivalents via a pre-decoder. ### What is Minimax? > "Minimax" is an **experimental RISC-V implementation** intended to establish if an RVC-optimized CPU is, in practice, any simpler than an ordinary RV32I core with pre-decoder. ## Implementation of Minimax ### Software: [microcode.S](https://github.com/yptang5488/minimax/blob/main/asm/microcode.S) Translate RV32I instructions into the RVC assembly codes. ```assembly= /* Fetch instruction, which may be half-word aligned. */ c.mv x15, x3 # load PC c.andi x15, 3 c.mv x9, x3 # load PC c.andi x9, -4 /* strip LSBs and fetch */ c.lw x8, 0(x9) # load instruction of PC c.beqz x15, 1f # jump to label 1 ``` - Load PC and corresponding instruction to `x15` and `x8`. - Jump to label `1` to decode the instruction. ```assembly= 1: c.mv x4, x8 # x4: instruction c.j op32_entry ``` - Jump to the label `op32_entry`, namely the entry of instruction decoder. ```assembly= op32_entry: c.mv x8, x4 1: /* Isolate opcode into x5 - note we strip the lower bits, which are always 11 */ c.srli x8, 2 c.mv x9, x8 c.andi x9, 0x1f c.mv x5, x9 /* Isolate rd */ c.srli x8, 5 c.mv x9, x8 c.andi x9, 0x1f c.mv x6, x9 /* isolate funct3, left shifted by 1 for jump tables */ c.srli x8, 5 c.mv x9, x8 c.andi x9, 0x7 c.slli x9, 1 c.mv x16, x9 /* isolate rs1 */ c.srli x8, 3 c.mv x9, x8 c.andi x9, 0x1f c.mv x17, x9 /* look up rs1 value from register file (we mostly need it) */ x.peek x18, 17 /* isolate rs2/shamt */ c.srli x8, 5 c.mv x9, x8 c.andi x9, 0x1f c.mv x19, x9 /* look up rs2 value from register file (we sometimes need it) */ x.peek x20, 19 /* isolate funct7 */ c.srli x8, 5 c.mv x21, x8 ``` - Split the instruction into `opcode`, `rd`, `funt3`, `rs1`, `rs2` and `funt7` section. - Read the values of `rs1` and `rs2` from `Register File` using `x.peek`. ```assembly= /* create jump based on opcode */ c.mv x8, x5 # x8: opcode c.slli x8, 1 /* 1 compressed instruction per opcode */ /* Table jump */ c.jal .+2 # store the next line address to $ra , and jump to the next 2 lines c.addi ra, 6 /* offset to table base */ c.add ra, x8 c.jr ra ``` - According to `opcode`, jump to the entry of corresponding table label. ```assembly= tablec: c.jal .+2 c.addi ra, 6 /* offset to table base */ c.add ra, x16 # x16: funt3 c.jr ra c.j add_sub /* c.0: ADD/SUB */ c.j sll /* c.1: SLL */ c.j slt /* c.2: SLT */ c.j sltu /* c.3: SLTU */ c.j xor /* c.4: XOR */ c.j srl_sra /* c.5: SRL/SRA */ c.j or /* c.6: OR */ c.j and /* c.7: AND */ ``` - Label `tablec` consists of R-type instructions, jump to specific instruction according to `funt3`. ```assembly= add_sub: c.mv x8, x18 c.mv x9, x20 /* disambiguate add/sub */ c.mv x10, x4 c.mv x11, x4 c.slli x11, 2 c.srli x11, 2 c.sub x10, x11 c.beqz x10, 1f c.li x10, -1 c.xor x9, x10 c.addi x9, 1 1: c.add x8, x9 c.j poke_ret_rv32 ``` - Label `add_sub` implement the `add` and `sub` with RVC, writing the result to `x8`. - Jump to label `poke_ret_rv32`. ```assembly= /* Placed here because c.bnez/c.beqz have limited range and are used in * relative branches */ poke_ret_rv32: x.poke 6, x8 ret_rv32: c.addi x3, 4 x.thunk 3 ``` - Write the value of `rd` back to `Register File` using `x.poke`. - Jump to the next instruction of RV32I using `x.thunk`. ### Hardware: [minimax.v](https://github.com/yptang5488/minimax/blob/main/rtl/minimax.v) Deal with the 16-bits RVC instructions in **3-stages** pipeline (IF, ID, EXE) CPU. The following is a brief summary of some of the most important registers. - Instruction control signals There are total 32 instruction control signals such as `op16_addi4spn_d`, `op16_lw_d`, `op16_add_d`, etc. In order to get the control signals, different types of masks are defined to operate on the instruction `inst`. ![image](https://hackmd.io/_uploads/r1VvhYgF6.png) Take `c.lw` for example, due to `funct3 = inst[15:13]` and `opcode = inst[1:0]`, it use the 16-bit mask `16'b111_0_00000_00000_11` to get these information and signal `op16_lw_d`. ```verilog= wire [15:0] inst_type_masked_d = inst & 16'b111_0_00000_00000_11; assign op16_lw_d = (inst_type_masked_d == 16'b010_0_00000_00000_00); ``` - Register File write/read data - `addrD_Port` : write register file port It is defined using "bitwise-and" and "bitwise-or" to determine the value from the correct part of the instruction. ```verilog= addrD_port <= (5'b00001 & {5{op16_jal_d | op16_jalr_d | op32_d}}) // write return address into ra | (regD[4:0] & {5{op16_slli_setrd_e & ~bubble_e}}) | (...) | (...) | ... ``` - `addrS_Port` : read-only register file port Use the similar way as `addrD_Port` to define. ```verilog= // READ-ONLY register file port addrS_port <= (regD[4:0] & {5{op16_slli_setrs_e & ~bubble_e}}) | (5'b00010 & {5{op16_addi4spn_d | op16_lwsp_d | op16_swsp_d}}) | (...) | (...) | ... ``` - ALU calculation registers - `aluA`, `aluB` : 2 inputs of ALU - `aluS` : `aluA + aluB` or `aluA - aluB` - `aluX` : final result outputs from ALU ### Installation & Compilation Clone the repository of [minimax](https://github.com/gsmecher/minimax) and run the test. ```shell $ git clone https://github.com/gsmecher/minimax.git $ cd minimax/tests $ make ``` :::spoiler Makefile in `minimax/test` ```makefile= ifeq ($(wildcard /.dockerenv),) # This Makefile section is evaluated when we aren't inside a Docker # environment. It re-executes make from within Docker and we end up below. .PHONY: default dockerimage shell quick DOCKERNAME=gsmecher/minimax-verification:1 # Invoke Docker default quick: $(DOCKERSTAMP) @echo Moving inside Docker... docker run --user "$(shell id -u):$(shell id -g)" \ -v $(shell git rev-parse --show-toplevel):/minimax \ $(DOCKERNAME) make -C /minimax/test $@ TESTNAME=$(TESTNAME) # Launch a shell within Docker environment shell: $(DOCKERSTAMP) @echo Moving inside Docker... docker run -it --user "$(shell id -u):$(shell id -g)" \ -v $(shell git rev-parse --show-toplevel):/minimax \ $(DOCKERNAME) /bin/bash -i $(if $(CMD), -c "$(CMD)") # Initialize or update the Docker environment dockerimage: Dockerfile docker build -t $(DOCKERNAME) . else # This section of the Makefile executes inside Docker, and runs tests. .PHONY: asm default quick asm: $(MAKE) -C ../asm default: asm # Grab a copy of the riscv-arch-test repo. We keep (and apply on # checkout) some clean-up patches here - hopefully these can be phased # out over time. if [ ! -d riscv-arch-test ]; then \ riscof --verbose info arch-test --clone; \ find patches -name '*.patch' -exec patch -d riscv-arch-test -p1 -i ../{} \; ;\ fi # Execute tests. riscof run --config=config.ini \ --suite=riscv-arch-test/riscv-test-suite \ --env=riscv-arch-test/riscv-test-suite/env quick: asm ./quick $(TESTNAME) endif ``` ::: In the makefile, the following things are done. #### 1. Invoke Docker Check if it is in the docker image, if not, moving inside docker. ```docker docker run --user "1000:1000" \ -v /home/cgvsl/P76111131/112-1/CA/git_CA/minimax:/minimax \ gsmecher/minimax-verification:1 make -C /minimax/test default TESTNAME= ``` #### 2. Execute [makefile](https://github.com/gsmecher/minimax/blob/main/asm/Makefile) in minimax/asm ```shell= make: Entering directory '/minimax/test' make -C ../asm make[1]: Entering directory '/minimax/asm' ... ... ... make[1]: Leaving directory '/minimax/asm' ``` - Get `microcode.bin` and translate it to `microcode.hex`. ```shell= # microcode.S -> microcode.o -(linker:bare.ld)-> microcode.elf -> microcode.bin -(./bin2hex)-> microcode.hex riscv32-corev-elf-gcc -march=rv32ic_zcb -mabi=ilp32 -fno-pic -nostartfiles -nostdlib -c -o microcode.o microcode.S riscv32-corev-elf-gcc -march=rv32ic -mabi=ilp32 -fno-pic -Wl,--no-relax -nostartfiles -nostdlib -T bare.ld -o microcode.elf microcode.o riscv32-corev-elf-objcopy -O binary microcode.elf microcode.bin python3 ./bin2hex microcode.bin microcode.hex ``` - Get `blink.bin`. ```shell= # blink.S -> blink.o -(linker:bare.ld)-> blink.elf -> blink.bin -(./bin2hex & microcode.hex)-> blink.mem riscv32-corev-elf-gcc -march=rv32ic_zcb -mabi=ilp32 -fno-pic -nostartfiles -nostdlib -c -o blink.o blink.S riscv32-corev-elf-gcc -march=rv32ic -mabi=ilp32 -fno-pic -Wl,--no-relax -nostartfiles -nostdlib -T bare.ld -o blink.elf blink.o riscv32-corev-elf-objcopy -O binary blink.elf blink.bin ``` - Concate `blink.bin`, `microcode.hex` into `blink.mem` with hexadecimal type. ```shell python3 ./bin2hex --microcode=microcode.hex blink.bin blink.mem ``` #### 3. Use [RISCOF](https://github.com/riscv-software-src/riscof) - Grab a copy of the `riscv-arch-test` repo. - Keep (and apply on checkout) some clean-up patches here - hopefully these can be phased out over time. ```shell= if [ ! -d riscv-arch-test ]; then \ riscof --verbose info arch-test --clone; \ find patches -name '*.patch' -exec patch -d riscv-arch-test -p1 -i ../{} \; ;\ fi ``` #### 4. Execute tests ::: info :mag: RISCOF document: [**Running of RISCOF**](https://riscof.readthedocs.io/en/stable/installation.html#running-riscof) ::: - Test on the each of the models and compare the signature values to guarantee correctness. ```shell= # Execute tests. riscof run --config=config.ini \ --suite=riscv-arch-test/riscv-test-suite \ --env=riscv-arch-test/riscv-test-suite/env ``` 1. `config.ini`: configuration file Define simulation environment including `reference plugin` and `DUT (Device Under Test) Plugin`. ```python= [RISCOF] ReferencePlugin=sail_cSim ReferencePluginPath=sail_cSim DUTPlugin=minimax DUTPluginPath=minimax [minimax] pluginpath=minimax ispec=minimax/minimax_isa.yaml pspec=minimax/minimax_platform.yaml target_run=1 [sail_cSim] pluginpath=sail_cSim ``` ::: spoiler The setting format of `DUT` and `BASE` class is defined in [riscof/constants.py](https://github.com/riscv-software-src/riscof/blob/108fa2d4e824b3e37fcff82e4fbbb7243127320b/riscof/constants.py#L28) ``` python= config_temp = ...[RISCOF] ReferencePlugin={0} ReferencePluginPath={1} DUTPlugin={2} DUTPluginPath={3} [{2}] pluginpath={3} ispec={3}/{2}_isa.yaml pspec={3}/{2}_platform.yaml target_run=1 [{0}] pluginpath={1} ... ``` ::: 2. Build the `DUT` and `Reference` model in [riscof/framework/main.py](https://github.com/riscv-software-src/riscof/blob/108fa2d4e824b3e37fcff82e4fbbb7243127320b/riscof/framework/main.py#L159). ```python= def run(dut, base, dut_isa_spec, dut_platform_spec, work_dir, cntr_args): # Setting up models dut.initialise(constants.suite, work_dir, constants.env) base.initialise(constants.suite, work_dir, constants.env) #Loading Specs ispec = utils.load_yaml(dut_isa_spec) pspec = utils.load_yaml(dut_platform_spec) #... logger.info("Running Build for DUT") dut.build(dut_isa_spec, dut_platform_spec) logger.info("Running Build for Reference") base.build(dut_isa_spec, dut_platform_spec) results = test.run_tests(dut, base, ispec['hart0'], pspec, work_dir, cntr_args) return results ``` 3. Selecting Tests: `generate_test_pool()` is in [riscof/framework/test.py](https://github.com/riscv-software-src/riscof/blob/108fa2d4e824b3e37fcff82e4fbbb7243127320b/riscof/framework/test.py#L282). Load yaml file and choose the test pool of "RV32IC". ```python= def generate_test_pool(ispec, pspec, workdir, dbfile = None): #... if not macros == []: try: isa = prod_isa(ispec['ISA'],db[file]['isa']) except TestSelectError as e: logger.error("Error in test: "+str(file)+"\n"+str(e)) continue if '32' in isa: xlen = '32' elif '64' in isa: xlen = '64' elif '128' in isa: xlen = '128' macros.append("XLEN=" + xlen) # ... # ... build test_list.yaml return (test_list, test_pool) ``` 4. Run the tests: `run_tests()` is in [riscof/framework/test.py](https://github.com/riscv-software-src/riscof/blob/108fa2d4e824b3e37fcff82e4fbbb7243127320b/riscof/framework/test.py#L370). Running the tests on both `DUT` and `reference` models and execute signature checking. ```python= def generate_test_pool(ispec, pspec, workdir, dbfile = None): #... logger.info("Running Tests on DUT.") dut.runTests(dut_test_list) logger.info("Running Tests on Reference Model.") base.runTests(base_test_list) logger.info("Initiating signature checking.") for file in test_list: testentry = test_list[file] work_dir = testentry['work_dir'] res = os.path.join(dut_test_list[file]['work_dir'],dut.name[:-1]+".signature") ref = os.path.join(base_test_list[file]['work_dir'],base.name[:-1]+".signature") result, diff = compare_signature(res, ref) # ... # ... return results ``` ## FuseSOC ### Installation Process and Error - Install FuseSOC ```shell= $ pip3 install --upgrade --user fusesoc ``` - Look up the FuseSOC library. ```shell= $ fusesoc library list ``` - Error message: **No libraries registered** ```shell= Available cores: ERROR: NO libraries registered ``` - **Solution**: Clone `minimax` to `fusesoc_libraries`. ```shell= $ fusesoc library add minimax https://github.com/gsmecher/minimax ``` - Run FuseSOC ```shell= $ fusesoc run --target=sim ::minimax:1.0.0 ``` - Error message: No Vivado command `xelab` ```shell= ERROR: make: xelab: Command not found ``` - **Solution**: Install [Vivado](https://hackmd.io/PkutYx30QH-H3qknn9SkuA?both#Vivado). ### Execution - Run FuseSOC ```shell $ fusesoc run --target=sim ::minimax:1.0.0 ``` - Error in `minimax.v` There are several errors in the verilog file, such as "unknown type" and "syntax error" and we solved them all. ```shell ERROR: [VRFC 10-4982] syntax error near 'case' ERROR: [VRFC 10-4982] syntax error near '+' ERROR: [VRFC 10-2939] 'logic' is an unknown type ``` In the case, the `logic` type, `unique case` cannot be defined, it is possible that the author used a different version of verilog (ex. SystemVerilog), so the code is not compatible. - Error in `minimax_tb.vhd` ```shell ERROR: [VRFC 10-719] formal port/generic <trace> is not declared in <minimax> ERROR: [VRFC 10-719] formal port/generic <inst_regce> is not declared in <minimax> ERROR: [VRFC 10-3353] formal port 'rdata' has no actual or default value ``` There are still several errors in this file and we are still finding causes and solutions. ## Vivado ### What is Vivado? Vivado is an EDA tool that provides a complete development environment for designing and programming Xilinx FPGAs (Field Programmable Gate Arrays) and SoCs (Systems on Chips) for digital design projects. **Tool Command Language (TCL)** is used as a script for the Xilinx Vivado tool. ```tcl= #!/usr/bin/Vivado/2019.1/bin -S vivado -mode batch -source create_project arty_a7 arty_a7 -part "xc7a35t-csg324-1" # RTL Sources read_vhdl -vhdl2008 [file normalize "../rtl/blinker.vhd"] read_verilog -sv [file normalize "../rtl/minimax.v"] set_property top blinker [current_fileset] # Blinker assembly add_files ../asm/blink.mem # Constraints add_files -fileset constrs_1 arty_a7.xdc # Ensure we're aggressively optimizing set_property STEPS.SYNTH_DESIGN.ARGS.DIRECTIVE AreaOptimized_high [get_runs synth_1] start_gui ``` The RTL sources, FPGA part, blinking device and `.xdc` constraints are set in the file. ### Installation Process and Error We install the **Vivado Lab** version for the first time and we find that there are some unsupported functions and flags. - Execute the command at the path we install Vivado Lab ```shell $ source settings64.sh $ vivado_lab $ vivado_lab -source arty_a7.tcl ``` - Error message : **load file error** ```shell application-specific initialization failed: couldn't load file "librdi_commontasks.so": libtinfo.so.5: cannot open shared object file: No such file or directory ``` - [Solution](https://support.xilinx.com/s/article/76585?language=en_US) : We solve the problem by using the following commands. ```shell $ sudo apt update $ sudo apt install libtinfo-dev $ sudo ln -s /lib/x86_64-linux-gnu/libtinfo.so.6 /lib/x86_64-linux-gnu/libtinfo.so.5 ``` - Error message : **Vivado version incompatible** ```shell= create_project no -part flag ``` Because **"Vivado-Lab"** version has fewer functions than the normal one. - Error message : **no part matched** Run Vivado gui successfully but can't find the matched part `"xc7a35t-csg324-1"` with minimax. ![image](https://hackmd.io/_uploads/ryheUwbYa.png) After checking the [Vivado Design Suite User Guide (p.9)](https://docs.xilinx.com/v/u/2019.1-English/ug973-vivado-release-notes-install-license), we ensure this FPGA version is included in Vivado WebPACK Tool, but we don't choose the Artix FPGA options when installing. ### Successful installing We download the version of `Vivado 2019.1`. ```shell $ source /usr/bin/Vivado/2019.1/settings64.sh $ vivado -source arty_a7.tcl # run the tcl for first time ``` :::warning Can you show some synthesis information? ::: ### Simulation To facilitate the observation of signal interactions, we have modified the types of `clk`, `rst` and `inst` from `wire` to `reg`. This adjustment enables us to manually control the signal value, allowing us to drive the CPU and analyze the signal behavior. The designa Sources and constraints are shown in the following picture : ![image](https://hackmd.io/_uploads/BJCyJt-KT.png) We take the instruction `c.add x3, x5` for example, analyze the important registers and wires by checking waveform graph shown on Vivado. ![image](https://hackmd.io/_uploads/BJ-urPbtT.png) - **Instruction decode** According to the [The RISC-V Compressed Instruction Set Manual (p. 12, 17)](https://riscv.org/wp-content/uploads/2015/11/riscv-compressed-spec-v1.9.pdf), > ![圖片](https://hackmd.io/_uploads/BJmgsP-Y6.png) > > ![圖片](https://hackmd.io/_uploads/Hywoiw-ta.png) the instruction `c.add x3, x5`, which is represented as `0x9196`, will be decoded into the following parts. | funt4 | rd/rs1 | rs2 | opcode | |:-----:|:----------:|:----------:|:------:| | 1001 | 00011 (x3) | 00101 (x5) | 10 | So we can get the following information. * `funt4` & `opcode` = `C.ADD` * `rd` idx = `rs1` idx = `x3` * `rs2` idx = `x5` ::: success In this experiment, we set the write back register to be `x10`, so the result of `ALU` will be sent to `x10`. ::: - **mask and control signal** ```verilog wire [15:0] inst_type_masked_mj_d = inst & 16'b111_1_00000_00000_11; assign op16_add_d = (inst_type_masked_mj_d == 16'b100_1_00000_00000_10) & ~op16_jalr_d & ~op16_ebreak_d; ``` `inst_type_masked_mj_d` get `funt4`(MSB 4 bits) and `opcode`(LSB 2 bits). ![image](https://hackmd.io/_uploads/Hyd4cv-Kp.png) To access the control signal `op16_add_d` correctly, it would get `c.jal` and `c.ebreak` first because there is zero existing in register part. - `inst_type_masked_mj_d` = `0x8002` - `op16_add_d` = `1` - **Register File** ```verilog= // write/read address in Register File addrD_port <= (inst[11:7] & ({5{op16_add_d | ...}}) | ... addrS_port <= (inst[6:2] & {5{op16_add_d | ...}}) | ... ``` - `addrD_port` = `b'00011` & `b'11111` = `b'00011` (`3`) - `addrS_port` = `b'00101` & `b'11111` = `b'00101` (`5`) - `regD` = `register_file`[`addrD_port`] = `0x00000007` (`7`) - `regS` = `register_file`[`addrS_port`] = `0x00000011` (`17`) - **ALU** ```verilog= // inputs of ALU assign aluA = aluA_imm | (regD & {32{op16_add_e | ...}}); assign aluB = aluB_imm | regS; // add or sub assign aluS = op16_sub_e ? (aluA - aluB) : (aluA + aluB); // result of ALU assign aluX = (aluS & ( {32{op16_add_e | ...}} ) | ... )) ``` - `aluA` = `0x00000000` | `0x00000007` = `0x00000007` (`7`) - `aluB` = `0x00000000` | `0x00000011` = `0x00000011` (`17`) - `aluS` = `0x00000007` + `0x00000011` = `0x00000018` (`24`) - `aluX` = `0x00000018` (`24`) - **write back** ```verilog= reg wb; always @(posedge clk) begin wb <= ~reset & ( op16_add_d | ... ); // writeback if (|(addrD[4:0]) & ~(stall_e | bubble_e) & (wb | rack)) begin //register_file[addrD] <= aluX; register_file[10] <= aluX; end end ``` - singal control `wb` = `1` - We assign the write back register as `x10` to check if it write back the correct value, and finally we get the right value `x10` = `0x00000018` (`24`). ## Reference - [Minimax GitHub](https://github.com/gsmecher/minimax) - [RISCOF Doc](https://riscof.readthedocs.io/en/stable/intro.html) / [RISCOF GitHub](https://github.com/riscv-software-src/riscof/tree/108fa2d4e824b3e37fcff82e4fbbb7243127320b) - [FuseSoc GitHub](https://github.com/olofk/fusesoc) / [LED to believe GitHub (step of running FuseSoc)](https://github.com/fusesoc/blinky) - [The RISC-V Compressed Instruction Set Manual](https://riscv.org/wp-content/uploads/2015/11/riscv-compressed-spec-v1.9.pdf)