Align the roadmap with a Linux-capable RV32IMA target

Broaden the documented end target from RV32IM plus machine-mode support to a
Linux-capable RV32IMA core with Zicsr, Zifencei, M/S/U privilege, and Sv32.

Add atomics as a required Phase 12 milestone, move the optional pipeline and
memory-system work later, and introduce explicit Linux bring-up preparation for
boot ABI, device tree, UART driver compatibility, OpenSBI versus direct M-mode
boot, and kernel/initramfs handoff.

Tighten compiler guidance so the advertised -march string follows the hardware
that is actually decoded: rv32im for early bare-metal work, then
rv32im_zicsr_zifencei after CSR/fence.i support, and rv32ima_zicsr_zifencei once
atomics land. The roadmap also calls out loud illegal-instruction halts instead
of silently treating unsupported operations as NOPs.
This commit is contained in:
2026-04-28 11:51:22 +02:00
parent e98b3694ab
commit bcbf1fa616
3 changed files with 259 additions and 68 deletions
+189 -49
View File
@@ -5,13 +5,15 @@
---
## Memory Map (target — fixed in Phase 0, evolves through Phase 13)
## Memory Map (target — fixed in Phase 0, evolves through Phase 14)
```
0x0000_0000 0x0FFF_FFFF reserved (SPI flash boot region, Phase 13)
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)
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 13
0x8000_0000 0x8000_FFFF data BRAM (64 KB) → DRAM 0x8000_00000x8FFF_FFFF in Phase 14
```
Reset PC = `0x2000_0000`. Locking this in now keeps the linker script and crt0
@@ -237,9 +239,22 @@ Future role: Final. These instructions don't change.
### 6.1 — Word Load/Store (lw, sw)
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 a simple memory bus
interface with address, write data, read data, write enable, and valid/ready
signals.
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 same
interface without rework. Required signals (see `CLAUDE.md` for the full
spec):
- `req_valid` / `req_ready` — request handshake
- `rsp_valid` / `rsp_ready` — response handshake (independent)
- `req_addr`, `req_we`, `req_wdata`, `req_size`, `req_wstrb`
- `rsp_rdata`, `rsp_err`
For Phase 6.1 specifically: `req_size` is always `2'b10` (word), `req_wstrb`
is `4'b1111` on stores, `rsp_err` is unused (BRAM never faults). The signals
exist in the bundle from day one even when tied off.
Note: BRAM is 1-cycle read latency, so a load takes one extra cycle beyond
the EXECUTE state. The control FSM extends to FETCH → EXECUTE → MEM_WAIT →
@@ -269,9 +284,21 @@ to 32 bits, lbu zero-extends).
Why: C uses char (byte) and short (halfword) types constantly. String operations are
byte-by-byte. You can't run real C code without these.
Testbench focus: Sign extension (loading 0xFF as signed byte should give 0xFFFFFFFF,
as unsigned should give 0x000000FF). Unaligned access behavior (RISC-V base spec says
misaligned access can trap — decide if you want to support it or trap).
Implementation: the load/store unit drives `req_size` (00=byte, 01=halfword,
10=word) and `req_wstrb` (which byte lane within the 32-bit word is being
written). On load, the unit muxes the right byte/halfword out of `rsp_rdata`
and applies sign/zero extension based on the opcode.
Decision for this project: **trap on misaligned access** rather than support
it in hardware. Hardware support for misaligned word access on Artix-7 is
expensive (two BRAM cycles + merge logic) and the kernel can emulate via
trap. The bus signals misalignment via `rsp_err`, and Phase 9's trap logic
turns that into a load/store address-misaligned exception.
Testbench focus: Sign extension (loading 0xFF as signed byte should give
0xFFFFFFFF, as unsigned should give 0x000000FF). Byte lane selection (storing
0xAB at address `0x8000_0001` must update only that byte). Misaligned access
returns `rsp_err`.
Future role: Final. These instructions don't change.
@@ -373,16 +400,24 @@ crt0 grows when you add CSRs (setting up trap vectors in startup).
What: Write a trivial main() that prints a string to the UART by writing bytes
to `0x1000_0000`. Compile with:
```
riscv64-unknown-elf-gcc -march=rv32im_zicsr_zifencei -mabi=ilp32 \
riscv64-unknown-elf-gcc -march=rv32im -mabi=ilp32 \
-nostdlib -T linker.ld -o firmware.elf crt0.S main.c
```
Convert: `objcopy -O binary firmware.elf firmware.bin`. Convert binary to
`.mem` format. Load into BRAM. Run.
Why the long march string: GCC 11+ split `Zicsr` and `Zifencei` out of the
RV32I base. `-march=rv32im` alone fails or warns. `Zifencei` is harmless
(`fence.i` is a NOP until you have caches); `Zicsr` will only be emitted by
code that uses CSR ops, so it's safe to advertise even before Phase 9.
March string discipline: advertise only what the hardware decodes. `rv32im`
is correct for Phase 8 because CSRs and `fence.i` aren't decoded yet. When
the program (or crt0) uses CSR/fence.i ops before that, the bus will return
`rsp_err` and Phase 8.3's illegal-instruction halt will catch it visibly —
much better than NOPing through a bug. Switch to `rv32im_zicsr_zifencei` in
Phase 9 once CSRs land, and `rv32ima_zicsr_zifencei` in Phase 12 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`
instructions (since those moved out of base RV32I in the 2019 spec). Pure
arithmetic + UART-poke C does not need them.
Why: This proves your CPU is compatible with a real compiler. Any bugs in your
instruction implementation will surface here — GCC will use instructions in
@@ -392,12 +427,23 @@ Expect to iterate: GCC will probably emit an instruction you haven't implemented
That's fine — check the illegal instruction, add it, resynthesize. This is normal.
### 8.3 — Fill Remaining RV32I Gaps
What: Implement any RV32I instructions you deferred. Common ones: all shift variants
(sll, srl, sra, slli, srli, srai), set-less-than variants (slti, sltiu), fence (NOP
for now — you have no cache), ecall/ebreak (NOP for now — you have no trap handling).
What: Implement any RV32I instructions you deferred. Common ones: all shift
variants (sll, srl, sra, slli, srli, srai), set-less-than variants (slti,
sltiu), `fence` (decode as NOP — there is no cache yet, so memory ordering
is trivially satisfied).
Why: GCC's output will exercise the full ISA. You need complete RV32I coverage for
any non-trivial C code to work.
`ecall` / `ebreak` / unrecognized opcodes / `fence.i` / any CSR op all go
to a single illegal-instruction handler that **halts the core and asserts a
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,
`mcause = 11` ecall-from-M).
Why: GCC's output will exercise the full ISA. You need complete RV32I
coverage for any non-trivial C code to work, and you need loud failures —
not silent ones — for the instructions you haven't implemented yet.
Test: Compile progressively more complex C programs. String manipulation, struct
usage, switch statements (these generate jump tables — exercises jalr with computed
@@ -473,60 +519,154 @@ the PLIC manages them. This is required for Linux.
---
## Phase 12 — Pipeline (Optional but educational)
## Phase 12 — A Extension (Atomics)
What: Insert pipeline registers between your stages (fetch|decode|execute|memory|
writeback). Handle data hazards (forwarding/stalling) and control hazards (branch
prediction or pipeline flush).
What: Implement the RV32A atomic instructions: `lr.w` / `sc.w` (load-reserved /
store-conditional) and the AMO ops (`amoswap.w`, `amoadd.w`, `amoand.w`,
`amoor.w`, `amoxor.w`, `amomin.w`, `amomax.w`, `amominu.w`, `amomaxu.w`).
Switch the GCC march string to `rv32ima_zicsr_zifencei`. Run `rv32ua` from
`riscv-tests`.
Why: Your single-cycle core's clock speed is limited by the longest combinational
path (probably through the ALU). Pipelining lets each stage run in one short cycle.
This is the classic computer architecture exercise and deeply educational.
Why now: Mainline Linux's RISC-V kernel is built against `rv32ima` /
`rv64ima` — atomics are not optional for an unmodified kernel build. The
kernel uses LR/SC and AMO for spinlocks, refcounts, and futexes; without
them you maintain a non-standard fork. Adding A before the Linux work means
you discover atomics-related bugs in a small, focused phase rather than
wedged inside a kernel boot.
Why optional: A single-cycle core can run at maybe 50-80 MHz on the Artix-7. That's
plenty for booting Linux. Pipeline if you want to learn, not because you must.
Single-hart implementation: the reservation set is just one register
(reserved address + valid bit). LR sets it; any store to that address (or
context switch) clears it; SC checks it and either commits or returns 1.
AMOs are "read, op, write" sequences that the bus must perform atomically —
on this single-master core that's free; the load/store unit just holds the
bus across the read-modify-write. This becomes meaningful only when DRAM
or DMA enters the picture.
Future role: Final for single-hart. If you ever go multi-hart, the
reservation set and bus become more involved.
---
## Phase 13 — SPI Flash Boot + DRAM
## Phase 13 — Pipeline (Optional but educational)
What: Add an SPI flash controller to boot from the on-board flash (instead of BRAM
initialization). Integrate AMD/Xilinx MIG IP for the DDR3 on the Arty A7. Update
your memory map: flash at boot, copy to DRAM, jump to DRAM.
What: Insert pipeline registers between your stages (fetch|decode|execute|
memory|writeback). Handle data hazards (forwarding/stalling) and control
hazards (branch prediction or pipeline flush).
Why: BRAM is tiny (a few hundred KB on Artix-7 100T). Linux needs megabytes. DRAM
gives you 256MB. Flash gives you persistent storage for the bootloader. This is
how real embedded systems boot.
Why: Your single-cycle core's clock speed is limited by the longest
combinational path (probably through the ALU). Pipelining lets each stage
run in one short cycle. This is the classic computer architecture exercise
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.
---
## Phase 14 — S-Mode, U-Mode, Sv32 Virtual Memory
## Phase 14 — SPI Flash Boot + DRAM
What: Add supervisor and user privilege modes. Implement Sv32 page table walking
(two-level page tables, 4KB pages). Add the satp CSR, page fault exceptions, and
sfence.vma.
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
it at `0x0000_00000x00FF_FFFF` inside the reserved boot aperture.
Integrate AMD/Xilinx MIG IP for the DDR3L on the Arty (256 MB). Map DRAM at
`0x8000_00000x8FFF_FFFF`, replacing the data BRAM in that range.
Why: Linux runs the kernel in S-mode, user programs in U-mode. Virtual memory gives
each process its own address space and protects the kernel from user code. This is the
last major architectural piece before Linux.
Boot flow: reset PC moves to `0x0000_0000` (flash). A small first-stage
copies the kernel/firmware image from flash to DRAM, sets up the trap
vector, and jumps to DRAM.
Why: BRAM is tiny (~600 KB total on Artix-7 100T). Linux needs megabytes.
DRAM gives you 256 MB; flash gives you persistent storage for the
bootloader. This is how real embedded systems boot.
Bus implications: DRAM via MIG has multi-cycle, variable-latency responses.
This is the first time `req_valid`/`req_ready`/`rsp_valid`/`rsp_ready`
actually matter — the handshake you wired in Phase 6.1 finally earns its
keep.
---
## Phase 15 — Linux
## Phase 15 — S-Mode, U-Mode, Sv32 Virtual Memory
What: Port a minimal Linux kernel (or use an existing RISC-V port). Write a device
tree for your SoC. Build an initramfs with BusyBox. Boot to a shell prompt.
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
exceptions (instruction/load/store page-fault), `sfence.vma`, and S-mode
CSRs (`sstatus`, `stvec`, `sepc`, `scause`, `stval`, `sie`, `sip`,
`sscratch`).
Why: This is the summit. A Linux shell running on a CPU you built from scratch.
Why: Linux runs the kernel in S-mode and user programs in U-mode. Virtual
memory gives each process its own address space and protects the kernel
from user code. This is the last big architectural piece before Linux.
Test: a hand-written M-mode "kernel" that maps a U-mode page, traps on
syscall, returns a value, then returns to U-mode. Verify all three privilege
modes round-trip via `mret` / `sret`.
---
## Phase 16 — Linux Boot Contract (SBI / Device Tree / ABI)
What: Lock down everything Linux expects at the moment of `kernel_entry`.
This is a real subproject, not a footnote inside "port Linux".
Required deliverables:
- **Boot register state at kernel entry**: `a0` = hart ID (0 for single-hart),
`a1` = physical address of the device tree blob, `satp = 0`, all other
state per the [RISC-V Linux boot protocol](https://docs.kernel.org/arch/riscv/boot.html).
- **Image alignment**: rv32 kernel image base must be 4 MiB-aligned in
physical memory (rv64 is 2 MiB). Reflect this in the linker layout for the
loaded kernel and the bootloader's copy destination.
- **Device tree**: hand-write a `.dts` describing CPU (with `riscv,isa =
"rv32ima_zicsr_zifencei"`, `mmu-type = "riscv,sv32"`), memory (DRAM base
+ size), CLINT (timer + soft IPI), PLIC (interrupt controller bindings),
and the UART node. Compile with `dtc` to a `.dtb` and ship it in flash
alongside the kernel.
- **UART driver/binding decision**: the split TX/RX/status UART from
Phase 7 is *not* `8250/16550`-compatible. Pick one:
- (a) Add a 16550-subset wrapper (THR/RBR shared at offset 0, LSR at
offset 5, IER at offset 1, FCR/LCR mostly stubs). Then the in-tree
`8250_dw` or `of_serial` driver works with a stock binding.
- (b) Write a small custom Linux serial driver and define a
`compatible = "fpgacore,uart"` binding. More work, more learning.
- **Boot firmware**: choose between
- (a) **Direct M-mode Linux**: use the kernel's `CONFIG_RISCV_M_MODE`
path. Less portable, no SBI required, fewer moving pieces. Reasonable
for a learning bring-up.
- (b) **OpenSBI + S-mode Linux**: build OpenSBI as the M-mode firmware,
Linux runs in S-mode. Standard production path, more Vivado/Linux build
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
debugging exercise on a known-good ABI surface.
---
## Phase 17 — Linux
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
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
scratch.
---
## Quick Reference: What You Need Installed
- Vivado 2025.2 or later (synthesis, simulation, ILA, VIO)
- RISC-V GCC toolchain (`riscv64-unknown-elf-gcc`, multilib build — needs to
support `rv32im_zicsr_zifencei` / `ilp32`)
- `riscv-tests` repo cloned and buildable (Phase 8.5 onward)
- 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).
- `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)
- 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
- The Linux RISC-V boot protocol doc (`Documentation/arch/riscv/boot.rst`)