Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,8 @@ doc/api
*.out
*.elf
*.bin

# Heimdall runtime SQLite databases, created when the daemon/tests run with a
# working directory inside a package (not part of the source tree)
*.db
objects
705 changes: 201 additions & 504 deletions LICENSE

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Workspace-wide production analysis baseline.
# Individual packages include this file.

include: package:lints/recommended.yaml

analyzer:
language:
strict-casts: true
strict-inference: true
strict-raw-types: true
198 changes: 198 additions & 0 deletions devices.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
# Declarative SoC definitions.
# Each device has a base config and a set of targets (FPGA or ASIC).
{
river-hdl,
sky130-pdk ? null,
gf180mcu-pdk ? null,
}:
let
creek-v1-base = {
socName = "creek_v1";
cores = [ "rc1-s" ];
interconnect = "wishbone";
clockFreq = 48000000;
memories = [
"0x20000000:16M:flash"
"0x80000000:128M:dram"
];
devices = [
"clint:0x02000000"
"plic:0x04000000"
"uart:0x10000000:ns16550a"
];
};

stream-v1-base = {
socName = "stream_v1";
cores = [ "rc1-n" ];
interconnect = "wishbone";
clockFreq = 12000000;
memories = [
"0x20000000:16M:flash"
"0x80000000:1M:sram"
];
devices = [
"clint:0x02000000"
"plic:0x04000000"
"uart:0x10000000:ns16550a"
];
};
in
{
creek-v1-orangecrab = {
ip = river-hdl.mkSoC (
creek-v1-base
// {
target = "ecp5:lfe5u-25f:CSFBGA285";
clockFreq = 48000000;
oscFreq = 48000000;
memories = [
"0x20000000:16M:flash:orangecrab"
"0x80000000:64K:sram"
"0x90000000:128M:dram:orangecrab"
];
bootProgram = "monitor";
pins = [
"clk=A9"
"uart_tx=uart@tx:N17"
"uart_rx=uart@rx:M18"
];
}
);
};

# Digilent Arty S7-50 (xc7s50, csga324, 100MHz osc), Xilinx via the openXC7
# flow. DDR3 runs as x8 on the low byte-lane (arty-s7-x8, 128MB): the
# hardware-verified read and write path at cmdslot=2,wrshift=-1. The x16 high
# lane (per-lane write-leveling) is still under debug. Boot monitor runs from
# the 64K EBR SRAM and DDR is promoted separately. Pins from the working
# bring-up (clk=R2, UART R12/V12).
creek-v1-arty = {
ip = river-hdl.mkSoC (
creek-v1-base
// {
target = "spartan7:xc7s50:csga324";
clockFreq = 25000000;
oscFreq = 100000000;
memories = [
"0x20000000:16M:flash:arty-s7"
"0x80000000:64K:sram"
"0x90000000:128M:dram:arty-s7-x8:ddr3fast=true,clockfreq=400000000,cmdslot=2,wrshift=-1,trainable=true"
];
bootProgram = "monitor";
pins = [
"clk=R2"
"uart_tx=uart@tx:R12"
"uart_rx=uart@rx:V12"
];
}
);
};

creek-v1-sky130 = {
ip = river-hdl.mkSoC (
creek-v1-base
// {
target = "sky130:hd";
pdkRoot = "${sky130-pdk}/${sky130-pdk.pdkPath}";
}
);
# asix.mkTapeout metadata: topCell matches the genip SoC name, and the
# clock period is derived from the device's target frequency.
topCell = creek-v1-base.socName;
clockPeriodNs = 1.0e9 / creek-v1-base.clockFreq;
pdk = sky130-pdk;
};

creek-v1-gf180mcu = {
ip = river-hdl.mkSoC (
creek-v1-base
// {
target = "gf180mcu:3v3";
pdkRoot = "${gf180mcu-pdk}/${gf180mcu-pdk.pdkPath}";
}
);
topCell = creek-v1-base.socName;
clockPeriodNs = 1.0e9 / creek-v1-base.clockFreq;
pdk = gf180mcu-pdk;
};

