refactor(hw/sim): reorganize CAM top-level tests into per-noise-config suites

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
This commit is contained in:
2026-05-21 21:19:33 +08:00
parent 5a1d3ea977
commit 424cf6e1d3
16 changed files with 1197 additions and 712 deletions

View File

@@ -1,22 +1,31 @@
PYTHON ?= python
MODULE_TESTS := cam_core_banked candidate_fifo match_engine_pipeline cam_write_noise cam_read_noise popcount_pipeline topk_tracker result_serializer
TOP_CONFIGS := no_noise write_noise read_noise
.PHONY: help test-all test-top test-modules test-module test-model test-perf clean $(MODULE_TESTS:%=test-module-%)
.PHONY: help test-all test-top test-top-all test-modules test-module test-model test-perf clean $(MODULE_TESTS:%=test-module-%) $(TOP_CONFIGS:%=test-top-%)
help:
@echo "Available hw/sim targets:"
@echo " make test-model"
@echo " make test-top"
@echo " make test-top # 只运行默认顶层配置 (no_noise)"
@echo " make test-top-all # 运行所有顶层噪声配置"
@echo " make test-top-no_noise # 无噪声集成测试"
@echo " make test-top-write_noise # 写入噪声集成测试"
@echo " make test-top-read_noise # 读取噪声集成测试"
@echo " make test-module MODULE=cam_core_banked"
@echo " make test-modules"
@echo " make test-perf"
@echo " make test-all"
@echo " make clean"
test-all: test-model test-top test-modules
test-all: test-model test-top-all test-modules
test-top:
$(MAKE) -C tests/top
test-top: test-top-no_noise
test-top-all: $(TOP_CONFIGS:%=test-top-%)
$(TOP_CONFIGS:%=test-top-%):
$(MAKE) -C tests/top/$(@:test-top-%=%)
test-modules: $(MODULE_TESTS:%=test-module-%)
@@ -34,7 +43,9 @@ test-perf:
$(MAKE) -C tests/perf
clean:
$(MAKE) -C tests/top clean
@for config in $(TOP_CONFIGS); do \
$(MAKE) -C tests/top/$$config clean || exit $$?; \
done
@for module in $(MODULE_TESTS); do \
$(MAKE) -C tests/modules/$$module clean || exit $$?; \
done

View File

