diff --git a/9_Firmware/9_2_FPGA/cfar_ca.v b/9_Firmware/9_2_FPGA/cfar_ca.v new file mode 100644 index 0000000..cc97597 --- /dev/null +++ b/9_Firmware/9_2_FPGA/cfar_ca.v @@ -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 diff --git a/9_Firmware/9_2_FPGA/radar_receiver_final.v b/9_Firmware/9_2_FPGA/radar_receiver_final.v index ffa463d..544258e 100644 --- a/9_Firmware/9_2_FPGA/radar_receiver_final.v +++ b/9_Firmware/9_2_FPGA/radar_receiver_final.v @@ -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; diff --git a/9_Firmware/9_2_FPGA/radar_system_top.v b/9_Firmware/9_2_FPGA/radar_system_top.v index 2e7b76c..f8826e8 100644 --- a/9_Firmware/9_2_FPGA/radar_system_top.v +++ b/9_Firmware/9_2_FPGA/radar_system_top.v @@ -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 + end else begin + 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 diff --git a/9_Firmware/9_2_FPGA/run_regression.sh b/9_Firmware/9_2_FPGA/run_regression.sh index c9e81f4..fe1b749 100755 --- a/9_Firmware/9_2_FPGA/run_regression.sh +++ b/9_Firmware/9_2_FPGA/run_regression.sh @@ -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)) diff --git a/9_Firmware/9_2_FPGA/tb/tb_cfar_ca.v b/9_Firmware/9_2_FPGA/tb/tb_cfar_ca.v new file mode 100644 index 0000000..d4b180d --- /dev/null +++ b/9_Firmware/9_2_FPGA/tb/tb_cfar_ca.v @@ -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 diff --git a/9_Firmware/9_2_FPGA/tb/tb_radar_receiver_final.v b/9_Firmware/9_2_FPGA/tb/tb_radar_receiver_final.v index 43b16a9..adc78e7 100644 --- a/9_Firmware/9_2_FPGA/tb/tb_radar_receiver_final.v +++ b/9_Firmware/9_2_FPGA/tb/tb_radar_receiver_final.v @@ -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() ); // ============================================================================ diff --git a/9_Firmware/9_2_FPGA/tb/tb_system_e2e.v b/9_Firmware/9_2_FPGA/tb/tb_system_e2e.v index 04b71f1..77166c4 100644 --- a/9_Firmware/9_2_FPGA/tb/tb_system_e2e.v +++ b/9_Firmware/9_2_FPGA/tb/tb_system_e2e.v @@ -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 // ================================================================