---
# System prepended metadata

title: 'Assignment 2: RISC-V Toolchain'

---

# Assignment 2: RISC-V Toolchain
#### contributed by <`bbchen`>

# [Uf8](https://hackmd.io/@bbchen/arch2025-HW1)

You can find the detailed problem description and testing method in my [HW1](https://hackmd.io/@bbchen/arch2025-HW1).

After building the system mode.

The directory contains a comprehensive bare-metal test suite:
- Makefile
- uf8 encode/decode in RISC-V assembly
- uf8 encode/decode function code in c
- Performance counter functions for RISC-V
- Startup code in assembly
- Linker scripts

---
### Makefile

```makefile
include ../../mk/toolchain.mk

ARCH = -march=rv32i_zicsr -mabi=ilp32 
LINKER_SCRIPT = linker.ld

EMU ?= ../../build/rv32emu

AFLAGS = -g $(ARCH)
LDFLAGS = -T $(LINKER_SCRIPT)


ifdef FAST
	CFLAGS = -g -march=rv32i_zicsr -mabi=ilp32 -Ofast
else ifdef SIZE
	CFLAGS = -g -march=rv32i_zicsr -mabi=ilp32 -Os
else
	CFLAGS = -g -march=rv32i_zicsr -mabi=ilp32 -O0
endif

ifneq ($(DEFINES),)
CFLAGS += $(DEFINES)
endif

EXEC = test.elf

CC = $(CROSS_COMPILE)gcc
AS = $(CROSS_COMPILE)as
LD = $(CROSS_COMPILE)ld
OBJDUMP = $(CROSS_COMPILE)objdump

OBJS = start.o perfcounter.o main.o uf8.o

.PHONY: all run dump clean

all: $(EXEC)

$(EXEC): $(OBJS) $(LINKER_SCRIPT)
	$(LD) $(LDFLAGS) -o $@ $(OBJS)

%.o: %.S
	$(AS) $(AFLAGS) $< -o $@

%.o: %.c
	$(CC) $(CFLAGS) $< -o $@ -c

run: $(EXEC)
	@test -f $(EMU) || (echo "Error: $(EMU) not found" && exit 1)
	@grep -q "ENABLE_ELF_LOADER=1" ../../build/.config || (echo "Error: ENABLE_ELF_LOADER=1 not set" && exit 1)
	@grep -q "ENABLE_SYSTEM=1" ../../build/.config || (echo "Error: ENABLE_SYSTEM=1 not set" && exit 1)
	$(EMU) $<

dump: $(EXEC)
	$(OBJDUMP) -Ds $< | less

clean:
	rm -f $(EXEC) $(OBJS)
```
This `Makefile` builds a `bare-metal` RV32I program .
It sets the RISC-V cross-toolchain and arch flags, compiles `start.S`, `perfcounter.S`, `main.c`, and `uf8.S` into objects, then links them with `linker.ld` to produce `test.elf`. 

The run target sanity-checks that rv32emu exists and that ENABLE_ELF_LOADER=1 and ENABLE_SYSTEM=1 are set, then runs the ELF in the emulator. 


---

### main.c
:::spoiler c code

```c
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>


static inline void write_str_syscall(int fd, const char *ptr, long len) {
    register long a0 asm("a0") = fd;
    register const char *a1 asm("a1") = ptr;
    register long a2 asm("a2") = len;
    register long a7 asm("a7") = 64;   // system_write

    asm volatile("ecall"
                 : "+r"(a0)              
                 : "r"(a1), "r"(a2), "r"(a7)
                 : "memory", "cc");      
}

#define printstr(ptr, length) write_str_syscall(1, (ptr), (length))

#define TEST_OUTPUT(msg, length) printstr((msg), (length))

#define TEST_LOGGER(msg)                          \
do {                                              \
    static const char _s_[] = msg;                \
    TEST_OUTPUT(_s_, (long)(sizeof(_s_) - 1));    \
} while (0)

extern uint64_t get_cycles(void);
extern uint64_t get_instret(void);

// /* Bare metal memcpy implementation */
void *memcpy(void *dest, const void *src, size_t n)
{
    uint8_t *d = (uint8_t *) dest;
    const uint8_t *s = (const uint8_t *) src;
    while (n--)
        *d++ = *s++;
    return dest;
}

/* Software division for RV32I (no M extension) */
static unsigned long udiv(unsigned long dividend, unsigned long divisor)
{
    if (divisor == 0)
        return 0;

    unsigned long quotient = 0;
    unsigned long remainder = 0;

    for (int i = 31; i >= 0; i--) {
        remainder <<= 1;
        remainder |= (dividend >> i) & 1;

        if (remainder >= divisor) {
            remainder -= divisor;
            quotient |= (1UL << i);
        }
    }

    return quotient;
}

static unsigned long umod(unsigned long dividend, unsigned long divisor)
{
    if (divisor == 0)
        return 0;

    unsigned long remainder = 0;

    for (int i = 31; i >= 0; i--) {
        remainder <<= 1;
        remainder |= (dividend >> i) & 1;

        if (remainder >= divisor) {
            remainder -= divisor;
        }
    }

    return remainder;
}

/* Software multiplication for RV32I (no M extension) */
static uint32_t umul(uint32_t a, uint32_t b)
{
    uint32_t result = 0;
    while (b) {
        if (b & 1)
            result += a;
        a <<= 1;
        b >>= 1;
    }
    return result;
}

/* Provide __mulsi3 for GCC */
uint32_t __mulsi3(uint32_t a, uint32_t b)
{
    return umul(a, b);
}

/* Simple integer to hex string conversion */
static void print_hex(unsigned long val)
{
    char buf[20];
    char *p = buf + sizeof(buf) - 1;
    *p = '\n';
    p--;

    if (val == 0) {
        *p = '0';
        p--;
    } else {
        while (val > 0) {
            int digit = val & 0xf;
            *p = (digit < 10) ? ('0' + digit) : ('a' + digit - 10);
            p--;
            val >>= 4;
        }
    }

    p++;
    printstr(p, (buf + sizeof(buf) - p));
}

/* Simple integer to decimal string conversion */
static void print_dec(unsigned long val)
{
    char buf[20];
    char *p = buf + sizeof(buf) - 1;
    *p = '\n';
    p--;

    if (val == 0) {
        *p = '0';
        p--;
    } else {
        while (val > 0) {
            *p = '0' + umod(val, 10);
            p--;
            val = udiv(val, 10);
        }
    }

    p++;
    printstr(p, (buf + sizeof(buf) - p));
}


/* ============= Declaration ============= */

typedef uint8_t uf8;

extern uint32_t clz_asm(uint32_t x);

extern uint32_t uf8_decode_asm(uf8 fl);

extern uf8 uf8_encode_asm(uint32_t value);



/* ============= Decode Encode  ============= */



static inline unsigned clz(uint32_t x)
{
    int n = 32, c = 16;
    do {
        uint32_t y = x >> c;
        if (y) {
            n -= c;
            x = y;
        }
        c >>= 1;
    } while (c);
    return n - x;
}

/* Decode uf8 to uint32_t */
uint32_t uf8_decode(uf8 fl)
{
    uint32_t mantissa = fl & 0x0f;
    uint8_t exponent = fl >> 4;
    uint32_t offset = (0x7FFF >> (15 - exponent)) << 4;
    return (mantissa << exponent) + offset;
}

/* Encode uint32_t to uf8 */
uf8 uf8_encode(uint32_t value)
{
    /* Use CLZ for fast exponent calculation */
    if (value < 16)
        return value;

    /* Find appropriate exponent using CLZ hint */
    int lz = clz(value);
    int msb = 31 - lz;

    /* Start from a good initial guess */
    uint8_t exponent = 0;
    uint32_t overflow = 0;

    if (msb >= 5) {
        /* Estimate exponent - the formula is empirical */
        exponent = msb - 4;
        if (exponent > 15)
            exponent = 15;

        /* Calculate overflow for estimated exponent */
        for (uint8_t e = 0; e < exponent; e++)
            overflow = (overflow << 1) + 16;

        /* Adjust if estimate was off */
        while (exponent > 0 && value < overflow) {
            overflow = (overflow - 16) >> 1;
            exponent--;
        }
    }

    /* Find exact exponent */
    while (exponent < 15) {
        uint32_t next_overflow = (overflow << 1) + 16;
        if (value < next_overflow)
            break;
        overflow = next_overflow;
        exponent++;
    }

    uint8_t mantissa = (value - overflow) >> exponent;
    return (exponent << 4) | mantissa;
}

/* ============= Test Suite ============= */

static bool test_c(void)
{
    int32_t previous_value = -1;
    bool passed = true;

    for (int i = 0; i < 256; i++) {
        uint8_t fl = i;
        int32_t value = uf8_decode(fl);
        uint8_t fl2 = uf8_encode(value);

        if (fl != fl2) {
            passed = false;
        }

        if (value <= previous_value) {
            passed = false;
        }

        previous_value = value;
    }

    return passed;
}


static bool test_s(void)
{
    int32_t previous_value = -1;
    bool passed = true;

    for (int i = 0; i < 256; i++) {
        uint8_t fl = i;
        int32_t value = uf8_decode_asm(fl);
        uint8_t fl2 = uf8_encode_asm(value);

        if (fl != fl2) {
            passed = false;
        }

        if (value <= previous_value) {
            passed = false;
        }

        previous_value = value;
    }

    return passed;
}



int main(void)
{
    uint64_t start_cycles, end_cycles, cycles_elapsed;
    uint64_t start_instret, end_instret, instret_elapsed;

    TEST_LOGGER("\n=== Uf8 Tests Start ===\n\n");

    /* Test : CUf8 */
    TEST_LOGGER("\n=== C Tests ===\n\n");
    start_cycles = get_cycles();
    start_instret = get_instret();

    bool success_c = test_c();

    end_cycles = get_cycles();
    end_instret = get_instret();
    cycles_elapsed = end_cycles - start_cycles;
    instret_elapsed = end_instret - start_instret;

    TEST_LOGGER("  Cycles: ");
    print_dec((unsigned long) cycles_elapsed);
    TEST_LOGGER("  Instructions: ");
    print_dec((unsigned long) instret_elapsed);
    TEST_LOGGER("\n");


    TEST_LOGGER("\n=== Assembly Tests ===\n\n");

    /* Test : CUf8 */
    start_cycles = get_cycles();
    start_instret = get_instret();

    bool success_s = test_s();

    end_cycles = get_cycles();
    end_instret = get_instret();
    cycles_elapsed = end_cycles - start_cycles;
    instret_elapsed = end_instret - start_instret;

    TEST_LOGGER("  Cycles: ");
    print_dec((unsigned long) cycles_elapsed);
    TEST_LOGGER("  Instructions: ");
    print_dec((unsigned long) instret_elapsed);
    TEST_LOGGER("\n");

    if((success_s || success_c) == false) {
        TEST_LOGGER("Some tests failed.\n");
        return 1;
    }
    else{
        TEST_LOGGER("All tests passed.\n");
    }
    return 0;
}
```
:::

1. Basic Syscall I/O

    This part provide minimal output without relying on libc (printf).
- `printstr()`
- `TEST_LOGGER()` 

---

2. Printing Utilities

| Function | Description |
| :--- | :--- |
| `print_hex(val)` | Prints integer in hex format |
| `print_dec(val)` | Prints integer in decimal format |

---

3. Performance Counters
```c
extern uint64_t get_cycles(void);
extern uint64_t get_instret(void);
```
Used in both C and assembly tests to measure performance.

---

4. Software Implementations (No M Extension)

| Function | Purpose |
| :--- | :--- |
| `memcpy()` | Simple byte copy |
| `udiv()` / `umod()` | Bitwise restoring division/modulus |
| `umul()` / `__mulsi3()` | Shift-add multiplication  |

---

5. UF8 Codec Interface
```c
typedef uint8_t uf8;

extern uint32_t clz_asm(uint32_t x);
extern uint32_t uf8_decode_asm(uf8 fl);
extern uf8 uf8_encode_asm(uint32_t value);
```

Assembly versions provided for benchmarking.

---

6. UF8 function and test suites in c

    Use the same method as in HW1 for testing.

---

### uf8 encode/decode in RISC-V assembly

:::spoiler Assembly

```assembly
.section .text
.align  2

/* --------- uf8_decode --------- */
.globl  uf8_decode_asm
.type   uf8_decode_asm, @function

uf8_decode_asm:

    addi    sp, sp, -4
    sw      ra, 0(sp)

    andi t0, a0, 0x0f #t0 = mantissa
    srli t1, a0, 4    #t1 = exponent
    
    # offset = ((1 << exponent) - 1) << 4
    addi t2, zero, 1
    sll t2, t2, t1
    addi t2, t2, -1
    slli t2, t2, 4 #t2 = offset
    
    sll a0,t0, t1
    add a0,a0, t2

    lw      ra, 0(sp)              # restore return addr
    addi    sp, sp, 4

    ret

/* --------- uf8_encode --------- */
.globl  uf8_encode_asm
.type   uf8_encode_asm, @function

uf8_encode_asm:

    li   t0, 16
    slt  t0, a0 ,t0
    bne  t0, zero, return_value
    
    addi  sp, sp, -28
    sw    s0, 24(sp)
    sw    s1, 20(sp)
    sw    s2, 16(sp)
    sw    s3, 12(sp)
    sw    s4, 8(sp)
    sw    s5, 4(sp)
    sw    ra, 0(sp)


    
    add  s0, a0, zero #s0 = value
    
    #Find appropriate exponent using CLZ hint
    jal  ra, clz_asm
    add  s1, a0, x0  #s1 = lz
    li   t0, 31
    sub  s2, t0, s1    #s2 = msb
    
    
    add s3, zero, zero #s3 = exponent
    add s4, zero, zero #s4 = overflow


    li  t0, 5
    blt t0, s2, 1f

    addi s3, s2, -4

    li  t0, 15
    ble s3, t0, 2f
    li  s3, 15

# Calculate overflow for estimated exponent
2:
    li  s5, 0
3:
    bge  s5, s3, 4f
    slli s4, s4, 1
    addi s4, s4, 16
    addi s5, s5, 1
    j    3b

# Adjust if estimate was off 
4:
    slt t0, x0, s3
    slt t1, s0, s4
    and t0, t0, t1

    beq t0, x0, 1f

    addi  s4, s4, -16
    srli  s4, s4, 1
    addi  s3, s3, -1

    j     4b 


#Find exact exponent
1:
    addi t0, x0, 15

    bge  s3, t0, encode_done
    slli s6, s4, 1
    addi s6, s6, 16

    blt  s0, s6, encode_done
    mv   s4, s6
    addi s3, s3, 1
    j    1b

encode_done:
    sub  s7, s0, s4
    srl  s7, s7, s3

    slli  a0, s3, 4
    or    a0, a0, s7
    
    lw    ra, 0(sp)
    lw    s5, 4(sp)
    lw    s4, 8(sp)
    lw    s3, 12(sp)
    lw    s2, 16(sp)
    lw    s1, 20(sp)
    lw    s0, 24(sp)
    addi  sp, sp, 28
    
return_value:
    
    ret

/* --------- clz (count leading zeros, 0..32) --------- */
.globl  clz_asm
.type   clz_asm, @function

clz_asm:
    addi t0, zero, 32 #t0 = n
    addi t1, zero, 16 #t1 = c
    add t3, a0, zero #t3 = x
    
clz_Loop:
    srl t2, t3, t1 #t2 = y
    beq t2, zero, clz_y_0
    sub t0, t0, t1
    add t3, t2, zero #t3 = x
    
clz_y_0:
    srli t1,t1,1 # c >> 1
    
    bne  t1, x0, clz_Loop
    sub  a0,t0,t3
    ret

```
:::

Since `rv32emu` (ithis assignment) and `Ripes` ( assignment 1) are not compatible , 
we need to modify/correct the system calls first so that the assembly code can execute successfully on `rv32emu`.

When writing assembly that will be linked with C code, we often need to mark functions as `global` (visible to the linker and other object files).


>
>- `.globl uf8_decode_asm`
>    Declares the symbol uf8_decode_asm as global. 
>    
>    This allows a C file that declares.
>    ```
>    extern uint32_t uf8_decode_asm(uint8_t);
>    ```
>    to link correctly to this assembly function.

---

### Building the Test Suite

```
make
```

Build artifacts:
- `main.o` - Test implementation
- `perfcounter.o` - CSR 
- `start.o` - startup code with BSS initialization
- `uf8.o` - uf8 encode/decode implementation
- `test.elf` - linked executable

### Running test

```
make run
```

Output:
```
=== Uf8 Tests Start ===

=== C Tests ===

  Cycles: 71316
  Instructions: 71316


=== Assembly Tests ===

  Cycles: 41762
  Instructions: 41762

All tests passed.
```

The UF8 test results show that both the `C` and `assembly` implementations produce identical and correct outputs, confirming functional equivalence.

However, the performance measurements reveal a clear improvement in the assembly version.

The C version required 71,316 cycles/instructions, while the assembly version used only 41,762, achieving roughly a 1.7× speedup.

This demonstrates that manual optimization in assembly can significantly reduce instruction count and improve execution efficiency on RV32I processors lacking hardware multiplication and division.

---

The compilation options above use the default `-O0` setting.

I also experimented with different optimization levels, `-Ofast` (optimized for speed) and `-Os` (optimized for size).

- Ofast
    ```
    make FAST=1
    ```

    Output:
    ```
    === Uf8 Tests Start ===

    === C Tests ===

      Cycles: 19802
      Instructions: 19801


    === Assembly Tests ===

      Cycles: 63308
      Instructions: 38414

    All tests passed.
    ```

- Os
    ```
    make SIZE=1
    ```
    
    Output:
    ```
    === Uf8 Tests Start ===

    === C Tests ===

      Cycles: 32459
      Instructions: 32459

    All tests passed.
    ```

After applying different compiler optimization levels, the performance of the UF8 test program changed noticeably.

When optimized with `-Ofast`, the C implementation improved dramatically to `19,802` cycles, showing that compiler optimization can effectively reduce redundant instructions.

In contrast, the assembly version did not benefit from compiler optimizations and even became slower (63,308 cycles) since hand-tuned assembly is already optimized and may be affected by different inlining or alignment.

Using `-Os` (size optimization) increased the C execution time (32,459 cycles) but reduced code size.

:::warning
When creating an assembly file, make sure the `.S` extension is capitalized.
I spent half a day debugging this issue : )
:::

