Add BIOS and tiny kernel roadmap phases

This commit is contained in:
2026-04-28 13:23:03 +02:00
parent 07dd8e21f0
commit e8631501e8
18 changed files with 344 additions and 77 deletions
+148 -33
View File
@@ -21,17 +21,22 @@ Recommended casual target: reach Phase 8, where GCC-built C runs on your CPU and
`riscv-tests` gives you confidence in the ISA implementation. Everything after
that is enrichment or a long-term expedition.
Recommended middle-game target: continue through Phases 9-11 and build your own
BIOS, ELF loader, and tiny kernel before attempting Linux. This gives you a
playable computer with your own firmware and command line without requiring the
full Linux boot contract.
---
## Memory Map (target — fixed in Phase 0, evolves through Phase 14)
## Memory Map (target — fixed in Phase 0, evolves through Phase 17)
```
0x0000_0000 0x0FFF_FFFF reserved boot aperture (256 MB decode region;
actual SPI flash is 16 MB on Arty A7,
mapped at 0x0000_00000x00FF_FFFF in Phase 14)
mapped at 0x0000_00000x00FF_FFFF in Phase 17)
0x1000_0000 0x1000_0FFF MMIO (UART; later: timer, PLIC)
0x2000_0000 0x2000_FFFF instruction BRAM (64 KB)
0x8000_0000 0x8000_FFFF data BRAM (64 KB) → DRAM 0x8000_00000x8FFF_FFFF in Phase 14
0x8000_0000 0x8000_FFFF data BRAM (64 KB) → DRAM 0x8000_00000x8FFF_FFFF in Phase 17
```
Reset PC = `0x2000_0000`. Locking this in now keeps the linker script and crt0
@@ -165,7 +170,7 @@ extension (bit 31 of the instruction is always the sign bit in RISC-V — that's
deliberate design choice). Verify that the decoder handles all R-type, I-type, S-type,
B-type, U-type, and J-type correctly.
Future role: When you add CSR instructions (Phase 9), you'll add a new case to the
Future role: When you add CSR instructions (Phase 12), you'll add a new case to the
decoder and a new field to the struct. The structure of the decoder doesn't change.
---
@@ -260,8 +265,8 @@ What: Add a data BRAM mapped at `0x8000_0000` (64 KB) and a load/store unit.
For now, only 32-bit aligned access (lw and sw).
Define the full memory bus contract here, not just the subset Phase 6.1
uses. Defining it once means Phase 6.2 (sub-word), Phase 7 (MMIO), Phase 9
(access faults), Phase 12 (atomics), and Phase 14 (DRAM) all plug into the
uses. Defining it once means Phase 6.2 (sub-word), Phase 7 (MMIO), Phase 12
(access faults), Phase 15 (atomics), and Phase 17 (DRAM) all plug into the
same interface without rework. The contract (see `CLAUDE.md` for the full
spec) is two payload structs (`mem_req_t`, `mem_rsp_t`) plus loose
valid/ready signals on each channel:
@@ -442,8 +447,8 @@ program (or crt0) emits a CSR or `fence.i` op before then, the **decoder**
asserts `illegal_instr` and the Phase 8.3 halt latches the offending PC and
instruction word — catching the bug visibly rather than NOPing through it.
(Illegal instructions are a decode event, not a bus event; the memory bus
is not involved.) Switch to `rv32im_zicsr_zifencei` in Phase 9 once CSRs
land, and `rv32ima_zicsr_zifencei` in Phase 12 once atomics land.
is not involved.) Switch to `rv32im_zicsr_zifencei` in Phase 12 once CSRs
land, and `rv32ima_zicsr_zifencei` in Phase 15 once atomics land.
Note on GCC 11+: newer toolchains require `_zicsr` / `_zifencei` in the
march string only when source code actually uses CSR or `fence.i`
@@ -469,7 +474,7 @@ testbench-visible `illegal_instr` signal** with the offending PC and
instruction word latched. Do NOT decode them as NOPs — GCC emits `ebreak`
in `__builtin_trap`, abort paths, and some divide-by-zero configurations,
and silently NOPing past those destroys debuggability. The halt becomes a
real exception in Phase 9 (`mcause = 2` illegal, `mcause = 3` ebreak,
real exception in Phase 12 (`mcause = 2` illegal, `mcause = 3` ebreak,
`mcause = 11` ecall-from-M).
Why: GCC's output will exercise the full ISA. You need complete RV32I
@@ -480,10 +485,11 @@ Test: Compile progressively more complex C programs. String manipulation, struct
usage, switch statements (these generate jump tables — exercises jalr with computed
addresses), recursive functions.
### 8.4 — Milestone: Meaningful C Program
What: Write something real — a serial monitor that accepts commands over UART
and responds. Or a tiny Forth interpreter. Something interactive that proves
the core is solid.
### 8.4 — Milestone: Meaningful Standalone C Program
What: Write something real but still self-contained — a checksum demo, tiny
benchmark, string/array exercise, or simple UART-driven calculator. Keep the
full serial monitor for Phase 9, where it becomes the BIOS instead of a one-off
test program.
Why: Confidence builder. You now have a working RISC-V computer that runs
compiled C and talks over serial. Everything after this is enrichment.
@@ -499,7 +505,7 @@ BRAM, runs the core until the test signals completion, and reports pass/fail.
Why: Hand-written testbenches catch the bugs you thought to look for. The
official suite catches the ones you didn't — corner cases in shifts, sign
extension on byte loads, immediate decoding for every format, M-extension
overflow cases. Doing this *before* Phase 9 means you're building trap
overflow cases. Doing this *before* Phase 12 means you're building trap
handling on a known-good ISA implementation, not stacking unknowns.
Expect to find bugs. That's the point. Fix each one, re-run the suite, move
@@ -510,7 +516,116 @@ becomes regression coverage for the rest of the project.
---
## Phase 9 — CSRs + M-Mode Trap Handling [Hard stretch]
## Phase 9 — GCC-Built BIOS / Serial Monitor [Easy/fun]
What: Build a small BIOS in freestanding C/assembly with its own linker script,
startup code, UART driver, and command loop. It runs from the existing BRAM
image and gives you an interactive prompt over serial.
Suggested commands:
- `help` — list commands
- `peek <addr>` / `poke <addr> <value>` — inspect and edit memory-mapped state
- `dump <addr> <len>` — hex-dump memory
- `fill <addr> <len> <value>` — initialize memory
- `regs` — print a software-maintained register/trap snapshot when available
- `memtest` — run a small RAM/BRAM test
- `load` — receive a raw binary or hex stream over UART
- `run <addr>` — jump to a loaded program
- `reboot` — return to reset or spin until manual reset
Important hardware contract: a UART-loaded program needs somewhere executable
to live. In the early Harvard design, instruction BRAM is fetch-only unless you
deliberately expose a writable path. Pick one simple learning-friendly option:
- make instruction BRAM dual-port, with the CPU fetch path on one port and a
D-bus write/debug port on the other;
- add a small "program RAM" window that is writable through the D-bus and
fetchable through the I-bus;
- or defer `run`/ELF execution until you add one of those executable write paths.
Why: This is the first point where the board feels like your own computer. It
also becomes a practical debug tool for later hardware and firmware work.
Test: Boot to a prompt, use `poke`/`peek` on UART registers and RAM, load a tiny
raw program, jump to it, and have it return or print a message.
Future role: The BIOS can remain as a recovery/debug monitor even after flash,
DRAM, and Linux enter the picture.
---
## Phase 10 — Minimal ELF Loader [Easy/fun]
What: Teach the BIOS to receive and run the simplest possible RISC-V ELF32
binaries. Support statically linked, non-relocatable, little-endian
`EM_RISCV` executables with `PT_LOAD` segments only. Copy each loadable segment
to its physical address, zero the `memsz - filesz` tail for BSS, set a known
stack pointer, and jump to `e_entry`.
Deliberate limits:
- no dynamic linking
- no relocations
- no virtual memory
- no privilege separation
- no demand paging
- no filesystem
ABI for tiny programs: start with a tiny fixed contract. For example, `a0`
receives a pointer to a BIOS call table, `sp` points at the top of data RAM,
and returning from `main` jumps back to the monitor with an integer status.
Before Phase 12 traps exist, programs are trusted firmware payloads, not isolated
user processes.
Why: ELF loading is a major confidence milestone. You are no longer baking every
program into the FPGA bitstream; you can compile a new binary, send it over
serial, and run it on your CPU.
Test: Compile several tiny programs with fixed link addresses: hello world,
integer arithmetic, memory copy, and a program that returns a status code to
the BIOS. Confirm the BIOS rejects malformed or unsupported ELF files loudly.
Future role: This loader becomes the conceptual ancestor of the later bootloader
that copies kernels from flash to DRAM.
---
## Phase 11 — Tiny Kernel + Command Shell [Easy/fun]
What: Build a tiny kernel as a separate GCC-built ELF loaded by the BIOS.
Before Phase 12, "kernel" means bare-machine firmware with a command shell, not
an isolated privileged OS. It owns a simple console, command parser, memory
allocator, and a table of services. Keep it intentionally small: no MMU, no
userspace isolation, no real filesystem.
Suggested kernel features:
- banner and prompt
- `help`, `uptime`, `mem`, `dump`, `loadelf`, `run`, `reboot`
- direct UART console driver or BIOS-backed console calls
- bump allocator for kernel data structures
- simple program table showing loaded ELF images
- trusted program launch with an agreed calling convention
- status return from launched programs back to the shell
Simple ELF binaries: compile tiny freestanding programs that use the kernel or
BIOS service table for `putchar`, `getchar`, and exit. The first binaries can be
as small as "print a line", "sum an array", and "echo typed characters". The
goal is not POSIX; the goal is a complete end-to-end loop: compile on your host,
send over UART, load as ELF, run on your CPU, return to your shell.
Why: This is the rewarding middle game between "bare-metal C works" and "Linux
boot hangs somewhere in early init." You get OS-shaped learning while the system
is still small enough to understand in one sitting.
Future role: Phase 12 turns `ecall`, exceptions, and trap vectors into real
architectural mechanisms. At that point, the ad hoc service table can evolve into
a syscall ABI and the kernel can start handling faults instead of trusting every
program.
---
## Phase 12 — CSRs + M-Mode Trap Handling [Hard stretch]
What: Add Control and Status Registers (mstatus, mtvec, mepc, mcause, mtval, mie,
mip) and the CSR instructions (csrrw, csrrs, csrrc, csrrwi, csrrsi, csrrci). Add
@@ -527,18 +642,18 @@ plus S-mode (supervisor mode), which you'll add later.
---
## Phase 10 — Timer [Hard stretch]
## Phase 13 — Timer [Hard stretch]
What: Implement mtime (a free-running 64-bit counter) and mtimecmp (comparison
register). When mtime >= mtimecmp, a timer interrupt fires.
Why: Every operating system needs a timer tick for scheduling. Even bare-metal
firmware needs delays and timeouts. This is your first interrupt source, which also
validates that the trap handling from Phase 9 actually works end-to-end.
validates that the trap handling from Phase 12 actually works end-to-end.
---
## Phase 11 — Interrupt Controller [Hard stretch]
## Phase 14 — Interrupt Controller [Hard stretch]
What: Build a minimal PLIC (Platform-Level Interrupt Controller) or a simplified
version. Connect UART RX as an interrupt source. Implement interrupt priority and
@@ -550,7 +665,7 @@ the PLIC manages them. This is required for Linux.
---
## Phase 12 — A Extension (Atomics) [Hard stretch]
## Phase 15 — A Extension (Atomics) [Hard stretch]
What: Implement the RV32A atomic instructions: `lr.w` / `sc.w` (load-reserved /
store-conditional) and the AMO ops (`amoswap.w`, `amoadd.w`, `amoand.w`,
@@ -578,7 +693,7 @@ reservation set and bus become more involved.
---
## Phase 13 — Pipeline (Optional but educational) [Hard stretch]
## Phase 16 — Pipeline (Optional but educational) [Hard stretch]
What: Insert pipeline registers between your stages (fetch|decode|execute|
memory|writeback). Handle data hazards (forwarding/stalling) and control
@@ -592,11 +707,11 @@ and deeply educational.
Why optional: A non-pipelined core can run at 50 MHz on the Artix-7. That's
plenty for booting Linux. Pipeline if you want to learn, not because you
must — and if you do, do it before DRAM/firmware work piles on. Re-running
`riscv-tests` (Phase 8.5 + 12) after pipelining catches regressions.
`riscv-tests` (Phase 8.5 + 15) after pipelining catches regressions.
---
## Phase 14 — SPI Flash Boot + DRAM [Overkill/hard]
## Phase 17 — SPI Flash Boot + DRAM [Overkill/hard]
What: Add an SPI flash controller to boot from the on-board flash (instead
of BRAM initialization). The Arty A7-100T has a 16 MB Quad-SPI flash; map
@@ -619,7 +734,7 @@ keep.
---
## Phase 15 — S-Mode, U-Mode, Sv32 Virtual Memory [Overkill/hard]
## Phase 18 — S-Mode, U-Mode, Sv32 Virtual Memory [Overkill/hard]
What: Add supervisor and user privilege modes. Implement Sv32 page table
walking (two-level page tables, 4 KB pages). Add the `satp` CSR, page-fault
@@ -637,7 +752,7 @@ modes round-trip via `mret` / `sret`.
---
## Phase 16 — Linux Boot Contract (SBI / Device Tree / ABI) [Overkill/hard]
## Phase 19 — Linux Boot Contract (SBI / Device Tree / ABI) [Overkill/hard]
What: Lock down everything Linux expects at the moment of `kernel_entry`.
This is a real subproject, not a footnote inside "port Linux".
@@ -676,16 +791,16 @@ Required deliverables:
surface area.
Why: Skipping this phase means hitting all of these problems simultaneously
during Phase 17 bring-up, where the failure mode is "kernel hangs silently
in early boot" with no console. Doing it explicitly turns Phase 17 into a
during Phase 20 bring-up, where the failure mode is "kernel hangs silently
in early boot" with no console. Doing it explicitly turns Phase 20 into a
debugging exercise on a known-good ABI surface.
---
## Phase 17 — Linux [Overkill/hard]
## Phase 20 — Linux [Overkill/hard]
What: Build a minimal RISC-V Linux kernel against the device tree and
boot path defined in Phase 16. Build an initramfs with BusyBox. Load
boot path defined in Phase 19. Build an initramfs with BusyBox. Load
kernel + DTB + initramfs to flash. Boot to a shell prompt over UART.
Why: This is the summit. A Linux shell running on a CPU you built from
@@ -697,12 +812,12 @@ scratch.
- Vivado 2025.2 or later (synthesis, simulation, ILA, VIO)
- RISC-V GCC toolchain (`riscv64-unknown-elf-gcc`, multilib build). March
string evolves: `rv32im` (Phase 8) → `rv32im_zicsr_zifencei` (Phase 9) →
`rv32ima_zicsr_zifencei` (Phase 12).
string evolves: `rv32im` (Phase 8) → `rv32im_zicsr_zifencei` (Phase 12) →
`rv32ima_zicsr_zifencei` (Phase 15).
- `riscv-tests` repo cloned and buildable (Phase 8.5 onward; rv32ua added at
Phase 12)
- Device-tree compiler `dtc` (Phase 16+)
- OpenSBI source (Phase 16+, only if you choose the SBI boot path)
Phase 15)
- Device-tree compiler `dtc` (Phase 19+)
- OpenSBI source (Phase 19+, only if you choose the SBI boot path)
- Terminal program (minicom, picocom, or PuTTY) for UART, 115200 8N1
- Text editor you like for SystemVerilog
- The RISC-V ISA spec (Volume 1: Unprivileged, Volume 2: Privileged) — free PDFs