# iCESugar v1.5 (iCE40UP5K-SG48, 12MHz). The up5k holds only ~128KB on-chip,
# so the shared 16M-flash/1M-sram base map does not fit. Override it with a
# single 64KB on-chip data SRAM and drop the external regions.
stream-v1-ice40 = {
ip = river-hdl.mkSoC (
stream-v1-base
// {
target = "ice40:up5k:sg48";
memories = [
"0x20000000:16M:flash"
"0x80000000:128K:sram"
];
# No PLIC: this board has no routed interrupt sources and the unused
# 32-source arbiter costs ~900 cells of the up5k. The CLINT covers the
# timer. External IRQs can return with a slimmer controller if needed.
devices = [
"clint:0x02000000"
"uart:0x10000000:ns16550a"
];
# Serial boot monitor in the boot ROM (no cache-as-RAM): prints a banner
# then loads checksummed payloads into the SRAM over the UART.
bootProgram = "monitor";
# Board UART (iCELink USB-CDC bridge): FPGA tx=6, rx=4 per the official
# iCESugar pcf. Pins 14/15 are SPI-flash lines, not the UART.
pins = [
"clk=35"
"uart_tx=uart@tx:6"
"uart_rx=uart@rx:4"
];
}
);
};

# OrangeCrab r0.2 (LFE5U-25F, csfbga285, 48MHz osc). Pins proven on this
# board by the NixVegas SoC (clk=A9, uart on feather N17/M18 via an
# external USB-TTL adapter; the USB-C port is raw, no onboard bridge).
# Same monitor profile as the iCESugar: rc1-n + 64KB byte-masked EBR SRAM.
stream-v1-orangecrab = {
ip = river-hdl.mkSoC (
stream-v1-base
// {
target = "ecp5:lfe5u-25f:CSFBGA285";
clockFreq = 48000000;
# The OrangeCrab oscillator is 48MHz (not the 12MHz default): with
# the default the system PLL multiplies x4 and the whole SoC would
# run at 192MHz on hardware.
oscFreq = 48000000;
# DDR3 (MT41K64M16, 128MB, hardware-verified) sits beside the SRAM
# boot path; promoting it to main RAM at 0x80000000 is a follow-up.
# The dram region pulls the board's full sdram_* pad constraint set
# with it.
memories = [
"0x20000000:16M:flash:orangecrab"
"0x80000000:64K:sram"
"0x90000000:128M:dram:orangecrab"
];
bootProgram = "monitor";
pins = [
"clk=A9"
"uart_tx=uart@tx:N17"
"uart_rx=uart@rx:M18"
];
}
);
};

stream-v1-gf180mcu = {
ip = river-hdl.mkSoC (
stream-v1-base
// {
target = "gf180mcu:3v3";
pdkRoot = "${gf180mcu-pdk}/${gf180mcu-pdk.pdkPath}";
}
);
topCell = stream-v1-base.socName;
clockPeriodNs = 1.0e9 / stream-v1-base.clockFreq;
pdk = gf180mcu-pdk;
};
}
155 changes: 154 additions & 1 deletion docs/core/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,22 @@ This allows for a wide variety of applications and a modular design.
- Intended to run Linux
- Superscalar

## Execution Modes

A River core is built in one of two execution modes, picked per profile by the
`ExecutionMode` config (`inOrder` or `outOfOrder`).

In `inOrder` mode (the nano, micro, small, and full profiles) the core runs the
microcode execution unit described below: one instruction is fetched, decoded,
and executed at a time. This is the compact configuration intended for small
FPGAs and tight area budgets.

In `outOfOrder` mode (the macro profile) the same front-end feeds an out-of-order
dual-issue backend with register renaming, an issue queue, multiple functional
units, and a reorder buffer for in-order commit. The two modes share the fetch
and decode logic and the microcode definitions, they differ in how execution and
retirement are scheduled.

