diff --git a/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/ADAR1000_Manager.cpp b/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/ADAR1000_Manager.cpp index 0e7c906..32fc9ab 100644 --- a/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/ADAR1000_Manager.cpp +++ b/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/ADAR1000_Manager.cpp @@ -10,15 +10,15 @@ extern SPI_HandleTypeDef hspi1; extern UART_HandleTypeDef huart3; // Chip Select GPIO definitions -static const struct { - GPIO_TypeDef* port; - uint16_t pin; -} CHIP_SELECTS[4] = { - {ADAR_1_CS_3V3_GPIO_Port, ADAR_1_CS_3V3_Pin}, // ADAR1000 #1 - {ADAR_2_CS_3V3_GPIO_Port, ADAR_2_CS_3V3_Pin}, // ADAR1000 #2 - {ADAR_3_CS_3V3_GPIO_Port, ADAR_3_CS_3V3_Pin}, // ADAR1000 #3 - {ADAR_4_CS_3V3_GPIO_Port, ADAR_4_CS_3V3_Pin} // ADAR1000 #4 -}; +static const struct { + GPIO_TypeDef* port; + uint16_t pin; +} CHIP_SELECTS[4] = { + {ADAR_1_CS_3V3_GPIO_Port, ADAR_1_CS_3V3_Pin}, // ADAR1000 #1 + {ADAR_2_CS_3V3_GPIO_Port, ADAR_2_CS_3V3_Pin}, // ADAR1000 #2 + {ADAR_3_CS_3V3_GPIO_Port, ADAR_3_CS_3V3_Pin}, // ADAR1000 #3 + {ADAR_4_CS_3V3_GPIO_Port, ADAR_4_CS_3V3_Pin} // ADAR1000 #4 +}; // ADAR1000 Vector Modulator lookup tables (128-state phase grid, 2.8125 deg step). // @@ -255,15 +255,15 @@ bool ADAR1000Manager::setBeamAngle(float angle_degrees, BeamDirection direction) for (uint8_t dev = 0; dev < devices_.size(); ++dev) { for (uint8_t ch = 0; ch < 4; ++ch) { - if (direction == BeamDirection::TX) { - adarSetTxPhase(dev, ch + 1, phase_settings[ch], BROADCAST_OFF); - adarSetTxVgaGain(dev, ch + 1, kDefaultTxVgaGain, BROADCAST_OFF); - } else { - adarSetRxPhase(dev, ch + 1, phase_settings[ch], BROADCAST_OFF); - adarSetRxVgaGain(dev, ch + 1, kDefaultRxVgaGain, BROADCAST_OFF); - } - } - } + if (direction == BeamDirection::TX) { + adarSetTxPhase(dev, ch + 1, phase_settings[ch], BROADCAST_OFF); + adarSetTxVgaGain(dev, ch + 1, kDefaultTxVgaGain, BROADCAST_OFF); + } else { + adarSetRxPhase(dev, ch + 1, phase_settings[ch], BROADCAST_OFF); + adarSetRxVgaGain(dev, ch + 1, kDefaultRxVgaGain, BROADCAST_OFF); + } + } + } return true; } @@ -433,10 +433,15 @@ bool ADAR1000Manager::initializeSingleDevice(uint8_t deviceIndex) { adarWrite(deviceIndex, REG_ADC_CONTROL, ADAR1000_ADC_2MHZ_CLK | ADAR1000_ADC_EN, BROADCAST_OFF); // Verify communication with scratchpad test + // Audit F-4.4: on SPI failure, previously marked the device initialized + // anyway, so downstream (e.g. PA enable) could drive PA gates out-of-spec + // on a dead bus. Now propagate the failure so initializeAllDevices aborts. DIAG("BF", " dev[%u] verifying SPI communication...", deviceIndex); bool comms_ok = verifyDeviceCommunication(deviceIndex); if (!comms_ok) { - DIAG_WARN("BF", " dev[%u] scratchpad verify FAILED but marking initialized anyway", deviceIndex); + DIAG_ERR("BF", " dev[%u] scratchpad verify FAILED -- device NOT marked initialized", deviceIndex); + devices_[deviceIndex]->initialized = false; + return false; } devices_[deviceIndex]->initialized = true; @@ -522,7 +527,7 @@ bool ADAR1000Manager::initializeADTR1107Sequence() { HAL_UART_Transmit(&huart3, success, sizeof(success) - 1, 1000); return true; -} +} bool ADAR1000Manager::setAllDevicesTXMode() { DIAG("BF", "setAllDevicesTXMode(): ADTR1107 -> TX, then configure ADAR1000s"); @@ -648,7 +653,7 @@ void ADAR1000Manager::setADTR1107Mode(BeamDirection direction) { } DIAG("BF", " ADTR1107 RX mode complete"); } -} +} void ADAR1000Manager::setADTR1107Control(bool tx_mode) { DIAG("BF", "setADTR1107Control(%s): setting TR switch on all %u devices, settling %lu us", @@ -727,7 +732,7 @@ void ADAR1000Manager::setLNABias(bool enable) { for (uint8_t dev = 0; dev < devices_.size(); ++dev) { adarWrite(dev, REG_LNA_BIAS_ON, lna_bias, BROADCAST_OFF); } -} +} void ADAR1000Manager::delayUs(uint32_t microseconds) { // Simple implementation - for F7 @ 216MHz, each loop ~7 cycles ≈ 0.032us @@ -835,12 +840,26 @@ uint8_t ADAR1000Manager::adarRead(uint8_t deviceIndex, uint32_t mem_addr) { } void ADAR1000Manager::adarSetBit(uint8_t deviceIndex, uint32_t mem_addr, uint8_t bit, uint8_t broadcast) { + // Audit F-4.2: broadcast-RMW is unsafe. The read samples a single device + // but the write fans out to all four, overwriting the other three with + // deviceIndex's state. Reject and surface the mistake. + if (broadcast == BROADCAST_ON) { + DIAG_ERR("BF", "adarSetBit: broadcast RMW is unsafe, ignored (dev=%u addr=0x%03lX bit=%u)", + deviceIndex, (unsigned long)mem_addr, bit); + return; + } uint8_t temp = adarRead(deviceIndex, mem_addr); uint8_t data = temp | (1 << bit); adarWrite(deviceIndex, mem_addr, data, broadcast); } void ADAR1000Manager::adarResetBit(uint8_t deviceIndex, uint32_t mem_addr, uint8_t bit, uint8_t broadcast) { + // Audit F-4.2: see adarSetBit. + if (broadcast == BROADCAST_ON) { + DIAG_ERR("BF", "adarResetBit: broadcast RMW is unsafe, ignored (dev=%u addr=0x%03lX bit=%u)", + deviceIndex, (unsigned long)mem_addr, bit); + return; + } uint8_t temp = adarRead(deviceIndex, mem_addr); uint8_t data = temp & ~(1 << bit); adarWrite(deviceIndex, mem_addr, data, broadcast); @@ -904,7 +923,7 @@ void ADAR1000Manager::adarSetTxPhase(uint8_t deviceIndex, uint8_t channel, uint8 adarWrite(deviceIndex, mem_addr_i, i_val, broadcast); adarWrite(deviceIndex, mem_addr_q, q_val, broadcast); - adarWrite(deviceIndex, REG_LOAD_WORKING, 0x1, broadcast); + adarWrite(deviceIndex, REG_LOAD_WORKING, LD_WRK_REGS_LDTX_OVERRIDE, broadcast); } void ADAR1000Manager::adarSetRxVgaGain(uint8_t deviceIndex, uint8_t channel, uint8_t gain, uint8_t broadcast) { @@ -929,11 +948,11 @@ void ADAR1000Manager::adarSetTxVgaGain(uint8_t deviceIndex, uint8_t channel, uin adarWrite(deviceIndex, REG_LOAD_WORKING, LD_WRK_REGS_LDTX_OVERRIDE, broadcast); } -void ADAR1000Manager::adarSetTxBias(uint8_t deviceIndex, uint8_t broadcast) { - adarWrite(deviceIndex, REG_BIAS_CURRENT_TX, kTxBiasCurrent, broadcast); - adarWrite(deviceIndex, REG_BIAS_CURRENT_TX_DRV, kTxDriverBiasCurrent, broadcast); - adarWrite(deviceIndex, REG_LOAD_WORKING, 0x2, broadcast); -} +void ADAR1000Manager::adarSetTxBias(uint8_t deviceIndex, uint8_t broadcast) { + adarWrite(deviceIndex, REG_BIAS_CURRENT_TX, kTxBiasCurrent, broadcast); + adarWrite(deviceIndex, REG_BIAS_CURRENT_TX_DRV, kTxDriverBiasCurrent, broadcast); + adarWrite(deviceIndex, REG_LOAD_WORKING, 0x2, broadcast); +} uint8_t ADAR1000Manager::adarAdcRead(uint8_t deviceIndex, uint8_t broadcast) { adarWrite(deviceIndex, REG_ADC_CONTROL, ADAR1000_ADC_ST_CONV, broadcast); 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 3777d33..b16ced3 100644 --- a/9_Firmware/9_2_FPGA/constraints/adc_clk_mmcm.xdc +++ b/9_Firmware/9_2_FPGA/constraints/adc_clk_mmcm.xdc @@ -88,8 +88,9 @@ set_false_path -hold -from [get_ports {adc_d_p[*]}] -to [get_clocks adc_dco_p] # Timing margin for 400 MHz critical paths # -------------------------------------------------------------------------- # Extra setup uncertainty forces Vivado to leave margin for temperature/voltage/ -# aging variation. Reduced from 200 ps to 100 ps after NCO→mixer pipeline -# register fix eliminated the dominant timing bottleneck (WNS went from +0.002ns -# to comfortable margin). 100 ps still provides ~4% guardband on the 2.5ns period. -# This is additive to the existing jitter-based uncertainty (~53 ps). -set_clock_uncertainty -setup -add 0.100 [get_clocks clk_mmcm_out0] +# aging variation. 150 ps absolute covers the built-in jitter-based value +# (~53 ps) plus ~100 ps temperature/voltage/aging guardband. +# NOTE: Vivado's set_clock_uncertainty does NOT accept -add; prior use of +# -add 0.100 was silently rejected as a CRITICAL WARNING, so no guardband +# was applied. Use an absolute value. (audit finding F-0.8) +set_clock_uncertainty -setup 0.150 [get_clocks clk_mmcm_out0] diff --git a/9_Firmware/9_2_FPGA/ddc_400m.v b/9_Firmware/9_2_FPGA/ddc_400m.v index baafa5a..195fb41 100644 --- a/9_Firmware/9_2_FPGA/ddc_400m.v +++ b/9_Firmware/9_2_FPGA/ddc_400m.v @@ -634,6 +634,11 @@ cdc_adc_to_processing #( // FIR Filter Instances // ============================================================================ +// FIR overflow flags (audit F-6.2 — previously dangling, now OR'd into +// module-level filter_overflow so the receiver can see FIR arithmetic overflow) +wire fir_i_overflow; +wire fir_q_overflow; + // FIR I channel fir_lowpass_parallel_enhanced fir_i_inst ( .clk(clk_100m), @@ -643,10 +648,10 @@ fir_lowpass_parallel_enhanced fir_i_inst ( .data_out(fir_i_out), .data_out_valid(fir_valid_i), .fir_ready(fir_i_ready), - .filter_overflow() + .filter_overflow(fir_i_overflow) ); -// FIR Q channel +// FIR Q channel fir_lowpass_parallel_enhanced fir_q_inst ( .clk(clk_100m), .reset_n(reset_n), @@ -655,10 +660,11 @@ fir_lowpass_parallel_enhanced fir_q_inst ( .data_out(fir_q_out), .data_out_valid(fir_valid_q), .fir_ready(fir_q_ready), - .filter_overflow() + .filter_overflow(fir_q_overflow) ); assign fir_valid = fir_valid_i & fir_valid_q; +assign filter_overflow = fir_i_overflow | fir_q_overflow; // ============================================================================ // Enhanced Output Stage diff --git a/9_Firmware/9_2_FPGA/mti_canceller.v b/9_Firmware/9_2_FPGA/mti_canceller.v index 418a5ad..3191bf7 100644 --- a/9_Firmware/9_2_FPGA/mti_canceller.v +++ b/9_Firmware/9_2_FPGA/mti_canceller.v @@ -58,7 +58,12 @@ module mti_canceller #( input wire mti_enable, // 1=MTI active, 0=pass-through // ========== STATUS ========== - output reg mti_first_chirp // 1 during first chirp (output muted) + output reg mti_first_chirp, // 1 during first chirp (output muted) + + // Audit F-6.3: count of saturated samples since last reset. Saturation + // here produces spurious Doppler harmonics (phantom targets at ±fs/2) + // and was previously invisible to the MCU. Saturates at 0xFF. + output reg [7:0] mti_saturation_count ); // ============================================================================ @@ -104,18 +109,30 @@ assign diff_q_sat = (diff_q_full > $signed({{2{1'b0}}, {(DATA_WIDTH-1){1'b1}}})) ? $signed({1'b1, {(DATA_WIDTH-1){1'b0}}}) : diff_q_full[DATA_WIDTH-1:0]; +// Saturation detection (F-6.3): the top two bits of the DATA_WIDTH+1 signed +// difference disagree iff the value exceeds the DATA_WIDTH signed range. +wire diff_i_overflow = (diff_i_full[DATA_WIDTH] != diff_i_full[DATA_WIDTH-1]); +wire diff_q_overflow = (diff_q_full[DATA_WIDTH] != diff_q_full[DATA_WIDTH-1]); + // ============================================================================ // MAIN LOGIC // ============================================================================ always @(posedge clk or negedge reset_n) begin if (!reset_n) begin - range_i_out <= {DATA_WIDTH{1'b0}}; - range_q_out <= {DATA_WIDTH{1'b0}}; - range_valid_out <= 1'b0; - range_bin_out <= 6'd0; - has_previous <= 1'b0; - mti_first_chirp <= 1'b1; + range_i_out <= {DATA_WIDTH{1'b0}}; + range_q_out <= {DATA_WIDTH{1'b0}}; + range_valid_out <= 1'b0; + range_bin_out <= 6'd0; + has_previous <= 1'b0; + mti_first_chirp <= 1'b1; + mti_saturation_count <= 8'd0; end else begin + // Count saturated MTI-active samples (F-6.3). Clamp at 0xFF. + if (range_valid_in && mti_enable && has_previous + && (diff_i_overflow || diff_q_overflow) + && (mti_saturation_count != 8'hFF)) begin + mti_saturation_count <= mti_saturation_count + 8'd1; + end // Default: no valid output range_valid_out <= 1'b0; diff --git a/9_Firmware/9_2_FPGA/radar_receiver_final.v b/9_Firmware/9_2_FPGA/radar_receiver_final.v index e86c34d..e00cb7d 100644 --- a/9_Firmware/9_2_FPGA/radar_receiver_final.v +++ b/9_Firmware/9_2_FPGA/radar_receiver_final.v @@ -74,7 +74,16 @@ module radar_receiver_final ( // 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 + output wire [3:0] agc_current_gain, // Effective gain_shift encoding + + // DDC overflow diagnostics (audit F-6.1 — previously deleted at boundary). + // Not yet plumbed into the USB status packet (protocol contract is frozen); + // exposed here for gpio aggregation and ILA mark_debug visibility. + output wire ddc_overflow_any, + output wire [2:0] ddc_saturation_count, + + // MTI 2-pulse canceller saturation count (audit F-6.3). + output wire [7:0] mti_saturation_count_out ); // ========== INTERNAL SIGNALS ========== @@ -211,6 +220,16 @@ wire signed [17:0] ddc_out_q; wire ddc_valid_i; wire ddc_valid_q; +// DDC diagnostic signals (audit F-6.1 — all outputs previously unconnected) +wire [1:0] ddc_status_w; +wire [7:0] ddc_diagnostics_w; +wire ddc_mixer_saturation; +wire ddc_filter_overflow; + +(* mark_debug = "true" *) wire ddc_mixer_saturation_dbg = ddc_mixer_saturation; +(* mark_debug = "true" *) wire ddc_filter_overflow_dbg = ddc_filter_overflow; +(* mark_debug = "true" *) wire [7:0] ddc_diagnostics_dbg = ddc_diagnostics_w; + ddc_400m_enhanced ddc( .clk_400m(clk_400m), // 400MHz clock from ADC DCO .clk_100m(clk), // 100MHz system clock //used by the 2 FIR @@ -219,12 +238,28 @@ ddc_400m_enhanced ddc( .adc_data_valid_i(adc_valid), // Valid at 400MHz .adc_data_valid_q(adc_valid), // Valid at 400MHz .baseband_i(ddc_out_i), // I output at 100MHz - .baseband_q(ddc_out_q), // Q output at 100MHz + .baseband_q(ddc_out_q), // Q output at 100MHz .baseband_valid_i(ddc_valid_i), // Valid at 100MHz - .baseband_valid_q(ddc_valid_q), - .mixers_enable(1'b1) + .baseband_valid_q(ddc_valid_q), + .mixers_enable(1'b1), + // Diagnostics (audit F-6.1) — previously all unconnected + .ddc_status(ddc_status_w), + .ddc_diagnostics(ddc_diagnostics_w), + .mixer_saturation(ddc_mixer_saturation), + .filter_overflow(ddc_filter_overflow), + // Test/debug inputs — explicit tie-low (were floating) + .test_mode(2'b00), + .test_phase_inc(16'h0000), + .force_saturation(1'b0), + .reset_monitors(1'b0), + .debug_sample_count(), + .debug_internal_i(), + .debug_internal_q() ); +assign ddc_overflow_any = ddc_mixer_saturation | ddc_filter_overflow; +assign ddc_saturation_count = ddc_diagnostics_w[7:5]; + ddc_input_interface ddc_if ( .clk(clk), .reset_n(reset_n), @@ -391,7 +426,8 @@ mti_canceller #( .range_valid_out(mti_range_valid), .range_bin_out(mti_range_bin), .mti_enable(host_mti_enable), - .mti_first_chirp(mti_first_chirp) + .mti_first_chirp(mti_first_chirp), + .mti_saturation_count(mti_saturation_count_out) ); // ========== FRAME SYNC FROM TRANSMITTER ========== @@ -430,12 +466,12 @@ assign range_data_32bit = {mti_range_q, mti_range_i}; assign range_data_valid = mti_range_valid; // ========== DOPPLER PROCESSOR ========== -doppler_processor_optimized #( - .DOPPLER_FFT_SIZE(16), - .RANGE_BINS(64), - .CHIRPS_PER_FRAME(32), - .CHIRPS_PER_SUBFRAME(16) -) doppler_proc ( +doppler_processor_optimized #( + .DOPPLER_FFT_SIZE(16), + .RANGE_BINS(64), + .CHIRPS_PER_FRAME(32), + .CHIRPS_PER_SUBFRAME(16) +) doppler_proc ( .clk(clk), .reset_n(reset_n), .range_data(range_data_32bit), @@ -498,4 +534,4 @@ assign agc_saturation_count = gc_saturation_count; assign agc_peak_magnitude = gc_peak_magnitude; assign agc_current_gain = gc_current_gain; -endmodule +endmodule diff --git a/9_Firmware/9_2_FPGA/radar_system_top.v b/9_Firmware/9_2_FPGA/radar_system_top.v index ffedfe9..fd4a9aa 100644 --- a/9_Firmware/9_2_FPGA/radar_system_top.v +++ b/9_Firmware/9_2_FPGA/radar_system_top.v @@ -198,6 +198,14 @@ wire [7:0] rx_agc_saturation_count; wire [7:0] rx_agc_peak_magnitude; wire [3:0] rx_agc_current_gain; +// DDC overflow diagnostics (audit F-6.1) — plumbed out of receiver so the +// DDC mixer_saturation / filter_overflow ports are no longer deleted at +// the boundary. Aggregated into gpio_dig5 alongside AGC saturation. +wire rx_ddc_overflow_any; +wire [2:0] rx_ddc_saturation_count; +// MTI saturation count (audit F-6.3). OR'd into gpio_dig5 for MCU visibility. +wire [7:0] rx_mti_saturation_count; + // Data packing for USB wire [31:0] usb_range_profile; wire usb_range_valid; @@ -243,12 +251,12 @@ reg [5:0] host_chirps_per_elev; // Opcode 0x15 (default 32) reg host_status_request; // Opcode 0xFF (self-clearing pulse) // Fix 4: Doppler/chirps mismatch protection -// DOPPLER_FRAME_CHIRPS is the fixed chirp count expected by the staggered-PRI -// Doppler path (16 long + 16 short). If host sets chirps_per_elev to a -// different value, Doppler accumulation is corrupted. Clamp at command decode -// and flag the mismatch so the host knows. -localparam DOPPLER_FRAME_CHIRPS = 32; // Total chirps per Doppler frame -reg chirps_mismatch_error; // Set if host tried to set chirps != FFT size +// DOPPLER_FRAME_CHIRPS is the fixed chirp count expected by the staggered-PRI +// Doppler path (16 long + 16 short). If host sets chirps_per_elev to a +// different value, Doppler accumulation is corrupted. Clamp at command decode +// and flag the mismatch so the host knows. +localparam DOPPLER_FRAME_CHIRPS = 32; // Total chirps per Doppler frame +reg chirps_mismatch_error; // Set if host tried to set chirps != FFT size // Fix 7: Range-mode register (opcode 0x20) // Future-proofing for 3km/10km antenna switching. @@ -562,7 +570,12 @@ radar_receiver_final rx_inst ( // AGC status outputs .agc_saturation_count(rx_agc_saturation_count), .agc_peak_magnitude(rx_agc_peak_magnitude), - .agc_current_gain(rx_agc_current_gain) + .agc_current_gain(rx_agc_current_gain), + // DDC overflow diagnostics (audit F-6.1) + .ddc_overflow_any(rx_ddc_overflow_any), + .ddc_saturation_count(rx_ddc_saturation_count), + // MTI saturation count (audit F-6.3) + .mti_saturation_count_out(rx_mti_saturation_count) ); // ============================================================================ @@ -578,21 +591,21 @@ assign rx_doppler_data_valid = rx_doppler_valid; // ============================================================================ // DC NOTCH FILTER (post-Doppler-FFT, pre-CFAR) // ============================================================================ -// Zeros out Doppler bins within ±host_dc_notch_width of DC for BOTH -// sub-frames in the dual 16-pt FFT architecture. -// doppler_bin[4:0] = {sub_frame, bin[3:0]}: -// Sub-frame 0: bins 0-15, DC = bin 0, wrap = bin 15 -// Sub-frame 1: bins 16-31, DC = bin 16, wrap = bin 31 -// notch_width=1 → zero bins {0,16}. notch_width=2 → zero bins -// {0,1,15,16,17,31}. etc. -// When host_dc_notch_width=0: pass-through (no zeroing). - -wire dc_notch_active; -wire [4:0] dop_bin_unsigned = rx_doppler_bin; -wire [3:0] bin_within_sf = dop_bin_unsigned[3:0]; -assign dc_notch_active = (host_dc_notch_width != 3'd0) && - (bin_within_sf < {1'b0, host_dc_notch_width} || - bin_within_sf > (4'd15 - {1'b0, host_dc_notch_width} + 4'd1)); +// Zeros out Doppler bins within ±host_dc_notch_width of DC for BOTH +// sub-frames in the dual 16-pt FFT architecture. +// doppler_bin[4:0] = {sub_frame, bin[3:0]}: +// Sub-frame 0: bins 0-15, DC = bin 0, wrap = bin 15 +// Sub-frame 1: bins 16-31, DC = bin 16, wrap = bin 31 +// notch_width=1 → zero bins {0,16}. notch_width=2 → zero bins +// {0,1,15,16,17,31}. etc. +// When host_dc_notch_width=0: pass-through (no zeroing). + +wire dc_notch_active; +wire [4:0] dop_bin_unsigned = rx_doppler_bin; +wire [3:0] bin_within_sf = dop_bin_unsigned[3:0]; +assign dc_notch_active = (host_dc_notch_width != 3'd0) && + (bin_within_sf < {1'b0, host_dc_notch_width} || + bin_within_sf > (4'd15 - {1'b0, host_dc_notch_width} + 4'd1)); // Notched Doppler data: zero I/Q when in notch zone, pass through otherwise wire [31:0] notched_doppler_data = dc_notch_active ? 32'd0 : rx_doppler_output; @@ -959,19 +972,19 @@ always @(posedge clk_100m_buf or negedge sys_reset_n) begin 8'h13: host_short_chirp_cycles <= usb_cmd_value; 8'h14: host_short_listen_cycles <= usb_cmd_value; 8'h15: begin - // Fix 4: Clamp chirps_per_elev to the fixed Doppler frame size. - // If host requests a different value, clamp and set error flag. - if (usb_cmd_value[5:0] > DOPPLER_FRAME_CHIRPS[5:0]) begin - host_chirps_per_elev <= DOPPLER_FRAME_CHIRPS[5:0]; - chirps_mismatch_error <= 1'b1; - end else if (usb_cmd_value[5:0] == 6'd0) begin - host_chirps_per_elev <= DOPPLER_FRAME_CHIRPS[5:0]; - chirps_mismatch_error <= 1'b1; - end else begin - host_chirps_per_elev <= usb_cmd_value[5:0]; - // Clear error only if value matches FFT size exactly - chirps_mismatch_error <= (usb_cmd_value[5:0] != DOPPLER_FRAME_CHIRPS[5:0]); - end + // Fix 4: Clamp chirps_per_elev to the fixed Doppler frame size. + // If host requests a different value, clamp and set error flag. + if (usb_cmd_value[5:0] > DOPPLER_FRAME_CHIRPS[5:0]) begin + host_chirps_per_elev <= DOPPLER_FRAME_CHIRPS[5:0]; + chirps_mismatch_error <= 1'b1; + end else if (usb_cmd_value[5:0] == 6'd0) begin + host_chirps_per_elev <= DOPPLER_FRAME_CHIRPS[5:0]; + chirps_mismatch_error <= 1'b1; + end else begin + host_chirps_per_elev <= usb_cmd_value[5:0]; + // Clear error only if value matches FFT size exactly + chirps_mismatch_error <= (usb_cmd_value[5:0] != DOPPLER_FRAME_CHIRPS[5:0]); + end end 8'h16: host_gain_shift <= usb_cmd_value[3:0]; // Fix 3: digital gain 8'h20: host_range_mode <= usb_cmd_value[1:0]; // Fix 7: range mode @@ -1040,7 +1053,13 @@ assign system_status = status_reg; // DIG_6: AGC enable flag — mirrors host_agc_enable so STM32 outer-loop AGC // tracks the FPGA register as single source of truth. // DIG_7: Reserved (tied low for future use). -assign gpio_dig5 = (rx_agc_saturation_count != 8'd0); +// gpio_dig5: "signal-chain clipped" — asserts on AGC saturation, DDC mixer/FIR +// overflow, or MTI 2-pulse saturation. Audit F-6.1/F-6.3: these were all +// previously invisible to the MCU. +assign gpio_dig5 = (rx_agc_saturation_count != 8'd0) + | rx_ddc_overflow_any + | (rx_ddc_saturation_count != 3'd0) + | (rx_mti_saturation_count != 8'd0); assign gpio_dig6 = host_agc_enable; assign gpio_dig7 = 1'b0; @@ -1075,4 +1094,4 @@ always @(posedge clk_100m_buf) begin end `endif -endmodule +endmodule