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:
29
hw/sim/tests/modules/result_serializer/Makefile
Normal file
29
hw/sim/tests/modules/result_serializer/Makefile
Normal file
@@ -0,0 +1,29 @@
|
||||
SIM_ROOT := $(abspath ../../..)
|
||||
RTL_ROOT := $(abspath $(SIM_ROOT)/../rtl)
|
||||
include $(SIM_ROOT)/mk/rtl-sources.mk
|
||||
|
||||
TOPLEVEL := result_serializer
|
||||
COCOTB_TEST_MODULES := tests.modules.result_serializer.test_result_serializer
|
||||
VERILOG_SOURCES := $(RTL_ROOT)/core/result_serializer.sv
|
||||
|
||||
# Allow override from command line (e.g. make TOPK_K=1)
|
||||
TOPK_K ?= 4
|
||||
EXTRA_ARGS += +define+TOPK_K=$(TOPK_K)
|
||||
export TOPK_K
|
||||
|
||||
# Parameter-specific build directory so K variants never collide.
|
||||
SIM_BUILD := sim_build/k_$(TOPK_K)
|
||||
|
||||
include $(SIM_ROOT)/mk/cocotb-common.mk
|
||||
|
||||
# ── K-specific test targets ──────────────────────────────────────────────
|
||||
# Recompile with K=1 and filter to the K=1 test.
|
||||
.PHONY: test-k1
|
||||
|
||||
test-k1:
|
||||
$(MAKE) -B -f Makefile results.xml TOPK_K=1 \
|
||||
COCOTB_TEST_FILTER=serializer_handles_k1
|
||||
|
||||
# Also remove variant build directories on clean.
|
||||
clean::
|
||||
rm -rf sim_build/k_1
|
||||
212
hw/sim/tests/modules/result_serializer/test_result_serializer.py
Normal file
212
hw/sim/tests/modules/result_serializer/test_result_serializer.py
Normal file
@@ -0,0 +1,212 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
import cocotb
|
||||
from cocotb.clock import Clock
|
||||
from cocotb.triggers import RisingEdge
|
||||
|
||||
# ── Parameter defaults (matching cam_params.svh: NUM_ROWS=4096, HASH_BITS=512) ──
|
||||
# K is read from env so Makefile override (e.g. make TOPK_K=1) propagates to tests.
|
||||
K = int(os.environ.get('TOPK_K', '4'))
|
||||
ROW_BITS = 12
|
||||
SCORE_BITS = 10 # $clog2(512 + 1)
|
||||
# Mirror the RTL's RANK_BITS formula: (K <= 1) ? 1 : $clog2(K)
|
||||
RANK_BITS = 1 if K <= 1 else (K - 1).bit_length()
|
||||
|
||||
|
||||
def pack(values: list[int], width: int) -> int:
|
||||
"""Pack a list of values into a flattened integer (LSB = index 0)."""
|
||||
result = 0
|
||||
for idx, value in enumerate(values):
|
||||
result |= value << (idx * width)
|
||||
return result
|
||||
|
||||
|
||||
# ── Helpers ────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
async def reset_serializer(dut):
|
||||
"""Drive reset active-low for 5 cycles, then release; zero all inputs."""
|
||||
dut.rst_n.value = 0
|
||||
dut.start_i.value = 0
|
||||
dut.result_ready_i.value = 0
|
||||
dut.topk_rows_i.value = 0
|
||||
dut.topk_scores_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 assert_output_stable(dut, rank: int, row: int, score: int,
|
||||
last: bool, valid: bool):
|
||||
"""Assert output signals match expected values."""
|
||||
assert int(dut.result_rank_o.value) == rank, \
|
||||
f"rank: expected {rank}, got {int(dut.result_rank_o.value)}"
|
||||
assert int(dut.result_row_o.value) == row, \
|
||||
f"row: expected {row}, got {int(dut.result_row_o.value)}"
|
||||
assert int(dut.result_score_o.value) == score, \
|
||||
f"score: expected {score}, got {int(dut.result_score_o.value)}"
|
||||
assert bool(dut.result_last_o.value) == last, \
|
||||
f"last: expected {last}, got {bool(dut.result_last_o.value)}"
|
||||
assert bool(dut.result_valid_o.value) == valid, \
|
||||
f"valid: expected {valid}, got {bool(dut.result_valid_o.value)}"
|
||||
|
||||
|
||||
# ── Test 1: Serializer outputs rank order and last ─────────────────────────────
|
||||
|
||||
|
||||
@cocotb.test()
|
||||
async def serializer_outputs_rank_order_and_last(dut):
|
||||
"""
|
||||
Start serialization with K=4 results. Clock through all 4 ranks, verifying
|
||||
row/score/last at each step. Assert done_o pulses after last rank.
|
||||
|
||||
Gated: requires TOPK_K=4 (the default). Use ``make test-k1`` for K=1.
|
||||
"""
|
||||
if K != 4:
|
||||
cocotb.log.info("Skipping (designed for TOPK_K=4, got %d)", K)
|
||||
return
|
||||
|
||||
cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start())
|
||||
await reset_serializer(dut)
|
||||
|
||||
# Pre-defined top-4 results
|
||||
rows = [42, 17, 99, 5]
|
||||
scores = [10, 9, 8, 7]
|
||||
|
||||
# Drive packed inputs and assert start
|
||||
dut.topk_rows_i.value = pack(rows, ROW_BITS)
|
||||
dut.topk_scores_i.value = pack(scores, SCORE_BITS)
|
||||
dut.start_i.value = 1
|
||||
await RisingEdge(dut.clk)
|
||||
dut.start_i.value = 0
|
||||
|
||||
# DUT should be busy, valid, showing rank 0
|
||||
assert bool(dut.busy_o.value), "busy_o should be high after start"
|
||||
await assert_output_stable(dut, rank=0, row=rows[0], score=scores[0],
|
||||
last=False, valid=True)
|
||||
|
||||
# Cycle through remaining ranks with ready held high
|
||||
for r in range(1, K):
|
||||
dut.result_ready_i.value = 1
|
||||
await RisingEdge(dut.clk)
|
||||
is_last = (r == K - 1)
|
||||
await assert_output_stable(dut, rank=r, row=rows[r], score=scores[r],
|
||||
last=is_last, valid=True)
|
||||
|
||||
# After last rank fire, next cycle should show done_o and deassert busy
|
||||
await RisingEdge(dut.clk)
|
||||
assert bool(dut.done_o.value), "done_o should be high after last rank fire"
|
||||
assert not bool(dut.busy_o.value), "busy_o should be low after serialization done"
|
||||
assert not bool(dut.result_valid_o.value), \
|
||||
"result_valid_o should be low after serialization done"
|
||||
|
||||
# done_o should be single-cycle pulse
|
||||
await RisingEdge(dut.clk)
|
||||
assert not bool(dut.done_o.value), "done_o should be single-cycle pulse"
|
||||
assert int(dut.result_rank_o.value) == 0, \
|
||||
"rank should reset to 0 after done"
|
||||
|
||||
|
||||
# ── Test 2: Holds output when not ready ────────────────────────────────────────
|
||||
|
||||
|
||||
@cocotb.test()
|
||||
async def serializer_holds_output_when_not_ready(dut):
|
||||
"""
|
||||
After start, with result_ready_i=0, output remains stable.
|
||||
After ready is asserted, advances to next rank.
|
||||
|
||||
Gated: requires TOPK_K=4 (the default). Use ``make test-k1`` for K=1.
|
||||
"""
|
||||
if K != 4:
|
||||
cocotb.log.info("Skipping (designed for TOPK_K=4, got %d)", K)
|
||||
return
|
||||
|
||||
cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start())
|
||||
await reset_serializer(dut)
|
||||
|
||||
rows = [10, 20, 30, 40]
|
||||
scores = [5, 4, 3, 2]
|
||||
|
||||
dut.topk_rows_i.value = pack(rows, ROW_BITS)
|
||||
dut.topk_scores_i.value = pack(scores, SCORE_BITS)
|
||||
dut.result_ready_i.value = 0 # not ready
|
||||
dut.start_i.value = 1
|
||||
await RisingEdge(dut.clk)
|
||||
dut.start_i.value = 0
|
||||
|
||||
# Output should present rank 0 but hold because ready is low
|
||||
await assert_output_stable(dut, rank=0, row=rows[0], score=scores[0],
|
||||
last=False, valid=True)
|
||||
|
||||
# Stall for 3 cycles — mutating inputs to verify they don't leak through
|
||||
# (snapshot must hold original values captured at start)
|
||||
for cycle in range(3):
|
||||
# Corrupt the live inputs — a buggy combinational slice would change
|
||||
dut.topk_rows_i.value = pack([cycle + 100, 0, 0, 0], ROW_BITS)
|
||||
dut.topk_scores_i.value = pack([cycle + 50, 0, 0, 0], SCORE_BITS)
|
||||
await RisingEdge(dut.clk)
|
||||
await assert_output_stable(dut, rank=0, row=rows[0], score=scores[0],
|
||||
last=False, valid=True)
|
||||
|
||||
# Restore original inputs and assert ready for 1 cycle; rank advances to 1
|
||||
dut.topk_rows_i.value = pack(rows, ROW_BITS)
|
||||
dut.topk_scores_i.value = pack(scores, SCORE_BITS)
|
||||
dut.result_ready_i.value = 1
|
||||
await RisingEdge(dut.clk)
|
||||
await assert_output_stable(dut, rank=1, row=rows[1], score=scores[1],
|
||||
last=False, valid=True)
|
||||
|
||||
# Stall again at rank 1 for 2 cycles — mutate inputs again
|
||||
dut.result_ready_i.value = 0
|
||||
for cycle in range(2):
|
||||
dut.topk_rows_i.value = pack([cycle + 200, 0, 0, 0], ROW_BITS)
|
||||
dut.topk_scores_i.value = pack([cycle + 100, 0, 0, 0], SCORE_BITS)
|
||||
await RisingEdge(dut.clk)
|
||||
await assert_output_stable(dut, rank=1, row=rows[1], score=scores[1],
|
||||
last=False, valid=True)
|
||||
|
||||
|
||||
# ── Test 3: K=1 edge case ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@cocotb.test()
|
||||
async def serializer_handles_k1(dut):
|
||||
"""
|
||||
When K=1 (overridden via generics), start and fire produce done_o and
|
||||
deassert busy on the next cycle. Run via ``make test-k1``.
|
||||
"""
|
||||
if K != 1:
|
||||
cocotb.log.info("Skipping (designed for TOPK_K=1, got %d)", K)
|
||||
return
|
||||
|
||||
cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start())
|
||||
await reset_serializer(dut)
|
||||
|
||||
rows = [100]
|
||||
scores = [50]
|
||||
|
||||
dut.topk_rows_i.value = pack(rows, ROW_BITS)
|
||||
dut.topk_scores_i.value = pack(scores, SCORE_BITS)
|
||||
dut.start_i.value = 1
|
||||
await RisingEdge(dut.clk)
|
||||
dut.start_i.value = 0
|
||||
|
||||
# K=1: rank 0 is also the last; valid asserted
|
||||
assert bool(dut.busy_o.value), "busy_o should be high after start"
|
||||
await assert_output_stable(dut, rank=0, row=rows[0], score=scores[0],
|
||||
last=True, valid=True)
|
||||
|
||||
# Fire the one and only result
|
||||
dut.result_ready_i.value = 1
|
||||
await RisingEdge(dut.clk)
|
||||
|
||||
# done_o should pulse, busy deasserted
|
||||
assert bool(dut.done_o.value), "done_o should pulse after K=1 fire"
|
||||
assert not bool(dut.busy_o.value), "busy_o should be low after done"
|
||||
assert not bool(dut.result_valid_o.value), "valid should be low after done"
|
||||
assert int(dut.result_rank_o.value) == 0, "rank should reset to 0"
|
||||
Reference in New Issue
Block a user