Compare commits
10 Commits
e98b3694ab
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 72c970fc20 | |||
| 329610807e | |||
| e8631501e8 | |||
| 07dd8e21f0 | |||
| 91ca63cf73 | |||
| fdf9292b8a | |||
| c1ffb0ee41 | |||
| b008b37d49 | |||
| 8edfc86027 | |||
| bcbf1fa616 |
+25
@@ -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.*
|
||||
@@ -2,61 +2,85 @@
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is a custom RISC-V RV32IM CPU core written in SystemVerilog, targeting the
|
||||
Digilent Arty A7 100T (xc7a100tcsg324-1) with Vivado 2025.2 or later.
|
||||
This is a custom RISC-V CPU core written in SystemVerilog, targeting the
|
||||
Digilent Arty A7 100T (xc7a100tcsg324-1) with Vivado 2025.2 or later. It
|
||||
starts as RV32IM and grows toward **RV32IMA + Zicsr + Zifencei + M/S/U
|
||||
privilege + Sv32** for a Linux-capable SoC.
|
||||
|
||||
The goal is incremental development from a single-cycle core to a Linux-capable SoC.
|
||||
See ROADMAP.md for the full phased plan.
|
||||
|
||||
## Conventions
|
||||
|
||||
- Language: SystemVerilog (not Verilog). Use SV features: packages, structs, enums,
|
||||
always_comb, always_ff, logic (not reg/wire).
|
||||
- All inter-stage signals are defined as structs in `rtl/pkg/rv32_pkg.sv`. Always
|
||||
import this package. When adding new functionality, extend the existing structs
|
||||
rather than adding loose wires.
|
||||
- All inter-stage signals are defined as structs in `FPGA/rtl/pkg/rv32_pkg.sv`.
|
||||
Always import this package. When adding new functionality, extend the existing
|
||||
structs rather than adding loose wires.
|
||||
- Module naming: `rv32_<block>` (e.g., `rv32_alu`, `rv32_decode`, `rv32_regfile`).
|
||||
- File naming: one module per file, filename matches module name.
|
||||
- Testbenches: `tb/tb_<module>.sv`. Use Vivado simulator.
|
||||
- Testbenches: `FPGA/tb/tb_<module>.sv`. Use Vivado simulator.
|
||||
- Clock: single clock domain, active rising edge, signal named `clk`. Target
|
||||
frequency: 50 MHz (derived from the Arty's 100 MHz oscillator via MMCM/2).
|
||||
- Reset: synchronous active-high, signal named `rst`. The Arty's `CK_RST` button
|
||||
is active-low; the top-level wraps it through a 2-FF synchronizer and inverts
|
||||
to produce the internal `rst`. The rest of the design only sees synchronous
|
||||
active-high.
|
||||
- BRAM init files: `mem/*.mem` in hex format, one 32-bit word per line.
|
||||
- Firmware source: `fw/` directory. Built with `riscv64-unknown-elf-gcc`
|
||||
(multilib build required), `-march=rv32im_zicsr_zifencei -mabi=ilp32`. The
|
||||
full march string is required by GCC 11+ even before CSRs are implemented;
|
||||
`Zifencei` is a NOP until caches exist (Phase 13+).
|
||||
- BRAM init files: `FPGA/mem/*.mem` in hex format, one 32-bit word per line.
|
||||
- Firmware source: `Software/fw/` directory. Built with `riscv64-unknown-elf-gcc`
|
||||
(multilib build required). March string evolves with the implemented ISA:
|
||||
- Phase 8 (pre-CSR bare-metal): `-march=rv32im -mabi=ilp32`
|
||||
- Phase 12+ (CSRs/Zifencei decoded): `-march=rv32im_zicsr_zifencei -mabi=ilp32`
|
||||
- Phase 15+ (atomics): `-march=rv32ima_zicsr_zifencei -mabi=ilp32`
|
||||
Don't advertise an extension before its ops are decoded — the compiler is
|
||||
free to emit them, and "trap as illegal" early is much better than NOPing.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
FPGA/ — everything that ends up on the FPGA
|
||||
rtl/ — synthesizable source
|
||||
pkg/ — packages (rv32_pkg.sv)
|
||||
core/ — CPU core modules (ALU, M unit, decoder, regfile, datapath)
|
||||
periph/ — peripherals (UART, timer, PLIC, etc.)
|
||||
top/ — top-level and SoC integration
|
||||
tb/ — testbenches (tb_<module>.sv)
|
||||
mem/ — BRAM init files (.mem)
|
||||
fw/ — firmware source (C, assembly, linker scripts)
|
||||
constraints/ — Vivado XDC constraint files
|
||||
docs/ — block diagrams, notes
|
||||
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
|
||||
|
||||
RV32IM base. Extensions added incrementally:
|
||||
- Zicsr (CSR instructions) — Phase 9
|
||||
- Zifencei (fence.i) — implemented as NOP until caches exist
|
||||
- M-mode privileged — Phase 9
|
||||
- S-mode + U-mode + Sv32 — Phase 14
|
||||
End target: **RV32IMA + Zicsr + Zifencei + M/S/U privilege + Sv32** (Linux-capable).
|
||||
|
||||
Extensions land incrementally:
|
||||
- RV32IM base — Phases 1-8
|
||||
- Zicsr (CSR instructions) + M-mode trap handling — Phase 12
|
||||
- Zifencei (`fence.i`) — decoded but NOP until caches exist (Phase 17+)
|
||||
- A (atomics: LR/SC + AMO) — Phase 15 (required for mainline Linux)
|
||||
- S-mode + U-mode + Sv32 — Phase 18
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
- Single-cycle first, pipeline later (Phase 12). Stages are separated in the code
|
||||
even without pipeline registers between them.
|
||||
- Single-cycle first, pipeline later (Phase 16, optional). Stages are separated
|
||||
in the code even without pipeline registers between them.
|
||||
- "Single-cycle" is a logical model, not a literal one cycle per instruction.
|
||||
BRAM has a 1-cycle read latency, so fetch is registered and loads take 2
|
||||
cycles. The datapath stalls fetch while a multi-cycle operation is in flight.
|
||||
@@ -73,10 +97,12 @@ RV32IM base. Extensions added incrementally:
|
||||
## 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_0000–0x00FF_FFFF in Phase 17
|
||||
0x1000_0000 – 0x1000_0FFF MMIO (UART; later: timer, PLIC)
|
||||
0x2000_0000 – 0x2000_FFFF instruction BRAM (64 KB)
|
||||
0x8000_0000 – 0x8000_FFFF data BRAM (64 KB) → grows to DRAM 0x8000_0000–0x8FFF_FFFF in Phase 13
|
||||
0x8000_0000 – 0x8000_FFFF data BRAM (64 KB) → grows to DRAM 0x8000_0000–0x8FFF_FFFF in Phase 17
|
||||
```
|
||||
|
||||
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)
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
- Always check rv32_pkg.sv first to understand current struct definitions and enums.
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,4 @@
|
||||
# FPGA/
|
||||
|
||||
Everything that ends up on the FPGA: SystemVerilog source, testbenches,
|
||||
constraints, memory init files, and the Vivado project.
|
||||
@@ -0,0 +1,4 @@
|
||||
# FPGA/constraints/
|
||||
|
||||
Vivado XDC constraint files: pin assignments for the Arty A7, clock
|
||||
definitions, and timing constraints.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,4 @@
|
||||
# FPGA/rtl/
|
||||
|
||||
Synthesizable SystemVerilog source. One module per file, filename matches
|
||||
module name (e.g. `rv32_alu.sv`).
|
||||
@@ -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>`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,4 @@
|
||||
# FPGA/tb/
|
||||
|
||||
SystemVerilog testbenches, one per module under test. Naming: `tb_<module>.sv`.
|
||||
Run with the Vivado simulator (xsim).
|
||||
@@ -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.*
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
@@ -1,13 +1,15 @@
|
||||
# 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
|
||||
|
||||
- Board: Digilent Arty A7 100T (xc7a100tcsg324-1)
|
||||
- Toolchain: Vivado 2025.2 or later
|
||||
- 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)
|
||||
|
||||
## 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
|
||||
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
|
||||
pipelined later (Phase 12).
|
||||
pipelined later (Phase 16, optional).
|
||||
|
||||
## Memory Map
|
||||
|
||||
| Address Range | Region |
|
||||
|---------------------------|----------------------------------------------|
|
||||
| `0x0000_0000–0x0FFF_FFFF` | reserved (SPI flash boot region, Phase 13) |
|
||||
| `0x0000_0000–0x0FFF_FFFF` | reserved boot aperture (Phase 17: 16 MB SPI flash at low end) |
|
||||
| `0x1000_0000–0x1000_0FFF` | MMIO (UART; later: timer, PLIC) |
|
||||
| `0x2000_0000–0x2000_FFFF` | instruction BRAM (64 KB) |
|
||||
| `0x8000_0000–0x8000_FFFF` | data BRAM (64 KB) → DRAM in Phase 13 |
|
||||
| `0x8000_0000–0x8000_FFFF` | data BRAM (64 KB) → DRAM in Phase 17 |
|
||||
|
||||
Reset PC = `0x2000_0000`.
|
||||
|
||||
@@ -42,21 +44,30 @@ UART registers (split layout):
|
||||
|
||||
Requires:
|
||||
- Vivado 2025.2 or later
|
||||
- RISC-V GCC toolchain — multilib `riscv64-unknown-elf-gcc` supporting
|
||||
`-march=rv32im_zicsr_zifencei -mabi=ilp32`
|
||||
- RISC-V GCC toolchain — multilib `riscv64-unknown-elf-gcc`. March string
|
||||
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)
|
||||
- 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
|
||||
|
||||
See `ROADMAP.md` for the full phased build plan.
|
||||
|
||||
## Current Phase
|
||||
|
||||
Phase 0 — Architecture contract (package definitions + block diagram)
|
||||
|
||||
## References
|
||||
|
||||
- [RISC-V ISA Spec Vol 1 (Unprivileged)](https://riscv.org/technical/specifications/)
|
||||
|
||||
+361
-69
@@ -1,17 +1,42 @@
|
||||
# RV32IM CPU Core — Build Roadmap
|
||||
# RISC-V CPU Core — Build Roadmap
|
||||
## Target: Digilent Arty A7 100T / Vivado 2025.2+ / SystemVerilog
|
||||
## Clock: 50 MHz (100 MHz xtal ÷ 2 via MMCM)
|
||||
## 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_0000–0x00FF_FFFF in Phase 17)
|
||||
0x1000_0000 – 0x1000_0FFF MMIO (UART; later: timer, PLIC)
|
||||
0x2000_0000 – 0x2000_FFFF instruction BRAM (64 KB)
|
||||
0x8000_0000 – 0x8000_FFFF data BRAM (64 KB) → DRAM 0x8000_0000–0x8FFF_FFFF in Phase 13
|
||||
0x8000_0000 – 0x8000_FFFF data BRAM (64 KB) → DRAM 0x8000_0000–0x8FFF_FFFF in Phase 17
|
||||
```
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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,
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — First CPU ("It's Alive")
|
||||
## Phase 4 — First CPU ("It's Alive") [Easy/fun]
|
||||
|
||||
### 4.1 — Fetch + Datapath Integration
|
||||
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
|
||||
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)
|
||||
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
|
||||
interface with address, write data, read data, write enable, and valid/ready
|
||||
signals.
|
||||
For now, only 32-bit aligned access (lw and sw).
|
||||
|
||||
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
|
||||
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
|
||||
byte-by-byte. You can't run real C code without these.
|
||||
|
||||
Testbench focus: Sign extension (loading 0xFF as signed byte should give 0xFFFFFFFF,
|
||||
as unsigned should give 0x000000FF). Unaligned access behavior (RISC-V base spec says
|
||||
misaligned access can trap — decide if you want to support it or trap).
|
||||
Implementation: the load/store unit drives `req.size` (00=byte, 01=halfword,
|
||||
10=word) and `req.wstrb` (which byte lane within the 32-bit word is being
|
||||
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.
|
||||
|
||||
@@ -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
|
||||
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
|
||||
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
|
||||
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
|
||||
```
|
||||
Convert: `objcopy -O binary firmware.elf firmware.bin`. Convert binary to
|
||||
`.mem` format. Load into BRAM. Run.
|
||||
|
||||
Why the long march string: GCC 11+ split `Zicsr` and `Zifencei` out of the
|
||||
RV32I base. `-march=rv32im` alone fails or warns. `Zifencei` is harmless
|
||||
(`fence.i` is a NOP until you have caches); `Zicsr` will only be emitted by
|
||||
code that uses CSR ops, so it's safe to advertise even before Phase 9.
|
||||
March string discipline: advertise only what the hardware decodes. `rv32im`
|
||||
is correct for Phase 8 because CSRs and `fence.i` aren't decoded yet. If the
|
||||
program (or crt0) emits a CSR or `fence.i` op before then, the **decoder**
|
||||
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
|
||||
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.
|
||||
|
||||
### 8.3 — Fill Remaining RV32I Gaps
|
||||
What: Implement any RV32I instructions you deferred. Common ones: all shift variants
|
||||
(sll, srl, sra, slli, srli, srai), set-less-than variants (slti, sltiu), fence (NOP
|
||||
for now — you have no cache), ecall/ebreak (NOP for now — you have no trap handling).
|
||||
What: Implement any RV32I instructions you deferred. Common ones: all shift
|
||||
variants (sll, srl, sra, slli, srli, srai), set-less-than variants (slti,
|
||||
sltiu), `fence` (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
|
||||
any non-trivial C code to work.
|
||||
`ecall` / `ebreak` / unrecognized opcodes / `fence.i` / any CSR op all go
|
||||
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
|
||||
usage, switch statements (these generate jump tables — exercises jalr with computed
|
||||
addresses), recursive functions.
|
||||
|
||||
### 8.4 — Milestone: Meaningful C Program
|
||||
What: Write something real — a serial monitor that accepts commands over UART
|
||||
and responds. Or a tiny Forth interpreter. Something interactive that proves
|
||||
the core is solid.
|
||||
### 8.4 — Milestone: Meaningful Standalone C Program
|
||||
What: Write something real but still self-contained — a checksum demo, tiny
|
||||
benchmark, string/array exercise, or simple UART-driven calculator. Keep the
|
||||
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
|
||||
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
|
||||
official suite catches the ones you didn't — corner cases in shifts, sign
|
||||
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.
|
||||
|
||||
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,
|
||||
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
|
||||
register). When mtime >= mtimecmp, a timer interrupt fires.
|
||||
|
||||
Why: Every operating system needs a timer tick for scheduling. Even bare-metal
|
||||
firmware needs delays and timeouts. This is your first interrupt source, which also
|
||||
validates that the trap handling from Phase 9 actually works end-to-end.
|
||||
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
|
||||
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 12 — Pipeline (Optional but educational)
|
||||
## Phase 15 — A Extension (Atomics) [Hard stretch]
|
||||
|
||||
What: Insert pipeline registers between your stages (fetch|decode|execute|memory|
|
||||
writeback). Handle data hazards (forwarding/stalling) and control hazards (branch
|
||||
prediction or pipeline flush).
|
||||
What: Implement the RV32A atomic instructions: `lr.w` / `sc.w` (load-reserved /
|
||||
store-conditional) and the AMO ops (`amoswap.w`, `amoadd.w`, `amoand.w`,
|
||||
`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
|
||||
path (probably through the ALU). Pipelining lets each stage run in one short cycle.
|
||||
This is the classic computer architecture exercise and deeply educational.
|
||||
Why now: Mainline Linux's RISC-V kernel is built against `rv32ima` /
|
||||
`rv64ima` — atomics are not optional for an unmodified kernel build. The
|
||||
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
|
||||
plenty for booting Linux. Pipeline if you want to learn, not because you must.
|
||||
Single-hart implementation: the reservation set is just one register
|
||||
(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 13 — SPI Flash Boot + DRAM
|
||||
## Phase 16 — Pipeline (Optional but educational) [Hard stretch]
|
||||
|
||||
What: Add an SPI flash controller to boot from the on-board flash (instead of BRAM
|
||||
initialization). Integrate AMD/Xilinx MIG IP for the DDR3 on the Arty A7. Update
|
||||
your memory map: flash at boot, copy to DRAM, jump to DRAM.
|
||||
What: Insert pipeline registers between your stages (fetch|decode|execute|
|
||||
memory|writeback). Handle data hazards (forwarding/stalling) and control
|
||||
hazards (branch prediction or pipeline flush).
|
||||
|
||||
Why: BRAM is tiny (a few hundred KB on Artix-7 100T). Linux needs megabytes. DRAM
|
||||
gives you 256MB. Flash gives you persistent storage for the bootloader. This is
|
||||
how real embedded systems boot.
|
||||
Why: Your single-cycle core's clock speed is limited by the longest
|
||||
combinational path (probably through the ALU). Pipelining lets each stage
|
||||
run in one short cycle. This is the classic computer architecture exercise
|
||||
and deeply educational.
|
||||
|
||||
Why optional: A 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
|
||||
(two-level page tables, 4KB pages). Add the satp CSR, page fault exceptions, and
|
||||
sfence.vma.
|
||||
What: Add an SPI flash controller to boot from the on-board flash (instead
|
||||
of BRAM initialization). The Arty A7-100T has a 16 MB Quad-SPI flash; map
|
||||
it at `0x0000_0000–0x00FF_FFFF` inside the reserved boot aperture.
|
||||
Integrate AMD/Xilinx MIG IP for the DDR3L on the Arty (256 MB). Map DRAM at
|
||||
`0x8000_0000–0x8FFF_FFFF`, replacing the data BRAM in that range.
|
||||
|
||||
Why: Linux runs the kernel in S-mode, user programs in U-mode. Virtual memory gives
|
||||
each process its own address space and protects the kernel from user code. This is the
|
||||
last major architectural piece before Linux.
|
||||
Boot flow: reset PC moves to `0x0000_0000` (flash). A small first-stage
|
||||
copies the kernel/firmware image from flash to DRAM, sets up the trap
|
||||
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 15 — Linux
|
||||
## Phase 18 — S-Mode, U-Mode, Sv32 Virtual Memory [Overkill/hard]
|
||||
|
||||
What: Port a minimal Linux kernel (or use an existing RISC-V port). Write a device
|
||||
tree for your SoC. Build an initramfs with BusyBox. Boot to a shell prompt.
|
||||
What: Add supervisor and user privilege modes. Implement Sv32 page table
|
||||
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
|
||||
|
||||
- Vivado 2025.2 or later (synthesis, simulation, ILA, VIO)
|
||||
- RISC-V GCC toolchain (`riscv64-unknown-elf-gcc`, multilib build — needs to
|
||||
support `rv32im_zicsr_zifencei` / `ilp32`)
|
||||
- `riscv-tests` repo cloned and buildable (Phase 8.5 onward)
|
||||
- RISC-V GCC toolchain (`riscv64-unknown-elf-gcc`, multilib build). March
|
||||
string evolves: `rv32im` (Phase 8) → `rv32im_zicsr_zifencei` (Phase 12) →
|
||||
`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
|
||||
- Text editor you like for SystemVerilog
|
||||
- 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`)
|
||||
|
||||
@@ -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/
|
||||
@@ -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`.
|
||||
@@ -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).
|
||||
@@ -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`).
|
||||
@@ -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).
|
||||
@@ -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
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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/
|
||||
@@ -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/
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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/
|
||||
|
||||
Reference in New Issue
Block a user