Broaden the documented end target from RV32IM plus machine-mode support to a Linux-capable RV32IMA core with Zicsr, Zifencei, M/S/U privilege, and Sv32. Add atomics as a required Phase 12 milestone, move the optional pipeline and memory-system work later, and introduce explicit Linux bring-up preparation for boot ABI, device tree, UART driver compatibility, OpenSBI versus direct M-mode boot, and kernel/initramfs handoff. Tighten compiler guidance so the advertised -march string follows the hardware that is actually decoded: rv32im for early bare-metal work, then rv32im_zicsr_zifencei after CSR/fence.i support, and rv32ima_zicsr_zifencei once atomics land. The roadmap also calls out loud illegal-instruction halts instead of silently treating unsupported operations as NOPs.
7.5 KiB
CLAUDE.md
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.
The goal is incremental development from a single-cycle core to 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'sCK_RSTbutton is active-low; the top-level wraps it through a 2-FF synchronizer and inverts to produce the internalrst. The rest of the design only sees synchronous active-high. - BRAM init files:
mem/*.memin hex format, one 32-bit word per line. - Firmware source:
fw/directory. Built withriscv64-unknown-elf-gcc(multilib build required). March string evolves with the implemented ISA:- Phase 8 (pre-CSR bare-metal):
-march=rv32im -mabi=ilp32 - Phase 9+ (CSRs/Zifencei decoded):
-march=rv32im_zicsr_zifencei -mabi=ilp32 - Phase 12+ (atomics):
-march=rv32ima_zicsr_zifencei -mabi=ilp32Don'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.
- Phase 8 (pre-CSR bare-metal):
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 9
- Zifencei (
fence.i) — decoded but NOP until caches exist (Phase 14+) - A (atomics: LR/SC + AMO) — Phase 12 (required for mainline Linux)
- S-mode + U-mode + Sv32 — Phase 15
Key Design Decisions
- Single-cycle first, pipeline later (Phase 13, 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/donehandshake. 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 14
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 14
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 16
(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, 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.
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)
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
Notes:
req_valid/req_readyandrsp_valid/rsp_readyare independent handshakes; a slave may take multiple cycles between accepting a request and producing a response (BRAM = 1 cycle, DRAM = many).req_wstrbis mandatory from Phase 6.2 onward (sub-word stores). For Phase 6.1 (word-only), tie strobes to4'b1111on stores.rsp_errlets 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_amoopcode field rather than a new bus.
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.