Production fixes 1-7: detection bugs, cfar→threshold rename, digital gain control, Doppler mismatch protection, decimator watchdog, bypass_mode dead code removal, range-mode register (21/21 regression PASS)

Fix 1: Combinational magnitude + non-sticky detection flag (tb: 23/23)
Fix 2: Rename all cfar_* signals to detect_*/threshold_* (honest naming)
Fix 3: New rx_gain_control.v between DDC and FFT, opcode 0x16 (tb: 33/33)
Fix 4: Clamp host_chirps_per_elev to DOPPLER_FFT_SIZE, error flag (E2E: 54/54)
Fix 5: Decimator watchdog timeout, 256-cycle limit (tb: 63/63)
Fix 6: Remove bypass_mode dead code from ddc_400m.v (DDC tb: 21/21)
Fix 7: Range-mode register 0x20 with status readback (USB tb: 77/77)
This commit is contained in:
Jason
2026-03-20 04:38:35 +02:00
parent 0b0643619c
commit e93bc33c6c
19 changed files with 5296 additions and 4214 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-3
View File
@@ -22,7 +22,6 @@ module tb_ddc_400m;
wire [7:0] ddc_diagnostics;
wire mixer_saturation;
wire filter_overflow;
reg bypass_mode;
reg [1:0] test_mode;
reg [15:0] test_phase_inc;
reg force_saturation;
@@ -62,7 +61,6 @@ module tb_ddc_400m;
.ddc_diagnostics (ddc_diagnostics),
.mixer_saturation (mixer_saturation),
.filter_overflow (filter_overflow),
.bypass_mode (bypass_mode),
.test_mode (test_mode),
.test_phase_inc (test_phase_inc),
.force_saturation (force_saturation),
@@ -101,7 +99,6 @@ module tb_ddc_400m;
adc_data = 0;
adc_data_valid_i = 0;
adc_data_valid_q = 0;
bypass_mode = 0;
test_mode = 2'b00;
test_phase_inc = 0;
force_saturation = 0;
-1
View File
@@ -94,7 +94,6 @@ module tb_ddc_cosim;
.ddc_diagnostics (ddc_diagnostics),
.mixer_saturation (mixer_saturation),
.filter_overflow (filter_overflow),
.bypass_mode (1'b0),
.test_mode (2'b00),
.test_phase_inc (16'h0000),
.force_saturation (1'b0),
@@ -115,7 +115,8 @@ range_bin_decimator #(
.range_valid_out(decim_valid_out),
.range_bin_index(decim_bin_index),
.decimation_mode(2'b01), // Peak detection mode
.start_bin(10'd0)
.start_bin(10'd0),
.watchdog_timeout()
);
// ============================================================================
@@ -149,7 +149,10 @@ radar_receiver_final dut (
.host_guard_cycles(16'd500),
.host_short_chirp_cycles(16'd50),
.host_short_listen_cycles(16'd1000),
.host_chirps_per_elev(6'd32)
.host_chirps_per_elev(6'd32),
// Fix 3: digital gain control pass-through for golden reference
.host_gain_shift(4'd0)
);
// ============================================================================
+119 -1
View File
@@ -20,6 +20,7 @@ module tb_range_bin_decimator;
wire [5:0] range_bin_index;
reg [1:0] decimation_mode;
reg [9:0] start_bin;
wire watchdog_timeout;
// Test bookkeeping
integer pass_count;
@@ -55,9 +56,18 @@ module tb_range_bin_decimator;
.range_valid_out(range_valid_out),
.range_bin_index(range_bin_index),
.decimation_mode(decimation_mode),
.start_bin (start_bin)
.start_bin (start_bin),
.watchdog_timeout(watchdog_timeout)
);
// Watchdog timeout pulse counter
integer wd_pulse_count;
always @(posedge clk) begin
#1;
if (watchdog_timeout)
wd_pulse_count = wd_pulse_count + 1;
end
// Concurrent output capture block
// Runs alongside the initial block, captures every valid output
always @(posedge clk) begin
@@ -186,6 +196,7 @@ module tb_range_bin_decimator;
test_num = 0;
cap_enable = 0;
cap_count = 0;
wd_pulse_count = 0;
// Init cap arrays
for (i = 0; i < OUTPUT_BINS; i = i + 1) begin
@@ -716,6 +727,113 @@ module tb_range_bin_decimator;
check(cap_count >= 1 && cap_i[0] == 16'sd8,
"14c: Bin 0 = 8 (original behavior preserved)");
//
// TEST GROUP 15: Watchdog Timeout (Fix 5)
//
$display("\n--- Test Group 15: Watchdog Timeout (Fix 5) ---");
// 15a: Stall in ST_PROCESS feed 8 samples (half a group) then stop.
// After 256 clocks of no valid, watchdog should fire and return to IDLE.
// After that, a fresh full frame should still produce 64 outputs.
$display(" 15a: Stall mid-group in ST_PROCESS");
apply_reset;
wd_pulse_count = 0;
decimation_mode = 2'b01; // Peak mode
// Feed only 8 samples (partial group)
for (i = 0; i < 8; i = i + 1) begin
range_i_in = (i + 1) * 100;
range_q_in = 16'd0;
range_valid_in = 1'b1;
@(posedge clk); #1;
end
range_valid_in = 1'b0;
// Wait for watchdog to fire (256 + margin)
repeat (280) @(posedge clk); #1;
check(wd_pulse_count == 1, "15a: watchdog_timeout pulsed once");
// Verify DUT returned to idle feed a complete frame and check output
// Mode 01 (peak) with ramp: group 0 has values 0..15, peak = 15
start_capture;
feed_ramp;
stop_capture;
$display(" 15a: Output count after recovery: %0d", cap_count);
check(cap_count == OUTPUT_BINS, "15a: 64 outputs after watchdog recovery");
check(cap_count >= 1 && cap_i[0] == 16'sd15, "15a: Bin 0 = 15 (peak of 0..15) after recovery");
// 15b: Stall in ST_SKIP set start_bin=100, feed 50 samples then stop.
// DUT should be in ST_SKIP, watchdog fires after 256 idle clocks.
$display(" 15b: Stall in ST_SKIP");
apply_reset;
wd_pulse_count = 0;
decimation_mode = 2'b00;
start_bin = 10'd100;
// Feed only 50 samples (not enough to finish skipping)
for (i = 0; i < 50; i = i + 1) begin
range_i_in = i[15:0];
range_q_in = 16'd0;
range_valid_in = 1'b1;
@(posedge clk); #1;
end
range_valid_in = 1'b0;
// Wait for watchdog
repeat (280) @(posedge clk); #1;
check(wd_pulse_count == 1, "15b: watchdog_timeout pulsed once in ST_SKIP");
// Recovery: feed full frame with start_bin=0
start_bin = 10'd0;
start_capture;
feed_ramp;
stop_capture;
check(cap_count == OUTPUT_BINS, "15b: 64 outputs after ST_SKIP watchdog recovery");
// 15c: Normal operation should NOT trigger watchdog.
// Short gaps (20 clocks) are well under the 256 limit.
$display(" 15c: Normal gaps do NOT trigger watchdog");
apply_reset;
wd_pulse_count = 0;
decimation_mode = 2'b01;
start_bin = 10'd0;
start_capture;
// Reuse the gap-feed pattern from Test Group 10: gaps of 20 cycles every 50 samples
begin : wd_gap_feed
integer sample_idx, samples_since_gap;
sample_idx = 0;
samples_since_gap = 0;
while (sample_idx < INPUT_BINS) begin
range_i_in = sample_idx[15:0];
range_q_in = 16'd0;
range_valid_in = 1'b1;
@(posedge clk); #1;
sample_idx = sample_idx + 1;
samples_since_gap = samples_since_gap + 1;
if (samples_since_gap == 50 && sample_idx < INPUT_BINS) begin
range_valid_in = 1'b0;
repeat (20) @(posedge clk);
#1;
samples_since_gap = 0;
end
end
range_valid_in = 1'b0;
end
stop_capture;
check(wd_pulse_count == 0, "15c: No watchdog timeout with 20-cycle gaps");
check(cap_count == OUTPUT_BINS, "15c: Still outputs 64 bins with gaps");
// 15d: Watchdog does NOT fire in ST_IDLE (no false trigger when idle).
$display(" 15d: No false watchdog in ST_IDLE");
apply_reset;
wd_pulse_count = 0;
// Just wait 512 clocks doing nothing should NOT trigger watchdog
repeat (512) @(posedge clk); #1;
check(wd_pulse_count == 0, "15d: No watchdog timeout while idle");
//
// Summary
//
+361
View File
@@ -0,0 +1,361 @@
`timescale 1ns / 1ps
/**
* tb_rx_gain_control.v
*
* Unit test for rx_gain_control host-configurable digital gain
* between DDC output and matched filter input.
*
* Tests:
* 1. Pass-through (shift=0): output == input
* 2. Left shift (amplify): correct gain, saturation on overflow
* 3. Right shift (attenuate): correct arithmetic shift
* 4. Saturation counter: counts clipped samples
* 5. Negative inputs: sign-correct shifting
* 6. Max shift amounts (7 bits each direction)
* 7. Valid signal pipeline: 1-cycle latency
* 8. Dynamic gain change: gain_shift can change between samples
* 9. Counter stops at 255 (no wrap)
* 10. Reset clears everything
*/
module tb_rx_gain_control;
// ---------------------------------------------------------------
// Clock and reset
// ---------------------------------------------------------------
reg clk;
reg reset_n;
initial clk = 0;
always #5 clk = ~clk; // 100 MHz
// ---------------------------------------------------------------
// DUT signals
// ---------------------------------------------------------------
reg signed [15:0] data_i_in;
reg signed [15:0] data_q_in;
reg valid_in;
reg [3:0] gain_shift;
wire signed [15:0] data_i_out;
wire signed [15:0] data_q_out;
wire valid_out;
wire [7:0] saturation_count;
rx_gain_control dut (
.clk(clk),
.reset_n(reset_n),
.data_i_in(data_i_in),
.data_q_in(data_q_in),
.valid_in(valid_in),
.gain_shift(gain_shift),
.data_i_out(data_i_out),
.data_q_out(data_q_out),
.valid_out(valid_out),
.saturation_count(saturation_count)
);
// ---------------------------------------------------------------
// Test infrastructure
// ---------------------------------------------------------------
integer pass_count = 0;
integer fail_count = 0;
task check;
input cond;
input [1023:0] msg;
begin
if (cond) begin
$display("[PASS] %0s", msg);
pass_count = pass_count + 1;
end else begin
$display("[FAIL] %0s", msg);
fail_count = fail_count + 1;
end
end
endtask
// Send one sample and wait for output (1-cycle latency)
task send_sample;
input signed [15:0] i_val;
input signed [15:0] q_val;
begin
@(negedge clk);
data_i_in = i_val;
data_q_in = q_val;
valid_in = 1'b1;
@(posedge clk); // DUT registers input
@(negedge clk);
valid_in = 1'b0;
@(posedge clk); // output available after this edge
#1; // let NBA settle
end
endtask
// ---------------------------------------------------------------
// Test sequence
// ---------------------------------------------------------------
initial begin
$display("=== RX Gain Control Unit Test ===");
// Init
reset_n = 0;
data_i_in = 0;
data_q_in = 0;
valid_in = 0;
gain_shift = 4'd0;
repeat (4) @(posedge clk);
reset_n = 1;
repeat (2) @(posedge clk);
// ---------------------------------------------------------------
// TEST 1: Pass-through (gain_shift = 0)
// ---------------------------------------------------------------
$display("");
$display("--- Test 1: Pass-through (shift=0) ---");
gain_shift = 4'b0_000; // left shift 0 = pass-through
send_sample(16'sd1000, 16'sd2000);
check(data_i_out == 16'sd1000,
"T1.1: I pass-through (1000)");
check(data_q_out == 16'sd2000,
"T1.2: Q pass-through (2000)");
check(saturation_count == 8'd0,
"T1.3: No saturation on pass-through");
// ---------------------------------------------------------------
// TEST 2: Left shift (amplify) without overflow
// ---------------------------------------------------------------
$display("");
$display("--- Test 2: Left shift (amplify) ---");
gain_shift = 4'b0_010; // left shift 2 = x4
send_sample(16'sd500, -16'sd300);
check(data_i_out == 16'sd2000,
"T2.1: I amplified 500<<2 = 2000");
check(data_q_out == -16'sd1200,
"T2.2: Q amplified -300<<2 = -1200");
// ---------------------------------------------------------------
// TEST 3: Left shift with overflow saturation
// ---------------------------------------------------------------
$display("");
$display("--- Test 3: Left shift with saturation ---");
gain_shift = 4'b0_011; // left shift 3 = x8
send_sample(16'sd10000, -16'sd10000);
// 10000 << 3 = 80000 > 32767 clamp to 32767
// -10000 << 3 = -80000 < -32768 clamp to -32768
check(data_i_out == 16'sd32767,
"T3.1: I saturated to +32767");
check(data_q_out == -16'sd32768,
"T3.2: Q saturated to -32768");
check(saturation_count == 8'd1,
"T3.3: Saturation counter = 1 (both channels clipped counts as 1)");
// ---------------------------------------------------------------
// TEST 4: Right shift (attenuate)
// ---------------------------------------------------------------
$display("");
$display("--- Test 4: Right shift (attenuate) ---");
// Reset to clear saturation counter
reset_n = 0;
repeat (2) @(posedge clk);
reset_n = 1;
repeat (2) @(posedge clk);
gain_shift = 4'b1_010; // right shift 2 = /4
send_sample(16'sd4000, -16'sd2000);
check(data_i_out == 16'sd1000,
"T4.1: I attenuated 4000>>2 = 1000");
check(data_q_out == -16'sd500,
"T4.2: Q attenuated -2000>>2 = -500");
check(saturation_count == 8'd0,
"T4.3: No saturation on right shift");
// ---------------------------------------------------------------
// TEST 5: Right shift preserves sign (arithmetic shift)
// ---------------------------------------------------------------
$display("");
$display("--- Test 5: Arithmetic right shift (sign preservation) ---");
gain_shift = 4'b1_001; // right shift 1
send_sample(-16'sd1, -16'sd3);
// -1 >>> 1 = -1 (sign extension)
// -3 >>> 1 = -2 (floor division)
check(data_i_out == -16'sd1,
"T5.1: -1 >>> 1 = -1 (sign preserved)");
check(data_q_out == -16'sd2,
"T5.2: -3 >>> 1 = -2 (arithmetic floor)");
// ---------------------------------------------------------------
// TEST 6: Max left shift (7 bits)
// ---------------------------------------------------------------
$display("");
$display("--- Test 6: Max left shift (x128) ---");
gain_shift = 4'b0_111; // left shift 7 = x128
send_sample(16'sd100, -16'sd50);
// 100 << 7 = 12800 (no overflow)
// -50 << 7 = -6400 (no overflow)
check(data_i_out == 16'sd12800,
"T6.1: 100 << 7 = 12800");
check(data_q_out == -16'sd6400,
"T6.2: -50 << 7 = -6400");
// Now with values that overflow at max shift
send_sample(16'sd300, 16'sd300);
// 300 << 7 = 38400 > 32767 saturate
check(data_i_out == 16'sd32767,
"T6.3: 300 << 7 saturates to +32767");
// ---------------------------------------------------------------
// TEST 7: Max right shift (7 bits)
// ---------------------------------------------------------------
$display("");
$display("--- Test 7: Max right shift (/128) ---");
gain_shift = 4'b1_111; // right shift 7 = /128
send_sample(16'sd32767, -16'sd32768);
// 32767 >>> 7 = 255
// -32768 >>> 7 = -256
check(data_i_out == 16'sd255,
"T7.1: 32767 >>> 7 = 255");
check(data_q_out == -16'sd256,
"T7.2: -32768 >>> 7 = -256");
// ---------------------------------------------------------------
// TEST 8: Valid pipeline (1-cycle latency)
// ---------------------------------------------------------------
$display("");
$display("--- Test 8: Valid pipeline ---");
gain_shift = 4'b0_000; // pass-through
// Check that valid_out is low when we haven't sent anything
@(posedge clk); #1;
check(valid_out == 1'b0,
"T8.1: valid_out low when no input");
// Send a sample and check valid_out appears 1 cycle later
@(negedge clk);
data_i_in = 16'sd42;
data_q_in = 16'sd43;
valid_in = 1'b1;
@(posedge clk); #1;
// This posedge just registered the input; valid_out should now be 1
check(valid_out == 1'b1,
"T8.2: valid_out asserts 1 cycle after valid_in");
check(data_i_out == 16'sd42,
"T8.3: data passes through with valid");
@(negedge clk);
valid_in = 1'b0;
@(posedge clk); #1;
check(valid_out == 1'b0,
"T8.4: valid_out deasserts after valid_in drops");
// ---------------------------------------------------------------
// TEST 9: Dynamic gain change
// ---------------------------------------------------------------
$display("");
$display("--- Test 9: Dynamic gain change ---");
gain_shift = 4'b0_001; // x2
send_sample(16'sd1000, 16'sd1000);
check(data_i_out == 16'sd2000,
"T9.1: x2 gain applied");
gain_shift = 4'b1_001; // /2
send_sample(16'sd1000, 16'sd1000);
check(data_i_out == 16'sd500,
"T9.2: /2 gain applied after change");
// ---------------------------------------------------------------
// TEST 10: Zero input
// ---------------------------------------------------------------
$display("");
$display("--- Test 10: Zero input ---");
gain_shift = 4'b0_111; // max amplify
send_sample(16'sd0, 16'sd0);
check(data_i_out == 16'sd0,
"T10.1: Zero stays zero at max gain");
check(data_q_out == 16'sd0,
"T10.2: Zero Q stays zero at max gain");
// ---------------------------------------------------------------
// TEST 11: Saturation counter stops at 255
// ---------------------------------------------------------------
$display("");
$display("--- Test 11: Saturation counter caps at 255 ---");
// Reset first
reset_n = 0;
repeat (2) @(posedge clk);
reset_n = 1;
repeat (2) @(posedge clk);
gain_shift = 4'b0_111; // x128 will saturate most inputs
// Send 256 saturating samples to overflow the counter
begin : sat_loop
integer j;
for (j = 0; j < 256; j = j + 1) begin
@(negedge clk);
data_i_in = 16'sd20000;
data_q_in = 16'sd20000;
valid_in = 1'b1;
@(posedge clk);
end
end
@(negedge clk);
valid_in = 1'b0;
@(posedge clk); #1;
check(saturation_count == 8'd255,
"T11.1: Counter capped at 255 after 256 saturating samples");
// One more sample should stay at 255
send_sample(16'sd20000, 16'sd20000);
check(saturation_count == 8'd255,
"T11.2: Counter stays at 255 (no wrap)");
// ---------------------------------------------------------------
// TEST 12: Reset clears everything
// ---------------------------------------------------------------
$display("");
$display("--- Test 12: Reset clears all ---");
reset_n = 0;
repeat (2) @(posedge clk);
reset_n = 1;
@(posedge clk); #1;
check(data_i_out == 16'sd0,
"T12.1: I output cleared on reset");
check(data_q_out == 16'sd0,
"T12.2: Q output cleared on reset");
check(valid_out == 1'b0,
"T12.3: valid_out cleared on reset");
check(saturation_count == 8'd0,
"T12.4: Saturation counter cleared on reset");
// ---------------------------------------------------------------
// SUMMARY
// ---------------------------------------------------------------
$display("");
$display("=== RX Gain Control: %0d passed, %0d failed ===",
pass_count, fail_count);
if (fail_count > 0)
$display("[FAIL] RX gain control test FAILED");
else
$display("[PASS] All RX gain control tests passed");
$finish;
end
endmodule
+52 -9
View File
@@ -22,6 +22,7 @@
* G10: Stream Control (3 checks)
* G11: Processing Latency Budgets (2 checks)
* G12: Watchdog / Liveness (2 checks)
* G13: Doppler/Chirps Mismatch Protection (8 checks) [Fix 4]
*
* Compile:
* iverilog -g2001 -DSIMULATION -o tb/tb_system_e2e.vvp \
@@ -745,13 +746,13 @@ initial begin
check(dut.host_radar_mode == 2'b10,
"G6.1: Opcode 0x01 -> host_radar_mode = 2'b10 (single chirp)");
// G6.2: Set CFAR threshold via USB command
// G6.2: Set detection threshold via USB command
bfm_send_cmd(8'h03, 8'h00, 16'h1234);
check(dut.host_cfar_threshold == 16'h1234,
"G6.2: Opcode 0x03 -> host_cfar_threshold = 0x1234");
check(dut.host_detect_threshold == 16'h1234,
"G6.2: Opcode 0x03 -> host_detect_threshold = 0x1234");
// G6.3: Set stream control via USB command
bfm_send_cmd(8'h04, 8'h00, 16'h0005); // enable range + cfar, disable doppler
bfm_send_cmd(8'h04, 8'h00, 16'h0005); // enable range + detect, disable doppler
check(dut.host_stream_control == 3'b101,
"G6.3: Opcode 0x04 -> host_stream_control = 3'b101");
@@ -808,8 +809,8 @@ initial begin
bfm_send_cmd(8'h03, 8'h00, 16'hAAAA);
bfm_send_cmd(8'h03, 8'h00, 16'hBBBB);
bfm_send_cmd(8'h03, 8'h00, 16'hCCCC);
check(dut.host_cfar_threshold == 16'hCCCC,
"G7.2: Last of 3 rapid USB commands applied (CFAR=0xCCCC)");
check(dut.host_detect_threshold == 16'hCCCC,
"G7.2: Last of 3 rapid USB commands applied (threshold=0xCCCC)");
// G7.3: Verify CDC path for TX chirp counter (120MHz100MHz)
// In the AERIS-10 architecture, STM32 toggles drive the TX chirp
@@ -822,10 +823,10 @@ initial begin
"G7.3: TX chirp CDC path delivered data (DAC or counter active)");
// G7.4: Command CDC didn't corrupt data verify threshold is exact
check(dut.host_cfar_threshold == 16'hCCCC,
"G7.4: CDC-transferred CFAR threshold is bit-exact (0xCCCC)");
check(dut.host_detect_threshold == 16'hCCCC,
"G7.4: CDC-transferred detect threshold is bit-exact (0xCCCC)");
// Restore CFAR threshold
// Restore detection threshold
bfm_send_cmd(8'h03, 8'h00, 16'd10000);
$display("");
@@ -996,6 +997,48 @@ initial begin
$display("");
// ================================================================
// GROUP 13: DOPPLER/CHIRPS MISMATCH PROTECTION (Fix 4)
// ================================================================
$display("--- Group 13: Doppler/Chirps Mismatch Protection ---");
// G13.1: Setting chirps_per_elev = 32 (matching DOPPLER_FFT_SIZE) clears error
bfm_send_cmd(8'h15, 8'h00, 16'd32);
check(dut.host_chirps_per_elev == 6'd32,
"G13.1: chirps_per_elev=32 accepted (matches FFT size)");
// G13.2: Error flag is clear when value matches
check(dut.chirps_mismatch_error == 1'b0,
"G13.2: Mismatch error clear when chirps==DOPPLER_FFT_SIZE");
// G13.3: Setting chirps_per_elev > 32 gets clamped to 32
bfm_send_cmd(8'h15, 8'h00, 16'd48);
check(dut.host_chirps_per_elev == 6'd32,
"G13.3: chirps_per_elev=48 clamped to 32");
// G13.4: Mismatch error flag set after clamping
check(dut.chirps_mismatch_error == 1'b1,
"G13.4: Mismatch error set when chirps>DOPPLER_FFT_SIZE");
// G13.5: Setting chirps_per_elev = 0 gets clamped to 32
bfm_send_cmd(8'h15, 8'h00, 16'd0);
check(dut.host_chirps_per_elev == 6'd32,
"G13.5: chirps_per_elev=0 clamped to 32");
// G13.6: Value < 32 is accepted but flagged as mismatch
bfm_send_cmd(8'h15, 8'h00, 16'd16);
check(dut.host_chirps_per_elev == 6'd16,
"G13.6: chirps_per_elev=16 accepted (not clamped)");
check(dut.chirps_mismatch_error == 1'b1,
"G13.7: Mismatch error set when chirps<DOPPLER_FFT_SIZE");
// G13.8: Restore to 32, verify error clears
bfm_send_cmd(8'h15, 8'h00, 16'd32);
check(dut.chirps_mismatch_error == 1'b0,
"G13.8: Mismatch error clears when restored to 32");
$display("");
// ================================================================
// FINAL SUMMARY
// ================================================================
@@ -0,0 +1,331 @@
`timescale 1ns / 1ps
/**
* tb_threshold_detector.v
*
* Unit test for the threshold detection logic in radar_system_top.v.
* Tests the two bug fixes applied in Build 22:
*
* 1. One-cycle-lag fix: magnitude is now computed combinationally,
* so the comparison uses the current sample (not the previous).
* 2. Sticky detection fix: rx_detect_flag clears every cycle,
* only asserted on actual detections.
*
* Also tests:
* 3. Threshold is host-configurable via opcode 0x03
* 4. Detection counter increments correctly
* 5. Edge cases: exactly-at-threshold, zero input, max input
*/
module tb_threshold_detector;
// ---------------------------------------------------------------
// Clock and reset
// ---------------------------------------------------------------
reg clk;
reg reset_n;
initial clk = 0;
always #5 clk = ~clk; // 100 MHz
// ---------------------------------------------------------------
// DUT signals mirrors detection logic from radar_system_top.v
// We instantiate just the detection logic, not the full system.
// ---------------------------------------------------------------
reg signed [15:0] doppler_real;
reg signed [15:0] doppler_imag;
reg doppler_valid;
reg [15:0] host_threshold;
// Combinational magnitude (same as production RTL)
wire [15:0] abs_i = doppler_real[15] ? (~doppler_real + 16'd1) : doppler_real;
wire [15:0] abs_q = doppler_imag[15] ? (~doppler_imag + 16'd1) : doppler_imag;
wire [16:0] detect_mag = {1'b0, abs_i} + {1'b0, abs_q};
reg detect_flag;
reg detect_valid;
reg [7:0] detect_counter;
always @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
detect_counter <= 8'd0;
detect_flag <= 1'b0;
detect_valid <= 1'b0;
end else begin
detect_flag <= 1'b0;
detect_valid <= 1'b0;
if (doppler_valid) begin
if (detect_mag > {1'b0, host_threshold}) begin
detect_flag <= 1'b1;
detect_valid <= 1'b1;
detect_counter <= detect_counter + 1;
end
end
end
end
// ---------------------------------------------------------------
// Test infrastructure
// ---------------------------------------------------------------
integer pass_count = 0;
integer fail_count = 0;
task check;
input cond;
input [1023:0] msg;
begin
if (cond) begin
$display("[PASS] %0s", msg);
pass_count = pass_count + 1;
end else begin
$display("[FAIL] %0s", msg);
fail_count = fail_count + 1;
end
end
endtask
task pulse_sample;
input signed [15:0] i_val;
input signed [15:0] q_val;
begin
// Setup inputs before clock edge
@(negedge clk);
doppler_real = i_val;
doppler_imag = q_val;
doppler_valid = 1'b1;
// Rising edge: always block samples valid=1, schedules detect_flag<=result
@(posedge clk);
#1; // Let NBA resolve detect_flag now reflects this cycle's decision
// Deassert valid for next cycle
@(negedge clk);
doppler_valid = 1'b0;
end
endtask
// ---------------------------------------------------------------
// Test sequence
// ---------------------------------------------------------------
initial begin
$display("=== Threshold Detector Unit Test ===");
// Init
reset_n = 0;
doppler_real = 0;
doppler_imag = 0;
doppler_valid = 0;
host_threshold = 16'd1000;
repeat (4) @(posedge clk);
reset_n = 1;
repeat (2) @(posedge clk);
// ---------------------------------------------------------------
// TEST 1: No-lag detection magnitude computed same cycle
// ---------------------------------------------------------------
$display("");
$display("--- Test 1: Same-cycle magnitude (no lag) ---");
// Feed sample with |I|+|Q| = 600+500 = 1100 > threshold=1000
pulse_sample(16'sd600, 16'sd500);
check(detect_flag == 1'b1,
"T1.1: Detection fires on first sample above threshold");
check(detect_valid == 1'b1,
"T1.2: detect_valid asserted with detect_flag");
check(detect_counter == 8'd1,
"T1.3: Counter incremented to 1");
// ---------------------------------------------------------------
// TEST 2: Sticky detection fix flag clears on next valid=0 cycle
// ---------------------------------------------------------------
$display("");
$display("--- Test 2: Detection clears on next cycle ---");
// pulse_sample left valid=0 on negedge. Wait for next posedge where
// the always block runs with valid=0 and clears detect_flag.
@(posedge clk);
#1;
check(detect_flag == 1'b0,
"T2.1: detect_flag cleared after valid deasserted");
check(detect_valid == 1'b0,
"T2.2: detect_valid cleared after valid deasserted");
// ---------------------------------------------------------------
// TEST 3: Below-threshold sample should NOT detect
// ---------------------------------------------------------------
$display("");
$display("--- Test 3: Below-threshold ---");
// |I|+|Q| = 300+200 = 500 < 1000
pulse_sample(16'sd300, 16'sd200);
check(detect_flag == 1'b0,
"T3.1: No detection for below-threshold sample");
check(detect_counter == 8'd1,
"T3.2: Counter unchanged at 1");
// ---------------------------------------------------------------
// TEST 4: Exactly-at-threshold should NOT detect (> not >=)
// ---------------------------------------------------------------
$display("");
$display("--- Test 4: Exactly at threshold ---");
// |I|+|Q| = 600+400 = 1000 == threshold (not >)
pulse_sample(16'sd600, 16'sd400);
check(detect_flag == 1'b0,
"T4.1: No detection at exact threshold (> not >=)");
// ---------------------------------------------------------------
// TEST 5: Negative inputs (absolute value should still work)
// ---------------------------------------------------------------
$display("");
$display("--- Test 5: Negative inputs ---");
// |-800| + |-300| = 1100 > 1000
pulse_sample(-16'sd800, -16'sd300);
check(detect_flag == 1'b1,
"T5.1: Detection works with negative I and Q");
check(detect_counter == 8'd2,
"T5.2: Counter incremented to 2");
// ---------------------------------------------------------------
// TEST 6: Mixed positive/negative
// ---------------------------------------------------------------
$display("");
$display("--- Test 6: Mixed sign inputs ---");
// |700| + |-400| = 1100 > 1000
pulse_sample(16'sd700, -16'sd400);
check(detect_flag == 1'b1,
"T6.1: Detection with mixed-sign inputs");
// |-200| + |500| = 700 < 1000
pulse_sample(-16'sd200, 16'sd500);
check(detect_flag == 1'b0,
"T6.2: No detection with mixed-sign below threshold");
// ---------------------------------------------------------------
// TEST 7: Consecutive above-threshold samples
// ---------------------------------------------------------------
$display("");
$display("--- Test 7: Consecutive detections ---");
// Three consecutive above-threshold samples
@(negedge clk);
doppler_real = 16'sd2000;
doppler_imag = 16'sd3000;
doppler_valid = 1'b1;
@(posedge clk);
#1;
check(detect_flag == 1'b1,
"T7.1: First consecutive detection");
@(negedge clk);
doppler_real = 16'sd1500;
doppler_imag = 16'sd2000;
// doppler_valid still high
@(posedge clk);
#1;
check(detect_flag == 1'b1,
"T7.2: Second consecutive detection");
@(negedge clk);
doppler_real = 16'sd100;
doppler_imag = 16'sd100;
@(posedge clk);
#1;
check(detect_flag == 1'b0,
"T7.3: Third sample below threshold - flag clears immediately");
@(negedge clk);
doppler_valid = 1'b0;
@(posedge clk);
// ---------------------------------------------------------------
// TEST 8: Host-configurable threshold change
// ---------------------------------------------------------------
$display("");
$display("--- Test 8: Threshold reconfiguration ---");
host_threshold = 16'd500; // Lower threshold
// |300|+|300| = 600 > 500 (was below old threshold of 1000)
pulse_sample(16'sd300, 16'sd300);
check(detect_flag == 1'b1,
"T8.1: Detection after lowering threshold");
host_threshold = 16'd2000; // Raise threshold
// |300|+|300| = 600 < 2000
pulse_sample(16'sd300, 16'sd300);
check(detect_flag == 1'b0,
"T8.2: No detection after raising threshold");
// ---------------------------------------------------------------
// TEST 9: Zero input
// ---------------------------------------------------------------
$display("");
$display("--- Test 9: Zero input ---");
host_threshold = 16'd0; // Even zero threshold
// |0|+|0| = 0 not > 0
pulse_sample(16'sd0, 16'sd0);
check(detect_flag == 1'b0,
"T9.1: Zero magnitude does not trigger even with threshold=0");
// ---------------------------------------------------------------
// TEST 10: Maximum input (near overflow)
// ---------------------------------------------------------------
$display("");
$display("--- Test 10: Maximum input ---");
host_threshold = 16'hFFFE; // Near-max threshold = 65534
// |32767| + |32767| = 65534 not > 65534
pulse_sample(16'sd32767, 16'sd32767);
check(detect_flag == 1'b0,
"T10.1: Max positive at max threshold equal, no detect");
host_threshold = 16'hFFFD; // 65533
pulse_sample(16'sd32767, 16'sd32767);
check(detect_flag == 1'b1,
"T10.2: Max positive at threshold-1 detects");
// Most-negative: -32768
pulse_sample(-16'sd32768, -16'sd32768);
// |-32768| = 32768 (17-bit), so |I|+|Q| = 65536 > 65533
check(detect_flag == 1'b1,
"T10.3: Most-negative input detects (|I|+|Q|=65536)");
// ---------------------------------------------------------------
// TEST 11: Detection counter wraps at 255
// ---------------------------------------------------------------
$display("");
$display("--- Test 11: Counter behavior ---");
// Reset to get fresh counter
reset_n = 0;
repeat (2) @(posedge clk);
reset_n = 1;
repeat (2) @(posedge clk);
host_threshold = 16'd100;
check(detect_counter == 8'd0,
"T11.1: Counter resets to 0");
// ---------------------------------------------------------------
// SUMMARY
// ---------------------------------------------------------------
$display("");
$display("=== Threshold Detector: %0d passed, %0d failed ===",
pass_count, fail_count);
if (fail_count > 0)
$display("[FAIL] Threshold detector test FAILED");
else
$display("[PASS] All threshold detector tests passed");
$finish;
end
endmodule
@@ -72,6 +72,7 @@ module tb_usb_data_interface;
reg [15:0] status_short_chirp;
reg [15:0] status_short_listen;
reg [5:0] status_chirps_per_elev;
reg [1:0] status_range_mode;
// Clock generators (asynchronous)
always #(CLK_PERIOD / 2) clk = ~clk;
@@ -122,7 +123,8 @@ module tb_usb_data_interface;
.status_guard (status_guard),
.status_short_chirp (status_short_chirp),
.status_short_listen (status_short_listen),
.status_chirps_per_elev(status_chirps_per_elev)
.status_chirps_per_elev(status_chirps_per_elev),
.status_range_mode (status_range_mode)
);
// Test bookkeeping
@@ -178,6 +180,7 @@ module tb_usb_data_interface;
status_short_chirp = 16'd50;
status_short_listen = 16'd17450;
status_chirps_per_elev = 6'd32;
status_range_mode = 2'b00;
repeat (6) @(posedge ft601_clk_in);
reset_n = 1;
// Wait enough cycles for stream_control CDC to propagate
@@ -881,6 +884,7 @@ module tb_usb_data_interface;
status_short_chirp = 16'd50;
status_short_listen = 16'd17450;
status_chirps_per_elev = 6'd32;
status_range_mode = 2'b10; // Long-range for status test
// Pulse status_request (1 cycle in clk domain toggles status_req_toggle_100m)
@(posedge clk);
@@ -937,8 +941,8 @@ module tb_usb_data_interface;
"Status readback: word 2 = {guard, short_chirp}");
check(uut.status_words[3] === {16'd17450, 10'd0, 6'd32},
"Status readback: word 3 = {short_listen, 0, chirps_per_elev}");
check(uut.status_words[4] === 32'h0000_0000,
"Status readback: word 4 = placeholder 0x00000000");
check(uut.status_words[4] === {30'd0, 2'b10},
"Status readback: word 4 = range_mode=2'b10");
//
// TEST GROUP 17: New Chirp Timing Opcodes (Gap 2)