@@ -1,3 +1,17 @@
# -*- coding: utf-8 -*-
"""
参考模型ref_model的纯 Python 单元测试。
本文件不涉及任何 RTL / Verilator 仿真,仅验证 Python 参考模型的正确性。
所有 RTL-vs-模型 的对比测试(如顶层 test_cam_basic.py都依赖此参考模型
因此这里是整个测试体系的「基石」——参考模型如果有 bug所有对比测试都将失效。
测试覆盖:
1. 分组翻转掩码 — 完全速率 (rate=1/1) 的正确位翻转模式
2. 分组翻转掩码 — 零速率 (rate=0/100) 不应产生任何翻转
3. 评分函数语义 — 确认是「匹配位数」而非「汉明距离」
4. 读取噪声模型 — 相同输入 + 相同种子 = 可复现结果
"""
from __future__ import annotations
from model.ref_model import (
@@ -7,7 +21,26 @@ from model.ref_model import (
)
# ==============================================================================
# 测试 1完全速率下的分组翻转掩码生成
# ==============================================================================
def test_grouped_flip_mask_full_rate_one_bit_per_64_bit_group():
"""
验证 generate_grouped_flip_mask 在 rate_num=1, rate_den=1 时的行为。
背景:
- CAM 的 write noise 模块将 512-bit 哈希按 64-bit 分组,每组最多翻转 1 位。
- random_value 的位域含义(每 group 14 bits
bits [5:0] → sample未使用
bits [13:6] → bit_idx选择该组内翻转哪一位
本测试:
- 构造一个 random_value使每个 group 的 bit_idx = group+1
- 断言生成的 mask 恰好有 8 个位被置位(每 group 一个)
- 断言每个被翻转的位位置与预期一致
"""
random_value = 0
for group in range(8):
bit_idx = group + 1
@@ -17,38 +50,88 @@ def test_grouped_flip_mask_full_rate_one_bit_per_64_bit_group():
mask = generate_grouped_flip_mask(
random_value=random_value,
hash_bits=512,
noise_bits=8,
rate_num=1,
rate_den=1,
hash_bits=512, # 8 组 × 64 bits/组
noise_bits=8, # 每组的 bit_idx 位宽
rate_num=1, # 分子 = 1
rate_den=1, # 分母 = 1 → 100% 概率,每组都翻转
)
# 预期:每组的 bit_idx 位被翻转
expected = 0
for group in range(8):
expected |= 1 << (group * 64 + group + 1)
assert mask == expected
assert mask.bit_count() == 8
assert mask.bit_count() == 8 # 恰好 8 位被翻转(每组一位)
# ==============================================================================
# 测试 2零速率下不应产生任何翻转
# ==============================================================================
def test_grouped_flip_mask_zero_rate_no_flips():
"""
验证 rate_num=0 时,无论 random_value 为何值mask 都应为 0。
这是写入噪声的「零噪声」配置边界测试——
确保 RTL 参数 WRITE_NOISE_RATE_NUM=0 能真正关闭噪声注入。
"""
mask = generate_grouped_flip_mask(
random_value=(1 << 128) - 1,
random_value=(1 << 128) - 1, # 全 1 的 random_value
hash_bits=512,
noise_bits=8,
rate_num=0,
rate_num=0, # 分子 = 0 → 翻转概率为 0
rate_den=100,
)
assert mask == 0
assert mask == 0 # mask 必须全 0一个位都不翻
# ==============================================================================
# 测试 3评分函数语义 — 确认是「XNOR 匹配位数」而非「汉明距离」
# ==============================================================================
def test_score_is_bit_match_popcount_not_hamming_distance():
"""
关键语义验证xnor_popcount_score 计算的是匹配位的数量,不是汉明距离。
示例:
query = 0b1010
stored = 0b1000
XNOR = 0b1101 → popcount = 3有 3 个位匹配)
汉明距离 = 1 → 只有一个位不同
为什么这个区分很重要:
- 如果 RTL 或模型错误地使用了汉明距离作为分数,则:
完全匹配的分数会是 0 而非 hash_bits512
Top-K 排序会颠倒(分数低的反而排前面)
- 这会导致整个 CAM 检索系统返回错误结果
"""
query = 0b1010
stored = 0b1000
assert xnor_popcount_score(query, stored, width=4) == 3
# ==============================================================================
# 测试 4读取噪声模型的可复现性确定性种子
# ==============================================================================
def test_read_noise_model_is_reproducible_after_reset_seed():
"""
验证 match_top1_with_read_noise 在相同参数下产生相同结果。
为什么这个测试至关重要:
- RTL 中的 read noise PRNG 使用固定种子 (0x6A09E667F3BCC909)
- 参考模型必须使用相同的种子来复现 RTL 的噪声行为
- 如果两次调用结果不同,说明模型存在非确定性 bug
(如未重置 PRNG 状态、或使用了非确定性随机源)
测试数据:
- 8 行不同模式的 512-bit 哈希全0、全1、稀疏值
- 噪声配置rate=1%, lanes=8, noise_bits=8
"""
rows = [0, (1 << 512) - 1, 0x1234, 0x5678, 0x9ABC, 0xDEF0, 0x1357, 0x2468]
query = rows[2]
kwargs = dict(
@@ -65,6 +148,7 @@ def test_read_noise_model_is_reproducible_after_reset_seed():
first = match_top1_with_read_noise(**kwargs)
second = match_top1_with_read_noise(**kwargs)
# 两次调用的 Top-1 结果和分数数组必须完全一致
assert first.top1_index == second.top1_index
assert first.top1_score == second.top1_score
assert first.scores.tolist() == second.scores.tolist()

View File

@@ -0,0 +1 @@
# Result serializer module tests

View File

@@ -0,0 +1 @@
# Top-K tracker module tests

View File

@@ -1,9 +1,13 @@
SIM_ROOT := $(abspath ../..)
SIM_ROOT := $(abspath ../../..)
RTL_ROOT := $(abspath $(SIM_ROOT)/../rtl)
include $(SIM_ROOT)/mk/rtl-sources.mk
TOPLEVEL := cam_top
COCOTB_TEST_MODULES := tests.top.test_cam_basic
COCOTB_TEST_MODULES := tests.top.no_noise.test_no_noise
VERILOG_SOURCES := $(RTL_CAM_TOP)
# 禁用所有噪声模块
WRITE_NOISE_EN := 0
READ_NOISE_EN := 0
include $(SIM_ROOT)/mk/cocotb-common.mk

View File

@@ -0,0 +1 @@
# CAM top-level no-noise integration tests

View File

@@ -0,0 +1,348 @@
# -*- coding: utf-8 -*-
"""
CAM 顶层cam_topno_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

View File

@@ -0,0 +1,25 @@
SIM_ROOT := $(abspath ../../..)
RTL_ROOT := $(abspath $(SIM_ROOT)/../rtl)
include $(SIM_ROOT)/mk/rtl-sources.mk
TOPLEVEL := cam_top
COCOTB_TEST_MODULES := tests.top.read_noise.test_read_noise
VERILOG_SOURCES := $(RTL_CAM_TOP)
# 读取噪声开启,写入噪声默认关闭
READ_NOISE_EN := 1
READ_NOISE_RATE_NUM := 1
READ_NOISE_RATE_DEN := 100
WRITE_NOISE_EN := 0
include $(SIM_ROOT)/mk/cocotb-common.mk
# ── 写入+读取双重噪声子目标 ─────────────────────────────────────────
.PHONY: test-with-write-noise
test-with-write-noise:
$(MAKE) -B -f Makefile results.xml WRITE_NOISE_EN=1 \
COCOTB_TEST_FILTER=read_noise_model_match
clean::
rm -rf sim_build

View File

@@ -0,0 +1 @@
# CAM top-level read-noise integration tests

View File

@@ -0,0 +1,115 @@
# -*- coding: utf-8 -*-
"""
CAM 读取噪声read_noise集成测试。
本文件针对 READ_NOISE_EN=1 的编译配置,验证 RTL 的读取噪声行为
与 Python 参考模型ref_model一致。
=== 测试内容 ===
read_noise_model_match — 读取噪声模型匹配:
写入原始哈希,预测含写入噪声(如果 WRITE_NOISE_EN=1的存储值
再用 match_top1_with_read_noise 计算含读取噪声的期望结果,
与 RTL 实际 Top-1 进行比对。
=== 架构背景 ===
CAM 硬件由以下流水线组成:
Write Noise → Banked Core Storage → Read Noise → Match Engine Pipeline
Top-K Tracker → Result Serializer
本测试覆盖的是 Read Noise → Match Engine 段。
写入噪声WRITE_NOISE_EN通过 Makefile 的 test-with-write-noise 子目标
启用,测试代码内部已兼容两种配置。
"""
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 (
generate_write_flip_mask,
match_top1_with_read_noise,
random_hashes,
unpack_score_debug_flat,
)
from tests.top.utils import (
dut_hash_bits,
dut_lanes,
dut_num_rows,
get_param,
query_once,
reset_dut,
write_rows,
)
# ═══════════════════════════════════════════════════════════════════════════════
# 测试:读取噪声模型匹配
# ── READ_NOISE_EN=1 由 Makefile 保证,测试代码中不再重复门控
# ═══════════════════════════════════════════════════════════════════════════════
@cocotb.test()
async def read_noise_model_match(dut):
"""读取噪声模型匹配:验证 RTL 的读取噪声行为与 Python 参考模型一致。
与写入噪声不同,读取噪声发生在查询阶段(每次查询向哈希值注入噪声),
因此:
- 如果先有写入噪声,存储行已经被翻转过一次
- 然后查询时还会再注入一层读取噪声
- 两层噪声使用不同的种子(写: 0xB504..., 读: 0x6A09...
本测试:
1. 用 Python 模型预计算存储后的哈希(含写入噪声)
2. 用 match_top1_with_read_noise 预计算含读取噪声的期望结果
3. 写入原始值到 RTL查询比对结果
"""
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)
lanes = dut_lanes(dut)
rng = np.random.default_rng(123)
rows = random_hashes(rng, num_rows, width=hash_bits)
# If write noise is enabled, apply write flip masks to predict stored rows
stored_rows = list(rows)
if get_param(dut, "WRITE_NOISE_EN", 0):
seed = 0xB504_F32D_B504_F32D
prng_state = (seed << 64) | seed
stored_rows = []
for row in rows:
flip, prng_state = generate_write_flip_mask(
prng_state,
hash_bits,
get_param(dut, "WRITE_NOISE_BITS", 8),
get_param(dut, "WRITE_NOISE_RATE_NUM", 1),
get_param(dut, "WRITE_NOISE_RATE_DEN", 100),
)
stored_rows.append(row ^ flip)
query = rows[min(5, num_rows - 1)]
await write_rows(dut, rows)
top1_index, top1_score, score_debug = await query_once(dut, query)
expected = match_top1_with_read_noise(
query,
stored_rows,
width=hash_bits,
lanes=lanes,
noise_bits=get_param(dut, "READ_NOISE_BITS", 8),
rate_num=get_param(dut, "READ_NOISE_RATE_NUM", 1),
rate_den=get_param(dut, "READ_NOISE_RATE_DEN", 100),
seed=0x6A09_E667_F3BC_C909,
)
assert top1_index == expected.top1_index
assert top1_score == expected.top1_score
if score_debug is not None:
assert np.array_equal(score_debug, expected.scores)

View File

@@ -1,664 +0,0 @@
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 ( # noqa: E402
generate_write_flip_mask,
match_top1,
match_top1_with_read_noise,
random_hashes,
unpack_score_debug_flat,
)
DEFAULT_NUM_ROWS = 4096
DEFAULT_HASH_BITS = 512
DEFAULT_LANES = 8
DEFAULT_SCORE_BITS = 10
def _get_param(dut, name, default=None):
"""Read a Verilator-exposed parameter from the DUT."""
try:
val = getattr(dut, name, None)
if val is not None:
return int(val.value)
except Exception:
pass
return default
def dut_num_rows(dut):
val = _get_param(dut, "NUM_ROWS", None)
if val is not None:
return val
# Derive from wr_addr width (ROW_BITS): NUM_ROWS = 2^ROW_BITS
return 1 << len(dut.wr_addr)
def dut_hash_bits(dut):
val = _get_param(dut, "HASH_BITS", None)
if val is not None:
return val
# Derive from write_hash signal width
return len(dut.write_hash)
def dut_lanes(dut):
val = _get_param(dut, "LANES", None)
if val is not None:
return val
# Derive from rd_resp_row_ids width / ROW_BITS
return len(dut.rd_resp_row_ids) // len(dut.wr_addr)
def dut_score_bits(dut):
val = _get_param(dut, "SCORE_BITS", None)
if val is not None:
return val
# Derive from top1_score signal width
return len(dut.top1_score)
# ── Helpers ──────────────────────────────────────────────────────────────────
async def collect_topk(dut, timeout_cycles: int = 2000):
"""Collect all serial Top-K result beats with result_ready held high.
Returns list of (rank, row, score, last) tuples.
Raises AssertionError if the stream does not complete within timeout.
"""
dut.result_ready.value = 1
beats = []
for _ in range(timeout_cycles):
if int(dut.result_valid.value):
rank = int(dut.result_rank.value)
row = int(dut.result_row.value)
score = int(dut.result_score.value)
last = int(dut.result_last.value)
beats.append((rank, row, score, last))
if last:
return beats
await RisingEdge(dut.clk)
raise AssertionError("Top-K result stream did not finish")
async def query_topk_once(dut, query, timeout_cycles=2000):
"""Issue a query, collect full serial Top-K stream, and return beats + Top-1 metadata.
Returns (beats, top1_index, top1_score, score_debug).
After this call the full result stream has been consumed and the DUT is idle.
"""
await wait_idle(dut)
dut.query_hash.value = int(query)
dut.query_valid.value = 1
# Wait for handshake
while True:
await RisingEdge(dut.clk)
if int(dut.query_ready.value):
break
dut.query_valid.value = 0
# Consume full serial result stream
beats = await collect_topk(dut, timeout_cycles=timeout_cycles)
# score_debug is available after query completes
num_rows = dut_num_rows(dut)
score_bits = dut_score_bits(dut)
score_debug = None
if hasattr(dut, "score_debug_flat"):
score_debug = unpack_score_debug_flat(
int(dut.score_debug_flat.value),
num_rows,
score_bits,
)
return beats, beats[0][1], beats[0][2], score_debug
async def reset_dut(dut):
"""Reset the DUT with new handshake interface."""
dut.rst_n.value = 0
dut.wr_valid.value = 0
dut.wr_addr.value = 0
dut.write_hash.value = 0
dut.query_valid.value = 0
dut.query_hash.value = 0
dut.result_ready.value = 1
for _ in range(5):
await RisingEdge(dut.clk)
dut.rst_n.value = 1
for _ in range(2):
await RisingEdge(dut.clk)
async def wait_idle(dut):
"""Wait until both wr_ready=1 and query_ready=1 (system fully idle)."""
while not (int(dut.wr_ready.value) and int(dut.query_ready.value)):
await RisingEdge(dut.clk)
async def write_row(dut, addr, value):
"""Write a single row using wr_valid/wr_ready handshake."""
await wait_idle(dut)
dut.wr_addr.value = addr
dut.write_hash.value = int(value)
dut.wr_valid.value = 1
# Wait for handshake
while True:
await RisingEdge(dut.clk)
if int(dut.wr_ready.value):
break
dut.wr_valid.value = 0
# Wait for write pipeline to drain
await wait_idle(dut)
async def write_rows(dut, rows):
"""Write all rows sequentially."""
for idx, value in enumerate(rows):
await write_row(dut, idx, value)
async def query_once(dut, query):
"""Issue a query and return (top1_index, top1_score, score_debug).
Consumes the full serial Top-K stream and returns rank-0 data.
"""
_, top1_index, top1_score, score_debug = await query_topk_once(dut, query)
return top1_index, top1_score, score_debug
# ── Compile smoke test ────────────────────────────────────────────────────────
@cocotb.test()
async def compile_includes_grouped_noise_helper(dut):
"""Compilation test: new grouped noise helper must elaborate with cam_top."""
cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start())
await reset_dut(dut)
assert int(dut.wr_ready.value) in (0, 1)
# ── Test A: Baseline (WRITE_NOISE_EN=0) ─────────────────────────────────────
@cocotb.test()
async def baseline_no_noise(dut):
"""Verify write+query works exactly like the old CAM when noise disabled."""
noise_en = _get_param(dut, "WRITE_NOISE_EN", 0)
read_noise_en = _get_param(dut, "READ_NOISE_EN", 0)
if noise_en or read_noise_en:
dut._log.info("Skipping baseline_no_noise: requires noise disabled.")
return
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)
# ── Test B: Zero noise rate (WRITE_NOISE_EN=1, RATE_NUM=0) ──────────────────
@cocotb.test()
async def zero_rate_noise(dut):
"""Noise module connected but THRESHOLD=0 → no flips."""
noise_en = _get_param(dut, "WRITE_NOISE_EN", 1)
rate_num = _get_param(dut, "WRITE_NOISE_RATE_NUM", 1)
read_noise_en = _get_param(dut, "READ_NOISE_EN", 0)
if not noise_en or rate_num != 0 or read_noise_en:
dut._log.info("Skipping zero_rate_noise: requires WRITE_NOISE_EN=1, RATE_NUM=0, READ_NOISE_EN=0.")
return
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)
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
assert top1_score == hash_bits
if score_debug is not None:
assert np.array_equal(score_debug, expected.scores)
# ── Test C: 100% noise rate (RATE_NUM=1, RATE_DEN=1) ───────────────────────
@cocotb.test()
async def full_rate_noise(dut):
"""WRITE_NOISE_RATE_NUM=1, WRITE_NOISE_RATE_DEN=1 → every group flips."""
noise_en = _get_param(dut, "WRITE_NOISE_EN", 1)
rate_num = _get_param(dut, "WRITE_NOISE_RATE_NUM", 1)
rate_den = _get_param(dut, "WRITE_NOISE_RATE_DEN", 100)
if not noise_en or rate_num != 1 or rate_den != 1:
dut._log.info("Skipping full_rate_noise: requires WRITE_NOISE_EN=1, RATE_NUM=1, RATE_DEN=1.")
return
if not hasattr(dut, "score_debug_flat"):
dut._log.info("Skipping full_rate_noise: requires SIM_DEBUG (score_debug_flat).")
return
cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start())
await reset_dut(dut)
hash_bits = dut_hash_bits(dut)
num_rows = dut_num_rows(dut)
noise_bits = _get_param(dut, "WRITE_NOISE_BITS", 8)
all_zero = 0
all_one = (1 << hash_bits) - 1
RTL_SEED = 0xB504_F32D_B504_F32D
prng_state = (RTL_SEED << 64) | RTL_SEED
flip0, prng_state = generate_write_flip_mask(
prng_state, hash_bits, noise_bits, rate_num, rate_den,
)
expected_row0 = all_zero ^ flip0
flip1, prng_state = generate_write_flip_mask(
prng_state, hash_bits, noise_bits, rate_num, rate_den,
)
expected_row1 = all_one ^ flip1
rows = [0] * num_rows
rows[0] = all_zero
rows[1] = all_one
await write_rows(dut, rows)
top1_index, top1_score, score_debug = await query_once(dut, expected_row0)
assert score_debug is not None, "score_debug required for full_rate_noise"
assert int(score_debug[0]) == hash_bits, (
f"Row 0: expected exact match, score={score_debug[0]} != {hash_bits}"
)
top1_index, top1_score, score_debug = await query_once(dut, expected_row1)
assert score_debug is not None
assert int(score_debug[1]) == hash_bits, (
f"Row 1: expected exact match, score={score_debug[1]} != {hash_bits}"
)
# ── Test D: Default ~1% noise, reproducible ────────────────────────────────
@cocotb.test()
async def default_noise_reproducible(dut):
"""Fixed seed → deterministic write noise. Two identical runs produce same results."""
noise_en = _get_param(dut, "WRITE_NOISE_EN", 1)
if not noise_en:
dut._log.info("Skipping default_noise_reproducible: requires WRITE_NOISE_EN=1.")
return
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(42)
rows = random_hashes(rng, num_rows, width=hash_bits)
await write_rows(dut, rows)
query = rows[min(50, num_rows - 1)]
top1_index_1, top1_score_1, _ = await query_once(dut, query)
await reset_dut(dut)
await write_rows(dut, rows)
top1_index_2, top1_score_2, _ = await query_once(dut, query)
assert top1_index_1 == top1_index_2
assert top1_score_1 == top1_score_2
# ── Preserved legacy tests (only meaningful for noise disabled) ──────────────
@cocotb.test()
async def known_hamming_distance(dut):
"""Hamming distance verification — exact scores only valid without noise."""
if _get_param(dut, "WRITE_NOISE_EN", 1) or _get_param(dut, "READ_NOISE_EN", 0):
dut._log.info("Skipping known_hamming_distance: requires noise disabled.")
return
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):
"""Tie-break: lowest row index wins — only verified without noise."""
if _get_param(dut, "WRITE_NOISE_EN", 1) or _get_param(dut, "READ_NOISE_EN", 0):
dut._log.info("Skipping tie_break_policy: requires noise disabled.")
return
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):
"""All-zero / all-one boundary — only verified without noise."""
if _get_param(dut, "WRITE_NOISE_EN", 1) or _get_param(dut, "READ_NOISE_EN", 0):
dut._log.info("Skipping all_zero_all_one_boundary: requires noise disabled.")
return
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
# ── Test E: Exact RTL-vs-model PRNG mask match ──────────────────────────────
@cocotb.test()
async def exact_noise_model_match(dut):
"""Verify RTL stored hashes match ref_model.py for a known seed and rate."""
noise_en = _get_param(dut, "WRITE_NOISE_EN", 1)
rate_num = _get_param(dut, "WRITE_NOISE_RATE_NUM", 1)
rate_den = _get_param(dut, "WRITE_NOISE_RATE_DEN", 100)
if not noise_en or rate_num == 0:
dut._log.info("Skipping exact_noise_model_match: requires WRITE_NOISE_EN=1, RATE_NUM>0.")
return
if _get_param(dut, "READ_NOISE_EN", 0):
dut._log.info("Skipping exact_noise_model_match: requires READ_NOISE_EN=0 (read noise corrupts score comparison).")
return
if not hasattr(dut, "score_debug_flat"):
dut._log.info("Skipping exact_noise_model_match: requires SIM_DEBUG.")
return
cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start())
await reset_dut(dut)
hash_bits = dut_hash_bits(dut)
noise_bits = _get_param(dut, "WRITE_NOISE_BITS", 8)
n_test_rows = 4
rng = np.random.default_rng(99)
rows = random_hashes(rng, n_test_rows, width=hash_bits)
RTL_SEED = 0xB504_F32D_B504_F32D
prng_state = (RTL_SEED << 64) | RTL_SEED
expected_stored = []
for row in rows:
flip, prng_state = generate_write_flip_mask(
prng_state, hash_bits, noise_bits, rate_num, rate_den,
)
expected_stored.append(row ^ flip)
for idx, val in enumerate(rows):
await write_row(dut, idx, val)
for idx, expected in enumerate(expected_stored):
top1_index, top1_score, score_debug = await query_once(dut, expected)
assert score_debug is not None, "score_debug required for mask match verification"
assert int(score_debug[idx]) == hash_bits, (
f"Row {idx}: expected stored hash to match model prediction, "
f"score={score_debug[idx]} != {hash_bits}"
)
# ── Test F: Half-duplex write-priority arbitration ───────────────────────────
@cocotb.test()
async def half_duplex_write_priority(dut):
"""When wr_valid and query_valid are both high, write wins and query is held off."""
if _get_param(dut, "WRITE_NOISE_EN", 1) or _get_param(dut, "READ_NOISE_EN", 0):
dut._log.info("Skipping half_duplex_write_priority: requires noise disabled.")
return
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
# ── Test G: Banked pipeline no-noise Top-1 ───────────────────────────────────
@cocotb.test()
async def banked_pipeline_no_noise_top1(dut):
"""No-noise banked pipeline returns the same Top-1 as the pure model."""
if _get_param(dut, "WRITE_NOISE_EN", 0) or _get_param(dut, "READ_NOISE_EN", 0):
dut._log.info("Skipping banked_pipeline_no_noise_top1: requires noise disabled.")
return
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
# ── Test H: Query scan blocks writes until result consumed ───────────────────
@cocotb.test()
async def query_scan_blocks_writes_until_result_consumed(dut):
"""Half-duplex: active query scan deasserts wr_ready."""
if _get_param(dut, "WRITE_NOISE_EN", 0) or _get_param(dut, "READ_NOISE_EN", 0):
dut._log.info("Skipping query_scan_blocks_writes: requires noise disabled.")
return
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
# ── Test I: Read noise model match ──────────────────────────────────────────
@cocotb.test()
async def read_noise_model_match(dut):
"""Read noise uses grouped masks and matches the Python model for one query."""
if not _get_param(dut, "READ_NOISE_EN", 0):
dut._log.info("Skipping read_noise_model_match: requires READ_NOISE_EN=1.")
return
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)
lanes = dut_lanes(dut)
rng = np.random.default_rng(123)
rows = random_hashes(rng, num_rows, width=hash_bits)
# If write noise is enabled, apply write flip masks to predict stored rows
stored_rows = list(rows)
if _get_param(dut, "WRITE_NOISE_EN", 0):
seed = 0xB504_F32D_B504_F32D
prng_state = (seed << 64) | seed
stored_rows = []
for row in rows:
flip, prng_state = generate_write_flip_mask(
prng_state,
hash_bits,
_get_param(dut, "WRITE_NOISE_BITS", 8),
_get_param(dut, "WRITE_NOISE_RATE_NUM", 1),
_get_param(dut, "WRITE_NOISE_RATE_DEN", 100),
)
stored_rows.append(row ^ flip)
query = rows[min(5, num_rows - 1)]
await write_rows(dut, rows)
top1_index, top1_score, score_debug = await query_once(dut, query)
expected = match_top1_with_read_noise(
query,
stored_rows,
width=hash_bits,
lanes=lanes,
noise_bits=_get_param(dut, "READ_NOISE_BITS", 8),
rate_num=_get_param(dut, "READ_NOISE_RATE_NUM", 1),
rate_den=_get_param(dut, "READ_NOISE_RATE_DEN", 100),
seed=0x6A09_E667_F3BC_C909,
)
assert top1_index == expected.top1_index
assert top1_score == expected.top1_score
if score_debug is not None:
assert np.array_equal(score_debug, expected.scores)

256
hw/sim/tests/top/utils.py Normal file
View File

@@ -0,0 +1,256 @@
# -*- coding: utf-8 -*-
"""
CAM 顶层测试的共享辅助函数。
被 no_noise/、write_noise/、read_noise/ 等配置目录的测试文件共同引用。
提供:
- Verilator 参数读取(带回退推断)
- DUT 复位、空闲等待
- 行写入 / 批量写入(握手协议)
- 查询发起 / Top-K 结果流收集
- score_debug 解包SIM_DEBUG 模式)
所有函数都是 async调用者需要处于 cocotb 协程上下文中。
"""
from __future__ import annotations
import numpy as np
from cocotb.triggers import RisingEdge
from model.ref_model import ( # noqa: E402
match_top1,
unpack_score_debug_flat,
)
# ── 默认拓扑参数(当 Verilator 参数不可用时使用) ───────────────────────────
DEFAULT_NUM_ROWS = 4096
DEFAULT_HASH_BITS = 512
DEFAULT_LANES = 8
DEFAULT_SCORE_BITS = 10
# ═══════════════════════════════════════════════════════════════════════════════
# 参数读取(从 Verilator 参数或信号宽度推断)
# ═══════════════════════════════════════════════════════════════════════════════
def get_param(dut, name, default=None):
"""从 DUT 读取 Verilator 暴露的参数值,失败则返回 default。
某些参数仅在特定编译配置下暴露(如 SIM_DEBUG 下的 score_debug_flat
此时回退到 default 是预期行为而非错误。
"""
try:
val = getattr(dut, name, None)
if val is not None:
return int(val.value)
except Exception:
pass
return default
def dut_num_rows(dut):
"""获取 NUM_ROWS优先读参数否则从 wr_addr 位宽推断 (NUM_ROWS = 2^ROW_BITS)。"""
val = get_param(dut, "NUM_ROWS", None)
if val is not None:
return val
return 1 << len(dut.wr_addr)
def dut_hash_bits(dut):
"""获取 HASH_BITS优先读参数否则从 write_hash 信号位宽推断。"""
val = get_param(dut, "HASH_BITS", None)
if val is not None:
return val
return len(dut.write_hash)
def dut_lanes(dut):
"""获取 LANES优先读参数否则从 rd_resp_row_ids / wr_addr 位宽推断。"""
val = get_param(dut, "LANES", None)
if val is not None:
return val
return len(dut.rd_resp_row_ids) // len(dut.wr_addr)
def dut_score_bits(dut):
"""获取 SCORE_BITS优先读参数否则从 top1_score 信号位宽推断。"""
val = get_param(dut, "SCORE_BITS", None)
if val is not None:
return val
return len(dut.top1_score)
# ═══════════════════════════════════════════════════════════════════════════════
# 协议层辅助函数(复位 / 空闲等待 / 行写入 / 查询)
# ═══════════════════════════════════════════════════════════════════════════════
async def reset_dut(dut):
"""复位 DUTrst_n 拉低 5 周期,释放后再等 2 周期。
所有控制信号在复位期间保持无效电平。
result_ready 初始化为 1准备接收结果
"""
dut.rst_n.value = 0
dut.wr_valid.value = 0
dut.wr_addr.value = 0
dut.write_hash.value = 0
dut.query_valid.value = 0
dut.query_hash.value = 0
dut.result_ready.value = 1
for _ in range(5):
await RisingEdge(dut.clk)
dut.rst_n.value = 1
for _ in range(2):
await RisingEdge(dut.clk)
async def wait_idle(dut):
"""等待 DUT 完全空闲wr_ready=1 且 query_ready=1。
这是发起新操作前的前置条件——CAM 是半双工的,
同一时刻只能进行写入或查询。
"""
while not (int(dut.wr_ready.value) and int(dut.query_ready.value)):
await RisingEdge(dut.clk)
async def write_row(dut, addr, value):
"""写入单行:使用 wr_valid/wr_ready 握手协议。
流程:等待空闲 → 驱动地址和数据 → 等待握手完成 → 等待写流水线排空。
"""
await wait_idle(dut)
dut.wr_addr.value = addr
dut.write_hash.value = int(value)
dut.wr_valid.value = 1
while True:
await RisingEdge(dut.clk)
if int(dut.wr_ready.value):
break
dut.wr_valid.value = 0
await wait_idle(dut)
async def write_rows(dut, rows):
"""按顺序写入所有行(行索引 = 数组下标)。"""
for idx, value in enumerate(rows):
await write_row(dut, idx, value)
async def collect_topk(dut, timeout_cycles: int = 2000):
"""收集串行 Top-K 结果流的所有 beat。
保持 result_ready=1逐个时钟周期采样 result_valid
直到 result_last 被断言。
返回:[(rank, row, score, last), ...] 列表
超时则抛出 AssertionError。
注意:此函数会「消耗」整个结果流,调用后 DUT 回到空闲状态。
"""
dut.result_ready.value = 1
beats = []
for _ in range(timeout_cycles):
if int(dut.result_valid.value):
rank = int(dut.result_rank.value)
row = int(dut.result_row.value)
score = int(dut.result_score.value)
last = int(dut.result_last.value)
beats.append((rank, row, score, last))
if last:
return beats
await RisingEdge(dut.clk)
raise AssertionError("Top-K result stream did not finish")
async def query_topk_once(dut, query, timeout_cycles=2000):
"""发起一次查询并收集完整的串行 Top-K 结果流。
完整流程:
1. 等待 DUT 空闲
2. 通过 query_valid/query_ready 握手发送查询
3. 消费完整的结果流
4. 读取 score_debug_flat如果存在
返回:(beats, top1_index, top1_score, score_debug)
- beats: [(rank, row, score, last), ...]
- score_debug: np.ndarray 或 NoneSIM_DEBUG 模式)
"""
await wait_idle(dut)
dut.query_hash.value = int(query)
dut.query_valid.value = 1
# 等待查询握手完成
while True:
await RisingEdge(dut.clk)
if int(dut.query_ready.value):
break
dut.query_valid.value = 0
# 消费完整串行结果流
beats = await collect_topk(dut, timeout_cycles=timeout_cycles)
# score_debug 在查询完成后可用(需 SIM_DEBUG 编译)
num_rows = dut_num_rows(dut)
score_bits = dut_score_bits(dut)
score_debug = None
if hasattr(dut, "score_debug_flat"):
score_debug = unpack_score_debug_flat(
int(dut.score_debug_flat.value),
num_rows,
score_bits,
)
return beats, beats[0][1], beats[0][2], score_debug
async def query_once(dut, query):
"""发起查询,返回 (top1_index, top1_score, score_debug)。
内部调用 query_topk_once 并消费完整结果流,仅保留 rank-0 数据。
"""
_, top1_index, top1_score, score_debug = await query_topk_once(dut, query)
return top1_index, top1_score, score_debug
# ═══════════════════════════════════════════════════════════════════════════════
# 便捷验证函数
# ═══════════════════════════════════════════════════════════════════════════════
def assert_baseline_top1(query, rows, top1_index, top1_score, hash_bits,
query_index, score_debug=None):
"""验证无噪声场景下的基线 Top-1 结果。
检查项:
1. Top-1 与 match_top1 参考模型一致
2. 查询自身所在行 → score == hash_bits完全匹配
3. score_debug 数组(如果存在)与模型一致
"""
expected = match_top1(query, rows, width=hash_bits)
assert top1_index == expected.top1_index, (
f"top1_index mismatch: {top1_index} != {expected.top1_index}"
)
assert top1_score == expected.top1_score, (
f"top1_score mismatch: {top1_score} != {expected.top1_score}"
)
assert top1_index == query_index, (
f"Expected query_index={query_index} to match self, got top1_index={top1_index}"
)
assert top1_score == hash_bits, (
f"Self-query should score {hash_bits}, got {top1_score}"
)
if score_debug is not None:
assert np.array_equal(score_debug, expected.scores), (
"score_debug does not match model scores"
)

View File

@@ -0,0 +1,29 @@
SIM_ROOT := $(abspath ../../..)
RTL_ROOT := $(abspath $(SIM_ROOT)/../rtl)
include $(SIM_ROOT)/mk/rtl-sources.mk
TOPLEVEL := cam_top
COCOTB_TEST_MODULES := tests.top.write_noise.test_write_noise
VERILOG_SOURCES := $(RTL_CAM_TOP)
# 写入噪声 ~1% 默认速率
WRITE_NOISE_EN := 1
WRITE_NOISE_RATE_NUM := 1
WRITE_NOISE_RATE_DEN := 100
READ_NOISE_EN := 0
include $(SIM_ROOT)/mk/cocotb-common.mk
# ── 速率变体子目标 ─────────────────────────────────────────────────
.PHONY: test-zero-rate test-full-rate
test-zero-rate:
$(MAKE) -B -f Makefile results.xml WRITE_NOISE_RATE_NUM=0 \
COCOTB_TEST_FILTER=zero_rate_noise
test-full-rate:
$(MAKE) -B -f Makefile results.xml WRITE_NOISE_RATE_NUM=1 WRITE_NOISE_RATE_DEN=1 \
COCOTB_TEST_FILTER=full_rate_noise
clean::
rm -rf sim_build

View File

@@ -0,0 +1 @@
# CAM top-level write-noise integration tests

View File

@@ -0,0 +1,274 @@
# -*- coding: utf-8 -*-
"""
CAM 写入噪声Write Noise集成测试 —— 专用配置。
本文件测试 WRITE_NOISE_EN=1, READ_NOISE_EN=0 配置下,
写入噪声模块的正确性。默认噪声率约 1%NUM=1, DEN=100
=== 测试列表 ===
1. default_noise_reproducible — 固定种子 = 确定性噪声,两次运行结果一致
2. exact_noise_model_match — RTL 存储的哈希与 ref_model.py 的 PRNG 掩码逐位匹配
3. zero_rate_noise — 写入噪声模块连接但 RATE_NUM=0 → 无翻转
4. full_rate_noise — 100% 写入噪声率,与 Python 模型对比
=== 架构背景 ===
写入噪声流水线位置Write Noise → Banked Core Storage → Read Noise → Match Engine
本测试覆盖完整的 cam_top 链路,写入噪声为唯一活跃噪声源。
=== Makefile 子目标 ===
test-zero-rate : make test-zero-rate (WRITE_NOISE_RATE_NUM=0)
test-full-rate : make test-full-rate (WRITE_NOISE_RATE_NUM=1, RATE_DEN=1, SIM_DEBUG)
"""
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 (
generate_write_flip_mask,
match_top1,
random_hashes,
)
from tests.top.utils import (
collect_topk,
dut_hash_bits,
dut_num_rows,
get_param,
query_once,
query_topk_once,
reset_dut,
wait_idle,
write_row,
write_rows,
)
# ═══════════════════════════════════════════════════════════════════════════════
# 测试 1默认噪声 ~1%、可复现
# ── 固定种子 → 两次相同写入产生相同结果
# ═══════════════════════════════════════════════════════════════════════════════
@cocotb.test()
async def default_noise_reproducible(dut):
"""可复现性测试:相同种子、相同数据 → 两次独立运行结果一致。
流程:
1. 写入全部行,查询 row 50 → 记录 top1_index 和 top1_score
2. 复位
3. 再次写入相同数据,查询相同行 → 记录结果
4. 断言两次结果完全一致
如果结果不一致,说明 RTL 的 PRNG 状态没有正确复位,
或存在跨运行的状态残留。
"""
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(42)
rows = random_hashes(rng, num_rows, width=hash_bits)
await write_rows(dut, rows)
query = rows[min(50, num_rows - 1)]
top1_index_1, top1_score_1, _ = await query_once(dut, query)
await reset_dut(dut)
await write_rows(dut, rows)
top1_index_2, top1_score_2, _ = await query_once(dut, query)
assert top1_index_1 == top1_index_2
assert top1_score_1 == top1_score_2
# ═══════════════════════════════════════════════════════════════════════════════
# 测试 2精确 RTL-vs-模型 PRNG 掩码匹配
# ── RTL 存储的哈希与 ref_model.py 生成的掩码逐位一致
# ═══════════════════════════════════════════════════════════════════════════════
@cocotb.test()
async def exact_noise_model_match(dut):
"""精确噪声模型匹配RTL 的 PRNG 输出必须与 Python 参考模型逐位一致。
测试方法:
1. 用固定 RTL seed 和已知噪声参数,在 Python 中预计算每行的 flip 掩码
2. 预期存储值 = 原始值 XOR flip_mask
3. 写入原始值到 RTL查询预期存储值
4. 断言每行 score = HASH_BITS完全匹配
这验证了 RTL 的 LFSR 实现与 Python 模型的 PRNG 使用相同的
多项式、相同的位宽、相同的种子初始化序列。
"""
if not hasattr(dut, "score_debug_flat"):
dut._log.info("Skipping exact_noise_model_match: requires SIM_DEBUG.")
return
rtol = None
atol = None
cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start())
await reset_dut(dut)
hash_bits = dut_hash_bits(dut)
noise_bits = get_param(dut, "WRITE_NOISE_BITS", 8)
rate_num = get_param(dut, "WRITE_NOISE_RATE_NUM", 1)
rate_den = get_param(dut, "WRITE_NOISE_RATE_DEN", 100)
n_test_rows = 4
rng = np.random.default_rng(99)
rows = random_hashes(rng, n_test_rows, width=hash_bits)
RTL_SEED = 0xB504_F32D_B504_F32D
prng_state = (RTL_SEED << 64) | RTL_SEED
expected_stored = []
for row in rows:
flip, prng_state = generate_write_flip_mask(
prng_state,
hash_bits,
noise_bits,
rate_num,
rate_den,
)
expected_stored.append(row ^ flip)
for idx, val in enumerate(rows):
await write_row(dut, idx, val)
for idx, expected in enumerate(expected_stored):
top1_index, top1_score, score_debug = await query_once(dut, expected)
assert score_debug is not None, (
"score_debug required for mask match verification"
)
assert int(score_debug[idx]) == hash_bits, (
f"Row {idx}: expected stored hash to match model prediction, "
f"score={score_debug[idx]} != {hash_bits}"
)
# ═══════════════════════════════════════════════════════════════════════════════
# 测试 3零噪声率WRITE_NOISE_EN=1, RATE_NUM=0
# ── 噪声模块已连接但翻转概率为 0 → 行为应与无噪声一致
# ═══════════════════════════════════════════════════════════════════════════════
@cocotb.test()
async def zero_rate_noise(dut):
"""零速率噪声WRITE_NOISE_RATE_NUM=0 时不应有任何位被翻转。
这是噪声模块的边界测试——验证 RATE_NUM=0 确实禁用了翻转,
而非产生「默认速率」的噪声。
"""
rate_num = get_param(dut, "WRITE_NOISE_RATE_NUM", 1)
if rate_num != 0:
dut._log.info(
"Skipping zero_rate_noise: requires WRITE_NOISE_RATE_NUM=0."
)
return
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)
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
assert top1_score == hash_bits
if score_debug is not None:
assert np.array_equal(score_debug, expected.scores)
# ═══════════════════════════════════════════════════════════════════════════════
# 测试 4100% 噪声率RATE_NUM=1, RATE_DEN=1
# ── 每组都翻转 → 精确验证 PRNG 掩码生成
# ═══════════════════════════════════════════════════════════════════════════════
@cocotb.test()
async def full_rate_noise(dut):
"""完全速率噪声:每组 100% 翻转概率。
使用固定 RTL seed (0xB504F32DB504F32D),用 Python 模型预计算
写入全 0 和全 1 行后应存储的哈希值,然后验证 RTL 实际存储的哈希
与模型预测完全一致。
这是最低容忍度的噪声测试——要求 score_debug_flatSIM_DEBUG
且每行的分数必须精确等于 HASH_BITS。
"""
rate_num = get_param(dut, "WRITE_NOISE_RATE_NUM", 1)
rate_den = get_param(dut, "WRITE_NOISE_RATE_DEN", 100)
if rate_num != 1 or rate_den != 1:
dut._log.info(
"Skipping full_rate_noise: requires WRITE_NOISE_RATE_NUM=1, RATE_DEN=1."
)
return
if not hasattr(dut, "score_debug_flat"):
dut._log.info(
"Skipping full_rate_noise: requires SIM_DEBUG (score_debug_flat)."
)
return
cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start())
await reset_dut(dut)
hash_bits = dut_hash_bits(dut)
num_rows = dut_num_rows(dut)
noise_bits = get_param(dut, "WRITE_NOISE_BITS", 8)
all_zero = 0
all_one = (1 << hash_bits) - 1
RTL_SEED = 0xB504_F32D_B504_F32D
prng_state = (RTL_SEED << 64) | RTL_SEED
flip0, prng_state = generate_write_flip_mask(
prng_state,
hash_bits,
noise_bits,
rate_num,
rate_den,
)
expected_row0 = all_zero ^ flip0
flip1, prng_state = generate_write_flip_mask(
prng_state,
hash_bits,
noise_bits,
rate_num,
rate_den,
)
expected_row1 = all_one ^ flip1
rows = [0] * num_rows
rows[0] = all_zero
rows[1] = all_one
await write_rows(dut, rows)
top1_index, top1_score, score_debug = await query_once(dut, expected_row0)
assert score_debug is not None, "score_debug required for full_rate_noise"
assert int(score_debug[0]) == hash_bits, (
f"Row 0: expected exact match, score={score_debug[0]} != {hash_bits}"
)
top1_index, top1_score, score_debug = await query_once(dut, expected_row1)
assert score_debug is not None
assert int(score_debug[1]) == hash_bits, (
f"Row 1: expected exact match, score={score_debug[1]} != {hash_bits}"
)