mirror of
https://github.com/SikongJueluo/Mini-Nav.git
synced 2026-07-12 20:15:31 +08:00
Split the monolithic test_cam_basic.py into separate test suites organized by noise configuration (no_noise, write_noise, read_noise), with shared utilities extracted to tests/top/utils.py. - Remove test_cam_basic.py; add no_noise/, write_noise/, read_noise/ test suites with Makefiles that set noise parameters statically - Extract helpers (reset_dut, write_rows, query_once, collect_topk, etc.) into tests/top/utils.py - Update hw/sim/Makefile with per-config test targets and a test-top-all meta-target - Update scripts/run_cam_correctness.py to build per-directory instead of centrally, removing inline parameter overrides - Add __init__.py for result_serializer and topk_tracker module tests - Expand docstrings in test_ref_model_noise.py with architectural context and test rationale
349 lines
13 KiB
Python
349 lines
13 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
CAM 顶层(cam_top)no_noise 配置集成测试(WRITE_NOISE_EN=0, READ_NOISE_EN=0)。
|
||
|
||
所有噪声模块禁用,验证 CAM 在无噪声下的标准行为。
|
||
|
||
=== 测试清单 ===
|
||
|
||
- compile_includes_grouped_noise_helper — 编译冒烟
|
||
- baseline_no_noise — 基线检索正确性
|
||
- known_hamming_distance — 汉明距离验证
|
||
- tie_break_policy — 平局决胜
|
||
- all_zero_all_one_boundary — 全 0 / 全 1 边界
|
||
- half_duplex_write_priority — 半双工写入优先
|
||
- banked_pipeline_no_noise_top1 — 分块流水线 Top-1
|
||
- query_scan_blocks_writes_until_result_consumed — 查询阻塞写入
|
||
|
||
=== 配置背景 ===
|
||
|
||
本目录固定使用 WRITE_NOISE_EN=0 和 READ_NOISE_EN=0 编译,
|
||
因此所有测试无需运行时参数门控——Makefile 保证配置正确。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import cocotb
|
||
import numpy as np
|
||
from cocotb.clock import Clock
|
||
from cocotb.triggers import RisingEdge
|
||
from model.ref_model import (
|
||
match_top1,
|
||
random_hashes,
|
||
)
|
||
|
||
from tests.top.utils import (
|
||
collect_topk,
|
||
dut_hash_bits,
|
||
dut_num_rows,
|
||
query_once,
|
||
query_topk_once,
|
||
reset_dut,
|
||
wait_idle,
|
||
write_row,
|
||
write_rows,
|
||
)
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# 编译冒烟测试 — 验证 cam_top 能正确 elaborate
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
|
||
@cocotb.test()
|
||
async def compile_includes_grouped_noise_helper(dut):
|
||
"""编译测试:验证群组噪声辅助模块被正确包含在 cam_top 中。
|
||
|
||
不验证功能,只确保 Verilator elaboration 不会报错。
|
||
"""
|
||
cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start())
|
||
await reset_dut(dut)
|
||
assert int(dut.wr_ready.value) in (0, 1)
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# 测试 A:基线(WRITE_NOISE_EN=0, READ_NOISE_EN=0)
|
||
# ── 验证写+查在噪声关闭时与旧 CAM 行为完全一致
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
|
||
@cocotb.test()
|
||
async def baseline_no_noise(dut):
|
||
"""基线测试:噪声全部关闭时,检索结果必须与 Python 参考模型完全一致。
|
||
|
||
验证内容:
|
||
1. Top-1 索引和分数与 match_top1 模型一致
|
||
2. 查询自身所在行 → 分数必须等于 HASH_BITS(完全匹配)
|
||
3. 串行 Top-K 流的第一个 beat rank=0
|
||
4. 最后一个 beat 的 result_last=1(流终止)
|
||
5. top1_index/top1_score 别名与第一个 beat 的值一致
|
||
6. score_debug(如果存在)与模型分数数组逐元素一致
|
||
"""
|
||
cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start())
|
||
await reset_dut(dut)
|
||
|
||
num_rows = dut_num_rows(dut)
|
||
hash_bits = dut_hash_bits(dut)
|
||
rng = np.random.default_rng(1)
|
||
rows = random_hashes(rng, num_rows, width=hash_bits)
|
||
query_index = min(123, num_rows - 1)
|
||
query = rows[query_index]
|
||
|
||
await write_rows(dut, rows)
|
||
beats, top1_index, top1_score, score_debug = await query_topk_once(dut, query)
|
||
|
||
expected = match_top1(query, rows, width=hash_bits)
|
||
|
||
assert top1_index == expected.top1_index
|
||
assert top1_score == expected.top1_score
|
||
assert top1_index == query_index
|
||
assert top1_score == hash_bits
|
||
|
||
# Serial Top-K stream verification: beats from the single query above
|
||
assert beats[0][0] == 0, "First beat must have rank 0"
|
||
assert beats[-1][3] == 1, "Last beat must assert result_last"
|
||
# Verify top1 aliases match first beat after stream fully consumed
|
||
assert int(dut.top1_index.value) == beats[0][1]
|
||
assert int(dut.top1_score.value) == beats[0][2]
|
||
# Verify returned top1 matches first beat rank0
|
||
assert top1_index == beats[0][1]
|
||
assert top1_score == beats[0][2]
|
||
|
||
if score_debug is not None:
|
||
assert np.array_equal(score_debug, expected.scores)
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# 遗留测试 — 仅在噪声关闭时有意义(精确分数才有效)
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
|
||
@cocotb.test()
|
||
async def known_hamming_distance(dut):
|
||
"""汉明距离验证:写入已知模式的哈希,验证分数计算正确。
|
||
|
||
测试数据:
|
||
- Row 0: 全 0
|
||
- Row 10: 低 7 位为 1 → score = hash_bits - 7
|
||
- Row 11: 低 31 位为 1 → score = hash_bits - 31
|
||
- Row 12: 低 128 位为 1→ score = hash_bits - 128
|
||
- query = 0
|
||
|
||
验证:Top-1 为 row 0(完全匹配),各行的 score_debug
|
||
精确等于理论汉明距离对应的匹配位数。
|
||
"""
|
||
cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start())
|
||
await reset_dut(dut)
|
||
|
||
num_rows = dut_num_rows(dut)
|
||
hash_bits = dut_hash_bits(dut)
|
||
|
||
query = 0
|
||
rows = [0] * num_rows
|
||
rows[min(10, num_rows - 1)] = (1 << 7) - 1
|
||
rows[min(11, num_rows - 1)] = (1 << 31) - 1
|
||
rows[min(12, num_rows - 1)] = (1 << 128) - 1
|
||
|
||
await write_rows(dut, rows)
|
||
top1_index, top1_score, score_debug = await query_once(dut, query)
|
||
|
||
assert top1_index == 0
|
||
assert top1_score == hash_bits
|
||
|
||
if score_debug is not None:
|
||
assert int(score_debug[min(10, num_rows - 1)]) == hash_bits - 7
|
||
assert int(score_debug[min(11, num_rows - 1)]) == hash_bits - 31
|
||
assert int(score_debug[min(12, num_rows - 1)]) == hash_bits - 128
|
||
|
||
|
||
@cocotb.test()
|
||
async def tie_break_policy(dut):
|
||
"""平局决胜策略:分数相同时,行号最小的获胜。
|
||
|
||
设置:
|
||
- row 10, 20, 200 都存储了 query 的值(满分匹配)
|
||
- 其余行随机填充
|
||
|
||
预期:top1_index = 10(不是 20 或 200)
|
||
"""
|
||
cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start())
|
||
await reset_dut(dut)
|
||
|
||
num_rows = dut_num_rows(dut)
|
||
hash_bits = dut_hash_bits(dut)
|
||
rng = np.random.default_rng(2)
|
||
rows = random_hashes(rng, num_rows, width=hash_bits)
|
||
query = rows[min(200, num_rows - 1)]
|
||
rows[10] = query
|
||
rows[20] = query
|
||
rows[min(200, num_rows - 1)] = query
|
||
|
||
await write_rows(dut, rows)
|
||
top1_index, top1_score, _ = await query_once(dut, query)
|
||
|
||
assert top1_index == 10
|
||
assert top1_score == hash_bits
|
||
|
||
|
||
@cocotb.test()
|
||
async def all_zero_all_one_boundary(dut):
|
||
"""全 0 / 全 1 边界测试:验证极端哈希值的检索正确性。
|
||
|
||
存储:
|
||
- row 0: 全 0 (0x000...000)
|
||
- row 1: 全 1 (0xFFF...FFF)
|
||
- 查询 = 全 0
|
||
|
||
预期:Top-1 = row 0, score = hash_bits
|
||
row 1 的 score = 0(全 0 与全 1 无任何匹配位)
|
||
"""
|
||
cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start())
|
||
await reset_dut(dut)
|
||
|
||
num_rows = dut_num_rows(dut)
|
||
hash_bits = dut_hash_bits(dut)
|
||
|
||
rows = [0] * num_rows
|
||
rows[0] = 0
|
||
rows[1] = (1 << hash_bits) - 1
|
||
|
||
query = 0
|
||
await write_rows(dut, rows)
|
||
top1_index, top1_score, score_debug = await query_once(dut, query)
|
||
|
||
assert top1_score == hash_bits
|
||
assert top1_index == 0
|
||
|
||
if score_debug is not None:
|
||
assert int(score_debug[0]) == hash_bits
|
||
assert int(score_debug[1]) == 0
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# 测试 F:半双工写入优先级仲裁
|
||
# ── wr_valid 和 query_valid 同时有效 → 写入优先,查询被暂缓
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
|
||
@cocotb.test()
|
||
async def half_duplex_write_priority(dut):
|
||
"""半双工仲裁:同时发起写入和查询 → 写入必须胜出。
|
||
|
||
流程:
|
||
1. 预先写入 row 0(值为 test_val)
|
||
2. 同时驱动 wr_valid 和 query_valid,写入 row 1,查询 test_val
|
||
3. 断言:写入被接受,row 1 被正确写入
|
||
4. 随后查询 test_val → 应返回 row 0 和 row 1 都是满分
|
||
|
||
如果仲裁逻辑错误(查询胜出或同时处理),两个行中至少有一个会丢失。
|
||
"""
|
||
cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start())
|
||
await reset_dut(dut)
|
||
|
||
hash_bits = dut_hash_bits(dut)
|
||
test_val = (1 << hash_bits) - 1
|
||
await write_row(dut, 0, test_val)
|
||
|
||
await wait_idle(dut)
|
||
assert int(dut.wr_ready.value) == 1
|
||
assert int(dut.query_ready.value) == 1
|
||
|
||
dut.wr_valid.value = 1
|
||
dut.wr_addr.value = 1
|
||
dut.write_hash.value = 0
|
||
dut.query_valid.value = 1
|
||
dut.query_hash.value = test_val
|
||
|
||
await RisingEdge(dut.clk)
|
||
|
||
dut.wr_valid.value = 0
|
||
dut.query_valid.value = 0
|
||
|
||
await wait_idle(dut)
|
||
|
||
top1_index, top1_score, _ = await query_once(dut, test_val)
|
||
assert top1_index == 0
|
||
assert top1_score == hash_bits
|
||
|
||
top1_index, top1_score, _ = await query_once(dut, 0)
|
||
assert top1_index == 1
|
||
assert top1_score == hash_bits
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# 测试 G:分块流水线无噪声 Top-1
|
||
# ── 验证分块存储架构在无噪声时返回正确的 Top-1
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
|
||
@cocotb.test()
|
||
async def banked_pipeline_no_noise_top1(dut):
|
||
"""分块流水线 Top-1:无噪声时,分块架构的结果必须与纯模型一致。
|
||
|
||
这是 banked_pipeline 的冒烟测试——验证分块存储核心、
|
||
通道合并逻辑、以及匹配引擎流水线在端到端场景中协同工作。
|
||
"""
|
||
cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start())
|
||
await reset_dut(dut)
|
||
|
||
num_rows = dut_num_rows(dut)
|
||
hash_bits = dut_hash_bits(dut)
|
||
rng = np.random.default_rng(7)
|
||
rows = random_hashes(rng, num_rows, width=hash_bits)
|
||
query_index = min(17, num_rows - 1)
|
||
query = rows[query_index]
|
||
|
||
await write_rows(dut, rows)
|
||
top1_index, top1_score, score_debug = await query_once(dut, query)
|
||
expected = match_top1(query, rows, width=hash_bits)
|
||
|
||
assert top1_index == expected.top1_index
|
||
assert top1_score == expected.top1_score
|
||
assert top1_index == query_index
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# 测试 H:查询扫描期间阻塞写入
|
||
# ── 活跃的查询扫描会撤销 wr_ready,直到结果被消费完毕
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
|
||
@cocotb.test()
|
||
async def query_scan_blocks_writes_until_result_consumed(dut):
|
||
"""半双工阻塞:查询扫描活跃期间,wr_ready 必须保持低电平。
|
||
|
||
流程:
|
||
1. 写入全部行
|
||
2. 发起查询(query_valid=1)
|
||
3. 在结果被消费之前,尝试写入 → 断言 wr_ready=0
|
||
4. 消费完整结果流 → DUT 回到空闲
|
||
|
||
这验证了半双工协议中「读期间禁止写」的约束。
|
||
"""
|
||
cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start())
|
||
await reset_dut(dut)
|
||
|
||
num_rows = dut_num_rows(dut)
|
||
hash_bits = dut_hash_bits(dut)
|
||
rows = [0] * num_rows
|
||
rows[0] = (1 << hash_bits) - 1
|
||
await write_rows(dut, rows)
|
||
|
||
await wait_idle(dut)
|
||
dut.query_hash.value = rows[0]
|
||
dut.query_valid.value = 1
|
||
await RisingEdge(dut.clk)
|
||
dut.query_valid.value = 0
|
||
|
||
dut.wr_valid.value = 1
|
||
dut.wr_addr.value = 1
|
||
dut.write_hash.value = 0
|
||
await RisingEdge(dut.clk)
|
||
assert int(dut.wr_ready.value) == 0
|
||
dut.wr_valid.value = 0
|
||
|
||
# Consume full serial stream so the DUT returns idle
|
||
beats = await collect_topk(dut, timeout_cycles=2000)
|
||
assert len(beats) > 0
|
||
assert beats[-1][3] == 1 # last asserted
|