Skip to content

System Top — rtl/system_top.sv (v4, historical)

As of v5, system_top no longer wires these sub-modules directly — it assembles three dies (base_die_top, cpu_chiplet_top, npu_chiplet_top) connected by UCIe links. See chiplets.md for the current package-level view. This page is kept for the pieces that are still accurate: the MOESI coherency controller and halt sentinel are unmodified, just relocated onto base_die_top and cpu_chiplet_top respectively. The instantiation diagram and cpu_ready derivation below describe v4 only and are no longer how the chip is wired — see the "Halt Detection" and "Sub-module Instantiation" notes inline.

system_top was the chip boundary in v4. It instantiated and wired all sub-modules, provided the instruction SRAM, and handled halt detection. The L1 data cache is replaced in v4 by the MOESI coherency controller + L2.


Module Interface

Inputs:
  clk, rst_n                          — clock and active-low reset
  net_valid, net_instruction[31:0]    — incoming instruction stream
  npu_result_ready                    — NPU result consumer ready
  dram_ack                            — DRAM word acknowledge pulse
  dram_rdata[31:0]                    — DRAM read data

Outputs:
  net_ready                           — backpressure to network source
  npu_result_valid                    — NPU result available
  npu_result_index[3:0]               — NPU output lane (0–15)
  npu_result_data[31:0]               — NPU output value (INT32)
  dram_read, dram_write               — DRAM transaction request flags
  dram_addr[31:0]                     — DRAM address
  dram_wdata[31:0]                    — DRAM write data (cache writeback)
  halt                                — CPU has fetched HALT instruction

Testbench / DMA ports (exposed for cocotb):
  npu_dma_read                        — NPU DMA read request
  npu_dma_addr[31:0]                  — NPU DMA target address
  npu_dma_ack                         — NPU DMA single-word ack pulse
  npu_dma_rdata[31:0]                 — NPU DMA read data

Instruction SRAM

256 words (1 KiB) of synchronous SRAM, written by the NoC router and read by the CPU fetch stage.

logic [31:0] imem [0:255];
logic [7:0]  imem_wr_ptr;   // word write pointer

Write: On every cpu_accept pulse from the NoC router, the current net_instruction is stored at imem[imem_wr_ptr] and the pointer increments. The single-cycle pulse prevents double-writes when net_valid is held across MUL/DIV stall cycles.

Read: active_instruction = imem[instr_addr[9:2]] — the CPU's instr_addr indexes words directly.

Valid guard: Fetch is valid only if the requested address is within the loaded region:

assign instr_valid = (instr_addr < imem_wr_byte_addr);

where imem_wr_byte_addr = {22'b0, imem_wr_ptr, 2'b0}. Instructions beyond the write pointer return instr_valid=0 and the CPU inserts NOPs.


MOESI Coherency Controller

In v4, the L1 data cache and external data memory interface are replaced by duck_coherency_ctrl, a 4-way set-associative L2 cache with two masters:

CPU (read/write)   ─────┐
                        ├──► duck_coherency_ctrl ──► DRAM
NPU DMA (read)     ─────┘

The CPU interface signals from riscv_core:

Signal Direction Description
cpu_addr[31:0] cpu → ctrl Access address
cpu_wdata[31:0] cpu → ctrl Write data
cpu_write cpu → ctrl 1 = write, 0 = read
cpu_req cpu → ctrl Request valid
cpu_rdata[31:0] ctrl → cpu Read data
cpu_ack ctrl → cpu Transaction complete pulse
mem_stall ctrl → cpu Freeze pipeline (cache miss)

The NPU DMA port is wired to the top-level npu_dma_* pins, accessible to the cocotb testbench (or a future NPU DMA hardware master).

