This repository contains all task outputs for 34 weeks of RISC-V programming practice.
π Click on a week below to view its tasks.
Sure! Hereβs the full complete markdown block including all the C and bash code parts fully wrapped with proper syntax, ready to paste directly:
π§ Task 1: Toolchain Setup & Sanity Check
Steps Performed:
- Extracted the toolchain using:
tar -xzf riscv-toolchain-rv32imac-x86_64-ubuntu.tar.gz Added following to ~/.bashrc:
export PATH=$HOME/riscv/bin:$PATH
Verified installation:
riscv32-unknown-elf-gcc --version
riscv32-unknown-elf-gdb --version
riscv32-unknown-elf-objdump --version
π Task 2: Compile βHello, RISC-Vβ
β Simple C code:
#include <stdio.h>
int main() {
printf("Hello, RISC-V!\n");
return 0;
}β Compiled with:
riscv32-unknown-elf-gcc -march=rv32imc -mabi=ilp32 -o hello.elf hello.cβ Confirmed with:
file hello.elfπ Task 3: Generate .s File & Analyze Prologue
Command Used:
riscv32-unknown-elf-gcc -S -O0 hello.c -o hello.s
Understanding Prologue & Epilogue
πΈ Prologue (function entry):
addi sp,sp,-16 # Reserve 16 bytes on the stack
sw ra,12(sp) # Save return address (ra) at offset 12β‘οΈ This sets up the stack frame and saves the return address so the function can safely return later. πΈ Epilogue (function exit):
lw ra,12(sp) # Restore return address
addi sp,sp,16 # Restore the stack pointer
ret # Return to the callerβ‘οΈ This restores the state before the function was called and jumps back using the saved return address.
π Task 4: Hex Dump & Disassembly
Commands Used:
riscv32-unknown-elf-objdump -d hello.elf > hello.dump
riscv32-unknown-elf-objcopy -O ihex hello.elf hello.hexYou can inspect it with:
cat hello.dumpriscv32-unknown-elf-objcopy -O ihex hello.elf hello.hexYou can view it with:
cat hello.hexπ§Ύ Task 5: ABI & Register Cheat-Sheet
| Register | ABI Name | Usage |
|---|---|---|
| x0 | zero | Constant zero |
| x1 | ra | Return address |
| x2 | sp | Stack pointer |
| x3 | gp | Global pointer |
| x4 | tp | Thread pointer |
| x5βx7 | t0βt2 | Temporaries |
| x8βx9 | s0βs1 | Saved registers |
| x10βx17 | a0βa7 | Function args/ret |
| x18βx27 | s2βs11 | Saved registers |
| x28βx31 | t3βt6 | Temporaries |
Calling convention:
a0βa7β Function arguments and return valuess0βs11β Callee-saved (preserved across function calls)t0βt6β Caller-saved (can be overwritten by callees)
π Task 6: GDB Debugging
which riscv32-unknown-elf-gdbriscv32-unknown-elf-gdb --versionfile hello.elfriscv32-unknown-elf-objdump -h hello.elf
riscv32-unknown-elf-readelf -l hello.elfGDB Session
riscv32-unknown-elf-gdb hello.elf
Disassemble main:
(gdb) disassemble main
(gdb) info symbol 0x10170
(gdb) x/10i 0x10162
(gdb) info symbol 0x100e2
(gdb) x/5i 0x100e2
(gdb) x/s 0x1245c
(gdb) x/1xw 0x10162
(gdb) x/1xw 0x10170π₯οΈ Task 7: Emulator Run using QEMU
Step 1: Install Required Packages (if not done)
Just in case:
sudo apt update
sudo apt install build-essential device-tree-compiler libglib2.0-dev libpixman-1-dev git \
libexpat-dev libgmp-dev libmpc-dev libmpfr-dev libz-dev python3 gawk bison flex texinfo \
libtool autoconf automakeStep 2: Clone and Build OpenSBI
cd ~/riscv-projects/week1
git clone https://github.com/riscv-software-src/opensbi.git
cd opensbiBuild for 32-bit:
make PLATFORM=generic CROSS_COMPILE=riscv32-unknown-elf-Check if hello.elf exists In your terminal:
find ~/riscv-projects/ -name hello.elfIf this shows a path like:
/home/harshini123/riscv-projects/week1/hello.elfRun QEMU like this (replace the path with yours):
qemu-system-riscv32 -nographic \
-machine virt \
-bios ~/riscv-projects/week1/opensbi/build/platform/generic/firmware/fw_dynamic.elf \
-kernel ~/riscv-projects/week1/hello.elfHello c program
// hello.c
volatile char *uart = (volatile char *)0x10000000;
void _start() {
const char *str = "Hello, RISC-V!\n";
while (*str) *uart = *str++;
while (1); // hang
}Linker code
MEMORY
{
ROM (rx) : ORIGIN = 0x80200000, LENGTH = 512K
RAM (rw) : ORIGIN = 0x84000000, LENGTH = 128K
}
SECTIONS
{
. = ORIGIN(ROM);
.text : {
*(.text*)
} > ROM
.rodata : {
*(.rodata*)
} > ROM
.data : {
*(.data*)
} > RAM
.bss : {
*(.bss*)
*(COMMON)
} > RAM
}Compile using
riscv32-unknown-elf-gcc -T linker.ld -nostartfiles -o hello.elf hello.cπ Task 8: GCC Optimization (-O0 vs -O2)
Commands Used:
riscv32-unknown-elf-gcc -S -O0 hello.c -o hello_O0.s
riscv32-unknown-elf-gcc -S -O2 hello.c -o hello_O2.sComparison:
-O0 (no optimization): includes full function call overhead, redundant instructions.
-O2 (optimized): inlines functions, removes dead code, reuses registers efficiently.
βοΈ Task 9: Inline Assembly β Reading Cycle Counter
C Code with Inline Assembly:
#define UART0 0x10000000
#define uart_tx (*((volatile char *)UART0))
#include <stdint.h>
void uart_putchar(char c) {
uart_tx = c;
}
void uart_puts(const char *s) {
while (*s) {
uart_putchar(*s++);
}
}
void uart_putnum(uint32_t num) {
char buf[10];
int i = 0;
if (num == 0) {
uart_putchar('0');
return;
}
while (num > 0 && i < 10) {
buf[i++] = '0' + (num % 10);
num /= 10;
}
while (i--) {
uart_putchar(buf[i]);
}
}
static inline uint32_t add_inline(uint32_t a, uint32_t b) {
uint32_t result;
asm volatile ("add %0, %1, %2" : "=r"(result) : "r"(a), "r"(b));
return result;
}
static inline uint32_t demo_volatile(uint32_t input) {
uint32_t output;
asm volatile ("slli %0, %1, 1" : "=r"(output) : "r"(input));
return output;
}
void _start() {
uart_puts("=== Inline Assembly: No CSRs ===\n");
uart_puts("15 + 25 = ");
uart_putnum(add_inline(15, 25));
uart_putchar('\n');
uart_puts("5 << 1 = ");
uart_putnum(demo_volatile(5));
uart_putchar('\n');
while (1) {}
}Compile
riscv32-unknown-elf-gcc -nostdlib -march=rv32imc -mabi=ilp32 -Wl,-e,_start -o inline_assembly_nocsr.elf inline_assembly.cGenerate assembly file
riscv32-unknown-elf-gcc -S inline_assembly.cView inline assembly in generated code
echo "=== Generated Assembly with Inline Code ==="
grep -A 5 -B 5 -E "(add|slli|mv)" inline_assembly.sComplete verification
echo "=== Task 9: Inline Assembly Implementation ==="
echo -e "\n1. Source code created:"
ls -la inline_assembly.c
echo -e "\n2. Compilation:"
riscv32-unknown-elf-gcc -nostdlib -nostartfiles -nodefaultlibs -march=rv32imc -mabi=ilp32 -Wl,-e,_start -o inline_assembly.elf inline_assembly.c \
&& echo "β Compiled!" \
|| echo "β Compilation failed"
echo -e "\n3. Assembly generation:"
riscv32-unknown-elf-gcc -S inline_assembly.c && echo "β Assembly generated!" || echo "β Failed to generate assembly"
echo -e "\n4. Inline assembly found in generated code:"
grep -A 2 -B 2 -E "add|slli|mv" inline_assembly.s | head -10file inline_assembly.elfπ Task 10: Memory-Mapped I/O using Volatile vs Non-Volatile
Demonstrate the importance of using volatile for memory-mapped I/O in RISC-V bare-metal programming by comparing two versions:
- β
gpio_vol.cβ withvolatile - β
gpio_novol.cβ withoutvolatile
- Tells the compiler not to optimize memory accesses.
- Required for memory-mapped I/O since values can change outside the program's control (via hardware).
- Prevents removal or reordering of
*gpio = ...operations.
gpio_vol.c
#include <stdint.h>
#define UART0 0x10000000
#define GPIO_ADDR 0x10012000
#define uart_tx (*((volatile char *)UART0))
#define gpio_reg (*((volatile uint32_t *)GPIO_ADDR))
// Send a character to UART
void uart_putchar(char c) {
uart_tx = c;
}
// Send a string to UART
void uart_puts(const char *s) {
while (*s) {
uart_putchar(*s++);
}
}
// Convert number to decimal and print to UART
void uart_putnum(uint32_t num) {
char buf[10];
int i = 0;
if (num == 0) {
uart_putchar('0');
return;
}
while (num > 0) {
buf[i++] = '0' + (num % 10);
num /= 10;
}
while (i--) {
uart_putchar(buf[i]);
}
}
// Perform GPIO operations
void gpio_task10_demo() {
uart_puts("=== Task 10: GPIO Demo ===\n");
// Write 0x1 to GPIO (set pin high)
gpio_reg = 0x1;
uart_puts("GPIO written: 0x1\n");
// Read back and toggle
uint32_t current = gpio_reg;
gpio_reg = ~current;
uart_puts("GPIO toggled.\n");
// Set bit 0
gpio_reg |= (1 << 0);
uart_puts("Bit 0 set.\n");
// Clear bit 1
gpio_reg &= ~(1 << 1);
uart_puts("Bit 1 cleared.\n");
}
// Entry point (no main)
void _start() {
gpio_task10_demo();
while (1) {
// Infinite loop (bare-metal style)
}
}gpio_novol.c
#include <stdint.h>
#define UART0 0x10000000
#define GPIO_ADDR 0x10012000
#define uart_tx (*((volatile char *)UART0))
#define gpio_ptr ((uint32_t *)GPIO_ADDR) // β Not volatile on purpose
void uart_putchar(char c) {
uart_tx = c;
}
void uart_puts(const char *s) {
while (*s) {
uart_putchar(*s++);
}
}
// This function omits `volatile`, so the compiler may optimize away writes
void toggle_gpio_no_volatile(void) {
uart_puts("Writing to GPIO without volatile...\n");
*gpio_ptr = 0x1; // Set high
*gpio_ptr = 0x0; // Set low
*gpio_ptr = 0x1; // Set high again β may be optimized away
uart_puts("Done writing GPIO without volatile.\n");
}
// Bare-metal entry point
void _start() {
toggle_gpio_no_volatile();
while (1) {}
}Compilation
riscv32-unknown-elf-gcc -nostdlib -nostartfiles -nodefaultlibs \
-march=rv32imc -mabi=ilp32 -Wl,-e,_start -o gpio_vol.elf gpio_vol.c
riscv32-unknown-elf-gcc -nostdlib -nostartfiles -nodefaultlibs \
-march=rv32imc -mabi=ilp32 -Wl,-e,_start -o gpio_novol.elf gpio_novol.cAssembly Analysis (Optimized with -O2)
riscv32-unknown-elf-gcc -S -O2 gpio_vol.c -o gpio_vol.s
riscv32-unknown-elf-gcc -S -O2 gpio_novol.c -o gpio_novol.sWith volatile (gpio_vol.s) β memory operations preserved:
105: sw a3,0(a4)
115: lw a4,0(a3)
119: sw a4,0(a3)
128: lw a4,0(a3)
132: sw a4,0(a3)Without volatile (gpio_novol.s) β some writes optimized away:
54: sw a3,0(a4) # Only one memory write remains
70: sw ra,12(sp) # Function prologue, not GPIOβ
Memory instructions (sw, lw) are optimized out in the non-volatile version.

