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:
@@ -28,6 +28,7 @@ in
|
|||||||
|
|
||||||
enterShell = ''
|
enterShell = ''
|
||||||
export UV_PROJECT_ENVIRONMENT=$MAMBA_ROOT_PREFIX/envs/mini-nav
|
export UV_PROJECT_ENVIRONMENT=$MAMBA_ROOT_PREFIX/envs/mini-nav
|
||||||
|
unset PYTHONPATH
|
||||||
|
|
||||||
eval "$(micromamba shell hook --shell bash)"
|
eval "$(micromamba shell hook --shell bash)"
|
||||||
micromamba activate mini-nav
|
micromamba activate mini-nav
|
||||||
|
|||||||
126
hw/rtl/noise/noise_mask_bernoulli.sv
Normal file
126
hw/rtl/noise/noise_mask_bernoulli.sv
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
`timescale 1ns / 1ps
|
||||||
|
`include "cam_params.svh"
|
||||||
|
|
||||||
|
module noise_mask_bernoulli #(
|
||||||
|
parameter int HASH_BITS = `HASH_BITS,
|
||||||
|
parameter int PROB_BITS = 8,
|
||||||
|
parameter int PRNG_WORDS = 2,
|
||||||
|
parameter int BITS_PER_CYCLE = 32,
|
||||||
|
parameter logic [63:0] SEED = 64'hD1B5_4A32_9E37_79B9
|
||||||
|
) (
|
||||||
|
input logic clk,
|
||||||
|
input logic rst_n,
|
||||||
|
input logic start_i,
|
||||||
|
input logic [PROB_BITS-1:0] threshold_i,
|
||||||
|
output logic busy_o,
|
||||||
|
output logic done_o,
|
||||||
|
output logic [HASH_BITS-1:0] mask_o
|
||||||
|
);
|
||||||
|
|
||||||
|
localparam int RANDOM_BITS = PRNG_WORDS * 128;
|
||||||
|
localparam int BLOCKS = HASH_BITS / BITS_PER_CYCLE;
|
||||||
|
localparam int BLOCK_INDEX_BITS = (BLOCKS <= 1) ? 1 : $clog2(BLOCKS);
|
||||||
|
localparam logic [BLOCK_INDEX_BITS-1:0] LAST_BLOCK_IDX = BLOCK_INDEX_BITS'(BLOCKS - 1);
|
||||||
|
|
||||||
|
typedef enum logic [1:0] {
|
||||||
|
STATE_IDLE,
|
||||||
|
STATE_PRIME,
|
||||||
|
STATE_RUN
|
||||||
|
} state_t;
|
||||||
|
|
||||||
|
state_t state_q;
|
||||||
|
logic [BLOCK_INDEX_BITS-1:0] block_idx_q;
|
||||||
|
logic [PROB_BITS-1:0] threshold_q;
|
||||||
|
logic [HASH_BITS-1:0] mask_q;
|
||||||
|
logic prng_en;
|
||||||
|
logic [RANDOM_BITS-1:0] random_bits;
|
||||||
|
logic [BITS_PER_CYCLE-1:0] mask_slice;
|
||||||
|
|
||||||
|
assign busy_o = (state_q != STATE_IDLE);
|
||||||
|
assign mask_o = mask_q;
|
||||||
|
assign prng_en = (state_q == STATE_PRIME) ||
|
||||||
|
((state_q == STATE_RUN) && (block_idx_q != LAST_BLOCK_IDX));
|
||||||
|
|
||||||
|
`ifndef SYNTHESIS
|
||||||
|
initial begin
|
||||||
|
if (HASH_BITS <= 0) $fatal(1, "HASH_BITS must be > 0");
|
||||||
|
if (PROB_BITS <= 0) $fatal(1, "PROB_BITS must be > 0");
|
||||||
|
if (PRNG_WORDS <= 0) $fatal(1, "PRNG_WORDS must be > 0");
|
||||||
|
if (BITS_PER_CYCLE <= 0) $fatal(1, "BITS_PER_CYCLE must be > 0");
|
||||||
|
if (HASH_BITS % BITS_PER_CYCLE != 0) $fatal(1, "HASH_BITS must be divisible by BITS_PER_CYCLE");
|
||||||
|
if (RANDOM_BITS != BITS_PER_CYCLE * PROB_BITS) $fatal(1, "PRNG_WORDS*128 must equal BITS_PER_CYCLE*PROB_BITS");
|
||||||
|
if (SEED == 64'h0) $fatal(1, "SEED must be non-zero");
|
||||||
|
for (int w = 0; w < PRNG_WORDS; w++) begin
|
||||||
|
if ({SEED, SEED ^ (64'(w+1))} == 128'h0) $fatal(1, "PRNG seed must be non-zero");
|
||||||
|
for (int w2 = w+1; w2 < PRNG_WORDS; w2++) begin
|
||||||
|
if ({SEED, SEED ^ (64'(w+1))} == {SEED, SEED ^ (64'(w2+1))})
|
||||||
|
$fatal(1, "PRNG seeds must differ");
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
`endif
|
||||||
|
|
||||||
|
always_ff @(posedge clk or negedge rst_n) begin
|
||||||
|
if (!rst_n) begin
|
||||||
|
state_q <= STATE_IDLE;
|
||||||
|
block_idx_q <= '0;
|
||||||
|
threshold_q <= '0;
|
||||||
|
mask_q <= '0;
|
||||||
|
done_o <= 1'b0;
|
||||||
|
end else begin
|
||||||
|
done_o <= 1'b0;
|
||||||
|
|
||||||
|
unique case (state_q)
|
||||||
|
STATE_IDLE: begin
|
||||||
|
block_idx_q <= '0;
|
||||||
|
if (start_i) begin
|
||||||
|
threshold_q <= threshold_i;
|
||||||
|
mask_q <= '0;
|
||||||
|
state_q <= STATE_PRIME;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
STATE_PRIME: begin
|
||||||
|
state_q <= STATE_RUN;
|
||||||
|
end
|
||||||
|
|
||||||
|
STATE_RUN: begin
|
||||||
|
mask_q[block_idx_q * BITS_PER_CYCLE +: BITS_PER_CYCLE] <= mask_slice;
|
||||||
|
|
||||||
|
if (block_idx_q == LAST_BLOCK_IDX) begin
|
||||||
|
state_q <= STATE_IDLE;
|
||||||
|
block_idx_q <= '0;
|
||||||
|
done_o <= 1'b1;
|
||||||
|
end else begin
|
||||||
|
block_idx_q <= block_idx_q + 1'b1;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
default: begin
|
||||||
|
state_q <= STATE_IDLE;
|
||||||
|
block_idx_q <= '0;
|
||||||
|
end
|
||||||
|
endcase
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
generate
|
||||||
|
for (genvar w = 0; w < PRNG_WORDS; w++) begin : gen_prng
|
||||||
|
localparam logic [127:0] PRNG_SEED = {SEED, SEED ^ (64'(w+1))};
|
||||||
|
random128 prng_inst (
|
||||||
|
.clk (clk),
|
||||||
|
.rst_n (rst_n),
|
||||||
|
.enable (prng_en),
|
||||||
|
.seed (PRNG_SEED),
|
||||||
|
.out (random_bits[w*128 +: 128])
|
||||||
|
);
|
||||||
|
end
|
||||||
|
endgenerate
|
||||||
|
|
||||||
|
generate
|
||||||
|
for (genvar i = 0; i < BITS_PER_CYCLE; i++) begin : gen_bernoulli
|
||||||
|
assign mask_slice[i] = (random_bits[i*PROB_BITS +: PROB_BITS] < threshold_q);
|
||||||
|
end
|
||||||
|
endgenerate
|
||||||
|
|
||||||
|
endmodule
|
||||||
@@ -5,6 +5,7 @@ endif
|
|||||||
RTL_RANDOM := $(RTL_ROOT)/random/random128.sv
|
RTL_RANDOM := $(RTL_ROOT)/random/random128.sv
|
||||||
|
|
||||||
RTL_NOISE_MASK := $(RTL_ROOT)/noise/noise_mask_grouped.sv
|
RTL_NOISE_MASK := $(RTL_ROOT)/noise/noise_mask_grouped.sv
|
||||||
|
RTL_BERNOULLI_NOISE_MASK := $(RTL_ROOT)/noise/noise_mask_bernoulli.sv $(RTL_RANDOM)
|
||||||
RTL_WRITE_NOISE := $(RTL_NOISE_MASK) $(RTL_RANDOM) $(RTL_ROOT)/noise/cam_write_noise.sv
|
RTL_WRITE_NOISE := $(RTL_NOISE_MASK) $(RTL_RANDOM) $(RTL_ROOT)/noise/cam_write_noise.sv
|
||||||
RTL_READ_NOISE := $(RTL_NOISE_MASK) $(RTL_RANDOM) $(RTL_ROOT)/noise/cam_read_noise.sv
|
RTL_READ_NOISE := $(RTL_NOISE_MASK) $(RTL_RANDOM) $(RTL_ROOT)/noise/cam_read_noise.sv
|
||||||
|
|
||||||
|
|||||||
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