System Top — rtl/system_top.sv (v4, historical)
As of v5,
system_topno 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 ontobase_die_topandcpu_chiplet_toprespectively. The instantiation diagram andcpu_readyderivation 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