feat(hw/rtl): implement full Top-K CAM search pipeline with serial result output

- add TOPK_K, FIFO_DEPTH, RESULT_SERIAL parameters to cam_params
- add candidate_fifo: synchronous ready/valid FIFO for (row, score) candidates
- add topk_tracker: tracks top-K candidates with clear/ready handshake
- add result_serializer: serializes packed Top-K array into rank-ordered stream
- refactor match_engine_pipeline from 4-state to 8-state Top-K pipeline
- extend cam_top with serial Top-K interface (result_rank/row/score/last)
- add backward-compatible top1_index/top1_score aliases from rank-0 beat
- add comprehensive tests for all new modules
This commit is contained in:
2026-05-19 18:19:05 +08:00
parent 8bcad1f23f
commit e4cbb5e30d
21 changed files with 2152 additions and 124 deletions

View File

@@ -0,0 +1,9 @@
SIM_ROOT := $(abspath ../../..)
RTL_ROOT := $(abspath $(SIM_ROOT)/../rtl)
include $(SIM_ROOT)/mk/rtl-sources.mk
TOPLEVEL := topk_tracker
COCOTB_TEST_MODULES := tests.modules.topk_tracker.test_topk_tracker
VERILOG_SOURCES := $(RTL_ROOT)/core/topk_tracker.sv
include $(SIM_ROOT)/mk/cocotb-common.mk

View File

@@ -0,0 +1,133 @@
from __future__ import annotations
import cocotb
from cocotb.clock import Clock
from cocotb.triggers import RisingEdge
# ── Parameter defaults (matching cam_params.svh with NUM_ROWS=4096, HASH_BITS=512) ──
K = 4
ROW_BITS = 12
SCORE_BITS = 10 # $clog2(512 + 1)
ROW_SENTINEL = (1 << ROW_BITS) - 1 # all-ones
def golden(candidates, k=K):
"""Return top-k candidates sorted by score desc, then row asc (higher score wins; lower row breaks ties)."""
return sorted(candidates, key=lambda item: (-item[1], item[0]))[:k]
def unpack(dut, k=K):
"""Read packed topk_rows_o and topk_scores_o; return list of (row, score) tuples ordered by rank."""
rows_packed = int(dut.topk_rows_o.value)
scores_packed = int(dut.topk_scores_o.value)
result = []
for i in range(k):
row = (rows_packed >> (i * ROW_BITS)) & ((1 << ROW_BITS) - 1)
score = (scores_packed >> (i * SCORE_BITS)) & ((1 << SCORE_BITS) - 1)
result.append((row, score))
return result
# ── Helpers ──────────────────────────────────────────────────────────────────────
async def reset_tracker(dut):
"""Assert reset for 5 cycles, release, then wait 2 cycles."""
dut.rst_n.value = 0
dut.clear_i.value = 0
dut.candidate_valid_i.value = 0
for _ in range(5):
await RisingEdge(dut.clk)
dut.rst_n.value = 1
for _ in range(2):
await RisingEdge(dut.clk)
async def push_candidate(dut, row: int, score: int):
"""Drive one candidate handshake (candidate_ready_o is always 1)."""
dut.candidate_valid_i.value = 1
dut.candidate_row_i.value = row
dut.candidate_score_i.value = score
await RisingEdge(dut.clk)
dut.candidate_valid_i.value = 0
# ── Test 1: Basic ordering ───────────────────────────────────────────────────────
@cocotb.test()
async def tracker_orders_by_score_then_lower_row(dut):
"""
Push candidates [(5,10), (3,11), (2,11), (9,7), (1,11), (0,3)] and verify
the tracker's Top-4 matches golden: [(1,11), (2,11), (3,11), (5,10)].
"""
cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start())
await reset_tracker(dut)
candidates = [(5, 10), (3, 11), (2, 11), (9, 7), (1, 11), (0, 3)]
for row, score in candidates:
await push_candidate(dut, row, score)
result = unpack(dut)
expected = golden(candidates)
assert result == expected, f"Expected {expected}, got {result}"
assert dut.update_pending_o.value == 0, "update_pending_o should be 0"
# ── Test 2: Clear restarts query ─────────────────────────────────────────────────
@cocotb.test()
async def tracker_clear_starts_new_query(dut):
"""
After candidate (7,60), pulse clear_i, send candidate (4,12).
Expect rank 0 = (4,12), remaining slots = sentinel rows, score 0.
"""
cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start())
await reset_tracker(dut)
# Push (7, 60)
await push_candidate(dut, 7, 60)
# Pulse clear
dut.clear_i.value = 1
await RisingEdge(dut.clk)
dut.clear_i.value = 0
# Push (4, 12)
await push_candidate(dut, 4, 12)
result = unpack(dut)
expected = [(4, 12)] + [(ROW_SENTINEL, 0)] * (K - 1)
assert result == expected, f"Expected {expected}, got {result}"
# ── Test 3: Random validation against golden ─────────────────────────────────────
@cocotb.test()
async def tracker_matches_random_python_golden(dut):
"""
Push 100 candidates (row in [0,63], score in [0,127]) seeded at 42.
Verify final Top-4 matches Python golden model.
"""
import random
random.seed(42)
cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start())
await reset_tracker(dut)
max_row = 64
max_score = 128
candidates = []
for _ in range(100):
row = random.randint(0, max_row - 1)
score = random.randint(0, max_score - 1)
candidates.append((row, score))
await push_candidate(dut, row, score)
result = unpack(dut)
expected = golden(candidates)
assert result == expected, f"Expected {expected}, got {result}"