Compare commits

...

10 Commits

Author SHA1 Message Date
imple 72c970fc20 Ignore local build artifacts 2026-04-28 14:31:37 +02:00
imple 329610807e Add FPGA project workspace layout 2026-04-28 14:30:50 +02:00
imple e8631501e8 Add BIOS and tiny kernel roadmap phases 2026-04-28 13:32:31 +02:00
imple 07dd8e21f0 Label roadmap phase difficulty 2026-04-28 13:23:03 +02:00
imple 91ca63cf73 Replace stale tutorial reference links 2026-04-28 12:56:05 +02:00
imple fdf9292b8a Refine tutorial roadmap references 2026-04-28 12:40:53 +02:00
imple c1ffb0ee41 Remove stale phase status from README
Drop the Current Phase section because it is a progress marker that would go stale or create noisy commits as the roadmap advances.

Add a minimal .gitignore for local Codex state only, keeping broader ignore rules out until the repo actually needs them.
2026-04-28 12:24:05 +02:00
imple b008b37d49 Add phase-by-phase tutorial notes
Add a Tutorial tree that mirrors the roadmap from Phase 0 through Linux bring-up.
Each phase and subphase gets a short learning note with consistent sections for
context, goals, new concepts, mental model, learning tasks, pitfalls, tooling,
testing, and references.

The tutorial material is intentionally explanatory rather than implementation
code. It gives a systems-oriented learner enough FPGA, SystemVerilog, RISC-V,
firmware, and Linux bring-up context to approach each roadmap phase without
turning the notes into copy-paste RTL.
2026-04-28 12:11:23 +02:00
imple 8edfc86027 Tighten memory bus and trap ownership contracts
Convert the memory-bus guidance from a loose list of request/response fields into
packed mem_req_t and mem_rsp_t payload structs carried by separate valid/ready
handshakes. Spell out that the same channel shape is instantiated independently
for the instruction bus and data bus so the design remains Harvard at the bus
level while still sharing a single contract.

Clarify fault ownership before RTL exists: slaves only raise rsp.err for access
faults they can own, the LSU detects misaligned loads/stores before issuing a
D-bus request, the decoder owns illegal instructions and ebreak/ecall decode
events, and the future MMU owns page faults before translated requests reach the
bus.

