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
+103 -30
View File
@@ -2,10 +2,11 @@
## Project Overview ## Project Overview
This is a custom RISC-V RV32IM CPU core written in SystemVerilog, targeting the This is a custom RISC-V CPU core written in SystemVerilog, targeting the
Digilent Arty A7 100T (xc7a100tcsg324-1) with Vivado 2025.2 or later. Digilent Arty A7 100T (xc7a100tcsg324-1) with Vivado 2025.2 or later. It
starts as RV32IM and grows toward **RV32IMA + Zicsr + Zifencei + M/S/U
privilege + Sv32** for a Linux-capable SoC.
The goal is incremental development from a single-cycle core to a Linux-capable SoC.
See ROADMAP.md for the full phased plan. See ROADMAP.md for the full phased plan.
## Conventions ## Conventions
@@ -104,38 +105,110 @@ or write a custom Linux serial driver + device-tree binding.
## Memory Bus Contract ## Memory Bus Contract
Single channel, valid/ready handshake, defined once in `rv32_pkg.sv` and Single channel shape, valid/ready handshake, defined once in `rv32_pkg.sv`.
reused by every master/slave (CPU, instruction BRAM, data BRAM, MMIO, Each channel has two parts: a **payload struct** (single-direction) and a
DRAM later). Master drives a request, slave drives a response. loose **valid/ready handshake pair** (one signal in each direction). The
payload structs are packed and directional; valid/ready stays outside the
struct to avoid mixing directions.
``` The same channel shape is **instanced twice** — once for the I-bus
Request (master → slave): (fetch ↔ instruction memory) and once for the D-bus (LSU ↔ data memory and
req_valid : logic — request is valid this cycle MMIO). Harvard at the bus level, not just at the BRAM level.
req_ready : logic — slave accepts (driven by slave)
req_addr : logic [31:0] — byte address
req_we : logic — 1 = write, 0 = read
req_size : logic [1:0] — 00=byte, 01=halfword, 10=word
req_wdata : logic [31:0] — write data (lane-aligned per wstrb)
req_wstrb : logic [3:0] — byte enables (writes only)
Response (slave → master): ```systemverilog
rsp_valid : logic — response is valid this cycle // Payload — master → slave
rsp_ready : logic — master accepts (driven by master) typedef struct packed {
rsp_rdata : logic [31:0] — read data (reads only) logic [31:0] addr;
rsp_err : logic — fault: unmapped, misaligned, access violation logic we; // 1 = write, 0 = read (always 0 on I-bus)
logic [1:0] size; // 00=byte, 01=halfword, 10=word
logic [31:0] wdata; // lane-aligned per wstrb
logic [3:0] wstrb; // byte enables (writes only)
logic [3:0] amo; // AMO op encoding (D-bus only, Phase 12+)
} mem_req_t;
// Payload — slave → master
typedef struct packed {
logic [31:0] rdata; // valid on reads only
logic err; // slave-reported access fault
} mem_rsp_t;
// Per-channel signals (instance once for I-bus, once for D-bus):
// logic <ch>_req_valid; // master → slave
// logic <ch>_req_ready; // slave → master
// mem_req_t <ch>_req; // master → slave
//
// logic <ch>_rsp_valid; // slave → master
// logic <ch>_rsp_ready; // master → slave
// mem_rsp_t <ch>_rsp; // slave → master
``` ```
Notes: If you later prefer a SystemVerilog `interface`, use master/slave modports
- `req_valid`/`req_ready` and `rsp_valid`/`rsp_ready` are independent to enforce direction. Until then, loose signals + payload structs are the
handshakes; a slave may take multiple cycles between accepting a request simplest path that survives every Vivado quirk.
and producing a response (BRAM = 1 cycle, DRAM = many).
- `req_wstrb` is mandatory from Phase 6.2 onward (sub-word stores). For Handshake notes:
- Standard valid/ready: a beat transfers when both `valid` and `ready` are
high in the same cycle. Request and response handshakes are independent;
a slave may take multiple cycles between accepting a request and
producing a response (BRAM = 1 cycle, DRAM = many).
- `wstrb` is mandatory from Phase 6.2 onward (sub-word stores). For
Phase 6.1 (word-only), tie strobes to `4'b1111` on stores. Phase 6.1 (word-only), tie strobes to `4'b1111` on stores.
- `rsp_err` lets the bus signal misalignment, unmapped MMIO, and (later) - `amo` is unused (drive `4'h0`) until Phase 12; on the I-bus it is always
page faults uniformly. The CPU's trap logic (Phase 9) consumes it as unused.
load/store access-fault exceptions. - `err` is **only** raised by a slave to report access faults it owns:
- Atomics (Phase 12) extend this with an `req_amo` opcode field rather than unmapped address, peripheral-access violation, eventually DRAM ECC fault.
a new bus. It does NOT carry misalignment (the LSU detects that locally and never
issues), illegal instructions (decode event, not bus), or page faults
(MMU generates them before the bus). Keeping `err` to one bit is fine
because the master that issued the transaction has all the context
needed to classify it.
### I-bus / D-bus instance plan
Both buses use the same `mem_req_t` / `mem_rsp_t` struct, instanced
separately. Per-phase progression:
| Phase | I-bus | D-bus |
|-------|--------------------------------|---------------------------------------------|
| 4 | CPU fetch ↔ instruction BRAM | (none yet) |
| 6 | unchanged | LSU ↔ data BRAM (single slave) |
| 7 | unchanged | LSU ↔ decoder → {data BRAM, UART MMIO} |
| 9-12 | unchanged | + timer (Phase 10), PLIC (Phase 11) |
| 14 | I-bus → arbiter → DRAM | D-bus → arbiter → DRAM (or MIG dual-port) |
Both buses can fault independently; classification happens in the masters
(see below).
### Trap cause classification (who owns which mcause)
`rsp.err` is one bit on the wire. The **master** that issued the
transaction adds context to produce the precise architectural cause:
| Source | mcause | Detected by |
|-------------------------------------|--------|-----------------------------------|
| Instruction address misaligned | 0 | fetch unit (PC[1:0] != 0) |
| Instruction access fault | 1 | fetch unit (I-bus `rsp.err`) |
| Illegal instruction | 2 | decoder (`illegal_instr`) |
| Breakpoint (`ebreak`) | 3 | decoder |
| Load address misaligned | 4 | LSU (size + addr LSBs, pre-issue) |
| Load access fault | 5 | LSU (D-bus `rsp.err` on read) |
| Store/AMO address misaligned | 6 | LSU (size + addr LSBs, pre-issue) |
| Store/AMO access fault | 7 | LSU (D-bus `rsp.err` on write) |
| Ecall from M-mode | 11 | decoder |
| Instruction page fault | 12 | I-side MMU (Phase 15) |
| Load page fault | 13 | D-side MMU (Phase 15) |
| Store/AMO page fault | 15 | D-side MMU (Phase 15) |
Two consequences:
- The LSU detects misalignment **before** issuing a bus request — never
send a request you already know will trap.
- Page faults (Phase 15) are raised by the MMU during translation,
before the (translated) request hits the bus.
### Atomics (Phase 12)
LR/SC and AMO use the `amo` field of `mem_req_t` (D-bus only) rather than
a separate bus. Single-hart reservation tracking lives in the LSU.
## When Helping With This Project ## When Helping With This Project
+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 ## Target: Digilent Arty A7 100T / Vivado 2025.2+ / SystemVerilog
## Clock: 50 MHz (100 MHz xtal ÷ 2 via MMCM) ## Clock: 50 MHz (100 MHz xtal ÷ 2 via MMCM)
## End goal: Boot Linux on a custom RISC-V core ## 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. 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). 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. Define the full memory bus contract here, not just the subset Phase 6.1
Defining it once means Phase 6.2 (sub-word), Phase 7 (MMIO), Phase 9 (access uses. Defining it once means Phase 6.2 (sub-word), Phase 7 (MMIO), Phase 9
faults), Phase 12 (atomics), and Phase 14 (DRAM) all plug into the same (access faults), Phase 12 (atomics), and Phase 14 (DRAM) all plug into the
interface without rework. Required signals (see `CLAUDE.md` for the full same interface without rework. The contract (see `CLAUDE.md` for the full
spec): 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 - `req_valid` (master→slave) / `req_ready` (slave→master) + `mem_req_t req`
- `rsp_valid` / `rsp_ready` — response handshake (independent) - `rsp_valid` (slave→master) / `rsp_ready` (master→slave) + `mem_rsp_t rsp`
- `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` `mem_req_t` carries `addr`, `we`, `size`, `wdata`, `wstrb`, `amo`.
is `4'b1111` on stores, `rsp_err` is unused (BRAM never faults). The signals `mem_rsp_t` carries `rdata`, `err`.
exist in the bundle from day one even when tied off.
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 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 → 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 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. byte-by-byte. You can't run real C code without these.
Implementation: the load/store unit drives `req_size` (00=byte, 01=halfword, 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 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` written). On load, the unit muxes the right byte/halfword out of `rsp.rdata`
and applies sign/zero extension based on the opcode. and applies sign/zero extension based on the opcode.
Decision for this project: **trap on misaligned access** rather than support Decision for this project: **trap on misaligned access** rather than support
it in hardware. Hardware support for misaligned word access on Artix-7 is 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 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 trap.
turns that into a load/store address-misaligned exception.
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 Testbench focus: Sign extension (loading 0xFF as signed byte should give
0xFFFFFFFF, as unsigned should give 0x000000FF). Byte lane selection (storing 0xFFFFFFFF, as unsigned should give 0x000000FF). Byte lane selection
0xAB at address `0x8000_0001` must update only that byte). Misaligned access (storing 0xAB at address `0x8000_0001` must update only that byte).
returns `rsp_err`. Misaligned `lh`/`lw`/`sh`/`sw` produce the right mcause without ever
touching the bus.
Future role: Final. These instructions don't change. 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. `.mem` format. Load into BRAM. Run.
March string discipline: advertise only what the hardware decodes. `rv32im` 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 is correct for Phase 8 because CSRs and `fence.i` aren't decoded yet. If the
the program (or crt0) uses CSR/fence.i ops before that, the bus will return program (or crt0) emits a CSR or `fence.i` op before then, the **decoder**
`rsp_err` and Phase 8.3's illegal-instruction halt will catch it visibly — asserts `illegal_instr` and the Phase 8.3 halt latches the offending PC and
much better than NOPing through a bug. Switch to `rv32im_zicsr_zifencei` in instruction word — catching the bug visibly rather than NOPing through it.
Phase 9 once CSRs land, and `rv32ima_zicsr_zifencei` in Phase 12 once (Illegal instructions are a decode event, not a bus event; the memory bus
atomics land. 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 Note on GCC 11+: newer toolchains require `_zicsr` / `_zifencei` in the
march string only when source code actually uses CSR or `fence.i` 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 - **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 physical memory (rv64 is 2 MiB). Reflect this in the linker layout for the
loaded kernel and the bootloader's copy destination. loaded kernel and the bootloader's copy destination.
- **Device tree**: hand-write a `.dts` describing CPU (with `riscv,isa = - **Device tree**: hand-write a `.dts` describing CPU, memory (DRAM base +
"rv32ima_zicsr_zifencei"`, `mmu-type = "riscv,sv32"`), memory (DRAM base size), CLINT (timer + soft IPI), PLIC (interrupt controller bindings),
+ size), CLINT (timer + soft IPI), PLIC (interrupt controller bindings), and the UART node. For the CPU node, prefer the **modern** ISA properties:
and the UART node. Compile with `dtc` to a `.dtb` and ship it in flash ```
alongside the kernel. 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 - **UART driver/binding decision**: the split TX/RX/status UART from
Phase 7 is *not* `8250/16550`-compatible. Pick one: Phase 7 is *not* `8250/16550`-compatible. Pick one:
- (a) Add a 16550-subset wrapper (THR/RBR shared at offset 0, LSR at - (a) Add a 16550-subset wrapper (THR/RBR shared at offset 0, LSR at