---

# [hanoi](https://hackmd.io/@sysprog/arch2025-quiz2-sol)

This problem approaches the Tower of Hanoi using an `iterative Gray code–based` method instead of traditional recursion.

The key idea is that each Gray code sequence changes by exactly one bit at a time, and this single bit flip directly indicates which disk should be moved.

This strategy ensures that only one disk moves per step and that larger disks are never placed on smaller ones, fully preserving the rules of the Tower of Hanoi. 

---

The directory contains a comprehensive bare-metal test suite:
- Makefile
- hanoi in RISC-V assembly
- hanoi function code in c using iterative method
- Performance counter functions for RISC-V
- Startup code in assembly
- Linker scripts


### Makefile
:::spoiler Makefile

```Makefile
include ../../mk/toolchain.mk

ARCH = -march=rv32izicsr
LINKER_SCRIPT = linker.ld

EMU ?= ../../build/rv32emu

AFLAGS = -g $(ARCH)
LDFLAGS = -T $(LINKER_SCRIPT)

ifdef FAST
	CFLAGS = -g -march=rv32i_zicsr -mabi=ilp32 -Ofast
else ifdef SIZE
	CFLAGS = -g -march=rv32i_zicsr -mabi=ilp32 -Os
else
	CFLAGS = -g -march=rv32i_zicsr -mabi=ilp32 -O0
endif

ifneq ($(DEFINES),)
CFLAGS += $(DEFINES)
endif


EXEC = test.elf

CC = $(CROSS_COMPILE)gcc
AS = $(CROSS_COMPILE)as
LD = $(CROSS_COMPILE)ld
OBJDUMP = $(CROSS_COMPILE)objdump

OBJS = start.o perfcounter.o main.o hanoi_asm.o

.PHONY: all run dump clean

all: $(EXEC)

$(EXEC): $(OBJS) $(LINKER_SCRIPT)
	$(LD) $(LDFLAGS) -o $@ $(OBJS)

%.o: %.S
	$(AS) $(AFLAGS) $< -o $@

%.o: %.c
	$(CC) $(CFLAGS) $< -o $@ -c

run: $(EXEC)
	@test -f $(EMU) || (echo "Error: $(EMU) not found" && exit 1)
	@grep -q "ENABLE_ELF_LOADER=1" ../../build/.config || (echo "Error: ENABLE_ELF_LOADER=1 not set" && exit 1)
	@grep -q "ENABLE_SYSTEM=1" ../../build/.config || (echo "Error: ENABLE_SYSTEM=1 not set" && exit 1)
	$(EMU) $<

dump: $(EXEC)
	$(OBJDUMP) -Ds $< | less

clean:
	rm -f $(EXEC) $(OBJS)

```
:::


