A bare-metal RISC-V RV32IM emulated development platform for CS281 Systems Architecture at Drexel University. Built on Renode, it gives students a realistic hardware environment — GPIO, UART, timers, interrupts — without requiring physical hardware.
📄 CS281 Technical Reference Manual — memory map, peripheral registers, interrupt reference, boot sequence, and register quick reference. (Also available as CS281_TRM.docx for editing.)
hardware/ Shared platform definition (used by every lab)
cs281_board.repl Renode machine description (CPU, memory, peripherals)
cs281_run.resc Renode script — normal run
cs281_debug.resc Renode script — GDB debug session
lib/
cs281.ld Linker script (ROM @ 0x20000000, RAM @ 0x40000000)
startup.S Reset handler: stack init, .data copy, .bss zero
uart.S uart_putchar / uart_puts / uart_getchar
cs281.inc Assembly register map (.equ definitions)
cs281.h C register map (for C labs)
docs/
CS281_TRM.docx Technical Reference Manual / datasheet
lab1-blinky/ Lab 1: blink LED0 with busy-wait delay
main.S
Makefile
.vscode/ Tasks, launch config, IntelliSense settings
lab2-blinky2/ Lab 2: blink LED0 with CLINT timer interrupt and WFI
main.S
Makefile
.vscode/ Tasks, launch config, IntelliSense settings
lab3-arrays/ Lab 3: array operations (sum, min, max, reverse) in assembly
main.S
Makefile
.vscode/ Tasks, launch config, IntelliSense settings
lab4a-timers-c/ Lab 4a: LiteX Timer interrupt in C (reference implementation)
main.c
Makefile
.vscode/ Tasks, launch config, IntelliSense settings
lab4b-timers/ Lab 4b: LiteX Timer interrupt in assembly (student TODO)
main.S
Makefile
.vscode/ Tasks, launch config, IntelliSense settings
lab5-gpio-input/ Lab 5: GPIO input interrupts — button presses toggle LEDs
main.S
Makefile
.vscode/ Tasks, launch config, IntelliSense settings
The CS281 board is a custom virtual platform defined in hardware/cs281_board.repl and emulated by Renode. It is not based on any real chip — it is designed specifically for this course so that the memory map, peripherals, and interrupt assignments are as clean and teachable as possible. Full register-level documentation is in the CS281 Technical Reference Manual.
Processor — RISC-V RV32IM + Zicsr running in machine mode (M-mode only). RV32I is the base 32-bit integer ISA; M adds hardware multiply and divide; Zicsr adds the CSR instructions needed for interrupt handling. There is no MMU, no OS, no privilege levels below M-mode — what you write runs directly on the (emulated) metal.
Memory — Two regions. ROM at 0x20000000 (256 KB) holds the program and read-only data and is loaded from the ELF at boot. RAM at 0x40000000 (512 KB) holds .data, .bss, the heap, and the stack (top at 0x40080000). The split between load address (ROM) and run address (RAM) for initialized data is handled by the linker script and the boot sequence in startup.S.
Peripherals — All peripherals are memory-mapped; reading or writing the right address controls the hardware directly. The board includes:
- UART (
0xe0001800) — LiteX UART connected to a telnet server on port 3456. Three registers:RXTX(read/write a byte),TXFULL(poll before writing),RXEMPTY(poll before reading). - LiteX Timer (
0xe0002000) — A countdown timer with configurable load, auto-reload, and an IRQ on line 11. Useful for periodic interrupts without touching the CLINT. Important: the timer uses 8-bit CSR sub-registers — the 32-bit LOAD and RELOAD values are each split across four 8-bit registers at consecutive 4-byte addresses, written MSB first (offsets 0x00–0x0C for LOAD, 0x10–0x1C for RELOAD). Control registers:ENat 0x20,EV_PENDINGat 0x3C (write 1 to clear),EV_ENABLEat 0x40 (write 1 to route to CPU line 11). See the TRM Section 6 for the complete register table and ISR pattern. - CLINT (
0xe0005000) — The standard RISC-V Core-Level Interruptor. ProvidesMTIME(a 64-bit free-running counter at 100 MHz) andMTIMECMP(fires a machine timer interrupt on line 7 whenMTIME ≥ MTIMECMP). Also providesMSIP(software interrupt on line 3). - GPIO Output (
0xe0015000) — Four LEDs. Write a bitmask: bit 0 = LED0, bit 1 = LED1, bit 2 = LED2, bit 3 = LED3. - GPIO Input (
0xe0015400) — Four inputs with an IRQ on line 12 triggered by rising edge of enabled pins. Bit 0 = BTN0, bit 1 = BTN1, bit 2 = DIP_SW0, bit 3 = DIP_SW1. Simulate from the Renode monitor:sysbus.gpio_in OnGPIO 0 True(press) /sysbus.gpio_in OnGPIO 0 False(release). - 7-Segment Display (
0xe0004400) — Eight GPIO output bits, one per segment (a–g + decimal point). Write a bitmask to light individual segments; common digit patterns are defined incs281.inc. - Buzzer (
0xe0004000) — Single-bit GPIO output. Write0x1to activate,0x0to silence.
Interrupts — The CPU has three interrupt lines wired up. Line 3 (MSIP) and line 7 (MTIP) come from the CLINT; line 11 (MEIP) comes from the LiteX Timer; line 12 comes from GPIO Input. Enable a specific interrupt by setting the corresponding bit in mie, then set mstatus.MIE (bit 3) to open the global gate. Point mtvec at your trap handler before enabling anything.
| Memory Region | Address | Size |
|---|---|---|
| ROM | 0x20000000 |
256 KB |
| RAM | 0x40000000 |
512 KB |
| UART | 0xe0001800 |
— |
| LiteX Timer | 0xe0002000 |
— |
| Buzzer | 0xe0004000 |
— |
| 7-Segment Display | 0xe0004400 |
— |
| CLINT | 0xe0005000 |
— |
| GPIO Output (LEDs) | 0xe0015000 |
— |
| GPIO Input (BTN/DIP) | 0xe0015400 |
— |
→ See hardware/cs281_board.repl for the full Renode platform definition and hardware/docs/CS281_TRM.pdf for complete register documentation.
| Tool | Notes |
|---|---|
| Renode | 1.16+ — the emulator |
| RISC-V GNU Toolchain | Provides riscv64-elf-as, riscv64-elf-ld, riscv64-elf-gdb |
telnet |
For UART and monitor connections |
| VS Code (optional) | .vscode/ configs are provided in each lab for convenience, but any editor works |
See docs/setup.md for step-by-step installation instructions on macOS, Windows, and Linux.
cd lab1-blinky
make # assemble and link → build/lab1.elf
make run # launch Renode headlessIn a second terminal:
make uart-connect # telnet to UART — see LED0 ON / LED0 OFF output| Target | Description |
|---|---|
make / make all |
Build build/lab1.elf |
make clean |
Remove build/ |
make run |
Run in Renode (headless) |
make run-debug |
Run with GDB stub on :3333, CPU halted |
make debug-attach |
Attach riscv64-elf-gdb (command-line) |
make uart-connect |
telnet localhost 3456 — UART output |
make monitor-connect |
telnet localhost 1234 — Renode monitor |
make disasm |
Disassemble ELF to stdout |
Once connected via make monitor-connect:
pause # halt CPU
start # resume
cpu PC # show program counter
sysbus.gpio_led0 State # read LED0 state (True/False)
sysbus.gpio_in OnGPIO 0 True # BTN0 press (rising edge → IRQ)
sysbus.gpio_in OnGPIO 0 False # BTN0 release
sysbus.gpio_in OnGPIO 1 True # BTN1 press
quit # exit Renode
| Lab | Topic |
|---|---|
Lab 1 (lab1-blinky) |
GPIO output, UART, software busy-wait delay |
Lab 2 (lab2-blinky2) |
CLINT timer interrupt, mtvec, WFI, MTIMECMP |
Lab 3 (lab3-arrays) |
Indexed memory access, loops, RISC-V calling convention |
Lab 4a (lab4a-timers-c) |
LiteX Timer interrupt in C — read before Lab 4b |
Lab 4b (lab4b-timers) |
LiteX Timer interrupt in assembly — student implementation |
Lab 5 (lab5-gpio-input) |
GPIO input interrupts, rising-edge trigger, IRQ_PENDING |
- CPU: RISC-V RV32IM + Zicsr (32-bit, integer multiply/divide, CSR instructions)
- Emulator: Renode with LiteX-compatible peripheral models
- Language: Assembly-first (C supported via same toolchain and linker script)
- Debug: GDB stub in Renode + VS Code Native Debug extension (
webfreak.debug, external server mode)
A terminal TUI that renders a virtual board — colored LEDs, a 7-segment display, DIP switches, a CPU status indicator, and a live UART output pane. Connects to Renode over TCP so you can interact with the hardware without touching the monitor prompt. Written in Go using the Charm ecosystem; cross-compiles to macOS, Linux, and Windows.
╭─────────────────────────────────────────────────────────────────────╮
│ CS281 Virtual Development Board │
│ ◉ RUNNING │
│─────────────────────────────────────────────────────────────────────│
│ INPUTS │ 7-SEG │ LEDs │ STATUS │
│ │ │ │ │
│ [0] BTN0 mom │ ─── │ ⬤ LED0 ○LED1 │ CPU ● RUN │
│ [1] BTN1 mom │ █ █ │ ○ LED2 ○LED3 │ │
│ [2] DIP0 ▽off │ ─── │ │ ╔══════╗ │
│ [3] DIP1 ▽off │ █ █ │ │ ║ RST ║ │
│ │ ─── │ │ ╚══════╝ │
│─────────────────────────────────────────────────────────────────────│
│ UART OUTPUT ↑↓ / j k / scroll │
│─────────────────────────────────────────────────────────────────────│
│ Main loop running... │
│ . . . . [BTN0] LED0 ON . . . [BTN0] LED0 OFF . . . │
│─────────────────────────────────────────────────────────────────────│
│ [0] BTN0 [1] BTN1 [2] DIP0 [3] DIP1 │
│ [R] Reset [P] Power Down [Q] Quit │
╰─────────────────────────────────────────────────────────────────────╯
Power-up sequence — pressing P connects to Renode and runs a self-test animation: each LED cycles on/off, each DIP flips, and the 7-segment counts 0–9. UART output is buffered silently behind a "Testing Board..." banner, then streams live once the board reports POST OK. Press Esc to skip the self-test.
Reset — pressing R while running performs a soft reset: the CPU is paused,
all GPIO inputs are cleared, the program counter is rewound to _start
(0x20000000), and the CPU is resumed. The firmware re-runs its boot sequence
without closing the Renode session.
CPU status — the STATUS panel shows ● RUN (green) or ○ HLT (amber) based
on whether the program counter is advancing. Programs that halt or spin on wfi
without activity for ~6 seconds are flagged as halted.
→ See experimental/ui/README.md for build instructions, keyboard controls, flags, and architecture details.
Drexel University — CS281 Systems Architecture