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
|
## 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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user