This Makefile builds and runs a bare-metal `RV32I (Zicsr)` program on rv32emu, similar to the one used in the `uf8` project, and supports different optimization levels (`-Ofast`, `-Os`, or `-O0`) during compilation.

---

### main.c
:::spoiler c code

```c
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

static inline void write_str_syscall(int fd, const char *ptr, long len) {
    register long a0 asm("a0") = fd;
    register const char *a1 asm("a1") = ptr;
    register long a2 asm("a2") = len;
    register long a7 asm("a7") = 64;   // system_write

    asm volatile("ecall"
                 : "+r"(a0)              
                 : "r"(a1), "r"(a2), "r"(a7)
                 : "memory", "cc");      
}

#define printstr(ptr, length) write_str_syscall(1, (ptr), (length))

#define TEST_OUTPUT(msg, length) printstr((msg), (length))

#define TEST_LOGGER(msg)                          \
do {                                              \
    static const char _s_[] = msg;                \
    TEST_OUTPUT(_s_, (long)(sizeof(_s_) - 1));    \
} while (0)

extern uint64_t get_cycles(void);
extern uint64_t get_instret(void);


/* Software division for RV32I (no M extension) */
static unsigned long udiv(unsigned long dividend, unsigned long divisor)
{
    if (divisor == 0)
        return 0;

    unsigned long quotient = 0;
    unsigned long remainder = 0;

    for (int i = 31; i >= 0; i--) {
        remainder <<= 1;
        remainder |= (dividend >> i) & 1;

        if (remainder >= divisor) {
            remainder -= divisor;
            quotient |= (1UL << i);
        }
    }

    return quotient;
}

static unsigned long umod(unsigned long dividend, unsigned long divisor)
{
    if (divisor == 0)
        return 0;

    unsigned long remainder = 0;

    for (int i = 31; i >= 0; i--) {
        remainder <<= 1;
        remainder |= (dividend >> i) & 1;

        if (remainder >= divisor) {
            remainder -= divisor;
        }
    }

    return remainder;
}

/* Software multiplication for RV32I (no M extension) */
static uint32_t umul(uint32_t a, uint32_t b)
{
    uint32_t result = 0;
    while (b) {
        if (b & 1)
            result += a;
        a <<= 1;
        b >>= 1;
    }
    return result;
}

/* Provide __mulsi3 for GCC */
uint32_t __mulsi3(uint32_t a, uint32_t b)
{
    return umul(a, b);
}

/* Simple integer to hex string conversion */
static void print_hex(unsigned long val)
{
    char buf[20];
    char *p = buf + sizeof(buf) - 1;
    *p = '\n';
    p--;

    if (val == 0) {
        *p = '0';
        p--;
    } else {
        while (val > 0) {
            int digit = val & 0xf;
            *p = (digit < 10) ? ('0' + digit) : ('a' + digit - 10);
            p--;
            val >>= 4;
        }
    }

    p++;
    printstr(p, (buf + sizeof(buf) - p));
}

/* Simple integer to decimal string conversion */
void print_dec(unsigned long val)
{
    char buf[20];
    char *p = buf + sizeof(buf) - 1;
    // *p = '\n';
    // p--;

    if (val == 0) {
        *p = '0';
        p--;
    } else {
        while (val > 0) {
            *p = '0' + umod(val, 10);
            p--;
            val = udiv(val, 10);
        }
    }

    p++;
    printstr(p, (buf + sizeof(buf) - p));
}


void print_char(char c)
{
    char buf[1];
    buf[0] = c;

    printstr(buf, 1);
}

/* ============= Declaration ============= */

extern uint32_t hanoi_asm();

/* ============= Hanoi  ============= */

typedef struct {
    int v[3];   
    int top;
} Stack;

static void push(Stack *s, int x) { s->v[++s->top] = x; }
static int  pop (Stack *s)        { return s->v[s->top--]; }
static int  peek(const Stack *s)  { return (s->top >= 0) ? s->v[s->top] : 0; } 


static void move_between(Stack *a, Stack *b, char la, char lb) {
    int ta = peek(a), tb = peek(b);
    int x;
    const char A = 'A';
    const char B = 'B';

    if (ta == 0) {                      
        x = pop(b); push(a, x);
    } else if (tb == 0) {              
        x = pop(a); push(b, x);
    } else if (ta < tb) {               
        x = pop(a); push(b, x);
    } else {                           
        x = pop(b); push(a, x);
    }
        TEST_LOGGER("Move Disk ");
        print_dec(x);
        TEST_LOGGER(" from ");
        print_char(la);
        TEST_LOGGER(" to ");
        print_char(lb);
        TEST_LOGGER("\n");
}

void hanoi_iter(int n) {
    Stack A = {.top = -1}, B = {.top = -1}, C = {.top = -1};
    for (int d = n; d >= 1; --d) push(&A, d);

    const int total = (1 << n) - 1;                
    int r = 0;                        

    for (int step = 0; step < total; ++step) {
        if (r == 0)      move_between(&A, &C, 'A', 'C');
        else if (r == 1) move_between(&A, &B, 'A', 'B');
        else             move_between(&B, &C, 'B', 'C');

        ++r;
        if (r == 3) r = 0;
    }
}


int main(void)
{
    uint64_t start_cycles, end_cycles, cycles_elapsed;
    uint64_t start_instret, end_instret, instret_elapsed;

    TEST_LOGGER("\n=== hanoi Tests Start ===\n\n");

    TEST_LOGGER("\n=== Assembly Tests ===\n\n");
    start_cycles = get_cycles();
    start_instret = get_instret();

    hanoi_asm();

    end_cycles = get_cycles();
    end_instret = get_instret();
    cycles_elapsed = end_cycles - start_cycles;
    instret_elapsed = end_instret - start_instret;

    TEST_LOGGER("\n");
    TEST_LOGGER("Cycles: ");
    print_dec((unsigned long) cycles_elapsed);
    TEST_LOGGER("\n");
    TEST_LOGGER("Instructions: ");
    print_dec((unsigned long) instret_elapsed);
    TEST_LOGGER("\n");


    TEST_LOGGER("\n=== C Tests ===\n\n");

    start_cycles = get_cycles();
    start_instret = get_instret();

    hanoi_iter(3);

    end_cycles = get_cycles();
    end_instret = get_instret();
    cycles_elapsed = end_cycles - start_cycles;
    instret_elapsed = end_instret - start_instret;

    TEST_LOGGER("\n");
    TEST_LOGGER("Cycles: ");
    print_dec((unsigned long) cycles_elapsed);
    TEST_LOGGER("\n");
    TEST_LOGGER("Instructions: ");
    print_dec((unsigned long) instret_elapsed);
    TEST_LOGGER("\n");

    return 0;
}

```
:::


