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 32fc9ab..7defe4d 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 @@ -735,10 +735,21 @@ void ADAR1000Manager::setLNABias(bool enable) { } void ADAR1000Manager::delayUs(uint32_t microseconds) { - // Simple implementation - for F7 @ 216MHz, each loop ~7 cycles ≈ 0.032us - volatile uint32_t cycles = microseconds * 10; // Adjust this multiplier for your clock - while (cycles--) { - __NOP(); + // Audit F-4.7: the prior implementation was a calibrated __NOP() busy-loop + // that silently drifted with compiler optimization, cache state, and flash + // wait-states. The ADAR1000 PLL/TX settling times require a real clock, so + // we poll the DWT cycle counter instead. One-time TRCENA/CYCCNTENA enable + // is idempotent; subsequent calls skip the init branch via DWT->CTRL read. + if ((DWT->CTRL & DWT_CTRL_CYCCNTENA_Msk) == 0U) { + CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; + DWT->CYCCNT = 0U; + DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; + } + const uint32_t cycles_per_us = SystemCoreClock / 1000000U; + const uint32_t start = DWT->CYCCNT; + const uint32_t target = microseconds * cycles_per_us; + while ((DWT->CYCCNT - start) < target) { + /* CYCCNT wraps cleanly modulo 2^32 — subtraction stays correct. */ } } diff --git a/9_Firmware/9_2_FPGA/cdc_modules.v b/9_Firmware/9_2_FPGA/cdc_modules.v index cd270d3..d0416de 100644 --- a/9_Firmware/9_2_FPGA/cdc_modules.v +++ b/9_Firmware/9_2_FPGA/cdc_modules.v @@ -17,7 +17,12 @@ module cdc_adc_to_processing #( input wire [WIDTH-1:0] src_data, input wire src_valid, output wire [WIDTH-1:0] dst_data, - output wire dst_valid + output wire dst_valid, + // Audit F-1.2: overrun pulse in src_clk domain. Asserts for 1 src cycle + // whenever src_valid fires while the previous sample has not yet been + // acknowledged by the destination edge-detector (i.e., the transaction + // the CDC is silently dropping). Hold/count externally. + output wire overrun `ifdef FORMAL ,output wire [WIDTH-1:0] fv_src_data_reg, output wire [1:0] fv_src_toggle @@ -130,6 +135,36 @@ module cdc_adc_to_processing #( assign dst_data = dst_data_reg; assign dst_valid = dst_valid_reg; + // ------------------------------------------------------------------ + // Audit F-1.2: overrun detection + // + // The src-side `src_toggle` counter flips on each latched src_valid. + // We feed back a 1-bit "ack" toggle from the dst domain (flipped each + // time dst_valid fires) through a STAGES-deep synchronizer into the + // src domain. If a new src_valid arrives while src_toggle[0] already + // differs from the acked value, the previous sample is still in flight + // and this new latch drops it. Emit a 1-cycle overrun pulse. + // ------------------------------------------------------------------ + reg dst_ack_toggle; + always @(posedge dst_clk) begin + if (!dst_reset_n) dst_ack_toggle <= 1'b0; + else if (dst_valid_reg) dst_ack_toggle <= ~dst_ack_toggle; + end + + (* ASYNC_REG = "TRUE" *) reg [STAGES-1:0] ack_sync_chain; + always @(posedge src_clk) begin + if (!src_reset_n) ack_sync_chain <= {STAGES{1'b0}}; + else ack_sync_chain <= {ack_sync_chain[STAGES-2:0], dst_ack_toggle}; + end + wire ack_in_src = ack_sync_chain[STAGES-1]; + + reg overrun_r; + always @(posedge src_clk) begin + if (!src_reset_n) overrun_r <= 1'b0; + else overrun_r <= src_valid && (src_toggle[0] != ack_in_src); + end + assign overrun = overrun_r; + `ifdef FORMAL assign fv_src_data_reg = src_data_reg; assign fv_src_toggle = src_toggle; 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 b16ced3..c3c1254 100644 --- a/9_Firmware/9_2_FPGA/constraints/adc_clk_mmcm.xdc +++ b/9_Firmware/9_2_FPGA/constraints/adc_clk_mmcm.xdc @@ -47,8 +47,16 @@ set_max_delay -datapath_only -from [get_clocks clk_mmcm_out0] \ set_false_path -from [get_clocks clk_100m] -to [get_clocks clk_mmcm_out0] set_false_path -from [get_clocks clk_mmcm_out0] -to [get_clocks clk_100m] -set_false_path -from [get_clocks clk_mmcm_out0] -to [get_clocks ft601_clk_in] -set_false_path -from [get_clocks ft601_clk_in] -to [get_clocks clk_mmcm_out0] +# Audit F-0.6: the clock name `ft601_clk_in` does not exist on either 50T +# (FT2232H, clock is `ft_clkout`) or 200T builds in the current RTL. Waive +# against whichever USB-domain clock is actually defined in the build. +foreach _usb_clk {ft_clkout ft601_clk ft601_clk_in} { + if {[llength [get_clocks -quiet $_usb_clk]] > 0} { + set_false_path -from [get_clocks clk_mmcm_out0] -to [get_clocks $_usb_clk] + set_false_path -from [get_clocks $_usb_clk] -to [get_clocks clk_mmcm_out0] + } +} +unset _usb_clk set_false_path -from [get_clocks clk_mmcm_out0] -to [get_clocks clk_120m_dac] set_false_path -from [get_clocks clk_120m_dac] -to [get_clocks clk_mmcm_out0] @@ -59,7 +67,10 @@ set_false_path -from [get_clocks clk_120m_dac] -to [get_clocks clk_mmcm_out0] # LOCKED is not a valid timing startpoint (it's a combinational output of the # MMCM primitive). Use -through instead of -from to waive all paths that pass # through the LOCKED net. This avoids the CRITICAL WARNING from Build 19/20. -set_false_path -through [get_pins rx_inst/adc/mmcm_inst/mmcm_adc_400m/LOCKED] +# Audit F-0.7: the literal hierarchical path was missing the `u_core/` +# prefix and silently matched no pins. Use a hierarchical wildcard to +# catch the MMCM LOCKED pin regardless of wrapper hierarchy. +set_false_path -through [get_pins -hierarchical -filter {REF_PIN_NAME == LOCKED}] # -------------------------------------------------------------------------- # Hold waiver for source-synchronous ADC capture (BUFIO-clocked IDDR) diff --git a/9_Firmware/9_2_FPGA/constraints/xc7a50t_ftg256.xdc b/9_Firmware/9_2_FPGA/constraints/xc7a50t_ftg256.xdc index bc45ca0..c63e995 100644 --- a/9_Firmware/9_2_FPGA/constraints/xc7a50t_ftg256.xdc +++ b/9_Firmware/9_2_FPGA/constraints/xc7a50t_ftg256.xdc @@ -107,8 +107,15 @@ set_property PACKAGE_PIN C4 [get_ports {ft_clkout}] set_property IOSTANDARD LVCMOS33 [get_ports {ft_clkout}] create_clock -name ft_clkout -period 16.667 [get_ports {ft_clkout}] set_input_jitter [get_clocks ft_clkout] 0.2 -# N-type MRCC pin requires dedicated route override (Place 30-876) -set_property CLOCK_DEDICATED_ROUTE FALSE [get_nets {ft_clkout_IBUF}] +# N-type MRCC pin requires dedicated route override (Place 30-876). +# Audit F-0.4: the literal net name `ft_clkout_IBUF` exists post-synth but +# the XDC scan happens before synthesis, when the IBUF net does not yet +# exist — Vivado reported `No nets matched 'ft_clkout_IBUF'` + CRITICAL +# WARNING. Use -hierarchical -filter + -quiet so the constraint matches +# post-synth without warning during pre-synth XDC scan. The TCL duplicate +# at scripts/50t/build_50t.tcl:119 remains as belt-and-suspenders. +set_property -quiet CLOCK_DEDICATED_ROUTE FALSE \ + [get_nets -quiet -hierarchical -filter {NAME =~ *ft_clkout_IBUF}] # ============================================================================ # RESET (Active-Low) @@ -408,7 +415,17 @@ set_false_path -from [get_ports {stm32_mixers_enable}] # - Reset deassertion order is not functionally critical — all registers # come out of reset within a few cycles of each other # -------------------------------------------------------------------------- -set_false_path -from [get_cells reset_sync_reg[*]] -to [get_pins -filter {REF_PIN_NAME == CLR} -of_objects [get_cells -hierarchical -filter {PRIMITIVE_TYPE =~ REGISTER.*.*}]] +# Audit F-0.5: the literal cell name `reset_sync_reg[*]` does not match any +# cell in the post-synth netlist. The actual sync regs are +# `u_core/reset_sync_reg[0..1]`, `u_core/rx_inst/ddc/reset_sync_400m_reg[*]`, +# `u_core/gen_ft2232h.usb_inst/ft_reset_sync_reg[*]`, and peers under +# `u_core/reset_sync_120m_reg[*]`, `u_core/reset_sync_ft601_reg[*]`, +# `u_core/rx_inst/adc/reset_sync_400m_reg[*]`. The waiver below covers all +# of them by matching any register whose name contains `reset_sync`. +# Without this, STA runs recovery/removal on the fanout of each sync-chain +# output register (up to ~1000 loads pre-PR#113 replication). +set_false_path -from [get_cells -hierarchical -filter {NAME =~ *reset_sync*_reg*}] \ + -to [get_pins -hierarchical -filter {REF_PIN_NAME == CLR || REF_PIN_NAME == PRE}] # -------------------------------------------------------------------------- # Clock Domain Crossing false paths diff --git a/9_Firmware/9_2_FPGA/ddc_400m.v b/9_Firmware/9_2_FPGA/ddc_400m.v index 195fb41..f26a06e 100644 --- a/9_Firmware/9_2_FPGA/ddc_400m.v +++ b/9_Firmware/9_2_FPGA/ddc_400m.v @@ -25,7 +25,10 @@ module ddc_400m_enhanced ( input wire reset_monitors, output wire [31:0] debug_sample_count, output wire [17:0] debug_internal_i, - output wire [17:0] debug_internal_q + output wire [17:0] debug_internal_q, + // Audit F-1.2: sticky CIC→FIR CDC overrun flag (clk_400m domain). Goes + // high on the first dropped sample and stays high until reset_monitors. + output wire cdc_cic_fir_overrun ); // Parameters for numerical precision @@ -148,22 +151,20 @@ always @(posedge clk_400m or negedge reset_n) begin end end -// CDC synchronization for control signals (2-stage synchronizers) -(* ASYNC_REG = "TRUE" *) reg [1:0] mixers_enable_sync_chain; +// CDC synchronization for control signals (2-stage synchronizers). +// Audit F-1.3: the mixers_enable synchronizer was dead — its _sync output +// was never consumed (the NCO phase_valid uses the raw port), and the only +// caller (radar_receiver_final.v) ties the port to 1'b1. Removed. (* ASYNC_REG = "TRUE" *) reg [1:0] force_saturation_sync_chain; -wire mixers_enable_sync; wire force_saturation_sync; -assign mixers_enable_sync = mixers_enable_sync_chain[1]; assign force_saturation_sync = force_saturation_sync_chain[1]; // Sync reset via reset_400m (replicated, max_fanout=50). Was async on // reset_n_400m — see "400 MHz RESET DISTRIBUTION" comment above. always @(posedge clk_400m) begin if (reset_400m) begin - mixers_enable_sync_chain <= 2'b00; force_saturation_sync_chain <= 2'b00; end else begin - mixers_enable_sync_chain <= {mixers_enable_sync_chain[0], mixers_enable}; force_saturation_sync_chain <= {force_saturation_sync_chain[0], force_saturation}; end end @@ -600,7 +601,10 @@ assign cic_valid = cic_valid_i & cic_valid_q; wire fir_in_valid_i, fir_in_valid_q; wire fir_valid_i, fir_valid_q; wire fir_i_ready, fir_q_ready; -wire [17:0] fir_d_in_i, fir_d_in_q; +wire [17:0] fir_d_in_i, fir_d_in_q; +// Audit F-1.2: per-lane CIC→FIR CDC overrun pulses (clk_400m domain) +wire cdc_fir_i_overrun; +wire cdc_fir_q_overrun; cdc_adc_to_processing #( .WIDTH(18), @@ -613,7 +617,8 @@ cdc_adc_to_processing #( .src_data(cic_i_out), .src_valid(cic_valid_i), .dst_data(fir_d_in_i), - .dst_valid(fir_in_valid_i) + .dst_valid(fir_in_valid_i), + .overrun(cdc_fir_i_overrun) ); cdc_adc_to_processing #( @@ -627,9 +632,21 @@ cdc_adc_to_processing #( .src_data(cic_q_out), .src_valid(cic_valid_q), .dst_data(fir_d_in_q), - .dst_valid(fir_in_valid_q) + .dst_valid(fir_in_valid_q), + .overrun(cdc_fir_q_overrun) ); +// Audit F-1.2: sticky-latch the two per-lane overrun pulses in the 400 MHz +// domain and expose a single module-level flag. Cleared only by +// reset_monitors (or reset_n via reset_400m), matching the other DDC +// diagnostic latches (overflow/saturation). +reg cdc_cic_fir_overrun_sticky; +always @(posedge clk_400m) begin + if (reset_400m || reset_monitors) cdc_cic_fir_overrun_sticky <= 1'b0; + else if (cdc_fir_i_overrun || cdc_fir_q_overrun) cdc_cic_fir_overrun_sticky <= 1'b1; +end +assign cdc_cic_fir_overrun = cdc_cic_fir_overrun_sticky; + // ============================================================================ // FIR Filter Instances // ============================================================================ diff --git a/9_Firmware/9_2_FPGA/radar_receiver_final.v b/9_Firmware/9_2_FPGA/radar_receiver_final.v index e00cb7d..24eed03 100644 --- a/9_Firmware/9_2_FPGA/radar_receiver_final.v +++ b/9_Firmware/9_2_FPGA/radar_receiver_final.v @@ -83,7 +83,19 @@ module radar_receiver_final ( 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 + output wire [7:0] mti_saturation_count_out, + + // Range-bin decimator watchdog (audit F-6.4 — previously tied off + // with an ILA-only note). A high pulse here means the decimator + // FSM has not seen the expected number of input samples within + // its timeout window, i.e. the upstream FIR/CDC has stalled. + output wire range_decim_watchdog, + + // Audit F-1.2: sticky CIC→FIR CDC overrun flag. Asserts on the first + // silent sample drop between the 400 MHz CIC output and the 100 MHz + // FIR input; stays high until the next reset. OR'd into the GPIO + // diagnostic bit at the top level. + output wire ddc_cic_fir_overrun ); // ========== INTERNAL SIGNALS ========== @@ -254,7 +266,8 @@ ddc_400m_enhanced ddc( .reset_monitors(1'b0), .debug_sample_count(), .debug_internal_i(), - .debug_internal_q() + .debug_internal_q(), + .cdc_cic_fir_overrun(ddc_cic_fir_overrun) ); assign ddc_overflow_any = ddc_mixer_saturation | ddc_filter_overflow; @@ -404,7 +417,7 @@ range_bin_decimator #( .range_bin_index(decimated_range_bin), .decimation_mode(2'b01), // Peak detection mode .start_bin(10'd0), - .watchdog_timeout() // Diagnostic — unconnected (monitored via ILA if needed) + .watchdog_timeout(range_decim_watchdog) // Audit F-6.4 — plumbed out ); // ========== MTI CANCELLER (Ground Clutter Removal) ========== diff --git a/9_Firmware/9_2_FPGA/radar_system_top.v b/9_Firmware/9_2_FPGA/radar_system_top.v index fd4a9aa..fa80791 100644 --- a/9_Firmware/9_2_FPGA/radar_system_top.v +++ b/9_Firmware/9_2_FPGA/radar_system_top.v @@ -205,6 +205,11 @@ 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; +// Range-bin decimator watchdog (audit F-6.4). High = decimator stalled. +wire rx_range_decim_watchdog; +// CIC→FIR CDC overrun sticky (audit F-1.2). High = at least one baseband +// sample has been silently dropped between the 400 MHz CIC and 100 MHz FIR. +wire rx_ddc_cic_fir_overrun; // Data packing for USB wire [31:0] usb_range_profile; @@ -575,7 +580,10 @@ radar_receiver_final rx_inst ( .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) + .mti_saturation_count_out(rx_mti_saturation_count), + // Range-bin decimator watchdog (audit F-6.4) + .range_decim_watchdog(rx_range_decim_watchdog), + .ddc_cic_fir_overrun(rx_ddc_cic_fir_overrun) ); // ============================================================================ @@ -884,6 +892,19 @@ endgenerate // we simply sample them in clk_100m when the CDC'd pulse arrives. // Step 1: Toggle on cmd_valid pulse (ft601_clk domain) +// +// CDC INVARIANT (audit F-1.1): usb_cmd_opcode / usb_cmd_addr / usb_cmd_value +// / usb_cmd_data MUST be driven to their final values BEFORE usb_cmd_valid +// asserts, and held stable for at least (STAGES + 1) clk_100m cycles after +// (i.e., until cmd_valid_100m has pulsed in the destination domain). These +// buses cross from ft601_clk to clk_100m as quasi-static data, NOT through +// a synchronizer — only the toggle bit above is CDC'd. If a future edit +// moves the cmd_* register write to the SAME cycle as the toggle flip, or +// drops the stability hold, the clk_100m sampler at the command decoder +// will latch metastable bits and dispatch on a garbage opcode. +// The source-side FSM in usb_data_interface_ft2232h.v / usb_data_interface.v +// currently satisfies this by assigning the cmd_* buses several cycles +// before pulsing cmd_valid and leaving them stable until the next command. reg cmd_valid_toggle_ft601; always @(posedge ft601_clk_buf or negedge sys_reset_ft601_n) begin if (!sys_reset_ft601_n) @@ -1059,7 +1080,9 @@ assign system_status = status_reg; 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); + | (rx_mti_saturation_count != 8'd0) + | rx_range_decim_watchdog // audit F-6.4 + | rx_ddc_cic_fir_overrun; // audit F-1.2 assign gpio_dig6 = host_agc_enable; assign gpio_dig7 = 1'b0;