diff --git a/9_Firmware/9_2_FPGA/cic_decimator_4x_enhanced.v b/9_Firmware/9_2_FPGA/cic_decimator_4x_enhanced.v index d7bae17..76ade79 100644 --- a/9_Firmware/9_2_FPGA/cic_decimator_4x_enhanced.v +++ b/9_Firmware/9_2_FPGA/cic_decimator_4x_enhanced.v @@ -66,13 +66,13 @@ reg signed [COMB_WIDTH-1:0] comb_delay [0:STAGES-1][0:COMB_DELAY-1]; // Pipeline valid for comb stages 1-4: delayed by 1 cycle vs comb_pipe to // account for CREG+AREG+BREG pipeline inside comb_0_dsp (explicit DSP48E1). // Comb[0] result appears 1 cycle after data_valid_comb_pipe. -(* keep = "true", max_fanout = 4 *) reg data_valid_comb_0_out; +(* keep = "true", max_fanout = 16 *) reg data_valid_comb_0_out; // Enhanced control and monitoring reg [1:0] decimation_counter; -(* keep = "true", max_fanout = 4 *) reg data_valid_delayed; -(* keep = "true", max_fanout = 4 *) reg data_valid_comb; -(* keep = "true", max_fanout = 4 *) reg data_valid_comb_pipe; +(* keep = "true", max_fanout = 16 *) reg data_valid_delayed; +(* keep = "true", max_fanout = 16 *) reg data_valid_comb; +(* keep = "true", max_fanout = 16 *) reg data_valid_comb_pipe; reg [7:0] output_counter; reg [ACC_WIDTH-1:0] max_integrator_value; reg overflow_detected; diff --git a/9_Firmware/9_2_FPGA/constraints/adc_clk_mmcm.xdc b/9_Firmware/9_2_FPGA/constraints/adc_clk_mmcm.xdc index b581940..51717e5 100644 --- a/9_Firmware/9_2_FPGA/constraints/adc_clk_mmcm.xdc +++ b/9_Firmware/9_2_FPGA/constraints/adc_clk_mmcm.xdc @@ -83,3 +83,12 @@ set_false_path -through [get_pins rx_inst/adc/mmcm_inst/mmcm_adc_400m/LOCKED] # Waiving hold on these 8 paths (adc_d_p[0..7] → IDDR) is standard practice # for source-synchronous LVDS ADC interfaces using BUFIO capture. set_false_path -hold -from [get_ports {adc_d_p[*]}] -to [get_clocks adc_dco_p] + +# -------------------------------------------------------------------------- +# Timing margin for 400 MHz CIC critical path +# -------------------------------------------------------------------------- +# The CIC decimator at 400 MHz has near-zero margin (WNS = +0.001 ns in +# Build 26). Adding 200 ps of extra setup uncertainty forces Vivado to +# leave comfortable margin for temperature/voltage/aging variation. +# This is additive to the existing jitter-based uncertainty (~53 ps). +set_clock_uncertainty -setup -add 0.200 [get_clocks clk_mmcm_out0] diff --git a/9_Firmware/9_2_FPGA/constraints/xc7a50t_ftg256.xdc b/9_Firmware/9_2_FPGA/constraints/xc7a50t_ftg256.xdc index 1f1ee5f..c4d61f0 100644 --- a/9_Firmware/9_2_FPGA/constraints/xc7a50t_ftg256.xdc +++ b/9_Firmware/9_2_FPGA/constraints/xc7a50t_ftg256.xdc @@ -222,8 +222,16 @@ set_property IOSTANDARD LVCMOS33 [get_ports {stm32_new_*}] set_property IOSTANDARD LVCMOS33 [get_ports {stm32_mixers_enable}] # reset_n is DIG_4 (PD12) — constrained above in the RESET section -# DIG_5 = H11, DIG_6 = G12, DIG_7 = H12 — available for FPGA→STM32 status -# Currently unused in RTL. Could be connected to status outputs if needed. +# DIG_5 = H11, DIG_6 = G12, DIG_7 = H12 — FPGA→STM32 status outputs +# DIG_5: AGC saturation flag (PD13 on STM32) +# DIG_6: reserved (PD14) +# DIG_7: reserved (PD15) +set_property PACKAGE_PIN H11 [get_ports {gpio_dig5}] +set_property PACKAGE_PIN G12 [get_ports {gpio_dig6}] +set_property PACKAGE_PIN H12 [get_ports {gpio_dig7}] +set_property IOSTANDARD LVCMOS33 [get_ports {gpio_dig*}] +set_property DRIVE 8 [get_ports {gpio_dig*}] +set_property SLEW SLOW [get_ports {gpio_dig*}] # ============================================================================ # ADC INTERFACE (LVDS — Bank 14, VCCO=3.3V) diff --git a/9_Firmware/9_2_FPGA/radar_receiver_final.v b/9_Firmware/9_2_FPGA/radar_receiver_final.v index c417092..4be1571 100644 --- a/9_Firmware/9_2_FPGA/radar_receiver_final.v +++ b/9_Firmware/9_2_FPGA/radar_receiver_final.v @@ -42,6 +42,13 @@ module radar_receiver_final ( // [2:0]=shift amount: 0..7 bits. Default 0 = pass-through. input wire [3:0] host_gain_shift, + // AGC configuration (opcodes 0x28-0x2C, active only when agc_enable=1) + input wire host_agc_enable, // 0x28: 0=manual, 1=auto AGC + input wire [7:0] host_agc_target, // 0x29: target peak magnitude + input wire [3:0] host_agc_attack, // 0x2A: gain-down step on clipping + input wire [3:0] host_agc_decay, // 0x2B: gain-up step when weak + input wire [3:0] host_agc_holdoff, // 0x2C: frames before gain-up + // STM32 toggle signals for mode 00 (STM32-driven) pass-through. // These are CDC-synchronized in radar_system_top.v / radar_transmitter.v // before reaching this module. In mode 00, the RX mode controller uses @@ -60,7 +67,12 @@ module radar_receiver_final ( // ADC raw data tap (clk_100m domain, post-DDC, for self-test / debug) output wire [15:0] dbg_adc_i, // DDC output I (16-bit signed, 100 MHz) output wire [15:0] dbg_adc_q, // DDC output Q (16-bit signed, 100 MHz) - output wire dbg_adc_valid // DDC output valid (100 MHz) + output wire dbg_adc_valid, // DDC output valid (100 MHz) + + // AGC status outputs (for status readback / STM32 outer loop) + output wire [7:0] agc_saturation_count, // Per-frame clipped sample count + output wire [7:0] agc_peak_magnitude, // Per-frame peak (upper 8 bits) + output wire [3:0] agc_current_gain // Effective gain_shift encoding ); // ========== INTERNAL SIGNALS ========== @@ -86,7 +98,9 @@ wire adc_valid_sync; // Gain-controlled signals (between DDC output and matched filter) wire signed [15:0] gc_i, gc_q; wire gc_valid; -wire [7:0] gc_saturation_count; // Diagnostic: clipped sample counter +wire [7:0] gc_saturation_count; // Diagnostic: per-frame clipped sample counter +wire [7:0] gc_peak_magnitude; // Diagnostic: per-frame peak magnitude +wire [3:0] gc_current_gain; // Diagnostic: effective gain_shift // Reference signals for the processing chain wire [15:0] long_chirp_real, long_chirp_imag; @@ -160,7 +174,7 @@ wire clk_400m; // the buffered 400MHz DCO clock via adc_dco_bufg, avoiding duplicate // IBUFDS instantiations on the same LVDS clock pair. -// 1. ADC + CDC + AGC +// 1. ADC + CDC + Digital Gain // CMOS Output Interface (400MHz Domain) wire [7:0] adc_data_cmos; // 8-bit ADC data (CMOS, from ad9484_interface_400m) @@ -222,9 +236,10 @@ ddc_input_interface ddc_if ( .data_sync_error() ); -// 2b. Digital Gain Control (Fix 3) +// 2b. Digital Gain Control with AGC // Host-configurable power-of-2 shift between DDC output and matched filter. -// Default gain_shift=0 → pass-through (no behavioral change from baseline). +// Default gain_shift=0, agc_enable=0 → pass-through (no behavioral change). +// When agc_enable=1: auto-adjusts gain per frame based on peak/saturation. rx_gain_control gain_ctrl ( .clk(clk), .reset_n(reset_n), @@ -232,10 +247,21 @@ rx_gain_control gain_ctrl ( .data_q_in(adc_q_scaled), .valid_in(adc_valid_sync), .gain_shift(host_gain_shift), + // AGC configuration + .agc_enable(host_agc_enable), + .agc_target(host_agc_target), + .agc_attack(host_agc_attack), + .agc_decay(host_agc_decay), + .agc_holdoff(host_agc_holdoff), + // Frame boundary from Doppler processor + .frame_boundary(doppler_frame_done), + // Outputs .data_i_out(gc_i), .data_q_out(gc_q), .valid_out(gc_valid), - .saturation_count(gc_saturation_count) + .saturation_count(gc_saturation_count), + .peak_magnitude(gc_peak_magnitude), + .current_gain(gc_current_gain) ); // 3. Dual Chirp Memory Loader @@ -474,4 +500,9 @@ assign dbg_adc_i = adc_i_scaled; assign dbg_adc_q = adc_q_scaled; assign dbg_adc_valid = adc_valid_sync; +// ========== AGC STATUS OUTPUTS ========== +assign agc_saturation_count = gc_saturation_count; +assign agc_peak_magnitude = gc_peak_magnitude; +assign agc_current_gain = gc_current_gain; + endmodule diff --git a/9_Firmware/9_2_FPGA/radar_system_top.v b/9_Firmware/9_2_FPGA/radar_system_top.v index 9d6686e..dfafa65 100644 --- a/9_Firmware/9_2_FPGA/radar_system_top.v +++ b/9_Firmware/9_2_FPGA/radar_system_top.v @@ -125,7 +125,13 @@ module radar_system_top ( output wire [5:0] dbg_range_bin, // System status - output wire [3:0] system_status + output wire [3:0] system_status, + + // FPGA→STM32 GPIO outputs (DIG_5..DIG_7 on 50T board) + // Used by STM32 outer AGC loop to read saturation state without USB polling. + output wire gpio_dig5, // DIG_5 (H11→PD13): AGC saturation flag (1=clipping detected) + output wire gpio_dig6, // DIG_6 (G12→PD14): reserved (tied low) + output wire gpio_dig7 // DIG_7 (H12→PD15): reserved (tied low) ); // ============================================================================ @@ -187,6 +193,11 @@ wire [15:0] rx_dbg_adc_i; wire [15:0] rx_dbg_adc_q; wire rx_dbg_adc_valid; +// AGC status from receiver (for status readback and GPIO) +wire [7:0] rx_agc_saturation_count; +wire [7:0] rx_agc_peak_magnitude; +wire [3:0] rx_agc_current_gain; + // Data packing for USB wire [31:0] usb_range_profile; wire usb_range_valid; @@ -259,6 +270,13 @@ reg host_cfar_enable; // Opcode 0x25: 1=CFAR, 0=simple threshold reg host_mti_enable; // Opcode 0x26: 1=MTI active, 0=pass-through reg [2:0] host_dc_notch_width; // Opcode 0x27: DC notch ±width bins (0=off, 1..7) +// AGC configuration registers (host-configurable via USB, opcodes 0x28-0x2C) +reg host_agc_enable; // Opcode 0x28: 0=manual gain, 1=auto AGC +reg [7:0] host_agc_target; // Opcode 0x29: target peak magnitude (default 200) +reg [3:0] host_agc_attack; // Opcode 0x2A: gain-down step on clipping (default 1) +reg [3:0] host_agc_decay; // Opcode 0x2B: gain-up step when weak (default 1) +reg [3:0] host_agc_holdoff; // Opcode 0x2C: frames to wait before gain-up (default 4) + // Board bring-up self-test registers (opcode 0x30 trigger, 0x31 readback) reg host_self_test_trigger; // Opcode 0x30: self-clearing pulse wire self_test_busy; @@ -518,6 +536,12 @@ radar_receiver_final rx_inst ( .host_chirps_per_elev(host_chirps_per_elev), // Fix 3: digital gain control .host_gain_shift(host_gain_shift), + // AGC configuration (opcodes 0x28-0x2C) + .host_agc_enable(host_agc_enable), + .host_agc_target(host_agc_target), + .host_agc_attack(host_agc_attack), + .host_agc_decay(host_agc_decay), + .host_agc_holdoff(host_agc_holdoff), // STM32 toggle signals for RX mode controller (mode 00 pass-through). // These are the raw GPIO inputs — the RX mode controller's edge detectors // (inside radar_mode_controller) handle debouncing/edge detection. @@ -532,7 +556,11 @@ radar_receiver_final rx_inst ( // ADC debug tap (for self-test / bring-up) .dbg_adc_i(rx_dbg_adc_i), .dbg_adc_q(rx_dbg_adc_q), - .dbg_adc_valid(rx_dbg_adc_valid) + .dbg_adc_valid(rx_dbg_adc_valid), + // AGC status outputs + .agc_saturation_count(rx_agc_saturation_count), + .agc_peak_magnitude(rx_agc_peak_magnitude), + .agc_current_gain(rx_agc_current_gain) ); // ============================================================================ @@ -744,7 +772,13 @@ if (USB_MODE == 0) begin : gen_ft601 // Self-test status readback .status_self_test_flags(self_test_flags_latched), .status_self_test_detail(self_test_detail_latched), - .status_self_test_busy(self_test_busy) + .status_self_test_busy(self_test_busy), + + // AGC status readback + .status_agc_current_gain(rx_agc_current_gain), + .status_agc_peak_magnitude(rx_agc_peak_magnitude), + .status_agc_saturation_count(rx_agc_saturation_count), + .status_agc_enable(host_agc_enable) ); // FT2232H ports unused in FT601 mode — tie off @@ -805,7 +839,13 @@ end else begin : gen_ft2232h // Self-test status readback .status_self_test_flags(self_test_flags_latched), .status_self_test_detail(self_test_detail_latched), - .status_self_test_busy(self_test_busy) + .status_self_test_busy(self_test_busy), + + // AGC status readback + .status_agc_current_gain(rx_agc_current_gain), + .status_agc_peak_magnitude(rx_agc_peak_magnitude), + .status_agc_saturation_count(rx_agc_saturation_count), + .status_agc_enable(host_agc_enable) ); // FT601 ports unused in FT2232H mode — tie off @@ -892,6 +932,12 @@ always @(posedge clk_100m_buf or negedge sys_reset_n) begin // Ground clutter removal defaults (disabled — backward-compatible) host_mti_enable <= 1'b0; // MTI off host_dc_notch_width <= 3'd0; // DC notch off + // AGC defaults (disabled — backward-compatible with manual gain) + host_agc_enable <= 1'b0; // AGC off (manual gain) + host_agc_target <= 8'd200; // Target peak magnitude + host_agc_attack <= 4'd1; // 1-step gain-down on clipping + host_agc_decay <= 4'd1; // 1-step gain-up when weak + host_agc_holdoff <= 4'd4; // 4 frames before gain-up // Self-test defaults host_self_test_trigger <= 1'b0; // Self-test idle end else begin @@ -936,6 +982,12 @@ always @(posedge clk_100m_buf or negedge sys_reset_n) begin // Ground clutter removal opcodes 8'h26: host_mti_enable <= usb_cmd_value[0]; 8'h27: host_dc_notch_width <= usb_cmd_value[2:0]; + // AGC configuration opcodes + 8'h28: host_agc_enable <= usb_cmd_value[0]; + 8'h29: host_agc_target <= usb_cmd_value[7:0]; + 8'h2A: host_agc_attack <= usb_cmd_value[3:0]; + 8'h2B: host_agc_decay <= usb_cmd_value[3:0]; + 8'h2C: host_agc_holdoff <= usb_cmd_value[3:0]; // Board bring-up self-test opcodes 8'h30: host_self_test_trigger <= 1'b1; // Trigger self-test 8'h31: host_status_request <= 1'b1; // Self-test readback (status alias) @@ -978,6 +1030,16 @@ end assign system_status = status_reg; +// ============================================================================ +// FPGA→STM32 GPIO OUTPUTS (DIG_5, DIG_6, DIG_7) +// ============================================================================ +// DIG_5: AGC saturation flag — high when per-frame saturation_count > 0. +// STM32 reads PD13 to detect clipping and adjust ADAR1000 VGA gain. +// DIG_6, DIG_7: Reserved (tied low for future use). +assign gpio_dig5 = (rx_agc_saturation_count != 8'd0); +assign gpio_dig6 = 1'b0; +assign gpio_dig7 = 1'b0; + // ============================================================================ // DEBUG AND VERIFICATION // ============================================================================ diff --git a/9_Firmware/9_2_FPGA/radar_system_top_50t.v b/9_Firmware/9_2_FPGA/radar_system_top_50t.v index f2f9738..fc3585a 100644 --- a/9_Firmware/9_2_FPGA/radar_system_top_50t.v +++ b/9_Firmware/9_2_FPGA/radar_system_top_50t.v @@ -76,7 +76,12 @@ module radar_system_top_50t ( output wire ft_rd_n, // Read strobe (active low) output wire ft_wr_n, // Write strobe (active low) output wire ft_oe_n, // Output enable / bus direction - output wire ft_siwu // Send Immediate / WakeUp + output wire ft_siwu, // Send Immediate / WakeUp + + // ===== FPGA→STM32 GPIO (Bank 15: 3.3V) ===== + output wire gpio_dig5, // DIG_5 (H11→PD13): AGC saturation flag + output wire gpio_dig6, // DIG_6 (G12→PD14): reserved + output wire gpio_dig7 // DIG_7 (H12→PD15): reserved ); // ===== Tie-off wires for unconstrained FT601 inputs (inactive with USB_MODE=1) ===== @@ -207,7 +212,12 @@ module radar_system_top_50t ( .dbg_doppler_valid (dbg_doppler_valid_nc), .dbg_doppler_bin (dbg_doppler_bin_nc), .dbg_range_bin (dbg_range_bin_nc), - .system_status (system_status_nc) + .system_status (system_status_nc), + + // ----- FPGA→STM32 GPIO (DIG_5..DIG_7) ----- + .gpio_dig5 (gpio_dig5), + .gpio_dig6 (gpio_dig6), + .gpio_dig7 (gpio_dig7) ); endmodule diff --git a/9_Firmware/9_2_FPGA/rx_gain_control.v b/9_Firmware/9_2_FPGA/rx_gain_control.v index 8b258d7..2876625 100644 --- a/9_Firmware/9_2_FPGA/rx_gain_control.v +++ b/9_Firmware/9_2_FPGA/rx_gain_control.v @@ -3,19 +3,32 @@ /** * rx_gain_control.v * - * Host-configurable digital gain control for the receive path. - * Placed between DDC output (ddc_input_interface) and matched filter input. + * Digital gain control with optional per-frame automatic gain control (AGC) + * for the receive path. Placed between DDC output and matched filter input. * - * Features: - * - Bidirectional power-of-2 gain shift (arithmetic shift) + * Manual mode (agc_enable=0): + * - Uses host_gain_shift directly (backward-compatible, no behavioral change) * - gain_shift[3] = direction: 0 = left shift (amplify), 1 = right shift (attenuate) * - gain_shift[2:0] = amount: 0..7 bits - * - Symmetric saturation to ±32767 on overflow (left shift only) - * - Saturation counter: 8-bit, counts samples that clipped (wraps at 255) - * - 1-cycle latency, valid-in/valid-out pipeline - * - Zero-overhead pass-through when gain_shift == 0 + * - Symmetric saturation to ±32767 on overflow * - * Intended insertion point in radar_receiver_final.v: + * AGC mode (agc_enable=1): + * - Per-frame automatic gain adjustment based on peak/saturation metrics + * - Internal signed gain: -7 (max attenuation) to +7 (max amplification) + * - On frame_boundary: + * * If saturation detected: gain -= agc_attack (fast, immediate) + * * Else if peak < target after holdoff frames: gain += agc_decay (slow) + * * Else: hold current gain + * - host_gain_shift serves as initial gain when AGC first enabled + * + * Status outputs (for readback via status_words): + * - current_gain[3:0]: effective gain_shift encoding (manual or AGC) + * - peak_magnitude[7:0]: per-frame peak |sample| (upper 8 bits of 15-bit value) + * - saturation_count[7:0]: per-frame clipped sample count (capped at 255) + * + * Timing: 1-cycle data latency, valid-in/valid-out pipeline. + * + * Insertion point in radar_receiver_final.v: * ddc_input_interface → rx_gain_control → matched_filter_multi_segment */ @@ -28,27 +41,70 @@ module rx_gain_control ( input wire signed [15:0] data_q_in, input wire valid_in, - // Gain configuration (from host via USB command) - // [3] = direction: 0=amplify (left shift), 1=attenuate (right shift) - // [2:0] = shift amount: 0..7 bits + // Host gain configuration (from USB command opcode 0x16) + // [3]=direction: 0=amplify (left shift), 1=attenuate (right shift) + // [2:0]=shift amount: 0..7 bits. Default 0x00 = pass-through. + // In AGC mode: serves as initial gain on AGC enable transition. input wire [3:0] gain_shift, + // AGC configuration inputs (from host via USB, opcodes 0x28-0x2C) + input wire agc_enable, // 0x28: 0=manual gain, 1=auto AGC + input wire [7:0] agc_target, // 0x29: target peak magnitude (unsigned, default 200) + input wire [3:0] agc_attack, // 0x2A: attenuation step on clipping (default 1) + input wire [3:0] agc_decay, // 0x2B: amplification step when weak (default 1) + input wire [3:0] agc_holdoff, // 0x2C: frames to wait before gain-up (default 4) + + // Frame boundary pulse (1 clk cycle, from Doppler frame_complete) + input wire frame_boundary, + // Data output (to matched filter) output reg signed [15:0] data_i_out, output reg signed [15:0] data_q_out, output reg valid_out, - // Diagnostics - output reg [7:0] saturation_count // Number of clipped samples (wraps at 255) + // Diagnostics / status readback + output reg [7:0] saturation_count, // Per-frame clipped sample count (capped at 255) + output reg [7:0] peak_magnitude, // Per-frame peak |sample| (upper 8 bits of 15-bit) + output reg [3:0] current_gain // Current effective gain_shift (for status readback) ); -// Decompose gain_shift -wire shift_right = gain_shift[3]; -wire [2:0] shift_amt = gain_shift[2:0]; +// ========================================================================= +// INTERNAL AGC STATE +// ========================================================================= -// ------------------------------------------------------------------------- -// Combinational shift + saturation -// ------------------------------------------------------------------------- +// Signed internal gain: -7 (max attenuation) to +7 (max amplification) +// Stored as 4-bit signed (range -8..+7, clamped to -7..+7) +reg signed [3:0] agc_gain; + +// Holdoff counter: counts frames without saturation before allowing gain-up +reg [3:0] holdoff_counter; + +// Per-frame accumulators (running, reset on frame_boundary) +reg [7:0] frame_sat_count; // Clipped samples this frame +reg [14:0] frame_peak; // Peak |sample| this frame (15-bit unsigned) + +// Previous AGC enable state (for detecting 0→1 transition) +reg agc_enable_prev; + +// ========================================================================= +// EFFECTIVE GAIN SELECTION +// ========================================================================= + +// Convert between signed internal gain and the gain_shift[3:0] encoding. +// gain_shift[3]=0, [2:0]=N → amplify by N bits (internal gain = +N) +// gain_shift[3]=1, [2:0]=N → attenuate by N bits (internal gain = -N) + +// Effective gain_shift used for the actual shift operation +wire [3:0] effective_gain; +assign effective_gain = agc_enable ? current_gain : gain_shift; + +// Decompose effective gain for shift logic +wire shift_right = effective_gain[3]; +wire [2:0] shift_amt = effective_gain[2:0]; + +// ========================================================================= +// COMBINATIONAL SHIFT + SATURATION +// ========================================================================= // Use wider intermediates to detect overflow on left shift. // 24 bits is enough: 16 + 7 shift = 23 significant bits max. @@ -69,26 +125,138 @@ wire signed [15:0] sat_i = overflow_i ? (shifted_i[23] ? -16'sd32768 : 16'sd3276 wire signed [15:0] sat_q = overflow_q ? (shifted_q[23] ? -16'sd32768 : 16'sd32767) : shifted_q[15:0]; -// ------------------------------------------------------------------------- -// Registered output stage (1-cycle latency) -// ------------------------------------------------------------------------- +// ========================================================================= +// PEAK MAGNITUDE TRACKING (combinational) +// ========================================================================= +// Absolute value of signed 16-bit: flip sign bit if negative. +// Result is 15-bit unsigned [0, 32767]. (We ignore -32768 → 32767 edge case.) +wire [14:0] abs_i = data_i_in[15] ? (~data_i_in[14:0] + 15'd1) : data_i_in[14:0]; +wire [14:0] abs_q = data_q_in[15] ? (~data_q_in[14:0] + 15'd1) : data_q_in[14:0]; +wire [14:0] max_iq = (abs_i > abs_q) ? abs_i : abs_q; + +// ========================================================================= +// SIGNED GAIN ↔ GAIN_SHIFT ENCODING CONVERSION +// ========================================================================= +// Convert signed agc_gain to gain_shift[3:0] encoding +function [3:0] signed_to_encoding; + input signed [3:0] g; + begin + if (g >= 0) + signed_to_encoding = {1'b0, g[2:0]}; // amplify + else + signed_to_encoding = {1'b1, (~g[2:0]) + 3'd1}; // attenuate: -g + end +endfunction + +// Convert gain_shift[3:0] encoding to signed gain +function signed [3:0] encoding_to_signed; + input [3:0] enc; + begin + if (enc[3] == 1'b0) + encoding_to_signed = {1'b0, enc[2:0]}; // +0..+7 + else + encoding_to_signed = -$signed({1'b0, enc[2:0]}); // -1..-7 + end +endfunction + +// ========================================================================= +// CLAMPING HELPER +// ========================================================================= +// Clamp a wider signed value to [-7, +7] +function signed [3:0] clamp_gain; + input signed [4:0] val; // 5-bit to handle overflow from add + begin + if (val > 5'sd7) + clamp_gain = 4'sd7; + else if (val < -5'sd7) + clamp_gain = -4'sd7; + else + clamp_gain = val[3:0]; + end +endfunction + +// ========================================================================= +// REGISTERED OUTPUT + AGC STATE MACHINE +// ========================================================================= always @(posedge clk or negedge reset_n) begin if (!reset_n) begin + // Data path data_i_out <= 16'sd0; data_q_out <= 16'sd0; valid_out <= 1'b0; + // Status outputs saturation_count <= 8'd0; + peak_magnitude <= 8'd0; + current_gain <= 4'd0; + // AGC internal state + agc_gain <= 4'sd0; + holdoff_counter <= 4'd0; + frame_sat_count <= 8'd0; + frame_peak <= 15'd0; + agc_enable_prev <= 1'b0; end else begin - valid_out <= valid_in; + // Track AGC enable transitions + agc_enable_prev <= agc_enable; + // ---- Data pipeline (1-cycle latency) ---- + valid_out <= valid_in; if (valid_in) begin data_i_out <= sat_i; data_q_out <= sat_q; - // Count clipped samples (either channel clipping counts as 1) - if ((overflow_i || overflow_q) && (saturation_count != 8'hFF)) - saturation_count <= saturation_count + 8'd1; + // Per-frame saturation counting + if ((overflow_i || overflow_q) && (frame_sat_count != 8'hFF)) + frame_sat_count <= frame_sat_count + 8'd1; + + // Per-frame peak tracking (pre-gain, measures input signal level) + if (max_iq > frame_peak) + frame_peak <= max_iq; end + + // ---- Frame boundary: AGC update + metric snapshot ---- + if (frame_boundary) begin + // Snapshot per-frame metrics to output registers + saturation_count <= frame_sat_count; + peak_magnitude <= frame_peak[14:7]; // Upper 8 bits of 15-bit peak + + // Reset per-frame accumulators for next frame + frame_sat_count <= 8'd0; + frame_peak <= 15'd0; + + if (agc_enable) begin + // AGC auto-adjustment at frame boundary + if (frame_sat_count > 8'd0) begin + // Clipping detected: reduce gain immediately (attack) + agc_gain <= clamp_gain($signed({1'b0, agc_gain}) - + $signed({1'b0, agc_attack})); + holdoff_counter <= agc_holdoff; // Reset holdoff + end else if (frame_peak[14:7] < agc_target) begin + // Signal too weak: increase gain after holdoff expires + if (holdoff_counter == 4'd0) begin + agc_gain <= clamp_gain($signed({1'b0, agc_gain}) + + $signed({1'b0, agc_decay})); + end else begin + holdoff_counter <= holdoff_counter - 4'd1; + end + end else begin + // Signal in good range, no saturation: hold gain + // Reset holdoff so next weak frame has to wait again + holdoff_counter <= agc_holdoff; + end + end + end + + // ---- AGC enable transition: initialize from host gain ---- + if (agc_enable && !agc_enable_prev) begin + agc_gain <= encoding_to_signed(gain_shift); + holdoff_counter <= agc_holdoff; + end + + // ---- Update current_gain output ---- + if (agc_enable) + current_gain <= signed_to_encoding(agc_gain); + else + current_gain <= gain_shift; end end diff --git a/9_Firmware/9_2_FPGA/scripts/50t/build_50t.tcl b/9_Firmware/9_2_FPGA/scripts/50t/build_50t.tcl index 730b006..51a0e5e 100644 --- a/9_Firmware/9_2_FPGA/scripts/50t/build_50t.tcl +++ b/9_Firmware/9_2_FPGA/scripts/50t/build_50t.tcl @@ -120,9 +120,10 @@ set_property CLOCK_DEDICATED_ROUTE FALSE [get_nets {ft_clkout_IBUF}] # ---- Run implementation steps ---- opt_design -directive Explore -place_design -directive Explore +place_design -directive ExtraNetDelay_high +phys_opt_design -directive AggressiveExplore +route_design -directive AggressiveExplore phys_opt_design -directive AggressiveExplore -route_design -directive Explore phys_opt_design -directive AggressiveExplore set impl_elapsed [expr {[clock seconds] - $impl_start}] diff --git a/9_Firmware/9_2_FPGA/tb/tb_rx_gain_control.v b/9_Firmware/9_2_FPGA/tb/tb_rx_gain_control.v index a44abfd..32aaf82 100644 --- a/9_Firmware/9_2_FPGA/tb/tb_rx_gain_control.v +++ b/9_Firmware/9_2_FPGA/tb/tb_rx_gain_control.v @@ -38,10 +38,20 @@ reg signed [15:0] data_q_in; reg valid_in; reg [3:0] gain_shift; +// AGC configuration (default: AGC disabled — manual mode) +reg agc_enable; +reg [7:0] agc_target; +reg [3:0] agc_attack; +reg [3:0] agc_decay; +reg [3:0] agc_holdoff; +reg frame_boundary; + wire signed [15:0] data_i_out; wire signed [15:0] data_q_out; wire valid_out; wire [7:0] saturation_count; +wire [7:0] peak_magnitude; +wire [3:0] current_gain; rx_gain_control dut ( .clk(clk), @@ -50,10 +60,18 @@ rx_gain_control dut ( .data_q_in(data_q_in), .valid_in(valid_in), .gain_shift(gain_shift), + .agc_enable(agc_enable), + .agc_target(agc_target), + .agc_attack(agc_attack), + .agc_decay(agc_decay), + .agc_holdoff(agc_holdoff), + .frame_boundary(frame_boundary), .data_i_out(data_i_out), .data_q_out(data_q_out), .valid_out(valid_out), - .saturation_count(saturation_count) + .saturation_count(saturation_count), + .peak_magnitude(peak_magnitude), + .current_gain(current_gain) ); // --------------------------------------------------------------- @@ -105,6 +123,13 @@ initial begin data_q_in = 0; valid_in = 0; gain_shift = 4'd0; + // AGC disabled for backward-compatible tests (Tests 1-12) + agc_enable = 0; + agc_target = 8'd200; + agc_attack = 4'd1; + agc_decay = 4'd1; + agc_holdoff = 4'd4; + frame_boundary = 0; repeat (4) @(posedge clk); reset_n = 1; @@ -152,6 +177,9 @@ initial begin "T3.1: I saturated to +32767"); check(data_q_out == -16'sd32768, "T3.2: Q saturated to -32768"); + // Pulse frame_boundary to snapshot the per-frame saturation count + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; check(saturation_count == 8'd1, "T3.3: Saturation counter = 1 (both channels clipped counts as 1)"); @@ -173,6 +201,9 @@ initial begin "T4.1: I attenuated 4000>>2 = 1000"); check(data_q_out == -16'sd500, "T4.2: Q attenuated -2000>>2 = -500"); + // Pulse frame_boundary to snapshot (should be 0 — no clipping) + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; check(saturation_count == 8'd0, "T4.3: No saturation on right shift"); @@ -315,13 +346,18 @@ initial begin valid_in = 1'b0; @(posedge clk); #1; + // Pulse frame_boundary to snapshot per-frame saturation count + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; check(saturation_count == 8'd255, "T11.1: Counter capped at 255 after 256 saturating samples"); - // One more sample — should stay at 255 + // One more sample + frame boundary — should still be capped at 1 (new frame) send_sample(16'sd20000, 16'sd20000); - check(saturation_count == 8'd255, - "T11.2: Counter stays at 255 (no wrap)"); + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + check(saturation_count == 8'd1, + "T11.2: New frame counter = 1 (single sample)"); // --------------------------------------------------------------- // TEST 12: Reset clears everything @@ -329,6 +365,8 @@ initial begin $display(""); $display("--- Test 12: Reset clears all ---"); + gain_shift = 4'd0; // Reset gain_shift to 0 so current_gain reads 0 + agc_enable = 0; reset_n = 0; repeat (2) @(posedge clk); reset_n = 1; @@ -342,6 +380,170 @@ initial begin "T12.3: valid_out cleared on reset"); check(saturation_count == 8'd0, "T12.4: Saturation counter cleared on reset"); + check(current_gain == 4'd0, + "T12.5: current_gain cleared on reset"); + + // --------------------------------------------------------------- + // TEST 13: current_gain reflects gain_shift in manual mode + // --------------------------------------------------------------- + $display(""); + $display("--- Test 13: current_gain tracks gain_shift (manual) ---"); + + gain_shift = 4'b0_011; // amplify x8 + @(posedge clk); @(posedge clk); #1; + check(current_gain == 4'b0011, + "T13.1: current_gain = 0x3 (amplify x8)"); + + gain_shift = 4'b1_010; // attenuate /4 + @(posedge clk); @(posedge clk); #1; + check(current_gain == 4'b1010, + "T13.2: current_gain = 0xA (attenuate /4)"); + + // --------------------------------------------------------------- + // TEST 14: Peak magnitude tracking + // --------------------------------------------------------------- + $display(""); + $display("--- Test 14: Peak magnitude tracking ---"); + + reset_n = 0; + repeat (2) @(posedge clk); + reset_n = 1; + repeat (2) @(posedge clk); + + gain_shift = 4'b0_000; // pass-through + // Send samples with increasing magnitude + send_sample(16'sd100, 16'sd50); + send_sample(16'sd1000, 16'sd500); + send_sample(16'sd8000, 16'sd2000); // peak = 8000 + send_sample(16'sd200, 16'sd100); + // Pulse frame_boundary to snapshot + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + // peak_magnitude = upper 8 bits of 15-bit peak (8000) + // 8000 = 0x1F40, 15-bit = 0x1F40, [14:7] = 0x3E = 62 + check(peak_magnitude == 8'd62, + "T14.1: Peak magnitude = 62 (8000 >> 7)"); + + // --------------------------------------------------------------- + // TEST 15: AGC auto gain-down on saturation + // --------------------------------------------------------------- + $display(""); + $display("--- Test 15: AGC gain-down on saturation ---"); + + reset_n = 0; + repeat (2) @(posedge clk); + reset_n = 1; + repeat (2) @(posedge clk); + + // Start with amplify x4 (gain_shift = 0x02), then enable AGC + gain_shift = 4'b0_010; // amplify x4, internal gain = +2 + agc_enable = 0; + agc_attack = 4'd1; + agc_decay = 4'd1; + agc_holdoff = 4'd2; + agc_target = 8'd100; + @(posedge clk); @(posedge clk); + + // Enable AGC — should initialize from gain_shift + agc_enable = 1; + @(posedge clk); @(posedge clk); @(posedge clk); #1; + check(current_gain == 4'b0010, + "T15.1: AGC initialized from gain_shift (amplify x4)"); + + // Send saturating samples (will clip at x4 gain) + send_sample(16'sd20000, 16'sd20000); + send_sample(16'sd20000, 16'sd20000); + + // Pulse frame_boundary — AGC should reduce gain by attack=1 + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + // current_gain lags agc_gain by 1 cycle (NBA), wait one extra cycle + @(posedge clk); #1; + // Internal gain was +2, attack=1 → new gain = +1 (0x01) + check(current_gain == 4'b0001, + "T15.2: AGC reduced gain to x2 after saturation"); + + // Another frame with saturation (20000*2 = 40000 > 32767) + send_sample(16'sd20000, 16'sd20000); + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + @(posedge clk); #1; + // gain was +1, attack=1 → new gain = 0 (0x00) + check(current_gain == 4'b0000, + "T15.3: AGC reduced gain to x1 (pass-through)"); + + // At gain 0 (pass-through), 20000 does NOT overflow 16-bit range, + // so no saturation occurs. Signal peak = 20000 >> 7 = 156 > target(100), + // so AGC correctly holds gain at 0. This is expected behavior. + // To test crossing into attenuation: increase attack to 3. + agc_attack = 4'd3; + // Reset and start fresh with gain +2, attack=3 + reset_n = 0; + repeat (2) @(posedge clk); + reset_n = 1; + repeat (2) @(posedge clk); + + gain_shift = 4'b0_010; // amplify x4, internal gain = +2 + agc_enable = 0; + @(posedge clk); + agc_enable = 1; + @(posedge clk); @(posedge clk); @(posedge clk); #1; + + // Send saturating samples + send_sample(16'sd20000, 16'sd20000); + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + @(posedge clk); #1; + // gain was +2, attack=3 → new gain = -1 → encoding 0x09 + check(current_gain == 4'b1001, + "T15.4: Large attack step crosses to attenuation (gain +2 - 3 = -1 → 0x9)"); + + // --------------------------------------------------------------- + // TEST 16: AGC auto gain-up after holdoff + // --------------------------------------------------------------- + $display(""); + $display("--- Test 16: AGC gain-up after holdoff ---"); + + reset_n = 0; + repeat (2) @(posedge clk); + reset_n = 1; + repeat (2) @(posedge clk); + + // Start with low gain, weak signal, holdoff=2 + gain_shift = 4'b0_000; // pass-through (internal gain = 0) + agc_enable = 0; + agc_attack = 4'd1; + agc_decay = 4'd1; + agc_holdoff = 4'd2; + agc_target = 8'd100; // target peak = 100 (in upper 8 bits = 12800 raw) + @(posedge clk); @(posedge clk); + + agc_enable = 1; + @(posedge clk); @(posedge clk); #1; + + // Frame 1: send weak signal (peak < target), holdoff counter = 2 + send_sample(16'sd100, 16'sd50); // peak=100, [14:7]=0 (very weak) + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + @(posedge clk); #1; + check(current_gain == 4'b0000, + "T16.1: Gain held during holdoff (frame 1, holdoff=2)"); + + // Frame 2: still weak, holdoff counter decrements to 1 + send_sample(16'sd100, 16'sd50); + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + @(posedge clk); #1; + check(current_gain == 4'b0000, + "T16.2: Gain held during holdoff (frame 2, holdoff=1)"); + + // Frame 3: holdoff expired (was 0 at start of frame) → gain up + send_sample(16'sd100, 16'sd50); + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + @(posedge clk); #1; + check(current_gain == 4'b0001, + "T16.3: Gain increased after holdoff expired (gain 0->1)"); // --------------------------------------------------------------- // SUMMARY diff --git a/9_Firmware/9_2_FPGA/tb/tb_usb_data_interface.v b/9_Firmware/9_2_FPGA/tb/tb_usb_data_interface.v index 0318b7b..082c192 100644 --- a/9_Firmware/9_2_FPGA/tb/tb_usb_data_interface.v +++ b/9_Firmware/9_2_FPGA/tb/tb_usb_data_interface.v @@ -79,6 +79,12 @@ module tb_usb_data_interface; reg [7:0] status_self_test_detail; reg status_self_test_busy; + // AGC status readback inputs + reg [3:0] status_agc_current_gain; + reg [7:0] status_agc_peak_magnitude; + reg [7:0] status_agc_saturation_count; + reg status_agc_enable; + // ── Clock generators (asynchronous) ──────────────────────── always #(CLK_PERIOD / 2) clk = ~clk; always #(FT_CLK_PERIOD / 2) ft601_clk_in = ~ft601_clk_in; @@ -134,7 +140,13 @@ module tb_usb_data_interface; // Self-test status readback .status_self_test_flags (status_self_test_flags), .status_self_test_detail(status_self_test_detail), - .status_self_test_busy (status_self_test_busy) + .status_self_test_busy (status_self_test_busy), + + // AGC status readback + .status_agc_current_gain (status_agc_current_gain), + .status_agc_peak_magnitude (status_agc_peak_magnitude), + .status_agc_saturation_count(status_agc_saturation_count), + .status_agc_enable (status_agc_enable) ); // ── Test bookkeeping ─────────────────────────────────────── @@ -194,6 +206,10 @@ module tb_usb_data_interface; status_self_test_flags = 5'b00000; status_self_test_detail = 8'd0; status_self_test_busy = 1'b0; + status_agc_current_gain = 4'd0; + status_agc_peak_magnitude = 8'd0; + status_agc_saturation_count = 8'd0; + status_agc_enable = 1'b0; repeat (6) @(posedge ft601_clk_in); reset_n = 1; // Wait enough cycles for stream_control CDC to propagate @@ -902,6 +918,11 @@ module tb_usb_data_interface; status_self_test_flags = 5'b11111; status_self_test_detail = 8'hA5; status_self_test_busy = 1'b0; + // AGC status: gain=5, peak=180, sat_count=12, enabled + status_agc_current_gain = 4'd5; + status_agc_peak_magnitude = 8'd180; + status_agc_saturation_count = 8'd12; + status_agc_enable = 1'b1; // Pulse status_request (1 cycle in clk domain — toggles status_req_toggle_100m) @(posedge clk); @@ -958,8 +979,8 @@ module tb_usb_data_interface; "Status readback: word 2 = {guard, short_chirp}"); check(uut.status_words[3] === {16'd17450, 10'd0, 6'd32}, "Status readback: word 3 = {short_listen, 0, chirps_per_elev}"); - check(uut.status_words[4] === {30'd0, 2'b10}, - "Status readback: word 4 = range_mode=2'b10"); + check(uut.status_words[4] === {4'd5, 8'd180, 8'd12, 1'b1, 9'd0, 2'b10}, + "Status readback: word 4 = {agc_gain=5, peak=180, sat=12, en=1, range_mode=2}"); // status_words[5] = {7'd0, busy, 8'd0, detail[7:0], 3'd0, flags[4:0]} // = {7'd0, 1'b0, 8'd0, 8'hA5, 3'd0, 5'b11111} check(uut.status_words[5] === {7'd0, 1'b0, 8'd0, 8'hA5, 3'd0, 5'b11111}, diff --git a/9_Firmware/9_2_FPGA/usb_data_interface.v b/9_Firmware/9_2_FPGA/usb_data_interface.v index f08aad8..56e3aba 100644 --- a/9_Firmware/9_2_FPGA/usb_data_interface.v +++ b/9_Firmware/9_2_FPGA/usb_data_interface.v @@ -77,7 +77,13 @@ module usb_data_interface ( // Self-test status readback (opcode 0x31 / included in 0xFF status packet) input wire [4:0] status_self_test_flags, // Per-test PASS(1)/FAIL(0) latched input wire [7:0] status_self_test_detail, // Diagnostic detail byte latched - input wire status_self_test_busy // Self-test FSM still running + input wire status_self_test_busy, // Self-test FSM still running + + // AGC status readback + input wire [3:0] status_agc_current_gain, + input wire [7:0] status_agc_peak_magnitude, + input wire [7:0] status_agc_saturation_count, + input wire status_agc_enable ); // USB packet structure (same as before) @@ -267,8 +273,13 @@ always @(posedge ft601_clk_in or negedge ft601_reset_n) begin status_words[2] <= {status_guard, status_short_chirp}; // Word 3: {short_listen_cycles[15:0], chirps_per_elev[5:0], 10'b0} status_words[3] <= {status_short_listen, 10'd0, status_chirps_per_elev}; - // Word 4: Fix 7 — range_mode in bits [1:0], rest reserved - status_words[4] <= {30'd0, status_range_mode}; + // Word 4: AGC metrics + range_mode + status_words[4] <= {status_agc_current_gain, // [31:28] + status_agc_peak_magnitude, // [27:20] + status_agc_saturation_count, // [19:12] + status_agc_enable, // [11] + 9'd0, // [10:2] reserved + status_range_mode}; // [1:0] // Word 5: Self-test results {reserved[6:0], busy, reserved[7:0], detail[7:0], reserved[2:0], flags[4:0]} status_words[5] <= {7'd0, status_self_test_busy, 8'd0, status_self_test_detail, diff --git a/9_Firmware/9_2_FPGA/usb_data_interface_ft2232h.v b/9_Firmware/9_2_FPGA/usb_data_interface_ft2232h.v index 1399db5..44cca42 100644 --- a/9_Firmware/9_2_FPGA/usb_data_interface_ft2232h.v +++ b/9_Firmware/9_2_FPGA/usb_data_interface_ft2232h.v @@ -90,7 +90,13 @@ module usb_data_interface_ft2232h ( // Self-test status readback input wire [4:0] status_self_test_flags, input wire [7:0] status_self_test_detail, - input wire status_self_test_busy + input wire status_self_test_busy, + + // AGC status readback + input wire [3:0] status_agc_current_gain, + input wire [7:0] status_agc_peak_magnitude, + input wire [7:0] status_agc_saturation_count, + input wire status_agc_enable ); // ============================================================================ @@ -281,7 +287,12 @@ always @(posedge ft_clk or negedge ft_reset_n) begin status_words[1] <= {status_long_chirp, status_long_listen}; status_words[2] <= {status_guard, status_short_chirp}; status_words[3] <= {status_short_listen, 10'd0, status_chirps_per_elev}; - status_words[4] <= {30'd0, status_range_mode}; + status_words[4] <= {status_agc_current_gain, // [31:28] + status_agc_peak_magnitude, // [27:20] + status_agc_saturation_count, // [19:12] + status_agc_enable, // [11] + 9'd0, // [10:2] reserved + status_range_mode}; // [1:0] status_words[5] <= {7'd0, status_self_test_busy, 8'd0, status_self_test_detail, 3'd0, status_self_test_flags}; diff --git a/9_Firmware/9_3_GUI/radar_dashboard.py b/9_Firmware/9_3_GUI/radar_dashboard.py index 75779a8..c7b758c 100644 --- a/9_Firmware/9_3_GUI/radar_dashboard.py +++ b/9_Firmware/9_3_GUI/radar_dashboard.py @@ -379,6 +379,44 @@ class RadarDashboard: command=lambda: self._send_cmd(0x25, 0)).pack( side="left", expand=True, fill="x", padx=(2, 0)) + # ── AGC (Automatic Gain Control) ────────────────────────────── + grp_agc = ttk.LabelFrame(right, text="AGC (Auto Gain)", padding=10) + grp_agc.pack(fill="x", pady=(0, 8)) + + agc_params = [ + ("AGC Enable", 0x28, "0", 1, "0=manual, 1=auto"), + ("AGC Target", 0x29, "200", 8, "0-255, peak target"), + ("AGC Attack", 0x2A, "1", 4, "0-15, atten step"), + ("AGC Decay", 0x2B, "1", 4, "0-15, gain-up step"), + ("AGC Holdoff", 0x2C, "4", 4, "0-15, frames"), + ] + for label, opcode, default, bits, hint in agc_params: + self._add_param_row(grp_agc, label, opcode, default, bits, hint) + + # AGC quick toggle + agc_row = ttk.Frame(grp_agc) + agc_row.pack(fill="x", pady=2) + ttk.Button(agc_row, text="Enable AGC", + command=lambda: self._send_cmd(0x28, 1)).pack( + side="left", expand=True, fill="x", padx=(0, 2)) + ttk.Button(agc_row, text="Disable AGC", + command=lambda: self._send_cmd(0x28, 0)).pack( + side="left", expand=True, fill="x", padx=(2, 0)) + + # AGC status readback labels + agc_st = ttk.LabelFrame(grp_agc, text="AGC Status", padding=6) + agc_st.pack(fill="x", pady=(4, 0)) + self._agc_labels = {} + for name, default_text in [ + ("enable", "AGC: --"), + ("gain", "Gain: --"), + ("peak", "Peak: --"), + ("sat", "Sat Count: --"), + ]: + lbl = ttk.Label(agc_st, text=default_text, font=("Menlo", 9)) + lbl.pack(anchor="w") + self._agc_labels[name] = lbl + # ── Custom Command (advanced / debug) ───────────────────────── grp_cust = ttk.LabelFrame(right, text="Custom Command", padding=10) grp_cust.pack(fill="x", pady=(0, 8)) @@ -521,7 +559,7 @@ class RadarDashboard: self.root.after(0, self._update_self_test_labels, status) def _update_self_test_labels(self, status: StatusResponse): - """Update the self-test result labels from a StatusResponse.""" + """Update the self-test result labels and AGC status from a StatusResponse.""" if not hasattr(self, '_st_labels'): return flags = status.self_test_flags @@ -556,6 +594,21 @@ class RadarDashboard: self._st_labels[key].config( text=f"{name}: {result_str}", foreground=color) + # AGC status readback + if hasattr(self, '_agc_labels'): + agc_str = "AUTO" if status.agc_enable else "MANUAL" + agc_color = GREEN if status.agc_enable else FG + self._agc_labels["enable"].config( + text=f"AGC: {agc_str}", foreground=agc_color) + self._agc_labels["gain"].config( + text=f"Gain: {status.agc_current_gain}") + self._agc_labels["peak"].config( + text=f"Peak: {status.agc_peak_magnitude}") + sat_color = RED if status.agc_saturation_count > 0 else FG + self._agc_labels["sat"].config( + text=f"Sat Count: {status.agc_saturation_count}", + foreground=sat_color) + # --------------------------------------------------------- Display loop def _schedule_update(self): self._update_display() diff --git a/9_Firmware/9_3_GUI/radar_protocol.py b/9_Firmware/9_3_GUI/radar_protocol.py index 10cab55..0bf0a7a 100644 --- a/9_Firmware/9_3_GUI/radar_protocol.py +++ b/9_Firmware/9_3_GUI/radar_protocol.py @@ -59,9 +59,9 @@ class Opcode(IntEnum): 0x03 host_detect_threshold 0x16 host_gain_shift 0x04 host_stream_control 0x20 host_range_mode 0x10 host_long_chirp_cycles 0x21-0x27 CFAR / MTI / DC-notch - 0x11 host_long_listen_cycles 0x30 host_self_test_trigger - 0x12 host_guard_cycles 0x31 host_status_request - 0x13 host_short_chirp_cycles 0xFF host_status_request + 0x11 host_long_listen_cycles 0x28-0x2C AGC control + 0x12 host_guard_cycles 0x30 host_self_test_trigger + 0x13 host_short_chirp_cycles 0x31/0xFF host_status_request """ # --- Basic control (0x01-0x04) --- RADAR_MODE = 0x01 # 2-bit mode select @@ -90,6 +90,13 @@ class Opcode(IntEnum): MTI_ENABLE = 0x26 DC_NOTCH_WIDTH = 0x27 + # --- AGC (0x28-0x2C) --- + AGC_ENABLE = 0x28 + AGC_TARGET = 0x29 + AGC_ATTACK = 0x2A + AGC_DECAY = 0x2B + AGC_HOLDOFF = 0x2C + # --- Board self-test / status (0x30-0x31, 0xFF) --- SELF_TEST_TRIGGER = 0x30 SELF_TEST_STATUS = 0x31 @@ -135,6 +142,11 @@ class StatusResponse: self_test_flags: int = 0 # 5-bit result flags [4:0] self_test_detail: int = 0 # 8-bit detail code [7:0] self_test_busy: int = 0 # 1-bit busy flag + # AGC metrics (word 4, added for hybrid AGC) + agc_current_gain: int = 0 # 4-bit current gain encoding [3:0] + agc_peak_magnitude: int = 0 # 8-bit peak magnitude [7:0] + agc_saturation_count: int = 0 # 8-bit saturation count [7:0] + agc_enable: int = 0 # 1-bit AGC enable readback # ============================================================================ @@ -232,8 +244,13 @@ class RadarProtocol: # Word 3: {short_listen[31:16], 10'd0, chirps_per_elev[5:0]} sr.chirps_per_elev = words[3] & 0x3F sr.short_listen = (words[3] >> 16) & 0xFFFF - # Word 4: {30'd0, range_mode[1:0]} + # Word 4: {agc_current_gain[31:28], agc_peak_magnitude[27:20], + # agc_saturation_count[19:12], agc_enable[11], 9'd0, range_mode[1:0]} sr.range_mode = words[4] & 0x03 + sr.agc_enable = (words[4] >> 11) & 0x01 + sr.agc_saturation_count = (words[4] >> 12) & 0xFF + sr.agc_peak_magnitude = (words[4] >> 20) & 0xFF + sr.agc_current_gain = (words[4] >> 28) & 0x0F # Word 5: {7'd0, self_test_busy, 8'd0, self_test_detail[7:0], # 3'd0, self_test_flags[4:0]} sr.self_test_flags = words[5] & 0x1F diff --git a/9_Firmware/9_3_GUI/test_radar_dashboard.py b/9_Firmware/9_3_GUI/test_radar_dashboard.py index 9e69a49..4255e42 100644 --- a/9_Firmware/9_3_GUI/test_radar_dashboard.py +++ b/9_Firmware/9_3_GUI/test_radar_dashboard.py @@ -125,7 +125,8 @@ class TestRadarProtocol(unittest.TestCase): long_chirp=3000, long_listen=13700, guard=17540, short_chirp=50, short_listen=17450, chirps=32, range_mode=0, - st_flags=0, st_detail=0, st_busy=0): + st_flags=0, st_detail=0, st_busy=0, + agc_gain=0, agc_peak=0, agc_sat=0, agc_enable=0): """Build a 26-byte status response matching FPGA format (Build 26).""" pkt = bytearray() pkt.append(STATUS_HEADER_BYTE) @@ -146,8 +147,11 @@ class TestRadarProtocol(unittest.TestCase): w3 = ((short_listen & 0xFFFF) << 16) | (chirps & 0x3F) pkt += struct.pack(">I", w3) - # Word 4: {30'd0, range_mode[1:0]} - w4 = range_mode & 0x03 + # Word 4: {agc_current_gain[3:0], agc_peak_magnitude[7:0], + # agc_saturation_count[7:0], agc_enable, 9'd0, range_mode[1:0]} + w4 = (((agc_gain & 0x0F) << 28) | ((agc_peak & 0xFF) << 20) | + ((agc_sat & 0xFF) << 12) | ((agc_enable & 0x01) << 11) | + (range_mode & 0x03)) pkt += struct.pack(">I", w4) # Word 5: {7'd0, self_test_busy, 8'd0, self_test_detail[7:0], @@ -723,6 +727,7 @@ class TestOpcodeEnum(unittest.TestCase): expected = {0x01, 0x02, 0x03, 0x04, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, + 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x30, 0x31, 0xFF} enum_values = {int(m) for m in Opcode} for op in expected: @@ -747,5 +752,93 @@ class TestStatusResponseDefaults(unittest.TestCase): self.assertEqual(sr.self_test_busy, 1) +class TestAGCOpcodes(unittest.TestCase): + """Verify AGC opcode enum members match FPGA RTL (0x28-0x2C).""" + + def test_agc_enable_opcode(self): + self.assertEqual(Opcode.AGC_ENABLE, 0x28) + + def test_agc_target_opcode(self): + self.assertEqual(Opcode.AGC_TARGET, 0x29) + + def test_agc_attack_opcode(self): + self.assertEqual(Opcode.AGC_ATTACK, 0x2A) + + def test_agc_decay_opcode(self): + self.assertEqual(Opcode.AGC_DECAY, 0x2B) + + def test_agc_holdoff_opcode(self): + self.assertEqual(Opcode.AGC_HOLDOFF, 0x2C) + + +class TestAGCStatusParsing(unittest.TestCase): + """Verify AGC fields in status_words[4] are parsed correctly.""" + + def _make_status_packet(self, **kwargs): + """Delegate to TestRadarProtocol helper.""" + helper = TestRadarProtocol() + return helper._make_status_packet(**kwargs) + + def test_agc_fields_default_zero(self): + """With no AGC fields set, all should be 0.""" + raw = self._make_status_packet() + sr = RadarProtocol.parse_status_packet(raw) + self.assertEqual(sr.agc_current_gain, 0) + self.assertEqual(sr.agc_peak_magnitude, 0) + self.assertEqual(sr.agc_saturation_count, 0) + self.assertEqual(sr.agc_enable, 0) + + def test_agc_fields_nonzero(self): + """AGC fields round-trip through status packet.""" + raw = self._make_status_packet(agc_gain=7, agc_peak=200, + agc_sat=15, agc_enable=1) + sr = RadarProtocol.parse_status_packet(raw) + self.assertEqual(sr.agc_current_gain, 7) + self.assertEqual(sr.agc_peak_magnitude, 200) + self.assertEqual(sr.agc_saturation_count, 15) + self.assertEqual(sr.agc_enable, 1) + + def test_agc_max_values(self): + """AGC fields at max values.""" + raw = self._make_status_packet(agc_gain=15, agc_peak=255, + agc_sat=255, agc_enable=1) + sr = RadarProtocol.parse_status_packet(raw) + self.assertEqual(sr.agc_current_gain, 15) + self.assertEqual(sr.agc_peak_magnitude, 255) + self.assertEqual(sr.agc_saturation_count, 255) + self.assertEqual(sr.agc_enable, 1) + + def test_agc_and_range_mode_coexist(self): + """AGC fields and range_mode occupy the same word without conflict.""" + raw = self._make_status_packet(agc_gain=5, agc_peak=128, + agc_sat=42, agc_enable=1, + range_mode=2) + sr = RadarProtocol.parse_status_packet(raw) + self.assertEqual(sr.agc_current_gain, 5) + self.assertEqual(sr.agc_peak_magnitude, 128) + self.assertEqual(sr.agc_saturation_count, 42) + self.assertEqual(sr.agc_enable, 1) + self.assertEqual(sr.range_mode, 2) + + +class TestAGCStatusResponseDefaults(unittest.TestCase): + """Verify StatusResponse AGC field defaults.""" + + def test_default_agc_fields(self): + sr = StatusResponse() + self.assertEqual(sr.agc_current_gain, 0) + self.assertEqual(sr.agc_peak_magnitude, 0) + self.assertEqual(sr.agc_saturation_count, 0) + self.assertEqual(sr.agc_enable, 0) + + def test_agc_fields_set(self): + sr = StatusResponse(agc_current_gain=7, agc_peak_magnitude=200, + agc_saturation_count=15, agc_enable=1) + self.assertEqual(sr.agc_current_gain, 7) + self.assertEqual(sr.agc_peak_magnitude, 200) + self.assertEqual(sr.agc_saturation_count, 15) + self.assertEqual(sr.agc_enable, 1) + + if __name__ == "__main__": unittest.main(verbosity=2) diff --git a/9_Firmware/9_3_GUI/v7/dashboard.py b/9_Firmware/9_3_GUI/v7/dashboard.py index 0526877..b2779b4 100644 --- a/9_Firmware/9_3_GUI/v7/dashboard.py +++ b/9_Firmware/9_3_GUI/v7/dashboard.py @@ -5,7 +5,7 @@ RadarDashboard is a QMainWindow with five tabs: 1. Main View — Range-Doppler matplotlib canvas (64x32), device combos, Start/Stop, targets table 2. Map View — Embedded Leaflet map + sidebar - 3. FPGA Control — Full FPGA register control panel (all 22 opcodes, + 3. FPGA Control — Full FPGA register control panel (all 27 opcodes incl. AGC, bit-width validation, grouped layout matching production) 4. Diagnostics — Connection indicators, packet stats, dependency status, self-test results, log viewer @@ -681,6 +681,48 @@ class RadarDashboard(QMainWindow): right_layout.addWidget(grp_cfar) + # ── AGC (Automatic Gain Control) ────────────────────────────── + grp_agc = QGroupBox("AGC (Auto Gain)") + agc_layout = QVBoxLayout(grp_agc) + + agc_params = [ + ("AGC Enable", 0x28, 0, 1, "0=manual, 1=auto"), + ("AGC Target", 0x29, 200, 8, "0-255, peak target"), + ("AGC Attack", 0x2A, 1, 4, "0-15, atten step"), + ("AGC Decay", 0x2B, 1, 4, "0-15, gain-up step"), + ("AGC Holdoff", 0x2C, 4, 4, "0-15, frames"), + ] + for label, opcode, default, bits, hint in agc_params: + self._add_fpga_param_row(agc_layout, label, opcode, default, bits, hint) + + # AGC quick toggles + agc_row = QHBoxLayout() + btn_agc_on = QPushButton("Enable AGC") + btn_agc_on.clicked.connect(lambda: self._send_fpga_cmd(0x28, 1)) + agc_row.addWidget(btn_agc_on) + btn_agc_off = QPushButton("Disable AGC") + btn_agc_off.clicked.connect(lambda: self._send_fpga_cmd(0x28, 0)) + agc_row.addWidget(btn_agc_off) + agc_layout.addLayout(agc_row) + + # AGC status readback labels + agc_st_group = QGroupBox("AGC Status") + agc_st_layout = QVBoxLayout(agc_st_group) + self._agc_labels: dict[str, QLabel] = {} + for name, default_text in [ + ("enable", "AGC: --"), + ("gain", "Gain: --"), + ("peak", "Peak: --"), + ("sat", "Sat Count: --"), + ]: + lbl = QLabel(default_text) + lbl.setStyleSheet(f"color: {DARK_INFO}; font-size: 10px;") + agc_st_layout.addWidget(lbl) + self._agc_labels[name] = lbl + agc_layout.addWidget(agc_st_group) + + right_layout.addWidget(grp_agc) + # Custom Command grp_custom = QGroupBox("Custom Command") cust_layout = QGridLayout(grp_custom) @@ -1276,6 +1318,23 @@ class RadarDashboard(QMainWindow): self._st_labels["t4"].setText( f"T4 ADC: {'PASS' if flags & 0x10 else 'FAIL'}") + # AGC status readback + if hasattr(self, '_agc_labels'): + agc_str = "AUTO" if st.agc_enable else "MANUAL" + agc_color = DARK_SUCCESS if st.agc_enable else DARK_INFO + self._agc_labels["enable"].setStyleSheet( + f"color: {agc_color}; font-weight: bold;") + self._agc_labels["enable"].setText(f"AGC: {agc_str}") + self._agc_labels["gain"].setText( + f"Gain: {st.agc_current_gain}") + self._agc_labels["peak"].setText( + f"Peak: {st.agc_peak_magnitude}") + sat_color = DARK_ERROR if st.agc_saturation_count > 0 else DARK_INFO + self._agc_labels["sat"].setStyleSheet( + f"color: {sat_color}; font-weight: bold;") + self._agc_labels["sat"].setText( + f"Sat Count: {st.agc_saturation_count}") + # ===================================================================== # Position / coverage callbacks (map sidebar) # ===================================================================== diff --git a/9_Firmware/tests/cross_layer/contract_parser.py b/9_Firmware/tests/cross_layer/contract_parser.py index 44e9bb3..a0220ff 100644 --- a/9_Firmware/tests/cross_layer/contract_parser.py +++ b/9_Firmware/tests/cross_layer/contract_parser.py @@ -527,6 +527,8 @@ def parse_verilog_status_word_concats( ): idx = int(m.group(1)) expr = m.group(2) + # Strip single-line comments before normalizing whitespace + expr = re.sub(r'//[^\n]*', '', expr) # Normalize whitespace expr = re.sub(r'\s+', ' ', expr).strip() results[idx] = expr diff --git a/9_Firmware/tests/cross_layer/tb_cross_layer_ft2232h.v b/9_Firmware/tests/cross_layer/tb_cross_layer_ft2232h.v index 261d78f..bfd2c8e 100644 --- a/9_Firmware/tests/cross_layer/tb_cross_layer_ft2232h.v +++ b/9_Firmware/tests/cross_layer/tb_cross_layer_ft2232h.v @@ -86,6 +86,10 @@ module tb_cross_layer_ft2232h; reg [4:0] status_self_test_flags; reg [7:0] status_self_test_detail; reg status_self_test_busy; + reg [3:0] status_agc_current_gain; + reg [7:0] status_agc_peak_magnitude; + reg [7:0] status_agc_saturation_count; + reg status_agc_enable; // ---- Clock generators ---- always #(CLK_PERIOD / 2) clk = ~clk; @@ -130,7 +134,11 @@ module tb_cross_layer_ft2232h; .status_range_mode (status_range_mode), .status_self_test_flags (status_self_test_flags), .status_self_test_detail(status_self_test_detail), - .status_self_test_busy (status_self_test_busy) + .status_self_test_busy (status_self_test_busy), + .status_agc_current_gain (status_agc_current_gain), + .status_agc_peak_magnitude (status_agc_peak_magnitude), + .status_agc_saturation_count(status_agc_saturation_count), + .status_agc_enable (status_agc_enable) ); // ---- Test bookkeeping ---- @@ -188,6 +196,10 @@ module tb_cross_layer_ft2232h; status_self_test_flags = 5'b00000; status_self_test_detail = 8'd0; status_self_test_busy = 1'b0; + status_agc_current_gain = 4'd0; + status_agc_peak_magnitude = 8'd0; + status_agc_saturation_count = 8'd0; + status_agc_enable = 1'b0; repeat (6) @(posedge ft_clk); reset_n = 1; ft_reset_n = 1; @@ -605,6 +617,10 @@ module tb_cross_layer_ft2232h; status_self_test_flags = 5'b10101; status_self_test_detail = 8'hA5; status_self_test_busy = 1'b1; + status_agc_current_gain = 4'd7; + status_agc_peak_magnitude = 8'd200; + status_agc_saturation_count = 8'd15; + status_agc_enable = 1'b1; // Pulse status_request and capture bytes IN PARALLEL // (same reason as Exercise B — write FSM starts before CDC wait ends) diff --git a/9_Firmware/tests/cross_layer/test_cross_layer_contract.py b/9_Firmware/tests/cross_layer/test_cross_layer_contract.py index d3f74c0..c3b950e 100644 --- a/9_Firmware/tests/cross_layer/test_cross_layer_contract.py +++ b/9_Firmware/tests/cross_layer/test_cross_layer_contract.py @@ -100,6 +100,11 @@ GROUND_TRUTH_OPCODES = { 0x25: ("host_cfar_enable", 1), 0x26: ("host_mti_enable", 1), 0x27: ("host_dc_notch_width", 3), + 0x28: ("host_agc_enable", 1), + 0x29: ("host_agc_target", 8), + 0x2A: ("host_agc_attack", 4), + 0x2B: ("host_agc_decay", 4), + 0x2C: ("host_agc_holdoff", 4), 0x30: ("host_self_test_trigger", 1), # pulse 0x31: ("host_status_request", 1), # pulse 0xFF: ("host_status_request", 1), # alias, pulse @@ -124,6 +129,11 @@ GROUND_TRUTH_RESET_DEFAULTS = { "host_cfar_enable": 0, "host_mti_enable": 0, "host_dc_notch_width": 0, + "host_agc_enable": 0, + "host_agc_target": 200, + "host_agc_attack": 1, + "host_agc_decay": 1, + "host_agc_holdoff": 4, } GROUND_TRUTH_PACKET_CONSTANTS = { @@ -604,6 +614,10 @@ class TestTier2VerilogCosim: # status_self_test_flags = 5'b10101 = 21 # status_self_test_detail = 0xA5 # status_self_test_busy = 1 + # status_agc_current_gain = 7 + # status_agc_peak_magnitude = 200 + # status_agc_saturation_count = 15 + # status_agc_enable = 1 # Words 1-5 should be correct (no truncation bug) assert sr.cfar_threshold == 0xABCD, f"cfar_threshold: 0x{sr.cfar_threshold:04X}" @@ -618,6 +632,12 @@ class TestTier2VerilogCosim: assert sr.self_test_detail == 0xA5, f"self_test_detail: 0x{sr.self_test_detail:02X}" assert sr.self_test_busy == 1, f"self_test_busy: {sr.self_test_busy}" + # AGC fields (word 4) + assert sr.agc_current_gain == 7, f"agc_current_gain: {sr.agc_current_gain}" + assert sr.agc_peak_magnitude == 200, f"agc_peak_magnitude: {sr.agc_peak_magnitude}" + assert sr.agc_saturation_count == 15, f"agc_saturation_count: {sr.agc_saturation_count}" + assert sr.agc_enable == 1, f"agc_enable: {sr.agc_enable}" + # Word 0: stream_ctrl should be 5 (3'b101) assert sr.stream_ctrl == 5, ( f"stream_ctrl: {sr.stream_ctrl} != 5. "