This program compares two Tower of Hanoi implementations — one in assembly (`hanoi_asm`) and one in C (`hanoi_iter`). 

The arithmetic and I/O parts in this program are identical to those in the previous main.c used for the uf8 project. They include the same software implementations of `multiplication`, `division`, and `modulo` operations , as well as the same system call–based `print functions`.

---

### hanoi in RISC-V assembly

:::spoiler Assembly

```assembly
.data
str1:       .string  "Move Disk "
str2:       .string  " from "
str3:       .string  " to "
strA:       .string  "A"
strB:       .string  "B"
strC:       .string  "C"
str:        .string  "\n"


.text

.globl  hanoi_asm
.type   hanoi_asm, @function

hanoi_asm:
    addi    x2, x2, -36
    sw      x8, 0(x2)
    sw      x9, 4(x2)
    sw      x18, 8(x2)
    sw      x19, 12(x2)
    sw      x20, 16(x2)
    sw      ra, 32(x2)   

    li      x5, 0x15
    sw      x5, 20(x2)
    sw      x5, 24(x2)
    sw      x5, 28(x2)

    # Fix disk positions (BLANK 1-3: neutralize x5 effect)
    # BLANK 1: Fix position at x2+20
    sw      x0, 20(x2)

    # BLANK 2: Fix position at x2+24
    sw      x0, 24(x2)

    # BLANK 3: Fix position at x2+28
    sw      x0, 28(x2)

    addi    x8, x0, 1
game_loop:
    # BLANK 4: Check loop termination (2^3 moves)
    addi    x5, x0, 8
    beq     x8, x5, finish_game

    # Gray code formula: gray(n) = n XOR (n >> k)
    # BLANK 5: What is k for Gray code?
    srli    x5, x8, 1

    # BLANK 6: Complete Gray(n) calculation
    xor     x6, x8, x5

    # BLANK 7-8: Calculate previous value and its shift
    addi    x7, x8, -1
    srli    x28, x7, 1

    # BLANK 9: Generate Gray(n-1)
    xor     x7, x7, x28

    # BLANK 10: Which bits changed?
    xor     x5, x6, x7

    # Initialize disk number
    addi    x9, x0, 0

    # BLANK 11: Mask for testing LSB
    andi    x6, x5, 1

    # BLANK 12: Branch if disk 0 moves
    bne     x6, x0, disk_found

    # BLANK 13: Set disk 1
    addi    x9, x0, 1

    # BLANK 14: Test second bit with proper mask
    andi    x6, x5, 2
    bne     x6, x0, disk_found

    # BLANK 15: Last disk number
    addi    x9, x0, 2

disk_found:
    # BLANK 16: Check impossible pattern (multiple bits)
    andi    x30, x5, 5
    addi    x31, x0, 5
    beq     x30, x31, pattern_match
    jal     x0, continue_move

pattern_match:
continue_move:

    # BLANK 17: Word-align disk index (multiply by what?)
    slli    x5, x9, 2

    # BLANK 18: Base offset for disk array
    addi    x5, x5, 20
    add     x5, x2, x5
    lw      x18, 0(x5)

    bne     x9, x0, handle_large

    # BLANK 19: Small disk moves by how many positions?
    addi    x19, x18, 2

    # BLANK 20: Number of pegs
    addi    x6, x0, 3
    blt     x19, x6, display_move
    sub     x19, x19, x6
    jal     x0, display_move

handle_large:
    # BLANK 21: Load reference disk position
    lw      x6, 20(x2)

    # BLANK 22: Sum of all peg indices (0+1+2)
    addi    x19, x0, 3
    sub     x19, x19, x18
    sub     x19, x19, x6

display_move:

    # print "Move Disk "
    la   a0, str1
    call puts

    # # print disk number (1..3) as one char
    addi a0, x9, 1
    call print_dec

    # print " from "
    la   a0, str2
    call puts

    addi t0, x18, 65
    mv   a0, t0
    call print_char



print_to:
    # print " to "
    la   a0, str3
    call puts

    addi t0, x19, 65
    mv   a0, t0
    call print_char


next_move:

    la   a0, str
    call puts

    slli    x5, x9, 2
    addi    x5, x5, 20
    add     x5, x2, x5

    sw      x19, 0(x5)

    addi    x8, x8, 1
    jal     x0, game_loop

finish_game:
    lw      x8, 0(x2)
    lw      x9, 4(x2)
    lw      x18, 8(x2)
    lw      x19, 12(x2)
    lw      x20, 16(x2)
    lw      x1,  32(x2)
    addi    x2, x2, 36

    ret    

# print
puts:
    add   t0, a0, x0       
    add   t1, a0, x0       
1:  lbu   t2, 0(t1)
    beq   t2, x0, 2f
    addi  t1, t1, 1
    j     1b
2:  sub   a2, t1, t0      
    mv    a1, t0           
    li    a0, 1            
    li    a7, 64           
    ecall
    ret
```