π¦ Task 11: Custom Linker Script and Bare-Metal Setup
Create and test a custom linker script for a bare-metal RISC-V RV32IMC program. Ensure correct placement of .text, .data, and .bss sections in memory.
| File | Purpose |
|---|---|
minimal.ld |
Custom linker script |
test_linker.c |
C file with variables in .data and .bss |
start.S |
Minimal _start assembly to call main() |
min_link.ld Highlights:
- Places
.textat 0x00000000 (Flash/ROM) - Places
.dataand.bssat 0x10000000 (SRAM) - Defines
_stack_topat top of SRAM
MEMORY {
FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 256K
SRAM (rwx) : ORIGIN = 0x10000000, LENGTH = 64K
}
SECTIONS {
.text : { *(.text.start) *(.text*) *(.rodata*) } > FLASH
.data : { _data_start = .; *(.data*) _data_end = .; } > SRAM
.bss : { _bss_start = .; *(.bss*) _bss_end = .; } > SRAM
_stack_top = ORIGIN(SRAM) + LENGTH(SRAM);
}test_link.c
uint32_t counter = 0x12345678; // Goes to .data
uint32_t status_flag; // Goes to .bss
void write_pattern(void) {
counter = 0xCAFEBABE;
status_flag = 0x0000DEAD;
}
void main(void) {
write_pattern();
while (1) {}
}start.S (Minimal Entry Point):
.section .text.start
.globl _start
_start:
la sp, _stack_top
call main
halt:
j haltCompile with Custom Linker Script
Compile assembly and C code with custom linker script
riscv32-unknown-elf-gcc -c start.S -o start.o
riscv32-unknown-elf-gcc -c test_link.c -o test_link.oLink with custom linker script
riscv32-unknown-elf-ld -T min_link.ld start.o test_link.o -o test_link.elfComplete working build script
#!/bin/bash
echo "=== Task 11: Linker Script Implementation ==="
# Step 1: Compile all sources
echo "1. Compiling with custom linker script..."
riscv32-unknown-elf-gcc -c start.S -o start.o
riscv32-unknown-elf-gcc -c test_link.c -o test_link.o
riscv32-unknown-elf-ld -T min_link.ld start.o test_link.o -o test_link.elf
if [ $? -ne 0 ]; then
echo "β Compilation failed!"
exit 1
else
echo "β Compilation successful!"
fi
# Step 2: Verify memory layout
echo -e "\n2. Verifying memory layout:"
echo "Text section should be at 0x00000000:"
riscv32-unknown-elf-objdump -h test_link.elf | grep ".text"
echo "Data section should be at 0x10000000:"
riscv32-unknown-elf-objdump -h test_link.elf | grep -E "\.data|\.sdata"
# Step 3: Display symbol table (for verification)
echo -e "\n3. Symbol addresses:"
riscv32-unknown-elf-nm test_link.elf | head -10
echo -e "\nβ Linker script test completed successfully!"chmod +x build_link_test.sh
./build_link_test.shπ§ Flash Memory (0x00000000 - 0x0003FFFF)
This section of memory is meant for storing the program's code permanently. It holds things like the instructions, constant values, and the entry point of the program (like _start). Flash memory is non-volatile, which means its contents stay even when the power is off. In my linker script, Iβve allocated 256KB for it. It's mostly read-only during execution. β‘ SRAM (0x10000000 - 0x1000FFFF)
SRAM is where the program keeps temporary data while it's running. It stores global variables, the BSS segment (uninitialized data), and also supports the heap and stack. Unlike Flash, SRAM is volatile β meaning it loses everything when power is cut. It allows both reading and writing, and it's very fast. Iβve allocated 64KB for SRAM in the linker script. π Flash vs SRAM β Why They're at Different Addresses
These memory regions are placed at different base addresses for several good reasons:
Architecture Design: Flash and SRAM often sit on separate memory buses, so their address ranges are distinct.
Performance: Flash is great for fetching instructions, while SRAM is better for handling data read/write operations quickly.
Power Saving: In low-power modes, Flash can be turned off while SRAM stays active to preserve temporary data.
Security: Flash is more secure since it can be made read-only during execution, while SRAM needs to be flexible for runtime changes.
π‘ Task 12: LED Blink Using Memory-Mapped GPIO
Create a bare-metal program that toggles an LED using memory-mapped I/O, controlled via GPIO register access. This exercise also uses a custom linker script and manual startup code (_start).
| File | Description |
|---|---|
task12_led_blink.c |
Blinks LED using GPIO register toggling |
led_start.s |
Assembly _start that sets up stack and calls main |
led_blink.ld |
Custom linker script for code/data placement |
GPIO_BASE:0x10012000- Output Register:
GPIO_BASE + 0x00 - Direction Register:
GPIO_BASE + 0x04 - Only GPIO pin 0 is used (toggled repeatedly)
task12_led_blink.c
#define GPIO_BASE 0x10012000
#define GPIO_OUT (*(volatile uint32_t *)(GPIO_BASE + 0x00))
#define GPIO_DIR (*(volatile uint32_t *)(GPIO_BASE + 0x04))
void delay(volatile uint32_t count) {
while (count--) {
__asm__ volatile ("nop");
}
}
void main(void) {
GPIO_DIR |= (1 << 0); // Set GPIO pin 0 as output
while (1) {
GPIO_OUT ^= (1 << 0); // Toggle pin 0
delay(100000);
}
}led_start.s
.section .text.start
.globl _start
_start:
lui sp, %hi(_stack_top)
addi sp, sp, %lo(_stack_top)
call main
hang:
j hangled_blink.ld
ENTRY(_start)
MEMORY {
FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 256K
SRAM (rwx) : ORIGIN = 0x10000000, LENGTH = 64K
}
SECTIONS {
.text : { *(.text.start) *(.text*) *(.rodata*) } > FLASH
.data : { _data_start = .; *(.data*) _data_end = .; } > SRAM
.bss : { _bss_start = .; *(.bss*) _bss_end = .; } > SRAM
_stack_top = ORIGIN(SRAM) + LENGTH(SRAM);
}Compile the LED blink program
riscv32-unknown-elf-gcc -c led_start.s -o led_start.o
riscv32-unknown-elf-gcc -c led_blink.c -o led_blink.oLink with custom linker script
riscv32-unknown-elf-ld -T led_blink_link.ld led_start.o led_blink.o -o led_blink.elfComplete build script
#!/bin/bash
echo "=== Task 12: LED Blink Implementation ==="
Compile everything
echo "1. Compiling LED blink program..."
riscv32-unknown-elf-gcc -c led_start.s -o led_start.o
riscv32-unknown-elf-gcc -c task12_led_blink.c -o task12_led_blink.o
riscv32-unknown-elf-ld -T led_blink.ld led_start.o task12_led_blink.o -o task12_led_blink.elf
echo "β Compilation successful!"
Verify results
echo -e "\n2. Verifying LED blink program:"
file task12_led_blink.elf
echo -e "\n3. Checking memory layout:"
riscv32-unknown-elf-objdump -h task12_led_blink.elf | grep -E "(text|data)"
echo -e "\n4. GPIO register usage in disassembly:"
riscv32-unknown-elf-objdump -d task12_led_blink.elf | grep -A 5 -B 5 "0x10012000"
echo -e "\nβ LED blink program ready!"chmod +x build_led_blink.sh
./build_led_blink.shLED Blink Algorithm:
Initialization: Set GPIO pin 0 as output using direction register
Main Loop: Infinite loop with LED toggle and delay
Toggle Operation: XOR output register bit 0 to alternate LED state
Timing Control: Delay function with configurable count for visible blinking
β±οΈ Task 13: Machine Timer Interrupt Using RISC-V CSRs
π File Overview
| File | Description |
|---|---|
timer_interrupt.c |
Sets up timer and defines C interrupt handler |
start_inter.S |
Assembly _start and trap redirection |
link.ld |
Custom linker script for .text, .data, etc. |
timer_inter.c
#include <stdint.h>
#define CLINT_BASE 0x02000000
#define MTIMECMP (*(volatile uint64_t *)(CLINT_BASE + 0x4000))
#define MTIME (*(volatile uint64_t *)(CLINT_BASE + 0xBFF8))
#define GPIO_BASE 0x10012000
#define GPIO_OUT (*(volatile uint32_t *)(GPIO_BASE + 0x00))
#define GPIO_DIR (*(volatile uint32_t *)(GPIO_BASE + 0x04))
#define MIE_MTIE (1 << 7)
#define MSTATUS_MIE (1 << 3)
static inline void write_csr(const char *csr, uint32_t value) {
if (csr == "mstatus") {
__asm__ volatile("csrw mstatus, %0" :: "r"(value));
} else if (csr == "mie") {
__asm__ volatile("csrw mie, %0" :: "r"(value));
}
}
void timer_init() {
uint64_t now = MTIME;
MTIMECMP = now + 500000; // Schedule next timer interrupt
write_csr("mie", MIE_MTIE); // Enable machine timer interrupt
write_csr("mstatus", MSTATUS_MIE); // Global interrupt enable
}
// Interrupt handler attribute
void __attribute__((interrupt)) machine_timer_handler(void) {
// Toggle GPIO pin 0
GPIO_OUT ^= (1 << 0);
// Schedule next interrupt
MTIMECMP = MTIME + 500000;
}
// Dummy main loop
void main(void) {
GPIO_DIR |= (1 << 0); // Make GPIO pin 0 output
timer_init();
while (1) {
// Wait for timer interrupt
}
}start_inter.s
.section .text.start
.global _start
_start:
# Set up stack pointer
lui sp, %hi(_stack_top)
addi sp, sp, %lo(_stack_top)
# Initialize trap vector
la t0, trap_handler
csrw mtvec, t0
# Call main program
call main
# Infinite loop (shouldn't reach here)
1: j 1b
# Simple trap handler (if needed)
trap_handler:
# Save context
addi sp, sp, -64
sw ra, 0(sp)
sw t0, 4(sp)
sw t1, 8(sp)
sw t2, 12(sp)
sw a0, 16(sp)
sw a1, 20(sp)
# Call C interrupt handler
call machine_timer_handler
# Restore context
lw ra, 0(sp)
lw t0, 4(sp)
lw t1, 8(sp)
lw t2, 12(sp)
lw a0, 16(sp)
lw a1, 20(sp)
addi sp, sp, 64
# Return from interrupt
mret
.size _start, . - _start
.size trap_handler, . - trap_handlerlink_inter.ld
/*
* Linker Script for Timer Interrupt - RV32IMC
* Places .text at 0x00000000 (Flash/ROM)
* Places .data at 0x10000000 (SRAM)
*/
ENTRY(_start)
MEMORY
{
FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 256K
SRAM (rwx) : ORIGIN = 0x10000000, LENGTH = 64K
}
SECTIONS
{
/* Text section in Flash at 0x00000000 */
.text 0x00000000 : {
*(.text.start) /* Entry point first */
*(.text*) /* All other text */
*(.rodata*) /* Read-only data */
} > FLASH
/* Data section in SRAM at 0x10000000 */
.data 0x10000000 : {
_data_start = .;
*(.data*) /* Initialized data */
_data_end = .;
} > SRAM
/* BSS section in SRAM */
.bss : {
_bss_start = .;
*(.bss*) /* Uninitialized data */
_bss_end = .;
} > SRAM
/* Stack at end of SRAM */
_stack_top = ORIGIN(SRAM) + LENGTH(SRAM);
}Compile Timer Interrupt Program Compile the timer interrupt program with CSR support
riscv32-unknown-elf-gcc -march=rv32imac_zicsr -c start_inter.s -o start_inter.o
riscv32-unknown-elf-gcc -march=rv32imac_zicsr -c timer_inter.c -o timer_inter.o
riscv32-unknown-elf-ld -T link_inter.ld start_inter.o timer_inter.o -o timer_inter.elfcomplete working build script build_timer_inter.sh
#!/bin/bash
echo "=== Task 13: Timer Interrupt Implementation ==="
# Compile everything with zicsr extension
echo "1. Compiling timer interrupt program..."
riscv32-unknown-elf-gcc -march=rv32imac_zicsr -c start_inter.s -o start_inter.o
riscv32-unknown-elf-gcc -march=rv32imac_zicsr -c timer_inter.c -o timer_inter.o
riscv32-unknown-elf-ld -T link_inter.ld start_inter.o timer_inter.o -o timer_inter.elf
echo "β Compilation successful!"
# Verify results
echo -e "\n2. Verifying timer interrupt program:"
file timer_inter.elf
echo -e "\n3. Checking interrupt-related symbols:"
riscv32-unknown-elf-nm timer_inter.elf | grep -E "(interrupt|timer|handler)"
echo -e "\n4. CSR operations in disassembly:"
riscv32-unknown-elf-objdump -d timer_inter.elf | grep -A 3 -B 1 "csr"
echo -e "\nβ Timer interrupt program ready!"chmod +x build_timer_inter.sh
./build_timer_inter.shπ Task 14: Circular Queue - With & Without Atomic Operations
Implement and compare two circular queue designs:
- β
With atomic operations (
__atomic_exchange_n) - β Without atomic operations (single-core safe only)
| File | Description |
|---|---|
task14_queue_atomic.c |
Thread-safe queue using spinlocks |
task14_queue_non_atomic.c |
Basic queue for single-core use |
start.s |
Minimal startup assembly |
linker.ld |
Bare-metal linker script |
atomic_queue.c
#include <stdint.h>
#include <stdbool.h>
#define QUEUE_SIZE 8
volatile uint32_t queue[QUEUE_SIZE];
volatile uint32_t head = 0;
volatile uint32_t tail = 0;
volatile uint32_t lock = 0;
static inline void lock_acquire(volatile uint32_t *lock) {
while (__atomic_exchange_n(lock, 1, __ATOMIC_ACQUIRE) != 0) {}
}
static inline void lock_release(volatile uint32_t *lock) {
__atomic_store_n(lock, 0, __ATOMIC_RELEASE);
}
bool enqueue(uint32_t value) {
lock_acquire(&lock);
uint32_t next_tail = (tail + 1) % QUEUE_SIZE;
if (next_tail == head) {
lock_release(&lock);
return false;
}
queue[tail] = value;
tail = next_tail;
lock_release(&lock);
return true;
}
bool dequeue(uint32_t *value) {
lock_acquire(&lock);
if (head == tail) {
lock_release(&lock);
return false;
}
*value = queue[head];
head = (head + 1) % QUEUE_SIZE;
lock_release(&lock);
return true;
}
void test_queue(void) {
uint32_t val;
enqueue(10); enqueue(20); enqueue(30);
dequeue(&val); dequeue(&val);
enqueue(40); enqueue(50);
while (dequeue(&val)) { (void)val; }
}
void main(void) {
test_queue();
while (1);
}no_atomic_queue.c
#include <stdint.h>
#include <stdbool.h>
#define QUEUE_SIZE 8
volatile uint32_t queue[QUEUE_SIZE];
volatile uint32_t head = 0;
volatile uint32_t tail = 0;
bool enqueue(uint32_t value) {
uint32_t next_tail = (tail + 1) % QUEUE_SIZE;
if (next_tail == head) return false;
queue[tail] = value;
tail = next_tail;
return true;
}
bool dequeue(uint32_t *value) {
if (head == tail) return false;
*value = queue[head];
head = (head + 1) % QUEUE_SIZE;
return true;
}
void test_queue(void) {
uint32_t val;
enqueue(10); enqueue(20); enqueue(30);
dequeue(&val); dequeue(&val);
enqueue(40); enqueue(50);
while (dequeue(&val)) { (void)val; }
}
void main(void) {
test_queue();
while (1);
}atomic_start.s
.section .text.start
.globl _start
_start:
lui sp, %hi(_stack_top)
addi sp, sp, %lo(_stack_top)
call main
hang:
j hang
.size _start, . - _startatomic_link.ld
ENTRY(_start)
MEMORY {
FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 256K
SRAM (rwx) : ORIGIN = 0x10000000, LENGTH = 64K
}
SECTIONS {
.text : { *(.text.start) *(.text*) *(.rodata*) } > FLASH
.data : { _data_start = .; *(.data*) _data_end = .; } > SRAM
.bss : { _bss_start = .; *(.bss*) _bss_end = .; } > SRAM
_stack_top = ORIGIN(SRAM) + LENGTH(SRAM);
}Compile the atomic operations program with RV32IMAC
riscv32-unknown-elf-gcc -march=rv32imac -c atomic_start.s -o atomic_start.o
riscv32-unknown-elf-gcc -march=rv32imac -c atomic_queue.c -o atomic_queue.oLink with custom linker script
riscv32-unknown-elf-ld -T atomic_link.ld atomic_start.o atomic_queue.o -o atomic_queue.elfAlso compile non-atomic version for comparison
riscv32-unknown-elf-gcc -march=rv32imc -c no_atomic_queue.c -o no_atomic_queue.o
riscv32-unknown-elf-ld -T atomic_link.ld atomic_start.o no_atomic_queue.o -o no_atomic_queue.elfComplete working build script
#!/bin/bash
echo "=== Task 14: Atomic Extension Demonstration ==="
# Compile with RV32IMAC (includes atomic extension)
echo "1. Compiling with atomic extension (RV32IMAC)..."
riscv32-unknown-elf-gcc -march=rv32imac -c atomic_start.s -o atomic_start.o
riscv32-unknown-elf-gcc -march=rv32imac -c atomic_queue.c -o atomic_queue.o
riscv32-unknown-elf-ld -T atomic.ld atomic_start.o atomic_queue.o -o atomic_queue.elf
echo "2. Compiling without atomic extension (RV32IMC)..."
riscv32-unknown-elf-gcc -march=rv32imc -c no_atomic_queue.c -o no_atomic_queue.o
riscv32-unknown-elf-ld -T atomic_link.ld atomic_start.o no_atomic_queue.o -o no_atomic_queue.elf
echo "β Compilation successful!"
# Verify results
echo -e "\n3. Verifying atomic operations program:"
file atomic_queue.elf
echo -e "\n4. Checking for atomic instructions:"
riscv32-unknown-elf-gcc -march=rv32imac -S atomic_queue.c
grep -E "(lr\.w|sc\.w|amoadd|amoswap|amoand|amoor)" atomic_queue.s
echo -e "\n5. Disassembly showing atomic instructions:"
riscv32-unknown-elf-objdump -d atomic_queue.elf | grep -A 2 -B 2 "lr\.w\|sc\.w\|amo"
echo -e "\nβ Atomic extension demonstration ready!"chmod +x build_atomic.sh
./build_atomic.shπ Task 15: Mutex Using Spinlock with LR/SC
Demonstrate how to implement a mutex (mutual exclusion) in bare-metal RISC-V using a software spinlock based on load-reserved / store-conditional (lr.w / sc.w) instructions.
The goal is to protect a shared counter from concurrent access by two simulated "threads".
| Component | Purpose |
|---|---|
lr.w |
Load-reserved word β marks address for atomic use |
sc.w |
Store-conditional β only stores if reservation is valid |
sw zero |
Releases the lock (writes 0 to lock variable) |
| Spinlock | Loops until lock is acquired (no preemption) |
| File | Description |
|---|---|
task15_complete_mutex.c |
C code simulating two threads using mutex |
start.s |
Assembly _start to call main |
mutex.ld |
Custom linker script |
full_mutex.c
#include <stdint.h>
/* =======================================
Shared Resources
======================================= */
volatile int mutex_lock = 0;
volatile int shared_counter = 0;
volatile int thread1_count = 0;
volatile int thread2_count = 0;
/* =======================================
Spinlock Acquire using LR/SC
======================================= */
void lock_acquire(volatile int *lock) {
int temp;
__asm__ volatile (
"1:\n"
" lr.w %0, (%1)\n" // Load-reserved
" bnez %0, 1b\n" // Retry if already locked
" li %0, 1\n" // Prepare value to store
" sc.w %0, %0, (%1)\n" // Attempt store-conditional
" bnez %0, 1b\n" // Retry if SC failed
: "=&r"(temp)
: "r"(lock)
: "memory"
);
}
/* =======================================
Spinlock Release
======================================= */
void lock_release(volatile int *lock) {
__asm__ volatile (
"sw zero, 0(%0)\n" // Store 0 to release lock
:
: "r"(lock)
: "memory"
);
}
/* =======================================
Critical Section (Mutex Protected)
======================================= */
void safe_increment(int thread_id, int iterations) {
for (int i = 0; i < iterations; i++) {
lock_acquire(&mutex_lock);
// Begin critical section
shared_counter++;
if (thread_id == 1)
thread1_count++;
else
thread2_count++;
// End critical section
lock_release(&mutex_lock);
}
}
/* =======================================
Pseudo Threads
======================================= */
void thread1(void) {
safe_increment(1, 50000);
}
void thread2(void) {
safe_increment(2, 50000);
}
/* =======================================
Fake Delay to Simulate Overlap
======================================= */
void delay(volatile int count) {
while (count--) {
__asm__ volatile ("nop");
}
}
/* =======================================
Main Function
======================================= */
int main() {
// Reset all shared variables
mutex_lock = 0;
shared_counter = 0;
thread1_count = 0;
thread2_count = 0;
// Simulated concurrent thread execution
thread1();
delay(1000);
thread2();
return 0;
}mutex.ld
/* Linker script for Task 15: Mutex Demo */
ENTRY(_start)
MEMORY {
FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 256K
SRAM (rwx) : ORIGIN = 0x10000000, LENGTH = 64K
}
SECTIONS {
.text : {
*(.text.start) /* Entry point */
*(.text*) /* Application code */
*(.rodata*) /* Read-only constants */
} > FLASH
.data : {
_data_start = .;
*(.data*)
_data_end = .;
} > SRAM
.bss : {
_bss_start = .;
*(.bss*)
_bss_end = .;
} > SRAM
_stack_top = ORIGIN(SRAM) + LENGTH(SRAM);
}Compile the complete single file version
riscv32-unknown-elf-gcc -march=rv32imac -c full_mutex.c -o full_mutex.o
riscv32-unknown-elf-ld -T mutex.ld start.o full_mutex.o -o full_mutex.elfVerify programs compile successfully
file full_mutex.elfCheck LR/SC instructions are generated
cho -e "\n=== LR/SC Instructions Found ==="
riscv32-unknown-elf-gcc -march=rv32imac -S full_mutex.c
grep -E "(lr\.w|sc\.w)" full_mutex.sπ¨οΈ Task 16: Retarget printf() to UART without OS
Implement a working printf() in a bare-metal RISC-V environment by:
- Overriding the
_write()syscall from Newlib - Redirecting its output to a memory-mapped UART
- Running without any operating system
| File | Description |
|---|---|
no_os.c |
C source file with custom _write() and main() |
start.s |
Startup assembly to initialize stack and call main() |
linker.ld |
Linker script to define memory layout and entry point |
build_no_os.sh |
Script to compile and verify the full ELF |
- β
Uses
UART0_BASEat0x10000000(QEMUvirtdefault) - β
Retargets Newlibβs
printf()through_write()syscall - β
Sends characters byte-by-byte using
uart_putchar() - β Includes stubbed syscalls to prevent linker errors
#define UART0_BASE 0x10000000
#define UART0_TX (*(volatile uint8_t *)UART0_BASE)
void uart_putchar(char c) {
UART0_TX = c;
}
ssize_t _write(int fd, const void *buf, size_t count) {
const char *str = (const char *)buf;
for (size_t i = 0; i < count; i++) {
uart_putchar(str[i]);
}
return count;
}
int main() {
printf("Hello, UART!\n");
printf("Decimal: %d, Hex: 0x%x\n", 123, 123);
return 0;
}Compilation Command (from script)
riscv32-unknown-elf-gcc -march=rv32imac -mabi=ilp32 \
-nostartfiles -T linker.ld -o no_os.elf \
start.s no_os.c -lc -lgccRun in QEMU (virt machine)
qemu-system-riscv32 -nographic -machine virt \
-bios fw_jump.bin -kernel no_os.elf












































