e98b3694ab
Replace the first broad project sketch with more concrete hardware assumptions for implementation on the Arty A7 100T. Document the 50 MHz MMCM-derived clock, synchronous active-high reset after the board reset synchronizer, and a split instruction/data BRAM map with reset PC at 0x2000_0000. Update the UART layout to separate TX, RX, and status registers, and make the README/roadmap agree on the address ranges that firmware and the future linker script will use. Also split the M extension out of the combinational ALU into a dedicated multi-cycle unit with start/busy/done handshaking, describe BRAM latency and stall behavior in the single-cycle logical model, and add riscv-tests as the planned compliance check once GCC-generated programs are running.
533 lines
24 KiB
Markdown
533 lines
24 KiB
Markdown
# RV32IM 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)
|
||
|
||
```
|
||
0x0000_0000 – 0x0FFF_FFFF reserved (SPI flash boot region, Phase 13)
|
||
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
|
||
```
|
||
|
||
Reset PC = `0x2000_0000`. Locking this in now keeps the linker script and crt0
|
||
stable across phases.
|
||
|
||
---
|
||
|
||
## Phase 0 — Architecture Contract
|
||
|
||
### 0.1 — SystemVerilog Package
|
||
What: Create a `.sv` package file with enums (ALU operations, opcode types, branch
|
||
types) and structs (fetch_out_t, decode_out_t, exec_out_t, mem_out_t) that define
|
||
the signals passed between pipeline stages.
|
||
|
||
Why: This is the "API" of your CPU. Every module you build will import this package.
|
||
If you get the struct definitions reasonable now, adding new instructions later means
|
||
adding a field to a struct — not re-plumbing wires across the whole design.
|
||
|
||
Future role: These structs stay forever. When you pipeline the core, the pipeline
|
||
registers are literally just these structs stored in flip-flops. When you add CSR
|
||
instructions, you add a field to decode_out_t. The package grows but never gets
|
||
replaced.
|
||
|
||
### 0.2 — Block Diagram (on paper)
|
||
What: Draw the major blocks (fetch, decode, ALU, register file, memory, writeback)
|
||
and the signals between them. Label signals with the struct names from 0.1.
|
||
|
||
Why: You need a physical reference to look at while coding. Screen diagrams get
|
||
buried in tabs. Paper on a wall stays visible.
|
||
|
||
Future role: You'll update this as the design grows. It becomes your architectural
|
||
source of truth when things get complex.
|
||
|
||
---
|
||
|
||
## Phase 1 — ALU + M Unit
|
||
|
||
### 1.1 — Combinational ALU + Simulation
|
||
What: Build a combinational ALU that handles all RV32I arithmetic/logic ops
|
||
(add, sub, and, or, xor, slt, sltu, shifts). Inputs: two 32-bit operands +
|
||
operation select from your enum. Outputs: 32-bit result.
|
||
|
||
Why: The ALU is the computational heart. Building it first gives you a
|
||
self-contained module to practice your testbench workflow.
|
||
|
||
Testbench focus: signed vs unsigned comparisons (SLT vs SLTU), arithmetic shift
|
||
right vs logical shift right, signed overflow.
|
||
|
||
Future role: This module is final. It goes into the finished core unchanged.
|
||
|
||
### 1.2 — Multi-cycle M Unit + Simulation
|
||
What: A separate module for the M extension (mul, mulh, mulhsu, mulhu, div,
|
||
divu, rem, remu) with a `start` / `busy` / `done` handshake. Multiply uses
|
||
DSP48s and completes in 2-3 cycles; divide is iterative (one bit per cycle,
|
||
~33 cycles for 32-bit). Outputs a 32-bit result on `done`.
|
||
|
||
Why M extension early: GCC emits mul/div constantly; without hardware, libgcc
|
||
emulation is slow and painful. Why a separate module: a 32-bit combinational
|
||
divider will not meet timing on Artix-7 at any reasonable Fmax — divide must
|
||
be iterative. Baking the stall handshake in from day one means the datapath
|
||
(Phase 4) is built around it from the start, not retrofitted.
|
||
|
||
Testbench focus: division by zero (RISC-V spec gives specific results, not an
|
||
exception — `div(x,0) = -1`, `rem(x,0) = x`), signed overflow on division
|
||
(`INT_MIN / -1` returns `INT_MIN`), `mulh*` upper-half results, back-to-back
|
||
operations without dropping `busy`.
|
||
|
||
Future role: Final. The handshake interface is reused unchanged when the core
|
||
is pipelined.
|
||
|
||
### 1.3 — ALU + M Unit on FPGA with VIO
|
||
What: Synthesize both on the Arty. Attach Vivado VIO cores to inputs, outputs,
|
||
and the M unit handshake. Use the Vivado hardware manager to drive operands
|
||
and read results.
|
||
|
||
Why: Confirms behavior in real hardware and catches synthesis issues early
|
||
(timing on the multiply path, divide unit FSM state encoding). Also gets you
|
||
comfortable with the VIO workflow.
|
||
|
||
What is VIO: A Vivado IP that lets you poke values into signals and read them
|
||
out through JTAG, right from the Vivado GUI. Think virtual switches and LEDs
|
||
with 32-bit width and no board wiring.
|
||
|
||
Future role: VIO familiarity pays off throughout the project.
|
||
|
||
---
|
||
|
||
## Phase 2 — Register File
|
||
|
||
### 2.1 — Register File Module + Simulation
|
||
What: Build the RISC-V register file — 32 registers, each 32 bits wide. Two read
|
||
ports (rs1, rs2), one write port (rd). Register x0 is hardwired to zero (writes to
|
||
it are ignored, reads always return 0).
|
||
|
||
Why: Every instruction reads from and/or writes to registers. This is the second
|
||
fundamental building block after the ALU. It's simple (it's basically a small RAM
|
||
with some special behavior on address 0) but getting the read/write timing right
|
||
matters.
|
||
|
||
Testbench focus: read-after-write in the same cycle (does the new value forward?),
|
||
write to x0 then read x0 (must be zero), read two different registers simultaneously.
|
||
|
||
Future role: This module is final. Might eventually get a third read port if you add
|
||
certain extensions, but for RV32IM it's done.
|
||
|
||
### 2.2 — Optional: VIO validation
|
||
What: Put it on FPGA with VIO if you want extra confidence. Usually sim is enough
|
||
for a register file since the logic is simple.
|
||
|
||
---
|
||
|
||
## Phase 3 — Decoder
|
||
|
||
### 3.1 — Instruction Decoder + Simulation
|
||
What: A combinational module that takes a raw 32-bit instruction word and produces
|
||
your decode_out_t struct: ALU operation, source/destination register addresses,
|
||
immediate value (sign-extended), memory read/write flags, branch type, etc.
|
||
|
||
Why: The decoder is the "brain" that tells every other module what to do. RISC-V has
|
||
six instruction formats (R, I, S, B, U, J) and the immediate bits are scattered
|
||
differently in each. Getting the immediate extraction and sign extension right is the
|
||
main challenge.
|
||
|
||
How to test: Use the RISC-V toolchain as your reference. Write small assembly
|
||
snippets, assemble them with `riscv64-unknown-elf-as`, then `objdump -d` to get the
|
||
binary encoding. Feed those encodings to your testbench and verify every output field.
|
||
This also gets you familiar with the cross-compilation toolchain you'll need later.
|
||
|
||
Testbench focus: Every instruction format. Pay special attention to immediate sign
|
||
extension (bit 31 of the instruction is always the sign bit in RISC-V — that's a
|
||
deliberate design choice). Verify that the decoder handles all R-type, I-type, S-type,
|
||
B-type, U-type, and J-type correctly.
|
||
|
||
Future role: When you add CSR instructions (Phase 9), you'll add a new case to the
|
||
decoder and a new field to the struct. The structure of the decoder doesn't change.
|
||
|
||
---
|
||
|
||
## Phase 4 — First CPU ("It's Alive")
|
||
|
||
### 4.1 — Fetch + Datapath Integration
|
||
What: Create an instruction BRAM initialized from a .mem file, mapped at
|
||
`0x2000_0000`. Add a PC register that resets to `0x2000_0000` and advances
|
||
by 4 when the next instruction is ready. Wire the chain:
|
||
BRAM[PC] → decoder → ALU/M unit → register file writeback. Support R-type and
|
||
I-type arithmetic for now (add, sub, addi, and, or, xor, slt, slti, lui,
|
||
auipc, shifts), plus the M-extension ops via the M unit's handshake.
|
||
|
||
Why: This is the first time all your modules work together as a CPU. It can't
|
||
branch, it can't access data memory, but it executes a sequence of arithmetic
|
||
instructions correctly. The wiring is where most bugs live — wrong bit ranges,
|
||
swapped operands, forgetting to connect a signal.
|
||
|
||
Important — "single-cycle" is a logical model: BRAM has a 1-cycle read
|
||
latency, so fetch is registered (PC → address one cycle, instruction available
|
||
the next). Each instruction takes 2 cycles end-to-end at minimum, and stalls
|
||
extend that whenever the M unit is busy. Plan the control FSM around this:
|
||
states like FETCH → EXECUTE, with a stall in EXECUTE while `m_busy` is high.
|
||
|
||
How to test: Hand-assemble 10-20 instructions into a .mem file. Calculate the
|
||
expected register state after each instruction by hand. Compare against what
|
||
the CPU actually produces.
|
||
|
||
What is a .mem file: A text file with hex values, one per line. Vivado can
|
||
initialize BRAMs from these. Each line is one 32-bit instruction.
|
||
|
||
Future role: This is your core. Everything from here on adds capabilities to it.
|
||
|
||
### 4.2 — ILA Verification on FPGA
|
||
What: Synthesize the CPU on the Arty. Attach ILA (Integrated Logic Analyzer) probes
|
||
to the PC, register write port, and ALU output. Set a trigger (e.g., when PC reaches
|
||
the last instruction). Inspect the captured waveform.
|
||
|
||
Why: Confirms hardware behavior matches simulation. Gets you comfortable with ILA,
|
||
which becomes your primary debugging tool for all future phases.
|
||
|
||
What is ILA: A Vivado IP that captures signal values over time into on-chip memory,
|
||
like an oscilloscope for internal FPGA signals. You set trigger conditions and view
|
||
waveforms in the Vivado hardware manager.
|
||
|
||
Future role: You'll drop ILA probes on different signals throughout the project.
|
||
Knowing how to use it well is arguably the most important FPGA debug skill.
|
||
|
||
---
|
||
|
||
## Phase 5 — Branches and Jumps (Control Flow)
|
||
|
||
### 5.1 — Branch Instructions
|
||
What: Add a branch comparator (separate from the ALU — it checks rs1 vs rs2 for
|
||
equality, less-than, etc.) and a mux that selects between PC+4 and the branch target.
|
||
Implement all B-type branches: beq, bne, blt, bge, bltu, bgeu.
|
||
|
||
Why: Without branches, the CPU can only run straight-line code. Branches give
|
||
you loops and conditionals — the core of any real program. Keeping the branch
|
||
comparator separate from the ALU is mostly a clarity choice: branches and
|
||
arithmetic have different semantics, and the comparator is small. (Whether you
|
||
resolve branches in decode or execute when you pipeline later is a separate
|
||
trade-off involving forwarding paths — don't lock that decision in now.)
|
||
|
||
Test: A loop that increments a register from 0 to 10, then falls through. Verify with
|
||
ILA that the branch is taken exactly 10 times and the final register value is 10.
|
||
|
||
Future role: When you pipeline, branch handling becomes the source of pipeline hazards
|
||
and you'll add branch prediction. But the comparator logic stays.
|
||
|
||
### 5.2 — Jump Instructions (jal, jalr)
|
||
What: Add jal (jump and link — PC-relative) and jalr (jump and link register —
|
||
absolute). Both store PC+4 into the destination register before jumping.
|
||
|
||
Why: jal/jalr are how RISC-V implements function calls and returns. `jal` calls a
|
||
function, `jalr` returns from it (by jumping to the address saved in the link
|
||
register). Without these, you can't have functions — and C is nothing but functions.
|
||
|
||
Test: Write a call/return sequence. Main calls a subroutine via jal, subroutine does
|
||
some work, returns via jalr. Verify the return address is correct and execution
|
||
resumes in the right place.
|
||
|
||
Future role: Final. These instructions don't change.
|
||
|
||
---
|
||
|
||
## Phase 6 — Load/Store (Data Memory)
|
||
|
||
### 6.1 — Word Load/Store (lw, sw)
|
||
What: Add a data BRAM 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.
|
||
|
||
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 →
|
||
WRITEBACK for loads. This is exactly why valid/ready is in the bus from
|
||
day one.
|
||
|
||
Why: A CPU without data memory can only work with 32 values (the registers). Load/
|
||
store connects the CPU to the outside world. Starting with word-only access keeps the
|
||
byte-lane logic simple while you verify the memory path works.
|
||
|
||
Why valid/ready now: BRAM responds in one cycle, so ready is always high. But when
|
||
you swap in DRAM later, responses take many cycles. If your interface already has
|
||
valid/ready handshaking, the swap is painless. If you hardwire it now, you'll have
|
||
to redesign the memory path later. Five minutes of future-proofing saves a weekend
|
||
of rework.
|
||
|
||
Test: Store values to various addresses, load them back, verify they match.
|
||
|
||
Future role: The bus interface is the foundation for your entire memory map (BRAM,
|
||
DRAM, UART, timer, interrupt controller — all hang off this bus).
|
||
|
||
### 6.2 — Byte and Halfword Access (lb, lbu, lh, lhu, sb, sh)
|
||
What: Add byte and halfword loads (signed and unsigned) and stores. This means byte
|
||
lane selection (which byte within a 32-bit word) and sign extension (lb sign-extends
|
||
to 32 bits, lbu zero-extends).
|
||
|
||
Why: C uses char (byte) and short (halfword) types constantly. String operations are
|
||
byte-by-byte. You can't run real C code without these.
|
||
|
||
Testbench focus: Sign extension (loading 0xFF as signed byte should give 0xFFFFFFFF,
|
||
as unsigned should give 0x000000FF). Unaligned access behavior (RISC-V base spec says
|
||
misaligned access can trap — decide if you want to support it or trap).
|
||
|
||
Future role: Final. These instructions don't change.
|
||
|
||
### 6.3 — ILA on the Memory Bus
|
||
What: Attach ILA to the memory bus signals. Run a program that computes Fibonacci
|
||
numbers and stores them into a data array. Verify the memory contents.
|
||
|
||
Why: Memory bugs are subtle (off-by-one addresses, wrong byte lane, sign extension
|
||
errors). ILA on the bus lets you see exactly what's happening each cycle.
|
||
|
||
---
|
||
|
||
## Phase 7 — Memory-Mapped UART
|
||
|
||
### 7.1 — UART TX Module
|
||
What: A standalone UART transmitter. Baud rate 115200, 8 data bits, no parity,
|
||
1 stop bit (8N1). At 50 MHz the baud divisor is `50_000_000 / 115_200 ≈ 434`
|
||
clocks per bit. Interface: input byte, send signal, busy flag.
|
||
|
||
Why: This is your CPU's mouth. Once connected, the CPU can print to a serial terminal.
|
||
This replaces ILA as your primary debug tool for software — you can printf-debug your
|
||
C programs.
|
||
|
||
Test standalone: Wrap it in a tiny FSM that sends "hello\n" on reset. Synthesize,
|
||
connect to your terminal at 115200 baud. If you want, verify the waveform on your
|
||
scope — you'll see actual start bits, data bits, stop bits on the TX pin.
|
||
|
||
Future role: This exact module gets memory-mapped in 7.3 and eventually becomes the
|
||
console for your BIOS and Linux.
|
||
|
||
### 7.2 — UART RX Module
|
||
What: A standalone UART receiver. Same 8N1 format. Oversamples the input (typically
|
||
16x baud rate) to find bit centers. Outputs received byte + valid flag.
|
||
|
||
Why: This is your CPU's ear. Needed for any interactive console — typing commands,
|
||
sending data to the board.
|
||
|
||
Test: Send bytes from your terminal, verify they appear correctly. If you want to be
|
||
extra thorough, use your Eclypse + DAC zmod to generate serial waveforms with
|
||
deliberate timing skew and verify the receiver handles it.
|
||
|
||
Future role: Same as TX — becomes the console UART.
|
||
|
||
### 7.3 — Bus Decoder + Memory-Mapped I/O
|
||
What: Add an address decoder to your memory bus. Routing:
|
||
|
||
```
|
||
0x2000_0000 – 0x2000_FFFF instruction BRAM
|
||
0x8000_0000 – 0x8000_FFFF data BRAM
|
||
0x1000_0000 UART TX data (W: byte to send)
|
||
0x1000_0004 UART RX data (R: pops one byte from RX FIFO)
|
||
0x1000_0008 UART status (R: bit0 = tx_busy, bit1 = rx_valid)
|
||
```
|
||
|
||
Note this is a split register layout (separate TX/RX/status addresses), not
|
||
a 16550-style shared data register. Cleaner for a learning core; can be
|
||
reshaped later if you want a 16550-compatible model.
|
||
|
||
Why: Memory-mapped I/O is how CPUs talk to peripherals in the real world. The
|
||
CPU doesn't know it's talking to a UART — it just does a store to an address,
|
||
and the bus decoder routes it to the right place. This is the same pattern
|
||
used by every SoC ever.
|
||
|
||
Test: Write a program that stores ASCII bytes to `0x1000_0000` in a loop,
|
||
polling `0x1000_0008` bit 0 between writes. See the message in your terminal.
|
||
This is the most important milestone in the project — your CPU is now a
|
||
computer that communicates with the outside world through software.
|
||
|
||
Future role: The bus decoder grows as you add peripherals (timer, PLIC, DRAM)
|
||
but the structure stays. UART mapping stays at these addresses.
|
||
|
||
### 7.4 — "Hello World" (Hand-Assembled)
|
||
What: Hand-write an assembly program that prints "hello from rv32" to the UART
|
||
address (`0x1000_0000`), polling status bit 0 to wait for `tx_busy` to clear.
|
||
Assemble it, load into BRAM, run.
|
||
|
||
Why: Pure emotional milestone. Your CPU, your UART, your program, your message on
|
||
screen.
|
||
|
||
---
|
||
|
||
## Phase 8 — GCC Toolchain Integration
|
||
|
||
### 8.1 — Linker Script + Startup Code
|
||
What: Write a linker script that tells GCC where your instruction memory, data
|
||
memory, and stack live. Anchor `.text`/`.rodata` at `0x2000_0000`,
|
||
`.data`/`.bss` at `0x8000_0000`, stack growing down from the top of data BRAM
|
||
(`0x8001_0000`). Write crt0.S (C runtime startup): set the stack pointer,
|
||
zero out the BSS section (uninitialized global variables), call main.
|
||
|
||
Why: GCC doesn't just compile C to instructions — it expects a runtime environment.
|
||
The linker script defines the memory layout, and crt0 sets up the minimal environment
|
||
that C code assumes exists (a stack, zeroed globals).
|
||
|
||
Future role: The linker script evolves as your memory map grows (adding DRAM, flash).
|
||
crt0 grows when you add CSRs (setting up trap vectors in startup).
|
||
|
||
### 8.2 — First GCC Program
|
||
What: Write a trivial main() that prints a string to the UART by writing bytes
|
||
to `0x1000_0000`. Compile with:
|
||
```
|
||
riscv64-unknown-elf-gcc -march=rv32im_zicsr_zifencei -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.
|
||
|
||
Why: This proves your CPU is compatible with a real compiler. Any bugs in your
|
||
instruction implementation will surface here — GCC will use instructions in
|
||
combinations you never thought to hand-test.
|
||
|
||
Expect to iterate: GCC will probably emit an instruction you haven't implemented yet.
|
||
That's fine — check the illegal instruction, add it, resynthesize. This is normal.
|
||
|
||
### 8.3 — Fill Remaining RV32I Gaps
|
||
What: Implement any RV32I instructions you deferred. Common ones: all shift variants
|
||
(sll, srl, sra, slli, srli, srai), set-less-than variants (slti, sltiu), fence (NOP
|
||
for now — you have no cache), ecall/ebreak (NOP for now — you have no trap handling).
|
||
|
||
Why: GCC's output will exercise the full ISA. You need complete RV32I coverage for
|
||
any non-trivial C code to work.
|
||
|
||
Test: Compile progressively more complex C programs. String manipulation, struct
|
||
usage, switch statements (these generate jump tables — exercises jalr with computed
|
||
addresses), recursive functions.
|
||
|
||
### 8.4 — Milestone: Meaningful C Program
|
||
What: Write something real — a serial monitor that accepts commands over UART
|
||
and responds. Or a tiny Forth interpreter. Something interactive that proves
|
||
the core is solid.
|
||
|
||
Why: Confidence builder. You now have a working RISC-V computer that runs
|
||
compiled C and talks over serial. Everything after this is enrichment.
|
||
|
||
### 8.5 — riscv-tests Compliance
|
||
What: Clone `riscv-software-src/riscv-tests`. Build the `rv32ui-p-*` and
|
||
`rv32um-p-*` tests (the "p" variant assumes physical addressing, no virtual
|
||
memory — perfect for this stage). Each test is a tiny program that exercises
|
||
one or more instructions and writes a pass/fail code to a known address.
|
||
Write a small testbench harness that loads each test ELF into instruction
|
||
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
|
||
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
|
||
on when rv32ui and rv32um pass clean.
|
||
|
||
Future role: Re-run the suite after every meaningful change to the core. It
|
||
becomes regression coverage for the rest of the project.
|
||
|
||
---
|
||
|
||
## Phase 9 — CSRs + M-Mode Trap Handling
|
||
|
||
What: Add Control and Status Registers (mstatus, mtvec, mepc, mcause, mtval, mie,
|
||
mip) and the CSR instructions (csrrw, csrrs, csrrc, csrrwi, csrrsi, csrrci). Add
|
||
mret (return from trap). Implement the trap entry mechanism: on an exception or
|
||
interrupt, save PC to mepc, jump to mtvec, set mcause.
|
||
|
||
Why: Traps and interrupts are how the CPU handles errors (illegal instruction, bad
|
||
memory access) and hardware events (timer fired, UART received a byte). No operating
|
||
system can function without this. M-mode (machine mode) is the highest privilege
|
||
level and the only one you need for bare-metal code and simple kernels.
|
||
|
||
Future role: This is the foundation for all OS-level functionality. Linux needs this
|
||
plus S-mode (supervisor mode), which you'll add later.
|
||
|
||
---
|
||
|
||
## Phase 10 — Timer
|
||
|
||
What: Implement mtime (a free-running 64-bit counter) and mtimecmp (comparison
|
||
register). When mtime >= mtimecmp, a timer interrupt fires.
|
||
|
||
Why: Every operating system needs a timer tick for scheduling. Even bare-metal
|
||
firmware needs delays and timeouts. This is your first interrupt source, which also
|
||
validates that the trap handling from Phase 9 actually works end-to-end.
|
||
|
||
---
|
||
|
||
## Phase 11 — Interrupt Controller
|
||
|
||
What: Build a minimal PLIC (Platform-Level Interrupt Controller) or a simplified
|
||
version. Connect UART RX as an interrupt source. Implement interrupt priority and
|
||
enable/disable.
|
||
|
||
Why: Polling the UART wastes CPU cycles. With interrupts, the CPU does other work
|
||
and only handles UART when data arrives. A real system has many interrupt sources —
|
||
the PLIC manages them. This is required for Linux.
|
||
|
||
---
|
||
|
||
## Phase 12 — Pipeline (Optional but educational)
|
||
|
||
What: Insert pipeline registers between your stages (fetch|decode|execute|memory|
|
||
writeback). Handle data hazards (forwarding/stalling) and control hazards (branch
|
||
prediction or pipeline flush).
|
||
|
||
Why: Your single-cycle core's clock speed is limited by the longest combinational
|
||
path (probably through the ALU). Pipelining lets each stage run in one short cycle.
|
||
This is the classic computer architecture exercise and deeply educational.
|
||
|
||
Why optional: A single-cycle core can run at maybe 50-80 MHz on the Artix-7. That's
|
||
plenty for booting Linux. Pipeline if you want to learn, not because you must.
|
||
|
||
---
|
||
|
||
## Phase 13 — SPI Flash Boot + DRAM
|
||
|
||
What: Add an SPI flash controller to boot from the on-board flash (instead of BRAM
|
||
initialization). Integrate AMD/Xilinx MIG IP for the DDR3 on the Arty A7. Update
|
||
your memory map: flash at boot, copy to DRAM, jump to DRAM.
|
||
|
||
Why: BRAM is tiny (a few hundred KB on Artix-7 100T). Linux needs megabytes. DRAM
|
||
gives you 256MB. Flash gives you persistent storage for the bootloader. This is
|
||
how real embedded systems boot.
|
||
|
||
---
|
||
|
||
## Phase 14 — S-Mode, U-Mode, Sv32 Virtual Memory
|
||
|
||
What: Add supervisor and user privilege modes. Implement Sv32 page table walking
|
||
(two-level page tables, 4KB pages). Add the satp CSR, page fault exceptions, and
|
||
sfence.vma.
|
||
|
||
Why: Linux runs the kernel in S-mode, user programs in U-mode. Virtual memory gives
|
||
each process its own address space and protects the kernel from user code. This is the
|
||
last major architectural piece before Linux.
|
||
|
||
---
|
||
|
||
## Phase 15 — Linux
|
||
|
||
What: Port a minimal Linux kernel (or use an existing RISC-V port). Write a device
|
||
tree for your SoC. Build an initramfs with BusyBox. Boot to a shell prompt.
|
||
|
||
Why: This is the summit. A Linux shell running on a CPU you built from scratch.
|
||
|
||
---
|
||
|
||
## Quick Reference: What You Need Installed
|
||
|
||
- Vivado 2025.2 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)
|
||
- 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
|