Add BIOS and tiny kernel roadmap phases
This commit is contained in:
+148
-33
@@ -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_0000–0x00FF_FFFF in Phase 14)
|
||||
mapped at 0x0000_0000–0x00FF_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_0000–0x8FFF_FFFF in Phase 14
|
||||
0x8000_0000 – 0x8000_FFFF data BRAM (64 KB) → DRAM 0x8000_0000–0x8FFF_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
|
||||
|
||||
Reference in New Issue
Block a user