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)
- Load and run a CPU program that stores known values to
0x1000/0x1004— these remain Modified (dirty) in L2. - After the CPU halts, drive an NPU DMA read to those addresses.
- The coherency controller must detect the dirty L2 line, flush it to DRAM (MOESI M→S), then serve the NPU from DRAM.
- 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 |
FAIL — BUG-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) | FAIL — BUG-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)."
Related
- duck_coherency_ctrl.md — the FSM under test, with the same scenario walked through state-by-state
- BUG-003, BUG-006, UNSOLVED-001
- verification.md — current pass/fail table across all suites