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:
@@ -2,10 +2,11 @@
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is a custom RISC-V RV32IM CPU core written in SystemVerilog, targeting the
|
||||
Digilent Arty A7 100T (xc7a100tcsg324-1) with Vivado 2025.2 or later.
|
||||
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. 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.
|
||||
|
||||
## Conventions
|
||||
@@ -104,38 +105,110 @@ or write a custom Linux serial driver + device-tree binding.
|
||||
|
||||
## Memory Bus Contract
|
||||
|
||||
Single channel, valid/ready handshake, defined once in `rv32_pkg.sv` and
|
||||
reused by every master/slave (CPU, instruction BRAM, data BRAM, MMIO,
|
||||
DRAM later). Master drives a request, slave drives a response.
|
||||
Single channel shape, valid/ready handshake, defined once in `rv32_pkg.sv`.
|
||||
Each channel has two parts: a **payload struct** (single-direction) and a
|
||||
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.
|
||||
|
||||
```
|
||||
Request (master → slave):
|
||||
req_valid : logic — request is valid this cycle
|
||||
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)
|
||||
The same channel shape is **instanced twice** — once for the I-bus
|
||||
(fetch ↔ instruction memory) and once for the D-bus (LSU ↔ data memory and
|
||||
MMIO). Harvard at the bus level, not just at the BRAM level.
|
||||
|
||||
Response (slave → master):
|
||||
rsp_valid : logic — response is valid this cycle
|
||||
rsp_ready : logic — master accepts (driven by master)
|
||||
rsp_rdata : logic [31:0] — read data (reads only)
|
||||
rsp_err : logic — fault: unmapped, misaligned, access violation
|
||||
```systemverilog
|
||||
// Payload — master → slave
|
||||
typedef struct packed {
|
||||
logic [31:0] addr;
|
||||
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:
|
||||
- `req_valid`/`req_ready` and `rsp_valid`/`rsp_ready` are independent
|
||||
handshakes; a slave may take multiple cycles between accepting a request
|
||||
and producing a response (BRAM = 1 cycle, DRAM = many).
|
||||
- `req_wstrb` is mandatory from Phase 6.2 onward (sub-word stores). For
|
||||
If you later prefer a SystemVerilog `interface`, use master/slave modports
|
||||
to enforce direction. Until then, loose signals + payload structs are the
|
||||
simplest path that survives every Vivado quirk.
|
||||
|
||||
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.
|
||||
- `rsp_err` lets the bus signal misalignment, unmapped MMIO, and (later)
|
||||
page faults uniformly. The CPU's trap logic (Phase 9) consumes it as
|
||||
load/store access-fault exceptions.
|
||||
- Atomics (Phase 12) extend this with an `req_amo` opcode field rather than
|
||||
a new bus.
|
||||
- `amo` is unused (drive `4'h0`) until Phase 12; on the I-bus it is always
|
||||
unused.
|
||||
- `err` is **only** raised by a slave to report access faults it owns:
|
||||
unmapped address, peripheral-access violation, eventually DRAM ECC fault.
|
||||
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
|
||||
|
||||
|
||||
+51
-32
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user