:::



- Added `puts` subroutine : 
    - The `puts` function scans a null-terminated string to calculate its length, then performs the `ecall` to print it.

- Call `print_char` in main : 
    - Before calling, the ASCII code of the character to be printed is placed in register `a0`and then Prints a single character.
    - This prints the peg name `A`, `B`, or `C`.
    
- Call `print_dec` in main : 
    - Prints a non-negative integer in decimal form that is placed in `a0`.


---

### Building the Test Suite

```
make
```

Build artifacts:
- `main.o` - Test implementation
- `perfcounter.o` - CSR 
- `start.o` - startup code with BSS initialization
- `hanoi_asm.o` - hanoi implementation
- `test.elf` - linked executable

### Running test

1. The compilation options use the default `-O0` setting.
    ```
    make run
    ```


    Output:
    ```
    === hanoi Tests Start ===


    === Assembly Tests ===

    Move Disk 1 from A to C
    Move Disk 2 from A to B
    Move Disk 1 from C to B
    Move Disk 3 from A to C
    Move Disk 1 from B to A
    Move Disk 2 from B to C
    Move Disk 1 from A to C

    Cycles: 10552
    Instructions: 10552

    === C Tests ===

    Move Disk 1 from A to C
    Move Disk 2 from A to B
    Move Disk 1 from B to C
    Move Disk 3 from A to C
    Move Disk 1 from A to B
    Move Disk 2 from B to C
    Move Disk 1 from A to C

    Cycles: 11101
    Instructions: 11101
    ```
