SimRV Baremetal Programming & MMIO Guide
This guide details the physical memory map, MMIO device registers, and compilation workflow for developing baremetal programs to run on SimRV.
Physical Memory Map
SimRV uses a physical memory map where memory-mapped I/O (MMIO) devices are allocated in lower address ranges, and RAM is mapped starting at 0x80000000.
| Device / Region | Base Address | Size | Description |
|---|---|---|---|
| UART 16550A | 0x10000000 |
0x00000100 (256 B) |
Serial console input/output port |
| Framebuffer Config | 0x30000000 |
0x00001000 (4 KB) |
Video mode control, keyboard, and mouse inputs |
| Framebuffer Memory | 0x30001000 |
0x001FF000 (~2 MB) |
Video memory pixel buffer (e.g. 320x200 RGBA8888) |
| Audio Controller | 0x30200000 |
0x00010000 (64 KB) |
8-channel SFX and MIDI audio control |
| VirtIO Console | 0x40000000 |
0x08000000 (128 MB) |
Virtio console device |
| VirtIO Disk | 0x48000000 |
0x08000000 (128 MB) |
Virtio block storage device |
| PLIC | 0x50000000 |
0x04000000 (64 MB) |
Platform-Level Interrupt Controller |
| CLINT | 0x60000000 |
0x000C0000 (768 KB) |
Core Local Interruptor (mtimecmp / software interrupts) |
| RTC | 0x70000000 |
0x00001000 (4 KB) |
Real-Time Clock (mtime mirror) |
| System RAM | 0x80000000 |
Up to 512 MB | Architectural RAM space |
Writing Baremetal Programs
To develop a baremetal program, you need:
1. An assembly startup file (startup.S) to initialize the stack pointer and invoke your main entry point.
2. A linker script (link.ld) to map the sections starting at the physical RAM base (0x80000000).
3. An exit routine using the tohost termination register to signal the simulator to shut down.
1. Linker Script (link.ld)
OUTPUT_ARCH( "riscv" )
ENTRY(_start)
SECTIONS
{
. = 0x80000000;
.text : {
*(.text.init)
*(.text .text.*)
}
.rodata : { *(.rodata .rodata.*) }
.data : { *(.data .data.*) }
.bss : { *(.bss .bss.*) }
}
2. Startup Assembly (startup.S)
Initialize the stack pointer sp to a safe region in RAM (e.g., 0x8E000000) and call main:
.section .text.init
.global _start
_start:
# Set up stack pointer to 0x8E000000
li sp, 0x8E000000
# Call main C/C++ function
jal main
# Fallback exit: write 1 to tohost (0x80001000) to terminate
_startup_exit:
li t0, 0x80001000
li t1, 1
sw t1, 0(t0)
loop:
j loop
3. Compiling and Packaging
Compile your source files using a RISC-V cross-compiler and dump it to a raw binary file:
# Compile and Link
riscv64-unknown-elf-gcc -march=rv32imac -mabi=ilp32 -T link.ld startup.S main.c -o program.elf -ffreestanding -O2 -nostdlib
# Convert ELF to raw flat binary image
riscv64-unknown-elf-objcopy -O binary program.elf program.bin
4. Running the Program
Run the resulting binary image in SimRV in baremetal mode (-b / --baremetal). SimRV starts in interactive TUI mode by default:
# Interactive TUI mode (Default)
./build/rv32-release/SimRV -b -m program.bin
# Headless / CLI-only mode
./build/rv32-release/SimRV -b -m program.bin -c
# Cycle-accurate 5-stage pipeline simulation
./build/rv32-release/SimRV -b -m program.bin -C
Device Driver API Examples
UART 16550A Serial I/O
The UART registers are aligned to 4-byte boundaries (word-addressed) or 1-byte boundaries.
#define UART_BASE 0x10000000
#define UART_REG(offset) ((volatile uint32_t*)(UART_BASE + (offset)))
#define UART_THR UART_REG(0x00) // Transmitter Holding Register (write)
#define UART_LSR UART_REG(0x14) // Line Status Register (read)
void uart_putc(char c) {
// Wait until Transmit Holding Register Empty (THRE) bit 5 is set
while ((*UART_LSR & 0x20) == 0);
*UART_THR = c;
}
void uart_puts(const char* s) {
while (*s) {
uart_putc(*s++);
}
}
Custom Framebuffer Display
The custom framebuffer device supports simple double-buffered or direct draw routines.
#define FB_CTRL_BASE 0x30000000
#define FB_WIDTH_REG ((volatile uint32_t*)(FB_CTRL_BASE + 0x00))
#define FB_HEIGHT_REG ((volatile uint32_t*)(FB_CTRL_BASE + 0x04))
#define FB_FORMAT_REG ((volatile uint32_t*)(FB_CTRL_BASE + 0x08))
#define FB_FLUSH_REG ((volatile uint32_t*)(FB_CTRL_BASE + 0x0C))
#define FB_PIXELS ((volatile uint32_t*)0x30001000)
void display_init(uint32_t width, uint32_t height) {
*FB_WIDTH_REG = width;
*FB_HEIGHT_REG = height;
*FB_FORMAT_REG = 1; // 1 = RGBA8888
}
void display_draw_pixel(uint32_t x, uint32_t y, uint32_t color) {
uint32_t width = *FB_WIDTH_REG;
FB_PIXELS[y * width + x] = color;
}
void display_flush() {
*FB_FLUSH_REG = 1; // Signal simulator to redraw screen
}
Keyboard Input events
Keyboard events are queried through the Framebuffer registers.
#define FB_KEY_REG ((volatile uint32_t*)(FB_CTRL_BASE + 0x10))
#define FB_KEY_STATUS ((volatile uint32_t*)(FB_CTRL_BASE + 0x14))
int get_keyboard_event(int* pressed, char* key) {
if (*FB_KEY_STATUS) {
uint32_t packed = *FB_KEY_REG;
*pressed = (packed >> 31) & 1; // 1 if pressed, 0 if released
*key = packed & 0xFF; // ASCII code
return 1; // Event received
}
return 0; // No event
}
Real-Time Clock / Timer Delay
Use the RTC mtime register at 0x70000000 to measure accurate delays. mtime runs at approximately 10 MHz.