246 lines
12 KiB
Markdown
246 lines
12 KiB
Markdown
# CLAUDE.md
|
||
|
||
## Project Overview
|
||
|
||
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.
|
||
|
||
See ROADMAP.md for the full phased plan.
|
||
|
||
## Conventions
|
||
|
||
- Language: SystemVerilog (not Verilog). Use SV features: packages, structs, enums,
|
||
always_comb, always_ff, logic (not reg/wire).
|
||
- All inter-stage signals are defined as structs in `FPGA/rtl/pkg/rv32_pkg.sv`.
|
||
Always import this package. When adding new functionality, extend the existing
|
||
structs rather than adding loose wires.
|
||
- Module naming: `rv32_<block>` (e.g., `rv32_alu`, `rv32_decode`, `rv32_regfile`).
|
||
- File naming: one module per file, filename matches module name.
|
||
- Testbenches: `FPGA/tb/tb_<module>.sv`. Use Vivado simulator.
|
||
- Clock: single clock domain, active rising edge, signal named `clk`. Target
|
||
frequency: 50 MHz (derived from the Arty's 100 MHz oscillator via MMCM/2).
|
||
- Reset: synchronous active-high, signal named `rst`. The Arty's `CK_RST` button
|
||
is active-low; the top-level wraps it through a 2-FF synchronizer and inverts
|
||
to produce the internal `rst`. The rest of the design only sees synchronous
|
||
active-high.
|
||
- BRAM init files: `FPGA/mem/*.mem` in hex format, one 32-bit word per line.
|
||
- Firmware source: `Software/fw/` directory. Built with `riscv64-unknown-elf-gcc`
|
||
(multilib build required). March string evolves with the implemented ISA:
|
||
- Phase 8 (pre-CSR bare-metal): `-march=rv32im -mabi=ilp32`
|
||
- Phase 12+ (CSRs/Zifencei decoded): `-march=rv32im_zicsr_zifencei -mabi=ilp32`
|
||
- Phase 15+ (atomics): `-march=rv32ima_zicsr_zifencei -mabi=ilp32`
|
||
Don't advertise an extension before its ops are decoded — the compiler is
|
||
free to emit them, and "trap as illegal" early is much better than NOPing.
|
||
|
||
## Directory Structure
|
||
|
||
```
|
||
FPGA/ — everything that ends up on the FPGA
|
||
rtl/ — synthesizable source
|
||
pkg/ — packages (rv32_pkg.sv)
|
||
core/ — CPU core modules (ALU, M unit, decoder, regfile, datapath)
|
||
periph/ — peripherals (UART, timer, PLIC, etc.)
|
||
top/ — top-level and SoC integration
|
||
tb/ — testbenches (tb_<module>.sv)
|
||
constraints/ — Vivado XDC constraint files
|
||
mem/ — BRAM init files (.mem)
|
||
vivado/ — Vivado project workspace (regenerated, not committed)
|
||
only create_project.tcl is committed; .xpr and
|
||
*.runs / *.cache / *.hw / *.sim / *.gen / *.srcs /
|
||
*.ip_user_files are git-ignored
|
||
Software/ — software that runs on the CPU
|
||
fw/ — bare-metal firmware, crt0, linker scripts (Phase 8+)
|
||
bios/ — BIOS / monitor + ELF loader (Phase 9, 10)
|
||
kernel/ — tiny custom kernel, later Linux artifacts (Phase 11, 19, 20)
|
||
Docs/ — block diagrams, architectural notes
|
||
```
|
||
|
||
The Vivado project lives in `FPGA/vivado/`, not at the repo root. The `.xpr`
|
||
itself is **not** committed — `create_project.tcl` is the source of truth, and
|
||
each developer regenerates the project locally with
|
||
`vivado -mode batch -source FPGA/vivado/create_project.tcl`. RTL and testbench
|
||
files are added to the project as **references** to `../rtl/...` and
|
||
`../tb/...` so the actual source of truth stays in version control. All
|
||
generated state (`*.xpr`, `*.runs/`, `*.cache/`, `*.hw/`, `*.sim/`,
|
||
`*.ip_user_files/`, `*.gen/`, `*.srcs/`) is git-ignored.
|
||
|
||
## ISA Target
|
||
|
||
End target: **RV32IMA + Zicsr + Zifencei + M/S/U privilege + Sv32** (Linux-capable).
|
||
|
||
Extensions land incrementally:
|
||
- RV32IM base — Phases 1-8
|
||
- Zicsr (CSR instructions) + M-mode trap handling — Phase 12
|
||
- Zifencei (`fence.i`) — decoded but NOP until caches exist (Phase 17+)
|
||
- A (atomics: LR/SC + AMO) — Phase 15 (required for mainline Linux)
|
||
- S-mode + U-mode + Sv32 — Phase 18
|
||
|
||
## Key Design Decisions
|
||
|
||
- Single-cycle first, pipeline later (Phase 16, optional). Stages are separated
|
||
in the code even without pipeline registers between them.
|
||
- "Single-cycle" is a logical model, not a literal one cycle per instruction.
|
||
BRAM has a 1-cycle read latency, so fetch is registered and loads take 2
|
||
cycles. The datapath stalls fetch while a multi-cycle operation is in flight.
|
||
- Memory bus uses valid/ready handshake from day one, even though BRAM always
|
||
responds in one cycle. This is for future DRAM compatibility.
|
||
- M extension (multiply/divide) lives in a dedicated multi-cycle M unit
|
||
alongside the combinational ALU, with a `start`/`busy`/`done` handshake.
|
||
Multiply is 2-3 cycles via DSP48s; divide is iterative (~33 cycles). The
|
||
datapath stalls when the M unit is busy. Building this from day one avoids
|
||
retrofitting stall logic later.
|
||
- Harvard architecture (separate instruction and data memory) initially. Unified
|
||
memory when DRAM is added.
|
||
|
||
## Memory Map
|
||
|
||
```
|
||
0x0000_0000 – 0x0FFF_FFFF reserved boot aperture (256 MB decode region)
|
||
actual SPI flash device is 16 MB (Arty A7),
|
||
mapped at 0x0000_0000–0x00FF_FFFF in Phase 17
|
||
0x1000_0000 – 0x1000_0FFF MMIO (UART; later: timer, PLIC)
|
||
0x2000_0000 – 0x2000_FFFF instruction BRAM (64 KB)
|
||
0x8000_0000 – 0x8000_FFFF data BRAM (64 KB) → grows to DRAM 0x8000_0000–0x8FFF_FFFF in Phase 17
|
||
```
|
||
|
||
Reset PC = `0x2000_0000`. Linker script anchors text/rodata at `0x2000_0000`
|
||
and data/bss/stack at `0x8000_0000` from Phase 8 onward.
|
||
|
||
UART register layout (split, not 16550-style):
|
||
```
|
||
0x1000_0000 TX data (W: byte to send)
|
||
0x1000_0004 RX data (R: pops one byte from RX FIFO)
|
||
0x1000_0008 status (R: bit0 = tx_busy, bit1 = rx_valid)
|
||
```
|
||
|
||
This is fine for bare-metal firmware but is *not* directly Linux-compatible:
|
||
the in-tree `8250/16550` driver expects a different register layout. Phase 19
|
||
(Linux Boot Contract) revisits this — either add a 16550-compatible wrapper
|
||
or write a custom Linux serial driver + device-tree binding.
|
||
|
||
## Memory Bus Contract
|
||
|
||
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.
|
||
|
||
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.
|
||
|
||
```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 15+)
|
||
} 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
|
||
```
|
||
|
||
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.
|
||
- `amo` is unused (drive `4'h0`) until Phase 15; 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} |
|
||
| 12-15 | unchanged | + timer (Phase 13), PLIC (Phase 14) |
|
||
| 17 | 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 18) |
|
||
| Load page fault | 13 | D-side MMU (Phase 18) |
|
||
| Store/AMO page fault | 15 | D-side MMU (Phase 18) |
|
||
|
||
Two consequences:
|
||
- The LSU detects misalignment **before** issuing a bus request — never
|
||
send a request you already know will trap.
|
||
- Page faults (Phase 18) are raised by the MMU during translation,
|
||
before the (translated) request hits the bus.
|
||
|
||
### Atomics (Phase 15)
|
||
|
||
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
|
||
|
||
- Always check rv32_pkg.sv first to understand current struct definitions and enums.
|
||
- Reference the RISC-V ISA spec for instruction encoding and behavior.
|
||
- Prefer simulation-testable solutions. Every module should be verifiable standalone
|
||
before integration.
|
||
- Don't add Verilog-style code (reg, wire, always @). Use SV equivalents.
|
||
- When suggesting fixes, consider that the design is single-cycle — there are no
|
||
pipeline hazards yet, but the M unit and BRAM both introduce stalls.
|
||
- ISA correctness is verified against `riscv-tests` (rv32ui, rv32um) starting at
|
||
Phase 8.5. Hand-written testbenches are for module-level checks; the official
|
||
suite is the source of truth for instruction behavior.
|
||
- Vivado quirks: use (* dont_touch = "true" *) for signals that Vivado optimizes
|
||
away during debug. ILA/VIO probes need to be on nets that survive synthesis.
|
||
- The Arty A7 100T has: 101,440 logic cells, 4,860 Kbit BRAM, 240 DSP slices,
|
||
256MB DDR3L SDRAM, USB-UART bridge, 4 LEDs, 4 switches, 4 buttons.
|