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.
This commit is contained in:
+443
@@ -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
|
||||
Reference in New Issue
Block a user