diff --git a/CLAUDE.md b/CLAUDE.md index e98e079..eb9eac9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 _req_valid; // master → slave +// logic _req_ready; // slave → master +// mem_req_t _req; // master → slave +// +// logic _rsp_valid; // slave → master +// logic _rsp_ready; // master → slave +// mem_rsp_t _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 diff --git a/ROADMAP.md b/ROADMAP.md index f7fabae..6543808 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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