From 1c6cc900ae6a342989fcfcb6d74b1bf46dd25143 Mon Sep 17 00:00:00 2001 From: Tristan Ross Date: Tue, 28 Jul 2026 16:48:38 -0700 Subject: [PATCH 1/3] feat: working jtag --- devices.nix | 1 + nix/common-dart.nix | 2 +- .../lib/src/core/debug_subsystem.dart | 66 ++++++++++--- .../lib/src/core/jtag_bscan_tunnel.dart | 97 ++++++++++++------- .../river_hdl/lib/src/core/sba_wishbone.dart | 13 ++- packages/river_hdl/lib/src/genip.dart | 22 +++-- packages/river_hdl/test/dbg_elab_test.dart | 43 +++++++- .../test/debug/jtag_tunnel_dm_test.dart | 13 +-- .../river_hdl/test/sba_wishbone_test.dart | 28 ++++++ pubspec.lock | 4 +- pubspec.lock.json | 4 +- pubspec.yaml | 2 +- 12 files changed, 223 insertions(+), 72 deletions(-) diff --git a/devices.nix b/devices.nix index 9a01f93..d25490b 100644 --- a/devices.nix +++ b/devices.nix @@ -79,6 +79,7 @@ in "0x80000000:64K:sram" "0x90000000:128M:dram:arty-s7-x8:ddr3fast=true,clockfreq=400000000,cmdslot=2,wrshift=-1,trainable=true" ]; + devices = creek-v1-base.devices ++ [ "debug-jtag" ]; bootProgram = "monitor"; pins = [ "clk=R2" diff --git a/nix/common-dart.nix b/nix/common-dart.nix index fa8747d..69680e4 100644 --- a/nix/common-dart.nix +++ b/nix/common-dart.nix @@ -2,6 +2,6 @@ lib: { pubspecLock = lib.importJSON ../pubspec.lock.json; gitHashes = { - harbor = "sha256-Mnfls8OTiyFbRe7E3u/SvNDqQocxLpnMQpIDRSkq4uM="; + harbor = "sha256-B7kuqZmSlKgKDGhZhbxYrGr3ZeEA+zRcJ4szdXRb62A="; }; } diff --git a/packages/river_hdl/lib/src/core/debug_subsystem.dart b/packages/river_hdl/lib/src/core/debug_subsystem.dart index 54102a5..c96d8c4 100644 --- a/packages/river_hdl/lib/src/core/debug_subsystem.dart +++ b/packages/river_hdl/lib/src/core/debug_subsystem.dart @@ -21,13 +21,24 @@ import 'sba_wishbone.dart'; /// /// OpenOCD reaches it with `riscv use_bscan_tunnel 6 1` over the ECP5 TAP. /// HW-validation pending; the tunnel framing is the piece to confirm live. -class RiverDebugSubsystem extends BridgeModule { +class RiverDebugSubsystem extends BridgeModule implements HarborJtagDebug { + /// The debug module's JTAG IDCODE, advertised over the BSCAN tunnel. + final int idcode; + RiverDebugSubsystem( WishboneConfig config, { required int xlen, - int idcode = 0x10000001, + this.idcode = 0x10000001, + // FPGA target: selects the config-JTAG primitive. Xilinx (openXC7/vivado) + // taps the config TAP with BSCANE2 on USER1; everything else uses the ECP5 + // JTAGG. Null (ASIC / no FPGA target) falls back to JTAGG. + HarborDeviceTarget? target, String? name, }) : super('RiverDebugSubsystem', name: name ?? 'debug_jtag') { + final useBscane2 = + target is HarborFpgaTarget && + (target.vendor == HarborFpgaVendor.openXc7 || + target.vendor == HarborFpgaVendor.vivado); createPort('clk', PortDirection.input); createPort('reset', PortDirection.input); // Core-facing: from the core. @@ -51,21 +62,42 @@ class RiverDebugSubsystem extends BridgeModule { ); final bus = busRef.internalInterface as WishboneInterface; - // ECP5 config-JTAG user register taps (ER1). - final jtagg = Ecp5Jtagg(); - - // Tunnel: ER1 framed scan -> inner TAP signals. + // Tunnel: framed config-JTAG DR scan -> inner TAP signals. Fed by the + // vendor's config-JTAG user-register primitive below. final tunnel = JtagBscanTunnel(maxScanBits: xlen); tunnel.input('clk').srcConnection! <= input('clk'); tunnel.input('reset').srcConnection! <= input('reset'); - tunnel.input('jtck').srcConnection! <= jtagg.output('JTCK'); - tunnel.input('jtdi').srcConnection! <= jtagg.output('JTDI'); - tunnel.input('jshift').srcConnection! <= jtagg.output('JSHIFT'); - tunnel.input('jupdate').srcConnection! <= jtagg.output('JUPDATE'); - tunnel.input('jce1').srcConnection! <= jtagg.output('JCE1'); - tunnel.input('jrstn').srcConnection! <= jtagg.output('JRSTN'); - jtagg.input('JTDO1').srcConnection! <= tunnel.output('jtdo1'); - jtagg.input('JTDO2').srcConnection! <= Const(0); + + if (useBscane2) { + // Xilinx 7-series BSCANE2 on USER4 (JTAG_CHAIN=4, IR 0x23). riscv-openocd's + // bscan tunnel HARDCODES USER4 for tunneled DMI scans (riscv.c select_user4 + // = 0x23), so the DM must ride USER4, not USER1, or SEL never asserts and + // the tunnel returns zeros. Mapped onto the tunnel's JTAGG-style ports. + // jtck = TCK, the RAW scan clock (the tunnel FSM matches JTAGG's raw JTCK + // and is sim-validated against a continuously toggling clock, so DRCK, the + // gated data-register clock, mis-frames it). The tunnel gates advance with + // SEL & SHIFT; SEL = this user chain selected (the JCE1 equivalent); the + // active-high RESET inverts to the tunnel's active-low jrstn. + final bscan = XilinxBscane2(jtagChain: 4); + tunnel.input('jtck').srcConnection! <= bscan.output('TCK'); + tunnel.input('jtdi').srcConnection! <= bscan.output('TDI'); + tunnel.input('jshift').srcConnection! <= bscan.output('SHIFT'); + tunnel.input('jupdate').srcConnection! <= bscan.output('UPDATE'); + tunnel.input('jce1').srcConnection! <= bscan.output('SEL'); + tunnel.input('jrstn').srcConnection! <= ~bscan.output('RESET'); + bscan.input('TDO').srcConnection! <= tunnel.output('jtdo1'); + } else { + // ECP5 config-JTAG user register taps (ER1). + final jtagg = Ecp5Jtagg(); + tunnel.input('jtck').srcConnection! <= jtagg.output('JTCK'); + tunnel.input('jtdi').srcConnection! <= jtagg.output('JTDI'); + tunnel.input('jshift').srcConnection! <= jtagg.output('JSHIFT'); + tunnel.input('jupdate').srcConnection! <= jtagg.output('JUPDATE'); + tunnel.input('jce1').srcConnection! <= jtagg.output('JCE1'); + tunnel.input('jrstn').srcConnection! <= jtagg.output('JRSTN'); + jtagg.input('JTDO1').srcConnection! <= tunnel.output('jtdo1'); + jtagg.input('JTDO2').srcConnection! <= Const(0); + } // Nets feeding the DM's SBA response, driven by the adapter below. final sbaRdata = Logic(name: 'sba_rdata', width: xlen); @@ -121,4 +153,10 @@ class RiverDebugSubsystem extends BridgeModule { bus.datMosi <= adapter.output('wb_dat_mosi'); bus.sel <= adapter.output('wb_sel'); } + + @override + int get jtagInnerIrWidth => 5; // RISC-V DTM (dtmcs/dmi) IR width + + @override + int? get jtagDmIdcode => idcode; } diff --git a/packages/river_hdl/lib/src/core/jtag_bscan_tunnel.dart b/packages/river_hdl/lib/src/core/jtag_bscan_tunnel.dart index 713afbb..49522ba 100644 --- a/packages/river_hdl/lib/src/core/jtag_bscan_tunnel.dart +++ b/packages/river_hdl/lib/src/core/jtag_bscan_tunnel.dart @@ -1,35 +1,41 @@ import 'package:rohd/rohd.dart'; /// SiFive-style JTAG BSCAN tunnel (NESTED_TAP variant): lets OpenOCD reach the -/// River debug module over the FPGA config JTAG (ECP5 `JTAGG` ER1 user register, -/// driven by dirtyJtag) instead of a separate GPIO TAP. The inner -/// [RiverDebugModule] keeps its full TAP; this module reconstructs that TAP's -/// `tck/tms/tdi` from a framed ER1 DR scan and returns its `tdo` on `jtdo1`. +/// River debug module over the FPGA config JTAG (ECP5 `JTAGG` ER1 user register or +/// Xilinx `BSCANE2` USER4) instead of a separate GPIO TAP. The inner +/// [RiverDebugModule] keeps its full standard TAP; this module reconstructs that +/// TAP's `tck/tms/tdi` from a framed config-JTAG DR scan and returns its `tdo` on +/// `jtdo1`. /// -/// Frame (one ER1 DR scan, LSB-first), matching riscv-openocd -/// `riscv_add_bscan_tunneled_scan` for `BSCAN_TUNNEL_NESTED_TAP`: +/// Frame (one config-JTAG DR scan, LSB-first), matching riscv-openocd's +/// `BSCAN_TUNNEL_NESTED_TAP` frame (sel + width lead the Shift-DR window): /// [1 bit] sel : 1 = inner DR scan, 0 = inner IR scan -/// [7 bits] width : inner scan length N (NOT N-1), LSB first +/// [7 bits] width : inner scan length N, LSB first /// [N+1] payload: inner TDI; the +1 is the one-TCK in/out skew OpenOCD /// compensates by right-shifting the captured field /// [3 bits] idle : zeros; carry the inner TAP Exit1 -> Update -> Run/Idle /// Total frame = N + 12 bits. /// /// OpenOCD selects this tunnel with `riscv use_bscan_tunnel 0` (0 = -/// nested-tap; irwidth = inner DM IR width = 5). +/// nested-tap; irwidth = inner DM IR width = 5). Hardware-proven on the Arty S7: +/// OpenOCD examines the RISC-V DM and reads DDR over SBA through this. /// -/// The inner TAP is clocked only during the ER1 Shift-DR window (`jce1 & jshift`) -/// so it advances once per frame bit and is frozen between frames. The tunnel -/// synthesizes a full inner-TAP walk inside that window: -/// sel=1 (DR): RTI -> Select-DR -> Capture-DR -> Shift-DR(N+1) -> Exit1 -> Update -> RTI -/// sel=0 (IR): RTI -> Select-DR -> Select-IR -> Capture-IR -> Shift-IR(N+1) -> Exit1 -> Update -> RTI -/// This FSM and the inner DM both rising-edge-detect `tck` in the system clock -/// domain, so they advance in lockstep. +/// TCK (~1 MHz) is asynchronous to the system clock, so every config-JTAG level is +/// passed through a 2-FF synchronizer before the frame FSM edge-detects `jtck` or +/// samples `jtdi`/drives the inner TMS, so a level in flight at a system clock edge +/// can't be latched metastable. The inner TAP is clocked only during the Shift-DR +/// window (`jce1 & jshift`) so it advances once per frame bit and is frozen between +/// frames; the FSM synthesizes a full inner TAP walk inside that window: +/// sel=1 (DR): RTI -> Select-DR -> Capture-DR -> Shift-DR(N) -> Exit1 -> Update +/// sel=0 (IR): RTI -> Select-DR -> Select-IR -> Capture-IR -> Shift-IR(N) -> ... /// -/// HW-validation pending: the TDI/TDO skew (+1 payload bit) and header-to-shift -/// alignment are the parts to confirm against live OpenOCD. Framing + inner-TAP -/// walk are sim-tested against the real DM in jtag_tunnel_dm_test.dart (only the -/// `Ecp5Jtagg` primitive is an unsimulatable blackbox). +/// The frame counter is restarted by the `~active` edge reset (a config-JTAG +/// edge outside the Shift-DR window), which anchors each scan to its Capture-DR. +/// An earlier BSCANE2-CAPTURE anchor input was tried and removed: CAPTURE held +/// wider than one Capture-DR state on silicon and pinned the counter, killing the +/// tunnel. Framing + inner-TAP walk are sim-tested against the real DM in +/// jtag_tunnel_dm_test.dart (the config-JTAG primitive is an unsimulatable +/// blackbox; the async synchronizers only matter on real silicon). class JtagBscanTunnel extends Module { /// Width of the widest inner scan (the DMI register, ~41 bits). Sizes the /// payload counter. @@ -40,7 +46,7 @@ class JtagBscanTunnel extends Module { final clk = addInput('clk', Logic()); final reset = addInput('reset', Logic()); - // ER1 user-register side (from Ecp5Jtagg). + // Config-JTAG user-register side (from Ecp5Jtagg or XilinxBscane2). final jtck = addInput('jtck', Logic()); final jtdi = addInput('jtdi', Logic()); final jshift = addInput('jshift', Logic()); @@ -55,6 +61,19 @@ class JtagBscanTunnel extends Module { final innerTdi = addOutput('inner_tdi'); final innerTrstN = addOutput('inner_trst_n'); + // 2-FF synchronizers for the asynchronous config-JTAG inputs. Bit 0 is the + // metastability-catcher, bit 1 the synchronized level the FSM consumes. + final jtckSync = Logic(name: 'jtck_sync', width: 2); + final jtdiSync = Logic(name: 'jtdi_sync', width: 2); + final jshiftSync = Logic(name: 'jshift_sync', width: 2); + final jce1Sync = Logic(name: 'jce1_sync', width: 2); + final jrstnSync = Logic(name: 'jrstn_sync', width: 2); + final jtckS = jtckSync[1]; + final jtdiS = jtdiSync[1]; + final jshiftS = jshiftSync[1]; + final jce1S = jce1Sync[1]; + final jrstnS = jrstnSync[1]; + final cntW = (maxScanBits + 16).bitLength; final cnt = Logic(name: 'bit_cnt', width: cntW); // frame bit index final sel = Logic(name: 'sel'); // 1=DR, 0=IR @@ -62,12 +81,11 @@ class JtagBscanTunnel extends Module { final jtckPrev = Logic(name: 'jtck_prev'); final tdoCap = Logic(name: 'tdo_cap'); // registered inner tdo for jtdo1 - final jtckRise = (jtck & ~jtckPrev).named('jtck_rise'); - final active = (jce1 & jshift).named('tunnel_active'); // shifting ER1 DR + final jtckRise = (jtckS & ~jtckPrev).named('jtck_rise'); + final active = (jce1S & jshiftS).named('tunnel_active'); // shifting DR - // Header is 8 bits (sel + 7 width). OpenOCD sends width+1 payload bits but the - // inner TAP shifts exactly N; the extra bit is the one-TCK TDO skew. So the - // inner Shift window is bits 8 .. 8+N-1. + // Header is 8 bits (sel + 7 width). The inner Shift window is bits + // 8 .. 8+N-1; OpenOCD's extra payload bit is the one-TCK TDO skew. final headerBits = 8; final shiftStart = Const(headerBits, width: cntW); final lastShift = @@ -76,14 +94,12 @@ class JtagBscanTunnel extends Module { Const(1, width: cntW)) .named('last_shift'); - // Inner TAP in Shift (driving N real shifts)? final inShift = (cnt.gte(shiftStart) & cnt.lte(lastShift)).named( 'in_shift', ); final atLastShift = cnt.eq(lastShift).named('at_last_shift'); - // TMS schedule by frame bit. Inner TAP starts each frame in Run-Test/Idle - // (TLR on first access lands in RTI too, identical walk). + // TMS schedule by frame bit. Inner TAP starts each frame in Run-Test/Idle. // DR walk: tms=1 at bit 5 (RTI->Sel-DR); bits 6,7 tms=0 (Capture, Shift). // IR walk: tms=1 at bits 4,5 (Sel-DR, Sel-IR); bits 6,7 tms=0. // Then N shift bits 8..lastShift (tms=0); last asserts tms=1 (Shift->Exit1), @@ -105,9 +121,16 @@ class JtagBscanTunnel extends Module { ).named('inner_tms_val'); Sequential(clk, reset: reset, [ - jtckPrev < jtck, + // Advance the input synchronizers every system clock. + jtckSync < [jtckSync[0], jtck].swizzle(), + jtdiSync < [jtdiSync[0], jtdi].swizzle(), + jshiftSync < [jshiftSync[0], jshift].swizzle(), + jce1Sync < [jce1Sync[0], jce1].swizzle(), + jrstnSync < [jrstnSync[0], jrstn].swizzle(), + + jtckPrev < jtckS, If( - ~jrstn, + ~jrstnS, then: [cnt < Const(0, width: cntW)], orElse: [ If( @@ -117,13 +140,13 @@ class JtagBscanTunnel extends Module { active, then: [ cnt < cnt + 1, - If(cnt.eq(Const(0, width: cntW)), then: [sel < jtdi]), + If(cnt.eq(Const(0, width: cntW)), then: [sel < jtdiS]), // Shift width in LSB-first across bits 1..7. If( cnt.gte(Const(1, width: cntW)) & cnt.lte(Const(7, width: cntW)), then: [ - width < [jtdi, width.getRange(1, 7)].swizzle(), + width < [jtdiS, width.getRange(1, 7)].swizzle(), ], ), // Capture inner tdo while shifting (registered -> the +1 skew). @@ -141,12 +164,12 @@ class JtagBscanTunnel extends Module { ), ]); - // Inner TAP drive. Clock the inner TAP ONLY inside the ER1 Shift-DR window - // so it advances once per frame bit; tms/tdi are combinational per bit. - innerTck <= jtck & active; - innerTrstN <= jrstn; + // Inner TAP drive. Clock the inner TAP ONLY inside the Shift-DR window so it + // advances once per frame bit; tms/tdi are combinational per bit. + innerTck <= jtckS & active; + innerTrstN <= jrstnS; innerTms <= tmsVal; - innerTdi <= mux(inShift, jtdi, Const(0)); + innerTdi <= mux(inShift, jtdiS, Const(0)); jtdo1 <= tdoCap; } } diff --git a/packages/river_hdl/lib/src/core/sba_wishbone.dart b/packages/river_hdl/lib/src/core/sba_wishbone.dart index ed81afa..d95749d 100644 --- a/packages/river_hdl/lib/src/core/sba_wishbone.dart +++ b/packages/river_hdl/lib/src/core/sba_wishbone.dart @@ -81,10 +81,19 @@ class SbaWishboneAdapter extends Module { wbCyc <= sbaReq; wbStb <= sbaReq; wbWe <= sbaWe; + // Beat-align the bus address: the data rides its byte lane (shifted by + // byteOff below) with `sel`, so the address must point at the aligned beat, + // NOT the raw byte offset. The core's dcache/MMU drive the bus the same way + // (line-aligned addr + sel); an unaligned addr here makes the DDR downsizer + // route a high-lane (4-mod-8) access to the next beat, so SBA reads/writes of + // upper-32-bit halves land on the wrong word. + final alignedAddr = selBits == 0 + ? sbaAddr + : [sbaAddr.getRange(selBits, xlen), Const(0, width: selBits)].swizzle(); wbAdr <= (xlen >= addressWidth - ? sbaAddr.getRange(0, addressWidth) - : sbaAddr.zeroExtend(addressWidth)); + ? alignedAddr.getRange(0, addressWidth) + : alignedAddr.zeroExtend(addressWidth)); // Write data, sign-irrelevant zero-justified to the bus width, shifted up // into its addressed byte lane. diff --git a/packages/river_hdl/lib/src/genip.dart b/packages/river_hdl/lib/src/genip.dart index 30424d3..d806a10 100644 --- a/packages/river_hdl/lib/src/genip.dart +++ b/packages/river_hdl/lib/src/genip.dart @@ -1661,14 +1661,22 @@ class RiverGenIpConfig { busDataWidth: busConfig.dataWidth, useUsrmclk: isEcp5, useStartupe2: isXilinx, + // Standalone FPGA builds have no external pad ring, so the controller + // owns the bidirectional IO pad (one inout spi_io + internal tristate). + ownPads: isEcp5 || isXilinx, name: '${mem.type}_$i', ); soc.addPeripheral(flash); // Expose the SPI pads. The clock is absent on ECP5 (USRMCLK). Quad/dual // flash exposes split tristate IO (spi_io_out/oe/in). Standard mode is // spi_mosi/spi_miso. + // FPGA targets own the pad (single inout spi_io); otherwise expose the + // split tristate for an external pad ring / shared-bus mux. + final flashOwnsPads = isEcp5 || isXilinx; final dataPins = spiConfig.mode == HarborSpiFlashMode.standard ? const ['spi_cs_n', 'spi_mosi', 'spi_miso'] + : flashOwnsPads + ? const ['spi_cs_n', 'spi_io'] : const ['spi_cs_n', 'spi_io_out', 'spi_io_oe', 'spi_io_in']; final spiPins = [if (!isEcp5 && !isXilinx) 'spi_clk', ...dataPins]; // Prefix when more than one SPI device shares the pinout (multiple flash, @@ -1758,10 +1766,10 @@ class RiverGenIpConfig { _integrateUsbDfu(soc, busConfig, target); } else if (usbDfu && usbDfuMode == UsbDfuMode.software) { _integrateUsbDfuSoftware(soc, busConfig, target); - if (enableDebug) _integrateDebugJtag(soc, busConfig, debugCore!); + if (enableDebug) _integrateDebugJtag(soc, busConfig, debugCore!, target); soc.buildFabric(); } else { - if (enableDebug) _integrateDebugJtag(soc, busConfig, debugCore!); + if (enableDebug) _integrateDebugJtag(soc, busConfig, debugCore!, target); soc.buildFabric(); } @@ -1775,9 +1783,10 @@ class RiverGenIpConfig { HarborSoC soc, WishboneConfig busConfig, RiverCore core, + HarborDeviceTarget? target, ) { final xlen = busConfig.dataWidth; - final dbg = RiverDebugSubsystem(busConfig, xlen: xlen); + final dbg = RiverDebugSubsystem(busConfig, xlen: xlen, target: target); soc.addMaster(dbg, busInterfaceName: 'bus'); // To the core. @@ -1792,9 +1801,10 @@ class RiverGenIpConfig { dbg.input('reg_rdata').srcConnection! <= core.output('debug_reg_rdata'); dbg.input('reg_ready').srcConnection! <= core.output('debug_reg_ready'); - // No top-level JTAG pads: the TAP comes off the ECP5 config JTAG (dirtyJtag) - // via the JTAGG primitive inside the subsystem. OpenOCD reaches it with - // `riscv use_bscan_tunnel 6 1` over the ECP5 TAP. + // No top-level JTAG pads: the TAP comes off the FPGA config JTAG (ECP5 + // JTAGG or Xilinx BSCANE2 on USER1, selected by target) inside the + // subsystem. OpenOCD reaches it over the config TAP with + // `riscv use_bscan_tunnel`. } /// Integrates the USB DFU subsystem into [soc]: instantiates the subsystem diff --git a/packages/river_hdl/test/dbg_elab_test.dart b/packages/river_hdl/test/dbg_elab_test.dart index 7a894a7..b2b81fb 100644 --- a/packages/river_hdl/test/dbg_elab_test.dart +++ b/packages/river_hdl/test/dbg_elab_test.dart @@ -3,12 +3,53 @@ import 'package:river_hdl/river_hdl.dart'; import 'package:test/test.dart'; void main() { + final cfg = WishboneConfig(addressWidth: 64, dataWidth: 64, selWidth: 8); + test('RiverDebugSubsystem elaborates', () async { - final cfg = WishboneConfig(addressWidth: 64, dataWidth: 64, selWidth: 8); final sub = RiverDebugSubsystem(cfg, xlen: 64); await sub.build(); final sv = sub.generateSynth(); expect(sv.contains('RiverDebugModule'), isTrue); expect(sv.contains('SbaWishboneAdapter'), isTrue); }); + + test('no FPGA target defaults to the ECP5 JTAGG tap', () async { + final sub = RiverDebugSubsystem(cfg, xlen: 64); + await sub.build(); + final sv = sub.generateSynth(); + expect(sv.contains('JTAGG'), isTrue); + expect(sv.contains('BSCANE2'), isFalse); + }); + + test('Xilinx target taps BSCANE2 on USER1, not JTAGG', () async { + final sub = RiverDebugSubsystem( + cfg, + xlen: 64, + target: const HarborFpgaTarget.spartan7( + device: 'xc7s50', + package: 'csga324', + ), + ); + await sub.build(); + final sv = sub.generateSynth(); + expect(sv.contains('BSCANE2'), isTrue); + // USER4 (openocd's bscan tunnel hardcodes USER4). + expect(sv.contains('JTAG_CHAIN(4)'), isTrue); + expect(sv.contains('JTAGG'), isFalse); + }); + + test('ECP5 target taps the JTAGG', () async { + final sub = RiverDebugSubsystem( + cfg, + xlen: 64, + target: const HarborFpgaTarget.ecp5( + device: 'lfe5u-25f', + package: 'CSFBGA285', + ), + ); + await sub.build(); + final sv = sub.generateSynth(); + expect(sv.contains('JTAGG'), isTrue); + expect(sv.contains('BSCANE2'), isFalse); + }); } diff --git a/packages/river_hdl/test/debug/jtag_tunnel_dm_test.dart b/packages/river_hdl/test/debug/jtag_tunnel_dm_test.dart index ccc7423..77ef03b 100644 --- a/packages/river_hdl/test/debug/jtag_tunnel_dm_test.dart +++ b/packages/river_hdl/test/debug/jtag_tunnel_dm_test.dart @@ -53,12 +53,13 @@ class Er1Host { } Future _settle() async { - // Let the system clock sample the JTAG-domain levels (edge detect) and - // service SBA so DM bus accesses can drain. - await clk.nextPosedge; - _serviceSba(); - await clk.nextPosedge; - _serviceSba(); + // Let the system clock sample the JTAG-domain levels (2-FF synchronizer + + // edge detect) and service SBA so DM bus accesses can drain. Enough cycles + // to cover the synchronizer latency before the next JTAG level is applied. + for (var i = 0; i < 4; i++) { + await clk.nextPosedge; + _serviceSba(); + } } /// One ER1 TCK pulse with the given shift/update strobes and TDI; returns the diff --git a/packages/river_hdl/test/sba_wishbone_test.dart b/packages/river_hdl/test/sba_wishbone_test.dart index c9c035c..e3ec75d 100644 --- a/packages/river_hdl/test/sba_wishbone_test.dart +++ b/packages/river_hdl/test/sba_wishbone_test.dart @@ -109,6 +109,34 @@ void main() { await wr(0x05, 0x9a, 0); expect((await rd(0x05, 0)) & 0xff, equals(0x9a), reason: 'byte lane 5'); + // The bus address MUST be beat-aligned: the lane rides `sel`, so a high-half + // (4-mod-8) access must present the aligned beat address, not the raw byte + // offset, or the DDR downsizer routes it to the wrong beat. Drive a high-lane + // access and sample wb_adr while the request is live. + sbaWe.inject(1); + sbaAddr.inject(0x8000_1004); + sbaWdata.inject(0x1234); + sbaSize.inject(2); + sbaReq.inject(1); + await clk.nextPosedge; + expect( + dut.output('wb_adr').value.toInt() & 0x7, + equals(0), + reason: 'wb_adr beat-aligned (low 3 bits clear) for a 4-mod-8 access', + ); + expect( + dut.output('wb_adr').value.toInt(), + equals(0x8000_1000), + reason: 'wb_adr points at the aligned beat, sel selects the high lane', + ); + expect( + dut.output('wb_sel').value.toInt(), + equals(0xf0), + reason: 'high 32-bit lane selected', + ); + sbaReq.inject(0); + await clk.nextPosedge; + await Simulator.endSimulation(); }); } diff --git a/pubspec.lock b/pubspec.lock index 26f6eb1..d26e908 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -125,8 +125,8 @@ packages: dependency: "direct overridden" description: path: "packages/harbor" - ref: master - resolved-ref: "4f4352e654e735ae1d117d9087b7fa72583d8dea" + ref: "fix/arty-s7-ddr" + resolved-ref: cb646f8629d6b69a0e0e54bdf3171cfa0d48bbea url: "https://github.com/MidstallSoftware/harbor.git" source: git version: "0.0.1" diff --git a/pubspec.lock.json b/pubspec.lock.json index 22f48ec..1607054 100644 --- a/pubspec.lock.json +++ b/pubspec.lock.json @@ -154,8 +154,8 @@ "dependency": "direct overridden", "description": { "path": "packages/harbor", - "ref": "master", - "resolved-ref": "4f4352e654e735ae1d117d9087b7fa72583d8dea", + "ref": "fix/arty-s7-ddr", + "resolved-ref": "cb646f8629d6b69a0e0e54bdf3171cfa0d48bbea", "url": "https://github.com/MidstallSoftware/harbor.git" }, "source": "git", diff --git a/pubspec.yaml b/pubspec.yaml index 114f21d..ec67718 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,7 +17,7 @@ dependency_overrides: git: url: https://github.com/MidstallSoftware/harbor.git path: packages/harbor - ref: master + ref: fix/arty-s7-ddr dev_dependencies: coverage: ^1.15.0 From ecc0ca91ba99cce78cfdcce4ae8e3418c632053c Mon Sep 17 00:00:00 2001 From: Tristan Ross Date: Mon, 3 Aug 2026 00:24:14 -0700 Subject: [PATCH 2/3] feat: working ddr --- devices.nix | 30 ++- flake.lock | 67 +++++ flake.nix | 12 +- nix/common-dart.nix | 2 +- packages/river/lib/src/impl/core/v1.dart | 4 + packages/river/lib/src/river_base.dart | 12 + packages/river_emulator/lib/src/core.dart | 6 +- packages/river_hdl/lib/src/boards.dart | 34 ++- packages/river_hdl/lib/src/compat.dart | 17 +- packages/river_hdl/lib/src/core.dart | 41 +++- packages/river_hdl/lib/src/core/decoder.dart | 44 +++- packages/river_hdl/lib/src/core/exec.dart | 71 +++--- .../river_hdl/lib/src/core/fu_branch.dart | 19 +- packages/river_hdl/lib/src/core/issue.dart | 16 ++ packages/river_hdl/lib/src/core/pipeline.dart | 10 + packages/river_hdl/lib/src/core/stages.dart | 5 + packages/river_hdl/lib/src/genip.dart | 232 +++++++++++++++++- .../test/branch/cjalr_link_test.dart | 109 ++++++++ .../test/core/issue_queue_count_test.dart | 2 + .../test/csr/csrs_sie_smode_test.dart | 184 ++++++++++++++ .../test/debug/ebreak_debug_halt_test.dart | 5 +- pkgs/river-fpga/default.nix | 83 ++++++- pkgs/river-hdl/default.nix | 8 + pkgs/river-ip/default.nix | 4 +- pubspec.lock | 2 +- pubspec.lock.json | 2 +- 26 files changed, 935 insertions(+), 86 deletions(-) create mode 100644 packages/river_hdl/test/branch/cjalr_link_test.dart create mode 100644 packages/river_hdl/test/csr/csrs_sie_smode_test.dart diff --git a/devices.nix b/devices.nix index d25490b..6f1936b 100644 --- a/devices.nix +++ b/devices.nix @@ -62,11 +62,23 @@ in }; # 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). + # flow. The DDR3 is the silicon-proven ddr3v2 stack (the harbor-native ROHD + # port of UberDDR3): full x16 256MB (MT41K128M16) at 300MHz CK. This PHY runs + # its OWN calibration in hardware, so the host does not need a training pass. + # cmdslot=2 sets the command slot and wrshift=-1 sets the write launch. Add + # train=runtime to the dram params to expose the FSBL knob-ABI window instead + # (per controller, optional). + # + # FSBL-from-SRAM boot (the DDR bootstrap decouple): a 64K on-chip BRAM at + # 0x08000000 holds the FSBL stack, .data and .bss (its console struct + # included), so the FSBL runs, prints and can drive the DDR WITHOUT a working + # DRAM read first. The maskrom xipboot runs the FSBL in place from flash XIP. + # The FSBL scratch lives in the SRAM (weir tools/fdt_ld.zig routes it there + # when the tree has an mmio-sram node), then it copies main Weir into DRAM. + # DRAM stays at 0x80000000 so Weir and Ferrite link there unchanged. + # + # clk MUST be SSTL135: R2 is on the 1.35V DDR bank, so `clk=R2` alone + # (defaults to LVCMOS33) yields a dead SoC, the clock is never received. creek-v1-arty = { ip = river-hdl.mkSoC ( creek-v1-base @@ -76,13 +88,13 @@ in 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" + "0x08000000:64K:sram" + "0x80000000:256M:dram:arty-s7:ddr3v2=true,clockfreq=300000000,cmdslot=2,wrshift=-1,trainable=true" ]; devices = creek-v1-base.devices ++ [ "debug-jtag" ]; - bootProgram = "monitor"; + bootProgram = "xipboot"; pins = [ - "clk=R2" + "clk=R2 SSTL135" "uart_tx=uart@tx:R12" "uart_rx=uart@rx:V12" ]; diff --git a/flake.lock b/flake.lock index 8f2ec06..4401b02 100644 --- a/flake.lock +++ b/flake.lock @@ -41,6 +41,24 @@ "type": "github" } }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, "flakever": { "locked": { "lastModified": 1763450705, @@ -71,15 +89,64 @@ "type": "github" } }, + "nixpkgs_2": { + "locked": { + "lastModified": 1784497964, + "narHash": "sha256-WhdsTtaih3DgTPP/PX023b36UNQyMzEoi6GkPyGx0y4=", + "rev": "241313f4e8e508cb9b13278c2b0fa25b9ca27163", + "type": "tarball", + "url": "https://releases.nixos.org/nixos/unstable/nixos-26.11pre1037713.241313f4e8e5/nixexprs.tar.xz" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-unstable", + "type": "indirect" + } + }, + "openxc7": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1785707812, + "narHash": "sha256-aLlkja5n6AqI2KFpnjH+pLxgsxPEdYrXLrsuM7xthtw=", + "owner": "openXC7", + "repo": "toolchain-nix", + "rev": "b2cbf24785829e05266500b9d16e3f0460a009f6", + "type": "github" + }, + "original": { + "owner": "openXC7", + "repo": "toolchain-nix", + "type": "github" + } + }, "root": { "inputs": { "asix": "asix", "flake-parts": "flake-parts", "flakever": "flakever", "nixpkgs": "nixpkgs", + "openxc7": "openxc7", "treefmt-nix": "treefmt-nix_2" } }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, "treefmt-nix": { "inputs": { "nixpkgs": [ diff --git a/flake.nix b/flake.nix index 321ca7f..be79c7d 100644 --- a/flake.nix +++ b/flake.nix @@ -15,6 +15,7 @@ url = "github:MidstallSoftware/asix"; inputs.nixpkgs.follows = "nixpkgs"; }; + openxc7.url = "github:openXC7/toolchain-nix"; }; outputs = @@ -54,6 +55,7 @@ { system, pkgs, + inputs', ... }: let @@ -116,7 +118,14 @@ # above in _module.args.pkgs. overlayAttrs = { flakever = flakeverConfig; - river-hdl = pkgs.callPackage ./pkgs/river-hdl { }; + river-hdl = pkgs.callPackage ./pkgs/river-hdl { + # openXC7 Xilinx toolchain: mkFpga uses it for spartan7 targets. + # The chipdb/nextpnr/prjxray come from the flake's per-system + # packages; the python deps for prjxray's fasm2frames come from + # openXC7's own pinned nixpkgs (a consistent 3.12 set). + openxc7 = inputs'.openxc7.packages; + openxc7Nixpkgs = inputs.openxc7.inputs.nixpkgs.legacyPackages.${system}; + }; }; checks = { @@ -158,6 +167,7 @@ fpgaVendors = [ "ecp5" "ice40" + "spartan7" ]; asicVendors = [ "sky130" diff --git a/nix/common-dart.nix b/nix/common-dart.nix index 69680e4..fdb744a 100644 --- a/nix/common-dart.nix +++ b/nix/common-dart.nix @@ -2,6 +2,6 @@ lib: { pubspecLock = lib.importJSON ../pubspec.lock.json; gitHashes = { - harbor = "sha256-B7kuqZmSlKgKDGhZhbxYrGr3ZeEA+zRcJ4szdXRb62A="; + harbor = "sha256-FZd+6+ZqwIw50CgBRp4aC0bZhGJIamsuRuXplCiPGgs="; }; } diff --git a/packages/river/lib/src/impl/core/v1.dart b/packages/river/lib/src/impl/core/v1.dart index 091df6b..c1ade18 100644 --- a/packages/river/lib/src/impl/core/v1.dart +++ b/packages/river/lib/src/impl/core/v1.dart @@ -83,6 +83,10 @@ class RiverCoreConfigV1 extends RiverCoreConfig { // not just a perf choice. Microcode datapath (shared ALU) over the // static fabric: ~12k vs ~21k LUT on small FPGAs. microcodeMode: MicrocodeMode.full, + // Decode 2 pattern-ROM entries/cycle: halves the decode pattern-scan + // (the biggest per-instruction cost) for a couple of comparators of + // area, staying fully microcoded + patchable. Bigger tiers scale up. + microcodeDecodeLanes: 2, ); /// RC1.f - River Core V1 full (RV64GC_Zicsr_Zifencei), in-order single-issue, diff --git a/packages/river/lib/src/river_base.dart b/packages/river/lib/src/river_base.dart index c108d6d..c882fa0 100644 --- a/packages/river/lib/src/river_base.dart +++ b/packages/river/lib/src/river_base.dart @@ -206,6 +206,17 @@ class RiverCoreConfig { final List interrupts; final HarborMmuConfig mmu; final MicrocodeMode microcodeMode; + + /// Number of microcode-decode LANES: how many decode-pattern ROM entries the + /// dynamic decoder reads + compares against the instruction PER CYCLE. The + /// decoder's pattern search is O(patterns/lanes) cycles, so `lanes` trades a + /// few comparators of area for a proportionally shorter decode (the pattern + /// scan is the dominant per-instruction cost on the microcode datapath). 1 = + /// the plain one-per-cycle linear scan. Larger cores set more lanes; the ROM + /// stays ROM-driven and runtime-patchable either way. No effect unless + /// [microcodeMode] uses a standalone (ROM-scanning) decoder. + final int microcodeDecodeLanes; + final ExecutionMode executionMode; final IssueWidth issueWidth; @@ -300,6 +311,7 @@ class RiverCoreConfig { required this.interrupts, required this.mmu, this.microcodeMode = MicrocodeMode.none, + this.microcodeDecodeLanes = 1, this.executionMode = ExecutionMode.inOrder, this.issueWidth = IssueWidth.single, IssueWidth? commitWidth, diff --git a/packages/river_emulator/lib/src/core.dart b/packages/river_emulator/lib/src/core.dart index f4da6d5..42bb113 100644 --- a/packages/river_emulator/lib/src/core.dart +++ b/packages/river_emulator/lib/src/core.dart @@ -1017,9 +1017,9 @@ class RiverCore implements CsrContext { (t) => t.causeCode == mop.causeCode && t.interrupt == mop.isInterrupt, orElse: () => Trap.illegal, ); - // ecall: microcode hardcodes cause 8 (ecallU); real cause depends on - // originating mode: U/VU=8, HS=9, VS=10 (H), M=11. - if (mop.causeCode == 8 && !mop.isInterrupt) { + // The micro-op's modeCause bit re-encodes the cause by originating + // mode (ecall: U/VU=8, HS=9, VS=10 (H), M=11), matching the RTL. + if (mop.modeCause && !mop.isInterrupt) { trapKind = switch (mode) { PrivilegeMode.machine => Trap.ecallM, PrivilegeMode.supervisor => virt ? Trap.ecallVS : Trap.ecallS, diff --git a/packages/river_hdl/lib/src/boards.dart b/packages/river_hdl/lib/src/boards.dart index 30eb313..371f786 100644 --- a/packages/river_hdl/lib/src/boards.dart +++ b/packages/river_hdl/lib/src/boards.dart @@ -40,6 +40,17 @@ class DdrBoard { /// clean). False for boards whose write eye closes on its own (ECP5/x16). final bool writeVerify; + /// Whether the controller runs hardware MPR read-calibration before opening + /// the bus (sequencer sweeps each byte lane's read window x IDELAY tap against + /// the DRAM MPR pattern and locks the eye). The openXC7 Arty ddr3Fast path + /// needs it: the CK-based ISERDESE2 capture eye drifts per boot, so a static + /// tap only boots ~half the time (the Ferrite first-ifetch coin-flip). False + /// for boards whose read eye is stable (ECP5 DQS-strobed capture). + final bool readLevel; + + /// Post-read-cal cadence self-test gate (ddr3Fast). Requires [readLevel]. + final bool selfTest; + /// Per-board DDR tuning defaults, forwarded to the controller when a memory /// region does not override them (see genip's effective-value merge). Null /// leaves the genip global default in force. cmdSlot/wrShift/wrBeat/window @@ -60,6 +71,8 @@ class DdrBoard { this.dqsComplementPins = const {}, this.trainableRead = false, this.writeVerify = false, + this.readLevel = false, + this.selfTest = false, this.cmdSlot, this.wrShift, this.wrBeat, @@ -122,9 +135,11 @@ class DdrBoard { colWidth: 10, casLatency: 6, ), + // Read-cal PARKED OFF (see _artyS7x8): wedges the boot on HW. + readLevel: false, pins: { - 'sdram_ck': 'R5 SSTL135', - 'sdram_ck_n': 'T4 SSTL135', + 'sdram_ck': 'R5 DIFF_SSTL135', + 'sdram_ck_n': 'T4 DIFF_SSTL135', 'sdram_cke': 'T2 SSTL135', 'sdram_cs_n': 'R3 SSTL135', 'sdram_ras_n': 'U1 SSTL135', @@ -202,6 +217,11 @@ class DdrBoard { // 90-degree phase inert): reads clean, a re-driven write always lands, so // write-verify-retry makes CPU writes correct. writeVerify: true, + // Read-cal (MPR eye sweep) is PARKED OFF: on HW the sRdCal FSM does not + // complete (sim-clean, unsimmable on the real PHY), so its bus gate wedges + // the whole wishbone before the FSBL prints. The RTL stays behind the flag + // (readlevel=true) for on-board debugging; the default boot is the base. + readLevel: false, // HW-proven x8 DDR3 tuning, baked in so a plain build needs no per-region params. cmdSlot: 2, wrShift: -1, @@ -210,8 +230,8 @@ class DdrBoard { readRetry: 6, window: 5, pins: { - 'sdram_ck': 'R5 SSTL135', - 'sdram_ck_n': 'T4 SSTL135', + 'sdram_ck': 'R5 DIFF_SSTL135', + 'sdram_ck_n': 'T4 DIFF_SSTL135', 'sdram_cke': 'T2 SSTL135', 'sdram_cs_n': 'R3 SSTL135', 'sdram_ras_n': 'U1 SSTL135', @@ -270,6 +290,8 @@ class DdrBoard { casLatency: 6, ), writeVerify: true, + // Read-cal PARKED OFF (see _artyS7x8): wedges the boot on HW. + readLevel: false, cmdSlot: 2, wrShift: -1, wrBeat: 0, @@ -277,8 +299,8 @@ class DdrBoard { readRetry: 6, window: 5, pins: { - 'sdram_ck': 'R5 SSTL135', - 'sdram_ck_n': 'T4 SSTL135', + 'sdram_ck': 'R5 DIFF_SSTL135', + 'sdram_ck_n': 'T4 DIFF_SSTL135', 'sdram_cke': 'T2 SSTL135', 'sdram_cs_n': 'R3 SSTL135', 'sdram_ras_n': 'U1 SSTL135', diff --git a/packages/river_hdl/lib/src/compat.dart b/packages/river_hdl/lib/src/compat.dart index be5bae9..64b6f4b 100644 --- a/packages/river_hdl/lib/src/compat.dart +++ b/packages/river_hdl/lib/src/compat.dart @@ -511,6 +511,7 @@ final List kMicroOpTable = [ 'funct': BitRange(0, 4), 'causeCode': BitRange(5, 10), 'isInterrupt': BitRange(11, 11), + 'modeCause': BitRange(12, 12), }), toMap: (mop) { final m = mop as RiscVTrapOp; @@ -518,6 +519,7 @@ final List kMicroOpTable = [ 'funct': TrapMicroOp.funct, 'causeCode': m.causeCode, 'isInterrupt': m.isInterrupt ? 1 : 0, + 'modeCause': m.modeCause ? 1 : 0, }; }, ), @@ -545,10 +547,17 @@ final List kMicroOpTable = [ 5 + MicroOpLink.width + mxlen.size - 1, ), }), - toMap: (mop) => { - 'funct': WriteLinkRegisterMicroOp.funct, - 'link': MicroOpLink.rd.value, - 'pcOffset': 4, + toMap: (mop) { + // pcOffset is the instruction length that forms the link (return) address + // = PC + len. It MUST come from the op, not a fixed 4: compressed calls + // (c.jalr, rv_c.dart) carry pcOffset: 2, and hardcoding 4 here returned + // two bytes too far, breaking every function-pointer/vtable call on rc1-s. + final m = mop as RiscVWriteLinkRegister; + return { + 'funct': WriteLinkRegisterMicroOp.funct, + 'link': MicroOpLink.rd.value, + 'pcOffset': m.pcOffset, + }; }, ), MicroOpEncoding( diff --git a/packages/river_hdl/lib/src/core.dart b/packages/river_hdl/lib/src/core.dart index 455a10b..7bd54a3 100644 --- a/packages/river_hdl/lib/src/core.dart +++ b/packages/river_hdl/lib/src/core.dart @@ -810,17 +810,38 @@ class RiverCore extends BridgeModule { dtlbFlushOnPriv <= (csrs == null ? Const(0) : csrs.rpipelinectl[3]); // Microcode ROMs (optionally PATCHABLE at runtime via the rmicrocode* CSRs). - final microcodeDecodeRead = DataPortInterface( - microcode.patternWidth, - microcode.map.length.bitLength, - ); + // Parallel decode: the decode ROM packs `decodeLanes` pattern rows per word + // (lane 0 in the low bits) so the dynamic decoder reads + compares that many + // patterns per cycle, shortening its scan to ceil(patterns/lanes) cycles. + // lanes==1 is the classic one-per-cycle ROM. + final decodeLanes = config.microcodeDecodeLanes; + final patternW = microcode.patternWidth; + final decodeRowW = patternW * decodeLanes; + final decodeWords = (microcode.map.length + decodeLanes - 1) ~/ decodeLanes; + // Match the original count.bitLength convention (the RegisterFile/ROM index). + final decodeIdxW = decodeWords.bitLength; + // Pack: word w = pattern[w*lanes + l] << (l*patternW), padding the tail with + // the last real pattern (a never-false extra: if the instruction matched it + // the real lane matches first by priority, so no spurious hit). + final rawPatterns = microcode.encodedPatterns; + final packedPatterns = [ + for (var w = 0; w < decodeWords; w++) + [ + for (var l = 0; l < decodeLanes; l++) + rawPatterns[(w * decodeLanes + l).clamp( + 0, + rawPatterns.length - 1, + )] << + (l * patternW), + ].reduce((a, b) => a | b), + ]; + + final microcodeDecodeRead = DataPortInterface(decodeRowW, decodeIdxW); final microcodeExecRead = DataPortInterface( microcode.mopWidth(config.mxlen), microcode.mopIndexWidth(config.mxlen), ); - final decodeRowW = microcode.patternWidth; - final decodeIdxW = microcode.map.length.bitLength; final execRowW = microcode.mopWidth(config.mxlen); final execIdxW = microcode.mopIndexWidth(config.mxlen); @@ -894,7 +915,7 @@ class RiverCore extends BridgeModule { if (useEbrRom) { final rom = Ecp5InitRom( clk, - contents: microcode.encodedPatterns, + contents: packedPatterns, width: decodeRowW, rdAddr: microcodeDecodeRead.addr, wrEn: decodeWrite?.en, @@ -908,7 +929,7 @@ class RiverCore extends BridgeModule { // the decode ROM does not explode into flops. final rom = InferredInitRom( clk, - contents: microcode.encodedPatterns, + contents: packedPatterns, width: decodeRowW, rdAddr: microcodeDecodeRead.addr, wrEn: decodeWrite?.en, @@ -926,8 +947,8 @@ class RiverCore extends BridgeModule { reset, decodeWrite != null ? [wrapWriteForRegisterFile(decodeWrite)] : [], [wrapReadForRegisterFile(decodeRaw)], - numEntries: microcode.map.length, - resetValue: microcode.encodedPatterns, + numEntries: decodeWords, + resetValue: packedPatterns, definitionName: 'RiverMicrocodeLookup', ); final decodeDataReg = Logic( diff --git a/packages/river_hdl/lib/src/core/decoder.dart b/packages/river_hdl/lib/src/core/decoder.dart index 97f0095..3e9ba3d 100644 --- a/packages/river_hdl/lib/src/core/decoder.dart +++ b/packages/river_hdl/lib/src/core/decoder.dart @@ -336,11 +336,49 @@ class DynamicInstructionDecoder extends InstructionDecoder { microcode.typeStructs.length.bitLength, ); + // Parallel decode lanes: microcodeRead.data holds `lanes` pattern rows + // (lane 0 in the low bits). Match the instruction against every lane this + // cycle and priority-select the lowest-index matching row, so the scan + // advances `lanes` patterns per cycle instead of one. lanes==1 degenerates + // to reading the single row straight through. + final rowW = microcode.patternWidth; + final lanes = microcodeRead.data.width ~/ rowW; + Logic laneRow(int lane) => + microcodeRead.data.getRange(lane * rowW, (lane + 1) * rowW); + Logic rowField(Logic row, String name) { + final r = patternStruct.mapping[name]!; + return row.getRange(r.start, r.end + 1); + } + + Logic laneMatch(Logic row) { + final pm = (instr & rowField(row, 'mask')).eq(rowField(row, 'value')); + final nzf = mux( + rowField(row, 'nzfMask').neq(0), + (instr & rowField(row, 'nzfMask')).neq(0), + Const(1), + ); + final zf = mux( + rowField(row, 'zfMask').neq(0), + (instr & rowField(row, 'zfMask')).eq(0), + Const(1), + ); + return pm & nzf & zf; + } + + var selData = laneRow(lanes - 1); + for (var l = lanes - 2; l >= 0; l--) { + selData = mux( + laneMatch(laneRow(l)), + laneRow(l), + selData, + ).named('decodeSelData_$l'); + } + final pattern = Map.fromEntries( patternStruct.mapping.entries.map((entry) { final patternName = entry.key; final range = entry.value; - final value = microcodeRead.data.getRange(range.start, range.end + 1); + final value = selData.getRange(range.start, range.end + 1); return MapEntry(patternName, value); }), ); @@ -395,7 +433,9 @@ class DynamicInstructionDecoder extends InstructionDecoder { then: [microcodeRead.en < 0, done < 1, valid < 1], orElse: [ microcodeRead.en < 1, - microcodeRead.addr < _counter, + // _counter is sized for the unpacked pattern count; the packed ROM has + // ceil(patterns/lanes) words, so its address port is narrower. + microcodeRead.addr < _counter.getRange(0, microcodeRead.addr.width), If( microcodeRead.done, then: [ diff --git a/packages/river_hdl/lib/src/core/exec.dart b/packages/river_hdl/lib/src/core/exec.dart index 3d075ff..eaab3a0 100644 --- a/packages/river_hdl/lib/src/core/exec.dart +++ b/packages/river_hdl/lib/src/core/exec.dart @@ -1096,12 +1096,39 @@ abstract class ExecutionUnit extends Module { Logic causeCode, [ Logic? tval, String? suffix, + Logic? modeCause, ]) { suffix ??= ''; + // A trap op with modeCause set re-encodes its cause from the originating + // privilege/virt: ECALL becomes U/VU=8, HS=9, VS=10, M=11. Every other trap + // keeps its fixed causeCode. Centralized here so the static path, the + // microcode path, and any future privilege-dependent trap share one cause + // encoding (the switched cause also feeds delegation via + // selectTrapTargetMode below). + final effCause = (modeCause == null) + ? causeCode + : mux( + modeCause, + mux( + currentMode.eq(Const(PrivilegeMode.machine.id, width: 3)), + Const(11, width: 6), + mux( + currentMode.eq(Const(PrivilegeMode.supervisor.id, width: 3)), + mux( + virtIn ?? Const(0), + Const(10, width: 6), + Const(9, width: 6), + ), + Const(8, width: 6), + ), + ), + causeCode, + ); + if (csrRead == null || csrWrite == null) { return [ - trapCause < encodeCause(trapInterrupt, causeCode).slice(5, 0), + trapCause < encodeCause(trapInterrupt, effCause).slice(5, 0), trapTval < (tval ?? Const(0, width: mxlen.size)), output('trapEpc') < currentPc, output('trap') < 1, @@ -1114,7 +1141,7 @@ abstract class ExecutionUnit extends Module { final newMode = selectTrapTargetMode( trapInterrupt, - causeCode, + effCause, currentMode, mideleg, medeleg, @@ -1126,7 +1153,7 @@ abstract class ExecutionUnit extends Module { trapCause < encodeCause( trapInterrupt, - causeCode, + effCause, ).slice(5, 0).named('cause$suffix'), trapTval < (tval ?? Const(0, width: mxlen.size)), output('trapEpc') < currentPc, @@ -1149,7 +1176,7 @@ abstract class ExecutionUnit extends Module { stvec ?? Const(0, width: mxlen.size), ) : (mtvec ?? Const(0, width: mxlen.size))), - causeCode, + effCause, trapInterrupt, suffix: suffix, ), @@ -2434,9 +2461,15 @@ class DynamicExecutionUnit extends ExecutionUnit { ], ), CaseItem(Const(TrapMicroOp.funct, width: funct.width), [ + // The micro-op's own modeCause bit drives the switch: when + // set (ecall), rawTrap re-encodes the cause by privilege. + // No RTL heuristic on the cause value. ...rawTrap( mop['Trap']!['isInterrupt']!, mop['Trap']!['causeCode']!, + null, + null, + mop['Trap']!['modeCause']!, ), ]), CaseItem(Const(BranchIfMicroOp.funct, width: funct.width), [ @@ -4992,37 +5025,19 @@ class StaticExecutionUnit extends ExecutionUnit { ); } } else if (mop is RiscVTrapOp) { - // ECALL's cause depends on the originating privilege/virt: - // U/VU=8, HS=9, VS=10, M=11. (ebreak and the rest keep their - // fixed cause.) Harbor's microcode hardcodes 8. - final isEcall = !mop.isInterrupt && mop.causeCode == 8; - final causeCode = isEcall - ? mux( - currentMode.eq( - Const(PrivilegeMode.machine.id, width: 3), - ), - Const(11, width: 6), - mux( - currentMode.eq( - Const(PrivilegeMode.supervisor.id, width: 3), - ), - mux( - virtIn ?? Const(0), - Const(10, width: 6), - Const(9, width: 6), - ), - Const(8, width: 6), - ), - ) - : Const(mop.causeCode, width: 6); + // The micro-op's modeCause bit decides: ecall re-encodes its + // cause by privilege (U/VU=8, HS=9, VS=10, M=11); ebreak and + // the rest keep their fixed cause. Same switch the microcode + // path uses, driven by the same flag. steps.add( CaseItem( Const(i, width: maxLen.bitLength), rawTrap( Const(mop.isInterrupt ? 1 : 0), - causeCode, + Const(mop.causeCode, width: 6), null, '_${op.mnemonic}', + Const(mop.modeCause ? 1 : 0), ), ), ); diff --git a/packages/river_hdl/lib/src/core/fu_branch.dart b/packages/river_hdl/lib/src/core/fu_branch.dart index 08c7224..0588665 100644 --- a/packages/river_hdl/lib/src/core/fu_branch.dart +++ b/packages/river_hdl/lib/src/core/fu_branch.dart @@ -36,6 +36,7 @@ class BranchUnit extends Module { required Logic issueCondition, required Logic issueIsJump, required Logic issueIsJalr, + required Logic issueIsCompressed, required Logic issuePredictedTaken, required Logic flush, this.xlen = 64, @@ -63,6 +64,10 @@ class BranchUnit extends Module { /// Whether this is JALR (target = rs1 + imm, not pc + imm). issueIsJalr = addInput('issue_is_jalr', issueIsJalr); + /// Whether the branch/jump instruction was a 2-byte compressed (RVC) op, so + /// the link (return) address is PC+2 rather than PC+4. + issueIsCompressed = addInput('issue_is_compressed', issueIsCompressed); + /// Predicted taken (from front-end, for detecting mispredictions). issuePredictedTaken = addInput( 'issue_predicted_taken', @@ -125,8 +130,18 @@ class BranchUnit extends Module { issuePc + issueImm, ).named('branch_target'); - // Next sequential PC (for not-taken branches and link address) - final nextPc = (issuePc + Const(4, width: xlen)).named('next_pc'); + // Next sequential PC (for not-taken branches and link address). The link + // (rd = return address) must be PC + instruction length: 2 for a compressed + // call (c.jalr), 4 for full-width. A fixed +4 returned two bytes past a + // c.jalr, corrupting every function-pointer/vtable call. + final nextPc = + (issuePc + + mux( + issueIsCompressed, + Const(2, width: xlen), + Const(4, width: xlen), + )) + .named('next_pc'); // Actual taken: unconditional jumps are always taken final actualTaken = (issueIsJump | branchTaken).named('actual_taken'); diff --git a/packages/river_hdl/lib/src/core/issue.dart b/packages/river_hdl/lib/src/core/issue.dart index 891937e..04abc22 100644 --- a/packages/river_hdl/lib/src/core/issue.dart +++ b/packages/river_hdl/lib/src/core/issue.dart @@ -129,6 +129,8 @@ class IssueQueue extends Module { Logic get dispatchBranchCondition => output('dispatch_branch_condition'); Logic get dispatchBranchIsJump => output('dispatch_branch_is_jump'); Logic get dispatchBranchIsJalr => output('dispatch_branch_is_jalr'); + Logic get dispatchBranchIsCompressed => + output('dispatch_branch_is_compressed'); /// CSR dispatch. Logic get dispatchCsrValid => output('dispatch_csr_valid'); @@ -157,6 +159,7 @@ class IssueQueue extends Module { required Logic enqBranchCond0, required Logic enqIsJump0, required Logic enqIsJalr0, + required Logic enqIsCompressed0, required Logic enqUseImm0, required Logic enqCsrOp0, required Logic enqCsrAddr0, @@ -177,6 +180,7 @@ class IssueQueue extends Module { required Logic enqBranchCond1, required Logic enqIsJump1, required Logic enqIsJalr1, + required Logic enqIsCompressed1, required Logic enqUseImm1, required Logic enqCsrOp1, required Logic enqCsrAddr1, @@ -245,6 +249,7 @@ class IssueQueue extends Module { enqBranchCond0 = addInput('enq_branch_cond_0', enqBranchCond0, width: 3); enqIsJump0 = addInput('enq_is_jump_0', enqIsJump0); enqIsJalr0 = addInput('enq_is_jalr_0', enqIsJalr0); + enqIsCompressed0 = addInput('enq_is_compressed_0', enqIsCompressed0); enqUseImm0 = addInput('enq_use_imm_0', enqUseImm0); enqCsrOp0 = addInput('enq_csr_op_0', enqCsrOp0, width: 3); enqCsrAddr0 = addInput('enq_csr_addr_0', enqCsrAddr0, width: 12); @@ -266,6 +271,7 @@ class IssueQueue extends Module { enqBranchCond1 = addInput('enq_branch_cond_1', enqBranchCond1, width: 3); enqIsJump1 = addInput('enq_is_jump_1', enqIsJump1); enqIsJalr1 = addInput('enq_is_jalr_1', enqIsJalr1); + enqIsCompressed1 = addInput('enq_is_compressed_1', enqIsCompressed1); enqUseImm1 = addInput('enq_use_imm_1', enqUseImm1); enqCsrOp1 = addInput('enq_csr_op_1', enqCsrOp1, width: 3); enqCsrAddr1 = addInput('enq_csr_addr_1', enqCsrAddr1, width: 12); @@ -357,6 +363,7 @@ class IssueQueue extends Module { addOutput('dispatch_branch_condition', width: 3); addOutput('dispatch_branch_is_jump'); addOutput('dispatch_branch_is_jalr'); + addOutput('dispatch_branch_is_compressed'); // Dispatch outputs, CSR addOutput('dispatch_csr_valid'); @@ -434,6 +441,10 @@ class IssueQueue extends Module { depth, (i) => Logic(name: 'iq_isjalr_$i'), ); + final entryIsCompressed = List.generate( + depth, + (i) => Logic(name: 'iq_iscompressed_$i'), + ); final entryUseImm = List.generate( depth, (i) => Logic(name: 'iq_useimm_$i'), @@ -800,6 +811,8 @@ class IssueQueue extends Module { muxField(entryBranchCond, dispBranchIdx); output('dispatch_branch_is_jump') <= muxField(entryIsJump, dispBranchIdx); output('dispatch_branch_is_jalr') <= muxField(entryIsJalr, dispBranchIdx); + output('dispatch_branch_is_compressed') <= + muxField(entryIsCompressed, dispBranchIdx); // Drive CSR dispatch outputs dispatchCsrValid <= dispCsrFound; @@ -843,6 +856,7 @@ class IssueQueue extends Module { ...List.generate(depth, (i) => entryBranchCond[i] < 0), ...List.generate(depth, (i) => entryIsJump[i] < 0), ...List.generate(depth, (i) => entryIsJalr[i] < 0), + ...List.generate(depth, (i) => entryIsCompressed[i] < 0), ...List.generate(depth, (i) => entryUseImm[i] < 0), ...List.generate(depth, (i) => entryCsrOp[i] < 0), ...List.generate(depth, (i) => entryCsrAddr[i] < 0), @@ -919,6 +933,7 @@ class IssueQueue extends Module { entryBranchCond[i] < enqBranchCond0, entryIsJump[i] < enqIsJump0, entryIsJalr[i] < enqIsJalr0, + entryIsCompressed[i] < enqIsCompressed0, entryUseImm[i] < enqUseImm0, entryCsrOp[i] < enqCsrOp0, entryCsrAddr[i] < enqCsrAddr0, @@ -953,6 +968,7 @@ class IssueQueue extends Module { entryBranchCond[i] < enqBranchCond1, entryIsJump[i] < enqIsJump1, entryIsJalr[i] < enqIsJalr1, + entryIsCompressed[i] < enqIsCompressed1, entryUseImm[i] < enqUseImm1, entryCsrOp[i] < enqCsrOp1, entryCsrAddr[i] < enqCsrAddr1, diff --git a/packages/river_hdl/lib/src/core/pipeline.dart b/packages/river_hdl/lib/src/core/pipeline.dart index a3b467f..060747a 100644 --- a/packages/river_hdl/lib/src/core/pipeline.dart +++ b/packages/river_hdl/lib/src/core/pipeline.dart @@ -706,6 +706,7 @@ class RiverPipeline extends Module { kBranchCond, kIsJump, kIsJalr, + kIsCompressed, kUseImm, kSignExtend, if (dualDispatch) ...[ @@ -729,6 +730,7 @@ class RiverPipeline extends Module { kBranchCond1, kIsJump1, kIsJalr1, + kIsCompressed1, kUseImm1, kSignExtend1, ], @@ -792,6 +794,10 @@ class RiverPipeline extends Module { decodeNode[kBranchCond] <= ctrlRom.branchCond; decodeNode[kIsJump] <= ctrlRom.isJump; decodeNode[kIsJalr] <= ctrlRom.isJalr; + // Compressed-ness rides alongside the instruction (kInstruction <= + // fetchOutResult = cfb.instr0), so the branch unit can form the link as + // PC+2. Non-compressed fetchers carry no C-ext ops, so 0 is correct there. + decodeNode[kIsCompressed] <= (cfb?.compressed0 ?? Const(0)); decodeNode[kUseImm] <= ctrlRom.useImm; decodeNode[kSignExtend] <= ~ctrlRom.memUnsigned; decodeNode.valid <= decodeDone & decodeValid; @@ -819,6 +825,7 @@ class RiverPipeline extends Module { decodeNode[kBranchCond1] <= ctrlRom1.branchCond; decodeNode[kIsJump1] <= ctrlRom1.isJump; decodeNode[kIsJalr1] <= ctrlRom1.isJalr; + decodeNode[kIsCompressed1] <= cfb.compressed1; decodeNode[kUseImm1] <= ctrlRom1.useImm; decodeNode[kSignExtend1] <= ~ctrlRom1.memUnsigned; } @@ -1308,6 +1315,7 @@ class RiverPipeline extends Module { enqBranchCond0: renameNode[kBranchCond], enqIsJump0: renameNode[kIsJump], enqIsJalr0: renameNode[kIsJalr], + enqIsCompressed0: renameNode[kIsCompressed], enqUseImm0: renameNode[kUseImm], // CSR op = funct3 (instr[14:12]); CSR address = instr[31:20]. The // CsrUnit maps funct3 → read/set/clear (+ immediate variants). @@ -1344,6 +1352,7 @@ class RiverPipeline extends Module { : Const(0, width: 3), enqIsJump1: dualDispatch ? renameNode[kIsJump1] : Const(0), enqIsJalr1: dualDispatch ? renameNode[kIsJalr1] : Const(0), + enqIsCompressed1: dualDispatch ? renameNode[kIsCompressed1] : Const(0), enqUseImm1: dualDispatch ? renameNode[kUseImm1] : Const(0), enqCsrOp1: dualDispatch ? fitWidth(renameNode[kInstruction1], 32).slice(14, 12) @@ -1628,6 +1637,7 @@ class RiverPipeline extends Module { issueCondition: iq.dispatchBranchCondition, issueIsJump: iq.dispatchBranchIsJump, issueIsJalr: iq.dispatchBranchIsJalr, + issueIsCompressed: iq.dispatchBranchIsCompressed, issuePredictedTaken: branchUnitPredTaken, flush: flushOrRedirect, xlen: mxlen.size, diff --git a/packages/river_hdl/lib/src/core/stages.dart b/packages/river_hdl/lib/src/core/stages.dart index e1ef871..1dd6a28 100644 --- a/packages/river_hdl/lib/src/core/stages.dart +++ b/packages/river_hdl/lib/src/core/stages.dart @@ -135,6 +135,10 @@ const kIsJump = HarborPayload('IS_JUMP'); /// Register-indirect jump target (jalr). const kIsJalr = HarborPayload('IS_JALR'); +/// The fetched instruction was a 2-byte compressed (RVC) op. Needed so the +/// branch unit forms the JAL/JALR link as PC+2, not PC+4. +const kIsCompressed = HarborPayload('IS_COMPRESSED'); + /// ALU second operand is the immediate (I-type). const kUseImm = HarborPayload('USE_IMM'); @@ -189,6 +193,7 @@ const kAluFunct1 = HarborPayload('ALU_FUNCT_1', width: 7); const kBranchCond1 = HarborPayload('BRANCH_COND_1', width: 3); const kIsJump1 = HarborPayload('IS_JUMP_1'); const kIsJalr1 = HarborPayload('IS_JALR_1'); +const kIsCompressed1 = HarborPayload('IS_COMPRESSED_1'); const kUseImm1 = HarborPayload('USE_IMM_1'); const kSignExtend1 = HarborPayload('SIGN_EXTEND_1'); diff --git a/packages/river_hdl/lib/src/genip.dart b/packages/river_hdl/lib/src/genip.dart index d806a10..e36b0e5 100644 --- a/packages/river_hdl/lib/src/genip.dart +++ b/packages/river_hdl/lib/src/genip.dart @@ -36,6 +36,11 @@ class DeviceParams { /// Expose the CPU read-training MMIO window (HarborDdrController.trainableRead). final bool? trainable; + /// ddr3v2 training mode: `train=runtime` exposes the knob-ABI window so the + /// FSBL sweep engine drives calibration; `train=hw` (default) keeps the + /// controller's internal cal FSM. Distinct from the legacy [trainable]. + final bool? runtimeTrain; + /// ddr3Fast command CK edge (0..3), forwarded to the Xilinx PHY. final int? cmdSlot; @@ -60,6 +65,21 @@ class DeviceParams { /// Hardware write-verify-retry on every array write. final bool? writeVerify; + /// Hardware MPR read-calibration: the sequencer sweeps each byte lane's read + /// window x IDELAY tap against the DRAM MPR pattern and locks the eye before + /// the bus opens (ddr3Fast only). Kills the per-boot read coin-flip. + final bool? readLevel; + + /// Post-read-cal cadence self-test: bus opens only after a varied-cadence read + /// verify passes (ddr3Fast + readlevel only). Kills the read cadence coin-flip. + final bool? selfTest; + + /// Expose the DDR controller's dbg_la LA-probe bundle ([3:0]=stateCode, + /// [4]=rd_cal_active, [5]=phy.rdValid) to top pins so a logic analyzer can + /// watch the sequencer FSM + read completion live (bus-independent). The 6 + /// dbg_la[*] pins must be assigned via --pin. + final bool? laprobe; + /// Diagnostic MR3.MPR mode (every read returns the part MPR pattern). final bool? mpr; @@ -67,6 +87,25 @@ class DeviceParams { /// Per controller: two dram devices may differ. final bool? ddr3Fast; + /// Use the new silicon-proven [HarborDdr3] stack (Ddr3Controller + Ddr3Phy, + /// with proper calibration + bank anticipate) instead of the legacy + /// HarborDdrController + DdrSequencer/DdrPhyXilinx. Implies ddr3Fast. + final bool? ddr3v2; + + /// Open the ddr3Fast read window from the DRAM's read strobe (DQS as data) so + /// each read self-frames, instead of a fixed CL tap that mis-frames under the + /// i+d cadence. ddr3Fast only. + final bool? dqsGate; + + /// LiteDRAM read margin: extra CK added to the programmed DRAM CAS latency + /// (MR0) so the read burst lands one memory-clock phase INTO the ISERDESE2 + /// word, not at the CLKDIV edge where setup/hold is marginal and the i+d read + /// cadence tips it over. LiteDRAM s7ddrphy notes "Artix-7 requires read data + /// one memory-clock phase into the ISERDESE2 word for reliable read leveling" + /// (originally CL+1 in MR0). The DRAM drives read data [readClExtra] CK later, + /// clTicks/MR0 track it, and the FSBL read sweep re-centres. ddr3Fast only. + final int? readClExtra; + /// DRAM clock-domain (CDC) frequency in Hz. Above the oscillator PLLs the /// `ddr` domain to a higher DLL-off / DLL-on rate than the core drives. final int? clockFreq; @@ -91,6 +130,7 @@ class DeviceParams { const DeviceParams({ this.trainable, + this.runtimeTrain, this.cmdSlot, this.wrShift, this.wrBeat, @@ -99,8 +139,14 @@ class DeviceParams { this.readRetry, this.window, this.writeVerify, + this.readLevel, + this.selfTest, + this.laprobe, this.mpr, this.ddr3Fast, + this.ddr3v2, + this.dqsGate, + this.readClExtra, this.clockFreq, this.oscFreq, this.mode, @@ -111,6 +157,7 @@ class DeviceParams { /// Accepted param keys (case-insensitive), for error messages. static const _keys = [ 'trainable', + 'train', 'cmdslot', 'wrshift', 'wrbeat', @@ -119,8 +166,14 @@ class DeviceParams { 'readretry', 'window', 'writeverify', + 'readlevel', + 'selftest', + 'laprobe', 'mpr', 'ddr3fast', + 'ddr3v2', + 'dqsgate', + 'readclextra', 'clockfreq', 'oscfreq', 'mode', @@ -149,6 +202,7 @@ class DeviceParams { /// contain `=`. static DeviceParams parse(String s) { bool? trainable; + bool? runtimeTrain; int? cmdSlot; int? wrShift; int? wrBeat; @@ -157,8 +211,14 @@ class DeviceParams { int? readRetry; int? window; bool? writeVerify; + bool? readLevel; + bool? selfTest; + bool? laprobe; bool? mpr; bool? ddr3Fast; + bool? ddr3v2; + bool? dqsGate; + int? readClExtra; int? clockFreq; int? oscFreq; String? mode; @@ -174,6 +234,11 @@ class DeviceParams { switch (key) { case 'trainable': trainable = _parseBool(val); + case 'train': + if (val != 'hw' && val != 'runtime') { + throw FormatException('train must be hw|runtime, got: $val'); + } + runtimeTrain = val == 'runtime'; case 'cmdslot': cmdSlot = int.parse(val); case 'wrshift': @@ -190,10 +255,22 @@ class DeviceParams { window = int.parse(val); case 'writeverify': writeVerify = _parseBool(val); + case 'readlevel': + readLevel = _parseBool(val); + case 'selftest': + selfTest = _parseBool(val); + case 'laprobe': + laprobe = _parseBool(val); case 'mpr': mpr = _parseBool(val); case 'ddr3fast': ddr3Fast = _parseBool(val); + case 'ddr3v2': + ddr3v2 = _parseBool(val); + case 'dqsgate': + dqsGate = _parseBool(val); + case 'readclextra': + readClExtra = int.parse(val); case 'clockfreq': clockFreq = int.parse(val); case 'oscfreq': @@ -217,6 +294,7 @@ class DeviceParams { } return DeviceParams( trainable: trainable, + runtimeTrain: runtimeTrain, cmdSlot: cmdSlot, wrShift: wrShift, wrBeat: wrBeat, @@ -225,8 +303,14 @@ class DeviceParams { readRetry: readRetry, window: window, writeVerify: writeVerify, + readLevel: readLevel, + selfTest: selfTest, + laprobe: laprobe, mpr: mpr, ddr3Fast: ddr3Fast, + ddr3v2: ddr3v2, + dqsGate: dqsGate, + readClExtra: readClExtra, clockFreq: clockFreq, oscFreq: oscFreq, mode: mode, @@ -239,6 +323,44 @@ class DeviceParams { /// Deprecated alias, retained while [MemoryRegion.ddrParams] still uses this name. typedef DdrRegionParams = DeviceParams; +/// Target-aware flash partition layout. Pure, so the DT `fixed-partitions` node +/// and xipboot's jump target computed from it stay in agreement across call +/// sites. An FPGA reserves slot 0 for the config bitstream (master-SPI self-boot +/// from flash); an ASIC has no fabric to configure, so the FSBL is the reset +/// payload at offset 0. +({int fsblOffset, int firmwareOffset, List partitions}) +flashLayout(Object? target, int flashSize) { + // The uncompressed 7-series bitstream size is FIXED per device (the full + // configuration memory), design-independent. Round the reserved slot up to + // 1 MiB so the FSBL clears it. + const bitstreamBytes = {'xc7s50': 2192012}; + final isFpga = target is HarborFpgaTarget; + final bitSlot = isFpga + ? (((bitstreamBytes[target.device] ?? 0x300000) + 0xfffff) & ~0xfffff) + : 0; + final fsblOffset = bitSlot; + const fsblSize = 0x100000; // 1 MiB, generous for the XIP FSBL + final firmwareOffset = fsblOffset + fsblSize; + return ( + fsblOffset: fsblOffset, + firmwareOffset: firmwareOffset, + partitions: [ + if (isFpga) + HarborFlashPartition(label: 'fpga-bitstream', offset: 0, size: bitSlot), + HarborFlashPartition( + label: 'river-fsbl', + offset: fsblOffset, + size: fsblSize, + ), + HarborFlashPartition( + label: 'river-firmware', + offset: firmwareOffset, + size: flashSize - firmwareOffset, + ), + ], + ); +} + class MemoryRegion { final int address; final int size; @@ -918,8 +1040,11 @@ class RiverGenIpConfig { // old global flags. /// True when ANY `dram` device selects the ISERDESE2 datapath. - bool get ddr3Fast => - devices.any((d) => d.type == 'dram' && (d.params?.ddr3Fast ?? false)); + bool get ddr3Fast => devices.any( + (d) => + d.type == 'dram' && + ((d.params?.ddr3Fast ?? false) || (d.params?.ddr3v2 ?? false)), + ); /// DRAM clock-domain (CDC) frequency: the first `dram` device that sets one. int? get ddrClockFrequency { @@ -1426,6 +1551,48 @@ class RiverGenIpConfig { ddr3Cwl = tCkPs >= 2500 ? 5 : (tCkPs >= 1875 ? 6 : 7); } final ddr3CwlEff = ddr3Cwl; + // --- new silicon-proven Ddr3Controller stack (ddr3v2) --- + if (mem.ddrParams?.ddr3v2 ?? false) { + final ddr = HarborDdr3( + config: board.config, + baseAddress: mem.address, + clockHz: ctrlHz, + busAddressWidth: busConfig.addressWidth, + busDataWidth: busConfig.dataWidth, + target: target, + // Match the DDR3 CK the tree actually solves (set clockfreq= + // 300000000 on the device for the proven 300 MHz x16 point). + ckPeriodPs: (1e6 / tree.ddrCkMhz).round(), + // train=runtime exposes the knob-ABI window for the FSBL engine. + runtimeTrainable: mem.ddrParams?.runtimeTrain ?? false, + name: '${mem.type}_$i', + ); + soc.addPeripheral(ddr); + if (mem.ddrParams?.runtimeTrain ?? false) { + // The knob-ABI window is a second bus slave carved from the top page + // of the DRAM aperture (usableSize excludes it). Map it explicitly. + soc.addPeripheralSlave( + ddr, + 'train', + BusAddressRange(ddr.trainBase, HarborDdr3.trainWindowSize), + ); + } + ddr.input('ddr_clk').srcConnection! <= tree.controller; + final sysDomainForDdr = soc.clockDomain('sys'); + if (sysDomainForDdr == null) { + throw StateError('ddr3v2 needs the sys clock domain for ddr_reset'); + } + ddr.input('ddr_reset').srcConnection! <= sysDomainForDdr.reset; + ddr.input('ddr_ck_fast').srcConnection! <= tree.ddrCk; + ddr.input('ddr_ck90_fast').srcConnection! <= tree.ddrCk90; + ddr.input('ddr_ck_dqs_fast').srcConnection! <= tree.ddrCkDqs; + ddr.input('ddr_idelay_ref').srcConnection! <= tree.idelayRef; + final padPorts = [...DdrBoard.padPorts, 'sdram_dqs_n']; + for (final pad in padPorts) { + soc.exposePin(ddr, pad, externalName: pad); + } + continue; + } final ddr = HarborDdrController( config: board.config, baseAddress: mem.address, @@ -1437,9 +1604,15 @@ class RiverGenIpConfig { asyncClock: true, // The real-speed ISERDESE2 DW8 read gearbox. ddr3Fast: true, + // Open the read window from the DRAM's read strobe (DQS as data) so each + // read self-frames instead of a fixed CL tap (region `dqsgate` param). + dqsGatedRead: mem.ddrParams?.dqsGate ?? false, ddr3FastCkMhz: tree.ddrCkMhz, ddr3FastIdelayRefMhz: tree.idelayRefMhz, - ddr3FastCl: ddr3Cl, + // LiteDRAM one-memory-clock-phase read margin: the DRAM drives read + // data readClExtra CK later so the burst lands INTO the ISERDESE2 word + // (not at the marginal CLKDIV edge). CWL/writes untouched. + ddr3FastCl: ddr3Cl + (mem.ddrParams?.readClExtra ?? 0), ddr3FastCwl: ddr3CwlEff, // ddr3Fast write/command timing. Effective value = region param, else // board default, else the global default. cmdSlot/wrShift/window fall @@ -1475,6 +1648,27 @@ class RiverGenIpConfig { // DdrBoard so a plain build is correct with no extra flag. A region // param can still force it on/off. writeVerify: mem.ddrParams?.writeVerify ?? board.writeVerify, + // Hardware MPR read-calibration before the bus opens (openXC7 ddr3Fast + // read eye drifts per boot). Board default, region param can override. + readLevel: mem.ddrParams?.readLevel ?? board.readLevel, + selfTest: mem.ddrParams?.selfTest ?? board.selfTest, + // Write-leveling / write-DQS actuator on the ddr3Fast (Xilinx) path. + // This was MISSING from this controller call (only the legacy/ECP5 call + // below had it), so writeLevel silently defaulted to false and the whole + // Xilinx WL + write-beat block never built. Same DLL-on-band gate as the + // ECP5 call. Needed for the runtime write-beat override at rated CK. + writeLevel: + (ddrClockFrequency != null && + ddrClockFrequency! > oscFrequency) && + (mem.ddrParams?.trainable ?? + board.trainable ?? + const { + 'ddrtest', + 'ddrprobe', + 'ddrlevel', + 'ddreye', + 'ddrdiag', + }.contains(bootProgram)), name: '${mem.type}_$i', ); soc.addPeripheral(ddr); @@ -1630,19 +1824,32 @@ class RiverGenIpConfig { for (final pad in padPorts) { soc.exposePin(ddr, pad, externalName: pad); } + // LA probe: expose the DDR controller's 6-bit dbg_la bundle + // ([3:0]=stateCode [4]=rd_cal_active [5]=phy.rdValid) as top pins so a + // logic analyzer can watch the read-cal FSM + read completion live + // (bus-independent). Gated on the dram `laprobe=true` param; the + // dbg_la[*] pads come from --pin. + if (mem.ddrParams?.laprobe ?? false) { + soc.exposePin(ddr, 'dbg_la', externalName: 'dbg_la'); + } } else if (mem.type == 'flash') { // Real SPI NOR flash with XIP: the CPU fetches firmware directly from the // part, no on-chip copy. 16MB maps to the W25Q128 (the OrangeCrab/ // iCEBreaker part). Other sizes get a generic quad-read config sized to // the region. + // Target-aware partition map (fpga-bitstream on FPGA + river-fsbl + + // river-firmware): the FSBL reads its firmware offset from this and + // Linux exposes each as /dev/mtdN. + final flashParts = flashLayout(target, mem.size).partitions; final spiConfig = mem.size == 16 * 1024 * 1024 - ? const HarborSpiFlashConfig.w25q128() + ? HarborSpiFlashConfig.w25q128(partitions: flashParts) : HarborSpiFlashConfig( size: mem.size, mode: HarborSpiFlashMode.quad, readCommand: 0x6B, addressBytes: mem.size > 16 * 1024 * 1024 ? 4 : 3, dummyCycles: 8, + partitions: flashParts, ); // The config-flash clock has no I/O pad on either family: route it // through the ECP5 USRMCLK macro or the Xilinx STARTUPE2 (USRCCLKO -> @@ -2429,13 +2636,20 @@ class RiverGenIpConfig { // from the BRAM boot ROM (reset vector), warm up the flash XIP controller // (the Xilinx STARTUPE2/CCLK path is not fetch-ready at the first // cold-reset cycle), then jump to the FSBL executing IN PLACE from flash. - // The FSBL is flashed at the flash region base (QSPI 0, free because the - // bitstream is JTAG-loaded). Main Weir sits above it and the FSBL copies - // it into DRAM. + // The FSBL lives at the `river-fsbl` partition offset: on an FPGA that is + // ABOVE the config bitstream (slot 0 holds the bitstream for master-SPI + // self-boot), on an ASIC it is flash base. Main Weir sits above the FSBL + // and the FSBL copies it into DRAM. Same layout the DT partitions carry. final flash = flashRegion; if (flash == null) { throw StateError('xipboot boot program needs a flash region'); } + // Use the HARBOR target (buildTarget()), not the genip `target` field: + // flashLayout keys FPGA-vs-ASIC on `is HarborFpgaTarget`, and the field + // is genip's own Target type, so it would wrongly fall to the ASIC + // offset 0 and xipboot would jump into the bitstream slot instead of the + // relocated FSBL. Matches the flash/DT site (buildSoC uses buildTarget()). + final fsblOffset = flashLayout(buildTarget(), flash.size).fsblOffset; final stackMem = memories.firstWhere( (m) => m.type != 'flash', orElse: () => flash, @@ -2444,8 +2658,8 @@ class RiverGenIpConfig { RiverMaskromConfig( isa: coreConfig.isa, resetVector: coreConfig.resetVector, - flashSource: flash.address, - copyDest: flash.address, // jump target = FSBL entry (flash base) + flashSource: flash.address + fsblOffset, + copyDest: flash.address + fsblOffset, // jump target = FSBL entry copySize: 256, // warmup read window stackTop: stackMem.address + stackMem.size, bootMode: RiverBootMode.xipLaunch, diff --git a/packages/river_hdl/test/branch/cjalr_link_test.dart b/packages/river_hdl/test/branch/cjalr_link_test.dart new file mode 100644 index 0000000..7e69d52 --- /dev/null +++ b/packages/river_hdl/test/branch/cjalr_link_test.dart @@ -0,0 +1,109 @@ +import 'package:river/river.dart'; +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import '../core_harness.dart'; + +/// Regression for the rc1-s (creek microcode) core computing the JAL/JALR LINK +/// address from a fixed +4 instead of the actual instruction length. On RV64 +/// the only compressed call is `c.jalr` (there is no `c.jal`), so a +4 link is +/// invisible to direct-call-heavy code and only bites function-pointer/vtable +/// dispatch: the callee returns two bytes too far, into the middle of the next +/// instruction. This is exactly the trap storm Ferrite hits on its first +/// `std.Io.Writer` `drain` call (sepc = call+4, not call+2). +/// +/// `c.jalr rs1` must set ra (x1) = PC + 2 (the address of the instruction right +/// after the 2-byte `c.jalr`), and jump to rs1. A full-width `jalr`/`jal` must +/// set ra = PC + 4. The two lengths share the microcode link path; this pins +/// the compressed case with an ABSOLUTE expectation (not an emulator-golden +/// compare, which would pass if the emulator shares the bug). +HarborMmuConfig _mmu() => HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare, RiscVPagingMode.sv39], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + hasSupervisorUserMemory: true, + hasMakeExecutableReadable: true, +); + +const _clk = HarborClockConfig( + name: 'test', + rate: HarborFixedClockRate(12000000), +); + +RiverCoreConfig _rc1s() => RiverCoreConfigV1.small( + mmu: _mmu(), + interrupts: [], + clock: _clk, + resetVector: 0, +); + +// rc1-ma: the OoO/dual-issue macro tier. fu_branch.dart hardcodes the link as +// PC+4 too, so this is expected to share the c.jalr link bug (follow-up). +RiverCoreConfig _rc1ma() => + RiverCoreConfigV1.macro(mmu: _mmu(), interrupts: [], clock: _clk); + +String _memString(List words) { + final sb = StringBuffer('@0\n'); + for (final w in words) { + for (var b = 0; b < 4; b++) { + sb.write(((w >> (b * 8)) & 0xFF).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + } + return '${sb.toString().trimRight()}\n'; +} + +// Program (RV64, reset vector 0). c.jalr sits at 0x8 (2 bytes), so a correct +// link is ra = 0xa; a +4-length bug yields ra = 0xc. The jump TARGET (0x10) is +// correct either way, so the program terminates at nextPc = 0x14 regardless and +// we read ra back to judge the link. +// 0: auipc x5, 0x0 x5 = 0 0x00000297 +// 4: addi x5, x5, 0x10 x5 = 0x10 (target) 0x01028293 +// 8: c.jalr x5 ra = 0xa; pc = 0x10 0x9282 (low half @8) +// a: c.nop 0x0001 (high half @a) +// c: nop (jumped over) 0x00000013 +// 10: addi x6, x0, 0x99 x6 = 0x99 (target ran) 0x09900313 +// 14: nop (halt here) 0x00000013 +const _program = [ + 0x00000297, + 0x01028293, + 0x00019282, // low: c.jalr x5 @0x8 ; high: c.nop @0xa + 0x00000013, + 0x09900313, + 0x00000013, +]; + +Future _runLinkTest(RiverCoreConfig config) async { + await Simulator.reset(); + await coreTest( + _memString(_program), + { + // x6 proves the jump target (0x10) was reached at all. + Register.x6: 0x99, + // The claim under test: ra = address after the 2-byte c.jalr = 0xa. + // With the +4 link bug this is 0xc and the test fails. + Register.x1: 0xa, + }, + config, + nextPc: 0x14, + ); +} + +void main() { + test( + 'c.jalr link address is PC+2, not PC+4 (rc1-s microcode)', + () async { + await _runLinkTest(_rc1s()); + }, + timeout: Timeout(Duration(minutes: 5)), + ); + + test( + 'c.jalr link address is PC+2, not PC+4 (rc1-ma OoO)', + () async { + await _runLinkTest(_rc1ma()); + }, + timeout: Timeout(Duration(minutes: 5)), + ); +} diff --git a/packages/river_hdl/test/core/issue_queue_count_test.dart b/packages/river_hdl/test/core/issue_queue_count_test.dart index 127ab92..67b821a 100644 --- a/packages/river_hdl/test/core/issue_queue_count_test.dart +++ b/packages/river_hdl/test/core/issue_queue_count_test.dart @@ -41,6 +41,7 @@ void main() { enqBranchCond0: z(3), enqIsJump0: Const(0), enqIsJalr0: Const(0), + enqIsCompressed0: Const(0), enqUseImm0: Const(1), enqCsrOp0: z(3), enqCsrAddr0: z(12), @@ -61,6 +62,7 @@ void main() { enqBranchCond1: z(3), enqIsJump1: Const(0), enqIsJalr1: Const(0), + enqIsCompressed1: Const(0), enqUseImm1: Const(0), enqCsrOp1: z(3), enqCsrAddr1: z(12), diff --git a/packages/river_hdl/test/csr/csrs_sie_smode_test.dart b/packages/river_hdl/test/csr/csrs_sie_smode_test.dart new file mode 100644 index 0000000..21d37db --- /dev/null +++ b/packages/river_hdl/test/csr/csrs_sie_smode_test.dart @@ -0,0 +1,184 @@ +import 'package:river/river.dart'; +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import '../core_harness.dart'; + +/// rc1-s (creek microcode) privilege-return regression. +/// +/// On real hardware Ferrite's `csrs sie` (timer_smode.zig:32) traps ILLEGAL, +/// and a Weir M-mode diagnostic proved the core was in U-MODE (mstatus.MPP=0) +/// at that instruction even though Weir dropped to S-mode (MPP=S) and Ferrite's +/// earlier stvec write ran in S. So a privilege RETURN between there landed in +/// U instead of the saved MPP. The prime suspect is the SBI ecall-return `mret` +/// that immediately precedes the timer's `csrs sie`. Linux does constant +/// U/S/M transitions and cannot boot on a broken privilege return. +RiverCoreConfig _rc1s() => RiverCoreConfigV1.small( + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare, RiscVPagingMode.sv39], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + hasSupervisorUserMemory: true, + hasMakeExecutableReadable: true, + ), + interrupts: [], + clock: const HarborClockConfig( + name: 'test', + rate: HarborFixedClockRate(12000000), + ), + resetVector: 0, +); + +// Emit ONE contiguous block from @0, gaps filled with nop. A per-word `@addr` +// form makes SparseMemoryStorage take sub-8-byte writes that mis-pack a word +// holding zero bytes (e.g. ecall 0x00000073), so a zero-heavy instruction reads +// back corrupted. A single contiguous load never triggers that. +String _memString(Map words) { + const nop = 0x00000013; + final maxAddr = words.keys.reduce((a, b) => a > b ? a : b); + final sb = StringBuffer('@0\n'); + for (var addr = 0; addr <= maxAddr + 4; addr += 4) { + final w = words[addr] ?? nop; + for (var b = 0; b < 4; b++) { + sb.write(((w >> (b * 8)) & 0xFF).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + } + return sb.toString(); +} + +void main() { + test( + 'csrs sie is legal from S-mode (rc1-s microcode)', + () async { + await Simulator.reset(); + // Drop to S (MPP=S) then csrs sie: must NOT trap (x7 reaches 0x99). + final program = { + 0x00: 0x34151073, // csrw mepc, x10 + 0x04: 0x30059073, // csrw mstatus, x11 + 0x08: 0x30200073, // mret + 0x40: 0x10462073, // csrs sie, x12 (S-mode) + 0x44: 0x09900393, // addi x7, x0, 0x99 + 0x48: 0x00000013, // nop + }; + await coreTest( + _memString(program), + {Register.x7: 0x99}, + _rc1s(), + initRegisters: { + Register.x10: 0x40, + Register.x11: 0x800, + Register.x12: 0x20, + }, + nextPc: 0x4c, + ); + }, + timeout: Timeout(Duration(minutes: 5)), + ); + + test( + 'ecall from S-mode reaches the M handler with correct mepc/mcause', + () async { + await Simulator.reset(); + // Isolate the TRAP-ENTRY: drop to S (mret), ecall to M, and HALT in the M + // handler (no return mret, so no divergence). Read what the ecall trap set. + // 0x00 csrw mtvec, x13 0x30569073 (x13=0x80) + // 0x04 csrw mepc, x10 0x34151073 (x10=0x40) + // 0x08 csrw mstatus,x11 0x30059073 (x11=0x800 MPP=S) + // 0x0c mret 0x30200073 -> S at 0x40 + // 0x40 ecall 0x00000073 -> M at 0x80 + // 0x80 addi x8,x0,0x11 0x01100413 (handler ran) + // 0x84 csrr x15, mepc 0x341027f3 (should be 0x40 = the ecall PC) + // 0x88 csrr x16, mcause 0x34202873 (should be 9 = ecall-from-S) + // 0x8c nop (halt) 0x00000013 + final entryProgram = { + 0x00: 0x30569073, + 0x04: 0x34151073, + 0x08: 0x30059073, + 0x0c: 0x30200073, + 0x40: 0x00000073, + 0x44: 0x00000013, + 0x80: 0x01100413, + 0x84: 0x341027f3, + 0x88: 0x34202873, + 0x8c: 0x00000013, + }; + await coreTest( + _memString(entryProgram), + { + Register.x8: 0x11, // M handler entered => ecall from S trapped to M + Register.x15: 0x40, // mepc captured the ecall PC + Register.x16: 0x9, // mcause = ecall-from-S + }, + _rc1s(), + initRegisters: { + Register.x13: 0x80, + Register.x10: 0x40, + Register.x11: 0x800, + }, + nextPc: 0x8c, + ); + }, + timeout: Timeout(Duration(minutes: 5)), + ); + + test( + 'ecall from S-mode: mret returns to S, not U (rc1-s microcode)', + () async { + await Simulator.reset(); + // Enter S-mode, ecall to M, the M handler bumps mepc and mret's back. The + // return MUST land in S: the following `csrs sie` is legal only in S. If the + // core drops to U on the ecall-return mret, csrs sie traps and x7 never = 0x99. + // 0x00 csrw mtvec, x13 0x30569073 (x13=0x80 M handler) + // 0x04 csrw mepc, x10 0x34151073 (x10=0x40 S entry) + // 0x08 csrw mstatus,x11 0x30059073 (x11=0x800 MPP=S) + // 0x0c mret 0x30200073 + // 0x40 ecall 0x00000073 (S -> M) + // 0x44 addi x9,x0,0x55 0x05500493 (ecall returned marker) + // 0x48 csrs sie, x12 0x10462073 (legal only if back in S) + // 0x4c addi x7,x0,0x99 0x09900393 (no-trap marker = S) + // 0x50 nop (halt) 0x00000013 + // 0x80 csrr x14, mepc 0x34102773 + // 0x84 addi x14,x14,4 0x00470713 + // 0x88 csrw mepc, x14 0x34171073 + // 0x8c mret 0x30200073 (return to 0x44 at MPP=S) + final program = { + 0x00: 0x30569073, + 0x04: 0x34151073, + 0x08: 0x30059073, + 0x0c: 0x30200073, + 0x40: 0x00000073, + 0x44: 0x05500493, + 0x48: 0x10462073, + 0x4c: 0x09900393, + 0x50: 0x00000013, + // M ecall handler: mark x8 (handler entered), bump mepc, mret. + 0x80: 0x01100413, // addi x8, x0, 0x11 + 0x84: 0x34102773, // csrr x14, mepc + 0x88: 0x00470713, // addi x14, x14, 4 + 0x8c: 0x34171073, // csrw mepc, x14 + 0x90: 0x30200073, // mret + }; + await coreTest( + _memString(program), + { + Register.x8: 0x11, // M ecall handler ran (ecall -> M works) + Register.x9: + 0x55, // ecall round-trip completed (mret returned to 0x44) + Register.x7: + 0x99, // csrs sie did NOT trap => mret returned to S, not U + }, + _rc1s(), + initRegisters: { + Register.x13: 0x80, // mtvec = M ecall handler + Register.x10: 0x40, // mepc = S entry + Register.x11: 0x800, // mstatus.MPP = S + Register.x12: 0x20, // sie bits + }, + nextPc: 0x54, + ); + }, + timeout: Timeout(Duration(minutes: 5)), + ); +} diff --git a/packages/river_hdl/test/debug/ebreak_debug_halt_test.dart b/packages/river_hdl/test/debug/ebreak_debug_halt_test.dart index 1997c2e..7a17591 100644 --- a/packages/river_hdl/test/debug/ebreak_debug_halt_test.dart +++ b/packages/river_hdl/test/debug/ebreak_debug_halt_test.dart @@ -150,7 +150,10 @@ void main() { dResume.inject(0); // run; expect self-halt on ebreak within N cycles var sawHalt = false; - for (var i = 0; i < 60; i++) { + // The rc1-s microcode core is ~80-85 cyc/instr in this bare harness, so the + // ebreak commits (self-halt) around cycle 166. The loop breaks on halt, so a + // generous bound just avoids a premature give-up, it does not slow the pass. + for (var i = 0; i < 400; i++) { await clk.nextPosedge; if (halted.value.isValid && halted.value.toInt() == 1) { sawHalt = true; diff --git a/pkgs/river-fpga/default.nix b/pkgs/river-fpga/default.nix index c2db821..96aa147 100644 --- a/pkgs/river-fpga/default.nix +++ b/pkgs/river-fpga/default.nix @@ -2,6 +2,14 @@ # # Usage: # mkFpga { ip = self'.packages.creek-v1; } +# mkFpga { ip = self'.packages.creek-v1-arty; seed = 3; } +# +# Two flows, picked from the IP target vendor: +# - iCE40 / ECP5: yosys + nextpnr + icestorm/trellis (nixpkgs tools). +# - spartan7 (Xilinx 7-series): the openXC7 flow (nextpnr-xilinx + prjxray). +# The generated Makefile drives synth -> pnr -> pack; this derivation +# feeds it the chipdb, prjxray-db and part, plus the python path that +# prjxray's fasm2frames script imports. # # Produces: synth JSON, PnR output, bitstream { @@ -11,6 +19,8 @@ nextpnr, icestorm, trellis, + openxc7 ? null, + openxc7Nixpkgs ? null, }: lib.extendMkDerivation { @@ -18,6 +28,7 @@ lib.extendMkDerivation { excludeDrvArgNames = [ "ip" + "seed" ]; extendDrvArgs = @@ -25,22 +36,64 @@ lib.extendMkDerivation { { ip, name ? "river-fpga-${ip.socName}", + # nextpnr-xilinx placer seed. creek is dense on the xc7s50, so the route + # is seed-sensitive; bump this if a build fails to route. + seed ? 2, ... }@args: - builtins.removeAttrs args [ "ip" ] - // { - inherit name; + let + targetParts = lib.splitString ":" (ip.target or ""); + vendor = if targetParts != [ ] then builtins.head targetParts else null; + # openXC7 Xilinx 7-series flow. + isOpenXc7 = vendor == "spartan7"; + device = builtins.elemAt targetParts 1; # e.g. xc7s50 + package = builtins.elemAt targetParts 2; # e.g. csga324 + part = "${device}${package}"; - dontUnpack = true; - dontConfigure = true; + # openXC7 toolchain pieces (only forced on the spartan7 path). + chipdb = "${openxc7.nextpnr-xilinx-chipdb.spartan7}/${part}.bin"; + xrayDb = "${openxc7.nextpnr-xilinx}/share/nextpnr/external/prjxray-db"; + pyPkgs = openxc7Nixpkgs.python312Packages; + # prjxray's fasm2frames is a bare python script. Reproduce the openXC7 + # devShell PYTHONPATH so its fasm/prjxray/textx imports resolve. + fasmPythonPath = lib.concatStringsSep ":" [ + "${openxc7.fasm}/lib/python3.12/site-packages" + "${openxc7.prjxray}/usr/share/python3" + "${pyPkgs.textx}/lib/python3.12/site-packages" + "${pyPkgs.arpeggio}/lib/python3.12/site-packages" + "${pyPkgs.pyyaml}/lib/python3.12/site-packages" + "${pyPkgs.intervaltree}/lib/python3.12/site-packages" + "${pyPkgs.sortedcontainers}/lib/python3.12/site-packages" + "${pyPkgs.simplejson}/lib/python3.12/site-packages" + ]; - nativeBuildInputs = (args.nativeBuildInputs or [ ]) ++ [ + latticeTools = [ yosys nextpnr icestorm # icepack trellis # ecppack ]; + xilinxTools = [ + yosys + openxc7.nextpnr-xilinx + openxc7.prjxray + openxc7Nixpkgs.python312 + ]; + in + + builtins.removeAttrs args [ + "ip" + "seed" + ] + // { + inherit name; + + dontUnpack = true; + dontConfigure = true; + + nativeBuildInputs = + (args.nativeBuildInputs or [ ]) ++ (if isOpenXc7 then xilinxTools else latticeTools); buildPhase = '' runHook preBuild @@ -49,7 +102,21 @@ lib.extendMkDerivation { cp -r ${ip}/* . chmod -R u+w . - make all + ${ + if isOpenXc7 then + '' + export PYTHONPATH="${fasmPythonPath}''${PYTHONPATH:+:$PYTHONPATH}" + make all \ + CHIPDB=${chipdb} \ + XRAY_DB=${xrayDb} \ + PART=${part}-1 \ + SEED=${toString seed} + '' + else + '' + make all + '' + } runHook postBuild ''; @@ -62,12 +129,14 @@ lib.extendMkDerivation { cp *.json $out/ 2>/dev/null || true cp *.asc $out/ 2>/dev/null || true cp *.config $out/ 2>/dev/null || true + cp *.fasm $out/ 2>/dev/null || true cp *.bin $out/ 2>/dev/null || true cp *.bit $out/ 2>/dev/null || true cp *.dts $out/ 2>/dev/null || true cp *.dot $out/ 2>/dev/null || true cp *.pcf $out/ 2>/dev/null || true cp *.lpf $out/ 2>/dev/null || true + cp *.xdc $out/ 2>/dev/null || true runHook postInstall ''; diff --git a/pkgs/river-hdl/default.nix b/pkgs/river-hdl/default.nix index 2470592..cda8277 100644 --- a/pkgs/river-hdl/default.nix +++ b/pkgs/river-hdl/default.nix @@ -8,6 +8,12 @@ trellis, surfer, flakever, + # openXC7 toolchain package set (from the openxc7 flake input). Null keeps + # mkFpga on the iCE40/ECP5 path only; spartan7 targets need it. + openxc7 ? null, + # openXC7's own pinned nixpkgs, for the python 3.12 deps that prjxray's + # fasm2frames imports. Null alongside a spartan7 target is a build error. + openxc7Nixpkgs ? null, }: buildDartApplication (finalAttrs: { pname = "river-hdl"; @@ -44,6 +50,8 @@ buildDartApplication (finalAttrs: { nextpnr icestorm trellis + openxc7 + openxc7Nixpkgs ; }; }; diff --git a/pkgs/river-ip/default.nix b/pkgs/river-ip/default.nix index bff796f..7d4ef88 100644 --- a/pkgs/river-ip/default.nix +++ b/pkgs/river-ip/default.nix @@ -83,7 +83,9 @@ lib.extendMkDerivation { deviceFlags = lib.concatMapStringsSep " " (d: "--device ${d}") devices; targetFlag = lib.optionalString (target != null) "--target ${target}"; pdkRootFlag = lib.optionalString (pdkRoot != null) "--pdk-root ${pdkRoot}"; - pinFlags = lib.concatMapStringsSep " " (p: "--pin ${p}") pins; + # Quote each pin: a spec may carry a space-separated IOSTANDARD/attr + # (e.g. "clk=R2 SSTL135"), which must reach genip as ONE --pin argument. + pinFlags = lib.concatMapStringsSep " " (p: "--pin '${p}'") pins; bootProgramFlag = lib.optionalString (bootProgram != null) "--boot-program ${bootProgram}"; in builtins.removeAttrs args [ diff --git a/pubspec.lock b/pubspec.lock index d26e908..153bb65 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -126,7 +126,7 @@ packages: description: path: "packages/harbor" ref: "fix/arty-s7-ddr" - resolved-ref: cb646f8629d6b69a0e0e54bdf3171cfa0d48bbea + resolved-ref: "8783b4c7bfa2e7ee3acfc07432f4e869a59a312f" url: "https://github.com/MidstallSoftware/harbor.git" source: git version: "0.0.1" diff --git a/pubspec.lock.json b/pubspec.lock.json index 1607054..0558a4b 100644 --- a/pubspec.lock.json +++ b/pubspec.lock.json @@ -155,7 +155,7 @@ "description": { "path": "packages/harbor", "ref": "fix/arty-s7-ddr", - "resolved-ref": "cb646f8629d6b69a0e0e54bdf3171cfa0d48bbea", + "resolved-ref": "8783b4c7bfa2e7ee3acfc07432f4e869a59a312f", "url": "https://github.com/MidstallSoftware/harbor.git" }, "source": "git", From 7268250ff793f5b1cd605d45f888d8a684dee22f Mon Sep 17 00:00:00 2001 From: Tristan Ross Date: Sat, 29 Aug 2026 13:36:59 -0700 Subject: [PATCH 3/3] refactor: booting linux --- .gitignore | 5 + devices.nix | 120 +++- flake.lock | 71 +- flake.nix | 68 +- nix/common-dart.nix | 2 +- nix/module.nix | 58 ++ nix/nixos-sdcard.nix | 199 ++++++ packages/river/lib/src/csr_address.dart | 2 + packages/river/lib/src/impl/core/v1.dart | 9 +- packages/river/lib/src/river_base.dart | 2 +- packages/river_emulator/lib/src/csr.dart | 8 + .../river_emulator/lib/src/csr_address.dart | 2 + packages/river_hdl/bin/river_genip.dart | 7 +- packages/river_hdl/bin/river_sim.dart | 2 +- packages/river_hdl/lib/river_hdl.dart | 1 - packages/river_hdl/lib/src/compat.dart | 8 + packages/river_hdl/lib/src/core.dart | 409 ++++++++++-- .../lib/src/core/compressed_fetch_buffer.dart | 47 +- packages/river_hdl/lib/src/core/csr.dart | 174 ++++- .../lib/src/core/debug_subsystem.dart | 71 +- packages/river_hdl/lib/src/core/decoder.dart | 309 ++++++--- packages/river_hdl/lib/src/core/exec.dart | 134 +++- packages/river_hdl/lib/src/core/fetcher.dart | 17 +- .../lib/src/core/jtag_bscan_tunnel.dart | 175 ----- .../river_hdl/lib/src/core/microcode_alu.dart | 19 +- packages/river_hdl/lib/src/core/mmu.dart | 62 +- packages/river_hdl/lib/src/core/pipeline.dart | 125 +++- packages/river_hdl/lib/src/genip.dart | 623 +++++++++++++++++- packages/river_hdl/lib/src/microcode_rom.dart | 8 + .../branch/caddi16sp_jump_repro_test.dart | 82 +++ .../branch/caddi16sp_latency_repro_test.dart | 83 +++ .../test/branch/caddi16sp_repro_test.dart | 124 ++++ .../test/branch/sd_regfile_repro_test.dart | 321 +++++++++ .../cache/amoadd_loop_coherence_test.dart | 76 +++ .../cache/amoadd_paged_coherence_test.dart | 105 +++ .../cache/dcache_atomic_coherence_test.dart | 198 ++++++ packages/river_hdl/test/core_harness.dart | 65 +- .../river_hdl/test/csr/counteren_test.dart | 107 +++ packages/river_hdl/test/csr/envcfg_test.dart | 85 +++ .../test/csr/mcycle_increment_test.dart | 79 +++ .../river_hdl/test/csr/stvec_width_test.dart | 70 ++ packages/river_hdl/test/dbg_elab_test.dart | 30 + .../river_hdl/test/debug/debug_core_test.dart | 482 +++++++------- .../test/debug/jtag_tunnel_dm_test.dart | 2 +- .../river_hdl/test/debug/trigger_test.dart | 219 ++++++ .../test/decode/amo_decode_test.dart | 25 + .../test/decode/combined_repro_test.dart | 240 +++++++ .../test/decode/decode_lanes_equiv_test.dart | 400 +++++++++++ .../decode/icache_evict_redirect_test.dart | 77 +++ .../test/decode/lanes1_repro_test.dart | 226 +++++++ .../test/decode/mext_hang_repro_test.dart | 225 +++++++ .../test/decode/paged_diag_test.dart | 183 +++++ .../test/decode/paged_evict_repro_test.dart | 79 +++ .../decode/paged_retauipc_repro_test.dart | 142 ++++ .../test/decode/reloc_cadd_repro_test.dart | 149 +++++ .../test/decode/retauipc_repro_test.dart | 117 ++++ .../test/decode/rtype_hang_repro_test.dart | 226 +++++++ .../river_hdl/test/device_parse_test.dart | 96 +++ .../test/fetch/coldline_refill_race_test.dart | 103 +++ .../test/fetch/fabric_contention_test.dart | 136 ++++ .../test/fetch/misaligned_amo_trap_test.dart | 102 +++ .../test/fetch/straddle_amo_test.dart | 157 +++++ .../test/fetch/straddle_jump_repro_test.dart | 103 +++ .../interconnect/ddr3_ctrlgear_soc_test.dart | 78 +++ .../river_hdl/test/interconnect/soc_test.dart | 4 +- .../test/interrupt/amo_ticket_lost_test.dart | 117 ++++ .../interrupt/async_interrupt_take_test.dart | 340 ++++++++++ .../irq_during_paged_fetch_test.dart | 141 ++++ .../interrupt/irq_during_paged_load_test.dart | 172 +++++ .../interrupt/irq_during_refill_test.dart | 127 ++++ .../interrupt/irq_reg_integrity_test.dart | 126 ++++ .../irq_seq_handler_coldfetch_test.dart | 179 +++++ .../interrupt/irq_stale_writeport_test.dart | 133 ++++ .../river_hdl/test/matrix_instructions.dart | 20 + .../river_hdl/test/mmu/core_mmu_test.dart | 17 +- .../test/mmu/fetch_fault_noicache_test.dart | 160 +++++ .../test/mmu/high_megapage_fetch_test.dart | 89 +++ .../test/mmu/icache_fetch_fault_test.dart | 177 +++++ .../river_hdl/test/mmu/mmu_fault_test.dart | 10 +- .../test/mmu/mmu_superpage_test.dart | 164 +++++ .../test/mmu/nonident_fetch_test.dart | 82 +++ .../test/mmu/satp_disable_fetch_test.dart | 117 ++++ .../test/mmu/trampoline_fault_jump_test.dart | 191 ++++++ .../test/trap/illegal_zero_test.dart | 73 ++ .../test/trap/mmode_notranslate_test.dart | 74 +++ .../test/trap/mret_to_smode_repro_test.dart | 177 +++++ .../river_hdl/test/trap/sfence_vma_test.dart | 66 ++ packages/river_maskrom/lib/src/maskrom.dart | 44 ++ .../test/maskrom_banner_sim_test.dart | 126 ++++ pkgs/nextpnr-chipdb/default.nix | 49 ++ pkgs/river-fpga/default.nix | 25 +- pkgs/river-hdl/default.nix | 7 + pkgs/river-ip/default.nix | 10 +- pubspec.lock | 6 +- pubspec.lock.json | 6 +- pubspec.yaml | 3 +- 96 files changed, 10064 insertions(+), 697 deletions(-) create mode 100644 nix/module.nix create mode 100644 nix/nixos-sdcard.nix delete mode 100644 packages/river_hdl/lib/src/core/jtag_bscan_tunnel.dart create mode 100644 packages/river_hdl/test/branch/caddi16sp_jump_repro_test.dart create mode 100644 packages/river_hdl/test/branch/caddi16sp_latency_repro_test.dart create mode 100644 packages/river_hdl/test/branch/caddi16sp_repro_test.dart create mode 100644 packages/river_hdl/test/branch/sd_regfile_repro_test.dart create mode 100644 packages/river_hdl/test/cache/amoadd_loop_coherence_test.dart create mode 100644 packages/river_hdl/test/cache/amoadd_paged_coherence_test.dart create mode 100644 packages/river_hdl/test/cache/dcache_atomic_coherence_test.dart create mode 100644 packages/river_hdl/test/csr/counteren_test.dart create mode 100644 packages/river_hdl/test/csr/envcfg_test.dart create mode 100644 packages/river_hdl/test/csr/mcycle_increment_test.dart create mode 100644 packages/river_hdl/test/csr/stvec_width_test.dart create mode 100644 packages/river_hdl/test/debug/trigger_test.dart create mode 100644 packages/river_hdl/test/decode/combined_repro_test.dart create mode 100644 packages/river_hdl/test/decode/decode_lanes_equiv_test.dart create mode 100644 packages/river_hdl/test/decode/icache_evict_redirect_test.dart create mode 100644 packages/river_hdl/test/decode/lanes1_repro_test.dart create mode 100644 packages/river_hdl/test/decode/mext_hang_repro_test.dart create mode 100644 packages/river_hdl/test/decode/paged_diag_test.dart create mode 100644 packages/river_hdl/test/decode/paged_evict_repro_test.dart create mode 100644 packages/river_hdl/test/decode/paged_retauipc_repro_test.dart create mode 100644 packages/river_hdl/test/decode/reloc_cadd_repro_test.dart create mode 100644 packages/river_hdl/test/decode/retauipc_repro_test.dart create mode 100644 packages/river_hdl/test/decode/rtype_hang_repro_test.dart create mode 100644 packages/river_hdl/test/fetch/coldline_refill_race_test.dart create mode 100644 packages/river_hdl/test/fetch/fabric_contention_test.dart create mode 100644 packages/river_hdl/test/fetch/misaligned_amo_trap_test.dart create mode 100644 packages/river_hdl/test/fetch/straddle_amo_test.dart create mode 100644 packages/river_hdl/test/fetch/straddle_jump_repro_test.dart create mode 100644 packages/river_hdl/test/interconnect/ddr3_ctrlgear_soc_test.dart create mode 100644 packages/river_hdl/test/interrupt/amo_ticket_lost_test.dart create mode 100644 packages/river_hdl/test/interrupt/async_interrupt_take_test.dart create mode 100644 packages/river_hdl/test/interrupt/irq_during_paged_fetch_test.dart create mode 100644 packages/river_hdl/test/interrupt/irq_during_paged_load_test.dart create mode 100644 packages/river_hdl/test/interrupt/irq_during_refill_test.dart create mode 100644 packages/river_hdl/test/interrupt/irq_reg_integrity_test.dart create mode 100644 packages/river_hdl/test/interrupt/irq_seq_handler_coldfetch_test.dart create mode 100644 packages/river_hdl/test/interrupt/irq_stale_writeport_test.dart create mode 100644 packages/river_hdl/test/mmu/fetch_fault_noicache_test.dart create mode 100644 packages/river_hdl/test/mmu/high_megapage_fetch_test.dart create mode 100644 packages/river_hdl/test/mmu/icache_fetch_fault_test.dart create mode 100644 packages/river_hdl/test/mmu/mmu_superpage_test.dart create mode 100644 packages/river_hdl/test/mmu/nonident_fetch_test.dart create mode 100644 packages/river_hdl/test/mmu/satp_disable_fetch_test.dart create mode 100644 packages/river_hdl/test/mmu/trampoline_fault_jump_test.dart create mode 100644 packages/river_hdl/test/trap/illegal_zero_test.dart create mode 100644 packages/river_hdl/test/trap/mmode_notranslate_test.dart create mode 100644 packages/river_hdl/test/trap/mret_to_smode_repro_test.dart create mode 100644 packages/river_hdl/test/trap/sfence_vma_test.dart create mode 100644 packages/river_maskrom/test/maskrom_banner_sim_test.dart create mode 100644 pkgs/nextpnr-chipdb/default.nix diff --git a/.gitignore b/.gitignore index a4288d4..a0da856 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,8 @@ doc/api # working directory inside a package (not part of the source tree) *.db objects + +*.dtb +*.dts +*.asl +*.aml diff --git a/devices.nix b/devices.nix index 6f1936b..097124f 100644 --- a/devices.nix +++ b/devices.nix @@ -37,6 +37,66 @@ let "uart:0x10000000:ns16550a" ]; }; + + # Delta: the SoC family above Creek. It carries the River Core V1 Full core + # (rc1-f), whose F/D FPU makes it NixOS-capable (a stock rv64gc/lp64d distro + # needs hardware float, which the creek rc1-s core lacks). Delta sits between + # Creek and the future Oceanic family. This is scaffold: the base and the first + # bring-up target below track creek's structure, retuned as the family fills in. + delta-v1-base = { + socName = "delta_v1"; + cores = [ "rc1-f" ]; + interconnect = "wishbone"; + clockFreq = 48000000; + memories = [ + "0x20000000:16M:flash" + "0x80000000:128M:dram" + ]; + devices = [ + "clint:0x02000000" + "plic:0x04000000" + "uart:0x10000000:ns16550a" + ]; + }; + + # Shared Arty S7-50 override for the delta bring-up (native 4-bit SDIO, 33.33 + # MHz). The base delta-v1-arty and the DMA/clock comparison variants below all + # derive from this so the only differences are the axis under test. + delta-arty-attrs = { + target = "spartan7:xc7s50:csga324"; + board = "arty-s7-50"; + clockFreq = 33333333; + oscFreq = 100000000; + memories = [ + "0x20000000:16M:flash:arty-s7" + "0x08000000:64K:sram" + "0x80000000:256M:dram:arty-s7:ddr3v2=true,clockfreq=300000000,cmdslot=2,wrshift=-1,trainable=true" + ]; + # Native 4-bit SDIO host (4x the 1-bit SPI throughput) with an ADMA engine on + # the fabric. dmashared puts the ADMA on the PRIMARY channel (no separate + # channel + converge crossbar), which de-congests the DDR CDC and lifts the + # ddr_clk route from ~67 to ~80 MHz on this dense xc7s50. + devices = delta-v1-base.devices ++ [ + "sdio:0x10001000:dma=true,samplefall=true" + "debug-jtag:triggers=4" + ]; + bootProgram = "xipboot"; + pins = [ + "clk=R2 SSTL135" + "uart_tx=uart@tx:R12" + "uart_rx=uart@rx:V12" + "reset_n=C18" + # Native SDIO on the Arty S7 PmodSD header (iface= is spi-only, so the SD + # pads bind by explicit pin). LVCMOS33 is the default I/O standard. + "sdio_sd_clk=N14" + "sdio_sd_cmd=L18" + "sdio_sd_dat0=M14" + "sdio_sd_dat1=M16" + "sdio_sd_dat2=M17" + "sdio_sd_dat3=L17" + "sdio_sd_cd=M18" + ]; + }; in { creek-v1-orangecrab = { @@ -84,24 +144,80 @@ in creek-v1-base // { target = "spartan7:xc7s50:csga324"; - clockFreq = 25000000; + # Board catalog: supplies the Pmod connector pin map so the SD-in-SPI + # device below resolves `iface=pmod@ja` to the JA header sites. The + # explicit target/pins above still win, so the DDR-proven build is + # unchanged. + board = "arty-s7-50"; + # Driven at 33.33 MHz, the proven rate the deployed firmware targets + # (project-river-40mhz). The old 25 MHz value was a conservative + # placeholder that mis-framed the UART and skewed the timebase. + clockFreq = 33333333; oscFreq = 100000000; memories = [ "0x20000000:16M:flash:arty-s7" "0x08000000:64K:sram" "0x80000000:256M:dram:arty-s7:ddr3v2=true,clockfreq=300000000,cmdslot=2,wrshift=-1,trainable=true" ]; - devices = creek-v1-base.devices ++ [ "debug-jtag" ]; + # PmodSD (SD card in SPI mode) on Pmod JA. `iface=pmod@ja` binds + # cs/mosi/miso/sck to JA1..JA4 via the board's connector catalog. Weir + # discovers the harbor,spi node and probes for a card (conduit sd_spi). + devices = creek-v1-base.devices ++ [ + "spi:0x10001000:iface=pmod@ja,sdcard=true" + "debug-jtag" + ]; bootProgram = "xipboot"; pins = [ "clk=R2 SSTL135" "uart_tx=uart@tx:R12" "uart_rx=uart@rx:V12" + # Arty S7 RESET button (ck_rst, active-low, shared with the FTDI SRST). + # Adds an external reset ORed into the power-on reset so a press (or an + # FTDI reset) restarts the SoC without a full FPGA reconfig. + "reset_n=C18" + ]; + } + ); + }; + + # Delta bring-up on the Arty S7-50, SCAFFOLD. This mirrors creek-v1-arty (same + # board, DDR3, SD card, self-boot) but swaps the rc1-s core for rc1-f (Full, + # with FPU) so a stock rv64gc NixOS can run. The SD card on Pmod JA is the + # rootfs device. NOTE: rc1-f is larger than rc1-s (it adds the FPU), and creek + # already routes tight on the xc7s50, so fit/route here is UNPROVEN; the real + # Delta board is likely a larger part. Kept on the Arty for continuity of the + # bring-up flow until a bigger board is wired in. + # DMA-SPI at the proven 33.33 MHz (project-river-40mhz). dma=true gives the + # integrated SPI DMA master: SD blocks stream straight to DRAM, no per-byte CPU + # poll. Weir finds it via the `harbor,dma` DT property and falls back to PIO. + delta-v1-arty = { + ip = river-hdl.mkSoC (delta-v1-base // delta-arty-attrs); + }; + + # DMA comparison variant: SPI WITHOUT the DMA master (PIO block reads). Same + # everything else, so a DMA-vs-PIO A/B on identical timing. + delta-v1-arty-pio = { + ip = river-hdl.mkSoC ( + delta-v1-base + // delta-arty-attrs + // { + devices = delta-v1-base.devices ++ [ + "spi:0x10001000:iface=pmod@ja,sdcard=true" + "debug-jtag:triggers=4" ]; } ); }; + # Clock sweep of the DMA-SPI build: 40 MHz (the core-datapath ceiling, may not + # close) and 20 MHz (timing-safe floor). Bracket the proven 33.33 MHz baseline. + delta-v1-arty-40mhz = { + ip = river-hdl.mkSoC (delta-v1-base // delta-arty-attrs // { clockFreq = 40000000; }); + }; + delta-v1-arty-20mhz = { + ip = river-hdl.mkSoC (delta-v1-base // delta-arty-attrs // { clockFreq = 20000000; }); + }; + creek-v1-sky130 = { ip = river-hdl.mkSoC ( creek-v1-base diff --git a/flake.lock b/flake.lock index 4401b02..5875782 100644 --- a/flake.lock +++ b/flake.lock @@ -10,15 +10,15 @@ "locked": { "lastModified": 1777243449, "narHash": "sha256-NtXLD5EqWrTqf0k15BT6IkC3y6T7LLoG9005Y1gfP5E=", - "owner": "MidstallSoftware", - "repo": "asix", + "ref": "refs/heads/master", "rev": "31303a2eb9ee8b4504baa3fa5ae0bf08bc1268cc", - "type": "github" + "revCount": 3, + "type": "git", + "url": "https://git.lilithsemi.com/LilithSemi/asix" }, "original": { - "owner": "MidstallSoftware", - "repo": "asix", - "type": "github" + "type": "git", + "url": "https://git.lilithsemi.com/LilithSemi/asix" } }, "flake-parts": { @@ -74,6 +74,32 @@ "type": "github" } }, + "harbor": { + "inputs": { + "flakever": [ + "flakever" + ], + "nixpkgs": [ + "nixpkgs" + ], + "treefmt-nix": [ + "treefmt-nix" + ] + }, + "locked": { + "lastModified": 1788033340, + "narHash": "sha256-O+y5lb3cR3Ebm92NUTCgCvbJyLlJ0De/d9j5Jn5F8/U=", + "ref": "refs/heads/master", + "rev": "ddc1d70d05a67ba5a9fe0be33d3852795d07581e", + "revCount": 32, + "type": "git", + "url": "https://git.lilithsemi.com/LilithSemi/harbor" + }, + "original": { + "type": "git", + "url": "https://git.lilithsemi.com/LilithSemi/harbor" + } + }, "nixpkgs": { "locked": { "lastModified": 1776555070, @@ -127,9 +153,11 @@ "asix": "asix", "flake-parts": "flake-parts", "flakever": "flakever", + "harbor": "harbor", "nixpkgs": "nixpkgs", "openxc7": "openxc7", - "treefmt-nix": "treefmt-nix_2" + "treefmt-nix": "treefmt-nix_2", + "weir": "weir" } }, "systems": { @@ -187,6 +215,35 @@ "repo": "treefmt-nix", "type": "github" } + }, + "weir": { + "inputs": { + "flake-parts": [ + "flake-parts" + ], + "flakever": [ + "flakever" + ], + "nixpkgs": [ + "nixpkgs" + ], + "treefmt-nix": [ + "treefmt-nix" + ] + }, + "locked": { + "lastModified": 1787442515, + "narHash": "sha256-0nVSFfaNeQJe0gUlwDQG+GRiaqM3gY+4Yl7i0+wXghk=", + "ref": "refs/heads/master", + "rev": "35bae07702e4e6a82db3a54eed1135f950f5cf31", + "revCount": 7, + "type": "git", + "url": "https://git.lilithsemi.com/LilithSemi/weir" + }, + "original": { + "type": "git", + "url": "https://git.lilithsemi.com/LilithSemi/weir" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index be79c7d..9b0aed4 100644 --- a/flake.nix +++ b/flake.nix @@ -10,12 +10,28 @@ url = "github:numtide/treefmt-nix"; inputs.nixpkgs.follows = "nixpkgs"; }; - # PDKs (sky130, gf180mcu) + the silicon backend (mkTapeout/mkVerify). asix = { - url = "github:MidstallSoftware/asix"; + url = "git+https://git.lilithsemi.com/LilithSemi/asix"; inputs.nixpkgs.follows = "nixpkgs"; }; openxc7.url = "github:openXC7/toolchain-nix"; + weir = { + url = "git+https://git.lilithsemi.com/LilithSemi/weir"; + inputs = { + nixpkgs.follows = "nixpkgs"; + flake-parts.follows = "flake-parts"; + flakever.follows = "flakever"; + treefmt-nix.follows = "treefmt-nix"; + }; + }; + harbor = { + url = "git+https://git.lilithsemi.com/LilithSemi/harbor"; + inputs = { + nixpkgs.follows = "nixpkgs"; + flakever.follows = "flakever"; + treefmt-nix.follows = "treefmt-nix"; + }; + }; }; outputs = @@ -25,6 +41,8 @@ flake-parts, flakever, treefmt-nix, + weir, + harbor, ... }@inputs: let @@ -44,7 +62,10 @@ inputs.treefmt-nix.flakeModule ]; - flake.versionTemplate = "1.1pre--"; + flake = { + versionTemplate = "1.1pre--"; + nixosModules.default = ./nix/module.nix; + }; systems = [ "aarch64-linux" @@ -105,6 +126,8 @@ inherit system; overlays = [ inputs.asix.overlays.default + inputs.weir.overlays.default + inputs.harbor.overlays.default self.overlays.default ]; }; @@ -184,6 +207,42 @@ isFpga = ip: builtins.elem (targetVendor ip) fpgaVendors; isAsic = ip: builtins.elem (targetVendor ip) asicVendors; + # NixOS-capable: stock riscv64 userspace is rv64gc (lp64d), so the + # SoC needs a hardware-FPU core. The in-order rc1-f "full" core and + # the out-of-order rc1-ma "macro" (rc1-f-class ISA, OoO) both have + # F/D; creek's rc1-s / stream's rc1-n SIGILL in userspace. + nixosCores = [ + "rc1-f" + "rc1-ma" + ]; + isNixosCapable = ip: lib.any (c: builtins.elem c nixosCores) (ip.cores or [ ]); + + # A GPT+ESP+ext4 SD image of a NixOS system for this SoC, boot-through + # Weir (see nix/nixos-sdcard.nix). Cross-compiled to riscv64 from the + # build host. The hardware.river module supplies the firmware + the + # harbor-kmod driver package. + mkNixosSdcard = + name: ip: + (inputs.nixpkgs.lib.nixosSystem { + modules = [ + ./nix/module.nix + ./nix/nixos-sdcard.nix + { + hardware.river = { + enable = true; + ipPackage = ip; + }; + nixpkgs.hostPlatform = "riscv64-linux"; + nixpkgs.buildPlatform = system; + # module.nix needs the Weir firmware + harbor-kmod driver. + nixpkgs.overlays = [ + inputs.harbor.overlays.default + inputs.weir.overlays.default + ]; + } + ]; + }).config.system.build.image; + mkDevicePackages = name: cfg: let @@ -200,6 +259,9 @@ // lib.optionalAttrs (isFpga ip) { "${name}-bitstream" = pkgs.river-hdl.mkFpga { inherit ip; }; } + // lib.optionalAttrs (isFpga ip && isNixosCapable ip) { + "${name}-nixos-sdcard" = mkNixosSdcard name ip; + } // lib.optionalAttrs (isAsic ip) { "${name}-tapeout" = tapeout; "${name}-verify" = pkgs.asix.mkVerify { diff --git a/nix/common-dart.nix b/nix/common-dart.nix index fdb744a..eff2518 100644 --- a/nix/common-dart.nix +++ b/nix/common-dart.nix @@ -2,6 +2,6 @@ lib: { pubspecLock = lib.importJSON ../pubspec.lock.json; gitHashes = { - harbor = "sha256-FZd+6+ZqwIw50CgBRp4aC0bZhGJIamsuRuXplCiPGgs="; + harbor = "sha256-O+y5lb3cR3Ebm92NUTCgCvbJyLlJ0De/d9j5Jn5F8/U="; }; } diff --git a/nix/module.nix b/nix/module.nix new file mode 100644 index 0000000..e1f53ce --- /dev/null +++ b/nix/module.nix @@ -0,0 +1,58 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.hardware.river; + + # Compile the ACPI DSDT that genip emitted into the IP package (one `.asl`) + # into an AML blob for Weir to embed via `-Daml`. + aml = + pkgs.runCommand "river.aml" + { + nativeBuildInputs = [ pkgs.acpica-tools ]; + } + '' + iasl -p ./river ${cfg.ipPackage}/*.asl + mv ./river.aml $out + ''; + + # Compile the device tree source that genip emitted into the IP package (one + # `.dts`) into a DTB for Weir to embed via `-Ddtb`. + dtb = + pkgs.runCommand "river.dtb" + { + nativeBuildInputs = [ pkgs.dtc ]; + } + '' + dtc -I dts -O dtb -o $out ${cfg.ipPackage}/*.dts + ''; + + weirFirmware = pkgs.weir.overrideAttrs ( + f: p: { + zigBuildFlags = (p.zigBuildFlags or [ ]) ++ [ + "-Daml=${aml}" + "-Ddtb=${dtb}" + ]; + } + ); +in +{ + options.hardware.river = { + enable = lib.mkEnableOption "River hardware support"; + ipPackage = lib.mkOption { + description = '' + The IP package for the configuration of River. + ''; + type = lib.types.package; + }; + }; + + config = lib.mkIf cfg.enable { + boot.extraModulePackages = [ config.boot.kernelPackages.harbor-kmod ]; + + system.build.river-firmware = weirFirmware; + }; +} diff --git a/nix/nixos-sdcard.nix b/nix/nixos-sdcard.nix new file mode 100644 index 0000000..b31fc11 --- /dev/null +++ b/nix/nixos-sdcard.nix @@ -0,0 +1,199 @@ +# A NixOS system for a River SoC that boots from an SD card through Weir. +# +# Weir is the platform UEFI firmware: it looks for a GPT ESP and loads +# \EFI\BOOT\BOOTRISCV64.EFI. We put systemd-boot there, which then boots NixOS +# (kernel + initrd + loader entry off the same ESP), and the initrd mounts the +# ext4 root from the SD (harbor_spi + mmc_spi). +# +# The image comes from the NixOS `image.repart` module: build-host systemd-repart +# under fakeroot assembles a GPT image with no VM and no target execution, so it +# cross-compiles to riscv64 from an x86_64/aarch64 host with no qemu/binfmt. GPT +# matters here - Weir does gpt.findEsp() and, failing that, reads the WHOLE disk +# at LBA 0 as one FAT. It never scans MBR partitions, so the MBR layout from +# sd-image.nix would be invisible to it. +# +# The flake sets hardware.river.ipPackage, nixpkgs.hostPlatform (riscv64-linux) +# and nixpkgs.buildPlatform (the build host, for cross). +{ + config, + lib, + pkgs, + modulesPath, + ... +}: +let + # Force a kernel option off, overriding the NixOS common-config default, and + # tolerate kconfig keeping it on if something still selects it (so a stray + # dependency never fails the build). + off = lib.mkForce (lib.kernel.option lib.kernel.no); + + # systemd-boot EFI binary for the target arch, and the EFI removable-media + # fallback name Weir's boot manager loads. + efiArch = "riscv64"; + sdbootEfi = "${pkgs.systemd}/lib/systemd/boot/efi/systemd-boot${efiArch}.efi"; + + kernelPath = "${config.system.build.kernel}/${config.system.boot.loader.kernelFile}"; + initrdPath = "${config.system.build.initialRamdisk}/${config.system.boot.loader.initrdFile}"; + toplevel = config.system.build.toplevel; + + rootPartLabel = "nixos"; + + # The systemd-boot loader entry. Root is mounted from the initrd's baked-in + # fileSystems config; init= points at this generation's stage-2. + loaderEntry = pkgs.writeText "nixos.conf" '' + title NixOS + linux /EFI/nixos/kernel.efi + initrd /EFI/nixos/initrd + options init=${toplevel}/init ${lib.concatStringsSep " " config.boot.kernelParams} + ''; + loaderConf = pkgs.writeText "loader.conf" '' + default nixos + timeout 3 + ''; +in +{ + imports = [ + # minimal, not base: base is the installer/rescue profile (testdisk, vim, + # pciutils, usbutils, nvme-cli, cryptsetup, w3m, ...) and enables btrfs/zfs/ + # xfs/ntfs/cifs support, dragging in those tools AND kernel modules. minimal + # strips all of it: empty default packages, docs off, extra services off. + "${modulesPath}/profiles/minimal.nix" + "${modulesPath}/image/repart.nix" + ]; + + # This board boots to a serial root login off the SD and does nothing else, so + # strip the kernel to that. Disable the big subsystems it has no hardware or use + # for: display, sound, USB, wireless/Bluetooth, media, virtualisation, RDMA. The + # RISC-V core + PLIC/CLINT timer + ns16550a serial + SPI + MMC/SD + ext4 + + # initramfs (kept by the modules below and the in-tree defaults) still build. + # NET core stays (systemd needs AF_UNIX/loopback); only the driver bloat goes. + boot.kernelPatches = [ + { + name = "delta-minimal"; + patch = null; + structuredExtraConfig = { + DRM = off; # no display of any kind (serial console only) + FB = off; + SOUND = off; # no audio hardware + USB_SUPPORT = off; # no USB on the delta bring-up + WLAN = off; # no radios + BT = off; + NFC = off; + MEDIA_SUPPORT = off; # no capture/tuner hardware + VIRTUALIZATION = off; # not a hypervisor or KVM guest + INFINIBAND = off; # no RDMA fabric + CAN = off; # niche buses this board lacks + HAMRADIO = off; + # Drop DWARF debug info: it bloats every module (and the vmlinux), which + # inflates the initrd and the on-SD closure. No effect on the stripped + # Image, but a big cut to /lib/modules. Switch the debug-info CHOICE to + # "none" (DEBUG_INFO is selected by it, so setting DEBUG_INFO=n alone + # would be forced back on). + DEBUG_INFO_NONE = lib.mkForce lib.kernel.yes; + # River RC1 cores do NO unaligned access in hardware: every misaligned + # load/store/AMO traps and is emulated (Weir in M-mode). The default + # RISCV_PROBE_UNALIGNED_ACCESS benchmarks unaligned speed at boot by + # hammering unaligned copies (check_unaligned_access), which on this core + # is thousands of trap-and-emulate round trips and takes HOURS. Assume + # emulated and skip the boot probe entirely. + RISCV_EMULATED_UNALIGNED_ACCESS = lib.mkForce lib.kernel.yes; + RISCV_PROBE_UNALIGNED_ACCESS = off; + }; + } + ]; + + # Serial console on the ns16550a; no framebuffer on this SoC. + # + # Raise the device-unit timeout. serial-getty@ttyS0 waits for + # dev-ttyS0.device, which systemd plugs when udev processes the tty. On this + # slow SoC (and under emulation) udev can take longer than the 90 s default to + # reach the port behind the other coldplug events, so the getty fails its + # dependency and no login appears. 5 minutes gives udev room to catch up. + boot.kernelParams = [ + # Route the earliest kernel output (before the ns16550 driver binds) through + # the SBI console, i.e. Weir's ecall putchar. Without it a panic during early + # boot writes to a console that does not exist yet and looks like a silent + # hang. keep_bootcon leaves it active through the ttyS0 handoff. + "earlycon=sbi" + "keep_bootcon" + "console=ttyS0,115200n8" + "systemd.default_device_timeout_sec=300" + ]; + boot.consoleLogLevel = lib.mkDefault 7; + + # We lay systemd-boot down as image contents (see image.repart below), so no + # activation-time bootloader installer runs. + boot.loader.grub.enable = false; + + # Don't pull NixOS's default initrd module set (USB HID, ehci/ohci/xhci, ahci, + # ata_piix, floppy, common NICs, ...). This board is serial + SD-over-SPI only, + # none of that hardware exists, and several of those modules no longer build + # once the kernel strip above disables USB/etc. (a requested-but-missing module + # fails modules-shrunk). We list exactly what stage-1 needs below. + boot.initrd.includeDefaultModules = false; + + # Stage-1 must reach the SD to mount root: the Harbor SPI controller driver + # (out-of-tree, via hardware.river's harbor-kmod) plus the in-tree SD-over-SPI + # + block + ext4 stack. + boot.initrd.kernelModules = [ + "harbor_spi" + "harbor_sdio" + "mmc_spi" + "mmc_block" + "ext4" + ]; + boot.initrd.availableKernelModules = [ + "harbor_spi" + "harbor_sdio" + "mmc_spi" + "mmc_block" + ]; + + fileSystems."/" = { + device = "/dev/disk/by-partlabel/${rootPartLabel}"; + fsType = "ext4"; + }; + + # Keep the closure small for a 256 MiB board bring-up. + documentation.enable = lib.mkDefault false; + documentation.nixos.enable = lib.mkDefault false; + system.stateVersion = lib.mkDefault "24.11"; + + # A root login on the serial console so we can see it came up. + users.users.root.initialPassword = lib.mkDefault "root"; + services.getty.autologinUser = lib.mkDefault "root"; + + # GPT image: a FAT ESP (systemd-boot + kernel + initrd + entry) and an ext4 + # root holding the system closure. `Type = "linux-generic"` (not "root") keeps + # the partition type arch-neutral - "root" would tag it for the BUILD host's + # arch when cross-compiling. Root is found by its GPT partition label. + image.repart = { + name = "${config.system.name}-nixos-sdcard"; + partitions = { + "esp" = { + contents = { + "/EFI/BOOT/BOOTRISCV64.EFI".source = sdbootEfi; + "/EFI/systemd/systemd-boot${efiArch}.efi".source = sdbootEfi; + "/EFI/nixos/kernel.efi".source = kernelPath; + "/EFI/nixos/initrd".source = initrdPath; + "/loader/loader.conf".source = loaderConf; + "/loader/entries/nixos.conf".source = loaderEntry; + }; + repartConfig = { + Type = "esp"; + Format = "vfat"; + SizeMinBytes = "256M"; + }; + }; + "root" = { + storePaths = [ toplevel ]; + repartConfig = { + Type = "linux-generic"; + Format = "ext4"; + Label = rootPartLabel; + Minimize = "guess"; + }; + }; + }; + }; +} diff --git a/packages/river/lib/src/csr_address.dart b/packages/river/lib/src/csr_address.dart index 247d1cc..1ee3ddb 100644 --- a/packages/river/lib/src/csr_address.dart +++ b/packages/river/lib/src/csr_address.dart @@ -17,6 +17,7 @@ enum CsrAddress { mie(0x304), mtvec(0x305), mcounteren(0x306), + menvcfg(0x30A), mstatush(0x310), // Machine Trap Handling @@ -35,6 +36,7 @@ enum CsrAddress { sie(0x104), stvec(0x105), scounteren(0x106), + senvcfg(0x10A), // Supervisor Trap Handling sscratch(0x140), diff --git a/packages/river/lib/src/impl/core/v1.dart b/packages/river/lib/src/impl/core/v1.dart index c1ade18..cb0606f 100644 --- a/packages/river/lib/src/impl/core/v1.dart +++ b/packages/river/lib/src/impl/core/v1.dart @@ -70,6 +70,7 @@ class RiverCoreConfigV1 extends RiverCoreConfig { required super.mmu, required super.interrupts, required super.clock, + super.regfileReadLatency, HarborL1CacheConfig? l1cache, }) : super( l1cache: l1cache ?? _rc1L1(), @@ -100,6 +101,7 @@ class RiverCoreConfigV1 extends RiverCoreConfig { required super.mmu, required super.interrupts, required super.clock, + super.regfileReadLatency, HarborL1CacheConfig? l1cache, }) : super( l1cache: l1cache ?? _rc1L1(), @@ -122,7 +124,12 @@ class RiverCoreConfigV1 extends RiverCoreConfig { executionMode: ExecutionMode.inOrder, issueWidth: IssueWidth.single, // Same scalar personality as [small] (+ F/D), so it carries the same - // L1 caches (see [_rc1L1]). + // L1 caches (see [_rc1L1]) and the same microcode datapath: the shared + // ALU keeps area near ~12k (+ FPU) instead of the ~21k-LUT static + // fabric, and it lets yosys synthesize (the static exec unit's giant + // FSM does not converge). Matches [small]'s microcode settings. + microcodeMode: MicrocodeMode.full, + microcodeDecodeLanes: 2, ); /// RC1.ma - River Core V1 macro (RV64GC_Zba_Zbb_Zbs): out-of-order, diff --git a/packages/river/lib/src/river_base.dart b/packages/river/lib/src/river_base.dart index c882fa0..e7f7cf5 100644 --- a/packages/river/lib/src/river_base.dart +++ b/packages/river/lib/src/river_base.dart @@ -188,7 +188,7 @@ class InterruptController { /// River's allocated RISC-V architecture ID (`marchid`), from the official /// registry: https://github.com/riscv/riscv-isa-manual/blob/main/marchid.md -/// (Midstall Software, entry 49). This is the default [RiverCoreConfig.archId]. +/// (River, entry 49). This is the default [RiverCoreConfig.archId]. const int riverArchId = 49; class RiverCoreConfig { diff --git a/packages/river_emulator/lib/src/csr.dart b/packages/river_emulator/lib/src/csr.dart index 09256d4..29fec5d 100644 --- a/packages/river_emulator/lib/src/csr.dart +++ b/packages/river_emulator/lib/src/csr.dart @@ -350,6 +350,14 @@ class CsrFile { CsrAddress.scounteren.address, ); + // senvcfg/menvcfg: River implements none of the envcfg-controlled features + // (Zicbo, pointer-masking, Sstc, Svpbmt), so every field is WARL-0 (writes + // drop, reads return 0). They exist so Linux's csrw/csrr (envcfg_update_bits + // context switch, try_to_set_pmm probe) do not trap illegal. Mirrors the HDL + // (csr.dart applyMask(.., 0)). + csrs[CsrAddress.senvcfg.address] = MaskedCsr(CsrAddress.senvcfg.address, 0); + csrs[CsrAddress.menvcfg.address] = MaskedCsr(CsrAddress.menvcfg.address, 0); + csrs[CsrAddress.satp.address] = MaskedCsr( CsrAddress.satp.address, fullMask, diff --git a/packages/river_emulator/lib/src/csr_address.dart b/packages/river_emulator/lib/src/csr_address.dart index 120c927..d6b6085 100644 --- a/packages/river_emulator/lib/src/csr_address.dart +++ b/packages/river_emulator/lib/src/csr_address.dart @@ -17,6 +17,7 @@ enum CsrAddress { mie(0x304), mtvec(0x305), mcounteren(0x306), + menvcfg(0x30A), mstatush(0x310), // Machine Trap Handling @@ -35,6 +36,7 @@ enum CsrAddress { sie(0x104), stvec(0x105), scounteren(0x106), + senvcfg(0x10A), // Supervisor Trap Handling sscratch(0x140), diff --git a/packages/river_hdl/bin/river_genip.dart b/packages/river_hdl/bin/river_genip.dart index fbef0e1..1b9f665 100644 --- a/packages/river_hdl/bin/river_genip.dart +++ b/packages/river_hdl/bin/river_genip.dart @@ -19,7 +19,7 @@ Future main(List arguments) async { abbr: 'c', help: 'Core model', defaultsTo: ['rc1-mi'], - allowed: ['rc1-n', 'rc1-mi', 'rc1-s', 'rc1-m'], + allowed: ['rc1-n', 'rc1-mi', 'rc1-s', 'rc1-m', 'rc1-f'], ) ..addOption( 'interconnect', @@ -58,7 +58,8 @@ Future main(List arguments) async { 'target', abbr: 't', help: - 'Target (FPGA: ecp5:dev:pkg, ice40:dev:pkg; ASIC: sky130:hd, gf180mcu:3v3)', + 'Target (FPGA: ecp5:dev:pkg, ice40:dev:pkg; ASIC: sky130:hd, ' + 'gf180mcu:3v3; SIM: verilator, verilator:trace)', ) ..addOption( 'board', @@ -188,6 +189,8 @@ Future main(List arguments) async { print(' Target: ${t.vendor} ${t.device} (${t.package})'); case AsicTarget(): print(' Target: ${t.pdk} (${t.variant})'); + case SimTarget(): + print(' Target: verilator (sim${t.trace ? ', trace' : ''})'); } } diff --git a/packages/river_hdl/bin/river_sim.dart b/packages/river_hdl/bin/river_sim.dart index 2eade4b..b1a472e 100644 --- a/packages/river_hdl/bin/river_sim.dart +++ b/packages/river_hdl/bin/river_sim.dart @@ -53,7 +53,7 @@ Future main(List arguments) async { abbr: 'c', help: 'Core model', defaultsTo: ['rc1-mi'], - allowed: ['rc1-n', 'rc1-mi', 'rc1-s', 'rc1-m'], + allowed: ['rc1-n', 'rc1-mi', 'rc1-s', 'rc1-m', 'rc1-f'], ) ..addOption( 'clock-freq', diff --git a/packages/river_hdl/lib/river_hdl.dart b/packages/river_hdl/lib/river_hdl.dart index 0f810cf..0937205 100644 --- a/packages/river_hdl/lib/river_hdl.dart +++ b/packages/river_hdl/lib/river_hdl.dart @@ -5,7 +5,6 @@ export 'src/data_port.dart'; export 'src/core/csr.dart'; export 'src/core/debug.dart'; export 'src/core/debug_subsystem.dart'; -export 'src/core/jtag_bscan_tunnel.dart'; export 'src/core/sba_wishbone.dart'; export 'src/core/debug_pump.dart'; export 'src/core/decoder.dart'; diff --git a/packages/river_hdl/lib/src/compat.dart b/packages/river_hdl/lib/src/compat.dart index 64b6f4b..abfd281 100644 --- a/packages/river_hdl/lib/src/compat.dart +++ b/packages/river_hdl/lib/src/compat.dart @@ -188,6 +188,14 @@ class MicroOpAluFunct { static const int remw = 26; static const int remuw = 27; static const int masked = 28; + // Zbb min/max (RiscVAluFunct enum indices). rc1-f has no Zbb so the ROM never + // emits these, but the shared ALU implements them (reusing slt/sltu) so the + // AMO read-modify-write combine can route min/max/minu/maxu through this ONE + // unit instead of a dedicated 9-way afunct mux. + static const int minOp = 31; + static const int maxOp = 32; + static const int minuOp = 33; + static const int maxuOp = 34; // Zicond: conditional-zero. Values are the RiscVAluFunct enum indices (the // funct stored in the microcode ROM), not a dense local numbering. static const int czeroEqz = 61; diff --git a/packages/river_hdl/lib/src/core.dart b/packages/river_hdl/lib/src/core.dart index 7bd54a3..f97655d 100644 --- a/packages/river_hdl/lib/src/core.dart +++ b/packages/river_hdl/lib/src/core.dart @@ -22,10 +22,21 @@ class RiverCore extends BridgeModule { RiverCore( this.config, { Map srcIrqs = const {}, + // Machine timer / software interrupt-pending lines from the CLINT. Drive + // mip.MTIP(7) and mip.MSIP(3) respectively; null ties the bit to 0. The SoC + // wires these from the CLINT's timer_irq/sw_irq outputs (see genip). + Logic? timerPending, + Logic? swPending, + Logic? timeIn, List staticInstructions = const [], WishboneConfig? busConfig, HarborDeviceTarget? target, bool withDebug = false, + // Number of hardware execute-breakpoint triggers (RISC-V mcontrol, type 2) + // exposed to the Debug Module. 0 = no triggers (byte-identical to before). + // Each trigger is tselect/tdata1/tdata2 and fires a cause-2 debug entry when + // an enabled execute trigger's tdata2 matches the instruction PC. + int debugTriggers = 0, // Expose plain bustap_ack/bustap_datmiso outputs mirroring the Wishbone // master's incoming ACK/read data for a logic analyzer. ACK is an input // here, so it must be mirrored to a plain output to stay hierarchy-legal. @@ -35,6 +46,11 @@ class RiverCore extends BridgeModule { // seeds the OoO physical regfile. Asserted only while frozen during seeding; // null leaves it tied off. Logic? prfSeedMode, + // Test-only backdoor: the privilege the core holds coming out of reset. + // Real RISC-V resets to machine; a unit test that exercises S/U behavior + // (e.g. paged data translation) can pre-position the core in that mode + // instead of running a boot-time mret. null keeps the machine-mode reset. + int? resetPrivilege, super.name = 'river_core', }) : super('RiverCore') { final wbConfig = @@ -79,6 +95,9 @@ class RiverCore extends BridgeModule { // High when a committing ebreak should enter Debug Mode (dcsr.ebreak* set for // the current privilege) instead of taking a breakpoint trap. Logic? ebreakDebug; + // Single-step in flight: set on resume when dcsr.step is high, cleared when + // the one stepped instruction commits (which re-enters Debug Mode, cause 4). + Logic? stepping; Logic? haltReqIn; Logic? resumeReqIn; // Abstract-command register access (driven by the Debug Module while halted). @@ -93,6 +112,22 @@ class RiverCore extends BridgeModule { Logic? dbgRegAddr12; // low 12 bits of the regno = the CSR address Logic? dbgCsrSel; // halted && the regno is a CSR (borrow the CSR read port) Logic? dbgCsrData; // CSR file read result for a debug CSR access + // Hardware execute triggers (mcontrol, type 2). Present only when + // debugTriggers > 0. tdata1[i]/tdata2[i] are the per-trigger config/match; + // triggerMatch fires when an enabled execute trigger matches the current PC. + final hasTriggers = withDebug && debugTriggers > 0; + final tselectW = hasTriggers + ? (debugTriggers <= 1 ? 1 : (debugTriggers - 1).bitLength) + : 1; + Logic? tselect; + final List tdata1 = []; + final List tdata2 = []; + Logic? triggerMatch; // execute trigger matched the committing PC this cycle + Logic? dbgTselWrite; // debugger writing tselect + Logic? dbgTdata1Write; // debugger writing tdata1 (selected trigger) + Logic? dbgTdata2Write; // debugger writing tdata2 (selected trigger) + Logic? dbgTrigRdata; // read value for tselect/tdata1/tdata2/tinfo + Logic? dbgIsTrigCsr; // regno is one of the trigger CSRs (0x7a0..0x7a4) if (withDebug) { createPort('debug_halt_req', PortDirection.input); createPort('debug_resume_req', PortDirection.input); @@ -114,6 +149,7 @@ class RiverCore extends BridgeModule { debugDpc = Logic(name: 'debugDpc', width: config.mxlen.size); debugDcsr = Logic(name: 'debugDcsr', width: 32); ebreakDebug = Logic(name: 'ebreakDebug'); + stepping = Logic(name: 'stepping'); output('debug_halted') <= debugHalted; output('debug_dpc') <= debugDpc; @@ -140,8 +176,76 @@ class RiverCore extends BridgeModule { dbgCsrSel = ~isGpr & debugHalted; dbgCsrData = Logic(name: 'dbgCsrData', width: config.mxlen.size); } - // The register file is zero-latency, so the access is always ready. - output('debug_reg_ready') <= Const(1); + // debug_reg_ready is driven below, once `regs` exists: it must honor the + // regfile read latency (a registered BRAM read is 1+ cycles behind the + // address), otherwise the Debug Module latches stale read data. + + // Hardware execute triggers (RISC-V trigger module, mcontrol type 2). + // OpenOCD programs tdata1/tdata2 over the abstract command while halted; + // an enabled execute trigger whose tdata2 matches the committing PC fires + // a cause-2 debug entry BEFORE the matched instruction runs (dpc = pc). + if (hasTriggers) { + tselect = Logic(name: 'tselect', width: tselectW); + for (var i = 0; i < debugTriggers; i++) { + tdata1.add(Logic(name: 'tdata1_$i', width: 32)); + tdata2.add(Logic(name: 'tdata2_$i', width: config.mxlen.size)); + } + final isTsel = dbgRegAddr12.eq(0x7a0); + final isTd1 = dbgRegAddr12.eq(0x7a1); + final isTd2 = dbgRegAddr12.eq(0x7a2); + final isTinfo = dbgRegAddr12.eq(0x7a4); + dbgIsTrigCsr = isTsel | isTd1 | isTd2 | isTinfo; + final wr = input('debug_reg_write') & debugHalted; + dbgTselWrite = wr & isTsel; + dbgTdata1Write = wr & isTd1; + dbgTdata2Write = wr & isTd2; + // Read multiplexers over the selected trigger. tselect out of range + // reads 0 so a debugger can count triggers (0..debugTriggers-1). Widen + // tselect before the compare: debugTriggers may not fit in tselectW bits + // (e.g. 4 needs 3 bits but tselectW is 2), which would truncate the bound + // and force the reads to 0 (breaks OpenOCD trigger discovery). + final selInRange = tselect.zeroExtend(32).lt(debugTriggers); + Logic selTdata1 = Const(0, width: 32); + Logic selTdata2 = Const(0, width: config.mxlen.size); + for (var i = 0; i < debugTriggers; i++) { + selTdata1 = mux(tselect.eq(i), tdata1[i], selTdata1); + selTdata2 = mux(tselect.eq(i), tdata2[i], selTdata2); + } + selTdata1 = mux(selInRange, selTdata1, Const(0, width: 32)); + selTdata2 = mux( + selInRange, + selTdata2, + Const(0, width: config.mxlen.size), + ); + // tinfo: bit N set for supported trigger type N. Only mcontrol (2). + dbgTrigRdata = mux( + isTsel, + tselect.zeroExtend(config.mxlen.size), + mux( + isTd1, + selTdata1.zeroExtend(config.mxlen.size), + mux( + isTd2, + selTdata2, + Const(1 << 2, width: config.mxlen.size), // tinfo + ), + ), + ); + // Combinational execute-match against the committing PC. + Logic m = Const(0); + for (var i = 0; i < debugTriggers; i++) { + final t1 = tdata1[i]; + final typeOk = t1.getRange(28, 32).eq(2); + final execute = t1[2]; + final actionDebug = t1.getRange(12, 16).eq(1); + final modeEn = + (t1[6] & mode.eq(PrivilegeMode.machine.id)) | + (t1[4] & mode.eq(PrivilegeMode.supervisor.id)) | + (t1[3] & mode.eq(PrivilegeMode.user.id)); + m = m | (typeOk & execute & actionDebug & modeEn & tdata2[i].eq(pc)); + } + triggerMatch = m.named('triggerMatch'); + } } final pagingMode = Logic( @@ -287,6 +391,7 @@ class RiverCore extends BridgeModule { final icMemDone = Logic(name: 'icMemDone'); final icMemValid = Logic(name: 'icMemValid'); + final icMemFault = Logic(name: 'icMemFault'); final icMemRdata = Logic(name: 'icMemRdata', width: config.mxlen.size); final icFlush = Logic(name: 'icFlush'); // Driven from pipeline.fence below (forward ref); flushes the MMU fetch TLB. @@ -312,6 +417,7 @@ class RiverCore extends BridgeModule { icache.input('flush').srcConnection! <= icFlush; icache.input('mem_done').srcConnection! <= icMemDone; icache.input('mem_valid').srcConnection! <= icMemValid; + icache.input('mem_fault').srcConnection! <= icMemFault; icache.input('mem_rdata').srcConnection! <= icMemRdata; if (dualDispatch) { icache.input('req_addr1').srcConnection! <= pipeFetchRead1!.addr; @@ -381,9 +487,14 @@ class RiverCore extends BridgeModule { privMode: config.mmu.hasPaging ? mode : null, sum: config.mmu.hasPaging ? enableSum : null, mxr: config.mmu.hasPaging ? enableMxr : null, - // Translate instruction fetches (below M-mode). Gated off when an icache - // sits in front, since the icache does not yet propagate ifetch_fault. - translateFetch: config.mmu.hasPaging && !useICache, + // Translate instruction fetches (below M-mode). The icache is VIRTUALLY + // addressed, so its refill request carries a virtual address that the MMU + // MUST translate; gating this off left the refill reading the untranslated + // virtual address, which fetches garbage under any non-identity map (e.g. + // Linux's swapper mapping virtual 0xffffffff8000xxxx -> physical + // 0x8aaxxxxx). Fetch page-fault (ifetch_fault) reporting through the icache + // is still a gap, but a valid mapping (the common case) now translates. + translateFetch: config.mmu.hasPaging, tlbFlush: config.mmu.hasPaging ? mmuTlbFlush : null, dtlbFlushOnPrivChange: config.mmu.hasPaging ? dtlbFlushOnPriv : null, ); @@ -393,8 +504,11 @@ class RiverCore extends BridgeModule { // come from the cache. flush on fence.i (driven from the pipeline below). icMemDone <= mmu.ifetchDone; icMemValid <= mmu.ifetchValid; + icMemFault <= mmu.ifetchFault; icMemRdata <= mmu.ifetchRdata; - pipeFetchRead.done <= icache!.respValid; + // A faulting fetch is delivered as done AND not valid with respFault set, so + // the FetchUnit raises an instruction page fault (see ifetchFault below). + pipeFetchRead.done <= icache!.respValid | icache.respFault; pipeFetchRead.valid <= icache.respValid; pipeFetchRead.data <= icache.respData; if (dualDispatch) { @@ -563,16 +677,18 @@ class RiverCore extends BridgeModule { final gprOrCsr = dbgCsrData == null ? regs.rd0Data : mux(dbgIsGpr!, regs.rd0Data, dbgCsrData); + final nonTrig = mux( + dbgIsDcsr!, + debugDcsr!.zeroExtend(config.mxlen.size), + mux( + dbgIsMisa!, + Const(config.isa.misaValue, width: config.mxlen.size), + mux(dbgIsDpc!, debugDpc!, gprOrCsr), + ), + ); + // Trigger CSRs (tselect/tdata1/tdata2/tinfo) win when addressed. output('debug_reg_rdata') <= - mux( - dbgIsDcsr!, - debugDcsr!.zeroExtend(config.mxlen.size), - mux( - dbgIsMisa!, - Const(config.isa.misaValue, width: config.mxlen.size), - mux(dbgIsDpc!, debugDpc!, gprOrCsr), - ), - ); + (hasTriggers ? mux(dbgIsTrigCsr!, dbgTrigRdata!, nonTrig) : nonTrig); } rs2Read.data <= mux(rs2Read.en, regs.rd1Data, Const(0, width: config.mxlen.size)); @@ -597,6 +713,18 @@ class RiverCore extends BridgeModule { rdWrite.done <= rdWrite.en; rdWrite.valid <= rdWrite.en; + // The Debug Module borrows read port 0 to service abstract register-access + // commands. Its ready must lag reg_read by the regfile read latency (the same + // delay the operand read applies), otherwise the DM latches stale read data on + // a registered-BRAM regfile (readLatency >= 1, e.g. the Xilinx/ECP5 builds). + if (withDebug) { + output('debug_reg_ready') <= + delayReadHandshake( + input('debug_reg_read') | input('debug_reg_write'), + 'dbgRegReady', + ); + } + // Interrupts. Logic externalPending = Const(0); for (final entry in srcIrqs.entries) { @@ -604,6 +732,18 @@ class RiverCore extends BridgeModule { final anyFromThis = sig.or(); externalPending = externalPending | anyFromThis; } + // Per-cause CLINT lines: timer -> mip.MTIP, software -> mip.MSIP. Kept + // distinct from externalPending (MEIP) so the SBI timer and IPIs reach the + // right mcause, not the external-interrupt handler. + final timerPendingIn = timerPending == null + ? null + : addInput('timerPending', timerPending); + final swPendingIn = swPending == null + ? null + : addInput('swPending', swPending); + final timeInIn = timeIn == null + ? null + : addInput('timeIn', timeIn, width: timeIn.width); // CSR file. final csrRead = DataPortInterface(config.mxlen.size, 12); @@ -731,6 +871,9 @@ class RiverCore extends BridgeModule { mhartid: config.hartId, rpipelineCap: config.rpipelineCap, externalPending: externalPending, + timerPending: timerPendingIn, + swPending: swPendingIn, + timeIn: timeInIn, hasSupervisor: config.hasSupervisor, hasUser: config.hasUser, hasHypervisor: config.hasHypervisor, @@ -1074,6 +1217,80 @@ class RiverCore extends BridgeModule { ? null : addInput('prfSeedMode', prfSeedMode); + // Async interrupt take. Computes the highest-priority pending+enabled + // interrupt; the exec vectors it at an instruction boundary. M-interrupts + // (MSI/MTI/MEI = bits 3/7/11) are pending/enabled in mip/mie; S-interrupts + // (SSI/STI/SEI = 1/5/9) in the separate sip/sie (Weir writes sip.STIP for the + // SBI timer). Global enable per RISC-V: an M-interrupt is taken in S/U + // always and in M only if mstatus.MIE; an S-interrupt is taken in U always + // and in S only if sstatus.SIE, never in M. Priority MEI>MSI>MTI>SEI>SSI>STI. + Logic? interruptTake; + Logic? interruptCause; + if (csrs != null) { + final xlen = config.mxlen.size; + final isM = mode.eq(Const(PrivilegeMode.machine.id, width: 3)); + final isS = mode.eq(Const(PrivilegeMode.supervisor.id, width: 3)); + final isU = mode.eq(Const(PrivilegeMode.user.id, width: 3)); + final mMie = csrs.mstatus[3]; // mstatus.MIE + final sSie = (csrs.sstatus ?? csrs.mstatus)[1]; // sstatus.SIE + final mGlobal = ((isM & mMie) | ~isM).named('mIntGlobal'); + final sGlobal = ((isS & sSie) | isU).named('sIntGlobal'); + final mPend = (csrs.mip & csrs.mie).named('mIntPend'); + final sPend = config.hasSupervisor + ? (csrs.sip! & csrs.sie!).named('sIntPend') + : Const(0, width: xlen); + // (bit, isSupervisor), listed lowest priority first so the folds below let + // the highest priority win. + const order = [ + (5, true), (1, true), (9, true), // STI, SSI, SEI + (7, false), (3, false), (11, false), // MTI, MSI, MEI + ]; + Logic take = Const(0); + Logic cause = Const(0, width: 6); + for (final entry in order) { + final bit = entry.$1; + final isSup = entry.$2; + final t = + ((isSup ? sPend[bit] : mPend[bit]) & (isSup ? sGlobal : mGlobal)) + .named('intTake_$bit'); + cause = mux(t, Const(bit, width: 6), cause); + take = take | t; + } + // Register the interrupt-take decision. The mip/mie/mstatus/mode fold above + // otherwise sits combinationally in series with the exec -> nextPc -> fetch + // redirect (exec.dart:973 muxes the WHOLE exec output on interruptTake), + // and its wide fanout congests the fabric. On the timing-marginal openXC7 + // delta build that combinational path costs ~7 MHz of core Fmax (measured: + // 41 MHz interrupt-off vs 34 MHz interrupt-on), and openXC7 STA is + // optimistic about the core clock, so that margin is load-bearing on + // silicon (an async interrupt taken here intermittently wedged the fetch). + // Delaying the take by one cycle is safe: the interrupt is async and + // level-sensitive, and the readyExecution gate plus the post-redirect fetch + // bubble let mstatus.MIE clear before the next instruction boundary, so a + // just-taken interrupt cannot double-fire. + final interruptTakeReg = Logic(name: 'interruptTakeReg'); + final interruptCauseReg = Logic(name: 'interruptCauseReg', width: 6); + Sequential(clk, [ + If( + reset, + then: [interruptTakeReg < 0, interruptCauseReg < 0], + orElse: [interruptTakeReg < take, interruptCauseReg < cause], + ), + ]); + // Re-apply the global interrupt-enable COMBINATIONALLY. interruptTakeReg + // reflects the enable state from one cycle ago; if software just cleared + // SIE/MIE (csrrci sstatus/mstatus to enter a critical section) the stale + // registered take must not fire a SPURIOUS interrupt into the now-disabled + // section. The heavy mip&mie + priority fold stays registered (the timing + // win); the global enable is a couple of mstatus bits, cheap and off the + // critical path. The registered cause identifies the privilege: S-causes + // are 1/5/9 (cause[1]=0), M-causes are 3/7/11 (cause[1]=1). + final takeSup = (~interruptCauseReg[1]).named('intTakeSup'); + final curGlobal = mux(takeSup, sGlobal, mGlobal).named('curIntGlobal'); + interruptTake = (interruptTakeReg & curGlobal).named('interruptTake'); + interruptCause = interruptCauseReg.named('interruptCause'); + } + // Pipeline. pipeline = RiverPipeline( clk, @@ -1111,6 +1328,8 @@ class RiverCore extends BridgeModule { medeleg: csrs?.medeleg, mtvec: csrs?.mtvec, stvec: csrs?.stvec, + interruptTake: interruptTake, + interruptCause: interruptCause, mepc: csrs?.mepc, sepc: (csrs != null && config.hasSupervisor) ? csrs.sepc : null, virt: virt, @@ -1121,8 +1340,8 @@ class RiverCore extends BridgeModule { prfSeedEn: prfSeedModeIn == null ? null : (prfSeedModeIn & rdWrite.en), prfSeedAddr: prfSeedModeIn == null ? null : rdWrite.addr, prfSeedData: prfSeedModeIn == null ? null : rdWrite.data, - ifetchFault: (config.mmu.hasPaging && !useICache) - ? mmu.ifetchFault + ifetchFault: config.mmu.hasPaging + ? (useICache ? icache!.respFault : mmu.ifetchFault) : null, rdWrite1: rdWrite1, wr0Ready: wr0Ready, @@ -1183,7 +1402,12 @@ class RiverCore extends BridgeModule { csrTrapTargetIsM <= pipeline.nextMode.eq(Const(PrivilegeMode.machine.id, width: 3)); csrTrapPc <= pipeline.trapEpc; - csrTrapCauseVal <= pipeline.trapCause.zeroExtend(xlen); + // mcause = (interrupt << XLEN-1) | cause. trapCause carries only the low + // cause code (also used for delegation); the interrupt bit rides the + // separate trapInterrupt signal. + csrTrapCauseVal <= + (pipeline.trapInterrupt.zeroExtend(xlen) << (xlen - 1)) | + pipeline.trapCause.zeroExtend(xlen); csrTrapTval <= pipeline.trapTval; csrReturnActive <= committing & pipeline.isReturn; csrReturnFromM <= pipeline.returnLevel.eq(Const(3, width: 3)); @@ -1251,6 +1475,16 @@ class RiverCore extends BridgeModule { ? Const(0, width: xlen) : (csrs!.vstvec! & ~Const(0x3, width: xlen)); + // The PC the committing instruction advances to (xRET restores from *epc, + // everything else takes the pipeline's next PC). Used as dpc for single-step. + final committedNextPc = mux( + pipeline.isReturn, + retPc, + (csrTrapToVS == null + ? pipeline.nextPc + : mux(csrTrapToVS, vsTrapPc, pipeline.nextPc)), + ); + // Core state machine. The normal (non-halted) advance body, captured so // debug-halt can gate it. final coreBody = [ @@ -1297,6 +1531,21 @@ class RiverCore extends BridgeModule { // Speculative fetch keeps the pipeline enabled and self-sequences, // so the commit fires every `done` cycle (distinct instructions). if (!config.speculativeFetch) pipelineEnable < 0, + // Single-step: this commit is the one stepped instruction; re-enter + // Debug Mode at the next PC (cause 4) and freeze the pipeline. + if (withDebug) + If( + stepping!, + then: [ + debugHalted! < 1, + pipelineEnable < 0, + debugDpc! < committedNextPc, + debugDcsr! < + (debugDcsr! & Const(0xFFFFFE3F, width: 32)) | + Const(4 << 6, width: 32), + stepping! < Const(0), + ], + ), ], ), // Re-enable the pipeline once `done` drops (the next fetch is @@ -1314,12 +1563,21 @@ class RiverCore extends BridgeModule { pipelineEnable < 0, pc < config.resetVector, sp < 0, - // RISC-V resets to machine mode (PrivilegeMode.machine == 3). - mode < PrivilegeMode.machine.id, + // RISC-V resets to machine mode (PrivilegeMode.machine == 3). Tests + // may override this to pre-position the core in S/U (resetPrivilege). + mode < (resetPrivilege ?? PrivilegeMode.machine.id), if (virt != null) virt < 0, fence < 0, interruptHold < 0, if (withDebug) debugHalted! < 0, + if (withDebug) stepping! < Const(0), + // Trigger reset: type=2 (mcontrol), no control bits, no match addr. + if (hasTriggers) tselect! < Const(0, width: tselectW), + if (hasTriggers) + for (var i = 0; i < debugTriggers; i++) ...[ + tdata1[i] < Const(2 << 28, width: 32), + tdata2[i] < Const(0, width: config.mxlen.size), + ], if (withDebug) debugDpc! < config.resetVector, // dcsr reset: debugver=4 (0.13.2), prv=3 (machine), cause=0. if (withDebug) debugDcsr! < Const(0x40000003, width: 32), @@ -1336,7 +1594,7 @@ class RiverCore extends BridgeModule { // A debugger may rewrite dpc to redirect where we resume. If( input('debug_reg_write') & dbgIsDpc!, - then: [debugDpc! < dbgRegWdata!], + then: [debugDpc! < dbgRegWdata], ), // A debugger may write dcsr (ebreak/step/prv bits). debugver // (31:28) is read-only and cause (8:6) is hardware-set, so @@ -1345,13 +1603,55 @@ class RiverCore extends BridgeModule { input('debug_reg_write') & dbgIsDcsr!, then: [ debugDcsr! < - (dbgRegWdata.getRange(0, 32) & + (dbgRegWdata!.getRange(0, 32) & Const(0x0FFFFE3F, width: 32)) | Const(0x40000000, width: 32) | (debugDcsr & Const(0x000001C0, width: 32)), ], ), - If(resumeReqIn!, then: [debugHalted < 0, pc < debugDpc]), + // A debugger programs the hardware triggers (tselect picks + // one; tdata1 is the mcontrol config, type forced to 2; + // tdata2 is the match address). + if (hasTriggers) ...[ + If( + dbgTselWrite!, + then: [tselect! < dbgRegWdata!.getRange(0, tselectW)], + ), + If( + dbgTdata1Write!, + then: [ + for (var i = 0; i < debugTriggers; i++) + If( + tselect!.eq(i), + then: [ + tdata1[i] < + (dbgRegWdata!.getRange(0, 32) & + Const(0x0FFFFFFF, width: 32)) | + Const(2 << 28, width: 32), + ], + ), + ], + ), + If( + dbgTdata2Write!, + then: [ + for (var i = 0; i < debugTriggers; i++) + If( + tselect!.eq(i), + then: [tdata2[i] < dbgRegWdata!], + ), + ], + ), + ], + If( + resumeReqIn!, + then: [ + debugHalted < 0, + pc < debugDpc, + // Arm single-step for this resume if dcsr.step is set. + stepping! < debugDcsr![2], + ], + ), ], orElse: [ If( @@ -1366,20 +1666,55 @@ class RiverCore extends BridgeModule { Const(3 << 6, width: 32), ], orElse: [ - If( - ebreakDebug!, - then: [ - // ebreak entered Debug Mode: freeze at the ebreak, - // latch its pc into dpc, cause = 1 (ebreak). - debugHalted < 1, - pipelineEnable < 0, - debugDpc < pipeline.trapEpc, - debugDcsr < - (debugDcsr & Const(0xFFFFFE3F, width: 32)) | - Const(1 << 6, width: 32), - ], - orElse: coreBody, - ), + // A hardware execute trigger fires BEFORE its matched + // instruction runs: freeze at pc, cause = 2 (trigger). + // The ternary keeps debugTriggers==0 byte-identical. + hasTriggers + ? If( + triggerMatch!, + then: [ + debugHalted < 1, + pipelineEnable < 0, + debugDpc < pc, + debugDcsr < + (debugDcsr & + Const(0xFFFFFE3F, width: 32)) | + Const(2 << 6, width: 32), + ], + orElse: [ + If( + ebreakDebug!, + then: [ + debugHalted < 1, + pipelineEnable < 0, + debugDpc < pipeline.trapEpc, + debugDcsr < + (debugDcsr & + Const( + 0xFFFFFE3F, + width: 32, + )) | + Const(1 << 6, width: 32), + ], + orElse: coreBody, + ), + ], + ) + : If( + ebreakDebug!, + then: [ + // ebreak entered Debug Mode: freeze at the + // ebreak, latch pc into dpc, cause = 1. + debugHalted < 1, + pipelineEnable < 0, + debugDpc < pipeline.trapEpc, + debugDcsr < + (debugDcsr & + Const(0xFFFFFE3F, width: 32)) | + Const(1 << 6, width: 32), + ], + orElse: coreBody, + ), ], ), ], diff --git a/packages/river_hdl/lib/src/core/compressed_fetch_buffer.dart b/packages/river_hdl/lib/src/core/compressed_fetch_buffer.dart index 9ba4931..351923e 100644 --- a/packages/river_hdl/lib/src/core/compressed_fetch_buffer.dart +++ b/packages/river_hdl/lib/src/core/compressed_fetch_buffer.dart @@ -27,6 +27,12 @@ class CompressedFetchBuffer extends Module { Logic get valid1 => output('valid1'); Logic get compressed1 => output('compressed1'); + /// Asserted with `valid0` when the head instruction could not be fetched + /// because its translation faulted (the refill returned done AND not valid with + /// `fault` set). The pipeline runs the slot as a bubble and the exec stage + /// raises an instruction page fault at [pc0] instead of executing. + Logic get fetchFault => output('fetch_fault'); + /// FIFO depth in words (power of two >= 4 so a 4-halfword window always spans /// available words even at 32-bit data width). final int depth; @@ -41,6 +47,7 @@ class CompressedFetchBuffer extends Module { Logic? redirectPc, Logic? consume0, Logic? consume1, + Logic? fault, this.depth = 4, super.name = 'compressed_fetch_buffer', }) : super(definitionName: 'CompressedFetchBuffer') { @@ -61,6 +68,7 @@ class CompressedFetchBuffer extends Module { ); consume0 = addInput('consume0', consume0 ?? Const(0)); consume1 = addInput('consume1', consume1 ?? Const(0)); + fault = addInput('fault', fault ?? Const(0)); memRead = memRead.clone() ..connectIO( @@ -79,6 +87,7 @@ class CompressedFetchBuffer extends Module { addOutput('pc1', width: w); addOutput('valid1'); addOutput('compressed1'); + addOutput('fetch_fault'); final dataW = memRead.data.width; final wordBytes = dataW ~/ 8; @@ -106,6 +115,10 @@ class CompressedFetchBuffer extends Module { final reading = Logic(name: 'reading'); final discard = Logic(name: 'discard'); final started = Logic(name: 'started'); + // Held once the head-word fetch faults, until a redirect flushes the buffer + // (the exec stage traps and resteers). While set the head slot is presented + // as a valid bubble carrying fetch_fault. + final faulted = Logic(name: 'faulted'); Logic wordAtRel(int rel) { // wordArr[(head + rel) mod depth] @@ -151,15 +164,20 @@ class CompressedFetchBuffer extends Module { final aligner = InstructionAligner(window, validHalves, laneCount: 4); - instr0 <= aligner.instr0; + // A faulting head is a bubble: present a NOP (addi x0,x0,0) so the decoder + // resolves cleanly in one cycle, valid0 high so it reaches exec, and + // fetch_fault set so exec raises the instruction page fault at pc0 (headPc, + // the faulting PC). No second lane on a fault. + instr0 <= mux(faulted, Const(0x13, width: 32), aligner.instr0); pc0 <= headPc; - valid0 <= aligner.valid0 & enable; - compressed0 <= aligner.compressed0; + valid0 <= (aligner.valid0 | faulted) & enable; + compressed0 <= mux(faulted, Const(0), aligner.compressed0); instr1 <= aligner.instr1; // pc1 = headPc + size0*2. pc1 <= headPc + (aligner.size0.zeroExtend(w) << 1); - valid1 <= aligner.valid1 & enable; + valid1 <= aligner.valid1 & ~faulted & enable; compressed1 <= aligner.compressed1; + fetchFault <= faulted & enable; // -- Consume / advance ------------------------------------------------- final c0 = (consume0 & aligner.valid0 & enable & ~redirect).named('c0'); @@ -183,6 +201,21 @@ class CompressedFetchBuffer extends Module { // -- Read engine (fill the word FIFO; held-en, response-attributed) ----- final readDone = (memRead.done & memRead.valid).named('read_done'); + // The head-word read faulted (done AND not valid with `fault` set) while the + // FIFO is empty, so the faulting word IS the head instruction. Buffered valid + // words ahead of it are consumed first; the read holds at the faulting word + // (produce stays low) until the FIFO drains, then this catches. + final faultCatch = + (reading & + memRead.done & + ~memRead.valid & + fault & + ~discard & + ~redirect & + enable & + wordCount.eq(0) & + ~faulted) + .named('fault_catch'); final produce = (reading & readDone & ~discard & ~redirect & enable & ~fifoFull).named( 'produce', @@ -205,13 +238,14 @@ class CompressedFetchBuffer extends Module { reading < 0, discard < 0, started < 0, + faulted < 0, memRead.en < 0, memRead.addr < 0, ], orElse: [ If( ~enable, - then: [reading < 0, discard < 0, memRead.en < 0], + then: [reading < 0, discard < 0, faulted < 0, memRead.en < 0], orElse: [ If( redirect, @@ -224,6 +258,7 @@ class CompressedFetchBuffer extends Module { fetchPc < (redirectPc & alignMask), reading < 1, discard < 1, // drop the one stale in-flight word + faulted < 0, // the trap resteered; the fault is delivered memRead.en < 1, memRead.addr < (redirectPc & alignMask), ], @@ -245,6 +280,8 @@ class CompressedFetchBuffer extends Module { headPc < headPc + (consumed.zeroExtend(w) << 1), fetchPc < nFetchPc, reading < 1, + // Latch a head-word fetch fault; held until a redirect flushes. + If(faultCatch, then: [faulted < 1]), memRead.en < 1, If( ~started, diff --git a/packages/river_hdl/lib/src/core/csr.dart b/packages/river_hdl/lib/src/core/csr.dart index c749d76..509b445 100644 --- a/packages/river_hdl/lib/src/core/csr.dart +++ b/packages/river_hdl/lib/src/core/csr.dart @@ -80,8 +80,14 @@ class SimpleRwCsr extends CsrConfig { } class CounterCsr extends CsrConfig { + // mcycle/minstret are M-mode read/write per the privileged spec, so the + // access MUST be readWrite. The access also gates the backdoor write path: + // rohd_hcl runs every backdoor write value through Csr.getWriteData, which + // for a readOnly register returns the CURRENT value and drops the new data. + // With readOnly the per-cycle hardware increment (see _wireCounters) was + // silently discarded, so the counters stayed stuck at their reset value 0. CounterCsr(String name) - : super(name: name, access: CsrAccess.readOnly, fields: const []); + : super(name: name, access: CsrAccess.readWrite, fields: const []); } class RiscVCsrFile extends Module { @@ -120,6 +126,10 @@ class RiscVCsrFile extends Module { CsrBackdoorInterface? _mcycleBd; CsrBackdoorInterface? _minstretBd; + // The live machine-timer value (CLINT mtime), read out for the `time` CSR + // (rdtime). Null when the SoC has no CLINT, in which case `time` is not added. + Logic? _timeIn; + // Trap save-state / xRET restore controls (driven by core.dart). All // optional; when null the trap CSRs are not hardware-written (csrr/csrw work). Logic? _trapActive; // 1-cycle pulse: a synchronous trap is retiring @@ -145,6 +155,9 @@ class RiscVCsrFile extends Module { int mhartid = 0, int rpipelineCap = 0, Logic? externalPending, + Logic? timerPending, + Logic? swPending, + Logic? timeIn, this.hasSupervisor = false, this.hasUser = false, this.hasPaging = false, @@ -181,6 +194,19 @@ class RiscVCsrFile extends Module { width: externalPending.width, ); } + // Machine timer/software interrupt-pending lines, driven by the CLINT + // (timer_irq = mtime>=mtimecmp -> mip.MTIP; sw_irq = msip -> mip.MSIP). + // Read-only to software, hardware-owned, mirroring externalPending -> MEIP. + if (timerPending != null) { + timerPending = addInput('timerPending', timerPending); + } + if (swPending != null) { + swPending = addInput('swPending', swPending); + } + // Live CLINT mtime, exposed to software through the read-only `time` CSR. + if (timeIn != null) { + _timeIn = addInput('timeIn', timeIn, width: timeIn.width); + } _trapActive = trapActive == null ? null @@ -228,6 +254,8 @@ class RiscVCsrFile extends Module { addOutput('satp', width: mxlen.size); addOutput('sepc', width: mxlen.size); addOutput('sstatus', width: mxlen.size); + addOutput('sie', width: mxlen.size); + addOutput('sip', width: mxlen.size); } if (hasHypervisor) { @@ -348,6 +376,10 @@ class RiscVCsrFile extends Module { _csrTop.getBackdoorPortsByAddr(0, CsrAddress.sepc.address).rdData!; output('sstatus') <= _csrTop.getBackdoorPortsByAddr(0, CsrAddress.sstatus.address).rdData!; + output('sie') <= + _csrTop.getBackdoorPortsByAddr(0, CsrAddress.sie.address).rdData!; + output('sip') <= + _csrTop.getBackdoorPortsByAddr(0, CsrAddress.sip.address).rdData!; } if (hasHypervisor) { @@ -373,9 +405,18 @@ class RiscVCsrFile extends Module { } final mipBd = _csrTop.getBackdoorPortsByAddr(0, CsrAddress.mip.address); - if (externalPending != null) { + // Hardware owns the machine interrupt-pending bits: MEIP(11)<-externalPending, + // MTIP(7)<-timerPending, MSIP(3)<-swPending. Each is set from its line when + // present; the other mip bits (the WARL S-bits, if any) pass through the + // read-back value so a software write to them survives. + if (externalPending != null || timerPending != null || swPending != null) { + var mipNext = mip; + if (externalPending != null) + mipNext = mipNext.withSet(11, externalPending); + if (timerPending != null) mipNext = mipNext.withSet(7, timerPending); + if (swPending != null) mipNext = mipNext.withSet(3, swPending); mipBd.wrEn! <= Const(1); - mipBd.wrData! <= mip.withSet(11, externalPending); + mipBd.wrData! <= mipNext; } else { // Must still drive the backdoor write port: an undriven wrEn floats to X // and the CsrBlock's ElseIf(backdoorWrEn) corrupts mip to X. @@ -516,6 +557,39 @@ class RiscVCsrFile extends Module { isBackdoorWritable: false, ), + // mcounteren: the machine counter-enable register. The privileged spec + // requires it when U-mode is implemented. It gates U-mode access to the + // cycle/time/instret counters. Only CY/TM/IR (bits 2:0) are writable, one + // per implemented counter; the HPM bits are WARL-0 (mask in + // _maskWriteData). Weir writes 0x7 to it during the S-mode handoff, and a + // Linux kernel likewise programs it, so an absent register would trap the + // write as illegal. + if (hasUser) + CsrInstanceConfig( + arch: SimpleRwCsr('mcounteren', mxlen.size), + addr: CsrAddress.mcounteren.address, + resetValue: 0, + width: mxlen.size, + isBackdoorWritable: false, + ), + + // menvcfg: the machine environment-configuration register. The privileged + // spec requires it when S-mode is implemented. OpenSBI/Weir and Linux + // both read/write it (Sstc STCE, PBMTE, CBZE/CBIE, FIOM). River supports + // none of those features, so every field is WARL-0 (mask 0 in + // _maskWriteData): writes are dropped, reads return 0. That is the correct + // "feature absent" report - e.g. Linux's try_to_set_pmm reads PMM back as + // 0 and gracefully concludes pointer masking is unavailable. An ABSENT + // register would instead trap the access as illegal. + if (hasSupervisor) + CsrInstanceConfig( + arch: SimpleRwCsr('menvcfg', mxlen.size), + addr: CsrAddress.menvcfg.address, + resetValue: 0, + width: mxlen.size, + isBackdoorWritable: false, + ), + // Smstateen machine-level state-enable CSRs. Only SE0 (bit 63) is writable // (masked in _maskWriteData); the access gating lives in the legality path. if (hasStateen) @@ -614,6 +688,34 @@ class RiscVCsrFile extends Module { width: mxlen.size, isBackdoorWritable: false, ), + // scounteren: the supervisor counter-enable register. The privileged + // spec requires it when S-mode is implemented. It gates U-mode access to + // the cycle/time/instret counters. Only CY/TM/IR (bits 2:0) are + // writable; the HPM bits are WARL-0 (mask in _maskWriteData). The Linux + // RISC-V head code writes it unconditionally, so an absent register + // traps the write as illegal and stops the kernel before start_kernel. + CsrInstanceConfig( + arch: SimpleRwCsr('scounteren', mxlen.size), + addr: CsrAddress.scounteren.address, + resetValue: 0, + width: mxlen.size, + isBackdoorWritable: false, + ), + // senvcfg: the supervisor environment-configuration register. Required + // when S-mode is implemented (priv spec 1.12+). Linux writes it from the + // context-switch path (envcfg_update_bits) and probes it in + // try_to_set_pmm/tagged_addr_init. River implements none of its features + // (Zicbo CBIE/CBCFE/CBZE, pointer-masking PMM, FIOM), so every field is + // WARL-0 (mask 0 in _maskWriteData): writes drop, reads return 0. That + // correctly reports "feature absent"; an ABSENT register would trap the + // csrw/csrr as illegal (fu_csr raises cause 2 on an unimplemented CSR). + CsrInstanceConfig( + arch: SimpleRwCsr('senvcfg', mxlen.size), + addr: CsrAddress.senvcfg.address, + resetValue: 0, + width: mxlen.size, + isBackdoorWritable: false, + ), ], // Hypervisor (H) + VS-shadow CSRs. Gated on hasHypervisor. hgeip is @@ -762,6 +864,10 @@ class RiscVCsrFile extends Module { isBackdoorWritable: true, ), + // NOTE: `time` (rdtime, 0xC01) is NOT registered as a CsrBlock CSR (that + // perturbs rohd_hcl's backdoor indexing). Its read legality and data are + // handled directly in _wireLegalityAndFrontdoor from the live CLINT mtime. + // River custom cache control CSRs CsrInstanceConfig( arch: SimpleRwCsr('rcachectl', mxlen.size), @@ -914,7 +1020,17 @@ class RiscVCsrFile extends Module { Logic _maskWriteData(Logic addr12, Logic data) { Logic out = data; - final vecMask = Const(0xFFFFFFFC, width: mxlen.size); + // *tvec BASE is the full XLEN address (bits [xlen-1:2]); only the 2-bit MODE + // field [1:0] is WARL (River implements direct=0). A 0xFFFFFFFC literal here + // truncated the base to 32 bits, so an RV64 high-virtual trap vector + // (0xffffffff8000xxxx, e.g. Linux relocate_enable_mmu's stvec) read back as + // its low 32 bits and the trampoline fault looped. Mask all base bits. + final vecMask = Const( + LogicValue.ofBigInt( + (BigInt.one << mxlen.size) - BigInt.from(4), + mxlen.size, + ), + ); final fullMask = Const(~0, width: mxlen.size); Logic applyMask(int addr, Logic mask) { @@ -943,9 +1059,36 @@ class RiscVCsrFile extends Module { Const(_sieSipMask, width: mxlen.size), ); out = applyMask(CsrAddress.satp.address, fullMask); + // scounteren: only the counters River actually implements are writable + // (WARL). CY (bit0) and IR (bit2) are backed by mcycle/minstret, so they + // stay writable. TM (bit1) is WARL-0 because River has NO native `time` + // CSR wired to the CLINT mtime: the time CSR reads 0, so S-mode rdtime + // MUST keep trapping to the SBI (Weir) timer emulation, which reads the + // real mtime over the bus. Leaving TM writable let firmware's 0x7 write + // enable a direct S-mode read of the dead time CSR (always 0), which + // stalled systemd-boot's countdown timer forever. Mask = CY|IR = 0x5. + out = applyMask( + CsrAddress.scounteren.address, + Const(0x5, width: mxlen.size), + ); + // senvcfg/menvcfg: River implements none of the envcfg-controlled features + // (Zicbo, pointer-masking, Sstc, Svpbmt), so all fields are WARL-0. Mask 0 + // drops every write and the register reads back its reset value (0). This + // is the correct "feature absent" report and, crucially, makes Linux's + // try_to_set_pmm read PMM back as 0 and disable pointer masking instead of + // assuming a masking feature River does not actually provide. + out = applyMask(CsrAddress.senvcfg.address, Const(0, width: mxlen.size)); + out = applyMask(CsrAddress.menvcfg.address, Const(0, width: mxlen.size)); } if (hasUser) { + // mcounteren: same counter set as scounteren. TM (bit1) is WARL-0 (no + // native `time` CSR; rdtime is SBI-emulated), CY|IR stay writable. See the + // scounteren note above. Weir writes 0x7 here, TM lands as 0. + out = applyMask( + CsrAddress.mcounteren.address, + Const(0x5, width: mxlen.size), + ); out = applyMask( CsrAddress.ustatus.address, Const(_ustatusMask, width: mxlen.size), @@ -1014,8 +1157,18 @@ class RiscVCsrFile extends Module { rdAddr12 <= vsRedirect(csrRead.addr.slice(11, 0), 'rd'); wrAddr12 <= vsRedirect(csrWrite.addr.slice(11, 0), 'wr'); + // `time` (0xC01) is served from the live CLINT mtime, not the CsrBlock, so + // it is legal to read (at any privilege, U-level CSR) whenever mtime is + // wired. Its data is muxed in below. + final isTimeRd = (_timeIn == null) + ? Const(0) + : rdAddr12 + .eq(Const(CsrAddress.time.address, width: 12)) + .named('csrIsTime'); final rdLegal = - _addrExists(rdAddr12) & _privOk(rdAddr12) & _stateenOk(rdAddr12); + (_addrExists(rdAddr12) | isTimeRd) & + _privOk(rdAddr12) & + _stateenOk(rdAddr12); // _isFrontdoorWritable is a strict subset of _addrExists (same register // list, readWrite regs only), so it implies _addrExists. Dropping the // redundant existence term removes the _addrExists OR-tree from @@ -1027,7 +1180,14 @@ class RiscVCsrFile extends Module { _fdRead.addr <= rdAddr12; _fdRead.en <= csrRead.en & rdLegal; - csrRead.data <= _fdRead.data; + // `time` (rdtime) returns the live CLINT mtime, not a stored register, so the + // OS clocksource tracks the same counter its timer events compare against. + if (_timeIn != null) { + csrRead.data <= + mux(isTimeRd, _timeIn!.getRange(0, mxlen.size), _fdRead.data); + } else { + csrRead.data <= _fdRead.data; + } csrRead.done <= csrRead.en; csrRead.valid <= csrRead.en & rdLegal; @@ -1230,6 +1390,8 @@ class RiscVCsrFile extends Module { Logic? get stvec => hasSupervisor ? output('stvec') : null; Logic? get sstatus => hasSupervisor ? output('sstatus') : null; + Logic? get sie => hasSupervisor ? output('sie') : null; + Logic? get sip => hasSupervisor ? output('sip') : null; Logic? get hstatus => hasHypervisor ? output('hstatus') : null; Logic? get hedeleg => hasHypervisor ? output('hedeleg') : null; Logic? get vstvec => hasHypervisor ? output('vstvec') : null; diff --git a/packages/river_hdl/lib/src/core/debug_subsystem.dart b/packages/river_hdl/lib/src/core/debug_subsystem.dart index c96d8c4..ca6235e 100644 --- a/packages/river_hdl/lib/src/core/debug_subsystem.dart +++ b/packages/river_hdl/lib/src/core/debug_subsystem.dart @@ -3,7 +3,6 @@ import 'package:rohd_bridge/rohd_bridge.dart'; import 'package:river/river.dart'; import 'debug.dart'; -import 'jtag_bscan_tunnel.dart'; import 'sba_wishbone.dart'; /// SoC-level JTAG debug subsystem. On ECP5 it reaches the debugger over the FPGA @@ -39,8 +38,20 @@ class RiverDebugSubsystem extends BridgeModule implements HarborJtagDebug { target is HarborFpgaTarget && (target.vendor == HarborFpgaVendor.openXc7 || target.vendor == HarborFpgaVendor.vivado); + // Verilator has no config-JTAG primitive to tap, so there is no user + // register to tunnel through and no tunnel at all: the TAP is exposed as + // raw top-level pins for the harness to bit-bang. OpenOCD then talks to a + // plain RISC-V TAP, WITHOUT `riscv use_bscan_tunnel`. + final rawJtag = target is HarborSimTarget; createPort('clk', PortDirection.input); createPort('reset', PortDirection.input); + if (rawJtag) { + createPort('jtag_tck', PortDirection.input); + createPort('jtag_tms', PortDirection.input); + createPort('jtag_tdi', PortDirection.input); + createPort('jtag_trst', PortDirection.input); + addOutput('jtag_tdo'); + } // Core-facing: from the core. createPort('hart_halted', PortDirection.input); createPort('reg_rdata', PortDirection.input, width: xlen); @@ -64,11 +75,16 @@ class RiverDebugSubsystem extends BridgeModule implements HarborJtagDebug { // Tunnel: framed config-JTAG DR scan -> inner TAP signals. Fed by the // vendor's config-JTAG user-register primitive below. - final tunnel = JtagBscanTunnel(maxScanBits: xlen); - tunnel.input('clk').srcConnection! <= input('clk'); - tunnel.input('reset').srcConnection! <= input('reset'); + JtagBscanTunnel? tunnel; + if (!rawJtag) { + tunnel = JtagBscanTunnel(maxScanBits: xlen); + tunnel.input('clk').srcConnection! <= input('clk'); + tunnel.input('reset').srcConnection! <= input('reset'); + } - if (useBscane2) { + if (rawJtag) { + // Nothing between the pins and the TAP. + } else if (useBscane2) { // Xilinx 7-series BSCANE2 on USER4 (JTAG_CHAIN=4, IR 0x23). riscv-openocd's // bscan tunnel HARDCODES USER4 for tunneled DMI scans (riscv.c select_user4 // = 0x23), so the DM must ride USER4, not USER1, or SEL never asserts and @@ -78,24 +94,26 @@ class RiverDebugSubsystem extends BridgeModule implements HarborJtagDebug { // gated data-register clock, mis-frames it). The tunnel gates advance with // SEL & SHIFT; SEL = this user chain selected (the JCE1 equivalent); the // active-high RESET inverts to the tunnel's active-low jrstn. + final t = tunnel!; final bscan = XilinxBscane2(jtagChain: 4); - tunnel.input('jtck').srcConnection! <= bscan.output('TCK'); - tunnel.input('jtdi').srcConnection! <= bscan.output('TDI'); - tunnel.input('jshift').srcConnection! <= bscan.output('SHIFT'); - tunnel.input('jupdate').srcConnection! <= bscan.output('UPDATE'); - tunnel.input('jce1').srcConnection! <= bscan.output('SEL'); - tunnel.input('jrstn').srcConnection! <= ~bscan.output('RESET'); - bscan.input('TDO').srcConnection! <= tunnel.output('jtdo1'); + t.input('jtck').srcConnection! <= bscan.output('TCK'); + t.input('jtdi').srcConnection! <= bscan.output('TDI'); + t.input('jshift').srcConnection! <= bscan.output('SHIFT'); + t.input('jupdate').srcConnection! <= bscan.output('UPDATE'); + t.input('jce1').srcConnection! <= bscan.output('SEL'); + t.input('jrstn').srcConnection! <= ~bscan.output('RESET'); + bscan.input('TDO').srcConnection! <= t.output('jtdo1'); } else { // ECP5 config-JTAG user register taps (ER1). + final t = tunnel!; final jtagg = Ecp5Jtagg(); - tunnel.input('jtck').srcConnection! <= jtagg.output('JTCK'); - tunnel.input('jtdi').srcConnection! <= jtagg.output('JTDI'); - tunnel.input('jshift').srcConnection! <= jtagg.output('JSHIFT'); - tunnel.input('jupdate').srcConnection! <= jtagg.output('JUPDATE'); - tunnel.input('jce1').srcConnection! <= jtagg.output('JCE1'); - tunnel.input('jrstn').srcConnection! <= jtagg.output('JRSTN'); - jtagg.input('JTDO1').srcConnection! <= tunnel.output('jtdo1'); + t.input('jtck').srcConnection! <= jtagg.output('JTCK'); + t.input('jtdi').srcConnection! <= jtagg.output('JTDI'); + t.input('jshift').srcConnection! <= jtagg.output('JSHIFT'); + t.input('jupdate').srcConnection! <= jtagg.output('JUPDATE'); + t.input('jce1').srcConnection! <= jtagg.output('JCE1'); + t.input('jrstn').srcConnection! <= jtagg.output('JRSTN'); + jtagg.input('JTDO1').srcConnection! <= t.output('jtdo1'); jtagg.input('JTDO2').srcConnection! <= Const(0); } @@ -106,10 +124,11 @@ class RiverDebugSubsystem extends BridgeModule implements HarborJtagDebug { final dm = RiverDebugModule( input('clk'), input('reset'), - tunnel.output('inner_tck'), - tunnel.output('inner_tms'), - tunnel.output('inner_tdi'), - tunnel.output('inner_trst_n'), + rawJtag ? input('jtag_tck') : tunnel!.output('inner_tck'), + rawJtag ? input('jtag_tms') : tunnel!.output('inner_tms'), + rawJtag ? input('jtag_tdi') : tunnel!.output('inner_tdi'), + // The harness drives TRST active-high; the TAP wants active-low. + rawJtag ? ~input('jtag_trst') : tunnel!.output('inner_trst_n'), hartHalted: input('hart_halted'), regRdata: input('reg_rdata'), regReady: input('reg_ready'), @@ -118,7 +137,11 @@ class RiverDebugSubsystem extends BridgeModule implements HarborJtagDebug { xlen: xlen, idcode: idcode, ); - tunnel.input('inner_tdo').srcConnection! <= dm.tdo; + if (rawJtag) { + output('jtag_tdo') <= dm.tdo; + } else { + tunnel!.input('inner_tdo').srcConnection! <= dm.tdo; + } // DM outputs to the core. output('halt_req') <= dm.haltReq; diff --git a/packages/river_hdl/lib/src/core/decoder.dart b/packages/river_hdl/lib/src/core/decoder.dart index 3e9ba3d..8da214f 100644 --- a/packages/river_hdl/lib/src/core/decoder.dart +++ b/packages/river_hdl/lib/src/core/decoder.dart @@ -300,6 +300,32 @@ class DynamicInstructionDecoder extends InstructionDecoder { /// instruction re-searches fresh. late final Logic _held; + /// Decode is pipelined into two registered stages to cut the critical path + /// (which was instr -> match cone -> wide field-select Cases -> output regs, + /// 91% routing on the xc7s50). Stage 1 (the ROM search) latches ONLY the + /// match result into these registers; stage 2 drives the wide `type`/`opIndex` + /// field Cases from the REGISTERED opIndex/type, so the second cone is short + /// and local and `instr` is no longer its select. Costs one extra decode + /// cycle, negligible at ~67 CPI, and absorbed by the exec unit's existing + /// tolerance for variable decode latency (the ROM search is already variable). + late final Logic _matchedStage; + late final Logic _matchOpIndex; + late final Logic _matchType; + late final Logic _matchInstr; + + /// Armed the first time the ROM search reaches the zero-filled tail (mask==0) + /// with no match, to force ONE restart from index 0 before declaring illegal. + /// The scan counter is meant to start at 0 for each instruction (reset on + /// match, and at the enable-drop commit boundary), but at a jump/redirect the + /// next instruction can be fetched back-to-back and start its search from the + /// STALE counter, skip its own (earlier) pattern, and hit the tail. Restarting + /// once from 0 lets a valid instruction match on the clean pass; only a + /// genuine illegal reaches the tail again after a full 0-based scan. This is + /// the robust guard that the counter-reset alone did not cover (seen on HW as + /// an illegal trap on the `auipc` jalr-return targets in Linux's _start_kernel + /// call sequences). Cleared on a match and at the decode boundary (reset()). + late final Logic _rescanned; + DynamicInstructionDecoder( super.clk, super.reset, @@ -321,10 +347,26 @@ class DynamicInstructionDecoder extends InstructionDecoder { width: microcode.decodeLookup.length.bitLength, ); _held = Logic(name: 'held'); + _matchedStage = Logic(name: 'matchedStage'); + _rescanned = Logic(name: 'rescanned'); + _matchOpIndex = Logic(name: 'matchOpIndex', width: microcode.opIndexWidth); + _matchType = Logic( + name: 'matchType', + width: microcode.typeStructs.length.bitLength, + ); + _matchInstr = Logic(name: 'matchInstr', width: 32); } @override - List reset() => [_counter < 0, _held < 0]; + List reset() => [ + _counter < 0, + _held < 0, + _matchedStage < 0, + _rescanned < 0, + _matchOpIndex < 0, + _matchType < 0, + _matchInstr < 0, + ]; @override List decodeMicrocode( @@ -424,6 +466,71 @@ class DynamicInstructionDecoder extends InstructionDecoder { .eq(pattern['value']!) .named('patternMatch'); + // STAGE 2 field extraction, sourced from the REGISTERED match result. This + // is the identical field logic as before, just driven by _matchInstr / + // _matchType / _matchOpIndex so this cone launches from compact local + // registers instead of the 32-bit `instr` + the match cone. A function so + // each use gets fresh Conditionals (ROHD nodes cannot be shared). + List computeFields() => [ + _held < 1, + _matchedStage < 0, + index < _matchOpIndex.zeroExtend(index.width), + ...fields.entries.map((entry) => entry.value < 0), + ...instrTypeMap.entries.map((entry) => entry.value < 0), + Case(_matchType, [ + for (final e in instrTypeMap.entries.indexed) + CaseItem(Const(e.$1, width: instrTypeMap.length.bitLength), [ + e.$2.value < 1, + done < 1, + valid < 1, + ...microcode.typeStructs[e.$2.key]!.fields.entries + .where((entry) => entry.key != 'imm') + .map((entry) { + final fieldName = entry.key; + final fieldOutput = fields[fieldName]!; + final range = entry.value; + final extracted = _matchInstr.slice(range.end, range.start); + final value = extracted.width <= fieldOutput.width + ? extracted.zeroExtend(fieldOutput.width) + : extracted.slice(fieldOutput.width - 1, 0); + return fieldOutput < value.named(fieldName); + }), + fields['imm']! < decodeImm(e.$2.key, _matchInstr), + ]), + ]), + // Per-op override: implicit fixed registers + RVC immediate descramble, + // keyed on the matched opIndex (later so it wins). Also re-sourced. + if (overrideOps.isNotEmpty) + Case(_matchOpIndex, [ + for (final e in overrideOps) + CaseItem(Const(e.key, width: opIdxWidth), [ + fields['rd']! < compReg(e.value, _matchInstr, 'rd'), + fields['rs1']! < compReg(e.value, _matchInstr, 'rs1'), + fields['rs2']! < compReg(e.value, _matchInstr, 'rs2'), + fields['imm']! < immFor(e.value, _matchInstr), + ]), + ]), + ]; + + // "No valid decode this cycle" output clear (fresh Conditionals each call). + List clearOutputs() => [ + done < 0, + valid < 0, + index < 0, + ...instrTypeMap.entries.map((entry) => entry.value < 0), + ...fields.entries.map((entry) => entry.value < 0), + ]; + + // First cycle of a NEW instruction: the fetch holds `pc_in` stable across the + // multi-cycle scan, so `pc_in != pc_out` (pc_out is last cycle's pc_in) is + // high for exactly the first scan cycle. On that cycle we restart the ROM + // scan at row 0 COMBINATIONALLY, so a jump/redirect (e.g. a jalr return + // target fetched back-to-back with enable still high) can never inherit the + // previous instruction's stale scan index. This is the timing-independent + // form of the counter-reset: the register reset alone fixes only next cycle's + // address, which loses the race on HW under real fetch latency. + final newInstr = input('pc_in').neq(pcOut).named('decodeNewInstr'); + return [ If( _held, @@ -432,97 +539,139 @@ class DynamicInstructionDecoder extends InstructionDecoder { // reset() at the commit boundary. then: [microcodeRead.en < 0, done < 1, valid < 1], orElse: [ - microcodeRead.en < 1, - // _counter is sized for the unpacked pattern count; the packed ROM has - // ceil(patterns/lanes) words, so its address port is narrower. - microcodeRead.addr < _counter.getRange(0, microcodeRead.addr.width), If( - microcodeRead.done, - then: [ + _matchedStage, + // STAGE 2: the ROM search matched last cycle. Drive the wide field + // Cases off the registered opIndex/type/instr (a short, local cone) + // and hand off. No ROM access this cycle. + then: [microcodeRead.en < 0, ...computeFields()], + orElse: [ + // STAGE 1: search the microcode ROM, one packed row per cycle. + microcodeRead.en < 1, + // _counter is sized for the unpacked pattern count; the packed ROM + // has ceil(patterns/lanes) words, so its address port is narrower. + // On a new instruction, force the read address to row 0 THIS cycle. + microcodeRead.addr < + mux( + newInstr, + Const(0, width: microcodeRead.addr.width), + _counter.getRange(0, microcodeRead.addr.width), + ), If( - microcodeRead.valid, + newInstr, + // Fresh instruction: this cycle's ROM output is still the prior + // address (read latency 1), so ignore it. We prefetch row 0 now + // (address above) and set the counter so row 1 is prefetched next + // cycle; the scan then proceeds 0,1,2,... from a clean start. then: [ + _counter < Const(1, width: _counter.width), + _rescanned < 0, + ...clearOutputs(), + ], + orElse: [ If( - patternMatch & nzfMatch & zfMatch, + microcodeRead.done, then: [ - _held < 1, - index < pattern['opIndex']!.zeroExtend(index.width), - ...fields.entries.map((entry) => entry.value < 0), - ...instrTypeMap.entries.map((entry) => entry.value < 0), - Case(pattern['type']!, [ - for (final e in instrTypeMap.entries.indexed) - CaseItem( - Const(e.$1, width: instrTypeMap.length.bitLength), - [ - e.$2.value < 1, - done < 1, - valid < 1, - ...microcode.typeStructs[e.$2.key]!.fields.entries - .where((entry) => entry.key != 'imm') - .map((entry) { - final fieldName = entry.key; - final fieldOutput = fields[fieldName]!; - final range = entry.value; - final extracted = instr.slice( - range.end, - range.start, - ); - final value = - extracted.width <= fieldOutput.width - ? extracted.zeroExtend( - fieldOutput.width, - ) - : extracted.slice( - fieldOutput.width - 1, - 0, - ); - return fieldOutput < value.named(fieldName); - }), - fields['imm']! < decodeImm(e.$2.key, instr), + If( + microcodeRead.valid, + then: [ + If( + patternMatch & + nzfMatch & + zfMatch & + pattern['mask']!.neq(0), + // Match: latch ONLY the result; the field Cases run next + // cycle in stage 2. done/valid stay low until then. Exec + // already tolerates variable decode latency (the ROM + // search length itself varies). mask!=0 excludes the + // all-zero tail entries (see the end-of-ROM check below). + then: [ + _matchedStage < 1, + // Reset the ROM scan counter to 0 on every match so the + // NEXT instruction searches from index 0. The search + // otherwise stops AT the match index, and the + // decode-boundary reset (on `enable` dropping) does not + // always fire when the next instruction is fetched + // back-to-back at a jump/redirect (e.g. a jalr return + // target). Without this, that next instruction searches + // from the stale match index, skips its own (earlier) + // pattern, runs into the zero tail, and asserts a + // SPURIOUS illegal-instruction trap on a perfectly valid + // instruction. Seen on HW as an illegal trap on the + // `auipc` return targets in Linux's _start_kernel + // call sequences after the MMU is enabled. + _counter < 0, + _rescanned < 0, + _matchOpIndex < pattern['opIndex']!, + _matchType < pattern['type']!, + _matchInstr < instr, + done < 0, + valid < 0, + ], + orElse: [ + If( + pattern['mask']!.eq(0), + // Reached the zero-filled tail past the last real + // pattern with no match this whole (0-based) pass: the + // ROM read stays "valid" beyond the encoded patterns + // and returns all-zero words (the ROM zero-pads to its + // power-of-two depth). A zero pattern has mask 0 and + // would spuriously match EVERY instruction as op 0; no + // real op has mask 0 (each constrains its opcode). + // On the FIRST tail hit, restart the search from index + // 0 (arm _rescanned) instead of declaring illegal: the + // counter may have started stale at a jump/redirect + // (e.g. a jalr return target) and skipped this + // instruction's real pattern. A valid instruction + // matches on the clean 0-based pass. Only the SECOND + // tail hit (after a full 0-based scan found nothing, + // _rescanned set) is a genuine end-of-ROM -> assert + // illegal (done=1, valid=0). + then: [ + If( + _rescanned, + then: [ + done < 1, + valid < 0, + index < 0, + ...instrTypeMap.entries.map( + (entry) => entry.value < 0, + ), + ...fields.entries.map( + (entry) => entry.value < 0, + ), + ], + orElse: [ + _counter < 0, + _rescanned < 1, + ...clearOutputs(), + ], + ), + ], + orElse: [ + _counter < (_counter + 1), + ...clearOutputs(), + ], + ), ], ), - ]), - // Per-op override: apply implicit fixed registers and the RVC - // immediate descramble for compressed ops (the type-based - // extraction above is blind to op.fixedRs1 etc. and op.immKind). - // Keyed on the matched opIndex, later in the list so it wins. - if (overrideOps.isNotEmpty) - Case(pattern['opIndex']!, [ - for (final e in overrideOps) - CaseItem(Const(e.key, width: opIdxWidth), [ - fields['rd']! < compReg(e.value, instr, 'rd'), - fields['rs1']! < compReg(e.value, instr, 'rs1'), - fields['rs2']! < compReg(e.value, instr, 'rs2'), - fields['imm']! < immFor(e.value, instr), - ]), - ]), - ], - orElse: [ - _counter < (_counter + 1), - done < 0, - valid < 0, - index < 0, - ...instrTypeMap.entries.map((entry) => entry.value < 0), - ...fields.entries.map((entry) => entry.value < 0), + ], + orElse: [ + done < 1, + valid < 0, + index < 0, + ...instrTypeMap.entries.map( + (entry) => entry.value < 0, + ), + ...fields.entries.map((entry) => entry.value < 0), + ], + ), ], + orElse: clearOutputs(), ), ], - orElse: [ - done < 1, - valid < 0, - index < 0, - ...instrTypeMap.entries.map((entry) => entry.value < 0), - ...fields.entries.map((entry) => entry.value < 0), - ], ), ], - orElse: [ - done < 0, - valid < 0, - index < 0, - ...instrTypeMap.entries.map((entry) => entry.value < 0), - ...fields.entries.map((entry) => entry.value < 0), - ], ), ], ), diff --git a/packages/river_hdl/lib/src/core/exec.dart b/packages/river_hdl/lib/src/core/exec.dart index eaab3a0..dc280e0 100644 --- a/packages/river_hdl/lib/src/core/exec.dart +++ b/packages/river_hdl/lib/src/core/exec.dart @@ -137,6 +137,12 @@ abstract class ExecutionUnit extends Module { late final Logic? medeleg; late final Logic? mtvec; late final Logic? stvec; + // Async interrupt take (computed in core.dart from mip&mie + mode/delegation). + // When [interruptTake] is high at an instruction boundary (mopStep==0), an + // interrupt trap with cause [interruptCause] is taken instead of the fetched + // instruction, vectoring through the shared trap helpers. + late final Logic? interruptTake; + late final Logic? interruptCause; late final Logic? virtIn; // V-bit: VS-mode access to an HS-only CSR -> cause 22 // Smstateen SE0 bits, for the VS-mode state-enable virtual-instruction nuance. @@ -260,6 +266,7 @@ abstract class ExecutionUnit extends Module { Logic get nextMode => output('nextMode'); Logic get trap => output('trap'); Logic get trapCause => output('trapCause'); + Logic get trapInterrupt => output('trapInterrupt'); Logic get trapTval => output('trapTval'); Logic get trapEpc => output('trapEpc'); Logic get isReturn => output('isReturn'); @@ -297,6 +304,8 @@ abstract class ExecutionUnit extends Module { Logic? medeleg, Logic? mtvec, Logic? stvec, + Logic? interruptTake, + Logic? interruptCause, Logic? virtIn, Logic? mstateen0Se0, Logic? hstateen0Se0, @@ -449,6 +458,17 @@ abstract class ExecutionUnit extends Module { } else { this.stvec = null; } + if (interruptTake != null) { + this.interruptTake = addInput('interruptTake', interruptTake); + this.interruptCause = addInput( + 'interruptCause', + interruptCause!, + width: 6, + ); + } else { + this.interruptTake = null; + this.interruptCause = null; + } if (virtIn != null) { this.virtIn = addInput('virtIn', virtIn); } else { @@ -473,6 +493,10 @@ abstract class ExecutionUnit extends Module { addOutput('nextMode', width: 3); addOutput('trap'); addOutput('trapCause', width: 6); + // 1 when the committed trap is an interrupt (async), 0 for a synchronous + // exception. The core sets mcause bit XLEN-1 from this; trapCause carries + // only the low cause code (also used for delegation indexing). + addOutput('trapInterrupt'); addOutput('trapTval', width: mxlen.size); // PC of the trapping instruction → {m,s}epc. Captured here (not from the // core's live pc register, which has already advanced to tvec by the time @@ -528,6 +552,10 @@ abstract class ExecutionUnit extends Module { numEntries: 32, dataWidth: 64, name: 'fp_regfile', + // RISC-V has no hardwired-zero float register: f0/ft0 is a normal + // storage entry (unlike integer x0). Without this the default + // reservedZero=true forces f0 to read as zero regardless of writes. + reservedZero: false, ); fpRegs.input('clk').srcConnection! <= clk; fpRegs.input('reset').srcConnection! <= reset; @@ -867,6 +895,7 @@ abstract class ExecutionUnit extends Module { mopStep < 0, done < 0, output('trap') < 0, + output('trapInterrupt') < 0, output('trapEpc') < currentPc, output('isReturn') < 0, output('returnLevel') < 0, @@ -938,41 +967,72 @@ abstract class ExecutionUnit extends Module { output('memGuest') < 0, // A fetch fault means there is no instruction to run: raise an // instruction page fault at currentPc (the faulting PC) instead. + // An async interrupt is taken only at a CLEAN instruction boundary: + // mopStep==0 AND no memory or register-write side effect is in + // flight. mopStep==0 alone is NOT a clean boundary. An atomic runs + // its whole read-modify-write at mopStep==0 (the read-completion + // wrapper issues the write and the write-completion wrapper writes + // rd, neither advances mopStep), so mopStep stays 0 across the + // memRead wait, the memWrite wait and the rd commit. Taking the + // interrupt during that window lets the posted write commit on + // silicon while rd and the PC do not retire, so the atomic re-runs + // and applies the operation twice (a skipped ticket that deadlocks + // a ticket spinlock). It also leaves memRead/memWrite.en asserted + // into the handler, because rawTrap does not clear them. Gating on + // the held (registered) memRead.en, memWrite.en and rdWrite.en + // holds the interrupt off until the access retires, so the atomic + // is indivisible with respect to the interrupt. At a true boundary + // all three are 0 and epc is the not-yet-run instruction. It + // vectors through the same rawTrap path as a synchronous trap. If( - fetchFaultIn, - then: doTrap(Trap.instructionPageFault, currentPc), - orElse: microcodeRead != null - ? cycleMicrocode( - instrIndex, - mopStep, - microcodeRead, - alu: alu, - rs1: rs1, - rs2: rs2, - rd: rd, - imm: imm, - fields: fields, - memRead: memRead, - memWrite: memWrite, - rs1Read: rs1Read, - rs2Read: rs2Read, - rdWrite: rdWrite, - ) - : cycle( - instrIndex, - mopStep, - alu: alu, - rs1: rs1, - rs2: rs2, - rd: rd, - imm: imm, - fields: fields, - memRead: memRead, - memWrite: memWrite, - rs1Read: rs1Read, - rs2Read: rs2Read, - rdWrite: rdWrite, - ), + (this.interruptTake ?? Const(0)) & + mopStep.eq(0) & + ~memRead.en & + ~memWrite.en & + ~rdWrite.en, + then: rawTrap( + Const(1), + this.interruptCause ?? Const(0, width: 6), + Const(0, width: mxlen.size), + ), + orElse: [ + If( + fetchFaultIn, + then: doTrap(Trap.instructionPageFault, currentPc), + orElse: microcodeRead != null + ? cycleMicrocode( + instrIndex, + mopStep, + microcodeRead, + alu: alu, + rs1: rs1, + rs2: rs2, + rd: rd, + imm: imm, + fields: fields, + memRead: memRead, + memWrite: memWrite, + rs1Read: rs1Read, + rs2Read: rs2Read, + rdWrite: rdWrite, + ) + : cycle( + instrIndex, + mopStep, + alu: alu, + rs1: rs1, + rs2: rs2, + rd: rd, + imm: imm, + fields: fields, + memRead: memRead, + memWrite: memWrite, + rs1Read: rs1Read, + rs2Read: rs2Read, + rdWrite: rdWrite, + ), + ), + ], ), ], orElse: [ @@ -1129,6 +1189,7 @@ abstract class ExecutionUnit extends Module { if (csrRead == null || csrWrite == null) { return [ trapCause < encodeCause(trapInterrupt, effCause).slice(5, 0), + output('trapInterrupt') < trapInterrupt, trapTval < (tval ?? Const(0, width: mxlen.size)), output('trapEpc') < currentPc, output('trap') < 1, @@ -1155,6 +1216,7 @@ abstract class ExecutionUnit extends Module { trapInterrupt, effCause, ).slice(5, 0).named('cause$suffix'), + output('trapInterrupt') < trapInterrupt, trapTval < (tval ?? Const(0, width: mxlen.size)), output('trapEpc') < currentPc, @@ -1237,6 +1299,8 @@ class DynamicExecutionUnit extends ExecutionUnit { super.medeleg, super.mtvec, super.stvec, + super.interruptTake, + super.interruptCause, super.virtIn, super.mstateen0Se0, super.hstateen0Se0, @@ -2873,6 +2937,8 @@ class StaticExecutionUnit extends ExecutionUnit { super.medeleg, super.mtvec, super.stvec, + super.interruptTake, + super.interruptCause, super.virtIn, super.mstateen0Se0, super.hstateen0Se0, diff --git a/packages/river_hdl/lib/src/core/fetcher.dart b/packages/river_hdl/lib/src/core/fetcher.dart index 3006996..672c98c 100644 --- a/packages/river_hdl/lib/src/core/fetcher.dart +++ b/packages/river_hdl/lib/src/core/fetcher.dart @@ -370,14 +370,25 @@ class FetchUnit extends Module { // and starves the data port (a translated load would never run). enableRead < 0, memRead.addr < (pcLatch & alignment), - result < instrResult, - if (hasCompressed) compressed < isComp, + // On a fetch fault deliver a NOP (addi x0,x0,0): the fetched bits + // are garbage, and the microcode decoder will not validate garbage + // (decode_valid stays low), so exec never runs and the fetch_fault + // override never fires. A NOP decodes cleanly, exec runs, and the + // held fetch_fault turns it into an instruction page fault. + result < mux(faulted, Const(0x13, width: 32), instrResult), + if (hasCompressed) compressed < mux(faulted, Const(0), isComp), ]), - // Disabled: drop transient state. + // Disabled: drop transient state. `faulted` is per-instruction + // transient state too: the pipeline squashes the fetcher (~enable) + // when it traps on a fetch fault and resteers via currentPc, so if + // `faulted` is not dropped here it stays latched and the NEXT + // (successfully fetched) instruction is delivered with a stale + // fetch_fault -> a spurious instruction page fault loop. Iff(~enable, [ complete < 0, phase2 < 0, pcLatch < pc, + faulted < 0, if (hasCompressed) compressed < 0, enableRead < 0, memRead.addr < 0, diff --git a/packages/river_hdl/lib/src/core/jtag_bscan_tunnel.dart b/packages/river_hdl/lib/src/core/jtag_bscan_tunnel.dart deleted file mode 100644 index 49522ba..0000000 --- a/packages/river_hdl/lib/src/core/jtag_bscan_tunnel.dart +++ /dev/null @@ -1,175 +0,0 @@ -import 'package:rohd/rohd.dart'; - -/// SiFive-style JTAG BSCAN tunnel (NESTED_TAP variant): lets OpenOCD reach the -/// River debug module over the FPGA config JTAG (ECP5 `JTAGG` ER1 user register or -/// Xilinx `BSCANE2` USER4) instead of a separate GPIO TAP. The inner -/// [RiverDebugModule] keeps its full standard TAP; this module reconstructs that -/// TAP's `tck/tms/tdi` from a framed config-JTAG DR scan and returns its `tdo` on -/// `jtdo1`. -/// -/// Frame (one config-JTAG DR scan, LSB-first), matching riscv-openocd's -/// `BSCAN_TUNNEL_NESTED_TAP` frame (sel + width lead the Shift-DR window): -/// [1 bit] sel : 1 = inner DR scan, 0 = inner IR scan -/// [7 bits] width : inner scan length N, LSB first -/// [N+1] payload: inner TDI; the +1 is the one-TCK in/out skew OpenOCD -/// compensates by right-shifting the captured field -/// [3 bits] idle : zeros; carry the inner TAP Exit1 -> Update -> Run/Idle -/// Total frame = N + 12 bits. -/// -/// OpenOCD selects this tunnel with `riscv use_bscan_tunnel 0` (0 = -/// nested-tap; irwidth = inner DM IR width = 5). Hardware-proven on the Arty S7: -/// OpenOCD examines the RISC-V DM and reads DDR over SBA through this. -/// -/// TCK (~1 MHz) is asynchronous to the system clock, so every config-JTAG level is -/// passed through a 2-FF synchronizer before the frame FSM edge-detects `jtck` or -/// samples `jtdi`/drives the inner TMS, so a level in flight at a system clock edge -/// can't be latched metastable. The inner TAP is clocked only during the Shift-DR -/// window (`jce1 & jshift`) so it advances once per frame bit and is frozen between -/// frames; the FSM synthesizes a full inner TAP walk inside that window: -/// sel=1 (DR): RTI -> Select-DR -> Capture-DR -> Shift-DR(N) -> Exit1 -> Update -/// sel=0 (IR): RTI -> Select-DR -> Select-IR -> Capture-IR -> Shift-IR(N) -> ... -/// -/// The frame counter is restarted by the `~active` edge reset (a config-JTAG -/// edge outside the Shift-DR window), which anchors each scan to its Capture-DR. -/// An earlier BSCANE2-CAPTURE anchor input was tried and removed: CAPTURE held -/// wider than one Capture-DR state on silicon and pinned the counter, killing the -/// tunnel. Framing + inner-TAP walk are sim-tested against the real DM in -/// jtag_tunnel_dm_test.dart (the config-JTAG primitive is an unsimulatable -/// blackbox; the async synchronizers only matter on real silicon). -class JtagBscanTunnel extends Module { - /// Width of the widest inner scan (the DMI register, ~41 bits). Sizes the - /// payload counter. - final int maxScanBits; - - JtagBscanTunnel({this.maxScanBits = 64, super.name = 'jtag_bscan_tunnel'}) - : super(definitionName: 'JtagBscanTunnel') { - final clk = addInput('clk', Logic()); - final reset = addInput('reset', Logic()); - - // Config-JTAG user-register side (from Ecp5Jtagg or XilinxBscane2). - final jtck = addInput('jtck', Logic()); - final jtdi = addInput('jtdi', Logic()); - final jshift = addInput('jshift', Logic()); - final jupdate = addInput('jupdate', Logic()); - final jce1 = addInput('jce1', Logic()); - final jrstn = addInput('jrstn', Logic()); - final innerTdo = addInput('inner_tdo', Logic()); - - final jtdo1 = addOutput('jtdo1'); - final innerTck = addOutput('inner_tck'); - final innerTms = addOutput('inner_tms'); - final innerTdi = addOutput('inner_tdi'); - final innerTrstN = addOutput('inner_trst_n'); - - // 2-FF synchronizers for the asynchronous config-JTAG inputs. Bit 0 is the - // metastability-catcher, bit 1 the synchronized level the FSM consumes. - final jtckSync = Logic(name: 'jtck_sync', width: 2); - final jtdiSync = Logic(name: 'jtdi_sync', width: 2); - final jshiftSync = Logic(name: 'jshift_sync', width: 2); - final jce1Sync = Logic(name: 'jce1_sync', width: 2); - final jrstnSync = Logic(name: 'jrstn_sync', width: 2); - final jtckS = jtckSync[1]; - final jtdiS = jtdiSync[1]; - final jshiftS = jshiftSync[1]; - final jce1S = jce1Sync[1]; - final jrstnS = jrstnSync[1]; - - final cntW = (maxScanBits + 16).bitLength; - final cnt = Logic(name: 'bit_cnt', width: cntW); // frame bit index - final sel = Logic(name: 'sel'); // 1=DR, 0=IR - final width = Logic(name: 'width', width: 7); // inner scan length N - final jtckPrev = Logic(name: 'jtck_prev'); - final tdoCap = Logic(name: 'tdo_cap'); // registered inner tdo for jtdo1 - - final jtckRise = (jtckS & ~jtckPrev).named('jtck_rise'); - final active = (jce1S & jshiftS).named('tunnel_active'); // shifting DR - - // Header is 8 bits (sel + 7 width). The inner Shift window is bits - // 8 .. 8+N-1; OpenOCD's extra payload bit is the one-TCK TDO skew. - final headerBits = 8; - final shiftStart = Const(headerBits, width: cntW); - final lastShift = - (Const(headerBits, width: cntW) + - width.zeroExtend(cntW) - - Const(1, width: cntW)) - .named('last_shift'); - - final inShift = (cnt.gte(shiftStart) & cnt.lte(lastShift)).named( - 'in_shift', - ); - final atLastShift = cnt.eq(lastShift).named('at_last_shift'); - - // TMS schedule by frame bit. Inner TAP starts each frame in Run-Test/Idle. - // DR walk: tms=1 at bit 5 (RTI->Sel-DR); bits 6,7 tms=0 (Capture, Shift). - // IR walk: tms=1 at bits 4,5 (Sel-DR, Sel-IR); bits 6,7 tms=0. - // Then N shift bits 8..lastShift (tms=0); last asserts tms=1 (Shift->Exit1), - // bit lastShift+1 tms=1 (Exit1->Update), trailing bits tms=0 (Update->RTI). - final w4 = cnt.eq(Const(4, width: cntW)); - final w5 = cnt.eq(Const(5, width: cntW)); - final exitFirst = cnt.eq(lastShift + Const(1, width: cntW)); - - final tmsDr = w5.named('tms_dr_walk'); - final tmsIr = (w4 | w5).named('tms_ir_walk'); - final tmsVal = mux( - inShift, - atLastShift, // last shift bit exits Shift -> Exit1 - mux( - cnt.gt(lastShift), - exitFirst, // Exit1 -> Update on the first post-shift bit, then RTI - mux(sel, tmsDr, tmsIr), // header walk (DR vs IR) - ), - ).named('inner_tms_val'); - - Sequential(clk, reset: reset, [ - // Advance the input synchronizers every system clock. - jtckSync < [jtckSync[0], jtck].swizzle(), - jtdiSync < [jtdiSync[0], jtdi].swizzle(), - jshiftSync < [jshiftSync[0], jshift].swizzle(), - jce1Sync < [jce1Sync[0], jce1].swizzle(), - jrstnSync < [jrstnSync[0], jrstn].swizzle(), - - jtckPrev < jtckS, - If( - ~jrstnS, - then: [cnt < Const(0, width: cntW)], - orElse: [ - If( - jtckRise, - then: [ - If( - active, - then: [ - cnt < cnt + 1, - If(cnt.eq(Const(0, width: cntW)), then: [sel < jtdiS]), - // Shift width in LSB-first across bits 1..7. - If( - cnt.gte(Const(1, width: cntW)) & - cnt.lte(Const(7, width: cntW)), - then: [ - width < [jtdiS, width.getRange(1, 7)].swizzle(), - ], - ), - // Capture inner tdo while shifting (registered -> the +1 skew). - If(inShift, then: [tdoCap < innerTdo]), - ], - orElse: [ - // Between scans (outer Capture-DR / Update / idle): restart the - // frame index so the next Shift-DR window begins clean. - cnt < Const(0, width: cntW), - ], - ), - ], - ), - ], - ), - ]); - - // Inner TAP drive. Clock the inner TAP ONLY inside the Shift-DR window so it - // advances once per frame bit; tms/tdi are combinational per bit. - innerTck <= jtckS & active; - innerTrstN <= jrstnS; - innerTms <= tmsVal; - innerTdi <= mux(inShift, jtdiS, Const(0)); - jtdo1 <= tdoCap; - } -} diff --git a/packages/river_hdl/lib/src/core/microcode_alu.dart b/packages/river_hdl/lib/src/core/microcode_alu.dart index 8aed21a..b526335 100644 --- a/packages/river_hdl/lib/src/core/microcode_alu.dart +++ b/packages/river_hdl/lib/src/core/microcode_alu.dart @@ -54,9 +54,18 @@ class MicrocodeAlu extends Module { final srlw = (a.slice(31, 0) >>> shamtW).signExtend(xlen); final sraw = (a.slice(31, 0) >> shamtW).signExtend(xlen); - // Compares. - final slt = bmSignedLt(a, b, xlen).zeroExtend(xlen); - final sltu = a.lt(b).zeroExtend(xlen); + // Compares (the raw bits are reused by min/max below). + final sLtBit = bmSignedLt(a, b, xlen); + final uLtBit = a.lt(b); + final slt = sLtBit.zeroExtend(xlen); + final sltu = uLtBit.zeroExtend(xlen); + + // Zbb min/max: select a or b on the same comparators. Not emitted by the + // rc1-f ROM, but the AMO combine drives these functs to reuse this unit. + final minR = mux(sLtBit, a, b); + final maxR = mux(sLtBit, b, a); + final minuR = mux(uLtBit, a, b); + final maxuR = mux(uLtBit, b, a); // Zicond conditional-zero. final bZero = b.eq(Const(0, width: xlen)); @@ -87,6 +96,10 @@ class MicrocodeAlu extends Module { item(MicroOpAluFunct.sraw, sraw), item(MicroOpAluFunct.slt, slt), item(MicroOpAluFunct.sltu, sltu), + item(MicroOpAluFunct.minOp, minR), + item(MicroOpAluFunct.maxOp, maxR), + item(MicroOpAluFunct.minuOp, minuR), + item(MicroOpAluFunct.maxuOp, maxuR), item(MicroOpAluFunct.czeroEqz, czE), item(MicroOpAluFunct.czeroNez, czN), ], diff --git a/packages/river_hdl/lib/src/core/mmu.dart b/packages/river_hdl/lib/src/core/mmu.dart index eb1cb1a..f9e1574 100644 --- a/packages/river_hdl/lib/src/core/mmu.dart +++ b/packages/river_hdl/lib/src/core/mmu.dart @@ -202,12 +202,17 @@ class RiverMmu extends Module { final ftlbValid = Logic(name: 'ftlbValid'); final ftlbVpn = Logic(name: 'ftlbVpn', width: xlen - 12); final ftlbPte = Logic(name: 'ftlbPte', width: xlen); + // Leaf level of the cached fetch translation, so a hit composes the physical + // address with the right superpage offset. + final ftlbLevel = Logic(name: 'ftlbLevel', width: 3); // Single-entry data TLB (mirrors the fetch TLB). `dtlbPte` holds the leaf so // R/W/U re-checks per access. Single-stage only: guest (two-stage) accesses // always walk (the cached leaf would be guest-physical). Flushed with ftlb. final dtlbValid = Logic(name: 'dtlbValid'); final dtlbVpn = Logic(name: 'dtlbVpn', width: xlen - 12); final dtlbPte = Logic(name: 'dtlbPte', width: xlen); + // Leaf level of the cached data translation (see ftlbLevel). + final dtlbLevel = Logic(name: 'dtlbLevel', width: 3); final satpShadowMode = Logic(name: 'satpShadowMode', width: 4); final satpShadowRoot = Logic(name: 'satpShadowRoot', width: xlen); // Shadow of the privilege mode, to detect a context switch for DTLBFC. @@ -278,9 +283,28 @@ class RiverMmu extends Module { } Logic pteNextBase(Logic pte) => (pte.slice(53, 10) << 12).zeroExtend(xlen); - // Translated physical address for a 4KB leaf: {PTE.PPN, vaddr[11:0]}. - Logic leafPa(Logic pte, Logic vaddr) => - [pte.slice(53, 10), vaddr.slice(11, 0)].swizzle().zeroExtend(xlen); + // Translated physical address for a leaf at `level`. A leaf above level 0 is + // a superpage, so the low VPN fields come from the virtual address, not the + // PTE PPN: level 1 (2MB) keeps vaddr[20:0], level 2 (1GB) vaddr[29:0], level + // 3 (512GB, Sv48) vaddr[38:0]. Taking only vaddr[11:0] for every level would + // alias all sub-pages of a superpage to its base. + Logic leafPa(Logic pte, Logic vaddr, Logic level) { + final ppn = pte.slice(53, 10); // 44-bit PPN + // {ppn[43:hi], vaddr[lo:0]}: hi is the first PPN bit kept from the PTE, lo + // is the top virtual bit taken from vaddr. Each pairing sums to 56 bits. + Logic compose(int hi, int lo) => + [ppn.slice(43, hi), vaddr.slice(lo, 0)].swizzle().zeroExtend(xlen); + return mux( + level.eq(0), + compose(0, 11), + mux( + level.eq(1), + compose(9, 20), + mux(level.eq(2), compose(18, 29), compose(27, 38)), + ), + ); + } + // First-level (root) PTE byte address. Logic ptePtr(Logic base, Logic vpn) => base + (vpn.zeroExtend(xlen) << 3); final fullSel = Const((1 << selW) - 1, width: selW); @@ -317,6 +341,22 @@ class RiverMmu extends Module { priv.neq(Const(PrivilegeMode.machine.id, width: 3)) & (virtIn == null ? Const(1) : ~virtIn)); + // Data (load/store) translation is ALSO off in machine mode. River does not + // implement mstatus.MPRV, so the effective data privilege is just the + // current privilege: an M-mode load/store is always physical. Without this + // gate, once supervisor enables paging (satp.MODE != 0) an M-mode access + // (e.g. the SBI firmware restoring its own stack in a trap handler) would be + // walked through the SUPERVISOR page tables - its physical address is not a + // valid supervisor VA, so the walk faults or the bus access never returns + // and the core hangs. The one M-mode exception is an explicit virtualized + // access (HLV/HSV): those carry [virtIn] and must translate through the + // guest tables regardless of the current privilege, so OR virtIn back in. + final dataPagingOn = priv == null + ? pagingOn + : (pagingOn & + (priv.neq(Const(PrivilegeMode.machine.id, width: 3)) | + (virtIn ?? Const(0)))); + // Fetch-TLB lookup for the requested fetch address. final satpChanged = hasPaging ? (satpMode!.neq(satpShadowMode) | satpRoot!.neq(satpShadowRoot)) @@ -334,7 +374,7 @@ class RiverMmu extends Module { ? leafPermFault(ftlbPte, Const(1), Const(0)) : Const(0); final ftlbPa = hasPaging - ? leafPa(ftlbPte, ifetchAddr) + ? leafPa(ftlbPte, ifetchAddr, ftlbLevel) : Const(0, width: xlen); // G-stage derived signals. twoStage = guest mode with a non-bare G-stage. @@ -378,7 +418,7 @@ class RiverMmu extends Module { ? ((~wbDatMiso[6] | (reqWe & ~wbDatMiso[7])) & dtlbUsable) : Const(0); final dtlbPa = hasPaging - ? leafPa(dtlbPte, dportAddr) + ? leafPa(dtlbPte, dportAddr, dtlbLevel) : Const(0, width: xlen); Sequential(clk, [ @@ -406,6 +446,8 @@ class RiverMmu extends Module { dtlbVpn < 0, dtlbPte < 0, ftlbPte < 0, + ftlbLevel < 0, + dtlbLevel < 0, satpShadowMode < 0, satpShadowRoot < 0, privShadow < 0, @@ -505,7 +547,7 @@ class RiverMmu extends Module { gWalking < 0, gTranslated < 1, walkArmed < 1, - walkAddr < leafPa(wbDatMiso, gReqAddr), + walkAddr < leafPa(wbDatMiso, gReqAddr, gWalkLevel), weR < gSaveWe, datMosiR < gSaveData, selR < gSaveSel, @@ -592,6 +634,7 @@ class RiverMmu extends Module { ftlbValid < 1, ftlbVpn < reqAddr.slice(xlen - 1, 12), ftlbPte < pteWithAd, + ftlbLevel < walkLevel, ], orElse: [ If( @@ -600,6 +643,7 @@ class RiverMmu extends Module { dtlbValid < 1, dtlbVpn < reqAddr.slice(xlen - 1, 12), dtlbPte < pteWithAd, + dtlbLevel < walkLevel, ], ), ], @@ -612,7 +656,7 @@ class RiverMmu extends Module { // back to its address (walkAddr still holds the PTE // pointer), then resume with the translated access. adWrite < 1, - adTransPa < leafPa(wbDatMiso, reqAddr), + adTransPa < leafPa(wbDatMiso, reqAddr, walkLevel), walkArmed < 1, weR < 1, datMosiR < pteWithAd, @@ -621,7 +665,7 @@ class RiverMmu extends Module { orElse: [ // A/D already set: arm the translated access directly. walkArmed < 1, - walkAddr < leafPa(wbDatMiso, reqAddr), + walkAddr < leafPa(wbDatMiso, reqAddr, walkLevel), weR < reqWe, datMosiR < reqWdata, // A fetch reads a full word; a dport uses its size. @@ -800,7 +844,7 @@ class RiverMmu extends Module { isFetchWalk < 0, if (hasPaging) If( - pagingOn, + dataPagingOn, then: [ If( dtlbHit, diff --git a/packages/river_hdl/lib/src/core/pipeline.dart b/packages/river_hdl/lib/src/core/pipeline.dart index 060747a..38ca708 100644 --- a/packages/river_hdl/lib/src/core/pipeline.dart +++ b/packages/river_hdl/lib/src/core/pipeline.dart @@ -39,6 +39,7 @@ class RiverPipeline extends Module { Logic get nextMode => output('nextMode'); Logic get trap => output('trap'); Logic get trapCause => output('trapCause'); + Logic get trapInterrupt => output('trapInterrupt'); Logic get trapTval => output('trapTval'); Logic get trapEpc => output('trapEpc'); Logic get isReturn => output('isReturn'); @@ -93,6 +94,8 @@ class RiverPipeline extends Module { Logic? medeleg, Logic? mtvec, Logic? stvec, + Logic? interruptTake, + Logic? interruptCause, // mret/sret return targets (for the OoO commit-stage fetcher redirect). Logic? mepc, Logic? sepc, @@ -276,6 +279,10 @@ class RiverPipeline extends Module { } if (mtvec != null) mtvec = addInput('mtvec', mtvec, width: mxlen.size); if (stvec != null) stvec = addInput('stvec', stvec, width: mxlen.size); + if (interruptTake != null) { + interruptTake = addInput('interruptTake', interruptTake); + interruptCause = addInput('interruptCause', interruptCause!, width: 6); + } if (mepc != null) mepc = addInput('mepc', mepc, width: mxlen.size); if (sepc != null) sepc = addInput('sepc', sepc, width: mxlen.size); final prfSeedEnIn = prfSeedEn == null @@ -315,6 +322,7 @@ class RiverPipeline extends Module { addOutput('nextMode', width: 3); addOutput('trap'); addOutput('trapCause', width: 6); + addOutput('trapInterrupt'); addOutput('trapTval', width: mxlen.size); addOutput('trapEpc', width: mxlen.size); addOutput('isReturn'); @@ -381,6 +389,7 @@ class RiverPipeline extends Module { redirectPc: fetchRedirectPc, consume0: bufConsume0, consume1: bufConsume1, + fault: ifetchFaultIn, depth: prefetchDepth < 4 ? 4 : prefetchDepth, ); fetcher = cfb; @@ -448,12 +457,15 @@ class RiverPipeline extends Module { fetchOutPc = fetcher.output('pc_out'); } - // The fetch-fault marker (FetchUnit only; the prefetch/dual fetchers do not - // carry fetch faults yet). Used to raise an instruction page fault. + // The fetch-fault marker. The plain FetchUnit and the compressed fetch buffer + // carry fetch faults (delivered as a bubble that exec turns into an + // instruction page fault); the prefetch/pipelined fetchers do not yet. final usePlainFetchUnit = !useCompressedFetch && !usePrefetch && !usePipelined; final fetchFaultSig = usePlainFetchUnit ? fetcher.output('fetch_fault') + : useCompressedFetch + ? cfb!.fetchFault : Const(0); // Helper: resize signal to target width (truncate or zero-extend) @@ -551,6 +563,8 @@ class RiverPipeline extends Module { medeleg: medeleg, mtvec: mtvec, stvec: stvec, + interruptTake: interruptTake, + interruptCause: interruptCause, virtIn: virt, mstateen0Se0: mstateen0Se0, hstateen0Se0: hstateen0Se0, @@ -585,6 +599,8 @@ class RiverPipeline extends Module { medeleg: medeleg, mtvec: mtvec, stvec: stvec, + interruptTake: interruptTake, + interruptCause: interruptCause, virtIn: virt, mstateen0Se0: mstateen0Se0, hstateen0Se0: hstateen0Se0, @@ -597,9 +613,53 @@ class RiverPipeline extends Module { final execDone = exec.done; final execValid = exec.valid; + // Illegal-instruction detection is the pipeline's responsibility, not the + // exec unit's: a fetched instruction whose decode finished with no + // matching operation (decodeDone & ~decodeValid) is a reserved or + // unimplemented encoding, e.g. the all-zero 0x0000 a jump into cleared + // memory lands on. The pipeline asserts the illegal-instruction exception + // here so the core faults at once instead of silently advancing past it + // (which let a bad PC sled forward through zeroed memory). + final decodeIllegal = + (fetchOutValid & fetchOutDone & decodeDone & ~decodeValid).named( + 'decodeIllegal', + ); + final illegalCause = Const(2, width: 6); // illegal instruction + final illegalIsIntr = Const(0); + final Logic illegalTrapMode; + final Logic illegalTrapPc; + if (mtvec != null) { + illegalTrapMode = selectTrapTargetModeTop( + illegalIsIntr, + illegalCause, + currentMode, + mideleg, + medeleg, + hasCsr: csrRead != null && csrWrite != null, + hasSupervisor: hasSupervisor, + ).named('illegalTrapMode'); + final tvec = stvec != null + ? mux( + illegalTrapMode.eq(Const(PrivilegeMode.machine.id, width: 3)), + mtvec, + stvec, + ) + : mtvec; + illegalTrapPc = computeTrapVectorPcTop( + tvec, + illegalCause, + illegalIsIntr, + mxlen, + suffix: 'Illegal', + ); + } else { + illegalTrapMode = currentMode; + illegalTrapPc = currentPc; + } + Sequential(clk, [ If( - reset | ~execDone, + reset | (~execDone & ~decodeIllegal), then: [ done < 0, valid < 0, @@ -608,6 +668,7 @@ class RiverPipeline extends Module { nextMode < 0, trap < 0, trapCause < 0, + trapInterrupt < 0, trapTval < 0, trapEpc < 0, isReturn < 0, @@ -616,20 +677,46 @@ class RiverPipeline extends Module { counter < 0, ], orElse: [ - done < fetchOutDone & decodeDone & execDone, - valid < fetchOutValid & decodeValid & execValid, - nextSp < exec.nextSp, - nextPc < exec.nextPc, - nextMode < exec.nextMode, - trap < exec.trap, - trapCause < exec.trapCause, - trapTval < exec.trapTval, - trapEpc < exec.trapEpc, - isReturn < exec.isReturn, - returnLevel < exec.returnLevel, - fence < exec.fence, - interruptHold < exec.interruptHold, - If(enable, then: [counter < (counter + 1)]), + If( + decodeIllegal, + // Decode matched nothing: commit an illegal-instruction trap + // (cause 2) at the faulting PC. exec never ran for this cycle, so + // these outputs come straight from the pipeline. + then: [ + done < 1, + valid < 1, + nextSp < currentSp, + nextPc < illegalTrapPc, + nextMode < illegalTrapMode, + trap < 1, + trapCause < illegalCause, + trapInterrupt < 0, + trapTval < 0, + trapEpc < currentPc, + isReturn < 0, + returnLevel < 0, + fence < 0, + interruptHold < 0, + If(enable, then: [counter < (counter + 1)]), + ], + orElse: [ + done < fetchOutDone & decodeDone & execDone, + valid < fetchOutValid & decodeValid & execValid, + nextSp < exec.nextSp, + nextPc < exec.nextPc, + nextMode < exec.nextMode, + trap < exec.trap, + trapCause < exec.trapCause, + trapInterrupt < exec.trapInterrupt, + trapTval < exec.trapTval, + trapEpc < exec.trapEpc, + isReturn < exec.isReturn, + returnLevel < exec.returnLevel, + fence < exec.fence, + interruptHold < exec.interruptHold, + If(enable, then: [counter < (counter + 1)]), + ], + ), ], ), ]); @@ -2168,6 +2255,7 @@ class RiverPipeline extends Module { nextMode < 0, trap < 0, trapCause < 0, + trapInterrupt < 0, trapTval < 0, trapEpc < 0, isReturn < 0, @@ -2210,6 +2298,9 @@ class RiverPipeline extends Module { // Trap from ROB commit trap < (rob.commitValid0 & rob.commitException0), trapCause < rob.commitCause0, + // OoO path takes no async interrupts yet; ROB commits only + // synchronous exceptions. + trapInterrupt < 0, trapTval < Const(0, width: mxlen.size), trapEpc < rob.commitPc0, // Privileged return (mret/sret): core.dart restores pc<-{m,s}epc and diff --git a/packages/river_hdl/lib/src/genip.dart b/packages/river_hdl/lib/src/genip.dart index e36b0e5..cbeb0ad 100644 --- a/packages/river_hdl/lib/src/genip.dart +++ b/packages/river_hdl/lib/src/genip.dart @@ -92,6 +92,13 @@ class DeviceParams { /// HarborDdrController + DdrSequencer/DdrPhyXilinx. Implies ddr3Fast. final bool? ddr3v2; + /// DDR3 controller-logic gearing (ddr3v2 only). 1 (absent) = the controller + /// runs on CK/4 (byte-identical to today). 2 = the CK/8 gearbox controller: + /// the DDR MMCM emits CLKOUT5 as CK/8, HarborDdr3 interposes the fabric 2:1 + /// gearbox, and the congestion-limited command scheduler gets timing margin + /// on a dense open-tools part while DDR CK stays at full speed. + final int? ctrlGear; + /// Open the ddr3Fast read window from the DRAM's read strobe (DQS as data) so /// each read self-frames, instead of a fixed CL tap that mis-frames under the /// i+d cadence. ddr3Fast only. @@ -128,6 +135,44 @@ class DeviceParams { /// Built-in firmware program baked into flash (e.g. `hexdump`). final String? program; + // --- spi/sdio device --- + + /// Board connector this device's pads bind to (e.g. `iface=pmod@ja`). Resolved + /// against the selected [HarborBoard]'s `interfaces` catalog, so the SoC need + /// not hand-enter the connector's pin sites. Used by the `spi` device. + final String? iface; + + /// An SD/MMC card is wired to this SPI controller (CS0, SPI mode). genip then + /// has Harbor emit an `mmc-spi-slot` device-tree child so Linux binds the + /// in-tree `mmc_spi` driver and exposes a mountable block device. + final bool? sdcard; + + /// Number of hardware execute-breakpoint triggers on a `debug-jtag` device. + /// 0/absent = none (byte-identical to before). OpenOCD programs these over + /// JTAG so it can breakpoint even hot, I-cached code without patching it. + final int? triggers; + + /// Give this `spi` device an integrated DMA engine: a second fabric master + /// that streams SD bytes straight to memory (no per-byte CPU poll). absent/ + /// false = byte-identical slave-only PIO. Firmware finds it via the device's + /// `dma` device-tree/ACPI property. + final bool? dma; + + /// Put the DMA master on the PRIMARY fabric channel (shared crossbar with the + /// core) instead of its own `dma` channel. The separate channel lifts the + /// wide DMA leg off the primary crossbar, but on a small device it adds a + /// second crossbar plus a converge arbiter that becomes the routing hotspot; + /// sharing is the topology that provably closes on xc7s50. Costs DMA/CPU + /// fabric contention. absent/false = the separate `dma` channel. + final bool? dmaShared; + + /// Sample the SDIO read DAT lines on the SD clock FALLING edge (half a period + /// later) instead of the rising edge. Gives the card-to-host round-trip more + /// settle time, the fix for marginal read capture at speed on a real board. + /// It is also a runtime CTRL[8] bit, so this only sets the reset default. + /// absent/false = rising-edge sample (the standard host default). + final bool? sampleFall; + const DeviceParams({ this.trainable, this.runtimeTrain, @@ -145,6 +190,7 @@ class DeviceParams { this.mpr, this.ddr3Fast, this.ddr3v2, + this.ctrlGear, this.dqsGate, this.readClExtra, this.clockFreq, @@ -152,6 +198,12 @@ class DeviceParams { this.mode, this.path, this.program, + this.iface, + this.sdcard, + this.triggers, + this.dma, + this.dmaShared, + this.sampleFall, }); /// Accepted param keys (case-insensitive), for error messages. @@ -172,6 +224,7 @@ class DeviceParams { 'mpr', 'ddr3fast', 'ddr3v2', + 'ctrlgear', 'dqsgate', 'readclextra', 'clockfreq', @@ -179,6 +232,12 @@ class DeviceParams { 'mode', 'path', 'program', + 'iface', + 'sdcard', + 'triggers', + 'dma', + 'dmashared', + 'samplefall', ]; static bool _parseBool(String v) { @@ -217,6 +276,7 @@ class DeviceParams { bool? mpr; bool? ddr3Fast; bool? ddr3v2; + int? ctrlGear; bool? dqsGate; int? readClExtra; int? clockFreq; @@ -224,6 +284,12 @@ class DeviceParams { String? mode; String? path; String? program; + String? iface; + bool? sdcard; + int? triggers; + bool? dma; + bool? dmaShared; + bool? sampleFall; for (final pair in s.split(',')) { final eq = pair.indexOf('='); if (eq < 0) { @@ -267,6 +333,11 @@ class DeviceParams { ddr3Fast = _parseBool(val); case 'ddr3v2': ddr3v2 = _parseBool(val); + case 'ctrlgear': + ctrlGear = int.parse(val); + if (ctrlGear != 1 && ctrlGear != 2) { + throw FormatException('ctrlgear must be 1 or 2, got: $val'); + } case 'dqsgate': dqsGate = _parseBool(val); case 'readclextra': @@ -286,6 +357,18 @@ class DeviceParams { path = val; case 'program': program = val; + case 'iface': + iface = val; + case 'sdcard': + sdcard = _parseBool(val); + case 'triggers': + triggers = int.parse(val); + case 'dma': + dma = _parseBool(val); + case 'dmashared': + dmaShared = _parseBool(val); + case 'samplefall': + sampleFall = _parseBool(val); default: throw FormatException( 'Unknown device param "$key"; accepted: ${_keys.join(', ')}', @@ -309,6 +392,7 @@ class DeviceParams { mpr: mpr, ddr3Fast: ddr3Fast, ddr3v2: ddr3v2, + ctrlGear: ctrlGear, dqsGate: dqsGate, readClExtra: readClExtra, clockFreq: clockFreq, @@ -316,6 +400,12 @@ class DeviceParams { mode: mode, path: path, program: program, + iface: iface, + sdcard: sdcard, + triggers: triggers, + dma: dma, + dmaShared: dmaShared, + sampleFall: sampleFall, ); } } @@ -440,11 +530,16 @@ class DeviceEntry { final int address; final String? compatible; + /// Trailing `key=val` tuning params carried over from the unified device, so + /// peripheral construction can read them (e.g. the `spi` device's `sdcard`). + final DeviceParams? params; + const DeviceEntry({ required this.name, required this.type, required this.address, this.compatible, + this.params, }); /// Parses `[name=]type:addr[:compat]`. @@ -670,6 +765,20 @@ sealed class Target { static Target parse(String spec) { final parts = spec.split(':'); + // Verilator simulation target: `verilator`, optionally `verilator:trace`. + // It has no device or package, so it is handled before the + // vendor:device:package arity check below. + if (parts[0] == 'verilator' || parts[0] == 'sim') { + // `verilator[:trace][:threads=N]`. threads is the run-time thread count + // of the Verilated model; omit it for the single-threaded default. + var simThreads = 1; + for (final p in parts) { + if (p.startsWith('threads=')) { + simThreads = int.parse(p.substring('threads='.length)); + } + } + return SimTarget(trace: parts.contains('trace'), threads: simThreads); + } if (parts.length < 2) { throw FormatException( 'Target format: vendor:device[:package], got: $spec', @@ -768,6 +877,57 @@ class FpgaTarget extends Target { } } +/// Verilator simulation target. Selected with `--target verilator` (or +/// `verilator:trace`). It maps to Harbor's [HarborSimTarget], which drives the +/// Verilator build emission in `HarborSoC.generateAll` (the C++ harness +/// `sim/main.cpp`, the Verilator `Makefile`, and the remote_bitbang OpenOCD +/// config). Un-Verilatable vendor IP (the DDR PHY, config-JTAG primitives) +/// swaps to a behavioral body under this target, and the debug TAP is exposed +/// as real top-level pins for the harness to bit-bang, so no config-JTAG tunnel. +class SimTarget extends Target { + /// Emit FST waveform tracing (opt-in; a large run-time cost). + final bool trace; + + /// Verilator `--trace-depth`, ignored when [trace] is false. + final int traceDepth; + + /// Optimisation level for Verilator and the generated C++. + final int optLevel; + + /// Extra Verilator warnings to suppress on top of Harbor's defaults. + final List extraWarningsOff; + + /// Run-time thread count of the Verilated model (`--threads`). 1 keeps the + /// single-threaded model; above 1 partitions the sim across host threads. + final int threads; + + const SimTarget({ + this.trace = false, + this.traceDepth = 99, + this.optLevel = 3, + this.extraWarningsOff = const [], + this.threads = 1, + }); + + @override + HarborDeviceTarget toHarborTarget({ + required String topCell, + required int frequency, + // Verilator has no pins, constraints, or PDK; those are ignored. + Map pins = const {}, + Map extraConstraints = const {}, + String? pdkRoot, + }) => HarborSimTarget( + topCell: topCell, + frequency: frequency, + trace: trace, + traceDepth: traceDepth, + optLevel: optLevel, + extraWarningsOff: extraWarningsOff, + threads: threads, + ); +} + // TODO: replace with the harbor target class class AsicTarget extends Target { final String pdk; @@ -975,6 +1135,7 @@ class RiverGenIpConfig { type: d.type, address: d.address!, compatible: d.compatible, + params: d.params, ), ]; @@ -1019,6 +1180,11 @@ class RiverGenIpConfig { /// (TAP+DTM+DM+SBA) as a second fabric master and build the core with debug. bool get enableDebug => _firstDeviceOfType('debug-jtag') != null; + /// Number of hardware execute-breakpoint triggers requested on the debug-jtag + /// device (`debug-jtag:triggers=N`). 0 when absent. + int get debugTriggers => + _firstDeviceOfType('debug-jtag')?.params?.triggers ?? 0; + // --- flash-firmware (derived from a `flash-firmware` device) --- /// Built-in firmware program baked into flash (the device `program` param). @@ -1117,6 +1283,9 @@ class RiverGenIpConfig { 'rc1-mi': RiverCoreConfigV1.micro, 'rc1-s': RiverCoreConfigV1.small, 'rc1-m': RiverCoreConfigV1.macro, + // River Core V1 Full: the full ISA with the F/D FPU. This is the core the + // Delta SoC family carries, so a stock rv64gc/lp64d NixOS runs. + 'rc1-f': RiverCoreConfigV1.full, }; RiscVMxlen get mxlen { @@ -1231,13 +1400,75 @@ class RiverGenIpConfig { /// entries become device bindings, the rest are simple constraint pins. List get effectivePins { final b = board; - if (b == null) return pins; - final userNames = {for (final p in pins) p.externalName}; - return [ - for (final e in b.pins.entries) - if (!userNames.contains(e.key)) _boardPinAssignment(e.key, e.value), - ...pins, - ]; + final catalog = b == null + ? pins + : [ + for (final e in b.pins.entries) + if (!{for (final p in pins) p.externalName}.contains(e.key)) + _boardPinAssignment(e.key, e.value), + ...pins, + ]; + return [...catalog, ..._ifacePins]; + } + + /// SPI role -> the HarborSpiController pad each connector role drives. The + /// board connector bakes the wiring convention (e.g. Digilent Pmod-SPI), so + /// this is a fixed role vocabulary the `spi` device consumes. + static const _spiIfaceRoleToPort = { + 'cs': 'spi_cs_n', + 'mosi': 'spi_mosi', + 'miso': 'spi_miso', + 'sck': 'spi_clk', + }; + + /// Device-bound pin assignments synthesised from a device's `iface=`. + /// Each `spi` device with an interface binds its four pads to the named board + /// connector's `cs`/`mosi`/`miso`/`sck` sites. The external pin name is + /// prefixed with the device name so it never collides with the config-flash + /// SPI pads (which also expose `spi_cs_n`). + List get _ifacePins { + final out = []; + for (final dev in devices) { + final ifaceName = dev.params?.iface; + if (ifaceName == null) continue; + if (dev.type != 'spi') { + throw ArgumentError( + 'iface= is only supported on `spi` devices, not "${dev.type}"', + ); + } + final b = board; + if (b == null) { + throw ArgumentError( + 'device "${dev.name}" has iface=$ifaceName but no --board is set to ' + 'resolve the connector; add board= to the SoC', + ); + } + final conn = b.interfaces[ifaceName]; + if (conn == null) { + throw ArgumentError( + 'board "${b.name}" has no interface "$ifaceName"; ' + 'known: ${b.interfaces.keys.join(', ')}', + ); + } + _spiIfaceRoleToPort.forEach((role, port) { + final site = conn[role]; + if (site == null) { + throw ArgumentError( + 'interface "$ifaceName" on board "${b.name}" is missing the SPI ' + 'role "$role" (need cs/mosi/miso/sck)', + ); + } + out.add( + PinAssignment( + externalName: '${dev.name}_$role', + deviceName: dev.name, + portName: port, + fpgaPin: site, + ), + ); + }); + } + return out; } Map get fpgaPinMap => { @@ -1299,13 +1530,26 @@ class RiverGenIpConfig { // never leaves reset. Fold the core clock onto a spare CLKOUT of the DDR3 // MMCM instead (one MMCM on the pin). A separate `ddr_osc` pin has no // contention and keeps its own core MMCM. - final useDdr3TreeCoreClk = ddr3Fast && ddrOscFrequency == null; + // Under Verilator there is no Xilinx MMCM/PLL to build the DDR3 clock tree + // (BUFG/PLLE2_ADV have no sim model), and the behavioral DRAM runs off the + // plain bus clock, so never hang the core clock off a DDR tree in sim: fall + // back to the behavioral clock generation like a boardless SoC. + final useDdr3TreeCoreClk = + ddr3Fast && ddrOscFrequency == null && effectiveTarget is! SimTarget; + // DDR controller gearing (shared tree): the CK/8 gearbox controller when any + // ddr3v2 DRAM sets ctrlgear=2. One tree serves every controller, so they use + // one gearRatio (the per-device HarborDdr3 controllerGearRatio matches it). + final ddrGearRatio = memories + .where((m) => m.ddrParams?.ddr3v2 ?? false) + .map((m) => m.ddrParams?.ctrlGear ?? 1) + .fold(1, (a, b) => b > a ? b : a); final xilinxDdr3Tree = useDdr3TreeCoreClk ? XilinxDdr3TreeSpec( sourceHz: oscFrequency, ddrCkHz: ddrClockFrequency ?? 333333333, coreClkHz: clockFrequency, dqsPhaseDeg: 180.0, + ddrGearRatio: ddrGearRatio, ) : null; final coreClock = HarborClockConfig( @@ -1338,10 +1582,15 @@ class RiverGenIpConfig { final soc = HarborSoC( name: name, - compatible: 'midstall,${name.replaceAll('_', '-')}', + compatible: 'lilith,${name.replaceAll('_', '-')}', busConfig: busConfig, - acpiOemId: 'MIDSTL', + acpiOemId: 'LILSMI', acpiOemTableId: 'RIVER', + // An FPGA target normally resets only at configuration (power-on). A + // `reset_n=` pin (e.g. a board RESET button) adds an active-low + // external reset ORed into that POR, so a press restarts the SoC. The + // pad is constrained by the generic pin loop; here we just enable the port. + externalReset: pins.any((p) => p.externalName == 'reset_n'), cpus: coreConfigs .map( (coreConfig) => HarborCpu( @@ -1425,16 +1674,43 @@ class RiverGenIpConfig { ], ); + // The CLINT (if present) drives each hart's machine timer/software interrupt + // lines. The core is built before the peripherals, so make a net per hart + // now, feed it to the core, and connect the CLINT output to it after the + // peripheral loop below. + final hasClint = mmioDevices.any((d) => d.type == 'clint'); + final coreTimerNets = []; + final coreSwNets = []; + final coreTimeNets = []; + RiverCore? debugCore; + var hartIndex = 0; for (final coreConfig in coreConfigs) { + Logic? timerNet; + Logic? swNet; + Logic? timeNet; + if (hasClint) { + timerNet = Logic(name: 'core${hartIndex}_timer_pending'); + swNet = Logic(name: 'core${hartIndex}_sw_pending'); + // The 64-bit CLINT mtime, fed to the core's `time` CSR (rdtime). + timeNet = Logic(name: 'core${hartIndex}_time', width: 64); + coreTimerNets.add(timerNet); + coreSwNets.add(swNet); + coreTimeNets.add(timeNet); + } final core = RiverCore( coreConfig, busConfig: busConfig, target: target, withDebug: enableDebug, + debugTriggers: enableDebug ? debugTriggers : 0, + timerPending: timerNet, + swPending: swNet, + timeIn: timeNet, ); soc.addMaster(core, busInterfaceName: 'dataBus'); debugCore ??= core; + hartIndex++; } // Boot ROM. The hello-world demo bakes the application directly into the @@ -1477,6 +1753,41 @@ class RiverGenIpConfig { final mem = memories[i]; final board = mem.ddrBoard; if (board != null && ddr3Fast) { + // Verilator: HarborDdr3 builds a behavioral DRAM (_HarborSimDram) on the + // bus/sys clock, loaded at runtime via +dram_image=. It has no DDR3 + // clock tree (PLLE2/BUFG, which have no sim model) and no PHY pads, so + // skip all of that FPGA plumbing and just add it as a bus slave. The + // clock/period values are ignored by the behavioral body. + if (target is HarborSimTarget && (mem.ddrParams?.ddr3v2 ?? false)) { + final ddr = HarborDdr3( + config: board.config, + baseAddress: mem.address, + clockHz: clockFrequency, + busAddressWidth: busConfig.addressWidth, + busDataWidth: busConfig.dataWidth, + target: target, + ckPeriodPs: 1250, + runtimeTrainable: false, + simExternalMem: true, + name: '${mem.type}_$i', + ); + soc.addPeripheral(ddr); + // The behavioral DRAM is a host-side C++ mmap model, so route its + // wishbone memory bus to the top for the model to drive, the same + // split-port exposure the SDIO card model uses. + for (final p in const [ + 'mem_stb', + 'mem_we', + 'mem_adr', + 'mem_dat_w', + 'mem_sel', + 'mem_ack', + 'mem_dat_r', + ]) { + soc.exposePin(ddr, p, externalName: '${ddr.name}_$p'); + } + continue; + } // Real-speed DDR3-667 (Xilinx ISERDESE2). // Build the DDR3 clock tree from the board oscillator: an MMCM (ZHOLD + // BUFG feedback, the only openXC7-lockable form) fans one VCO into ck333 @@ -1514,6 +1825,11 @@ class RiverGenIpConfig { // (single-oscillator core-clock-off-spare-CLKOUT path), reuse it so the // core and the DDR clocks share ONE MMCM. Otherwise build it here (the // separate `ddr_osc` pin case has no clock-pin contention). + // DDR controller gearing for THIS dram device (ddr3v2 only). Must match + // whatever the shared SoC tree was built with. + final ddrGear = (mem.ddrParams?.ddr3v2 ?? false) + ? (mem.ddrParams?.ctrlGear ?? 1) + : 1; final tree = soc.xilinxDdr3Clocks ?? buildXilinxDdr3ClockTree( @@ -1523,6 +1839,7 @@ class RiverGenIpConfig { ddrCkHz: ck333Hz, idelayRefHz: idelayRefHz, dqsPhaseDeg: dqsPhaseDeg, + ddrGearRatio: ddrGear, name: 'ddr3clk', ); // Controller clock = ctrl83 (CK/4). All DRAM us/ns timing counters derive @@ -1563,6 +1880,26 @@ class RiverGenIpConfig { // Match the DDR3 CK the tree actually solves (set clockfreq= // 300000000 on the device for the proven 300 MHz x16 point). ckPeriodPs: (1e6 / tree.ddrCkMhz).round(), + // ctrlgear=2: run the controller LOGIC on the tree's CK/8 clock + // (controllerClkPeriodPs = CK*4*gear -> the AC-timing counts are + // CK/8-correct, T6) and interpose the fabric gearbox. + controllerGearRatio: ddrGear, + // Strictly-ordered (non-posted) DRAM writes: a write is not ACKed to + // the fabric until it has crossed and committed. This buys two things, + // BOTH HW-proven necessary on the timing-marginal Arty S7 DDR: + // 1. Cross-master coherency: a later read by ANY master (CPU or SDIO + // ADMA) sees the write. Posted writes ACK early and leave a stale + // hole that QEMU + the functional ROHD sim never reproduce. + // 2. ADMA pacing: each ADMA card-read block-write waits for its + // commit, which throttles the sustained read to a rate the + // marginal DDR survives. Posted writes remove that pacing; the + // unthrottled ADMA over-stresses the DDR and the board RESETS + // mid-read (HW-verified 2026-08-15: a posted build resets at the + // boot-file read where this non-posted build loads the kernel). + // Costs CPU write throughput (~220-cycle commit per store), which the + // real fix (a fast, non-marginal DDR route, or per-master posted so + // only the ADMA is paced) would recover. See project #68. + postedWrites: false, // train=runtime exposes the knob-ABI window for the FSBL engine. runtimeTrainable: mem.ddrParams?.runtimeTrain ?? false, name: '${mem.type}_$i', @@ -1577,19 +1914,34 @@ class RiverGenIpConfig { BusAddressRange(ddr.trainBase, HarborDdr3.trainWindowSize), ); } - ddr.input('ddr_clk').srcConnection! <= tree.controller; - final sysDomainForDdr = soc.clockDomain('sys'); - if (sysDomainForDdr == null) { - throw StateError('ddr3v2 needs the sys clock domain for ddr_reset'); - } - ddr.input('ddr_reset').srcConnection! <= sysDomainForDdr.reset; - ddr.input('ddr_ck_fast').srcConnection! <= tree.ddrCk; - ddr.input('ddr_ck90_fast').srcConnection! <= tree.ddrCk90; - ddr.input('ddr_ck_dqs_fast').srcConnection! <= tree.ddrCkDqs; - ddr.input('ddr_idelay_ref').srcConnection! <= tree.idelayRef; - final padPorts = [...DdrBoard.padPorts, 'sdram_dqs_n']; - for (final pad in padPorts) { - soc.exposePin(ddr, pad, externalName: pad); + // Under Verilator, HarborDdr3 builds a behavioral DRAM (_HarborSimDram, + // loadable via +dram_image) with only the bus + clk/reset, and NO DDR3 + // PHY: no ddr_clk/ck_fast inputs and no physical pads. So the whole PHY + // wiring and pad exposure below is FPGA/ASIC only. The bus side and + // clk/reset are auto-wired by addPeripheral for both. + if (target is! HarborSimTarget) { + // gearRatio 1: single clock (ddr_clk = CK/4). gearRatio 2: the + // controller runs on CK/8 (tree.controllerClk = CLKOUT5) and the + // SERDES/PHY + gearbox on CK/4 (tree.controller) via ddr_serdes_clk. + ddr.input('ddr_clk').srcConnection! <= tree.controllerClk; + if (ddrGear > 1) { + ddr.input('ddr_serdes_clk').srcConnection! <= tree.controller; + } + final sysDomainForDdr = soc.clockDomain('sys'); + if (sysDomainForDdr == null) { + throw StateError( + 'ddr3v2 needs the sys clock domain for ddr_reset', + ); + } + ddr.input('ddr_reset').srcConnection! <= sysDomainForDdr.reset; + ddr.input('ddr_ck_fast').srcConnection! <= tree.ddrCk; + ddr.input('ddr_ck90_fast').srcConnection! <= tree.ddrCk90; + ddr.input('ddr_ck_dqs_fast').srcConnection! <= tree.ddrCkDqs; + ddr.input('ddr_idelay_ref').srcConnection! <= tree.idelayRef; + final padPorts = [...DdrBoard.padPorts, 'sdram_dqs_n']; + for (final pad in padPorts) { + soc.exposePin(ddr, pad, externalName: pad); + } } continue; } @@ -1947,6 +2299,100 @@ class RiverGenIpConfig { } } + // Wire the CLINT's per-hart timer_irq/sw_irq outputs into each core's + // machine timer/software interrupt-pending lines (mip.MTIP / mip.MSIP), the + // same output-drives-input idiom the debug subsystem uses below. Without + // this the CLINT outputs dangle and a Linux/SBI timer never fires. + if (hasClint && coreTimerNets.isNotEmpty) { + final clintDev = mmioDevices.firstWhere((d) => d.type == 'clint'); + final clint = peripheralsByName[clintDev.name]!; + for (var h = 0; h < coreTimerNets.length; h++) { + coreTimerNets[h] <= clint.output('timer_irq_$h'); + coreSwNets[h] <= clint.output('sw_irq_$h'); + coreTimeNets[h] <= clint.output('mtime_val'); + } + } + + // A DMA-capable SPI controller is BOTH a slave (its registers, added above) + // and a bus master (its `dma` interface streams SD bytes to memory). Attach + // that master to the fabric arbiter alongside the core. Its leg is pipelined + // because the SPI sits out at an I/O pad: a registered bus to the arbiter + // keeps the placer from stretching a die-crossing combinational route + // through the core's decode region (routing-congestion relief). The DMA is + // throughput-bound, so the two extra latency cycles do not matter. + for (final dev in mmioDevices) { + if ((dev.type == 'spi' || dev.type == 'sdio') && + (dev.params?.dma ?? false)) { + soc.addMaster( + peripheralsByName[dev.name]!, + busInterfaceName: 'dma', + pipeline: true, + // Put the DMA master on its OWN fabric channel, physically off the + // primary arbiter. Its wide 64-bit leg was the delta xc7s50 routing + // hotspot (it smeared the crossbar's arbitration mux through the + // core's decode region); on its own channel it meets the CPU fabric + // only at a converge arbiter in front of DRAM. Also the Linux + // topology: DMA traffic never stalls the CPU's primary fabric. + // With dmashared, the separate channel plus its converge arbiter is + // itself the routing hotspot on a small device, so share the primary + // crossbar instead (the topology that provably closes on xc7s50). + channel: (dev.params?.dmaShared ?? false) ? 'primary' : 'dma', + ); + } + } + + // Expose the SDIO controller's SD pads. CMD/DAT are bidirectional (ownPads + // inout, driven through IOBUFs inside the controller); clk is an output and + // card-detect an input. Board pins bind to these by external name. + for (final dev in mmioDevices) { + if (dev.type == 'sdio') { + final sdio = peripheralsByName[dev.name]!; + final pads = target is HarborSimTarget + // Verilator (ownPads=false): the split out/oe/in ports, so the C++ + // SD-card sim model reads the host's cmd/dat drive and injects the + // card's response/data on the `_in` lines. + ? const [ + 'sd_clk', + 'sd_cd', + 'sd_cmd_out', + 'sd_cmd_oe', + 'sd_cmd_in', + 'sd_dat_out', + 'sd_dat_oe', + 'sd_dat_in', + ] + // FPGA/ASIC (ownPads=true): one scalar inout pad per DAT lane + // (sd_dat0..3) plus cmd/clk/cd. + : const [ + 'sd_clk', + 'sd_cmd', + 'sd_cd', + 'sd_dat0', + 'sd_dat1', + 'sd_dat2', + 'sd_dat3', + ]; + for (final pad in pads) { + soc.exposePin(sdio, pad, externalName: '${dev.name}_$pad'); + } + } + } + + // Under Verilator the UART has no board pin, so its serial lines are not + // exposed by --pin. Expose tx/rx as real top-level ports so the harness's + // host-side UART sink (HarborUart.simModels, gated on topPort('tx')) can + // decode the transmit line to stdout, the same way the JTAG pins are raised + // for the remote_bitbang server. Mirrors the console a board gives us. + if (target is HarborSimTarget) { + for (final dev in mmioDevices) { + if (dev.type != 'uart') continue; + final uart = peripheralsByName[dev.name]!; + for (final line in const ['tx', 'rx']) { + soc.exposePin(uart, line, externalName: '${dev.name}_$line'); + } + } + } + // Expose peripheral pins referenced by --pin flags (and the board catalog). for (final pin in effectivePins) { if (!pin.isDevicePin) continue; @@ -1959,6 +2405,35 @@ class RiverGenIpConfig { soc.exposePin(peri, pin.portName, externalName: pin.externalName); } + // Build the fabric. When a DMA-capable device placed a master on the 'dma' + // channel (see addMaster above), keep that channel separate: it reaches only + // memory (dram) and converges with the primary channel at an arbiter in + // front of DRAM. This lifts the DMA's wide master off the primary crossbar + // (delta xc7s50 congestion relief) and is the Linux-throughput topology. + // Without a DMA channel this is byte-identical to the historic single fabric. + void finishFabric() { + final hasDmaChannel = mmioDevices.any( + (dev) => + (dev.type == 'spi' || dev.type == 'sdio') && + (dev.params?.dma ?? false) && + !(dev.params?.dmaShared ?? false), + ); + if (!hasDmaChannel) { + soc.buildFabric(pipeline: true); + return; + } + soc.buildFabric( + pipeline: true, + channelSlaves: { + 'primary': {for (final p in soc.peripherals) p.name}, + 'dma': { + for (final p in soc.peripherals) + if (p.name.startsWith('dram')) p.name, + }, + }, + ); + } + if (usbDfu && usbDfuMode == UsbDfuMode.hardware) { if (enableDebug) { // The hardware DFU path uses a fixed 2-master arbiter (core + DFU @@ -1974,10 +2449,10 @@ class RiverGenIpConfig { } else if (usbDfu && usbDfuMode == UsbDfuMode.software) { _integrateUsbDfuSoftware(soc, busConfig, target); if (enableDebug) _integrateDebugJtag(soc, busConfig, debugCore!, target); - soc.buildFabric(); + finishFabric(); } else { if (enableDebug) _integrateDebugJtag(soc, busConfig, debugCore!, target); - soc.buildFabric(); + finishFabric(); } return soc; @@ -1996,6 +2471,22 @@ class RiverGenIpConfig { final dbg = RiverDebugSubsystem(busConfig, xlen: xlen, target: target); soc.addMaster(dbg, busInterfaceName: 'bus'); + // Under Verilator the TAP is not tunnelled through a config-JTAG + // primitive, so its pins are real top-level ports for the generated + // remote_bitbang server to bit-bang. Names must match the harness that + // HarborSimTarget.generateMain emits. + if (target is HarborSimTarget) { + for (final pin in const [ + 'jtag_tck', + 'jtag_tms', + 'jtag_tdi', + 'jtag_trst', + 'jtag_tdo', + ]) { + soc.exposePin(dbg, pin, externalName: pin); + } + } + // To the core. core.input('debug_halt_req').srcConnection! <= dbg.output('halt_req'); core.input('debug_resume_req').srcConnection! <= dbg.output('resume_req'); @@ -2185,6 +2676,68 @@ class RiverGenIpConfig { busDataWidth: busConfig.dataWidth, sources: mmioDevices.length + 1, ); + case 'spi': + // Generic SPI master. Pads (spi_clk/spi_mosi/spi_miso/spi_cs_n) are + // exposed and bound to a board connector via `iface=` (see [_ifacePins]). + return HarborSpiController( + baseAddress: dev.address, + busAddressWidth: busConfig.addressWidth, + busDataWidth: busConfig.dataWidth, + sdCard: dev.params?.sdcard ?? false, + // Optional integrated DMA master (fast SD block reads). Its master + // interface is wired to the fabric via addMaster below; the address + // width matches the fabric so it can reach all of memory. + dma: dev.params?.dma ?? false, + dmaAddressWidth: busConfig.addressWidth, + name: dev.name, + ); + case 'sdio': + // Native SD/SDIO host. `ownPads` gives one bidirectional pad per CMD/DAT + // line (IOBUF), and `fabricDma` (from the `dma` param) exposes the ADMA + // engine as a Wishbone master wired to the fabric via addMaster below. + // 4-bit native SD: 4x the throughput of the 1-bit SPI path (the reason + // to use SDIO). DAT0..3 + CMD + CLK + CD map to the full PmodSD pinout. + return HarborSdioController( + baseAddress: dev.address, + config: HarborSdioConfig( + maxBusWidth: HarborSdioBusWidth.four, + // Reset default for the read-data sample edge. A real board with a + // marginal read-capture window (a long card round-trip at speed) + // sets samplefall=1 so the read DAT is sampled on the falling edge. + sampleReadOnFall: dev.params?.sampleFall ?? false, + ), + // FPGA/ASIC collapse CMD/DAT to bidirectional IOBUF pads; under + // Verilator keep the split out/oe/in ports so the C++ SD-card sim + // model can drive the response/data lines cleanly (inout pads are + // fiddly to drive from a host model), matching the ROHD SD oracle. + ownPads: target is! HarborSimTarget, + fabricDma: dev.params?.dma ?? false, + dmaAddressWidth: busConfig.addressWidth, + dmaDataWidth: busConfig.dataWidth, + busAddressWidth: busConfig.addressWidth, + busDataWidth: busConfig.dataWidth, + // On a posted-write DDR fabric an ADMA card-read's block writes are + // ACKed before they commit to DRAM, so raising data-done when the RX + // FIFO drains lets the CPU read the buffer while the last writes are + // still in flight (stale data -> the Arty S7 sustained-read reset). + // Fence the writes with a read-back so data-done means durable. Only + // when this SoC has both the ADMA and a real DDR memory; SRAM-only + // SoCs keep the byte-identical straight-through completion. + readBackBarrier: + (dev.params?.dma ?? false) && + memories.any((m) => m.ddrBoard != null), + // Drop CYC after EVERY DDR write beat (not every 16) on a DDR SoC. The + // ADMA otherwise holds CYC across a burst; the DRAM CDC bridge wedges + // on back-to-back held-CYC transactions on silicon (it needs CYC to + // drop between transactions, the same hazard l1_cache guards the CPU + // against). One beat per bus grant gives the CDC that guarantee. + dmaBurstBeats: + (dev.params?.dma ?? false) && + memories.any((m) => m.ddrBoard != null) + ? 1 + : 16, + name: dev.name, + ); default: return null; } @@ -2654,6 +3207,23 @@ class RiverGenIpConfig { (m) => m.type != 'flash', orElse: () => flash, ); + // Boot banner, e.g. "River maskrom (RC1.f, Delta V1), jumping to FSBL + // in flash". The core id "rc1-f" reads as "RC1.f" (uppercase the RC1 + // stem, keep the variant suffix) and the SoC name "delta_v1" reads as + // "Delta V1" (title-case each underscore-word). + final coreParts = cores.first.split('-'); + final coreName = [ + coreParts.first.toUpperCase(), + ...coreParts.skip(1), + ].join('.'); + final socDisplay = name + .split('_') + .map( + (w) => w.isEmpty ? w : '${w[0].toUpperCase()}${w.substring(1)}', + ) + .join(' '); + final banner = + 'River maskrom ($coreName, $socDisplay), jumping to FSBL in flash\r\n'; program = RiverMaskrom( RiverMaskromConfig( isa: coreConfig.isa, @@ -2663,6 +3233,9 @@ class RiverGenIpConfig { copySize: 256, // warmup read window stackTop: stackMem.address + stackMem.size, bootMode: RiverBootMode.xipLaunch, + bootMessage: banner, + uartBase: uart.address, + uartDivisor: (clockFrequency ~/ 115200).clamp(1, 0xffff), ), ); default: diff --git a/packages/river_hdl/lib/src/microcode_rom.dart b/packages/river_hdl/lib/src/microcode_rom.dart index 583e931..7239f8f 100644 --- a/packages/river_hdl/lib/src/microcode_rom.dart +++ b/packages/river_hdl/lib/src/microcode_rom.dart @@ -292,9 +292,17 @@ class MicrocodeRom { final isShiftImm = (op.opcode & 0x7F) == 0x13 && (op.funct3 == 0x1 || op.funct3 == 0x5); + // AMO/LR/SC: funct7[6:2] is funct5 (the op selector); funct7[1:0] are + // the aq/rl ordering hints and MUST NOT take part in the match, else + // ordered atomics (sc.w.rl, amoadd.w.aqrl, ...) fail to decode and trap + // illegal. Match funct5 only (bits 31:27); leave bits 26:25 don't-care. + final isAmo = (op.opcode & 0x7F) == 0x2F; if (isShiftImm) { mask |= (0x3F << 26); value |= ((op.funct7! >> 1) << 26); + } else if (isAmo) { + mask |= (0x1F << 27); + value |= ((op.funct7! >> 2) << 27); } else { mask |= (0x7F << 25); value |= (op.funct7! << 25); diff --git a/packages/river_hdl/test/branch/caddi16sp_jump_repro_test.dart b/packages/river_hdl/test/branch/caddi16sp_jump_repro_test.dart new file mode 100644 index 0000000..9954ede --- /dev/null +++ b/packages/river_hdl/test/branch/caddi16sp_jump_repro_test.dart @@ -0,0 +1,82 @@ +import 'package:river/river.dart'; +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import '../core_harness.dart'; + +/// Repro attempt 2 for the HW trap: rc1-f traps ILLEGAL (mcause 2) on +/// `c.addi16sp` (0x7179) at a NixOS EFI entry point 0x8200744c. The earlier +/// caddi16sp_repro_test FELL THROUGH into the c.addi16sp and passed. On real +/// hardware Weir JUMPS to the entry, so the c.addi16sp is the FIRST fetch at a +/// 4-byte-aligned jump target (a fresh, cold instruction stream). This repro +/// jumps to the c.addi16sp instead of falling through, to see whether the +/// 2-lane decoder mis-selects the lane at a jump target. +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + RiverCoreConfig full() => RiverCoreConfigV1.full( + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + ), + clock: const HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + ); + + int iimm(int imm, int rs1, int f3, int rd) => + ((imm & 0xFFF) << 20) | (rs1 << 15) | (f3 << 12) | (rd << 7) | 0x13; + + // Program: set sp, then JUMP forward to a 4-aligned c.addi16sp. + // @0x00 addi sp, x0, 0x400 + // @0x04 jal x0, +8 -> lands on 0x0c + // @0x08 addi x0, x0, 0 (unreached filler, keeps 0x0c 4-aligned) + // @0x0c c.addi16sp sp, -48 (0x7179, the JUMP TARGET, first fetch there) + // @0x0e addi x5, sp, 0 (capture the result) + // @0x12 jal x0, 0 (park) + // Expect sp = 0x400 - 48 = 0x3D0, x5 == 0x3D0. + String prog() { + final bytes = []; + void emit32(int w) { + for (var i = 0; i < 4; i++) { + bytes.add((w >> (i * 8)) & 0xFF); + } + } + + void emit16(int h) { + bytes.add(h & 0xFF); + bytes.add((h >> 8) & 0xFF); + } + + emit32(iimm(0x400, 0, 0x0, 2)); // 0x00 addi sp, x0, 0x400 + emit32(0x0080006f); // 0x04 jal x0, +8 -> 0x0c + emit32(iimm(0, 0, 0x0, 0)); // 0x08 nop filler + emit16(0x7179); // 0x0c c.addi16sp sp, -48 (jump target) + emit32(iimm(0, 2, 0x0, 5)); // 0x0e addi x5, sp, 0 + emit32(0x0000006f); // 0x12 jal x0, 0 (park) + + final sb = StringBuffer('@0\n'); + for (final b in bytes) { + sb.write(b.toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + return '$sb\n'; + } + + test( + 'c.addi16sp as a JUMP TARGET decodes on rc1-f (lanes=2) - x5 == sp-48', + timeout: Timeout(Duration(minutes: 6)), + () => coreTest( + prog(), + {Register.x5: 0x3D0, Register.x2: 0x3D0}, + full(), + nextPc: 0x12, + ), + ); +} diff --git a/packages/river_hdl/test/branch/caddi16sp_latency_repro_test.dart b/packages/river_hdl/test/branch/caddi16sp_latency_repro_test.dart new file mode 100644 index 0000000..9d6fa31 --- /dev/null +++ b/packages/river_hdl/test/branch/caddi16sp_latency_repro_test.dart @@ -0,0 +1,83 @@ +import 'package:river/river.dart'; +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import '../core_harness.dart'; + +/// Repro attempt 3 for the HW regression: rc1-f traps ILLEGAL (mcause 2) on +/// `c.addi16sp` (0x7179) at a NixOS EFI entry point, but ONLY at full speed on +/// real hardware (single-step, sim at zero fetch latency, and an SBA read all +/// show it decodes/executes fine). The decode-overrun fix (decoder end-of-ROM +/// mask==0 + the pipeline illegal-instruction trap) is the suspect: at full +/// speed a jump to a compressed instruction may momentarily present a decode +/// result that the new illegal path traps on. The zero-latency sim missed it. +/// This sweeps a multi-cycle fetch latency (DRAM/cache-like) while JUMPING to a +/// compressed instruction, to try to reproduce the spurious trap in sim. +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + RiverCoreConfig full() => RiverCoreConfigV1.full( + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + ), + clock: const HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + ); + + int iimm(int imm, int rs1, int f3, int rd) => + ((imm & 0xFFF) << 20) | (rs1 << 15) | (f3 << 12) | (rd << 7) | 0x13; + + // Set sp, JUMP to a 4-aligned c.addi16sp, capture the result. + String prog() { + final bytes = []; + void emit32(int w) { + for (var i = 0; i < 4; i++) { + bytes.add((w >> (i * 8)) & 0xFF); + } + } + + void emit16(int h) { + bytes.add(h & 0xFF); + bytes.add((h >> 8) & 0xFF); + } + + emit32(iimm(0x400, 0, 0x0, 2)); // 0x00 addi sp, x0, 0x400 + emit32(0x0080006f); // 0x04 jal x0, +8 -> 0x0c + emit32(iimm(0, 0, 0x0, 0)); // 0x08 nop filler + emit16(0x7179); // 0x0c c.addi16sp sp, -48 (JUMP TARGET) + emit32(iimm(0, 2, 0x0, 5)); // 0x0e addi x5, sp, 0 + emit32(0x0000006f); // 0x12 jal x0, 0 (park) + + final sb = StringBuffer('@0\n'); + for (final b in bytes) { + sb.write(b.toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + return '$sb\n'; + } + + // Sweep realistic fetch latencies. The HW fetch (DRAM through the I-cache) is + // multi-cycle; a spurious end-of-ROM/illegal fire would show up as x5 never + // reaching 0x3D0 (the core traps and parks at the trap vector instead). + for (final lat in [1, 2, 3, 4, 6]) { + test( + 'c.addi16sp jump target decodes at fetch latency $lat (lanes=2)', + timeout: Timeout(Duration(minutes: 6)), + () => coreTest( + prog(), + {Register.x5: 0x3D0, Register.x2: 0x3D0}, + full(), + nextPc: 0x12, + memLatency: lat, + ), + ); + } +} diff --git a/packages/river_hdl/test/branch/caddi16sp_repro_test.dart b/packages/river_hdl/test/branch/caddi16sp_repro_test.dart new file mode 100644 index 0000000..5bc5e31 --- /dev/null +++ b/packages/river_hdl/test/branch/caddi16sp_repro_test.dart @@ -0,0 +1,124 @@ +import 'package:river/river.dart'; +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import '../core_harness.dart'; + +/// Repro: on real hardware the rc1-f microcode core trapped ILLEGAL (mcause 2) +/// on `c.addi16sp` (0x7179) at a NixOS EFI entry point. c.addi16sp is a valid +/// RVC instruction, so a trap/mis-decode is a core bug. This runs it in sim to +/// see whether it reproduces deterministically (decode bug) or not (the HW trap +/// was the intermittent timing effect, which sim cannot show). +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + // rc1-f, RV64GC microcode, lanes=2 (the shipping config). + RiverCoreConfig full() => RiverCoreConfigV1.full( + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + ), + clock: const HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + ); + + // Same, but force the classic 1-lane decode scan, to isolate whether the + // 2-lane packed decode is the culprit. + RiverCoreConfig lanes1() => RiverCoreConfig( + clock: const HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + mxlen: RiscVMxlen.rv64, + extensions: [ + rvC, + rvZicsr, + rvZifencei, + rvM, + rvA, + rvF, + rvD, + rvFExtra, + rvDExtra, + rvPriv, + rv64i, + rv32i, + ], + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + ), + type: RiverCoreType.general, + executionMode: ExecutionMode.inOrder, + issueWidth: IssueWidth.single, + microcodeMode: MicrocodeMode.full, + microcodeDecodeLanes: 1, + ); + + int iimm(int imm, int rs1, int f3, int rd) => + ((imm & 0xFFF) << 20) | (rs1 << 15) | (f3 << 12) | (rd << 7) | 0x13; + + // Program (mixed 16/32-bit): + // @0x00 addi sp, x0, 0x400 (set sp the real way, mirrors to nextSp) + // @0x04 c.addi16sp sp, -48 (0x7179, the faulting instruction) + // @0x06 addi x5, sp, 0 (capture the result of the c.addi16sp) + // @0x0a jal x0, 0 (park) + // Expect: sp = 0x400 - 48 = 0x3D0, and x5 == 0x3D0. + String prog() { + final bytes = []; + void emit32(int w) { + for (var i = 0; i < 4; i++) { + bytes.add((w >> (i * 8)) & 0xFF); + } + } + + void emit16(int h) { + bytes.add(h & 0xFF); + bytes.add((h >> 8) & 0xFF); + } + + emit32(iimm(0x400, 0, 0x0, 2)); // addi sp, x0, 0x400 + emit16(0x7179); // c.addi16sp sp, -48 + emit32(iimm(0, 2, 0x0, 5)); // addi x5, sp, 0 + emit32(0x0000006f); // jal x0, 0 (park @ 0x0a) + + final sb = StringBuffer('@0\n'); + for (final b in bytes) { + sb.write(b.toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + return '$sb\n'; + } + + test( + 'c.addi16sp executes on rc1-f (lanes=2) - x5 == sp-48', + timeout: Timeout(Duration(minutes: 6)), + () => coreTest( + prog(), + {Register.x5: 0x3D0, Register.x2: 0x3D0}, + full(), + nextPc: 0x0a, + ), + ); + + test( + 'c.addi16sp executes on rc1-f (lanes=1) - x5 == sp-48', + timeout: Timeout(Duration(minutes: 6)), + () => coreTest( + prog(), + {Register.x5: 0x3D0, Register.x2: 0x3D0}, + lanes1(), + nextPc: 0x0a, + ), + ); +} diff --git a/packages/river_hdl/test/branch/sd_regfile_repro_test.dart b/packages/river_hdl/test/branch/sd_regfile_repro_test.dart new file mode 100644 index 0000000..916f737 --- /dev/null +++ b/packages/river_hdl/test/branch/sd_regfile_repro_test.dart @@ -0,0 +1,321 @@ +import 'package:river/river.dart'; +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import '../core_harness.dart'; +import '../matrix_encoders.dart'; + +/// Repro for the rc1-f (delta) register-file wipe seen on real hardware: the +/// first full SD 512-byte block read corrupts the GPR file (gp/sp/ra/tp all read +/// 0 over JTAG, CSRs survive) and Weir floods '0'. The SD path is the first heavy +/// user of `fence`/`fence r,rw` (volatile MMIO ordering) and of an MMIO store / +/// poll / byte-copy loop. These sub-tests seed witness registers that the code +/// under test never touches, so any that come back changed pin a wrong microcode +/// writeback. Absolute expectations (not an emulator-golden compare, which would +/// pass if the emulator shares the bug), same style as the c.jalr link repro. +HarborMmuConfig _mmu() => HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare, RiscVPagingMode.sv39], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + hasSupervisorUserMemory: true, + hasMakeExecutableReadable: true, +); + +const _clk = HarborClockConfig( + name: 'test', + rate: HarborFixedClockRate(12000000), +); + +// regfileReadLatency: 1 matches the Arty (openXc7) build, where the integer +// regfile is a registered-read BRAM. The harness otherwise builds a +// target-less core whose flop regfile reads combinationally (latency 0), so a +// microcode read-handshake bug in the registered-read path is invisible. Force +// latency 1 on the (still simulatable) flop backend to expose it. +RiverCoreConfig _rc1f() => RiverCoreConfigV1.full( + mmu: _mmu(), + interrupts: [], + clock: _clk, + resetVector: 0, + regfileReadLatency: 1, +); + +String _memString(List words) { + final sb = StringBuffer('@0\n'); + for (final w in words) { + for (var b = 0; b < 4; b++) { + sb.write(((w >> (b * 8)) & 0xFF).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + } + return '${sb.toString().trimRight()}\n'; +} + +// Witness registers the code under test must never modify. gp (x3) and tp (x4) +// are the strongest tells: software sets them once at boot and never again, and +// the ABI never spills them, so a change is a raw regfile write. +const _witness = { + Register.x3: 0x3333, // gp + Register.x4: 0x4444, // tp + Register.x8: 0x8888, // s0 + Register.x9: 0x9999, // s1 + Register.x18: 0x1818, // s2 +}; + +const _fence = 0x0ff0000f; // fence iorw, iorw +const _fenceRrw = + 0x0230000f; // fence r, rw (acquire/release form the SD path emits) +const _fenceI = 0x0000100f; // fence.i +const _nop = 0x00000013; + +void main() { + // A: fences alone. The SD path wraps every MMIO poke in these. + test( + 'rc1-f: fences do not disturb the register file', + () async { + await Simulator.reset(); + final program = [_fence, _fenceRrw, _fenceI, _nop]; + await coreTest( + _memString(program), + _witness, + _rc1f(), + initRegisters: _witness, + nextPc: program.length * 4, // 0x10 + ); + }, + timeout: Timeout(Duration(minutes: 5)), + ); + + // B: the MMIO store / status-poll / data-read / byte-copy loop, the shape of + // conduit readData over the harbor SPI master. a0 is the MMIO base (flat RAM + // here, so the status read returns 0 = ready and the loop drains), a1 the + // destination buffer, a2 the byte count. + test( + 'rc1-f: MMIO poke + byte-copy loop keeps the witnesses', + () async { + await Simulator.reset(); + // loop @0x00 (7 insns, 28 bytes); halt nop @0x1c. + final program = [ + store(16, 13, 10, 0x2), // sw a3, 16(a0) MMIO data write (a3 = 0xFF) + load( + 8, + 10, + 0x2, + 5, + ), // lw t0, 8(a0) status poll (reads 0 = ready) + load(16, 10, 0x4, 6), // lbu t1, 16(a0) data byte + store(0, 6, 11, 0x0), // sb t1, 0(a1) into the buffer + iimm(1, 11, 0x0, 11), // addi a1, a1, 1 + iimm(-1, 12, 0x0, 12), // addi a2, a2, -1 + branch(-24, 0, 12, 0x1), // bne a2, x0, loop + _nop, // 0x1c halt + ]; + await coreTest( + _memString(program), + _witness, + _rc1f(), + initRegisters: { + ..._witness, + Register.x10: 0x1000, // a0 MMIO base (flat RAM) + Register.x11: 0x2000, // a1 buffer + Register.x12: 4, // a2 count + Register.x13: 0xFF, // a3 data byte + }, + nextPc: program.length * 4, // 0x20 + ); + }, + timeout: Timeout(Duration(minutes: 5)), + ); + + // D: the instruction types the REAL sd_spi readBlocksImpl uses that A/B did + // not cover: halfword sh/lh/lhu, an indirect jalr call (the c.jalr bug's + // family), and sext.w/subw. Same loop shape, same witnesses. + test( + 'rc1-f: halfword + indirect-jalr + word-op loop keeps the witnesses', + () async { + await Simulator.reset(); + final program = [ + // loop @0x00 + store(132, 13, 10, 0x1), // sh a3, 132(a0) halfword store + load(130, 10, 0x5, 5), // lhu t0, 130(a0) halfword load unsigned + load(128, 10, 0x1, 6), // lh t1, 128(a0) halfword load signed + iimmW(0, 5, 0x0, 7), // sext.w t2, t0 + rtypeW(0x20, 6, 7, 0x0, 7), // subw t2, t2, t1 + jalr(0, 15, 1), // jalr ra, 0(a5) indirect call subroutine + iimm(-1, 12, 0x0, 12), // addi a2, a2, -1 + branch(-28, 0, 12, 0x1), // bne a2, x0, loop (back to 0x00) + jal(8, 0), // 0x20 j end (skip subroutine) + 0x00008067, // 0x24 ret (subroutine: jalr x0, 0(ra)) + _nop, // 0x28 halt + ]; + await coreTest( + _memString(program), + _witness, + _rc1f(), + initRegisters: { + ..._witness, + Register.x10: 0x1000, // a0 MMIO base + Register.x12: 3, // a2 count + Register.x13: 0xFF, // a3 store data + Register.x15: 0x24, // a5 subroutine address + }, + nextPc: program.length * 4, // 0x2c + ); + }, + timeout: Timeout(Duration(minutes: 5)), + ); + + // E: I-CACHE THRASH. rc1-f L1 I-cache is 64 bytes, direct-mapped, 8-byte lines + // (HarborL1CacheConfig.split iSize:64 ways:1 lineSize:8). Tests A/B/D are tiny + // and fit entirely in cache, so they never exercise eviction/refill. This loop + // BODY is 80 bytes (20 instrs) > the whole 64-byte cache, so every iteration + // evicts and refills every line. If the direct-mapped refill/tag path has a + // bug, the re-fetched instructions are wrong and the witnesses get scribbled + // or the loop mis-terminates. This is the stress the real 512-iteration SD + // read applies to the I-cache that a functional flop-sim of tiny code hides. + test( + 'rc1-f: I-cache thrash (loop body > cache) keeps the witnesses', + () async { + await Simulator.reset(); + final body = [ + for (var i = 0; i < 18; i++) + iimm(1, 5, 0x0, 5), // 18x addi t0, t0, 1 (0x00..0x44) + iimm( + -1, + 12, + 0x0, + 12, + ), // addi a2, a2, -1 (0x48) + branch( + -76, + 0, + 12, + 0x1, + ), // bne a2, x0, 0x00 (0x4c) + ]; + final program = [...body, _nop]; // halt nop at 0x50 + await coreTest( + _memString(program), + _witness, + _rc1f(), + initRegisters: { + ..._witness, + // 3 iterations: iter 1 fills + evicts lines, iters 2-3 re-fetch the + // evicted lines. Enough to exercise refill without exceeding the + // harness maxSimTime (a normal run finishes well under it, so a TIMEOUT + // here means the core wedged = the bug reproduced). + Register.x12: 3, // a2 loop count + }, + nextPc: body.length * 4, // 0x50 + ); + }, + timeout: Timeout(Duration(minutes: 6)), + ); + + // F: D-CACHE thrash + stack round-trip. rc1-f L1 D-cache is 256 bytes, + // direct-mapped, 8-byte lines. This is the closest model of what the SD block + // read does that IDENTIFY does not: save regs to the stack, then write a + // buffer LARGER than the D-cache (320 B > 256 B) that evicts the saved stack + // lines, then reload the saved regs (miss -> refill from memory). If the + // write-through / eviction / refill round-trip is buggy, the reloaded + // witnesses come back wrong - which on real code is the epilogue restoring a + // garbage `ra`, `ret` to a bad address, and the flood. The witnesses are + // clobbered to 0 after the save, so ONLY a correct reload restores them. + test( + 'rc1-f: D-cache thrash + stack reload keeps the witnesses', + () async { + await Simulator.reset(); + final program = [ + store( + 0, + 8, + 2, + 0x3, + ), // sd s0, 0(sp) save witnesses to stack (0x4000) + store(8, 9, 2, 0x3), // sd s1, 8(sp) + store(16, 18, 2, 0x3), // sd s2, 16(sp) + iimm( + 0, + 0, + 0x0, + 8, + ), // li s0, 0 clobber, so reload must restore + iimm(0, 0, 0x0, 9), // li s1, 0 + iimm(0, 0, 0x0, 18), // li s2, 0 + // loop @0x18: write 40 dwords (320 B) to the buffer, evicting the stack lines + store(0, 13, 10, 0x3), // sd a3, 0(a0) + iimm(8, 10, 0x0, 10), // addi a0, a0, 8 + iimm(-1, 12, 0x0, 12), // addi a2, a2, -1 + branch(-12, 0, 12, 0x1), // bne a2, x0, 0x18 + load( + 0, + 2, + 0x3, + 8, + ), // ld s0, 0(sp) reload (miss -> refill from memory) + load(8, 2, 0x3, 9), // ld s1, 8(sp) + load(16, 2, 0x3, 18), // ld s2, 16(sp) + _nop, // halt + ]; + await coreTest( + _memString(program), + _witness, + _rc1f(), + initRegisters: { + ..._witness, + // Cacheable DRAM (>= cacheableBase 0x80000000): loads here go through the + // fill path, so this exercises the real eviction/refill the SD read hits, + // not the uncached bypass. + Register.x2: 0x80010000, // sp stack base + Register.x10: 0x80010040, // a0 buffer (sp + 64) + Register.x12: 40, // a2 dword count (320 B > 256 B D-cache) + Register.x13: 0xAA, // a3 buffer fill data + }, + nextPc: (program.length - 1) * 4, // halt nop + ); + }, + timeout: Timeout(Duration(minutes: 6)), + ); + + // G: MINIMAL round-trip, NO thrash. Save 3 regs to the stack, clobber, reload. + // If s2 still comes back wrong with no eviction, the bug is a plain + // store/clobber/reload (or a 3rd-back-to-back-load) issue, not cache eviction. + test( + 'rc1-f: minimal stack round-trip (no thrash)', + () async { + await Simulator.reset(); + // Does a COMPRESSED c.addi16sp update the regfile copy of sp that FULL + // instructions read? readBlocks adjusts sp with c.addi16sp, then does full + // `sd`s. If c.addi16sp updates only the nextSp fast-path, the full store's + // sp (regfile[2]) is stale -> corruption. + // 0x00: lui x2, 0x1 sp = 0x1000 + // 0x04: addi x2, x2, 0x40 sp = 0x1040 (syncs nextSp + regfile[2]) + // 0x08: c.addi16sp sp, +16 sp = 0x1050 (0x6141) + // 0x0a: c.nop (0x0001) + // 0x0c: addi x5, x2, 0 x5 = sp read via REGFILE + // 0x10: nop + // The readBlocks pattern: compressed c.sdsp saves + c.ldsp restores around a + // cacheable sp (nextSp fast-path + D-cache). sp = 0xFFFFFFFF80001000. + // lui sp,0x80001 ; c.sdsp s0/s1/s2 ; clobber ; c.ldsp s0/s1/s2 ; nop + final program = [ + 0x80001137, // lui sp, 0x80001 + 0xE426E022, // c.sdsp s0,0(sp) | c.sdsp s1,8(sp) + 0x0001E84A, // c.sdsp s2,16(sp) | c.nop + 0x00000413, // li s0, 0 + 0x00000493, // li s1, 0 + 0x00000913, // li s2, 0 + 0x64A26402, // c.ldsp s0,0(sp) | c.ldsp s1,8(sp) + 0x00016942, // c.ldsp s2,16(sp) | c.nop + _nop, + ]; + await coreTest( + _memString(program), + _witness, // s0/s1/s2 must survive the compressed save/restore + _rc1f(), + initRegisters: _witness, + nextPc: 0x20, + ); + }, + timeout: Timeout(Duration(minutes: 6)), + ); +} diff --git a/packages/river_hdl/test/cache/amoadd_loop_coherence_test.dart b/packages/river_hdl/test/cache/amoadd_loop_coherence_test.dart new file mode 100644 index 0000000..144bb6b --- /dev/null +++ b/packages/river_hdl/test/cache/amoadd_loop_coherence_test.dart @@ -0,0 +1,76 @@ +import 'package:river/river.dart'; +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import '../core_harness.dart'; + +/// Faithful repro of the delta ticket-lock corruption: run the REAL amoadd +/// micro-op, through the REAL D-cache (rc1-f full config includes the split L1), +/// in a tight loop against ONE cached address, and require every increment to +/// land. On HW two amoadds lost an increment (a ticket ended up behind the +/// owner). The differential matrix cannot catch this: it drives a simple +/// MemoryModel, not the D-cache. Data lives at 0x80001000 (>= cacheableBase +/// 0x80000000) so it is actually cached; code runs from low (uncached). +/// +/// x5 = 0x80001000 (target), x7 = 1 (increment), x8 = N (count) +/// loop: amoadd.w.aqrl x6, x7, (x5) ; mem[x5] += 1, x6 = old +/// addi x8, x8, -1 +/// bnez x8, loop +/// jal x0, 0 ; park +void main() { + test( + 'amoadd.w.aqrl loop through the real D-cache never loses an increment', + timeout: Timeout(Duration(minutes: 6)), + () async { + const n = 40; + // amoadd.w.aqrl x6,x7,(x5): opcode 0x2f, f3=2, funct7=0x03 (amoadd aq=rl=1), + // rs2=7, rs1=5, rd=6. + const amoaddAqrl = + (0x03 << 25) | (7 << 20) | (5 << 15) | (2 << 12) | (6 << 7) | 0x2f; + final program = { + 0x00: 0x00100393, // addi x7, x0, 1 + 0x04: + (n << 20) | (0 << 15) | (0 << 12) | (8 << 7) | 0x13, // addi x8,x0,N + 0x08: amoaddAqrl, // loop: amoadd.w.aqrl x6,x7,(x5) + 0x0c: 0xfff40413, // addi x8, x8, -1 + 0x10: 0xfe041ce3, // bnez x8, -8 (back to 0x08) + 0x14: 0x0000006f, // jal x0, 0 (park) + }; + // Emit one contiguous block from @0. + final sb = StringBuffer('@0\n'); + final maxA = program.keys.reduce((a, b) => a > b ? a : b); + for (var addr = 0; addr <= maxA + 4; addr += 4) { + final w = program[addr] ?? 0x00000013; + for (var b = 0; b < 4; b++) { + sb.write(((w >> (b * 8)) & 0xFF).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + } + + await coreTest( + sb.toString(), + { + Register.x6: n - 1, // last amoadd returns the previous value (N-1) + Register.x8: 0, + }, + RiverCoreConfigV1.full( + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + ), + clock: const HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + ), + initRegisters: {Register.x5: 0x80001000}, + // final memory at 0x80001000 must be exactly N (no lost increments) + memStates: {0x80001000: n}, + nextPc: 0x14, + ); + }, + ); +} diff --git a/packages/river_hdl/test/cache/amoadd_paged_coherence_test.dart b/packages/river_hdl/test/cache/amoadd_paged_coherence_test.dart new file mode 100644 index 0000000..d3e47ca --- /dev/null +++ b/packages/river_hdl/test/cache/amoadd_paged_coherence_test.dart @@ -0,0 +1,105 @@ +import 'package:river/river.dart'; +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import '../core_harness.dart'; + +/// Paged repro attempt for the delta ticket-lock corruption. The M-mode HW +/// amoadd hammer loses nothing, so the failing variable is PAGING: the kernel +/// runs S-mode/Sv39 and delta's MMU has tlbLevels:[] (page-walks EVERY access), +/// so the amoadd's read phase and write phase each translate the SAME VA +/// independently. This runs the amoadd loop at a VIRTUAL address (0x80001000) +/// that a 1GB identity leaf maps to DRAM, so every amoadd traverses the MMU + +/// D-cache, and requires no increment to be lost. +/// +/// M-mode: csrw satp (Sv39, root@0x2000), sfence, mepc=loop, MPP=S, mret +/// S-mode loop: amoadd.w.aqrl x6,x7,(x5) ; addi x8,-1 ; bnez ; park +/// satp root: [0]=1GB identity leaf (code/PT), [2]=1GB identity leaf (DRAM) +void main() { + test( + 'paged amoadd loop through the MMU never loses an increment', + timeout: Timeout(Duration(minutes: 8)), + () async { + const n = 4; + const amo = + (0x03 << 25) | + (7 << 20) | + (5 << 15) | + (2 << 12) | + (6 << 7) | + 0x2f; // amoadd.w.aqrl x6,x7,(x5) + final words = { + 0x00: 0x18051073, // csrw satp, x10 + 0x04: 0x12000073, // sfence.vma + 0x08: 0x34159073, // csrw mepc, x11 + 0x0c: 0x30061073, // csrw mstatus, x12 (MPP=S) + 0x10: 0x30200073, // mret -> S-mode @ 0x40 + 0x40: amo, // loop: + 0x44: 0xfff40413, // addi x8, x8, -1 + 0x48: 0xfe041ce3, // bnez x8, -8 (-> 0x40) + 0x4c: 0x0000006f, // park + }; + + // Sv39 root page table @ PA 0x2000. 1GB leaves, V|R|W|X|A|D = 0xCF. + // root[0] (VA 0..0x3fffffff -> PA identity) : code + page table + // root[2] (VA 0x80000000.. -> PA identity) : DRAM (the lock) + final bytes = {}; // byte address -> byte + void putWord(int addr, int w) { + for (var b = 0; b < 4; b++) { + bytes[addr + b] = (w >> (b * 8)) & 0xFF; + } + } + + void putDword(int addr, int lo, int hi) { + putWord(addr, lo); + putWord(addr + 4, hi); + } + + words.forEach(putWord); + putDword( + 0x2000, + 0x000000CF, + 0x00000000, + ); // root[0] code identity (VA 0..1G) + // root[510] (VA 0xffffffff80000000.. -> PA 0x80000000, 1GB leaf): the kernel + // maps its high VA to a lower DRAM PA, so the VIVT D-cache tags a high VA + // while memory sits at a low PA. This is the untested atomic condition. + putDword(0x2000 + 510 * 8, 0x200000CF, 0x00000000); + + final maxA = bytes.keys.reduce((a, b) => a > b ? a : b); + final sb = StringBuffer('@0\n'); + for (var a = 0; a <= maxA + 1; a++) { + sb.write((bytes[a] ?? 0).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + + await coreTest( + sb.toString(), + {Register.x6: n - 1, Register.x8: 0}, + RiverCoreConfigV1.full( + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare, RiscVPagingMode.sv39], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + ), + clock: const HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + ), + initRegisters: { + Register.x10: 0x8000000000000002, // satp: Sv39, root PPN 0x2 + Register.x11: 0x40, // mepc = loop start + Register.x12: 0x800, // mstatus MPP = S + Register.x5: 0xffffffff80001000, // high kernel VA -> PA 0x80001000 + Register.x7: 1, // increment + Register.x8: n, // count + }, + memStates: {0x80001000: n}, + nextPc: 0x4c, + ); + }, + ); +} diff --git a/packages/river_hdl/test/cache/dcache_atomic_coherence_test.dart b/packages/river_hdl/test/cache/dcache_atomic_coherence_test.dart new file mode 100644 index 0000000..c0b3940 --- /dev/null +++ b/packages/river_hdl/test/cache/dcache_atomic_coherence_test.dart @@ -0,0 +1,198 @@ +import 'dart:async'; + +import 'package:harbor/harbor.dart'; +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +/// HW-observed on delta: a ticket spinlock corrupts (a ticket ends up BEHIND the +/// owner) because two `amoadd`s both read the same `next` - the first amoadd's +/// write was not visible to the second read. The core is a single in-order hart +/// with NO async interrupts, so a lost read-after-write can only be the D-cache. +/// +/// HarborL1DCache is write-through / no-write-allocate: a store writes straight +/// to memory and INVALIDATES the resident line (l1_cache.dart:807, gated on +/// `storeInv = committedHitOf(reqAddr)`). If that invalidation ever misses (e.g. +/// the store's hit-check races the just-filled line, so storeInv=0 and the stale +/// line survives), a following load returns the pre-store value. +/// +/// This drives the REAL D-cache with a latency-bearing memory model through the +/// exact amoadd shape (load A, then store A = loaded+1, repeated) and asserts +/// each load observes the previous store. Conflicting-line traffic forces fills +/// so the store-invalidate-vs-fill timing is exercised. The differential matrix +/// never hits this because it uses a simple MemoryModel, not the D-cache. +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + Future run({required int memLatency}) async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(); + final reqAddr = Logic(width: 64); + final reqValid = Logic(); + final reqWrite = Logic(); + final reqData = Logic(width: 64); + final reqSize = Logic(width: 3); + final flush = Logic(); + final memDone = Logic(); + final memValid = Logic(); + final memRdata = Logic(width: 64); + + final dc = HarborL1DCache( + config: HarborL1CacheConfig.split( + iSize: 64, + dSize: 256, + ways: 1, + lineSize: 8, + ).d, + xlen: 64, + ); + dc.input('clk').srcConnection! <= clk; + dc.input('reset').srcConnection! <= reset; + dc.input('req_addr').srcConnection! <= reqAddr; + dc.input('req_valid').srcConnection! <= reqValid; + dc.input('req_write').srcConnection! <= reqWrite; + dc.input('req_data').srcConnection! <= reqData; + dc.input('req_size').srcConnection! <= reqSize; + dc.input('flush').srcConnection! <= flush; + dc.input('mem_done').srcConnection! <= memDone; + dc.input('mem_valid').srcConnection! <= memValid; + dc.input('mem_rdata').srcConnection! <= memRdata; + await dc.build(); + + // Backing memory (word-addressed by aligned byte addr), with a fixed ack + // latency to mimic DRAM. The cache's memEn pulses; we respond after N cycles. + final mem = {}; + var pending = 0; // cycles until we answer the current mem op + var opActive = false; + + reset.inject(1); + reqValid.inject(0); + reqWrite.inject(0); + reqAddr.inject(0); + reqData.inject(0); + reqSize.inject(2); // word (.w) -> 4 bytes + flush.inject(0); + memDone.inject(0); + memValid.inject(0); + memRdata.inject(0); + + Simulator.setMaxSimTime(2000000); + unawaited(Simulator.run()); + await clk.nextPosedge; + reset.inject(0); + await clk.nextPosedge; + + // Memory responder: runs every cycle off the cache's mem_* outputs. + void memStep() { + final en = dc.memEn.value.toInt() == 1; + if (en && !opActive) { + opActive = true; + pending = memLatency; + } + if (opActive) { + if (pending > 0) { + pending--; + memDone.inject(0); + memValid.inject(0); + } else { + final we = dc.memWe.value.toInt() == 1; + final addr = dc.memAddr.value.toInt() & ~0x7; // 8-byte line word + if (we) { + // word (4B) write-through: honor size, low 4 bytes at the addr's word. + final a = dc.memAddr.value.toInt(); + final wd = dc.memWdata.value.toInt(); + final base = a & ~0x7; + final cur = mem[base] ?? 0; + // store the 4-byte lane the amoadd targets (addr is 4-aligned). + if ((a & 0x4) == 0) { + mem[base] = (cur & ~0xFFFFFFFF) | (wd & 0xFFFFFFFF); + } else { + mem[base] = (cur & 0xFFFFFFFF) | ((wd & 0xFFFFFFFF) << 32); + } + memRdata.inject(0); + } else { + memRdata.inject(mem[addr] ?? 0); + } + memDone.inject(1); + memValid.inject(1); + opActive = false; + } + } else { + memDone.inject(0); + memValid.inject(0); + } + } + + // Issue one request; drive memStep each cycle; return respData for a load. + Future req(int addr, {required bool write, int data = 0}) async { + reqAddr.inject(addr); + reqWrite.inject(write ? 1 : 0); + reqData.inject(data); + reqValid.inject(1); + // Wait for resp_valid, servicing memory each cycle. + var guard = 0; + while (true) { + memStep(); + await clk.nextPosedge; + if (dc.respValid.value.toInt() == 1) break; + if (++guard > 100000) { + throw StateError( + 'req to 0x${addr.toRadixString(16)} never completed', + ); + } + } + final rd = dc.respData.value.toInt() & 0xFFFFFFFF; + reqValid.inject(0); + // one idle cycle between requests (cache drops busy) + memStep(); + await clk.nextPosedge; + return rd; + } + + // The amoadd target and a conflicting address that maps to the SAME line + // (256B cache, 8B lines => 32 lines; +256 bytes aliases the same index). + const a = 0x80000040; + const conflict = 0x80000040 + 256; + mem[a & ~0x7] = 0; // owner/next both 0 + mem[conflict & ~0x7] = 0xdead; + + // amoadd loop: read A, write A = read+1. Also touch the conflicting line + // each iter to force A's line to be evicted/refilled (exercises the + // store-invalidate vs fill timing). + const iters = 64; + var expected = 0; + for (var k = 0; k < iters; k++) { + final got = await req(a, write: false); + expect( + got, + expected, + reason: + 'iter $k: load A returned 0x${got.toRadixString(16)}, ' + 'expected 0x${expected.toRadixString(16)} ' + '(a lost store => stale cached read = the ticket-lock bug)', + ); + expected = (got + 1) & 0xFFFFFFFF; + await req(a, write: true, data: expected); // amoadd write-back + // conflicting-line access to churn the direct-mapped line + await req(conflict, write: false); + } + + // Final memory value must reflect all increments. + expect( + mem[a & ~0x7]! & 0xFFFFFFFF, + iters, + reason: 'final A in memory should be $iters after $iters increments', + ); + + await Simulator.endSimulation(); + await Simulator.simulationEnded; + } + + test('D-cache amoadd read-after-write coherence, memLatency=1', () async { + await run(memLatency: 1); + }); + test('D-cache amoadd read-after-write coherence, memLatency=3', () async { + await run(memLatency: 3); + }); +} diff --git a/packages/river_hdl/test/core_harness.dart b/packages/river_hdl/test/core_harness.dart index cfba10d..ac952d1 100644 --- a/packages/river_hdl/test/core_harness.dart +++ b/packages/river_hdl/test/core_harness.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:rohd/rohd.dart'; import 'package:rohd_hcl/rohd_hcl.dart' hide DataPortInterface, DataPortGroup; @@ -15,9 +16,25 @@ Future coreTest( int nextPc = 4, int latency = 0, int memLatency = 0, + // Privilege the core holds coming out of reset. Defaults to machine (real + // RISC-V reset). Set to supervisor/user to exercise paged data translation + // without a boot-time mret: M-mode data accesses are always physical. + PrivilegeMode? startPriv, + // Cycle budget to reach nextPc. A wedged core never reaches it, so a small + // budget lets a repro fail in seconds instead of grinding the full default. + int maxCycles = 200000, + // Raise the machine-timer-pending line (mip.MTIP) at this run-loop cycle to + // inject an async timer interrupt mid-execution. Null = never (no interrupt + // input wired, so existing callers are unaffected). + int? raiseTimerIrqAt, + // Lower mip.MTIP at this run-loop cycle, modelling the handler clearing the + // timer (an mtimecmp write) so the interrupt is taken once and does not storm + // on every mret. Null = leave it asserted (level) once raised. + int? lowerTimerIrqAt, }) async { final clk = SimpleClockGenerator(20).clk; final reset = Logic(); + final timerIrq = raiseTimerIrqAt == null ? null : Logic(name: 'timerIrq'); final addrWidth = config.mxlen.size; final wbConfig = WishboneConfig( @@ -30,13 +47,26 @@ Future coreTest( // write also lands in the OoO prf so initRegisters reaches the OoO read path. final prfSeedMode = Logic(name: 'prfSeedMode'); - final core = RiverCore(config, busConfig: wbConfig, prfSeedMode: prfSeedMode); + final core = RiverCore( + config, + busConfig: wbConfig, + prfSeedMode: prfSeedMode, + resetPrivilege: startPriv?.id, + timerPending: timerIrq, + ); + timerIrq?.inject(0); core.input('clk').srcConnection! <= clk; core.input('reset').srcConnection! <= reset; await core.build(); + // Optional VCD dump for debugging (set RIVER_WAVE=/path/to.vcd). + final wavePath = Platform.environment['RIVER_WAVE']; + if (wavePath != null && wavePath.isNotEmpty) { + WaveDumper(core, outputPath: wavePath); + } + final storage = SparseMemoryStorage( addrWidth: addrWidth, dataWidth: config.mxlen.size, @@ -110,7 +140,7 @@ Future coreTest( storage.loadMemString(memString); }); - Simulator.setMaxSimTime(100000); + Simulator.setMaxSimTime(4000000); unawaited(Simulator.run()); await clk.nextPosedge; @@ -136,10 +166,37 @@ Future coreTest( await clk.nextPosedge; } - for (var i = 0; i < 5000; i++) { + final trace = Platform.environment['RIVER_TRACE']?.isNotEmpty ?? false; + final distinctPcs = []; + var reached = false; + for (var i = 0; i < maxCycles; i++) { await clk.nextPosedge; + if (i == raiseTimerIrqAt) timerIrq!.inject(1); + if (i == lowerTimerIrqAt) timerIrq!.inject(0); final pc = core.pipeline.nextPc.value; - if (pc.isValid && pc.toInt() == nextPc) break; + if (trace && pc.isValid) { + final v = pc.toInt(); + if (distinctPcs.isEmpty || distinctPcs.last != v) distinctPcs.add(v); + } + if (pc.isValid && pc.toInt() == nextPc) { + reached = true; + break; + } + } + if (trace && !reached) { + final tail = distinctPcs.length > 40 + ? distinctPcs.sublist(distinctPcs.length - 40) + : distinctPcs; + // ignore: avoid_print + print( + '[TRACE] did not reach 0x${nextPc.toRadixString(16)}; last PCs: ' + '${tail.map((p) => '0x${p.toRadixString(16)}').join(' ')}', + ); + for (final r in [Register.x1, Register.x5, Register.x6, Register.x10]) { + final rv = core.regs.getData(LogicValue.ofInt(r.value, 5)); + // ignore: avoid_print + print('[TRACE] $r = 0x${rv?.toInt().toRadixString(16)}'); + } } await Simulator.endSimulation(); diff --git a/packages/river_hdl/test/csr/counteren_test.dart b/packages/river_hdl/test/csr/counteren_test.dart new file mode 100644 index 0000000..524a9cc --- /dev/null +++ b/packages/river_hdl/test/csr/counteren_test.dart @@ -0,0 +1,107 @@ +import 'package:river/river.dart'; +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import '../core_harness.dart'; + +/// scounteren / mcounteren counter-enable CSR regression. +/// +/// On real hardware the Linux RISC-V head code writes `csrw scounteren, t0` +/// (0x10629073) before it installs its own trap vector. River implemented +/// S-mode (satp/sstatus/sie/...) but omitted scounteren (0x106) and mcounteren +/// (0x306). An access to an unimplemented CSR raises ILLEGAL instruction, so the +/// write trapped and the delta board stopped at Weir's diagnostic sink +/// (scause=2 sepc=0x8aa010ea) the first time the kernel ran from DRAM. +/// +/// The privileged spec requires scounteren when S-mode exists and mcounteren +/// when U-mode exists. Both gate lower-privilege access to cycle/time/instret, +/// so only bits CY/TM/IR (2:0) are writable; the HPM bits are WARL-0. +RiverCoreConfig _rc1s() => RiverCoreConfigV1.small( + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare, RiscVPagingMode.sv39], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + hasSupervisorUserMemory: true, + hasMakeExecutableReadable: true, + ), + interrupts: [], + clock: const HarborClockConfig( + name: 'test', + rate: HarborFixedClockRate(12000000), + ), + resetVector: 0, +); + +// Emit ONE contiguous block from @0, gaps filled with nop. A per-word `@addr` +// form makes SparseMemoryStorage take sub-8-byte writes that mis-pack a word +// holding zero bytes, so a zero-heavy instruction reads back corrupted. A single +// contiguous load never triggers that. +String _memString(Map words) { + const nop = 0x00000013; + final maxAddr = words.keys.reduce((a, b) => a > b ? a : b); + final sb = StringBuffer('@0\n'); + for (var addr = 0; addr <= maxAddr + 4; addr += 4) { + final w = words[addr] ?? nop; + for (var b = 0; b < 4; b++) { + sb.write(((w >> (b * 8)) & 0xFF).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + } + return sb.toString(); +} + +void main() { + test( + 'csrw scounteren from S-mode is legal and WARL-masks to CY/IR (TM off)', + () async { + await Simulator.reset(); + // Drop to S (MPP=S), then write scounteren with all-ones and read it back. + // A missing CSR would trap illegal here and x7 would never reach 0x99. + final program = { + 0x00: 0x34151073, // csrw mepc, x10 + 0x04: 0x30059073, // csrw mstatus, x11 (MPP=S) + 0x08: 0x30200073, // mret + 0x40: 0x10661073, // csrw scounteren, x12 (S-mode; x12 = all ones) + 0x44: 0x106022f3, // csrr x5, scounteren + 0x48: 0x09900393, // addi x7, x0, 0x99 + 0x4c: 0x00000013, // nop + }; + await coreTest( + _memString(program), + // scounteren reads back 0x5: HPM bits masked off, CY|IR kept, TM + // WARL-0 (no native `time` CSR; rdtime is SBI-emulated). + {Register.x5: 0x5, Register.x7: 0x99}, + _rc1s(), + initRegisters: { + Register.x10: 0x40, + Register.x11: 0x800, + Register.x12: 0xffffffff, + }, + nextPc: 0x50, + ); + }, + timeout: Timeout(Duration(minutes: 5)), + ); + + test( + 'csrw mcounteren from M-mode is legal and WARL-masks to CY/IR (TM off)', + () async { + await Simulator.reset(); + final program = { + 0x00: 0x30661073, // csrw mcounteren, x12 (M-mode; x12 = all ones) + 0x04: 0x306022f3, // csrr x5, mcounteren + 0x08: 0x09900393, // addi x7, x0, 0x99 + 0x0c: 0x00000013, // nop + }; + await coreTest( + _memString(program), + {Register.x5: 0x5, Register.x7: 0x99}, + _rc1s(), + initRegisters: {Register.x12: 0xffffffff}, + nextPc: 0x10, + ); + }, + timeout: Timeout(Duration(minutes: 5)), + ); +} diff --git a/packages/river_hdl/test/csr/envcfg_test.dart b/packages/river_hdl/test/csr/envcfg_test.dart new file mode 100644 index 0000000..f27b660 --- /dev/null +++ b/packages/river_hdl/test/csr/envcfg_test.dart @@ -0,0 +1,85 @@ +import 'package:river/river.dart'; +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import '../core_harness.dart'; + +/// senvcfg (0x10A) / menvcfg (0x30A) environment-configuration CSR regression. +/// +/// Linux 6.x accesses senvcfg from the context-switch path (envcfg_update_bits) +/// and probes it in try_to_set_pmm/tagged_addr_init; OpenSBI/Weir touch menvcfg. +/// River omitted both. An access to an unimplemented CSR raises ILLEGAL, so the +/// kernel would trap (the same failure shape as the earlier scounteren gap). +/// +/// River implements none of the envcfg-controlled features (Zicbo CBIE/CBCFE/ +/// CBZE, pointer-masking PMM, Sstc STCE, Svpbmt PBMTE), so every field is +/// WARL-0: writes drop, reads return 0. That is the correct "feature absent" +/// report; e.g. try_to_set_pmm reads PMM back as 0 and disables pointer masking. +RiverCoreConfig _rc1s() => RiverCoreConfigV1.small( + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare, RiscVPagingMode.sv39], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + hasSupervisorUserMemory: true, + hasMakeExecutableReadable: true, + ), + interrupts: [], + clock: const HarborClockConfig( + name: 'test', + rate: HarborFixedClockRate(12000000), + ), + resetVector: 0, +); + +String _memString(Map words) { + const nop = 0x00000013; + final maxAddr = words.keys.reduce((a, b) => a > b ? a : b); + final sb = StringBuffer('@0\n'); + for (var addr = 0; addr <= maxAddr + 4; addr += 4) { + final w = words[addr] ?? nop; + for (var b = 0; b < 4; b++) { + sb.write(((w >> (b * 8)) & 0xFF).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + } + return sb.toString(); +} + +void main() { + test( + 'menvcfg (M) + senvcfg (S) are legal and WARL-0 (write all-ones -> read 0)', + () async { + await Simulator.reset(); + // M-mode: write menvcfg all-ones, read back (expect 0). Then drop to S + // (MPP=S via mret) and do the same for senvcfg. A missing CSR would trap + // illegal and x7 would never reach 0x99. + final program = { + 0x00: 0x30A69073, // csrw menvcfg, x13 (x13 = all ones) + 0x04: 0x30A02373, // csrr x6, menvcfg + 0x08: 0x34151073, // csrw mepc, x10 + 0x0c: 0x30059073, // csrw mstatus, x11 (MPP=S) + 0x10: 0x30200073, // mret -> S-mode @ 0x40 + 0x40: 0x10A61073, // csrw senvcfg, x12 (S-mode; x12 = all ones) + 0x44: 0x10A022f3, // csrr x5, senvcfg + 0x48: 0x09900393, // addi x7, x0, 0x99 + 0x4c: 0x00000013, // nop + }; + await coreTest( + _memString(program), + // Both read back 0 (all fields WARL-0); x7=0x99 proves neither access + // trapped. + {Register.x5: 0x0, Register.x6: 0x0, Register.x7: 0x99}, + _rc1s(), + initRegisters: { + Register.x10: 0x40, + Register.x11: 0x800, + Register.x12: 0xffffffff, + Register.x13: 0xffffffff, + }, + nextPc: 0x4c, + ); + }, + timeout: Timeout(Duration(minutes: 6)), + ); +} diff --git a/packages/river_hdl/test/csr/mcycle_increment_test.dart b/packages/river_hdl/test/csr/mcycle_increment_test.dart new file mode 100644 index 0000000..f6a6e25 --- /dev/null +++ b/packages/river_hdl/test/csr/mcycle_increment_test.dart @@ -0,0 +1,79 @@ +import 'package:river/river.dart'; +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import '../core_harness.dart'; + +/// mcycle / minstret must actually count. +/// +/// CounterCsr was declared readOnly. rohd_hcl runs every backdoor write value +/// through Csr.getWriteData, which for a readOnly register returns the current +/// value and drops the new data. So the per-cycle hardware increment (the +/// backdoor write in RiscVCsrFile._wireCounters) was discarded and mcycle and +/// minstret stayed at their reset value 0. Linux read rdcycle (emulated from +/// mcycle by Weir) for its ChaCha CSPRNG and spun forever on a stuck counter. +/// +/// This reads each counter twice, a few cycles apart, and proves the second +/// read is strictly greater. On the stuck core both reads are 0 and each sltu +/// yields 0, so the test fails before the fix and passes after it. +RiverCoreConfig _rc1s() => RiverCoreConfigV1.small( + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare, RiscVPagingMode.sv39], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + hasSupervisorUserMemory: true, + hasMakeExecutableReadable: true, + ), + interrupts: [], + clock: const HarborClockConfig( + name: 'test', + rate: HarborFixedClockRate(12000000), + ), + resetVector: 0, +); + +// Emit ONE contiguous block from @0, gaps filled with nop. +String _memString(Map words) { + const nop = 0x00000013; + final maxAddr = words.keys.reduce((a, b) => a > b ? a : b); + final sb = StringBuffer('@0\n'); + for (var addr = 0; addr <= maxAddr + 4; addr += 4) { + final w = words[addr] ?? nop; + for (var b = 0; b < 4; b++) { + sb.write(((w >> (b * 8)) & 0xFF).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + } + return sb.toString(); +} + +void main() { + test( + 'mcycle and minstret advance (not stuck at 0)', + () async { + await Simulator.reset(); + // All reads happen in M-mode (reset privilege), where both 0xB00 and + // 0xB02 are legal. + final program = { + 0x00: 0xB00022f3, // csrr x5, mcycle + 0x04: 0xB0202473, // csrr x8, minstret + 0x08: 0x00000013, // nop + 0x0c: 0x00000013, // nop + 0x10: 0xB0002373, // csrr x6, mcycle + 0x14: 0xB02024f3, // csrr x9, minstret + 0x18: 0x0062b3b3, // sltu x7, x5, x6 (mcycle later > earlier) + 0x1c: 0x00943533, // sltu x10, x8, x9 (minstret later > earlier) + 0x20: 0x09900593, // addi x11, x0, 0x99 (sentinel: program ran) + 0x24: 0x00000013, // nop + }; + await coreTest( + _memString(program), + {Register.x7: 1, Register.x10: 1, Register.x11: 0x99}, + _rc1s(), + nextPc: 0x28, + ); + }, + timeout: Timeout(Duration(minutes: 5)), + ); +} diff --git a/packages/river_hdl/test/csr/stvec_width_test.dart b/packages/river_hdl/test/csr/stvec_width_test.dart new file mode 100644 index 0000000..71513e9 --- /dev/null +++ b/packages/river_hdl/test/csr/stvec_width_test.dart @@ -0,0 +1,70 @@ +import 'package:river/river.dart'; +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import '../core_harness.dart'; + +/// HW-observed: at relocate_enable_mmu the kernel writes stvec with a high +/// sign-extended virtual address (0xffffffff80001048) but it reads back as +/// 0x80001048 (upper 32 bits dropped), causing the trampoline fault to loop. +/// This checks csrw/csrr stvec round-trips a full 64-bit value (mtvec too) on +/// the delta full() lanes=2 config. satp already round-trips 64 bits on HW. +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + RiverCoreConfig full() => RiverCoreConfigV1.full( + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare, RiscVPagingMode.sv39], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + hasSupervisorUserMemory: true, + hasMakeExecutableReadable: true, + ), + clock: const HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + ); + + // t0=x5 seeded to the high virtual address; csrw stvec,t0 ; csrr t1,stvec. + // 0x00 csrw stvec, t0 (0x10529073) + // 0x04 csrr t1, stvec (0x10502373) t1=x6 + // 0x08 csrw mtvec, t0 (0x30529073) + // 0x0c csrr t2, mtvec (0x30502373) t2=x7 + // 0x10 jal x0, 0 park + String prog() { + final sb = StringBuffer('@0\n'); + void h32(int v) { + for (var i = 0; i < 4; i++) { + sb.write(((v >> (i * 8)) & 0xFF).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + } + + // Build t0 = 0xffffffff80001048 in-program (lui sign-extends bit 31 on RV64), + // so there is no seeded-register high-bit ambiguity. + h32(0x800012b7); // lui t0, 0x80001 -> 0xffffffff80001000 + h32(0x04828293); // addi t0, t0, 0x48 -> 0xffffffff80001048 + h32(0x10529073); // csrw stvec, t0 + h32(0x00000013); // nop + h32(0x00000013); // nop + h32(0x10502373); // csrr t1, stvec (t1=x6) + h32(0x0000006f); // jal x0, 0 (park) + sb.writeln(); + return sb.toString(); + } + + const hi = 0xffffffff80001048; + + test( + 'csrw/csrr stvec round-trips a 64-bit high virtual address (lanes=2)', + timeout: Timeout(Duration(minutes: 6)), + () async { + await coreTest(prog(), {Register.x6: hi}, full(), nextPc: 0x18); + }, + ); +} diff --git a/packages/river_hdl/test/dbg_elab_test.dart b/packages/river_hdl/test/dbg_elab_test.dart index b2b81fb..24b506b 100644 --- a/packages/river_hdl/test/dbg_elab_test.dart +++ b/packages/river_hdl/test/dbg_elab_test.dart @@ -52,4 +52,34 @@ void main() { expect(sv.contains('JTAGG'), isTrue); expect(sv.contains('BSCANE2'), isFalse); }); + + test( + 'Verilator target exposes a raw TAP, no vendor primitive, no tunnel', + () async { + final sub = RiverDebugSubsystem( + cfg, + xlen: 64, + target: const HarborSimTarget(), + ); + await sub.build(); + final sv = sub.generateSynth(); + // Verilator cannot compile either config-JTAG primitive, and with no user + // register to ride there is nothing for the bscan tunnel to decode. + expect(sv.contains('JTAGG'), isFalse); + expect(sv.contains('BSCANE2'), isFalse); + expect(sv.contains('JtagBscanTunnel'), isFalse); + // The TAP is driven straight off top-level pins that the generated C++ + // remote_bitbang server bit-bangs. + for (final p in ['jtag_tck', 'jtag_tms', 'jtag_tdi', 'jtag_trst']) { + expect( + sub.tryInput(p), + isNotNull, + reason: '$p must be a top-level input', + ); + } + expect(sub.tryOutput('jtag_tdo'), isNotNull); + // The debug module itself is unchanged. + expect(sv.contains('RiverDebugModule'), isTrue); + }, + ); } diff --git a/packages/river_hdl/test/debug/debug_core_test.dart b/packages/river_hdl/test/debug/debug_core_test.dart index bd2059f..560925d 100644 --- a/packages/river_hdl/test/debug/debug_core_test.dart +++ b/packages/river_hdl/test/debug/debug_core_test.dart @@ -119,261 +119,273 @@ class DebugRig { } void main() { - test('Debug Module halts and resumes the live core', () async { - await Simulator.reset(); - const xlen = 64; - final clk = SimpleClockGenerator(10).clk; - final reset = Logic(name: 'reset'); + // Exercise both a combinational regfile (readLatency 0) and a registered-BRAM + // regfile (readLatency 1, the Xilinx/ECP5 builds). The latency-1 case is a + // regression guard: debug_reg_ready must lag the abstract read by the regfile + // read latency, else the Debug Module latches stale GPR data (reads 0 on HW). + for (final readLatency in [0, 1]) { + test('Debug Module halts and resumes the live core ' + '(regfile latency $readLatency)', () async { + await Simulator.reset(); + const xlen = 64; + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); - final coreConfig = RiverCoreConfigV1.small( - interrupts: [], - // Distinctive mhartid so the abstract CSR read below is decisive: the old - // fall-through read 0 for any CSR; the borrowed-port read returns 0x42. - hartId: 0x42, - mmu: HarborMmuConfig( - mxlen: RiscVMxlen.rv64, - pagingModes: const [RiscVPagingMode.bare], - tlbLevels: const [], - pmp: HarborPmpConfig.none, - ), - clock: const HarborClockConfig( - name: 'sysclk', - rate: HarborFixedClockRate(48000000), - ), - resetVector: 0, - ); - final wbConfig = WishboneConfig( - addressWidth: xlen, - dataWidth: xlen, - selWidth: xlen ~/ 8, - ); + final coreConfig = RiverCoreConfigV1.small( + interrupts: [], + // Distinctive mhartid so the abstract CSR read below is decisive: the old + // fall-through read 0 for any CSR; the borrowed-port read returns 0x42. + hartId: 0x42, + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + ), + clock: const HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + regfileReadLatency: readLatency == 0 ? null : readLatency, + resetVector: 0, + ); + final wbConfig = WishboneConfig( + addressWidth: xlen, + dataWidth: xlen, + selWidth: xlen ~/ 8, + ); - final storage = SparseMemoryStorage( - addrWidth: xlen, - dataWidth: xlen, - alignAddress: (addr) => addr, - onInvalidRead: (addr, dataWidth) => - LogicValue.filled(dataWidth, LogicValue.zero), - ); - // Fill low memory with pairs of NOPs (addi x0,x0,0 == 0x13) so a running - // core marches its PC forward word by word. - for (var a = 0; a < 0x400; a += 8) { - storage.setData( - LogicValue.ofInt(a, xlen), - LogicValue.ofInt(0x0000001300000013, xlen), + final storage = SparseMemoryStorage( + addrWidth: xlen, + dataWidth: xlen, + alignAddress: (addr) => addr, + onInvalidRead: (addr, dataWidth) => + LogicValue.filled(dataWidth, LogicValue.zero), ); - } + // Fill low memory with pairs of NOPs (addi x0,x0,0 == 0x13) so a running + // core marches its PC forward word by word. + for (var a = 0; a < 0x400; a += 8) { + storage.setData( + LogicValue.ofInt(a, xlen), + LogicValue.ofInt(0x0000001300000013, xlen), + ); + } - final core = RiverCore(coreConfig, busConfig: wbConfig, withDebug: true); - core.input('clk').srcConnection! <= clk; - // Hart reset = external reset OR the DM's ndmreset (driven after the DM). - final coreReset = Logic(name: 'coreReset'); - core.input('reset').srcConnection! <= coreReset; - await core.build(); + final core = RiverCore(coreConfig, busConfig: wbConfig, withDebug: true); + core.input('clk').srcConnection! <= clk; + // Hart reset = external reset OR the DM's ndmreset (driven after the DM). + final coreReset = Logic(name: 'coreReset'); + core.input('reset').srcConnection! <= coreReset; + await core.build(); - final wb = core.interface('dataBus').interface as WishboneInterface; - final memRead = DataPortInterface(xlen, xlen); - final memWrite = DataPortInterface(xlen, xlen); - // ignore: unused_local_variable - final mem = MemoryModel( - clk, - reset, - [wrapWriteForRegisterFile(memWrite)], - [wrapReadForRegisterFile(memRead)], - storage: storage, - ); - memRead.en <= wb.cyc & wb.stb & ~wb.we; - memRead.addr <= wb.adr; - memWrite.en <= wb.cyc & wb.stb & wb.we; - memWrite.addr <= wb.adr; - memWrite.data <= wb.datMosi; - final wbAck = Logic(name: 'wbAck'); - Sequential(clk, [ - If( + final wb = core.interface('dataBus').interface as WishboneInterface; + final memRead = DataPortInterface(xlen, xlen); + final memWrite = DataPortInterface(xlen, xlen); + // ignore: unused_local_variable + final mem = MemoryModel( + clk, reset, - then: [wbAck < 0], - orElse: [ - If(wb.cyc & wb.stb & ~wbAck, then: [wbAck < 1], orElse: [wbAck < 0]), - ], - ), - ]); - wb.ack <= wbAck; - wb.datMiso <= memRead.data; + [wrapWriteForRegisterFile(memWrite)], + [wrapReadForRegisterFile(memRead)], + storage: storage, + ); + memRead.en <= wb.cyc & wb.stb & ~wb.we; + memRead.addr <= wb.adr; + memWrite.en <= wb.cyc & wb.stb & wb.we; + memWrite.addr <= wb.adr; + memWrite.data <= wb.datMosi; + final wbAck = Logic(name: 'wbAck'); + Sequential(clk, [ + If( + reset, + then: [wbAck < 0], + orElse: [ + If( + wb.cyc & wb.stb & ~wbAck, + then: [wbAck < 1], + orElse: [wbAck < 0], + ), + ], + ), + ]); + wb.ack <= wbAck; + wb.datMiso <= memRead.data; - final tck = Logic(name: 'tck'); - final tms = Logic(name: 'tms'); - final tdi = Logic(name: 'tdi'); - final trstN = Logic(name: 'trst_n'); - final sbaRdata = Logic(name: 'sba_rdata', width: xlen); - final sbaAck = Logic(name: 'sba_ack'); - final dbg = RiverDebugModule( - clk, - reset, - tck, - tms, - tdi, - trstN, - hartHalted: core.output('debug_halted'), - regRdata: core.output('debug_reg_rdata'), - regReady: core.output('debug_reg_ready'), - sbaRdata: sbaRdata, - sbaAck: sbaAck, - xlen: xlen, - idcode: 0x10000001, - ); - await dbg.build(); - core.input('debug_halt_req').srcConnection! <= dbg.haltReq; - core.input('debug_resume_req').srcConnection! <= dbg.resumeReq; - core.input('debug_reg_read').srcConnection! <= dbg.regRead; - core.input('debug_reg_write').srcConnection! <= dbg.regWrite; - core.input('debug_reg_addr').srcConnection! <= dbg.regAddr; - core.input('debug_reg_wdata').srcConnection! <= dbg.regWdata; - coreReset <= reset | dbg.ndmreset; + final tck = Logic(name: 'tck'); + final tms = Logic(name: 'tms'); + final tdi = Logic(name: 'tdi'); + final trstN = Logic(name: 'trst_n'); + final sbaRdata = Logic(name: 'sba_rdata', width: xlen); + final sbaAck = Logic(name: 'sba_ack'); + final dbg = RiverDebugModule( + clk, + reset, + tck, + tms, + tdi, + trstN, + hartHalted: core.output('debug_halted'), + regRdata: core.output('debug_reg_rdata'), + regReady: core.output('debug_reg_ready'), + sbaRdata: sbaRdata, + sbaAck: sbaAck, + xlen: xlen, + idcode: 0x10000001, + ); + await dbg.build(); + core.input('debug_halt_req').srcConnection! <= dbg.haltReq; + core.input('debug_resume_req').srcConnection! <= dbg.resumeReq; + core.input('debug_reg_read').srcConnection! <= dbg.regRead; + core.input('debug_reg_write').srcConnection! <= dbg.regWrite; + core.input('debug_reg_addr').srcConnection! <= dbg.regAddr; + core.input('debug_reg_wdata').srcConnection! <= dbg.regWdata; + coreReset <= reset | dbg.ndmreset; - reset.inject(1); - tck.inject(0); - tms.inject(0); - tdi.inject(0); - trstN.inject(1); - sbaRdata.inject(0); - sbaAck.inject(0); - Simulator.setMaxSimTime(500000000); - unawaited(Simulator.run()); - await clk.nextPosedge; - reset.inject(0); - // Let the core run a while so its PC is marching forward. - for (var i = 0; i < 40; i++) { + reset.inject(1); + tck.inject(0); + tms.inject(0); + tdi.inject(0); + trstN.inject(1); + sbaRdata.inject(0); + sbaAck.inject(0); + Simulator.setMaxSimTime(500000000); + unawaited(Simulator.run()); await clk.nextPosedge; - } + reset.inject(0); + // Let the core run a while so its PC is marching forward. + for (var i = 0; i < 40; i++) { + await clk.nextPosedge; + } - final rig = DebugRig( - core, - dbg, - clk, - tck, - tms, - tdi, - sbaRdata, - sbaAck, - storage, - xlen, - ); - await rig.resetTap(); - await rig.scanIr(5, 0x11); + final rig = DebugRig( + core, + dbg, + clk, + tck, + tms, + tdi, + sbaRdata, + sbaAck, + storage, + xlen, + ); + await rig.resetTap(); + await rig.scanIr(5, 0x11); - // Halt the hart. - await rig.dmWrite(0x10, (1 << 31) | 1); // dmcontrol: haltreq | dmactive - await rig.idle(8); - final dmHalted = await rig.dmRead(0x11); - expect((dmHalted >> 9) & 1, 1, reason: 'allhalted set after haltreq'); - expect(core.output('debug_halted').value.toInt(), 1); + // Halt the hart. + await rig.dmWrite(0x10, (1 << 31) | 1); // dmcontrol: haltreq | dmactive + await rig.idle(8); + final dmHalted = await rig.dmRead(0x11); + expect((dmHalted >> 9) & 1, 1, reason: 'allhalted set after haltreq'); + expect(core.output('debug_halted').value.toInt(), 1); - // PC must be frozen while halted. - final pcAtHalt = core.pipeline.nextPc.value.toInt(); - for (var i = 0; i < 30; i++) { - await clk.nextPosedge; - } - expect( - core.pipeline.nextPc.value.toInt(), - pcAtHalt, - reason: 'PC must not advance while halted', - ); + // PC must be frozen while halted. + final pcAtHalt = core.pipeline.nextPc.value.toInt(); + for (var i = 0; i < 30; i++) { + await clk.nextPosedge; + } + expect( + core.pipeline.nextPc.value.toInt(), + pcAtHalt, + reason: 'PC must not advance while halted', + ); - // Resume and confirm the core runs again. - await rig.dmWrite(0x10, (1 << 30) | 1); // resumereq | dmactive - await rig.idle(8); - expect( - core.output('debug_halted').value.toInt(), - 0, - reason: 'halt clears on resume', - ); - final dmRun = await rig.dmRead(0x11); - expect((dmRun >> 9) & 1, 0, reason: 'allhalted clears after resume'); + // Resume and confirm the core runs again. + await rig.dmWrite(0x10, (1 << 30) | 1); // resumereq | dmactive + await rig.idle(8); + expect( + core.output('debug_halted').value.toInt(), + 0, + reason: 'halt clears on resume', + ); + final dmRun = await rig.dmRead(0x11); + expect((dmRun >> 9) & 1, 0, reason: 'allhalted clears after resume'); - // ---- Abstract command: write a GPR, read it back, read dpc ---- - await rig.dmWrite(0x10, (1 << 31) | 1); // halt again - await rig.idle(8); + // ---- Abstract command: write a GPR, read it back, read dpc ---- + await rig.dmWrite(0x10, (1 << 31) | 1); // halt again + await rig.idle(8); - // command = access-register, aarsize=3 (64-bit), transfer, write, x6. - const writeX6 = (3 << 20) | (1 << 17) | (1 << 16) | 0x1006; - const readX6 = (3 << 20) | (1 << 17) | 0x1006; - await rig.dmWrite(0x04, 0x12345678); // data0 (low 32) - await rig.dmWrite(0x05, 0xDEADBEEF); // data1 (high 32) - await rig.dmWrite(0x17, writeX6); - await rig.idle(6); - expect( - core.regs.getData(LogicValue.ofInt(6, 5))!.toBigInt(), - BigInt.parse('DEADBEEF12345678', radix: 16), - reason: 'abstract command wrote x6', - ); + // command = access-register, aarsize=3 (64-bit), transfer, write, x6. + const writeX6 = (3 << 20) | (1 << 17) | (1 << 16) | 0x1006; + const readX6 = (3 << 20) | (1 << 17) | 0x1006; + await rig.dmWrite(0x04, 0x12345678); // data0 (low 32) + await rig.dmWrite(0x05, 0xDEADBEEF); // data1 (high 32) + await rig.dmWrite(0x17, writeX6); + await rig.idle(6); + expect( + core.regs.getData(LogicValue.ofInt(6, 5))!.toBigInt(), + BigInt.parse('DEADBEEF12345678', radix: 16), + reason: 'abstract command wrote x6', + ); - // Read it back into data0/data1. - await rig.dmWrite(0x17, readX6); - await rig.idle(6); - expect(await rig.dmRead(0x04), 0x12345678, reason: 'x6 low readback'); - expect(await rig.dmRead(0x05), 0xDEADBEEF, reason: 'x6 high readback'); + // Read it back into data0/data1. + await rig.dmWrite(0x17, readX6); + await rig.idle(6); + expect(await rig.dmRead(0x04), 0x12345678, reason: 'x6 low readback'); + expect(await rig.dmRead(0x05), 0xDEADBEEF, reason: 'x6 high readback'); - // dpc (CSR 0x7b1) reads the PC captured at halt. - final dpc = core.output('debug_dpc').value.toBigInt(); - const readDpc = (3 << 20) | (1 << 17) | 0x7b1; - await rig.dmWrite(0x17, readDpc); - await rig.idle(6); - expect( - await rig.dmRead(0x04), - (dpc & BigInt.from(0xFFFFFFFF)).toInt(), - reason: 'dpc low matches the latched halt PC', - ); + // dpc (CSR 0x7b1) reads the PC captured at halt. + final dpc = core.output('debug_dpc').value.toBigInt(); + const readDpc = (3 << 20) | (1 << 17) | 0x7b1; + await rig.dmWrite(0x17, readDpc); + await rig.idle(6); + expect( + await rig.dmRead(0x04), + (dpc & BigInt.from(0xFFFFFFFF)).toInt(), + reason: 'dpc low matches the latched halt PC', + ); - // A general CSR (mhartid, 0xF14) read over the abstract command: the Debug - // Module borrows the frozen CSR read port. Before this path existed any - // non-dpc/dcsr/misa CSR fell through to the GPR port and read 0; now it - // returns the real value (config hartId = 0x42). - const readMhartid = (3 << 20) | (1 << 17) | 0xF14; - await rig.dmWrite(0x17, readMhartid); - await rig.idle(6); - expect( - await rig.dmRead(0x04), - 0x42, - reason: 'mhartid read over JTAG returns the real CSR value, not 0', - ); + // A general CSR (mhartid, 0xF14) read over the abstract command: the Debug + // Module borrows the frozen CSR read port. Before this path existed any + // non-dpc/dcsr/misa CSR fell through to the GPR port and read 0; now it + // returns the real value (config hartId = 0x42). + const readMhartid = (3 << 20) | (1 << 17) | 0xF14; + await rig.dmWrite(0x17, readMhartid); + await rig.idle(6); + expect( + await rig.dmRead(0x04), + 0x42, + reason: 'mhartid read over JTAG returns the real CSR value, not 0', + ); - // misa (0x301) still served by its dedicated constant path. - const readMisa = (3 << 20) | (1 << 17) | 0x301; - await rig.dmWrite(0x17, readMisa); - await rig.idle(6); - expect( - await rig.dmRead(0x04), - coreConfig.isa.misaValue & 0xFFFFFFFF, - reason: 'misa low word still served over JTAG', - ); + // misa (0x301) still served by its dedicated constant path. + const readMisa = (3 << 20) | (1 << 17) | 0x301; + await rig.dmWrite(0x17, readMisa); + await rig.idle(6); + expect( + await rig.dmRead(0x04), + coreConfig.isa.misaValue & 0xFFFFFFFF, + reason: 'misa low word still served over JTAG', + ); - // ---- ndmreset: reset the hart over JTAG ---- - // x6 currently holds 0xDEADBEEF12345678 (written above). Setting - // dmcontrol.ndmreset (bit 1) drives the hart into reset, clearing its - // register file, while leaving the Debug Module alive. - await rig.dmWrite(0x10, (1 << 1) | 1); // ndmreset | dmactive - await rig.idle(6); - expect( - (await rig.dmRead(0x10) >> 1) & 1, - 1, - reason: 'dmcontrol reads back ndmreset asserted', - ); - expect( - core.regs.getData(LogicValue.ofInt(6, 5))!.toBigInt(), - BigInt.zero, - reason: 'ndmreset reset the hart, clearing x6', - ); + // ---- ndmreset: reset the hart over JTAG ---- + // x6 currently holds 0xDEADBEEF12345678 (written above). Setting + // dmcontrol.ndmreset (bit 1) drives the hart into reset, clearing its + // register file, while leaving the Debug Module alive. + await rig.dmWrite(0x10, (1 << 1) | 1); // ndmreset | dmactive + await rig.idle(6); + expect( + (await rig.dmRead(0x10) >> 1) & 1, + 1, + reason: 'dmcontrol reads back ndmreset asserted', + ); + expect( + core.regs.getData(LogicValue.ofInt(6, 5))!.toBigInt(), + BigInt.zero, + reason: 'ndmreset reset the hart, clearing x6', + ); - // Release ndmreset; the DM stayed alive throughout (still reads dmactive). - await rig.dmWrite(0x10, 1); // dmactive only - await rig.idle(4); - expect( - (await rig.dmRead(0x10) >> 1) & 1, - 0, - reason: 'dmcontrol reads back ndmreset released', - ); + // Release ndmreset; the DM stayed alive throughout (still reads dmactive). + await rig.dmWrite(0x10, 1); // dmactive only + await rig.idle(4); + expect( + (await rig.dmRead(0x10) >> 1) & 1, + 0, + reason: 'dmcontrol reads back ndmreset released', + ); - await Simulator.endSimulation(); - await Simulator.simulationEnded; - }); + await Simulator.endSimulation(); + await Simulator.simulationEnded; + }); + } } diff --git a/packages/river_hdl/test/debug/jtag_tunnel_dm_test.dart b/packages/river_hdl/test/debug/jtag_tunnel_dm_test.dart index 77ef03b..dfd423b 100644 --- a/packages/river_hdl/test/debug/jtag_tunnel_dm_test.dart +++ b/packages/river_hdl/test/debug/jtag_tunnel_dm_test.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'package:rohd/rohd.dart'; +import 'package:harbor/harbor.dart' show JtagBscanTunnel; import 'package:river_hdl/src/core/debug.dart'; -import 'package:river_hdl/src/core/jtag_bscan_tunnel.dart'; import 'package:test/test.dart'; /// Drives the [JtagBscanTunnel] + a real [RiverDebugModule] exactly as diff --git a/packages/river_hdl/test/debug/trigger_test.dart b/packages/river_hdl/test/debug/trigger_test.dart new file mode 100644 index 0000000..0449432 --- /dev/null +++ b/packages/river_hdl/test/debug/trigger_test.dart @@ -0,0 +1,219 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd_hcl/rohd_hcl.dart' hide DataPortInterface, DataPortGroup; +import 'package:river/river.dart'; +import 'package:river_hdl/river_hdl.dart'; +import 'package:test/test.dart'; + +// Isolated hardware-trigger test: drive the core's debug ports directly (no +// JTAG/DM) so we can iterate fast and wave-dump. Programs an execute trigger and +// checks the core re-enters Debug Mode at the match PC with cause 2. +Future main() async { + test('execute trigger halts the core at the match PC', () async { + await Simulator.reset(); + const xlen = 64; + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + + final coreConfig = RiverCoreConfigV1.small( + interrupts: [], + hartId: 0x1, + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + ), + clock: const HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + resetVector: 0, + ); + final wbConfig = WishboneConfig( + addressWidth: xlen, + dataWidth: xlen, + selWidth: xlen ~/ 8, + ); + final storage = SparseMemoryStorage( + addrWidth: xlen, + dataWidth: xlen, + alignAddress: (addr) => addr, + onInvalidRead: (addr, dataWidth) => + LogicValue.filled(dataWidth, LogicValue.zero), + ); + // NOP-fill low memory (addi x0,x0,0 == 0x13) so the PC marches forward. + for (var a = 0; a < 0x800; a += 8) { + storage.setData( + LogicValue.ofInt(a, xlen), + LogicValue.ofInt(0x0000001300000013, xlen), + ); + } + + final core = RiverCore( + coreConfig, + busConfig: wbConfig, + withDebug: true, + debugTriggers: 1, + ); + core.input('clk').srcConnection! <= clk; + core.input('reset').srcConnection! <= reset; + + // Direct debug-port controls. + final haltReq = Logic(name: 'haltReqCtl'); + final resumeReq = Logic(name: 'resumeReqCtl'); + final regRead = Logic(name: 'regReadCtl'); + final regWrite = Logic(name: 'regWriteCtl'); + final regAddr = Logic(name: 'regAddrCtl', width: 16); + final regWdata = Logic(name: 'regWdataCtl', width: xlen); + core.input('debug_halt_req').srcConnection! <= haltReq; + core.input('debug_resume_req').srcConnection! <= resumeReq; + core.input('debug_reg_read').srcConnection! <= regRead; + core.input('debug_reg_write').srcConnection! <= regWrite; + core.input('debug_reg_addr').srcConnection! <= regAddr; + core.input('debug_reg_wdata').srcConnection! <= regWdata; + + await core.build(); + + final wavePath = Platform.environment['RIVER_WAVE']; + if (wavePath != null && wavePath.isNotEmpty) { + WaveDumper(core, outputPath: wavePath); + } + + final wb = core.interface('dataBus').interface as WishboneInterface; + final memRead = DataPortInterface(xlen, xlen); + final memWrite = DataPortInterface(xlen, xlen); + // ignore: unused_local_variable + final mem = MemoryModel( + clk, + reset, + [wrapWriteForRegisterFile(memWrite)], + [wrapReadForRegisterFile(memRead)], + storage: storage, + ); + memRead.en <= wb.cyc & wb.stb & ~wb.we; + memRead.addr <= wb.adr; + memWrite.en <= wb.cyc & wb.stb & wb.we; + memWrite.addr <= wb.adr; + memWrite.data <= wb.datMosi; + final wbAck = Logic(name: 'wbAck'); + Sequential(clk, [ + If( + reset, + then: [wbAck < 0], + orElse: [ + If(wb.cyc & wb.stb & ~wbAck, then: [wbAck < 1], orElse: [wbAck < 0]), + ], + ), + ]); + wb.ack <= wbAck; + wb.datMiso <= memRead.data; + + // Init controls. + reset.inject(1); + haltReq.inject(0); + resumeReq.inject(0); + regRead.inject(0); + regWrite.inject(0); + regAddr.inject(0); + regWdata.inject(0); + + unawaited(Simulator.run()); + await clk.nextPosedge; + await clk.nextPosedge; + reset.inject(0); + + // Let the core run a few instructions from resetVector. + for (var i = 0; i < 200; i++) { + await clk.nextPosedge; + } + + // Halt. + haltReq.inject(1); + for (var i = 0; i < 40; i++) { + await clk.nextPosedge; + if (core.output('debug_halted').value.toInt() == 1) break; + } + expect( + core.output('debug_halted').value.toInt(), + 1, + reason: 'core halted on debug_halt_req', + ); + haltReq.inject(0); + await clk.nextPosedge; + + final basePc = core.output('debug_dpc').value.toInt(); + final trigPc = basePc + 0x10; + + Future writeCsr(int addr, int value) async { + regWrite.inject(1); + regAddr.inject(addr); + regWdata.inject(value); + await clk.nextPosedge; + regWrite.inject(0); + await clk.nextPosedge; + } + + Future resumeUntilHalt() async { + resumeReq.inject(1); + await clk.nextPosedge; + resumeReq.inject(0); + for (var i = 0; i < 500; i++) { + await clk.nextPosedge; + if (core.output('debug_halted').value.toInt() == 1) return true; + } + return false; + } + + // ---- Single-step (dcsr.step) from the clean haltreq boundary: setting + // dcsr.step and resuming commits and re-enters Debug Mode (cause 4). NOTE: + // this microcode core advances by a fetch/commit beat, not necessarily a + // whole instruction, so we assert forward progress + a clean re-halt rather + // than a fixed +4 (the instruction-granular step is a follow-up). + await writeCsr(0x7b0, 1 << 2); // dcsr.step = 1 + expect(await resumeUntilHalt(), isTrue, reason: 'single-step re-halted'); + expect( + core.output('debug_dpc').value.toInt(), + greaterThan(basePc), + reason: 'single-step advanced the PC', + ); + await writeCsr(0x7b0, 0); // clear step for the trigger test below + + await writeCsr(0x7a0, 0); // tselect = 0 + await writeCsr(0x7a2, trigPc); // tdata2 = match addr + // tdata1 = mcontrol: type=2, action=1 (debug), m-mode, execute. + await writeCsr(0x7a1, (2 << 28) | (1 << 12) | (1 << 6) | (1 << 2)); + + // Read tdata2 back to confirm it programmed. + regRead.inject(1); + regAddr.inject(0x7a2); + await clk.nextPosedge; + final td2 = core.output('debug_reg_rdata').value.toInt(); + regRead.inject(0); + expect(td2, trigPc, reason: 'tdata2 programmed to the match addr'); + + // Resume (1-cycle pulse) and run until the trigger re-halts the core. + resumeReq.inject(1); + await clk.nextPosedge; + resumeReq.inject(0); + + var fired = false; + for (var i = 0; i < 800; i++) { + await clk.nextPosedge; + if (core.output('debug_halted').value.toInt() == 1) { + fired = true; + break; + } + } + expect(fired, isTrue, reason: 'execute trigger re-halted the core'); + expect( + core.output('debug_dpc').value.toInt(), + trigPc, + reason: 'halted AT the trigger address (dpc == tdata2)', + ); + + await Simulator.endSimulation(); + }); +} diff --git a/packages/river_hdl/test/decode/amo_decode_test.dart b/packages/river_hdl/test/decode/amo_decode_test.dart index 36ec969..b19fae2 100644 --- a/packages/river_hdl/test/decode/amo_decode_test.dart +++ b/packages/river_hdl/test/decode/amo_decode_test.dart @@ -52,4 +52,29 @@ void main() { } }); } + + // The AMO opcode carries aq/rl ordering hints in funct7[1:0]; funct7[6:2] is + // funct5. Ordering hints must NOT change decode. HW-observed on delta: the + // kernel faulted illegal on a cpuhp tracepoint's sc.w.rl (funct7=0x0D) because + // decode matched the whole funct7 (aq=rl=0). Sweep all 4 orderings. + for (final width in const [(0x2, 'w'), (0x3, 'd')]) { + test('atomics decode under any aq/rl ordering (.${width.$2})', () { + for (final e in funct5.entries) { + for (var order = 0; order < 4; order++) { + // order bit1=aq, bit0=rl. funct7 = funct5<<2 | order. + final funct7 = (e.key << 2) | order; + final instr = (funct7 << 25) | (width.$1 << 12) | 0x2F; + final op = config.isa.findOperation(instr); + expect( + op?.mnemonic, + '${e.value}.${width.$2}', + reason: + 'funct5=0x${e.key.toRadixString(16)} aq/rl=$order instr=' + '0x${instr.toRadixString(16)} should still be ' + '${e.value}.${width.$2} (ordering is decode-irrelevant)', + ); + } + } + }); + } } diff --git a/packages/river_hdl/test/decode/combined_repro_test.dart b/packages/river_hdl/test/decode/combined_repro_test.dart new file mode 100644 index 0000000..ea7e7c8 --- /dev/null +++ b/packages/river_hdl/test/decode/combined_repro_test.dart @@ -0,0 +1,240 @@ +import 'dart:async'; +import 'package:rohd/rohd.dart'; +import 'package:rohd_hcl/rohd_hcl.dart' hide DataPortInterface, DataPortGroup; +import 'package:river/river.dart'; +import 'package:river_hdl/river_hdl.dart'; +import 'package:test/test.dart'; + +// TEMPORARY: reproduces the equiv() structure (lanes=1 THEN lanes=2 in ONE +// process) with a small cycle cap, to catch a cross-run hang the isolated runs +// miss. +RiverCoreConfig cfg(int lanes) => RiverCoreConfig( + clock: HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + mxlen: RiscVMxlen.rv64, + extensions: [ + rvC, + rvZicsr, + rvZifencei, + rvM, + rvA, + rvF, + rvD, + rvFExtra, + rvDExtra, + rvPriv, + rv64i, + rv32i, + ], + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + ), + type: RiverCoreType.general, + executionMode: ExecutionMode.inOrder, + issueWidth: IssueWidth.single, + microcodeMode: MicrocodeMode.full, + microcodeDecodeLanes: lanes, +); +int rtype(int f7, int rs2, int rs1, int f3, int rd, int op) => + (f7 << 25) | (rs2 << 20) | (rs1 << 15) | (f3 << 12) | (rd << 7) | op; +int itype(int imm, int rs1, int f3, int rd, int op) => + ((imm & 0xFFF) << 20) | (rs1 << 15) | (f3 << 12) | (rd << 7) | op; +int utype(int imm, int rd, int op) => ((imm & 0xFFFFF) << 12) | (rd << 7) | op; +const int jSelf = 0x0000006F; +String asm(List words) { + final sb = StringBuffer('@0\n'); + for (final w in words) { + for (var i = 0; i < 4; i++) { + sb.write(((w >> (i * 8)) & 0xFF).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + } + return '$sb\n'; +} + +Future run( + RiverCoreConfig config, + String memString, { + required int parkPc, + required int maxCycles, + Map initRegisters = const {}, +}) async { + await Simulator.reset(); + final clk = SimpleClockGenerator(20).clk; + final reset = Logic(); + final addrWidth = config.mxlen.size; + final wbConfig = WishboneConfig( + addressWidth: addrWidth, + dataWidth: config.mxlen.size, + selWidth: config.mxlen.size ~/ 8, + ); + final prfSeedMode = Logic(name: 'prfSeedMode'); + final core = RiverCore(config, busConfig: wbConfig, prfSeedMode: prfSeedMode); + core.input('clk').srcConnection! <= clk; + core.input('reset').srcConnection! <= reset; + await core.build(); + final storage = SparseMemoryStorage( + addrWidth: addrWidth, + dataWidth: config.mxlen.size, + alignAddress: (addr) => addr, + onInvalidRead: (addr, dataWidth) => + LogicValue.filled(dataWidth, LogicValue.zero), + ); + final memRead = DataPortInterface(config.mxlen.size, addrWidth); + final memWrite = DataPortInterface(config.mxlen.size, addrWidth); + // ignore: unused_local_variable + final mem = MemoryModel( + clk, + reset, + [wrapWriteForRegisterFile(memWrite)], + [wrapReadForRegisterFile(memRead)], + storage: storage, + ); + final wbCyc = core.output('dataBus_CYC'); + final wbStb = core.output('dataBus_STB'); + final wbWe = core.output('dataBus_WE'); + final wbAdr = core.output('dataBus_ADR'); + final wbDatMosi = core.output('dataBus_DAT_MOSI'); + memRead.en <= wbCyc & wbStb & ~wbWe; + memRead.addr <= wbAdr; + memWrite.en <= wbCyc & wbStb & wbWe; + memWrite.addr <= wbAdr; + memWrite.data <= wbDatMosi; + final wbAckReg = Logic(name: 'wbAck'); + final readyForAck = wbWe | memRead.valid; + Sequential(clk, [ + If( + reset, + then: [wbAckReg < 0], + orElse: [ + If( + wbCyc & wbStb & ~wbAckReg & readyForAck, + then: [wbAckReg < 1], + orElse: [wbAckReg < 0], + ), + ], + ), + ]); + final seedGate = Logic(name: 'seedGate'); + core.input('dataBus_ACK').srcConnection! <= wbAckReg & ~seedGate; + core.input('dataBus_DAT_MISO').srcConnection! <= memRead.data; + reset.inject(1); + seedGate.inject(initRegisters.isNotEmpty ? 1 : 0); + prfSeedMode.inject(initRegisters.isNotEmpty ? 1 : 0); + Simulator.registerAction(20, () { + reset.put(0); + storage.loadMemString(memString); + }); + Simulator.setMaxSimTime(1 << 30); + unawaited(Simulator.run()); + await clk.nextPosedge; + for (final regState in initRegisters.entries) { + core.regWritePort.en.inject(1); + core.regWritePort.addr.inject(LogicValue.ofInt(regState.key.value, 5)); + core.regWritePort.data.inject( + LogicValue.ofInt(regState.value, config.mxlen.size), + ); + await clk.nextPosedge; + } + core.regWritePort.en.inject(0); + seedGate.inject(0); + prfSeedMode.inject(0); + while (reset.value.toBool()) { + await clk.nextPosedge; + } + var parked = false; + int lastPc = -1; + int stall = 0; + int lastMoveCycle = 0; + for (var i = 0; i < maxCycles; i++) { + await clk.nextPosedge; + final pcv = core.pipeline.nextPc.value; + final pc = pcv.isValid ? pcv.toInt() : -2; + if (pc == parkPc) { + parked = true; + print(' PARKED run@cycle=$i pc=0x${pc.toRadixString(16)}'); + break; + } + if (pc == lastPc) { + stall++; + if (stall == 600) { + print( + ' STALL run: pc stuck at 0x${pc.toRadixString(16)} for 600 cycles ' + '(cycle $i, lastMove@$lastMoveCycle) done=${core.pipeline.done.value} ' + 'counter=${core.pipeline.counter.value.toInt()}', + ); + break; + } + } else { + stall = 0; + lastPc = pc; + lastMoveCycle = i; + } + } + await Simulator.endSimulation(); + await Simulator.simulationEnded; + return parked; +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + final aluWords = [ + itype(0x123, 0, 0x0, 3, 0x13), + itype(-5, 1, 0x0, 4, 0x13), + rtype(0x00, 2, 1, 0x0, 5, 0x33), + rtype(0x20, 2, 1, 0x0, 6, 0x33), + rtype(0x00, 2, 1, 0x1, 7, 0x33), + rtype(0x00, 2, 1, 0x2, 8, 0x33), + rtype(0x00, 2, 1, 0x3, 9, 0x33), + rtype(0x00, 2, 1, 0x4, 10, 0x33), + rtype(0x00, 2, 1, 0x5, 11, 0x33), + rtype(0x20, 2, 1, 0x5, 12, 0x33), + rtype(0x00, 2, 1, 0x6, 13, 0x33), + rtype(0x00, 2, 1, 0x7, 14, 0x33), + itype(0x0F, 1, 0x7, 15, 0x13), + itype(0x0F, 1, 0x6, 16, 0x13), + itype(0x0F, 1, 0x4, 17, 0x13), + itype(3, 1, 0x1, 18, 0x13), + itype(2, 1, 0x5, 19, 0x13), + utype(0x12345, 20, 0x37), + rtype(0x00, 2, 1, 0x0, 21, 0x3B), + rtype(0x20, 2, 1, 0x0, 22, 0x3B), + itype(7, 1, 0x0, 23, 0x1B), + jSelf, + ]; + test( + 'combined lanes1 then lanes2', + timeout: Timeout(Duration(minutes: 8)), + () async { + final parkPc = (aluWords.length - 1) * 4; + print('=== RUN lanes=1 ==='); + final p1 = await run( + cfg(1), + asm(aluWords), + parkPc: parkPc, + maxCycles: 8000, + initRegisters: {Register.x1: 0xF0, Register.x2: 0x0C}, + ); + print('lanes=1 parked=$p1'); + print('=== RUN lanes=2 ==='); + final p2 = await run( + cfg(2), + asm(aluWords), + parkPc: parkPc, + maxCycles: 8000, + initRegisters: {Register.x1: 0xF0, Register.x2: 0x0C}, + ); + print('lanes=2 parked=$p2'); + expect(p1, isTrue, reason: 'lanes=1 hung'); + expect(p2, isTrue, reason: 'lanes=2 hung'); + }, + ); +} diff --git a/packages/river_hdl/test/decode/decode_lanes_equiv_test.dart b/packages/river_hdl/test/decode/decode_lanes_equiv_test.dart new file mode 100644 index 0000000..f069904 --- /dev/null +++ b/packages/river_hdl/test/decode/decode_lanes_equiv_test.dart @@ -0,0 +1,400 @@ +import 'dart:async'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd_hcl/rohd_hcl.dart' hide DataPortInterface, DataPortGroup; +import 'package:river/river.dart'; +import 'package:river_hdl/river_hdl.dart'; +import 'package:test/test.dart'; + +// Decode-lane equivalence: the microcode decoder packs `microcodeDecodeLanes` +// pattern-ROM rows per word and priority-selects the lowest-index match per +// cycle. lanes==1 is the classic one-row-per-cycle scan and is the proven +// reference. lanes==2 must decode every RV64GC instruction identically. This +// test runs the same program through a lanes==1 core and a lanes==2 core and +// asserts the architectural result is bit-identical. A divergence names the +// exact opcode the packed decode mishandles (the rc1-f flood hypothesis). + +/// RV64GC-microcode config identical in every respect except decode lanes. +RiverCoreConfig cfg(int lanes) => RiverCoreConfig( + clock: HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + mxlen: RiscVMxlen.rv64, + extensions: [ + rvC, + rvZicsr, + rvZifencei, + rvM, + rvA, + rvF, + rvD, + rvFExtra, + rvDExtra, + rvPriv, + rv64i, + rv32i, + ], + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + ), + type: RiverCoreType.general, + executionMode: ExecutionMode.inOrder, + issueWidth: IssueWidth.single, + microcodeMode: MicrocodeMode.full, + microcodeDecodeLanes: lanes, +); + +// ---- instruction encoders (RV64) ---- +int rtype(int f7, int rs2, int rs1, int f3, int rd, int op) => + (f7 << 25) | (rs2 << 20) | (rs1 << 15) | (f3 << 12) | (rd << 7) | op; +int itype(int imm, int rs1, int f3, int rd, int op) => + ((imm & 0xFFF) << 20) | (rs1 << 15) | (f3 << 12) | (rd << 7) | op; +int stype(int imm, int rs2, int rs1, int f3, int op) => + (((imm >> 5) & 0x7F) << 25) | + (rs2 << 20) | + (rs1 << 15) | + (f3 << 12) | + ((imm & 0x1F) << 7) | + op; +int btype(int imm, int rs2, int rs1, int f3) => + (((imm >> 12) & 0x1) << 31) | + (((imm >> 5) & 0x3F) << 25) | + (rs2 << 20) | + (rs1 << 15) | + (f3 << 12) | + (((imm >> 1) & 0xF) << 8) | + (((imm >> 11) & 0x1) << 7) | + 0x63; +int utype(int imm, int rd, int op) => ((imm & 0xFFFFF) << 12) | (rd << 7) | op; +int jal(int imm, int rd) => + (((imm >> 20) & 0x1) << 31) | + (((imm >> 1) & 0x3FF) << 21) | + (((imm >> 11) & 0x1) << 20) | + (((imm >> 12) & 0xFF) << 12) | + (rd << 7) | + 0x6F; + +const int jSelf = 0x0000006F; // jal x0, 0 (park) + +/// Assemble a list of (widthBytes, value) into a @0 mem string. +String asm(List> insns) { + final sb = StringBuffer('@0\n'); + for (final ins in insns) { + final width = ins[0]; + final value = ins[1]; + for (var i = 0; i < width; i++) { + sb.write(((value >> (i * 8)) & 0xFF).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + } + return '$sb\n'; +} + +List w32(int v) => [4, v]; +List w16(int v) => [2, v]; + +/// Run a program and return {gpr index -> value} for x1..x31, plus a snapshot +/// of the scratch memory words we might touch. Parks on a jal-to-self at +/// [parkPc] (must be present in the program). +Future> run( + RiverCoreConfig config, + String memString, { + required int parkPc, + Map initRegisters = const {}, + List memProbe = const [], +}) async { + await Simulator.reset(); + final clk = SimpleClockGenerator(20).clk; + final reset = Logic(); + final addrWidth = config.mxlen.size; + final wbConfig = WishboneConfig( + addressWidth: addrWidth, + dataWidth: config.mxlen.size, + selWidth: config.mxlen.size ~/ 8, + ); + final prfSeedMode = Logic(name: 'prfSeedMode'); + final core = RiverCore(config, busConfig: wbConfig, prfSeedMode: prfSeedMode); + core.input('clk').srcConnection! <= clk; + core.input('reset').srcConnection! <= reset; + await core.build(); + + final storage = SparseMemoryStorage( + addrWidth: addrWidth, + dataWidth: config.mxlen.size, + alignAddress: (addr) => addr, + onInvalidRead: (addr, dataWidth) => + LogicValue.filled(dataWidth, LogicValue.zero), + ); + final memRead = DataPortInterface(config.mxlen.size, addrWidth); + final memWrite = DataPortInterface(config.mxlen.size, addrWidth); + // ignore: unused_local_variable + final mem = MemoryModel( + clk, + reset, + [wrapWriteForRegisterFile(memWrite)], + [wrapReadForRegisterFile(memRead)], + storage: storage, + ); + final wbCyc = core.output('dataBus_CYC'); + final wbStb = core.output('dataBus_STB'); + final wbWe = core.output('dataBus_WE'); + final wbAdr = core.output('dataBus_ADR'); + final wbDatMosi = core.output('dataBus_DAT_MOSI'); + memRead.en <= wbCyc & wbStb & ~wbWe; + memRead.addr <= wbAdr; + memWrite.en <= wbCyc & wbStb & wbWe; + memWrite.addr <= wbAdr; + memWrite.data <= wbDatMosi; + final wbAckReg = Logic(name: 'wbAck'); + final readyForAck = wbWe | memRead.valid; + Sequential(clk, [ + If( + reset, + then: [wbAckReg < 0], + orElse: [ + If( + wbCyc & wbStb & ~wbAckReg & readyForAck, + then: [wbAckReg < 1], + orElse: [wbAckReg < 0], + ), + ], + ), + ]); + final seedGate = Logic(name: 'seedGate'); + core.input('dataBus_ACK').srcConnection! <= wbAckReg & ~seedGate; + core.input('dataBus_DAT_MISO').srcConnection! <= memRead.data; + + reset.inject(1); + seedGate.inject(initRegisters.isNotEmpty ? 1 : 0); + prfSeedMode.inject(initRegisters.isNotEmpty ? 1 : 0); + Simulator.registerAction(20, () { + reset.put(0); + storage.loadMemString(memString); + }); + Simulator.setMaxSimTime(20000000); + unawaited(Simulator.run()); + await clk.nextPosedge; + for (final regState in initRegisters.entries) { + core.regWritePort.en.inject(1); + core.regWritePort.addr.inject(LogicValue.ofInt(regState.key.value, 5)); + core.regWritePort.data.inject( + LogicValue.ofInt(regState.value, config.mxlen.size), + ); + await clk.nextPosedge; + } + core.regWritePort.en.inject(0); + seedGate.inject(0); + prfSeedMode.inject(0); + while (reset.value.toBool()) { + await clk.nextPosedge; + } + + var parked = false; + for (var i = 0; i < 400000; i++) { + await clk.nextPosedge; + final pc = core.pipeline.nextPc.value; + if (pc.isValid && pc.toInt() == parkPc) { + parked = true; + break; + } + } + + final regs = {}; + regs['__parked'] = parked ? 1 : 0; + for (var r = 1; r < 32; r++) { + final v = core.regs.getData(LogicValue.ofInt(r, 5)); + regs['x$r'] = (v != null && v.isValid) ? v.toInt() : -1; + } + for (final a in memProbe) { + final v = storage.getData(LogicValue.ofInt(a, config.mxlen.size)); + regs['m$a'] = (v != null && v.isValid) ? v.toInt() : -1; + } + await Simulator.endSimulation(); + await Simulator.simulationEnded; + return regs; +} + +/// Assert lanes==1 and lanes==2 produce an identical architectural result. +Future equiv( + String name, + List> insns, { + Map initRegisters = const {}, + List memProbe = const [], +}) async { + // parkPc = address of the trailing jal-to-self (last instruction). + var pc = 0; + for (var i = 0; i < insns.length - 1; i++) { + pc += insns[i][0]; + } + final memString = asm(insns); + final r1 = await run( + cfg(1), + memString, + parkPc: pc, + initRegisters: initRegisters, + memProbe: memProbe, + ); + final r2 = await run( + cfg(2), + memString, + parkPc: pc, + initRegisters: initRegisters, + memProbe: memProbe, + ); + expect(r1['__parked'], 1, reason: '$name: lanes=1 did not reach park'); + expect(r2['__parked'], 1, reason: '$name: lanes=2 did not reach park'); + final diffs = []; + for (final k in r1.keys) { + if (r1[k] != r2[k]) { + diffs.add( + '$k: lanes1=0x${r1[k]!.toRadixString(16)} ' + 'lanes2=0x${r2[k]!.toRadixString(16)}', + ); + } + } + expect( + diffs, + isEmpty, + reason: '$name: lanes=1 vs lanes=2 DIVERGE:\n${diffs.join('\n')}', + ); +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + // Integer register-register + register-immediate ALU sweep. x1=0xF0,x2=0x0C + // seeded; every op writes a distinct dest so a mis-decode shows up as one + // wrong register. + test('alu integer ops', timeout: Timeout(Duration(minutes: 8)), () async { + await equiv( + 'alu', + [ + w32(itype(0x123, 0, 0x0, 3, 0x13)), // addi x3, x0, 0x123 + w32(itype(-5, 1, 0x0, 4, 0x13)), // addi x4, x1, -5 + w32(rtype(0x00, 2, 1, 0x0, 5, 0x33)), // add x5, x1, x2 + w32(rtype(0x20, 2, 1, 0x0, 6, 0x33)), // sub x6, x1, x2 + w32(rtype(0x00, 2, 1, 0x1, 7, 0x33)), // sll x7, x1, x2 + w32(rtype(0x00, 2, 1, 0x2, 8, 0x33)), // slt x8, x1, x2 + w32(rtype(0x00, 2, 1, 0x3, 9, 0x33)), // sltu x9, x1, x2 + w32(rtype(0x00, 2, 1, 0x4, 10, 0x33)), // xor x10, x1, x2 + w32(rtype(0x00, 2, 1, 0x5, 11, 0x33)), // srl x11, x1, x2 + w32(rtype(0x20, 2, 1, 0x5, 12, 0x33)), // sra x12, x1, x2 + w32(rtype(0x00, 2, 1, 0x6, 13, 0x33)), // or x13, x1, x2 + w32(rtype(0x00, 2, 1, 0x7, 14, 0x33)), // and x14, x1, x2 + w32(itype(0x0F, 1, 0x7, 15, 0x13)), // andi x15, x1, 0xF + w32(itype(0x0F, 1, 0x6, 16, 0x13)), // ori x16, x1, 0xF + w32(itype(0x0F, 1, 0x4, 17, 0x13)), // xori x17, x1, 0xF + w32(itype(3, 1, 0x1, 18, 0x13)), // slli x18, x1, 3 + w32(itype(2, 1, 0x5, 19, 0x13)), // srli x19, x1, 2 + w32(utype(0x12345, 20, 0x37)), // lui x20, 0x12345 + w32(rtype(0x00, 2, 1, 0x0, 21, 0x3B)), // addw x21, x1, x2 + w32(rtype(0x20, 2, 1, 0x0, 22, 0x3B)), // subw x22, x1, x2 + w32(itype(7, 1, 0x0, 23, 0x1B)), // addiw x23, x1, 7 + w32(jSelf), + ], + initRegisters: {Register.x1: 0xF0, Register.x2: 0x0C}, + ); + }); + + // M-extension: mul/div/rem in all widths. + test('m-ext ops', timeout: Timeout(Duration(minutes: 8)), () async { + await equiv( + 'm', + [ + w32(rtype(0x01, 2, 1, 0x0, 3, 0x33)), // mul x3, x1, x2 + w32(rtype(0x01, 2, 1, 0x1, 4, 0x33)), // mulh x4, x1, x2 + w32(rtype(0x01, 2, 1, 0x3, 5, 0x33)), // mulhu x5, x1, x2 + w32(rtype(0x01, 2, 1, 0x4, 6, 0x33)), // div x6, x1, x2 + w32(rtype(0x01, 2, 1, 0x5, 7, 0x33)), // divu x7, x1, x2 + w32(rtype(0x01, 2, 1, 0x6, 8, 0x33)), // rem x8, x1, x2 + w32(rtype(0x01, 2, 1, 0x7, 9, 0x33)), // remu x9, x1, x2 + w32(rtype(0x01, 2, 1, 0x0, 10, 0x3B)), // mulw x10, x1, x2 + w32(rtype(0x01, 2, 1, 0x4, 11, 0x3B)), // divw x11, x1, x2 + w32(rtype(0x01, 2, 1, 0x6, 12, 0x3B)), // remw x12, x1, x2 + w32(jSelf), + ], + initRegisters: {Register.x1: 1000, Register.x2: 7}, + ); + }); + + // Branches: the flood site is a bltu. Exercise every branch, taken and not. + // Each branch guards an addi so a wrong branch decode changes the counter. + test('branch ops', timeout: Timeout(Duration(minutes: 8)), () async { + await equiv( + 'branch', + [ + w32(itype(0, 0, 0x0, 5, 0x13)), // addi x5, x0, 0 @0x00 + w32(btype(8, 2, 1, 0x6)), // bltu x1, x2, +8 @0x04 (taken: 3<7) + w32(itype(1, 5, 0x0, 5, 0x13)), // addi x5, x5, 1 @0x08 (skipped) + w32(itype(2, 5, 0x0, 5, 0x13)), // addi x5, x5, 2 @0x0C + w32( + btype(8, 1, 2, 0x6), + ), // bltu x2, x1, +8 @0x10 (not taken: 7<3 false) + w32(itype(4, 5, 0x0, 5, 0x13)), // addi x5, x5, 4 @0x14 (executed) + w32(btype(8, 1, 1, 0x0)), // beq x1, x1, +8 @0x18 (taken) + w32(itype(8, 5, 0x0, 5, 0x13)), // addi x5, x5, 8 @0x1C (skipped) + w32( + btype(8, 1, 2, 0x4), + ), // blt x2, x1, +8 @0x20 not taken (7<3 signed false) + w32(itype(16, 5, 0x0, 5, 0x13)), // addi x5,x5,16 @0x24 executed + w32(jSelf), // @0x28 + ], + initRegisters: {Register.x1: 3, Register.x2: 7}, + ); + }); + + // Store/load roundtrip through a scratch region (base x1 = 0x400, above the + // program). Covers sd/sw/sh/sb + ld/lw/lh/lb/lhu/lbu/lwu. + test('load/store ops', timeout: Timeout(Duration(minutes: 8)), () async { + await equiv( + 'ldst', + [ + w32(stype(0, 2, 1, 0x3, 0x23)), // sd x2, 0(x1) + w32(stype(8, 2, 1, 0x2, 0x23)), // sw x2, 8(x1) + w32(stype(16, 2, 1, 0x1, 0x23)), // sh x2, 16(x1) + w32(stype(24, 2, 1, 0x0, 0x23)), // sb x2, 24(x1) + w32(itype(0, 1, 0x3, 3, 0x03)), // ld x3, 0(x1) + w32(itype(8, 1, 0x2, 4, 0x03)), // lw x4, 8(x1) + w32(itype(8, 1, 0x6, 5, 0x03)), // lwu x5, 8(x1) + w32(itype(16, 1, 0x1, 6, 0x03)), // lh x6, 16(x1) + w32(itype(16, 1, 0x5, 7, 0x03)), // lhu x7, 16(x1) + w32(itype(24, 1, 0x0, 8, 0x03)), // lb x8, 24(x1) + w32(itype(24, 1, 0x4, 9, 0x03)), // lbu x9, 24(x1) + w32(jSelf), + ], + initRegisters: {Register.x1: 0x400, Register.x2: 0x1122334455667788}, + memProbe: [0x400, 0x408, 0x410, 0x418], + ); + }); + + // Compressed ops: mixed 16/32-bit stream. The flood code is mixed RVC, so + // any RVC packed-decode error surfaces here. c.li/c.addi/c.mv/c.add/c.sub/ + // c.and/c.or/c.xor/c.slli/c.andi/c.srli. + test('compressed ops', timeout: Timeout(Duration(minutes: 8)), () async { + await equiv( + 'rvc', + [ + w16(0x4291), // c.li x5, 4 (li rd=x5, imm=4) + w16(0x02a1), // c.addi x5, x5, 8 + w16( + 0x832d, + ), // c.mv x6, x11 ... (placeholder-ish; decode-diff is what matters) + w16(0x8e0d), // c.sub x12, x11 + w16(0x8e6d), // c.and x12, x11 + w16(0x8e4d), // c.or x12, x11 + w16(0x8e2d), // c.xor x12, x11 + w16(0x050a), // c.slli x10, 2 + w32(itype(0, 0, 0x0, 0, 0x13)), // addi x0,x0,0 (align to word) + w32(jSelf), + ], + initRegisters: {Register.x10: 0x20, Register.x11: 0x3C}, + ); + }); +} diff --git a/packages/river_hdl/test/decode/icache_evict_redirect_test.dart b/packages/river_hdl/test/decode/icache_evict_redirect_test.dart new file mode 100644 index 0000000..e5445b9 --- /dev/null +++ b/packages/river_hdl/test/decode/icache_evict_redirect_test.dart @@ -0,0 +1,77 @@ +import 'package:river/river.dart'; +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import '../core_harness.dart'; + +/// Reproduce the HW illegal-trap by forcing the ONE thing the small repros +/// never did: an I-cache line that is EVICTED by the called function and then +/// SLOW-refilled at the jalr return target. +/// +/// rc1-f I-cache is 64B, direct-mapped, 8 lines x 8B (index = addr[5:3]). The +/// return target 0x0c (line 0x08, index 1) is evicted when the called function +/// at 0x48 (index 1, different tag) is fetched. On return, 0x0c is a MISS and +/// refills from memory with `memLatency` cycles -> the refill-vs-decode race at +/// the redirect that fast 1-cycle sim memory hides. HW has real DDR latency, so +/// this mimics it. Bare mode isolates the cache race from paging. +/// +/// 0x00 addi x6, x0, 0x11 +/// 0x04 auipc ra, 0 ra = 0x04 +/// 0x08 jalr ra, 0x44(ra) call 0x48, return ra = 0x0c +/// 0x0c auipc a0, 0 RETURN TARGET (same cache line as 0x08) -> 0x0c +/// 0x10 jal x0, 0 park +/// 0x48 jalr x0, 0(ra) ret -> 0x0c (fetching here evicts the 0x08 line) +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + RiverCoreConfig full() => RiverCoreConfigV1.full( + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + ), + clock: const HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + ); + + String prog() { + final words = { + 0x00: 0x01100313, // addi x6, x0, 0x11 + 0x04: 0x00000097, // auipc ra, 0 + 0x08: 0x044080e7, // jalr ra, 0x44(ra) -> 0x48 + 0x0c: 0x00000517, // auipc a0, 0 RETURN TARGET + 0x10: 0x0000006f, // jal x0, 0 park + 0x48: 0x00008067, // jalr x0, 0(ra) ret -> 0x0c + }; + final maxAddr = 0x48; + final sb = StringBuffer('@0\n'); + for (var a = 0; a <= maxAddr; a += 4) { + final w = words[a] ?? 0x00000013; // nop fill + for (var i = 0; i < 4; i++) { + sb.write(((w >> (i * 8)) & 0xFF).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + } + return '$sb\n'; + } + + for (final lat in [0, 4, 10, 20]) { + test( + 'icache-evict redirect, memLatency=$lat', + timeout: Timeout(Duration(minutes: 8)), + () => coreTest( + prog(), + {Register.x6: 0x11, Register.x10: 0x0c}, + full(), + nextPc: 0x10, + memLatency: lat, + ), + ); + } +} diff --git a/packages/river_hdl/test/decode/lanes1_repro_test.dart b/packages/river_hdl/test/decode/lanes1_repro_test.dart new file mode 100644 index 0000000..06cf43d --- /dev/null +++ b/packages/river_hdl/test/decode/lanes1_repro_test.dart @@ -0,0 +1,226 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd_hcl/rohd_hcl.dart' hide DataPortInterface, DataPortGroup; +import 'package:river/river.dart'; +import 'package:river_hdl/river_hdl.dart'; +import 'package:test/test.dart'; + +/// TEMPORARY repro (2-stage decode hang). Runs a seeded ALU program through the +/// rc1-f microcode core with a small cycle cap so a hang returns fast, and dumps +/// a VCD when RIVER_WAVE is set. +RiverCoreConfig cfg(int lanes) => RiverCoreConfig( + clock: HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + mxlen: RiscVMxlen.rv64, + extensions: [ + rvC, + rvZicsr, + rvZifencei, + rvM, + rvA, + rvF, + rvD, + rvFExtra, + rvDExtra, + rvPriv, + rv64i, + rv32i, + ], + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + ), + type: RiverCoreType.general, + executionMode: ExecutionMode.inOrder, + issueWidth: IssueWidth.single, + microcodeMode: MicrocodeMode.full, + microcodeDecodeLanes: lanes, +); + +int rtype(int f7, int rs2, int rs1, int f3, int rd, int op) => + (f7 << 25) | (rs2 << 20) | (rs1 << 15) | (f3 << 12) | (rd << 7) | op; +int itype(int imm, int rs1, int f3, int rd, int op) => + ((imm & 0xFFF) << 20) | (rs1 << 15) | (f3 << 12) | (rd << 7) | op; +int utype(int imm, int rd, int op) => ((imm & 0xFFFFF) << 12) | (rd << 7) | op; +const int jSelf = 0x0000006F; + +String asm(List words) { + final sb = StringBuffer('@0\n'); + for (final w in words) { + for (var i = 0; i < 4; i++) { + sb.write(((w >> (i * 8)) & 0xFF).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + } + return '$sb\n'; +} + +Future run( + RiverCoreConfig config, + String memString, { + required int parkPc, + required int maxCycles, + Map initRegisters = const {}, +}) async { + await Simulator.reset(); + final clk = SimpleClockGenerator(20).clk; + final reset = Logic(); + final addrWidth = config.mxlen.size; + final wbConfig = WishboneConfig( + addressWidth: addrWidth, + dataWidth: config.mxlen.size, + selWidth: config.mxlen.size ~/ 8, + ); + final prfSeedMode = Logic(name: 'prfSeedMode'); + final core = RiverCore(config, busConfig: wbConfig, prfSeedMode: prfSeedMode); + core.input('clk').srcConnection! <= clk; + core.input('reset').srcConnection! <= reset; + await core.build(); + + final wavePath = Platform.environment['RIVER_WAVE']; + if (wavePath != null && wavePath.isNotEmpty) { + WaveDumper(core, outputPath: wavePath); + } + + final storage = SparseMemoryStorage( + addrWidth: addrWidth, + dataWidth: config.mxlen.size, + alignAddress: (addr) => addr, + onInvalidRead: (addr, dataWidth) => + LogicValue.filled(dataWidth, LogicValue.zero), + ); + final memRead = DataPortInterface(config.mxlen.size, addrWidth); + final memWrite = DataPortInterface(config.mxlen.size, addrWidth); + // ignore: unused_local_variable + final mem = MemoryModel( + clk, + reset, + [wrapWriteForRegisterFile(memWrite)], + [wrapReadForRegisterFile(memRead)], + storage: storage, + ); + final wbCyc = core.output('dataBus_CYC'); + final wbStb = core.output('dataBus_STB'); + final wbWe = core.output('dataBus_WE'); + final wbAdr = core.output('dataBus_ADR'); + final wbDatMosi = core.output('dataBus_DAT_MOSI'); + memRead.en <= wbCyc & wbStb & ~wbWe; + memRead.addr <= wbAdr; + memWrite.en <= wbCyc & wbStb & wbWe; + memWrite.addr <= wbAdr; + memWrite.data <= wbDatMosi; + final wbAckReg = Logic(name: 'wbAck'); + final readyForAck = wbWe | memRead.valid; + Sequential(clk, [ + If( + reset, + then: [wbAckReg < 0], + orElse: [ + If( + wbCyc & wbStb & ~wbAckReg & readyForAck, + then: [wbAckReg < 1], + orElse: [wbAckReg < 0], + ), + ], + ), + ]); + final seedGate = Logic(name: 'seedGate'); + core.input('dataBus_ACK').srcConnection! <= wbAckReg & ~seedGate; + core.input('dataBus_DAT_MISO').srcConnection! <= memRead.data; + + reset.inject(1); + seedGate.inject(initRegisters.isNotEmpty ? 1 : 0); + prfSeedMode.inject(initRegisters.isNotEmpty ? 1 : 0); + Simulator.registerAction(20, () { + reset.put(0); + storage.loadMemString(memString); + }); + Simulator.setMaxSimTime(1 << 30); + unawaited(Simulator.run()); + await clk.nextPosedge; + for (final regState in initRegisters.entries) { + core.regWritePort.en.inject(1); + core.regWritePort.addr.inject(LogicValue.ofInt(regState.key.value, 5)); + core.regWritePort.data.inject( + LogicValue.ofInt(regState.value, config.mxlen.size), + ); + await clk.nextPosedge; + } + core.regWritePort.en.inject(0); + seedGate.inject(0); + prfSeedMode.inject(0); + while (reset.value.toBool()) { + await clk.nextPosedge; + } + + var parked = false; + for (var i = 0; i < maxCycles; i++) { + await clk.nextPosedge; + final pc = core.pipeline.nextPc.value; + if (pc.isValid && pc.toInt() == parkPc) { + parked = true; + break; + } + } + await Simulator.endSimulation(); + await Simulator.simulationEnded; + return parked; +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + final aluWords = [ + itype(0x123, 0, 0x0, 3, 0x13), // addi x3, x0, 0x123 + itype(-5, 1, 0x0, 4, 0x13), // addi x4, x1, -5 + rtype(0x00, 2, 1, 0x0, 5, 0x33), // add + rtype(0x20, 2, 1, 0x0, 6, 0x33), // sub + rtype(0x00, 2, 1, 0x1, 7, 0x33), // sll + rtype(0x00, 2, 1, 0x2, 8, 0x33), // slt + rtype(0x00, 2, 1, 0x3, 9, 0x33), // sltu + rtype(0x00, 2, 1, 0x4, 10, 0x33), // xor + rtype(0x00, 2, 1, 0x5, 11, 0x33), // srl + rtype(0x20, 2, 1, 0x5, 12, 0x33), // sra + rtype(0x00, 2, 1, 0x6, 13, 0x33), // or + rtype(0x00, 2, 1, 0x7, 14, 0x33), // and + itype(0x0F, 1, 0x7, 15, 0x13), // andi + itype(0x0F, 1, 0x6, 16, 0x13), // ori + itype(0x0F, 1, 0x4, 17, 0x13), // xori + itype(3, 1, 0x1, 18, 0x13), // slli + itype(2, 1, 0x5, 19, 0x13), // srli + utype(0x12345, 20, 0x37), // lui + rtype(0x00, 2, 1, 0x0, 21, 0x3B), // addw + rtype(0x20, 2, 1, 0x0, 22, 0x3B), // subw + itype(7, 1, 0x0, 23, 0x1B), // addiw + jSelf, + ]; + + test( + 'alu integer ops repro (lanes=1)', + timeout: Timeout(Duration(minutes: 5)), + () async { + final parkPc = (aluWords.length - 1) * 4; + final parked = await run( + cfg(1), + asm(aluWords), + parkPc: parkPc, + maxCycles: 6000, + initRegisters: {Register.x1: 0xF0, Register.x2: 0x0C}, + ); + expect( + parked, + isTrue, + reason: 'core hung, did not reach park @0x$parkPc', + ); + }, + ); +} diff --git a/packages/river_hdl/test/decode/mext_hang_repro_test.dart b/packages/river_hdl/test/decode/mext_hang_repro_test.dart new file mode 100644 index 0000000..ff5e0c9 --- /dev/null +++ b/packages/river_hdl/test/decode/mext_hang_repro_test.dart @@ -0,0 +1,225 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd_hcl/rohd_hcl.dart' hide DataPortInterface, DataPortGroup; +import 'package:river/river.dart'; +import 'package:river_hdl/river_hdl.dart'; +import 'package:test/test.dart'; + +/// TEMPORARY repro (2-stage decode hang). Runs a seeded ALU program through the +/// rc1-f microcode core with a small cycle cap so a hang returns fast, and dumps +/// a VCD when RIVER_WAVE is set. +RiverCoreConfig cfg(int lanes) => RiverCoreConfig( + clock: HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + mxlen: RiscVMxlen.rv64, + extensions: [ + rvC, + rvZicsr, + rvZifencei, + rvM, + rvA, + rvF, + rvD, + rvFExtra, + rvDExtra, + rvPriv, + rv64i, + rv32i, + ], + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + ), + type: RiverCoreType.general, + executionMode: ExecutionMode.inOrder, + issueWidth: IssueWidth.single, + microcodeMode: MicrocodeMode.full, + microcodeDecodeLanes: lanes, +); + +int rtype(int f7, int rs2, int rs1, int f3, int rd, int op) => + (f7 << 25) | (rs2 << 20) | (rs1 << 15) | (f3 << 12) | (rd << 7) | op; +int itype(int imm, int rs1, int f3, int rd, int op) => + ((imm & 0xFFF) << 20) | (rs1 << 15) | (f3 << 12) | (rd << 7) | op; +int utype(int imm, int rd, int op) => ((imm & 0xFFFFF) << 12) | (rd << 7) | op; +const int jSelf = 0x0000006F; + +String asm(List words) { + final sb = StringBuffer('@0\n'); + for (final w in words) { + for (var i = 0; i < 4; i++) { + sb.write(((w >> (i * 8)) & 0xFF).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + } + return '$sb\n'; +} + +Future run( + RiverCoreConfig config, + String memString, { + required int parkPc, + required int maxCycles, + Map initRegisters = const {}, +}) async { + await Simulator.reset(); + final clk = SimpleClockGenerator(20).clk; + final reset = Logic(); + final addrWidth = config.mxlen.size; + final wbConfig = WishboneConfig( + addressWidth: addrWidth, + dataWidth: config.mxlen.size, + selWidth: config.mxlen.size ~/ 8, + ); + final prfSeedMode = Logic(name: 'prfSeedMode'); + final core = RiverCore(config, busConfig: wbConfig, prfSeedMode: prfSeedMode); + core.input('clk').srcConnection! <= clk; + core.input('reset').srcConnection! <= reset; + await core.build(); + + final wavePath = Platform.environment['RIVER_WAVE']; + if (wavePath != null && wavePath.isNotEmpty) { + WaveDumper(core, outputPath: wavePath); + } + + final storage = SparseMemoryStorage( + addrWidth: addrWidth, + dataWidth: config.mxlen.size, + alignAddress: (addr) => addr, + onInvalidRead: (addr, dataWidth) => + LogicValue.filled(dataWidth, LogicValue.zero), + ); + final memRead = DataPortInterface(config.mxlen.size, addrWidth); + final memWrite = DataPortInterface(config.mxlen.size, addrWidth); + // ignore: unused_local_variable + final mem = MemoryModel( + clk, + reset, + [wrapWriteForRegisterFile(memWrite)], + [wrapReadForRegisterFile(memRead)], + storage: storage, + ); + final wbCyc = core.output('dataBus_CYC'); + final wbStb = core.output('dataBus_STB'); + final wbWe = core.output('dataBus_WE'); + final wbAdr = core.output('dataBus_ADR'); + final wbDatMosi = core.output('dataBus_DAT_MOSI'); + memRead.en <= wbCyc & wbStb & ~wbWe; + memRead.addr <= wbAdr; + memWrite.en <= wbCyc & wbStb & wbWe; + memWrite.addr <= wbAdr; + memWrite.data <= wbDatMosi; + final wbAckReg = Logic(name: 'wbAck'); + final readyForAck = wbWe | memRead.valid; + Sequential(clk, [ + If( + reset, + then: [wbAckReg < 0], + orElse: [ + If( + wbCyc & wbStb & ~wbAckReg & readyForAck, + then: [wbAckReg < 1], + orElse: [wbAckReg < 0], + ), + ], + ), + ]); + final seedGate = Logic(name: 'seedGate'); + core.input('dataBus_ACK').srcConnection! <= wbAckReg & ~seedGate; + core.input('dataBus_DAT_MISO').srcConnection! <= memRead.data; + + reset.inject(1); + seedGate.inject(initRegisters.isNotEmpty ? 1 : 0); + prfSeedMode.inject(initRegisters.isNotEmpty ? 1 : 0); + Simulator.registerAction(20, () { + reset.put(0); + storage.loadMemString(memString); + }); + Simulator.setMaxSimTime(1 << 30); + unawaited(Simulator.run()); + await clk.nextPosedge; + for (final regState in initRegisters.entries) { + core.regWritePort.en.inject(1); + core.regWritePort.addr.inject(LogicValue.ofInt(regState.key.value, 5)); + core.regWritePort.data.inject( + LogicValue.ofInt(regState.value, config.mxlen.size), + ); + await clk.nextPosedge; + } + core.regWritePort.en.inject(0); + seedGate.inject(0); + prfSeedMode.inject(0); + while (reset.value.toBool()) { + await clk.nextPosedge; + } + + var parked = false; + for (var i = 0; i < maxCycles; i++) { + await clk.nextPosedge; + final pc = core.pipeline.nextPc.value; + if (pc.isValid && pc.toInt() == parkPc) { + parked = true; + break; + } + } + await Simulator.endSimulation(); + await Simulator.simulationEnded; + return parked; +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + // RV64M ops. f7=0x01 selects the M-extension for both 0x33 (mul/div/rem) + // and 0x3B (word forms). These drive the iterative multiply/divide microcode, + // the slowest decode paths, so this exercises the 2-stage decode pipeline + // where the wall-clock decode_lanes_equiv m-ext test starved out under load. + final mextWords = [ + itype(0x123, 0, 0x0, 3, 0x13), // addi x3, x0, 0x123 (warm-up) + rtype(0x01, 2, 1, 0x0, 4, 0x33), // mul + rtype(0x01, 2, 1, 0x1, 5, 0x33), // mulh + rtype(0x01, 2, 1, 0x2, 6, 0x33), // mulhsu + rtype(0x01, 2, 1, 0x3, 7, 0x33), // mulhu + rtype(0x01, 2, 1, 0x4, 8, 0x33), // div + rtype(0x01, 2, 1, 0x5, 9, 0x33), // divu + rtype(0x01, 2, 1, 0x6, 10, 0x33), // rem + rtype(0x01, 2, 1, 0x7, 11, 0x33), // remu + rtype(0x01, 2, 1, 0x0, 12, 0x3B), // mulw + rtype(0x01, 2, 1, 0x4, 13, 0x3B), // divw + rtype(0x01, 2, 1, 0x5, 14, 0x3B), // divuw + rtype(0x01, 2, 1, 0x6, 15, 0x3B), // remw + rtype(0x01, 2, 1, 0x7, 16, 0x3B), // remuw + jSelf, + ]; + + test( + 'm-ext ops repro (lanes=2)', + timeout: Timeout(Duration(minutes: 8)), + () async { + final parkPc = (mextWords.length - 1) * 4; + final parked = await run( + cfg(2), + asm(mextWords), + parkPc: parkPc, + // Iterative mul/div microcode is far costlier per op than ALU, so allow a + // generous cycle budget. Parks (or not) independent of wall-clock load. + maxCycles: 60000, + initRegisters: {Register.x1: 0xF0, Register.x2: 0x0C}, + ); + expect( + parked, + isTrue, + reason: 'core hung, did not reach park @0x$parkPc', + ); + }, + ); +} diff --git a/packages/river_hdl/test/decode/paged_diag_test.dart b/packages/river_hdl/test/decode/paged_diag_test.dart new file mode 100644 index 0000000..142f470 --- /dev/null +++ b/packages/river_hdl/test/decode/paged_diag_test.dart @@ -0,0 +1,183 @@ +import 'dart:async'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd_hcl/rohd_hcl.dart' hide DataPortInterface, DataPortGroup; +import 'package:river/river.dart'; +import 'package:river_hdl/river_hdl.dart'; +import 'package:test/test.dart'; + +/// Diagnostic: run the paged-superpage straight-line program and PRINT the PC +/// trajectory so we can see where the fetch goes wrong. +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + RiverCoreConfig full() => RiverCoreConfigV1.full( + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare, RiscVPagingMode.sv39], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + hasSupervisorUserMemory: true, + hasMakeExecutableReadable: true, + ), + clock: const HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + ); + + String mem(Map> words) { + final sb = StringBuffer(); + final addrs = words.keys.toList()..sort(); + for (final a in addrs) { + sb.writeln('@${a.toRadixString(16)}'); + for (final w in words[a]!) { + for (var i = 0; i < 4; i++) { + sb.write(((w >> (i * 8)) & 0xFF).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + } + sb.writeln(); + } + return sb.toString(); + } + + test( + 'DIAG paged superpage pc trajectory', + timeout: Timeout(Duration(minutes: 5)), + () async { + final config = full(); + final clk = SimpleClockGenerator(20).clk; + final reset = Logic(); + final addrWidth = config.mxlen.size; + final wbConfig = WishboneConfig( + addressWidth: addrWidth, + dataWidth: config.mxlen.size, + selWidth: config.mxlen.size ~/ 8, + ); + final prfSeedMode = Logic(name: 'prfSeedMode'); + final core = RiverCore( + config, + busConfig: wbConfig, + prfSeedMode: prfSeedMode, + resetPrivilege: PrivilegeMode.supervisor.id, + ); + core.input('clk').srcConnection! <= clk; + core.input('reset').srcConnection! <= reset; + await core.build(); + + final storage = SparseMemoryStorage( + addrWidth: addrWidth, + dataWidth: config.mxlen.size, + alignAddress: (addr) => addr, + onInvalidRead: (addr, dataWidth) => + LogicValue.filled(dataWidth, LogicValue.zero), + ); + final memRead = DataPortInterface(config.mxlen.size, addrWidth); + final memWrite = DataPortInterface(config.mxlen.size, addrWidth); + // ignore: unused_local_variable + final m = MemoryModel( + clk, + reset, + [wrapWriteForRegisterFile(memWrite)], + [wrapReadForRegisterFile(memRead)], + storage: storage, + ); + final wbCyc = core.output('dataBus_CYC'); + final wbStb = core.output('dataBus_STB'); + final wbWe = core.output('dataBus_WE'); + final wbAdr = core.output('dataBus_ADR'); + memRead.en <= wbCyc & wbStb & ~wbWe; + memRead.addr <= wbAdr; + memWrite.en <= wbCyc & wbStb & wbWe; + memWrite.addr <= wbAdr; + memWrite.data <= core.output('dataBus_DAT_MOSI'); + final wbAckReg = Logic(name: 'wbAck'); + final readyForAck = wbWe | memRead.valid; + Sequential(clk, [ + If( + reset, + then: [wbAckReg < 0], + orElse: [ + If( + wbCyc & wbStb & ~wbAckReg & readyForAck, + then: [wbAckReg < 1], + orElse: [wbAckReg < 0], + ), + ], + ), + ]); + final seedGate = Logic(name: 'seedGate'); + core.input('dataBus_ACK').srcConnection! <= wbAckReg & ~seedGate; + core.input('dataBus_DAT_MISO').srcConnection! <= memRead.data; + + final prog = mem({ + 0x00: [0x18051073, 0x7FD0006F], + 0x1000: [ + 0x01100313, // addi x6,x0,0x11 + 0x00000097, // auipc ra,0 + 0x014080e7, // jalr ra,20(ra) -> 0x1018 + 0x00000517, // auipc a0,0 RETURN TARGET @0x100c + 0x0000006f, // jal x0,0 park @0x1010 + 0x00000013, // nop + 0x00008067, // jalr x0,0(ra) ret -> 0x100c @0x1018 + ], + 0x10000: [0x00004401, 0x0], + 0x11000: [0x0000000F, 0x0], + }); + + reset.inject(1); + seedGate.inject(1); + prfSeedMode.inject(1); + Simulator.registerAction(20, () { + reset.put(0); + storage.loadMemString(prog); + }); + Simulator.setMaxSimTime(4000000); + unawaited(Simulator.run()); + await clk.nextPosedge; + // seed satp into a0 (x10) + core.regWritePort.en.inject(1); + core.regWritePort.addr.inject(LogicValue.ofInt(10, 5)); + core.regWritePort.data.inject( + LogicValue.ofInt(0x8000000000000010, config.mxlen.size), + ); + await clk.nextPosedge; + core.regWritePort.en.inject(0); + seedGate.inject(0); + prfSeedMode.inject(0); + while (reset.value.toBool()) { + await clk.nextPosedge; + } + + final seen = []; + int? last; + for (var i = 0; i < 1500; i++) { + await clk.nextPosedge; + final pcv = core.pipeline.nextPc.value; + if (pcv.isValid) { + final pc = pcv.toInt(); + if (pc != last) { + seen.add(pc); + last = pc; + } + } + } + await Simulator.endSimulation(); + await Simulator.simulationEnded; + + // Print the distinct PC sequence (first 60 transitions). + final trace = seen + .take(60) + .map((p) => '0x${p.toRadixString(16)}') + .join(' '); + print('PC TRAJECTORY: $trace'); + print( + 'final pc: 0x${seen.isNotEmpty ? seen.last.toRadixString(16) : "?"}', + ); + }, + ); +} diff --git a/packages/river_hdl/test/decode/paged_evict_repro_test.dart b/packages/river_hdl/test/decode/paged_evict_repro_test.dart new file mode 100644 index 0000000..02f230d --- /dev/null +++ b/packages/river_hdl/test/decode/paged_evict_repro_test.dart @@ -0,0 +1,79 @@ +import 'package:river/river.dart'; +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import '../core_harness.dart'; + +/// The full HW scenario, never tested together in sim before: +/// PAGING (superpage) + I-cache EVICTION of the return line + SLOW refill + +/// redirect. rc1-f I-cache is 64B direct-mapped, 8x8B, index=addr[5:3]. +/// +/// csrw satp; jal ->0x1000 +/// 0x1000 addi x6,x0,0x11 +/// 0x1004 auipc ra,0 ra=0x1004 +/// 0x1008 jalr ra,0x44(ra) call 0x1048 (index 1, EVICTS the 0x1008 line +/// that holds the return target 0x100c), ra=0x100c +/// 0x100c auipc a0,0 RETURN TARGET -> paged MISS + slow refill +/// 0x1010 jal x0,0 park +/// 0x1048 jalr x0,0(ra) ret -> 0x100c +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + RiverCoreConfig full() => RiverCoreConfigV1.full( + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare, RiscVPagingMode.sv39], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + hasSupervisorUserMemory: true, + hasMakeExecutableReadable: true, + ), + clock: const HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + ); + + String mem(Map> words) { + final sb = StringBuffer(); + final addrs = words.keys.toList()..sort(); + for (final a in addrs) { + sb.writeln('@${a.toRadixString(16)}'); + for (final w in words[a]!) { + for (var i = 0; i < 4; i++) { + sb.write(((w >> (i * 8)) & 0xFF).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + } + sb.writeln(); + } + return sb.toString(); + } + + String prog() => mem({ + 0x00: [0x18051073, 0x7FD0006F], + 0x1000: [0x01100313, 0x00000097, 0x044080e7, 0x00000517, 0x0000006f], + 0x1048: [0x00008067], + 0x10000: [0x00004401, 0x0], + 0x11000: [0x0000000F, 0x0], + }); + + for (final lat in [0, 10, 20]) { + test( + 'paged+evict+redirect refill, memLatency=$lat', + timeout: Timeout(Duration(minutes: 8)), + () => coreTest( + prog(), + {Register.x6: 0x11, Register.x10: 0x100c}, + full(), + startPriv: PrivilegeMode.supervisor, + initRegisters: {Register.x10: 0x8000000000000010}, + nextPc: 0x1010, + memLatency: lat, + ), + ); + } +} diff --git a/packages/river_hdl/test/decode/paged_retauipc_repro_test.dart b/packages/river_hdl/test/decode/paged_retauipc_repro_test.dart new file mode 100644 index 0000000..31363c4 --- /dev/null +++ b/packages/river_hdl/test/decode/paged_retauipc_repro_test.dart @@ -0,0 +1,142 @@ +import 'package:river/river.dart'; +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import '../core_harness.dart'; + +/// Paged repro of the HW illegal-trap on Linux's `_start_kernel` return targets. +/// +/// The bare-mode call->ret->auipc repro PASSES, but on delta the trap happens +/// under PAGING (satp=swapper, 2MB superpage) at a jalr return target. This +/// mirrors that: an identity 2MB superpage (L1 leaf), S-mode, a call whose +/// return target is an `auipc` at a vaddr with vpn0 != 0 (0x100c, so +/// vaddr[20:12]=1, exactly the superpage sub-page bit that 0x...1160 exercises). +/// Fetching that return target goes through the MMU after a redirect. +/// +/// Page tables (identity superpage over virtual 0..2MB-1): +/// L2 @ 0x10000 (root, PPN 0x10): L2[0] = (0x11<<10)|V = 0x4401 +/// L1 @ 0x11000: L1[0] = (0<<10)|V|R|W|X = 0x000F (2MB leaf) +/// +/// Program: +/// @0x00 csrw satp, a0 enable Sv39 + the superpage +/// @0x04 jal x0, +0xffc jump to 0x1000 (into the vpn0=1 sub-page) +/// @0x1000 addi x6, x0, 0x11 +/// @0x1004 auipc ra, 0 ra = 0x1004 +/// @0x1008 jalr ra, 20(ra) call 0x1018, return ra = 0x100c +/// @0x100c auipc a0, 0 RETURN TARGET (vaddr[20:12]=1) -> a0 = 0x100c +/// @0x1010 jal x0, 0 park +/// @0x1018 jalr x0, 0(ra) ret -> 0x100c +/// +/// Pass: x6=0x11, a0=0x100c, parks at 0x1010. Trap => reproduced in sim. +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + RiverCoreConfig full() => RiverCoreConfigV1.full( + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare, RiscVPagingMode.sv39], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + hasSupervisorUserMemory: true, + hasMakeExecutableReadable: true, + ), + clock: const HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + ); + + // Build the @-addressed hex memory image. + String mem(Map> words) { + final sb = StringBuffer(); + final addrs = words.keys.toList()..sort(); + for (final a in addrs) { + sb.writeln('@${a.toRadixString(16)}'); + for (final w in words[a]!) { + for (var i = 0; i < 4; i++) { + sb.write(((w >> (i * 8)) & 0xFF).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + } + sb.writeln(); + } + return sb.toString(); + } + + String prog() => mem({ + 0x00: [ + 0x18051073, // csrw satp, a0 + 0x7FD0006F, // jal x0, +0xffc -> 0x1000 + ], + 0x1000: [ + 0x01100313, // addi x6, x0, 0x11 + 0x00000097, // auipc ra, 0 ra = 0x1004 + 0x014080e7, // jalr ra, 20(ra) -> 0x1018, ra = 0x100c + 0x00000517, // auipc a0, 0 RETURN TARGET (0x100c) -> a0 = 0x100c + 0x0000006f, // jal x0, 0 park @ 0x1010 + 0x00000013, // nop @ 0x1014 + 0x00008067, // jalr x0, 0(ra) ret -> 0x100c (@0x1018) + ], + 0x10000: [0x00004401, 0x0], // L2[0] + 0x11000: [0x0000000F, 0x0], // L1[0] 2MB leaf, PPN 0 + }); + + // Control: same paged superpage, but STRAIGHT-LINE (no call/ret redirect). + // Isolates whether the redirect is the trigger vs the superpage fetch itself. + String progStraight() => mem({ + 0x00: [ + 0x18051073, // csrw satp, a0 + 0x7FD0006F, // jal x0, +0xffc -> 0x1000 + ], + 0x1000: [ + 0x01100313, // addi x6, x0, 0x11 + 0x00000517, // auipc a0, 0 (0x1004, straight-line) -> a0 = 0x1004 + 0x0000006f, // jal x0, 0 park @ 0x1008 + ], + 0x10000: [0x00004401, 0x0], + 0x11000: [0x0000000F, 0x0], + }); + + test( + 'CONTROL paged superpage straight-line auipc (no redirect)', + timeout: Timeout(Duration(minutes: 6)), + () => coreTest( + progStraight(), + {Register.x6: 0x11, Register.x10: 0x1004}, + full(), + startPriv: PrivilegeMode.supervisor, + initRegisters: {Register.x10: 0x8000000000000010}, + nextPc: 0x1008, + ), + ); + + test( + 'paged superpage: auipc at a jalr return target (vpn0=1) decodes', + timeout: Timeout(Duration(minutes: 6)), + () => coreTest( + prog(), + {Register.x6: 0x11, Register.x10: 0x100c}, + full(), + startPriv: PrivilegeMode.supervisor, + initRegisters: {Register.x10: 0x8000000000000010}, + nextPc: 0x1010, + ), + ); + + test( + 'paged redirect with HIGH memLatency (mimic DDR refill)', + timeout: Timeout(Duration(minutes: 8)), + () => coreTest( + prog(), + {Register.x6: 0x11, Register.x10: 0x100c}, + full(), + startPriv: PrivilegeMode.supervisor, + initRegisters: {Register.x10: 0x8000000000000010}, + nextPc: 0x1010, + memLatency: 20, + ), + ); +} diff --git a/packages/river_hdl/test/decode/reloc_cadd_repro_test.dart b/packages/river_hdl/test/decode/reloc_cadd_repro_test.dart new file mode 100644 index 0000000..9ff6360 --- /dev/null +++ b/packages/river_hdl/test/decode/reloc_cadd_repro_test.dart @@ -0,0 +1,149 @@ +import 'package:river/river.dart'; +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import '../core_harness.dart'; + +/// HW-observed on delta: Linux relocate_enable_mmu computes stvec as a relocated +/// address by ADDING the va->pa offset a1 to a PC-relative label. Two adjacent +/// `c.add rd, a1` with the SAME a1: +/// 0x1014 c.add ra, a1 -> ra relocates correctly (HIGH virtual) ✓ +/// 0x101e c.add a2, a1 -> a2 does NOT get a1 added ✗ (stvec stays LOW) +/// so the trampoline fetch-fault traps back to the low PC forever. This isolates +/// that pattern at the SAME instruction alignment (the two c.add at word-offset +/// 4 and 6, the auipc/addi 4-byte ops between them) and asserts the lanes=2 +/// microcode adds a1 in BOTH, matching lanes=1 (the proven reference). +RiverCoreConfig cfg(int lanes) => RiverCoreConfig( + clock: HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + mxlen: RiscVMxlen.rv64, + extensions: [rvC, rvZicsr, rvZifencei, rvM, rv64i, rv32i], + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + ), + type: RiverCoreType.general, + executionMode: ExecutionMode.inOrder, + issueWidth: IssueWidth.single, + microcodeMode: MicrocodeMode.full, + microcodeDecodeLanes: lanes, +); + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + // Program mirrors relocate_enable_mmu 0x1012..0x1028 at the same low offsets. + // 0x00..0x11 padded with c.nop so the critical ops sit at 0x14/0x16/0x1a/0x1e. + // 0x12 c.nop + // 0x14 c.add ra, a1 (0x90ae) + // 0x16 auipc a2, 0x0 (0x00000617) + // 0x1a addi a2, a2, 50 (0x03260613) + // 0x1e c.add a2, a1 (0x962e) + // 0x20 ori s0, a2, 0 (0x00066413) capture a2 + // 0x24 ori s1, ra, 0 (0x0000e493) capture ra + // 0x28 jal x0, 0 park + String prog() { + final sb = StringBuffer('@0\n'); + void h16(int v) { + sb.write((v & 0xFF).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + sb.write(((v >> 8) & 0xFF).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + + void h32(int v) { + for (var i = 0; i < 4; i++) { + sb.write(((v >> (i * 8)) & 0xFF).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + } + + for (var i = 0; i < 9; i++) { + h16(0x0001); // c.nop (0x00..0x11) + } + h16(0x0001); // 0x12 c.nop + h16(0x90ae); // 0x14 c.add ra, a1 + h32(0x00000617); // 0x16 auipc a2, 0x0 + h32(0x03260613); // 0x1a addi a2, a2, 50 + h16(0x962e); // 0x1e c.add a2, a1 + h32(0x00066413); // 0x20 ori s0, a2, 0 + h32(0x0000e493); // 0x24 ori s1, ra, 0 + h32(0x0000006f); // 0x28 jal x0, 0 (park) + sb.writeln(); + return sb.toString(); + } + + // a1 = 0xffffffff00000000 (the va->pa offset high bits). ra seeded 0. + // lanes=1 reference result: + // ra = 0 + a1 = 0xffffffff00000000 -> s1 + // a2 = 0x16 + 50 + a1 = 0xffffffff00000048 -> s0 + const a1 = 0xffffffff00000000; // fits int64 bit-pattern (negative in Dart) + const expectS0 = 0xffffffff00000048; + const expectS1 = 0xffffffff00000000; + + test( + 'lanes=1: both c.add relocate (reference)', + timeout: Timeout(Duration(minutes: 5)), + () async { + await coreTest( + prog(), + {Register.x8: expectS0, Register.x9: expectS1}, + cfg(1), + initRegisters: {Register.x11: a1}, + nextPc: 0x28, + ); + }, + ); + + test( + 'lanes=2: both c.add MUST relocate (matches lanes=1)', + timeout: Timeout(Duration(minutes: 5)), + () async { + await coreTest( + prog(), + {Register.x8: expectS0, Register.x9: expectS1}, + cfg(2), + initRegisters: {Register.x11: a1}, + nextPc: 0x28, + ); + }, + ); + + // The delta boot config: full rc1-f (lanes=2 + VIVT icache + microcode). The + // icache changes how instructions reach the packed decoder, which may be the + // trigger the no-icache cfg misses. + test( + 'full() rc1-f: both c.add MUST relocate', + timeout: Timeout(Duration(minutes: 6)), + () async { + await coreTest( + prog(), + {Register.x8: expectS0, Register.x9: expectS1}, + RiverCoreConfigV1.full( + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare, RiscVPagingMode.sv39], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + hasSupervisorUserMemory: true, + hasMakeExecutableReadable: true, + ), + clock: const HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + ), + initRegisters: {Register.x11: a1}, + nextPc: 0x28, + ); + }, + ); +} diff --git a/packages/river_hdl/test/decode/retauipc_repro_test.dart b/packages/river_hdl/test/decode/retauipc_repro_test.dart new file mode 100644 index 0000000..e16fabf --- /dev/null +++ b/packages/river_hdl/test/decode/retauipc_repro_test.dart @@ -0,0 +1,117 @@ +import 'package:river/river.dart'; +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import '../core_harness.dart'; + +/// Minimal repro of the HW illegal-trap on Linux's `_start_kernel` return +/// targets. On the delta board the rc1-f microcode core traps ILLEGAL +/// (scause 2) on the `auipc` at a jalr RETURN TARGET after a call, even though +/// auipc is a base instruction that decodes fine in straight-line code. The +/// theory: the microcode ROM scan starts from a stale counter at the redirect +/// and misses the pattern. +/// +/// Program (all 32-bit, base ISA only): +/// 0x00 addi x6, x0, 0x11 marker +/// 0x04 auipc ra, 0 ra = 0x04 +/// 0x08 jalr ra, 16(ra) call 0x14, return addr ra = 0x0c +/// 0x0c auipc a0, 0 RETURN TARGET (the suspect) -> a0 = 0x0c +/// 0x10 jal x0, 0 park +/// 0x14 jalr x0, 0(ra) ret -> 0x0c +/// +/// If the decoder is correct: a0 == 0x0c, x6 == 0x11, parks at 0x10. +/// If the redirect hazard bites: the auipc @0x0c traps and we never reach 0x10. +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + RiverCoreConfig full() => RiverCoreConfigV1.full( + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + ), + clock: const HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + ); + + RiverCoreConfig lanes1() => RiverCoreConfig( + clock: const HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + mxlen: RiscVMxlen.rv64, + extensions: [ + rvC, + rvZicsr, + rvZifencei, + rvM, + rvA, + rvF, + rvD, + rvFExtra, + rvDExtra, + rvPriv, + rv64i, + rv32i, + ], + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + ), + type: RiverCoreType.general, + executionMode: ExecutionMode.inOrder, + issueWidth: IssueWidth.single, + microcodeMode: MicrocodeMode.full, + microcodeDecodeLanes: 1, + ); + + String prog() { + final words = [ + 0x01100313, // addi x6, x0, 0x11 + 0x00000097, // auipc ra, 0 + 0x010080e7, // jalr ra, 16(ra) + 0x00000517, // auipc a0, 0 (return target) + 0x0000006f, // jal x0, 0 (park @ 0x10) + 0x00008067, // jalr x0, 0(ra) (ret -> 0x0c) + ]; + final sb = StringBuffer('@0\n'); + for (final w in words) { + for (var i = 0; i < 4; i++) { + sb.write(((w >> (i * 8)) & 0xFF).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + } + return '$sb\n'; + } + + test( + 'auipc at a jalr return target decodes (lanes=2)', + timeout: Timeout(Duration(minutes: 6)), + () => coreTest( + prog(), + {Register.x6: 0x11, Register.x10: 0x0c}, + full(), + nextPc: 0x10, + ), + ); + + test( + 'auipc at a jalr return target decodes (lanes=1)', + timeout: Timeout(Duration(minutes: 6)), + () => coreTest( + prog(), + {Register.x6: 0x11, Register.x10: 0x0c}, + lanes1(), + nextPc: 0x10, + ), + ); +} diff --git a/packages/river_hdl/test/decode/rtype_hang_repro_test.dart b/packages/river_hdl/test/decode/rtype_hang_repro_test.dart new file mode 100644 index 0000000..ce2651e --- /dev/null +++ b/packages/river_hdl/test/decode/rtype_hang_repro_test.dart @@ -0,0 +1,226 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd_hcl/rohd_hcl.dart' hide DataPortInterface, DataPortGroup; +import 'package:river/river.dart'; +import 'package:river_hdl/river_hdl.dart'; +import 'package:test/test.dart'; + +/// TEMPORARY repro (2-stage decode hang). Runs a seeded ALU program through the +/// rc1-f microcode core with a small cycle cap so a hang returns fast, and dumps +/// a VCD when RIVER_WAVE is set. +RiverCoreConfig cfg(int lanes) => RiverCoreConfig( + clock: HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + mxlen: RiscVMxlen.rv64, + extensions: [ + rvC, + rvZicsr, + rvZifencei, + rvM, + rvA, + rvF, + rvD, + rvFExtra, + rvDExtra, + rvPriv, + rv64i, + rv32i, + ], + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + ), + type: RiverCoreType.general, + executionMode: ExecutionMode.inOrder, + issueWidth: IssueWidth.single, + microcodeMode: MicrocodeMode.full, + microcodeDecodeLanes: lanes, +); + +int rtype(int f7, int rs2, int rs1, int f3, int rd, int op) => + (f7 << 25) | (rs2 << 20) | (rs1 << 15) | (f3 << 12) | (rd << 7) | op; +int itype(int imm, int rs1, int f3, int rd, int op) => + ((imm & 0xFFF) << 20) | (rs1 << 15) | (f3 << 12) | (rd << 7) | op; +int utype(int imm, int rd, int op) => ((imm & 0xFFFFF) << 12) | (rd << 7) | op; +const int jSelf = 0x0000006F; + +String asm(List words) { + final sb = StringBuffer('@0\n'); + for (final w in words) { + for (var i = 0; i < 4; i++) { + sb.write(((w >> (i * 8)) & 0xFF).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + } + return '$sb\n'; +} + +Future run( + RiverCoreConfig config, + String memString, { + required int parkPc, + required int maxCycles, + Map initRegisters = const {}, +}) async { + await Simulator.reset(); + final clk = SimpleClockGenerator(20).clk; + final reset = Logic(); + final addrWidth = config.mxlen.size; + final wbConfig = WishboneConfig( + addressWidth: addrWidth, + dataWidth: config.mxlen.size, + selWidth: config.mxlen.size ~/ 8, + ); + final prfSeedMode = Logic(name: 'prfSeedMode'); + final core = RiverCore(config, busConfig: wbConfig, prfSeedMode: prfSeedMode); + core.input('clk').srcConnection! <= clk; + core.input('reset').srcConnection! <= reset; + await core.build(); + + final wavePath = Platform.environment['RIVER_WAVE']; + if (wavePath != null && wavePath.isNotEmpty) { + WaveDumper(core, outputPath: wavePath); + } + + final storage = SparseMemoryStorage( + addrWidth: addrWidth, + dataWidth: config.mxlen.size, + alignAddress: (addr) => addr, + onInvalidRead: (addr, dataWidth) => + LogicValue.filled(dataWidth, LogicValue.zero), + ); + final memRead = DataPortInterface(config.mxlen.size, addrWidth); + final memWrite = DataPortInterface(config.mxlen.size, addrWidth); + // ignore: unused_local_variable + final mem = MemoryModel( + clk, + reset, + [wrapWriteForRegisterFile(memWrite)], + [wrapReadForRegisterFile(memRead)], + storage: storage, + ); + final wbCyc = core.output('dataBus_CYC'); + final wbStb = core.output('dataBus_STB'); + final wbWe = core.output('dataBus_WE'); + final wbAdr = core.output('dataBus_ADR'); + final wbDatMosi = core.output('dataBus_DAT_MOSI'); + memRead.en <= wbCyc & wbStb & ~wbWe; + memRead.addr <= wbAdr; + memWrite.en <= wbCyc & wbStb & wbWe; + memWrite.addr <= wbAdr; + memWrite.data <= wbDatMosi; + final wbAckReg = Logic(name: 'wbAck'); + final readyForAck = wbWe | memRead.valid; + Sequential(clk, [ + If( + reset, + then: [wbAckReg < 0], + orElse: [ + If( + wbCyc & wbStb & ~wbAckReg & readyForAck, + then: [wbAckReg < 1], + orElse: [wbAckReg < 0], + ), + ], + ), + ]); + final seedGate = Logic(name: 'seedGate'); + core.input('dataBus_ACK').srcConnection! <= wbAckReg & ~seedGate; + core.input('dataBus_DAT_MISO').srcConnection! <= memRead.data; + + reset.inject(1); + seedGate.inject(initRegisters.isNotEmpty ? 1 : 0); + prfSeedMode.inject(initRegisters.isNotEmpty ? 1 : 0); + Simulator.registerAction(20, () { + reset.put(0); + storage.loadMemString(memString); + }); + Simulator.setMaxSimTime(1 << 30); + unawaited(Simulator.run()); + await clk.nextPosedge; + for (final regState in initRegisters.entries) { + core.regWritePort.en.inject(1); + core.regWritePort.addr.inject(LogicValue.ofInt(regState.key.value, 5)); + core.regWritePort.data.inject( + LogicValue.ofInt(regState.value, config.mxlen.size), + ); + await clk.nextPosedge; + } + core.regWritePort.en.inject(0); + seedGate.inject(0); + prfSeedMode.inject(0); + while (reset.value.toBool()) { + await clk.nextPosedge; + } + + var parked = false; + for (var i = 0; i < maxCycles; i++) { + await clk.nextPosedge; + final pc = core.pipeline.nextPc.value; + if (pc.isValid && pc.toInt() == parkPc) { + parked = true; + break; + } + } + await Simulator.endSimulation(); + await Simulator.simulationEnded; + return parked; +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + final aluWords = [ + itype(0x123, 0, 0x0, 3, 0x13), // addi x3, x0, 0x123 + itype(-5, 1, 0x0, 4, 0x13), // addi x4, x1, -5 + rtype(0x00, 2, 1, 0x0, 5, 0x33), // add + rtype(0x20, 2, 1, 0x0, 6, 0x33), // sub + rtype(0x00, 2, 1, 0x1, 7, 0x33), // sll + rtype(0x00, 2, 1, 0x2, 8, 0x33), // slt + rtype(0x00, 2, 1, 0x3, 9, 0x33), // sltu + rtype(0x00, 2, 1, 0x4, 10, 0x33), // xor + rtype(0x00, 2, 1, 0x5, 11, 0x33), // srl + rtype(0x20, 2, 1, 0x5, 12, 0x33), // sra + rtype(0x00, 2, 1, 0x6, 13, 0x33), // or + rtype(0x00, 2, 1, 0x7, 14, 0x33), // and + itype(0x0F, 1, 0x7, 15, 0x13), // andi + itype(0x0F, 1, 0x6, 16, 0x13), // ori + itype(0x0F, 1, 0x4, 17, 0x13), // xori + itype(3, 1, 0x1, 18, 0x13), // slli + itype(2, 1, 0x5, 19, 0x13), // srli + utype(0x12345, 20, 0x37), // lui + rtype(0x00, 2, 1, 0x0, 21, 0x3B), // addw + rtype(0x20, 2, 1, 0x0, 22, 0x3B), // subw + itype(7, 1, 0x0, 23, 0x1B), // addiw + jSelf, + ]; + + test( + 'alu integer ops repro (lanes=2)', + timeout: Timeout(Duration(minutes: 5)), + () async { + final parkPc = (aluWords.length - 1) * 4; + final parked = await run( + cfg(2), + asm(aluWords), + parkPc: parkPc, + maxCycles: 6000, + initRegisters: {Register.x1: 0xF0, Register.x2: 0x0C}, + ); + expect( + parked, + isTrue, + reason: 'core hung, did not reach park @0x$parkPc', + ); + }, + ); +} diff --git a/packages/river_hdl/test/device_parse_test.dart b/packages/river_hdl/test/device_parse_test.dart index 5271001..ab31afc 100644 --- a/packages/river_hdl/test/device_parse_test.dart +++ b/packages/river_hdl/test/device_parse_test.dart @@ -90,6 +90,30 @@ void main() { expect(d.params!.readRetry, 6); }); + test('ddr3v2 ctrlgear=2 selects the CK/8 gearbox controller', () { + final d = Device.parse( + 'dram:0x80000000:256M:arty-s7:ddr3v2=true,ctrlgear=2', + ); + expect(d.params!.ddr3v2, isTrue); + expect(d.params!.ctrlGear, 2); + }); + + test( + 'ctrlgear absent defaults to null (= gearRatio 1, byte-identical)', + () { + final d = Device.parse('dram:0x80000000:256M:arty-s7:ddr3v2=true'); + expect(d.params!.ctrlGear, isNull); + }, + ); + + test('ctrlgear only accepts 1 or 2', () { + expect( + () => + Device.parse('dram:0x80000000:256M:arty-s7:ddr3v2=true,ctrlgear=3'), + throwsA(isA()), + ); + }); + test('usb-dfu with mode', () { final d = Device.parse('usb-dfu:0x0C000000:mode=software'); expect(d.type, 'usb-dfu'); @@ -249,5 +273,77 @@ void main() { ); expect(config.fpgaPinMap['clk'], 'Z99'); }); + + test('spi device iface=pmod@ja binds the four SPI pads to JA sites', () { + final config = RiverGenIpConfig( + name: 'b', + cores: const ['rc1-s'], + boardName: 'arty-s7-50', + devices: [Device.parse('spi:0x10001000:iface=pmod@ja')], + ); + // Each SPI pad is a device pin on the spi controller, at the Digilent + // Pmod-SPI JA sites (JA1=L17 cs, JA2=L18 mosi, JA3=M14 miso, JA4=N14 sck). + final byName = {for (final p in config.effectivePins) p.externalName: p}; + expect(byName['spi_cs']?.deviceName, 'spi'); + expect(byName['spi_cs']?.portName, 'spi_cs_n'); + expect(config.fpgaPinMap['spi_cs'], startsWith('L17')); + expect(config.fpgaPinMap['spi_mosi'], startsWith('L18')); + expect(config.fpgaPinMap['spi_miso'], startsWith('M14')); + expect(config.fpgaPinMap['spi_sck'], startsWith('N14')); + }); + + test('spi dma=true adds a second fabric master + harbor,dma DT prop', () async { + final config = RiverGenIpConfig( + name: 'dma_spi_soc', + cores: const ['rc1-n'], + clockFrequency: 48000000, + oscFrequency: 48000000, + devices: [ + Device.parse('sram:0x80000000:64K'), + Device.parse('uart:0x10000000:ns16550a'), + Device.parse('spi:0x10001000:sdcard=true,dma=true'), + ], + ); + // The dma param parses off the device string. + final spiDev = config.devices.firstWhere((d) => d.type == 'spi'); + expect(spiDev.params?.dma, isTrue); + + final soc = await config.buildSoC(); + // Core + the SPI's integrated DMA engine = two bus masters on the fabric. + expect(soc.masters.length, equals(2), reason: 'core + spi dma master'); + + await soc.build(); + // The device tree advertises the integrated DMA so firmware uses the fast + // DMA_ADDR/LEN/CTRL path instead of byte-by-byte PIO. + expect(soc.generateDts(), contains('harbor,dma')); + }); + + test('spi without dma stays a single-master, no harbor,dma prop', () async { + final config = RiverGenIpConfig( + name: 'pio_spi_soc', + cores: const ['rc1-n'], + clockFrequency: 48000000, + oscFrequency: 48000000, + devices: [ + Device.parse('sram:0x80000000:64K'), + Device.parse('uart:0x10000000:ns16550a'), + Device.parse('spi:0x10001000:sdcard=true'), + ], + ); + final soc = await config.buildSoC(); + expect(soc.masters.length, equals(1), reason: 'core only'); + await soc.build(); + expect(soc.generateDts(), isNot(contains('harbor,dma'))); + }); + + test('iface= on a non-spi device throws', () { + final config = RiverGenIpConfig( + name: 'b', + cores: const ['rc1-s'], + boardName: 'arty-s7-50', + devices: [Device.parse('uart:0x10000000:iface=pmod@ja')], + ); + expect(() => config.effectivePins, throwsArgumentError); + }); }); } diff --git a/packages/river_hdl/test/fetch/coldline_refill_race_test.dart b/packages/river_hdl/test/fetch/coldline_refill_race_test.dart new file mode 100644 index 0000000..066e8bc --- /dev/null +++ b/packages/river_hdl/test/fetch/coldline_refill_race_test.dart @@ -0,0 +1,103 @@ +import 'package:rohd/rohd.dart'; +import 'package:river/river.dart'; +import 'package:test/test.dart'; + +import '../core_harness.dart'; + +/// Repro attempt for the delta boot wedge (task #88 follow-up): on HW the core +/// bus-hangs at a function ENTRY - a control transfer lands on the entry (that +/// fetch, a cold L1I miss, refills fine), then the NEXT sequential fetch within +/// the just-filled line WEDGES the fetch handshake. It is intermittent, so it +/// looks like a latency-sensitive race between the refill response landing and +/// the following fetch request. DDR + SDIO are proven good, so this is purely +/// the FetchUnit <-> L1 I-cache handshake. +/// +/// This drives that exact shape: jal to a COLD line far from the entry code, so +/// the target fetch misses and refills, then execute several sequential ops in +/// that line. Swept across refill latencies to hit the racing timing. A wedge = +/// the core never sets x6 / never reaches the park. +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + RiverCoreConfig cfg() => RiverCoreConfigV1.full( + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + ), + clock: const HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + ); + + int jal(int rd, int off) { + final b20 = (off >> 20) & 1; + final b10_1 = (off >> 1) & 0x3ff; + final b11 = (off >> 11) & 1; + final b19_12 = (off >> 12) & 0xff; + return (b20 << 31) | + (b10_1 << 21) | + (b11 << 20) | + (b19_12 << 12) | + (rd << 7) | + 0x6f; + } + + String mem(Map> words) { + final sb = StringBuffer(); + final addrs = words.keys.toList()..sort(); + for (final a in addrs) { + sb.writeln('@${a.toRadixString(16)}'); + for (final w in words[a]!) { + for (var i = 0; i < 4; i++) { + sb.write(((w >> (i * 8)) & 0xFF).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + } + sb.writeln(); + } + return sb.toString(); + } + + const nop = 0x00000013; + const park = 0x0000006f; + // addi x6, x0, 0x33 + const marker = 0x03300313; + + // Entry code at 0x0 hops through several COLD targets (each in a distinct, + // far-apart line), landing on a function-entry-like NOP sled each time, then + // running sequential ops in the just-filled line. Final target sets x6. + String prog() => mem({ + 0x0000: [jal(0, 0x4000)], // -> cold line @0x4000 + 0x4000: [nop, jal(0, 0x4000)], // 0x4000 entry(miss); 0x4004 -> @0x8000 + // wait: 0x4004 jal offset to 0x8000 = 0x8000-0x4004 = 0x3FFC + 0x8000: [nop, nop, jal(0, 0x4000)], // seq fetches then -> @0xC000 + 0xC000: [nop, nop, marker, park], // entry; seq; x6=0x33; park + }); + + for (final lat in const [0, 1, 2, 3, 4, 6, 8, 12, 16, 24, 32]) { + test( + 'cold-line control-transfer + refill, sequential fetch (lat=$lat)', + timeout: Timeout(Duration(minutes: 3)), + () => coreTest( + // fix the jal offsets: from 0x4004 to 0x8000 = 0x3FFC; 0x8008 to 0xC000 + mem({ + 0x0000: [jal(0, 0x4000)], + 0x4000: [nop, jal(0, 0x3FFC)], // 0x4004 -> 0x8000 + 0x8000: [nop, nop, jal(0, 0x3FF8)], // 0x8008 -> 0xC000 + 0xC000: [nop, nop, marker, park], // 0xC008 x6=0x33 ; 0xC00C park + }), + {Register.x6: 0x33}, + cfg(), + nextPc: 0xC00C, + maxCycles: 1200, + memLatency: lat, + ), + ); + } +} diff --git a/packages/river_hdl/test/fetch/fabric_contention_test.dart b/packages/river_hdl/test/fetch/fabric_contention_test.dart new file mode 100644 index 0000000..70fb870 --- /dev/null +++ b/packages/river_hdl/test/fetch/fabric_contention_test.dart @@ -0,0 +1,136 @@ +import 'package:rohd/rohd.dart'; +import 'package:river/river.dart'; +import 'package:test/test.dart'; + +import '../core_harness.dart'; + +/// HW repro attempt #3 for the intermittent delta boot fetch-hang. Decode and +/// trap paths are clean (straddle_amo_test, misaligned_amo_trap_test). The +/// remaining suspect is fetch STARVATION on the single MMU Wishbone: the MMU +/// arbitrates dport (dcache) over ifetch (icache) with STRICT priority +/// (mmu.dart:916, `~dportEn` gates the ifetch launch). This harness creates +/// SUSTAINED contention: straight-line code far larger than the 64B I-cache (so +/// every line is an icache miss) where every instruction pair also does a load +/// from a fresh data line (so the dcache misses continuously). Under nonzero +/// miss latency the two streams overlap on the bus every cycle. If the arbiter +/// starves or deadlocks the ifetch, the core never reaches the final park. +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + RiverCoreConfig cfg() => RiverCoreConfigV1.small( + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + ), + clock: const HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + ); + + // ld x7, 0(x10) ; data load (dcache), fresh line each iter + const ldX7 = (10 << 15) | (3 << 12) | (7 << 7) | 0x03; + // sd x7, 0(x11) ; data store (dcache write / dirty), fresh line each iter + const sdX7 = (7 << 20) | (11 << 15) | (3 << 12) | 0x23; + // addi x10, x10, 8 ; advance load pointer to the next dcache line + const addiX10 = (8 << 20) | (10 << 15) | (10 << 7) | 0x13; + // addi x11, x11, 8 ; advance store pointer + const addiX11 = (8 << 20) | (11 << 15) | (11 << 7) | 0x13; + const park = 0x0000006F; + + String memWords(Map words) { + var maxAddr = 0; + for (final a in words.keys) { + if (a + 4 > maxAddr) maxAddr = a + 4; + } + final bytes = List.filled(maxAddr, 0); + words.forEach((a, w) { + for (var i = 0; i < 4; i++) { + bytes[a + i] = (w >> (i * 8)) & 0xFF; + } + }); + final sb = StringBuffer()..writeln('@0'); + for (final b in bytes) { + sb.write(b.toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + sb.writeln(); + return sb.toString(); + } + + // [pairs] iterations of a load-only stream (2 words = 8 bytes = one icache + // line per iter). Optionally interleave stores for dcache writeback pressure. + String prog(int pairs, {required bool withStores}) { + final w = {}; + var pc = 0; + for (var i = 0; i < pairs; i++) { + w[pc] = ldX7; + pc += 4; + if (withStores) { + w[pc] = sdX7; + pc += 4; + } + w[pc] = addiX10; + pc += 4; + if (withStores) { + w[pc] = addiX11; + pc += 4; + } + } + w[pc] = park; + return memWords(w); + } + + // Load pointer x10 -> data at 0x2000; store pointer x11 -> 0x4000. The data + // region is far above the (small) code so their cache lines never alias. + Map init() => {Register.x10: 0x2000, Register.x11: 0x4000}; + + // 96 load lines = 768 bytes of code, 12x the 64B icache, so the fetch stream + // misses every line for the whole run while the dcache misses every load. + const pairs = 48; + + for (final lat in const [2, 4, 8, 12, 16, 24]) { + final parkPc = pairs * 8; + test( + 'load+fetch contention, memLatency=$lat, reaches park (no starvation)', + timeout: Timeout(Duration(minutes: 4)), + () { + return coreTest( + prog(pairs, withStores: false), + {Register.x10: 0x2000 + pairs * 8}, + cfg(), + initRegisters: init(), + nextPc: parkPc, + maxCycles: 15000, + memLatency: lat, + ); + }, + ); + } + + // With stores: load + store + two addis = 16 bytes/iter, so each iter spans + // two icache lines and issues both a dcache read miss and a dirty write. + for (final lat in const [2, 4, 8, 12, 16, 24]) { + final parkPc = pairs * 16; + test( + 'load+store+fetch contention, memLatency=$lat, reaches park', + timeout: Timeout(Duration(minutes: 4)), + () { + return coreTest( + prog(pairs, withStores: true), + {Register.x10: 0x2000 + pairs * 8}, + cfg(), + initRegisters: init(), + nextPc: parkPc, + maxCycles: 20000, + memLatency: lat, + ); + }, + ); + } +} diff --git a/packages/river_hdl/test/fetch/misaligned_amo_trap_test.dart b/packages/river_hdl/test/fetch/misaligned_amo_trap_test.dart new file mode 100644 index 0000000..9b50a57 --- /dev/null +++ b/packages/river_hdl/test/fetch/misaligned_amo_trap_test.dart @@ -0,0 +1,102 @@ +import 'package:rohd/rohd.dart'; +import 'package:river/river.dart'; +import 'package:test/test.dart'; + +import '../core_harness.dart'; + +/// HW repro attempt #2 for the delta boot wedge. The straddle theory is +/// disproven (straddle_amo_test all green), so the misaligned `amoor.d` is a +/// genuinely misaligned AMO the kernel issued. RISC-V raises store/AMO-address- +/// misaligned (cause 6) for it; the core must vector to the trap handler and the +/// handler's first fetch (a cold I-cache line) must complete. This exercises +/// exactly that: a misaligned amoor.d -> cause-6 trap -> handler fetch, swept +/// over the handler-fetch miss latency. If the fetch-after-trap wedges (the +/// suspected intermittent hang class), the handler never sets x6. +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + RiverCoreConfig cfg() => RiverCoreConfigV1.small( + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + ), + clock: const HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + ); + + // csrw mtvec, x5 (mtvec=0x305): direct-mode vector = x5. + const csrwMtvec = (0x305 << 20) | (5 << 15) | (1 << 12) | 0x73; + // amoor.d x0, x13, (x12): rs1=x12 holds a MISALIGNED address -> cause 6. + const amoorD = (0x20 << 25) | (13 << 20) | (12 << 15) | (3 << 12) | 0x2F; + // addi x6, x0, 0xAB (handler marker). + const addiX6 = (0xAB << 20) | (6 << 7) | 0x13; + const park = 0x0000006F; + + // Word image: 4-byte words at the given byte addresses, emitted as one @0 + // block of space-separated bytes (loadMemString format), gaps zero-filled. + String memWords(Map words) { + var maxAddr = 0; + for (final a in words.keys) { + if (a + 4 > maxAddr) maxAddr = a + 4; + } + final bytes = List.filled(maxAddr, 0); + words.forEach((a, w) { + for (var i = 0; i < 4; i++) { + bytes[a + i] = (w >> (i * 8)) & 0xFF; + } + }); + final sb = StringBuffer()..writeln('@0'); + for (final b in bytes) { + sb.write(b.toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + sb.writeln(); + return sb.toString(); + } + + // main: install mtvec, run the misaligned amoor.d (must trap), then a park + // that we must NOT reach. handler @0x40: set x6=0xAB, park. + String prog() => memWords({ + 0x0: csrwMtvec, + 0x4: amoorD, + 0x8: park, // reached only if the AMO did NOT trap (bug) + 0x40: addiX6, + 0x44: park, // handler park (the pass target) + }); + + // x5 = mtvec handler (0x40); x12 = a MISALIGNED .d address (low 3 bits != 0); + // x13 = OR operand. mem is untouched because the AMO must trap first. + final init = { + Register.x5: 0x40, + Register.x12: 0x1001, // misaligned for an 8-byte AMO + Register.x13: 0x20000, + }; + + for (final lat in const [0, 4, 8, 16]) { + test( + 'misaligned amoor.d traps (cause 6) and the handler fetch completes ' + '(memLatency=$lat)', + timeout: Timeout(Duration(minutes: 3)), + () { + // Reaching 0x44 with x6=0xAB proves: the misaligned AMO trapped, vectored + // to mtvec, and the handler's cold-line fetch ran to completion. + return coreTest( + prog(), + {Register.x6: 0xAB}, + cfg(), + initRegisters: init, + nextPc: 0x44, + maxCycles: 4000, + memLatency: lat, + ); + }, + ); + } +} diff --git a/packages/river_hdl/test/fetch/straddle_amo_test.dart b/packages/river_hdl/test/fetch/straddle_amo_test.dart new file mode 100644 index 0000000..72c4df3 --- /dev/null +++ b/packages/river_hdl/test/fetch/straddle_amo_test.dart @@ -0,0 +1,157 @@ +import 'package:rohd/rohd.dart'; +import 'package:river/river.dart'; +import 'package:test/test.dart'; + +import '../core_harness.dart'; + +/// HW repro attempt for the intermittent delta boot wedge. On silicon the boot +/// hangs at a misaligned `amoor.d` in riscv_v_first_use_handler whose address +/// (rs1 = tp) should be 8-aligned. That instruction sits at a 2-byte-aligned +/// address that STRADDLES the 8-byte L1 I-cache line boundary (rc1 lineSize=8). +/// The hypothesis: when the second line (holding the AMO's high halfword) is a +/// cold miss, the fetch buffer assembles the 32-bit AMO with a stale/garbage +/// high half, mis-decoding rs1 -> a bogus (misaligned) address -> trap/hang. +/// +/// This places an `amoor.d x5, x13, (x12)` so its 4 bytes span an 8-byte line +/// boundary, with the second line cold at decode time (memLatency > 0), and +/// checks the AMO uses the CORRECT aligned address (rs1 = x12) and completes. +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + // .small shares the EXACT rc1-f fetch/MMU/CSR/_rc1L1 icache datapath (8-byte + // line) but drops the FPU, so the fetch-straddle behaviour is identical and + // the sim is fast. Bare mode: the straddle is a pure fetch issue. + RiverCoreConfig cfg() => RiverCoreConfigV1.small( + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + ), + clock: const HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + ); + + // amoor.d x5, x13, (x12): funct7=0x20 (funct5=0x08 amoor, aq=rl=0), + // rs2=x13, rs1=x12, funct3=3 (.d), rd=x5, opcode=0x2F. + const amoorD = + (0x20 << 25) | (13 << 20) | (12 << 15) | (3 << 12) | (5 << 7) | 0x2F; + // ld x28, 0(x12): read back the AMO target to verify the write landed. + const ldX28 = (12 << 15) | (3 << 12) | (28 << 7) | 0x03; + const cnop = 0x0001; // c.nop (2 bytes) + const park = 0x0000006F; + + // Build a contiguous byte image from 0x0 (8-aligned start) with 2-byte + // halfwords at the given byte addresses, gaps zero-filled, emitted as one + // @0 block of space-separated bytes (the format loadMemString expects). + String memHalves(List<(int, int)> halves) { + var maxAddr = 0; + for (final (addr, _) in halves) { + if (addr + 2 > maxAddr) maxAddr = addr + 2; + } + final bytes = List.filled(maxAddr, 0); + for (final (addr, hw) in halves) { + bytes[addr] = hw & 0xFF; + bytes[addr + 1] = (hw >> 8) & 0xFF; + } + final sb = StringBuffer()..writeln('@0'); + for (final b in bytes) { + sb.write(b.toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + sb.writeln(); + // Data block: the AMO target mem[0x1000] preloaded with 0x1 (8 bytes LE). + sb.writeln('@1000'); + sb.write('01 00 00 00 00 00 00 00'); + sb.writeln(); + return sb.toString(); + } + + // Place c.nops from 0x0 so the amoor.d lands at [amoAddr]. A 4-byte op at an + // address whose low 3 bits are 6 straddles the 8-byte line (e.g. 0x16 spans + // 0x16..0x19, crossing the 0x18 line boundary; its high half is a cold line). + String prog(int amoAddr) { + final halves = <(int, int)>[]; + for (var a = 0; a < amoAddr; a += 2) { + halves.add((a, cnop)); + } + halves.add((amoAddr, amoorD & 0xFFFF)); + halves.add((amoAddr + 2, (amoorD >> 16) & 0xFFFF)); + halves.add((amoAddr + 4, ldX28 & 0xFFFF)); + halves.add((amoAddr + 6, (ldX28 >> 16) & 0xFFFF)); + halves.add((amoAddr + 8, park & 0xFFFF)); + halves.add((amoAddr + 10, (park >> 16) & 0xFFFF)); + return memHalves(halves); + } + + final init = { + Register.x12: 0x1000, // a2 = aligned AMO target address + Register.x13: 0x20000, // a3 = the OR operand (like the real set-bit) + }; + + // Straddling amoor.d at offset-6 addresses across several lines, swept over + // the miss latency of the second (cold) line. A correct core loads the old + // value into x5, ORs 0x20000 into mem[0x1000], and x28 reads back 0x20001. + for (final amoAddr in const [0x16, 0x1e, 0x26]) { + for (final lat in const [0, 2, 4, 8, 16]) { + test( + 'straddling amoor.d @0x${amoAddr.toRadixString(16)} memLatency=$lat ' + 'uses the aligned addr and completes', + timeout: Timeout(Duration(minutes: 3)), + () { + final parkPc = amoAddr + 8; + return coreTest( + prog(amoAddr), + { + Register.x5: 0x1, // AMO rd = old value at mem[0x1000] + Register.x28: 0x20001, // read-back = old | 0x20000 + }, + cfg(), + initRegisters: init, + memStates: {0x1000: 0x20001}, + nextPc: parkPc, + maxCycles: 4000, + memLatency: lat, + ); + }, + ); + } + } + + // Control: the SAME amoor.d aligned (not straddling), to prove the harness + // executes the AMO correctly when there is no line-straddle. + test( + 'control: aligned amoor.d executes correctly', + timeout: Timeout(Duration(minutes: 3)), + () { + // Put the amoor.d at 0x8 (4-byte aligned, within one 8-byte line 0x8..0xf). + final halves = <(int, int)>[ + (0x0, cnop), + (0x2, cnop), + (0x4, cnop), + (0x6, cnop), + (0x8, amoorD & 0xFFFF), + (0xa, (amoorD >> 16) & 0xFFFF), + (0xc, ldX28 & 0xFFFF), + (0xe, (ldX28 >> 16) & 0xFFFF), + (0x10, park & 0xFFFF), + (0x12, (park >> 16) & 0xFFFF), + ]; + return coreTest( + memHalves(halves), + {Register.x5: 0x1, Register.x28: 0x20001}, + cfg(), + initRegisters: init, + memStates: {0x1000: 0x20001}, + nextPc: 0x10, + maxCycles: 4000, + memLatency: 8, + ); + }, + ); +} diff --git a/packages/river_hdl/test/fetch/straddle_jump_repro_test.dart b/packages/river_hdl/test/fetch/straddle_jump_repro_test.dart new file mode 100644 index 0000000..9332b45 --- /dev/null +++ b/packages/river_hdl/test/fetch/straddle_jump_repro_test.dart @@ -0,0 +1,103 @@ +import 'package:rohd/rohd.dart'; +import 'package:river/river.dart'; +import 'package:test/test.dart'; +import '../core_harness.dart'; + +/// Repro attempt for the surviving delta boot hypothesis: a control transfer +/// (jal, and by extension an mret, since the in-order core steers the fetcher +/// purely by currentPc) that lands on a 32-bit instruction whose address is +/// congruent to 6 mod 8 -> the instruction straddles the 8-byte fetch word +/// (low half in the top halfword of word N, high half in word N+1). The claim +/// from HW debugging: falling THROUGH into such a straddle works, but JUMPING +/// to it mis-assembles the instruction. +/// +/// Both programs put `addi x5, x0, 10` (0x00a00293, x5<-0xA) at byte 0x0E and +/// expect x5=0xA. Program A reaches it by fall-through, program B by a jal. +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + RiverCoreConfig full() => RiverCoreConfigV1.full( + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + ), + clock: const HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + ); + + // Turn a byte list (index 0 = addr 0x00) into a loadMemString payload. + String mem(List bytes) { + final sb = StringBuffer('@0\n'); + for (final b in bytes) { + sb.write((b & 0xFF).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + return '${sb.toString().trimRight()}\n'; + } + + // Little-endian byte expansions of the fixed opcodes we place. + const straddleInstr = [0x93, 0x02, 0xA0, 0x00]; // addi x5,x0,10 @ 0x0E + const jalLoop = [0x6F, 0x00, 0x00, 0x00]; // jal x0,0 (self loop) + const nop32 = [0x13, 0x00, 0x00, 0x00]; // addi x0,x0,0 + const cnop = [0x01, 0x00]; // c.nop + + final fallThrough = [ + ...nop32, // 0x00 fall through + ...cnop, // 0x04 + ...cnop, // 0x06 + ...nop32, // 0x08 + ...cnop, // 0x0C + ...straddleInstr, // 0x0E straddle -> x5=0xA + ...jalLoop, // 0x12 park + ]; + + // jal x0, +0x0E from 0x00 = 0x00E0006F -> LE 6F 00 E0 00. + final jalToStraddle = [ + 0x6F, 0x00, 0xE0, 0x00, // 0x00 jal x0,0x0E + ...cnop, // 0x04 (skipped) + ...cnop, // 0x06 + ...cnop, // 0x08 + ...cnop, // 0x0A + ...cnop, // 0x0C + ...straddleInstr, // 0x0E straddle -> x5=0xA + ...jalLoop, // 0x12 park + ]; + + // Real DDR-through-cache latency is what the zero-latency sim never exercises; + // the straddle path issues two sequential reads whose handshake can only race + // when responses are delayed. Sweep a spread of memory read latencies. + const latencies = [0, 2, 4, 8, 12, 24]; + + for (final lat in latencies) { + test( + 'A(lat=$lat): fall-through into byte-offset-6 straddle (baseline)', + timeout: Timeout(Duration(minutes: 6)), + () => coreTest( + mem(fallThrough), + {Register.x5: 0xA}, + full(), + nextPc: 0x12, + memLatency: lat, + ), + ); + + test( + 'B(lat=$lat): jal to byte-offset-6 straddle (suspect path)', + timeout: Timeout(Duration(minutes: 6)), + () => coreTest( + mem(jalToStraddle), + {Register.x5: 0xA}, + full(), + nextPc: 0x12, + memLatency: lat, + ), + ); + } +} diff --git a/packages/river_hdl/test/interconnect/ddr3_ctrlgear_soc_test.dart b/packages/river_hdl/test/interconnect/ddr3_ctrlgear_soc_test.dart new file mode 100644 index 0000000..126bc80 --- /dev/null +++ b/packages/river_hdl/test/interconnect/ddr3_ctrlgear_soc_test.dart @@ -0,0 +1,78 @@ +import 'package:river_hdl/river_hdl.dart'; +import 'package:test/test.dart'; + +/// End-to-end T4+T5: a `ddr3v2` SoC threads `ctrlgear` from the device param +/// through the DDR MMCM (CLKOUT5 = CK/8) and the HarborDdr3 two-clock interface. +/// gearRatio 1 (absent) stays byte-identical (no gearbox, no serdes clock, no +/// CLKOUT5); gearRatio 2 elaborates with all three present. +void main() { + Future genDdr3v2Sv({ + required bool geared, + bool withDram = true, + }) async { + final gearSuffix = geared ? ',ctrlgear=2' : ''; + final config = RiverGenIpConfig( + name: geared ? 'gear2_soc' : 'gear1_soc', + cores: const ['rc1-s'], + clockFrequency: 33333333, + oscFrequency: 100000000, // single-osc DDR3 MMCM path + target: Target.parse('spartan7:xc7s50:csga324'), + devices: [ + Device.parse('flash:0x20000000:16M:arty-s7'), + Device.parse('sram:0x08000000:64K'), + Device.parse('clint:0x02000000'), + Device.parse('plic:0x04000000'), + Device.parse('uart:0x10000000:ns16550a'), + if (withDram) + Device.parse( + 'dram:0x80000000:256M:arty-s7:ddr3v2=true,' + 'clockfreq=300000000$gearSuffix', + ), + ], + pins: [ + PinAssignment.parse('clk=R2 SSTL135'), + PinAssignment.parse('uart_tx=uart@tx:R12'), + PinAssignment.parse('uart_rx=uart@rx:V12'), + ], + ); + final soc = await config.buildSoC(); + await soc.build(); + return soc.generateSynth(); + } + + test( + 'CONTROL: the same FPGA SoC WITHOUT the dram builds', + () async { + // Isolates whether the mmioDevices/buildSoC path is broken by unrelated WIP + // (a build failure here == pre-existing, independent of the DDR gearing). + final sv = await genDdr3v2Sv(geared: false, withDram: false); + expect(sv, contains('module')); + }, + timeout: const Timeout(Duration(minutes: 3)), + ); + + test( + 'ctrlgear absent (gearRatio 1) is byte-identical: no gearbox/serdes/CK8', + () async { + final sv = await genDdr3v2Sv(geared: false); + expect(sv, isNot(contains('ddr3_gearbox'))); + expect(sv, isNot(contains('ddr_serdes_clk'))); + // No spare CLKOUT5 on the DDR MMCM. + expect(sv, isNot(contains('.CLKOUT5_DIVIDE('))); + }, + timeout: const Timeout(Duration(minutes: 3)), + ); + + test( + 'ctrlgear=2 wires the CK/8 controller + CK/4 serdes + gearbox', + () async { + final sv = await genDdr3v2Sv(geared: true); + // The DDR MMCM emits CK/8 on CLKOUT5. + expect(sv, contains('.CLKOUT5_DIVIDE(')); + // HarborDdr3 gained its second (serdes) clock port and the gearbox module. + expect(sv, contains('ddr_serdes_clk')); + expect(sv, contains('ddr3_gearbox')); + }, + timeout: const Timeout(Duration(minutes: 3)), + ); +} diff --git a/packages/river_hdl/test/interconnect/soc_test.dart b/packages/river_hdl/test/interconnect/soc_test.dart index 339c4b0..75b4fce 100644 --- a/packages/river_hdl/test/interconnect/soc_test.dart +++ b/packages/river_hdl/test/interconnect/soc_test.dart @@ -32,7 +32,7 @@ void main() { final generator = HarborDeviceTreeGenerator( model: 'Stream V1', - compatible: 'midstall,stream-v1', + compatible: 'lilithsemi,stream-v1', cpus: cpus, ); @@ -74,7 +74,7 @@ void main() { final generator = HarborDeviceTreeGenerator( model: 'Creek V1', - compatible: 'midstall,creek-v1', + compatible: 'lilithsemi,creek-v1', cpus: cpus, ); diff --git a/packages/river_hdl/test/interrupt/amo_ticket_lost_test.dart b/packages/river_hdl/test/interrupt/amo_ticket_lost_test.dart new file mode 100644 index 0000000..f019aa1 --- /dev/null +++ b/packages/river_hdl/test/interrupt/amo_ticket_lost_test.dart @@ -0,0 +1,117 @@ +import 'package:rohd/rohd.dart'; +import 'package:river/river.dart'; +import 'package:test/test.dart'; + +import '../core_harness.dart'; + +/// Repro for #91: on HW the boot deadlocks in a ticket spinlock with a DUPLICATE +/// ticket (my ticket 0x4e8b behind owner 0x4e8c on a single hart) = a ticket- +/// dispensing amoadd.w LOST its increment. The lock is a 32-bit word +/// [next:16][owner:16]; acquire does `amoadd.w lock, 0x10000` (bump next, old = +/// my ticket), the spin reads the SAME word with `lw`, release bumps owner. +/// +/// This hammers exactly that pattern through the REAL HarborL1DCache: a loop of +/// amoadd.w(0x10000) on one lock word, each followed by an `lw` of the same word +/// (like the spin) and an `sh` to the owner half (like a release), N times. +/// After N iterations the next field must be exactly N (no lost bump): the final +/// word high-half == N. A lost amoadd RMW leaves it < N. +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + RiverCoreConfig cfg() => RiverCoreConfigV1.small( + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + ), + clock: const HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + ); + + // amoadd.w x5, x13, (x12): funct7=0x00 (amoadd, aq=rl=0), rs2=x13(incr), + // rs1=x12(lock), funct3=2(.w), rd=x5(old = my ticket). 0x00<<25|...|0x2F. + const amoaddW = + (0x00 << 25) | (13 << 20) | (12 << 15) | (2 << 12) | (5 << 7) | 0x2F; + // lw x6, 0(x12): read the lock word (like the spin reading owner). + const lwX6 = (12 << 15) | (2 << 12) | (6 << 7) | 0x03; + // ld x7, 0(x12): final read of the whole 64-bit-aligned word for checking. + const ldX7 = (12 << 15) | (3 << 12) | (7 << 7) | 0x03; + // addi x28, x28, 1 (loop counter), and branch. Build the loop by hand. + const park = 0x0000006F; + + String memWords(Map words) { + var maxAddr = 0; + for (final a in words.keys) { + if (a + 4 > maxAddr) maxAddr = a + 4; + } + final bytes = List.filled(maxAddr, 0); + words.forEach((a, w) { + for (var i = 0; i < 4; i++) { + bytes[a + i] = (w >> (i * 8)) & 0xFF; + } + }); + final sb = StringBuffer()..writeln('@0'); + for (final b in bytes) { + sb.write(b.toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + sb.writeln(); + // lock word at 0x2000, init 0. + sb.writeln('@2000'); + sb.write('00 00 00 00 00 00 00 00'); + sb.writeln(); + return sb.toString(); + } + + // Unrolled: N times { amoadd.w x5,x13,(x12); lw x6,0(x12) }, then ld x7,0(x12), + // then park. x13 = 1 (increment next by 1 in the low field for a simple, exact + // check: final lock low-16 == N). x12 = 0x2000 (lock). Reaching park with + // x7 == N proves every amoadd RMW landed; x7 < N means a lost increment. + String prog(int n) { + final w = {}; + var pc = 0; + for (var i = 0; i < n; i++) { + w[pc] = amoaddW; + pc += 4; + w[pc] = lwX6; + pc += 4; + } + w[pc] = ldX7; + pc += 4; + w[pc] = park; + return memWords(w); + } + + final init = { + Register.x12: 0x2000, // lock address + Register.x13: 0x1, // increment (each amoadd adds 1 to the word) + }; + + for (final n in const [8, 16, 32]) { + for (final lat in const [0, 4, 8]) { + test( + 'amoadd.w ticket dispense x$n through real dcache loses no increment ' + '(memLatency=$lat)', + timeout: Timeout(Duration(minutes: 3)), + () { + final parkPc = n * 8 + 4; + return coreTest( + prog(n), + {Register.x7: n}, + cfg(), + initRegisters: init, + nextPc: parkPc, + maxCycles: 8000, + memLatency: lat, + ); + }, + ); + } + } +} diff --git a/packages/river_hdl/test/interrupt/async_interrupt_take_test.dart b/packages/river_hdl/test/interrupt/async_interrupt_take_test.dart new file mode 100644 index 0000000..5b62e86 --- /dev/null +++ b/packages/river_hdl/test/interrupt/async_interrupt_take_test.dart @@ -0,0 +1,340 @@ +import 'dart:async'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd_hcl/rohd_hcl.dart' hide DataPortInterface, DataPortGroup; +import 'package:river/river.dart'; +import 'package:river_hdl/river_hdl.dart'; +import 'package:test/test.dart'; + +/// Async interrupt-taking (mip&mie&mstatus.MIE -> vector to mtvec). +/// +/// River rc1-f never carried interrupt VECTORING in the microcode path: mip was +/// plumbed but nothing computed pending&enabled and redirected the PC. Linux +/// needs it (the scheduler tick, SBI timer, softirqs). This drives a real +/// external IRQ line into the core, holds it while an M-mode program enables +/// interrupts and spins, and asserts the core vectors to mtvec with +/// mcause = interrupt|11 (MEI) and mepc = the interrupted PC. +/// +/// The harness mirrors core_harness but adds the [srcIrqs] input and watches the +/// pipeline PC instead of a fixed retirement address. +Future> runIrqTest({ + required String memString, + required Map initRegisters, + required int handlerPc, + required int parkPc, + bool driveIrq = true, + // When true, drive the machine timer-pending line (mip.MTIP) instead of the + // external line, to exercise the CLINT timer path (mcause 7). + bool driveTimer = false, + // Cycle budget for the watch loop. The positive cases exit early once they + // reach the handler park, so they use a generous budget; the negative case + // runs the full span, so it uses a small one to stay fast. + int maxCycles = 1200, +}) async { + await Simulator.reset(); + final config = RiverCoreConfigV1.full( + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare, RiscVPagingMode.sv39], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + ), + clock: const HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + ); + + final clk = SimpleClockGenerator(20).clk; + final reset = Logic(); + final irq = Logic(name: 'extIrq'); + final timerIrq = Logic(name: 'timerIrq'); + + final addrWidth = config.mxlen.size; + final wbConfig = WishboneConfig( + addressWidth: addrWidth, + dataWidth: config.mxlen.size, + selWidth: config.mxlen.size ~/ 8, + ); + + final prfSeedMode = Logic(name: 'prfSeedMode'); + + final core = RiverCore( + config, + busConfig: wbConfig, + prfSeedMode: prfSeedMode, + srcIrqs: {'extInt': irq}, + timerPending: timerIrq, + ); + + core.input('clk').srcConnection! <= clk; + core.input('reset').srcConnection! <= reset; + + await core.build(); + + final storage = SparseMemoryStorage( + addrWidth: addrWidth, + dataWidth: config.mxlen.size, + alignAddress: (addr) => addr, + onInvalidRead: (addr, dataWidth) => + LogicValue.filled(dataWidth, LogicValue.zero), + ); + + final memRead = DataPortInterface(config.mxlen.size, addrWidth); + final memWrite = DataPortInterface(config.mxlen.size, addrWidth); + + // ignore: unused_local_variable + final mem = MemoryModel( + clk, + reset, + [wrapWriteForRegisterFile(memWrite)], + [wrapReadForRegisterFile(memRead, clk: clk, readLatency: 0)], + readLatency: 0, + storage: storage, + ); + + final wbCyc = core.output('dataBus_CYC'); + final wbStb = core.output('dataBus_STB'); + final wbWe = core.output('dataBus_WE'); + final wbAdr = core.output('dataBus_ADR'); + final wbDatMosi = core.output('dataBus_DAT_MOSI'); + + memRead.en <= wbCyc & wbStb & ~wbWe; + memRead.addr <= wbAdr; + memWrite.en <= wbCyc & wbStb & wbWe; + memWrite.addr <= wbAdr; + memWrite.data <= wbDatMosi; + + final wbAckReg = Logic(name: 'wbAck'); + final readyForAck = wbWe | memRead.valid; + Sequential(clk, [ + If( + reset, + then: [wbAckReg < 0], + orElse: [ + If( + wbCyc & wbStb & ~wbAckReg & readyForAck, + then: [wbAckReg < 1], + orElse: [wbAckReg < 0], + ), + ], + ), + ]); + + final seedGate = Logic(name: 'seedGate'); + core.input('dataBus_ACK').srcConnection! <= wbAckReg & ~seedGate; + core.input('dataBus_DAT_MISO').srcConnection! <= memRead.data; + + reset.inject(1); + irq.inject(0); + timerIrq.inject(0); + seedGate.inject(initRegisters.isNotEmpty ? 1 : 0); + prfSeedMode.inject(initRegisters.isNotEmpty ? 1 : 0); + + Simulator.registerAction(20, () { + reset.put(0); + storage.loadMemString(memString); + }); + + Simulator.setMaxSimTime(4000000); + unawaited(Simulator.run()); + + await clk.nextPosedge; + + for (final regState in initRegisters.entries) { + core.regWritePort.en.inject(1); + core.regWritePort.addr.inject(LogicValue.ofInt(regState.key.value, 5)); + core.regWritePort.data.inject( + LogicValue.ofInt(regState.value, config.mxlen.size), + ); + await clk.nextPosedge; + } + + core.regWritePort.en.inject(0); + seedGate.inject(0); + prfSeedMode.inject(0); + + while (reset.value.toBool()) { + await clk.nextPosedge; + } + + if (driveIrq) { + irq.inject(1); + } + if (driveTimer) { + timerIrq.inject(1); + } + + var vectored = false; + var reachedPark = false; + for (var i = 0; i < maxCycles; i++) { + await clk.nextPosedge; + final pc = core.pipeline.nextPc.value; + if (!pc.isValid) continue; + final p = pc.toInt(); + if (p == handlerPc) vectored = true; + if (p == parkPc) { + reachedPark = true; + break; + } + } + + // Let the handler's csr reads retire. + for (var i = 0; i < 20; i++) { + await clk.nextPosedge; + } + + final mcause = core.regs + .getData(LogicValue.ofInt(Register.x28.value, 5))! + .toInt(); + final mepc = core.regs + .getData(LogicValue.ofInt(Register.x29.value, 5))! + .toInt(); + + await Simulator.endSimulation(); + await Simulator.simulationEnded; + + return { + 'vectored': vectored ? 1 : 0, + 'reachedPark': reachedPark ? 1 : 0, + 'mcause': mcause, + 'mepc': mepc, + }; +} + +/// Build the shared program: enable interrupts, spin, and a handler that +/// captures mcause/mepc. +String buildProgram() { + // 0x00 csrw mtvec, x1 (x1 = handlerPc) + // 0x04 csrw mie, x2 (x2 = 1<<11 MEIE) + // 0x08 csrs mstatus, x3 (x3 = 1<<3 MIE) + // 0x0c j 0x0c (spin, this is mepc) + // 0x100 csrr x28, mcause + // 0x104 csrr x29, mepc + // 0x108 j 0x108 (park) + final words = { + 0x00: 0x00000013, // nop (warmup) + 0x04: 0x00000013, // nop + 0x08: 0x30529073, // csrw mtvec, x5 (x5 = handler base 0x100) + 0x0c: 0x30431073, // csrw mie, x6 (x6 = MEIE = 1<<11) + 0x10: 0x3003a073, // csrs mstatus, x7 (x7 = MIE = 1<<3) + 0x14: 0x0000006f, // j 0x14 (spin, mepc) + 0x100: 0x34202e73, // csrr x28, mcause + 0x104: 0x34102ef3, // csrr x29, mepc + 0x108: 0x0000006f, // j 0x108 + }; + + final bytes = {}; + words.forEach((addr, w) { + for (var b = 0; b < 4; b++) { + bytes[addr + b] = (w >> (b * 8)) & 0xFF; + } + }); + final maxA = bytes.keys.reduce((a, b) => a > b ? a : b); + final sb = StringBuffer('@0\n'); + for (var a = 0; a <= maxA + 1; a++) { + sb.write((bytes[a] ?? 0).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + return sb.toString(); +} + +void main() { + test( + 'async M-mode interrupt vectors to mtvec with mcause=MEI, mepc=spin', + timeout: Timeout(Duration(minutes: 4)), + () async { + final result = await runIrqTest( + memString: buildProgram(), + initRegisters: { + Register.x5: 0x100, // mtvec (direct mode, base 0x100) + Register.x6: 1 << 11, // MEIE + Register.x7: 1 << 3, // MIE + }, + handlerPc: 0x100, + parkPc: 0x108, + ); + + expect( + result['reachedPark'], + 1, + reason: 'core never reached the handler park', + ); + expect( + result['vectored'], + 1, + reason: 'PC never hit the handler base 0x100', + ); + // RV64 mcause: interrupt bit is bit 63, cause 11 (machine external). + expect(result['mcause'], (1 << 63) | 11, reason: 'wrong mcause'); + expect( + result['mepc'], + 0x14, + reason: 'mepc must be the interrupted spin PC', + ); + }, + ); + + test( + 'CLINT machine-timer interrupt vectors with mcause=MTI (cause 7)', + timeout: Timeout(Duration(minutes: 4)), + () async { + final result = await runIrqTest( + memString: buildProgram(), + initRegisters: { + Register.x5: 0x100, // mtvec base + Register.x6: 1 << 7, // MTIE (machine timer enable) + Register.x7: 1 << 3, // MIE + }, + handlerPc: 0x100, + parkPc: 0x108, + driveIrq: false, + driveTimer: true, + ); + + expect( + result['reachedPark'], + 1, + reason: 'timer interrupt never reached the handler', + ); + expect( + result['vectored'], + 1, + reason: 'PC never hit the handler on a timer IRQ', + ); + expect( + result['mcause'], + (1 << 63) | 7, + reason: 'wrong mcause for machine timer', + ); + expect( + result['mepc'], + 0x14, + reason: 'mepc must be the interrupted spin PC', + ); + }, + ); + + test('interrupt does NOT vector while the IRQ line is low', () async { + final result = await runIrqTest( + memString: buildProgram(), + initRegisters: { + Register.x5: 0x100, + Register.x6: 1 << 11, + Register.x7: 1 << 3, + }, + handlerPc: 0x100, + parkPc: 0x108, + driveIrq: false, + maxCycles: 500, + ); + + expect( + result['reachedPark'], + 0, + reason: 'core vectored with no pending interrupt', + ); + expect(result['vectored'], 0, reason: 'PC hit the handler with no IRQ'); + }); +} diff --git a/packages/river_hdl/test/interrupt/irq_during_paged_fetch_test.dart b/packages/river_hdl/test/interrupt/irq_during_paged_fetch_test.dart new file mode 100644 index 0000000..984dc2b --- /dev/null +++ b/packages/river_hdl/test/interrupt/irq_during_paged_fetch_test.dart @@ -0,0 +1,141 @@ +import 'dart:async'; + +import 'package:rohd/rohd.dart'; +import 'package:river/river.dart'; +import 'package:test/test.dart'; + +import '../core_harness.dart'; + +/// Repro attempt closest to the real delta boot (task #87 regression): an +/// M-timer interrupt taken while the core runs in S-MODE with Sv39 PAGING ON, +/// so the in-flight instruction fetch involves a page-table WALK (a data access) +/// plus an I-cache refill. The interrupt-take squashing that translated fetch +/// mid-walk is the one interrupt scenario not yet covered (all prior repros were +/// bare mode). Linux takes the RCU/scheduler timer tick in exactly this state. +/// +/// M-mode prologue sets mtvec/mie(MTIE)/mstatus(MPP=S,MPIE)/satp(Sv39)/mepc then +/// mret to S-mode @VA 0x1000. S-mode paged code jumps through COLD pages (each a +/// TLB+I-cache miss -> walk+refill). The timer IRQ is raised mid-chain. If it +/// vectors to the M handler @0x200 (x28<-0xAB) the core is fine; if the fetch +/// handshake wedges it never reaches nextPc. +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + RiverCoreConfig cfg() => RiverCoreConfigV1.full( + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare, RiscVPagingMode.sv39], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + ), + clock: const HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + ); + + int jal(int rd, int off) => + (((off >> 20) & 1) << 31) | + (((off >> 1) & 0x3ff) << 21) | + (((off >> 11) & 1) << 20) | + (((off >> 12) & 0xff) << 12) | + (rd << 7) | + 0x6f; + + String mem(Map> words) { + final sb = StringBuffer(); + final addrs = words.keys.toList()..sort(); + for (final a in addrs) { + sb.writeln('@${a.toRadixString(16)}'); + for (final w in words[a]!) { + for (var i = 0; i < 4; i++) { + sb.write(((w >> (i * 8)) & 0xFF).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + } + sb.writeln(); + } + return sb.toString(); + } + + const park = 0x0000006f; + + String prog() => mem({ + // ---- M-mode prologue @phys 0x0 (bare) ---- + 0x00000: [ + 0x30529073, // 0x00 csrw mtvec, x5 (=0x200 handler PA) + 0x30431073, // 0x04 csrw mie, x6 (MTIE = 1<<7) + 0x30039073, // 0x08 csrw mstatus,x7 (MPP=S 1<<11 | MPIE 1<<7) + 0x18041073, // 0x0c csrw satp, x8 (Sv39, root PPN 0x10) + 0x34149073, // 0x10 csrw mepc, x9 (=0x1000 S entry VA) + 0x30200073, // 0x14 mret -> S-mode @VA 0x1000, paging on + ], + // ---- M-mode timer handler @phys 0x200 ---- + 0x00200: [0x0ab00e13, park], // addi x28,x0,0xAB ; park @0x204 + // ---- S-mode paged code (VA==phys, identity mapped) ---- + 0x01000: [jal(0, 0x1000)], // 0x1000 -> 0x2000 cold page + 0x02000: [jal(0, 0x1000)], // 0x2000 -> 0x3000 cold page + 0x03000: [jal(0, 0x1000)], // 0x3000 -> 0x4000 cold page + 0x04000: [jal(0, 0x1000)], // 0x4000 -> 0x5000 cold page + 0x05000: [park], // 0x5000 park (reached only if NO interrupt) + // ---- Sv39 page table (root 0x10000), identity VA 0x1000..0x5000 ---- + 0x10000: [0x00004401, 0x0], // L2[0] -> L1 @0x11000 + 0x11000: [0x00004801, 0x0], // L1[0] -> L0 @0x12000 + 0x12000: [ + 0x00000000, 0x0, // L0[0] VA0x0 unmapped + 0x0000040f, 0x0, // L0[1] VA0x1000 -> phys0x1000 (V R W X) + 0x0000080f, 0x0, // L0[2] VA0x2000 -> phys0x2000 + 0x00000c0f, 0x0, // L0[3] VA0x3000 -> phys0x3000 + 0x0000100f, 0x0, // L0[4] VA0x4000 -> phys0x4000 + 0x0000140f, 0x0, // L0[5] VA0x5000 -> phys0x5000 + ], + }); + + final init = { + Register.x5: 0x200, // mtvec (M handler, direct) + Register.x6: 1 << 7, // MTIE + Register.x7: (1 << 11) | (1 << 7), // MPP=S | MPIE + Register.x8: 0x8000000000000010, // satp Sv39 root PPN 0x10 + Register.x9: 0x1000, // mepc = S entry VA + }; + + test( + 'control: M->S mret + Sv39 paged jump chain runs to park (no IRQ)', + timeout: Timeout(Duration(minutes: 5)), + () { + return coreTest( + prog(), + {}, + cfg(), + initRegisters: init, + nextPc: 0x5000, + maxCycles: 3000, + memLatency: 8, + ); + }, + ); + + // Run each IRQ timing in ISOLATION (-n) to avoid ROHD accumulation - a single + // sweep run gives false timeouts. Each is its own test. + for (final at in const [40, 48, 56, 64, 72, 80]) { + test( + 'timer IRQ during S-mode paged fetch vectors to handler (raiseAt=$at)', + timeout: Timeout(Duration(minutes: 5)), + () { + return coreTest( + prog(), + {Register.x28: 0xAB}, + cfg(), + initRegisters: init, + nextPc: 0x204, + maxCycles: 3000, + memLatency: 8, + raiseTimerIrqAt: at, + ); + }, + ); + } +} diff --git a/packages/river_hdl/test/interrupt/irq_during_paged_load_test.dart b/packages/river_hdl/test/interrupt/irq_during_paged_load_test.dart new file mode 100644 index 0000000..b14f536 --- /dev/null +++ b/packages/river_hdl/test/interrupt/irq_during_paged_load_test.dart @@ -0,0 +1,172 @@ +import 'package:rohd/rohd.dart'; +import 'package:river/river.dart'; +import 'package:test/test.dart'; + +import '../core_harness.dart'; + +/// Strongest remaining hypothesis for the delta intermittent wedge (#91): an +/// async timer interrupt taken while a LOAD or AMO is mid page-table WALK. Every +/// prior integrity repro ran in bare mode; irq_during_paged_fetch covers the +/// FETCH walk, but the DATA-side walk of a load/amo has never been interrupted +/// in sim. Linux takes the scheduler/RCU tick in exactly this state: satp on, +/// S-mode, a load of a live pointer in flight. If the interrupt-take does not +/// cleanly abort and re-issue the data walk, the load's destination register +/// gets a stale/garbage value, the corrupted-pointer root of the misaligned +/// amoor.d / duplicate-ticket HW wedge. +/// +/// M prologue -> mret to S-mode @VA0x1000 (paging on). S code loads from three +/// COLD pages (each a fresh TLB miss -> data walk) and does an amoadd, then jal +/// to park. A timer IRQ is fired across a cycle sweep so the take lands during a +/// data walk; the M handler sets x28 and MRETs back. Every loaded value MUST +/// survive regardless of IRQ timing. A divergence pinpoints the bug. +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + RiverCoreConfig cfg() => RiverCoreConfigV1.full( + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare, RiscVPagingMode.sv39], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + ), + clock: const HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + ); + + int jal(int rd, int off) => + (((off >> 20) & 1) << 31) | + (((off >> 1) & 0x3ff) << 21) | + (((off >> 11) & 1) << 20) | + (((off >> 12) & 0xff) << 12) | + (rd << 7) | + 0x6f; + int ld(int rd, int rs1) => (rs1 << 15) | (3 << 12) | (rd << 7) | 0x03; + // amoadd.d rd, rs2, (rs1) + int amoaddD(int rd, int rs2, int rs1) => + (rs2 << 20) | (rs1 << 15) | (3 << 12) | (rd << 7) | 0x2F; + + String mem(Map> words) { + final sb = StringBuffer(); + final addrs = words.keys.toList()..sort(); + for (final a in addrs) { + sb.writeln('@${a.toRadixString(16)}'); + for (final w in words[a]!) { + for (var i = 0; i < 4; i++) { + sb.write(((w >> (i * 8)) & 0xFF).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + } + sb.writeln(); + } + return sb.toString(); + } + + const park = 0x0000006f; + const mret = 0x30200073; + + // Data words as two 32-bit halves (little-endian dword). + List dword(int lo, int hi) => [lo, hi]; + + String prog() => mem({ + // ---- M-mode prologue @phys 0x0 (bare) ---- + 0x00000: [ + 0x30529073, // csrw mtvec, x5 (=0x200 handler PA) + 0x30431073, // csrw mie, x6 (MTIE = 1<<7) + 0x30039073, // csrw mstatus,x7 (MPP=S 1<<11 | MPIE 1<<7) + 0x18041073, // csrw satp, x8 (Sv39, root PPN 0x10) + 0x34149073, // csrw mepc, x9 (=0x1000 S entry VA) + mret, // -> S-mode @VA 0x1000, paging on + ], + // ---- M-mode timer handler @phys 0x200: mark x28, MRET back ---- + 0x00200: [0x0ab00e13, mret], + // ---- S-mode paged code @VA 0x1000 ---- + 0x01000: [ + ld(18, 20), // x18 = *(VA 0x2000) cold -> data walk + ld(19, 21), // x19 = *(VA 0x3000) cold -> data walk + amoaddD(22, 23, 24), // x22 = old *(VA 0x4000); *0x4000 += x23 + ld(25, 24), // x25 = *(VA 0x4000) post-amo + jal(0, 0x5000 - 0x1010), // -> park @VA 0x5000 + ], + 0x05000: [park], + // ---- data pages (identity mapped) ---- + 0x02000: dword(0x1111, 0), // *0x2000 = 0x1111 + 0x03000: dword(0x2222, 0), // *0x3000 = 0x2222 + 0x04000: dword(0x3333, 0), // *0x4000 = 0x3333 + // ---- Sv39 page table (root 0x10000), identity VA 0x1000..0x5000 ---- + 0x10000: [0x00004401, 0x0], // L2[0] -> L1 @0x11000 + 0x11000: [0x00004801, 0x0], // L1[0] -> L0 @0x12000 + 0x12000: [ + 0x00000000, 0x0, // L0[0] VA0x0 unmapped + 0x0000040f, 0x0, // L0[1] VA0x1000 -> phys0x1000 (V R W X) + 0x0000080f, 0x0, // L0[2] VA0x2000 -> phys0x2000 + 0x00000c0f, 0x0, // L0[3] VA0x3000 -> phys0x3000 + 0x0000100f, 0x0, // L0[4] VA0x4000 -> phys0x4000 + 0x0000140f, 0x0, // L0[5] VA0x5000 -> phys0x5000 + ], + }); + + final init = { + Register.x5: 0x200, // mtvec (M handler) + Register.x6: 1 << 7, // MTIE + Register.x7: (1 << 11) | (1 << 7), // MPP=S | MPIE + Register.x8: 0x8000000000000010, // satp Sv39 root PPN 0x10 + Register.x9: 0x1000, // mepc = S entry VA + Register.x20: 0x2000, // load ptr A + Register.x21: 0x3000, // load ptr B + Register.x23: 0x10, // amoadd addend + Register.x24: 0x4000, // amo/load ptr C + }; + + final expected = { + Register.x18: 0x1111, // *0x2000 + Register.x19: 0x2222, // *0x3000 + Register.x22: 0x3333, // old *0x4000 (amoadd returns old) + Register.x25: 0x3343, // *0x4000 post-amo (0x3333 + 0x10) + }; + + test( + 'control: paged loads+amo run to park (no IRQ)', + timeout: Timeout(Duration(minutes: 5)), + () { + return coreTest( + prog(), + expected, + cfg(), + initRegisters: init, + nextPc: 0x5000, + maxCycles: 4000, + memLatency: 8, + ); + }, + ); + + // Fire the IRQ across the window where the data walks are in flight. Each is + // its own isolated test (ROHD accumulation makes a single sweep give false + // timeouts). The M handler MRETs back, the interrupted load/amo re-issues its + // walk, and every loaded value MUST match [expected]. A mismatch = the take + // corrupted a data-walk load's rd (the wedge root). + for (final at in const [24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112]) { + test( + 'timer IRQ during S-mode paged data walk preserves loads (raiseAt=$at)', + timeout: Timeout(Duration(minutes: 5)), + () { + return coreTest( + prog(), + expected, + cfg(), + initRegisters: init, + nextPc: 0x5000, + maxCycles: 4000, + memLatency: 8, + raiseTimerIrqAt: at, + lowerTimerIrqAt: at + 4, + ); + }, + ); + } +} diff --git a/packages/river_hdl/test/interrupt/irq_during_refill_test.dart b/packages/river_hdl/test/interrupt/irq_during_refill_test.dart new file mode 100644 index 0000000..4101139 --- /dev/null +++ b/packages/river_hdl/test/interrupt/irq_during_refill_test.dart @@ -0,0 +1,127 @@ +import 'package:rohd/rohd.dart'; +import 'package:river/river.dart'; +import 'package:test/test.dart'; + +import 'dart:async'; + +import '../core_harness.dart'; + +/// Repro for the delta NixOS boot wedge (task #87 regression). The boot got +/// FARTHER before async interrupt-taking was added; it now bus-hangs at random +/// function entries once timer interrupts start firing (RCU init). Hypothesis: +/// an async interrupt taken at an instruction boundary redirects the fetch to +/// tvec WHILE an L1I cold-line REFILL is in flight; the refill response is never +/// consumed and the fetch handshake deadlocks. The #87 test only ran +/// readLatency:0 + a tight spin loop, so it never had a refill in flight. +/// +/// Built on the proven coreTest harness (handles latency + cold-line jumps). +/// Program: enable the machine timer interrupt, then jump through a chain of +/// COLD cache lines (constant refills). A timer IRQ is raised mid-chain (during +/// a refill). If the core vectors to the handler @0x200 it is fine; if it wedges +/// it never reaches nextPc. +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + RiverCoreConfig cfg() => RiverCoreConfigV1.full( + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + ), + clock: const HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + ); + + int jal(int rd, int off) => + (((off >> 20) & 1) << 31) | + (((off >> 1) & 0x3ff) << 21) | + (((off >> 11) & 1) << 20) | + (((off >> 12) & 0xff) << 12) | + (rd << 7) | + 0x6f; + + String mem(Map> words) { + final sb = StringBuffer(); + final addrs = words.keys.toList()..sort(); + for (final a in addrs) { + sb.writeln('@${a.toRadixString(16)}'); + for (final w in words[a]!) { + for (var i = 0; i < 4; i++) { + sb.write(((w >> (i * 8)) & 0xFF).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + } + sb.writeln(); + } + return sb.toString(); + } + + const nop = 0x00000013; + const park = 0x0000006f; + + // Enable-interrupts prologue @0x0, cold-line jump chain, handler @0x200. + String prog() => mem({ + 0x00000: [ + nop, nop, // 0x00,0x04 warmup + 0x30529073, // 0x08 csrw mtvec, x5 (=0x200) + 0x30431073, // 0x0c csrw mie, x6 (MTIE = 1<<7) + 0x3003a073, // 0x10 csrs mstatus, x7 (MIE = 1<<3) + jal(0, 0x4000 - 0x14), // 0x14 -> 0x4000 cold + ], + 0x00200: [0x0ab00e13, park], // handler: addi x28,x0,0xAB ; park @0x204 + 0x04000: [jal(0, 0x4000)], // 0x4000 -> 0x8000 cold + 0x08000: [jal(0, 0x4000)], // 0x8000 -> 0xC000 cold + 0x0C000: [jal(0, 0x4000)], // 0xC000 -> 0x10000 cold + 0x10000: [park], // park (reached only if NO interrupt) + }); + + final init = { + Register.x5: 0x200, // mtvec (direct) + Register.x6: 1 << 7, // MTIE + Register.x7: 1 << 3, // MIE + }; + + test( + 'control: cold-line chain runs to park at latency=8 (no IRQ)', + timeout: Timeout(Duration(minutes: 4)), + () { + return coreTest( + prog(), + {}, + cfg(), + initRegisters: init, + nextPc: 0x10000, + maxCycles: 2000, + memLatency: 8, + ); + }, + ); + + // Sweep WHEN the timer IRQ is raised so it lands during a cold-line refill. + for (final at in const [20, 26, 32, 38, 44, 50, 56, 62]) { + test( + 'timer IRQ during cold-line refill vectors to handler (raiseAt=$at)', + timeout: Timeout(Duration(minutes: 4)), + () { + // If the interrupt vectors cleanly, x28=0xAB and nextPc=0x204 (handler + // park). If the fetch handshake wedges, neither is reached. + return coreTest( + prog(), + {Register.x28: 0xAB}, + cfg(), + initRegisters: init, + nextPc: 0x204, + maxCycles: 2000, + memLatency: 8, + raiseTimerIrqAt: at, + ); + }, + ); + } +} diff --git a/packages/river_hdl/test/interrupt/irq_reg_integrity_test.dart b/packages/river_hdl/test/interrupt/irq_reg_integrity_test.dart new file mode 100644 index 0000000..bdee4a2 --- /dev/null +++ b/packages/river_hdl/test/interrupt/irq_reg_integrity_test.dart @@ -0,0 +1,126 @@ +import 'package:rohd/rohd.dart'; +import 'package:river/river.dart'; +import 'package:test/test.dart'; + +import '../core_harness.dart'; + +/// Comprehensive differential integrity check for the delta intermittent wedge: +/// an async interrupt taken at ANY cycle over a diverse instruction stream +/// (loads, AMOs, arithmetic) must be transparent to the architectural registers. +/// The handler touches only x28 and mrets, so a correct core leaves x18..x24 at +/// their computed values regardless of when the IRQ fires. If River corrupts a +/// load's or AMO's destination (or any reg) on the take/return, one value +/// diverges, the corrupted-pointer root of the misaligned-amoor.d / duplicate- +/// ticket HW wedge. +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + RiverCoreConfig cfg() => RiverCoreConfigV1.small( + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + ), + clock: const HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + ); + + const park = 0x0000006f; + const mret = 0x30200073; + int addi(int rd, int rs1, int imm) => + ((imm & 0xfff) << 20) | (rs1 << 15) | (rd << 7) | 0x13; + int ld(int rd, int rs1) => (rs1 << 15) | (3 << 12) | (rd << 7) | 0x03; + int sd(int rs2, int rs1) => (rs2 << 20) | (rs1 << 15) | (3 << 12) | 0x23; + // amoadd.d rd, rs2, (rs1): funct7=0 amoadd, funct3=3 (.d) + int amoaddD(int rd, int rs2, int rs1) => + (rs2 << 20) | (rs1 << 15) | (3 << 12) | (rd << 7) | 0x2F; + + String mem(Map> words) { + final sb = StringBuffer(); + final addrs = words.keys.toList()..sort(); + for (final a in addrs) { + sb.writeln('@${a.toRadixString(16)}'); + for (final w in words[a]!) { + for (var i = 0; i < 4; i++) { + sb.write(((w >> (i * 8)) & 0xFF).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + } + sb.writeln(); + } + return sb.toString(); + } + + const setup = [ + 0x30529073, + 0x30431073, + 0x3003a073, + ]; // mtvec,x5 ; mie,x6 ; mstatus,x7 + // x10 = data base 0x2000. Stream: load pointers/values, do an AMO, arithmetic. + // x18 = *0x2000 ; x19 = *0x2008 ; x20 = x18+x19 ; amoadd.d x21,x19,(x10) ; + // x22 = *0x2000 (post-amo) ; x23 = x20 ^ ... ; x24 = x18 + 0x10. + final body = [ + ...setup, + ld(18, 10), // x18 = mem[x10] + ld(19, 10), // x19 = mem[x10] (same, both known) + addi(20, 18, 0x10), // x20 = x18 + 0x10 + amoaddD(21, 19, 10), // x21 = old mem[x10]; mem[x10] += x19 + ld(22, 10), // x22 = mem[x10] (post-amo) + addi(23, 20, 0x20), // x23 = x20 + 0x20 + addi(24, 18, 0x30), // x24 = x18 + 0x30 + for (var i = 0; i < 16; i++) 0x00000013, // nop pad + park, + ]; + final parkPc = (body.length - 1) * 4; + final handler = [0x0ab00e13, mret]; // addi x28,x0,0xAB ; mret + String prog() => mem({0x0: body, 0x300: handler}); + + // mem[0x2000] = 0x100. x10 = 0x2000. After: x18=x19=x22-... x21=old=0x100, + // mem[0x2000]=0x100+0x100=0x200, x22=0x200. x20=0x110, x23=0x130, x24=0x130. + final init = { + Register.x5: 0x300, + Register.x6: 1 << 7, + Register.x7: 1 << 3, + Register.x10: 0x2000, + }; + String progWithData() => prog() + '@2000\n00 01 00 00 00 00 00 00\n'; + + final expected = { + Register.x18: 0x100, + Register.x19: 0x100, + Register.x20: 0x110, + Register.x21: 0x100, + Register.x22: 0x200, + Register.x23: 0x130, + Register.x24: 0x130, + }; + + // Fire the IRQ at every cycle over the whole stream+pad. Every cycle must + // leave all x18..x24 at [expected] (interrupt is transparent). A divergence + // pinpoints the cycle/instruction where the take corrupts a register. + for (var at = 8; at <= 48; at++) { + test( + 'IRQ at cycle $at preserves all registers (diverse stream)', + timeout: Timeout(Duration(minutes: 3)), + () { + return coreTest( + progWithData(), + expected, + cfg(), + initRegisters: init, + nextPc: parkPc, + maxCycles: 5000, + memLatency: 2, + raiseTimerIrqAt: at, + lowerTimerIrqAt: at + 3, + ); + }, + ); + } +} diff --git a/packages/river_hdl/test/interrupt/irq_seq_handler_coldfetch_test.dart b/packages/river_hdl/test/interrupt/irq_seq_handler_coldfetch_test.dart new file mode 100644 index 0000000..96b45bb --- /dev/null +++ b/packages/river_hdl/test/interrupt/irq_seq_handler_coldfetch_test.dart @@ -0,0 +1,179 @@ +import 'dart:async'; + +import 'package:rohd/rohd.dart'; +import 'package:river/river.dart'; +import 'package:test/test.dart'; + +import '../core_harness.dart'; + +/// Closest sim repro yet for the HW #87 fetch-bus-hang. The prior interrupt +/// repros vectored to a 2-instruction handler (x28<-0xAB ; park), so they never +/// ran a SEQUENTIAL cold-line fetch stream AFTER landing on the vector, nor an +/// MRET back into cold interrupted code. HW wedges exactly there: the sequential +/// fetch right after a control transfer to a function entry hangs. +/// +/// The rc1 L1 icache is tiny (iSize=64B, lineSize=8B => 8 one-word lines), so a +/// sequential nop run thrashes it: a miss + fill every 8 bytes. Program: +/// main: enable M-timer IRQ, then a long sequential nop run (IRQ fires here). +/// handler @0x400: a long sequential nop run across cold lines, x28<-0xAB, MRET. +/// after MRET: main resumes mid-run (mepc = interrupted PC), reaches park. +/// If any sequential fetch after the vector or after MRET wedges, park (nextPc) +/// is never reached and x28 stays unset. +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + // .small shares the EXACT rc1-f fetch/MMU/CSR/icache datapath (in-order, + // single-issue, microcoded, _rc1L1 icache) but drops the FPU, so the interrupt + // + fetch interaction is identical and the sim is far faster. + RiverCoreConfig cfg() => RiverCoreConfigV1.small( + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + ), + clock: const HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + ); + + const nop = 0x00000013; + const park = 0x0000006f; + const mret = 0x30200073; + const addiX28 = 0x0ab00e13; // addi x28, x0, 0xAB + + String mem(Map> words) { + final sb = StringBuffer(); + final addrs = words.keys.toList()..sort(); + for (final a in addrs) { + sb.writeln('@${a.toRadixString(16)}'); + for (final w in words[a]!) { + for (var i = 0; i < 4; i++) { + sb.write(((w >> (i * 8)) & 0xFF).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + } + sb.writeln(); + } + return sb.toString(); + } + + // main: 3 setup CSRs, then a long sequential nop run, then park. + const setup = [ + 0x30529073, // csrw mtvec, x5 (=0x400 handler) + 0x30431073, // csrw mie, x6 (MTIE = 1<<7) + 0x3003a073, // csrs mstatus,x7 (MIE = 1<<3) + ]; + const mainNops = 60; // 240 bytes -> thrashes the 64B icache ~4x + final mainBody = [ + ...setup, + for (var i = 0; i < mainNops; i++) nop, + park, + ]; + final parkPc = (mainBody.length - 1) * 4; // address of the final park + + // handler @0x400: long sequential nop run (cold lines), set x28, PARK. Parking + // (not MRET) means the timer never needs clearing and the interrupt cannot + // storm; this isolates the exact HW pattern: the sequential cold-line fetch + // stream right AFTER landing on the vector entry. + const handlerNops = 48; // 192 bytes -> thrashes the icache ~3x after vector + final handlerBody = [ + for (var i = 0; i < handlerNops; i++) nop, + addiX28, + park, + ]; + final handlerParkPc = 0x400 + handlerNops * 4 + 4; // addr of handler park + + // Second handler @0x800: same long cold-line run, but MRET back into the + // interrupted (now cold) main code instead of parking. Tests the full round + // trip: vector -> handler cold-fetch -> mret -> re-fetch cold main -> park. + final handlerMretBody = [ + for (var i = 0; i < handlerNops; i++) nop, + addiX28, + mret, + ]; + + String prog() => mem({0x0: mainBody, 0x400: handlerBody}); + String progMret() => mem({0x0: mainBody, 0x800: handlerMretBody}); + + final init = { + Register.x5: 0x400, // mtvec (direct) + Register.x6: 1 << 7, // MTIE + Register.x7: 1 << 3, // MIE + }; + + test( + 'control: sequential main+handler runs to park (no IRQ)', + timeout: Timeout(Duration(minutes: 5)), + () { + return coreTest( + prog(), + {}, + cfg(), + initRegisters: init, + nextPc: parkPc, + maxCycles: 6000, + memLatency: 8, + ); + }, + ); + + // Sweep WHEN the timer IRQ is raised so it lands at different points of the + // main sequential run (different cache/fetch states). Run each ISOLATED. + for (final at in const [30, 40, 50, 60, 70, 90, 120]) { + test( + 'IRQ mid sequential run -> handler cold-fetch stream -> handler park ' + '(raiseAt=$at)', + timeout: Timeout(Duration(minutes: 5)), + () { + // Reaching handlerParkPc AND x28=0xAB proves: the interrupt vectored and + // the handler's SEQUENTIAL cold-line fetch stream after the vector ran to + // completion (the exact fetch pattern the HW wedges on). Hold MTIP; the + // handler parks (no mret) so it cannot re-fire. + return coreTest( + prog(), + {Register.x28: 0xAB}, + cfg(), + initRegisters: init, + nextPc: handlerParkPc, + maxCycles: 6000, + memLatency: 8, + raiseTimerIrqAt: at, + ); + }, + ); + } + + // MRET handler @0x800: the FIRST take enters the handler (MIE cleared on entry) + // and runs its sequential cold-line stream to the addi at 0x8c0 BEFORE any + // mret/storm. Holding MTIP and targeting 0x8c0 is a deterministic assertion + // that the interrupt vectors to 0x800 and the post-vector cold fetch completes. + // (The mret return itself is exercised by the storm behaviour: mret returns to + // mepc then re-vectors, proving the return path fetches correctly.) + final initMret = {...init, Register.x5: 0x800}; + const handlerMretPc = + 0x800 + handlerNops * 4 + 4; // mret @0x8c4 (addi retired) + for (final at in const [40, 60, 90]) { + test( + 'IRQ vectors to 0x800 handler, cold-fetch stream reaches mret ' + '(raiseAt=$at)', + timeout: Timeout(Duration(minutes: 5)), + () { + return coreTest( + progMret(), + {Register.x28: 0xAB}, + cfg(), + initRegisters: initMret, + nextPc: handlerMretPc, + maxCycles: 8000, + memLatency: 8, + raiseTimerIrqAt: at, + ); + }, + ); + } +} diff --git a/packages/river_hdl/test/interrupt/irq_stale_writeport_test.dart b/packages/river_hdl/test/interrupt/irq_stale_writeport_test.dart new file mode 100644 index 0000000..3172618 --- /dev/null +++ b/packages/river_hdl/test/interrupt/irq_stale_writeport_test.dart @@ -0,0 +1,133 @@ +import 'package:rohd/rohd.dart'; +import 'package:river/river.dart'; +import 'package:test/test.dart'; + +import '../core_harness.dart'; + +/// Repro for the delta intermittent wedge (unified root suspect): the async +/// interrupt-take path (exec.dart rawTrap) sets nextMode/trap/nextPc/epc but does +/// NOT clear rdWrite.en / memWrite.en. If those are still asserted from the +/// just-retired instruction while the datapath now computes the trap, a spurious +/// register (or memory) write with stale/garbage data fires on the take cycle, +/// corrupting the register the previous instruction wrote. That is exactly the +/// corrupted-pointer symptom on HW (misaligned amoor.d address, garbage lock +/// pointer), frequent but only fatal when it clobbers a live pointer. +/// +/// This writes known values into several registers, fires a timer IRQ across a +/// fine cycle sweep so the take lands right after a reg-writing instruction, and +/// checks the values SURVIVE. A stale write-port clobbers one -> mismatch. +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + RiverCoreConfig cfg() => RiverCoreConfigV1.small( + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + ), + clock: const HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + ); + + const park = 0x0000006f; + const mret = 0x30200073; + + String mem(Map> words) { + final sb = StringBuffer(); + final addrs = words.keys.toList()..sort(); + for (final a in addrs) { + sb.writeln('@${a.toRadixString(16)}'); + for (final w in words[a]!) { + for (var i = 0; i < 4; i++) { + sb.write(((w >> (i * 8)) & 0xFF).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + } + sb.writeln(); + } + return sb.toString(); + } + + // addi rd, x0, imm (imm small). Encoding: (imm<<20)|(0<<15)|(0<<12)|(rd<<7)|0x13. + int addi(int rd, int imm) => (imm << 20) | (rd << 7) | 0x13; + + // Setup: mtvec + mie + mstatus.MIE. Then a stream of reg-writes (addi) that + // set x18..x25 to distinctive values, then a nop pad, then park. A timer IRQ + // raised mid-stream should vector to the handler and mret back WITHOUT + // clobbering any x18..x25. The handler preserves them (it only touches x28). + const setup = [ + 0x30529073, // csrw mtvec, x5 (=0x300) + 0x30431073, // csrw mie, x6 (MTIE) + 0x3003a073, // csrs mstatus,x7 (MIE) + ]; + // distinctive values (small imms so addi is single-instruction) + final vals = { + 18: 0x111, + 19: 0x222, + 20: 0x333, + 21: 0x444, + 22: 0x555, + 23: 0x666, + 24: 0x777, + 25: 0x788, + }; + final writes = [for (final e in vals.entries) addi(e.key, e.value)]; + final mainBody = [ + ...setup, + ...writes, + for (var i = 0; i < 20; i++) 0x00000013, // nop pad (IRQ can land here too) + park, + ]; + final parkPc = (mainBody.length - 1) * 4; + + // handler @0x300: set x28=0xAB (marker), then MRET. It must NOT disturb + // x18..x25. Clear the timer first (lowerTimerIrqAt) so mret does not storm. + final handlerBody = [0x0ab00e13, mret]; // addi x28,x0,0xAB ; mret + String prog() => mem({0x0: mainBody, 0x300: handlerBody}); + + final init = { + Register.x5: 0x300, // mtvec + Register.x6: 1 << 7, // MTIE + Register.x7: 1 << 3, // MIE + }; + + // Fine sweep of the IRQ raise cycle across the reg-write stream + pad, so the + // take lands right after each reg-writing instruction. For EVERY cycle the + // x18..x25 values must survive (the handler mrets back and the stream/pad runs + // to park). A stale write-port on the take cycle corrupts one -> the reg check + // fails. lowerTimerIrqAt clears the IRQ mid-handler so a single take occurs. + for (var at = 10; at <= 40; at++) { + test( + 'IRQ at cycle $at does not clobber a written register (stale write-port)', + timeout: Timeout(Duration(minutes: 3)), + () { + return coreTest( + prog(), + { + Register.x18: 0x111, + Register.x19: 0x222, + Register.x20: 0x333, + Register.x21: 0x444, + Register.x22: 0x555, + Register.x23: 0x666, + Register.x24: 0x777, + Register.x25: 0x788, + }, + cfg(), + initRegisters: init, + nextPc: parkPc, + maxCycles: 4000, + memLatency: 2, + raiseTimerIrqAt: at, + lowerTimerIrqAt: at + 3, + ); + }, + ); + } +} diff --git a/packages/river_hdl/test/matrix_instructions.dart b/packages/river_hdl/test/matrix_instructions.dart index f7c9922..ba55d6a 100644 --- a/packages/river_hdl/test/matrix_instructions.dart +++ b/packages/river_hdl/test/matrix_instructions.dart @@ -667,6 +667,26 @@ List atomics(RiscVMxlen mxlen) => [ operand: 5, ), // signed/unsigned-differing operands _lrsc('lr/sc.w', 0x2), + // Ordered lr.w.aq / sc.w.rl: the aq/rl bits (funct7[1:0]) are ordering hints, + // decode-transparent, and run identically on the in-order core. Guards the + // HW-found delta bug where sc.w.rl (funct7=0x0D) raised illegal because the + // decoder matched the full funct7 instead of funct5. Same result as lr/sc.w. + MatrixCell( + 'lr.w.aq/sc.w.rl', + [ + iimm(0x100, 0, 0x0, 10), // x10 = addr + iimm(42, 0, 0x0, 11), // x11 = store value + amo(0x02, 0, 10, 0x2, 12) | (1 << 26), // lr.w.aq x12 = mem, reserve + amo(0x03, 11, 10, 0x2, 13) | (1 << 25), // sc.w.rl x13 = 0 (ok), mem = 42 + nop, + ], + dataMem: { + 0x100: [77], + }, + checkRegs: [Register.x12, Register.x13], + checkMem: [0x100], + nextPc: 0x14, + ), // sc-fail edge: a 2nd sc.w must FAIL (x14=1) since the 1st cleared the // reservation (folded from core_parity_test's LR/SC subtest). MatrixCell( diff --git a/packages/river_hdl/test/mmu/core_mmu_test.dart b/packages/river_hdl/test/mmu/core_mmu_test.dart index 29869f2..a29a37f 100644 --- a/packages/river_hdl/test/mmu/core_mmu_test.dart +++ b/packages/river_hdl/test/mmu/core_mmu_test.dart @@ -40,7 +40,11 @@ void main() { // PTEs: non-leaf = (nextPPN<<10)|V; leaf = (physPPN<<10)|V|R|W|X. // l2[0] @ 0x10000 = (0x11<<10)|1 = 0x4401 // l1[0] @ 0x11000 = (0x12<<10)|1 = 0x4801 + // l0[0] @ 0x12000 = (0x00<<10)|0xF = 0x000F (identity map of page 0) // l0[0x20]@ 0x12100 = (0x30<<10)|0xF = 0xC00F + // The paged tests run in S-mode, so the FETCH also translates: l0[0] identity + // maps the code page (0x0) so the instructions fetch cleanly while the data + // access at 0x20000 translates to the different page 0x30000. // Diagnostic: bare (paging off) 64-bit load through the modified MMU, no walk. test( 'bare ld (no paging) loads 0x30000', @@ -64,8 +68,8 @@ void main() { 'Sv39 dport load translates 0x20000 -> 0x30000', timeout: Timeout(Duration(seconds: 60)), () => coreTest( - // satp (MODE=8 Sv39, root PPN 0x10) is preloaded into a0; enable paging, - // then load from virtual 0x20000 (mapped to physical 0x30000). + // Translation applies only in S/U mode (River has no mstatus.MPRV, so an + // M-mode data access is always physical). Run this in S-mode via startPriv. // csrw satp, a0 (0x18051073) // lui a2, 0x20 (0x00020637) -> a2 = 0x20000 (virtual) // ld a1, 0(a2) (0x00063583) @@ -76,6 +80,8 @@ void main() { 01 44 00 00 00 00 00 00 @11000 01 48 00 00 00 00 00 00 +@12000 +0F 00 00 00 00 00 00 00 @12100 0F C0 00 00 00 00 00 00 @30000 @@ -83,6 +89,7 @@ void main() { ''', {Register.x11: 0xCAFEF00D}, config, + startPriv: PrivilegeMode.supervisor, initRegisters: { // satp: MODE=8 (Sv39) bits 63:60, root PPN = 0x10000>>12 = 0x10. Register.x10: 0x8000000000000010, @@ -110,6 +117,8 @@ void main() { 01 44 00 00 00 00 00 00 @11000 01 48 00 00 00 00 00 00 +@12000 +0F 00 00 00 00 00 00 00 @12100 0F C0 00 00 00 00 00 00 @30000 @@ -117,6 +126,7 @@ void main() { ''', const {}, config, + startPriv: PrivilegeMode.supervisor, initRegisters: {Register.x10: 0x8000000000000010}, // The translated physical address 0x30000 holds the stored value. memStates: {0x30000: 0x234}, @@ -168,6 +178,8 @@ void main() { 01 48 00 00 00 00 00 00 @12000 01 4C 00 00 00 00 00 00 +@13000 +0F 00 00 00 00 00 00 00 @13100 0F C0 00 00 00 00 00 00 @30000 @@ -175,6 +187,7 @@ void main() { ''', {Register.x11: 0xCAFEF00D}, sv48Config, + startPriv: PrivilegeMode.supervisor, initRegisters: { // satp: MODE=9 (Sv48) bits 63:60, root PPN 0x10. Register.x10: 0x9000000000000010, diff --git a/packages/river_hdl/test/mmu/fetch_fault_noicache_test.dart b/packages/river_hdl/test/mmu/fetch_fault_noicache_test.dart new file mode 100644 index 0000000..9eddcce --- /dev/null +++ b/packages/river_hdl/test/mmu/fetch_fault_noicache_test.dart @@ -0,0 +1,160 @@ +import 'dart:async'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd_hcl/rohd_hcl.dart' hide DataPortInterface, DataPortGroup; +import 'package:river/river.dart'; +import 'package:river_hdl/river_hdl.dart'; +import 'package:test/test.dart'; + +/// Isolation test: fetch to an unmapped page with NO icache (memFetchRead talks +/// straight to the MMU). Confirms the MMU raises the fetch fault and the +/// compressed fetch buffer delivers it as instructionPageFault (cause 12). If +/// this passes but the icache variant does not, the icache loses the fault. +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + test('unmapped fetch (no icache) raises instructionPageFault (12)', () async { + final config = RiverCoreConfig( + mxlen: RiscVMxlen.rv64, + extensions: kRva22S64Extensions, + type: RiverCoreType.general, + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare, RiscVPagingMode.sv39], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + hasSupervisorUserMemory: true, + hasMakeExecutableReadable: true, + ), + interrupts: [], + clock: const HarborClockConfig( + name: 'test', + rate: HarborFixedClockRate(10000), + ), + ); + + // csrw satp,a0 ; auipc t0,0x8 ; jalr x0,0(t0) ; nop then page tables. + const memString = '''@0 +73 10 05 18 97 82 00 00 67 80 02 00 13 00 00 00 +@10000 +01 44 00 00 00 00 00 00 +@11000 +01 48 00 00 00 00 00 00 +@12000 +0F 00 00 00 00 00 00 00 +'''; + + final clk = SimpleClockGenerator(20).clk; + final reset = Logic(); + final addrWidth = config.mxlen.size; + final wbConfig = WishboneConfig( + addressWidth: addrWidth, + dataWidth: config.mxlen.size, + selWidth: config.mxlen.size ~/ 8, + ); + + final core = RiverCore( + config, + busConfig: wbConfig, + resetPrivilege: PrivilegeMode.supervisor.id, + ); + core.input('clk').srcConnection! <= clk; + core.input('reset').srcConnection! <= reset; + await core.build(); + + final storage = SparseMemoryStorage( + addrWidth: addrWidth, + dataWidth: config.mxlen.size, + alignAddress: (addr) => addr, + onInvalidRead: (addr, dataWidth) => + LogicValue.filled(dataWidth, LogicValue.zero), + ); + + final memRead = DataPortInterface(config.mxlen.size, addrWidth); + final memWrite = DataPortInterface(config.mxlen.size, addrWidth); + // ignore: unused_local_variable + final mem = MemoryModel( + clk, + reset, + [wrapWriteForRegisterFile(memWrite)], + [wrapReadForRegisterFile(memRead, clk: clk, readLatency: 0)], + readLatency: 0, + storage: storage, + ); + + final wbCyc = core.output('dataBus_CYC'); + final wbStb = core.output('dataBus_STB'); + final wbWe = core.output('dataBus_WE'); + final wbAdr = core.output('dataBus_ADR'); + final wbDatMosi = core.output('dataBus_DAT_MOSI'); + + memRead.en <= wbCyc & wbStb & ~wbWe; + memRead.addr <= wbAdr; + memWrite.en <= wbCyc & wbStb & wbWe; + memWrite.addr <= wbAdr; + memWrite.data <= wbDatMosi; + + final wbAckReg = Logic(name: 'wbAck'); + final readyForAck = wbWe | memRead.valid; + Sequential(clk, [ + If( + reset, + then: [wbAckReg < 0], + orElse: [ + If( + wbCyc & wbStb & ~wbAckReg & readyForAck, + then: [wbAckReg < 1], + orElse: [wbAckReg < 0], + ), + ], + ), + ]); + core.input('dataBus_ACK').srcConnection! <= wbAckReg; + core.input('dataBus_DAT_MISO').srcConnection! <= memRead.data; + + reset.inject(1); + Simulator.registerAction(20, () { + reset.put(0); + core.regWritePort.en.inject(1); + core.regWritePort.addr.inject(LogicValue.ofInt(10, 5)); + core.regWritePort.data.inject(LogicValue.ofInt(0x8000000000000010, 64)); + storage.loadMemString(memString); + }); + Simulator.setMaxSimTime(200000); + unawaited(Simulator.run()); + + await clk.nextPosedge; + core.regWritePort.en.inject(0); + while (reset.value.toBool()) { + await clk.nextPosedge; + } + + var sawFetchFault = false; + for (var i = 0; i < 2000; i++) { + await clk.nextPosedge; + final trap = core.pipeline.trap.value; + if (trap.isValid && trap.toInt() == 1) { + final cause = core.pipeline.trapCause.value; + final epc = core.pipeline.trapEpc.value; + // ignore: avoid_print + print( + 'TRAP cause=${cause.toInt()} ' + 'epc=0x${epc.isValid ? epc.toInt().toRadixString(16) : "x"}', + ); + expect( + cause.toInt(), + Trap.instructionPageFault.causeCode, + reason: 'expected instructionPageFault (12), got ${cause.toInt()}', + ); + sawFetchFault = true; + break; + } + } + + await Simulator.endSimulation(); + await Simulator.simulationEnded; + expect(sawFetchFault, isTrue, reason: 'no fetch fault raised'); + }); +} diff --git a/packages/river_hdl/test/mmu/high_megapage_fetch_test.dart b/packages/river_hdl/test/mmu/high_megapage_fetch_test.dart new file mode 100644 index 0000000..571be16 --- /dev/null +++ b/packages/river_hdl/test/mmu/high_megapage_fetch_test.dart @@ -0,0 +1,89 @@ +import 'package:river/river.dart'; +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import '../core_harness.dart'; + +/// HW-observed on delta: after the trampoline fault-jumps into HIGH virtual, +/// River raises instructionPageFault on a VALID high-virtual (0xffffffff8...) +/// instruction fetch through a 2MB L1-leaf megapage (Linux relocate_enable_mmu +/// label 1). The mapping is valid+X+A+D and the DRAM holds the right bytes, yet +/// the fetch faults. This isolates that: enable Sv39, jump to a high-virtual +/// address mapped by a 2MB L1 leaf, and require the target executes (a correct +/// core reaches the park; buggy River never does because it faults). +/// +/// phys 0x00 (identity via a 1GB root leaf, runs before/after satp): +/// 0x00 csrw satp, a0 enable Sv39 +/// 0x04 lui t0, 0xc0000 t0 = 0xffffffffc0000000 (sign-extended) +/// 0x08 jalr x0, 0(t0) jump to HIGH virtual 0xffffffffc0000000 +/// phys 0x200000 (= virt 0xffffffffc0000000 via a 2MB L1 leaf): +/// addi x6, x0, 0x11 +/// jal x0, 0 park (= virt 0xffffffffc0000004) +/// +/// Page tables (Sv39, root @ 0x10000): +/// root[0] = 0x000000EF 1GB leaf, virt 0x0..0x3fffffff -> phys identity +/// root[511] = 0x00004401 non-leaf -> L1_high @ 0x11000 +/// L1_high[0]= 0x000800EF 2MB leaf, virt 0xffffffffc0000000 -> phys 0x200000 +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + RiverCoreConfig full() => RiverCoreConfigV1.full( + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare, RiscVPagingMode.sv39], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + hasSupervisorUserMemory: true, + hasMakeExecutableReadable: true, + ), + clock: const HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + ); + + String mem(Map> words) { + final sb = StringBuffer(); + final addrs = words.keys.toList()..sort(); + for (final a in addrs) { + sb.writeln('@${a.toRadixString(16)}'); + for (final w in words[a]!) { + for (var i = 0; i < 4; i++) { + sb.write(((w >> (i * 8)) & 0xFF).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + } + sb.writeln(); + } + return sb.toString(); + } + + // Direct jalr to the EXACT HW address 0xffffffff80001048 (VPN2=510, offset + // 0x1048 into a 2MB L1 leaf), NO preceding fault. Isolates address vs the + // trampoline fault-then-trap sequence. + // 0x00 csrw satp,a0 ; 0x04 lui t0,0x80001 ; 0x08 addi t0,t0,0x48 ; + // 0x0c jalr x0,0(t0) -> 0xffffffff80001048 + String prog() => mem({ + 0x00: [0x18051073, 0x800012b7, 0x04828293, 0x00028067], + 0x10000: [0x000000EF, 0x0], // root[0] 1GB leaf identity low + 0x10ff0: [0x00004401, 0x0], // root[510] -> L1_high + 0x11000: [0x000800EF, 0x0], // L1_high[0] 2MB leaf -> phys 0x200000 + 0x201048: [0x01100313, 0x0000006f], // addi x6,0x11 ; park (offset 0x1048) + }); + + test( + 'high-virtual 2MB-megapage instruction fetch executes (no spurious fault)', + timeout: Timeout(Duration(minutes: 6)), + () => coreTest( + prog(), + {Register.x6: 0x11}, + full(), + startPriv: PrivilegeMode.supervisor, + initRegisters: {Register.x10: 0x8000000000000010}, + nextPc: 0xffffffff8000104c, + ), + ); +} diff --git a/packages/river_hdl/test/mmu/icache_fetch_fault_test.dart b/packages/river_hdl/test/mmu/icache_fetch_fault_test.dart new file mode 100644 index 0000000..6eb1a78 --- /dev/null +++ b/packages/river_hdl/test/mmu/icache_fetch_fault_test.dart @@ -0,0 +1,177 @@ +import 'dart:async'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd_hcl/rohd_hcl.dart' hide DataPortInterface, DataPortGroup; +import 'package:river/river.dart'; +import 'package:river_hdl/river_hdl.dart'; +import 'package:test/test.dart'; + +/// A fetch whose translation faults must be delivered as an instruction page +/// fault, even with the VIVT icache and the microcode decoder in front. The +/// icache refills through the MMU fetch port; when that walk faults (done, not +/// valid) the fill FSM used to stall forever, and even once the fault reached the +/// FetchUnit the garbage bits did not decode (the microcode decoder never +/// validated them), so exec never ran and the held fetch fault was never taken. +/// With the fault propagated (mem_fault -> resp_fault -> FetchUnit) and a NOP +/// delivered in place of the faulting bits, the pipeline raises +/// instructionPageFault (cause 12) instead. +/// +/// 0x00 csrw satp, a0 enable Sv39 (a0 seeded to the root PPN) +/// 0x04 auipc t0, 0x8 t0 = 0x8004 +/// 0x08 jalr x0, 0(t0) jump to VIRTUAL 0x8004 (VPN0 = 8, L0[8] absent) +/// 0x0c nop +/// +/// Page tables (Sv39, identity for the low code page, nothing at VPN0 = 8): +/// L2[0]@0x10000 = 0x4401 ; L1[0]@0x11000 = 0x4801 ; L0[0]@0x12000 = 0x000F +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + RiverCoreConfig full() => RiverCoreConfigV1.full( + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare, RiscVPagingMode.sv39], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + hasSupervisorUserMemory: true, + hasMakeExecutableReadable: true, + ), + clock: const HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + ); + + test( + 'icache fetch to an unmapped page raises instructionPageFault (not a hang)', + timeout: Timeout(Duration(minutes: 6)), + () async { + final config = full(); + + // csrw satp,a0 ; auipc t0,0x8 ; jalr x0,0(t0) ; nop then page tables. + const memString = '''@0 +73 10 05 18 97 82 00 00 67 80 02 00 13 00 00 00 +@10000 +01 44 00 00 00 00 00 00 +@11000 +01 48 00 00 00 00 00 00 +@12000 +0F 00 00 00 00 00 00 00 +'''; + + final clk = SimpleClockGenerator(20).clk; + final reset = Logic(); + final addrWidth = config.mxlen.size; + final wbConfig = WishboneConfig( + addressWidth: addrWidth, + dataWidth: config.mxlen.size, + selWidth: config.mxlen.size ~/ 8, + ); + + final core = RiverCore( + config, + busConfig: wbConfig, + resetPrivilege: PrivilegeMode.supervisor.id, + ); + core.input('clk').srcConnection! <= clk; + core.input('reset').srcConnection! <= reset; + await core.build(); + + final storage = SparseMemoryStorage( + addrWidth: addrWidth, + dataWidth: config.mxlen.size, + alignAddress: (addr) => addr, + onInvalidRead: (addr, dataWidth) => + LogicValue.filled(dataWidth, LogicValue.zero), + ); + + final memRead = DataPortInterface(config.mxlen.size, addrWidth); + final memWrite = DataPortInterface(config.mxlen.size, addrWidth); + // ignore: unused_local_variable + final mem = MemoryModel( + clk, + reset, + [wrapWriteForRegisterFile(memWrite)], + [wrapReadForRegisterFile(memRead, clk: clk, readLatency: 0)], + readLatency: 0, + storage: storage, + ); + + final wbCyc = core.output('dataBus_CYC'); + final wbStb = core.output('dataBus_STB'); + final wbWe = core.output('dataBus_WE'); + final wbAdr = core.output('dataBus_ADR'); + final wbDatMosi = core.output('dataBus_DAT_MOSI'); + + memRead.en <= wbCyc & wbStb & ~wbWe; + memRead.addr <= wbAdr; + memWrite.en <= wbCyc & wbStb & wbWe; + memWrite.addr <= wbAdr; + memWrite.data <= wbDatMosi; + + final wbAckReg = Logic(name: 'wbAck'); + final readyForAck = wbWe | memRead.valid; + Sequential(clk, [ + If( + reset, + then: [wbAckReg < 0], + orElse: [ + If( + wbCyc & wbStb & ~wbAckReg & readyForAck, + then: [wbAckReg < 1], + orElse: [wbAckReg < 0], + ), + ], + ), + ]); + core.input('dataBus_ACK').srcConnection! <= wbAckReg; + core.input('dataBus_DAT_MISO').srcConnection! <= memRead.data; + + reset.inject(1); + Simulator.registerAction(20, () { + reset.put(0); + core.regWritePort.en.inject(1); + core.regWritePort.addr.inject(LogicValue.ofInt(10, 5)); + // satp: Sv39 (MODE 8) | root PPN 0x10. + core.regWritePort.data.inject(LogicValue.ofInt(0x8000000000000010, 64)); + storage.loadMemString(memString); + }); + Simulator.setMaxSimTime(400000); + unawaited(Simulator.run()); + + await clk.nextPosedge; + core.regWritePort.en.inject(0); + while (reset.value.toBool()) { + await clk.nextPosedge; + } + + var sawFetchFault = false; + for (var i = 0; i < 2000; i++) { + await clk.nextPosedge; + final trap = core.pipeline.trap.value; + if (trap.isValid && trap.toInt() == 1) { + final cause = core.pipeline.trapCause.value; + expect(cause.isValid, isTrue, reason: 'trapCause invalid'); + expect( + cause.toInt(), + Trap.instructionPageFault.causeCode, + reason: 'expected instructionPageFault (12), got ${cause.toInt()}', + ); + sawFetchFault = true; + break; + } + } + + await Simulator.endSimulation(); + await Simulator.simulationEnded; + + expect( + sawFetchFault, + isTrue, + reason: 'icache did not deliver the fetch fault (hung on the refill)', + ); + }, + ); +} diff --git a/packages/river_hdl/test/mmu/mmu_fault_test.dart b/packages/river_hdl/test/mmu/mmu_fault_test.dart index 2ad0583..6e2f67a 100644 --- a/packages/river_hdl/test/mmu/mmu_fault_test.dart +++ b/packages/river_hdl/test/mmu/mmu_fault_test.dart @@ -44,6 +44,8 @@ void main() { 01 44 00 00 00 00 00 00 @11000 01 48 00 00 00 00 00 00 +@12000 +0F 00 00 00 00 00 00 00 @12100 0B C0 00 00 00 00 00 00 '''; @@ -57,7 +59,13 @@ void main() { selWidth: config.mxlen.size ~/ 8, ); - final core = RiverCore(config, busConfig: wbConfig); + // Translation applies only in S/U mode (no MPRV in River), so the store + // page-fault must be observed from S-mode, not the M-mode reset default. + final core = RiverCore( + config, + busConfig: wbConfig, + resetPrivilege: PrivilegeMode.supervisor.id, + ); core.input('clk').srcConnection! <= clk; core.input('reset').srcConnection! <= reset; await core.build(); diff --git a/packages/river_hdl/test/mmu/mmu_superpage_test.dart b/packages/river_hdl/test/mmu/mmu_superpage_test.dart new file mode 100644 index 0000000..15c16bf --- /dev/null +++ b/packages/river_hdl/test/mmu/mmu_superpage_test.dart @@ -0,0 +1,164 @@ +import 'dart:async'; + +import 'package:rohd/rohd.dart'; +import 'package:river/river.dart'; +import 'package:river_hdl/river_hdl.dart'; +import 'package:test/test.dart'; + +/// Superpage (megapage) translation regression. +/// +/// The MMU must compose the physical address from the leaf LEVEL, not always +/// as a 4KB page. A level-1 leaf is a 2MB superpage, so the low bits +/// `vaddr[20:12]` come from the virtual address, not the PTE. The original +/// `leafPa` always took `vaddr[11:0]`, so every 4KB sub-page of a superpage +/// aliased to the superpage base. Linux `swapper_pg_dir` maps the kernel text +/// as a 2MB superpage, so the first instruction fetch under swapper landed +/// 0x1000 bytes low and read a wrong (illegal-decoding) word on delta. +/// +/// vaddr 0x201000 is 4KB into the 2MB superpage at guest 0x200000. With the +/// level-1 leaf PPN 0x40000, the correct physical is 0x40001000 (keeps the +/// vaddr[20:12] = 1 offset); the 4KB-only bug computes 0x40000000. +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + Future walkAndRead(int vaddr) async { + final clk = SimpleClockGenerator(20).clk; + final reset = Logic(name: 'reset'); + final dportEn = Logic(name: 'dportEn'); + final dportAddr = Logic(name: 'dportAddr', width: 64); + final satpMode = Logic(name: 'satpMode', width: 4); + final satpRoot = Logic(name: 'satpRoot', width: 64); + + final wbConfig = WishboneConfig( + addressWidth: 64, + dataWidth: 64, + selWidth: 8, + ); + final mmuConfig = HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare, RiscVPagingMode.sv39], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + hasSupervisorUserMemory: true, + hasMakeExecutableReadable: true, + ); + + final ackSrc = Logic(name: 'ackSrc'); + final misoSrc = Logic(name: 'misoSrc', width: 64); + + final mmu = RiverMmu( + clk, + reset, + Const(0), // ifetchEn + Const(0, width: 64), // ifetchAddr + dportEn, + dportAddr, + Const(0), // dportWe (read) + Const(0, width: 64), // dportWdata + Const(3, width: 3), // dportSize = 8 bytes + ackSrc, + misoSrc, + mmuConfig: mmuConfig, + busConfig: wbConfig, + satpMode: satpMode, + satpRoot: satpRoot, + ); + + await mmu.build(); + + // Mock combinational memory: + // root[0] @ 0x10000 -> non-leaf PTE, next table 0x11000 + // L1[1] @ 0x11008 -> LEAF (2MB superpage), PPN 0x40000, V|R|W|X + // 0x40001000 -> correct data (superpage sub-page offset kept) + // 0x40000000 -> wrong data (what the 4KB-only bug reads) + Logic memData(Logic a) => mux( + a.eq(0x10000), + Const(0x4401, width: 64), + mux( + a.eq(0x11008), + Const(0x1000000F, width: 64), + mux( + a.eq(0x40001000), + Const(0xCAFEF00D, width: 64), + mux( + a.eq(0x40000000), + Const(0xDEADBEEF, width: 64), + Const(0, width: 64), + ), + ), + ), + ); + misoSrc <= memData(mmu.wbAdr); + + final ackReg = Logic(name: 'ackReg'); + Sequential(clk, [ + If( + reset, + then: [ackReg < 0], + orElse: [ + If( + mmu.wbCyc & mmu.wbStb & ~ackReg, + then: [ackReg < 1], + orElse: [ackReg < 0], + ), + ], + ), + ]); + ackSrc <= ackReg; + + reset.inject(1); + dportEn.inject(0); + dportAddr.inject(0); + satpMode.inject(8); // Sv39 + satpRoot.inject(0x10); // root PPN -> 0x10000 + + Simulator.setMaxSimTime(10000); + unawaited(Simulator.run()); + + await clk.nextPosedge; + reset.inject(0); + await clk.nextPosedge; + + dportEn.inject(1); + dportAddr.inject(vaddr); + + var done = false; + var rdata = 0; + for (var i = 0; i < 40; i++) { + await clk.nextPosedge; + if (mmu.dportDone.value.toInt() == 1) { + done = true; + rdata = mmu.dportRdata.value.toInt(); + break; + } + } + + await Simulator.endSimulation(); + await Simulator.simulationEnded; + + expect(done, isTrue, reason: 'dportDone never asserted'); + return rdata; + } + + test( + 'Sv39 level-1 superpage keeps vaddr[20:12] in the physical address', + () async { + // 4KB into the 2MB superpage -> must read 0x40001000, not the base. + final rdata = await walkAndRead(0x201000); + expect( + rdata, + 0xCAFEF00D, + reason: 'superpage sub-page offset dropped (read the base instead)', + ); + }, + ); + + test('Sv39 level-1 superpage base sub-page still translates', () async { + // Offset 0 within the superpage -> base physical 0x40000000. + await Simulator.reset(); + final rdata = await walkAndRead(0x200000); + expect(rdata, 0xDEADBEEF); + }); +} diff --git a/packages/river_hdl/test/mmu/nonident_fetch_test.dart b/packages/river_hdl/test/mmu/nonident_fetch_test.dart new file mode 100644 index 0000000..cfbc41b --- /dev/null +++ b/packages/river_hdl/test/mmu/nonident_fetch_test.dart @@ -0,0 +1,82 @@ +import 'package:river/river.dart'; +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import '../core_harness.dart'; + +/// The bug the identity-mapped repros never caught: instruction fetch through a +/// NON-IDENTITY page mapping. With `translateFetch` gated off behind the icache, +/// the VIVT icache refills from the UNTRANSLATED virtual address, so a mapping +/// where virtual != physical fetches garbage. This is exactly Linux's swapper +/// (virtual 0xffffffff8000xxxx -> physical 0x8aaxxxxx). +/// +/// 0x00 csrw satp, a0 enable Sv39 +/// 0x04 auipc t0, 0x3 t0 = 0x3004 +/// 0x08 jalr x0, -4(t0) jump to VIRTUAL 0x3000 +/// @phys 0x1000 (= virt 0x3000 via L0[3]->PPN1, NON-IDENTITY): +/// 0x1000 addi x6, x0, 0x11 +/// 0x1004 jal x0, 0 park (= virtual 0x3004) +/// +/// Page tables (Sv39, identity for low code + one non-identity 4KB leaf): +/// L2[0]@0x10000 = 0x4401 ; L1[0]@0x11000 = 0x4801 ; L0@0x12000 +/// L0[0]@0x12000 = 0x000F (virt 0x0 -> phys 0x0) +/// L0[3]@0x12018 = 0x040F (virt 0x3000 -> phys 0x1000) NON-IDENTITY +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + RiverCoreConfig full() => RiverCoreConfigV1.full( + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare, RiscVPagingMode.sv39], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + hasSupervisorUserMemory: true, + hasMakeExecutableReadable: true, + ), + clock: const HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + ); + + String mem(Map> words) { + final sb = StringBuffer(); + final addrs = words.keys.toList()..sort(); + for (final a in addrs) { + sb.writeln('@${a.toRadixString(16)}'); + for (final w in words[a]!) { + for (var i = 0; i < 4; i++) { + sb.write(((w >> (i * 8)) & 0xFF).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + } + sb.writeln(); + } + return sb.toString(); + } + + String prog() => mem({ + 0x00: [0x18051073, 0x00003297, 0xFFC28067], + 0x1000: [0x01100313, 0x0000006f], + 0x10000: [0x00004401, 0x0], + 0x11000: [0x00004801, 0x0], + 0x12000: [0x0000000F, 0x0], + 0x12018: [0x0000040F, 0x0], + }); + + test( + 'non-identity paged fetch: virt 0x3000 -> phys 0x1000 decodes', + timeout: Timeout(Duration(minutes: 6)), + () => coreTest( + prog(), + {Register.x6: 0x11}, + full(), + startPriv: PrivilegeMode.supervisor, + initRegisters: {Register.x10: 0x8000000000000010}, + nextPc: 0x3004, + ), + ); +} diff --git a/packages/river_hdl/test/mmu/satp_disable_fetch_test.dart b/packages/river_hdl/test/mmu/satp_disable_fetch_test.dart new file mode 100644 index 0000000..09e8989 --- /dev/null +++ b/packages/river_hdl/test/mmu/satp_disable_fetch_test.dart @@ -0,0 +1,117 @@ +import 'package:river/river.dart'; +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import '../core_harness.dart'; + +/// Repro for the delta NixOS boot wedge (task #88): the kernel's page-directory +/// switch trampoline (relocate_enable_mmu family) does +/// sfence.vma ; csrw satp, 0 (disable paging) ; auipc ... +/// The instruction right AFTER `csrw satp, 0` must be fetched with paging OFF +/// (a plain bare physical fetch). On delta River instead walked it with the +/// stale paging-ON satp and took a spurious instruction page fault on the now +/// physical (unmapped-as-a-VA) PC, then wedged. +/// +/// This isolates the hazard: run paged (Sv39), put `csrw satp, x0` at the end of +/// a MAPPED page (VA 0x1000 -> phys 0x1000), and the next instruction on the +/// NEXT page (VA/phys 0x2000) which is deliberately UNMAPPED under the active +/// satp. With paging correctly disabled the fetch of 0x2000 is a bare physical +/// access to real code (x6 <- 0x22). If the disable does not take effect for +/// that fetch, VA 0x2000 is unmapped -> instruction page fault, x6 stays 0. +/// +/// Layout (all identity for what is mapped): +/// phys 0x0000 csrw satp, a0 enable Sv39 (a0 seeded = 0x8..0010) +/// phys 0x0004 jal x0, +0x1FF8 -> VA 0x1FFC (end of the mapped page) +/// phys 0x1FFC csrw satp, x0 disable paging (bare) +/// phys 0x2000 addi x6, x0,0x22 <- bare-only fetch, the test +/// phys 0x2004 jal x0, 0 park +/// Page table (Sv39, root 0x10000): VA 0x0 -> phys 0x0, VA 0x1000 -> phys 0x1000, +/// VA 0x2000 UNMAPPED. +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + // small (RV64IMAC microcode, no FPU) shares the fetch/MMU/CSR datapath with + // delta's full rc1-f but elaborates/sims much faster - the satp fetch hazard + // lives in that shared logic, so it reproduces here too. + RiverCoreConfig full() => RiverCoreConfigV1.small( + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare, RiscVPagingMode.sv39], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + hasSupervisorUserMemory: true, + hasMakeExecutableReadable: true, + ), + clock: const HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + ); + + int jal(int rd, int off) { + final b20 = (off >> 20) & 1; + final b10_1 = (off >> 1) & 0x3ff; + final b11 = (off >> 11) & 1; + final b19_12 = (off >> 12) & 0xff; + return (b20 << 31) | + (b10_1 << 21) | + (b11 << 20) | + (b19_12 << 12) | + (rd << 7) | + 0x6f; + } + + String mem(Map> words) { + final sb = StringBuffer(); + final addrs = words.keys.toList()..sort(); + for (final a in addrs) { + sb.writeln('@${a.toRadixString(16)}'); + for (final w in words[a]!) { + for (var i = 0; i < 4; i++) { + sb.write(((w >> (i * 8)) & 0xFF).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + } + sb.writeln(); + } + return sb.toString(); + } + + String prog() => mem({ + 0x0000: [0x18051073, jal(0, 0x1FF4)], // csrw satp,a0 ; jal ->0x1FF8 + // Block starts at the 8-byte-aligned 0x1FF8 (loadMemString aligns block + // starts down to 8B): filler nop @0x1FF8, then csrw satp, a1 @0x1FFC. + // csrw satp, a1 (a1=0, disable) - NOT `csrw satp, x0` (0x18001073), which + // River skips (rs1=x0 no-write); the real relocate_enable_mmu trampoline + // uses a register holding 0. + 0x1FF8: [ + 0x12000073, + 0x18059073, + ], // sfence.vma @0x1FF8 ; csrw satp,a1 @0x1FFC + 0x2000: [0x02200313, 0x0000006f], // addi x6,x0,0x22 ; park + 0x10000: [0x00004401, 0x0], // L2[0] -> L1 @0x11000 + 0x11000: [0x00004801, 0x0], // L1[0] -> L0 @0x12000 + 0x12000: [ + 0x0000000F, 0x0, // L0[0] VA0x0 -> phys0x0 (V R W X) + 0x0000040F, 0x0, // L0[1] VA0x1000 -> phys0x1000 + 0x00000000, 0x0, // L0[2] VA0x2000 UNMAPPED + ], + }); + + test( + 'csrw satp,0 disable takes effect for the very next fetch (bare)', + timeout: Timeout(Duration(minutes: 6)), + () => coreTest( + prog(), + {Register.x6: 0x22}, + full(), + startPriv: PrivilegeMode.supervisor, + initRegisters: {Register.x10: 0x8000000000000010}, // a0 = Sv39 root 0x10 + nextPc: 0x2004, + maxCycles: 700, // wedge fails fast instead of grinding the full budget + ), + ); +} diff --git a/packages/river_hdl/test/mmu/trampoline_fault_jump_test.dart b/packages/river_hdl/test/mmu/trampoline_fault_jump_test.dart new file mode 100644 index 0000000..2dd2191 --- /dev/null +++ b/packages/river_hdl/test/mmu/trampoline_fault_jump_test.dart @@ -0,0 +1,191 @@ +import 'dart:async'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd_hcl/rohd_hcl.dart' hide DataPortInterface, DataPortGroup; +import 'package:river/river.dart'; +import 'package:river_hdl/river_hdl.dart'; +import 'package:test/test.dart'; + +/// Reproduces the delta HW trampoline sequence exactly: a fetch FAULTS (current +/// low PC unmapped by the just-written satp), the pipeline traps to stvec = a +/// HIGH virtual address that IS mapped (2MB L1 leaf), and the fetch there must +/// succeed. On HW River instead faults AGAIN on that valid high fetch (scause=12 +/// loop). Observes pipeline.trap/trapCause directly (vector-independent) and +/// counts traps: exactly ONE instruction page fault (the intended low miss) is +/// expected, then the high target runs. A second fault = the bug. +/// +/// phys 0x00 (bare until satp): +/// 0x00 lui t1, 0x80001 t1 = 0xffffffff80001000 +/// 0x04 addi t1, t1, 0x48 t1 = 0xffffffff80001048 (stvec target) +/// 0x08 csrw stvec, t1 +/// 0x0c csrw satp, a0 enable Sv39 (maps ONLY high, not low PC) +/// 0x10 -> trap to stvec = high 0x..1048 +/// phys 0x201048 (= virt 0xffffffff80001048 via 2MB L1 leaf): +/// addi x6, x0, 0x11 +/// jal x0, 0 +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + test( + 'trampoline: fault -> trap to mapped high 2MB-leaf fetch succeeds', + timeout: Timeout(Duration(minutes: 6)), + () async { + final config = RiverCoreConfigV1.full( + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare, RiscVPagingMode.sv39], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + hasSupervisorUserMemory: true, + hasMakeExecutableReadable: true, + ), + clock: const HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + ); + + // M-mode setup: delegate instruction page fault (medeleg bit 12) to S-mode, + // set stvec = high target, satp = a table mapping ONLY high (root[0] invalid), + // then mret into S-mode at low 0x100. The S-mode fetch of 0x100 faults -> + // delegated -> stvec = high 2MB-leaf address -> must fetch/execute there. + // 0x00 lui a1,0x1 ; 0x04 csrw medeleg,a1 (=0x1000, bit12) + // 0x08 lui a2,0x80001 ; 0x0c addi a2,a2,0x48 (a2=0xffffffff80001048) + // 0x10 csrw stvec,a2 ; 0x14 csrw satp,a0 + // 0x18 addi a3,x0,0x100 ; 0x1c csrw mepc,a3 + // 0x20 lui a4,0x1 ; 0x24 srli a4,a4,1 (=0x800 MPP=S) + // 0x28 csrw mstatus,a4 ; 0x2c mret -> S-mode @0x100 (faults) + const memString = '''@0 +b7 15 00 00 73 90 25 30 37 16 00 80 13 06 86 04 73 10 56 10 73 10 05 18 93 06 00 10 73 90 16 34 37 17 00 00 13 57 17 00 73 10 07 30 73 00 20 30 +@10ff0 +01 44 00 00 00 00 00 00 +@11000 +ef 00 08 00 00 00 00 00 +@201048 +13 03 10 01 6f 00 00 00 +'''; + + final clk = SimpleClockGenerator(20).clk; + final reset = Logic(); + final aw = config.mxlen.size; + final wb = WishboneConfig( + addressWidth: aw, + dataWidth: aw, + selWidth: aw ~/ 8, + ); + final core = RiverCore(config, busConfig: wb); + core.input('clk').srcConnection! <= clk; + core.input('reset').srcConnection! <= reset; + await core.build(); + + final storage = SparseMemoryStorage( + addrWidth: aw, + dataWidth: aw, + alignAddress: (a) => a, + onInvalidRead: (a, w) => LogicValue.filled(w, LogicValue.zero), + ); + final memRead = DataPortInterface(aw, aw); + final memWrite = DataPortInterface(aw, aw); + // ignore: unused_local_variable + final mem = MemoryModel( + clk, + reset, + [wrapWriteForRegisterFile(memWrite)], + [wrapReadForRegisterFile(memRead, clk: clk, readLatency: 0)], + readLatency: 0, + storage: storage, + ); + final cyc = core.output('dataBus_CYC'); + final stb = core.output('dataBus_STB'); + final we = core.output('dataBus_WE'); + final adr = core.output('dataBus_ADR'); + final mosi = core.output('dataBus_DAT_MOSI'); + memRead.en <= cyc & stb & ~we; + memRead.addr <= adr; + memWrite.en <= cyc & stb & we; + memWrite.addr <= adr; + memWrite.data <= mosi; + final ack = Logic(); + Sequential(clk, [ + If( + reset, + then: [ack < 0], + orElse: [ + If( + cyc & stb & ~ack & (we | memRead.valid), + then: [ack < 1], + orElse: [ack < 0], + ), + ], + ), + ]); + core.input('dataBus_ACK').srcConnection! <= ack; + core.input('dataBus_DAT_MISO').srcConnection! <= memRead.data; + + reset.inject(1); + Simulator.registerAction(20, () { + reset.put(0); + core.regWritePort.en.inject(1); + core.regWritePort.addr.inject(LogicValue.ofInt(10, 5)); // a0 + core.regWritePort.data.inject(LogicValue.ofInt(0x8000000000000010, 64)); + storage.loadMemString(memString); + }); + Simulator.setMaxSimTime(400000); + unawaited(Simulator.run()); + await clk.nextPosedge; + core.regWritePort.en.inject(0); + while (reset.value.toBool()) { + await clk.nextPosedge; + } + + var faults = 0; + var reachedHigh = false; + var lastPc = 0; + final trail = []; + for (var i = 0; i < 4000; i++) { + await clk.nextPosedge; + final trap = core.pipeline.trap.value; + if (trap.isValid && trap.toInt() == 1) { + final cause = core.pipeline.trapCause.value; + if (cause.isValid && + cause.toInt() == Trap.instructionPageFault.causeCode) { + faults++; + } + } + final npc = core.pipeline.nextPc.value; + if (npc.isValid) { + lastPc = npc.toInt(); + final s = '0x${lastPc.toRadixString(16)}'; + if (trail.isEmpty || trail.last != s) trail.add(s); + if (lastPc == 0xffffffff8000104c) { + reachedHigh = true; + break; + } + } + } + expect( + faults, + greaterThan(0), + reason: + 'the intended low-page trampoline miss should fault at least once', + ); + await Simulator.endSimulation(); + await Simulator.simulationEnded; + + // Exactly one fault (the intended low miss). The high fetch must NOT fault. + expect( + reachedHigh, + isTrue, + reason: + 'never reached the high park; faults=$faults ' + 'lastPc=0x${lastPc.toRadixString(16)} (River faulted on the valid ' + 'high 2MB-leaf fetch = the bug)', + ); + final x6 = core.regs.getData(LogicValue.ofInt(6, 5))!.toInt(); + expect(x6, 0x11, reason: 'high target did not execute'); + }, + ); +} diff --git a/packages/river_hdl/test/trap/illegal_zero_test.dart b/packages/river_hdl/test/trap/illegal_zero_test.dart new file mode 100644 index 0000000..9cb9b28 --- /dev/null +++ b/packages/river_hdl/test/trap/illegal_zero_test.dart @@ -0,0 +1,73 @@ +import 'package:river/river.dart'; +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import '../core_harness.dart'; + +/// All-zero-instruction trap regression. +/// +/// The RISC-V ISA reserves the 16-bit encoding 0x0000 as a guaranteed-illegal +/// instruction, specifically so a jump into zeroed memory faults at once instead +/// of running forward. On the delta board the kernel booted past scounteren, +/// then a bad control transfer sent the PC into a zeroed region and the core +/// SLED forward two bytes at a time for hundreds of MB without ever trapping. +/// That is only possible if the core decodes 0x0000 as a nop rather than an +/// illegal instruction. This test pins the required behaviour: fetching 0x0000 +/// must raise an illegal-instruction trap (cause 2) and vector to mtvec. +RiverCoreConfig _rc1f() => RiverCoreConfigV1.small( + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare, RiscVPagingMode.sv39], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + hasSupervisorUserMemory: true, + hasMakeExecutableReadable: true, + ), + interrupts: [], + clock: const HarborClockConfig( + name: 'test', + rate: HarborFixedClockRate(12000000), + ), + resetVector: 0, +); + +String _memString(Map words) { + const nop = 0x00000013; + final maxAddr = words.keys.reduce((a, b) => a > b ? a : b); + final sb = StringBuffer('@0\n'); + for (var addr = 0; addr <= maxAddr + 4; addr += 4) { + final w = words[addr] ?? nop; + for (var b = 0; b < 4; b++) { + sb.write(((w >> (b * 8)) & 0xFF).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + } + return sb.toString(); +} + +void main() { + test( + 'fetching 0x0000 raises illegal-instruction (cause 2), not a silent nop', + () async { + await Simulator.reset(); + // mtvec = 0x40 (direct). At 0x04 sits 0x0000 (illegal compressed). A + // correct core vectors to 0x40 and mcause reads 2. A core that treats + // 0x0000 as a nop instead runs the 0xAA sentinel at 0x08 and never traps. + final program = { + 0x00: 0x30571073, // csrw mtvec, x14 (x14 = 0x40) + 0x04: 0x00000000, // 0x0000: illegal compressed instruction -> trap + 0x08: 0x0aa00393, // addi x7, x0, 0xAA (sentinel: must NOT run) + 0x40: 0x342022f3, // handler: csrr x5, mcause (== 2) + 0x44: 0x00000013, // nop + }; + await coreTest( + _memString(program), + {Register.x5: 2}, // illegal-instruction cause + _rc1f(), + initRegisters: {Register.x14: 0x40}, + nextPc: 0x48, + ); + }, + timeout: Timeout(Duration(minutes: 5)), + ); +} diff --git a/packages/river_hdl/test/trap/mmode_notranslate_test.dart b/packages/river_hdl/test/trap/mmode_notranslate_test.dart new file mode 100644 index 0000000..8c84cee --- /dev/null +++ b/packages/river_hdl/test/trap/mmode_notranslate_test.dart @@ -0,0 +1,74 @@ +import 'package:river/river.dart'; +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import '../core_harness.dart'; + +/// M-mode data accesses must NOT be translated, even when supervisor has enabled +/// paging (satp.MODE != 0). River has no mstatus.MPRV, so an M-mode load/store +/// is always physical. Before the fix the MMU walked M-mode loads/stores through +/// the supervisor page tables the moment satp was set: on the delta board the +/// SBI firmware (Weir, M-mode) restored its stack in a trap handler right after +/// the kernel enabled Sv39, the physical stack address was not a valid VA, and +/// the walk hung the core. Here satp points at a bogus root; a translated access +/// would fault/miss, a physical (bypassed) access reads the real value. +RiverCoreConfig _rc1f() => RiverCoreConfigV1.small( + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare, RiscVPagingMode.sv39], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + hasSupervisorUserMemory: true, + hasMakeExecutableReadable: true, + ), + interrupts: [], + clock: const HarborClockConfig( + name: 'test', + rate: HarborFixedClockRate(12000000), + ), + resetVector: 0, +); + +String _memString(Map words) { + const nop = 0x00000013; + final maxAddr = words.keys.reduce((a, b) => a > b ? a : b); + final sb = StringBuffer('@0\n'); + for (var addr = 0; addr <= maxAddr + 4; addr += 4) { + final w = words[addr] ?? nop; + for (var b = 0; b < 4; b++) { + sb.write(((w >> (b * 8)) & 0xFF).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + } + return sb.toString(); +} + +void main() { + test( + 'M-mode load with Sv39 satp set reads physical (no translation)', + () async { + await Simulator.reset(); + // csrw satp, x11 (x11 = Sv39 mode + bogus root PPN=1). Then ld x7, 0(x12) + // from physical 0x200 which holds 0xABCD. In M-mode the access must bypass + // paging and read 0xABCD; a page-table walk would read a garbage PTE. + final program = { + 0x00: 0x18059073, // csrw satp, x11 + 0x04: 0x00063383, // ld x7, 0(x12) + 0x08: 0x00000013, // nop + 0x200: 0x0000abcd, // data at physical 0x200 (low word) + 0x204: 0x00000000, // high word + }; + await coreTest( + _memString(program), + {Register.x7: 0xabcd}, + _rc1f(), + initRegisters: { + Register.x11: 0x8000000000000001, // satp: Sv39, root PPN 1 + Register.x12: 0x200, + }, + nextPc: 0x0c, + ); + }, + timeout: Timeout(Duration(minutes: 5)), + ); +} diff --git a/packages/river_hdl/test/trap/mret_to_smode_repro_test.dart b/packages/river_hdl/test/trap/mret_to_smode_repro_test.dart new file mode 100644 index 0000000..91e84a1 --- /dev/null +++ b/packages/river_hdl/test/trap/mret_to_smode_repro_test.dart @@ -0,0 +1,177 @@ +import 'package:rohd/rohd.dart'; +import 'package:river/river.dart'; +import 'package:test/test.dart'; +import '../core_harness.dart'; + +/// Repro attempt: does MRET with mstatus.MPP=S actually drop the core to +/// S-mode? On HW the delta NixOS boot wedges with priv=M while PC is kernel +/// code right after an __sbi_ecall + mret, suggesting mret returns to the epc +/// but leaves the privilege at M. This isolates that: set MPP=S, mret to a +/// target, then execute an M-ONLY csr read (mhartid, 0xf14). In real S-mode +/// that traps illegal (-> mtvec handler sets x7=0xDEAD). If mret wrongly stayed +/// in M-mode, the mhartid read succeeds and x7 stays 0. +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + RiverCoreConfig full() => RiverCoreConfigV1.full( + interrupts: [], + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + ), + clock: const HarborClockConfig( + name: 'sysclk', + rate: HarborFixedClockRate(48000000), + ), + ); + + int csrw(int csr, int rs1) => (csr << 20) | (rs1 << 15) | (0x1 << 12) | 0x73; + int csrr(int csr, int rd) => + (csr << 20) | (0 << 15) | (0x2 << 12) | (rd << 7) | 0x73; + int bne(int rs1, int rs2, int off) { + final b12 = (off >> 12) & 1; + final b11 = (off >> 11) & 1; + final b10_5 = (off >> 5) & 0x3f; + final b4_1 = (off >> 1) & 0xf; + return (b12 << 31) | + (b10_5 << 25) | + (rs2 << 20) | + (rs1 << 15) | + (0x1 << 12) | + (b4_1 << 8) | + (b11 << 7) | + 0x63; + } + + const ecall = 0x00000073; + int addi(int rd, int rs1, int imm) => + ((imm & 0xFFF) << 20) | (rs1 << 15) | (rd << 7) | 0x13; + int slli(int rd, int rs1, int sh) => + (sh << 20) | (rs1 << 15) | (0x1 << 12) | (rd << 7) | 0x13; + const mret = 0x30200073; + const jalLoop = 0x0000006F; + const nop = 0x00000013; + + String words(List ws) { + final sb = StringBuffer(); + for (final w in ws) { + for (var b = 0; b < 4; b++) { + sb.write(((w >> (b * 8)) & 0xFF).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + } + return sb.toString().trimRight(); + } + + test( + 'mret with MPP=S drops to S-mode (M-only csr then traps)', + timeout: Timeout(Duration(minutes: 6)), + () { + // Layout (word index -> byte addr = idx*4): + // 0 mtvec = 0x60 (handler) @0x00 + // 1 csrw mtvec, x14 + // 2 mepc = 0x40 (target) @0x08 + // 3 csrw mepc, x14 + // 4 x12 = 1 @0x10 + // 5 slli x12,x12,11 (MPP=S=0x800) + // 6 csrw mstatus, x12 + // 7 mret -> S-mode @0x40 @0x1c + // 8..15 nops + // 16 @0x40 target: x5 = 0xAB + // 17 @0x44 csrr x6, mhartid (M-only) -> illegal in S -> mtvec + // 18 @0x48 jal loop (only reached if NO trap = mret bug) + // ... + // 24 @0x60 handler: x7 = 0xDEAD + // 25 @0x64 jal loop + final prog = words([ + addi(14, 0, 0x60), // 0 + csrw(0x305, 14), // 1 mtvec + addi(14, 0, 0x40), // 2 + csrw(0x341, 14), // 3 mepc + addi(12, 0, 1), // 4 + slli(12, 12, 11), // 5 MPP=S (0x800) + csrw(0x300, 12), // 6 mstatus + mret, // 7 @0x1c + nop, nop, nop, nop, nop, nop, nop, nop, // 8..15 + addi(5, 0, 0xAB), // 16 @0x40 target (S-mode) + csrr(0xf14, 6), // 17 @0x44 mhartid: illegal in S-mode + jalLoop, // 18 @0x48 park (reached only if NOT trapped) + nop, nop, nop, nop, nop, // 19..23 + addi( + 7, + 0, + 0xDEAD & 0x7FF, + ), // 24 @0x60 handler marker (0x6AD in 11 bits) + jalLoop, // 25 @0x64 park + ]); + // x5=0xAB proves we reached the target. x7=0x2AD proves the M-only read + // trapped, i.e. mret correctly entered S-mode. If mret left the core in + // M-mode, x7 stays 0 (bug reproduced). + return coreTest( + '@0\n$prog\n', + {Register.x5: 0xAB, Register.x7: 0x6AD}, + full(), + nextPc: 0x64, + ); + }, + ); + + test( + 'ecall-from-S round trip: hw trap-entry MPP=S, mret returns to S-mode', + timeout: Timeout(Duration(minutes: 6)), + () { + // This is the REAL hang shape: S-mode does `ecall`, the hardware trap + // entry must save mstatus.MPP=S, the M-mode handler mrets, and the core + // must land back in S-mode. Observed on HW: after an SBI ecall the core + // sits at a kernel PC with priv=M (mret left it in M). + // + // M setup (0x00): mtvec=HANDLER(0x80), MPP=S, mepc=SCODE(0x40), mret + // SCODE (0x40, S): ecall -> handler -> returns here -> x5=0xAB -> + // csrr mhartid (M-only): traps in S -> handler illegal + // HANDLER (0x80, M): if mcause==9 (ecall) skip+4 & mret; + // else (illegal probe) x7=0x6AD, park. + final prog = words([ + // ---- M setup @0x00 ---- + addi(14, 0, 0x80), // 0 HANDLER + csrw(0x305, 14), // 1 mtvec + addi(14, 0, 0x40), // 2 SCODE + csrw(0x341, 14), // 3 mepc + addi(12, 0, 1), // 4 + slli(12, 12, 11), // 5 MPP=S + csrw(0x300, 12), // 6 mstatus + mret, // 7 @0x1c -> S @0x40 + nop, nop, nop, nop, nop, nop, nop, nop, // 8..15 (0x20..0x3c) + // ---- SCODE @0x40 (S-mode) ---- + ecall, // 16 @0x40 -> M handler + addi(5, 0, 0xAB), // 17 @0x44 returned (S-mode if mret correct) + csrr(0xf14, 6), // 18 @0x48 mhartid: illegal in S -> handler + jalLoop, // 19 @0x4c park (only if M-mode bug: no trap) + nop, nop, nop, nop, nop, nop, nop, nop, // 20..27 (0x50..0x6c) + nop, nop, nop, nop, // 28..31 (0x70..0x7c) + // ---- HANDLER @0x80 (M-mode) ---- + csrr(0x342, 13), // 32 @0x80 x13 = mcause + addi(15, 0, 9), // 33 @0x84 x15 = 9 (ecall-from-S) + bne(13, 15, 0x14), // 34 @0x88 if mcause!=9 -> illegal_path @0x9c + csrr(0x341, 14), // 35 @0x8c x14 = mepc + addi(14, 14, 4), // 36 @0x90 skip ecall + csrw(0x341, 14), // 37 @0x94 mepc = mepc+4 + mret, // 38 @0x98 -> back to S @0x44 + addi(7, 0, 0x6AD), // 39 @0x9c illegal_path: S-mode confirmed + jalLoop, // 40 @0xa0 park + ]); + // x5=0xAB: the ecall round trip returned. x7=0x6AD: it returned in + // S-mode (the M-only mhartid read trapped). If mret returned to M-mode, + // x7 stays 0 = bug reproduced. + return coreTest( + '@0\n$prog\n', + {Register.x5: 0xAB, Register.x7: 0x6AD}, + full(), + nextPc: 0xa0, + ); + }, + ); +} diff --git a/packages/river_hdl/test/trap/sfence_vma_test.dart b/packages/river_hdl/test/trap/sfence_vma_test.dart new file mode 100644 index 0000000..df03a90 --- /dev/null +++ b/packages/river_hdl/test/trap/sfence_vma_test.dart @@ -0,0 +1,66 @@ +import 'package:river/river.dart'; +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import '../core_harness.dart'; + +/// sfence.vma must be implemented by any S-mode core (rc1-f/delta has S-mode +/// via rvPriv but no hypervisor). It was previously defined ONLY in the +/// hypervisor extension (rvH), so delta did not decode it: the kernel's +/// relocate_enable_mmu runs sfence.vma around the satp write, and without it the +/// first fetch after paging is enabled mis-translates and the core runs off the +/// rails. This pins the fix: sfence.vma decodes and executes (TLB fence, PC+4), +/// it does NOT raise an illegal-instruction trap. +RiverCoreConfig _rc1f() => RiverCoreConfigV1.small( + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare, RiscVPagingMode.sv39], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + hasSupervisorUserMemory: true, + hasMakeExecutableReadable: true, + ), + interrupts: [], + clock: const HarborClockConfig( + name: 'test', + rate: HarborFixedClockRate(12000000), + ), + resetVector: 0, +); + +String _memString(Map words) { + const nop = 0x00000013; + final maxAddr = words.keys.reduce((a, b) => a > b ? a : b); + final sb = StringBuffer('@0\n'); + for (var addr = 0; addr <= maxAddr + 4; addr += 4) { + final w = words[addr] ?? nop; + for (var b = 0; b < 4; b++) { + sb.write(((w >> (b * 8)) & 0xFF).toRadixString(16).padLeft(2, '0')); + sb.write(' '); + } + } + return sb.toString(); +} + +void main() { + test( + 'sfence.vma is legal (executes, advances pc) not an illegal trap', + () async { + await Simulator.reset(); + // sfence.vma (0x12000073, rs1=rs2=0) then a sentinel. If sfence.vma were + // unimplemented it would trap and x7 would never reach 0x99. + final program = { + 0x00: 0x12000073, // sfence.vma + 0x04: 0x09900393, // addi x7, x0, 0x99 + 0x08: 0x00000013, // nop + }; + await coreTest( + _memString(program), + {Register.x7: 0x99}, + _rc1f(), + nextPc: 0x0c, + ); + }, + timeout: Timeout(Duration(minutes: 5)), + ); +} diff --git a/packages/river_maskrom/lib/src/maskrom.dart b/packages/river_maskrom/lib/src/maskrom.dart index 8c52609..d732230 100644 --- a/packages/river_maskrom/lib/src/maskrom.dart +++ b/packages/river_maskrom/lib/src/maskrom.dart @@ -44,6 +44,14 @@ class RiverMaskromConfig { /// poll until an image lands in RAM, jump to the reported entry. Skips the copy. final RiverDfuConfig? dfu; + /// Optional boot banner. When [bootMessage] and [uartBase] are both set, the + /// maskrom brings up the UART and prints the banner before handing off (the + /// xipLaunch path prints it just before jumping to the FSBL). [uartDivisor] is + /// the ns16550a baud divisor (uart clock / baud). + final String? bootMessage; + final int? uartBase; + final int uartDivisor; + const RiverMaskromConfig({ required this.isa, required this.resetVector, @@ -53,6 +61,9 @@ class RiverMaskromConfig { required this.stackTop, this.bootMode = RiverBootMode.sram, this.dfu, + this.bootMessage, + this.uartBase, + this.uartDivisor = 1, }); } @@ -60,6 +71,9 @@ class RiverMaskrom extends Module { @override final RiscVIsaConfig isa; + /// Unique-label counter for the banner's per-character TX-wait loops. + int _uid = 0; + RiverMaskrom(RiverMaskromConfig config) : isa = config.isa { register(Register.x2).bind(li(config.stackTop)); @@ -76,6 +90,9 @@ class RiverMaskrom extends Module { // No copy: warm up the flash XIP controller, then jump to the FSBL running // in place. flashSource = warmup read window; copyDest = FSBL entry. _warmupRead(config.flashSource, config.copySize); + if (config.bootMessage != null && config.uartBase != null) { + _emitBanner(config.bootMessage!, config.uartBase!, config.uartDivisor); + } fence(); register(Register.x10).bind(li(0)); // a0 = hartid (boot hart) register( @@ -158,6 +175,33 @@ class RiverMaskrom extends Module { bne(register(Register.x10), register(Register.x12), loop); } + /// Bring up the ns16550a UART (8N1, [divisor] baud divisor) and print + /// [msg] as a boot banner. x13 holds the UART base throughout; each byte + /// polls THRE (LSR bit 5) before it writes THR. The banner is fire-and-forget: + /// nothing here is read back, so it never blocks the handoff to the FSBL. + void _emitBanner(String msg, int uartBase, int divisor) { + final div = divisor.clamp(1, 0xffff); + register(Register.x13).bind(li(uartBase)); + register(Register.x11).bind(li(0x83)); // LCR: DLAB=1, 8N1 + sb(register(Register.x13), register(Register.x11), offset: 3); + register(Register.x11).bind(li(div & 0xff)); + sb(register(Register.x13), register(Register.x11), offset: 0); + register(Register.x11).bind(li((div >> 8) & 0xff)); + sb(register(Register.x13), register(Register.x11), offset: 1); + register(Register.x11).bind(li(0x03)); // LCR: DLAB=0, 8N1 + sb(register(Register.x13), register(Register.x11), offset: 3); + + register(Register.x13).bind(li(uartBase)); + for (final c in msg.codeUnits) { + final wait = label('mrtx_${_uid++}'); + final lsr = lbu(register(Register.x13), offset: 5); + register(Register.x14).bind(andi(lsr, 0x20)); + beq(register(Register.x14), register(Register.x0), wait); + register(Register.x11).bind(li(c)); + sb(register(Register.x13), register(Register.x11)); + } + } + void _copyLoop(int src, int dst, int size) { // One loop-carried pointer (x10, source); dst is recomputed each iteration // as x10 + (dst - src). The ADL dead-code pass is not loop-aware: a separate diff --git a/packages/river_maskrom/test/maskrom_banner_sim_test.dart b/packages/river_maskrom/test/maskrom_banner_sim_test.dart new file mode 100644 index 0000000..8ca01a1 --- /dev/null +++ b/packages/river_maskrom/test/maskrom_banner_sim_test.dart @@ -0,0 +1,126 @@ +import 'dart:async'; + +import 'package:river/river.dart'; +import 'package:river_emulator/river_emulator.dart'; +import 'package:river_maskrom/river_maskrom.dart'; +import 'package:test/test.dart'; + +/// Emulator sim of the xipLaunch maskrom banner: proves the boot ROM brings up +/// the UART and prints its banner before it hands off to the FSBL. The banner +/// runs after the flash warm-up read and before the jalr to the FSBL entry, so +/// the test breaks the instant the full banner lands (it never has to reach a +/// valid FSBL). +RiverCoreConfig _rv64(int romBase) => RiverCoreConfigV1.small( + mmu: HarborMmuConfig( + mxlen: RiscVMxlen.rv64, + pagingModes: const [RiscVPagingMode.bare, RiscVPagingMode.sv39], + tlbLevels: const [], + pmp: HarborPmpConfig.none, + hasSupervisorUserMemory: true, + hasMakeExecutableReadable: true, + ), + interrupts: [], + clock: const HarborClockConfig( + name: 'test', + rate: HarborFixedClockRate(12000000), + ), + resetVector: romBase, +); + +void main() { + test('xipLaunch maskrom prints its banner over the UART', () async { + const romBase = 0x10000; + const uartBase = 0x10000000; + const flashBase = 0x20000000; + const banner = + 'River maskrom (RC1.f, Delta V1), jumping to FSBL in flash\r\n'; + final config = _rv64(romBase); + + final rom = RiverMaskrom( + RiverMaskromConfig( + isa: config.isa, + resetVector: romBase, + flashSource: flashBase, // warm-up read window + copyDest: flashBase, // FSBL entry (never reached: we break on banner) + copySize: 64, + stackTop: romBase + 0x8000, + bootMode: RiverBootMode.xipLaunch, + bootMessage: banner, + uartBase: uartBase, + uartDivisor: 12000000 ~/ 115200, + ), + ); + await rom.build(); + final romBytes = rom.generateBinary(); + + final romMem = Sram( + RiverDevice( + name: 'rom', + compatible: 'river,sram', + range: BusAddressRange(romBase, 0x10000), + clockFrequency: 12000000, + ), + ); + // Flash region the warm-up read walks (contents do not matter). + final flash = Sram( + RiverDevice( + name: 'flash', + compatible: 'river,sram', + range: BusAddressRange(flashBase, 0x1000), + clockFrequency: 12000000, + ), + ); + final uartOut = []; + final outCtl = StreamController>(sync: true); + final inCtl = StreamController>(sync: true); + outCtl.stream.listen(uartOut.addAll); + final uart = Uart( + RiverDevice( + name: 'uart0', + compatible: 'ns16550a', + range: BusAddressRange(uartBase, 0x1000), + interrupts: [0], + clockFrequency: 12000000, + ), + input: inCtl.stream, + output: outCtl.sink, + ); + + for (var i = 0; i < romBytes.length; i++) { + romMem.data[i] = romBytes[i]; + } + + final core = RiverCore( + config, + memDevices: Map.fromEntries([romMem.mem!, flash.mem!, uart.mem!]), + ); + + var pc = romBase; + try { + for (var i = 0; i < 2000000; i++) { + final instr = await core.fetch(pc); + pc = await core.cycle(pc, instr); + if (String.fromCharCodes(uartOut).contains(banner)) break; + } + } on Object { + // Running off into the (empty) flash after the jalr to the FSBL entry is + // expected: the banner prints before the handoff, so the capture below + // is already complete. + } + + // The emulated UART drains its TX FIFO on async timers, so the final byte + // can still be in flight when the core stops. Let it settle. + await uart.flush(); + + final out = String.fromCharCodes(uartOut); + // ignore: avoid_print + print( + 'maskrom UART: ${out.replaceAll('\r', '\\r').replaceAll('\n', '\\n')}', + ); + expect( + out, + contains(banner), + reason: 'maskrom did not print its banner; got: $out', + ); + }); +} diff --git a/pkgs/nextpnr-chipdb/default.nix b/pkgs/nextpnr-chipdb/default.nix new file mode 100644 index 0000000..b08185a --- /dev/null +++ b/pkgs/nextpnr-chipdb/default.nix @@ -0,0 +1,49 @@ +# Builds a nextpnr-xilinx chip database (.bin) for a single 7-series part, from +# the prjxray-db and bbaexport that ship inside a given nextpnr-xilinx. The BBA +# schema is tied to the nextpnr SOURCE version, so this MUST be built from the +# same nextpnr-xilinx that will consume it (that is the whole reason it lives +# here rather than reusing a mismatched prebuilt chipdb). +{ + lib, + stdenvNoCC, + nextpnr-xilinx, + pypy310, +}: + +{ + device, # e.g. "xc7s50" + package, # e.g. "csga324" + family ? "spartan7", +}: + +let + part = "${device}${package}"; +in +stdenvNoCC.mkDerivation { + name = "nextpnr-xilinx-chipdb-${part}"; + inherit (nextpnr-xilinx) version; + + dontUnpack = true; + dontConfigure = true; + + nativeBuildInputs = [ pypy310 ]; + + buildPhase = '' + runHook preBuild + mkdir -p $out + db=${nextpnr-xilinx}/share/nextpnr/external/prjxray-db/${family} + # Pick the first speed grade directory for the part (e.g. ${part}-1). + sg=$(basename $(ls -d $db/${part}-* | sort -n | head -1)) + echo "bba-exporting $sg -> ${part}.bin" + pypy3.10 ${nextpnr-xilinx}/share/nextpnr/python/bbaexport.py --device "$sg" --bba ${part}.bba + ${nextpnr-xilinx}/bin/bbasm -l ${part}.bba $out/${part}.bin + runHook postBuild + ''; + + dontInstall = true; + + meta = { + description = "nextpnr-xilinx chipdb for ${part}"; + platforms = lib.platforms.all; + }; +} diff --git a/pkgs/river-fpga/default.nix b/pkgs/river-fpga/default.nix index 96aa147..e3b1f75 100644 --- a/pkgs/river-fpga/default.nix +++ b/pkgs/river-fpga/default.nix @@ -17,8 +17,16 @@ stdenvNoCC, yosys, nextpnr, + # nextpnr-xilinx (0.8.2, from nixpkgs): the router that handles the dense + # creek SoC. The openXC7 0.9.x router regressed and cannot route it. + nextpnr-xilinx, + # Chipdb builder function {device, package} -> derivation, built from the same + # nextpnr-xilinx (the BBA schema is tied to the nextpnr source version). + nextpnrChipdb, icestorm, trellis, + # openXC7 supplies only the prjxray pack tools (fasm2frames/xc7frames2bit) and + # the python fasm module; nextpnr + chipdb + prjxray-db come from nixpkgs. openxc7 ? null, openxc7Nixpkgs ? null, }: @@ -51,15 +59,22 @@ lib.extendMkDerivation { package = builtins.elemAt targetParts 2; # e.g. csga324 part = "${device}${package}"; - # openXC7 toolchain pieces (only forced on the spartan7 path). - chipdb = "${openxc7.nextpnr-xilinx-chipdb.spartan7}/${part}.bin"; - xrayDb = "${openxc7.nextpnr-xilinx}/share/nextpnr/external/prjxray-db"; + # Toolchain pieces (only forced on the spartan7 path). nextpnr + chipdb + + # prjxray-db come from nixpkgs (the routing 0.8.2 nextpnr); only the + # prjxray pack tools + fasm come from openXC7. + chipdb = "${nextpnrChipdb { inherit device package; }}/${part}.bin"; + xrayDb = "${nextpnr-xilinx}/share/nextpnr/external/prjxray-db"; pyPkgs = openxc7Nixpkgs.python312Packages; # prjxray's fasm2frames is a bare python script. Reproduce the openXC7 # devShell PYTHONPATH so its fasm/prjxray/textx imports resolve. + # `fasm.parser` unconditionally does `import pyximport; pyximport.install()` + # (to JIT the fast antlr parser, falling back to the pure-python textx + # parser already listed below), so Cython must be on the path or the import + # aborts before the fallback. fasmPythonPath = lib.concatStringsSep ":" [ "${openxc7.fasm}/lib/python3.12/site-packages" "${openxc7.prjxray}/usr/share/python3" + "${pyPkgs.cython}/lib/python3.12/site-packages" "${pyPkgs.textx}/lib/python3.12/site-packages" "${pyPkgs.arpeggio}/lib/python3.12/site-packages" "${pyPkgs.pyyaml}/lib/python3.12/site-packages" @@ -76,8 +91,8 @@ lib.extendMkDerivation { ]; xilinxTools = [ yosys - openxc7.nextpnr-xilinx - openxc7.prjxray + nextpnr-xilinx # 0.8.2 from nixpkgs (routes creek) + openxc7.prjxray # fasm2frames + xc7frames2bit openxc7Nixpkgs.python312 ]; in diff --git a/pkgs/river-hdl/default.nix b/pkgs/river-hdl/default.nix index cda8277..77ea684 100644 --- a/pkgs/river-hdl/default.nix +++ b/pkgs/river-hdl/default.nix @@ -4,6 +4,9 @@ callPackage, yosys, nextpnr, + # nextpnr-xilinx from nixpkgs is the 0.8.2 source that routes the dense creek + # SoC; the openXC7 flow's 0.9.x router regressed and fails to route it. + nextpnr-xilinx, icestorm, trellis, surfer, @@ -48,11 +51,15 @@ buildDartApplication (finalAttrs: { inherit yosys nextpnr + nextpnr-xilinx icestorm trellis openxc7 openxc7Nixpkgs ; + # Chipdb builder (a function of device/package), built from the same + # nixpkgs nextpnr-xilinx so the BBA schema matches. + nextpnrChipdb = callPackage ../nextpnr-chipdb { }; }; }; }) diff --git a/pkgs/river-ip/default.nix b/pkgs/river-ip/default.nix index 7d4ef88..73d3215 100644 --- a/pkgs/river-ip/default.nix +++ b/pkgs/river-ip/default.nix @@ -38,6 +38,9 @@ lib.extendMkDerivation { memories ? [ ], devices ? [ ], target ? null, + # Optional Harbor board name (e.g. "arty-s7-50"): supplies the board's + # connector catalog so a `spi:...:iface=pmod@ja` device resolves its pins. + board ? null, pdkRoot ? null, pins ? [ ], bootProgram ? null, @@ -51,8 +54,9 @@ lib.extendMkDerivation { "rc1-mi" "rc1-s" "rc1-m" + "rc1-f" ] - ) cores) "river-ip: cores must each be one of [rc1-n, rc1-mi, rc1-s, rc1-m]"; + ) cores) "river-ip: cores must each be one of [rc1-n, rc1-mi, rc1-s, rc1-m, rc1-f]"; assert lib.assertMsg (builtins.elem interconnect [ "wishbone" "axi" @@ -82,6 +86,7 @@ lib.extendMkDerivation { ) memories; deviceFlags = lib.concatMapStringsSep " " (d: "--device ${d}") devices; targetFlag = lib.optionalString (target != null) "--target ${target}"; + boardFlag = lib.optionalString (board != null) "--board ${board}"; pdkRootFlag = lib.optionalString (pdkRoot != null) "--pdk-root ${pdkRoot}"; # Quote each pin: a spec may carry a space-separated IOSTANDARD/attr # (e.g. "clk=R2 SSTL135"), which must reach genip as ONE --pin argument. @@ -97,6 +102,7 @@ lib.extendMkDerivation { "memories" "devices" "target" + "board" "pdkRoot" "pins" "bootProgram" @@ -113,7 +119,7 @@ lib.extendMkDerivation { buildPhase = '' runHook preBuild - river-genip ${cliArgs} ${coreFlags} ${memoryFlags} ${deviceFlags} ${targetFlag} ${pdkRootFlag} ${pinFlags} ${bootProgramFlag} --output "$out" + river-genip ${cliArgs} ${coreFlags} ${memoryFlags} ${deviceFlags} ${targetFlag} ${boardFlag} ${pdkRootFlag} ${pinFlags} ${bootProgramFlag} --output "$out" runHook postBuild ''; diff --git a/pubspec.lock b/pubspec.lock index 153bb65..abe23bd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -125,9 +125,9 @@ packages: dependency: "direct overridden" description: path: "packages/harbor" - ref: "fix/arty-s7-ddr" - resolved-ref: "8783b4c7bfa2e7ee3acfc07432f4e869a59a312f" - url: "https://github.com/MidstallSoftware/harbor.git" + ref: HEAD + resolved-ref: ddc1d70d05a67ba5a9fe0be33d3852795d07581e + url: "https://git.lilithsemi.com/LilithSemi/harbor" source: git version: "0.0.1" html: diff --git a/pubspec.lock.json b/pubspec.lock.json index 0558a4b..23a1a27 100644 --- a/pubspec.lock.json +++ b/pubspec.lock.json @@ -154,9 +154,9 @@ "dependency": "direct overridden", "description": { "path": "packages/harbor", - "ref": "fix/arty-s7-ddr", - "resolved-ref": "8783b4c7bfa2e7ee3acfc07432f4e869a59a312f", - "url": "https://github.com/MidstallSoftware/harbor.git" + "ref": "HEAD", + "resolved-ref": "ddc1d70d05a67ba5a9fe0be33d3852795d07581e", + "url": "https://git.lilithsemi.com/LilithSemi/harbor" }, "source": "git", "version": "0.0.1" diff --git a/pubspec.yaml b/pubspec.yaml index ec67718..a3fb1a8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,9 +15,8 @@ workspace: dependency_overrides: harbor: git: - url: https://github.com/MidstallSoftware/harbor.git + url: https://git.lilithsemi.com/LilithSemi/harbor path: packages/harbor - ref: fix/arty-s7-ddr dev_dependencies: coverage: ^1.15.0