Files
FPGA-Core/CLAUDE.md
T

11 KiB
Raw Blame History

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 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: 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: mem/*.mem in hex format, one 32-bit word per line.
  • Firmware source: 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

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)
mem/                  — BRAM init files (.mem)
fw/                   — firmware source (C, assembly, linker scripts)
constraints/          — Vivado XDC constraint files
docs/                 — block diagrams, notes

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_00000x00FF_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_00000x8FFF_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.

// 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.