Skip to content

tb/tb_moesi_integration.py — MOESI Coherency Integration Testbench

Tests duck_coherency_ctrl.sv's 2-master MOESI protocol end-to-end: a CPU program stores known values (dirty in L2), then the testbench drives NPU DMA reads to those same addresses and verifies the controller flushes the dirty line to DRAM before serving the NPU — proving the NPU never observes stale data.

What it drives

system_top's top-level ports, same as tb_server_dispatch_unit.py, plus the NPU DMA port specifically: npu_dma_read, npu_dma_addr (in), npu_dma_rdata, npu_dma_ack, npu_dma_stall (out).

Test scenario (shared across all three tests)

  1. Load and run a CPU program that stores known values to 0x1000/0x1004 — these remain Modified (dirty) in L2.
  2. After the CPU halts, drive an NPU DMA read to those addresses.
  3. The coherency controller must detect the dirty L2 line, flush it to DRAM (MOESI M→S), then serve the NPU from DRAM.
  4. Verify the NPU receives the CPU-written values, not stale DRAM zeros.

Test list

# Test Exercises Current status
TC-MOESI-1 test_moesi_cpu_write_npu_read CPU writes 42/99 to 0x1000/0x1004, NPU DMA reads both back FAILBUG-006
TC-MOESI-2 test_moesi_npu_invalidate_on_cpu_write NPU reads a line, CPU overwrites it, second NPU read must see the new value (invalidation path) FAILBUG-006
TC-MOESI-3 test_moesi_clean_npu_dma NPU reads an address the CPU never touched — fast path, no flush needed, DRAM zero expected PASS

TC-MOESI-1/2 are roadblocked pending human investigation — see UNSOLVED-001. The ST_NPU_GAP fix from BUG-003 is present in duck_coherency_ctrl.sv, but these tests regressed with the same symptom (NPU DMA reads back 0 instead of the CPU-written value) for reasons not yet root-caused.

Key helpers (local to this file — does not import from tb_server_dispatch_unit.py)

Component Purpose
generate_clock, dram_model, reset_dut Same shape as the main suite's, defined locally
load_asm_file(path) Parses <asm> \| <hex_encoding> \| <dest> \| <reg_check> format from moesi_weight_test.asm
load_cpu_program(dut, instructions) Feeds only the dest=="cpu" instructions through the NoC router
wait_for_halt(dut, timeout_cycles=2000) Polls dut.halt, drains HALT_DRAIN_CYCLES=20 extra cycles
npu_dma_read_word(dut, addr, timeout_cycles=500) Drives one NPU DMA read to completion, returns (data, cycles_taken)

How to run

docker run --rm -e MODULE=tb_moesi_integration -e ASM_TEST=moesi_weight_test.asm \
    -v "$(pwd)":/usr/src/server_dispatch riscv_sim:latest

Example — the NPU DMA read helper

async def npu_dma_read_word(dut, addr, timeout_cycles=500):
    dut.npu_dma_addr.value = addr
    dut.npu_dma_read.value = 1
    cycles = 0
    while True:
        await RisingEdge(dut.clk)
        cycles += 1
        if int(dut.npu_dma_ack.value):
            data = int(dut.npu_dma_rdata.value)
            dut.npu_dma_read.value = 0
            await RisingEdge(dut.clk)
            return data, cycles
        if cycles >= timeout_cycles:
            raise TimeoutError(f"NPU DMA read 0x{addr:08X} timed out")

test_moesi_cpu_write_npu_read then asserts word0 == 42 — when it fails, the assertion message spells out exactly what that means: "duck_coherency_ctrl did NOT flush the dirty L2 line before serving the NPU (MOESI M→S flush missing)."