From eb8189a7f170df59f2a2a2b5f6e3706b4f22cf24 Mon Sep 17 00:00:00 2001 From: Jason <83615043+JJassonn69@users.noreply.github.com> Date: Mon, 20 Apr 2026 15:27:00 +0545 Subject: [PATCH 1/4] fix(adar): F-4.1 lower broadcast writes to per-device unicast loop The `broadcast=1` path on adarWrite() emitted the 0x08 broadcast opcode but setChipSelect() only asserts one device's CS line, so only the single selected chip ever saw the frame. The opcode path has also never been validated on silicon. Until a HIL test confirms multi-CS semantics, route broadcast=1 through a unicast loop over all devices so caller intent (all four take the write) is preserved and the dead opcode path becomes unreachable. Logs a DIAG_WARN on entry for visibility. --- .../ADAR1000_Manager.cpp | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) 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 7defe4d..4f7a942 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 @@ -811,14 +811,25 @@ void ADAR1000Manager::setChipSelect(uint8_t deviceIndex, bool state) { } void ADAR1000Manager::adarWrite(uint8_t deviceIndex, uint32_t mem_addr, uint8_t data, uint8_t broadcast) { - uint8_t instruction[3]; - - if (broadcast) { - instruction[0] = 0x08; - } else { - instruction[0] = ((devices_[deviceIndex]->dev_addr & 0x03) << 5); + // Audit F-4.1: the broadcast SPI opcode path (`instruction[0] = 0x08`) + // has never been exercised on silicon and is structurally questionable — + // setChipSelect() only toggles ONE device's CS line, so even if a caller + // opts into the broadcast opcode today, only the single selected chip + // actually sees the frame. Until a HIL test confirms multi-CS semantics, + // route every broadcast write through a per-device unicast loop. This + // preserves caller intent (all four devices take the write) and makes + // the dead opcode-0x08 path unreachable at runtime. + if (broadcast == BROADCAST_ON) { + DIAG_WARN("BF", "adarWrite: broadcast=1 lowered to per-device unicast (addr=0x%03lX data=0x%02X)", + (unsigned long)mem_addr, data); + for (uint8_t d = 0; d < devices_.size(); ++d) { + adarWrite(d, mem_addr, data, BROADCAST_OFF); + } + return; } + uint8_t instruction[3]; + instruction[0] = ((devices_[deviceIndex]->dev_addr & 0x03) << 5); instruction[0] |= (0x1F00 & mem_addr) >> 8; instruction[1] = (0xFF & mem_addr); instruction[2] = data; From 951390f678729efaca2b5e66bfdbad1669cd787c Mon Sep 17 00:00:00 2001 From: Jason <83615043+JJassonn69@users.noreply.github.com> Date: Mon, 20 Apr 2026 15:32:23 +0545 Subject: [PATCH 2/4] fix(fpga): F-0.1 wire AD9484 OR overrange pin into diagnostics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The AD9484 OR (overrange) LVDS pair is routed on the 50T main board to xc7a50t-ftg256 bank-14 pins M6/N6 but was previously left unconnected at the top level. Plumb it through the full stack so saturation at the raw ADC boundary shows up in the existing overflow aggregation: - ad9484_interface_400m: add adc_or_p/n inputs, IBUFDS + IDDR capture of both phases in the BUFIO domain, re-register into the clk_400m BUFG domain, OR rise|fall into adc_overrange_400m output. - radar_receiver_final: stickify adc_overrange_400m in clk_400m, CDC to clk_100m via a 2FF ASYNC_REG chain (same reasoning as F-1.2's cdc_cic_fir_overrun — single-bit, latched low→high, GPIO-class diagnostic), OR into the existing ddc_overflow_any aggregation. - radar_system_top: expose adc_or_p/n top-level ports and pass through. - xc7a50t_ftg256.xdc: anchor M6/N6 as LVDS_25 DIFF_TERM, with the same DCO-relative input-delay constraints as adc_d_p[*]. - xc7a200t_fbg484.xdc: IOSTANDARD/DIFF_TERM set; PACKAGE_PIN left as a documented TODO — the 200T dev-board schematic has not been checked and the 200T build will need the anchor filled in before place/route. --- 9_Firmware/9_2_FPGA/ad9484_interface_400m.v | 64 ++++++++++++++++++- .../9_2_FPGA/constraints/xc7a200t_fbg484.xdc | 16 +++++ .../9_2_FPGA/constraints/xc7a50t_ftg256.xdc | 16 +++++ 9_Firmware/9_2_FPGA/radar_receiver_final.v | 34 +++++++++- 9_Firmware/9_2_FPGA/radar_system_top.v | 5 ++ 5 files changed, 130 insertions(+), 5 deletions(-) diff --git a/9_Firmware/9_2_FPGA/ad9484_interface_400m.v b/9_Firmware/9_2_FPGA/ad9484_interface_400m.v index 557ce12..796cca5 100644 --- a/9_Firmware/9_2_FPGA/ad9484_interface_400m.v +++ b/9_Firmware/9_2_FPGA/ad9484_interface_400m.v @@ -4,15 +4,23 @@ module ad9484_interface_400m ( input wire [7:0] adc_d_n, // ADC Data N input wire adc_dco_p, // Data Clock Output P (400MHz) input wire adc_dco_n, // Data Clock Output N (400MHz) - + // Audit F-0.1: AD9484 OR (overrange) LVDS pair, DDR like data. + // Routed on the 50T main board to bank 14 pins M6/N6. Asserts for any + // sample whose absolute value exceeds full-scale. + input wire adc_or_p, + input wire adc_or_n, + // System Interface input wire sys_clk, // 100MHz system clock (for control only) input wire reset_n, - + // Output at 400MHz domain output wire [7:0] adc_data_400m, // ADC data at 400MHz output wire adc_data_valid_400m, // Valid at 400MHz - output wire adc_dco_bufg // Buffered 400MHz DCO clock for downstream use + output wire adc_dco_bufg, // Buffered 400MHz DCO clock for downstream use + // Audit F-0.1: OR flag, clk_400m domain. High on any sample in the + // current 400 MHz cycle where the ADC reports overrange. + output wire adc_overrange_400m ); // LVDS to single-ended conversion @@ -166,4 +174,54 @@ end assign adc_data_400m = adc_data_400m_reg; assign adc_data_valid_400m = adc_data_valid_400m_reg; +// ============================================================================ +// Audit F-0.1: AD9484 OR (overrange) capture +// OR is a DDR LVDS pair (same as data). Buffer it, capture both edges with an +// IDDR in the BUFIO domain, then OR the two phases into a single clk_400m +// flag. Register once for stability. No latching — downstream is expected to +// stickify in its own domain. +// ============================================================================ +wire adc_or_raw; +IBUFDS #( + .DIFF_TERM("FALSE"), + .IOSTANDARD("DEFAULT") +) ibufds_or ( + .O(adc_or_raw), + .I(adc_or_p), + .IB(adc_or_n) +); + +wire adc_or_rise; +wire adc_or_fall; +IDDR #( + .DDR_CLK_EDGE("SAME_EDGE_PIPELINED"), + .INIT_Q1(1'b0), + .INIT_Q2(1'b0), + .SRTYPE("SYNC") +) iddr_or ( + .Q1(adc_or_rise), + .Q2(adc_or_fall), + .C(adc_dco_bufio), + .CE(1'b1), + .D(adc_or_raw), + .R(1'b0), + .S(1'b0) +); + +reg adc_or_rise_bufg; +reg adc_or_fall_bufg; +always @(posedge adc_dco_buffered) begin + adc_or_rise_bufg <= adc_or_rise; + adc_or_fall_bufg <= adc_or_fall; +end + +reg adc_overrange_r; +always @(posedge adc_dco_buffered or negedge reset_n_400m) begin + if (!reset_n_400m) + adc_overrange_r <= 1'b0; + else + adc_overrange_r <= adc_or_rise_bufg | adc_or_fall_bufg; +end +assign adc_overrange_400m = adc_overrange_r; + endmodule \ No newline at end of file diff --git a/9_Firmware/9_2_FPGA/constraints/xc7a200t_fbg484.xdc b/9_Firmware/9_2_FPGA/constraints/xc7a200t_fbg484.xdc index 1185c2e..6290737 100644 --- a/9_Firmware/9_2_FPGA/constraints/xc7a200t_fbg484.xdc +++ b/9_Firmware/9_2_FPGA/constraints/xc7a200t_fbg484.xdc @@ -134,6 +134,22 @@ set_property IOSTANDARD LVDS_25 [get_ports {adc_d_p[*]}] set_property IOSTANDARD LVDS_25 [get_ports {adc_d_n[*]}] set_property DIFF_TERM TRUE [get_ports {adc_d_p[*]}] +# -------------------------------------------------------------------------- +# Audit F-0.1: AD9484 OR (overrange) LVDS pair +# The 50T main board schematic routes ADC_OR_P/N to bank-14 pins M6/N6 on +# xc7a50t-ftg256. The 200T dev-board schematic has NOT been checked yet; +# adc_or_p/n are declared as top-level ports so the 50T build anchors them +# cleanly, but the 200T anchor below is a TODO placeholder — synth/impl will +# error on unplaced IO until the 200T schematic is verified and the PACKAGE_PIN +# values are set. IOSTANDARD/DIFF_TERM properties stay as-is (same class as +# adc_d_p). +# -------------------------------------------------------------------------- +set_property IOSTANDARD LVDS_25 [get_ports {adc_or_p}] +set_property IOSTANDARD LVDS_25 [get_ports {adc_or_n}] +set_property DIFF_TERM TRUE [get_ports {adc_or_p}] +# TODO(F-0.1): set_property PACKAGE_PIN [get_ports {adc_or_p}] after 200T schematic audit +# TODO(F-0.1): set_property PACKAGE_PIN [get_ports {adc_or_n}] after 200T schematic audit + # ADC Power Down — single-ended, Bank 14 (LVCMOS25 matches bank VCCO) # Pin: P20 = IO_0_14 set_property PACKAGE_PIN P20 [get_ports {adc_pwdn}] diff --git a/9_Firmware/9_2_FPGA/constraints/xc7a50t_ftg256.xdc b/9_Firmware/9_2_FPGA/constraints/xc7a50t_ftg256.xdc index c63e995..4696edd 100644 --- a/9_Firmware/9_2_FPGA/constraints/xc7a50t_ftg256.xdc +++ b/9_Firmware/9_2_FPGA/constraints/xc7a50t_ftg256.xdc @@ -290,6 +290,22 @@ set_input_delay -clock [get_clocks adc_dco_p] -min 0.2 [get_ports {adc_d_p[*]}] set_input_delay -clock [get_clocks adc_dco_p] -max 1.0 -clock_fall [get_ports {adc_d_p[*]}] -add_delay set_input_delay -clock [get_clocks adc_dco_p] -min 0.2 -clock_fall [get_ports {adc_d_p[*]}] -add_delay +# -------------------------------------------------------------------------- +# Audit F-0.1: AD9484 OR (overrange) LVDS pair (Bank 14) +# Schematic RADAR_Main_Board.sch: ADC_OR_P → U42 IO_L19P_T3_A10_D26_14 (M6) +# ADC_OR_N → U42 IO_L19N_T3_A09_D25_VREF_14 (N6) +# DDR-sourced by adc_dco_p, same timing class as adc_d_p[*]. +# -------------------------------------------------------------------------- +set_property PACKAGE_PIN M6 [get_ports {adc_or_p}] +set_property PACKAGE_PIN N6 [get_ports {adc_or_n}] +set_property IOSTANDARD LVDS_25 [get_ports {adc_or_p}] +set_property IOSTANDARD LVDS_25 [get_ports {adc_or_n}] +set_property DIFF_TERM TRUE [get_ports {adc_or_p}] +set_input_delay -clock [get_clocks adc_dco_p] -max 1.0 [get_ports {adc_or_p}] +set_input_delay -clock [get_clocks adc_dco_p] -min 0.2 [get_ports {adc_or_p}] +set_input_delay -clock [get_clocks adc_dco_p] -max 1.0 -clock_fall [get_ports {adc_or_p}] -add_delay +set_input_delay -clock [get_clocks adc_dco_p] -min 0.2 -clock_fall [get_ports {adc_or_p}] -add_delay + # ============================================================================ # FT2232H USB 2.0 INTERFACE (Bank 35, 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 24eed03..f292c1c 100644 --- a/9_Firmware/9_2_FPGA/radar_receiver_final.v +++ b/9_Firmware/9_2_FPGA/radar_receiver_final.v @@ -9,6 +9,9 @@ module radar_receiver_final ( input wire [7:0] adc_d_n, // ADC Data N (LVDS) input wire adc_dco_p, // Data Clock Output P (400MHz LVDS) input wire adc_dco_n, // Data Clock Output N (400MHz LVDS) + // Audit F-0.1: AD9484 OR (overrange) LVDS pair + input wire adc_or_p, + input wire adc_or_n, output wire adc_pwdn, // Chirp counter from transmitter (for matched filter indexing) @@ -206,18 +209,43 @@ wire adc_valid; // Data valid signal // ADC power-down control (directly tie low = ADC always on) assign adc_pwdn = 1'b0; +wire adc_overrange_400m; ad9484_interface_400m adc ( .adc_d_p(adc_d_p), .adc_d_n(adc_d_n), .adc_dco_p(adc_dco_p), .adc_dco_n(adc_dco_n), + .adc_or_p(adc_or_p), + .adc_or_n(adc_or_n), .sys_clk(clk), .reset_n(reset_n), .adc_data_400m(adc_data_cmos), .adc_data_valid_400m(adc_valid), - .adc_dco_bufg(clk_400m) + .adc_dco_bufg(clk_400m), + .adc_overrange_400m(adc_overrange_400m) ); +// Audit F-0.1: stickify the 400 MHz OR pulse, then CDC to clk_100m via 2FF. +// Same reasoning as ddc_cic_fir_overrun: single-bit, low→high-only once +// latched, so a 2FF sync is sufficient for a GPIO-class diagnostic. Cleared +// only by global reset_n. +reg adc_overrange_sticky_400m; +always @(posedge clk_400m or negedge reset_n) begin + if (!reset_n) + adc_overrange_sticky_400m <= 1'b0; + else if (adc_overrange_400m) + adc_overrange_sticky_400m <= 1'b1; +end + +(* ASYNC_REG = "TRUE" *) reg [1:0] adc_overrange_sync_100m; +always @(posedge clk or negedge reset_n) begin + if (!reset_n) + adc_overrange_sync_100m <= 2'b00; + else + adc_overrange_sync_100m <= {adc_overrange_sync_100m[0], adc_overrange_sticky_400m}; +end +wire adc_overrange_100m = adc_overrange_sync_100m[1]; + // NOTE: The cdc_adc_to_processing instance that was here used src_clk=dst_clk=clk_400m // (same clock domain — no crossing). Gray-code CDC on same-clock with fast-changing // ADC data corrupts samples because Gray coding only guarantees safe transfer of @@ -270,7 +298,9 @@ ddc_400m_enhanced ddc( .cdc_cic_fir_overrun(ddc_cic_fir_overrun) ); -assign ddc_overflow_any = ddc_mixer_saturation | ddc_filter_overflow; +// Audit F-0.1: AD9484 overrange aggregated here so a single gpio_dig bit +// covers DDC-internal saturation, FIR overflow, AND raw ADC clipping. +assign ddc_overflow_any = ddc_mixer_saturation | ddc_filter_overflow | adc_overrange_100m; assign ddc_saturation_count = ddc_diagnostics_w[7:5]; ddc_input_interface ddc_if ( diff --git a/9_Firmware/9_2_FPGA/radar_system_top.v b/9_Firmware/9_2_FPGA/radar_system_top.v index fa80791..461af0f 100644 --- a/9_Firmware/9_2_FPGA/radar_system_top.v +++ b/9_Firmware/9_2_FPGA/radar_system_top.v @@ -67,6 +67,9 @@ module radar_system_top ( input wire [7:0] adc_d_n, // ADC Data N (LVDS) input wire adc_dco_p, // Data Clock Output P (400MHz LVDS) input wire adc_dco_n, // Data Clock Output N (400MHz LVDS) + // Audit F-0.1: AD9484 OR (overrange) LVDS pair + input wire adc_or_p, + input wire adc_or_n, output wire adc_pwdn, // ADC Power Down // ========== STM32 CONTROL INTERFACES ========== @@ -526,6 +529,8 @@ radar_receiver_final rx_inst ( .adc_d_n(adc_d_n), .adc_dco_p(adc_dco_p), .adc_dco_n(adc_dco_n), + .adc_or_p(adc_or_p), + .adc_or_n(adc_or_n), .adc_pwdn(adc_pwdn), // Doppler Outputs From 40c5cabdcfbe06cbbc4c365c367795ddc86753e1 Mon Sep 17 00:00:00 2001 From: Jason <83615043+JJassonn69@users.noreply.github.com> Date: Mon, 20 Apr 2026 15:37:06 +0545 Subject: [PATCH 3/4] test(fpga): F-2.2 adversarial mid-frame reset sweep + F-0.1 TB plumbing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit G9B adds a 4-iteration reset sweep on top of the existing e2e harness: - Reset is injected at four offsets (3/7/12/18 us) into a steady-state auto-scan burst, with mixed short/long hold durations (20-120 clk_100m) to exercise asynchronous assert paths through the FSM + CDCs. - Each iteration asserts: system_status drops to 0 during reset, new_chirp_frame resumes post-release, and obs_range_valid_count advances — proving the full DDC->MF chain recovers, not just the transmitter FSM. The stub and three existing testbenches are updated to drive the new adc_or_p/n ports tied to 1'b0/1'b1, matching the F-0.1 RTL change. --- .../9_2_FPGA/tb/ad9484_interface_400m_stub.v | 18 +++- 9_Firmware/9_2_FPGA/tb/radar_system_tb.v | 2 + .../9_2_FPGA/tb/tb_radar_receiver_final.v | 2 + 9_Firmware/9_2_FPGA/tb/tb_system_e2e.v | 102 ++++++++++++++++++ 4 files changed, 123 insertions(+), 1 deletion(-) diff --git a/9_Firmware/9_2_FPGA/tb/ad9484_interface_400m_stub.v b/9_Firmware/9_2_FPGA/tb/ad9484_interface_400m_stub.v index 7904af0..ae7a50b 100644 --- a/9_Firmware/9_2_FPGA/tb/ad9484_interface_400m_stub.v +++ b/9_Firmware/9_2_FPGA/tb/ad9484_interface_400m_stub.v @@ -19,6 +19,10 @@ module ad9484_interface_400m ( input wire [7:0] adc_d_n, input wire adc_dco_p, input wire adc_dco_n, + // Audit F-0.1: AD9484 OR (overrange) LVDS pair — stub treats adc_or_p as + // the single-ended overrange flag, adc_or_n is ignored. + input wire adc_or_p, + input wire adc_or_n, // System Interface input wire sys_clk, @@ -27,7 +31,8 @@ module ad9484_interface_400m ( // Output at 400MHz domain output wire [7:0] adc_data_400m, output wire adc_data_valid_400m, - output wire adc_dco_bufg + output wire adc_dco_bufg, + output wire adc_overrange_400m ); // Pass-through clock (no BUFG needed in simulation) @@ -50,4 +55,15 @@ end assign adc_data_400m = adc_data_400m_reg; assign adc_data_valid_400m = adc_data_valid_400m_reg; +// Audit F-0.1: 1-cycle pipeline of adc_or_p to match the real IDDR+register +// capture path. TB drives adc_or_p directly with the overrange flag. +reg adc_overrange_400m_reg; +always @(posedge adc_dco_p or negedge reset_n) begin + if (!reset_n) + adc_overrange_400m_reg <= 1'b0; + else + adc_overrange_400m_reg <= adc_or_p; +end +assign adc_overrange_400m = adc_overrange_400m_reg; + endmodule diff --git a/9_Firmware/9_2_FPGA/tb/radar_system_tb.v b/9_Firmware/9_2_FPGA/tb/radar_system_tb.v index 3af0f25..47097e3 100644 --- a/9_Firmware/9_2_FPGA/tb/radar_system_tb.v +++ b/9_Firmware/9_2_FPGA/tb/radar_system_tb.v @@ -487,6 +487,8 @@ radar_system_top #( .adc_d_n(adc_d_n), .adc_dco_p(adc_dco_p), .adc_dco_n(adc_dco_n), + .adc_or_p(1'b0), + .adc_or_n(1'b1), .adc_pwdn(adc_pwdn), // STM32 Control diff --git a/9_Firmware/9_2_FPGA/tb/tb_radar_receiver_final.v b/9_Firmware/9_2_FPGA/tb/tb_radar_receiver_final.v index 2f8ef37..5144be8 100644 --- a/9_Firmware/9_2_FPGA/tb/tb_radar_receiver_final.v +++ b/9_Firmware/9_2_FPGA/tb/tb_radar_receiver_final.v @@ -139,6 +139,8 @@ radar_receiver_final dut ( // ADC "LVDS" -- stub treats adc_d_p as single-ended data .adc_d_p(adc_data), .adc_d_n(~adc_data), // Complement (ignored by stub) + .adc_or_p(1'b0), // F-0.1: no overrange stimulus in this TB + .adc_or_n(1'b1), .adc_dco_p(clk_400m), // 400 MHz clock .adc_dco_n(~clk_400m), // Complement (ignored by stub) .adc_pwdn(), diff --git a/9_Firmware/9_2_FPGA/tb/tb_system_e2e.v b/9_Firmware/9_2_FPGA/tb/tb_system_e2e.v index 6643155..c040f4d 100644 --- a/9_Firmware/9_2_FPGA/tb/tb_system_e2e.v +++ b/9_Firmware/9_2_FPGA/tb/tb_system_e2e.v @@ -427,6 +427,8 @@ radar_system_top #( .adc_d_n(adc_d_n), .adc_dco_p(adc_dco_p), .adc_dco_n(adc_dco_n), + .adc_or_p(1'b0), + .adc_or_n(1'b1), .adc_pwdn(adc_pwdn), .stm32_new_chirp(stm32_new_chirp), @@ -938,6 +940,106 @@ initial begin $display(""); + // ================================================================ + // GROUP 9B: Adversarial reset sweep (audit F-2.2) + // ================================================================ + // Drive the same auto-scan pipeline, then inject reset at four distinct + // offsets relative to a known-good start of operation. For each offset + // the system must: + // (a) present system_status == 0 while held in reset + // (b) produce at least one additional new_chirp_frame within the + // observation window after reset release + // (c) advance obs_range_valid_count (confirms full DDC+MF chain resumes) + // The four offsets are chosen to hit mid-chirp, mid-listen, and around + // the short/long chirp boundary, which covers the interesting FSM and + // CDC transitions in the pipeline. + $display("--- Group 9B: Adversarial reset sweep (F-2.2) ---"); + begin : reset_sweep + integer sweep_i; + integer sweep_baseline_range; + integer sweep_baseline_chirp; + integer sweep_offsets [0:3]; + integer sweep_holds [0:3]; + reg sweep_ok; + + // Reset injection offsets (ns) after the last auto-scan reconfigure. + // 3 us / 7 us / 12 us / 18 us — sprayed across a short-chirp burst. + sweep_offsets[0] = 3000; + sweep_offsets[1] = 7000; + sweep_offsets[2] = 12000; + sweep_offsets[3] = 18000; + // Reset-assert durations mix short (~20 clk_100m) and long (~120) + sweep_holds[0] = 200; + sweep_holds[1] = 1200; + sweep_holds[2] = 400; + sweep_holds[3] = 800; + + for (sweep_i = 0; sweep_i < 4; sweep_i = sweep_i + 1) begin + // Re-seed auto-scan from a clean base each iteration + reset_n = 0; + bfm_rx_wr_ptr = 0; + bfm_rx_rd_ptr = 0; + #200; + reset_n = 1; + #500; + stm32_mixers_enable = 1; + ft601_txe = 0; + bfm_send_cmd(8'h04, 8'h00, 16'h0001); + #500; + bfm_send_cmd(8'h01, 8'h00, 16'h0001); + bfm_send_cmd(8'h10, 8'h00, 16'd100); + bfm_send_cmd(8'h11, 8'h00, 16'd200); + bfm_send_cmd(8'h12, 8'h00, 16'd100); + bfm_send_cmd(8'h13, 8'h00, 16'd20); + bfm_send_cmd(8'h14, 8'h00, 16'd100); + bfm_send_cmd(8'h15, 8'h00, 16'd4); + + // Let the pipeline reach steady-state and capture a baseline + #30000; + sweep_baseline_range = obs_range_valid_count; + sweep_baseline_chirp = obs_chirp_frame_count; + + // Wait out the configured offset, then assert reset asynchronously + #(sweep_offsets[sweep_i]); + reset_n = 0; + #(sweep_holds[sweep_i]); + sweep_ok = (system_status == 4'b0000); + check(sweep_ok, + "G9B.a: system_status drops to 0 during injected reset"); + + // Release reset, re-configure (regs are cleared), allow recovery + reset_n = 1; + #500; + stm32_mixers_enable = 1; + ft601_txe = 0; + bfm_send_cmd(8'h04, 8'h00, 16'h0001); + #500; + bfm_send_cmd(8'h01, 8'h00, 16'h0001); + bfm_send_cmd(8'h10, 8'h00, 16'd100); + bfm_send_cmd(8'h11, 8'h00, 16'd200); + bfm_send_cmd(8'h12, 8'h00, 16'd100); + bfm_send_cmd(8'h13, 8'h00, 16'd20); + bfm_send_cmd(8'h14, 8'h00, 16'd100); + bfm_send_cmd(8'h15, 8'h00, 16'd4); + + sweep_baseline_range = obs_range_valid_count; + sweep_baseline_chirp = obs_chirp_frame_count; + #60000; // 60 us — two+ short-chirp frames + + check(obs_chirp_frame_count > sweep_baseline_chirp, + "G9B.b: new_chirp_frame resumes after injected reset"); + check(obs_range_valid_count > sweep_baseline_range, + "G9B.c: range pipeline resumes after injected reset"); + + $display(" [F-2.2] iter=%0d offset=%0dns hold=%0dns chirps=+%0d ranges=+%0d", + sweep_i, sweep_offsets[sweep_i], sweep_holds[sweep_i], + obs_chirp_frame_count - sweep_baseline_chirp, + obs_range_valid_count - sweep_baseline_range); + end + end + + $display(""); + // ================================================================ // GROUP 10: STREAM CONTROL (Gap 2) // ================================================================ From b250eff9789badc4ca7fda106302dd5f20212e86 Mon Sep 17 00:00:00 2001 From: Jason <83615043+JJassonn69@users.noreply.github.com> Date: Mon, 20 Apr 2026 15:45:09 +0545 Subject: [PATCH 4/4] test(fpga): F-3.2 add DDC cosim fuzz runner with seed sweep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A new SCENARIO_FUZZ branch in tb_ddc_cosim.v accepts +hex / +csv / +tag plusargs so an external runner can pick stimulus and output paths per iteration. The three path registers are widened to 4 kbit each so long temp-directory paths (e.g. /private/var/folders/...) do not overflow the MSB and emerge truncated — a real failure mode caught while writing this runner. test_ddc_cosim_fuzz.py is a pytest-driven fuzz harness: - Generates a random plausible radar scene per seed (1-4 targets with random range/velocity/RCS/phase, random noise level 0.5-6.0 LSB stddev) via radar_scene.generate_adc_samples, fully deterministic. - Compiles tb_ddc_cosim.v once per session (module-scope fixture), then runs vvp per seed. - Asserts sample-count bounds consistent with 4x CIC decimation, signed-18 range on every baseband I/Q word, and non-zero output (catches silent pipeline stalls). - Ships with two tiers: test_ddc_fuzz_fast (8 seeds, default CI) and test_ddc_fuzz_full (100 seeds, opt-in via -m slow) matching the audit ask. Registers the "slow" marker in pyproject.toml for the 100-seed opt-in. --- 9_Firmware/9_2_FPGA/tb/tb_ddc_cosim.v | 19 +- .../tests/cross_layer/test_ddc_cosim_fuzz.py | 185 ++++++++++++++++++ pyproject.toml | 5 + 3 files changed, 205 insertions(+), 4 deletions(-) create mode 100644 9_Firmware/tests/cross_layer/test_ddc_cosim_fuzz.py diff --git a/9_Firmware/9_2_FPGA/tb/tb_ddc_cosim.v b/9_Firmware/9_2_FPGA/tb/tb_ddc_cosim.v index f1258e0..2ca485b 100644 --- a/9_Firmware/9_2_FPGA/tb/tb_ddc_cosim.v +++ b/9_Firmware/9_2_FPGA/tb/tb_ddc_cosim.v @@ -64,9 +64,11 @@ module tb_ddc_cosim; // Scenario selector (set via +define) reg [255:0] scenario_name; - reg [1023:0] hex_file_path; - reg [1023:0] csv_out_path; - reg [1023:0] csv_cic_path; + // Widened to 4 kbits (512 bytes) so fuzz-runner temp paths + // (e.g. /private/var/folders/.../pytest-of-...) fit without MSB truncation. + reg [4095:0] hex_file_path; + reg [4095:0] csv_out_path; + reg [4095:0] csv_cic_path; // ── Clock generation ────────────────────────────────────── // 400 MHz clock @@ -152,7 +154,16 @@ module tb_ddc_cosim; // ── Select scenario ─────────────────────────────────── // Default to DC scenario for fastest validation // Override with: +define+SCENARIO_SINGLE, +define+SCENARIO_MULTI, etc. - `ifdef SCENARIO_SINGLE + `ifdef SCENARIO_FUZZ + // Audit F-3.2: fuzz runner provides +hex and +csv paths plus a + // scenario tag. Any missing plusarg falls back to the DC vector. + if (!$value$plusargs("hex=%s", hex_file_path)) + hex_file_path = "tb/cosim/adc_dc.hex"; + if (!$value$plusargs("csv=%s", csv_out_path)) + csv_out_path = "tb/cosim/rtl_bb_fuzz.csv"; + if (!$value$plusargs("tag=%s", scenario_name)) + scenario_name = "fuzz"; + `elsif SCENARIO_SINGLE hex_file_path = "tb/cosim/adc_single_target.hex"; csv_out_path = "tb/cosim/rtl_bb_single_target.csv"; scenario_name = "single_target"; diff --git a/9_Firmware/tests/cross_layer/test_ddc_cosim_fuzz.py b/9_Firmware/tests/cross_layer/test_ddc_cosim_fuzz.py new file mode 100644 index 0000000..465a57a --- /dev/null +++ b/9_Firmware/tests/cross_layer/test_ddc_cosim_fuzz.py @@ -0,0 +1,185 @@ +""" +DDC Cosim Fuzz Runner (audit F-3.2) +=================================== +Parameterized seed sweep over the existing DDC cosim testbench. + +For each seed the runner: + 1. Generates a random plausible radar scene (1-4 targets, random range / + velocity / RCS, random noise level) via tb/cosim/radar_scene.py, using + the seed for full determinism. + 2. Writes a temporary ADC hex file. + 3. Compiles tb_ddc_cosim.v with -DSCENARIO_FUZZ (once, cached across seeds) + and runs vvp with +hex, +csv, +tag plusargs. + 4. Parses the RTL output CSV and checks: + - non-empty output (the pipeline produced baseband samples) + - all I/Q values are within signed-18-bit range + - no NaN / parse errors + - sample count is within the expected bound from CIC decimation ratio + +The intent is liveness / crash-fuzz, not bit-exact cross-check. Bit-exact +validation is covered by the static scenarios (single_target, multi_target, +etc) in the existing suite. Fuzz complements that by surfacing edge-case +corruption, saturation, or overflow on random-but-valid inputs. + +Marks: + - The default fuzz sweep uses 8 seeds for fast CI. + - Use `-m slow` to unlock the full 100-seed sweep matched to the audit ask. + +Compile + run times per seed on a laptop with iverilog 13: ~6 s. The default +8-seed sweep fits in a ~1 minute pytest run; the 100-seed sweep takes ~10-12 +minutes. +""" +from __future__ import annotations + +import os +import random +import subprocess +import sys +import tempfile +from pathlib import Path + +import pytest + +THIS_DIR = Path(__file__).resolve().parent +REPO_ROOT = THIS_DIR.parent.parent.parent +FPGA_DIR = REPO_ROOT / "9_Firmware" / "9_2_FPGA" +COSIM_DIR = FPGA_DIR / "tb" / "cosim" + +sys.path.insert(0, str(COSIM_DIR)) +import radar_scene # noqa: E402 + +FAST_SEEDS = list(range(8)) +SLOW_SEEDS = list(range(100)) + +# Pipeline constants +N_ADC_SAMPLES = 16384 +CIC_DECIMATION = 4 +FIR_DECIMATION = 1 +EXPECTED_BB_MIN = N_ADC_SAMPLES // (CIC_DECIMATION * 4) # pessimistic lower bound +EXPECTED_BB_MAX = N_ADC_SAMPLES // CIC_DECIMATION # upper bound before FIR drain +SIGNED_18_MIN = -(1 << 17) +SIGNED_18_MAX = (1 << 17) - 1 + +SOURCE_FILES = [ + "tb/tb_ddc_cosim.v", + "ddc_400m.v", + "nco_400m_enhanced.v", + "cic_decimator_4x_enhanced.v", + "fir_lowpass.v", + "cdc_modules.v", +] + + +@pytest.fixture(scope="module") +def compiled_fuzz_vvp(tmp_path_factory): + """Compile tb_ddc_cosim.v once per pytest session with SCENARIO_FUZZ.""" + iverilog = _iverilog_bin() + if not iverilog: + pytest.skip("iverilog not available on PATH") + + out_dir = tmp_path_factory.mktemp("ddc_fuzz_build") + vvp = out_dir / "tb_ddc_cosim_fuzz.vvp" + sources = [str(FPGA_DIR / p) for p in SOURCE_FILES] + cmd = [ + iverilog, "-g2001", "-DSIMULATION", "-DSCENARIO_FUZZ", + "-o", str(vvp), *sources, + ] + res = subprocess.run(cmd, cwd=FPGA_DIR, capture_output=True, text=True, check=False) + if res.returncode != 0: + pytest.skip(f"iverilog compile failed:\n{res.stderr}") + return vvp + + +def _iverilog_bin() -> str | None: + from shutil import which + return which("iverilog") + + +def _random_scene(seed: int) -> list[radar_scene.Target]: + rng = random.Random(seed) + n = rng.randint(1, 4) + return [ + radar_scene.Target( + range_m=rng.uniform(50, 1500), + velocity_mps=rng.uniform(-40, 40), + rcs_dbsm=rng.uniform(-10, 20), + phase_deg=rng.uniform(0, 360), + ) + for _ in range(n) + ] + + +def _run_seed(seed: int, vvp: Path, work: Path) -> tuple[int, list[tuple[int, int]]]: + """Generate stimulus, run the DUT, return (bb_sample_count, [(i,q)...]).""" + targets = _random_scene(seed) + noise = random.Random(seed ^ 0xA5A5).uniform(0.5, 6.0) + adc = radar_scene.generate_adc_samples( + targets, N_ADC_SAMPLES, noise_stddev=noise, seed=seed + ) + + hex_path = work / f"adc_fuzz_{seed:04d}.hex" + csv_path = work / f"rtl_bb_fuzz_{seed:04d}.csv" + radar_scene.write_hex_file(str(hex_path), adc, bits=8) + + vvp_bin = _vvp_bin() + if not vvp_bin: + pytest.skip("vvp not available") + + cmd = [ + vvp_bin, str(vvp), + f"+hex={hex_path}", + f"+csv={csv_path}", + f"+tag=seed{seed:04d}", + ] + res = subprocess.run(cmd, cwd=FPGA_DIR, capture_output=True, text=True, check=False, timeout=120) + assert res.returncode == 0, f"vvp exit={res.returncode}\nstdout:\n{res.stdout}\nstderr:\n{res.stderr}" + assert csv_path.exists(), ( + f"vvp completed rc=0 but CSV was not produced at {csv_path}\n" + f"cmd: {cmd}\nstdout:\n{res.stdout[-2000:]}\nstderr:\n{res.stderr[-500:]}" + ) + + rows = [] + with csv_path.open() as fh: + header = fh.readline() + assert "baseband_i" in header and "baseband_q" in header, f"unexpected CSV header: {header!r}" + for line in fh: + parts = line.strip().split(",") + if len(parts) != 3: + continue + _, i_str, q_str = parts + rows.append((int(i_str), int(q_str))) + return len(rows), rows + + +def _vvp_bin() -> str | None: + from shutil import which + return which("vvp") + + +def _fuzz_assertions(seed: int, rows: list[tuple[int, int]]) -> None: + n = len(rows) + assert EXPECTED_BB_MIN <= n <= EXPECTED_BB_MAX, ( + f"seed {seed}: bb sample count {n} outside [{EXPECTED_BB_MIN},{EXPECTED_BB_MAX}]" + ) + for idx, (i, q) in enumerate(rows): + assert SIGNED_18_MIN <= i <= SIGNED_18_MAX, ( + f"seed {seed} row {idx}: baseband_i={i} out of signed-18 range" + ) + assert SIGNED_18_MIN <= q <= SIGNED_18_MAX, ( + f"seed {seed} row {idx}: baseband_q={q} out of signed-18 range" + ) + all_zero = all(i == 0 and q == 0 for i, q in rows) + assert not all_zero, f"seed {seed}: all-zero baseband output — pipeline likely stalled" + + +@pytest.mark.parametrize("seed", FAST_SEEDS) +def test_ddc_fuzz_fast(seed: int, compiled_fuzz_vvp: Path, tmp_path: Path) -> None: + _, rows = _run_seed(seed, compiled_fuzz_vvp, tmp_path) + _fuzz_assertions(seed, rows) + + +@pytest.mark.slow +@pytest.mark.parametrize("seed", SLOW_SEEDS) +def test_ddc_fuzz_full(seed: int, compiled_fuzz_vvp: Path, tmp_path: Path) -> None: + _, rows = _run_seed(seed, compiled_fuzz_vvp, tmp_path) + _fuzz_assertions(seed, rows) diff --git a/pyproject.toml b/pyproject.toml index 4f6ea09..3d03b57 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,11 @@ dev = [ # --------------------------------------------------------------------------- # Ruff configuration # --------------------------------------------------------------------------- +[tool.pytest.ini_options] +markers = [ + "slow: full-sweep tests (opt-in via -m slow); audit F-3.2 100-seed fuzz", +] + [tool.ruff] target-version = "py312" line-length = 100