mirror of
https://github.com/SikongJueluo/Mini-Nav.git
synced 2026-07-12 20:15:31 +08:00
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:
133
hw/sim/tests/modules/topk_tracker/test_topk_tracker.py
Normal file
133
hw/sim/tests/modules/topk_tracker/test_topk_tracker.py
Normal 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}"
|
||||
Reference in New Issue
Block a user