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
-6
View File
@@ -18,7 +18,6 @@ module ddc_400m_enhanced (
output wire [7:0] ddc_diagnostics,
output wire mixer_saturation,
output wire filter_overflow,
input wire bypass_mode, // Test mode
input wire [1:0] test_mode,
input wire [15:0] test_phase_inc,
@@ -90,10 +89,8 @@ end
// CDC synchronization for control signals (2-stage synchronizers)
(* ASYNC_REG = "TRUE" *) reg [1:0] mixers_enable_sync_chain;
(* ASYNC_REG = "TRUE" *) reg [1:0] bypass_mode_sync_chain;
(* ASYNC_REG = "TRUE" *) reg [1:0] force_saturation_sync_chain;
wire mixers_enable_sync;
wire bypass_mode_sync;
wire force_saturation_sync;
// Debug monitoring signals
@@ -139,17 +136,14 @@ assign debug_mixed_q_trunc = mixed_q[25:8];
// Clock Domain Crossing for Control Signals (2-stage synchronizers)
// ============================================================================
assign mixers_enable_sync = mixers_enable_sync_chain[1];
assign bypass_mode_sync = bypass_mode_sync_chain[1];
assign force_saturation_sync = force_saturation_sync_chain[1];
always @(posedge clk_400m or negedge reset_n_400m) begin
if (!reset_n_400m) begin
mixers_enable_sync_chain <= 2'b00;
bypass_mode_sync_chain <= 2'b00;
force_saturation_sync_chain <= 2'b00;
end else begin
mixers_enable_sync_chain <= {mixers_enable_sync_chain[0], mixers_enable};
bypass_mode_sync_chain <= {bypass_mode_sync_chain[0], bypass_mode};
force_saturation_sync_chain <= {force_saturation_sync_chain[0], force_saturation};
end
end
@@ -83,6 +83,7 @@ module fv_range_bin_decimator (
.range_bin_index (range_bin_index),
.decimation_mode (decimation_mode),
.start_bin (start_bin),
.watchdog_timeout (),
.fv_state (state),
.fv_in_bin_count (in_bin_count),
.fv_group_sample_count (group_sample_count),
+32 -6
View File
@@ -37,6 +37,11 @@ module radar_receiver_final (
input wire [15:0] host_short_listen_cycles,
input wire [5:0] host_chirps_per_elev,
// Digital gain control (Fix 3: between DDC output and matched filter)
// [3]=direction: 0=amplify(left shift), 1=attenuate(right shift)
// [2:0]=shift amount: 0..7 bits. Default 0 = pass-through.
input wire [3:0] host_gain_shift,
// STM32 toggle signals for mode 00 (STM32-driven) pass-through.
// These are CDC-synchronized in radar_system_top.v / radar_transmitter.v
// before reaching this module. In mode 00, the RX mode controller uses
@@ -66,6 +71,11 @@ wire mem_ready;
wire [15:0] adc_i_scaled, adc_q_scaled;
wire adc_valid_sync;
// Gain-controlled signals (between DDC output and matched filter)
wire signed [15:0] gc_i, gc_q;
wire gc_valid;
wire [7:0] gc_saturation_count; // Diagnostic: clipped sample counter
// Reference signals for the processing chain
wire [15:0] long_chirp_real, long_chirp_imag;
wire [15:0] short_chirp_real, short_chirp_imag;
@@ -176,8 +186,7 @@ ddc_400m_enhanced ddc(
.baseband_q(ddc_out_q), // Q output at 100MHz
.baseband_valid_i(ddc_valid_i), // Valid at 100MHz
.baseband_valid_q(ddc_valid_q),
.mixers_enable(1'b1),
.bypass_mode(1'b1)
.mixers_enable(1'b1)
);
ddc_input_interface ddc_if (
@@ -193,6 +202,22 @@ ddc_input_interface ddc_if (
.data_sync_error()
);
// 2b. Digital Gain Control (Fix 3)
// Host-configurable power-of-2 shift between DDC output and matched filter.
// Default gain_shift=0 pass-through (no behavioral change from baseline).
rx_gain_control gain_ctrl (
.clk(clk),
.reset_n(reset_n),
.data_i_in(adc_i_scaled),
.data_q_in(adc_q_scaled),
.valid_in(adc_valid_sync),
.gain_shift(host_gain_shift),
.data_i_out(gc_i),
.data_q_out(gc_q),
.valid_out(gc_valid),
.saturation_count(gc_saturation_count)
);
// 3. Dual Chirp Memory Loader
wire [9:0] sample_addr_from_chain;
@@ -257,9 +282,9 @@ assign range_profile_valid_out = range_valid;
matched_filter_multi_segment mf_dual (
.clk(clk),
.reset_n(reset_n),
.ddc_i({{2{adc_i_scaled[15]}}, adc_i_scaled}),
.ddc_q({{2{adc_q_scaled[15]}}, adc_q_scaled}),
.ddc_valid(adc_valid_sync),
.ddc_i({{2{gc_i[15]}}, gc_i}),
.ddc_q({{2{gc_q[15]}}, gc_q}),
.ddc_valid(gc_valid),
.use_long_chirp(use_long_chirp),
.chirp_counter(chirp_counter),
.mc_new_chirp(mc_new_chirp),
@@ -295,7 +320,8 @@ range_bin_decimator #(
.range_valid_out(decimated_range_valid),
.range_bin_index(decimated_range_bin),
.decimation_mode(2'b01), // Peak detection mode
.start_bin(10'd0)
.start_bin(10'd0),
.watchdog_timeout() // Diagnostic unconnected (monitored via ILA if needed)
);
// ========== FRAME SYNC USING chirp_counter ==========
+90 -34
View File
@@ -161,8 +161,8 @@ wire rx_range_valid;
wire [15:0] rx_doppler_real;
wire [15:0] rx_doppler_imag;
wire rx_doppler_data_valid;
reg rx_cfar_detection;
reg rx_cfar_valid;
reg rx_detect_flag; // Threshold detection result (was rx_cfar_detection)
reg rx_detect_valid; // Detection valid pulse (was rx_cfar_valid)
// Data packing for USB
wire [31:0] usb_range_profile;
@@ -170,8 +170,8 @@ wire usb_range_valid;
wire [15:0] usb_doppler_real;
wire [15:0] usb_doppler_imag;
wire usb_doppler_valid;
wire usb_cfar_detection;
wire usb_cfar_valid;
wire usb_detect_flag; // (was usb_cfar_detection)
wire usb_detect_valid; // (was usb_cfar_valid)
// System status
reg [3:0] status_reg;
@@ -188,9 +188,14 @@ wire [15:0] usb_cmd_value;
// Declared here (before rx_inst) so Icarus Verilog can resolve forward refs.
reg [1:0] host_radar_mode;
reg host_trigger_pulse;
reg [15:0] host_cfar_threshold;
reg [15:0] host_detect_threshold; // (was host_cfar_threshold)
reg [2:0] host_stream_control;
// Fix 3: Digital gain control register
// [3]=direction: 0=amplify, 1=attenuate. [2:0]=shift amount 0..7.
// Default 0x00 = pass-through (no gain change).
reg [3:0] host_gain_shift;
// Gap 2: Host-configurable chirp timing registers
// These override the compile-time defaults in radar_mode_controller when
// written via USB command. Defaults match the parameter values in
@@ -203,6 +208,22 @@ reg [15:0] host_short_listen_cycles; // Opcode 0x14 (default 17450)
reg [5:0] host_chirps_per_elev; // Opcode 0x15 (default 32)
reg host_status_request; // Opcode 0xFF (self-clearing pulse)
// Fix 4: Doppler/chirps mismatch protection
// DOPPLER_FFT_SIZE is compile-time (32). If host sets chirps_per_elev to a
// different value, Doppler accumulation is corrupted. Clamp at command decode
// and flag the mismatch so the host knows.
localparam DOPPLER_FFT_SIZE = 32; // Must match doppler_processor parameter
reg chirps_mismatch_error; // Set if host tried to set chirps != FFT size
// Fix 7: Range-mode register (opcode 0x20)
// Future-proofing for 3km/10km antenna switching.
// 2'b00 = Auto (default system selects based on scene)
// 2'b01 = Short-range (3km)
// 2'b10 = Long-range (10km)
// 2'b11 = Reserved
// Currently a configuration store only antenna/timing switching TBD.
reg [1:0] host_range_mode;
// ============================================================================
// CLOCK BUFFERING
// ============================================================================
@@ -446,6 +467,8 @@ radar_receiver_final rx_inst (
.host_short_chirp_cycles(host_short_chirp_cycles),
.host_short_listen_cycles(host_short_listen_cycles),
.host_chirps_per_elev(host_chirps_per_elev),
// Fix 3: digital gain control
.host_gain_shift(host_gain_shift),
// STM32 toggle signals for RX mode controller (mode 00 pass-through).
// These are the raw GPIO inputs the RX mode controller's edge detectors
// (inside radar_mode_controller) handle debouncing/edge detection.
@@ -464,30 +487,43 @@ assign rx_doppler_real = rx_doppler_output[15:0];
assign rx_doppler_imag = rx_doppler_output[31:16];
assign rx_doppler_data_valid = rx_doppler_valid;
// For this implementation, we'll create a simple CFAR detection simulation
// In a real system, this would come from a CFAR module
reg [7:0] cfar_counter;
reg [16:0] cfar_mag; // Approximate magnitude for threshold detection
// ============================================================================
// THRESHOLD DETECTOR (renamed from misleading "CFAR" — this is NOT CFAR)
// ============================================================================
// Simple magnitude threshold: |I|+|Q| > host_detect_threshold
// This is a placeholder until real CFAR (Gap 1) is implemented.
//
// BUG FIXES applied (Build 22):
// 1. cfar_mag was registered (<=) then compared in same always block,
// causing one-cycle-lag: comparison used PREVIOUS sample's magnitude.
// FIX: compute magnitude combinationally (wire), compare same cycle.
// 2. rx_cfar_detection was never cleared on non-detect cycles — stayed
// latched high after first detection until system reset.
// FIX: clear detection flag every cycle, set only on actual detect.
// Combinational magnitude: no pipeline lag
wire [16:0] detect_mag;
wire [15:0] detect_abs_i = rx_doppler_real[15] ? (~rx_doppler_real + 16'd1) : rx_doppler_real;
wire [15:0] detect_abs_q = rx_doppler_imag[15] ? (~rx_doppler_imag + 16'd1) : rx_doppler_imag;
assign detect_mag = {1'b0, detect_abs_i} + {1'b0, detect_abs_q};
reg [7:0] detect_counter;
always @(posedge clk_100m_buf or negedge sys_reset_n) begin
if (!sys_reset_n) begin
cfar_counter <= 8'd0;
rx_cfar_detection <= 1'b0;
rx_cfar_valid <= 1'b0;
cfar_mag <= 17'd0;
detect_counter <= 8'd0;
rx_detect_flag <= 1'b0;
rx_detect_valid <= 1'b0;
end else begin
rx_cfar_valid <= 1'b0;
// Default: clear every cycle (fixes sticky detection bug)
rx_detect_flag <= 1'b0;
rx_detect_valid <= 1'b0;
// Simple threshold detection on doppler magnitude
if (rx_doppler_valid) begin
// Calculate approximate magnitude (|I| + |Q|)
cfar_mag <= (rx_doppler_real[15] ? -rx_doppler_real : rx_doppler_real) +
(rx_doppler_imag[15] ? -rx_doppler_imag : rx_doppler_imag);
// Threshold detection (Gap 2: uses host-configurable threshold)
if (cfar_mag > {1'b0, host_cfar_threshold}) begin
rx_cfar_detection <= 1'b1;
rx_cfar_valid <= 1'b1;
cfar_counter <= cfar_counter + 1;
// Compare combinational magnitude against threshold (same cycle)
if (detect_mag > {1'b0, host_detect_threshold}) begin
rx_detect_flag <= 1'b1;
rx_detect_valid <= 1'b1;
detect_counter <= detect_counter + 1;
end
end
end
@@ -505,8 +541,8 @@ assign usb_doppler_real = rx_doppler_real;
assign usb_doppler_imag = rx_doppler_imag;
assign usb_doppler_valid = rx_doppler_valid;
assign usb_cfar_detection = rx_cfar_detection;
assign usb_cfar_valid = rx_cfar_valid;
assign usb_detect_flag = rx_detect_flag;
assign usb_detect_valid = rx_detect_valid;
// ============================================================================
// USB DATA INTERFACE INSTANTIATION
@@ -523,8 +559,8 @@ usb_data_interface usb_inst (
.doppler_real(usb_doppler_real),
.doppler_imag(usb_doppler_imag),
.doppler_valid(usb_doppler_valid),
.cfar_detection(usb_cfar_detection),
.cfar_valid(usb_cfar_valid),
.cfar_detection(usb_detect_flag),
.cfar_valid(usb_detect_valid),
// FT601 Interface
.ft601_data(ft601_data),
@@ -554,7 +590,7 @@ usb_data_interface usb_inst (
// Gap 2: Status readback inputs
.status_request(host_status_request),
.status_cfar_threshold(host_cfar_threshold),
.status_cfar_threshold(host_detect_threshold),
.status_stream_ctrl(host_stream_control),
.status_radar_mode(host_radar_mode),
.status_long_chirp(host_long_chirp_cycles),
@@ -562,7 +598,8 @@ usb_data_interface usb_inst (
.status_guard(host_guard_cycles),
.status_short_chirp(host_short_chirp_cycles),
.status_short_listen(host_short_listen_cycles),
.status_chirps_per_elev(host_chirps_per_elev)
.status_chirps_per_elev(host_chirps_per_elev),
.status_range_mode(host_range_mode)
);
// ============================================================================
@@ -608,15 +645,16 @@ wire cmd_valid_100m = cmd_valid_toggle_100m ^ cmd_valid_toggle_100m_prev;
// Sample cmd_data fields when CDC'd valid pulse arrives. Data is stable
// because the read FSM holds cmd_opcode/addr/value until the next command.
// NOTE: reg declarations for host_radar_mode, host_trigger_pulse,
// host_cfar_threshold, host_stream_control are in INTERNAL SIGNALS section
// host_detect_threshold, host_stream_control are in INTERNAL SIGNALS section
// above (before rx_inst) to avoid Icarus Verilog forward-reference errors.
always @(posedge clk_100m_buf or negedge sys_reset_n) begin
if (!sys_reset_n) begin
host_radar_mode <= 2'b01; // Default: auto-scan
host_trigger_pulse <= 1'b0;
host_cfar_threshold <= 16'd10000; // Default threshold
host_detect_threshold <= 16'd10000; // Default threshold
host_stream_control <= 3'b111; // Default: all streams enabled
host_gain_shift <= 4'd0; // Default: pass-through (no gain change)
// Gap 2: chirp timing defaults (match radar_mode_controller parameters)
host_long_chirp_cycles <= 16'd3000;
host_long_listen_cycles <= 16'd13700;
@@ -625,6 +663,8 @@ always @(posedge clk_100m_buf or negedge sys_reset_n) begin
host_short_listen_cycles <= 16'd17450;
host_chirps_per_elev <= 6'd32;
host_status_request <= 1'b0;
chirps_mismatch_error <= 1'b0;
host_range_mode <= 2'b00; // Default: auto
end else begin
host_trigger_pulse <= 1'b0; // Self-clearing pulse
host_status_request <= 1'b0; // Self-clearing pulse
@@ -632,7 +672,7 @@ always @(posedge clk_100m_buf or negedge sys_reset_n) begin
case (usb_cmd_opcode)
8'h01: host_radar_mode <= usb_cmd_value[1:0];
8'h02: host_trigger_pulse <= 1'b1;
8'h03: host_cfar_threshold <= usb_cmd_value;
8'h03: host_detect_threshold <= usb_cmd_value;
8'h04: host_stream_control <= usb_cmd_value[2:0];
// Gap 2: chirp timing configuration
8'h10: host_long_chirp_cycles <= usb_cmd_value;
@@ -640,7 +680,23 @@ always @(posedge clk_100m_buf or negedge sys_reset_n) begin
8'h12: host_guard_cycles <= usb_cmd_value;
8'h13: host_short_chirp_cycles <= usb_cmd_value;
8'h14: host_short_listen_cycles <= usb_cmd_value;
8'h15: host_chirps_per_elev <= usb_cmd_value[5:0];
8'h15: begin
// Fix 4: Clamp chirps_per_elev to DOPPLER_FFT_SIZE.
// If host requests a different value, clamp and set error flag.
if (usb_cmd_value[5:0] > DOPPLER_FFT_SIZE[5:0]) begin
host_chirps_per_elev <= DOPPLER_FFT_SIZE[5:0];
chirps_mismatch_error <= 1'b1;
end else if (usb_cmd_value[5:0] == 6'd0) begin
host_chirps_per_elev <= DOPPLER_FFT_SIZE[5:0];
chirps_mismatch_error <= 1'b1;
end else begin
host_chirps_per_elev <= usb_cmd_value[5:0];
// Clear error only if value matches FFT size exactly
chirps_mismatch_error <= (usb_cmd_value[5:0] != DOPPLER_FFT_SIZE[5:0]);
end
end
8'h16: host_gain_shift <= usb_cmd_value[3:0]; // Fix 3: digital gain
8'h20: host_range_mode <= usb_cmd_value[1:0]; // Fix 7: range mode
8'hFF: host_status_request <= 1'b1; // Gap 2: status readback
default: ;
endcase
+41 -2
View File
@@ -51,7 +51,10 @@ module range_bin_decimator #(
// Configuration
input wire [1:0] decimation_mode, // 00=decimate, 01=peak, 10=average
input wire [9:0] start_bin // First input bin to process
input wire [9:0] start_bin, // First input bin to process
// Diagnostics
output reg watchdog_timeout // Pulses high for 1 cycle on watchdog reset
`ifdef FORMAL
,
@@ -63,6 +66,11 @@ module range_bin_decimator #(
`endif
);
// Fix 5: Watchdog timeout if no valid input arrives for WATCHDOG_LIMIT
// clocks while in ST_PROCESS or ST_SKIP, return to ST_IDLE to prevent hang.
// 256 clocks at 100MHz = 2.56us, well beyond normal inter-sample gap.
localparam WATCHDOG_LIMIT = 10'd256;
// ============================================================================
// INTERNAL SIGNALS
// ============================================================================
@@ -85,6 +93,9 @@ localparam ST_DONE = 3'd4;
// Skip counter for start_bin
reg [9:0] skip_count;
// Watchdog counter counts consecutive clocks with no range_valid_in
reg [9:0] watchdog_count;
`ifdef FORMAL
assign fv_state = state;
assign fv_in_bin_count = in_bin_count;
@@ -128,6 +139,8 @@ always @(posedge clk or negedge reset_n) begin
group_sample_count <= 4'd0;
output_bin_count <= 6'd0;
skip_count <= 10'd0;
watchdog_count <= 10'd0;
watchdog_timeout <= 1'b0;
range_valid_out <= 1'b0;
range_i_out <= 16'd0;
range_q_out <= 16'd0;
@@ -140,8 +153,9 @@ always @(posedge clk or negedge reset_n) begin
decim_i <= 16'd0;
decim_q <= 16'd0;
end else begin
// Default: output not valid
// Default: output not valid, watchdog not triggered
range_valid_out <= 1'b0;
watchdog_timeout <= 1'b0;
case (state)
// ================================================================
@@ -152,6 +166,7 @@ always @(posedge clk or negedge reset_n) begin
group_sample_count <= 4'd0;
output_bin_count <= 6'd0;
skip_count <= 10'd0;
watchdog_count <= 10'd0;
peak_i <= 16'd0;
peak_q <= 16'd0;
peak_mag <= 17'd0;
@@ -198,6 +213,7 @@ always @(posedge clk or negedge reset_n) begin
// ================================================================
ST_SKIP: begin
if (range_valid_in) begin
watchdog_count <= 10'd0;
in_bin_count <= in_bin_count + 1;
if (skip_count >= start_bin) begin
@@ -226,6 +242,17 @@ always @(posedge clk or negedge reset_n) begin
end else begin
skip_count <= skip_count + 1;
end
end else begin
// No valid input increment watchdog
if (watchdog_count >= WATCHDOG_LIMIT - 1) begin
watchdog_timeout <= 1'b1;
state <= ST_IDLE;
`ifdef SIMULATION
$display("[RNG_DECIM] WATCHDOG: timeout in ST_SKIP after %0d idle clocks", WATCHDOG_LIMIT);
`endif
end else begin
watchdog_count <= watchdog_count + 1;
end
end
end
@@ -234,6 +261,7 @@ always @(posedge clk or negedge reset_n) begin
// ================================================================
ST_PROCESS: begin
if (range_valid_in) begin
watchdog_count <= 10'd0;
in_bin_count <= in_bin_count + 1;
// Mode-specific sample processing always process
@@ -273,6 +301,17 @@ always @(posedge clk or negedge reset_n) begin
end else begin
group_sample_count <= group_sample_count + 1;
end
end else begin
// No valid input increment watchdog
if (watchdog_count >= WATCHDOG_LIMIT - 1) begin
watchdog_timeout <= 1'b1;
state <= ST_IDLE;
`ifdef SIMULATION
$display("[RNG_DECIM] WATCHDOG: timeout in ST_PROCESS after %0d idle clocks", WATCHDOG_LIMIT);
`endif
end else begin
watchdog_count <= watchdog_count + 1;
end
end
end
+17 -4
View File
@@ -72,6 +72,7 @@ PROD_RTL=(
usb_data_interface.v
edge_detector.v
radar_mode_controller.v
rx_gain_control.v
)
# Source-only RTL (not instantiated at top level, but should still be lint-clean)
@@ -367,6 +368,14 @@ run_test "Doppler Processor (DSP48)" \
tb/tb_doppler_reg.vvp \
tb/tb_doppler_cosim.v doppler_processor.v xfft_32.v fft_engine.v
run_test "Threshold Detector (detection bugs)" \
tb/tb_threshold_detector.vvp \
tb/tb_threshold_detector.v
run_test "RX Gain Control (digital gain)" \
tb/tb_rx_gain_control.vvp \
tb/tb_rx_gain_control.v rx_gain_control.v
echo ""
# ===========================================================================
@@ -390,7 +399,8 @@ if [[ "$QUICK" -eq 0 ]]; then
cdc_modules.v fir_lowpass.v ddc_input_interface.v \
chirp_memory_loader_param.v latency_buffer.v \
matched_filter_multi_segment.v matched_filter_processing_chain.v \
range_bin_decimator.v doppler_processor.v xfft_32.v fft_engine.v
range_bin_decimator.v doppler_processor.v xfft_32.v fft_engine.v \
rx_gain_control.v
# Golden compare
run_test "Receiver (golden compare)" \
@@ -401,7 +411,8 @@ if [[ "$QUICK" -eq 0 ]]; then
cdc_modules.v fir_lowpass.v ddc_input_interface.v \
chirp_memory_loader_param.v latency_buffer.v \
matched_filter_multi_segment.v matched_filter_processing_chain.v \
range_bin_decimator.v doppler_processor.v xfft_32.v fft_engine.v
range_bin_decimator.v doppler_processor.v xfft_32.v fft_engine.v \
rx_gain_control.v
# Full system top (monitoring-only, legacy)
run_test "System Top (radar_system_tb)" \
@@ -414,7 +425,8 @@ if [[ "$QUICK" -eq 0 ]]; then
chirp_memory_loader_param.v latency_buffer.v \
matched_filter_multi_segment.v matched_filter_processing_chain.v \
range_bin_decimator.v doppler_processor.v xfft_32.v fft_engine.v \
usb_data_interface.v edge_detector.v radar_mode_controller.v
usb_data_interface.v edge_detector.v radar_mode_controller.v \
rx_gain_control.v
# E2E integration (46 strict checks: TX, RX, USB R/W, CDC, safety, reset)
run_test "System E2E (tb_system_e2e)" \
@@ -427,7 +439,8 @@ if [[ "$QUICK" -eq 0 ]]; then
chirp_memory_loader_param.v latency_buffer.v \
matched_filter_multi_segment.v matched_filter_processing_chain.v \
range_bin_decimator.v doppler_processor.v xfft_32.v fft_engine.v \
usb_data_interface.v edge_detector.v radar_mode_controller.v
usb_data_interface.v edge_detector.v radar_mode_controller.v \
rx_gain_control.v
else
echo " (skipped receiver golden + system top + E2E — use without --quick)"
SKIP=$((SKIP + 4))
+95
View File
@@ -0,0 +1,95 @@
`timescale 1ns / 1ps
/**
* rx_gain_control.v
*
* Host-configurable digital gain control for the receive path.
* Placed between DDC output (ddc_input_interface) and matched filter input.
*
* Features:
* - Bidirectional power-of-2 gain shift (arithmetic shift)
* - gain_shift[3] = direction: 0 = left shift (amplify), 1 = right shift (attenuate)
* - gain_shift[2:0] = amount: 0..7 bits
* - Symmetric saturation to ±32767 on overflow (left shift only)
* - Saturation counter: 8-bit, counts samples that clipped (wraps at 255)
* - 1-cycle latency, valid-in/valid-out pipeline
* - Zero-overhead pass-through when gain_shift == 0
*
* Intended insertion point in radar_receiver_final.v:
* ddc_input_interface rx_gain_control matched_filter_multi_segment
*/
module rx_gain_control (
input wire clk,
input wire reset_n,
// Data input (from DDC / ddc_input_interface)
input wire signed [15:0] data_i_in,
input wire signed [15:0] data_q_in,
input wire valid_in,
// Gain configuration (from host via USB command)
// [3] = direction: 0=amplify (left shift), 1=attenuate (right shift)
// [2:0] = shift amount: 0..7 bits
input wire [3:0] gain_shift,
// Data output (to matched filter)
output reg signed [15:0] data_i_out,
output reg signed [15:0] data_q_out,
output reg valid_out,
// Diagnostics
output reg [7:0] saturation_count // Number of clipped samples (wraps at 255)
);
// Decompose gain_shift
wire shift_right = gain_shift[3];
wire [2:0] shift_amt = gain_shift[2:0];
// -------------------------------------------------------------------------
// Combinational shift + saturation
// -------------------------------------------------------------------------
// Use wider intermediates to detect overflow on left shift.
// 24 bits is enough: 16 + 7 shift = 23 significant bits max.
wire signed [23:0] shifted_i;
wire signed [23:0] shifted_q;
assign shifted_i = shift_right ? (data_i_in >>> shift_amt)
: (data_i_in <<< shift_amt);
assign shifted_q = shift_right ? (data_q_in >>> shift_amt)
: (data_q_in <<< shift_amt);
// Saturation: clamp to signed 16-bit range [-32768, +32767]
wire overflow_i = (shifted_i > 24'sd32767) || (shifted_i < -24'sd32768);
wire overflow_q = (shifted_q > 24'sd32767) || (shifted_q < -24'sd32768);
wire signed [15:0] sat_i = overflow_i ? (shifted_i[23] ? -16'sd32768 : 16'sd32767)
: shifted_i[15:0];
wire signed [15:0] sat_q = overflow_q ? (shifted_q[23] ? -16'sd32768 : 16'sd32767)
: shifted_q[15:0];
// -------------------------------------------------------------------------
// Registered output stage (1-cycle latency)
// -------------------------------------------------------------------------
always @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
data_i_out <= 16'sd0;
data_q_out <= 16'sd0;
valid_out <= 1'b0;
saturation_count <= 8'd0;
end else begin
valid_out <= valid_in;
if (valid_in) begin
data_i_out <= sat_i;
data_q_out <= sat_q;
// Count clipped samples (either channel clipping counts as 1)
if ((overflow_i || overflow_q) && (saturation_count != 8'hFF))
saturation_count <= saturation_count + 8'd1;
end
end
end
endmodule
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)
+4 -3
View File
@@ -71,7 +71,8 @@ module usb_data_interface (
input wire [15:0] status_guard, // Current guard cycles
input wire [15:0] status_short_chirp, // Current short chirp cycles
input wire [15:0] status_short_listen, // Current short listen cycles
input wire [5:0] status_chirps_per_elev // Current chirps per elevation
input wire [5:0] status_chirps_per_elev, // Current chirps per elevation
input wire [1:0] status_range_mode // Fix 7: Current range mode (0x20)
);
// USB packet structure (same as before)
@@ -262,8 +263,8 @@ always @(posedge ft601_clk_in or negedge ft601_reset_n) begin
status_words[2] <= {status_guard, status_short_chirp};
// Word 3: {short_listen_cycles[15:0], chirps_per_elev[5:0], 10'b0}
status_words[3] <= {status_short_listen, 10'd0, status_chirps_per_elev};
// Word 4: {system_status placeholder 32'h00000000}
status_words[4] <= 32'h0000_0000;
// Word 4: Fix 7 range_mode in bits [1:0], rest reserved
status_words[4] <= {30'd0, status_range_mode};
end
// Delayed version of sync[1] for edge detection