commit c7b19e8744aa802eab3b2a92cdf4f9d4b2428856 Author: Renat Nurgaliyev Date: Tue Apr 28 10:59:56 2026 +0200 Seed RISC-V core roadmap and project contracts Establish the repository as a documentation-first plan for a custom SystemVerilog RISC-V CPU targeting the Digilent Arty A7 100T. Add the initial README, roadmap, and contributor guidance that define the starting RV32IM direction, Vivado/RISC-V toolchain expectations, basic SystemVerilog conventions, and the phased path from an architecture contract toward a Linux-capable SoC. This commit intentionally contains planning and interface direction only; RTL, firmware, testbenches, and Vivado project files are left for later phases. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d52723d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,72 @@ +# 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.1. + +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_` (e.g., `rv32_alu`, `rv32_decode`, `rv32_regfile`). +- File naming: one module per file, filename matches module name. +- Testbenches: `tb/tb_.sv`. Use Vivado simulator. +- Clock: single clock domain, active rising edge, signal named `clk`. +- Reset: synchronous active-high, signal named `rst`. +- BRAM init files: `mem/*.mem` in hex format, one 32-bit word per line. +- Firmware source: `fw/` directory. Built with riscv64-unknown-elf-gcc, + -march=rv32im -mabi=ilp32. + +## Directory Structure + +``` +rtl/ — synthesizable source +pkg/ — packages (rv32_pkg.sv) +core/ — CPU core modules (ALU, decoder, regfile, datapath) +periph/ — peripherals (UART, timer, etc.) +top/ — top-level and SoC integration +tb/ — testbenches +mem/ — BRAM init files (.mem) +fw/ — firmware source (C, assembly, linker scripts) +constraints/ — Vivado XDC constraint files +docs/ — block diagrams, notes +``` + +## ISA Target + +RV32IM base. Extensions added incrementally: +- Zicsr (CSR instructions) — Phase 9 +- Zifencei (fence.i) — implemented as NOP until caches exist +- M-mode privileged — Phase 9 +- S-mode + U-mode + Sv32 — Phase 14 + +## Key Design Decisions + +- Single-cycle first, pipeline later (Phase 12). Stages are separated in the code + even without pipeline registers between them. +- 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) is in the ALU from the start to avoid rework. +- Harvard architecture (separate instruction and data memory) initially. Unified + memory when DRAM is added. + +## 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. +- 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d2cc742 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# FPGA-Core + +Custom RV32IM RISC-V CPU core built from scratch in SystemVerilog. + +## Target + +- Board: Digilent Arty A7 100T +- Toolchain: Vivado 2025.2.1 +- Language: SystemVerilog +- ISA: RV32IM + Zicsr + Zifencei + M-mode privileged (extending toward Linux) + +## Architecture + +Single-cycle, non-pipelined, Harvard architecture (separate instruction/data BRAM). +Designed for incremental extension — clean stage separation via typed structs so +pipelining and new instructions can be added without structural rework. + +## Memory Map + +| Address Range | Peripheral | +|-----------------------|--------------------| +| 0x00000000-0x0FFFFFFF | BRAM (instr + data)| +| 0x10000000 | UART TX/RX data | +| 0x10000004 | UART status | + +(Will expand as peripherals are added) + +## Building + +Requires: +- Vivado 2025.2.1 +- RISC-V GCC toolchain (`riscv64-unknown-elf-gcc`) +- Serial terminal (minicom/picocom/PuTTY) at 115200 baud + +Open `FPGA-Core.xpr` in Vivado. Synthesis and implementation target the xc7a100tcsg324-1. + +## Roadmap + +See `ROADMAP.md` for the full phased build plan. + +## Current Phase + +Phase 0 — Architecture contract (package definitions + block diagram) + +## References + +- [RISC-V ISA Spec Vol 1 (Unprivileged)](https://riscv.org/technical/specifications/) +- [RISC-V ISA Spec Vol 2 (Privileged)](https://riscv.org/technical/specifications/) +- [Arty A7 Reference Manual](https://digilent.com/reference/programmable-logic/arty-a7/reference-manual) diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..7485b0a --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,443 @@ +# RV32IM CPU Core — Build Roadmap +## Target: Digilent Arty A7 100T / Vivado 2025.2.1 / SystemVerilog +## End goal: Boot Linux on a custom RISC-V core + +--- + +## Phase 0 — Architecture Contract + +### 0.1 — SystemVerilog Package +What: Create a `.sv` package file with enums (ALU operations, opcode types, branch +types) and structs (fetch_out_t, decode_out_t, exec_out_t, mem_out_t) that define +the signals passed between pipeline stages. + +Why: This is the "API" of your CPU. Every module you build will import this package. +If you get the struct definitions reasonable now, adding new instructions later means +adding a field to a struct — not re-plumbing wires across the whole design. + +Future role: These structs stay forever. When you pipeline the core, the pipeline +registers are literally just these structs stored in flip-flops. When you add CSR +instructions, you add a field to decode_out_t. The package grows but never gets +replaced. + +### 0.2 — Block Diagram (on paper) +What: Draw the major blocks (fetch, decode, ALU, register file, memory, writeback) +and the signals between them. Label signals with the struct names from 0.1. + +Why: You need a physical reference to look at while coding. Screen diagrams get +buried in tabs. Paper on a wall stays visible. + +Future role: You'll update this as the design grows. It becomes your architectural +source of truth when things get complex. + +--- + +## Phase 1 — ALU + +### 1.1 — ALU Module + Simulation +What: Build a combinational ALU that handles all RV32I operations (add, sub, and, +or, xor, slt, sltu, shifts) AND the M extension operations (mul, mulh, mulhsu, +mulhu, div, divu, rem, remu). Inputs: two 32-bit operands + operation select from +your enum. Outputs: 32-bit result. + +Why: The ALU is the computational heart. Building it first gives you a self-contained +module to practice your testbench workflow. Including M extension now costs almost +nothing (a few extra case statements) but saves you from touching this module again +later. + +Why M extension from day one: GCC emits multiply/divide constantly. Without it, the +compiler falls back to software emulation via libgcc — slow and painful. Baking it in +now means your first GCC-compiled program just works. + +Testbench focus: signed vs unsigned comparisons (SLT vs SLTU), arithmetic shift +right vs logical shift right, signed overflow, division by zero (RISC-V spec says +specific results, not an exception). + +Future role: This module is final. It goes into the finished core unchanged. + +### 1.2 — ALU on FPGA with VIO +What: Synthesize the ALU on the Arty. Attach Vivado VIO (Virtual I/O) cores to the +inputs and outputs. Use the Vivado hardware manager to feed operands and read results +in real time. + +Why: Confirms the ALU works in real hardware, not just simulation. Gets you +comfortable with the VIO workflow — you'll use it again. Also catches synthesis +issues early (e.g., if your multiply path is too slow for the clock). + +What is VIO: A Vivado IP that lets you poke values into signals and read signals out +through JTAG, right from the Vivado GUI. Think of it as virtual switches and LEDs +but with 32-bit width and no board wiring. + +Future role: VIO familiarity pays off throughout the project. The ALU itself is final. + +--- + +## Phase 2 — Register File + +### 2.1 — Register File Module + Simulation +What: Build the RISC-V register file — 32 registers, each 32 bits wide. Two read +ports (rs1, rs2), one write port (rd). Register x0 is hardwired to zero (writes to +it are ignored, reads always return 0). + +Why: Every instruction reads from and/or writes to registers. This is the second +fundamental building block after the ALU. It's simple (it's basically a small RAM +with some special behavior on address 0) but getting the read/write timing right +matters. + +Testbench focus: read-after-write in the same cycle (does the new value forward?), +write to x0 then read x0 (must be zero), read two different registers simultaneously. + +Future role: This module is final. Might eventually get a third read port if you add +certain extensions, but for RV32IM it's done. + +### 2.2 — Optional: VIO validation +What: Put it on FPGA with VIO if you want extra confidence. Usually sim is enough +for a register file since the logic is simple. + +--- + +## Phase 3 — Decoder + +### 3.1 — Instruction Decoder + Simulation +What: A combinational module that takes a raw 32-bit instruction word and produces +your decode_out_t struct: ALU operation, source/destination register addresses, +immediate value (sign-extended), memory read/write flags, branch type, etc. + +Why: The decoder is the "brain" that tells every other module what to do. RISC-V has +six instruction formats (R, I, S, B, U, J) and the immediate bits are scattered +differently in each. Getting the immediate extraction and sign extension right is the +main challenge. + +How to test: Use the RISC-V toolchain as your reference. Write small assembly +snippets, assemble them with `riscv64-unknown-elf-as`, then `objdump -d` to get the +binary encoding. Feed those encodings to your testbench and verify every output field. +This also gets you familiar with the cross-compilation toolchain you'll need later. + +Testbench focus: Every instruction format. Pay special attention to immediate sign +extension (bit 31 of the instruction is always the sign bit in RISC-V — that's a +deliberate design choice). Verify that the decoder handles all R-type, I-type, S-type, +B-type, U-type, and J-type correctly. + +Future role: When you add CSR instructions (Phase 9), you'll add a new case to the +decoder and a new field to the struct. The structure of the decoder doesn't change. + +--- + +## Phase 4 — First CPU ("It's Alive") + +### 4.1 — Fetch + Datapath Integration +What: Create an instruction BRAM initialized from a .mem file. Add a PC (program +counter) register that starts at 0 and increments by 4 each cycle. Wire the chain: +BRAM[PC] → decoder → ALU → register file writeback. Support only R-type and I-type +arithmetic for now (add, sub, addi, and, or, xor, slt, slti, lui, auipc, shifts). + +Why: This is the first time all your modules work together as a CPU. It can't +branch, it can't access data memory, but it executes a sequence of arithmetic +instructions correctly. The wiring is where most bugs live — wrong bit ranges, +swapped operands, forgetting to connect a signal. + +How to test: Hand-assemble 10-20 instructions into a .mem file. Calculate the +expected register state after each instruction by hand. Compare against what the +CPU actually produces. + +What is a .mem file: A text file with hex values, one per line. Vivado can +initialize BRAMs from these. Each line is one 32-bit instruction. + +Future role: This is your core. Everything from here on adds capabilities to it. + +### 4.2 — ILA Verification on FPGA +What: Synthesize the CPU on the Arty. Attach ILA (Integrated Logic Analyzer) probes +to the PC, register write port, and ALU output. Set a trigger (e.g., when PC reaches +the last instruction). Inspect the captured waveform. + +Why: Confirms hardware behavior matches simulation. Gets you comfortable with ILA, +which becomes your primary debugging tool for all future phases. + +What is ILA: A Vivado IP that captures signal values over time into on-chip memory, +like an oscilloscope for internal FPGA signals. You set trigger conditions and view +waveforms in the Vivado hardware manager. + +Future role: You'll drop ILA probes on different signals throughout the project. +Knowing how to use it well is arguably the most important FPGA debug skill. + +--- + +## Phase 5 — Branches and Jumps (Control Flow) + +### 5.1 — Branch Instructions +What: Add a branch comparator (separate from the ALU — it checks rs1 vs rs2 for +equality, less-than, etc.) and a mux that selects between PC+4 and the branch target. +Implement all B-type branches: beq, bne, blt, bge, bltu, bgeu. + +Why: Without branches, the CPU can only run straight-line code. Branches give you +loops and conditionals — the core of any real program. Keeping the branch comparator +separate from the ALU is a design choice that pays off when you pipeline later (branch +decision can be made in the decode stage without waiting for ALU). + +Test: A loop that increments a register from 0 to 10, then falls through. Verify with +ILA that the branch is taken exactly 10 times and the final register value is 10. + +Future role: When you pipeline, branch handling becomes the source of pipeline hazards +and you'll add branch prediction. But the comparator logic stays. + +### 5.2 — Jump Instructions (jal, jalr) +What: Add jal (jump and link — PC-relative) and jalr (jump and link register — +absolute). Both store PC+4 into the destination register before jumping. + +Why: jal/jalr are how RISC-V implements function calls and returns. `jal` calls a +function, `jalr` returns from it (by jumping to the address saved in the link +register). Without these, you can't have functions — and C is nothing but functions. + +Test: Write a call/return sequence. Main calls a subroutine via jal, subroutine does +some work, returns via jalr. Verify the return address is correct and execution +resumes in the right place. + +Future role: Final. These instructions don't change. + +--- + +## Phase 6 — Load/Store (Data Memory) + +### 6.1 — Word Load/Store (lw, sw) +What: Add a data BRAM and a load/store unit. For now, only 32-bit aligned access +(lw and sw). Define a simple memory bus interface with address, write data, read data, +write enable, and valid/ready signals. + +Why: A CPU without data memory can only work with 32 values (the registers). Load/ +store connects the CPU to the outside world. Starting with word-only access keeps the +byte-lane logic simple while you verify the memory path works. + +Why valid/ready now: BRAM responds in one cycle, so ready is always high. But when +you swap in DRAM later, responses take many cycles. If your interface already has +valid/ready handshaking, the swap is painless. If you hardwire it now, you'll have +to redesign the memory path later. Five minutes of future-proofing saves a weekend +of rework. + +Test: Store values to various addresses, load them back, verify they match. + +Future role: The bus interface is the foundation for your entire memory map (BRAM, +DRAM, UART, timer, interrupt controller — all hang off this bus). + +### 6.2 — Byte and Halfword Access (lb, lbu, lh, lhu, sb, sh) +What: Add byte and halfword loads (signed and unsigned) and stores. This means byte +lane selection (which byte within a 32-bit word) and sign extension (lb sign-extends +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. + +Testbench focus: Sign extension (loading 0xFF as signed byte should give 0xFFFFFFFF, +as unsigned should give 0x000000FF). Unaligned access behavior (RISC-V base spec says +misaligned access can trap — decide if you want to support it or trap). + +Future role: Final. These instructions don't change. + +### 6.3 — ILA on the Memory Bus +What: Attach ILA to the memory bus signals. Run a program that computes Fibonacci +numbers and stores them into a data array. Verify the memory contents. + +Why: Memory bugs are subtle (off-by-one addresses, wrong byte lane, sign extension +errors). ILA on the bus lets you see exactly what's happening each cycle. + +--- + +## Phase 7 — Memory-Mapped UART + +### 7.1 — UART TX Module +What: A standalone UART transmitter. Fixed baud rate (115200 is standard), 8 data +bits, no parity, 1 stop bit (8N1). Interface: input byte, send signal, busy flag. + +Why: This is your CPU's mouth. Once connected, the CPU can print to a serial terminal. +This replaces ILA as your primary debug tool for software — you can printf-debug your +C programs. + +Test standalone: Wrap it in a tiny FSM that sends "hello\n" on reset. Synthesize, +connect to your terminal at 115200 baud. If you want, verify the waveform on your +scope — you'll see actual start bits, data bits, stop bits on the TX pin. + +Future role: This exact module gets memory-mapped in 7.3 and eventually becomes the +console for your BIOS and Linux. + +### 7.2 — UART RX Module +What: A standalone UART receiver. Same 8N1 format. Oversamples the input (typically +16x baud rate) to find bit centers. Outputs received byte + valid flag. + +Why: This is your CPU's ear. Needed for any interactive console — typing commands, +sending data to the board. + +Test: Send bytes from your terminal, verify they appear correctly. If you want to be +extra thorough, use your Eclypse + DAC zmod to generate serial waveforms with +deliberate timing skew and verify the receiver handles it. + +Future role: Same as TX — becomes the console UART. + +### 7.3 — Bus Decoder + Memory-Mapped I/O +What: Add an address decoder to your memory bus. Address range 0x00000000-0x0FFFFFFF +routes to BRAM (instruction + data), address 0x10000000 routes to UART TX (write the +byte to send), 0x10000000 routes to UART RX (read the received byte). Add a status +register at 0x10000004 (TX busy flag, RX data available flag). + +Why: Memory-mapped I/O is how CPUs talk to peripherals in the real world. The CPU +doesn't know it's talking to a UART — it just does a store to an address, and the bus +decoder routes it to the right place. This is the same pattern used by every SoC ever. + +Test: Write a program that stores ASCII bytes to 0x10000000 in a loop. See the +message in your terminal. This is the most important milestone in the project — your +CPU is now a computer that communicates with the outside world through software. + +Future role: The bus decoder grows as you add peripherals (timer, interrupt controller, +DRAM) but the structure stays. UART mapping stays at this address. + +### 7.4 — "Hello World" (Hand-Assembled) +What: Hand-write an assembly program that prints "hello from rv32" to the UART +address. Assemble it, load into BRAM, run. + +Why: Pure emotional milestone. Your CPU, your UART, your program, your message on +screen. + +--- + +## Phase 8 — GCC Toolchain Integration + +### 8.1 — Linker Script + Startup Code +What: Write a linker script that tells GCC where your instruction memory, data memory, +and stack live. Write crt0.S (C runtime startup): set the stack pointer, zero out the +BSS section (uninitialized global variables), call main. + +Why: GCC doesn't just compile C to instructions — it expects a runtime environment. +The linker script defines the memory layout, and crt0 sets up the minimal environment +that C code assumes exists (a stack, zeroed globals). + +Future role: The linker script evolves as your memory map grows (adding DRAM, flash). +crt0 grows when you add CSRs (setting up trap vectors in startup). + +### 8.2 — First GCC Program +What: Write a trivial main() that prints a string to the UART by writing bytes to +0x10000000. Compile with: riscv64-unknown-elf-gcc -march=rv32im -mabi=ilp32 -nostdlib +-T linker.ld -o firmware.elf. Convert: objcopy -O binary firmware.elf firmware.bin. +Convert binary to .mem format. Load into BRAM. Run. + +Why: This proves your CPU is compatible with a real compiler. Any bugs in your +instruction implementation will surface here — GCC will use instructions in +combinations you never thought to hand-test. + +Expect to iterate: GCC will probably emit an instruction you haven't implemented yet. +That's fine — check the illegal instruction, add it, resynthesize. This is normal. + +### 8.3 — Fill Remaining RV32I Gaps +What: Implement any RV32I instructions you deferred. Common ones: all shift variants +(sll, srl, sra, slli, srli, srai), set-less-than variants (slti, sltiu), fence (NOP +for now — you have no cache), ecall/ebreak (NOP for now — you have no trap handling). + +Why: GCC's output will exercise the full ISA. You need complete RV32I coverage for +any non-trivial C code to work. + +Test: Compile progressively more complex C programs. String manipulation, struct +usage, switch statements (these generate jump tables — exercises jalr with computed +addresses), recursive functions. + +### 8.4 — Milestone: Meaningful C Program +What: Write something real — a serial monitor that accepts commands over UART and +responds. Or a tiny Forth interpreter. Something interactive that proves the core +is solid. + +Why: Confidence builder. You now have a working RISC-V computer that runs compiled C +and talks over serial. Everything after this is enrichment. + +--- + +## Phase 9 — CSRs + M-Mode Trap Handling + +What: Add Control and Status Registers (mstatus, mtvec, mepc, mcause, mtval, mie, +mip) and the CSR instructions (csrrw, csrrs, csrrc, csrrwi, csrrsi, csrrci). Add +mret (return from trap). Implement the trap entry mechanism: on an exception or +interrupt, save PC to mepc, jump to mtvec, set mcause. + +Why: Traps and interrupts are how the CPU handles errors (illegal instruction, bad +memory access) and hardware events (timer fired, UART received a byte). No operating +system can function without this. M-mode (machine mode) is the highest privilege +level and the only one you need for bare-metal code and simple kernels. + +Future role: This is the foundation for all OS-level functionality. Linux needs this +plus S-mode (supervisor mode), which you'll add later. + +--- + +## Phase 10 — Timer + +What: Implement mtime (a free-running 64-bit counter) and mtimecmp (comparison +register). When mtime >= mtimecmp, a timer interrupt fires. + +Why: Every operating system needs a timer tick for scheduling. Even bare-metal +firmware needs delays and timeouts. This is your first interrupt source, which also +validates that the trap handling from Phase 9 actually works end-to-end. + +--- + +## Phase 11 — Interrupt Controller + +What: Build a minimal PLIC (Platform-Level Interrupt Controller) or a simplified +version. Connect UART RX as an interrupt source. Implement interrupt priority and +enable/disable. + +Why: Polling the UART wastes CPU cycles. With interrupts, the CPU does other work +and only handles UART when data arrives. A real system has many interrupt sources — +the PLIC manages them. This is required for Linux. + +--- + +## Phase 12 — Pipeline (Optional but educational) + +What: Insert pipeline registers between your stages (fetch|decode|execute|memory| +writeback). Handle data hazards (forwarding/stalling) and control hazards (branch +prediction or pipeline flush). + +Why: Your single-cycle core's clock speed is limited by the longest combinational +path (probably through the ALU). Pipelining lets each stage run in one short cycle. +This is the classic computer architecture exercise and deeply educational. + +Why optional: A single-cycle core can run at maybe 50-80 MHz on the Artix-7. That's +plenty for booting Linux. Pipeline if you want to learn, not because you must. + +--- + +## Phase 13 — SPI Flash Boot + DRAM + +What: Add an SPI flash controller to boot from the on-board flash (instead of BRAM +initialization). Integrate AMD/Xilinx MIG IP for the DDR3 on the Arty A7. Update +your memory map: flash at boot, copy to DRAM, jump to DRAM. + +Why: BRAM is tiny (a few hundred KB on Artix-7 100T). Linux needs megabytes. DRAM +gives you 256MB. Flash gives you persistent storage for the bootloader. This is +how real embedded systems boot. + +--- + +## Phase 14 — S-Mode, U-Mode, Sv32 Virtual Memory + +What: Add supervisor and user privilege modes. Implement Sv32 page table walking +(two-level page tables, 4KB pages). Add the satp CSR, page fault exceptions, and +sfence.vma. + +Why: Linux runs the kernel in S-mode, user programs in U-mode. Virtual memory gives +each process its own address space and protects the kernel from user code. This is the +last major architectural piece before Linux. + +--- + +## Phase 15 — Linux + +What: Port a minimal Linux kernel (or use an existing RISC-V port). Write a device +tree for your SoC. Build an initramfs with BusyBox. Boot to a shell prompt. + +Why: This is the summit. A Linux shell running on a CPU you built from scratch. + +--- + +## Quick Reference: What You Need Installed + +- Vivado 2025.2.1 (synthesis, simulation, ILA, VIO) +- RISC-V GCC toolchain (riscv64-unknown-elf-gcc) +- Terminal program (minicom, picocom, or PuTTY) for UART +- Text editor you like for SystemVerilog +- The RISC-V ISA spec (Volume 1: Unprivileged, Volume 2: Privileged) — free PDFs