2. The compilation options use `-Ofast`
    ```
    make clean
    make FAST=1
    ```



    Output:
    ```
    === hanoi Tests Start ===


    === Assembly Tests ===

    Move Disk 1 from A to C
    Move Disk 2 from A to B
    Move Disk 1 from C to B
    Move Disk 3 from A to C
    Move Disk 1 from B to A
    Move Disk 2 from B to C
    Move Disk 1 from A to C

    Cycles: 4922
    Instructions: 4922

    === C Tests ===

    Move Disk 1 from A to C
    Move Disk 2 from A to B
    Move Disk 1 from B to C
    Move Disk 3 from A to C
    Move Disk 1 from A to B
    Move Disk 2 from B to C
    Move Disk 1 from A to C

    Cycles: 4032
    Instructions: 4032
    ```
3. The compilation options use `-Os`
    ```
    make clean
    make SIZE=1
    ```



    Output:
    ```
    === hanoi Tests Start ===


    === Assembly Tests ===

    Move Disk 1 from A to C
    Move Disk 2 from A to B
    Move Disk 1 from C to B
    Move Disk 3 from A to C
    Move Disk 1 from B to A
    Move Disk 2 from B to C
    Move Disk 1 from A to C

    Cycles: 4852
    Instructions: 4852

    === C Tests ===

    Move Disk 1 from A to C
    Move Disk 2 from A to B
    Move Disk 1 from B to C
    Move Disk 3 from A to C
    Move Disk 1 from A to B
    Move Disk 2 from B to C
    Move Disk 1 from A to C

    Cycles: 4339
    Instructions: 4339
    ```
---

Comparison of three compilation options for the Assembly and C versions of the Hanoi :


| Compilation Option | Assembly (cycles) | vs`-O0` | C (cycles) | vs`-O0`| Notes |
|:---:|:---:|:---:|:---:|:---:|---|
| `-O0` | **10,552** | — | **11,101** | — | Baseline setting |
| `-Ofast` | **4,922** | **−53%** | **4,032** | **−64%** | Aggressive speed optimization; biggest gain for the C version |
| `-Os` | **4,852** | **−54%** | **4,339** | **−61%** | Size-oriented optimization; asm slightly faster than `-Ofast` |

From the comparison above, both the Assembly and C implementations of the Hanoi program show significant performance improvements after optimization.



---

# [rsqrt](https://hackmd.io/@sysprog/arch2025-quiz3-sol)

The `rsqrt` module implements a fast reciprocal square root optimized for RV32I processors. It combines a `LUT` for initial approximation and Newton-Raphson iterations for refinement, all using fixed-point arithmetic and shift-add operations.

This approach can achieve around 3-8% precision error while maintaing high performance and low hardware cost.

