Integrate CA-CFAR detector: replace fixed-threshold comparator with adaptive sliding-window CFAR engine (22/22 regression PASS)

- Add cfar_ca.v: CA/GO/SO-CFAR with BRAM magnitude buffer, host-configurable
  guard cells, training cells, alpha multiplier, and mode selection
- Replace old threshold detector block in radar_system_top.v with cfar_ca
  instantiation; backward-compatible (cfar_enable defaults to 0)
- Add 5 new host registers: guard (0x21), train (0x22), alpha (0x23),
  mode (0x24), enable (0x25)
- Expose doppler_frame_done_out from radar_receiver_final for CFAR frame sync
- Add tb_cfar_ca.v standalone testbench (14 tests, 24 checks)
- Add Group 14 E2E tests: 13 checks covering range-mode (0x20) and all
  CFAR config registers (0x21-0x25) through full USB command path
- Update run_regression.sh with CFAR in lint, Phase 1, and integration compiles
This commit is contained in:
Jason
2026-03-20 04:57:34 +02:00
parent e93bc33c6c
commit f71923b67d
7 changed files with 1413 additions and 36 deletions
+529
View File
@@ -0,0 +1,529 @@
`timescale 1ns / 1ps
/**
* cfar_ca.v
*
* Cell-Averaging CFAR (Constant False Alarm Rate) Detector
* for the AERIS-10 phased-array radar.
*
* Replaces the simple magnitude threshold detector in radar_system_top.v
* (lines 474-514) with a proper adaptive-threshold CFAR algorithm.
*
* Architecture:
* Phase 1 (BUFFER): As Doppler processor outputs arrive, compute |I|+|Q|
* magnitude and store in BRAM. Address = {range_bin, doppler_bin}.
* When CFAR is disabled, applies simple threshold pass-through.
*
* Phase 2 (CFAR): After frame_complete pulse from Doppler processor,
* process each Doppler column independently:
* a) Read 64 magnitudes from BRAM for one Doppler bin (ST_COL_LOAD)
* b) Compute initial sliding window sums (ST_CFAR_INIT)
* c) Slide CUT through all 64 range bins (ST_CFAR_PROC)
* - 2 sub-cycles per CUT: THRESHOLD compute, then COMPARE + window update
* d) Advance to next Doppler column (ST_COL_NEXT)
*
* CFAR Modes (cfg_cfar_mode):
* 2'b00 = CA-CFAR: noise = leading_sum + lagging_sum
* 2'b01 = GO-CFAR: noise = max(leading_sum * lag_cnt, lagging_sum * lead_cnt)
* normalized picks larger average
* 2'b10 = SO-CFAR: noise = min(leading_sum * lag_cnt, lagging_sum * lead_cnt)
* 2'b11 = Reserved (falls back to CA-CFAR)
*
* Threshold computation:
* threshold = (alpha * noise_sum) >> ALPHA_FRAC_BITS
* Host sets alpha in Q4.4 fixed-point, pre-compensated for training cell count.
* Example: for T=8 cells per side (16 total), desired Pfa=1e-4:
* alpha_statistical 4.88
* alpha_fpga = alpha_statistical / 16 = 0.305 Q4.4 0x05
* Or host can set alpha per training cell if it accounts for count.
*
* Edge handling:
* At range boundaries where the full window doesn't fit, only available
* training cells are used. The noise estimate naturally reduces, raising
* false alarm rate at edges acceptable for radar (edge bins are
* typically clutter).
*
* Timing:
* Phase 2 takes ~(66 + T + 2*64) * 32 7000 cycles per frame @ 100 MHz
* = 70 µs. Frame period @ PRF=1932 Hz, 32 chirps = 16.6 ms. Fits easily.
*
* Resources:
* - 1 BRAM18K for magnitude buffer (2048 x 17 bits)
* - 1 DSP48 for alpha multiply
* - ~300 LUTs for FSM + sliding window + comparators
*
* Clock domain: clk (100 MHz, same as Doppler processor)
*/
module cfar_ca #(
parameter NUM_RANGE_BINS = 64,
parameter NUM_DOPPLER_BINS = 32,
parameter MAG_WIDTH = 17,
parameter ALPHA_WIDTH = 8,
parameter MAX_GUARD = 8,
parameter MAX_TRAIN = 16
) (
input wire clk,
input wire reset_n,
// ========== DOPPLER PROCESSOR INPUTS ==========
input wire [31:0] doppler_data,
input wire doppler_valid,
input wire [4:0] doppler_bin_in,
input wire [5:0] range_bin_in,
input wire frame_complete,
// ========== CONFIGURATION ==========
input wire [3:0] cfg_guard_cells,
input wire [4:0] cfg_train_cells,
input wire [ALPHA_WIDTH-1:0] cfg_alpha,
input wire [1:0] cfg_cfar_mode,
input wire cfg_cfar_enable,
input wire [15:0] cfg_simple_threshold,
// ========== DETECTION OUTPUTS ==========
output reg detect_flag,
output reg detect_valid,
output reg [5:0] detect_range,
output reg [4:0] detect_doppler,
output reg [MAG_WIDTH-1:0] detect_magnitude,
output reg [MAG_WIDTH-1:0] detect_threshold,
// ========== STATUS ==========
output reg [15:0] detect_count,
output wire cfar_busy,
output reg [7:0] cfar_status
);
// ============================================================================
// INTERNAL PARAMETERS
// ============================================================================
localparam TOTAL_CELLS = NUM_RANGE_BINS * NUM_DOPPLER_BINS;
localparam ADDR_WIDTH = 11;
localparam COL_BITS = 5;
localparam ROW_BITS = 6;
localparam SUM_WIDTH = MAG_WIDTH + 6; // 23 bits: sum of up to 64 magnitudes
localparam PROD_WIDTH = SUM_WIDTH + ALPHA_WIDTH; // 31 bits
localparam ALPHA_FRAC_BITS = 4; // Q4.4
// ============================================================================
// FSM STATES
// ============================================================================
localparam [3:0] ST_IDLE = 4'd0,
ST_BUFFER = 4'd1,
ST_COL_LOAD = 4'd2,
ST_CFAR_INIT = 4'd3,
ST_CFAR_THR = 4'd4, // Compute threshold
ST_CFAR_CMP = 4'd5, // Compare + update window
ST_COL_NEXT = 4'd6,
ST_DONE = 4'd7;
reg [3:0] state;
assign cfar_busy = (state != ST_IDLE);
// ============================================================================
// MAGNITUDE COMPUTATION (combinational)
// ============================================================================
wire signed [15:0] dop_i = doppler_data[15:0];
wire signed [15:0] dop_q = doppler_data[31:16];
wire [15:0] abs_i = dop_i[15] ? (~dop_i + 16'd1) : dop_i;
wire [15:0] abs_q = dop_q[15] ? (~dop_q + 16'd1) : dop_q;
wire [MAG_WIDTH-1:0] cur_mag = {1'b0, abs_i} + {1'b0, abs_q};
// ============================================================================
// MAGNITUDE BRAM (2048 x 17 bits)
// ============================================================================
reg mag_we;
reg [ADDR_WIDTH-1:0] mag_waddr;
reg [MAG_WIDTH-1:0] mag_wdata;
reg [ADDR_WIDTH-1:0] mag_raddr;
reg [MAG_WIDTH-1:0] mag_rdata;
(* ram_style = "block" *) reg [MAG_WIDTH-1:0] mag_mem [0:TOTAL_CELLS-1];
always @(posedge clk) begin
if (mag_we)
mag_mem[mag_waddr] <= mag_wdata;
mag_rdata <= mag_mem[mag_raddr];
end
// ============================================================================
// COLUMN LINE BUFFER (64 x 17 bits distributed RAM)
// ============================================================================
reg [MAG_WIDTH-1:0] col_buf [0:NUM_RANGE_BINS-1];
reg [ROW_BITS:0] col_load_idx;
// ============================================================================
// SLIDING WINDOW STATE
// ============================================================================
reg [SUM_WIDTH-1:0] leading_sum;
reg [SUM_WIDTH-1:0] lagging_sum;
reg [ROW_BITS:0] leading_count;
reg [ROW_BITS:0] lagging_count;
reg [ROW_BITS:0] cut_idx;
reg [COL_BITS-1:0] col_idx;
// Registered config (captured at frame start)
reg [3:0] r_guard;
reg [4:0] r_train;
reg [ALPHA_WIDTH-1:0] r_alpha;
reg [1:0] r_mode;
reg r_enable;
reg [15:0] r_simple_thr;
// Threshold pipeline register
reg [PROD_WIDTH-1:0] noise_product;
reg [MAG_WIDTH-1:0] adaptive_thr;
// Init counter for computing initial lagging sum
reg [ROW_BITS:0] init_idx;
// ============================================================================
// SLIDING WINDOW DELTA COMPUTATION (combinational)
// ============================================================================
// Compute net delta to leading_sum and lagging_sum when CUT advances by 1.
// All deltas computed combinationally, applied as a single NBA per register.
// Indices of cells entering/leaving the window when CUT moves from k to k+1:
// Leading: new training cell at index k+1-G-1 = k-G (was closest guard cell)
// cell falling off at index k+1-G-T-1 = k-G-T
// Lagging: cell leaving at index k+G+1 (enters guard zone)
// new cell entering at index k+1+G+T (at far end)
wire signed [ROW_BITS+1:0] lead_add_idx = $signed({1'b0, cut_idx}) - $signed({1'b0, r_guard});
wire signed [ROW_BITS+1:0] lead_rem_idx = $signed({1'b0, cut_idx}) - $signed({1'b0, r_guard}) - $signed({1'b0, r_train});
wire signed [ROW_BITS+1:0] lag_rem_idx = $signed({1'b0, cut_idx}) + $signed({1'b0, r_guard}) + 1;
wire signed [ROW_BITS+1:0] lag_add_idx = $signed({1'b0, cut_idx}) + 1 + $signed({1'b0, r_guard}) + $signed({1'b0, r_train});
wire lead_add_valid = (lead_add_idx >= 0) && (lead_add_idx < NUM_RANGE_BINS);
wire lead_rem_valid = (lead_rem_idx >= 0) && (lead_rem_idx < NUM_RANGE_BINS);
wire lag_rem_valid = (lag_rem_idx >= 0) && (lag_rem_idx < NUM_RANGE_BINS);
wire lag_add_valid = (lag_add_idx >= 0) && (lag_add_idx < NUM_RANGE_BINS);
// Safe col_buf read with bounds checking (combinational)
wire [MAG_WIDTH-1:0] lead_add_val = lead_add_valid ? col_buf[lead_add_idx[ROW_BITS-1:0]] : {MAG_WIDTH{1'b0}};
wire [MAG_WIDTH-1:0] lead_rem_val = lead_rem_valid ? col_buf[lead_rem_idx[ROW_BITS-1:0]] : {MAG_WIDTH{1'b0}};
wire [MAG_WIDTH-1:0] lag_rem_val = lag_rem_valid ? col_buf[lag_rem_idx[ROW_BITS-1:0]] : {MAG_WIDTH{1'b0}};
wire [MAG_WIDTH-1:0] lag_add_val = lag_add_valid ? col_buf[lag_add_idx[ROW_BITS-1:0]] : {MAG_WIDTH{1'b0}};
// Net deltas
wire signed [SUM_WIDTH:0] lead_delta = (lead_add_valid ? $signed({1'b0, lead_add_val}) : 0)
- (lead_rem_valid ? $signed({1'b0, lead_rem_val}) : 0);
wire signed [1:0] lead_cnt_delta = (lead_add_valid ? 1 : 0) - (lead_rem_valid ? 1 : 0);
wire signed [SUM_WIDTH:0] lag_delta = (lag_add_valid ? $signed({1'b0, lag_add_val}) : 0)
- (lag_rem_valid ? $signed({1'b0, lag_rem_val}) : 0);
wire signed [1:0] lag_cnt_delta = (lag_add_valid ? 1 : 0) - (lag_rem_valid ? 1 : 0);
// ============================================================================
// NOISE ESTIMATE COMPUTATION (combinational for CFAR mode selection)
// ============================================================================
reg [SUM_WIDTH-1:0] noise_sum_comb;
always @(*) begin
case (r_mode)
2'b00, 2'b11: begin // CA-CFAR
noise_sum_comb = leading_sum + lagging_sum;
end
2'b01: begin // GO-CFAR: pick sum from side with greater average
if (leading_count > 0 && lagging_count > 0) begin
// leading_avg > lagging_avg leading_sum * lagging_count > lagging_sum * leading_count
if (leading_sum * lagging_count > lagging_sum * leading_count)
noise_sum_comb = leading_sum;
else
noise_sum_comb = lagging_sum;
end else if (leading_count > 0)
noise_sum_comb = leading_sum;
else
noise_sum_comb = lagging_sum;
end
2'b10: begin // SO-CFAR: pick sum from side with smaller average
if (leading_count > 0 && lagging_count > 0) begin
if (leading_sum * lagging_count < lagging_sum * leading_count)
noise_sum_comb = leading_sum;
else
noise_sum_comb = lagging_sum;
end else if (leading_count > 0)
noise_sum_comb = leading_sum;
else
noise_sum_comb = lagging_sum;
end
default:
noise_sum_comb = leading_sum + lagging_sum;
endcase
end
// ============================================================================
// MAIN FSM
// ============================================================================
always @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
state <= ST_IDLE;
detect_flag <= 1'b0;
detect_valid <= 1'b0;
detect_range <= 6'd0;
detect_doppler <= 5'd0;
detect_magnitude <= {MAG_WIDTH{1'b0}};
detect_threshold <= {MAG_WIDTH{1'b0}};
detect_count <= 16'd0;
cfar_status <= 8'd0;
mag_we <= 1'b0;
mag_waddr <= {ADDR_WIDTH{1'b0}};
mag_wdata <= {MAG_WIDTH{1'b0}};
mag_raddr <= {ADDR_WIDTH{1'b0}};
col_load_idx <= 0;
col_idx <= 0;
cut_idx <= 0;
leading_sum <= 0;
lagging_sum <= 0;
leading_count <= 0;
lagging_count <= 0;
init_idx <= 0;
noise_product <= 0;
adaptive_thr <= 0;
r_guard <= 4'd2;
r_train <= 5'd8;
r_alpha <= 8'h30;
r_mode <= 2'b00;
r_enable <= 1'b0;
r_simple_thr <= 16'd10000;
end else begin
// Defaults: clear one-shot outputs
detect_valid <= 1'b0;
detect_flag <= 1'b0;
mag_we <= 1'b0;
case (state)
// ================================================================
// ST_IDLE: Wait for first Doppler output
// ================================================================
ST_IDLE: begin
cfar_status <= 8'd0;
if (doppler_valid) begin
// Capture configuration at frame start
r_guard <= cfg_guard_cells;
r_train <= (cfg_train_cells == 0) ? 5'd1 : cfg_train_cells;
r_alpha <= cfg_alpha;
r_mode <= cfg_cfar_mode;
r_enable <= cfg_cfar_enable;
r_simple_thr <= cfg_simple_threshold;
// Buffer first sample
mag_we <= 1'b1;
mag_waddr <= {range_bin_in, doppler_bin_in};
mag_wdata <= cur_mag;
// Simple threshold pass-through when CFAR disabled
if (!cfg_cfar_enable) begin
detect_flag <= (cur_mag > {1'b0, cfg_simple_threshold});
detect_valid <= 1'b1;
detect_range <= range_bin_in;
detect_doppler <= doppler_bin_in;
detect_magnitude <= cur_mag;
detect_threshold <= {1'b0, cfg_simple_threshold};
if (cur_mag > {1'b0, cfg_simple_threshold})
detect_count <= detect_count + 1;
end
state <= ST_BUFFER;
end
end
// ================================================================
// ST_BUFFER: Store magnitudes until frame complete
// ================================================================
ST_BUFFER: begin
cfar_status <= {4'd1, 4'd0};
if (doppler_valid) begin
mag_we <= 1'b1;
mag_waddr <= {range_bin_in, doppler_bin_in};
mag_wdata <= cur_mag;
if (!r_enable) begin
detect_flag <= (cur_mag > {1'b0, r_simple_thr});
detect_valid <= 1'b1;
detect_range <= range_bin_in;
detect_doppler <= doppler_bin_in;
detect_magnitude <= cur_mag;
detect_threshold <= {1'b0, r_simple_thr};
if (cur_mag > {1'b0, r_simple_thr})
detect_count <= detect_count + 1;
end
end
if (frame_complete) begin
if (r_enable) begin
col_idx <= 0;
col_load_idx <= 0;
mag_raddr <= {6'd0, 5'd0};
state <= ST_COL_LOAD;
end else begin
state <= ST_DONE;
end
end
end
// ================================================================
// ST_COL_LOAD: Read one Doppler column from BRAM
// ================================================================
// BRAM has 1-cycle read latency. Pipeline: present addr cycle N,
// capture data cycle N+1.
ST_COL_LOAD: begin
cfar_status <= {4'd2, 1'b0, col_idx[2:0]};
if (col_load_idx == 0) begin
// First address already presented, advance to range=1
mag_raddr <= {6'd1, col_idx};
col_load_idx <= 1;
end else if (col_load_idx <= NUM_RANGE_BINS) begin
// Capture previous read
col_buf[col_load_idx - 1] <= mag_rdata;
if (col_load_idx < NUM_RANGE_BINS) begin
mag_raddr <= {col_load_idx[ROW_BITS-1:0] + 6'd1, col_idx};
end
col_load_idx <= col_load_idx + 1;
end
if (col_load_idx == NUM_RANGE_BINS + 1) begin
// Column fully loaded initialize CFAR window
state <= ST_CFAR_INIT;
init_idx <= 0;
leading_sum <= 0;
lagging_sum <= 0;
leading_count <= 0;
lagging_count <= 0;
cut_idx <= 0;
end
end
// ================================================================
// ST_CFAR_INIT: Compute initial window sums for CUT=0
// ================================================================
// CUT=0 has no leading cells. Lagging cells are at
// indices [guard+1 .. guard+train] (if they exist).
// Iterate one training cell per cycle.
ST_CFAR_INIT: begin
cfar_status <= {4'd3, 1'b0, col_idx[2:0]};
if (init_idx < r_train) begin
if ((r_guard + 1 + init_idx) < NUM_RANGE_BINS) begin
lagging_sum <= lagging_sum + col_buf[r_guard + 1 + init_idx];
lagging_count <= lagging_count + 1;
end
init_idx <= init_idx + 1;
end else begin
// Initial sums ready begin CFAR sliding
state <= ST_CFAR_THR;
end
end
// ================================================================
// ST_CFAR_THR: Compute adaptive threshold for current CUT
// ================================================================
// Register the alpha * noise product. Result used next cycle.
ST_CFAR_THR: begin
cfar_status <= {4'd4, 1'b0, col_idx[2:0]};
noise_product <= r_alpha * noise_sum_comb;
state <= ST_CFAR_CMP;
end
// ================================================================
// ST_CFAR_CMP: Compare CUT against threshold + update window
// ================================================================
ST_CFAR_CMP: begin
cfar_status <= {4'd5, 1'b0, col_idx[2:0]};
// Threshold = noise_product >> ALPHA_FRAC_BITS
// Saturate to MAG_WIDTH bits
if (noise_product[PROD_WIDTH-1:ALPHA_FRAC_BITS+MAG_WIDTH] != 0)
adaptive_thr <= {MAG_WIDTH{1'b1}}; // Saturate
else
adaptive_thr <= noise_product[ALPHA_FRAC_BITS +: MAG_WIDTH];
// Output detection result
detect_magnitude <= col_buf[cut_idx[ROW_BITS-1:0]];
detect_range <= cut_idx[ROW_BITS-1:0];
detect_doppler <= col_idx;
detect_valid <= 1'b1;
// Compare: threshold computed this cycle from noise_product
begin : threshold_compare
reg [MAG_WIDTH-1:0] thr_val;
if (noise_product[PROD_WIDTH-1:ALPHA_FRAC_BITS+MAG_WIDTH] != 0)
thr_val = {MAG_WIDTH{1'b1}};
else
thr_val = noise_product[ALPHA_FRAC_BITS +: MAG_WIDTH];
detect_threshold <= thr_val;
if (col_buf[cut_idx[ROW_BITS-1:0]] > thr_val) begin
detect_flag <= 1'b1;
detect_count <= detect_count + 1;
end
end
// Update sliding window for next CUT
if (cut_idx < NUM_RANGE_BINS - 1) begin
// Apply pre-computed deltas (single NBA per register)
leading_sum <= $unsigned($signed({1'b0, leading_sum}) + lead_delta);
leading_count <= $unsigned($signed({1'b0, leading_count}) + {{(ROW_BITS){lead_cnt_delta[1]}}, lead_cnt_delta});
lagging_sum <= $unsigned($signed({1'b0, lagging_sum}) + lag_delta);
lagging_count <= $unsigned($signed({1'b0, lagging_count}) + {{(ROW_BITS){lag_cnt_delta[1]}}, lag_cnt_delta});
cut_idx <= cut_idx + 1;
state <= ST_CFAR_THR;
end else begin
state <= ST_COL_NEXT;
end
end
// ================================================================
// ST_COL_NEXT: Advance to next Doppler column or finish
// ================================================================
ST_COL_NEXT: begin
if (col_idx < NUM_DOPPLER_BINS - 1) begin
col_idx <= col_idx + 1;
col_load_idx <= 0;
mag_raddr <= {6'd0, col_idx + 5'd1};
state <= ST_COL_LOAD;
end else begin
state <= ST_DONE;
end
end
// ================================================================
// ST_DONE: Frame complete, return to idle
// ================================================================
ST_DONE: begin
cfar_status <= 8'd0;
state <= ST_IDLE;
`ifdef SIMULATION
$display("[CFAR] Frame complete: %0d total detections", detect_count);
`endif
end
default: state <= ST_IDLE;
endcase
end
end
// ============================================================================
// BRAM + LINE BUFFER INITIALIZATION (simulation only)
// ============================================================================
`ifdef SIMULATION
integer init_i;
initial begin
for (init_i = 0; init_i < TOTAL_CELLS; init_i = init_i + 1)
mag_mem[init_i] = 0;
for (init_i = 0; init_i < NUM_RANGE_BINS; init_i = init_i + 1)
col_buf[init_i] = 0;
end
`endif
endmodule
+5 -1
View File
@@ -48,7 +48,10 @@ module radar_receiver_final (
// these to synchronize receiver processing with STM32-timed chirps.
input wire stm32_new_chirp_rx,
input wire stm32_new_elevation_rx,
input wire stm32_new_azimuth_rx
input wire stm32_new_azimuth_rx,
// CFAR integration: expose Doppler frame_complete to top level
output wire doppler_frame_done_out
);
// ========== INTERNAL SIGNALS ==========
@@ -91,6 +94,7 @@ wire doppler_spectrum_valid;
wire [4:0] doppler_bin_out;
wire doppler_processing;
wire doppler_frame_done;
assign doppler_frame_done_out = doppler_frame_done;
// ========== RANGE BIN DECIMATOR SIGNALS ==========
wire signed [15:0] decimated_range_i;
+75 -31
View File
@@ -164,6 +164,9 @@ wire rx_doppler_data_valid;
reg rx_detect_flag; // Threshold detection result (was rx_cfar_detection)
reg rx_detect_valid; // Detection valid pulse (was rx_cfar_valid)
// Frame-complete signal from Doppler processor (for CFAR)
wire rx_frame_complete;
// Data packing for USB
wire [31:0] usb_range_profile;
wire usb_range_valid;
@@ -224,6 +227,13 @@ reg chirps_mismatch_error; // Set if host tried to set chirps != FFT
// Currently a configuration store only antenna/timing switching TBD.
reg [1:0] host_range_mode;
// CFAR configuration registers (host-configurable via USB)
reg [3:0] host_cfar_guard; // Opcode 0x21: guard cells per side (0..8)
reg [4:0] host_cfar_train; // Opcode 0x22: training cells per side (1..16)
reg [7:0] host_cfar_alpha; // Opcode 0x23: threshold multiplier (Q4.4)
reg [1:0] host_cfar_mode; // Opcode 0x24: 00=CA, 01=GO, 10=SO
reg host_cfar_enable; // Opcode 0x25: 1=CFAR, 0=simple threshold
// ============================================================================
// CLOCK BUFFERING
// ============================================================================
@@ -474,7 +484,9 @@ radar_receiver_final rx_inst (
// (inside radar_mode_controller) handle debouncing/edge detection.
.stm32_new_chirp_rx(stm32_new_chirp),
.stm32_new_elevation_rx(stm32_new_elevation),
.stm32_new_azimuth_rx(stm32_new_azimuth)
.stm32_new_azimuth_rx(stm32_new_azimuth),
// CFAR: Doppler frame-complete pulse
.doppler_frame_done_out(rx_frame_complete)
);
// ============================================================================
@@ -488,44 +500,64 @@ assign rx_doppler_imag = rx_doppler_output[31:16];
assign rx_doppler_data_valid = rx_doppler_valid;
// ============================================================================
// THRESHOLD DETECTOR (renamed from misleading "CFAR" — this is NOT CFAR)
// CFAR DETECTOR (replaces simple threshold detector)
// ============================================================================
// 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.
// Cell-Averaging CFAR with CA/GO/SO modes. When cfg_cfar_enable=0,
// falls back to simple magnitude threshold (backward-compatible).
// See cfar_ca.v for architecture details.
// 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};
wire cfar_detect_flag;
wire cfar_detect_valid;
wire [5:0] cfar_detect_range;
wire [4:0] cfar_detect_doppler;
wire [16:0] cfar_detect_magnitude;
wire [16:0] cfar_detect_threshold;
wire [15:0] cfar_detect_count;
wire cfar_busy_w;
wire [7:0] cfar_status_w;
reg [7:0] detect_counter;
cfar_ca cfar_inst (
.clk(clk_100m_buf),
.reset_n(sys_reset_n),
// Doppler processor outputs
.doppler_data(rx_doppler_output),
.doppler_valid(rx_doppler_valid),
.doppler_bin_in(rx_doppler_bin),
.range_bin_in(rx_range_bin),
.frame_complete(rx_frame_complete),
// Configuration
.cfg_guard_cells(host_cfar_guard),
.cfg_train_cells(host_cfar_train),
.cfg_alpha(host_cfar_alpha),
.cfg_cfar_mode(host_cfar_mode),
.cfg_cfar_enable(host_cfar_enable),
.cfg_simple_threshold(host_detect_threshold),
// Detection outputs
.detect_flag(cfar_detect_flag),
.detect_valid(cfar_detect_valid),
.detect_range(cfar_detect_range),
.detect_doppler(cfar_detect_doppler),
.detect_magnitude(cfar_detect_magnitude),
.detect_threshold(cfar_detect_threshold),
// Status
.detect_count(cfar_detect_count),
.cfar_busy(cfar_busy_w),
.cfar_status(cfar_status_w)
);
// Connect CFAR outputs to existing detection signals
// (rx_detect_flag/valid are regs — drive them from CFAR combinationally)
always @(posedge clk_100m_buf or negedge sys_reset_n) begin
if (!sys_reset_n) begin
detect_counter <= 8'd0;
rx_detect_flag <= 1'b0;
rx_detect_valid <= 1'b0;
end else begin
// Default: clear every cycle (fixes sticky detection bug)
rx_detect_flag <= 1'b0;
rx_detect_valid <= 1'b0;
if (rx_doppler_valid) begin
// 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
rx_detect_flag <= cfar_detect_flag;
rx_detect_valid <= cfar_detect_valid;
end
end
@@ -665,6 +697,12 @@ always @(posedge clk_100m_buf or negedge sys_reset_n) begin
host_status_request <= 1'b0;
chirps_mismatch_error <= 1'b0;
host_range_mode <= 2'b00; // Default: auto
// CFAR defaults (disabled by default — backward-compatible)
host_cfar_guard <= 4'd2; // 2 guard cells each side
host_cfar_train <= 5'd8; // 8 training cells each side
host_cfar_alpha <= 8'h30; // alpha=3.0 (Q4.4)
host_cfar_mode <= 2'b00; // CA-CFAR
host_cfar_enable <= 1'b0; // Disabled (simple threshold)
end else begin
host_trigger_pulse <= 1'b0; // Self-clearing pulse
host_status_request <= 1'b0; // Self-clearing pulse
@@ -697,6 +735,12 @@ always @(posedge clk_100m_buf or negedge sys_reset_n) begin
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
// CFAR configuration opcodes
8'h21: host_cfar_guard <= usb_cmd_value[3:0];
8'h22: host_cfar_train <= usb_cmd_value[4:0];
8'h23: host_cfar_alpha <= usb_cmd_value[7:0];
8'h24: host_cfar_mode <= usb_cmd_value[1:0];
8'h25: host_cfar_enable <= usb_cmd_value[0];
8'hFF: host_status_request <= 1'b1; // Gap 2: status readback
default: ;
endcase
+7 -2
View File
@@ -73,6 +73,7 @@ PROD_RTL=(
edge_detector.v
radar_mode_controller.v
rx_gain_control.v
cfar_ca.v
)
# Source-only RTL (not instantiated at top level, but should still be lint-clean)
@@ -376,6 +377,10 @@ run_test "RX Gain Control (digital gain)" \
tb/tb_rx_gain_control.vvp \
tb/tb_rx_gain_control.v rx_gain_control.v
run_test "CFAR CA Detector" \
tb/tb_cfar_ca.vvp \
tb/tb_cfar_ca.v cfar_ca.v
echo ""
# ===========================================================================
@@ -426,7 +431,7 @@ if [[ "$QUICK" -eq 0 ]]; then
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 \
rx_gain_control.v
rx_gain_control.v cfar_ca.v
# E2E integration (46 strict checks: TX, RX, USB R/W, CDC, safety, reset)
run_test "System E2E (tb_system_e2e)" \
@@ -440,7 +445,7 @@ if [[ "$QUICK" -eq 0 ]]; then
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 \
rx_gain_control.v
rx_gain_control.v cfar_ca.v
else
echo " (skipped receiver golden + system top + E2E — use without --quick)"
SKIP=$((SKIP + 4))
+704
View File
@@ -0,0 +1,704 @@
`timescale 1ns / 1ps
/**
* tb_cfar_ca.v
*
* Comprehensive testbench for cfar_ca.v (Cell-Averaging CFAR Detector).
* Uses [PASS]/[FAIL] markers compatible with run_regression.sh.
*
* Test plan:
* T1: CFAR disabled simple threshold pass-through (backward-compatible)
* T2: CA-CFAR with uniform noise floor no detections
* T3: CA-CFAR with single strong target exactly 1 detection at correct bin
* T4: CA-CFAR with two targets separated by > window size 2 detections
* T5: CA-CFAR edge handling targets at range bin 0 and 63
* T6: GO-CFAR mode with asymmetric noise correct target detection
* T7: SO-CFAR mode lower threshold, more sensitive
* T8: Alpha scaling higher alpha reduces detections
* T9: Guard cell effect target neighbor doesn't raise threshold
* T10: Zero guard, zero train corner case
* T11: Reset during processing clean recovery
* T12: Back-to-back frames second frame processes correctly
* T13: detect_count accumulates across frames
* T14: cfar_busy asserts during processing, deasserts after
*/
module tb_cfar_ca;
// ============================================================================
// PARAMETERS
// ============================================================================
parameter NUM_RANGE = 64;
parameter NUM_DOPPLER = 32;
parameter MAG_W = 17;
parameter ALPHA_W = 8;
parameter CLK_PERIOD = 10; // 100 MHz
// ============================================================================
// DUT SIGNALS
// ============================================================================
reg clk;
reg reset_n;
reg [31:0] doppler_data;
reg doppler_valid;
reg [4:0] doppler_bin_in;
reg [5:0] range_bin_in;
reg frame_complete;
reg [3:0] cfg_guard_cells;
reg [4:0] cfg_train_cells;
reg [ALPHA_W-1:0] cfg_alpha;
reg [1:0] cfg_cfar_mode;
reg cfg_cfar_enable;
reg [15:0] cfg_simple_threshold;
wire detect_flag;
wire detect_valid;
wire [5:0] detect_range;
wire [4:0] detect_doppler;
wire [MAG_W-1:0] detect_magnitude;
wire [MAG_W-1:0] detect_threshold;
wire [15:0] detect_count;
wire cfar_busy;
wire [7:0] cfar_status;
// ============================================================================
// TEST TRACKING
// ============================================================================
integer pass_count;
integer fail_count;
integer test_num;
reg [255:0] test_name;
// Detection capture (flagged detections only)
integer det_cap_count;
reg [5:0] det_cap_range [0:255];
reg [4:0] det_cap_doppler[0:255];
reg [MAG_W-1:0] det_cap_mag[0:255];
reg [MAG_W-1:0] det_cap_thr[0:255];
reg det_cap_flag [0:255];
integer det_total_valid; // Total valid outputs (including non-detections)
// ============================================================================
// DUT INSTANTIATION
// ============================================================================
cfar_ca #(
.NUM_RANGE_BINS(NUM_RANGE),
.NUM_DOPPLER_BINS(NUM_DOPPLER),
.MAG_WIDTH(MAG_W),
.ALPHA_WIDTH(ALPHA_W)
) dut (
.clk(clk),
.reset_n(reset_n),
.doppler_data(doppler_data),
.doppler_valid(doppler_valid),
.doppler_bin_in(doppler_bin_in),
.range_bin_in(range_bin_in),
.frame_complete(frame_complete),
.cfg_guard_cells(cfg_guard_cells),
.cfg_train_cells(cfg_train_cells),
.cfg_alpha(cfg_alpha),
.cfg_cfar_mode(cfg_cfar_mode),
.cfg_cfar_enable(cfg_cfar_enable),
.cfg_simple_threshold(cfg_simple_threshold),
.detect_flag(detect_flag),
.detect_valid(detect_valid),
.detect_range(detect_range),
.detect_doppler(detect_doppler),
.detect_magnitude(detect_magnitude),
.detect_threshold(detect_threshold),
.detect_count(detect_count),
.cfar_busy(cfar_busy),
.cfar_status(cfar_status)
);
// ============================================================================
// CLOCK GENERATION
// ============================================================================
initial clk = 0;
always #(CLK_PERIOD/2) clk = ~clk;
// ============================================================================
// HELPER TASKS
// ============================================================================
task check;
input integer tnum;
input [255:0] desc;
input condition;
begin
if (condition) begin
$display("[PASS(T%0d)] %0s", tnum, desc);
pass_count = pass_count + 1;
end else begin
$display("[FAIL(T%0d)] %0s", tnum, desc);
fail_count = fail_count + 1;
end
end
endtask
task do_reset;
begin
reset_n = 0;
doppler_data = 32'd0;
doppler_valid = 1'b0;
doppler_bin_in = 5'd0;
range_bin_in = 6'd0;
frame_complete = 1'b0;
repeat (5) @(posedge clk);
reset_n = 1;
repeat (2) @(posedge clk);
end
endtask
// Feed one Doppler sample (I/Q packed as {Q, I})
task feed_sample;
input [5:0] rbin;
input [4:0] dbin;
input signed [15:0] i_val;
input signed [15:0] q_val;
begin
@(posedge clk);
doppler_data <= {q_val, i_val};
doppler_valid <= 1'b1;
range_bin_in <= rbin;
doppler_bin_in <= dbin;
@(posedge clk);
doppler_valid <= 1'b0;
end
endtask
// Feed a complete frame with uniform noise + optional targets
// noise_level: base I value for all cells
// num_targets: number of target cells
// tgt_range[0..3], tgt_doppler[0..3], tgt_level[0..3]: target parameters
reg [5:0] tgt_range [0:7];
reg [4:0] tgt_doppler[0:7];
reg [15:0] tgt_level [0:7];
integer num_targets;
task feed_frame;
input [15:0] noise_level;
integer r, d, t;
reg is_target;
reg [15:0] i_val;
begin
// Feed all 64*32 = 2048 samples in Doppler processor output order:
// For each range bin, output all 32 Doppler bins
for (r = 0; r < NUM_RANGE; r = r + 1) begin
for (d = 0; d < NUM_DOPPLER; d = d + 1) begin
is_target = 0;
i_val = noise_level;
for (t = 0; t < num_targets; t = t + 1) begin
if (r == tgt_range[t] && d == tgt_doppler[t]) begin
is_target = 1;
i_val = tgt_level[t];
end
end
feed_sample(r[5:0], d[4:0], $signed(i_val), 16'sd0);
end
end
end
endtask
task pulse_frame_complete;
begin
@(posedge clk);
frame_complete <= 1'b1;
@(posedge clk);
frame_complete <= 1'b0;
end
endtask
// Wait for CFAR processing to complete (with timeout)
task wait_cfar_done;
input integer timeout_cycles;
integer countdown;
begin
countdown = timeout_cycles;
while (cfar_busy && countdown > 0) begin
@(posedge clk);
countdown = countdown - 1;
end
if (countdown == 0)
$display("[WARN] CFAR processing timeout after %0d cycles", timeout_cycles);
end
endtask
// Capture flagged detections during CFAR processing
task capture_detections;
input integer timeout_cycles;
integer countdown;
begin
det_cap_count = 0;
det_total_valid = 0;
countdown = timeout_cycles;
while ((cfar_busy || countdown == timeout_cycles) && countdown > 0) begin
@(posedge clk);
countdown = countdown - 1;
if (detect_valid) begin
det_total_valid = det_total_valid + 1;
// Only capture flagged detections (saves buffer space)
if (detect_flag && det_cap_count < 256) begin
det_cap_range[det_cap_count] = detect_range;
det_cap_doppler[det_cap_count] = detect_doppler;
det_cap_mag[det_cap_count] = detect_magnitude;
det_cap_thr[det_cap_count] = detect_threshold;
det_cap_flag[det_cap_count] = 1'b1;
det_cap_count = det_cap_count + 1;
end
end
end
end
endtask
// Count flagged detections (all captured entries are flagged)
function integer count_flagged_detections;
input integer dummy;
begin
count_flagged_detections = det_cap_count;
end
endfunction
// Find if a specific (range, doppler) was flagged as detection
function integer find_detection;
input [5:0] rbin;
input [4:0] dbin;
integer i;
begin
find_detection = 0;
for (i = 0; i < det_cap_count; i = i + 1) begin
if (det_cap_flag[i] && det_cap_range[i] == rbin && det_cap_doppler[i] == dbin)
find_detection = 1;
end
end
endfunction
// ============================================================================
// MAIN TEST SEQUENCE
// ============================================================================
integer i;
integer flagged;
initial begin
$dumpfile("tb_cfar_ca.vcd");
$dumpvars(0, tb_cfar_ca);
pass_count = 0;
fail_count = 0;
num_targets = 0;
// Default config: CA-CFAR, guard=2, train=8, alpha=3.0 (Q4.4 = 0x30)
cfg_guard_cells = 4'd2;
cfg_train_cells = 5'd8;
cfg_alpha = 8'h30;
cfg_cfar_mode = 2'b00;
cfg_cfar_enable = 1'b1;
cfg_simple_threshold = 16'd5000;
// ================================================================
// T1: CFAR disabled simple threshold pass-through
// ================================================================
test_num = 1;
do_reset;
cfg_cfar_enable = 1'b0;
cfg_simple_threshold = 16'd100;
// Feed a few samples: one below threshold, one above
feed_sample(6'd0, 5'd0, 16'sd50, 16'sd0); // mag=50 < 100 no detect
@(posedge clk); // let detect_valid propagate
check(1, "T1.1: CFAR disabled, below threshold -> no flag", detect_flag == 0);
feed_sample(6'd1, 5'd0, 16'sd200, 16'sd0); // mag=200 > 100 detect
@(posedge clk);
check(1, "T1.2: CFAR disabled, above threshold -> flag=1", detect_flag == 1);
// ================================================================
// T2: CA-CFAR uniform noise no detections
// ================================================================
test_num = 2;
do_reset;
cfg_cfar_enable = 1'b1;
cfg_guard_cells = 4'd2;
cfg_train_cells = 5'd8;
cfg_alpha = 8'h10; // alpha=1.0 in Q4.4 (low threshold still no detect if uniform)
cfg_cfar_mode = 2'b00;
num_targets = 0;
feed_frame(16'd1000); // Uniform noise: all cells = 1000
pulse_frame_complete;
capture_detections(20000);
flagged = count_flagged_detections(0);
// With uniform noise and alpha >= 1.0, threshold noise level
// Some edge cells might detect due to fewer training cells lower threshold
// Check that interior cells (away from edges) have no detections
check(2, "T2: Uniform noise, CA-CFAR: few or no interior detections", flagged < 20);
$display(" [INFO] T2: %0d detections out of %0d valid outputs (uniform noise)", flagged, det_total_valid);
// ================================================================
// T3: CA-CFAR single strong target detection at correct bin
// ================================================================
test_num = 3;
do_reset;
cfg_cfar_enable = 1'b1;
cfg_guard_cells = 4'd2;
cfg_train_cells = 5'd8;
cfg_alpha = 8'h10; // alpha=1.0
cfg_cfar_mode = 2'b00;
num_targets = 1;
tgt_range[0] = 6'd32;
tgt_doppler[0] = 5'd16;
tgt_level[0] = 16'd20000; // 20x noise level
feed_frame(16'd1000);
pulse_frame_complete;
capture_detections(20000);
flagged = count_flagged_detections(0);
check(3, "T3.1: Single strong target detected", flagged > 0);
check(3, "T3.2: Target at (32,16) flagged", find_detection(6'd32, 5'd16) == 1);
$display(" [INFO] T3: %0d total detections", flagged);
// ================================================================
// T4: Two targets well-separated both detected
// ================================================================
test_num = 4;
do_reset;
cfg_cfar_enable = 1'b1;
cfg_guard_cells = 4'd2;
cfg_train_cells = 5'd8;
cfg_alpha = 8'h10;
cfg_cfar_mode = 2'b00;
num_targets = 2;
tgt_range[0] = 6'd10; tgt_doppler[0] = 5'd5; tgt_level[0] = 16'd25000;
tgt_range[1] = 6'd50; tgt_doppler[1] = 5'd20; tgt_level[1] = 16'd25000;
feed_frame(16'd1000);
pulse_frame_complete;
capture_detections(20000);
flagged = count_flagged_detections(0);
check(4, "T4.1: Two targets: at least 2 detections", flagged >= 2);
check(4, "T4.2: Target at (10,5) detected", find_detection(6'd10, 5'd5) == 1);
check(4, "T4.3: Target at (50,20) detected", find_detection(6'd50, 5'd20) == 1);
// ================================================================
// T5: Edge targets range bin 0 and 63
// ================================================================
test_num = 5;
do_reset;
cfg_cfar_enable = 1'b1;
cfg_guard_cells = 4'd2;
cfg_train_cells = 5'd4;
cfg_alpha = 8'h08; // alpha=0.5 (low more sensitive at edges)
cfg_cfar_mode = 2'b00;
num_targets = 2;
tgt_range[0] = 6'd0; tgt_doppler[0] = 5'd0; tgt_level[0] = 16'd20000;
tgt_range[1] = 6'd63; tgt_doppler[1] = 5'd0; tgt_level[1] = 16'd20000;
feed_frame(16'd1000);
pulse_frame_complete;
capture_detections(20000);
// Edge targets have fewer training cells lower threshold should still detect
check(5, "T5.1: Edge target at (0,0) detected", find_detection(6'd0, 5'd0) == 1);
check(5, "T5.2: Edge target at (63,0) detected", find_detection(6'd63, 5'd0) == 1);
// ================================================================
// T6: GO-CFAR with asymmetric noise
// ================================================================
test_num = 6;
do_reset;
cfg_cfar_enable = 1'b1;
cfg_guard_cells = 4'd1;
cfg_train_cells = 5'd4;
cfg_alpha = 8'h10; // alpha=1.0
cfg_cfar_mode = 2'b01; // GO-CFAR
// Target at range=32, noise higher on one side (simulate clutter edge)
// Leading (range<32): noise=1000, Lagging (range>32): noise=5000
// GO-CFAR should use max(leading_avg, lagging_avg) = lagging
// Target must exceed the higher threshold
num_targets = 1;
tgt_range[0] = 6'd32; tgt_doppler[0] = 5'd0; tgt_level[0] = 16'd25000;
// Custom frame: asymmetric noise
begin : t6_feed
integer r, d;
reg [15:0] noise;
for (r = 0; r < NUM_RANGE; r = r + 1) begin
for (d = 0; d < NUM_DOPPLER; d = d + 1) begin
if (r == 32 && d == 0)
noise = 16'd25000;
else if (r < 32)
noise = 16'd1000;
else
noise = 16'd5000;
feed_sample(r[5:0], d[4:0], $signed(noise), 16'sd0);
end
end
end
pulse_frame_complete;
capture_detections(20000);
check(6, "T6: GO-CFAR with asymmetric noise: target at (32,0) detected", find_detection(6'd32, 5'd0) == 1);
// ================================================================
// T7: SO-CFAR more sensitive (lower threshold)
// ================================================================
test_num = 7;
do_reset;
cfg_cfar_enable = 1'b1;
cfg_guard_cells = 4'd1;
cfg_train_cells = 5'd4;
cfg_alpha = 8'h10;
cfg_cfar_mode = 2'b10; // SO-CFAR
// Same asymmetric scene SO-CFAR uses min(leading_avg, lagging_avg)
// Threshold lower should detect more easily
num_targets = 1;
tgt_range[0] = 6'd32; tgt_doppler[0] = 5'd0; tgt_level[0] = 16'd8000;
begin : t7_feed
integer r, d;
reg [15:0] noise;
for (r = 0; r < NUM_RANGE; r = r + 1) begin
for (d = 0; d < NUM_DOPPLER; d = d + 1) begin
if (r == 32 && d == 0)
noise = 16'd8000;
else if (r < 32)
noise = 16'd1000;
else
noise = 16'd5000;
feed_sample(r[5:0], d[4:0], $signed(noise), 16'sd0);
end
end
end
pulse_frame_complete;
capture_detections(20000);
check(7, "T7: SO-CFAR: target at (32,0) with modest level detected", find_detection(6'd32, 5'd0) == 1);
// ================================================================
// T8: High alpha fewer detections
// ================================================================
test_num = 8;
do_reset;
cfg_cfar_enable = 1'b1;
cfg_guard_cells = 4'd2;
cfg_train_cells = 5'd8;
cfg_alpha = 8'hF0; // alpha=15.0 (very high very few detections)
cfg_cfar_mode = 2'b00;
num_targets = 1;
tgt_range[0] = 6'd32; tgt_doppler[0] = 5'd16; tgt_level[0] = 16'd5000;
// Target only 5x noise shouldn't exceed alpha=15 threshold
feed_frame(16'd1000);
pulse_frame_complete;
capture_detections(20000);
flagged = count_flagged_detections(0);
check(8, "T8: High alpha=15.0: weak target NOT detected", find_detection(6'd32, 5'd16) == 0);
$display(" [INFO] T8: %0d detections with alpha=15.0", flagged);
// ================================================================
// T9: Guard cells prevent target leakage
// ================================================================
test_num = 9;
do_reset;
cfg_cfar_enable = 1'b1;
cfg_guard_cells = 4'd3; // 3 guard cells each side
cfg_train_cells = 5'd8;
cfg_alpha = 8'h10; // alpha=1.0
cfg_cfar_mode = 2'b00;
// Strong target at range=32. Neighbors (range 29-31, 33-35) are guard cells
// and should NOT inflate the noise estimate.
num_targets = 1;
tgt_range[0] = 6'd32; tgt_doppler[0] = 5'd0; tgt_level[0] = 16'd30000;
feed_frame(16'd1000);
pulse_frame_complete;
capture_detections(20000);
check(9, "T9: Guard cells: target at (32,0) detected with guard=3", find_detection(6'd32, 5'd0) == 1);
// ================================================================
// T10: Corner case zero guard, minimal train
// ================================================================
test_num = 10;
do_reset;
cfg_cfar_enable = 1'b1;
cfg_guard_cells = 4'd0; // No guard cells
cfg_train_cells = 5'd1; // Minimum training
cfg_alpha = 8'h10; // alpha=1.0
cfg_cfar_mode = 2'b00;
num_targets = 1;
tgt_range[0] = 6'd32; tgt_doppler[0] = 5'd0; tgt_level[0] = 16'd10000;
feed_frame(16'd1000);
pulse_frame_complete;
capture_detections(20000);
check(10, "T10: guard=0, train=1: target detected", find_detection(6'd32, 5'd0) == 1);
// ================================================================
// T11: Reset during processing clean recovery
// ================================================================
test_num = 11;
do_reset;
cfg_cfar_enable = 1'b1;
cfg_guard_cells = 4'd2;
cfg_train_cells = 5'd8;
cfg_alpha = 8'h10;
cfg_cfar_mode = 2'b00;
num_targets = 0;
feed_frame(16'd1000);
pulse_frame_complete;
// Wait partway into CFAR processing
repeat (200) @(posedge clk);
check(11, "T11.1: CFAR busy during processing", cfar_busy == 1);
// Reset mid-processing
reset_n = 0;
repeat (5) @(posedge clk);
reset_n = 1;
repeat (5) @(posedge clk);
check(11, "T11.2: After reset, CFAR not busy", cfar_busy == 0);
check(11, "T11.3: After reset, state is IDLE", dut.state == 4'd0);
// ================================================================
// T12: Back-to-back frames
// ================================================================
test_num = 12;
do_reset;
cfg_cfar_enable = 1'b1;
cfg_guard_cells = 4'd2;
cfg_train_cells = 5'd8;
cfg_alpha = 8'h10;
cfg_cfar_mode = 2'b00;
// Frame 1: no targets
num_targets = 0;
feed_frame(16'd1000);
pulse_frame_complete;
wait_cfar_done(20000);
// Frame 2: one target
num_targets = 1;
tgt_range[0] = 6'd20; tgt_doppler[0] = 5'd10; tgt_level[0] = 16'd25000;
feed_frame(16'd1000);
pulse_frame_complete;
capture_detections(20000);
check(12, "T12: Back-to-back frame 2: target at (20,10) detected", find_detection(6'd20, 5'd10) == 1);
// ================================================================
// T13: detect_count accumulates
// ================================================================
test_num = 13;
do_reset;
cfg_cfar_enable = 1'b1;
cfg_guard_cells = 4'd2;
cfg_train_cells = 5'd8;
cfg_alpha = 8'h10;
cfg_cfar_mode = 2'b00;
num_targets = 1;
tgt_range[0] = 6'd30; tgt_doppler[0] = 5'd0; tgt_level[0] = 16'd20000;
// Frame 1
feed_frame(16'd1000);
pulse_frame_complete;
wait_cfar_done(20000);
begin : t13_save
reg [15:0] count_after_frame1;
count_after_frame1 = detect_count;
$display(" [INFO] T13: detect_count after frame 1 = %0d", count_after_frame1);
// Frame 2 (same target)
feed_frame(16'd1000);
pulse_frame_complete;
wait_cfar_done(20000);
$display(" [INFO] T13: detect_count after frame 2 = %0d", detect_count);
check(13, "T13: detect_count increases after second frame", detect_count > count_after_frame1);
end
// ================================================================
// T14: cfar_busy signal
// ================================================================
test_num = 14;
do_reset;
cfg_cfar_enable = 1'b1;
cfg_guard_cells = 4'd2;
cfg_train_cells = 5'd8;
cfg_alpha = 8'h10;
cfg_cfar_mode = 2'b00;
num_targets = 0;
check(14, "T14.1: Initially not busy", cfar_busy == 0);
// Start feeding data
feed_sample(6'd0, 5'd0, 16'sd1000, 16'sd0);
@(posedge clk);
check(14, "T14.2: Busy after first sample", cfar_busy == 1);
// Feed rest of frame
begin : t14_feed
integer r, d;
for (r = 0; r < NUM_RANGE; r = r + 1) begin
for (d = 0; d < NUM_DOPPLER; d = d + 1) begin
if (r == 0 && d == 0) begin
// Already fed
end else begin
feed_sample(r[5:0], d[4:0], 16'sd1000, 16'sd0);
end
end
end
end
pulse_frame_complete;
wait_cfar_done(20000);
repeat (5) @(posedge clk);
check(14, "T14.3: Not busy after processing complete", cfar_busy == 0);
// ================================================================
// SUMMARY
// ================================================================
$display("");
$display("============================================");
$display(" CFAR CA Testbench Results");
$display("============================================");
$display(" PASS: %0d", pass_count);
$display(" FAIL: %0d", fail_count);
$display("============================================");
if (fail_count > 0)
$display("[FAIL] %0d test(s) failed", fail_count);
else
$display("[PASS] All %0d tests passed", pass_count);
$finish;
end
// ============================================================================
// WATCHDOG TIMEOUT
// ============================================================================
initial begin
#50_000_000; // 50 ms
$display("[FAIL] Global watchdog timeout");
$finish;
end
endmodule
@@ -152,7 +152,9 @@ radar_receiver_final dut (
.host_chirps_per_elev(6'd32),
// Fix 3: digital gain control pass-through for golden reference
.host_gain_shift(4'd0)
.host_gain_shift(4'd0),
// CFAR: frame-complete output (not used in this TB)
.doppler_frame_done_out()
);
// ============================================================================
+89
View File
@@ -23,6 +23,7 @@
* G11: Processing Latency Budgets (2 checks)
* G12: Watchdog / Liveness (2 checks)
* G13: Doppler/Chirps Mismatch Protection (8 checks) [Fix 4]
* G14: CFAR Configuration Registers (13 checks) [CFAR integration]
*
* Compile:
* iverilog -g2001 -DSIMULATION -o tb/tb_system_e2e.vvp \
@@ -1039,6 +1040,94 @@ initial begin
$display("");
// ================================================================
// GROUP 14: CFAR CONFIGURATION REGISTERS (CFAR integration)
// ================================================================
$display("--- Group 14: CFAR Configuration Registers ---");
// G14.1: Verify CFAR defaults after reset (we did a mid-test reset in G9)
// The registers were re-loaded in G9. Send fresh values to verify write path.
// --- Range Mode Register (0x20, Fix 7) ---
// G14.1: Set range_mode to short-range (0x01)
bfm_send_cmd(8'h20, 8'h00, 16'h0001);
check(dut.host_range_mode == 2'b01,
"G14.1: Opcode 0x20 -> host_range_mode = 2'b01 (short)");
// G14.2: Set range_mode to long-range (0x02)
bfm_send_cmd(8'h20, 8'h00, 16'h0002);
check(dut.host_range_mode == 2'b10,
"G14.2: Opcode 0x20 -> host_range_mode = 2'b10 (long)");
// G14.3: Restore range_mode to auto (0x00)
bfm_send_cmd(8'h20, 8'h00, 16'h0000);
check(dut.host_range_mode == 2'b00,
"G14.3: Opcode 0x20 -> host_range_mode = 2'b00 (auto)");
// --- CFAR Guard Cells (0x21) ---
// G14.4: Set guard cells to 4
bfm_send_cmd(8'h21, 8'h00, 16'h0004);
check(dut.host_cfar_guard == 4'd4,
"G14.4: Opcode 0x21 -> host_cfar_guard = 4");
// G14.5: Set guard cells to 0 (valid edge case)
bfm_send_cmd(8'h21, 8'h00, 16'h0000);
check(dut.host_cfar_guard == 4'd0,
"G14.5: Opcode 0x21 -> host_cfar_guard = 0 (edge case)");
// --- CFAR Training Cells (0x22) ---
// G14.6: Set training cells to 16
bfm_send_cmd(8'h22, 8'h00, 16'h0010);
check(dut.host_cfar_train == 5'd16,
"G14.6: Opcode 0x22 -> host_cfar_train = 16");
// G14.7: Set training cells to 1 (minimum)
bfm_send_cmd(8'h22, 8'h00, 16'h0001);
check(dut.host_cfar_train == 5'd1,
"G14.7: Opcode 0x22 -> host_cfar_train = 1 (min)");
// --- CFAR Alpha / Threshold Multiplier (0x23) ---
// G14.8: Set alpha to 0x48 (4.5 in Q4.4)
bfm_send_cmd(8'h23, 8'h00, 16'h0048);
check(dut.host_cfar_alpha == 8'h48,
"G14.8: Opcode 0x23 -> host_cfar_alpha = 0x48 (4.5 Q4.4)");
// G14.9: Set alpha to 0x10 (1.0 in Q4.4)
bfm_send_cmd(8'h23, 8'h00, 16'h0010);
check(dut.host_cfar_alpha == 8'h10,
"G14.9: Opcode 0x23 -> host_cfar_alpha = 0x10 (1.0 Q4.4)");
// --- CFAR Mode (0x24) ---
// G14.10: Set mode to GO-CFAR (0x01)
bfm_send_cmd(8'h24, 8'h00, 16'h0001);
check(dut.host_cfar_mode == 2'b01,
"G14.10: Opcode 0x24 -> host_cfar_mode = 2'b01 (GO-CFAR)");
// G14.11: Set mode to SO-CFAR (0x02)
bfm_send_cmd(8'h24, 8'h00, 16'h0002);
check(dut.host_cfar_mode == 2'b10,
"G14.11: Opcode 0x24 -> host_cfar_mode = 2'b10 (SO-CFAR)");
// --- CFAR Enable (0x25) ---
// G14.12: Enable CFAR
bfm_send_cmd(8'h25, 8'h00, 16'h0001);
check(dut.host_cfar_enable == 1'b1,
"G14.12: Opcode 0x25 -> host_cfar_enable = 1 (CFAR active)");
// G14.13: Disable CFAR (restore default)
bfm_send_cmd(8'h25, 8'h00, 16'h0000);
check(dut.host_cfar_enable == 1'b0,
"G14.13: Opcode 0x25 -> host_cfar_enable = 0 (simple threshold)");
// Restore CFAR registers to safe defaults for remainder of sim
bfm_send_cmd(8'h21, 8'h00, 16'h0002); // guard=2
bfm_send_cmd(8'h22, 8'h00, 16'h0008); // train=8
bfm_send_cmd(8'h23, 8'h00, 16'h0030); // alpha=3.0
bfm_send_cmd(8'h24, 8'h00, 16'h0000); // mode=CA
bfm_send_cmd(8'h25, 8'h00, 16'h0000); // enable=0
$display("");
// ================================================================
// FINAL SUMMARY
// ================================================================