rtl/duck_coherency_ctrl.sv — MOESI Coherency Controller + L2
Replaces l2_cache.sv in the current design. Adds a 2-master simplified MOESI coherency layer on top of the same 4-way write-back L2 geometry: Master 0 is the CPU (same interface l2_cache exposed), Master 1 is the NPU DMA (read-only, one word at a time, weight-loading path).
Where it fits
Instantiated once by base_die_top.sv as coherency_inst. The CPU side is fed by base_die_top's mem_req/mem_rsp bridge FSM (standing in for riscv_core); the NPU DMA side is wired straight to system_top's top-level npu_dma_* ports.
Parameters (fixed, same as l2_cache.sv)
| Name | Value | Meaning |
|---|---|---|
SETS |
16 | Number of sets |
WAYS |
4 | Associativity |
LINE_WORDS |
8 | Words per cache line (32 bytes) |
Total capacity: 16 × 4 × 32 B = 2 KiB. Address decomposition: [31:9] tag (23b), [8:5] set (4b), [4:2] word (3b), [1:0] byte offset (unused at word granularity).
Ports
| Direction | Name | Width | Description |
|---|---|---|---|
| in | clk, rst_n |
1, 1 | Clock / reset |
| in | cpu_read/cpu_write |
1/1 | CPU load/store request |
| in | cpu_addr/cpu_wdata |
32/32 | CPU access address / write data |
| in | cpu_byte_en |
4 | CPU store byte enables |
| out | cpu_rdata |
32 | Combinational hit read data |
| out | cpu_stall |
1 | High while miss FSM busy or on a miss |
| in | npu_dma_read |
1 | NPU DMA read request |
| in | npu_dma_addr |
32 | NPU DMA target address |
| out | npu_dma_rdata |
32 | NPU DMA read data |
| out | npu_dma_ack |
1 | 1-cycle pulse: DMA data ready |
| out | npu_dma_stall |
1 | 1 whenever state != ST_IDLE — CPU has strict priority |
| out | dram_read/dram_write |
1/1 | DRAM transaction request |
| out | dram_addr/dram_wdata |
32/32 | DRAM address / write data |
| out | dram_byte_en |
4 | Always 4'hF |
| in | dram_rdata |
32 | DRAM read data |
| in | dram_ack |
1 | 1 cycle after request, per the DRAM model |
Functionality — coherency protocol (simplified MOESI, 2-master)
- CPU write to a line marked
npu_present: clearsnpu_present(Invalidate — the NPU's copy is now stale). - NPU DMA read from an address with a CPU-dirty (Modified) line: (1) flush the entire dirty cacheline to DRAM (M→S transition), (2) clear the dirty flag (CPU line becomes Shared/Clean), (3) read the target word from DRAM for the NPU, (4) set
npu_presentfor that line (NPU enters Shared state). - NPU DMA read from a clean/absent line: read directly from DRAM, no flush needed.
- DRAM arbitration: CPU miss path has strict priority — NPU DMA is stalled (
npu_dma_stall=1) while any CPU miss FSM state is active.
FSM states
| State | Path | Purpose |
|---|---|---|
ST_IDLE (0) |
shared | Arbitrate CPU vs NPU; serve hits |
ST_WB_START/ST_WB_WAIT (1/2) |
CPU miss | Dirty victim writeback, 8 words |
ST_FILL (3) |
CPU miss | Burst fill from DRAM, 8 words |
ST_DONE (4) |
CPU miss | 1-cycle gap before re-serving the CPU |
ST_NPU_FLUSH/ST_NPU_FLWAIT (5/6) |
NPU DMA | Flush CPU-dirty line to DRAM (MOESI M→S) |
ST_NPU_DRAM (7) |
NPU DMA | Read target word from DRAM |
ST_NPU_DONE (8) |
NPU DMA | npu_dma_ack pulses this cycle |
ST_NPU_GAP (9) |
NPU DMA | 1-cycle gap after flush so dram_ack de-asserts before the DRAM read begins (avoids capturing a stale ack — see BUG-003) |
Example — NPU DMA read that must flush first
CPU: sw a1, 0(a0) # writes 42 to 0x1000 -> L2 line Modified (dirty=1)
jal x0,0 # halt
TB: npu_dma_addr=0x1000, npu_dma_read=1
ST_IDLE -> npu_cpu_dirty=1 (tag hit, dirty=1) -> ST_NPU_FLUSH
ST_NPU_FLUSH/FLWAIT # 8 words written to DRAM, dirty cleared
ST_NPU_GAP # let dram_ack settle
ST_NPU_DRAM # read word 0x1000 back from DRAM -> 42
ST_NPU_DONE # npu_dma_ack pulses, npu_dma_rdata=42
This exact scenario is tb_moesi_integration.py's test_moesi_cpu_write_npu_read — see tb_moesi_integration.md. It is currently failing (BUG-006 / UNSOLVED-001) despite the ST_NPU_GAP fix being present in this file.
Related
- l2_cache.md — the non-coherent predecessor with identical cache geometry
- duck_cache_pkg.md — MOESI state enum reference (docs-only, not imported)
- BUG-003, BUG-006, UNSOLVED-001