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:
+160
-71
@@ -1,9 +1,24 @@
|
||||
# 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
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
@@ -32,43 +47,55 @@ source of truth when things get complex.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — ALU
|
||||
## Phase 1 — ALU + M Unit
|
||||
|
||||
### 1.1 — ALU Module + Simulation
|
||||
What: Build a combinational ALU that handles all RV32I operations (add, sub, and,
|
||||
or, xor, slt, sltu, shifts) AND the M extension operations (mul, mulh, mulhsu,
|
||||
mulhu, div, divu, rem, remu). Inputs: two 32-bit operands + operation select from
|
||||
your enum. Outputs: 32-bit result.
|
||||
### 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. Including M extension now costs almost
|
||||
nothing (a few extra case statements) but saves you from touching this module again
|
||||
later.
|
||||
|
||||
Why M extension from day one: GCC emits multiply/divide constantly. Without it, the
|
||||
compiler falls back to software emulation via libgcc — slow and painful. Baking it in
|
||||
now means your first GCC-compiled program just works.
|
||||
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, division by zero (RISC-V spec says
|
||||
specific results, not an exception).
|
||||
right vs logical shift right, signed overflow.
|
||||
|
||||
Future role: This module is final. It goes into the finished core unchanged.
|
||||
|
||||
### 1.2 — ALU on FPGA with VIO
|
||||
What: Synthesize the ALU on the Arty. Attach Vivado VIO (Virtual I/O) cores to the
|
||||
inputs and outputs. Use the Vivado hardware manager to feed operands and read results
|
||||
in real time.
|
||||
### 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: Confirms the ALU works in real hardware, not just simulation. Gets you
|
||||
comfortable with the VIO workflow — you'll use it again. Also catches synthesis
|
||||
issues early (e.g., if your multiply path is too slow for the clock).
|
||||
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.
|
||||
|
||||
What is VIO: A Vivado IP that lets you poke values into signals and read signals out
|
||||
through JTAG, right from the Vivado GUI. Think of it as virtual switches and LEDs
|
||||
but with 32-bit width and no board wiring.
|
||||
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: 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")
|
||||
|
||||
### 4.1 — Fetch + Datapath Integration
|
||||
What: Create an instruction BRAM initialized from a .mem file. Add a PC (program
|
||||
counter) register that starts at 0 and increments by 4 each cycle. Wire the chain:
|
||||
BRAM[PC] → decoder → ALU → register file writeback. Support only R-type and I-type
|
||||
arithmetic for now (add, sub, addi, and, or, xor, slt, slti, lui, auipc, shifts).
|
||||
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.
|
||||
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.
|
||||
@@ -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.
|
||||
Implement all B-type branches: beq, bne, blt, bge, bltu, bgeu.
|
||||
|
||||
Why: Without branches, the CPU can only run straight-line code. Branches give you
|
||||
loops and conditionals — the core of any real program. Keeping the branch comparator
|
||||
separate from the ALU is a design choice that pays off when you pipeline later (branch
|
||||
decision can be made in the decode stage without waiting for ALU).
|
||||
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.
|
||||
@@ -199,9 +236,15 @@ Future role: Final. These instructions don't change.
|
||||
## Phase 6 — Load/Store (Data Memory)
|
||||
|
||||
### 6.1 — Word Load/Store (lw, sw)
|
||||
What: Add a data BRAM and a load/store unit. For now, only 32-bit aligned access
|
||||
(lw and sw). Define a simple memory bus interface with address, write data, read data,
|
||||
write enable, and valid/ready signals.
|
||||
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
|
||||
@@ -244,8 +287,9 @@ errors). ILA on the bus lets you see exactly what's happening each cycle.
|
||||
## Phase 7 — Memory-Mapped UART
|
||||
|
||||
### 7.1 — UART TX Module
|
||||
What: A standalone UART transmitter. Fixed baud rate (115200 is standard), 8 data
|
||||
bits, no parity, 1 stop bit (8N1). Interface: input byte, send signal, busy flag.
|
||||
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
|
||||
@@ -272,25 +316,37 @@ deliberate timing skew and verify the receiver handles it.
|
||||
Future role: Same as TX — becomes the console UART.
|
||||
|
||||
### 7.3 — Bus Decoder + Memory-Mapped I/O
|
||||
What: Add an address decoder to your memory bus. Address range 0x00000000-0x0FFFFFFF
|
||||
routes to BRAM (instruction + data), address 0x10000000 routes to UART TX (write the
|
||||
byte to send), 0x10000000 routes to UART RX (read the received byte). Add a status
|
||||
register at 0x10000004 (TX busy flag, RX data available flag).
|
||||
What: Add an address decoder to your memory bus. Routing:
|
||||
|
||||
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.
|
||||
```
|
||||
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)
|
||||
```
|
||||
|
||||
Test: Write a program that stores ASCII bytes to 0x10000000 in a loop. See the
|
||||
message in your terminal. This is the most important milestone in the project — your
|
||||
CPU is now a computer that communicates with the outside world through software.
|
||||
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.
|
||||
|
||||
Future role: The bus decoder grows as you add peripherals (timer, interrupt controller,
|
||||
DRAM) but the structure stays. UART mapping stays at this address.
|
||||
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. 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
|
||||
screen.
|
||||
@@ -300,9 +356,11 @@ screen.
|
||||
## Phase 8 — GCC Toolchain Integration
|
||||
|
||||
### 8.1 — Linker Script + Startup Code
|
||||
What: Write a linker script that tells GCC where your instruction memory, data memory,
|
||||
and stack live. Write crt0.S (C runtime startup): set the stack pointer, zero out the
|
||||
BSS section (uninitialized global variables), call main.
|
||||
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
|
||||
@@ -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).
|
||||
|
||||
### 8.2 — First GCC Program
|
||||
What: Write a trivial main() that prints a string to the UART by writing bytes to
|
||||
0x10000000. Compile with: riscv64-unknown-elf-gcc -march=rv32im -mabi=ilp32 -nostdlib
|
||||
-T linker.ld -o firmware.elf. Convert: objcopy -O binary firmware.elf firmware.bin.
|
||||
Convert binary to .mem format. Load into BRAM. Run.
|
||||
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
|
||||
@@ -337,12 +404,32 @@ usage, switch statements (these generate jump tables — exercises jalr with com
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -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
|
||||
|
||||
- Vivado 2025.2.1 (synthesis, simulation, ILA, VIO)
|
||||
- RISC-V GCC toolchain (riscv64-unknown-elf-gcc)
|
||||
- Terminal program (minicom, picocom, or PuTTY) for UART
|
||||
- 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
|
||||
|
||||
Reference in New Issue
Block a user