diff --git a/.justfile b/.justfile index d1cde93..726ddcc 100644 --- a/.justfile +++ b/.justfile @@ -50,13 +50,6 @@ remote-dry cmd: # ── CAM verification ────────────────────────────────────────────────────────── -# Run all 4 integrated CAM configurations (no-noise, write-only, read-only, combined) -cam-test-all: - just remote "make -C hw/sim clean && make -C hw/sim test-top NUM_ROWS=64 LANES=8 HASH_BITS=512 WRITE_NOISE_EN=0 READ_NOISE_EN=0" - just remote "make -C hw/sim clean && make -C hw/sim test-top NUM_ROWS=64 LANES=8 HASH_BITS=512 WRITE_NOISE_EN=1 WRITE_NOISE_RATE_NUM=1 WRITE_NOISE_RATE_DEN=100 READ_NOISE_EN=0" - just remote "make -C hw/sim clean && make -C hw/sim test-top NUM_ROWS=64 LANES=8 HASH_BITS=512 WRITE_NOISE_EN=0 READ_NOISE_EN=1 READ_NOISE_RATE_NUM=1 READ_NOISE_RATE_DEN=100" - just remote "make -C hw/sim clean && make -C hw/sim test-top NUM_ROWS=64 LANES=8 HASH_BITS=512 WRITE_NOISE_EN=1 WRITE_NOISE_RATE_NUM=1 WRITE_NOISE_RATE_DEN=100 READ_NOISE_EN=1 READ_NOISE_RATE_NUM=1 READ_NOISE_RATE_DEN=100" - # Run all CAM tests in one pass (model + top + all modules) cam-test-full: just remote "make -C hw/sim clean && make -C hw/sim test-all" diff --git a/.stignore b/.stignore index 2622256..6eb4476 100644 --- a/.stignore +++ b/.stignore @@ -15,3 +15,4 @@ outputs **/__pycache__ **/sim_build **/__marimo__ +*.fst diff --git a/hw/rtl/core/match_engine_pipeline.sv b/hw/rtl/core/match_engine_pipeline.sv index 83bd0d1..f7d31d2 100644 --- a/hw/rtl/core/match_engine_pipeline.sv +++ b/hw/rtl/core/match_engine_pipeline.sv @@ -31,9 +31,10 @@ module match_engine_pipeline ( //========================================================================== // State encoding //========================================================================== - typedef enum logic [2:0] { + typedef enum logic [3:0] { S_IDLE, S_ISSUE_READ, + S_WAIT_READ_RESP, S_WAIT_SCORE, S_STAGE_CANDIDATES, S_PUSH_CANDIDATES, @@ -373,6 +374,13 @@ module match_engine_pipeline ( end S_ISSUE_READ: begin + // Emit rd_valid_o for exactly one cycle (combinational), then + // transition to S_WAIT_READ_RESP. This ensures the read-noise + // PRNG in cam_read_noise only advances once per batch issue. + state_q <= S_WAIT_READ_RESP; + end + + S_WAIT_READ_RESP: begin if (rd_valid_i) begin // Capture staging data for popcount pipeline rd_stage_valid_q <= 1'b1; @@ -380,8 +388,7 @@ module match_engine_pipeline ( rd_stage_hashes_q <= rd_hashes_i; rd_stage_lane_valid_q <= rd_lane_valid_i; // Remember the lane mask for this batch (used in S_WAIT_SCORE - // to determine batch_scores_done — survives until overwritten - // by the next S_ISSUE_READ). + // to determine batch_scores_done). batch_lane_mask_q <= rd_lane_valid_i; issue_base_q <= issue_base_q + `LANES; issued_batches_q <= issued_batches_q + 1; diff --git a/hw/sim/Makefile b/hw/sim/Makefile index 2a7535b..d054d40 100644 --- a/hw/sim/Makefile +++ b/hw/sim/Makefile @@ -7,7 +7,7 @@ TOP_CONFIGS := no_noise write_noise read_noise help: @echo "Available hw/sim targets:" @echo " make test-model" - @echo " make test-top # 只运行默认顶层配置 (no_noise)" + @echo " make test-top # 默认运行所有顶层噪声配置" @echo " make test-top-all # 运行所有顶层噪声配置" @echo " make test-top-no_noise # 无噪声集成测试" @echo " make test-top-write_noise # 写入噪声集成测试" @@ -20,7 +20,7 @@ help: test-all: test-model test-top-all test-modules -test-top: test-top-no_noise +test-top: test-top-all test-top-all: $(TOP_CONFIGS:%=test-top-%) diff --git a/hw/sim/mk/cocotb-common.mk b/hw/sim/mk/cocotb-common.mk index dafcfc8..8cc7d97 100644 --- a/hw/sim/mk/cocotb-common.mk +++ b/hw/sim/mk/cocotb-common.mk @@ -79,4 +79,4 @@ COMPILE_ARGS += -Wno-WIDTHEXPAND COMPILE_ARGS += -Wno-UNOPTFLAT endif -include $(shell uv run cocotb-config --makefiles)/Makefile.sim +include $(shell cocotb-config --makefiles)/Makefile.sim diff --git a/hw/sim/tests/top/no_noise/test_no_noise.py b/hw/sim/tests/top/no_noise/test_no_noise.py index d35706f..c7986f9 100644 --- a/hw/sim/tests/top/no_noise/test_no_noise.py +++ b/hw/sim/tests/top/no_noise/test_no_noise.py @@ -35,6 +35,7 @@ from model.ref_model import ( from tests.top.utils import ( collect_topk, dut_hash_bits, + dut_lanes, dut_num_rows, query_once, query_topk_once, @@ -44,7 +45,6 @@ from tests.top.utils import ( write_rows, ) - # ═══════════════════════════════════════════════════════════════════════════════ # 编译冒烟测试 — 验证 cam_top 能正确 elaborate # ═══════════════════════════════════════════════════════════════════════════════ @@ -343,6 +343,8 @@ async def query_scan_blocks_writes_until_result_consumed(dut): dut.wr_valid.value = 0 # Consume full serial stream so the DUT returns idle - beats = await collect_topk(dut, timeout_cycles=2000) + beats = await collect_topk( + dut, timeout_cycles=max(2000, (dut_num_rows(dut) // dut_lanes(dut)) * 24 + 100) + ) assert len(beats) > 0 assert beats[-1][3] == 1 # last asserted diff --git a/hw/sim/tests/top/utils.py b/hw/sim/tests/top/utils.py index 2ad95a2..e31adb6 100644 --- a/hw/sim/tests/top/utils.py +++ b/hw/sim/tests/top/utils.py @@ -170,14 +170,33 @@ async def collect_topk(dut, timeout_cycles: int = 2000): raise AssertionError("Top-K result stream did not finish") -async def query_topk_once(dut, query, timeout_cycles=2000): +# ── 默认超时估算 ────────────────────────────────────────────────── + + +def dut_query_timeout_cycles(dut): + """基于 DUT 参数估算完整查询(扫描 + 串行结果输出)的超时周期数。 + + 各通道串行输出一个 beat 需要约 24 个流水线周期; + 总超时 = ceil(全部行数 / 通道数) * 24 + 固定裕量 2000 周期。 + 至少返回 2000 周期以防止极小配置下的不合理值。 + + Example: 4096 rows / 8 lanes → ceil(4096/8) * 24 + 2000 = 14288 cycles. + """ + num_rows = dut_num_rows(dut) + lanes = dut_lanes(dut) + batches = (num_rows + lanes - 1) // lanes # ceil division, no math import + return max(2000, batches * 24 + 2000) + + +async def query_topk_once(dut, query, timeout_cycles=None): """发起一次查询并收集完整的串行 Top-K 结果流。 完整流程: 1. 等待 DUT 空闲 - 2. 通过 query_valid/query_ready 握手发送查询 - 3. 消费完整的结果流 - 4. 读取 score_debug_flat(如果存在) + 2. 等待 query_ready 为高 + 3. 通过 query_valid/query_ready 握手发送查询 + 4. 消费完整的结果流 + 5. 读取 score_debug_flat(如果存在) 返回:(beats, top1_index, top1_score, score_debug) - beats: [(rank, row, score, last), ...] @@ -186,16 +205,21 @@ async def query_topk_once(dut, query, timeout_cycles=2000): await wait_idle(dut) dut.query_hash.value = int(query) - dut.query_valid.value = 1 - # 等待查询握手完成 - while True: + # 等待 query_ready 为高(DUT 已就绪),避免组合逻辑下降沿导致的 + # valid&&ready 握手丢失问题 + while not int(dut.query_ready.value): await RisingEdge(dut.clk) - if int(dut.query_ready.value): - break + # assert query_valid 覆盖一个上升沿完成握手 + dut.query_valid.value = 1 + await RisingEdge(dut.clk) dut.query_valid.value = 0 + # 若调用者未指定超时,根据 DUT 参数动态估算 + if timeout_cycles is None: + timeout_cycles = dut_query_timeout_cycles(dut) + # 消费完整串行结果流 beats = await collect_topk(dut, timeout_cycles=timeout_cycles) @@ -213,12 +237,19 @@ async def query_topk_once(dut, query, timeout_cycles=2000): return beats, beats[0][1], beats[0][2], score_debug -async def query_once(dut, query): +async def query_once(dut, query, timeout_cycles=None): """发起查询,返回 (top1_index, top1_score, score_debug)。 内部调用 query_topk_once 并消费完整结果流,仅保留 rank-0 数据。 + + Parameters + ---------- + timeout_cycles : int or None + 传递给 query_topk_once 的超时周期数。None 表示根据 DUT 参数动态估算。 """ - _, top1_index, top1_score, score_debug = await query_topk_once(dut, query) + _, top1_index, top1_score, score_debug = await query_topk_once( + dut, query, timeout_cycles=timeout_cycles, + ) return top1_index, top1_score, score_debug