mirror of
https://github.com/SikongJueluo/Mini-Nav.git
synced 2026-07-12 20:15:31 +08:00
feat(cam): add Bernoulli noise mask module and cocotb tests
- Add noise_mask_bernoulli.sv RTL module for probabilistic masking - Add cocotb test suite with reset, threshold, determinism, and distribution checks - Update rtl-sources.mk to include new module - Fix PYTHONPATH in devenv.nix shell hook
This commit is contained in:
11
hw/sim/tests/modules/noise_mask_bernoulli/Makefile
Normal file
11
hw/sim/tests/modules/noise_mask_bernoulli/Makefile
Normal file
@@ -0,0 +1,11 @@
|
||||
SIM_ROOT := $(abspath ../../..)
|
||||
RTL_ROOT := $(abspath $(SIM_ROOT)/../rtl)
|
||||
include $(SIM_ROOT)/mk/rtl-sources.mk
|
||||
|
||||
TOPLEVEL := noise_mask_bernoulli
|
||||
COCOTB_TEST_MODULES := tests.modules.noise_mask_bernoulli.test_noise_mask_bernoulli
|
||||
VERILOG_SOURCES := $(RTL_BERNOULLI_NOISE_MASK)
|
||||
|
||||
HASH_BITS ?= 512
|
||||
|
||||
include $(SIM_ROOT)/mk/cocotb-common.mk
|
||||
1
hw/sim/tests/modules/noise_mask_bernoulli/__init__.py
Normal file
1
hw/sim/tests/modules/noise_mask_bernoulli/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Cocotb tests for noise_mask_bernoulli."""
|
||||
@@ -0,0 +1,242 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import cocotb
|
||||
from cocotb.clock import Clock
|
||||
from cocotb.triggers import RisingEdge, Timer
|
||||
|
||||
HASH_BITS = 512
|
||||
DEFAULT_BLOCKS = 16
|
||||
DEFAULT_PRIME_CYCLES = 1
|
||||
DEFAULT_START_TO_DONE_CYCLES = DEFAULT_PRIME_CYCLES + DEFAULT_BLOCKS
|
||||
|
||||
|
||||
def popcount(value: int) -> int:
|
||||
return int(value).bit_count()
|
||||
|
||||
|
||||
async def reset_dut(dut):
|
||||
"""Reset the DUT and drive inactive defaults."""
|
||||
dut.rst_n.value = 0
|
||||
dut.start_i.value = 0
|
||||
dut.threshold_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 start_and_wait_done(dut, threshold: int) -> tuple[int, int]:
|
||||
"""Start one generation and return (mask, cycles from accepted start to done)."""
|
||||
dut.threshold_i.value = threshold
|
||||
dut.start_i.value = 1
|
||||
await Timer(1, unit="step")
|
||||
await RisingEdge(dut.clk)
|
||||
await Timer(1, unit="step")
|
||||
dut.start_i.value = 0
|
||||
|
||||
cycles = 0
|
||||
while True:
|
||||
assert cycles < 128, "timed out waiting for done_o"
|
||||
if bool(dut.done_o.value):
|
||||
break
|
||||
assert bool(dut.busy_o.value), "busy_o should stay high until done_o"
|
||||
await RisingEdge(dut.clk)
|
||||
await Timer(1, unit="step")
|
||||
cycles += 1
|
||||
|
||||
mask = int(dut.mask_o.value)
|
||||
await RisingEdge(dut.clk)
|
||||
await Timer(1, unit="step")
|
||||
assert not bool(dut.done_o.value), "done_o should be a single-cycle pulse"
|
||||
assert not bool(dut.busy_o.value), "busy_o should be low after done_o"
|
||||
assert int(dut.mask_o.value) == mask, "mask_o should remain stable after done_o"
|
||||
return mask, cycles
|
||||
|
||||
|
||||
@cocotb.test()
|
||||
async def reset_outputs_idle_and_clear_mask(dut):
|
||||
cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start())
|
||||
await reset_dut(dut)
|
||||
|
||||
assert not bool(dut.busy_o.value), "busy_o should be low after reset"
|
||||
assert not bool(dut.done_o.value), "done_o should be low after reset"
|
||||
assert int(dut.mask_o.value) == 0, "mask_o should reset to zero"
|
||||
|
||||
|
||||
@cocotb.test()
|
||||
async def threshold_zero_generates_empty_mask(dut):
|
||||
cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start())
|
||||
await reset_dut(dut)
|
||||
|
||||
mask, cycles = await start_and_wait_done(dut, threshold=0)
|
||||
|
||||
assert mask == 0, "threshold=0 should generate all-zero mask"
|
||||
assert popcount(mask) == 0, "threshold=0 popcount should be zero"
|
||||
assert cycles == DEFAULT_START_TO_DONE_CYCLES, (
|
||||
f"default latency should be {DEFAULT_START_TO_DONE_CYCLES} cycles, got {cycles}"
|
||||
)
|
||||
|
||||
|
||||
@cocotb.test()
|
||||
async def test_deterministic_across_reset(dut):
|
||||
"""Same threshold after reset should produce identical mask sequence."""
|
||||
cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start())
|
||||
await reset_dut(dut)
|
||||
|
||||
masks_a = []
|
||||
for _ in range(3):
|
||||
mask, _ = await start_and_wait_done(dut, threshold=128)
|
||||
masks_a.append(mask)
|
||||
|
||||
await reset_dut(dut)
|
||||
|
||||
masks_b = []
|
||||
for _ in range(3):
|
||||
mask, _ = await start_and_wait_done(dut, threshold=128)
|
||||
masks_b.append(mask)
|
||||
|
||||
assert masks_a == masks_b, "Mask sequence should be deterministic across reset"
|
||||
|
||||
|
||||
@cocotb.test()
|
||||
async def test_threshold_monotonicity(dut):
|
||||
"""Higher threshold should yield higher average popcount."""
|
||||
cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start())
|
||||
await reset_dut(dut)
|
||||
|
||||
N = 32
|
||||
masks_64 = []
|
||||
for _ in range(N):
|
||||
mask, _ = await start_and_wait_done(dut, threshold=64)
|
||||
masks_64.append(mask)
|
||||
|
||||
await reset_dut(dut)
|
||||
masks_192 = []
|
||||
for _ in range(N):
|
||||
mask, _ = await start_and_wait_done(dut, threshold=192)
|
||||
masks_192.append(mask)
|
||||
|
||||
avg_64 = sum(popcount(m) for m in masks_64) / N
|
||||
avg_192 = sum(popcount(m) for m in masks_192) / N
|
||||
|
||||
# threshold=64: expected popcount ~512*64/256 = 128
|
||||
# threshold=192: expected popcount ~512*192/256 = 384
|
||||
assert 60 < avg_64 < 135, f"avg popcount at threshold=64 out of range: {avg_64}"
|
||||
assert 365 < avg_192 < 405, f"avg popcount at threshold=192 out of range: {avg_192}"
|
||||
assert avg_192 > avg_64 + 200, (
|
||||
f"threshold=192 avg ({avg_192}) should exceed threshold=64 avg ({avg_64}) by margin"
|
||||
)
|
||||
|
||||
|
||||
@cocotb.test()
|
||||
async def test_threshold_255_not_all_ones(dut):
|
||||
"""threshold=255 should produce dense masks but not force all ones."""
|
||||
cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start())
|
||||
await reset_dut(dut)
|
||||
|
||||
masks = []
|
||||
for _ in range(8):
|
||||
mask, _ = await start_and_wait_done(dut, threshold=255)
|
||||
masks.append(mask)
|
||||
|
||||
popcounts = [popcount(m) for m in masks]
|
||||
avg = sum(popcounts) / len(popcounts)
|
||||
|
||||
# At least one mask should have some zero bits
|
||||
assert any(pc < HASH_BITS for pc in popcounts), (
|
||||
"threshold=255 should produce at least one mask with some zero bits"
|
||||
)
|
||||
# Average density should be very high (p=255/256)
|
||||
assert avg > HASH_BITS * 0.9, f"threshold=255 average popcount too low: {avg}"
|
||||
|
||||
|
||||
@cocotb.test()
|
||||
async def test_busy_start_ignored(dut):
|
||||
"""Pulsing start_i while busy_o is high must be ignored.
|
||||
|
||||
Start one generation, then pulse start_i with a different threshold_i while
|
||||
the DUT is busy, and prove the busy pulse is ignored (no second done_o,
|
||||
same latency, same mask as an uninterrupted run).
|
||||
"""
|
||||
cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start())
|
||||
await reset_dut(dut)
|
||||
|
||||
# --- Baseline: single uninterrupted generation with threshold=64 ---
|
||||
mask_ref, cycles_ref = await start_and_wait_done(dut, threshold=64)
|
||||
assert cycles_ref == DEFAULT_START_TO_DONE_CYCLES, (
|
||||
f"Baseline latency should be {DEFAULT_START_TO_DONE_CYCLES}, got {cycles_ref}"
|
||||
)
|
||||
|
||||
# --- Test: pulse spurious start with threshold=255 while busy ---
|
||||
await reset_dut(dut)
|
||||
|
||||
# Start legitimate operation with threshold=64
|
||||
dut.threshold_i.value = 64
|
||||
dut.start_i.value = 1
|
||||
await Timer(1, unit="step")
|
||||
await RisingEdge(dut.clk) # sampled: FSM transitions to PRIME
|
||||
await Timer(1, unit="step")
|
||||
dut.start_i.value = 0
|
||||
|
||||
assert bool(dut.busy_o.value), "busy_o should be high after start"
|
||||
|
||||
# Let FSM reach RUN (PRIME→RUN at next posedge, then one RUN cycle)
|
||||
await RisingEdge(dut.clk) # PRIME → RUN
|
||||
await Timer(1, unit="step")
|
||||
await RisingEdge(dut.clk) # RUN, first block processed
|
||||
await Timer(1, unit="step")
|
||||
|
||||
assert bool(dut.busy_o.value), "DUT must be busy during RUN"
|
||||
|
||||
# Pulse start_i with threshold=255 while DUT is busy (must be ignored)
|
||||
dut.threshold_i.value = 255
|
||||
dut.start_i.value = 1
|
||||
await Timer(1, unit="step")
|
||||
await RisingEdge(dut.clk) # sampled but FSM in RUN → ignored
|
||||
await Timer(1, unit="step")
|
||||
dut.start_i.value = 0
|
||||
|
||||
# Count remaining cycles until done_o
|
||||
# Cycles already consumed from the legitimate start:
|
||||
# 1 for PRIME + 1 for RUN before spurious + 1 for spurious-start cycle = 3
|
||||
elapsed = 3
|
||||
cycles_from_spurious = 0
|
||||
while True:
|
||||
if bool(dut.done_o.value):
|
||||
break
|
||||
assert elapsed + cycles_from_spurious < 128, "timed out waiting for done_o"
|
||||
assert bool(dut.busy_o.value), "busy_o should stay high until done_o"
|
||||
await RisingEdge(dut.clk)
|
||||
await Timer(1, unit="step")
|
||||
cycles_from_spurious += 1
|
||||
|
||||
total_cycles = elapsed + cycles_from_spurious
|
||||
assert total_cycles == DEFAULT_START_TO_DONE_CYCLES, (
|
||||
f"Latency should be {DEFAULT_START_TO_DONE_CYCLES} despite spurious start, "
|
||||
f"got {total_cycles} ({elapsed}+{cycles_from_spurious})"
|
||||
)
|
||||
|
||||
mask_test = int(dut.mask_o.value)
|
||||
await RisingEdge(dut.clk)
|
||||
await Timer(1, unit="step")
|
||||
|
||||
# Verify done_o single-cycle and busy_o falls low
|
||||
assert not bool(dut.done_o.value), "done_o should be a single-cycle pulse"
|
||||
assert not bool(dut.busy_o.value), "busy_o should be low after done_o"
|
||||
assert int(dut.mask_o.value) == mask_test, (
|
||||
"mask_o should remain stable after done_o"
|
||||
)
|
||||
|
||||
# Compare mask to baseline (same seed + same threshold → same mask)
|
||||
assert mask_test == mask_ref, (
|
||||
"Spurious start must not corrupt in-flight mask generation"
|
||||
)
|
||||
|
||||
# Verify no second done_o from the ignored start
|
||||
for _ in range(32):
|
||||
await RisingEdge(dut.clk)
|
||||
await Timer(1, unit="step")
|
||||
assert not bool(dut.done_o.value), (
|
||||
"Spurious start must not queue a second operation"
|
||||
)
|
||||
Reference in New Issue
Block a user