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:
2026-04-28 11:29:01 +02:00
parent c7b19e8744
commit e98b3694ab
3 changed files with 241 additions and 102 deletions
+47 -12
View File
@@ -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_00000x8FFF_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,
+29 -14
View File
@@ -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_00000x0FFF_FFFF` | reserved (SPI flash boot region, Phase 13) |
| 0x10000000 | UART TX/RX data | | `0x1000_00000x1000_0FFF` | MMIO (UART; later: timer, PLIC) |
| 0x10000004 | UART status | | `0x2000_00000x2000_FFFF` | instruction BRAM (64 KB) |
| `0x8000_00000x8000_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
View File
@@ -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_00000x8FFF_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