You can see more detail description of this problem [here](https://hackmd.io/@sysprog/arch2025-quiz3-sol).

---

### Implement rsqrt in C

1. clz
    ```clike
    static int clz(uint32_t x){
        if(!x) return 32;
        int n = 0;
        if((x & 0xFFFF0000) == 0) { n += 16; x <<= 16; }
        if((x & 0xFF000000) == 0) { n += 8; x <<= 8; }
        if((x & 0xF0000000) == 0) { n += 4; x <<= 4; }
        if((x & 0xC0000000) == 0) { n += 2; x <<= 2; }
        if((x & 0x80000000) == 0) { n += 1; }
        return n;
    }
    ```
    Determines the most significant bit position of the input `x`.
    This identifies the approximate magnitude of `x` and selects an initial estimate from the `LUT`.
    
2. Initial Estimate using LUT
    ```clike
    static const uint32_t rsqrt_table[32] = {
        65536, 46341, 32768, 23170, 16384,
        11585, 8192, 5793, 4096, 2896,
        2048, 1448, 1024, 724, 512,
        362, 256, 181, 128, 90,
        64, 45, 32, 23, 16,
        11, 8, 6, 4, 3,
        2, 1
    };
    ```
    The `rsqrt_table[32]` stores precomputed fixed-point values representing
    $$ \frac{65536}{\sqrt{2^{exp}}} $$
    corresponding to each possible MSB position (`exp`).
    
3. Linear Interpolation
    ```clike
    if (x > (1u << exp)) {
    uint32_t y_next = (exp < 31) ? rsqrt_table[exp + 1] : 0;
    uint32_t delta  = y - y_next;

    // numer = ((uint32_t)(x - (1u<<exp)) << 16) 
    uint32_t t      = x - (1u << exp);
    uint32_t numer_hi = t >> 16;   // high 32 
    uint32_t numer_lo = t << 16;   // low  32 

    uint32_t frac = shr64_lo32(numer_hi, numer_lo, exp);
    uint32_t p_hi, p_lo;
    mul32_lohi(delta, frac, &p_hi, &p_lo);
    uint32_t prod_shr16 = shr64_lo32(p_hi, p_lo, 16);

    y = y - prod_shr16;
    }
    ```
    For input between power of two $(2^{exp} < x < 2^{exp+1})$ ,the code linearly interpolates between adjacent table values (`y` and `y_next`) to refine the initial estimate:
    $$ y = y_{base} - (y_{base} - y_{next})*fraction $$

4. Newton-Raphson Iteration
    ```clike
    for(int iter = 0; iter < 2; iter++) {
    uint64_t y_sq = mul32(y, y);
    uint32_t y2 = (uint32_t)(y_sq >> 16);
    uint64_t xy2 = (uint64_t)mul32(x, y2);
    uint64_t factor_num = (3u << 16);
    factor_num = (xy2 >= factor_num) ? 0 : (factor_num - xy2);
    uint32_t factor = (uint32_t)(factor_num >> 1);
    y = (uint32_t)((mul32(y, factor)) >> 16);
    }
    ```
    Applies two fixed-point refinement steps using the formula:
    $$y_{n+1} = y_n \times \frac{3 - x⋅y_n^2/2^{16}}{2} $$
    implemented entirely with 32-bits arithemetic.
    
    In this implementation, I decided to perform `two` iteration, which provides a good balance between accuracy and computational cost.
    
    And finally do the fixed-point scaling because values are scaled by $2^{16}$ , shifts (>>16) compensate for scalling after each multiplication.
    
---

### Analyzing precision and performance

:::spoiler Test code

```c
void test_rsqrt(void)
{
    const uint32_t test_vals[] = {
        1, 2, 3, 4, 5, 8, 10, 16, 31, 32,
        64, 100, 255, 256, 512, 1000, 4096,
        65536, 100000
    };

    const uint32_t ref_vals[] = {
        65536, 46341, 37837, 32768, 29309, 23170, 20724, 16384, 11771, 11585,
        8192, 6554, 4104, 4096, 2896, 2072, 1024,
        256, 207
    };

    const int N = sizeof(test_vals)/sizeof(test_vals[0]);

    uint64_t total_abs_err = 0;
    uint64_t total_rel_bp  = 0;  
    uint32_t worst_x_abs = 0, worst_abs = 0;
    uint32_t worst_rel_bp = 0;

    TEST_LOGGER("\n--- rsqrt Test (integer) ---\n");

    for (int i = 0; i < N; i++) {
        uint32_t x    = test_vals[i];
        uint32_t y    = rsqrt(x);                 
        uint32_t ref  = ref_vals[i];    

        uint32_t abs_err = (y >= ref) ? (y - ref) : (ref - y);


        uint32_t rel_bp = udiv(abs_err * 10000u + (ref >> 1), ref);

        total_abs_err += abs_err;
        total_rel_bp  += rel_bp;
        if (abs_err > worst_abs)   { worst_abs = abs_err; worst_x_abs = x; }
        if (rel_bp  > worst_rel_bp){ worst_rel_bp = rel_bp; }

        TEST_LOGGER("x=");
        print_dec(x);
        TEST_LOGGER(" rsqrt=");
        print_dec(y);
        TEST_LOGGER(" ref=");
        print_dec(ref);
        TEST_LOGGER(" abs=");
        print_dec(abs_err);
        TEST_LOGGER(" rel=");
        print_dec(udiv(rel_bp , 100));
        TEST_LOGGER(".");
        print_dec(umod(rel_bp , 100));
        TEST_LOGGER("\n");
    }

    uint32_t avg_abs = (uint32_t)(udiv(total_abs_err , N));
    uint32_t avg_bp  = (uint32_t)(udiv(total_rel_bp  , N));

    TEST_LOGGER("\n--- Summary ---\n");
    TEST_LOGGER("Avg abs err: ");
    print_dec(avg_abs);
    TEST_LOGGER("\nAvg rel err: ");
    print_dec(udiv(avg_bp , 100));
    TEST_LOGGER(".");
    print_dec(umod(avg_bp , 100));
    TEST_LOGGER("\n");

    TEST_LOGGER("Worst abs err: ");
    print_dec(worst_abs);
    TEST_LOGGER(" (x=");
    print_dec(worst_x_abs);
    TEST_LOGGER(")\n");

    TEST_LOGGER("Worst rel err: ");
    print_dec(udiv(worst_rel_bp , 100));
    TEST_LOGGER(".");
    print_dec(umod(worst_rel_bp , 100));
    TEST_LOGGER("\n");

}
```
:::


To verify the correctness and precision of the `rsqrt()` function, I implment a dedicated test routine `test_rsqrt()`.

The function compares the computed reciprocal square root against reference fixed-point values for a set of representative input integers.


---


#### Test set 

A list of 19 integer inputs (`test_vals[]`) was selected, ranging from small values(1-10) to larger ones (up to 100,000).

Each input has a reference result(`ref_vals[]`) corresponding to the ideal fixed-point value of $\frac{1}{\sqrt{x}}\times2^{16}$.

---

#### Test loop

For each input `x`
- The function computes `y = rsqrt(x)` using the implemented algorithm.
- The absolute error is calculated as
$$ {abs\_err} = | y - ref | $$
- The relative error is calculated as
$$rel\_bp = \frac{|y-ref|}{ref}\times10000 $$

---

#### Error Accumulation

- `total_abs_error` and `total_rel_bp` accumlate errors over all test cases.
- The test also tracks both the average and worst-case error.

---

#### Test result

```
=== rsqrt Test Start ===

--- rsqrt Test (integer) ---
x=1 rsqrt=65536 ref=65536 abs=0 rel=0.0
x=2 rsqrt=46341 ref=46341 abs=0 rel=0.0
x=3 rsqrt=37837 ref=37837 abs=0 rel=0.0
x=4 rsqrt=32768 ref=32768 abs=0 rel=0.0
x=5 rsqrt=29309 ref=29309 abs=0 rel=0.0
x=8 rsqrt=23171 ref=23170 abs=1 rel=0.0
x=10 rsqrt=20724 ref=20724 abs=0 rel=0.0
x=16 rsqrt=16384 ref=16384 abs=0 rel=0.0
x=31 rsqrt=11771 ref=11771 abs=0 rel=0.0
x=32 rsqrt=11587 ref=11585 abs=2 rel=0.2
x=64 rsqrt=8192 ref=8192 abs=0 rel=0.0
x=100 rsqrt=6557 ref=6554 abs=3 rel=0.5
x=255 rsqrt=4110 ref=4104 abs=6 rel=0.15
x=256 rsqrt=4096 ref=4096 abs=0 rel=0.0
x=512 rsqrt=2907 ref=2896 abs=11 rel=0.38
x=1000 rsqrt=2072 ref=2072 abs=0 rel=0.0
x=4096 rsqrt=1024 ref=1024 abs=0 rel=0.0
x=65536 rsqrt=256 ref=256 abs=0 rel=0.0
x=100000 rsqrt=239 ref=207 abs=32 rel=15.46

--- Summary ---
Avg abs err: 2
Avg rel err: 0.84
Worst abs err: 32 (x=100000)
Worst rel err: 15.46

Cycles: 422239
Instret: 422239
```

The test result show that the implemented `rsqrt()` function performs accurately for most inputs, with only minor deviations for large values.

The **average absolute error** is 2, and the **average relative error** is only **0.84%**, confirming excellent overall precision.

For conclusion, the fixed-point `rsqrt()` achieves a strong balance between speed and precision, making it -well-suited for application without floating-point hardware.

---

# [Fibonacci sequence](https://en.wikipedia.org/wiki/Fibonacci_sequence)

To evaluate performance, I tested three implementations of the Fibonacci function: the recursive C version, the iterative C version, and my handwritten RISC-V assembly version.


### Recursive Version `fib(n)`

Based directly on the mathematical definition :
$$ F(n) = F(n-1) + F(n-2),\ F(0) = 0, F(1) =1 $$
The function call itself twice for each `n > 1` and sums the result.


```c
static uint64_t fib(uint64_t n)
{
    if (n <= 1)
        return n;
    return fib(n - 1) + fib(n - 2);
}
```

---

### Iterative Version `fib_iter_c(n)`

Computes Fibonacci numbers `bottom-up`, keeping only the last two values in memory.
```clike
volatile uint64_t fib_iter_c(uint64_t n) {
    if (n <= 1) return n;
    volatile uint64_t f0 = 0, f1 = 1;

    for (uint64_t i = 1; i < n; ++i) {
        volatile uint64_t f2 = f0 + f1;
        f0 = f1;
        f1 = f2;
    }
    return f1;
}
```
1. Initialize `f0 = 0`, `f1 = 1`
2. For each iteration, compute `f2 = f0 + f1`, then shift `f0 = f1`, `f1 = f2`.
3. After the loop, `f1` holds $F(n)$.

---

### Handwritten assembly

I implement the iterative version directly in RISC-V assembly.

:::spoiler Assembly

```
.globl  fib_iter
.type   fib_iter, @function

.text

fib_iter:
    # if (n == 0) return 0;
    beqz    a0, .Lret0

    # if (n == 1) return 1;
    addi    t0, a0, -1
    beqz    t0, .Lret1

    li      t1, 0          # f0
    li      t2, 1          # f1
.Lloop:
    add     t3, t1, t2     # f2 = f0 + f1
    mv      t1, t2         # f0 = f1
    mv      t2, t3         # f1 = f2
    addi    t0, t0, -1
    bnez    t0, .Lloop

    mv      a0, t2
    li      a1, 0
    ret

.Lret0:
    li      a0, 0
    li      a1, 0
    ret
.Lret1:
    li      a0, 1
    li      a1, 0
    ret
```
:::

---

### Test result

To compare efficiency, all version computed `fib(19)` under identical condition.

I used `getticks()` function to measure the cycle count.

```c
Recursion (C): 
elapsed cycle: 133630

Iteration (C): 
elapsed cycle: 462

Assembly: 
elapsed cycle: 108
```

- The recursive version is the least efficient, as each call creates two new recursive branches, leading to exponential growth in execution time.

- The iterative C version significantly reduces complexity to linear time by reusing results in each loop iteration.

- The assembly version achieves the best performance because it removes compiler overhead (stack frame setup, variable access) and directly manipulates registers, minimizing instruction count and memory access.


The result clearly demonstrate that:

    Assembly > Iterative C > Recursive C 
in both speed and instruction efficiency.