duck_coherency_ctrl coherency_ctrl (
    .clk         (clk),
    .rst_n       (rst_n),
    // CPU port
    .cpu_addr    (cpu_mem_addr),
    .cpu_wdata   (cpu_mem_wdata),
    .cpu_write   (cpu_mem_write),
    .cpu_req     (cpu_mem_req),
    .cpu_rdata   (cpu_mem_rdata),
    .cpu_ack     (cpu_mem_ack),
    .mem_stall   (mem_stall),
    // NPU DMA port
    .npu_dma_read  (npu_dma_read),
    .npu_dma_addr  (npu_dma_addr),
    .npu_dma_ack   (npu_dma_ack),
    .npu_dma_rdata (npu_dma_rdata),
    // DRAM interface
    .dram_read   (dram_read),
    .dram_write  (dram_write),
    .dram_addr   (dram_addr),
    .dram_wdata  (dram_wdata),
    .dram_rdata  (dram_rdata),
    .dram_ack    (dram_ack)
);

Halt Detection

The halt sentinel is the RISC-V infinite-loop self-branch: jal x0, 0 = 0x0000_006F.

localparam [31:0] HALT_WORD = 32'h0000_006F;

assign halt = instr_valid && (active_instruction == HALT_WORD);

halt is observation-only — read directly by the testbench (wait_for_halt() in tb/tb_server_dispatch_unit.py) to know when to drain and check registers. RTL does not call $finish itself: simulation lifecycle belongs entirely to the cocotb regression manager. An earlier debug block here fired $finish 20 cycles after halt, but it raced cocotb's own end-of-regression shutdown and intermittently killed the simulator mid-report (SimFailure: Simulator shut down prematurely) — removed; see docs/issues/BUG-004.md.

v5 update: halt now lives in cpu_chiplet_top.sv (imem moved there) and is additionally gated on !core_stall, not just instr_valid && (active_instruction == HALT_WORD) as shown above. Splitting network ingestion from CPU stall cycles (see cpu_ready note below) let the fetch pointer race ahead to the halt word while an earlier MUL/DIV/REM was still retiring, firing halt too early — see docs/issues/BUG-007.md.


Sub-module Instantiation (v4)

system_top
  ├── noc_router  (router_inst)
  │     net_valid/instruction ← top-level net inputs
  │     cpu_accept  → SRAM write + imem_wr_ptr++
  │     npu_valid/instruction → npu_inst
  │
  ├── duck_coherency_ctrl  (coherency_ctrl)
  │     cpu_addr/wdata/write/req ← riscv_core
  │     cpu_rdata/ack/mem_stall  → riscv_core
  │     npu_dma_* ← top-level ports (cocotb / future DMA HW)
  │     dram_* ← → top-level DRAM ports
  │
  ├── npu  (npu_inst)
  │     npu_valid/ready/instruction ← noc_router
  │     result_valid/index/data → top-level result ports
  │
  └── riscv_core  (cpu_core_inst)
        instr_addr → imem index
        instr_data ← imem[instr_addr[9:2]]
        instr_valid ← address guard
        mem_* ←→ duck_coherency_ctrl (cpu port)
        mem_stall ← duck_coherency_ctrl
        core_stall → cpu_ready inversion (MUL/DIV busy only)

cpu_ready signal (v4): Derived from core_stall (ALU MUL/DIV busy) only — not from mem_stall. The coherency controller applies mem_stall directly to the pipeline stages inside riscv_core, independently of cpu_ready / net_ready.

assign cpu_ready = !core_stall;

This decoupling is deliberate: a cache miss should stall the CPU pipeline but not stall the NoC router from accepting new NPU instructions from the network.

v5 update: in base_die_top.sv, cpu_ready is now driven by the CPU link's instr_load channel credit (ld_tx_ready) instead of !core_stall — a further decoupling in the same spirit, now covering MUL/DIV stalls too, not just cache misses. See chiplets.md and BUG-007 for the tradeoff this introduced.


Reset Behaviour

On rst_n=0: - imem_wr_ptr resets to 0 - duck_coherency_ctrl resets all tags, dirty bits, npu_present flags, and FSM state to 0 - riscv_core flushes all pipeline registers - All sub-modules reset independently via shared rst_n