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:
+189
-49
@@ -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_0000–0x00FF_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_0000–0x8FFF_FFFF in Phase 13
|
||||
0x8000_0000 – 0x8000_FFFF data BRAM (64 KB) → DRAM 0x8000_0000–0x8FFF_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_0000–0x00FF_FFFF` inside the reserved boot aperture.
|
||||
Integrate AMD/Xilinx MIG IP for the DDR3L on the Arty (256 MB). Map DRAM at
|
||||
`0x8000_0000–0x8FFF_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`)
|
||||
|
||||
Reference in New Issue
Block a user