mirror of
https://github.com/SikongJueluo/Mini-Nav.git
synced 2026-07-13 04:25:32 +08:00
- 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
213 lines
8.2 KiB
Python
213 lines
8.2 KiB
Python
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"
|