From f393e96d692f90a2f99e2fefb84a18337b707f68 Mon Sep 17 00:00:00 2001 From: Jason <83615043+JJassonn69@users.noreply.github.com> Date: Thu, 16 Apr 2026 16:18:52 +0545 Subject: [PATCH 1/7] feat(fpga): make FT2232H default USB interface, rewrite FT601 write FSM, add clock-loss watchdog - Set USB_MODE default to 1 (FT2232H) in radar_system_top.v; 200T build overrides to USB_MODE=0 via build_200t.tcl generic property - Rewrite FT601 write FSM: 4-state architecture with 3-word packed data, pending-flag gating, and frame sync counter - Add FT2232H read FSM rd_cmd_complete flag, stream field zeroing, and range_data_ready 1-cycle pipeline delay in both USB modules - Implement clock-loss watchdog: ft_heartbeat toggle + 16-bit timeout counter drives ft_clk_lost, feeding ft_effective_reset_n via 2-stage ASYNC_REG synchronizer chain - Fix sample_counter reset literal width (11'd0 -> 12'd0) - Add FT2232H I/O timing constraints to 50T XDC; fix dac_clk comments - Document vestigial ft601_txe_n/rxf_n ports (needed for 200T XDC) - Tie off AGC ports on TE0713 dev wrapper - Rewrite tb_usb_data_interface.v for new 4-state FSM (89 checks) - Add USB_MODE=1 regression runs; remove dead CHECK 5/6 loop - Update diag_log.h USB interface comment --- .../9_1_1_C_Cpp_Libraries/diag_log.h | 2 +- 9_Firmware/9_2_FPGA/constraints/README.md | 15 +- .../9_2_FPGA/constraints/xc7a50t_ftg256.xdc | 53 ++- 9_Firmware/9_2_FPGA/radar_system_top.v | 2 +- .../radar_system_top_te0713_umft601x_dev.v | 7 +- 9_Firmware/9_2_FPGA/run_regression.sh | 59 +-- .../9_2_FPGA/scripts/200t/build_200t.tcl | 3 + 9_Firmware/9_2_FPGA/tb/radar_system_tb.v | 12 +- 9_Firmware/9_2_FPGA/tb/tb_system_e2e.v | 22 +- .../9_2_FPGA/tb/tb_usb_data_interface.v | 390 ++++++++---------- 9_Firmware/9_2_FPGA/usb_data_interface.v | 315 ++++++++------ .../9_2_FPGA/usb_data_interface_ft2232h.v | 150 ++++++- 12 files changed, 623 insertions(+), 407 deletions(-) diff --git a/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/diag_log.h b/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/diag_log.h index 62f737b..a7cd4f9 100644 --- a/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/diag_log.h +++ b/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/diag_log.h @@ -112,7 +112,7 @@ extern "C" { * "BF" -- ADAR1000 beamformer * "PA" -- Power amplifier bias/monitoring * "FPGA" -- FPGA communication and handshake - * "USB" -- FT601 USB data path + * "USB" -- USB data path (FT2232H production / FT601 premium) * "PWR" -- Power sequencing and rail monitoring * "IMU" -- IMU/GPS/barometer sensors * "MOT" -- Stepper motor/scan mechanics diff --git a/9_Firmware/9_2_FPGA/constraints/README.md b/9_Firmware/9_2_FPGA/constraints/README.md index b4a16fd..8eb8705 100644 --- a/9_Firmware/9_2_FPGA/constraints/README.md +++ b/9_Firmware/9_2_FPGA/constraints/README.md @@ -32,8 +32,8 @@ the `USB_MODE` parameter in `radar_system_top.v`: | USB_MODE | Interface | Bus Width | Speed | Board Target | |----------|-----------|-----------|-------|--------------| -| 0 (default) | FT601 (USB 3.0) | 32-bit | 100 MHz | 200T premium dev board | -| 1 | FT2232H (USB 2.0) | 8-bit | 60 MHz | 50T production board | +| 0 | FT601 (USB 3.0) | 32-bit | 100 MHz | 200T premium dev board | +| 1 (default) | FT2232H (USB 2.0) | 8-bit | 60 MHz | 50T production board | ### How USB_MODE Works @@ -72,7 +72,8 @@ The parameter is set via a **wrapper module** that overrides the default: ``` - **200T dev board**: `radar_system_top` is used directly as the top module. - `USB_MODE` defaults to `0` (FT601). No wrapper needed. + `USB_MODE` defaults to `1` (FT2232H) since production is the primary target. + Override with `.USB_MODE(0)` for FT601 builds. ### RTL Files by USB Interface @@ -158,7 +159,7 @@ The build scripts automatically select the correct top module and constraints: You do NOT need to set `USB_MODE` manually. The top module selection handles it: - `radar_system_top_50t` forces `USB_MODE=1` internally -- `radar_system_top` defaults to `USB_MODE=0` +- `radar_system_top` defaults to `USB_MODE=1` (FT2232H, production default) ## How to Select Constraints in Vivado @@ -190,9 +191,9 @@ read_xdc constraints/te0713_te0701_minimal.xdc | Target | Top module | USB_MODE | USB Interface | Notes | |--------|------------|----------|---------------|-------| | 50T Production (FTG256) | `radar_system_top_50t` | 1 | FT2232H (8-bit) | Wrapper sets USB_MODE=1, ties off FT601 | -| 200T Dev (FBG484) | `radar_system_top` | 0 (default) | FT601 (32-bit) | No wrapper needed | -| Trenz TE0712/TE0701 | `radar_system_top_te0712_dev` | 0 (default) | FT601 (32-bit) | Minimal bring-up wrapper | -| Trenz TE0713/TE0701 | `radar_system_top_te0713_dev` | 0 (default) | FT601 (32-bit) | Alternate SoM wrapper | +| 200T Dev (FBG484) | `radar_system_top` | 0 (override) | FT601 (32-bit) | Build script overrides default USB_MODE=1 | +| Trenz TE0712/TE0701 | `radar_system_top_te0712_dev` | 0 (override) | FT601 (32-bit) | Minimal bring-up wrapper | +| Trenz TE0713/TE0701 | `radar_system_top_te0713_dev` | 0 (override) | FT601 (32-bit) | Alternate SoM wrapper | ## Trenz Split Status diff --git a/9_Firmware/9_2_FPGA/constraints/xc7a50t_ftg256.xdc b/9_Firmware/9_2_FPGA/constraints/xc7a50t_ftg256.xdc index c4d61f0..c9e9b28 100644 --- a/9_Firmware/9_2_FPGA/constraints/xc7a50t_ftg256.xdc +++ b/9_Firmware/9_2_FPGA/constraints/xc7a50t_ftg256.xdc @@ -70,9 +70,10 @@ set_input_jitter [get_clocks clk_100m] 0.1 # NOTE: The physical DAC (U3, AD9708) receives its clock directly from the # AD9523 via a separate net (DAC_CLOCK), NOT from the FPGA. The FPGA # uses this clock input for internal DAC data timing only. The RTL port -# `dac_clk` is an output that assigns clk_120m directly — it has no -# separate physical pin on this board and should be removed from the -# RTL or left unconnected. +# `dac_clk` is an RTL output that assigns clk_120m directly. It has no +# physical pin on the 50T board and is left unconnected here. The port +# CANNOT be removed from the RTL because the 200T board uses it with +# ODDR clock forwarding (pin H17, see xc7a200t_fbg484.xdc). # FIX: Moved from C13 (IO_L12N = N-type) to D13 (IO_L12P = P-type MRCC). # Clock inputs must use the P-type pin of an MRCC pair (PLIO-9 DRC). set_property PACKAGE_PIN D13 [get_ports {clk_120m_dac}] @@ -332,6 +333,44 @@ set_property DRIVE 8 [get_ports {ft_data[*]}] # ft_clkout constrained above in CLOCK CONSTRAINTS section (C4, 60 MHz) +# -------------------------------------------------------------------------- +# FT2232H Source-Synchronous Timing Constraints +# -------------------------------------------------------------------------- +# FT2232H 245 Synchronous FIFO mode timing (60 MHz, period = 16.667 ns): +# +# FPGA Read Path (FT2232H drives data, FPGA samples): +# - Data valid before CLKOUT rising edge: t_vr(max) = 7.0 ns +# - Data hold after CLKOUT rising edge: t_hr(min) = 0.0 ns +# - Input delay max = period - t_vr = 16.667 - 7.0 = 9.667 ns +# - Input delay min = t_hr = 0.0 ns +# +# FPGA Write Path (FPGA drives data, FT2232H samples): +# - Data setup before next CLKOUT rising: t_su = 5.0 ns +# - Data hold after CLKOUT rising: t_hd = 0.0 ns +# - Output delay max = period - t_su = 16.667 - 5.0 = 11.667 ns +# - Output delay min = t_hd = 0.0 ns +# -------------------------------------------------------------------------- + +# Input delays: FT2232H → FPGA (data bus and status signals) +set_input_delay -clock [get_clocks ft_clkout] -max 9.667 [get_ports {ft_data[*]}] +set_input_delay -clock [get_clocks ft_clkout] -min 0.0 [get_ports {ft_data[*]}] +set_input_delay -clock [get_clocks ft_clkout] -max 9.667 [get_ports {ft_rxf_n}] +set_input_delay -clock [get_clocks ft_clkout] -min 0.0 [get_ports {ft_rxf_n}] +set_input_delay -clock [get_clocks ft_clkout] -max 9.667 [get_ports {ft_txe_n}] +set_input_delay -clock [get_clocks ft_clkout] -min 0.0 [get_ports {ft_txe_n}] + +# Output delays: FPGA → FT2232H (control strobes and data bus when writing) +set_output_delay -clock [get_clocks ft_clkout] -max 11.667 [get_ports {ft_data[*]}] +set_output_delay -clock [get_clocks ft_clkout] -min 0.0 [get_ports {ft_data[*]}] +set_output_delay -clock [get_clocks ft_clkout] -max 11.667 [get_ports {ft_rd_n}] +set_output_delay -clock [get_clocks ft_clkout] -min 0.0 [get_ports {ft_rd_n}] +set_output_delay -clock [get_clocks ft_clkout] -max 11.667 [get_ports {ft_wr_n}] +set_output_delay -clock [get_clocks ft_clkout] -min 0.0 [get_ports {ft_wr_n}] +set_output_delay -clock [get_clocks ft_clkout] -max 11.667 [get_ports {ft_oe_n}] +set_output_delay -clock [get_clocks ft_clkout] -min 0.0 [get_ports {ft_oe_n}] +set_output_delay -clock [get_clocks ft_clkout] -max 11.667 [get_ports {ft_siwu}] +set_output_delay -clock [get_clocks ft_clkout] -min 0.0 [get_ports {ft_siwu}] + # ============================================================================ # STATUS / DEBUG OUTPUTS — NO PHYSICAL CONNECTIONS # ============================================================================ @@ -418,10 +457,10 @@ set_property BITSTREAM.CONFIG.UNUSEDPIN Pullup [current_design] # 4. JTAG: FPGA_TCK (L7), FPGA_TDI (N7), FPGA_TDO (N8), FPGA_TMS (M7). # Dedicated pins — no XDC constraints needed. # -# 5. dac_clk port: The RTL top module declares `dac_clk` as an output, but -# the physical board wires the DAC clock (AD9708 CLOCK pin) directly from -# the AD9523, not from the FPGA. This port should be removed from the RTL -# or left unconnected. It currently just assigns clk_120m_dac passthrough. +# 5. dac_clk port: Not connected on the 50T board (DAC clocked directly from +# AD9523). The RTL port exists for 200T board compatibility, where the FPGA +# forwards the DAC clock via ODDR to pin H17 with generated clock and +# timing constraints (see xc7a200t_fbg484.xdc). Do NOT remove from RTL. # # ============================================================================ # END OF CONSTRAINTS diff --git a/9_Firmware/9_2_FPGA/radar_system_top.v b/9_Firmware/9_2_FPGA/radar_system_top.v index dfafa65..a21cd54 100644 --- a/9_Firmware/9_2_FPGA/radar_system_top.v +++ b/9_Firmware/9_2_FPGA/radar_system_top.v @@ -142,7 +142,7 @@ module radar_system_top ( parameter USE_LONG_CHIRP = 1'b1; // Default to long chirp parameter DOPPLER_ENABLE = 1'b1; // Enable Doppler processing parameter USB_ENABLE = 1'b1; // Enable USB data transfer -parameter USB_MODE = 0; // 0=FT601 (32-bit, 200T), 1=FT2232H (8-bit, 50T) +parameter USB_MODE = 1; // 0=FT601 (32-bit, 200T), 1=FT2232H (8-bit, 50T production default) // ============================================================================ // INTERNAL SIGNALS diff --git a/9_Firmware/9_2_FPGA/radar_system_top_te0713_umft601x_dev.v b/9_Firmware/9_2_FPGA/radar_system_top_te0713_umft601x_dev.v index 121b507..68e0314 100644 --- a/9_Firmware/9_2_FPGA/radar_system_top_te0713_umft601x_dev.v +++ b/9_Firmware/9_2_FPGA/radar_system_top_te0713_umft601x_dev.v @@ -138,7 +138,12 @@ usb_data_interface usb_inst ( .status_range_mode(2'b01), .status_self_test_flags(5'b11111), .status_self_test_detail(8'hA5), - .status_self_test_busy(1'b0) + .status_self_test_busy(1'b0), + // AGC status: tie off with benign defaults (no AGC on dev board) + .status_agc_current_gain(4'd0), + .status_agc_peak_magnitude(8'd0), + .status_agc_saturation_count(8'd0), + .status_agc_enable(1'b0) ); endmodule diff --git a/9_Firmware/9_2_FPGA/run_regression.sh b/9_Firmware/9_2_FPGA/run_regression.sh index 43aac1d..9d45878 100755 --- a/9_Firmware/9_2_FPGA/run_regression.sh +++ b/9_Firmware/9_2_FPGA/run_regression.sh @@ -70,6 +70,7 @@ PROD_RTL=( xfft_16.v fft_engine.v usb_data_interface.v + usb_data_interface_ft2232h.v edge_detector.v radar_mode_controller.v rx_gain_control.v @@ -219,26 +220,9 @@ run_lint_static() { fi done - # --- Single-line regex checks across all production RTL --- - for f in "$@"; do - [[ -f "$f" ]] || continue - case "$f" in tb/*) continue ;; esac - - local linenum=0 - while IFS= read -r line; do - linenum=$((linenum + 1)) - - # CHECK 5: $readmemh / $readmemb in synthesizable code - # (Only valid in simulation blocks — flag if outside `ifdef SIMULATION) - # This is hard to check line-by-line without tracking ifdefs. - # Skip for v1. - - # CHECK 6: Unused `include files (informational only) - # Skip for v1. - - : # placeholder — prevents empty loop body - done < "$f" - done + # CHECK 5 ($readmemh in synth code) and CHECK 6 (unused includes) + # require multi-line ifdef tracking / cross-file analysis. Not feasible + # with line-by-line regex. Omitted — use Vivado lint instead. if [[ "$err_count" -gt 0 ]]; then echo -e "${RED}FAIL${NC} ($err_count errors, $warn_count warnings)" @@ -452,7 +436,7 @@ if [[ "$QUICK" -eq 0 ]]; then chirp_memory_loader_param.v latency_buffer.v \ matched_filter_multi_segment.v matched_filter_processing_chain.v \ range_bin_decimator.v doppler_processor.v xfft_16.v fft_engine.v \ - usb_data_interface.v edge_detector.v radar_mode_controller.v \ + usb_data_interface.v usb_data_interface_ft2232h.v edge_detector.v radar_mode_controller.v \ rx_gain_control.v cfar_ca.v mti_canceller.v fpga_self_test.v # E2E integration (46 strict checks: TX, RX, USB R/W, CDC, safety, reset) @@ -466,11 +450,40 @@ if [[ "$QUICK" -eq 0 ]]; then chirp_memory_loader_param.v latency_buffer.v \ matched_filter_multi_segment.v matched_filter_processing_chain.v \ range_bin_decimator.v doppler_processor.v xfft_16.v fft_engine.v \ - usb_data_interface.v edge_detector.v radar_mode_controller.v \ + usb_data_interface.v usb_data_interface_ft2232h.v edge_detector.v radar_mode_controller.v \ + rx_gain_control.v cfar_ca.v mti_canceller.v fpga_self_test.v + + # USB_MODE=1 (FT2232H production) variants of system tests + run_test "System Top USB_MODE=1 (FT2232H)" \ + tb/tb_system_ft2232h_reg.vvp \ + -DUSB_MODE_1 \ + tb/radar_system_tb.v radar_system_top.v \ + radar_transmitter.v dac_interface_single.v plfm_chirp_controller.v \ + radar_receiver_final.v tb/ad9484_interface_400m_stub.v \ + ddc_400m.v nco_400m_enhanced.v cic_decimator_4x_enhanced.v \ + cdc_modules.v fir_lowpass.v ddc_input_interface.v \ + chirp_memory_loader_param.v latency_buffer.v \ + matched_filter_multi_segment.v matched_filter_processing_chain.v \ + range_bin_decimator.v doppler_processor.v xfft_16.v fft_engine.v \ + usb_data_interface.v usb_data_interface_ft2232h.v edge_detector.v radar_mode_controller.v \ + rx_gain_control.v cfar_ca.v mti_canceller.v fpga_self_test.v + + run_test "System E2E USB_MODE=1 (FT2232H)" \ + tb/tb_system_e2e_ft2232h_reg.vvp \ + -DUSB_MODE_1 \ + tb/tb_system_e2e.v radar_system_top.v \ + radar_transmitter.v dac_interface_single.v plfm_chirp_controller.v \ + radar_receiver_final.v tb/ad9484_interface_400m_stub.v \ + ddc_400m.v nco_400m_enhanced.v cic_decimator_4x_enhanced.v \ + cdc_modules.v fir_lowpass.v ddc_input_interface.v \ + chirp_memory_loader_param.v latency_buffer.v \ + matched_filter_multi_segment.v matched_filter_processing_chain.v \ + range_bin_decimator.v doppler_processor.v xfft_16.v fft_engine.v \ + usb_data_interface.v usb_data_interface_ft2232h.v edge_detector.v radar_mode_controller.v \ rx_gain_control.v cfar_ca.v mti_canceller.v fpga_self_test.v else echo " (skipped receiver golden + system top + E2E — use without --quick)" - SKIP=$((SKIP + 4)) + SKIP=$((SKIP + 6)) fi echo "" diff --git a/9_Firmware/9_2_FPGA/scripts/200t/build_200t.tcl b/9_Firmware/9_2_FPGA/scripts/200t/build_200t.tcl index d7310cf..f048cfa 100644 --- a/9_Firmware/9_2_FPGA/scripts/200t/build_200t.tcl +++ b/9_Firmware/9_2_FPGA/scripts/200t/build_200t.tcl @@ -108,6 +108,9 @@ add_files -fileset constrs_1 -norecurse [file join $project_root "constraints" " set_property top $top_module [current_fileset] set_property verilog_define {FFT_XPM_BRAM} [current_fileset] +# Override USB_MODE to 0 (FT601) for 200T premium board. +# The RTL default is USB_MODE=1 (FT2232H, production 50T). +set_property generic {USB_MODE=0} [current_fileset] # ============================================================================== # 2. Synthesis 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 757ea3e..3af0f25 100644 --- a/9_Firmware/9_2_FPGA/tb/radar_system_tb.v +++ b/9_Firmware/9_2_FPGA/tb/radar_system_tb.v @@ -430,7 +430,13 @@ end // DUT INSTANTIATION // ============================================================================ -radar_system_top dut ( +radar_system_top #( +`ifdef USB_MODE_1 + .USB_MODE(1) // FT2232H interface (production 50T board) +`else + .USB_MODE(0) // FT601 interface (200T dev board) +`endif +) dut ( // System Clocks .clk_100m(clk_100m), .clk_120m_dac(clk_120m_dac), @@ -619,7 +625,11 @@ initial begin // Optional: dump specific signals for debugging $dumpvars(1, dut.tx_inst); $dumpvars(1, dut.rx_inst); + `ifdef USB_MODE_1 + $dumpvars(1, dut.gen_ft2232h.usb_inst); + `else $dumpvars(1, dut.gen_ft601.usb_inst); + `endif end endmodule 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 7a075c7..6643155 100644 --- a/9_Firmware/9_2_FPGA/tb/tb_system_e2e.v +++ b/9_Firmware/9_2_FPGA/tb/tb_system_e2e.v @@ -382,7 +382,13 @@ end // ============================================================================ // DUT INSTANTIATION // ============================================================================ -radar_system_top dut ( +radar_system_top #( +`ifdef USB_MODE_1 + .USB_MODE(1) // FT2232H interface (production 50T board) +`else + .USB_MODE(0) // FT601 interface (200T dev board) +`endif +) dut ( .clk_100m(clk_100m), .clk_120m_dac(clk_120m_dac), .ft601_clk_in(ft601_clk_in), @@ -554,10 +560,10 @@ initial begin do_reset; // CRITICAL: Configure stream control to range-only BEFORE any chirps - // fire. The USB write FSM blocks on doppler_valid_ft if doppler stream - // is enabled but no Doppler data arrives (needs 32 chirps/frame). - // Without this, the write FSM deadlocks and the read FSM can never - // activate (it requires write FSM == IDLE). + // fire. The USB write FSM gates on pending flags: if doppler stream is + // enabled but no Doppler data arrives (needs 32 chirps/frame), the FSM + // stays in IDLE waiting for doppler_data_pending. With the write FSM + // not in IDLE, the read FSM cannot activate (bus arbitration rule). bfm_send_cmd(8'h04, 8'h00, 16'h0001); // stream_control = range only // Wait for stream_control CDC to propagate (2-stage sync in ft601_clk) // Must be long enough that stream_ctrl_sync_1 is updated before any @@ -778,7 +784,7 @@ initial begin // Restore defaults for subsequent tests bfm_send_cmd(8'h01, 8'h00, 16'h0001); // mode = auto-scan - bfm_send_cmd(8'h04, 8'h00, 16'h0001); // keep range-only (prevents write FSM deadlock) + bfm_send_cmd(8'h04, 8'h00, 16'h0001); // keep range-only (TB lacks 32-chirp doppler data) bfm_send_cmd(8'h10, 8'h00, 16'd3000); // restore long chirp cycles $display(""); @@ -913,7 +919,7 @@ initial begin // Need to re-send configuration since reset clears all registers stm32_mixers_enable = 1; ft601_txe = 0; - bfm_send_cmd(8'h04, 8'h00, 16'h0001); // stream_control = range only (prevent deadlock) + bfm_send_cmd(8'h04, 8'h00, 16'h0001); // stream_control = range only (TB lacks doppler data) #500; // Wait for stream_control CDC bfm_send_cmd(8'h01, 8'h00, 16'h0001); // auto-scan bfm_send_cmd(8'h10, 8'h00, 16'd100); // short timing @@ -947,7 +953,7 @@ initial begin check(dut.host_stream_control == 3'b000, "G10.2: All streams disabled (stream_control = 3'b000)"); - // G10.3: Re-enable range only (keep range-only to prevent write FSM deadlock) + // G10.3: Re-enable range only (TB uses range-only — no doppler processing) bfm_send_cmd(8'h04, 8'h00, 16'h0001); // stream_control = 3'b001 check(dut.host_stream_control == 3'b001, "G10.3: Range stream re-enabled (stream_control = 3'b001)"); 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 082c192..e2862d5 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 @@ -6,15 +6,11 @@ module tb_usb_data_interface; localparam CLK_PERIOD = 10.0; // 100 MHz main clock localparam FT_CLK_PERIOD = 10.0; // 100 MHz FT601 clock (asynchronous) - // State definitions (mirror the DUT) - localparam [2:0] S_IDLE = 3'd0, - S_SEND_HEADER = 3'd1, - S_SEND_RANGE = 3'd2, - S_SEND_DOPPLER = 3'd3, - S_SEND_DETECT = 3'd4, - S_SEND_FOOTER = 3'd5, - S_WAIT_ACK = 3'd6, - S_SEND_STATUS = 3'd7; // Gap 2: status readback + // State definitions (mirror the DUT — 4-state packed-word FSM) + localparam [3:0] S_IDLE = 4'd0, + S_SEND_DATA_WORD = 4'd1, + S_SEND_STATUS = 4'd2, + S_WAIT_ACK = 4'd3; // ── Signals ──────────────────────────────────────────────── reg clk; @@ -219,9 +215,9 @@ module tb_usb_data_interface; end endtask - // ── Helper: wait for DUT to reach a specific state ───────── + // ── Helper: wait for DUT to reach a specific write FSM state ── task wait_for_state; - input [2:0] target; + input [3:0] target; input integer max_cyc; integer cnt; begin @@ -280,7 +276,7 @@ module tb_usb_data_interface; // Set data_pending flags directly via hierarchical access. // This is the standard TB technique for internal state setup — // bypasses the CDC path for immediate, reliable flag setting. - // Call BEFORE assert_range_valid in tests that need SEND_DOPPLER/DETECT. + // Call BEFORE assert_range_valid in tests that need doppler/cfar data. task preload_pending_data; begin @(posedge ft601_clk_in); @@ -354,24 +350,26 @@ module tb_usb_data_interface; end endtask - // Drive a complete packet through the FSM by sequentially providing - // range, doppler (4x), and cfar valid pulses. + // Drive a complete data packet through the new 3-word packed FSM. + // Pre-loads pending flags, triggers range_valid, and waits for IDLE. + // With the new FSM, all data is pre-packed in IDLE then sent as 3 words. task drive_full_packet; input [31:0] rng; input [15:0] dr; input [15:0] di; input det; begin - // Pre-load pending flags so FSM enters doppler/cfar states + // Set doppler/cfar captured values via CDC inputs + @(posedge clk); + doppler_real = dr; + doppler_imag = di; + cfar_detection = det; + @(posedge clk); + // Pre-load pending flags so FSM includes doppler/cfar in packet preload_pending_data; + // Trigger the packet assert_range_valid(rng); - wait_for_state(S_SEND_DOPPLER, 100); - pulse_doppler_once(dr, di); - pulse_doppler_once(dr, di); - pulse_doppler_once(dr, di); - pulse_doppler_once(dr, di); - wait_for_state(S_SEND_DETECT, 100); - pulse_cfar_once(det); + // Wait for complete packet cycle: IDLE → SEND_DATA_WORD(×3) → WAIT_ACK → IDLE wait_for_state(S_IDLE, 100); end endtask @@ -414,101 +412,138 @@ module tb_usb_data_interface; "ft601_siwu_n=1 after reset"); // ════════════════════════════════════════════════════════ - // TEST GROUP 2: Range data packet + // TEST GROUP 2: Data packet word packing // - // Use backpressure to freeze the FSM at specific states - // so we can reliably sample outputs. + // New FSM packs 11-byte data into 3 × 32-bit words: + // Word 0: {HEADER, range[31:24], range[23:16], range[15:8]} + // Word 1: {range[7:0], dop_re_hi, dop_re_lo, dop_im_hi} + // Word 2: {dop_im_lo, detection, FOOTER, 0x00} BE=1110 + // + // The DUT uses range_data_ready (1-cycle delayed range_valid_ft) + // to trigger packing. Doppler/CFAR _cap registers must be + // pre-loaded via hierarchical access because no valid pulse is + // given in this test (we only want to verify packing, not CDC). // ════════════════════════════════════════════════════════ - $display("\n--- Test Group 2: Range Data Packet ---"); + $display("\n--- Test Group 2: Data Packet Word Packing ---"); apply_reset; + ft601_txe = 1; // Stall so we can inspect packed words - // Stall at SEND_HEADER so we can verify first range word later - ft601_txe = 1; + // Set known doppler/cfar values on clk-domain inputs + @(posedge clk); + doppler_real = 16'hABCD; + doppler_imag = 16'hEF01; + cfar_detection = 1'b1; + @(posedge clk); + + // Pre-load pending flags AND captured-data registers directly. + // No doppler/cfar valid pulses are given, so the CDC capture path + // never fires — we must set the _cap registers via hierarchical + // access for the word-packing checks to be meaningful. preload_pending_data; + @(posedge ft601_clk_in); + uut.doppler_real_cap = 16'hABCD; + uut.doppler_imag_cap = 16'hEF01; + uut.cfar_detection_cap = 1'b1; + @(posedge ft601_clk_in); + assert_range_valid(32'hDEAD_BEEF); - wait_for_state(S_SEND_HEADER, 50); - repeat (2) @(posedge ft601_clk_in); #1; - check(uut.current_state === S_SEND_HEADER, - "Stalled in SEND_HEADER (backpressure)"); - // Release: FSM drives header then moves to SEND_RANGE_DATA + // FSM should be in SEND_DATA_WORD, stalled on ft601_txe=1 + wait_for_state(S_SEND_DATA_WORD, 50); + repeat (2) @(posedge ft601_clk_in); #1; + + check(uut.current_state === S_SEND_DATA_WORD, + "Stalled in SEND_DATA_WORD (backpressure)"); + + // Verify pre-packed words + // range_profile = 0xDEAD_BEEF → range[31:24]=0xDE, [23:16]=0xAD, [15:8]=0xBE, [7:0]=0xEF + // Word 0: {0xAA, 0xDE, 0xAD, 0xBE} + check(uut.data_pkt_word0 === {8'hAA, 8'hDE, 8'hAD, 8'hBE}, + "Word 0: {HEADER=AA, range[31:8]}"); + // Word 1: {0xEF, 0xAB, 0xCD, 0xEF} + check(uut.data_pkt_word1 === {8'hEF, 8'hAB, 8'hCD, 8'hEF}, + "Word 1: {range[7:0], dop_re, dop_im_hi}"); + // Word 2: {0x01, detection_byte, 0x55, 0x00} + // detection_byte bit 7 = frame_start (sample_counter==0 → 1), bit 0 = cfar=1 + // so detection_byte = 8'b1000_0001 = 8'h81 + check(uut.data_pkt_word2 === {8'h01, 8'h81, 8'h55, 8'h00}, + "Word 2: {dop_im_lo, det=81, FOOTER=55, pad=00}"); + check(uut.data_pkt_be2 === 4'b1110, + "Word 2 BE=1110 (3 valid bytes + 1 pad)"); + + // Release backpressure and verify word 0 appears on bus. + // On the first posedge with !ft601_txe the FSM drives word 0 and + // advances data_word_idx 0→1 via NBA. After #1 the NBA has + // resolved, so we see idx=1 and ft601_data_out=word0. ft601_txe = 0; @(posedge ft601_clk_in); #1; - // Now the FSM registered the header output and will transition - // At the NEXT posedge the state becomes SEND_RANGE_DATA - @(posedge ft601_clk_in); #1; - - check(uut.current_state === S_SEND_RANGE, - "Entered SEND_RANGE_DATA after header"); - - // The first range word should be on the data bus (byte_counter=0 just - // drove range_profile_cap, byte_counter incremented to 1) - check(uut.ft601_data_out === 32'hDEAD_BEEF || uut.byte_counter <= 8'd1, - "Range data word 0 driven (range_profile_cap)"); + check(uut.ft601_data_out === {8'hAA, 8'hDE, 8'hAD, 8'hBE}, + "Word 0 driven on data bus after backpressure release"); check(ft601_wr_n === 1'b0, - "Write strobe active during range data"); - + "Write strobe active during SEND_DATA_WORD"); check(ft601_be === 4'b1111, - "Byte enable=1111 for range data"); + "Byte enable=1111 for word 0"); + check(uut.ft601_data_oe === 1'b1, + "Data bus output enabled during SEND_DATA_WORD"); - // Wait for all 4 range words to complete - wait_for_state(S_SEND_DOPPLER, 50); - #1; - check(uut.current_state === S_SEND_DOPPLER, - "Advanced to SEND_DOPPLER_DATA after 4 range words"); + // Next posedge: FSM drives word 1, advances idx 1→2. + // After NBA: idx=2, ft601_data_out=word1. + @(posedge ft601_clk_in); #1; + check(uut.data_word_idx === 2'd2, + "data_word_idx advanced past word 1 (now 2)"); + check(uut.ft601_data_out === {8'hEF, 8'hAB, 8'hCD, 8'hEF}, + "Word 1 driven on data bus"); + check(ft601_be === 4'b1111, + "Byte enable=1111 for word 1"); + + // Next posedge: FSM drives word 2, idx resets 2→0, + // and current_state transitions to WAIT_ACK. + @(posedge ft601_clk_in); #1; + check(uut.current_state === S_WAIT_ACK, + "Transitioned to WAIT_ACK after 3 data words"); + check(uut.ft601_data_out === {8'h01, 8'h81, 8'h55, 8'h00}, + "Word 2 driven on data bus"); + check(ft601_be === 4'b1110, + "Byte enable=1110 for word 2 (last byte is pad)"); + + // Then back to IDLE + @(posedge ft601_clk_in); #1; + check(uut.current_state === S_IDLE, + "Returned to IDLE after WAIT_ACK"); // ════════════════════════════════════════════════════════ - // TEST GROUP 3: Header verification (stall to observe) + // TEST GROUP 3: Header and footer verification // ════════════════════════════════════════════════════════ - $display("\n--- Test Group 3: Header Verification ---"); + $display("\n--- Test Group 3: Header and Footer Verification ---"); apply_reset; - ft601_txe = 1; // Stall at SEND_HEADER + ft601_txe = 1; // Stall to inspect @(posedge clk); - range_profile = 32'hCAFE_BABE; - range_valid = 1; - repeat (4) @(posedge ft601_clk_in); + doppler_real = 16'h0000; + doppler_imag = 16'h0000; + cfar_detection = 1'b0; @(posedge clk); - range_valid = 0; - repeat (3) @(posedge ft601_clk_in); + preload_pending_data; + assert_range_valid(32'hCAFE_BABE); - wait_for_state(S_SEND_HEADER, 50); + wait_for_state(S_SEND_DATA_WORD, 50); repeat (2) @(posedge ft601_clk_in); #1; - check(uut.current_state === S_SEND_HEADER, - "Stalled in SEND_HEADER with backpressure"); - - // Release backpressure - header will be latched at next posedge - ft601_txe = 0; - @(posedge ft601_clk_in); #1; - - check(uut.ft601_data_out[7:0] === 8'hAA, - "Header byte 0xAA on data bus"); - check(ft601_be === 4'b0001, - "Byte enable=0001 for header (lower byte only)"); - check(ft601_wr_n === 1'b0, - "Write strobe active during header"); - check(uut.ft601_data_oe === 1'b1, - "Data bus output enabled during header"); + // Header is in byte 3 (MSB) of word 0 + check(uut.data_pkt_word0[31:24] === 8'hAA, + "Header byte 0xAA in word 0 MSB"); + // Footer is in byte 1 (bits [15:8]) of word 2 + check(uut.data_pkt_word2[15:8] === 8'h55, + "Footer byte 0x55 in word 2"); // ════════════════════════════════════════════════════════ - // TEST GROUP 4: Doppler data verification + // TEST GROUP 4: Doppler data capture verification // ════════════════════════════════════════════════════════ - $display("\n--- Test Group 4: Doppler Data Verification ---"); + $display("\n--- Test Group 4: Doppler Data Capture ---"); apply_reset; ft601_txe = 0; - // Preload only doppler pending (not cfar) so the FSM sends - // doppler data. After doppler, SEND_DETECT sees cfar_data_pending=0 - // and skips to SEND_FOOTER, then WAIT_ACK, then IDLE. - preload_doppler_pending; - assert_range_valid(32'h0000_0001); - wait_for_state(S_SEND_DOPPLER, 100); - #1; - check(uut.current_state === S_SEND_DOPPLER, - "Reached SEND_DOPPLER_DATA"); - // Provide doppler data via valid pulse (updates captured values) @(posedge clk); doppler_real = 16'hAAAA; @@ -524,110 +559,70 @@ module tb_usb_data_interface; check(uut.doppler_imag_cap === 16'h5555, "doppler_imag captured correctly"); - // The FSM has doppler_data_pending set and sends 4 bytes, then - // transitions past SEND_DETECT (cfar_data_pending=0) to IDLE. + // Drive a packet with pending doppler + cfar (both needed for gating + // since all streams are enabled after reset/apply_reset). + preload_pending_data; + assert_range_valid(32'h0000_0001); wait_for_state(S_IDLE, 100); #1; check(uut.current_state === S_IDLE, - "Doppler done, packet completed"); + "Packet completed with doppler data"); + check(uut.doppler_data_pending === 1'b0, + "doppler_data_pending cleared after packet"); // ════════════════════════════════════════════════════════ // TEST GROUP 5: CFAR detection data // ════════════════════════════════════════════════════════ $display("\n--- Test Group 5: CFAR Detection Data ---"); - // Start a new packet with both doppler and cfar pending to verify - // cfar data is properly sent in SEND_DETECTION_DATA. apply_reset; ft601_txe = 0; preload_pending_data; assert_range_valid(32'h0000_0002); - // FSM races through: HEADER -> RANGE -> DOPPLER -> DETECT -> FOOTER -> IDLE - // All pending flags consumed proves SEND_DETECT was entered. wait_for_state(S_IDLE, 200); #1; check(uut.cfar_data_pending === 1'b0, - "Starting in SEND_DETECTION_DATA"); - - // Verify the full packet completed with cfar data consumed + "cfar_data_pending cleared after packet"); check(uut.current_state === S_IDLE && uut.doppler_data_pending === 1'b0 && uut.cfar_data_pending === 1'b0, - "CFAR detection sent, FSM advanced past SEND_DETECTION_DATA"); + "CFAR detection sent, all pending flags cleared"); // ════════════════════════════════════════════════════════ - // TEST GROUP 6: Footer check - // - // Strategy: drive packet with ft601_txe=0 all the way through. - // The SEND_FOOTER state is only active for 1 cycle, but we can - // poll the state machine at each ft601_clk_in edge to observe - // it. We use a monitor-style approach: run the packet and - // capture what ft601_data_out contains when we see SEND_FOOTER. + // TEST GROUP 6: Footer retained after packet // ════════════════════════════════════════════════════════ - $display("\n--- Test Group 6: Footer Check ---"); + $display("\n--- Test Group 6: Footer Retention ---"); apply_reset; ft601_txe = 0; - // Drive packet through range data + @(posedge clk); + cfar_detection = 1'b1; + @(posedge clk); preload_pending_data; assert_range_valid(32'hFACE_FEED); - wait_for_state(S_SEND_DOPPLER, 100); - // Feed doppler data (need 4 pulses) - pulse_doppler_once(16'h1111, 16'h2222); - pulse_doppler_once(16'h1111, 16'h2222); - pulse_doppler_once(16'h1111, 16'h2222); - pulse_doppler_once(16'h1111, 16'h2222); - wait_for_state(S_SEND_DETECT, 100); - // Feed cfar data, but keep ft601_txe=0 so it flows through - pulse_cfar_once(1'b1); - - // Now the FSM should pass through SEND_FOOTER quickly. - // Use wait_for_state to reach SEND_FOOTER, or it may already - // be at WAIT_ACK/IDLE. Let's catch WAIT_ACK or IDLE. - // The footer values are latched into registers, so we can - // verify them even after the state transitions. - // Key verification: the FOOTER constant (0x55) must have been - // driven. We check this by looking at the constant definition. - // Since we can't easily freeze the FSM at SEND_FOOTER without - // also stalling SEND_DETECTION_DATA (both check ft601_txe), - // we verify the footer indirectly: - // 1. The packet completed (reached IDLE/WAIT_ACK) - // 2. ft601_data_out last held 0x55 during SEND_FOOTER - wait_for_state(S_IDLE, 100); #1; - // If we reached IDLE, the full sequence ran including footer check(uut.current_state === S_IDLE, "Full packet incl. footer completed, back in IDLE"); - // The registered ft601_data_out should still hold 0x55 from - // SEND_FOOTER (WAIT_ACK and IDLE don't overwrite ft601_data_out). - // Actually, looking at the DUT: WAIT_ACK only sets wr_n=1 and - // data_oe=0, it doesn't change ft601_data_out. So it retains 0x55. - check(uut.ft601_data_out[7:0] === 8'h55, - "ft601_data_out retains footer 0x55 after packet"); + // The last word driven was word 2 which contains footer 0x55. + // WAIT_ACK and IDLE don't overwrite ft601_data_out, so it retains + // the last driven value. + check(uut.ft601_data_out[15:8] === 8'h55, + "ft601_data_out retains footer 0x55 in word 2 position"); - // Verify WAIT_ACK behavior by doing another packet and catching it + // Verify WAIT_ACK → IDLE transition apply_reset; ft601_txe = 0; preload_pending_data; assert_range_valid(32'h1234_5678); - wait_for_state(S_SEND_DOPPLER, 100); - pulse_doppler_once(16'hABCD, 16'hEF01); - pulse_doppler_once(16'hABCD, 16'hEF01); - pulse_doppler_once(16'hABCD, 16'hEF01); - pulse_doppler_once(16'hABCD, 16'hEF01); - wait_for_state(S_SEND_DETECT, 100); - pulse_cfar_once(1'b0); - // WAIT_ACK lasts exactly 1 ft601_clk_in cycle then goes IDLE. - // Poll for IDLE (which means WAIT_ACK already happened). wait_for_state(S_IDLE, 100); #1; check(uut.current_state === S_IDLE, "Returned to IDLE after WAIT_ACK"); check(ft601_wr_n === 1'b1, - "ft601_wr_n deasserted in IDLE (was deasserted in WAIT_ACK)"); + "ft601_wr_n deasserted in IDLE"); check(uut.ft601_data_oe === 1'b0, - "Data bus released in IDLE (was released in WAIT_ACK)"); + "Data bus released in IDLE"); // ════════════════════════════════════════════════════════ // TEST GROUP 7: Full packet sequence (end-to-end) @@ -646,23 +641,24 @@ module tb_usb_data_interface; // ════════════════════════════════════════════════════════ $display("\n--- Test Group 8: FIFO Backpressure ---"); apply_reset; - ft601_txe = 1; + ft601_txe = 1; // FIFO full — stall + preload_pending_data; assert_range_valid(32'hBBBB_CCCC); - wait_for_state(S_SEND_HEADER, 50); + wait_for_state(S_SEND_DATA_WORD, 50); repeat (10) @(posedge ft601_clk_in); #1; - check(uut.current_state === S_SEND_HEADER, - "Stalled in SEND_HEADER when ft601_txe=1 (FIFO full)"); + check(uut.current_state === S_SEND_DATA_WORD, + "Stalled in SEND_DATA_WORD when ft601_txe=1 (FIFO full)"); check(ft601_wr_n === 1'b1, "ft601_wr_n not asserted during backpressure stall"); ft601_txe = 0; - repeat (2) @(posedge ft601_clk_in); #1; + repeat (6) @(posedge ft601_clk_in); #1; - check(uut.current_state !== S_SEND_HEADER, - "Resumed from SEND_HEADER after backpressure released"); + check(uut.current_state === S_IDLE || uut.current_state === S_WAIT_ACK, + "Resumed and completed after backpressure released"); // ════════════════════════════════════════════════════════ // TEST GROUP 9: Clock divider @@ -705,13 +701,6 @@ module tb_usb_data_interface; ft601_txe = 0; preload_pending_data; assert_range_valid(32'h1111_2222); - wait_for_state(S_SEND_DOPPLER, 100); - pulse_doppler_once(16'h3333, 16'h4444); - pulse_doppler_once(16'h3333, 16'h4444); - pulse_doppler_once(16'h3333, 16'h4444); - pulse_doppler_once(16'h3333, 16'h4444); - wait_for_state(S_SEND_DETECT, 100); - pulse_cfar_once(1'b0); wait_for_state(S_WAIT_ACK, 50); #1; @@ -805,7 +794,7 @@ module tb_usb_data_interface; // Start a write packet preload_pending_data; assert_range_valid(32'hFACE_FEED); - wait_for_state(S_SEND_HEADER, 50); + wait_for_state(S_SEND_DATA_WORD, 50); @(posedge ft601_clk_in); #1; // While write FSM is active, assert RXF=0 (host has data) @@ -818,13 +807,6 @@ module tb_usb_data_interface; // Deassert RXF, complete the write packet ft601_rxf = 1; - wait_for_state(S_SEND_DOPPLER, 100); - pulse_doppler_once(16'hAAAA, 16'hBBBB); - pulse_doppler_once(16'hAAAA, 16'hBBBB); - pulse_doppler_once(16'hAAAA, 16'hBBBB); - pulse_doppler_once(16'hAAAA, 16'hBBBB); - wait_for_state(S_SEND_DETECT, 100); - pulse_cfar_once(1'b1); wait_for_state(S_IDLE, 100); @(posedge ft601_clk_in); #1; @@ -841,32 +823,42 @@ module tb_usb_data_interface; // ════════════════════════════════════════════════════════ // TEST GROUP 15: Stream Control Gating (Gap 2) // Verify that disabling individual streams causes the write - // FSM to skip those data phases. + // FSM to zero those fields in the packed words. // ════════════════════════════════════════════════════════ $display("\n--- Test Group 15: Stream Control Gating (Gap 2) ---"); // 15a: Disable doppler stream (stream_control = 3'b101 = range + cfar only) apply_reset; - ft601_txe = 0; + ft601_txe = 1; // Stall to inspect packed words stream_control = 3'b101; // range + cfar, no doppler // Wait for CDC propagation (2-stage sync) repeat (6) @(posedge ft601_clk_in); - // Preload cfar pending so the FSM enters the SEND_DETECT data path - // (without it, SEND_DETECT skips immediately on !cfar_data_pending). - preload_cfar_pending; - // Drive range valid — triggers write FSM - assert_range_valid(32'hAA11_BB22); - // FSM: IDLE -> SEND_HEADER -> SEND_RANGE (doppler disabled) -> SEND_DETECT -> FOOTER - // The FSM races through SEND_DETECT in 1 cycle (cfar_data_pending is consumed). - // Verify the packet completed correctly (doppler was skipped). - wait_for_state(S_IDLE, 200); - #1; - // Reaching IDLE proves: HEADER -> RANGE -> (skip DOPPLER) -> DETECT -> FOOTER -> ACK -> IDLE. - // cfar_data_pending consumed confirms SEND_DETECT was entered. - check(uut.current_state === S_IDLE && uut.cfar_data_pending === 1'b0, - "Stream gate: reached SEND_DETECT (range sent, doppler skipped)"); + @(posedge clk); + doppler_real = 16'hAAAA; + doppler_imag = 16'hBBBB; + cfar_detection = 1'b1; + @(posedge clk); + preload_cfar_pending; + assert_range_valid(32'hAA11_BB22); + + wait_for_state(S_SEND_DATA_WORD, 200); + repeat (2) @(posedge ft601_clk_in); #1; + + // With doppler disabled, doppler fields in words 1 and 2 should be zero + // Word 1: {range[7:0], 0x00, 0x00, 0x00} (doppler zeroed) + check(uut.data_pkt_word1[23:0] === 24'h000000, + "Stream gate: doppler bytes zeroed in word 1 when disabled"); + + // Word 2 byte 3 (dop_im_lo) should also be zero + check(uut.data_pkt_word2[31:24] === 8'h00, + "Stream gate: dop_im_lo zeroed in word 2 when disabled"); + + // Let it complete + ft601_txe = 0; + wait_for_state(S_IDLE, 100); + #1; check(uut.current_state === S_IDLE, "Stream gate: packet completed without doppler"); @@ -951,28 +943,6 @@ module tb_usb_data_interface; "Status readback: returned to IDLE after 8-word response"); // Verify the status snapshot was captured correctly. - // status_words[0] = {0xFF, 3'b000, mode[1:0], 5'b0, stream_ctrl[2:0], cfar_threshold[15:0]} - // = {8'hFF, 3'b000, 2'b01, 5'b00000, 3'b101, 16'hABCD} - // = 0xFF_09_05_ABCD... let's compute: - // Byte 3: 0xFF = 8'hFF - // Byte 2: {3'b000, 2'b01} = 5'b00001 + 3 high bits of next field... - // Actually the packing is: {8'hFF, 3'b000, status_radar_mode[1:0], 5'b00000, status_stream_ctrl[2:0], status_cfar_threshold[15:0]} - // = {8'hFF, 3'b000, 2'b01, 5'b00000, 3'b101, 16'hABCD} - // = 8'hFF, 5'b00001, 8'b00000101, 16'hABCD - // = FF_09_05_ABCD? Let me compute carefully: - // Bits [31:24] = 8'hFF = 0xFF - // Bits [23:21] = 3'b000 - // Bits [20:19] = 2'b01 (mode) - // Bits [18:14] = 5'b00000 - // Bits [13:11] = 3'b101 (stream_ctrl) - // Bits [10:0] = ... wait, cfar_threshold is 16 bits → [15:0] - // Total bits = 8+3+2+5+3+16 = 37 bits — won't fit in 32! - // Re-reading the RTL: the packing at line 241 is: - // {8'hFF, 3'b000, status_radar_mode, 5'b00000, status_stream_ctrl, status_cfar_threshold} - // = 8 + 3 + 2 + 5 + 3 + 16 = 37 bits - // This would be truncated to 32 bits. Let me re-read the actual RTL to check. - // For now, just verify status_words[1] (word index 1 in the packet = idx 2 in FSM) - // status_words[1] = {status_long_chirp, status_long_listen} = {16'd3000, 16'd13700} check(uut.status_words[1] === {16'd3000, 16'd13700}, "Status readback: word 1 = {long_chirp, long_listen}"); check(uut.status_words[2] === {16'd17540, 16'd50}, diff --git a/9_Firmware/9_2_FPGA/usb_data_interface.v b/9_Firmware/9_2_FPGA/usb_data_interface.v index 4f32e13..2545591 100644 --- a/9_Firmware/9_2_FPGA/usb_data_interface.v +++ b/9_Firmware/9_2_FPGA/usb_data_interface.v @@ -1,3 +1,17 @@ +/** + * usb_data_interface.v + * + * FT601 USB 3.0 SuperSpeed FIFO Interface (32-bit bus, 100 MHz ft601_clk). + * Used on the 200T premium dev board. Production 50T board uses + * usb_data_interface_ft2232h.v (FT2232H, 8-bit, 60 MHz) instead. + * + * USB disconnect recovery: + * A clock-activity watchdog in the clk domain detects when ft601_clk_in + * stops (USB cable unplugged). After ~0.65 ms of silence (65536 system + * clocks) it asserts ft601_clk_lost, which is OR'd into the FT-domain + * reset so FSMs and FIFOs return to a clean state. When ft601_clk_in + * resumes, a 2-stage reset synchronizer deasserts the reset cleanly. + */ module usb_data_interface ( input wire clk, // Main clock (100MHz recommended) input wire reset_n, @@ -15,13 +29,18 @@ module usb_data_interface ( // FT601 Interface (Slave FIFO mode) // Data bus inout wire [31:0] ft601_data, // 32-bit bidirectional data bus - output reg [3:0] ft601_be, // Byte enable (4 lanes for 32-bit mode) + output reg [3:0] ft601_be, // Byte enable (active-HIGH per FT601 datasheet / AN_378) // Control signals - output reg ft601_txe_n, // Transmit enable (active low) - output reg ft601_rxf_n, // Receive enable (active low) - input wire ft601_txe, // TXE: Transmit FIFO Not Full (high = space available to write) - input wire ft601_rxf, // RXF: Receive FIFO Not Empty (high = data available to read) + // VESTIGIAL OUTPUTS — kept for 200T board port compatibility. + // On the 200T, these are constrained to physical pins G21 (TXE) and + // G22 (RXF) in xc7a200t_fbg484.xdc. Removing them from the RTL would + // break the 200T build. They are reset to 1 and never driven; the + // actual FT601 flow-control inputs are ft601_txe and ft601_rxf below. + output reg ft601_txe_n, // VESTIGIAL: unused output, always 1 + output reg ft601_rxf_n, // VESTIGIAL: unused output, always 1 + input wire ft601_txe, // TXE: Transmit FIFO Not Full (active-low: 0 = space available) + input wire ft601_rxf, // RXF: Receive FIFO Not Empty (active-low: 0 = data available) output reg ft601_wr_n, // Write strobe (active low) output reg ft601_rd_n, // Read strobe (active low) output reg ft601_oe_n, // Output enable (active low) @@ -97,21 +116,26 @@ localparam FT601_BURST_SIZE = 512; // Max burst size in bytes // ============================================================================ // WRITE FSM State definitions (Verilog-2001 compatible) // ============================================================================ -localparam [2:0] IDLE = 3'd0, - SEND_HEADER = 3'd1, - SEND_RANGE_DATA = 3'd2, - SEND_DOPPLER_DATA = 3'd3, - SEND_DETECTION_DATA = 3'd4, - SEND_FOOTER = 3'd5, - WAIT_ACK = 3'd6, - SEND_STATUS = 3'd7; // Gap 2: status readback +// Rewritten: data packet is now 3 x 32-bit writes (11 payload bytes + 1 pad). +// Word 0: {HEADER, range[31:24], range[23:16], range[15:8]} BE=1111 +// Word 1: {range[7:0], doppler_real[15:8], doppler_real[7:0], doppler_imag[15:8]} BE=1111 +// Word 2: {doppler_imag[7:0], detection, FOOTER, 8'h00} BE=1110 +localparam [3:0] IDLE = 4'd0, + SEND_DATA_WORD = 4'd1, + SEND_STATUS = 4'd2, + WAIT_ACK = 4'd3; -reg [2:0] current_state; -reg [7:0] byte_counter; -reg [31:0] data_buffer; +reg [3:0] current_state; +reg [1:0] data_word_idx; // 0..2 for 3-word data packet reg [31:0] ft601_data_out; reg ft601_data_oe; // Output enable for bidirectional data bus +// Pre-packed data words (registered snapshot of CDC'd data) +reg [31:0] data_pkt_word0; +reg [31:0] data_pkt_word1; +reg [31:0] data_pkt_word2; +reg [3:0] data_pkt_be2; // BE for last word (BE=1110 since byte 3 is pad) + // ============================================================================ // READ FSM State definitions (Gap 4: USB Read Path) // ============================================================================ @@ -184,6 +208,67 @@ always @(posedge clk or negedge reset_n) begin end end +// ============================================================================ +// CLOCK-ACTIVITY WATCHDOG (clk domain) +// ============================================================================ +// Detects when ft601_clk_in stops (USB cable unplugged). A toggle register +// in the ft601_clk domain flips every edge. The clk domain synchronizes it +// and checks for transitions. If no transition is seen for 2^16 = 65536 +// clk cycles (~0.65 ms at 100 MHz), ft601_clk_lost asserts. + +// Toggle register: flips every ft601_clk edge (ft601_clk domain) +reg ft601_heartbeat; +always @(posedge ft601_clk_in or negedge ft601_reset_n) begin + if (!ft601_reset_n) + ft601_heartbeat <= 1'b0; + else + ft601_heartbeat <= ~ft601_heartbeat; +end + +// Synchronize heartbeat into clk domain (2-stage) +(* ASYNC_REG = "TRUE" *) reg [1:0] ft601_hb_sync; +reg ft601_hb_prev; +reg [15:0] ft601_clk_timeout; +reg ft601_clk_lost; + +always @(posedge clk or negedge reset_n) begin + if (!reset_n) begin + ft601_hb_sync <= 2'b00; + ft601_hb_prev <= 1'b0; + ft601_clk_timeout <= 16'd0; + ft601_clk_lost <= 1'b0; + end else begin + ft601_hb_sync <= {ft601_hb_sync[0], ft601_heartbeat}; + ft601_hb_prev <= ft601_hb_sync[1]; + + if (ft601_hb_sync[1] != ft601_hb_prev) begin + // ft601_clk is alive — reset counter, clear lost flag + ft601_clk_timeout <= 16'd0; + ft601_clk_lost <= 1'b0; + end else if (!ft601_clk_lost) begin + if (ft601_clk_timeout == 16'hFFFF) + ft601_clk_lost <= 1'b1; + else + ft601_clk_timeout <= ft601_clk_timeout + 16'd1; + end + end +end + +// Effective FT601-domain reset: asserted by global reset OR clock loss. +// Deassertion synchronized to ft601_clk via 2-stage sync to avoid +// metastability on the recovery edge. +(* ASYNC_REG = "TRUE" *) reg [1:0] ft601_reset_sync; +wire ft601_reset_raw_n = ft601_reset_n & ~ft601_clk_lost; + +always @(posedge ft601_clk_in or negedge ft601_reset_raw_n) begin + if (!ft601_reset_raw_n) + ft601_reset_sync <= 2'b00; + else + ft601_reset_sync <= {ft601_reset_sync[0], 1'b1}; +end + +wire ft601_effective_reset_n = ft601_reset_sync[1]; + // FT601-domain captured data (sampled from holding regs on sync'd edge) reg [31:0] range_profile_cap; reg [15:0] doppler_real_cap; @@ -197,6 +282,18 @@ reg cfar_detection_cap; reg doppler_data_pending; reg cfar_data_pending; +// 1-cycle delayed range trigger. range_valid_ft fires on the same clock +// edge that range_profile_cap is captured (non-blocking). If the FSM +// reads range_profile_cap on that same edge it sees the STALE value. +// Delaying the trigger by one cycle guarantees the capture register has +// settled before the FSM packs the data words. +reg range_data_ready; + +// Frame sync: sample counter (ft601_clk domain, wraps at NUM_CELLS) +// Bit 7 of detection byte is set when sample_counter == 0 (frame start). +localparam [11:0] NUM_CELLS = 12'd2048; // 64 range x 32 doppler +reg [11:0] sample_counter; + // Gap 2: CDC for stream_control (clk_100m -> ft601_clk_in) // stream_control changes infrequently (only on host USB command), so // per-bit 2-stage synchronizers are sufficient. No Gray coding needed @@ -228,8 +325,8 @@ wire range_valid_ft; wire doppler_valid_ft; wire cfar_valid_ft; -always @(posedge ft601_clk_in or negedge ft601_reset_n) begin - if (!ft601_reset_n) begin +always @(posedge ft601_clk_in or negedge ft601_effective_reset_n) begin + if (!ft601_effective_reset_n) begin range_valid_sync <= 2'b00; doppler_valid_sync <= 2'b00; cfar_valid_sync <= 2'b00; @@ -240,6 +337,7 @@ always @(posedge ft601_clk_in or negedge ft601_reset_n) begin doppler_real_cap <= 16'd0; doppler_imag_cap <= 16'd0; cfar_detection_cap <= 1'b0; + range_data_ready <= 1'b0; // Fix #5: Default to range-only on reset (prevents write FSM deadlock) stream_ctrl_sync_0 <= 3'b001; stream_ctrl_sync_1 <= 3'b001; @@ -302,6 +400,10 @@ always @(posedge ft601_clk_in or negedge ft601_reset_n) begin if (cfar_valid_sync[1] && !cfar_valid_sync_d) begin cfar_detection_cap <= cfar_detection_hold; end + + // 1-cycle delayed trigger: ensures range_profile_cap has settled + // before the FSM reads it for word packing. + range_data_ready <= range_valid_ft; end end @@ -314,11 +416,11 @@ assign cfar_valid_ft = cfar_valid_sync[1] && !cfar_valid_sync_d; // FT601 data bus direction control assign ft601_data = ft601_data_oe ? ft601_data_out : 32'hzzzz_zzzz; -always @(posedge ft601_clk_in or negedge ft601_reset_n) begin - if (!ft601_reset_n) begin +always @(posedge ft601_clk_in or negedge ft601_effective_reset_n) begin + if (!ft601_effective_reset_n) begin current_state <= IDLE; read_state <= RD_IDLE; - byte_counter <= 0; + data_word_idx <= 2'd0; ft601_data_out <= 0; ft601_data_oe <= 0; ft601_be <= 4'b1111; // All bytes enabled for 32-bit mode @@ -336,6 +438,11 @@ always @(posedge ft601_clk_in or negedge ft601_reset_n) begin cmd_value <= 16'd0; doppler_data_pending <= 1'b0; cfar_data_pending <= 1'b0; + data_pkt_word0 <= 32'd0; + data_pkt_word1 <= 32'd0; + data_pkt_word2 <= 32'd0; + data_pkt_be2 <= 4'b1110; + sample_counter <= 12'd0; // NOTE: ft601_clk_out is driven by the clk-domain always block below. // Do NOT assign it here (ft601_clk_in domain) — causes multi-driven net. end else begin @@ -424,124 +531,66 @@ always @(posedge ft601_clk_in or negedge ft601_reset_n) begin current_state <= SEND_STATUS; status_word_idx <= 3'd0; end - // Trigger write FSM on range_valid edge (primary data source). - // Doppler/cfar data_pending flags are checked inside - // SEND_DOPPLER_DATA and SEND_DETECTION_DATA to skip or send. - // Do NOT trigger on pending flags alone — they're sticky and - // would cause repeated packet starts without new range data. - else if (range_valid_ft && stream_range_en) begin + // Trigger on range_data_ready (1 cycle after range_valid_ft) + // so that range_profile_cap has settled from the CDC block. + // Gate on pending flags: only send when all enabled + // streams have fresh data (avoids stale doppler/CFAR) + else if (range_data_ready && stream_range_en + && (!stream_doppler_en || doppler_data_pending) + && (!stream_cfar_en || cfar_data_pending)) begin // Don't start write if a read is about to begin if (ft601_rxf) begin // rxf=1 means no host data pending - current_state <= SEND_HEADER; - byte_counter <= 0; + // Pack 11-byte data packet into 3 x 32-bit words + // Doppler fields zeroed when stream disabled + // CFAR field zeroed when stream disabled + data_pkt_word0 <= {HEADER, + range_profile_cap[31:24], + range_profile_cap[23:16], + range_profile_cap[15:8]}; + data_pkt_word1 <= {range_profile_cap[7:0], + stream_doppler_en ? doppler_real_cap[15:8] : 8'd0, + stream_doppler_en ? doppler_real_cap[7:0] : 8'd0, + stream_doppler_en ? doppler_imag_cap[15:8] : 8'd0}; + data_pkt_word2 <= {stream_doppler_en ? doppler_imag_cap[7:0] : 8'd0, + stream_cfar_en + ? {(sample_counter == 12'd0), 6'b0, cfar_detection_cap} + : {(sample_counter == 12'd0), 7'd0}, + FOOTER, + 8'h00}; // pad byte + data_pkt_be2 <= 4'b1110; // 3 valid bytes + 1 pad + data_word_idx <= 2'd0; + current_state <= SEND_DATA_WORD; end end end - - SEND_HEADER: begin - if (!ft601_txe) begin // FT601 TX FIFO not empty - ft601_data_oe <= 1; - ft601_data_out <= {24'b0, HEADER}; - ft601_be <= 4'b0001; // Only lower byte valid - ft601_wr_n <= 0; // Assert write strobe - // Gap 2: skip to first enabled stream - if (stream_range_en) - current_state <= SEND_RANGE_DATA; - else if (stream_doppler_en) - current_state <= SEND_DOPPLER_DATA; - else if (stream_cfar_en) - current_state <= SEND_DETECTION_DATA; - else - current_state <= SEND_FOOTER; // No streams — send footer only - end - end - - SEND_RANGE_DATA: begin + + SEND_DATA_WORD: begin if (!ft601_txe) begin ft601_data_oe <= 1; - ft601_be <= 4'b1111; // All bytes valid for 32-bit word - - case (byte_counter) - 0: ft601_data_out <= range_profile_cap; - 1: ft601_data_out <= {range_profile_cap[23:0], 8'h00}; - 2: ft601_data_out <= {range_profile_cap[15:0], 16'h0000}; - 3: ft601_data_out <= {range_profile_cap[7:0], 24'h000000}; + ft601_wr_n <= 0; + case (data_word_idx) + 2'd0: begin + ft601_data_out <= data_pkt_word0; + ft601_be <= 4'b1111; + end + 2'd1: begin + ft601_data_out <= data_pkt_word1; + ft601_be <= 4'b1111; + end + 2'd2: begin + ft601_data_out <= data_pkt_word2; + ft601_be <= data_pkt_be2; + end + default: ; endcase - - ft601_wr_n <= 0; - - if (byte_counter == 3) begin - byte_counter <= 0; - // Gap 2: skip disabled streams - if (stream_doppler_en) - current_state <= SEND_DOPPLER_DATA; - else if (stream_cfar_en) - current_state <= SEND_DETECTION_DATA; - else - current_state <= SEND_FOOTER; + if (data_word_idx == 2'd2) begin + data_word_idx <= 2'd0; + current_state <= WAIT_ACK; end else begin - byte_counter <= byte_counter + 1; + data_word_idx <= data_word_idx + 2'd1; end end end - - SEND_DOPPLER_DATA: begin - if (!ft601_txe && doppler_data_pending) begin - ft601_data_oe <= 1; - ft601_be <= 4'b1111; - - case (byte_counter) - 0: ft601_data_out <= {doppler_real_cap, doppler_imag_cap}; - 1: ft601_data_out <= {doppler_imag_cap, doppler_real_cap[15:8], 8'h00}; - 2: ft601_data_out <= {doppler_real_cap[7:0], doppler_imag_cap[15:8], 16'h0000}; - 3: ft601_data_out <= {doppler_imag_cap[7:0], 24'h000000}; - endcase - - ft601_wr_n <= 0; - - if (byte_counter == 3) begin - byte_counter <= 0; - doppler_data_pending <= 1'b0; - if (stream_cfar_en) - current_state <= SEND_DETECTION_DATA; - else - current_state <= SEND_FOOTER; - end else begin - byte_counter <= byte_counter + 1; - end - end else if (!doppler_data_pending) begin - // No doppler data available yet — skip to next stream - byte_counter <= 0; - if (stream_cfar_en) - current_state <= SEND_DETECTION_DATA; - else - current_state <= SEND_FOOTER; - end - end - - SEND_DETECTION_DATA: begin - if (!ft601_txe && cfar_data_pending) begin - ft601_data_oe <= 1; - ft601_be <= 4'b0001; - ft601_data_out <= {24'b0, 7'b0, cfar_detection_cap}; - ft601_wr_n <= 0; - cfar_data_pending <= 1'b0; - current_state <= SEND_FOOTER; - end else if (!cfar_data_pending) begin - // No CFAR data available yet — skip to footer - current_state <= SEND_FOOTER; - end - end - - SEND_FOOTER: begin - if (!ft601_txe) begin - ft601_data_oe <= 1; - ft601_be <= 4'b0001; - ft601_data_out <= {24'b0, FOOTER}; - ft601_wr_n <= 0; - current_state <= WAIT_ACK; - end - end // Gap 2: Status readback — send 6 x 32-bit status words // Format: HEADER, status_words[0..5], FOOTER @@ -581,6 +630,14 @@ always @(posedge ft601_clk_in or negedge ft601_reset_n) begin WAIT_ACK: begin ft601_wr_n <= 1; ft601_data_oe <= 0; // Release data bus + // Clear pending flags — data consumed + doppler_data_pending <= 1'b0; + cfar_data_pending <= 1'b0; + // Advance frame sync counter + if (sample_counter == NUM_CELLS - 12'd1) + sample_counter <= 12'd0; + else + sample_counter <= sample_counter + 12'd1; current_state <= IDLE; end endcase @@ -613,8 +670,8 @@ ODDR #( `else // Simulation: behavioral clock forwarding reg ft601_clk_out_sim; -always @(posedge ft601_clk_in or negedge ft601_reset_n) begin - if (!ft601_reset_n) +always @(posedge ft601_clk_in or negedge ft601_effective_reset_n) begin + if (!ft601_effective_reset_n) ft601_clk_out_sim <= 1'b0; else ft601_clk_out_sim <= 1'b1; 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 44cca42..6244138 100644 --- a/9_Firmware/9_2_FPGA/usb_data_interface_ft2232h.v +++ b/9_Firmware/9_2_FPGA/usb_data_interface_ft2232h.v @@ -36,6 +36,13 @@ * Clock domains: * clk = 100 MHz system clock (radar data domain) * ft_clk = 60 MHz from FT2232H CLKOUT (USB FIFO domain) + * + * USB disconnect recovery: + * A clock-activity watchdog in the clk domain detects when ft_clk stops + * (USB cable unplugged). After ~0.65 ms of silence (65536 system clocks) + * it asserts ft_clk_lost, which is OR'd into the FT-domain reset so + * FSMs and FIFOs return to a clean state. When ft_clk resumes, a 2-stage + * reset synchronizer deasserts the reset cleanly in the ft_clk domain. */ module usb_data_interface_ft2232h ( @@ -59,7 +66,9 @@ module usb_data_interface_ft2232h ( output reg ft_rd_n, // Read strobe (active low) output reg ft_wr_n, // Write strobe (active low) output reg ft_oe_n, // Output enable (active low) — bus direction - output reg ft_siwu, // Send Immediate / WakeUp + output reg ft_siwu, // Send Immediate / WakeUp — UNUSED: held low. + // SIWU could flush the TX FIFO for lower latency + // but is not needed at current data rates. Deferred. // Clock from FT2232H (directly used — no ODDR forwarding needed) input wire ft_clk, // 60 MHz from FT2232H CLKOUT @@ -134,6 +143,7 @@ localparam [2:0] RD_IDLE = 3'd0, reg [2:0] rd_state; reg [1:0] rd_byte_cnt; // 0..3 for 4-byte command word reg [31:0] rd_shift_reg; // Shift register to assemble 4-byte command +reg rd_cmd_complete; // Set when all 4 bytes received (distinguishes from abort) // ============================================================================ // DATA BUS DIRECTION CONTROL @@ -192,6 +202,70 @@ always @(posedge clk or negedge reset_n) begin end end +// ============================================================================ +// CLOCK-ACTIVITY WATCHDOG (clk domain) +// ============================================================================ +// Detects when ft_clk stops (USB cable unplugged). A toggle register in the +// ft_clk domain flips every ft_clk edge. The clk domain synchronizes it and +// checks for transitions. If no transition is seen for 2^16 = 65536 clk +// cycles (~0.65 ms at 100 MHz), ft_clk_lost asserts. +// +// ft_clk_lost feeds into the effective reset for the ft_clk domain so that +// FSMs and capture registers return to a clean state automatically. + +// Toggle register: flips every ft_clk edge (ft_clk domain) +reg ft_heartbeat; +always @(posedge ft_clk or negedge ft_reset_n) begin + if (!ft_reset_n) + ft_heartbeat <= 1'b0; + else + ft_heartbeat <= ~ft_heartbeat; +end + +// Synchronize heartbeat into clk domain (2-stage) +(* ASYNC_REG = "TRUE" *) reg [1:0] ft_hb_sync; +reg ft_hb_prev; +reg [15:0] ft_clk_timeout; +reg ft_clk_lost; + +always @(posedge clk or negedge reset_n) begin + if (!reset_n) begin + ft_hb_sync <= 2'b00; + ft_hb_prev <= 1'b0; + ft_clk_timeout <= 16'd0; + ft_clk_lost <= 1'b0; + end else begin + ft_hb_sync <= {ft_hb_sync[0], ft_heartbeat}; + ft_hb_prev <= ft_hb_sync[1]; + + if (ft_hb_sync[1] != ft_hb_prev) begin + // ft_clk is alive — reset counter, clear lost flag + ft_clk_timeout <= 16'd0; + ft_clk_lost <= 1'b0; + end else if (!ft_clk_lost) begin + if (ft_clk_timeout == 16'hFFFF) + ft_clk_lost <= 1'b1; + else + ft_clk_timeout <= ft_clk_timeout + 16'd1; + end + end +end + +// Effective FT-domain reset: asserted by global reset OR clock loss. +// Deassertion synchronized to ft_clk via 2-stage sync to avoid +// metastability on the recovery edge. +(* ASYNC_REG = "TRUE" *) reg [1:0] ft_reset_sync; +wire ft_reset_raw_n = ft_reset_n & ~ft_clk_lost; + +always @(posedge ft_clk or negedge ft_reset_raw_n) begin + if (!ft_reset_raw_n) + ft_reset_sync <= 2'b00; + else + ft_reset_sync <= {ft_reset_sync[0], 1'b1}; +end + +wire ft_effective_reset_n = ft_reset_sync[1]; + // --- 3-stage synchronizers (ft_clk domain) --- // 3 stages for better MTBF at 60 MHz @@ -228,12 +302,25 @@ reg cfar_detection_cap; reg doppler_data_pending; reg cfar_data_pending; +// 1-cycle delayed range trigger. range_valid_ft fires on the same clock +// edge that range_profile_cap is captured (non-blocking). If the FSM +// reads range_profile_cap on that same edge it sees the STALE value. +// Delaying the trigger by one cycle guarantees the capture register has +// settled before the byte mux reads it. +reg range_data_ready; + +// Frame sync: sample counter (ft_clk domain, wraps at NUM_CELLS) +// Bit 7 of detection byte is set when sample_counter == 0 (frame start). +// This allows the Python host to resynchronize without a protocol change. +localparam [11:0] NUM_CELLS = 12'd2048; // 64 range x 32 doppler +reg [11:0] sample_counter; + // Status snapshot (ft_clk domain) reg [31:0] status_words [0:5]; integer si; // status_words loop index -always @(posedge ft_clk or negedge ft_reset_n) begin - if (!ft_reset_n) begin +always @(posedge ft_clk or negedge ft_effective_reset_n) begin + if (!ft_effective_reset_n) begin range_toggle_sync <= 3'b000; doppler_toggle_sync <= 3'b000; cfar_toggle_sync <= 3'b000; @@ -246,6 +333,7 @@ always @(posedge ft_clk or negedge ft_reset_n) begin doppler_real_cap <= 16'd0; doppler_imag_cap <= 16'd0; cfar_detection_cap <= 1'b0; + range_data_ready <= 1'b0; // Default to range-only on reset (prevents write FSM deadlock) stream_ctrl_sync_0 <= 3'b001; stream_ctrl_sync_1 <= 3'b001; @@ -279,6 +367,10 @@ always @(posedge ft_clk or negedge ft_reset_n) begin if (cfar_valid_ft) cfar_detection_cap <= cfar_detection_hold; + // 1-cycle delayed trigger: ensures range_profile_cap has settled + // before the FSM reads it via the byte mux. + range_data_ready <= range_valid_ft; + // Status snapshot on request if (status_req_ft) begin // Word 0: {0xFF[31:24], mode[23:22], stream[21:19], 3'b000[18:16], threshold[15:0]} @@ -315,11 +407,16 @@ always @(*) begin 5'd2: data_pkt_byte = range_profile_cap[23:16]; 5'd3: data_pkt_byte = range_profile_cap[15:8]; 5'd4: data_pkt_byte = range_profile_cap[7:0]; // range LSB - 5'd5: data_pkt_byte = doppler_real_cap[15:8]; // doppler_real MSB - 5'd6: data_pkt_byte = doppler_real_cap[7:0]; // doppler_real LSB - 5'd7: data_pkt_byte = doppler_imag_cap[15:8]; // doppler_imag MSB - 5'd8: data_pkt_byte = doppler_imag_cap[7:0]; // doppler_imag LSB - 5'd9: data_pkt_byte = {7'b0, cfar_detection_cap}; // detection + // Doppler fields: zero when stream_doppler_en is off + 5'd5: data_pkt_byte = stream_doppler_en ? doppler_real_cap[15:8] : 8'd0; + 5'd6: data_pkt_byte = stream_doppler_en ? doppler_real_cap[7:0] : 8'd0; + 5'd7: data_pkt_byte = stream_doppler_en ? doppler_imag_cap[15:8] : 8'd0; + 5'd8: data_pkt_byte = stream_doppler_en ? doppler_imag_cap[7:0] : 8'd0; + // Detection field: zero when stream_cfar_en is off + // Bit 7 = frame_start flag (sample_counter == 0), bit 0 = cfar_detection + 5'd9: data_pkt_byte = stream_cfar_en + ? {(sample_counter == 12'd0), 6'b0, cfar_detection_cap} + : {(sample_counter == 12'd0), 7'd0}; 5'd10: data_pkt_byte = FOOTER; default: data_pkt_byte = 8'h00; endcase @@ -376,12 +473,13 @@ end // Write FSM and Read FSM share the bus. Write FSM operates when Read FSM // is idle. Read FSM takes priority when host has data available. -always @(posedge ft_clk or negedge ft_reset_n) begin - if (!ft_reset_n) begin +always @(posedge ft_clk or negedge ft_effective_reset_n) begin + if (!ft_effective_reset_n) begin wr_state <= WR_IDLE; wr_byte_idx <= 5'd0; rd_state <= RD_IDLE; rd_byte_cnt <= 2'd0; + rd_cmd_complete <= 1'b0; rd_shift_reg <= 32'd0; ft_data_out <= 8'd0; ft_data_oe <= 1'b0; @@ -396,6 +494,7 @@ always @(posedge ft_clk or negedge ft_reset_n) begin cmd_value <= 16'd0; doppler_data_pending <= 1'b0; cfar_data_pending <= 1'b0; + sample_counter <= 12'd0; end else begin // Default: clear one-shot signals cmd_valid <= 1'b0; @@ -437,17 +536,19 @@ always @(posedge ft_clk or negedge ft_reset_n) begin rd_shift_reg <= {rd_shift_reg[23:0], ft_data}; if (rd_byte_cnt == 2'd3) begin // All 4 bytes received - ft_rd_n <= 1'b1; - rd_byte_cnt <= 2'd0; - rd_state <= RD_DEASSERT; + ft_rd_n <= 1'b1; + rd_byte_cnt <= 2'd0; + rd_cmd_complete <= 1'b1; + rd_state <= RD_DEASSERT; end else begin rd_byte_cnt <= rd_byte_cnt + 2'd1; // Keep reading if more data available if (ft_rxf_n) begin // Host ran out of data mid-command — abort - ft_rd_n <= 1'b1; - rd_byte_cnt <= 2'd0; - rd_state <= RD_DEASSERT; + ft_rd_n <= 1'b1; + rd_byte_cnt <= 2'd0; + rd_cmd_complete <= 1'b0; + rd_state <= RD_DEASSERT; end end end @@ -456,7 +557,8 @@ always @(posedge ft_clk or negedge ft_reset_n) begin // Deassert OE (1 cycle after RD deasserted) ft_oe_n <= 1'b1; // Only process if we received a full 4-byte command - if (rd_byte_cnt == 2'd0) begin + if (rd_cmd_complete) begin + rd_cmd_complete <= 1'b0; rd_state <= RD_PROCESS; end else begin // Incomplete command — discard @@ -491,8 +593,13 @@ always @(posedge ft_clk or negedge ft_reset_n) begin wr_state <= WR_STATUS_SEND; wr_byte_idx <= 5'd0; end - // Trigger on range_valid edge (primary data trigger) - else if (range_valid_ft && stream_range_en) begin + // Trigger on range_data_ready (1 cycle after range_valid_ft) + // so that range_profile_cap has settled from the CDC block. + // Gate on pending flags: only send when all enabled + // streams have fresh data (avoids stale doppler/CFAR) + else if (range_data_ready && stream_range_en + && (!stream_doppler_en || doppler_data_pending) + && (!stream_cfar_en || cfar_data_pending)) begin if (ft_rxf_n) begin // No host read pending wr_state <= WR_DATA_SEND; wr_byte_idx <= 5'd0; @@ -538,6 +645,11 @@ always @(posedge ft_clk or negedge ft_reset_n) begin // Clear pending flags — data consumed doppler_data_pending <= 1'b0; cfar_data_pending <= 1'b0; + // Advance frame sync counter + if (sample_counter == NUM_CELLS - 12'd1) + sample_counter <= 12'd0; + else + sample_counter <= sample_counter + 12'd1; wr_state <= WR_IDLE; end From b22cadb42908029fa0fbc3b483d0741a9b2d3ee9 Mon Sep 17 00:00:00 2001 From: Jason <83615043+JJassonn69@users.noreply.github.com> Date: Thu, 16 Apr 2026 16:19:13 +0545 Subject: [PATCH 2/7] feat(gui): add FT601Connection class, USB interface selection in V65/V7 - Add FT601Connection in radar_protocol.py using ftd3xx library with proper setChipConfiguration re-enumeration handling (close, wait 2s, re-open) and 4-byte write alignment - Add USB Interface dropdown to V65 Tk GUI (FT2232H default, FT601 option) - Add USB Interface combo to V7 PyQt dashboard with Live/File mode toggle - Fix mock frame_start bit 7 in both FT2232H and FT601 connections - Use FPGA range data from USB packets instead of recomputing in Python - Export FT601Connection from v7/hardware.py and v7/__init__.py - Add 7 FT601Connection tests (91 total in test_GUI_V65_Tk.py) --- 9_Firmware/9_3_GUI/GUI_V65_Tk.py | 48 ++++-- 9_Firmware/9_3_GUI/radar_protocol.py | 206 +++++++++++++++++++++++++- 9_Firmware/9_3_GUI/test_GUI_V65_Tk.py | 57 ++++++- 9_Firmware/9_3_GUI/v7/__init__.py | 3 +- 9_Firmware/9_3_GUI/v7/dashboard.py | 49 ++++-- 9_Firmware/9_3_GUI/v7/hardware.py | 1 + 6 files changed, 336 insertions(+), 28 deletions(-) diff --git a/9_Firmware/9_3_GUI/GUI_V65_Tk.py b/9_Firmware/9_3_GUI/GUI_V65_Tk.py index 6ac8007..0ecae7b 100644 --- a/9_Firmware/9_3_GUI/GUI_V65_Tk.py +++ b/9_Firmware/9_3_GUI/GUI_V65_Tk.py @@ -59,7 +59,7 @@ except (ModuleNotFoundError, ImportError): # Import protocol layer (no GUI deps) from radar_protocol import ( - RadarProtocol, FT2232HConnection, + RadarProtocol, FT2232HConnection, FT601Connection, DataRecorder, RadarAcquisition, RadarFrame, StatusResponse, NUM_RANGE_BINS, NUM_DOPPLER_BINS, WATERFALL_DEPTH, @@ -388,10 +388,11 @@ class RadarDashboard: BANDWIDTH = 500e6 # Hz — chirp bandwidth C = 3e8 # m/s — speed of light - def __init__(self, root: tk.Tk, connection: FT2232HConnection, + def __init__(self, root: tk.Tk, mock: bool, recorder: DataRecorder, device_index: int = 0): self.root = root - self.conn = connection + self._mock = mock + self.conn: FT2232HConnection | FT601Connection | None = None self.recorder = recorder self.device_index = device_index @@ -485,6 +486,16 @@ class RadarDashboard: style="Accent.TButton") self.btn_connect.pack(side="right", padx=4) + # USB Interface selector (production FT2232H / premium FT601) + self._usb_iface_var = tk.StringVar(value="FT2232H (Production)") + self.cmb_usb_iface = ttk.Combobox( + top, textvariable=self._usb_iface_var, + values=["FT2232H (Production)", "FT601 (Premium)"], + state="readonly", width=20, + ) + self.cmb_usb_iface.pack(side="right", padx=4) + ttk.Label(top, text="USB:", font=("Menlo", 10)).pack(side="right") + self.btn_record = ttk.Button(top, text="Record", command=self._on_record) self.btn_record.pack(side="right", padx=4) @@ -1018,15 +1029,17 @@ class RadarDashboard: # ------------------------------------------------------------ Actions def _on_connect(self): - if self.conn.is_open: + if self.conn is not None and self.conn.is_open: # Disconnect if self._acq_thread is not None: self._acq_thread.stop() self._acq_thread.join(timeout=2) self._acq_thread = None self.conn.close() + self.conn = None self.lbl_status.config(text="DISCONNECTED", foreground=RED) self.btn_connect.config(text="Connect") + self.cmb_usb_iface.config(state="readonly") log.info("Disconnected") return @@ -1036,6 +1049,16 @@ class RadarDashboard: if self._replay_active: self._replay_stop() + # Create connection based on USB Interface selector + iface = self._usb_iface_var.get() + if "FT601" in iface: + self.conn = FT601Connection(mock=self._mock) + else: + self.conn = FT2232HConnection(mock=self._mock) + + # Disable interface selector while connecting/connected + self.cmb_usb_iface.config(state="disabled") + # Open connection in a background thread to avoid blocking the GUI self.lbl_status.config(text="CONNECTING...", foreground=YELLOW) self.btn_connect.config(state="disabled") @@ -1062,6 +1085,8 @@ class RadarDashboard: else: self.lbl_status.config(text="CONNECT FAILED", foreground=RED) self.btn_connect.config(text="Connect") + self.cmb_usb_iface.config(state="readonly") + self.conn = None def _on_record(self): if self.recorder.recording: @@ -1110,6 +1135,9 @@ class RadarDashboard: f"Opcode 0x{opcode:02X} is hardware-only (ignored in replay)")) return cmd = RadarProtocol.build_command(opcode, value) + if self.conn is None: + log.warning("No connection — command not sent") + return ok = self.conn.write(cmd) log.info(f"CMD 0x{opcode:02X} val={value} ({'OK' if ok else 'FAIL'})") @@ -1148,7 +1176,7 @@ class RadarDashboard: if self._replay_active or self._replay_ctrl is not None: self._replay_stop() if self._acq_thread is not None: - if self.conn.is_open: + if self.conn is not None and self.conn.is_open: self._on_connect() # disconnect else: # Connection dropped unexpectedly — just clean up the thread @@ -1547,17 +1575,17 @@ def main(): args = parser.parse_args() if args.live: - conn = FT2232HConnection(mock=False) + mock = False mode_str = "LIVE" else: - conn = FT2232HConnection(mock=True) + mock = True mode_str = "MOCK" recorder = DataRecorder() root = tk.Tk() - dashboard = RadarDashboard(root, conn, recorder, device_index=args.device) + dashboard = RadarDashboard(root, mock, recorder, device_index=args.device) if args.record: filepath = os.path.join( @@ -1582,8 +1610,8 @@ def main(): if dashboard._acq_thread is not None: dashboard._acq_thread.stop() dashboard._acq_thread.join(timeout=2) - if conn.is_open: - conn.close() + if dashboard.conn is not None and dashboard.conn.is_open: + dashboard.conn.close() if recorder.recording: recorder.stop() root.destroy() diff --git a/9_Firmware/9_3_GUI/radar_protocol.py b/9_Firmware/9_3_GUI/radar_protocol.py index e04d768..52176d2 100644 --- a/9_Firmware/9_3_GUI/radar_protocol.py +++ b/9_Firmware/9_3_GUI/radar_protocol.py @@ -6,6 +6,7 @@ Pure-logic module for USB packet parsing and command building. No GUI dependencies — safe to import from tests and headless scripts. USB Interface: FT2232H USB 2.0 (8-bit, 50T production board) via pyftdi + FT601 USB 3.0 (32-bit, 200T premium board) via ftd3xx USB Packet Protocol (11-byte): TX (FPGA→Host): @@ -22,7 +23,7 @@ import queue import logging import contextlib from dataclasses import dataclass, field -from typing import Any +from typing import Any, ClassVar from enum import IntEnum @@ -200,7 +201,9 @@ class RadarProtocol: range_i = _to_signed16(struct.unpack_from(">H", raw, 3)[0]) doppler_i = _to_signed16(struct.unpack_from(">H", raw, 5)[0]) doppler_q = _to_signed16(struct.unpack_from(">H", raw, 7)[0]) - detection = raw[9] & 0x01 + det_byte = raw[9] + detection = det_byte & 0x01 + frame_start = (det_byte >> 7) & 0x01 return { "range_i": range_i, @@ -208,6 +211,7 @@ class RadarProtocol: "doppler_i": doppler_i, "doppler_q": doppler_q, "detection": detection, + "frame_start": frame_start, } @staticmethod @@ -433,7 +437,191 @@ class FT2232HConnection: pkt += struct.pack(">h", np.clip(range_i, -32768, 32767)) pkt += struct.pack(">h", np.clip(dop_i, -32768, 32767)) pkt += struct.pack(">h", np.clip(dop_q, -32768, 32767)) - pkt.append(detection & 0x01) + # Bit 7 = frame_start (sample_counter == 0), bit 0 = detection + det_byte = (detection & 0x01) | (0x80 if idx == 0 else 0x00) + pkt.append(det_byte) + pkt.append(FOOTER_BYTE) + + buf += pkt + + self._mock_seq_idx = (start_idx + num_packets) % NUM_CELLS + return bytes(buf) + + +# ============================================================================ +# FT601 USB 3.0 Connection (premium board only) +# ============================================================================ + +# Optional ftd3xx import (FTDI's proprietary driver for FT60x USB 3.0 chips). +# pyftdi does NOT support FT601 — it only handles USB 2.0 chips (FT232H, etc.) +try: + import ftd3xx # type: ignore[import-untyped] + FTD3XX_AVAILABLE = True + _Ftd3xxError: type = ftd3xx.FTD3XXError # type: ignore[attr-defined] +except ImportError: + FTD3XX_AVAILABLE = False + _Ftd3xxError = OSError # fallback for type-checking; never raised + + +class FT601Connection: + """ + FT601 USB 3.0 SuperSpeed FIFO bridge — premium board only. + + The FT601 has a 32-bit data bus and runs at 100 MHz. + VID:PID = 0x0403:0x6030 or 0x6031 (FTDI FT60x). + + Requires the ``ftd3xx`` library (``pip install ftd3xx`` on Windows, + or ``libft60x`` on Linux). This is FTDI's proprietary USB 3.0 driver; + ``pyftdi`` only supports USB 2.0 and will NOT work with FT601. + + Public contract matches FT2232HConnection so callers can swap freely. + """ + + VID = 0x0403 + PID_LIST: ClassVar[list[int]] = [0x6030, 0x6031] + + def __init__(self, mock: bool = True): + self._mock = mock + self._dev = None + self._lock = threading.Lock() + self.is_open = False + # Mock state (reuses same synthetic data pattern) + self._mock_frame_num = 0 + self._mock_rng = np.random.RandomState(42) + + def open(self, device_index: int = 0) -> bool: + if self._mock: + self.is_open = True + log.info("FT601 mock device opened (no hardware)") + return True + + if not FTD3XX_AVAILABLE: + log.error( + "ftd3xx library required for FT601 hardware — " + "install with: pip install ftd3xx" + ) + return False + + try: + self._dev = ftd3xx.create(device_index, ftd3xx.OPEN_BY_INDEX) + if self._dev is None: + log.error("No FT601 device found at index %d", device_index) + return False + # Verify chip configuration — only reconfigure if needed. + # setChipConfiguration triggers USB re-enumeration, which + # invalidates the device handle and requires a re-open cycle. + cfg = self._dev.getChipConfiguration() + needs_reconfig = ( + cfg.FIFOMode != 0 # 245 FIFO mode + or cfg.ChannelConfig != 0 # 1 channel, 32-bit + or cfg.OptionalFeatureSupport != 0 + ) + if needs_reconfig: + cfg.FIFOMode = 0 + cfg.ChannelConfig = 0 + cfg.OptionalFeatureSupport = 0 + self._dev.setChipConfiguration(cfg) + # Device re-enumerates — close stale handle, wait, re-open + self._dev.close() + self._dev = None + import time + time.sleep(2.0) # wait for USB re-enumeration + self._dev = ftd3xx.create(device_index, ftd3xx.OPEN_BY_INDEX) + if self._dev is None: + log.error("FT601 not found after reconfiguration") + return False + log.info("FT601 reconfigured and re-opened (index %d)", device_index) + self.is_open = True + log.info("FT601 device opened (index %d)", device_index) + return True + except (OSError, _Ftd3xxError) as e: + log.error("FT601 open failed: %s", e) + self._dev = None + return False + + def close(self): + if self._dev is not None: + with contextlib.suppress(Exception): + self._dev.close() + self._dev = None + self.is_open = False + + def read(self, size: int = 4096) -> bytes | None: + """Read raw bytes from FT601. Returns None on error/timeout.""" + if not self.is_open: + return None + + if self._mock: + return self._mock_read(size) + + with self._lock: + try: + data = self._dev.readPipe(0x82, size, raw=True) + return bytes(data) if data else None + except (OSError, _Ftd3xxError) as e: + log.error("FT601 read error: %s", e) + return None + + def write(self, data: bytes) -> bool: + """Write raw bytes to FT601. Data must be 4-byte aligned for 32-bit bus.""" + if not self.is_open: + return False + + if self._mock: + log.info(f"FT601 mock write: {data.hex()}") + return True + + # Pad to 4-byte alignment (FT601 32-bit bus requirement). + # NOTE: Radar commands are already 4 bytes, so this should be a no-op. + remainder = len(data) % 4 + if remainder: + data = data + b"\x00" * (4 - remainder) + + with self._lock: + try: + written = self._dev.writePipe(0x02, data, raw=True) + return written == len(data) + except (OSError, _Ftd3xxError) as e: + log.error("FT601 write error: %s", e) + return False + + def _mock_read(self, size: int) -> bytes: + """Generate synthetic radar packets (same pattern as FT2232H mock).""" + time.sleep(0.05) + self._mock_frame_num += 1 + + buf = bytearray() + num_packets = min(NUM_CELLS, size // DATA_PACKET_SIZE) + start_idx = getattr(self, "_mock_seq_idx", 0) + + for n in range(num_packets): + idx = (start_idx + n) % NUM_CELLS + rbin = idx // NUM_DOPPLER_BINS + dbin = idx % NUM_DOPPLER_BINS + + range_i = int(self._mock_rng.normal(0, 100)) + range_q = int(self._mock_rng.normal(0, 100)) + if abs(rbin - 20) < 3: + range_i += 5000 + range_q += 3000 + + dop_i = int(self._mock_rng.normal(0, 50)) + dop_q = int(self._mock_rng.normal(0, 50)) + if abs(rbin - 20) < 3 and abs(dbin - 8) < 2: + dop_i += 8000 + dop_q += 4000 + + detection = 1 if (abs(rbin - 20) < 2 and abs(dbin - 8) < 2) else 0 + + pkt = bytearray() + pkt.append(HEADER_BYTE) + pkt += struct.pack(">h", np.clip(range_q, -32768, 32767)) + pkt += struct.pack(">h", np.clip(range_i, -32768, 32767)) + pkt += struct.pack(">h", np.clip(dop_i, -32768, 32767)) + pkt += struct.pack(">h", np.clip(dop_q, -32768, 32767)) + # Bit 7 = frame_start (sample_counter == 0), bit 0 = detection + det_byte = (detection & 0x01) | (0x80 if idx == 0 else 0x00) + pkt.append(det_byte) pkt.append(FOOTER_BYTE) buf += pkt @@ -600,6 +788,12 @@ class RadarAcquisition(threading.Thread): if sample.get("detection", 0): self._frame.detections[rbin, dbin] = 1 self._frame.detection_count += 1 + # Accumulate FPGA range profile data (matched-filter output) + # Each sample carries the range_i/range_q for this range bin. + # Accumulate magnitude across Doppler bins for the range profile. + ri = int(sample.get("range_i", 0)) + rq = int(sample.get("range_q", 0)) + self._frame.range_profile[rbin] += abs(ri) + abs(rq) self._sample_idx += 1 @@ -607,11 +801,11 @@ class RadarAcquisition(threading.Thread): self._finalize_frame() def _finalize_frame(self): - """Complete frame: compute range profile, push to queue, record.""" + """Complete frame: push to queue, record.""" self._frame.timestamp = time.time() self._frame.frame_number = self._frame_num - # Range profile = sum of magnitude across Doppler bins - self._frame.range_profile = np.sum(self._frame.magnitude, axis=1) + # range_profile is already accumulated from FPGA range_i/range_q + # data in _ingest_sample(). No need to synthesize from doppler magnitude. # Push to display queue (drop old if backed up) try: diff --git a/9_Firmware/9_3_GUI/test_GUI_V65_Tk.py b/9_Firmware/9_3_GUI/test_GUI_V65_Tk.py index de5f18f..1cd32ad 100644 --- a/9_Firmware/9_3_GUI/test_GUI_V65_Tk.py +++ b/9_Firmware/9_3_GUI/test_GUI_V65_Tk.py @@ -16,7 +16,7 @@ import unittest import numpy as np from radar_protocol import ( - RadarProtocol, FT2232HConnection, DataRecorder, RadarAcquisition, + RadarProtocol, FT2232HConnection, FT601Connection, DataRecorder, RadarAcquisition, RadarFrame, StatusResponse, Opcode, HEADER_BYTE, FOOTER_BYTE, STATUS_HEADER_BYTE, NUM_RANGE_BINS, NUM_DOPPLER_BINS, @@ -312,6 +312,61 @@ class TestFT2232HConnection(unittest.TestCase): self.assertFalse(conn.write(b"\x00\x00\x00\x00")) +class TestFT601Connection(unittest.TestCase): + """Test mock FT601 connection (mirrors FT2232H tests).""" + + def test_mock_open_close(self): + conn = FT601Connection(mock=True) + self.assertTrue(conn.open()) + self.assertTrue(conn.is_open) + conn.close() + self.assertFalse(conn.is_open) + + def test_mock_read_returns_data(self): + conn = FT601Connection(mock=True) + conn.open() + data = conn.read(4096) + self.assertIsNotNone(data) + self.assertGreater(len(data), 0) + conn.close() + + def test_mock_read_contains_valid_packets(self): + """Mock data should contain parseable data packets.""" + conn = FT601Connection(mock=True) + conn.open() + raw = conn.read(4096) + packets = RadarProtocol.find_packet_boundaries(raw) + self.assertGreater(len(packets), 0) + for start, end, ptype in packets: + if ptype == "data": + result = RadarProtocol.parse_data_packet(raw[start:end]) + self.assertIsNotNone(result) + conn.close() + + def test_mock_write(self): + conn = FT601Connection(mock=True) + conn.open() + cmd = RadarProtocol.build_command(0x01, 1) + self.assertTrue(conn.write(cmd)) + conn.close() + + def test_write_pads_to_4_bytes(self): + """FT601 write() should pad data to 4-byte alignment.""" + conn = FT601Connection(mock=True) + conn.open() + # 3-byte payload should be padded internally (no error) + self.assertTrue(conn.write(b"\x01\x02\x03")) + conn.close() + + def test_read_when_closed(self): + conn = FT601Connection(mock=True) + self.assertIsNone(conn.read()) + + def test_write_when_closed(self): + conn = FT601Connection(mock=True) + self.assertFalse(conn.write(b"\x00\x00\x00\x00")) + + class TestDataRecorder(unittest.TestCase): """Test HDF5 recording (skipped if h5py not available).""" diff --git a/9_Firmware/9_3_GUI/v7/__init__.py b/9_Firmware/9_3_GUI/v7/__init__.py index 1da6cdb..3789667 100644 --- a/9_Firmware/9_3_GUI/v7/__init__.py +++ b/9_Firmware/9_3_GUI/v7/__init__.py @@ -26,6 +26,7 @@ from .models import ( # Hardware interfaces — production protocol via radar_protocol.py from .hardware import ( FT2232HConnection, + FT601Connection, RadarProtocol, Opcode, RadarAcquisition, @@ -89,7 +90,7 @@ __all__ = [ # noqa: RUF022 "USB_AVAILABLE", "FTDI_AVAILABLE", "SCIPY_AVAILABLE", "SKLEARN_AVAILABLE", "FILTERPY_AVAILABLE", # hardware — production FPGA protocol - "FT2232HConnection", "RadarProtocol", "Opcode", + "FT2232HConnection", "FT601Connection", "RadarProtocol", "Opcode", "RadarAcquisition", "RadarFrame", "StatusResponse", "DataRecorder", "STM32USBInterface", # processing diff --git a/9_Firmware/9_3_GUI/v7/dashboard.py b/9_Firmware/9_3_GUI/v7/dashboard.py index c7e2c64..8c7233f 100644 --- a/9_Firmware/9_3_GUI/v7/dashboard.py +++ b/9_Firmware/9_3_GUI/v7/dashboard.py @@ -13,13 +13,14 @@ RadarDashboard is a QMainWindow with six tabs: 6. Settings — Host-side DSP parameters + About section Uses production radar_protocol.py for all FPGA communication: - - FT2232HConnection for real hardware + - FT2232HConnection for production board (FT2232H USB 2.0) + - FT601Connection for premium board (FT601 USB 3.0) — selectable from GUI - Unified replay via SoftwareFPGA + ReplayEngine + ReplayWorker - Mock mode (FT2232HConnection(mock=True)) for development The old STM32 magic-packet start flow has been removed. FPGA registers are controlled directly via 4-byte {opcode, addr, value_hi, value_lo} -commands sent over FT2232H. +commands sent over FT2232H or FT601. """ from __future__ import annotations @@ -55,6 +56,7 @@ from .models import ( ) from .hardware import ( FT2232HConnection, + FT601Connection, RadarProtocol, RadarFrame, StatusResponse, @@ -142,7 +144,7 @@ class RadarDashboard(QMainWindow): ) # Hardware interfaces — production protocol - self._connection: FT2232HConnection | None = None + self._connection: FT2232HConnection | FT601Connection | None = None self._stm32 = STM32USBInterface() self._recorder = DataRecorder() @@ -364,7 +366,7 @@ class RadarDashboard(QMainWindow): # Row 0: connection mode + device combos + buttons ctrl_layout.addWidget(QLabel("Mode:"), 0, 0) self._mode_combo = QComboBox() - self._mode_combo.addItems(["Mock", "Live FT2232H", "Replay"]) + self._mode_combo.addItems(["Mock", "Live", "Replay"]) self._mode_combo.setCurrentIndex(0) ctrl_layout.addWidget(self._mode_combo, 0, 1) @@ -377,6 +379,13 @@ class RadarDashboard(QMainWindow): refresh_btn.clicked.connect(self._refresh_devices) ctrl_layout.addWidget(refresh_btn, 0, 4) + # USB Interface selector (production FT2232H / premium FT601) + ctrl_layout.addWidget(QLabel("USB Interface:"), 0, 5) + self._usb_iface_combo = QComboBox() + self._usb_iface_combo.addItems(["FT2232H (Production)", "FT601 (Premium)"]) + self._usb_iface_combo.setCurrentIndex(0) + ctrl_layout.addWidget(self._usb_iface_combo, 0, 6) + self._start_btn = QPushButton("Start Radar") self._start_btn.setStyleSheet( f"QPushButton {{ background-color: {DARK_SUCCESS}; color: white; font-weight: bold; }}" @@ -1001,7 +1010,8 @@ class RadarDashboard(QMainWindow): self._conn_ft2232h = self._make_status_label("FT2232H") self._conn_stm32 = self._make_status_label("STM32 USB") - conn_layout.addWidget(QLabel("FT2232H:"), 0, 0) + self._conn_usb_label = QLabel("USB Data:") + conn_layout.addWidget(self._conn_usb_label, 0, 0) conn_layout.addWidget(self._conn_ft2232h, 0, 1) conn_layout.addWidget(QLabel("STM32 USB:"), 1, 0) conn_layout.addWidget(self._conn_stm32, 1, 1) @@ -1167,7 +1177,7 @@ class RadarDashboard(QMainWindow): about_lbl = QLabel( "AERIS-10 Radar System V7
" "PyQt6 Edition with Embedded Leaflet Map

" - "Data Interface: FT2232H USB 2.0 (production protocol)
" + "Data Interface: FT2232H USB 2.0 (production) / FT601 USB 3.0 (premium)
" "FPGA Protocol: 4-byte register commands, 0xAA/0xBB packets
" "Map: OpenStreetMap + Leaflet.js
" "Framework: PyQt6 + QWebEngine
" @@ -1224,7 +1234,7 @@ class RadarDashboard(QMainWindow): # ===================================================================== def _send_fpga_cmd(self, opcode: int, value: int): - """Send a 4-byte register command to the FPGA via FT2232H.""" + """Send a 4-byte register command to the FPGA via USB (FT2232H or FT601).""" if self._connection is None or not self._connection.is_open: logger.warning(f"Cannot send 0x{opcode:02X}={value}: no connection") return @@ -1287,16 +1297,26 @@ class RadarDashboard(QMainWindow): if "Mock" in mode: self._replay_mode = False - self._connection = FT2232HConnection(mock=True) + iface = self._usb_iface_combo.currentText() + if "FT601" in iface: + self._connection = FT601Connection(mock=True) + else: + self._connection = FT2232HConnection(mock=True) if not self._connection.open(): QMessageBox.critical(self, "Error", "Failed to open mock connection.") return elif "Live" in mode: self._replay_mode = False - self._connection = FT2232HConnection(mock=False) + iface = self._usb_iface_combo.currentText() + if "FT601" in iface: + self._connection = FT601Connection(mock=False) + iface_name = "FT601" + else: + self._connection = FT2232HConnection(mock=False) + iface_name = "FT2232H" if not self._connection.open(): QMessageBox.critical(self, "Error", - "Failed to open FT2232H. Check USB connection.") + f"Failed to open {iface_name}. Check USB connection.") return elif "Replay" in mode: self._replay_mode = True @@ -1368,6 +1388,7 @@ class RadarDashboard(QMainWindow): self._start_btn.setEnabled(False) self._stop_btn.setEnabled(True) self._mode_combo.setEnabled(False) + self._usb_iface_combo.setEnabled(False) self._demo_btn_main.setEnabled(False) self._demo_btn_map.setEnabled(False) n_frames = self._replay_engine.total_frames @@ -1417,6 +1438,7 @@ class RadarDashboard(QMainWindow): self._start_btn.setEnabled(False) self._stop_btn.setEnabled(True) self._mode_combo.setEnabled(False) + self._usb_iface_combo.setEnabled(False) self._demo_btn_main.setEnabled(False) self._demo_btn_map.setEnabled(False) self._status_label_main.setText(f"Status: Running ({mode})") @@ -1462,6 +1484,7 @@ class RadarDashboard(QMainWindow): self._start_btn.setEnabled(True) self._stop_btn.setEnabled(False) self._mode_combo.setEnabled(True) + self._usb_iface_combo.setEnabled(True) self._demo_btn_main.setEnabled(True) self._demo_btn_map.setEnabled(True) self._status_label_main.setText("Status: Radar stopped") @@ -1954,6 +1977,12 @@ class RadarDashboard(QMainWindow): self._set_conn_indicator(self._conn_ft2232h, conn_open) self._set_conn_indicator(self._conn_stm32, self._stm32.is_open) + # Update USB label to reflect which interface is active + if isinstance(self._connection, FT601Connection): + self._conn_usb_label.setText("FT601:") + else: + self._conn_usb_label.setText("FT2232H:") + gps_count = self._gps_packet_count if self._gps_worker: gps_count = self._gps_worker.gps_count diff --git a/9_Firmware/9_3_GUI/v7/hardware.py b/9_Firmware/9_3_GUI/v7/hardware.py index 84bbb9a..2d85dc7 100644 --- a/9_Firmware/9_3_GUI/v7/hardware.py +++ b/9_Firmware/9_3_GUI/v7/hardware.py @@ -25,6 +25,7 @@ if USB_AVAILABLE: sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) from radar_protocol import ( # noqa: F401 — re-exported for v7 package FT2232HConnection, + FT601Connection, RadarProtocol, Opcode, RadarAcquisition, From 6a11d33ef75b12f6ca77531e50df9fd6f6348674 Mon Sep 17 00:00:00 2001 From: Jason <83615043+JJassonn69@users.noreply.github.com> Date: Thu, 16 Apr 2026 16:19:30 +0545 Subject: [PATCH 3/7] docs: deprecate GUI V6, update docs for FT2232H production default - Add deprecation headers to GUI_V6.py and GUI_V6_Demo.py - Mark V6 as deprecated in GUI_versions.txt - Update README.md: replace V6 GIF reference with V65 PNG - Add FT2232H production notice banner to docs/index.html --- 9_Firmware/9_3_GUI/GUI_V6.py | 6 ++++++ 9_Firmware/9_3_GUI/GUI_V6_Demo.py | 6 ++++++ 9_Firmware/9_3_GUI/GUI_versions.txt | 2 +- README.md | 3 ++- docs/index.html | 5 +++++ 5 files changed, 20 insertions(+), 2 deletions(-) diff --git a/9_Firmware/9_3_GUI/GUI_V6.py b/9_Firmware/9_3_GUI/GUI_V6.py index 5688288..3518967 100644 --- a/9_Firmware/9_3_GUI/GUI_V6.py +++ b/9_Firmware/9_3_GUI/GUI_V6.py @@ -1,3 +1,9 @@ +# ============================================================================= +# DEPRECATED: GUI V6 is superseded by GUI_V65_Tk (tkinter) and V7 (PyQt6). +# This file is retained for reference only. Do not use for new development. +# Removal planned for next major release. +# ============================================================================= + import tkinter as tk from tkinter import ttk, messagebox import threading diff --git a/9_Firmware/9_3_GUI/GUI_V6_Demo.py b/9_Firmware/9_3_GUI/GUI_V6_Demo.py index dd4135c..2414238 100644 --- a/9_Firmware/9_3_GUI/GUI_V6_Demo.py +++ b/9_Firmware/9_3_GUI/GUI_V6_Demo.py @@ -1,5 +1,11 @@ #!/usr/bin/env python3 +# ============================================================================= +# DEPRECATED: GUI V6 Demo is superseded by GUI_V65_Tk and V7. +# This file is retained for reference only. Do not use for new development. +# Removal planned for next major release. +# ============================================================================= + """ Radar System GUI - Fully Functional Demo Version All buttons work, simulated radar data is generated in real-time diff --git a/9_Firmware/9_3_GUI/GUI_versions.txt b/9_Firmware/9_3_GUI/GUI_versions.txt index 5ed1fa2..27ceff7 100644 --- a/9_Firmware/9_3_GUI/GUI_versions.txt +++ b/9_Firmware/9_3_GUI/GUI_versions.txt @@ -6,7 +6,7 @@ GUI_V4 ==> Added pitch correction GUI_V5 ==> Added Mercury Color -GUI_V6 ==> Added USB3 FT601 support +GUI_V6 ==> Added USB3 FT601 support [DEPRECATED — superseded by V65/V7] GUI_V65_Tk ==> Board bring-up dashboard (FT2232H reader, real-time R-D heatmap, CFAR overlay, waterfall, host commands, HDF5 recording, replay, demo mode) radar_protocol ==> Protocol layer (packet parsing, command building, FT2232H connection, data recorder, acquisition thread) diff --git a/README.md b/README.md index 522a216..f8a52ec 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,8 @@ The AERIS-10 main sub-systems are: - Map integration - Radar control interface -![AERIS-10 GUI Demo](https://raw.githubusercontent.com/NawfalMotii79/PLFM_RADAR/main/8_Utils/GUI_V6.gif) +![AERIS-10 Dashboard](https://raw.githubusercontent.com/NawfalMotii79/PLFM_RADAR/main/8_Utils/GUI_V65_Tk.png) + ## 📊 Technical Specifications diff --git a/docs/index.html b/docs/index.html index e50b442..1a6744f 100644 --- a/docs/index.html +++ b/docs/index.html @@ -32,6 +32,11 @@
+
+

Production Board USB

+

FT2232H (USB 2.0)

+

50T production board uses FT2232H. FT601 USB 3.0 is available on 200T premium dev board only.

+

Tracked Timing Baseline

WNS +0.058 ns

From a03dd1329a197adc610c4645640b842a975958e1 Mon Sep 17 00:00:00 2001 From: Jason <83615043+JJassonn69@users.noreply.github.com> Date: Thu, 16 Apr 2026 16:48:43 +0545 Subject: [PATCH 4/7] fix(tests): update cross-layer tests for frame_start bit and stream-gated mux - TB byte 9 check: expect 0x81 (frame_start=1 after reset + cfar=1) - contract_parser: handle ternary expressions in data_pkt_byte mux (stream_doppler_en ? doppler_real_cap : 8'd0 pattern) - contract_parser: handle intermediate variable pattern for detection field (det_byte = raw[9]; detection = det_byte & 0x01) --- .../tests/cross_layer/contract_parser.py | 51 +++++++++++++++---- .../cross_layer/tb_cross_layer_ft2232h.v | 6 ++- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/9_Firmware/tests/cross_layer/contract_parser.py b/9_Firmware/tests/cross_layer/contract_parser.py index a0220ff..33e0a19 100644 --- a/9_Firmware/tests/cross_layer/contract_parser.py +++ b/9_Firmware/tests/cross_layer/contract_parser.py @@ -188,7 +188,7 @@ def parse_python_data_packet_fields(filepath: Path | None = None) -> list[DataPa width_bits=size * 8 )) - # Match detection = raw[9] & 0x01 + # Match detection = raw[9] & 0x01 (direct access) for m in re.finditer(r'(\w+)\s*=\s*raw\[(\d+)\]\s*&\s*(0x[0-9a-fA-F]+|\d+)', body): name = m.group(1) offset = int(m.group(2)) @@ -196,6 +196,24 @@ def parse_python_data_packet_fields(filepath: Path | None = None) -> list[DataPa name=name, byte_start=offset, byte_end=offset, width_bits=1 )) + # Match intermediate variable pattern: var = raw[N], then field = var & MASK + for m in re.finditer(r'(\w+)\s*=\s*raw\[(\d+)\]', body): + var_name = m.group(1) + offset = int(m.group(2)) + # Find fields derived from this intermediate variable + for m2 in re.finditer( + rf'(\w+)\s*=\s*(?:\({var_name}\s*>>\s*\d+\)\s*&|{var_name}\s*&)\s*' + r'(0x[0-9a-fA-F]+|\d+)', + body, + ): + name = m2.group(1) + # Skip if already captured by direct raw[] access pattern + if not any(f.name == name for f in fields): + fields.append(DataPacketField( + name=name, byte_start=offset, byte_end=offset, + width_bits=1 + )) + fields.sort(key=lambda f: f.byte_start) return fields @@ -583,12 +601,28 @@ def parse_verilog_data_mux( for m in re.finditer( r"5'd(\d+)\s*:\s*data_pkt_byte\s*=\s*(.+?);", - mux_body + mux_body, re.DOTALL ): idx = int(m.group(1)) expr = m.group(2).strip() entries.append((idx, expr)) + # Helper: extract the dominant signal name from a mux expression. + # Handles direct refs like ``range_profile_cap[31:24]``, ternaries + # like ``stream_doppler_en ? doppler_real_cap[15:8] : 8'd0``, and + # concat-ternaries like ``stream_cfar_en ? {…, cfar_detection_cap} : …``. + def _extract_signal(expr: str) -> str | None: + # If it's a ternary, use the true-branch to find the data signal + tern = re.match(r'\w+\s*\?\s*(.+?)\s*:\s*.+', expr, re.DOTALL) + target = tern.group(1) if tern else expr + # Look for a known data signal (xxx_cap pattern or cfar_detection_cap) + cap_match = re.search(r'(\w+_cap)\b', target) + if cap_match: + return cap_match.group(1) + # Fall back to first identifier before a bit-select + sig_match = re.match(r'(\w+?)(?:\[|$)', target) + return sig_match.group(1) if sig_match else None + # Group consecutive bytes by signal root name fields: list[DataPacketField] = [] i = 0 @@ -598,22 +632,21 @@ def parse_verilog_data_mux( i += 1 continue - # Extract signal name (e.g., range_profile_cap from range_profile_cap[31:24]) - sig_match = re.match(r'(\w+?)(?:\[|$)', expr) - if not sig_match: + signal = _extract_signal(expr) + if not signal: i += 1 continue - signal = sig_match.group(1) start_byte = idx end_byte = idx # Find consecutive bytes of the same signal j = i + 1 while j < len(entries): - next_idx, next_expr = entries[j] - if next_expr.startswith(signal): - end_byte = next_idx + _next_idx, next_expr = entries[j] + next_sig = _extract_signal(next_expr) + if next_sig == signal: + end_byte = _next_idx j += 1 else: break 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 94d0a82..107d36e 100644 --- a/9_Firmware/tests/cross_layer/tb_cross_layer_ft2232h.v +++ b/9_Firmware/tests/cross_layer/tb_cross_layer_ft2232h.v @@ -620,8 +620,10 @@ module tb_cross_layer_ft2232h; "Data pkt: byte 7 = 0x56 (doppler_imag MSB)"); check(captured_bytes[8] === 8'h78, "Data pkt: byte 8 = 0x78 (doppler_imag LSB)"); - check(captured_bytes[9] === 8'h01, - "Data pkt: byte 9 = 0x01 (cfar_detection=1)"); + // Byte 9 = {frame_start, 6'b0, cfar_detection} + // After reset sample_counter==0, so frame_start=1 → 0x81 + check(captured_bytes[9] === 8'h81, + "Data pkt: byte 9 = 0x81 (frame_start=1, cfar_detection=1)"); check(captured_bytes[10] === 8'h55, "Data pkt: byte 10 = 0x55 (footer)"); From 7a35f42e6106512c718711f86d44fe6bfa40eeb5 Mon Sep 17 00:00:00 2001 From: Jason <83615043+JJassonn69@users.noreply.github.com> Date: Thu, 16 Apr 2026 17:07:01 +0545 Subject: [PATCH 5/7] refactor(fpga): deduplicate RTL file lists in run_regression.sh Extract RECEIVER_RTL and SYSTEM_RTL shared arrays to replace 6 near-identical file lists. New modules now only need adding once. --- 9_Firmware/9_2_FPGA/run_regression.sh | 89 ++++++++++----------------- 1 file changed, 33 insertions(+), 56 deletions(-) diff --git a/9_Firmware/9_2_FPGA/run_regression.sh b/9_Firmware/9_2_FPGA/run_regression.sh index 9d45878..7ae9822 100755 --- a/9_Firmware/9_2_FPGA/run_regression.sh +++ b/9_Firmware/9_2_FPGA/run_regression.sh @@ -87,6 +87,33 @@ EXTRA_RTL=( frequency_matched_filter.v ) +# --------------------------------------------------------------------------- +# Shared RTL file lists for integration / system tests +# Centralised here so a new module only needs adding once. +# --------------------------------------------------------------------------- + +# Receiver chain (used by golden generate/compare tests) +RECEIVER_RTL=( + radar_receiver_final.v + radar_mode_controller.v + tb/ad9484_interface_400m_stub.v + ddc_400m.v nco_400m_enhanced.v cic_decimator_4x_enhanced.v + cdc_modules.v fir_lowpass.v ddc_input_interface.v + chirp_memory_loader_param.v latency_buffer.v + matched_filter_multi_segment.v matched_filter_processing_chain.v + range_bin_decimator.v doppler_processor.v xfft_16.v fft_engine.v + rx_gain_control.v mti_canceller.v +) + +# Full system top (receiver chain + TX + USB + detection + self-test) +SYSTEM_RTL=( + radar_system_top.v + radar_transmitter.v dac_interface_single.v plfm_chirp_controller.v + "${RECEIVER_RTL[@]}" + usb_data_interface.v usb_data_interface_ft2232h.v edge_detector.v + cfar_ca.v fpga_self_test.v +) + # ---- Layer A: iverilog -Wall compilation ---- run_lint_iverilog() { local label="$1" @@ -404,83 +431,33 @@ if [[ "$QUICK" -eq 0 ]]; then run_test "Receiver (golden generate)" \ tb/tb_rx_golden_reg.vvp \ -DGOLDEN_GENERATE \ - tb/tb_radar_receiver_final.v radar_receiver_final.v \ - radar_mode_controller.v tb/ad9484_interface_400m_stub.v \ - ddc_400m.v nco_400m_enhanced.v cic_decimator_4x_enhanced.v \ - cdc_modules.v fir_lowpass.v ddc_input_interface.v \ - chirp_memory_loader_param.v latency_buffer.v \ - matched_filter_multi_segment.v matched_filter_processing_chain.v \ - range_bin_decimator.v doppler_processor.v xfft_16.v fft_engine.v \ - rx_gain_control.v mti_canceller.v + tb/tb_radar_receiver_final.v "${RECEIVER_RTL[@]}" # Golden compare run_test "Receiver (golden compare)" \ tb/tb_rx_compare_reg.vvp \ - tb/tb_radar_receiver_final.v radar_receiver_final.v \ - radar_mode_controller.v tb/ad9484_interface_400m_stub.v \ - ddc_400m.v nco_400m_enhanced.v cic_decimator_4x_enhanced.v \ - cdc_modules.v fir_lowpass.v ddc_input_interface.v \ - chirp_memory_loader_param.v latency_buffer.v \ - matched_filter_multi_segment.v matched_filter_processing_chain.v \ - range_bin_decimator.v doppler_processor.v xfft_16.v fft_engine.v \ - rx_gain_control.v mti_canceller.v + tb/tb_radar_receiver_final.v "${RECEIVER_RTL[@]}" # Full system top (monitoring-only, legacy) run_test "System Top (radar_system_tb)" \ tb/tb_system_reg.vvp \ - tb/radar_system_tb.v radar_system_top.v \ - radar_transmitter.v dac_interface_single.v plfm_chirp_controller.v \ - radar_receiver_final.v tb/ad9484_interface_400m_stub.v \ - ddc_400m.v nco_400m_enhanced.v cic_decimator_4x_enhanced.v \ - cdc_modules.v fir_lowpass.v ddc_input_interface.v \ - chirp_memory_loader_param.v latency_buffer.v \ - matched_filter_multi_segment.v matched_filter_processing_chain.v \ - range_bin_decimator.v doppler_processor.v xfft_16.v fft_engine.v \ - usb_data_interface.v usb_data_interface_ft2232h.v edge_detector.v radar_mode_controller.v \ - rx_gain_control.v cfar_ca.v mti_canceller.v fpga_self_test.v + tb/radar_system_tb.v "${SYSTEM_RTL[@]}" # E2E integration (46 strict checks: TX, RX, USB R/W, CDC, safety, reset) run_test "System E2E (tb_system_e2e)" \ tb/tb_system_e2e_reg.vvp \ - tb/tb_system_e2e.v radar_system_top.v \ - radar_transmitter.v dac_interface_single.v plfm_chirp_controller.v \ - radar_receiver_final.v tb/ad9484_interface_400m_stub.v \ - ddc_400m.v nco_400m_enhanced.v cic_decimator_4x_enhanced.v \ - cdc_modules.v fir_lowpass.v ddc_input_interface.v \ - chirp_memory_loader_param.v latency_buffer.v \ - matched_filter_multi_segment.v matched_filter_processing_chain.v \ - range_bin_decimator.v doppler_processor.v xfft_16.v fft_engine.v \ - usb_data_interface.v usb_data_interface_ft2232h.v edge_detector.v radar_mode_controller.v \ - rx_gain_control.v cfar_ca.v mti_canceller.v fpga_self_test.v + tb/tb_system_e2e.v "${SYSTEM_RTL[@]}" # USB_MODE=1 (FT2232H production) variants of system tests run_test "System Top USB_MODE=1 (FT2232H)" \ tb/tb_system_ft2232h_reg.vvp \ -DUSB_MODE_1 \ - tb/radar_system_tb.v radar_system_top.v \ - radar_transmitter.v dac_interface_single.v plfm_chirp_controller.v \ - radar_receiver_final.v tb/ad9484_interface_400m_stub.v \ - ddc_400m.v nco_400m_enhanced.v cic_decimator_4x_enhanced.v \ - cdc_modules.v fir_lowpass.v ddc_input_interface.v \ - chirp_memory_loader_param.v latency_buffer.v \ - matched_filter_multi_segment.v matched_filter_processing_chain.v \ - range_bin_decimator.v doppler_processor.v xfft_16.v fft_engine.v \ - usb_data_interface.v usb_data_interface_ft2232h.v edge_detector.v radar_mode_controller.v \ - rx_gain_control.v cfar_ca.v mti_canceller.v fpga_self_test.v + tb/radar_system_tb.v "${SYSTEM_RTL[@]}" run_test "System E2E USB_MODE=1 (FT2232H)" \ tb/tb_system_e2e_ft2232h_reg.vvp \ -DUSB_MODE_1 \ - tb/tb_system_e2e.v radar_system_top.v \ - radar_transmitter.v dac_interface_single.v plfm_chirp_controller.v \ - radar_receiver_final.v tb/ad9484_interface_400m_stub.v \ - ddc_400m.v nco_400m_enhanced.v cic_decimator_4x_enhanced.v \ - cdc_modules.v fir_lowpass.v ddc_input_interface.v \ - chirp_memory_loader_param.v latency_buffer.v \ - matched_filter_multi_segment.v matched_filter_processing_chain.v \ - range_bin_decimator.v doppler_processor.v xfft_16.v fft_engine.v \ - usb_data_interface.v usb_data_interface_ft2232h.v edge_detector.v radar_mode_controller.v \ - rx_gain_control.v cfar_ca.v mti_canceller.v fpga_self_test.v + tb/tb_system_e2e.v "${SYSTEM_RTL[@]}" else echo " (skipped receiver golden + system top + E2E — use without --quick)" SKIP=$((SKIP + 6)) From 161e9a66e417ffc3f8b7d3af281aaedb47ac19f5 Mon Sep 17 00:00:00 2001 From: Jason <83615043+JJassonn69@users.noreply.github.com> Date: Thu, 16 Apr 2026 17:51:09 +0545 Subject: [PATCH 6/7] =?UTF-8?q?fix:=20clarify=20comments=20=E2=80=94=20AGC?= =?UTF-8?q?=20width,=20dual-USB=20docstring,=20BE=20datasheet=20ref?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 9_Firmware/9_2_FPGA/usb_data_interface.v | 4 ++-- 9_Firmware/9_3_GUI/v7/hardware.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/9_Firmware/9_2_FPGA/usb_data_interface.v b/9_Firmware/9_2_FPGA/usb_data_interface.v index 2545591..bcb0272 100644 --- a/9_Firmware/9_2_FPGA/usb_data_interface.v +++ b/9_Firmware/9_2_FPGA/usb_data_interface.v @@ -29,7 +29,7 @@ module usb_data_interface ( // FT601 Interface (Slave FIFO mode) // Data bus inout wire [31:0] ft601_data, // 32-bit bidirectional data bus - output reg [3:0] ft601_be, // Byte enable (active-HIGH per FT601 datasheet / AN_378) + output reg [3:0] ft601_be, // Byte enable (active-HIGH per DS_FT600Q-FT601Q Table 3.2) // Control signals // VESTIGIAL OUTPUTS — kept for 200T board port compatibility. @@ -374,7 +374,7 @@ always @(posedge ft601_clk_in or negedge ft601_effective_reset_n) begin // 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_saturation_count, // [19:12] 8-bit saturation count status_agc_enable, // [11] 9'd0, // [10:2] reserved status_range_mode}; // [1:0] diff --git a/9_Firmware/9_3_GUI/v7/hardware.py b/9_Firmware/9_3_GUI/v7/hardware.py index 2d85dc7..d36aa1a 100644 --- a/9_Firmware/9_3_GUI/v7/hardware.py +++ b/9_Firmware/9_3_GUI/v7/hardware.py @@ -47,8 +47,9 @@ class STM32USBInterface: Used ONLY for receiving GPS data from the MCU. - FPGA register commands are sent via FT2232H (see FT2232HConnection - from radar_protocol.py). The old send_start_flag() / send_settings() + FPGA register commands are sent via the USB data interface — either + FT2232HConnection (production) or FT601Connection (premium), both + from radar_protocol.py. The old send_start_flag() / send_settings() methods have been removed — they used an incompatible magic-packet protocol that the FPGA does not understand. """ From 76cfc71b194654111ba35f335c9c4106e56ee50c Mon Sep 17 00:00:00 2001 From: Jason <83615043+JJassonn69@users.noreply.github.com> Date: Thu, 16 Apr 2026 21:35:01 +0545 Subject: [PATCH 7/7] fix(gui): align radar parameters to FPGA truth (radar_scene.py) - Bandwidth 500 MHz -> 20 MHz, sample rate 4 MHz -> 100 MHz (DDC output) - Range formula: deramped FMCW -> matched-filter c/(2*Fs)*decimation - Velocity formula: use PRI (167 us) and chirps_per_subframe (16) - Carrier frequency: 10.525 GHz -> 10.5 GHz per radar_scene.py - Range per bin: 4.8 m -> 24 m, max range: 307 m -> 1536 m - Fix simulator target spawn range to match new coverage (50-1400 m) - Remove dead BANDWIDTH constant, add SAMPLE_RATE to V65 Tk - All 174 tests pass, ruff clean --- 9_Firmware/9_3_GUI/GUI_V65_Tk.py | 20 +++++------ 9_Firmware/9_3_GUI/test_v7.py | 30 +++++++++-------- 9_Firmware/9_3_GUI/v7/map_widget.py | 2 +- 9_Firmware/9_3_GUI/v7/models.py | 51 ++++++++++++++++------------- 9_Firmware/9_3_GUI/v7/workers.py | 4 +-- 5 files changed, 58 insertions(+), 49 deletions(-) diff --git a/9_Firmware/9_3_GUI/GUI_V65_Tk.py b/9_Firmware/9_3_GUI/GUI_V65_Tk.py index 0ecae7b..659e280 100644 --- a/9_Firmware/9_3_GUI/GUI_V65_Tk.py +++ b/9_Firmware/9_3_GUI/GUI_V65_Tk.py @@ -98,9 +98,10 @@ class DemoTarget: __slots__ = ("azimuth", "classification", "id", "range_m", "snr", "velocity") - # Physical range grid: 64 bins x ~4.8 m/bin = ~307 m max - _RANGE_PER_BIN: float = (3e8 / (2 * 500e6)) * 16 # ~4.8 m - _MAX_RANGE: float = _RANGE_PER_BIN * NUM_RANGE_BINS # ~307 m + # Physical range grid: 64 bins x ~24 m/bin = ~1536 m max + # Bin spacing = c / (2 * Fs) * decimation, where Fs = 100 MHz DDC output. + _RANGE_PER_BIN: float = (3e8 / (2 * 100e6)) * 16 # ~24 m + _MAX_RANGE: float = _RANGE_PER_BIN * NUM_RANGE_BINS # ~1536 m def __init__(self, tid: int): self.id = tid @@ -187,10 +188,10 @@ class DemoSimulator: mag = np.zeros((NUM_RANGE_BINS, NUM_DOPPLER_BINS), dtype=np.float64) det = np.zeros((NUM_RANGE_BINS, NUM_DOPPLER_BINS), dtype=np.uint8) - # Range/Doppler scaling (approximate) - range_per_bin = (3e8 / (2 * 500e6)) * 16 # ~4.8 m/bin + # Range/Doppler scaling: bin spacing = c/(2*Fs)*decimation + range_per_bin = (3e8 / (2 * 100e6)) * 16 # ~24 m/bin max_range = range_per_bin * NUM_RANGE_BINS - vel_per_bin = 1.484 # m/s per Doppler bin (from WaveformConfig) + vel_per_bin = 5.34 # m/s per Doppler bin (radar_scene.py: lam/(2*16*PRI)) for t in targets: if t.range_m > max_range or t.range_m < 0: @@ -385,7 +386,7 @@ class RadarDashboard: UPDATE_INTERVAL_MS = 100 # 10 Hz display refresh # Radar parameters used for range-axis scaling. - BANDWIDTH = 500e6 # Hz — chirp bandwidth + SAMPLE_RATE = 100e6 # Hz — DDC output I/Q rate (matched filter input) C = 3e8 # m/s — speed of light def __init__(self, root: tk.Tk, mock: bool, @@ -526,9 +527,8 @@ class RadarDashboard: def _build_display_tab(self, parent): # Compute physical axis limits - range_res = self.C / (2.0 * self.BANDWIDTH) # ~0.3 m per FFT bin - # After decimation 1024→64, each range bin = 16 FFT bins - range_per_bin = range_res * 16 + # Bin spacing = c / (2 * Fs_ddc) for matched-filter processing. + range_per_bin = self.C / (2.0 * self.SAMPLE_RATE) * 16 # ~24 m max_range = range_per_bin * NUM_RANGE_BINS doppler_bin_lo = 0 diff --git a/9_Firmware/9_3_GUI/test_v7.py b/9_Firmware/9_3_GUI/test_v7.py index 4f70ecd..636c5d4 100644 --- a/9_Firmware/9_3_GUI/test_v7.py +++ b/9_Firmware/9_3_GUI/test_v7.py @@ -65,9 +65,9 @@ class TestRadarSettings(unittest.TestCase): def test_defaults(self): s = _models().RadarSettings() - self.assertEqual(s.system_frequency, 10e9) - self.assertEqual(s.coverage_radius, 50000) - self.assertEqual(s.max_distance, 50000) + self.assertEqual(s.system_frequency, 10.5e9) + self.assertEqual(s.coverage_radius, 1536) + self.assertEqual(s.max_distance, 1536) class TestGPSData(unittest.TestCase): @@ -425,26 +425,28 @@ class TestWaveformConfig(unittest.TestCase): def test_defaults(self): from v7.models import WaveformConfig wc = WaveformConfig() - self.assertEqual(wc.sample_rate_hz, 4e6) - self.assertEqual(wc.bandwidth_hz, 500e6) - self.assertEqual(wc.chirp_duration_s, 300e-6) - self.assertEqual(wc.center_freq_hz, 10.525e9) + self.assertEqual(wc.sample_rate_hz, 100e6) + self.assertEqual(wc.bandwidth_hz, 20e6) + self.assertEqual(wc.chirp_duration_s, 30e-6) + self.assertEqual(wc.pri_s, 167e-6) + self.assertEqual(wc.center_freq_hz, 10.5e9) self.assertEqual(wc.n_range_bins, 64) self.assertEqual(wc.n_doppler_bins, 32) + self.assertEqual(wc.chirps_per_subframe, 16) self.assertEqual(wc.fft_size, 1024) self.assertEqual(wc.decimation_factor, 16) def test_range_resolution(self): - """range_resolution_m should be ~5.62 m/bin with ADI defaults.""" + """range_resolution_m should be ~23.98 m/bin (matched filter, 100 MSPS).""" from v7.models import WaveformConfig wc = WaveformConfig() - self.assertAlmostEqual(wc.range_resolution_m, 5.621, places=1) + self.assertAlmostEqual(wc.range_resolution_m, 23.983, places=1) def test_velocity_resolution(self): - """velocity_resolution_mps should be ~1.484 m/s/bin.""" + """velocity_resolution_mps should be ~5.34 m/s/bin (PRI=167us, 16 chirps).""" from v7.models import WaveformConfig wc = WaveformConfig() - self.assertAlmostEqual(wc.velocity_resolution_mps, 1.484, places=2) + self.assertAlmostEqual(wc.velocity_resolution_mps, 5.343, places=1) def test_max_range(self): """max_range_m = range_resolution * n_range_bins.""" @@ -466,7 +468,7 @@ class TestWaveformConfig(unittest.TestCase): """Non-default parameters correctly change derived values.""" from v7.models import WaveformConfig wc1 = WaveformConfig() - wc2 = WaveformConfig(bandwidth_hz=1e9) # double BW → halve range res + wc2 = WaveformConfig(sample_rate_hz=200e6) # double Fs → halve range bin self.assertAlmostEqual(wc2.range_resolution_m, wc1.range_resolution_m / 2, places=2) def test_zero_center_freq_velocity(self): @@ -925,9 +927,9 @@ class TestExtractTargetsFromFrame(unittest.TestCase): """Detection at range bin 10 → range = 10 * range_resolution.""" from v7.processing import extract_targets_from_frame frame = self._make_frame(det_cells=[(10, 16)]) # dbin=16 = center → vel=0 - targets = extract_targets_from_frame(frame, range_resolution=5.621) + targets = extract_targets_from_frame(frame, range_resolution=23.983) self.assertEqual(len(targets), 1) - self.assertAlmostEqual(targets[0].range, 10 * 5.621, places=2) + self.assertAlmostEqual(targets[0].range, 10 * 23.983, places=1) self.assertAlmostEqual(targets[0].velocity, 0.0, places=2) def test_velocity_sign(self): diff --git a/9_Firmware/9_3_GUI/v7/map_widget.py b/9_Firmware/9_3_GUI/v7/map_widget.py index fa0fcb1..951418a 100644 --- a/9_Firmware/9_3_GUI/v7/map_widget.py +++ b/9_Firmware/9_3_GUI/v7/map_widget.py @@ -98,7 +98,7 @@ class RadarMapWidget(QWidget): ) self._targets: list[RadarTarget] = [] self._pending_targets: list[RadarTarget] | None = None - self._coverage_radius = 50_000 # metres + self._coverage_radius = 1_536 # metres (64 bins x ~24 m/bin) self._tile_server = TileServer.OPENSTREETMAP self._show_coverage = True self._show_trails = False diff --git a/9_Firmware/9_3_GUI/v7/models.py b/9_Firmware/9_3_GUI/v7/models.py index c4b277c..07952d4 100644 --- a/9_Firmware/9_3_GUI/v7/models.py +++ b/9_Firmware/9_3_GUI/v7/models.py @@ -108,12 +108,12 @@ class RadarSettings: range_resolution and velocity_resolution should be calibrated to the actual waveform parameters. """ - system_frequency: float = 10e9 # Hz (carrier, used for velocity calc) - range_resolution: float = 781.25 # Meters per range bin (default: 50km/64) - velocity_resolution: float = 1.0 # m/s per Doppler bin (calibrate to waveform) - max_distance: float = 50000 # Max detection range (m) - map_size: float = 50000 # Map display size (m) - coverage_radius: float = 50000 # Map coverage radius (m) + system_frequency: float = 10.5e9 # Hz (carrier, used for velocity calc) + range_resolution: float = 24.0 # Meters per range bin (c/(2*Fs)*decim) + velocity_resolution: float = 1.0 # m/s per Doppler bin (calibrate to waveform) + max_distance: float = 1536 # Max detection range (m) + map_size: float = 2000 # Map display size (m) + coverage_radius: float = 1536 # Map coverage radius (m) @dataclass @@ -199,39 +199,46 @@ class WaveformConfig: Encapsulates the radar waveform so that range/velocity resolution can be derived automatically instead of hardcoded in RadarSettings. - Defaults match the ADI CN0566 Phaser capture parameters used in - the golden_reference cosim (4 MSPS, 500 MHz BW, 300 us chirp). + Defaults match the AERIS-10 production system parameters from + radar_scene.py / plfm_chirp_controller.v: + 100 MSPS DDC output, 20 MHz chirp BW, 30 us long chirp, + 167 us long-chirp PRI, X-band 10.5 GHz carrier. """ - sample_rate_hz: float = 4e6 # ADC sample rate - bandwidth_hz: float = 500e6 # Chirp bandwidth - chirp_duration_s: float = 300e-6 # Chirp ramp time - center_freq_hz: float = 10.525e9 # Carrier frequency + sample_rate_hz: float = 100e6 # DDC output I/Q rate (matched filter input) + bandwidth_hz: float = 20e6 # Chirp bandwidth (not used in range calc; + # retained for time-bandwidth product / display) + chirp_duration_s: float = 30e-6 # Long chirp ramp time + pri_s: float = 167e-6 # Pulse repetition interval (chirp + listen) + center_freq_hz: float = 10.5e9 # Carrier frequency (radar_scene.py: F_CARRIER) n_range_bins: int = 64 # After decimation - n_doppler_bins: int = 32 # After Doppler FFT + n_doppler_bins: int = 32 # Total Doppler bins (2 sub-frames x 16) + chirps_per_subframe: int = 16 # Chirps in one Doppler sub-frame fft_size: int = 1024 # Pre-decimation FFT length decimation_factor: int = 16 # 1024 → 64 @property def range_resolution_m(self) -> float: - """Meters per decimated range bin (FMCW deramped baseband). + """Meters per decimated range bin (matched-filter pulse compression). - For deramped FMCW: bin spacing = c * Fs * T / (2 * N_FFT * BW). - After decimation the bin spacing grows by *decimation_factor*. + For FFT-based matched filtering, each IFFT output bin spans + c / (2 * Fs) in range, where Fs is the I/Q sample rate at the + matched-filter input (DDC output). After decimation the bin + spacing grows by *decimation_factor*. """ c = 299_792_458.0 - raw_bin = ( - c * self.sample_rate_hz * self.chirp_duration_s - / (2.0 * self.fft_size * self.bandwidth_hz) - ) + raw_bin = c / (2.0 * self.sample_rate_hz) return raw_bin * self.decimation_factor @property def velocity_resolution_mps(self) -> float: - """m/s per Doppler bin. lambda / (2 * n_doppler * chirp_duration).""" + """m/s per Doppler bin. + + lambda / (2 * chirps_per_subframe * PRI), matching radar_scene.py. + """ c = 299_792_458.0 wavelength = c / self.center_freq_hz - return wavelength / (2.0 * self.n_doppler_bins * self.chirp_duration_s) + return wavelength / (2.0 * self.chirps_per_subframe * self.pri_s) @property def max_range_m(self) -> float: diff --git a/9_Firmware/9_3_GUI/v7/workers.py b/9_Firmware/9_3_GUI/v7/workers.py index c29f3bd..6bf115f 100644 --- a/9_Firmware/9_3_GUI/v7/workers.py +++ b/9_Firmware/9_3_GUI/v7/workers.py @@ -334,7 +334,7 @@ class TargetSimulator(QObject): self._add_random_target() def _add_random_target(self): - range_m = random.uniform(5000, 40000) + range_m = random.uniform(50, 1400) azimuth = random.uniform(0, 360) velocity = random.uniform(-100, 100) elevation = random.uniform(-5, 45) @@ -368,7 +368,7 @@ class TargetSimulator(QObject): for t in self._targets: new_range = t.range - t.velocity * 0.5 - if new_range < 500 or new_range > 50000: + if new_range < 10 or new_range > 1536: continue # target exits coverage — drop it new_vel = max(-150, min(150, t.velocity + random.uniform(-2, 2)))