Tighten memory bus and trap ownership contracts

Convert the memory-bus guidance from a loose list of request/response fields into
packed mem_req_t and mem_rsp_t payload structs carried by separate valid/ready
handshakes. Spell out that the same channel shape is instantiated independently
for the instruction bus and data bus so the design remains Harvard at the bus
level while still sharing a single contract.

Clarify fault ownership before RTL exists: slaves only raise rsp.err for access
faults they can own, the LSU detects misaligned loads/stores before issuing a
D-bus request, the decoder owns illegal instructions and ebreak/ecall decode
events, and the future MMU owns page faults before translated requests reach the
bus.

Update the roadmap to use the structured bus field names, correct the
misalignment tests and GCC illegal-instruction explanation, and prefer modern
RISC-V device-tree ISA properties with an optional legacy riscv,isa string for
older kernels.
This commit is contained in:
2026-04-28 12:00:39 +02:00
parent bcbf1fa616
commit 8edfc86027
2 changed files with 154 additions and 62 deletions
+51 -32
View File
@@ -1,4 +1,4 @@
# RV32IM CPU Core — Build Roadmap
# RISC-V CPU Core — Build Roadmap
## Target: Digilent Arty A7 100T / Vivado 2025.2+ / SystemVerilog
## Clock: 50 MHz (100 MHz xtal ÷ 2 via MMCM)
## End goal: Boot Linux on a custom RISC-V core
@@ -241,20 +241,23 @@ Future role: Final. These instructions don't change.
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 same
interface without rework. Required signals (see `CLAUDE.md` for the full
spec):
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. 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:
- `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`
- `req_valid` (master→slave) / `req_ready` (slave→master) + `mem_req_t req`
- `rsp_valid` (slave→master) / `rsp_ready` (master→slave) + `mem_rsp_t rsp`
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.
`mem_req_t` carries `addr`, `we`, `size`, `wdata`, `wstrb`, `amo`.
`mem_rsp_t` carries `rdata`, `err`.
For Phase 6.1 specifically: `req.size` is always `2'b10` (word),
`req.wstrb` is `4'b1111` on stores, `req.amo` is `4'h0`, `rsp.err` is
unused (BRAM never faults). The fields exist in the struct 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 →
@@ -284,21 +287,30 @@ 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.
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`
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.
trap.
Critically, the LSU detects misalignment **locally, before issuing a bus
request** — it has the opcode size and the low address bits available the
moment the instruction is decoded. No misaligned request is ever placed on
the D-bus. The LSU raises the architectural cause directly: mcause 4 for a
load (`lh`/`lw` with bad alignment) and mcause 6 for a store/AMO. This
keeps `rsp.err` reserved for slave-reported faults (unmapped, peripheral
access violations) and matches how the trap classification table in
`CLAUDE.md` divides responsibility.
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`.
0xFFFFFFFF, as unsigned should give 0x000000FF). Byte lane selection
(storing 0xAB at address `0x8000_0001` must update only that byte).
Misaligned `lh`/`lw`/`sh`/`sw` produce the right mcause without ever
touching the bus.
Future role: Final. These instructions don't change.
@@ -407,12 +419,13 @@ Convert: `objcopy -O binary firmware.elf firmware.bin`. Convert binary to
`.mem` format. Load into BRAM. Run.
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.
is correct for Phase 8 because CSRs and `fence.i` aren't decoded yet. If the
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.
Note on GCC 11+: newer toolchains require `_zicsr` / `_zifencei` in the
march string only when source code actually uses CSR or `fence.i`
@@ -618,11 +631,17 @@ Required deliverables:
- **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.
- **Device tree**: hand-write a `.dts` describing CPU, memory (DRAM base +
size), CLINT (timer + soft IPI), PLIC (interrupt controller bindings),
and the UART node. For the CPU node, prefer the **modern** ISA properties:
```
riscv,isa-base = "rv32i";
riscv,isa-extensions = "i", "m", "a", "zicsr", "zifencei";
mmu-type = "riscv,sv32";
```
Optionally include the legacy `riscv,isa = "rv32ima_zicsr_zifencei"`
string for older kernels that don't yet parse the split form. 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