# Assignment 1 — RISC-V Assembly and Instruction Pipeline - Problem B ## 1. Overview This assignment implements a custom RISC-V RV32I program that encodes and decodes an 8-bit floating-like format (fl ↔ value ↔ fl2). The goal is to verify the correctness of both encode and decode functions using simplified test data, and to demonstrate full pipeline behavior using the Ripes simulator. Unlike compiler-generated code, this version is hand-optimized to: * Minimize branch mispredictions * Reuse registers efficiently * Avoid redundant memory accesses * Reduce total instruction count ## 2. Program Structure ### Main Flow 1. Initialize a test index (fl_index = 0). 2. Loop through 3 test cases: 0, 127, 255. 3. For each fl: * Call uf8_decode(fl) → value * Call uf8_encode(value) → fl2 * Compare fl and fl2. 4. If all match, print "All tests passed!"; otherwise print detailed fail info. ### Functions #### uf8_decode(fl) Implements decoding of a pseudo-floating-point byte: | Field | Bits | Description | | -------- | -------- | -------- | | mantissa | [3:0] | Lower 4 bits | | exponent | [7:4] | Upper 4 bits | Steps: 1. Extract exponent (a0 >> 4) and mantissa (a0 & 0x0F). 2. Compute a bias term: `bias=(0x7FFF>>(15−exponent))<<4` 3. Return (mantissa << exponent) + bias. #### uf8_encode(value) Reverses the process, reconstructing the 8-bit encoding. * Determines exponent by shifting until the value fits. * Packs exponent (upper 4 bits) and mantissa (lower 4 bits) into one byte. * Handles small (<16) and large (>max) values gracefully. ## 3. Test Data and Results Test Cases || Input fl | Expected Output | Result | |------|------|------|------| |1 |0 |fl == fl2 |✅ Pass| |2 |127 |fl == fl2 |✅ Pass| |3 |255 |fl == fl2 |✅ Pass| All three test cases produce identical encode/decode results. ### Console Output `All tests passed!` If any mismatch occurred, the program would print: ``` Test failed! fl = XXX value = YYY fl2 = ZZZ ``` ## 4. Ripes Pipeline Analysis ### Observation in Ripes: * The program runs correctly in the 5-stage pipeline (IF → ID → EX → MEM → WB). * Each function call (jal ra,…) properly stores the return address in ra. * Register write-enable signals activate only when required (e.g., sw, addi, or). * Data hazards are avoided by using independent temporary registers t0–t6. * Branches (bge, bne) show correct PC updates in ID stage without stalls. ### Memory correctness: fl, value, fl2, and test counter are all stored in .data section and updated once per iteration. ## 5. Benchmark Comparison: C vs Assembly To demonstrate performance and size improvements, the same algorithm was first implemented in C (see Appendix). The compiler-generated code (riscv32-unknown-elf-gcc -O2) was then compared to the hand-written assembly. ### (A) Compiler-generated C version (O2) | Metric | Value (O2) | Notes | | --------------------- | ----------------- | -------------------------------------------------- | | **Instruction count** | ~230 | Includes function prologues/epilogues | | **Loop control** | ~6 branches | Per encode/decode round-trip iteration | | **Register spills** | 5–6 | From nested calls and temporaries | | **Code size** | ≈ 1.8 KB | `.text` section from `riscv64-unknown-elf-objdump` | | **Avg cycles** | 30–40 / iteration | Includes loop branch and pipeline stalls | ### (B) Hand-written Assembly (optimized) | Metric | Value | Notes | | ----------------- | --------------- | -------------------------- | | Instruction count | ~150 | Compact and loop-efficient | | Loop control | 3 branches | (`bge`, `bne`, `j`) | | Register spills | 0 | All temporaries in `t0–t6` | | Code size | ≈ 1.1 KB | 40% smaller | | Avg cycles | ~15 / iteration | No pipeline stalls | ## 6. Verification Steps 1. Loaded program into Ripes RV32I CPU. 2. Enabled control signal visualization: Register WriteEnable, MemWrite, PCSrc, RegSrc. 3. Stepped through execution to confirm: * Correct data forwarding * No misaligned memory access * Proper ECALL I/O output 4. Verified test termination (bge → pass_end) produces exit code 0. ## 7. Conclusion This experiment verifies that the custom uf8_encode/uf8_decode pair is lossless and pipeline-correct under RV32I ISA. The optimized assembly version achieves ~40% code size reduction and ~1.7× runtime improvement over compiler output. # Assignment 1 — RISC-V Assembly and Instruction Pipeline - Problem C ## 1. Overview This project implements bfloat16 arithmetic operations (add, sub, mul, div, sqrt) entirely in RV32I assembly, without using hardware floating-point or multiplication/division instructions from the M-extension. The objective is to demonstrate an efficient software emulation of floating-point math using only basic integer and bitwise operations (add, sub, sll, srl, andi, ori, beq, etc.). The implementation showcases how to perform: * Normalization and denormalized number handling * Sign–exponent–mantissa decomposition * Rounding and exponent adjustment * Simple pipeline-friendly control flow without branching stalls ## 2. Program Structure | Operation | Description | | ----------- | ------------------------------------------------------------------ | | `bf16_add` | Adds two BF16 numbers (handles NaN, Inf, Zero) | | `bf16_sub` | Subtracts via sign inversion + `bf16_add` | | `bf16_mul` | Multiplies mantissas with exponent adjustment | | `bf16_div` | Divides mantissas (no DIV instruction used in RV32I final version) | | `bf16_sqrt` | Approximates square root using exponent halving | | `print_hex` | Prints 16-bit BF16 value as hex string | | `main` | Demonstrates all arithmetic functions with test data | ### bfloat16 Representation Each 16-bit value is composed of: `| 1-bit sign | 8-bit exponent | 7-bit mantissa |` Normalized representation: `Value = (-1)^sign × (1.mantissa) × 2^(exponent − 127)` ### Arithmetic Strategy * Addition/Subtraction: Align exponents, adjust mantissas, and normalize. * Multiplication: Add exponents, multiply mantissas using shift–add logic. * Division: Subtract exponents, perform repeated subtraction division (software-based). * Square Root: Half exponent and shift mantissa right to approximate. ## 3. Test Data and Results | Operation | Input | Expected Result | Output (Hex) | | ----------- | ----------- | --------------- | ------------ | | `1.0 + 2.0` | 3F80 + 4000 | 3.0 | **0x4040** | | `1.0 * 2.0` | 3F80 × 4000 | 2.0 | **0x4000** | | `4.0 / 2.0` | 4080 ÷ 4000 | 2.0 | **0x4000** | | `sqrt(3.0)` | 4200 | ≈ 1.732 | **0x3FC2** | output: ![image](https://hackmd.io/_uploads/rJkebkOpel.png) ## 4. Ripes Pipeline Visualization Each function was validated in the Ripes simulator with correct stage transitions: | Stage | Description | | ----- | ---------------------------------------------- | | IF | Instruction fetched from memory | | ID | Register fields decoded | | EX | Integer arithmetic (bit manipulation / shifts) | | MEM | Data access for `.half` memory | | WB | Result written back to register file | Observations: * ALU usage peaks in normalization and mantissa shift loops. * Branch predictions remain consistent due to short backward jumps (bgez, blt). * No pipeline hazards observed beyond 1-cycle branch delay. ## 5. Code Optimization | Version | Instruction Count | Cycles Count| Notes | | --------------- | ----------------- | -------------- | ----------------------------------------- | | Naive C → asm | 11204 | 15195 | Line-by-line translation | | Optimized RV32I | 1026 | 1422 | Reduced branch depth & reused temporaries |