Update the roadmap to use the structured bus field names, correct the
misalignment tests and GCC illegal-instruction explanation, and prefer modern
RISC-V device-tree ISA properties with an optional legacy riscv,isa string for
older kernels.
2026-04-28 12:00:39 +02:00
imple bcbf1fa616 Align the roadmap with a Linux-capable RV32IMA target
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.
2026-04-28 11:51:22 +02:00
68 changed files with 3021 additions and 112 deletions
+25
View File
@@ -0,0 +1,25 @@
# Local Codex state
.codex
# OS / filesystem junk
.DS_Store
Thumbs.db
desktop.ini
# Editor / IDE state
.vscode/
.idea/
*.swp
*.swo
*~
# Vivado scratch dropped in the current working directory whenever Vivado is
# invoked from anywhere other than FPGA/vivado/ (e.g. running create_project.tcl
# from the repo root).
.Xil/
vivado*.jou
vivado*.log
vivado*.backup.*
webtalk*.jou
webtalk*.log
usage_statistics_webtalk.*
+162 -24
View File
@@ -2,61 +2,85 @@
## Project Overview ## Project Overview
This is a custom RISC-V RV32IM CPU core written in SystemVerilog, targeting the 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. 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.
The goal is incremental development from a single-cycle core to a Linux-capable SoC.
See ROADMAP.md for the full phased plan. See ROADMAP.md for the full phased plan.
## Conventions ## Conventions
- Language: SystemVerilog (not Verilog). Use SV features: packages, structs, enums, - Language: SystemVerilog (not Verilog). Use SV features: packages, structs, enums,
always_comb, always_ff, logic (not reg/wire). always_comb, always_ff, logic (not reg/wire).
- All inter-stage signals are defined as structs in `rtl/pkg/rv32_pkg.sv`. Always - All inter-stage signals are defined as structs in `FPGA/rtl/pkg/rv32_pkg.sv`.
import this package. When adding new functionality, extend the existing structs Always import this package. When adding new functionality, extend the existing
rather than adding loose wires. structs rather than adding loose wires.
- Module naming: `rv32_<block>` (e.g., `rv32_alu`, `rv32_decode`, `rv32_regfile`). - Module naming: `rv32_<block>` (e.g., `rv32_alu`, `rv32_decode`, `rv32_regfile`).
- File naming: one module per file, filename matches module name. - File naming: one module per file, filename matches module name.
- Testbenches: `tb/tb_<module>.sv`. Use Vivado simulator. - Testbenches: `FPGA/tb/tb_<module>.sv`. Use Vivado simulator.
- Clock: single clock domain, active rising edge, signal named `clk`. Target - 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). 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 - 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 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 to produce the internal `rst`. The rest of the design only sees synchronous
active-high. active-high.
- BRAM init files: `mem/*.mem` in hex format, one 32-bit word per line. - BRAM init files: `FPGA/mem/*.mem` in hex format, one 32-bit word per line.
- Firmware source: `fw/` directory. Built with `riscv64-unknown-elf-gcc` - Firmware source: `Software/fw/` directory. Built with `riscv64-unknown-elf-gcc`
(multilib build required), `-march=rv32im_zicsr_zifencei -mabi=ilp32`. The (multilib build required). March string evolves with the implemented ISA:
full march string is required by GCC 11+ even before CSRs are implemented; - Phase 8 (pre-CSR bare-metal): `-march=rv32im -mabi=ilp32`
`Zifencei` is a NOP until caches exist (Phase 13+). - 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 ## Directory Structure
``` ```
FPGA/ — everything that ends up on the FPGA
rtl/ — synthesizable source rtl/ — synthesizable source
pkg/ — packages (rv32_pkg.sv) pkg/ — packages (rv32_pkg.sv)
core/ — CPU core modules (ALU, M unit, decoder, regfile, datapath) core/ — CPU core modules (ALU, M unit, decoder, regfile, datapath)
periph/ — peripherals (UART, timer, PLIC, etc.) periph/ — peripherals (UART, timer, PLIC, etc.)
top/ — top-level and SoC integration top/ — top-level and SoC integration
tb/ — testbenches (tb_<module>.sv) tb/ — testbenches (tb_<module>.sv)
mem/ — BRAM init files (.mem)
fw/ — firmware source (C, assembly, linker scripts)
constraints/ — Vivado XDC constraint files constraints/ — Vivado XDC constraint files
docs/ block diagrams, notes 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 ## ISA Target
RV32IM base. Extensions added incrementally: End target: **RV32IMA + Zicsr + Zifencei + M/S/U privilege + Sv32** (Linux-capable).
- Zicsr (CSR instructions) — Phase 9
- Zifencei (fence.i) — implemented as NOP until caches exist Extensions land incrementally:
- M-mode privileged — Phase 9 - RV32IM base — Phases 1-8
- S-mode + U-mode + Sv32 — Phase 14 - 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 ## Key Design Decisions
- Single-cycle first, pipeline later (Phase 12). Stages are separated in the code - Single-cycle first, pipeline later (Phase 16, optional). Stages are separated
even without pipeline registers between them. in the code even without pipeline registers between them.
- "Single-cycle" is a logical model, not a literal one cycle per instruction. - "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 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. cycles. The datapath stalls fetch while a multi-cycle operation is in flight.
@@ -73,10 +97,12 @@ RV32IM base. Extensions added incrementally:
## Memory Map ## Memory Map
``` ```
0x0000_0000 0x0FFF_FFFF reserved (future SPI flash boot region, Phase 13) 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) 0x1000_0000 0x1000_0FFF MMIO (UART; later: timer, PLIC)
0x2000_0000 0x2000_FFFF instruction BRAM (64 KB) 0x2000_0000 0x2000_FFFF instruction BRAM (64 KB)
0x8000_0000 0x8000_FFFF data BRAM (64 KB) → grows to DRAM 0x8000_00000x8FFF_FFFF in Phase 13 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` Reset PC = `0x2000_0000`. Linker script anchors text/rodata at `0x2000_0000`
@@ -89,6 +115,118 @@ UART register layout (split, not 16550-style):
0x1000_0008 status (R: bit0 = tx_busy, bit1 = rx_valid) 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 ## When Helping With This Project
- Always check rv32_pkg.sv first to understand current struct definitions and enums. - Always check rv32_pkg.sv first to understand current struct definitions and enums.
+6
View File
@@ -0,0 +1,6 @@
# Docs/
Architectural notes and the Phase 0.2 block diagram. The diagram is the
visual source of truth for the datapath — draw on paper first, then capture a
digital copy here (e.g. `block-diagram.drawio` exported to SVG/PNG) once it
stabilizes. Update it whenever a stage-boundary struct or bus instance changes.
+4
View File
@@ -0,0 +1,4 @@
# FPGA/
Everything that ends up on the FPGA: SystemVerilog source, testbenches,
constraints, memory init files, and the Vivado project.
+4
View File
@@ -0,0 +1,4 @@
# FPGA/constraints/
Vivado XDC constraint files: pin assignments for the Arty A7, clock
definitions, and timing constraints.
+4
View File
@@ -0,0 +1,4 @@
# FPGA/mem/
BRAM init files (`.mem`) in hex format, one 32-bit word per line. Loaded by
`$readmemh` into instruction/data BRAM at elaboration.
+4
View File
@@ -0,0 +1,4 @@
# FPGA/rtl/
Synthesizable SystemVerilog source. One module per file, filename matches
module name (e.g. `rv32_alu.sv`).
+4
View File
@@ -0,0 +1,4 @@
# FPGA/rtl/core/
CPU core modules: ALU, M unit, decoder, register file, fetch, LSU, datapath,
and (later) CSR file and MMU. Module names follow `rv32_<block>`.
+4
View File
@@ -0,0 +1,4 @@
# FPGA/rtl/periph/
Memory-mapped peripherals: UART (Phase 7), CLINT timer (Phase 13), PLIC
(Phase 14), and any future devices. Each peripheral is a slave on the D-bus.
+4
View File
@@ -0,0 +1,4 @@
# FPGA/rtl/pkg/
Shared SystemVerilog packages — the type contract for the design.
`rv32_pkg.sv` (enums, stage-boundary structs, bus payloads) lives here.
+5
View File
@@ -0,0 +1,5 @@
# FPGA/rtl/top/
Top-level modules and SoC integration: clock/reset wrapping (MMCM, reset
synchronizer), bus arbitration, address decode, and the board-level top that
maps to the Arty A7 pins.
+4
View File
@@ -0,0 +1,4 @@
# FPGA/tb/
SystemVerilog testbenches, one per module under test. Naming: `tb_<module>.sv`.
Run with the Vivado simulator (xsim).
+34
View File
@@ -0,0 +1,34 @@
# Vivado-generated project state. The committed source of truth is
# create_project.tcl — everything below (including the .xpr itself, which
# embeds machine-specific paths) is regenerated by running that script.
# The project file itself — regenerated from create_project.tcl
*.xpr
# Project-wide generated directories (named after the .xpr basename)
*.cache/
*.hw/
*.sim/
*.runs/
*.ip_user_files/
*.gen/
*.srcs/
# Vivado scratch / temp
.Xil/
xsim.dir/
# Logs, journals, autosave backups
*.jou
*.log
*.str
*.backup.*
*.wdb
*.pb
vivado*.jou
vivado*.log
# Webtalk telemetry
webtalk*.jou
webtalk*.log
usage_statistics_webtalk.*
+19
View File
@@ -0,0 +1,19 @@
# FPGA/vivado/
The Vivado project workspace. The committed source of truth is
`create_project.tcl`; the project file (`FPGA-Core.xpr`) and all of Vivado's
generated state (`*.runs/`, `*.cache/`, `*.hw/`, `*.sim/`, `*.ip_user_files/`,
`*.gen/`, `*.srcs/`) are git-ignored — `.xpr` files embed machine-specific
paths and are regenerated locally.
RTL and testbench files are added to the project as **references** to
`../rtl/...` / `../tb/...` so the actual source of truth stays in version
control, not inside the project file.
To generate the project, run from the repo root:
```sh
vivado -mode batch -source FPGA/vivado/create_project.tcl
```
Then open the resulting `FPGA/vivado/FPGA-Core.xpr` in the Vivado GUI.
+43
View File
@@ -0,0 +1,43 @@
# create_project.tcl — generate FPGA/vivado/FPGA-Core.xpr
#
# Usage (from the repo root):
# vivado -mode batch -source FPGA/vivado/create_project.tcl
#
# Or from inside FPGA/vivado/:
# vivado -mode batch -source create_project.tcl
#
# Then open the resulting .xpr in the Vivado GUI. To regenerate from scratch,
# delete FPGA-Core.xpr and its sibling *.cache / *.runs / *.hw / *.sim /
# *.ip_user_files / *.gen directories, then re-run.
set script_dir [file normalize [file dirname [info script]]]
set fpga_dir [file normalize "$script_dir/.."]
set proj_name "FPGA-Core"
set part "xc7a100tcsg324-1"
if {[file exists "$script_dir/$proj_name.xpr"]} {
puts "Project already exists: $script_dir/$proj_name.xpr"
puts "Delete it (and its sibling *.cache, *.runs, ... dirs) first."
return
}
create_project $proj_name $script_dir -part $part
set_property target_language Verilog [current_project]
set_property simulator_language Mixed [current_project]
# RTL and testbenches are added as references (not copied) so the source of
# truth stays in git, not inside the .xpr. Passing a directory makes Vivado
# recursively pick up all HDL files under it.
add_files "$fpga_dir/rtl"
add_files -fileset sim_1 "$fpga_dir/tb"
set xdc_files [glob -nocomplain "$fpga_dir/constraints/*.xdc"]
if {[llength $xdc_files] > 0} {
add_files -fileset constrs_1 $xdc_files
}
update_compile_order -fileset sources_1
update_compile_order -fileset sim_1
puts "Created $script_dir/$proj_name.xpr"
+23 -12
View File
@@ -1,13 +1,15 @@
# FPGA-Core # FPGA-Core
Custom RV32IM RISC-V CPU core built from scratch in SystemVerilog. Custom RISC-V CPU core built from scratch in SystemVerilog. Starts as RV32IM,
grows to RV32IMA + Sv32 + M/S/U privilege en route to booting Linux.
## Target ## Target
- Board: Digilent Arty A7 100T (xc7a100tcsg324-1) - Board: Digilent Arty A7 100T (xc7a100tcsg324-1)
- Toolchain: Vivado 2025.2 or later - Toolchain: Vivado 2025.2 or later
- Language: SystemVerilog - Language: SystemVerilog
- ISA: RV32IM + Zicsr + Zifencei + M-mode privileged (extending toward Linux) - ISA: end target **RV32IMA + Zicsr + Zifencei + M/S/U privilege + Sv32**
(Linux-capable). Built incrementally — see `ROADMAP.md`.
- Clock: 50 MHz (100 MHz xtal ÷ 2 via MMCM) - Clock: 50 MHz (100 MHz xtal ÷ 2 via MMCM)
## Architecture ## Architecture
@@ -17,16 +19,16 @@ Custom RV32IM RISC-V CPU core built from scratch in SystemVerilog.
unit (DSP-based multiply, iterative divide) sit side by side; the datapath unit (DSP-based multiply, iterative divide) sit side by side; the datapath
stalls on M-unit and BRAM accesses. Inter-stage signals are typed structs in stalls on M-unit and BRAM accesses. Inter-stage signals are typed structs in
`rv32_pkg.sv` — the same structs become pipeline registers when the core is `rv32_pkg.sv` — the same structs become pipeline registers when the core is
pipelined later (Phase 12). pipelined later (Phase 16, optional).
## Memory Map ## Memory Map
| Address Range | Region | | Address Range | Region |
|---------------------------|----------------------------------------------| |---------------------------|----------------------------------------------|
| `0x0000_00000x0FFF_FFFF` | reserved (SPI flash boot region, Phase 13) | | `0x0000_00000x0FFF_FFFF` | reserved boot aperture (Phase 17: 16 MB SPI flash at low end) |
| `0x1000_00000x1000_0FFF` | MMIO (UART; later: timer, PLIC) | | `0x1000_00000x1000_0FFF` | MMIO (UART; later: timer, PLIC) |
| `0x2000_00000x2000_FFFF` | instruction BRAM (64 KB) | | `0x2000_00000x2000_FFFF` | instruction BRAM (64 KB) |
| `0x8000_00000x8000_FFFF` | data BRAM (64 KB) → DRAM in Phase 13 | | `0x8000_00000x8000_FFFF` | data BRAM (64 KB) → DRAM in Phase 17 |
Reset PC = `0x2000_0000`. Reset PC = `0x2000_0000`.
@@ -42,21 +44,30 @@ UART registers (split layout):
Requires: Requires:
- Vivado 2025.2 or later - Vivado 2025.2 or later
- RISC-V GCC toolchain — multilib `riscv64-unknown-elf-gcc` supporting - RISC-V GCC toolchain — multilib `riscv64-unknown-elf-gcc`. March string
`-march=rv32im_zicsr_zifencei -mabi=ilp32` evolves with the implementation: `rv32im` (Phase 8) →
`rv32im_zicsr_zifencei` (Phase 12) → `rv32ima_zicsr_zifencei` (Phase 15).
- `riscv-tests` (for compliance verification from Phase 8.5) - `riscv-tests` (for compliance verification from Phase 8.5)
- Serial terminal (minicom/picocom/PuTTY) at 115200 8N1 - Serial terminal (minicom/picocom/PuTTY) at 115200 8N1
Open `FPGA-Core.xpr` in Vivado. Synthesis and implementation target the xc7a100tcsg324-1. FPGA sources (RTL, testbenches, constraints, BRAM init files, and the Vivado
project script) live under `FPGA/`. Software that runs on the core (firmware,
BIOS, kernels) lives under `Software/`. The Vivado project itself is **not**
committed — generate it from the script and then open it:
```sh
vivado -mode batch -source FPGA/vivado/create_project.tcl
# then open FPGA/vivado/FPGA-Core.xpr in the Vivado GUI
```
Synthesis and implementation target the xc7a100tcsg324-1. RTL sources in
`FPGA/rtl/` and testbenches in `FPGA/tb/` are added to the project as
references — do not let Vivado copy them in.
## Roadmap ## Roadmap
See `ROADMAP.md` for the full phased build plan. See `ROADMAP.md` for the full phased build plan.
## Current Phase
Phase 0 — Architecture contract (package definitions + block diagram)
## References ## References
- [RISC-V ISA Spec Vol 1 (Unprivileged)](https://riscv.org/technical/specifications/) - [RISC-V ISA Spec Vol 1 (Unprivileged)](https://riscv.org/technical/specifications/)
+361 -69
View File
@@ -1,17 +1,42 @@
# RV32IM CPU Core — Build Roadmap # RISC-V CPU Core — Build Roadmap
## Target: Digilent Arty A7 100T / Vivado 2025.2+ / SystemVerilog ## Target: Digilent Arty A7 100T / Vivado 2025.2+ / SystemVerilog
## Clock: 50 MHz (100 MHz xtal ÷ 2 via MMCM) ## Clock: 50 MHz (100 MHz xtal ÷ 2 via MMCM)
## End goal: Boot Linux on a custom RISC-V core ## End goal: Boot Linux on a custom RISC-V core
--- ---
## Memory Map (target — fixed in Phase 0, evolves through Phase 13) ## Scope Guide
Each phase is marked with a learning-scope label:
- **Easy/fun** — good weekend/evening work. These phases have visible progress,
tight feedback loops, and form a complete worthwhile project even if you stop
before the Linux work.
- **Hard stretch** — still rewarding, but expect deeper debugging, more spec
reading, and longer gaps between visible milestones.
- **Overkill/hard** — Linux-capable SoC work. Valuable if you want the full
summit, but too much to treat as the baseline definition of success.
Recommended casual target: reach Phase 8, where GCC-built C runs on your CPU and
`riscv-tests` gives you confidence in the ISA implementation. Everything after
that is enrichment or a long-term expedition.
Recommended middle-game target: continue through Phases 9-11 and build your own
BIOS, ELF loader, and tiny kernel before attempting Linux. This gives you a
playable computer with your own firmware and command line without requiring the
full Linux boot contract.
---
## Memory Map (target — fixed in Phase 0, evolves through Phase 17)
``` ```
0x0000_0000 0x0FFF_FFFF reserved (SPI flash boot region, Phase 13) 0x0000_0000 0x0FFF_FFFF reserved boot aperture (256 MB decode region;
actual SPI flash is 16 MB on Arty A7,
mapped at 0x0000_00000x00FF_FFFF in Phase 17)
0x1000_0000 0x1000_0FFF MMIO (UART; later: timer, PLIC) 0x1000_0000 0x1000_0FFF MMIO (UART; later: timer, PLIC)
0x2000_0000 0x2000_FFFF instruction BRAM (64 KB) 0x2000_0000 0x2000_FFFF instruction BRAM (64 KB)
0x8000_0000 0x8000_FFFF data BRAM (64 KB) → DRAM 0x8000_00000x8FFF_FFFF in Phase 13 0x8000_0000 0x8000_FFFF data BRAM (64 KB) → DRAM 0x8000_00000x8FFF_FFFF in Phase 17
``` ```
Reset PC = `0x2000_0000`. Locking this in now keeps the linker script and crt0 Reset PC = `0x2000_0000`. Locking this in now keeps the linker script and crt0
@@ -19,7 +44,7 @@ stable across phases.
--- ---
## Phase 0 — Architecture Contract ## Phase 0 — Architecture Contract [Easy/fun]
### 0.1 — SystemVerilog Package ### 0.1 — SystemVerilog Package
What: Create a `.sv` package file with enums (ALU operations, opcode types, branch What: Create a `.sv` package file with enums (ALU operations, opcode types, branch
@@ -47,7 +72,7 @@ source of truth when things get complex.
--- ---
## Phase 1 — ALU + M Unit ## Phase 1 — ALU + M Unit [Easy/fun]
### 1.1 — Combinational ALU + Simulation ### 1.1 — Combinational ALU + Simulation
What: Build a combinational ALU that handles all RV32I arithmetic/logic ops What: Build a combinational ALU that handles all RV32I arithmetic/logic ops
@@ -99,7 +124,7 @@ Future role: VIO familiarity pays off throughout the project.
--- ---
## Phase 2 — Register File ## Phase 2 — Register File [Easy/fun]
### 2.1 — Register File Module + Simulation ### 2.1 — Register File Module + Simulation
What: Build the RISC-V register file — 32 registers, each 32 bits wide. Two read What: Build the RISC-V register file — 32 registers, each 32 bits wide. Two read
@@ -123,7 +148,7 @@ for a register file since the logic is simple.
--- ---
## Phase 3 — Decoder ## Phase 3 — Decoder [Easy/fun]
### 3.1 — Instruction Decoder + Simulation ### 3.1 — Instruction Decoder + Simulation
What: A combinational module that takes a raw 32-bit instruction word and produces What: A combinational module that takes a raw 32-bit instruction word and produces
@@ -145,12 +170,12 @@ extension (bit 31 of the instruction is always the sign bit in RISC-V — that's
deliberate design choice). Verify that the decoder handles all R-type, I-type, S-type, deliberate design choice). Verify that the decoder handles all R-type, I-type, S-type,
B-type, U-type, and J-type correctly. 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 Future role: When you add CSR instructions (Phase 12), you'll add a new case to the
decoder and a new field to the struct. The structure of the decoder doesn't change. decoder and a new field to the struct. The structure of the decoder doesn't change.
--- ---
## Phase 4 — First CPU ("It's Alive") ## Phase 4 — First CPU ("It's Alive") [Easy/fun]
### 4.1 — Fetch + Datapath Integration ### 4.1 — Fetch + Datapath Integration
What: Create an instruction BRAM initialized from a .mem file, mapped at What: Create an instruction BRAM initialized from a .mem file, mapped at
@@ -197,7 +222,7 @@ Knowing how to use it well is arguably the most important FPGA debug skill.
--- ---
## Phase 5 — Branches and Jumps (Control Flow) ## Phase 5 — Branches and Jumps (Control Flow) [Easy/fun]
### 5.1 — Branch Instructions ### 5.1 — Branch Instructions
What: Add a branch comparator (separate from the ALU — it checks rs1 vs rs2 for What: Add a branch comparator (separate from the ALU — it checks rs1 vs rs2 for
@@ -233,13 +258,29 @@ Future role: Final. These instructions don't change.
--- ---
## Phase 6 — Load/Store (Data Memory) ## Phase 6 — Load/Store (Data Memory) [Easy/fun]
### 6.1 — Word Load/Store (lw, sw) ### 6.1 — Word Load/Store (lw, sw)
What: Add a data BRAM mapped at `0x8000_0000` (64 KB) and a load/store unit. What: Add a data BRAM mapped at `0x8000_0000` (64 KB) and a load/store unit.
For now, only 32-bit aligned access (lw and sw). Define a simple memory bus For now, only 32-bit aligned access (lw and sw).
interface with address, write data, read data, write enable, and valid/ready
signals. Define the full memory bus contract here, not just the subset Phase 6.1
uses. Defining it once means Phase 6.2 (sub-word), Phase 7 (MMIO), Phase 12
(access faults), Phase 15 (atomics), and Phase 17 (DRAM) all plug into the
same interface without rework. The contract (see `CLAUDE.md` for the full
spec) is two payload structs (`mem_req_t`, `mem_rsp_t`) plus loose
valid/ready signals on each channel:
- `req_valid` (master→slave) / `req_ready` (slave→master) + `mem_req_t req`
- `rsp_valid` (slave→master) / `rsp_ready` (master→slave) + `mem_rsp_t rsp`
`mem_req_t` carries `addr`, `we`, `size`, `wdata`, `wstrb`, `amo`.
`mem_rsp_t` carries `rdata`, `err`.
For Phase 6.1 specifically: `req.size` is always `2'b10` (word),
`req.wstrb` is `4'b1111` on stores, `req.amo` is `4'h0`, `rsp.err` is
unused (BRAM never faults). The fields exist in the struct from day one
even when tied off.
Note: BRAM is 1-cycle read latency, so a load takes one extra cycle beyond Note: BRAM is 1-cycle read latency, so a load takes one extra cycle beyond
the EXECUTE state. The control FSM extends to FETCH → EXECUTE → MEM_WAIT → the EXECUTE state. The control FSM extends to FETCH → EXECUTE → MEM_WAIT →
@@ -269,9 +310,30 @@ to 32 bits, lbu zero-extends).
Why: C uses char (byte) and short (halfword) types constantly. String operations are 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. byte-by-byte. You can't run real C code without these.
Testbench focus: Sign extension (loading 0xFF as signed byte should give 0xFFFFFFFF, Implementation: the load/store unit drives `req.size` (00=byte, 01=halfword,
as unsigned should give 0x000000FF). Unaligned access behavior (RISC-V base spec says 10=word) and `req.wstrb` (which byte lane within the 32-bit word is being
misaligned access can trap — decide if you want to support it or trap). written). On load, the unit muxes the right byte/halfword out of `rsp.rdata`
and applies sign/zero extension based on the opcode.
Decision for this project: **trap on misaligned access** rather than support
it in hardware. Hardware support for misaligned word access on Artix-7 is
expensive (two BRAM cycles + merge logic) and the kernel can emulate via
trap.
Critically, the LSU detects misalignment **locally, before issuing a bus
request** — it has the opcode size and the low address bits available the
moment the instruction is decoded. No misaligned request is ever placed on
the D-bus. The LSU raises the architectural cause directly: mcause 4 for a
load (`lh`/`lw` with bad alignment) and mcause 6 for a store/AMO. This
keeps `rsp.err` reserved for slave-reported faults (unmapped, peripheral
access violations) and matches how the trap classification table in
`CLAUDE.md` divides responsibility.
Testbench focus: Sign extension (loading 0xFF as signed byte should give
0xFFFFFFFF, as unsigned should give 0x000000FF). Byte lane selection
(storing 0xAB at address `0x8000_0001` must update only that byte).
Misaligned `lh`/`lw`/`sh`/`sw` produce the right mcause without ever
touching the bus.
Future role: Final. These instructions don't change. Future role: Final. These instructions don't change.
@@ -284,7 +346,7 @@ errors). ILA on the bus lets you see exactly what's happening each cycle.
--- ---
## Phase 7 — Memory-Mapped UART ## Phase 7 — Memory-Mapped UART [Easy/fun]
### 7.1 — UART TX Module ### 7.1 — UART TX Module
What: A standalone UART transmitter. Baud rate 115200, 8 data bits, no parity, What: A standalone UART transmitter. Baud rate 115200, 8 data bits, no parity,
@@ -353,7 +415,7 @@ screen.
--- ---
## Phase 8 — GCC Toolchain Integration ## Phase 8 — GCC Toolchain Integration [Easy/fun]
### 8.1 — Linker Script + Startup Code ### 8.1 — Linker Script + Startup Code
What: Write a linker script that tells GCC where your instruction memory, data What: Write a linker script that tells GCC where your instruction memory, data
@@ -373,16 +435,25 @@ crt0 grows when you add CSRs (setting up trap vectors in startup).
What: Write a trivial main() that prints a string to the UART by writing bytes What: Write a trivial main() that prints a string to the UART by writing bytes
to `0x1000_0000`. Compile with: to `0x1000_0000`. Compile with:
``` ```
riscv64-unknown-elf-gcc -march=rv32im_zicsr_zifencei -mabi=ilp32 \ riscv64-unknown-elf-gcc -march=rv32im -mabi=ilp32 \
-nostdlib -T linker.ld -o firmware.elf crt0.S main.c -nostdlib -T linker.ld -o firmware.elf crt0.S main.c
``` ```
Convert: `objcopy -O binary firmware.elf firmware.bin`. Convert binary to Convert: `objcopy -O binary firmware.elf firmware.bin`. Convert binary to
`.mem` format. Load into BRAM. Run. `.mem` format. Load into BRAM. Run.
Why the long march string: GCC 11+ split `Zicsr` and `Zifencei` out of the March string discipline: advertise only what the hardware decodes. `rv32im`
RV32I base. `-march=rv32im` alone fails or warns. `Zifencei` is harmless is correct for Phase 8 because CSRs and `fence.i` aren't decoded yet. If the
(`fence.i` is a NOP until you have caches); `Zicsr` will only be emitted by program (or crt0) emits a CSR or `fence.i` op before then, the **decoder**
code that uses CSR ops, so it's safe to advertise even before Phase 9. asserts `illegal_instr` and the Phase 8.3 halt latches the offending PC and
instruction word — catching the bug visibly rather than NOPing through it.
(Illegal instructions are a decode event, not a bus event; the memory bus
is not involved.) Switch to `rv32im_zicsr_zifencei` in Phase 12 once CSRs
land, and `rv32ima_zicsr_zifencei` in Phase 15 once atomics land.
Note on GCC 11+: newer toolchains require `_zicsr` / `_zifencei` in the
march string only when source code actually uses CSR or `fence.i`
instructions (since those moved out of base RV32I in the 2019 spec). Pure
arithmetic + UART-poke C does not need them.
Why: This proves your CPU is compatible with a real compiler. Any bugs in your 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 instruction implementation will surface here — GCC will use instructions in
@@ -392,21 +463,33 @@ Expect to iterate: GCC will probably emit an instruction you haven't implemented
That's fine — check the illegal instruction, add it, resynthesize. This is normal. That's fine — check the illegal instruction, add it, resynthesize. This is normal.
### 8.3 — Fill Remaining RV32I Gaps ### 8.3 — Fill Remaining RV32I Gaps
What: Implement any RV32I instructions you deferred. Common ones: all shift variants What: Implement any RV32I instructions you deferred. Common ones: all shift
(sll, srl, sra, slli, srli, srai), set-less-than variants (slti, sltiu), fence (NOP variants (sll, srl, sra, slli, srli, srai), set-less-than variants (slti,
for now — you have no cache), ecall/ebreak (NOP for now — you have no trap handling). sltiu), `fence` (decode as NOP — there is no cache yet, so memory ordering
is trivially satisfied).
Why: GCC's output will exercise the full ISA. You need complete RV32I coverage for `ecall` / `ebreak` / unrecognized opcodes / `fence.i` / any CSR op all go
any non-trivial C code to work. to a single illegal-instruction handler that **halts the core and asserts a
testbench-visible `illegal_instr` signal** with the offending PC and
instruction word latched. Do NOT decode them as NOPs — GCC emits `ebreak`
in `__builtin_trap`, abort paths, and some divide-by-zero configurations,
and silently NOPing past those destroys debuggability. The halt becomes a
real exception in Phase 12 (`mcause = 2` illegal, `mcause = 3` ebreak,
`mcause = 11` ecall-from-M).
Why: GCC's output will exercise the full ISA. You need complete RV32I
coverage for any non-trivial C code to work, and you need loud failures —
not silent ones — for the instructions you haven't implemented yet.
Test: Compile progressively more complex C programs. String manipulation, struct Test: Compile progressively more complex C programs. String manipulation, struct
usage, switch statements (these generate jump tables — exercises jalr with computed usage, switch statements (these generate jump tables — exercises jalr with computed
addresses), recursive functions. addresses), recursive functions.
### 8.4 — Milestone: Meaningful C Program ### 8.4 — Milestone: Meaningful Standalone C Program
What: Write something real — a serial monitor that accepts commands over UART What: Write something real but still self-contained — a checksum demo, tiny
and responds. Or a tiny Forth interpreter. Something interactive that proves benchmark, string/array exercise, or simple UART-driven calculator. Keep the
the core is solid. full serial monitor for Phase 9, where it becomes the BIOS instead of a one-off
test program.
Why: Confidence builder. You now have a working RISC-V computer that runs Why: Confidence builder. You now have a working RISC-V computer that runs
compiled C and talks over serial. Everything after this is enrichment. compiled C and talks over serial. Everything after this is enrichment.
@@ -422,7 +505,7 @@ BRAM, runs the core until the test signals completion, and reports pass/fail.
Why: Hand-written testbenches catch the bugs you thought to look for. The Why: Hand-written testbenches catch the bugs you thought to look for. The
official suite catches the ones you didn't — corner cases in shifts, sign official suite catches the ones you didn't — corner cases in shifts, sign
extension on byte loads, immediate decoding for every format, M-extension extension on byte loads, immediate decoding for every format, M-extension
overflow cases. Doing this *before* Phase 9 means you're building trap overflow cases. Doing this *before* Phase 12 means you're building trap
handling on a known-good ISA implementation, not stacking unknowns. handling on a known-good ISA implementation, not stacking unknowns.
Expect to find bugs. That's the point. Fix each one, re-run the suite, move Expect to find bugs. That's the point. Fix each one, re-run the suite, move
@@ -433,7 +516,116 @@ becomes regression coverage for the rest of the project.
--- ---
## Phase 9 — CSRs + M-Mode Trap Handling ## Phase 9 — GCC-Built BIOS / Serial Monitor [Easy/fun]
What: Build a small BIOS in freestanding C/assembly with its own linker script,
startup code, UART driver, and command loop. It runs from the existing BRAM
image and gives you an interactive prompt over serial.
Suggested commands:
- `help` — list commands
- `peek <addr>` / `poke <addr> <value>` — inspect and edit memory-mapped state
- `dump <addr> <len>` — hex-dump memory
- `fill <addr> <len> <value>` — initialize memory
- `regs` — print a software-maintained register/trap snapshot when available
- `memtest` — run a small RAM/BRAM test
- `load` — receive a raw binary or hex stream over UART
- `run <addr>` — jump to a loaded program
- `reboot` — return to reset or spin until manual reset
Important hardware contract: a UART-loaded program needs somewhere executable
to live. In the early Harvard design, instruction BRAM is fetch-only unless you
deliberately expose a writable path. Pick one simple learning-friendly option:
- make instruction BRAM dual-port, with the CPU fetch path on one port and a
D-bus write/debug port on the other;
- add a small "program RAM" window that is writable through the D-bus and
fetchable through the I-bus;
- or defer `run`/ELF execution until you add one of those executable write paths.
Why: This is the first point where the board feels like your own computer. It
also becomes a practical debug tool for later hardware and firmware work.
Test: Boot to a prompt, use `poke`/`peek` on UART registers and RAM, load a tiny
raw program, jump to it, and have it return or print a message.
Future role: The BIOS can remain as a recovery/debug monitor even after flash,
DRAM, and Linux enter the picture.
---
## Phase 10 — Minimal ELF Loader [Easy/fun]
What: Teach the BIOS to receive and run the simplest possible RISC-V ELF32
binaries. Support statically linked, non-relocatable, little-endian
`EM_RISCV` executables with `PT_LOAD` segments only. Copy each loadable segment
to its physical address, zero the `memsz - filesz` tail for BSS, set a known
stack pointer, and jump to `e_entry`.
Deliberate limits:
- no dynamic linking
- no relocations
- no virtual memory
- no privilege separation
- no demand paging
- no filesystem
ABI for tiny programs: start with a tiny fixed contract. For example, `a0`
receives a pointer to a BIOS call table, `sp` points at the top of data RAM,
and returning from `main` jumps back to the monitor with an integer status.
Before Phase 12 traps exist, programs are trusted firmware payloads, not isolated
user processes.
Why: ELF loading is a major confidence milestone. You are no longer baking every
program into the FPGA bitstream; you can compile a new binary, send it over
serial, and run it on your CPU.
Test: Compile several tiny programs with fixed link addresses: hello world,
integer arithmetic, memory copy, and a program that returns a status code to
the BIOS. Confirm the BIOS rejects malformed or unsupported ELF files loudly.
Future role: This loader becomes the conceptual ancestor of the later bootloader
that copies kernels from flash to DRAM.
---
## Phase 11 — Tiny Kernel + Command Shell [Easy/fun]
What: Build a tiny kernel as a separate GCC-built ELF loaded by the BIOS.
Before Phase 12, "kernel" means bare-machine firmware with a command shell, not
an isolated privileged OS. It owns a simple console, command parser, memory
allocator, and a table of services. Keep it intentionally small: no MMU, no
userspace isolation, no real filesystem.
Suggested kernel features:
- banner and prompt
- `help`, `uptime`, `mem`, `dump`, `loadelf`, `run`, `reboot`
- direct UART console driver or BIOS-backed console calls
- bump allocator for kernel data structures
- simple program table showing loaded ELF images
- trusted program launch with an agreed calling convention
- status return from launched programs back to the shell
Simple ELF binaries: compile tiny freestanding programs that use the kernel or
BIOS service table for `putchar`, `getchar`, and exit. The first binaries can be
as small as "print a line", "sum an array", and "echo typed characters". The
goal is not POSIX; the goal is a complete end-to-end loop: compile on your host,
send over UART, load as ELF, run on your CPU, return to your shell.
Why: This is the rewarding middle game between "bare-metal C works" and "Linux
boot hangs somewhere in early init." You get OS-shaped learning while the system
is still small enough to understand in one sitting.
Future role: Phase 12 turns `ecall`, exceptions, and trap vectors into real
architectural mechanisms. At that point, the ad hoc service table can evolve into
a syscall ABI and the kernel can start handling faults instead of trusting every
program.
---
## Phase 12 — CSRs + M-Mode Trap Handling [Hard stretch]
What: Add Control and Status Registers (mstatus, mtvec, mepc, mcause, mtval, mie, What: Add Control and Status Registers (mstatus, mtvec, mepc, mcause, mtval, mie,
mip) and the CSR instructions (csrrw, csrrs, csrrc, csrrwi, csrrsi, csrrci). Add mip) and the CSR instructions (csrrw, csrrs, csrrc, csrrwi, csrrsi, csrrci). Add
@@ -450,18 +642,18 @@ plus S-mode (supervisor mode), which you'll add later.
--- ---
## Phase 10 — Timer ## Phase 13 — Timer [Hard stretch]
What: Implement mtime (a free-running 64-bit counter) and mtimecmp (comparison What: Implement mtime (a free-running 64-bit counter) and mtimecmp (comparison
register). When mtime >= mtimecmp, a timer interrupt fires. register). When mtime >= mtimecmp, a timer interrupt fires.
Why: Every operating system needs a timer tick for scheduling. Even bare-metal 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 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. validates that the trap handling from Phase 12 actually works end-to-end.
--- ---
## Phase 11 — Interrupt Controller ## Phase 14 — Interrupt Controller [Hard stretch]
What: Build a minimal PLIC (Platform-Level Interrupt Controller) or a simplified What: Build a minimal PLIC (Platform-Level Interrupt Controller) or a simplified
version. Connect UART RX as an interrupt source. Implement interrupt priority and version. Connect UART RX as an interrupt source. Implement interrupt priority and
@@ -473,60 +665,160 @@ the PLIC manages them. This is required for Linux.
--- ---
## Phase 12Pipeline (Optional but educational) ## Phase 15A Extension (Atomics) [Hard stretch]
What: Insert pipeline registers between your stages (fetch|decode|execute|memory| What: Implement the RV32A atomic instructions: `lr.w` / `sc.w` (load-reserved /
writeback). Handle data hazards (forwarding/stalling) and control hazards (branch store-conditional) and the AMO ops (`amoswap.w`, `amoadd.w`, `amoand.w`,
prediction or pipeline flush). `amoor.w`, `amoxor.w`, `amomin.w`, `amomax.w`, `amominu.w`, `amomaxu.w`).
Switch the GCC march string to `rv32ima_zicsr_zifencei`. Run `rv32ua` from
`riscv-tests`.
Why: Your single-cycle core's clock speed is limited by the longest combinational Why now: Mainline Linux's RISC-V kernel is built against `rv32ima` /
path (probably through the ALU). Pipelining lets each stage run in one short cycle. `rv64ima` — atomics are not optional for an unmodified kernel build. The
This is the classic computer architecture exercise and deeply educational. kernel uses LR/SC and AMO for spinlocks, refcounts, and futexes; without
them you maintain a non-standard fork. Adding A before the Linux work means
you discover atomics-related bugs in a small, focused phase rather than
wedged inside a kernel boot.
Why optional: A single-cycle core can run at maybe 50-80 MHz on the Artix-7. That's Single-hart implementation: the reservation set is just one register
plenty for booting Linux. Pipeline if you want to learn, not because you must. (reserved address + valid bit). LR sets it; any store to that address (or
context switch) clears it; SC checks it and either commits or returns 1.
AMOs are "read, op, write" sequences that the bus must perform atomically —
on this single-master core that's free; the load/store unit just holds the
bus across the read-modify-write. This becomes meaningful only when DRAM
or DMA enters the picture.
Future role: Final for single-hart. If you ever go multi-hart, the
reservation set and bus become more involved.
--- ---
## Phase 13SPI Flash Boot + DRAM ## Phase 16Pipeline (Optional but educational) [Hard stretch]
What: Add an SPI flash controller to boot from the on-board flash (instead of BRAM What: Insert pipeline registers between your stages (fetch|decode|execute|
initialization). Integrate AMD/Xilinx MIG IP for the DDR3 on the Arty A7. Update memory|writeback). Handle data hazards (forwarding/stalling) and control
your memory map: flash at boot, copy to DRAM, jump to DRAM. hazards (branch prediction or pipeline flush).
Why: BRAM is tiny (a few hundred KB on Artix-7 100T). Linux needs megabytes. DRAM Why: Your single-cycle core's clock speed is limited by the longest
gives you 256MB. Flash gives you persistent storage for the bootloader. This is combinational path (probably through the ALU). Pipelining lets each stage
how real embedded systems boot. run in one short cycle. This is the classic computer architecture exercise
and deeply educational.
Why optional: A non-pipelined core can run at 50 MHz on the Artix-7. That's
plenty for booting Linux. Pipeline if you want to learn, not because you
must — and if you do, do it before DRAM/firmware work piles on. Re-running
`riscv-tests` (Phase 8.5 + 15) after pipelining catches regressions.
--- ---
## Phase 14 — S-Mode, U-Mode, Sv32 Virtual Memory ## Phase 17 — SPI Flash Boot + DRAM [Overkill/hard]
What: Add supervisor and user privilege modes. Implement Sv32 page table walking What: Add an SPI flash controller to boot from the on-board flash (instead
(two-level page tables, 4KB pages). Add the satp CSR, page fault exceptions, and of BRAM initialization). The Arty A7-100T has a 16 MB Quad-SPI flash; map
sfence.vma. it at `0x0000_00000x00FF_FFFF` inside the reserved boot aperture.
Integrate AMD/Xilinx MIG IP for the DDR3L on the Arty (256 MB). Map DRAM at
`0x8000_00000x8FFF_FFFF`, replacing the data BRAM in that range.
Why: Linux runs the kernel in S-mode, user programs in U-mode. Virtual memory gives Boot flow: reset PC moves to `0x0000_0000` (flash). A small first-stage
each process its own address space and protects the kernel from user code. This is the copies the kernel/firmware image from flash to DRAM, sets up the trap
last major architectural piece before Linux. vector, and jumps to DRAM.
Why: BRAM is tiny (~600 KB total on Artix-7 100T). Linux needs megabytes.
DRAM gives you 256 MB; flash gives you persistent storage for the
bootloader. This is how real embedded systems boot.
Bus implications: DRAM via MIG has multi-cycle, variable-latency responses.
This is the first time `req_valid`/`req_ready`/`rsp_valid`/`rsp_ready`
actually matter — the handshake you wired in Phase 6.1 finally earns its
keep.
--- ---
## Phase 15Linux ## Phase 18S-Mode, U-Mode, Sv32 Virtual Memory [Overkill/hard]
What: Port a minimal Linux kernel (or use an existing RISC-V port). Write a device What: Add supervisor and user privilege modes. Implement Sv32 page table
tree for your SoC. Build an initramfs with BusyBox. Boot to a shell prompt. walking (two-level page tables, 4 KB pages). Add the `satp` CSR, page-fault
exceptions (instruction/load/store page-fault), `sfence.vma`, and S-mode
CSRs (`sstatus`, `stvec`, `sepc`, `scause`, `stval`, `sie`, `sip`,
`sscratch`).
Why: This is the summit. A Linux shell running on a CPU you built from scratch. Why: Linux runs the kernel in S-mode and 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 big architectural piece before Linux.
Test: a hand-written M-mode "kernel" that maps a U-mode page, traps on
syscall, returns a value, then returns to U-mode. Verify all three privilege
modes round-trip via `mret` / `sret`.
---
## Phase 19 — Linux Boot Contract (SBI / Device Tree / ABI) [Overkill/hard]
What: Lock down everything Linux expects at the moment of `kernel_entry`.
This is a real subproject, not a footnote inside "port Linux".
Required deliverables:
- **Boot register state at kernel entry**: `a0` = hart ID (0 for single-hart),
`a1` = physical address of the device tree blob, `satp = 0`, all other
state per the [RISC-V Linux boot protocol](https://docs.kernel.org/arch/riscv/boot.html).
- **Image alignment**: rv32 kernel image base must be 4 MiB-aligned in
physical memory (rv64 is 2 MiB). Reflect this in the linker layout for the
loaded kernel and the bootloader's copy destination.
- **Device tree**: hand-write a `.dts` describing CPU, memory (DRAM base +
size), CLINT (timer + soft IPI), PLIC (interrupt controller bindings),
and the UART node. For the CPU node, prefer the **modern** ISA properties:
```
riscv,isa-base = "rv32i";
riscv,isa-extensions = "i", "m", "a", "zicsr", "zifencei";
mmu-type = "riscv,sv32";
```
Optionally include the legacy `riscv,isa = "rv32ima_zicsr_zifencei"`
string for older kernels that don't yet parse the split form. Compile
with `dtc` to a `.dtb` and ship it in flash alongside the kernel.
- **UART driver/binding decision**: the split TX/RX/status UART from
Phase 7 is *not* `8250/16550`-compatible. Pick one:
- (a) Add a 16550-subset wrapper (THR/RBR shared at offset 0, LSR at
offset 5, IER at offset 1, FCR/LCR mostly stubs). Then the in-tree
`8250_dw` or `of_serial` driver works with a stock binding.
- (b) Write a small custom Linux serial driver and define a
`compatible = "fpgacore,uart"` binding. More work, more learning.
- **Boot firmware**: choose between
- (a) **Direct M-mode Linux**: use the kernel's `CONFIG_RISCV_M_MODE`
path. Less portable, no SBI required, fewer moving pieces. Reasonable
for a learning bring-up.
- (b) **OpenSBI + S-mode Linux**: build OpenSBI as the M-mode firmware,
Linux runs in S-mode. Standard production path, more Vivado/Linux build
surface area.
Why: Skipping this phase means hitting all of these problems simultaneously
during Phase 20 bring-up, where the failure mode is "kernel hangs silently
in early boot" with no console. Doing it explicitly turns Phase 20 into a
debugging exercise on a known-good ABI surface.
---
## Phase 20 — Linux [Overkill/hard]
What: Build a minimal RISC-V Linux kernel against the device tree and
boot path defined in Phase 19. Build an initramfs with BusyBox. Load
kernel + DTB + initramfs to flash. Boot to a shell prompt over UART.
Why: This is the summit. A Linux shell running on a CPU you built from
scratch.
--- ---
## Quick Reference: What You Need Installed ## Quick Reference: What You Need Installed
- Vivado 2025.2 or later (synthesis, simulation, ILA, VIO) - Vivado 2025.2 or later (synthesis, simulation, ILA, VIO)
- RISC-V GCC toolchain (`riscv64-unknown-elf-gcc`, multilib build — needs to - RISC-V GCC toolchain (`riscv64-unknown-elf-gcc`, multilib build). March
support `rv32im_zicsr_zifencei` / `ilp32`) string evolves: `rv32im` (Phase 8) → `rv32im_zicsr_zifencei` (Phase 12) →
- `riscv-tests` repo cloned and buildable (Phase 8.5 onward) `rv32ima_zicsr_zifencei` (Phase 15).
- `riscv-tests` repo cloned and buildable (Phase 8.5 onward; rv32ua added at
Phase 15)
- Device-tree compiler `dtc` (Phase 19+)
- OpenSBI source (Phase 19+, only if you choose the SBI boot path)
- Terminal program (minicom, picocom, or PuTTY) for UART, 115200 8N1 - Terminal program (minicom, picocom, or PuTTY) for UART, 115200 8N1
- Text editor you like for SystemVerilog - Text editor you like for SystemVerilog
- The RISC-V ISA spec (Volume 1: Unprivileged, Volume 2: Privileged) — free PDFs - The RISC-V ISA spec (Volume 1: Unprivileged, Volume 2: Privileged) — free PDFs
- The Linux RISC-V boot protocol doc (`Documentation/arch/riscv/boot.rst`)
+21
View File
@@ -0,0 +1,21 @@
# Bare-metal RISC-V build artifacts (riscv64-unknown-elf-gcc / ld / objcopy).
# Source (.c, .S, .h, *.ld linker scripts, Makefiles) stays tracked.
# Object / archive / dependency files
*.o
*.a
*.d
# Linked outputs
*.elf
*.bin
*.hex
*.map
*.lst
*.dis
*.dump
# Common build directories
build/
obj/
out/
+5
View File
@@ -0,0 +1,5 @@
# Software/
Software that runs on the CPU: bare-metal firmware, BIOS, and kernels.
Built with the RISC-V GCC toolchain (`riscv64-unknown-elf-gcc`). The `march`
string evolves with the implemented ISA — see `CLAUDE.md`.
+6
View File
@@ -0,0 +1,6 @@
# Software/bios/
BIOS / monitor: an interactive ROM program that talks over UART to peek/poke
memory, dump registers, and load programs into RAM. Includes the ELF loader.
**Phases:** 9 (BIOS monitor), 10 (ELF loader over UART).
+6
View File
@@ -0,0 +1,6 @@
# Software/fw/
Bare-metal firmware: `crt0.S`, linker script, and the first C programs that
run on the CPU. Also where `riscv-tests` integration lives.
**Phases:** 8 (GCC toolchain, first C program, `riscv-tests`).
+7
View File
@@ -0,0 +1,7 @@
# Software/kernel/
Kernels that run on the core. Starts as a tiny custom kernel for learning
trap/scheduling/syscalls; later hosts the Linux device tree, OpenSBI, and
mainline Linux build artifacts.
**Phases:** 11 (tiny kernel), 19 (Linux boot contract), 20 (Linux).
+41
View File
@@ -0,0 +1,41 @@
# FPGA-Core Tutorial
This tutorial tree follows `ROADMAP.md`. Each phase and subphase has a learning note
with the same structure:
- Context
- Goals
- New Concepts
- How To Think About It
- Learning Tasks
- Pitfalls
- Tooling And Testing
- References
The notes assume strong Linux/systems experience and beginner-to-intermediate FPGA and
digital-design experience. They intentionally avoid giving implementation code. The goal
is to explain what to learn, what to verify, and what mistakes to avoid.
## Phase Index
- [Phase 0 - Architecture Contract](phase-00-architecture-contract/phase-00.md)
- [Phase 1 - ALU + M Unit](phase-01-alu-m-unit/phase-01.md)
- [Phase 2 - Register File](phase-02-register-file/phase-02.md)
- [Phase 3 - Decoder](phase-03-decoder/phase-03.md)
- [Phase 4 - First CPU](phase-04-first-cpu/phase-04.md)
- [Phase 5 - Branches And Jumps](phase-05-control-flow/phase-05.md)
- [Phase 6 - Load/Store](phase-06-load-store/phase-06.md)
- [Phase 7 - Memory-Mapped UART](phase-07-uart-mmio/phase-07.md)
- [Phase 8 - GCC Toolchain Integration](phase-08-gcc-toolchain/phase-08.md)
- [Phase 9 - GCC-Built BIOS / Serial Monitor](phase-09-bios-monitor/phase-09.md)
- [Phase 10 - Minimal ELF Loader](phase-10-elf-loader/phase-10.md)
- [Phase 11 - Tiny Kernel + Command Shell](phase-11-tiny-kernel/phase-11.md)
- [Phase 12 - CSRs + M-Mode Traps](phase-12-csrs-traps/phase-12.md)
- [Phase 13 - Timer](phase-13-timer/phase-13.md)
- [Phase 14 - Interrupt Controller](phase-14-interrupt-controller/phase-14.md)
- [Phase 15 - Atomics](phase-15-atomics/phase-15.md)
- [Phase 16 - Pipeline](phase-16-pipeline/phase-16.md)
- [Phase 17 - SPI Flash + DRAM](phase-17-flash-dram/phase-17.md)
- [Phase 18 - M/S/U + Sv32](phase-18-privilege-sv32/phase-18.md)
- [Phase 19 - Linux Boot Contract](phase-19-linux-boot-contract/phase-19.md)
- [Phase 20 - Linux](phase-20-linux/phase-20.md)
@@ -0,0 +1,48 @@
# Phase 0.1 - SystemVerilog Package
## Context
The package is the shared type layer for the project. It prevents every module from
inventing its own encodings and signal names.
## Goals
- Define enums for ALU operations, branch types, memory sizes, and instruction classes.
- Define structs for stage outputs and memory request/response payloads.
- Make later extension possible without rewriting every module boundary.
## New Concepts
- Packed struct: a struct with a defined bit layout, suitable for wires and registers.
- Typedef: named type alias used to make RTL readable.
- Import: SystemVerilog mechanism for using package definitions in modules.
- Encoding: the bit pattern used for enum values in synthesized hardware.
## How To Think About It
The package is not a dumping ground. It should contain stable contracts and shared
definitions. If a signal is private to one module, keep it private.
## Learning Tasks
- List every planned stage boundary and define what information must cross it.
- Separate architectural information, such as register addresses, from local control.
- Decide naming conventions before writing dependent modules.
## Pitfalls
- Relying on implicit enum widths and later discovering mismatched synthesis behavior.
- Putting handshake `ready` signals inside one-way payload structs.
- Adding fields "just in case" without knowing who drives or consumes them.
## Tooling And Testing
- Compile the package alone and then with one tiny importing module.
- Inspect elaboration messages; type errors here are usually design-contract errors.
- Keep comments on fields short but precise about direction and ownership.
## References
- SystemVerilog packages and typedefs: https://www.chipverify.com/systemverilog/systemverilog-package
- LowRISC SystemVerilog style: https://github.com/lowRISC/style-guides/blob/master/VerilogCodingStyle.md
- RISC-V unprivileged ISA: https://riscv.org/technical/specifications/
@@ -0,0 +1,49 @@
# Phase 0.2 - Block Diagram
## Context
The block diagram is the visual source of truth for the datapath. It should show what
talks to what, what is registered, and what can stall.
## Goals
- Draw fetch, decode, register file, ALU, M unit, memory/LSU, and writeback.
- Label stage-boundary structs from the package.
- Mark clocked state, combinational paths, handshakes, and reset behavior.
## New Concepts
- Datapath: the route operands and results take through the CPU.
- Control path: signals that choose operations, mux inputs, stalls, and traps.
- Mux: hardware selector that chooses one of several inputs.
- Stall: deliberately holding state while waiting for a multi-cycle event.
## How To Think About It
Draw movement of information, not just boxes. A useful hardware diagram answers: where is
state stored, what changes each clock, what can wait, and what happens on an exception?
## Learning Tasks
- Use arrows for data flow and separate arrows for control when helpful.
- Mark every register or memory with a clock edge symbol.
- Identify the longest combinational path you expect in early phases.
## Pitfalls
- Drawing BRAM like a zero-latency array.
- Forgetting that multi-cycle units need explicit state and stall control.
- Hiding reset and trap paths because they are visually inconvenient.
## Tooling And Testing
- Keep the diagram in `Docs/` and update it when interfaces change.
- Compare waveform signal names against the diagram after each integration phase.
- Use the diagram as a checklist when adding ILA probes.
## References
- nand2tetris CPU/data-path intuition: https://www.nand2tetris.org/
- RISC-V reader for datapath examples: http://www.riscvbook.com/
- Vivado ILA overview: https://docs.xilinx.com/
@@ -0,0 +1,52 @@
# Phase 0 - Architecture Contract
## Context
Before writing RTL, define the vocabulary of the CPU: stage boundaries, signal bundles,
enums, reset behavior, memory map, and debug expectations. This is similar to defining an
internal kernel ABI before implementing subsystems.
## Goals
- Establish `rv32_pkg.sv` as the shared contract for the design.
- Decide what signals cross between fetch, decode, execute, memory, and writeback.
- Create a block diagram that can guide implementation and later refactors.
## New Concepts
- RTL: register-transfer level description of hardware behavior.
- Package: SystemVerilog namespace for shared types, constants, enums, and structs.
- Struct: named bundle of related signals; useful for stage outputs and bus payloads.
- Enum: symbolic encoding for choices such as ALU operation or branch type.
- Combinational logic: output depends only on current inputs.
- Sequential logic: output/state changes on a clock edge.
## How To Think About It
You are designing interfaces between hardware blocks, not function signatures. Every
field becomes wires or registers, so changing it later can ripple through the whole
datapath. Bias toward clarity and explicitness, not clever compression.
## Learning Tasks
- Sketch the datapath by hand and label every stage boundary.
- Write down which signals are data, control, status, or exception metadata.
- Decide which fields are needed now and which are placeholders for later phases.
## Pitfalls
- Overfitting the package to Phase 1 and then reworking it repeatedly.
- Mixing unrelated control signals into loose wires instead of structured bundles.
- Treating the diagram as disposable; it should be updated as the design changes.
## Tooling And Testing
- Use the package in the smallest possible test module to confirm Vivado accepts it.
- Keep package dependencies acyclic; shared types should not import implementation modules.
- Run syntax checks early, before several modules depend on a broken type.
## References
- Accellera IEEE standards downloads: https://www.accellera.org/downloads/ieee
- LowRISC style guide: https://github.com/lowRISC/style-guides/blob/master/VerilogCodingStyle.md
- RISC-V specifications: https://riscv.org/technical/specifications/
@@ -0,0 +1,48 @@
# Phase 1.1 - Combinational ALU
## Context
The ALU is the easiest place to learn the difference between software expressions and
hardware behavior. Every operation is hardware that exists in parallel behind a selector.
## Goals
- Implement RV32I arithmetic, logic, comparison, and shift operations.
- Verify every operation independently.
- Learn how signedness works in SystemVerilog.
## New Concepts
- Signedness: whether bits are interpreted as two's-complement signed values.
- Shift amount: RISC-V uses only the low 5 bits for RV32 shifts.
- Overflow: addition overflow is usually ignored for RV32I arithmetic results.
- Combinational completeness: every output is assigned for every input path.
## How To Think About It
The same 32-bit vector can be signed or unsigned depending on the operation. Make the
interpretation explicit in your design notes and tests.
## Learning Tasks
- Make a table of each ALU op, its operands, and expected result semantics.
- Hand-check edge cases such as `0`, `-1`, `INT_MIN`, and `INT_MAX`.
- Compare arithmetic right shift with logical right shift.
## Pitfalls
- Accidentally creating latches by not assigning outputs in every case.
- Using host-language intuition for signed comparisons.
- Forgetting that `slt` and `sltu` are different instructions.
## Tooling And Testing
- Use a small self-checking testbench before opening a waveform.
- Add waveform inspection for failing tests only; do not debug by staring first.
- Include cases where signed and unsigned answers differ.
## References
- RISC-V unprivileged ISA: https://riscv.org/technical/specifications/
- SystemVerilog types, operators, and expressions: https://systemverilog.dev/2.html
- Verilator warnings guide: https://verilator.org/guide/latest/warnings.html
@@ -0,0 +1,49 @@
# Phase 1.2 - Multi-Cycle M Unit
## Context
Multiply and divide belong in a dedicated unit because their timing and control behavior
are different from simple ALU operations.
## Goals
- Implement multiply, high-half multiply, divide, and remainder behavior.
- Learn multi-cycle control using `start`, `busy`, and `done`.
- Verify all RISC-V-defined edge cases.
## New Concepts
- High-half multiply: result uses bits 63:32 of a 64-bit product.
- Iterative divider: divider that computes one bit or small group of bits per cycle.
- FSM: finite-state machine controlling multi-cycle behavior.
- Latency: number of cycles from request to result.
## How To Think About It
Treat the M unit like a tiny asynchronous service from the core's perspective. The core
asks for work, waits, and resumes only when the result is stable.
## Learning Tasks
- Write down the expected result for divide-by-zero and signed overflow.
- Draw state transitions for idle, multiply, divide, done, and back-to-back requests.
- Decide when inputs are latched and when outputs are valid.
## Pitfalls
- Allowing operands to change while an operation is in flight.
- Returning C-language division behavior instead of RISC-V behavior.
- Forgetting `mulhsu`, which mixes signed and unsigned operands.
## Tooling And Testing
- Use long waveform windows to inspect division progress.
- Include tests that assert `start` while `busy` and confirm the intended behavior.
- Synthesize to confirm multiply maps to DSPs and divide maps to logic/state.
## References
- RISC-V M extension spec: https://riscv.org/technical/specifications/
- Project F multiplication: https://projectf.io/posts/multiplication-fpga-dsps/
- Project F division: https://projectf.io/posts/division-in-verilog/
@@ -0,0 +1,50 @@
# Phase 1.3 - ALU + M Unit On FPGA With VIO
## Context
After simulation, place the arithmetic blocks on real hardware and interact with them
through Vivado VIO. This teaches the difference between simulated correctness and
synthesized hardware behavior.
## Goals
- Learn how to instantiate debug IP around simple modules.
- Confirm arithmetic blocks synthesize and meet the 50 MHz target.
- Practice observing handshake behavior in hardware.
## New Concepts
- VIO: Vivado Virtual Input/Output IP, controlled over JTAG.
- JTAG: debug/control link used by Vivado Hardware Manager.
- Timing closure: proving signals settle before the next clock edge.
- Synthesis: compiling RTL into FPGA primitives.
## How To Think About It
This is not about exhaustive validation. It is about learning the FPGA tool loop:
synthesize, implement, program, poke signals, observe results, and read timing reports.
## Learning Tasks
- Identify which signals are useful to drive and which are useful to observe.
- Read the timing summary and locate the worst path.
- Compare VIO observations with simulator waveforms.
## Pitfalls
- Letting Vivado optimize away debug signals before VIO/ILA can observe them.
- Confusing VIO interaction latency with the design's actual clock-cycle behavior.
- Ignoring timing warnings because the manual test "seemed to work."
## Tooling And Testing
- Use VIO for low-bandwidth manual checks, not automated compliance.
- Keep a short lab notebook: bitstream, clock, operands, expected, observed.
- Learn where Vivado reports resource use for LUTs, FFs, DSPs, and BRAMs.
## References
- Vivado Design Suite documentation: https://docs.xilinx.com/
- Vivado debug IP overview: https://docs.xilinx.com/
- Digilent Arty A7 reference: https://digilent.com/reference/programmable-logic/arty-a7/reference-manual
+49
View File
@@ -0,0 +1,49 @@
# Phase 1 - ALU + M Unit
## Context
This phase builds the arithmetic core of the CPU. The ALU handles simple single-cycle
operations; the M unit handles multiplication and division with an explicit handshake.
## Goals
- Build and verify a combinational RV32I ALU.
- Build and verify a multi-cycle RV32M unit.
- Learn simulation-first hardware development before system integration.
## New Concepts
- ALU: arithmetic logic unit; performs integer arithmetic, comparisons, shifts, and logic.
- DSP slice: FPGA hard block optimized for multiplication and arithmetic.
- Multi-cycle unit: block that takes several clocks to produce a result.
- Handshake: protocol using signals such as start, busy, done, valid, or ready.
## How To Think About It
The ALU is pure combinational decision logic. The M unit is a small protocol machine. Do
not force division into the ALU just because it is mathematically an arithmetic operation.
## Learning Tasks
- Understand signed versus unsigned interpretation of the same 32 bits.
- Work through RISC-V division edge cases by hand.
- Draw the M-unit state machine before writing RTL.
## Pitfalls
- Implementing a combinational divider and then fighting timing forever.
- Treating signed comparisons as unsigned or vice versa.
- Dropping `done` or accepting a new `start` at the wrong time.
## Tooling And Testing
- Start with small deterministic test vectors, then add randomized checks.
- Use waveform inspection to confirm handshake timing, not only final results.
- Synthesize early to see whether multiply infers DSP resources.
## References
- RISC-V unprivileged ISA, integer and M extension chapters: https://riscv.org/technical/specifications/
- AMD/Xilinx DSP48 documentation: https://docs.xilinx.com/
- Project F FPGA arithmetic articles: https://projectf.io/posts/
@@ -0,0 +1,49 @@
# Phase 2.1 - Register File Module
## Context
This subphase turns the architectural register model into clocked hardware with two
combinational reads and one synchronous write.
## Goals
- Implement 32 registers of 32 bits.
- Enforce `x0` semantics.
- Verify read, write, and collision behavior.
## New Concepts
- Synchronous write: state changes only at the active clock edge.
- Combinational read: output changes when the address changes.
- Read-after-write: reading an address in the same cycle it is written.
- Bypass/forwarding: returning newly written data without waiting for storage update.
## How To Think About It
Choose behavior deliberately. A simple non-pipelined core can tolerate many choices, but
tests and later pipeline work become easier if the behavior is documented.
## Learning Tasks
- Write a truth table for `write_enable`, `rd`, `rs1`, and `rs2` interactions.
- Decide how reset affects registers, if at all.
- Confirm whether the register file should initialize to zero for simulation clarity.
## Pitfalls
- Resetting a large register file unnecessarily in hardware.
- Making simulation behavior differ from synthesis behavior.
- Forgetting that reads from `x0` must ignore any stored value.
## Tooling And Testing
- Use assertions for `x0` if your simulator supports them.
- Run repeated randomized write/read sequences.
- Inspect synthesis resource use; this may infer LUT RAM or flip-flops.
## References
- RISC-V ABI register names: https://riscv.org/technical/specifications/
- SystemVerilog always_ff guidance: https://github.com/lowRISC/style-guides/blob/master/VerilogCodingStyle.md
- Vivado synthesis guide: https://docs.xilinx.com/
@@ -0,0 +1,48 @@
# Phase 2.2 - Optional VIO Validation
## Context
The register file is simple enough that simulation is usually sufficient, but VIO can
teach practical debug techniques on real FPGA state.
## Goals
- Practice observing and driving register-file signals in hardware.
- Confirm reset/write/read expectations after synthesis.
- Learn when hardware debug is worth the overhead.
## New Concepts
- Probe: signal connected to debug IP for observation.
- Debug visibility: whether a net survives optimization and can be inspected.
- Hardware Manager: Vivado tool for programming and interacting with the FPGA.
## How To Think About It
Use VIO here only as a learning exercise. Do not make it a habit to replace systematic
simulation with manual poking.
## Learning Tasks
- Select a minimal set of probes: addresses, write data, write enable, read outputs.
- Compare one same-cycle read/write case with the simulator.
- Observe how a clocked write appears relative to your VIO actions.
## Pitfalls
- Over-instrumenting and making the debug design harder than the actual module.
- Forgetting that manual tests rarely cover corner cases.
- Treating VIO as evidence of complete correctness.
## Tooling And Testing
- Keep the VIO wrapper separate from the synthesizable register-file module.
- Remove or isolate debug IP before building later phases.
- Check resource utilization with and without debug IP.
## References
- Vivado debug documentation: https://docs.xilinx.com/
- Digilent Arty A7 reference: https://digilent.com/reference/programmable-logic/arty-a7/reference-manual
- LowRISC style guide: https://github.com/lowRISC/style-guides/blob/master/VerilogCodingStyle.md
@@ -0,0 +1,49 @@
# Phase 2 - Register File
## Context
The register file is the CPU's small, fast architectural storage. RISC-V has 32 integer
registers, with `x0` permanently reading as zero.
## Goals
- Build a 32 x 32-bit register file with two reads and one write.
- Define same-cycle read/write behavior intentionally.
- Learn how small memories map onto FPGA resources.
## New Concepts
- Read port: ability to read one register address in a cycle.
- Write port: ability to update one register address on a clock edge.
- Architectural register: register visible to software.
- Hardwired zero: `x0` ignores writes and always returns zero.
## How To Think About It
The register file is part storage and part contract with the ISA. Its behavior must be
boring, predictable, and heavily tested because every instruction depends on it.
## Learning Tasks
- Decide whether same-cycle read-after-write returns old or new data.
- Make a register naming table from ABI names to `x` numbers.
- Understand why two read ports are enough for RV32I/RV32M.
## Pitfalls
- Accidentally allowing writes to `x0`.
- Assuming FPGA block RAM behavior without checking read-port needs.
- Leaving same-cycle behavior unspecified until integration exposes it.
## Tooling And Testing
- Test all boundary addresses: `x0`, `x1`, `x31`.
- Test simultaneous reads of two different registers and the same register.
- Prefer a waveform check for one read/write collision case.
## References
- RISC-V register convention: https://riscv.org/technical/specifications/
- AMD/Xilinx memory resources: https://docs.xilinx.com/
- Project F memory initialization and FPGA memories: https://projectf.io/posts/
@@ -0,0 +1,49 @@
# Phase 3.1 - Instruction Decoder
## Context
This subphase creates the combinational decoder module that turns raw instruction bits
into `decode_out_t` fields.
## Goals
- Decode source/destination registers, immediates, ALU operation, memory operation, and branch type.
- Identify legal versus illegal instructions.
- Verify all instruction formats with known encodings.
## New Concepts
- Sign extension: copying a sign bit into higher bits to preserve signed value.
- Zero extension: filling upper bits with zero.
- Control signal: a signal that chooses what hardware action occurs.
- Decode table: mapping from instruction fields to operation semantics.
## How To Think About It
Decoder bugs often look like random CPU bugs later. Invest heavily here. If an immediate
bit is wrong, the ALU and branch logic can be perfect and the program will still fail.
## Learning Tasks
- Create a checklist for each instruction family and expected decode fields.
- For each immediate format, manually reconstruct the value from bit positions.
- Decide how `fence`, `fence.i`, CSR, `ecall`, and `ebreak` are represented before traps exist.
## Pitfalls
- Copying immediate extraction logic without understanding bit order.
- Missing `jalr`'s low-bit clearing rule later in execute/control flow.
- Letting default decode outputs accidentally describe a valid NOP.
## Tooling And Testing
- Generate encodings with the RISC-V assembler, not by hand alone.
- Use objdump to verify that your test words are the instructions you think they are.
- Add negative tests for illegal encodings.
## References
- RISC-V unprivileged ISA: https://riscv.org/technical/specifications/
- RISC-V opcode repository: https://github.com/riscv/riscv-opcodes
- GNU assembler manual: https://sourceware.org/binutils/docs/as/
+49
View File
@@ -0,0 +1,49 @@
# Phase 3 - Decoder
## Context
The decoder translates a 32-bit instruction word into control signals. It is where the
binary ISA becomes meaningful hardware intent.
## Goals
- Decode RV32I/RV32M instruction fields and immediates.
- Produce structured control output for later datapath integration.
- Learn to verify against assembler-generated encodings.
## New Concepts
- Opcode: primary instruction-class field.
- funct3/funct7: secondary fields that refine instruction meaning.
- Immediate: constant encoded inside an instruction, often split across bits.
- Illegal instruction: encoding the core does not implement or that is invalid.
## How To Think About It
The decoder is a classifier. It should not perform ALU work; it should describe what
work the datapath must perform and which operands/control paths are needed.
## Learning Tasks
- Draw bit layouts for R, I, S, B, U, and J formats.
- Hand-decode several assembled instructions.
- Decide where illegal instruction detection lives and how it reports failures.
## Pitfalls
- Misplacing B-type and J-type immediate bits.
- Forgetting sign extension on immediates.
- Treating all unknown encodings as harmless NOPs.
## Tooling And Testing
- Use assembler/objdump as a reference for encodings.
- Build tests around every format, not just every mnemonic.
- Keep decoder tests independent from ALU or register-file behavior.
## References
- RISC-V unprivileged ISA instruction formats: https://riscv.org/technical/specifications/
- RISC-V opcode map: https://github.com/riscv/riscv-opcodes
- GNU binutils RISC-V documentation: https://sourceware.org/binutils/docs/
@@ -0,0 +1,49 @@
# Phase 4.1 - Fetch + Datapath Integration
## Context
This subphase builds the first CPU loop: fetch an instruction, decode it, execute it,
write back a result, and advance PC.
## Goals
- Integrate instruction BRAM at `0x2000_0000`.
- Execute straight-line arithmetic and M-extension operations.
- Learn stall control around BRAM and the M unit.
## New Concepts
- Reset PC: address where execution begins after reset.
- FSM state: named control state such as fetch, execute, wait, writeback.
- Instruction retirement: point where an instruction's architectural effects are complete.
- Stall: holding PC/state while waiting for a dependency.
## How To Think About It
The CPU is a protocol participant with its memories and M unit. Correct sequencing matters
more than minimizing cycles.
## Learning Tasks
- Write a cycle-by-cycle timeline for a simple `addi` instruction.
- Write a cycle-by-cycle timeline for a multiply/divide instruction.
- Confirm the pre-Phase 12 illegal-instruction behavior: halt the core and expose
the offending PC and instruction word to the testbench.
## Pitfalls
- Double-executing an instruction because PC or state advances at the wrong time.
- Starting the M unit repeatedly while waiting for it to finish.
- Losing the destination register number while a multi-cycle op is in flight.
## Tooling And Testing
- Use a testbench that stops after a fixed halt condition or maximum cycle count.
- Compare final register state and intermediate writeback events.
- Inspect PC, instruction, state, register write enable, and writeback data in waveforms.
## References
- RISC-V unprivileged ISA: https://riscv.org/technical/specifications/
- Vivado simulation documentation: https://docs.xilinx.com/
- Project F memory initialization: https://projectf.io/posts/initialize-memory-in-verilog/
@@ -0,0 +1,49 @@
# Phase 4.2 - ILA Verification On FPGA
## Context
ILA lets you capture internal FPGA signals during real hardware execution. This is the
first serious hardware-debug milestone for the integrated CPU.
## Goals
- Learn how to select useful ILA probes.
- Capture PC, instruction, writeback, and state transitions.
- Compare hardware behavior to simulation.
## New Concepts
- ILA: Integrated Logic Analyzer, on-chip waveform capture over JTAG.
- Trigger: condition that starts or centers a capture.
- Sample depth: number of cycles stored in on-chip debug memory.
- Probe preservation: keeping signals from being optimized away.
## How To Think About It
ILA is an oscilloscope for internal logic. Use it to answer a specific question, not to
record everything. Every extra probe has cost.
## Learning Tasks
- Choose a trigger such as PC reaching the last instruction.
- Decide which signals prove correct instruction retirement.
- Compare one hardware trace with the simulator trace for the same program.
## Pitfalls
- Capturing too few cycles before/after the trigger.
- Debugging a different bitstream than the source you are reading.
- Assuming ILA changes nothing; debug IP consumes resources and can affect timing.
## Tooling And Testing
- Archive the program image, bitstream, and source commit used for hardware tests.
- Use `dont_touch` sparingly and only on debug-critical nets.
- Check timing again after inserting ILA.
## References
- Vivado ILA documentation: https://docs.xilinx.com/
- Digilent Arty A7 reference: https://digilent.com/reference/programmable-logic/arty-a7/reference-manual
- AMD/Xilinx debug methodology: https://docs.xilinx.com/
+49
View File
@@ -0,0 +1,49 @@
# Phase 4 - First CPU
## Context
This phase connects fetch, decode, arithmetic, register-file, and writeback into a minimal
non-pipelined CPU. It executes straight-line arithmetic programs from instruction BRAM.
## Goals
- Build the first integrated datapath.
- Learn how BRAM latency shapes control flow.
- Verify architectural state changes instruction by instruction.
## New Concepts
- BRAM: block RAM, dedicated on-chip FPGA memory with synchronous access.
- PC: program counter, the address of the current or next instruction.
- Writeback: stage where a result is committed to the register file.
- CPI: cycles per instruction.
## How To Think About It
This is not a performance exercise. It is an integration exercise. The goal is to make
one instruction at a time retire correctly while respecting real memory latency.
## Learning Tasks
- Draw the fetch/execute FSM.
- Decide exactly when PC advances.
- Trace a short program and record expected register state after each instruction.
## Pitfalls
- Treating BRAM like a combinational array.
- Updating PC while a multi-cycle M operation is still in progress.
- Writing back results for instructions that should not write a register.
## Tooling And Testing
- Start with very small programs and known final register values.
- Use simulation waveforms before ILA.
- Add a visible "halt" or terminal condition for test programs.
## References
- AMD/Xilinx block memory documentation: https://docs.xilinx.com/
- RISC-V unprivileged ISA: https://riscv.org/technical/specifications/
- Digital design FSM overview: https://www.chipverify.com/verilog/verilog-fsm
@@ -0,0 +1,49 @@
# Phase 5.1 - Branch Instructions
## Context
Branches compare two registers and conditionally update PC to a PC-relative target. They
enable loops and conditionals.
## Goals
- Implement `beq`, `bne`, `blt`, `bge`, `bltu`, and `bgeu`.
- Learn signed and unsigned branch comparisons.
- Verify target calculation and fall-through behavior.
## New Concepts
- PC-relative addressing: target is computed from current PC plus immediate.
- Branch comparator: dedicated comparison logic for branch conditions.
- B-type immediate: split immediate format used by branch instructions.
- Taken branch: branch condition true, PC becomes target.
## How To Think About It
Separate three questions: what is the target, is the condition true, and which PC should
be used next? Debugging is easier when those are visible independently.
## Learning Tasks
- Decode a B-type immediate by hand.
- Build test cases where signed and unsigned comparisons differ.
- Trace a loop from initialization through exit.
## Pitfalls
- Forgetting branch offsets are multiples of 2 bytes in the encoding.
- Comparing signed values with unsigned operators.
- Counting loop iterations incorrectly because PC update timing is unclear.
## Tooling And Testing
- Use assembler-generated branch encodings.
- In waveforms, inspect PC, immediate, comparator result, and next PC.
- Include backward and forward branch targets.
## References
- RISC-V unprivileged ISA, branch instructions: https://riscv.org/technical/specifications/
- RISC-V assembly manual: https://github.com/riscv-non-isa/riscv-asm-manual
- RISC-V opcode data: https://github.com/riscv/riscv-opcodes
@@ -0,0 +1,49 @@
# Phase 5.2 - Jump Instructions
## Context
Jumps provide unconditional control transfer and function-call mechanics. `jal` is
PC-relative; `jalr` jumps through a register plus immediate.
## Goals
- Implement `jal` and `jalr`.
- Store PC + 4 into the destination register.
- Verify call and return sequences.
## New Concepts
- Return address: address of the instruction after the call.
- Indirect jump: target comes from a register rather than only the instruction.
- J-type immediate: immediate format used by `jal`.
- Calling convention: software agreement for argument, return, and saved registers.
## How To Think About It
`jal` and `jalr` are both "write link, then redirect PC." The redirect source differs;
the writeback behavior is shared.
## Learning Tasks
- Hand-trace a call and return using `ra` (`x1`).
- Verify `jalr` target bit 0 clearing.
- Learn which registers the RISC-V ABI uses for calls.
## Pitfalls
- Writing the jump target instead of PC + 4 into `rd`.
- Mishandling `rd = x0`, which should discard the link.
- Forgetting `jalr` is commonly used for returns and function pointers.
## Tooling And Testing
- Test `jal`, `jalr`, and `jalr x0, ra, 0` return style.
- Use objdump to confirm labels assemble to expected offsets.
- Inspect register writeback and PC change in the same trace.
## References
- RISC-V unprivileged ISA, jumps: https://riscv.org/technical/specifications/
- RISC-V ELF psABI: https://github.com/riscv-non-isa/riscv-elf-psabi-doc
- RISC-V assembly manual: https://github.com/riscv-non-isa/riscv-asm-manual
@@ -0,0 +1,49 @@
# Phase 5 - Branches And Jumps
## Context
This phase adds control flow. The CPU stops being a straight-line executor and gains
loops, conditionals, function calls, and returns.
## Goals
- Implement conditional branches.
- Implement `jal` and `jalr`.
- Learn PC selection and control-transfer testing.
## New Concepts
- Branch target: destination address for a taken branch.
- Fall-through: next sequential PC, usually PC + 4.
- Link register: register receiving return address for calls.
- Control hazard: later pipeline issue where fetched instructions may be wrong.
## How To Think About It
Control flow is just PC update logic plus optional register writeback. Keep the
comparison, target calculation, and PC selection clearly separated.
## Learning Tasks
- Hand-compute branch and jump targets from instruction immediates.
- Trace a simple loop and count taken versus not-taken branches.
- Understand why `jalr` clears bit 0 of the target address.
## Pitfalls
- Off-by-four errors between current PC and next PC.
- Using unsigned comparison for signed branches.
- Forgetting that `jal`/`jalr` write PC + 4 to `rd`.
## Tooling And Testing
- Test both taken and not-taken paths for every branch type.
- Use short loops with known iteration counts.
- Probe PC source selection in simulation and ILA.
## References
- RISC-V branch and jump semantics: https://riscv.org/technical/specifications/
- RISC-V assembly examples: https://github.com/riscv-non-isa/riscv-asm-manual
- Computer architecture control flow overview: https://www.nand2tetris.org/
@@ -0,0 +1,49 @@
# Phase 6.1 - Word Load/Store
## Context
Word loads and stores are the first data-memory operations. They are simpler because
addresses must be 4-byte aligned and all byte lanes are active.
## Goals
- Add data BRAM.
- Issue D-bus requests for `lw` and `sw`.
- Learn request/response sequencing for loads.
## New Concepts
- Word: 32-bit value in RV32.
- Aligned access: address is a multiple of access size.
- Response latency: time from request acceptance to returned data.
- Bus decoder: logic that routes an address to the right memory/peripheral.
## How To Think About It
A store is mostly "send address and data." A load is "send address, wait for response,
then write back." That waiting must be explicit in the control FSM.
## Learning Tasks
- Draw a timeline for `sw` and `lw`.
- Mark when `req_valid`, `req_ready`, `rsp_valid`, and `rsp_ready` are meaningful.
- Decide what state is retained while waiting for load data.
## Pitfalls
- Writing back load data before BRAM response is valid.
- Advancing PC before a load has completed.
- Forgetting to tie unused `amo` and sub-word strobes consistently.
## Tooling And Testing
- Start with store-then-load of one word.
- Add tests for first and last valid data BRAM addresses.
- Use waveform checks for request acceptance and response timing.
## References
- RISC-V unprivileged ISA loads/stores: https://riscv.org/technical/specifications/
- Vivado BRAM documentation: https://docs.xilinx.com/
- AXI valid/ready concepts: https://developer.arm.com/documentation/ihi0022/latest
@@ -0,0 +1,49 @@
# Phase 6.2 - Byte And Halfword Access
## Context
Sub-word memory operations make C programs practical. `char`, `short`, strings, and packed
data all depend on byte and halfword accesses.
## Goals
- Implement `lb`, `lbu`, `lh`, `lhu`, `sb`, and `sh`.
- Handle byte lanes and sign/zero extension.
- Trap misaligned accesses locally in the LSU.
## New Concepts
- Sign extension: preserving negative value when widening a smaller signed type.
- Zero extension: widening an unsigned value by filling high bits with zero.
- Byte enable: control bit selecting which byte lane is written.
- Misaligned access: address not divisible by the access size.
## How To Think About It
Stores are about choosing byte lanes. Loads are about extracting the correct lane and
extending it correctly. Misalignment is known before the bus request starts.
## Learning Tasks
- Map address low bits to byte lanes.
- Work through examples loading `0x80` as signed and unsigned byte.
- Decide how halfword alignment is detected.
## Pitfalls
- Applying sign extension before selecting the correct byte/halfword.
- Letting a misaligned request reach memory.
- Confusing endianness with bit numbering in diagrams.
## Tooling And Testing
- Test every byte offset for `sb` and `lb/lbu`.
- Test halfword offsets 0 and 2 as legal and 1 and 3 as misaligned.
- Use memory initialization patterns that make byte order obvious.
## References
- RISC-V unprivileged ISA load/store chapter: https://riscv.org/technical/specifications/
- Endianness overview: https://en.wikipedia.org/wiki/Endianness
- Project F memory articles: https://projectf.io/posts/
@@ -0,0 +1,47 @@
# Phase 6.3 - ILA On The Memory Bus
## Context
Memory bugs often need cycle-level visibility. ILA on the D-bus lets you see requests,
responses, byte strobes, and stalls in real hardware.
## Goals
- Learn practical bus-level debug with ILA.
- Capture load/store transactions.
- Compare hardware memory behavior with simulation.
## New Concepts
- Transaction: one logical memory operation, possibly spanning multiple cycles.
- Probe grouping: collecting related signals for readable waveforms.
- Trigger condition: event that starts capture.
## How To Think About It
Probe at protocol boundaries. If the LSU and memory disagree, the bus waveform tells you
which side violated the contract.
## Learning Tasks
- Choose probes for D-bus request payload, request handshake, response payload, response handshake.
- Create one program with predictable stores and loads.
- Trigger on a target address or final store.
## Pitfalls
- Capturing only data and not the valid/ready signals.
- Forgetting byte strobes when debugging sub-word stores.
- Using ILA before simulation has narrowed the failure.
## Tooling And Testing
- Use short programs so captures fit in ILA memory.
- Keep memory addresses distinctive and easy to recognize.
- Re-run timing after adding ILA probes.
## References
- Vivado ILA documentation: https://docs.xilinx.com/
- Digilent Arty A7 reference: https://digilent.com/reference/programmable-logic/arty-a7/reference-manual
- AXI-Lite valid/ready concepts for bus-debug vocabulary: https://developer.arm.com/documentation/ihi0022/latest
+48
View File
@@ -0,0 +1,48 @@
# Phase 6 - Load/Store
## Context
Load/store connects the CPU to data memory and later to memory-mapped devices. RISC-V is
a load/store architecture: arithmetic works on registers, memory access is explicit.
## Goals
- Add data BRAM at `0x8000_0000`.
- Define and use the D-bus contract.
- Support word, byte, and halfword memory operations.
## New Concepts
- LSU: load/store unit, responsible for address generation and data formatting.
- D-bus: data-side memory request/response channel.
- Byte lane: one of the four bytes in a 32-bit word.
- Write strobe: byte-enable mask for sub-word stores.
## How To Think About It
Memory operations are protocol transactions. Address alignment, byte selection, sign
extension, response timing, and error classification all matter.
## Learning Tasks
- Draw how a byte store updates one lane of a 32-bit word.
- Decide how the LSU detects misalignment before bus issue.
- Trace a load from execute through response and writeback.
## Pitfalls
- Returning unshifted data for byte/halfword loads.
- Treating misalignment as a bus error instead of an architectural exception.
- Forgetting stores do not write back to the register file.
## Tooling And Testing
- Test all byte offsets within a word.
- Use memory dumps or waveform inspection for store byte lanes.
- Add maximum cycle timeouts to catch hung bus handshakes.
## References
- RISC-V load/store semantics: https://riscv.org/technical/specifications/
- AMD/Xilinx block RAM documentation: https://docs.xilinx.com/
- AXI-Lite valid/ready concepts for handshake comparison: https://developer.arm.com/documentation/ihi0022/latest
@@ -0,0 +1,49 @@
# Phase 7.1 - UART TX Module
## Context
The TX module serializes bytes into start bit, data bits, and stop bit at a fixed baud
rate. It is a small timing-driven FSM.
## Goals
- Build a standalone transmitter.
- Learn baud-rate timing from the 50 MHz system clock.
- Verify busy/send behavior before CPU integration.
## New Concepts
- Baud rate: symbols per second on the serial line.
- Start bit: low bit marking beginning of a frame.
- Stop bit: high bit marking end of a frame.
- Bit timer: counter that holds each serial bit for the right number of clocks.
## How To Think About It
UART TX is a deterministic shift-and-timer machine. The hard part is not data structure;
it is exact timing and clean handoff from "send byte" to "busy."
## Learning Tasks
- Compute clocks per bit at 50 MHz and 115200 baud.
- Draw TX line waveform for one byte.
- Decide when `busy` asserts and deasserts.
## Pitfalls
- Off-by-one errors in the bit timer.
- Accepting a new byte before the previous frame has completed.
- Forgetting idle line is high.
## Tooling And Testing
- Simulate one byte and inspect the waveform.
- Test back-to-back bytes.
- On hardware, send a fixed message before involving the CPU.
## References
- Digilent Arty A7 reference: https://digilent.com/reference/programmable-logic/arty-a7/reference-manual
- UART framing overview: https://en.wikipedia.org/wiki/Universal_asynchronous_receiver-transmitter
- Nandland UART tutorial: https://nandland.com/uart-serial-port-module/
@@ -0,0 +1,49 @@
# Phase 7.2 - UART RX Module
## Context
The RX module samples an asynchronous serial input and reconstructs bytes. It must handle
input synchronization and sampling near bit centers.
## Goals
- Build a standalone receiver.
- Learn oversampling and input synchronization.
- Produce a received byte plus valid indication.
## New Concepts
- Asynchronous input: signal not aligned to the FPGA clock.
- Synchronizer: flip-flop chain reducing metastability risk.
- Metastability: temporary uncertain state when sampling near an input transition.
- Oversampling: sampling multiple times per bit period to find stable centers.
## How To Think About It
RX is less forgiving than TX because the external signal is not clocked by your FPGA.
Respect clock-domain boundary hygiene even for a slow UART pin.
## Learning Tasks
- Draw how a start bit is detected.
- Decide the sample point for each data bit.
- Define what `valid` means and how it is cleared.
## Pitfalls
- Sampling the RX pin without synchronization.
- Sampling too close to bit transitions.
- Losing bytes because there is no buffering or clear valid/ready policy.
## Tooling And Testing
- Simulate with ideal frames and then slightly shifted frames.
- Test receiver behavior with back-to-back bytes.
- Add a FIFO later if software cannot poll fast enough.
## References
- Metastability overview: https://www.01signal.com/verilog-design/clockdomains/crossing-basics/
- Nandland UART receiver tutorial: https://nandland.com/uart-serial-port-module/
- Digilent Arty A7 reference: https://digilent.com/reference/programmable-logic/arty-a7/reference-manual
@@ -0,0 +1,48 @@
# Phase 7.3 - Bus Decoder + MMIO
## Context
The D-bus now routes requests to either RAM or UART registers. This is the first small
SoC-style memory map.
## Goals
- Decode address ranges for RAM and UART.
- Implement UART TX, RX, and status registers.
- Preserve the D-bus handshake contract.
## New Concepts
- Address map: assignment of address ranges to memory or devices.
- Register side effect: read or write that changes device state.
- Unmapped access: address with no valid target, later an access fault.
- Peripheral slave: bus endpoint that responds to device register accesses.
## How To Think About It
MMIO registers are a hardware/software ABI. Once firmware depends on them, changing
semantics becomes painful. Document behavior precisely.
## Learning Tasks
- Write a register table with access type and side effects.
- Decide read behavior for write-only registers and write behavior for read-only registers.
- Trace a store to UART TX through the D-bus decoder.
## Pitfalls
- Having two slaves respond to the same address.
- Letting no slave respond and hanging the bus forever.
- Making status bits unclear or inverted relative to software expectations.
## Tooling And Testing
- Unit-test the decoder separately from UART timing.
- Use ILA probes on selected slave, request, response, and UART status.
- Test unmapped access behavior once trap support exists.
## References
- Memory-mapped I/O overview: https://en.wikipedia.org/wiki/Memory-mapped_I/O
- RISC-V privileged spec for access faults later: https://riscv.org/technical/specifications/
- AXI-Lite valid/ready concepts for comparison: https://developer.arm.com/documentation/ihi0022/latest
@@ -0,0 +1,48 @@
# Phase 7.4 - Hello World
## Context
This is the first user-visible proof that software running on your CPU can communicate
with the outside world through MMIO.
## Goals
- Run a hand-written assembly program from instruction BRAM.
- Poll UART status and write bytes to TX data.
- Verify output in a terminal.
## New Concepts
- Polling: repeatedly reading status until a condition is true.
- Firmware: low-level software running directly on the hardware.
- Memory-mapped register access: using load/store instructions to control a device.
## How To Think About It
This milestone tests the full vertical slice: instruction fetch, decode, ALU, branches,
loads/stores, bus decode, UART, board pinout, and terminal settings.
## Learning Tasks
- Trace each instruction in the polling loop.
- Explain why the program waits for `tx_busy` to clear.
- Identify every hardware block involved in printing one character.
## Pitfalls
- Terminal configured for the wrong baud or line settings.
- Writing bytes without checking busy status.
- Debugging UART first when the actual bug is branch/load/store behavior.
## Tooling And Testing
- First verify UART standalone.
- In hardware, probe UART status and D-bus writes if the terminal is silent.
- Keep the program tiny and deterministic.
## References
- RISC-V assembly manual: https://github.com/riscv-non-isa/riscv-asm-manual
- Digilent Arty A7 reference: https://digilent.com/reference/programmable-logic/arty-a7/reference-manual
- UART overview: https://en.wikipedia.org/wiki/Universal_asynchronous_receiver-transmitter
+48
View File
@@ -0,0 +1,48 @@
# Phase 7 - Memory-Mapped UART
## Context
This phase connects software-visible memory addresses to a peripheral. UART becomes the
first external communication path driven by CPU instructions.
## Goals
- Build standalone TX and RX UART blocks.
- Map UART registers into the D-bus address space.
- Print a message from software running on the CPU.
## New Concepts
- MMIO: memory-mapped I/O; device registers accessed with normal loads/stores.
- Address decoder: logic routing addresses to selected slaves.
- Status register: read-only register exposing peripheral state.
- FIFO: queue buffering bytes between producer and consumer.
## How To Think About It
The CPU should not know about UART internals. It issues stores and loads; the bus decoder
and peripheral register file translate those into device behavior.
## Learning Tasks
- Draw the UART register map.
- Decide what happens if software writes while TX is busy.
- Decide when RX data is consumed and status changes.
## Pitfalls
- Making register side effects ambiguous.
- Forgetting software must poll status before writes.
- Combining peripheral timing with CPU timing too tightly.
## Tooling And Testing
- Verify UART standalone before MMIO integration.
- Use a terminal for end-to-end tests and ILA for bus/peripheral mismatches.
- Keep register behavior documented for firmware authors.
## References
- Digilent Arty A7 USB-UART notes: https://digilent.com/reference/programmable-logic/arty-a7/reference-manual
- RISC-V platform-level interrupt spec for later context: https://github.com/riscv/riscv-plic-spec
- Memory-mapped I/O overview: https://en.wikipedia.org/wiki/Memory-mapped_I/O
@@ -0,0 +1,49 @@
# Phase 8.1 - Linker Script + Startup Code
## Context
C programs assume a runtime environment. Bare metal provides none unless you create it:
memory layout, stack pointer, zeroed globals, and entry point.
## Goals
- Place code and read-only data in instruction memory.
- Place writable data, BSS, and stack in data memory.
- Understand what startup code must do before calling `main`.
## New Concepts
- Section: named region of an object file, such as `.text` or `.bss`.
- Load address: where bytes are stored in the image.
- Virtual/runtime address: where code expects them at execution.
- Stack pointer: register pointing to top of current stack frame.
## How To Think About It
The linker script is the contract between memory hardware and compiled software. If it
lies, the CPU may be correct and the program will still fail.
## Learning Tasks
- Draw the memory layout from reset PC through stack top.
- Identify which sections need initial contents and which are zero-filled.
- Understand why `.bss` must be cleared.
## Pitfalls
- Placing stack where it collides with `.bss` or data.
- Forgetting alignment requirements.
- Using library code that expects syscalls or a full C runtime.
## Tooling And Testing
- Inspect ELF sections and symbols with binutils.
- Disassemble startup code and confirm the first instructions are supported.
- Create a C program with global initialized and uninitialized variables.
## References
- GNU ld scripts: https://sourceware.org/binutils/docs/ld/Scripts.html
- RISC-V ELF psABI: https://github.com/riscv-non-isa/riscv-elf-psabi-doc
- OSDev bare bones concepts: https://wiki.osdev.org/Bare_Bones
@@ -0,0 +1,49 @@
# Phase 8.2 - First GCC Program
## Context
The first compiled program proves your hardware can execute compiler-generated code, not
just carefully hand-authored assembly.
## Goals
- Compile a tiny C firmware for `rv32im`.
- Convert the ELF into a memory image.
- Print through UART MMIO from C.
## New Concepts
- `-march`: compiler ISA target string.
- `-mabi`: ABI selection, here `ilp32` for 32-bit integer/long/pointer.
- objcopy: tool that converts ELF into raw binary or other formats.
- Disassembly: readable assembly representation of generated machine code.
## How To Think About It
Treat the compiler as an external producer of instructions. Your job is to verify that
every emitted instruction is implemented or fails loudly.
## Learning Tasks
- Compare C source to generated assembly.
- Identify every load/store used for UART access.
- Confirm no CSR or `fence.i` instructions appear before Phase 12.
## Pitfalls
- Using the wrong `-march` and accidentally generating unsupported instructions.
- Forgetting `volatile` on MMIO accesses in firmware.
- Assuming the compiler will preserve simple-looking loops exactly.
## Tooling And Testing
- Always inspect early firmware with objdump.
- Build at low optimization first, then compare optimized output later.
- Keep firmware small enough to single-step mentally.
## References
- GCC RISC-V options: https://gcc.gnu.org/onlinedocs/gcc/RISC-V-Options.html
- RISC-V ELF psABI: https://github.com/riscv-non-isa/riscv-elf-psabi-doc
- GNU objcopy: https://sourceware.org/binutils/docs/binutils/objcopy.html
@@ -0,0 +1,49 @@
# Phase 8.3 - Fill Remaining RV32I Gaps
## Context
Compiler output will expose missing instructions and edge cases. This subphase closes
the base ISA gaps before moving into traps and privilege.
## Goals
- Implement remaining RV32I instructions required by compiler output.
- Decode `fence` as safe in a cacheless design.
- Make unsupported system instructions halt visibly.
## New Concepts
- `fence`: memory-ordering instruction.
- `fence.i`: instruction-stream synchronization instruction.
- `ecall`: environment call, later a trap.
- `ebreak`: breakpoint instruction, later a trap.
## How To Think About It
Incomplete ISA behavior should be loud. A clean halt with PC and instruction word is far
better than silent execution through an unsupported instruction.
## Learning Tasks
- Make a list of every RV32I instruction and current support status.
- Disassemble increasingly complex C programs and mark new instructions.
- Understand why `fence` can be harmless without caches while `fence.i` still needs decode policy.
## Pitfalls
- Treating `ecall` or `ebreak` as NOPs.
- Missing uncommon instructions emitted by switch statements or stack setup.
- Debugging firmware before checking the disassembly.
## Tooling And Testing
- Add illegal-instruction tests.
- Keep a cycle limit in simulations to catch hangs.
- Use compiler output as a test source, not a proof of correctness.
## References
- RISC-V unprivileged ISA: https://riscv.org/technical/specifications/
- GCC RISC-V options: https://gcc.gnu.org/onlinedocs/gcc/RISC-V-Options.html
- RISC-V assembly manual: https://github.com/riscv-non-isa/riscv-asm-manual
@@ -0,0 +1,49 @@
# Phase 8.4 - Meaningful C Program
## Context
After minimal C works, write a program large enough to exercise stack, branches, calls,
arrays, strings, and UART interaction.
## Goals
- Run an interactive or semi-interactive firmware.
- Exercise real C control flow and memory behavior.
- Build confidence before adding privilege and traps.
## New Concepts
- Stack frame: per-call storage for return address, locals, and saved registers.
- Recursion: function calling itself, useful for stressing stack behavior.
- Jump table: compiler-generated table for some switch statements.
- Serial monitor: small firmware command interface over UART.
## How To Think About It
This is a system test, not a unit test. Failures can come from CPU, memory, compiler
assumptions, or firmware bugs. Use it after smaller tests pass.
## Learning Tasks
- Trace one function call through prologue and epilogue.
- Observe stack growth and confirm it stays within data memory.
- Add one feature at a time to firmware and inspect generated assembly.
## Pitfalls
- Pulling in library functions accidentally.
- Overflowing the tiny BRAM-backed stack.
- Debugging optimized code before unoptimized code works.
## Tooling And Testing
- Use map files to understand memory usage.
- Add firmware-visible test results through UART.
- Keep a known-good simple firmware for regression comparisons.
## References
- RISC-V ELF psABI: https://github.com/riscv-non-isa/riscv-elf-psabi-doc
- GNU ld map files: https://sourceware.org/binutils/docs/ld/Options.html
- Embedded Artistry bare-metal articles: https://embeddedartistry.com/
@@ -0,0 +1,49 @@
# Phase 8.5 - riscv-tests Compliance
## Context
Official instruction tests catch ISA bugs that ad hoc tests miss. Run them before adding
trap, interrupt, and privilege complexity.
## Goals
- Build and run `rv32ui-p-*` and `rv32um-p-*`.
- Create a simulation harness that loads test programs.
- Establish regression coverage for later phases.
## New Concepts
- Compliance test: focused program checking architectural behavior.
- Harness: wrapper that provides memory, stop condition, and pass/fail reporting.
- Signature region: memory region where tests record results.
- Regression: test suite run repeatedly to catch breakage.
## How To Think About It
Do not treat compliance tests as optional polish. They are how you avoid stacking new
features on top of unknown ISA bugs.
## Learning Tasks
- Understand how a riscv-test indicates pass/fail.
- Learn how ELF sections map into your BRAM model.
- Categorize failures by decode, execute, memory, or control-flow likely cause.
## Pitfalls
- Running tests with the wrong ISA target.
- Assuming the test harness memory layout matches your hardware automatically.
- Ignoring one failing test because normal firmware seems to work.
## Tooling And Testing
- Automate running all tests once the harness exists.
- Keep failing waveforms short and reproducible.
- Re-run the suite after every meaningful datapath change.
## References
- riscv-tests repository: https://github.com/riscv-software-src/riscv-tests
- RISC-V architectural tests: https://github.com/riscv-non-isa/riscv-arch-test
- RISC-V unprivileged ISA: https://riscv.org/technical/specifications/
@@ -0,0 +1,49 @@
# Phase 8 - GCC Toolchain Integration
## Context
This phase moves from hand-written assembly to compiled C. The CPU must now satisfy the
expectations of a real compiler, linker, and runtime startup.
## Goals
- Create linker script and startup code.
- Compile simple C programs for the implemented ISA.
- Run RISC-V compliance-style tests.
## New Concepts
- Linker script: file controlling where sections are placed in memory.
- crt0: startup code that prepares stack and globals before `main`.
- ELF: executable/linkable file format used by toolchains.
- ABI: software binary contract for registers, stack, calls, and data layout.
## How To Think About It
GCC is an unforgiving test generator. It will use legal instruction combinations you did
not think to test manually. That is exactly why this phase matters.
## Learning Tasks
- Understand sections: `.text`, `.rodata`, `.data`, `.bss`, and stack.
- Learn how ELF becomes a BRAM initialization image.
- Compare disassembly with your implemented instruction list.
## Pitfalls
- Advertising ISA extensions before hardware decodes them.
- Forgetting to initialize stack or zero `.bss`.
- Assuming "simple C" means "simple instruction stream."
## Tooling And Testing
- Use objdump for every early firmware image.
- Keep `-nostdlib` until you deliberately provide runtime support.
- Run compliance tests before adding traps and privilege complexity.
## References
- RISC-V ELF psABI: https://github.com/riscv-non-isa/riscv-elf-psabi-doc
- GNU linker scripts: https://sourceware.org/binutils/docs/ld/Scripts.html
- riscv-tests: https://github.com/riscv-software-src/riscv-tests
@@ -0,0 +1,49 @@
# Phase 9 - GCC-Built BIOS / Serial Monitor
## Context
This phase turns the GCC bring-up work into a persistent interactive firmware. The
CPU boots into a small monitor instead of a one-off test program.
## Goals
- Build a freestanding C/assembly BIOS with its own linker script and startup code.
- Provide a UART command prompt.
- Add simple commands for memory inspection, loading, and jumping to test payloads.
## New Concepts
- Monitor: small firmware that lets you inspect and control a machine interactively.
- Command parser: text interface that maps typed commands to firmware functions.
- Firmware ABI: the calling convention and data contract between loaded code and BIOS.
- Executable RAM window: memory that software can write and the fetch path can execute.
## How To Think About It
The BIOS is both a milestone and a tool. Keep it boring and reliable: UART in, UART out,
explicit commands, clear error messages, and no hidden dependencies on host tooling.
## Learning Tasks
- Decide where UART-loaded code can live and how the I-bus fetches it.
- Define a tiny BIOS call table for console I/O and returning to the monitor.
- Write down the exact register state expected by `run <addr>`.
- Add commands one at a time and test each on hardware.
## Pitfalls
- Loading code into data memory that the instruction fetch path cannot see.
- Letting a failed command corrupt the monitor's own stack or globals.
- Building a clever shell before the load/run/debug basics work.
## Tooling And Testing
- Test the monitor in simulation with scripted UART input where practical.
- Use a terminal program that can send raw files without changing line endings.
- Keep a known-good tiny payload that prints one line and returns to the monitor.
## References
- RISC-V ELF psABI: https://github.com/riscv-non-isa/riscv-elf-psabi-doc
- GNU linker scripts: https://sourceware.org/binutils/docs/ld/Scripts.html
- OSDev bare bones background: https://wiki.osdev.org/Bare_Bones
+51
View File
@@ -0,0 +1,51 @@
# Phase 10 - Minimal ELF Loader
## Context
This phase lets the BIOS receive GCC-built programs as ELF files and run them without
rebuilding the FPGA bitstream.
## Goals
- Parse enough ELF32 to recognize RISC-V executable files.
- Load `PT_LOAD` segments into memory and zero BSS.
- Jump to `e_entry` with a fixed tiny-program ABI.
## New Concepts
- ELF header: file-level metadata describing architecture, entry point, and tables.
- Program header: runtime load description used by loaders.
- `PT_LOAD`: segment that must be copied into memory before execution.
- BSS: zero-initialized memory represented by `memsz > filesz`.
## How To Think About It
Do not build a general-purpose dynamic loader. Build a strict loader for the exact
firmware shape you produce: static, little-endian, non-relocatable RV32 ELF files with
fixed physical addresses.
## Learning Tasks
- Read the ELF header fields needed to reject unsupported files.
- Map `p_paddr` or `p_vaddr` to your executable memory window.
- Define the stack pointer and argument registers at program entry.
- Decide how a loaded program returns a status code to the BIOS.
## Pitfalls
- Trusting malformed ELF offsets or sizes.
- Forgetting to zero the BSS tail of a loadable segment.
- Accidentally accepting relocatable or dynamically linked binaries.
- Loading a segment over the BIOS itself.
## Tooling And Testing
- Use `readelf -h -l` and `objdump -d` on every early payload.
- Start with one fixed link address and one loadable segment.
- Add loud rejection messages for unsupported ELF class, endianness, machine, or type.
## References
- ELF specification overview: https://refspecs.linuxfoundation.org/elf/elf.pdf
- RISC-V ELF psABI: https://github.com/riscv-non-isa/riscv-elf-psabi-doc
- GNU binutils `readelf` documentation: https://sourceware.org/binutils/docs/binutils/readelf.html
+50
View File
@@ -0,0 +1,50 @@
# Phase 11 - Tiny Kernel + Command Shell
## Context
This phase builds an OS-shaped firmware payload before the project moves into traps,
interrupts, privilege, DRAM, and Linux.
## Goals
- Load a separate GCC-built kernel ELF from the BIOS.
- Provide a kernel-owned command shell.
- Run tiny trusted ELF programs through a simple service table.
## New Concepts
- Kernel: central program that owns machine services. Before traps, this is still
trusted bare-machine firmware, not an isolated privileged OS.
- Service table: function-pointer ABI for console I/O, exit, and simple utilities.
- Bump allocator: simple allocator that hands out memory linearly.
- Program table: small kernel data structure tracking loaded payloads.
## How To Think About It
The goal is a complete loop, not a Unix clone: compile a tiny program on the host,
send it over UART, load it as ELF, run it on the CPU, and return to the shell.
## Learning Tasks
- Decide whether kernel console I/O calls the BIOS or drives UART directly.
- Define the tiny-program ABI: entry registers, stack, service table, and return path.
- Implement the smallest useful allocator.
- Add shell commands that expose real machine state, not decorative output.
## Pitfalls
- Pretending trusted payloads are isolated before traps and privilege modes exist.
- Growing the kernel into a second large project too early.
- Hiding loader or ABI bugs behind ad hoc special cases.
## Tooling And Testing
- Keep kernel and user-payload linker scripts separate.
- Build a tiny test suite of loaded programs: hello, echo, memory copy, return status.
- Run the same payloads after Phase 12 trap handling lands to catch ABI regressions.
## References
- OSDev kernel structure notes: https://wiki.osdev.org/Kernel
- RISC-V calling convention: https://github.com/riscv-non-isa/riscv-elf-psabi-doc
- Embedded Artistry first-fit allocator background: https://embeddedartistry.com/blog/2017/02/15/implementing-malloc-first-fit-free-list/
+52
View File
@@ -0,0 +1,52 @@
# Phase 12 - CSRs + M-Mode Trap Handling
## Context
This phase adds architectural control registers and the mechanism for exceptions and
interrupts. The CPU stops merely halting on faults and starts transferring control to
software handlers.
## Goals
- Implement key machine-mode CSRs.
- Implement CSR instructions and `mret`.
- Add trap entry for illegal instructions, breakpoints, ecalls, and access faults.
## New Concepts
- CSR: control and status register, special architectural register accessed by CSR instructions.
- Trap: synchronous exception or asynchronous interrupt that redirects execution.
- `mepc`: CSR holding PC to return to after a trap.
- `mcause`: CSR describing why the trap occurred.
- `mtvec`: CSR holding trap handler address.
- `mret`: instruction returning from machine-mode trap.
## How To Think About It
Traps are controlled context switches within the CPU. Hardware records enough state for
software to inspect the cause, handle it, and resume or terminate.
## Learning Tasks
- Draw the exact state updates on trap entry.
- Write a table of each exception source and its `mcause`, then compare it with
the trap-cause decisions in the roadmap.
- Understand which PC is saved for each exception type.
## Pitfalls
- Saving the wrong PC in `mepc`.
- Updating architectural state for a faulting instruction that should not retire.
- Treating all exceptions as illegal instructions.
## Tooling And Testing
- Create tiny programs that intentionally trigger one exception at a time.
- Inspect CSR values after trap entry in simulation.
- Test `mret` returning to a known instruction address.
## References
- RISC-V privileged architecture spec: https://riscv.org/technical/specifications/
- RISC-V CSR instruction semantics: https://riscv.org/technical/specifications/
- RISC-V educational trap handling notes: https://osblog.stephenmarz.com/
+48
View File
@@ -0,0 +1,48 @@
# Phase 13 - Timer
## Context
A timer provides a predictable interrupt source. Operating systems need timers for
scheduling, timeouts, and delays.
## Goals
- Implement a free-running machine timer counter.
- Implement compare register behavior.
- Generate and handle timer interrupts through the trap path.
## New Concepts
- `mtime`: monotonically increasing machine timer counter.
- `mtimecmp`: compare value that triggers a timer interrupt when reached.
- Interrupt: asynchronous trap caused by external or timer event.
- Pending bit: CSR bit indicating an interrupt source is waiting.
## How To Think About It
Timer hardware is simple; correct interrupt integration is the real lesson. You must
coordinate compare logic, pending state, enable bits, global interrupt enable, and trap entry.
## Learning Tasks
- Draw how `mtime`, `mtimecmp`, `mip`, and `mie` relate.
- Decide how software reads/writes a 64-bit timer from a 32-bit core.
- Trace a timer interrupt from compare match to handler entry.
## Pitfalls
- Generating level interrupts that never clear.
- Mishandling 64-bit register access from RV32 software.
- Taking interrupts while already in an unsafe state.
## Tooling And Testing
- Simulate with small compare values so tests finish quickly.
- Test enabling, disabling, pending, and clearing behavior separately.
- Add waveform probes for `mtime`, compare result, pending bit, and trap entry.
## References
- RISC-V privileged architecture spec: https://riscv.org/technical/specifications/
- RISC-V ACLINT specification: https://github.com/riscvarchive/riscv-aclint
- Linux timekeeping background: https://docs.kernel.org/timers/
@@ -0,0 +1,51 @@
# Phase 14 - Interrupt Controller
## Context
After the timer, the system needs a way to manage external interrupt sources such as UART
RX. A PLIC-like controller arbitrates and presents external interrupts to the CPU.
## Goals
- Build a minimal interrupt controller.
- Connect UART RX as an interrupt source.
- Learn interrupt priority, enable, pending, claim, and complete concepts.
## New Concepts
- PLIC: Platform-Level Interrupt Controller used by many RISC-V systems.
- Interrupt priority: ordering among pending interrupt sources.
- Claim: software reads which interrupt it should service.
- Complete: software tells the controller an interrupt has been handled.
- Edge/level interrupt: whether an event is a pulse or held condition.
## How To Think About It
An interrupt controller is hardware/software coordination. The device requests service,
the controller prioritizes it, the CPU traps, and software acknowledges the right places
in the right order.
## Learning Tasks
- Draw interrupt flow from UART RX byte to trap handler.
- Decide whether UART interrupt is level-sensitive or edge-sensitive.
- Understand claim/complete even if your first controller is simplified.
## Pitfalls
- Losing an interrupt event because it is only a one-cycle pulse.
- Clearing the device before software can observe why it interrupted.
- Taking an interrupt repeatedly because pending state is never cleared.
## Tooling And Testing
- Start with one interrupt source before adding priority.
- Test masked, unmasked, pending, claim, and complete behavior.
- Use ILA on interrupt request, pending, CPU external interrupt, and trap entry.
## References
- RISC-V PLIC specification: https://github.com/riscv/riscv-plic-spec
- RISC-V privileged architecture spec: https://riscv.org/technical/specifications/
- Linux interrupt concepts: https://docs.kernel.org/core-api/genericirq.html
+49
View File
@@ -0,0 +1,49 @@
# Phase 15 - A Extension
## Context
The RISC-V A extension adds atomic memory operations. Mainline Linux expects atomics for
locking, reference counts, futexes, and synchronization.
## Goals
- Implement LR/SC.
- Implement AMO read-modify-write instructions.
- Run RV32A tests before Linux work.
## New Concepts
- Atomic operation: memory operation that appears indivisible to other agents.
- LR/SC: load-reserved/store-conditional pair.
- Reservation: remembered address that allows a later store-conditional to succeed.
- AMO: atomic memory operation combining load, operation, and store.
## How To Think About It
In a single-hart, single-master system atomics are conceptually simple. The value is in
building the right architectural behavior now so the Linux path is realistic later.
## Learning Tasks
- Study success and failure cases for `sc.w`.
- Decide when the reservation is cleared.
- List AMO operations and their signed/unsigned comparison behavior.
## Pitfalls
- Returning the wrong success code from `sc.w`.
- Forgetting AMOs return the original memory value.
- Assuming single-hart shortcuts will remain valid if DMA or another master is added.
## Tooling And Testing
- Run `rv32ua` tests from riscv-tests or architectural tests.
- Use memory traces to verify read-modify-write ordering.
- Test reservation clearing on stores to the reserved address.
## References
- RISC-V unprivileged ISA, A extension: https://riscv.org/technical/specifications/
- riscv-tests: https://github.com/riscv-software-src/riscv-tests
- Linux atomic operations documentation: https://docs.kernel.org/core-api/wrappers/atomic_t.html
+50
View File
@@ -0,0 +1,50 @@
# Phase 16 - Pipeline
## Context
Pipelining overlaps multiple instructions to improve throughput. This is optional for
the Linux goal but highly educational for computer architecture.
## Goals
- Insert registers between conceptual stages.
- Handle data and control hazards.
- Re-run regression tests after changing timing structure.
## New Concepts
- Pipeline stage: portion of instruction work separated by registers.
- Hazard: situation where overlapping instructions would produce wrong behavior.
- Forwarding: using a result before it reaches the register file.
- Flush: discarding wrong-path instructions.
- Stall: holding one or more stages until a hazard clears.
## How To Think About It
A pipeline is not just adding registers. It changes when values are available and when
instructions retire. Correctness depends on explicit hazard handling.
## Learning Tasks
- Draw instruction timelines for dependent arithmetic operations.
- Identify where branch decisions occur and what must be flushed.
- Decide how multi-cycle M and memory operations interact with the pipeline.
## Pitfalls
- Adding pipeline registers before defining valid/kill/stall behavior.
- Forgetting load-use hazards.
- Letting exceptions retire out of order.
## Tooling And Testing
- Keep a non-pipelined core as a conceptual reference.
- Run compliance tests before and after each pipeline milestone.
- Add trace logging of retired instructions if possible.
## References
- Computer Organization and Design RISC-V edition: https://shop.elsevier.com/books/computer-organization-and-design-risc-v-edition/patterson/978-0-12-820331-6
- Hazard overview: https://en.wikipedia.org/wiki/Hazard_(computer_architecture)
- RISC-V formal interface concepts: https://github.com/SymbioticEDA/riscv-formal
+53
View File
@@ -0,0 +1,53 @@
# Phase 17 - SPI Flash Boot + DRAM
## Context
BRAM is small and initialized by the FPGA bitstream. A Linux-capable system needs
persistent boot storage and much larger RAM, so this phase adds flash boot and DDR3L.
## Goals
- Boot from on-board SPI flash.
- Integrate DDR3L through the Xilinx MIG IP.
- Move executable images into DRAM and run from there.
## New Concepts
- SPI flash: serial nonvolatile storage on the board.
- DRAM: external dynamic memory requiring a controller and refresh.
- MIG: Xilinx Memory Interface Generator IP for DDR memory controllers.
- Bootloader: small program that prepares memory and jumps to a larger image.
- Arbiter: logic choosing which bus master accesses shared memory.
## How To Think About It
This is the first major SoC integration phase. External memory is not a simple array:
latency varies, calibration matters, and reset sequencing becomes important.
The concrete map follows the roadmap: the 16 MB on-board SPI flash is mapped at
`0x0000_0000-0x00FF_FFFF` inside the reserved boot aperture, and the 256 MB DDR3L
DRAM replaces data BRAM at `0x8000_0000-0x8FFF_FFFF`.
## Learning Tasks
- Understand the Arty A7 memory devices and the fixed address ranges above.
- Draw boot flow from reset to flash fetch to DRAM copy to jump.
- Decide how I-bus and D-bus arbitrate for DRAM.
## Pitfalls
- Assuming DRAM is ready immediately after FPGA reset.
- Ignoring MIG clocking and reset requirements.
- Mixing boot ROM, flash aperture, and DRAM addresses without a clear map.
## Tooling And Testing
- Validate DRAM with a standalone memory test before CPU boot.
- Use ILA around MIG app interface and bus arbiter.
- Keep a tiny BRAM-resident fallback test path while debugging flash/DRAM.
## References
- Digilent Arty A7 reference: https://digilent.com/reference/programmable-logic/arty-a7/reference-manual
- AMD/Xilinx MIG documentation: https://docs.xilinx.com/
- DDR3 SDRAM background: https://en.wikipedia.org/wiki/DDR3_SDRAM
@@ -0,0 +1,51 @@
# Phase 18 - S-Mode, U-Mode, Sv32 Virtual Memory
## Context
Linux normally runs with privilege separation and virtual memory. This phase adds
supervisor/user modes and Sv32 address translation.
## Goals
- Add supervisor and user privilege modes.
- Implement Sv32 page-table walking.
- Add S-mode CSRs and page-fault traps.
## New Concepts
- Privilege mode: execution level controlling access rights.
- S-mode: supervisor mode, where the OS kernel normally runs.
- U-mode: user mode, where applications normally run.
- MMU: memory management unit performing address translation and protection.
- Sv32: RISC-V 32-bit virtual-memory scheme with two-level page tables.
- TLB: translation lookaside buffer, cache of recent address translations.
## How To Think About It
Virtual memory is both translation and permission checking. A correct page walker that
ignores permissions is not enough for an OS.
## Learning Tasks
- Draw Sv32 virtual address fields, page table levels, and PTE format.
- Trace one virtual load through translation to physical memory.
- Learn which traps are page faults versus access faults.
## Pitfalls
- Confusing physical memory protection with virtual page permissions.
- Mishandling `satp` updates and `sfence.vma`.
- Letting user mode access supervisor-only pages.
## Tooling And Testing
- Start with hand-built page tables and one mapped page.
- Test instruction, load, and store page faults separately.
- Add trace visibility for virtual address, PTEs, physical address, and cause.
## References
- RISC-V privileged architecture spec, Sv32: https://riscv.org/technical/specifications/
- Linux memory-management docs: https://docs.kernel.org/mm/
- OSDev paging overview: https://wiki.osdev.org/Paging
@@ -0,0 +1,52 @@
# Phase 19 - Linux Boot Contract
## Context
Linux does not start from reset like bare-metal firmware. It expects a specific entry
state, memory layout, device tree, and usually firmware services.
## Goals
- Define the exact kernel entry ABI.
- Provide a device tree describing the SoC.
- Choose direct M-mode Linux or OpenSBI + S-mode Linux.
## New Concepts
- Device tree: data structure describing hardware to the kernel.
- DTB: compiled binary form of a device tree source.
- SBI: Supervisor Binary Interface, firmware API used by S-mode kernels.
- OpenSBI: common RISC-V machine-mode firmware implementation.
- Hart: RISC-V hardware thread, roughly a CPU core/thread.
## How To Think About It
This phase is about removing ambiguity before kernel bring-up. If the boot contract is
wrong, Linux often just hangs early with little output.
## Learning Tasks
- Read the RISC-V Linux boot protocol and list required register state.
- Draft a device tree matching your memory map and interrupt topology.
- Use the modern CPU ISA properties: `riscv,isa-base = "rv32i"` and
`riscv,isa-extensions = "i", "m", "a", "zicsr", "zifencei"`. Keep the legacy
`riscv,isa = "rv32ima_zicsr_zifencei"` string only as a compatibility fallback.
- Decide whether the first Linux attempt uses direct M-mode or OpenSBI.
## Pitfalls
- Passing the wrong DTB address in `a1`.
- Loading the kernel at the wrong alignment.
- Claiming device-tree compatibility with hardware you did not implement.
## Tooling And Testing
- Validate DTS with `dtc`.
- Keep early console strategy simple and documented.
- Build small firmware checks that print boot parameters before jumping to Linux.
## References
- RISC-V Linux boot protocol: https://docs.kernel.org/arch/riscv/boot.html
- RISC-V device-tree CPU bindings: https://www.kernel.org/doc/Documentation/devicetree/bindings/riscv/cpus.yaml
- OpenSBI: https://github.com/riscv-software-src/opensbi
+50
View File
@@ -0,0 +1,50 @@
# Phase 20 - Linux
## Context
This is the summit: boot a minimal Linux kernel with a BusyBox initramfs to a shell over
UART on the CPU and SoC you built.
## Goals
- Build a kernel matching the implemented ISA and platform.
- Provide DTB and initramfs.
- Reach an interactive shell or a controlled init process.
## New Concepts
- Kernel config: set of build-time options selecting architecture and drivers.
- Initramfs: initial root filesystem bundled or loaded with the kernel.
- BusyBox: compact Unix userland used in embedded systems.
- Early console: minimal logging path before normal console drivers initialize.
- Root filesystem: filesystem mounted as `/` by Linux.
## How To Think About It
Linux bring-up is system debugging under poor visibility. Work from known-good layers:
CPU tests, DRAM test, boot firmware, DTB validation, early console, then userspace.
## Learning Tasks
- Understand the kernel image format and load addresses for RV32.
- Build the smallest kernel config that matches your hardware.
- Trace boot log milestones and map them to hardware dependencies.
## Pitfalls
- Debugging Linux before compliance tests and DRAM tests are clean.
- Enabling drivers for devices you do not actually implement.
- Losing console output because UART binding or clock frequency is wrong.
## Tooling And Testing
- Use earlycon/earlyprintk-style mechanisms where applicable.
- Keep kernel, DTB, firmware, and bitstream versions tied together.
- Save full boot logs and compare against previous attempts.
## References
- Linux RISC-V documentation: https://docs.kernel.org/arch/riscv/
- Buildroot: https://buildroot.org/
- BusyBox: https://busybox.net/