Refine Arty A7 bring-up and memory-map assumptions
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.
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
This is a custom RISC-V RV32IM CPU core written in SystemVerilog, targeting the
|
This is a custom RISC-V RV32IM CPU core written in SystemVerilog, targeting the
|
||||||
Digilent Arty A7 100T (xc7a100tcsg324-1) with Vivado 2025.2.1.
|
Digilent Arty A7 100T (xc7a100tcsg324-1) with Vivado 2025.2 or later.
|
||||||
|
|
||||||
The goal is incremental development from a single-cycle core to a Linux-capable SoC.
|
The goal is incremental development from a single-cycle core to a Linux-capable SoC.
|
||||||
See ROADMAP.md for the full phased plan.
|
See ROADMAP.md for the full phased plan.
|
||||||
@@ -18,21 +18,27 @@ See ROADMAP.md for the full phased plan.
|
|||||||
- Module naming: `rv32_<block>` (e.g., `rv32_alu`, `rv32_decode`, `rv32_regfile`).
|
- Module naming: `rv32_<block>` (e.g., `rv32_alu`, `rv32_decode`, `rv32_regfile`).
|
||||||
- File naming: one module per file, filename matches module name.
|
- File naming: one module per file, filename matches module name.
|
||||||
- Testbenches: `tb/tb_<module>.sv`. Use Vivado simulator.
|
- Testbenches: `tb/tb_<module>.sv`. Use Vivado simulator.
|
||||||
- Clock: single clock domain, active rising edge, signal named `clk`.
|
- Clock: single clock domain, active rising edge, signal named `clk`. Target
|
||||||
- Reset: synchronous active-high, signal named `rst`.
|
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.
|
- BRAM init files: `mem/*.mem` in hex format, one 32-bit word per line.
|
||||||
- Firmware source: `fw/` directory. Built with riscv64-unknown-elf-gcc,
|
- Firmware source: `fw/` directory. Built with `riscv64-unknown-elf-gcc`
|
||||||
-march=rv32im -mabi=ilp32.
|
(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+).
|
||||||
|
|
||||||
## Directory Structure
|
## Directory Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
rtl/ — synthesizable source
|
rtl/ — synthesizable source
|
||||||
pkg/ — packages (rv32_pkg.sv)
|
pkg/ — packages (rv32_pkg.sv)
|
||||||
core/ — CPU core modules (ALU, decoder, regfile, datapath)
|
core/ — CPU core modules (ALU, M unit, decoder, regfile, datapath)
|
||||||
periph/ — peripherals (UART, timer, etc.)
|
periph/ — peripherals (UART, timer, PLIC, etc.)
|
||||||
top/ — top-level and SoC integration
|
top/ — top-level and SoC integration
|
||||||
tb/ — testbenches
|
tb/ — testbenches (tb_<module>.sv)
|
||||||
mem/ — BRAM init files (.mem)
|
mem/ — BRAM init files (.mem)
|
||||||
fw/ — firmware source (C, assembly, linker scripts)
|
fw/ — firmware source (C, assembly, linker scripts)
|
||||||
constraints/ — Vivado XDC constraint files
|
constraints/ — Vivado XDC constraint files
|
||||||
@@ -51,12 +57,38 @@ RV32IM base. Extensions added incrementally:
|
|||||||
|
|
||||||
- Single-cycle first, pipeline later (Phase 12). Stages are separated in the code
|
- Single-cycle first, pipeline later (Phase 12). Stages are separated in the code
|
||||||
even without pipeline registers between them.
|
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.
|
||||||
- Memory bus uses valid/ready handshake from day one, even though BRAM always
|
- Memory bus uses valid/ready handshake from day one, even though BRAM always
|
||||||
responds in one cycle. This is for future DRAM compatibility.
|
responds in one cycle. This is for future DRAM compatibility.
|
||||||
- M extension (multiply/divide) is in the ALU from the start to avoid rework.
|
- M extension (multiply/divide) lives in a dedicated multi-cycle M unit
|
||||||
|
alongside the combinational ALU, with a `start`/`busy`/`done` handshake.
|
||||||
|
Multiply is 2-3 cycles via DSP48s; divide is iterative (~33 cycles). The
|
||||||
|
datapath stalls when the M unit is busy. Building this from day one avoids
|
||||||
|
retrofitting stall logic later.
|
||||||
- Harvard architecture (separate instruction and data memory) initially. Unified
|
- Harvard architecture (separate instruction and data memory) initially. Unified
|
||||||
memory when DRAM is added.
|
memory when DRAM is added.
|
||||||
|
|
||||||
|
## Memory Map
|
||||||
|
|
||||||
|
```
|
||||||
|
0x0000_0000 – 0x0FFF_FFFF reserved (future 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) → grows to DRAM 0x8000_0000–0x8FFF_FFFF in Phase 13
|
||||||
|
```
|
||||||
|
|
||||||
|
Reset PC = `0x2000_0000`. Linker script anchors text/rodata at `0x2000_0000`
|
||||||
|
and data/bss/stack at `0x8000_0000` from Phase 8 onward.
|
||||||
|
|
||||||
|
UART register layout (split, not 16550-style):
|
||||||
|
```
|
||||||
|
0x1000_0000 TX data (W: byte to send)
|
||||||
|
0x1000_0004 RX data (R: pops one byte from RX FIFO)
|
||||||
|
0x1000_0008 status (R: bit0 = tx_busy, bit1 = rx_valid)
|
||||||
|
```
|
||||||
|
|
||||||
## When Helping With This Project
|
## When Helping With This Project
|
||||||
|
|
||||||
- Always check rv32_pkg.sv first to understand current struct definitions and enums.
|
- Always check rv32_pkg.sv first to understand current struct definitions and enums.
|
||||||
@@ -65,7 +97,10 @@ RV32IM base. Extensions added incrementally:
|
|||||||
before integration.
|
before integration.
|
||||||
- Don't add Verilog-style code (reg, wire, always @). Use SV equivalents.
|
- Don't add Verilog-style code (reg, wire, always @). Use SV equivalents.
|
||||||
- When suggesting fixes, consider that the design is single-cycle — there are no
|
- When suggesting fixes, consider that the design is single-cycle — there are no
|
||||||
pipeline hazards yet.
|
pipeline hazards yet, but the M unit and BRAM both introduce stalls.
|
||||||
|
- ISA correctness is verified against `riscv-tests` (rv32ui, rv32um) starting at
|
||||||
|
Phase 8.5. Hand-written testbenches are for module-level checks; the official
|
||||||
|
suite is the source of truth for instruction behavior.
|
||||||
- Vivado quirks: use (* dont_touch = "true" *) for signals that Vivado optimizes
|
- Vivado quirks: use (* dont_touch = "true" *) for signals that Vivado optimizes
|
||||||
away during debug. ILA/VIO probes need to be on nets that survive synthesis.
|
away during debug. ILA/VIO probes need to be on nets that survive synthesis.
|
||||||
- The Arty A7 100T has: 101,440 logic cells, 4,860 Kbit BRAM, 240 DSP slices,
|
- The Arty A7 100T has: 101,440 logic cells, 4,860 Kbit BRAM, 240 DSP slices,
|
||||||
|
|||||||
@@ -4,33 +4,48 @@ Custom RV32IM RISC-V CPU core built from scratch in SystemVerilog.
|
|||||||
|
|
||||||
## Target
|
## Target
|
||||||
|
|
||||||
- Board: Digilent Arty A7 100T
|
- Board: Digilent Arty A7 100T (xc7a100tcsg324-1)
|
||||||
- Toolchain: Vivado 2025.2.1
|
- Toolchain: Vivado 2025.2 or later
|
||||||
- Language: SystemVerilog
|
- Language: SystemVerilog
|
||||||
- ISA: RV32IM + Zicsr + Zifencei + M-mode privileged (extending toward Linux)
|
- ISA: RV32IM + Zicsr + Zifencei + M-mode privileged (extending toward Linux)
|
||||||
|
- Clock: 50 MHz (100 MHz xtal ÷ 2 via MMCM)
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
Single-cycle, non-pipelined, Harvard architecture (separate instruction/data BRAM).
|
"Single-cycle" logical model (no pipeline registers), Harvard architecture
|
||||||
Designed for incremental extension — clean stage separation via typed structs so
|
(separate instruction/data BRAM). The combinational ALU and a multi-cycle M
|
||||||
pipelining and new instructions can be added without structural rework.
|
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).
|
||||||
|
|
||||||
## Memory Map
|
## Memory Map
|
||||||
|
|
||||||
| Address Range | Peripheral |
|
| Address Range | Region |
|
||||||
|-----------------------|--------------------|
|
|---------------------------|----------------------------------------------|
|
||||||
| 0x00000000-0x0FFFFFFF | BRAM (instr + data)|
|
| `0x0000_0000–0x0FFF_FFFF` | reserved (SPI flash boot region, Phase 13) |
|
||||||
| 0x10000000 | UART TX/RX data |
|
| `0x1000_0000–0x1000_0FFF` | MMIO (UART; later: timer, PLIC) |
|
||||||
| 0x10000004 | UART status |
|
| `0x2000_0000–0x2000_FFFF` | instruction BRAM (64 KB) |
|
||||||
|
| `0x8000_0000–0x8000_FFFF` | data BRAM (64 KB) → DRAM in Phase 13 |
|
||||||
|
|
||||||
(Will expand as peripherals are added)
|
Reset PC = `0x2000_0000`.
|
||||||
|
|
||||||
|
UART registers (split layout):
|
||||||
|
|
||||||
|
| Address | Access | Meaning |
|
||||||
|
|---------------|--------|----------------------------------------------|
|
||||||
|
| `0x1000_0000` | W | TX data — byte to send |
|
||||||
|
| `0x1000_0004` | R | RX data — pops one byte from RX FIFO |
|
||||||
|
| `0x1000_0008` | R | status — bit0 = `tx_busy`, bit1 = `rx_valid` |
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
Requires:
|
Requires:
|
||||||
- Vivado 2025.2.1
|
- Vivado 2025.2 or later
|
||||||
- RISC-V GCC toolchain (`riscv64-unknown-elf-gcc`)
|
- RISC-V GCC toolchain — multilib `riscv64-unknown-elf-gcc` supporting
|
||||||
- Serial terminal (minicom/picocom/PuTTY) at 115200 baud
|
`-march=rv32im_zicsr_zifencei -mabi=ilp32`
|
||||||
|
- `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.
|
Open `FPGA-Core.xpr` in Vivado. Synthesis and implementation target the xc7a100tcsg324-1.
|
||||||
|
|
||||||
|
|||||||
+160
-71
@@ -1,9 +1,24 @@
|
|||||||
# RV32IM CPU Core — Build Roadmap
|
# RV32IM CPU Core — Build Roadmap
|
||||||
## Target: Digilent Arty A7 100T / Vivado 2025.2.1 / SystemVerilog
|
## 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
|
## 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
|
## Phase 0 — Architecture Contract
|
||||||
|
|
||||||
### 0.1 — SystemVerilog Package
|
### 0.1 — SystemVerilog Package
|
||||||
@@ -32,43 +47,55 @@ source of truth when things get complex.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Phase 1 — ALU
|
## Phase 1 — ALU + M Unit
|
||||||
|
|
||||||
### 1.1 — ALU Module + Simulation
|
### 1.1 — Combinational ALU + Simulation
|
||||||
What: Build a combinational ALU that handles all RV32I operations (add, sub, and,
|
What: Build a combinational ALU that handles all RV32I arithmetic/logic ops
|
||||||
or, xor, slt, sltu, shifts) AND the M extension operations (mul, mulh, mulhsu,
|
(add, sub, and, or, xor, slt, sltu, shifts). Inputs: two 32-bit operands +
|
||||||
mulhu, div, divu, rem, remu). Inputs: two 32-bit operands + operation select from
|
operation select from your enum. Outputs: 32-bit result.
|
||||||
your enum. Outputs: 32-bit result.
|
|
||||||
|
|
||||||
Why: The ALU is the computational heart. Building it first gives you a self-contained
|
Why: The ALU is the computational heart. Building it first gives you a
|
||||||
module to practice your testbench workflow. Including M extension now costs almost
|
self-contained module to practice your testbench workflow.
|
||||||
nothing (a few extra case statements) but saves you from touching this module again
|
|
||||||
later.
|
|
||||||
|
|
||||||
Why M extension from day one: GCC emits multiply/divide constantly. Without it, the
|
|
||||||
compiler falls back to software emulation via libgcc — slow and painful. Baking it in
|
|
||||||
now means your first GCC-compiled program just works.
|
|
||||||
|
|
||||||
Testbench focus: signed vs unsigned comparisons (SLT vs SLTU), arithmetic shift
|
Testbench focus: signed vs unsigned comparisons (SLT vs SLTU), arithmetic shift
|
||||||
right vs logical shift right, signed overflow, division by zero (RISC-V spec says
|
right vs logical shift right, signed overflow.
|
||||||
specific results, not an exception).
|
|
||||||
|
|
||||||
Future role: This module is final. It goes into the finished core unchanged.
|
Future role: This module is final. It goes into the finished core unchanged.
|
||||||
|
|
||||||
### 1.2 — ALU on FPGA with VIO
|
### 1.2 — Multi-cycle M Unit + Simulation
|
||||||
What: Synthesize the ALU on the Arty. Attach Vivado VIO (Virtual I/O) cores to the
|
What: A separate module for the M extension (mul, mulh, mulhsu, mulhu, div,
|
||||||
inputs and outputs. Use the Vivado hardware manager to feed operands and read results
|
divu, rem, remu) with a `start` / `busy` / `done` handshake. Multiply uses
|
||||||
in real time.
|
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: Confirms the ALU works in real hardware, not just simulation. Gets you
|
Why M extension early: GCC emits mul/div constantly; without hardware, libgcc
|
||||||
comfortable with the VIO workflow — you'll use it again. Also catches synthesis
|
emulation is slow and painful. Why a separate module: a 32-bit combinational
|
||||||
issues early (e.g., if your multiply path is too slow for the clock).
|
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.
|
||||||
|
|
||||||
What is VIO: A Vivado IP that lets you poke values into signals and read signals out
|
Testbench focus: division by zero (RISC-V spec gives specific results, not an
|
||||||
through JTAG, right from the Vivado GUI. Think of it as virtual switches and LEDs
|
exception — `div(x,0) = -1`, `rem(x,0) = x`), signed overflow on division
|
||||||
but with 32-bit width and no board wiring.
|
(`INT_MIN / -1` returns `INT_MIN`), `mulh*` upper-half results, back-to-back
|
||||||
|
operations without dropping `busy`.
|
||||||
|
|
||||||
Future role: VIO familiarity pays off throughout the project. The ALU itself is final.
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -126,19 +153,27 @@ decoder and a new field to the struct. The structure of the decoder doesn't chan
|
|||||||
## Phase 4 — First CPU ("It's Alive")
|
## Phase 4 — First CPU ("It's Alive")
|
||||||
|
|
||||||
### 4.1 — Fetch + Datapath Integration
|
### 4.1 — Fetch + Datapath Integration
|
||||||
What: Create an instruction BRAM initialized from a .mem file. Add a PC (program
|
What: Create an instruction BRAM initialized from a .mem file, mapped at
|
||||||
counter) register that starts at 0 and increments by 4 each cycle. Wire the chain:
|
`0x2000_0000`. Add a PC register that resets to `0x2000_0000` and advances
|
||||||
BRAM[PC] → decoder → ALU → register file writeback. Support only R-type and I-type
|
by 4 when the next instruction is ready. Wire the chain:
|
||||||
arithmetic for now (add, sub, addi, and, or, xor, slt, slti, lui, auipc, shifts).
|
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
|
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
|
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,
|
instructions correctly. The wiring is where most bugs live — wrong bit ranges,
|
||||||
swapped operands, forgetting to connect a signal.
|
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
|
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
|
expected register state after each instruction by hand. Compare against what
|
||||||
CPU actually produces.
|
the CPU actually produces.
|
||||||
|
|
||||||
What is a .mem file: A text file with hex values, one per line. Vivado can
|
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.
|
initialize BRAMs from these. Each line is one 32-bit instruction.
|
||||||
@@ -169,10 +204,12 @@ What: Add a branch comparator (separate from the ALU — it checks rs1 vs rs2 fo
|
|||||||
equality, less-than, etc.) and a mux that selects between PC+4 and the branch target.
|
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.
|
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
|
Why: Without branches, the CPU can only run straight-line code. Branches give
|
||||||
loops and conditionals — the core of any real program. Keeping the branch comparator
|
you loops and conditionals — the core of any real program. Keeping the branch
|
||||||
separate from the ALU is a design choice that pays off when you pipeline later (branch
|
comparator separate from the ALU is mostly a clarity choice: branches and
|
||||||
decision can be made in the decode stage without waiting for ALU).
|
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
|
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.
|
ILA that the branch is taken exactly 10 times and the final register value is 10.
|
||||||
@@ -199,9 +236,15 @@ Future role: Final. These instructions don't change.
|
|||||||
## Phase 6 — Load/Store (Data Memory)
|
## Phase 6 — Load/Store (Data Memory)
|
||||||
|
|
||||||
### 6.1 — Word Load/Store (lw, sw)
|
### 6.1 — Word Load/Store (lw, sw)
|
||||||
What: Add a data BRAM and a load/store unit. For now, only 32-bit aligned access
|
What: Add a data BRAM mapped at `0x8000_0000` (64 KB) and a load/store unit.
|
||||||
(lw and sw). Define a simple memory bus interface with address, write data, read data,
|
For now, only 32-bit aligned access (lw and sw). Define a simple memory bus
|
||||||
write enable, and valid/ready signals.
|
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/
|
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
|
store connects the CPU to the outside world. Starting with word-only access keeps the
|
||||||
@@ -244,8 +287,9 @@ errors). ILA on the bus lets you see exactly what's happening each cycle.
|
|||||||
## Phase 7 — Memory-Mapped UART
|
## Phase 7 — Memory-Mapped UART
|
||||||
|
|
||||||
### 7.1 — UART TX Module
|
### 7.1 — UART TX Module
|
||||||
What: A standalone UART transmitter. Fixed baud rate (115200 is standard), 8 data
|
What: A standalone UART transmitter. Baud rate 115200, 8 data bits, no parity,
|
||||||
bits, no parity, 1 stop bit (8N1). Interface: input byte, send signal, busy flag.
|
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.
|
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
|
This replaces ILA as your primary debug tool for software — you can printf-debug your
|
||||||
@@ -272,25 +316,37 @@ deliberate timing skew and verify the receiver handles it.
|
|||||||
Future role: Same as TX — becomes the console UART.
|
Future role: Same as TX — becomes the console UART.
|
||||||
|
|
||||||
### 7.3 — Bus Decoder + Memory-Mapped I/O
|
### 7.3 — Bus Decoder + Memory-Mapped I/O
|
||||||
What: Add an address decoder to your memory bus. Address range 0x00000000-0x0FFFFFFF
|
What: Add an address decoder to your memory bus. Routing:
|
||||||
routes to BRAM (instruction + data), address 0x10000000 routes to UART TX (write the
|
|
||||||
byte to send), 0x10000000 routes to UART RX (read the received byte). Add a status
|
|
||||||
register at 0x10000004 (TX busy flag, RX data available flag).
|
|
||||||
|
|
||||||
Why: Memory-mapped I/O is how CPUs talk to peripherals in the real world. The CPU
|
```
|
||||||
doesn't know it's talking to a UART — it just does a store to an address, and the bus
|
0x2000_0000 – 0x2000_FFFF instruction BRAM
|
||||||
decoder routes it to the right place. This is the same pattern used by every SoC ever.
|
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)
|
||||||
|
```
|
||||||
|
|
||||||
Test: Write a program that stores ASCII bytes to 0x10000000 in a loop. See the
|
Note this is a split register layout (separate TX/RX/status addresses), not
|
||||||
message in your terminal. This is the most important milestone in the project — your
|
a 16550-style shared data register. Cleaner for a learning core; can be
|
||||||
CPU is now a computer that communicates with the outside world through software.
|
reshaped later if you want a 16550-compatible model.
|
||||||
|
|
||||||
Future role: The bus decoder grows as you add peripherals (timer, interrupt controller,
|
Why: Memory-mapped I/O is how CPUs talk to peripherals in the real world. The
|
||||||
DRAM) but the structure stays. UART mapping stays at this address.
|
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)
|
### 7.4 — "Hello World" (Hand-Assembled)
|
||||||
What: Hand-write an assembly program that prints "hello from rv32" to the UART
|
What: Hand-write an assembly program that prints "hello from rv32" to the UART
|
||||||
address. Assemble it, load into BRAM, run.
|
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
|
Why: Pure emotional milestone. Your CPU, your UART, your program, your message on
|
||||||
screen.
|
screen.
|
||||||
@@ -300,9 +356,11 @@ screen.
|
|||||||
## Phase 8 — GCC Toolchain Integration
|
## Phase 8 — GCC Toolchain Integration
|
||||||
|
|
||||||
### 8.1 — Linker Script + Startup Code
|
### 8.1 — Linker Script + Startup Code
|
||||||
What: Write a linker script that tells GCC where your instruction memory, data memory,
|
What: Write a linker script that tells GCC where your instruction memory, data
|
||||||
and stack live. Write crt0.S (C runtime startup): set the stack pointer, zero out the
|
memory, and stack live. Anchor `.text`/`.rodata` at `0x2000_0000`,
|
||||||
BSS section (uninitialized global variables), call main.
|
`.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.
|
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
|
The linker script defines the memory layout, and crt0 sets up the minimal environment
|
||||||
@@ -312,10 +370,19 @@ Future role: The linker script evolves as your memory map grows (adding DRAM, fl
|
|||||||
crt0 grows when you add CSRs (setting up trap vectors in startup).
|
crt0 grows when you add CSRs (setting up trap vectors in startup).
|
||||||
|
|
||||||
### 8.2 — First GCC Program
|
### 8.2 — First GCC Program
|
||||||
What: Write a trivial main() that prints a string to the UART by writing bytes to
|
What: Write a trivial main() that prints a string to the UART by writing bytes
|
||||||
0x10000000. Compile with: riscv64-unknown-elf-gcc -march=rv32im -mabi=ilp32 -nostdlib
|
to `0x1000_0000`. Compile with:
|
||||||
-T linker.ld -o firmware.elf. Convert: objcopy -O binary firmware.elf firmware.bin.
|
```
|
||||||
Convert binary to .mem format. Load into BRAM. Run.
|
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
|
Why: This proves your CPU is compatible with a real compiler. Any bugs in your
|
||||||
instruction implementation will surface here — GCC will use instructions in
|
instruction implementation will surface here — GCC will use instructions in
|
||||||
@@ -337,12 +404,32 @@ usage, switch statements (these generate jump tables — exercises jalr with com
|
|||||||
addresses), recursive functions.
|
addresses), recursive functions.
|
||||||
|
|
||||||
### 8.4 — Milestone: Meaningful C Program
|
### 8.4 — Milestone: Meaningful C Program
|
||||||
What: Write something real — a serial monitor that accepts commands over UART and
|
What: Write something real — a serial monitor that accepts commands over UART
|
||||||
responds. Or a tiny Forth interpreter. Something interactive that proves the core
|
and responds. Or a tiny Forth interpreter. Something interactive that proves
|
||||||
is solid.
|
the core is solid.
|
||||||
|
|
||||||
Why: Confidence builder. You now have a working RISC-V computer that runs compiled C
|
Why: Confidence builder. You now have a working RISC-V computer that runs
|
||||||
and talks over serial. Everything after this is enrichment.
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -436,8 +523,10 @@ Why: This is the summit. A Linux shell running on a CPU you built from scratch.
|
|||||||
|
|
||||||
## Quick Reference: What You Need Installed
|
## Quick Reference: What You Need Installed
|
||||||
|
|
||||||
- Vivado 2025.2.1 (synthesis, simulation, ILA, VIO)
|
- Vivado 2025.2 or later (synthesis, simulation, ILA, VIO)
|
||||||
- RISC-V GCC toolchain (riscv64-unknown-elf-gcc)
|
- RISC-V GCC toolchain (`riscv64-unknown-elf-gcc`, multilib build — needs to
|
||||||
- Terminal program (minicom, picocom, or PuTTY) for UART
|
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
|
- Text editor you like for SystemVerilog
|
||||||
- The RISC-V ISA spec (Volume 1: Unprivileged, Volume 2: Privileged) — free PDFs
|
- The RISC-V ISA spec (Volume 1: Unprivileged, Volume 2: Privileged) — free PDFs
|
||||||
|
|||||||
Reference in New Issue
Block a user