## Components

River's core is split into 3 central components which matches the primary stages of the pipeline.
Expand All @@ -44,7 +60,8 @@ River's core is split into 3 central components which matches the primary stages
2. Decode Unit - Decodes instructions
3. Execution Unit - Executes instructions

There are more components based on whether the In-Core Scaler is enabled, however those are the three base modules which River utilizes.
These are the base modules every River core shares. The out-of-order backend adds
the rename, issue, functional-unit, and reorder-buffer blocks described later.

### Fetch Unit

Expand All @@ -56,6 +73,21 @@ If the instruction is compressed, the fetch unit emits that into the IR (instruc
If the instruction is not compressed, the fetch unit will then read the other half-word of memory and combine the two.
The full instruction will then be loaded into IR.

A 32-bit instruction can straddle a word boundary when compressed code shifts
alignment, so the fetch path can issue a second read and join the high half-word
of one word with the low half-word of the next before presenting the full
instruction.

Two optional fetch engines trade area for instruction throughput. A pipelined
prefetch fetcher issues the next instruction read while the current instruction
is still being consumed downstream, holding results in a small instruction FIFO so
fetch latency overlaps decode and execution instead of serialising with them. It
keeps exactly one bus read outstanding at a time and drains any in-flight stale
response on a redirect, so a response is never mis-attributed to the wrong
address. The fetch port is interconnect-neutral: it only assumes a single read in
flight and one in-order response pulse per request, so the in-tree MMU/Wishbone
port works and an AXI or TileLink adapter presenting the same contract would too.

### Decode Unit

River has 2 decode unit types; static & dynamic. The static decode unit reads the microcode at build time.
Expand Down Expand Up @@ -127,3 +159,124 @@ will hold the same values & have the same latching behavior.
- `sp` - Stack pointer
- `rd` - Register desination
- `pc` - Program counter

### Out-of-Order Pipeline

In `outOfOrder` mode the in-order front-end (fetch, decode, rename) feeds an
issue queue that dispatches to out-of-order functional units, with a reorder
buffer restoring in-order commit. The functional units are two ALUs, one memory
unit, one branch unit, and one CSR unit.

Renaming uses a Register Alias Table that maps the 32 architectural registers to
a larger physical register file. It renames two instructions per cycle and keeps
a committed snapshot of the table so a flush can roll the speculative mapping
back. Each issue-queue entry carries its reorder-buffer tag, physical source and
destination indices, per-source ready bits and captured values, and the operand
and control metadata the target functional unit needs. An entry issues once both
its sources are ready.

The reorder buffer entry records the program counter, the new and previous
physical destinations, completion and exception state, the result, and any
control-flow redirect target. Commit happens in program order: it frees the
previous physical destination, applies branch and jump redirects, and handles
privileged returns by restoring the program counter and privilege mode from the
saved exception program counter and status.

Stores use a store queue so they do not stall commit waiting on memory. A store
pushes an entry when it executes (address and data known), and three pointers
split the queue into a committed-and-draining region and a speculative region. A
committed store drains to memory in program order in the background, so several
writes can be in flight at once. A load waits until the queue is empty so it sees
all older stores, and a flush drops only the speculative tail while committed
entries keep draining.

### Register File

The register file is a configurable multiport block. The backend is chosen for
the target: a banked or arbitered multiport built from simpler memories for
simulation and ASIC, or an ECP5 block-RAM (EBR) backend on FPGA. Its read latency
is accounted for by the pipeline rather than assumed to be zero. In out-of-order
mode the physical register file is larger than the 32 architectural registers to
back renaming. When floating point is enabled a second register file holds the FP
registers and is routed into the execution datapath alongside the integer file.

### Floating-Point and Vector

Floating point and vector execution live in the microcode execution unit. When a
micro-op reads or writes a floating-point register, a dedicated floating-point
register file is instantiated and wired into the execution datapath. The F and D
extensions cover arithmetic, the comparison operations, and conversions, plus a
multi-cycle iterative divide that runs one operation at a time under a small state
machine. The floating-point datapath is built on the ROHD-HCL floating-point
library.

The vector engine adds a vector register file of 32 registers each VLEN bits
wide, present when the ISA includes the V extension. The vector configuration
instructions write the element width and grouping into vtype and the active
element count into vl, and vector operations read them back. Integer element
operations are generic across the supported element widths and run over the full
vector length with proper handling of the active length and the tail, and LMUL
register grouping walks the registers of a destination group in turn.

### Memory and Address Translation

The MMU sits between the core and a downstream Wishbone master. It has two
upstream ports, one for instruction fetch and one data port for loads, stores,
and page-table walks, with a priority arbiter that favors the data port. When
translation is active it performs a hardware page-table walk supporting Sv39 and
Sv48 for both loads and stores and raises page faults on bad entries. With the
hypervisor extension it performs two-stage translation, where every guest-stage
pointer and the final leaf of the first-stage walk are themselves translated
through the guest-stage walk, and it reports guest faults separately.

### Privilege and Control Registers

The core tracks machine, supervisor, and user privilege in a mode register. With
the hypervisor extension a virtualization bit is also tracked and is saved and
restored through the status register on traps and privileged returns. Control and
status registers are described by a typed field configuration, and only declared
field bits are reconstructed on a read, so a readable register needs an explicit
field. A combinational frontdoor read port serves the pipeline and is borrowed by
the debugger while the hart is halted. Trap delivery computes the handler address
from the trap-vector register in either direct or vectored form, and delegation
routes selected traps and interrupts to supervisor mode. Alongside the standard
registers, River defines custom control registers including a pipeline and
speculation control register and the registers used to patch microcode at run
time.

### Debug Support

River cores may be built with external RISC-V Debug (spec 0.13.2) support. When
enabled, the core gains a halt finite state machine that freezes the pipeline at
an instruction boundary when the debugger asserts a halt request. On halt the
program counter is latched into `dpc`, and on resume the counter is restored from
`dpc` so the debugger can redirect execution by writing it. The `dcsr` control
register (CSR 0x7b0) holds the debug version, the halt cause, the privilege at
halt, and the `ebreak` enable bits. With those bits set an executed `ebreak`
re-enters debug mode instead of taking a breakpoint trap.

While the pipeline is frozen its register file ports and combinational CSR read
port are idle, so the debugger borrows them. An abstract access-register command
reads or writes any GPR and reads any architectural CSR through these idle ports
without a dedicated debug datapath. The non-debug-maskable reset bit from the
debug module is OR-ed into the hart reset at the SoC level, so a debugger reset
drops the hart while leaving the debug logic itself alive.

### Debug Transport

The TAP, the Debug Transport Module, and the Debug Module are fused into one
system-clock-domain module. It samples the JTAG clock with rising-edge detection,
so a single bit-bang pulse advances the TAP by one step and there is no JTAG to
core clock-domain crossing to reason about. The DMI register is 41 bits. The
Debug Module also exposes a System Bus Access port, a small bus master that reads
and writes memory independently of the hart, so memory inspection works whether
the hart is halted or running. A System Bus to Wishbone adapter presents that
port as a second fabric master.

On an ECP5 the debugger reaches the Debug Module over the FPGA's own
configuration JTAG (the same port a flashing tool such as dirtyJtag drives)
through the `JTAGG` ER1 user register and a SiFive nested-tap BSCAN tunnel. This
needs no extra package pins. The tunnel module reconstructs the inner TAP's
`tck`, `tms`, and `tdi` from a framed scan of the ER1 data register and returns
its `tdo`, so the full Debug Module is reachable from a stock RISC-V OpenOCD with
`riscv use_bscan_tunnel`.
Loading
Loading