From 408f4d126fbabcbca48fd9220a6477ca949bb45a Mon Sep 17 00:00:00 2001 From: Jason <83615043+JJassonn69@users.noreply.github.com> Date: Tue, 7 Apr 2026 19:22:16 +0300 Subject: [PATCH] feat(usb): add FT2232H USB 2.0 interface for 50T production board Replace FT601 (USB 3.0, 32-bit) with FT2232H (USB 2.0, 8-bit) on the 50T production board per updated Eagle schematic (commit 0db0e7b). USB 3.0 via FT601 remains available on the 200T premium board. RTL changes: - Add usb_data_interface_ft2232h.v: 245 Sync FIFO interface with toggle CDC (3-stage) for reliable 100MHz->60MHz clock domain crossing, mux-based byte serialization for 11-byte data packets, 26-byte status packets, and 4-byte sequential command read FSM - Add USB_MODE parameter to radar_system_top.v with generate block: USB_MODE=0 selects FT601 (200T), USB_MODE=1 selects FT2232H (50T) - Wire FT2232H ports in radar_system_top_50t.v with USB_MODE=1 override, connect ft_clkout to shared clock input port - Add post-DSP retiming register in ddc_400m.v to fix marginal 400MHz timing path (WNS improved from +0.070ns to +0.088ns) Constraints: - Add FT2232H pin assignments for all 15 signals on Bank 35 (LVCMOS33) - Add 60MHz ft_clkout clock constraint (16.667ns) on MRCC N-type pin C4 - Add CLOCK_DEDICATED_ROUTE FALSE for N-type MRCC workaround - Add CDC false paths between ft_clkout and clk_100m/clk_120m_dac Build scripts: - Add PLIO-9 DRC demotion and CLOCK_DEDICATED_ROUTE property in build_50t.tcl - Add usb_data_interface_ft2232h.v to build_200t.tcl explicit file list Python host: - Add FT2232HConnection class using pyftdi SyncFIFO (VID 0x0403:0x6010) - Add compact 11-byte packet parser for FT2232H data packets - Update RadarAcquisition to support both FT601 and FT2232H connections Test results: - iverilog regression: 23/23 PASS - Vivado Build 15 (XC7A50T): WNS=+0.088ns, WHS=+0.059ns, 0 violations - DSP48E1: 112/120 (93.3%), LUTs: 10,060/32,600 (30.9%) --- .../9_2_FPGA/constraints/xc7a50t_ftg256.xdc | 104 +++- 9_Firmware/9_2_FPGA/ddc_400m.v | 58 +- 9_Firmware/9_2_FPGA/radar_system_top.v | 209 +++++-- 9_Firmware/9_2_FPGA/radar_system_top_50t.v | 45 +- .../9_2_FPGA/scripts/200t/build_200t.tcl | 1 + 9_Firmware/9_2_FPGA/scripts/50t/build_50t.tcl | 20 +- 9_Firmware/9_2_FPGA/tb/radar_system_tb.v | 2 +- .../9_2_FPGA/usb_data_interface_ft2232h.v | 535 ++++++++++++++++++ 9_Firmware/9_3_GUI/radar_protocol.py | 281 ++++++++- 9 files changed, 1119 insertions(+), 136 deletions(-) create mode 100644 9_Firmware/9_2_FPGA/usb_data_interface_ft2232h.v diff --git a/9_Firmware/9_2_FPGA/constraints/xc7a50t_ftg256.xdc b/9_Firmware/9_2_FPGA/constraints/xc7a50t_ftg256.xdc index 43c922c..1f1ee5f 100644 --- a/9_Firmware/9_2_FPGA/constraints/xc7a50t_ftg256.xdc +++ b/9_Firmware/9_2_FPGA/constraints/xc7a50t_ftg256.xdc @@ -15,7 +15,7 @@ # Bank 14: VCCO = 2.5V (ADC LVDS_25 data — placer-enforced; adc_pwdn as LVCMOS25) # Bank 15: VCCO = 3.3V (DAC, clocks, STM32 SPI 3.3V side, DIG bus, mixer) # Bank 34: VCCO = 1.8V (ADAR1000 beamformer control, SPI 1.8V side) -# Bank 35: VCCO = 3.3V (unused — no signal connections) +# Bank 35: VCCO = 3.3V (FT2232H USB 2.0 FIFO — 15 signals) # # DRC Fix History: # - PLIO-9: Moved clk_120m_dac from C13 (N-type) to D13 (P-type MRCC). @@ -25,8 +25,12 @@ # IBUFDS input buffers are VCCO-independent. BIVC-1 also waived via # set_property SEVERITY in the build script as an additional safety net. # in the build script. adc_pwdn (LVCMOS25) coexists in the same bank. -# - UCIO/NSTD: 118 unconstrained ports (FT601 unwired, status/debug outputs -# have no physical pins). Handled with SEVERITY demotion + default IOSTANDARD. +# - UCIO/NSTD: Unconstrained ports (FT601 ports inactive with USB_MODE=1, +# status/debug outputs have no physical pins). Handled with SEVERITY +# demotion + default IOSTANDARD. +# - PLIO-9: FT2232H CLKOUT routed to C4 (IO_L12N_T1_MRCC_35, N-type). +# Clock inputs normally use P-type MRCC pins, but IBUFG works correctly +# on N-type. Demote PLIO-9 to warning in build script. # ============================================================================ # ============================================================================ @@ -46,10 +50,10 @@ set_property CONFIG_VOLTAGE 3.3 [current_design] # and one LVCMOS33 output, this is safe to demote to a warning. # → Applied in build_50t_test.tcl: set_property SEVERITY {Warning} [get_drc_checks BIVC-1] # -# NSTD-1 / UCIO-1: Unconstrained ports — FT601 USB (unwired on this board), -# dac_clk (DAC clock comes from AD9523, not FPGA), and all status/debug -# outputs (no physical pins available). These ports are present in the -# shared RTL but have no connections on the 50T board. +# NSTD-1 / UCIO-1: Unconstrained ports — FT601 USB ports (inactive with +# USB_MODE=1 generate block), dac_clk (DAC clock comes from AD9523, not FPGA), +# and all status/debug outputs (no physical pins available). These ports are +# present in the shared RTL but have no connections on the 50T board. # → Applied in build_50t_test.tcl: set_property SEVERITY {Warning} [get_drc_checks {NSTD-1 UCIO-1}] # ============================================================================ @@ -90,11 +94,20 @@ create_clock -name adc_dco_p -period 2.5 [get_ports {adc_dco_p}] set_input_jitter [get_clocks adc_dco_p] 0.05 # -------------------------------------------------------------------------- -# FT601 Clock — COMMENTED OUT: FT601 (U6) is placed in schematic but has -# zero net connections. No physical clock pin exists on this board. +# FT2232H 60 MHz CLKOUT (Bank 35, MRCC pin C4) # -------------------------------------------------------------------------- -# create_clock -name ft601_clk_in -period 10.0 [get_ports {ft601_clk_in}] -# set_input_jitter [get_clocks ft601_clk_in] 0.1 +# The FT2232H provides a 60 MHz clock in 245 Synchronous FIFO mode. +# Pin C4 is IO_L12N_T1_MRCC_35 (N-type of MRCC pair). Vivado requires +# CLOCK_DEDICATED_ROUTE FALSE for clock inputs on N-type MRCC pins +# (Place 30-876). The schematic routes CLKOUT to C4; this cannot be +# changed without a board respin. The clock still uses an IBUFG and +# reaches the clock network — the constraint only disables the DRC check. +set_property PACKAGE_PIN C4 [get_ports {ft_clkout}] +set_property IOSTANDARD LVCMOS33 [get_ports {ft_clkout}] +create_clock -name ft_clkout -period 16.667 [get_ports {ft_clkout}] +set_input_jitter [get_clocks ft_clkout] 0.2 +# N-type MRCC pin requires dedicated route override (Place 30-876) +set_property CLOCK_DEDICATED_ROUTE FALSE [get_nets {ft_clkout_IBUF}] # ============================================================================ # RESET (Active-Low) @@ -262,26 +275,55 @@ set_input_delay -clock [get_clocks adc_dco_p] -max 1.0 -clock_fall [get_ports {a set_input_delay -clock [get_clocks adc_dco_p] -min 0.2 -clock_fall [get_ports {adc_d_p[*]}] -add_delay # ============================================================================ -# FT601 USB 3.0 INTERFACE — ACTIVE: NO PHYSICAL CONNECTIONS +# FT2232H USB 2.0 INTERFACE (Bank 35, VCCO=3.3V) # ============================================================================ -# The FT601 chip (U6, FT601Q-B-T) is placed in the Eagle schematic but has -# ZERO net connections — no signals are routed between it and the FPGA. -# Bank 35 (which would logically host FT601 signals) has no signal pins -# connected, only VCCO_35 power. +# FT2232H (U6) Channel A in 245 Synchronous FIFO mode. +# All signals are direct connections to FPGA Bank 35 (LVCMOS33). +# Pin mapping extracted from Eagle schematic (RADAR_Main_Board.sch). # -# ALL FT601 constraints are commented out. The RTL module usb_data_interface.v -# instantiates the FT601 interface, but it cannot function without physical -# pin assignments. To use USB, the schematic must be updated to wire the -# FT601 to FPGA Bank 35 pins, and then these constraints can be populated. -# -# Ports affected (from radar_system_top.v): -# ft601_data[31:0], ft601_be[1:0], ft601_txe_n, ft601_rxf_n, ft601_txe, -# ft601_rxf, ft601_wr_n, ft601_rd_n, ft601_oe_n, ft601_siwu_n, -# ft601_srb[1:0], ft601_swb[1:0], ft601_clk_out, ft601_clk_in -# -# TODO: Wire FT601 in schematic, then assign pins here. +# The FT2232H replaces the previously-unwired FT601 on the 50T production +# board. The 200T dev board retains FT601 USB 3.0 (32-bit). # ============================================================================ +# 8-bit bidirectional data bus (ADBUS0–ADBUS7) +set_property PACKAGE_PIN K1 [get_ports {ft_data[0]}] ;# ADBUS0 → IO_L22P_T3_35 +set_property PACKAGE_PIN J3 [get_ports {ft_data[1]}] ;# ADBUS1 → IO_L21P_T3_DQS_35 +set_property PACKAGE_PIN H3 [get_ports {ft_data[2]}] ;# ADBUS2 → IO_L21N_T3_DQS_35 +set_property PACKAGE_PIN G4 [get_ports {ft_data[3]}] ;# ADBUS3 → IO_L16N_T2_35 +set_property PACKAGE_PIN F2 [get_ports {ft_data[4]}] ;# ADBUS4 → IO_L15P_T2_DQS_35 +set_property PACKAGE_PIN D1 [get_ports {ft_data[5]}] ;# ADBUS5 → IO_L10N_T1_AD15N_35 +set_property PACKAGE_PIN C3 [get_ports {ft_data[6]}] ;# ADBUS6 → IO_L7P_T1_AD6P_35 +set_property PACKAGE_PIN C1 [get_ports {ft_data[7]}] ;# ADBUS7 → IO_L9P_T1_DQS_AD7P_35 +set_property IOSTANDARD LVCMOS33 [get_ports {ft_data[*]}] + +# Control signals (active low where noted) +set_property PACKAGE_PIN A2 [get_ports {ft_rxf_n}] ;# ACBUS0 → IO_L8N_T1_AD14N_35 +set_property PACKAGE_PIN B2 [get_ports {ft_txe_n}] ;# ACBUS1 → IO_L8P_T1_AD14P_35 +set_property PACKAGE_PIN A3 [get_ports {ft_rd_n}] ;# ACBUS2 → IO_L4N_T0_35 +set_property PACKAGE_PIN A4 [get_ports {ft_wr_n}] ;# ACBUS3 → IO_L3N_T0_DQS_AD5N_35 +set_property PACKAGE_PIN A5 [get_ports {ft_siwu}] ;# ACBUS4 → IO_L3P_T0_DQS_AD5P_35 +set_property PACKAGE_PIN B7 [get_ports {ft_oe_n}] ;# ACBUS6 → IO_L1P_T0_AD4P_35 +set_property IOSTANDARD LVCMOS33 [get_ports {ft_rxf_n}] +set_property IOSTANDARD LVCMOS33 [get_ports {ft_txe_n}] +set_property IOSTANDARD LVCMOS33 [get_ports {ft_rd_n}] +set_property IOSTANDARD LVCMOS33 [get_ports {ft_wr_n}] +set_property IOSTANDARD LVCMOS33 [get_ports {ft_siwu}] +set_property IOSTANDARD LVCMOS33 [get_ports {ft_oe_n}] + +# Output timing: SLEW FAST + DRIVE 8 for FT2232H signals +set_property SLEW FAST [get_ports {ft_rd_n}] +set_property SLEW FAST [get_ports {ft_wr_n}] +set_property SLEW FAST [get_ports {ft_oe_n}] +set_property SLEW FAST [get_ports {ft_siwu}] +set_property SLEW FAST [get_ports {ft_data[*]}] +set_property DRIVE 8 [get_ports {ft_rd_n}] +set_property DRIVE 8 [get_ports {ft_wr_n}] +set_property DRIVE 8 [get_ports {ft_oe_n}] +set_property DRIVE 8 [get_ports {ft_siwu}] +set_property DRIVE 8 [get_ports {ft_data[*]}] + +# ft_clkout constrained above in CLOCK CONSTRAINTS section (C4, 60 MHz) + # ============================================================================ # STATUS / DEBUG OUTPUTS — NO PHYSICAL CONNECTIONS # ============================================================================ @@ -333,7 +375,13 @@ set_false_path -from [get_clocks adc_dco_p] -to [get_clocks clk_100m] set_false_path -from [get_clocks clk_100m] -to [get_clocks clk_120m_dac] set_false_path -from [get_clocks clk_120m_dac] -to [get_clocks clk_100m] -# FT601 CDC paths removed — no ft601_clk_in clock defined (chip unwired) +# FT2232H CDC: clk_100m ↔ ft_clkout (60 MHz), toggle CDC in RTL +set_false_path -from [get_clocks clk_100m] -to [get_clocks ft_clkout] +set_false_path -from [get_clocks ft_clkout] -to [get_clocks clk_100m] + +# FT2232H CDC: clk_120m_dac ↔ ft_clkout (no direct crossing, but belt-and-suspenders) +set_false_path -from [get_clocks clk_120m_dac] -to [get_clocks ft_clkout] +set_false_path -from [get_clocks ft_clkout] -to [get_clocks clk_120m_dac] # ============================================================================ # PHYSICAL CONSTRAINTS diff --git a/9_Firmware/9_2_FPGA/ddc_400m.v b/9_Firmware/9_2_FPGA/ddc_400m.v index dea2f4d..c2bee9a 100644 --- a/9_Firmware/9_2_FPGA/ddc_400m.v +++ b/9_Firmware/9_2_FPGA/ddc_400m.v @@ -103,13 +103,17 @@ reg [7:0] signal_power_i, signal_power_q; // Internal mixing signals // DSP48E1 with AREG=1, BREG=1, MREG=1, PREG=1 handles all internal pipelining -// Latency: 3 cycles (1 for AREG/BREG, 1 for MREG, 1 for PREG) +// Latency: 4 cycles (1 for AREG/BREG, 1 for MREG, 1 for PREG, 1 for post-DSP retiming) wire signed [MIXER_WIDTH-1:0] adc_signed_w; reg signed [MIXER_WIDTH + NCO_WIDTH -1:0] mixed_i, mixed_q; reg mixed_valid; reg mixer_overflow_i, mixer_overflow_q; -// Pipeline valid tracking: 3-stage shift register to match DSP48E1 AREG+MREG+PREG latency -reg [2:0] dsp_valid_pipe; +// Pipeline valid tracking: 4-stage shift register (3 for DSP48E1 + 1 for post-DSP retiming) +reg [3:0] dsp_valid_pipe; +// Post-DSP retiming registers — breaks DSP48E1 CLK→P to fabric timing path +// This extra pipeline stage absorbs the 1.866ns DSP output prop delay + routing, +// ensuring WNS > 0 at 400 MHz regardless of placement seed +(* DONT_TOUCH = "TRUE" *) reg signed [MIXER_WIDTH+NCO_WIDTH-1:0] mult_i_retimed, mult_q_retimed; // Output stage registers reg signed [17:0] baseband_i_reg, baseband_q_reg; @@ -219,12 +223,12 @@ nco_400m_enhanced nco_core ( assign adc_signed_w = {1'b0, adc_data, {(MIXER_WIDTH-ADC_WIDTH-1){1'b0}}} - {1'b0, {ADC_WIDTH{1'b1}}, {(MIXER_WIDTH-ADC_WIDTH-1){1'b0}}} / 2; -// Valid pipeline: 3-stage shift register matching DSP48E1 AREG+MREG+PREG latency +// Valid pipeline: 4-stage shift register (3 for DSP48E1 AREG+MREG+PREG + 1 for retiming) always @(posedge clk_400m or negedge reset_n_400m) begin if (!reset_n_400m) begin - dsp_valid_pipe <= 3'b000; + dsp_valid_pipe <= 4'b0000; end else begin - dsp_valid_pipe <= {dsp_valid_pipe[1:0], (nco_ready && adc_data_valid_i && adc_data_valid_q)}; + dsp_valid_pipe <= {dsp_valid_pipe[2:0], (nco_ready && adc_data_valid_i && adc_data_valid_q)}; end end @@ -271,6 +275,17 @@ always @(posedge clk_400m or negedge reset_n_400m) begin end end +// Stage 4: Post-DSP retiming register (matches synthesis path) +always @(posedge clk_400m or negedge reset_n_400m) begin + if (!reset_n_400m) begin + mult_i_retimed <= 0; + mult_q_retimed <= 0; + end else begin + mult_i_retimed <= mult_i_reg; + mult_q_retimed <= mult_q_reg; + end +end + `else // ---- Direct DSP48E1 instantiation for Vivado synthesis ---- // This guarantees AREG/BREG/MREG are used, achieving timing closure at 400 MHz @@ -448,6 +463,19 @@ DSP48E1 #( wire signed [MIXER_WIDTH+NCO_WIDTH-1:0] mult_i_reg = dsp_p_i[MIXER_WIDTH+NCO_WIDTH-1:0]; wire signed [MIXER_WIDTH+NCO_WIDTH-1:0] mult_q_reg = dsp_p_q[MIXER_WIDTH+NCO_WIDTH-1:0]; +// Stage 4: Post-DSP retiming register — breaks DSP48E1 CLK→P to fabric path +// Without this, the DSP output prop delay (1.866ns) + routing (0.515ns) exceeds +// the 2.500ns clock period at slow process corner +always @(posedge clk_400m or negedge reset_n_400m) begin + if (!reset_n_400m) begin + mult_i_retimed <= 0; + mult_q_retimed <= 0; + end else begin + mult_i_retimed <= mult_i_reg; + mult_q_retimed <= mult_q_reg; + end +end + `endif // ============================================================================ @@ -464,7 +492,7 @@ always @(posedge clk_400m or negedge reset_n_400m) begin mixer_overflow_q <= 0; saturation_count <= 0; overflow_detected <= 0; - end else if (dsp_valid_pipe[2]) begin + end else if (dsp_valid_pipe[3]) begin // Force saturation for testing (applied after DSP output, not on input path) if (force_saturation_sync) begin mixed_i <= 34'h1FFFFFFFF; @@ -472,15 +500,15 @@ always @(posedge clk_400m or negedge reset_n_400m) begin mixer_overflow_i <= 1'b1; mixer_overflow_q <= 1'b1; end else begin - // Normal path: take DSP48E1 multiply result - mixed_i <= mult_i_reg; - mixed_q <= mult_q_reg; + // Normal path: take retimed DSP48E1 multiply result + mixed_i <= mult_i_retimed; + mixed_q <= mult_q_retimed; - // Overflow detection on current cycle's multiply result - mixer_overflow_i <= (mult_i_reg > (2**(MIXER_WIDTH+NCO_WIDTH-2)-1)) || - (mult_i_reg < -(2**(MIXER_WIDTH+NCO_WIDTH-2))); - mixer_overflow_q <= (mult_q_reg > (2**(MIXER_WIDTH+NCO_WIDTH-2)-1)) || - (mult_q_reg < -(2**(MIXER_WIDTH+NCO_WIDTH-2))); + // Overflow detection on retimed multiply result + mixer_overflow_i <= (mult_i_retimed > (2**(MIXER_WIDTH+NCO_WIDTH-2)-1)) || + (mult_i_retimed < -(2**(MIXER_WIDTH+NCO_WIDTH-2))); + mixer_overflow_q <= (mult_q_retimed > (2**(MIXER_WIDTH+NCO_WIDTH-2)-1)) || + (mult_q_retimed < -(2**(MIXER_WIDTH+NCO_WIDTH-2))); end mixed_valid <= 1; diff --git a/9_Firmware/9_2_FPGA/radar_system_top.v b/9_Firmware/9_2_FPGA/radar_system_top.v index bdb9102..9d6686e 100644 --- a/9_Firmware/9_2_FPGA/radar_system_top.v +++ b/9_Firmware/9_2_FPGA/radar_system_top.v @@ -7,12 +7,16 @@ * Integrates: * - Radar Transmitter (PLFM chirp generation) * - Radar Receiver (ADC interface, DDC, matched filtering, Doppler processing) - * - USB Data Interface (FT601 for high-speed data transfer) + * - USB Data Interface (FT601 USB 3.0 or FT2232H USB 2.0, selected by USB_MODE) * * Clock domains: * - clk_100m: System clock (100MHz) * - clk_120m_dac: DAC clock (120MHz) - * - ft601_clk: FT601 interface clock (100MHz from FT601) + * - ft601_clk: USB interface clock (100MHz FT601 or 60MHz FT2232H) + * + * USB_MODE parameter: + * 0 = FT601 (32-bit, USB 3.0) — 200T premium board + * 1 = FT2232H (8-bit, USB 2.0) — 50T production board */ module radar_system_top ( @@ -93,9 +97,19 @@ module radar_system_top ( input wire [1:0] ft601_srb, // Selected read buffer input wire [1:0] ft601_swb, // Selected write buffer - // Clock output (optional) + // Clock output (optional, FT601 only — not used for FT2232H) output wire ft601_clk_out, + // ========== FT2232H USB 2.0 INTERFACE (USB_MODE=1) ========== + // 8-bit bidirectional data bus (245 Synchronous FIFO mode, Channel A) + inout wire [7:0] ft_data, // 8-bit bidirectional data bus + input wire ft_rxf_n, // RX FIFO not empty (active low) + input wire ft_txe_n, // TX FIFO not full (active low) + output wire ft_rd_n, // Read strobe (active low) + output wire ft_wr_n, // Write strobe (active low) + output wire ft_oe_n, // Output enable / bus direction + output wire ft_siwu, // Send Immediate / WakeUp + // ========== STATUS OUTPUTS ========== // Beam position tracking @@ -122,6 +136,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) // ============================================================================ // INTERNAL SIGNALS @@ -667,67 +682,143 @@ assign usb_detect_flag = rx_detect_flag; assign usb_detect_valid = rx_detect_valid; // ============================================================================ -// USB DATA INTERFACE INSTANTIATION +// USB DATA INTERFACE INSTANTIATION (parametric: FT601 or FT2232H) // ============================================================================ -usb_data_interface usb_inst ( - .clk(clk_100m_buf), - .reset_n(sys_reset_n), - .ft601_reset_n(sys_reset_ft601_n), // FT601-domain synchronized reset - - // Radar data inputs - .range_profile(usb_range_profile), - .range_valid(usb_range_valid), - .doppler_real(usb_doppler_real), - .doppler_imag(usb_doppler_imag), - .doppler_valid(usb_doppler_valid), - .cfar_detection(usb_detect_flag), - .cfar_valid(usb_detect_valid), - - // FT601 Interface - .ft601_data(ft601_data), - .ft601_be(ft601_be), - .ft601_txe_n(ft601_txe_n), - .ft601_rxf_n(ft601_rxf_n), - .ft601_txe(ft601_txe), - .ft601_rxf(ft601_rxf), - .ft601_wr_n(ft601_wr_n), - .ft601_rd_n(ft601_rd_n), - .ft601_oe_n(ft601_oe_n), - .ft601_siwu_n(ft601_siwu_n), - .ft601_srb(ft601_srb), - .ft601_swb(ft601_swb), - .ft601_clk_out(ft601_clk_out), - .ft601_clk_in(ft601_clk_buf), - - // Host command outputs (Gap 4: USB Read Path) - .cmd_data(usb_cmd_data), - .cmd_valid(usb_cmd_valid), - .cmd_opcode(usb_cmd_opcode), - .cmd_addr(usb_cmd_addr), - .cmd_value(usb_cmd_value), +generate +if (USB_MODE == 0) begin : gen_ft601 + // ---- FT601 USB 3.0 (32-bit, 200T premium board) ---- + usb_data_interface usb_inst ( + .clk(clk_100m_buf), + .reset_n(sys_reset_n), + .ft601_reset_n(sys_reset_ft601_n), + + // Radar data inputs + .range_profile(usb_range_profile), + .range_valid(usb_range_valid), + .doppler_real(usb_doppler_real), + .doppler_imag(usb_doppler_imag), + .doppler_valid(usb_doppler_valid), + .cfar_detection(usb_detect_flag), + .cfar_valid(usb_detect_valid), + + // FT601 Interface + .ft601_data(ft601_data), + .ft601_be(ft601_be), + .ft601_txe_n(ft601_txe_n), + .ft601_rxf_n(ft601_rxf_n), + .ft601_txe(ft601_txe), + .ft601_rxf(ft601_rxf), + .ft601_wr_n(ft601_wr_n), + .ft601_rd_n(ft601_rd_n), + .ft601_oe_n(ft601_oe_n), + .ft601_siwu_n(ft601_siwu_n), + .ft601_srb(ft601_srb), + .ft601_swb(ft601_swb), + .ft601_clk_out(ft601_clk_out), + .ft601_clk_in(ft601_clk_buf), + + // Host command outputs + .cmd_data(usb_cmd_data), + .cmd_valid(usb_cmd_valid), + .cmd_opcode(usb_cmd_opcode), + .cmd_addr(usb_cmd_addr), + .cmd_value(usb_cmd_value), - // Gap 2: Stream control (clk_100m domain, CDC'd inside usb_data_interface) - .stream_control(host_stream_control), + // Stream control + .stream_control(host_stream_control), - // Gap 2: Status readback inputs - .status_request(host_status_request), - .status_cfar_threshold(host_detect_threshold), - .status_stream_ctrl(host_stream_control), - .status_radar_mode(host_radar_mode), - .status_long_chirp(host_long_chirp_cycles), - .status_long_listen(host_long_listen_cycles), - .status_guard(host_guard_cycles), - .status_short_chirp(host_short_chirp_cycles), - .status_short_listen(host_short_listen_cycles), - .status_chirps_per_elev(host_chirps_per_elev), - .status_range_mode(host_range_mode), + // Status readback inputs + .status_request(host_status_request), + .status_cfar_threshold(host_detect_threshold), + .status_stream_ctrl(host_stream_control), + .status_radar_mode(host_radar_mode), + .status_long_chirp(host_long_chirp_cycles), + .status_long_listen(host_long_listen_cycles), + .status_guard(host_guard_cycles), + .status_short_chirp(host_short_chirp_cycles), + .status_short_listen(host_short_listen_cycles), + .status_chirps_per_elev(host_chirps_per_elev), + .status_range_mode(host_range_mode), - // Self-test status readback - .status_self_test_flags(self_test_flags_latched), - .status_self_test_detail(self_test_detail_latched), - .status_self_test_busy(self_test_busy) -); + // Self-test status readback + .status_self_test_flags(self_test_flags_latched), + .status_self_test_detail(self_test_detail_latched), + .status_self_test_busy(self_test_busy) + ); + + // FT2232H ports unused in FT601 mode — tie off + assign ft_rd_n = 1'b1; + assign ft_wr_n = 1'b1; + assign ft_oe_n = 1'b1; + assign ft_siwu = 1'b0; + +end else begin : gen_ft2232h + // ---- FT2232H USB 2.0 (8-bit, 50T production board) ---- + usb_data_interface_ft2232h usb_inst ( + .clk(clk_100m_buf), + .reset_n(sys_reset_n), + .ft_reset_n(sys_reset_ft601_n), // Reuse same synchronized reset + + // Radar data inputs + .range_profile(usb_range_profile), + .range_valid(usb_range_valid), + .doppler_real(usb_doppler_real), + .doppler_imag(usb_doppler_imag), + .doppler_valid(usb_doppler_valid), + .cfar_detection(usb_detect_flag), + .cfar_valid(usb_detect_valid), + + // FT2232H Interface + .ft_data(ft_data), + .ft_rxf_n(ft_rxf_n), + .ft_txe_n(ft_txe_n), + .ft_rd_n(ft_rd_n), + .ft_wr_n(ft_wr_n), + .ft_oe_n(ft_oe_n), + .ft_siwu(ft_siwu), + .ft_clk(ft601_clk_buf), // Reuse BUFG'd USB clock + + // Host command outputs + .cmd_data(usb_cmd_data), + .cmd_valid(usb_cmd_valid), + .cmd_opcode(usb_cmd_opcode), + .cmd_addr(usb_cmd_addr), + .cmd_value(usb_cmd_value), + + // Stream control + .stream_control(host_stream_control), + + // Status readback inputs + .status_request(host_status_request), + .status_cfar_threshold(host_detect_threshold), + .status_stream_ctrl(host_stream_control), + .status_radar_mode(host_radar_mode), + .status_long_chirp(host_long_chirp_cycles), + .status_long_listen(host_long_listen_cycles), + .status_guard(host_guard_cycles), + .status_short_chirp(host_short_chirp_cycles), + .status_short_listen(host_short_listen_cycles), + .status_chirps_per_elev(host_chirps_per_elev), + .status_range_mode(host_range_mode), + + // Self-test status readback + .status_self_test_flags(self_test_flags_latched), + .status_self_test_detail(self_test_detail_latched), + .status_self_test_busy(self_test_busy) + ); + + // FT601 ports unused in FT2232H mode — tie off + assign ft601_be = 4'b0000; + assign ft601_txe_n = 1'b1; + assign ft601_rxf_n = 1'b1; + assign ft601_wr_n = 1'b1; + assign ft601_rd_n = 1'b1; + assign ft601_oe_n = 1'b1; + assign ft601_siwu_n = 1'b1; + assign ft601_clk_out = 1'b0; +end +endgenerate // ============================================================================ // USB COMMAND CDC: ft601_clk → clk_100m (Gap 4: USB Read Path) diff --git a/9_Firmware/9_2_FPGA/radar_system_top_50t.v b/9_Firmware/9_2_FPGA/radar_system_top_50t.v index ff2698e..f2f9738 100644 --- a/9_Firmware/9_2_FPGA/radar_system_top_50t.v +++ b/9_Firmware/9_2_FPGA/radar_system_top_50t.v @@ -6,12 +6,17 @@ * 50T Production Wrapper for radar_system_top * * The XC7A50T-FTG256 has only 69 usable IO pins, but radar_system_top - * declares 182 port bits (including FT601 USB 3.0, debug outputs, and + * declares many port bits (including FT601 USB 3.0, debug outputs, and * status signals that have no physical connections on the 50T board). * - * This wrapper exposes only the 64 physically-connected ports and ties - * off unused inputs. Unused outputs remain internally connected so the - * full radar pipeline is preserved in the netlist. + * This wrapper exposes the physically-connected ports and ties off unused + * inputs. Unused outputs remain internally connected so the full radar + * pipeline is preserved in the netlist. + * + * USB: FT2232H (USB 2.0, 8-bit, 245 Synchronous FIFO mode) + * - USB_MODE=1 selects the FT2232H interface in radar_system_top + * - FT2232H CLKOUT (60 MHz) connected to ft601_clk_in (shared clock port) + * - 15 signals on Bank 35 (VCCO=3.3V, LVCMOS33) */ module radar_system_top_50t ( @@ -61,11 +66,20 @@ module radar_system_top_50t ( input wire stm32_new_chirp, input wire stm32_new_elevation, input wire stm32_new_azimuth, - input wire stm32_mixers_enable + input wire stm32_mixers_enable, + + // ===== FT2232H USB 2.0 Interface (Bank 35: 3.3V) ===== + input wire ft_clkout, // 60 MHz from FT2232H CLKOUT (MRCC pin C4) + inout wire [7:0] ft_data, // 8-bit bidirectional data bus + input wire ft_rxf_n, // RX FIFO not empty (active low) + input wire ft_txe_n, // TX FIFO not full (active low) + output wire ft_rd_n, // Read strobe (active low) + output wire ft_wr_n, // Write strobe (active low) + output wire ft_oe_n, // Output enable / bus direction + output wire ft_siwu // Send Immediate / WakeUp ); - // ===== Tie-off wires for unconstrained inputs ===== - wire ft601_clk_in_tied = 1'b0; + // ===== Tie-off wires for unconstrained FT601 inputs (inactive with USB_MODE=1) ===== wire ft601_txe_tied = 1'b0; wire ft601_rxf_tied = 1'b0; wire [1:0] ft601_srb_tied = 2'b00; @@ -96,11 +110,13 @@ module radar_system_top_50t ( wire [3:0] system_status_nc; (* DONT_TOUCH = "TRUE" *) - radar_system_top u_core ( + radar_system_top #( + .USB_MODE(1) // FT2232H (8-bit USB 2.0) for 50T production + ) u_core ( // ----- Clocks & Reset ----- .clk_100m (clk_100m), .clk_120m_dac (clk_120m_dac), - .ft601_clk_in (ft601_clk_in_tied), + .ft601_clk_in (ft_clkout), // FT2232H 60 MHz CLKOUT → shared USB clock port .reset_n (reset_n), // ----- DAC ----- @@ -158,7 +174,16 @@ module radar_system_top_50t ( .stm32_new_azimuth (stm32_new_azimuth), .stm32_mixers_enable (stm32_mixers_enable), - // ----- FT601 (unwired on 50T) ----- + // ----- FT2232H USB 2.0 (active on 50T, USB_MODE=1) ----- + .ft_data (ft_data), + .ft_rxf_n (ft_rxf_n), + .ft_txe_n (ft_txe_n), + .ft_rd_n (ft_rd_n), + .ft_wr_n (ft_wr_n), + .ft_oe_n (ft_oe_n), + .ft_siwu (ft_siwu), + + // ----- FT601 (inactive with USB_MODE=1 — generate block ties off) ----- .ft601_data (ft601_data_internal), .ft601_be (ft601_be_nc), .ft601_txe_n (ft601_txe_n_nc), 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 2f2b876..d7310cf 100644 --- a/9_Firmware/9_2_FPGA/scripts/200t/build_200t.tcl +++ b/9_Firmware/9_2_FPGA/scripts/200t/build_200t.tcl @@ -79,6 +79,7 @@ set rtl_files [list \ "${rtl_dir}/cfar_ca.v" \ "${rtl_dir}/fpga_self_test.v" \ "${rtl_dir}/usb_data_interface.v" \ + "${rtl_dir}/usb_data_interface_ft2232h.v" \ "${rtl_dir}/xfft_16.v" \ "${rtl_dir}/fft_engine.v" \ ] diff --git a/9_Firmware/9_2_FPGA/scripts/50t/build_50t.tcl b/9_Firmware/9_2_FPGA/scripts/50t/build_50t.tcl index 29d68b7..730b006 100644 --- a/9_Firmware/9_2_FPGA/scripts/50t/build_50t.tcl +++ b/9_Firmware/9_2_FPGA/scripts/50t/build_50t.tcl @@ -68,12 +68,18 @@ add_files -fileset constrs_1 -norecurse [file join $project_root "constraints" " # conflict. set_property SEVERITY {Warning} [get_drc_checks BIVC-1] -# NSTD-1 / UCIO-1: 118 unconstrained port bits — FT601 USB 3.0 (chip unwired -# on 50T board), dac_clk (DAC clock from AD9523, not FPGA), and all -# status/debug outputs (no physical pins on FTG256 package). +# NSTD-1 / UCIO-1: Unconstrained port bits — FT601 USB ports (inactive with +# USB_MODE=1 generate block), dac_clk (DAC clock from AD9523, not FPGA), +# and all status/debug outputs (no physical pins on FTG256 package). set_property SEVERITY {Warning} [get_drc_checks NSTD-1] set_property SEVERITY {Warning} [get_drc_checks UCIO-1] +# PLIO-9: FT2232H CLKOUT is routed to C4 (IO_L12N_T1_MRCC_35), the N-type +# pin of a Multi-Region Clock-Capable pair. Clock inputs should ideally use +# the P-type pin, but IBUFG works correctly on either. The schematic routes +# to C4 and cannot be changed. Safe to demote. +set_property SEVERITY {Warning} [get_drc_checks PLIO-9] + # ===== SYNTHESIS ===== set synth_start [clock seconds] launch_runs synth_1 -jobs 8 @@ -103,6 +109,14 @@ set impl_start [clock seconds] set_property SEVERITY {Warning} [get_drc_checks BIVC-1] set_property SEVERITY {Warning} [get_drc_checks NSTD-1] set_property SEVERITY {Warning} [get_drc_checks UCIO-1] +set_property SEVERITY {Warning} [get_drc_checks PLIO-9] + +# FT2232H CLKOUT on C4 (N-type MRCC) — override dedicated clock route check. +# The schematic routes the FT2232H 60 MHz clock to the N-pin of a differential +# MRCC pair. Vivado Place 30-876 requires this property to allow placement. +# The clock still reaches the clock network via IBUFG — this only suppresses +# the DRC that demands the P-type pin. +set_property CLOCK_DEDICATED_ROUTE FALSE [get_nets {ft_clkout_IBUF}] # ---- Run implementation steps ---- opt_design -directive Explore 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 16cfc9c..757ea3e 100644 --- a/9_Firmware/9_2_FPGA/tb/radar_system_tb.v +++ b/9_Firmware/9_2_FPGA/tb/radar_system_tb.v @@ -619,7 +619,7 @@ initial begin // Optional: dump specific signals for debugging $dumpvars(1, dut.tx_inst); $dumpvars(1, dut.rx_inst); - $dumpvars(1, dut.usb_inst); + $dumpvars(1, dut.gen_ft601.usb_inst); end endmodule diff --git a/9_Firmware/9_2_FPGA/usb_data_interface_ft2232h.v b/9_Firmware/9_2_FPGA/usb_data_interface_ft2232h.v new file mode 100644 index 0000000..c157188 --- /dev/null +++ b/9_Firmware/9_2_FPGA/usb_data_interface_ft2232h.v @@ -0,0 +1,535 @@ +`timescale 1ns / 1ps + +/** + * usb_data_interface_ft2232h.v + * + * FT2232H USB 2.0 Hi-Speed FIFO Interface (245 Synchronous FIFO Mode) + * Channel A only — 8-bit data bus, 60 MHz CLKOUT from FT2232H. + * + * This module is the 50T production board equivalent of usb_data_interface.v + * (FT601, 32-bit, USB 3.0). Both share the same internal interface signals + * so they can be swapped via a generate block in radar_system_top.v. + * + * Data packet (FPGA→Host): 11 bytes + * Byte 0: 0xAA (header) + * Bytes 1-4: range_profile[31:0] = {range_q[15:0], range_i[15:0]} MSB first + * Bytes 5-6: doppler_real[15:0] MSB first + * Bytes 7-8: doppler_imag[15:0] MSB first + * Byte 9: {7'b0, cfar_detection} + * Byte 10: 0x55 (footer) + * + * Status packet (FPGA→Host): 26 bytes + * Byte 0: 0xBB (status header) + * Bytes 1-24: 6 × 32-bit status words, MSB first + * Byte 25: 0x55 (footer) + * + * Command (Host→FPGA): 4 bytes received sequentially + * Byte 0: opcode[7:0] + * Byte 1: addr[7:0] + * Byte 2: value[15:8] + * Byte 3: value[7:0] + * + * CDC: Toggle CDC (not level sync) for all valid pulse crossings from + * 100 MHz → 60 MHz. Toggle CDC is guaranteed to work regardless of + * clock frequency ratio. + * + * Clock domains: + * clk = 100 MHz system clock (radar data domain) + * ft_clk = 60 MHz from FT2232H CLKOUT (USB FIFO domain) + */ + +module usb_data_interface_ft2232h ( + input wire clk, // Main clock (100 MHz) + input wire reset_n, // System reset (clk domain) + input wire ft_reset_n, // FT2232H-domain synchronized reset + + // Radar data inputs (clk domain) + input wire [31:0] range_profile, + input wire range_valid, + input wire [15:0] doppler_real, + input wire [15:0] doppler_imag, + input wire doppler_valid, + input wire cfar_detection, + input wire cfar_valid, + + // FT2232H Physical Interface (245 Synchronous FIFO mode) + inout wire [7:0] ft_data, // 8-bit bidirectional data bus + input wire ft_rxf_n, // Receive FIFO not empty (active low) + input wire ft_txe_n, // Transmit FIFO not full (active low) + 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 + + // Clock from FT2232H (directly used — no ODDR forwarding needed) + input wire ft_clk, // 60 MHz from FT2232H CLKOUT + + // Host command outputs (ft_clk domain — CDC'd by consumer) + output reg [31:0] cmd_data, + output reg cmd_valid, + output reg [7:0] cmd_opcode, + output reg [7:0] cmd_addr, + output reg [15:0] cmd_value, + + // Stream control input (clk domain, CDC'd internally) + input wire [2:0] stream_control, + + // Status readback inputs (clk domain, CDC'd internally) + input wire status_request, + input wire [15:0] status_cfar_threshold, + input wire [2:0] status_stream_ctrl, + input wire [1:0] status_radar_mode, + input wire [15:0] status_long_chirp, + input wire [15:0] status_long_listen, + input wire [15:0] status_guard, + input wire [15:0] status_short_chirp, + input wire [15:0] status_short_listen, + input wire [5:0] status_chirps_per_elev, + input wire [1:0] status_range_mode, + + // Self-test status readback + input wire [4:0] status_self_test_flags, + input wire [7:0] status_self_test_detail, + input wire status_self_test_busy +); + +// ============================================================================ +// PACKET FORMAT CONSTANTS +// ============================================================================ +localparam HEADER = 8'hAA; +localparam FOOTER = 8'h55; +localparam STATUS_HEADER = 8'hBB; + +// Data packet: 11 bytes total +localparam DATA_PKT_LEN = 5'd11; +// Status packet: 26 bytes total (1 header + 24 data + 1 footer) +localparam STATUS_PKT_LEN = 5'd26; + +// ============================================================================ +// WRITE FSM STATES (FPGA → Host) +// ============================================================================ +localparam [2:0] WR_IDLE = 3'd0, + WR_DATA_SEND = 3'd1, + WR_STATUS_SEND = 3'd2, + WR_DONE = 3'd3; + +reg [2:0] wr_state; +reg [4:0] wr_byte_idx; // Byte counter within packet (0..10 data, 0..25 status) + +// ============================================================================ +// READ FSM STATES (Host → FPGA) +// ============================================================================ +localparam [2:0] RD_IDLE = 3'd0, + RD_OE_ASSERT = 3'd1, + RD_READING = 3'd2, + RD_DEASSERT = 3'd3, + RD_PROCESS = 3'd4; + +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 + +// ============================================================================ +// DATA BUS DIRECTION CONTROL +// ============================================================================ +reg [7:0] ft_data_out; +reg ft_data_oe; // 1 = FPGA drives bus, 0 = FT2232H drives bus + +assign ft_data = ft_data_oe ? ft_data_out : 8'hZZ; + +// ============================================================================ +// TOGGLE CDC: clk (100 MHz) → ft_clk (60 MHz) +// ============================================================================ +// Toggle CDC is used instead of level synchronizers because a 10 ns pulse +// on clk_100m could be missed by the 16.67 ns ft_clk period. Toggle CDC +// converts pulses to level transitions, which are always captured. + +// --- Toggle registers (clk domain) --- +reg range_valid_toggle; +reg doppler_valid_toggle; +reg cfar_valid_toggle; +reg status_req_toggle; + +// --- Holding registers (clk domain) --- +// Data captured on valid pulse, held stable for ft_clk domain to read +reg [31:0] range_profile_hold; +reg [15:0] doppler_real_hold; +reg [15:0] doppler_imag_hold; +reg cfar_detection_hold; + +always @(posedge clk or negedge reset_n) begin + if (!reset_n) begin + range_valid_toggle <= 1'b0; + doppler_valid_toggle <= 1'b0; + cfar_valid_toggle <= 1'b0; + status_req_toggle <= 1'b0; + range_profile_hold <= 32'd0; + doppler_real_hold <= 16'd0; + doppler_imag_hold <= 16'd0; + cfar_detection_hold <= 1'b0; + end else begin + if (range_valid) begin + range_valid_toggle <= ~range_valid_toggle; + range_profile_hold <= range_profile; + end + if (doppler_valid) begin + doppler_valid_toggle <= ~doppler_valid_toggle; + doppler_real_hold <= doppler_real; + doppler_imag_hold <= doppler_imag; + end + if (cfar_valid) begin + cfar_valid_toggle <= ~cfar_valid_toggle; + cfar_detection_hold <= cfar_detection; + end + if (status_request) + status_req_toggle <= ~status_req_toggle; + end +end + +// --- 3-stage synchronizers (ft_clk domain) --- +// 3 stages for better MTBF at 60 MHz + +(* ASYNC_REG = "TRUE" *) reg [2:0] range_toggle_sync; +(* ASYNC_REG = "TRUE" *) reg [2:0] doppler_toggle_sync; +(* ASYNC_REG = "TRUE" *) reg [2:0] cfar_toggle_sync; +(* ASYNC_REG = "TRUE" *) reg [2:0] status_toggle_sync; + +reg range_toggle_prev; +reg doppler_toggle_prev; +reg cfar_toggle_prev; +reg status_toggle_prev; + +// Edge-detected pulses in ft_clk domain +wire range_valid_ft = range_toggle_sync[2] ^ range_toggle_prev; +wire doppler_valid_ft = doppler_toggle_sync[2] ^ doppler_toggle_prev; +wire cfar_valid_ft = cfar_toggle_sync[2] ^ cfar_toggle_prev; +wire status_req_ft = status_toggle_sync[2] ^ status_toggle_prev; + +// --- Stream control CDC (per-bit 2-stage, changes infrequently) --- +(* ASYNC_REG = "TRUE" *) reg [2:0] stream_ctrl_sync_0; +(* ASYNC_REG = "TRUE" *) reg [2:0] stream_ctrl_sync_1; +wire stream_range_en = stream_ctrl_sync_1[0]; +wire stream_doppler_en = stream_ctrl_sync_1[1]; +wire stream_cfar_en = stream_ctrl_sync_1[2]; + +// --- Captured data in ft_clk domain --- +reg [31:0] range_profile_cap; +reg [15:0] doppler_real_cap; +reg [15:0] doppler_imag_cap; +reg cfar_detection_cap; + +// Data-pending flags (ft_clk domain) +reg doppler_data_pending; +reg cfar_data_pending; + +// Status snapshot (ft_clk domain) +reg [31:0] status_words [0:5]; + +always @(posedge ft_clk or negedge ft_reset_n) begin + if (!ft_reset_n) begin + range_toggle_sync <= 3'b000; + doppler_toggle_sync <= 3'b000; + cfar_toggle_sync <= 3'b000; + status_toggle_sync <= 3'b000; + range_toggle_prev <= 1'b0; + doppler_toggle_prev <= 1'b0; + cfar_toggle_prev <= 1'b0; + status_toggle_prev <= 1'b0; + range_profile_cap <= 32'd0; + doppler_real_cap <= 16'd0; + doppler_imag_cap <= 16'd0; + cfar_detection_cap <= 1'b0; + // Default to range-only on reset (prevents write FSM deadlock) + stream_ctrl_sync_0 <= 3'b001; + stream_ctrl_sync_1 <= 3'b001; + end else begin + // 3-stage toggle synchronizers + range_toggle_sync <= {range_toggle_sync[1:0], range_valid_toggle}; + doppler_toggle_sync <= {doppler_toggle_sync[1:0], doppler_valid_toggle}; + cfar_toggle_sync <= {cfar_toggle_sync[1:0], cfar_valid_toggle}; + status_toggle_sync <= {status_toggle_sync[1:0], status_req_toggle}; + + // Previous toggle value for edge detection + range_toggle_prev <= range_toggle_sync[2]; + doppler_toggle_prev <= doppler_toggle_sync[2]; + cfar_toggle_prev <= cfar_toggle_sync[2]; + status_toggle_prev <= status_toggle_sync[2]; + + // Stream control CDC (2-stage) + stream_ctrl_sync_0 <= stream_control; + stream_ctrl_sync_1 <= stream_ctrl_sync_0; + + // Capture data on toggle edge + if (range_valid_ft) + range_profile_cap <= range_profile_hold; + if (doppler_valid_ft) begin + doppler_real_cap <= doppler_real_hold; + doppler_imag_cap <= doppler_imag_hold; + end + if (cfar_valid_ft) + cfar_detection_cap <= cfar_detection_hold; + + // Status snapshot on request + if (status_req_ft) begin + status_words[0] <= {8'hFF, 3'b000, status_radar_mode, + 5'b00000, status_stream_ctrl, + status_cfar_threshold}; + status_words[1] <= {status_long_chirp, status_long_listen}; + status_words[2] <= {status_guard, status_short_chirp}; + status_words[3] <= {status_short_listen, 10'd0, status_chirps_per_elev}; + status_words[4] <= {30'd0, status_range_mode}; + status_words[5] <= {7'd0, status_self_test_busy, + 8'd0, status_self_test_detail, + 3'd0, status_self_test_flags}; + end + end +end + +// ============================================================================ +// WRITE DATA MUX — byte selection for data packet (11 bytes) +// ============================================================================ +// Mux-based byte selection is simpler than a shift register and gives +// explicit byte ordering for synthesis. + +reg [7:0] data_pkt_byte; + +always @(*) begin + case (wr_byte_idx) + 5'd0: data_pkt_byte = HEADER; + 5'd1: data_pkt_byte = range_profile_cap[31:24]; // range MSB + 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 + 5'd10: data_pkt_byte = FOOTER; + default: data_pkt_byte = 8'h00; + endcase +end + +// ============================================================================ +// WRITE DATA MUX — byte selection for status packet (26 bytes) +// ============================================================================ + +reg [7:0] status_pkt_byte; + +always @(*) begin + case (wr_byte_idx) + 5'd0: status_pkt_byte = STATUS_HEADER; + // Word 0 (bytes 1-4) + 5'd1: status_pkt_byte = status_words[0][31:24]; + 5'd2: status_pkt_byte = status_words[0][23:16]; + 5'd3: status_pkt_byte = status_words[0][15:8]; + 5'd4: status_pkt_byte = status_words[0][7:0]; + // Word 1 (bytes 5-8) + 5'd5: status_pkt_byte = status_words[1][31:24]; + 5'd6: status_pkt_byte = status_words[1][23:16]; + 5'd7: status_pkt_byte = status_words[1][15:8]; + 5'd8: status_pkt_byte = status_words[1][7:0]; + // Word 2 (bytes 9-12) + 5'd9: status_pkt_byte = status_words[2][31:24]; + 5'd10: status_pkt_byte = status_words[2][23:16]; + 5'd11: status_pkt_byte = status_words[2][15:8]; + 5'd12: status_pkt_byte = status_words[2][7:0]; + // Word 3 (bytes 13-16) + 5'd13: status_pkt_byte = status_words[3][31:24]; + 5'd14: status_pkt_byte = status_words[3][23:16]; + 5'd15: status_pkt_byte = status_words[3][15:8]; + 5'd16: status_pkt_byte = status_words[3][7:0]; + // Word 4 (bytes 17-20) + 5'd17: status_pkt_byte = status_words[4][31:24]; + 5'd18: status_pkt_byte = status_words[4][23:16]; + 5'd19: status_pkt_byte = status_words[4][15:8]; + 5'd20: status_pkt_byte = status_words[4][7:0]; + // Word 5 (bytes 21-24) + 5'd21: status_pkt_byte = status_words[5][31:24]; + 5'd22: status_pkt_byte = status_words[5][23:16]; + 5'd23: status_pkt_byte = status_words[5][15:8]; + 5'd24: status_pkt_byte = status_words[5][7:0]; + // Footer (byte 25) + 5'd25: status_pkt_byte = FOOTER; + default: status_pkt_byte = 8'h00; + endcase +end + +// ============================================================================ +// MAIN FSM (ft_clk domain) +// ============================================================================ +// 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 + wr_state <= WR_IDLE; + wr_byte_idx <= 5'd0; + rd_state <= RD_IDLE; + rd_byte_cnt <= 2'd0; + rd_shift_reg <= 32'd0; + ft_data_out <= 8'd0; + ft_data_oe <= 1'b0; + ft_rd_n <= 1'b1; + ft_wr_n <= 1'b1; + ft_oe_n <= 1'b1; + ft_siwu <= 1'b0; + cmd_data <= 32'd0; + cmd_valid <= 1'b0; + cmd_opcode <= 8'd0; + cmd_addr <= 8'd0; + cmd_value <= 16'd0; + doppler_data_pending <= 1'b0; + cfar_data_pending <= 1'b0; + end else begin + // Default: clear one-shot signals + cmd_valid <= 1'b0; + + // Data-pending flag management + if (doppler_valid_ft) + doppler_data_pending <= 1'b1; + if (cfar_valid_ft) + cfar_data_pending <= 1'b1; + + // ================================================================ + // READ FSM — Host → FPGA command path (4-byte sequential read) + // ================================================================ + case (rd_state) + RD_IDLE: begin + // Only start reading if write FSM is idle and host has data + if (wr_state == WR_IDLE && !ft_rxf_n) begin + ft_oe_n <= 1'b0; // Assert OE: FT2232H drives bus + ft_data_oe <= 1'b0; // FPGA releases bus + rd_state <= RD_OE_ASSERT; + end + end + + RD_OE_ASSERT: begin + // 1-cycle turnaround: OE asserted, bus settling + if (!ft_rxf_n) begin + ft_rd_n <= 1'b0; // Assert RD: start reading + rd_state <= RD_READING; + end else begin + // Host withdrew data — abort + ft_oe_n <= 1'b1; + rd_state <= RD_IDLE; + end + end + + RD_READING: begin + // Sample byte and shift into command register + // Byte order: opcode, addr, value_hi, value_lo + 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; + 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; + end + end + end + + RD_DEASSERT: 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 + rd_state <= RD_PROCESS; + end else begin + // Incomplete command — discard + rd_state <= RD_IDLE; + end + end + + RD_PROCESS: begin + // Decode the assembled command word + cmd_data <= rd_shift_reg; + cmd_opcode <= rd_shift_reg[31:24]; + cmd_addr <= rd_shift_reg[23:16]; + cmd_value <= rd_shift_reg[15:0]; + cmd_valid <= 1'b1; + rd_state <= RD_IDLE; + end + + default: rd_state <= RD_IDLE; + endcase + + // ================================================================ + // WRITE FSM — FPGA → Host data streaming (byte-sequential) + // ================================================================ + if (rd_state == RD_IDLE) begin + case (wr_state) + WR_IDLE: begin + ft_wr_n <= 1'b1; + ft_data_oe <= 1'b0; // Release data bus + + // Status readback takes priority + if (status_req_ft && ft_rxf_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 + if (ft_rxf_n) begin // No host read pending + wr_state <= WR_DATA_SEND; + wr_byte_idx <= 5'd0; + end + end + end + + WR_DATA_SEND: begin + if (!ft_txe_n) begin + // TXE# low = TX FIFO has room + ft_data_oe <= 1'b1; + ft_data_out <= data_pkt_byte; + ft_wr_n <= 1'b0; // Assert write strobe + + if (wr_byte_idx == DATA_PKT_LEN - 5'd1) begin + // Last byte of data packet + wr_state <= WR_DONE; + wr_byte_idx <= 5'd0; + end else begin + wr_byte_idx <= wr_byte_idx + 5'd1; + end + end + end + + WR_STATUS_SEND: begin + if (!ft_txe_n) begin + ft_data_oe <= 1'b1; + ft_data_out <= status_pkt_byte; + ft_wr_n <= 1'b0; + + if (wr_byte_idx == STATUS_PKT_LEN - 5'd1) begin + wr_state <= WR_DONE; + wr_byte_idx <= 5'd0; + end else begin + wr_byte_idx <= wr_byte_idx + 5'd1; + end + end + end + + WR_DONE: begin + ft_wr_n <= 1'b1; + ft_data_oe <= 1'b0; // Release data bus + // Clear pending flags — data consumed + doppler_data_pending <= 1'b0; + cfar_data_pending <= 1'b0; + wr_state <= WR_IDLE; + end + + default: wr_state <= WR_IDLE; + endcase + end + end +end + +endmodule diff --git a/9_Firmware/9_3_GUI/radar_protocol.py b/9_Firmware/9_3_GUI/radar_protocol.py index 415be09..9432929 100644 --- a/9_Firmware/9_3_GUI/radar_protocol.py +++ b/9_Firmware/9_3_GUI/radar_protocol.py @@ -2,17 +2,26 @@ """ AERIS-10 Radar Protocol Layer =============================== -Pure-logic module for FT601 packet parsing and command building. +Pure-logic module for USB packet parsing and command building. No GUI dependencies — safe to import from tests and headless scripts. -Matches usb_data_interface.v packet format exactly. +Supports two USB interfaces: + - FT601 USB 3.0 (32-bit, 200T dev board) via ftd3xx + - FT2232H USB 2.0 (8-bit, 50T production board) via pyftdi -USB Packet Protocol: +USB Packet Protocol (FT601, 35-byte): TX (FPGA→Host): Data packet: [0xAA] [range 4×32b] [doppler 4×32b] [det 1B] [0x55] Status packet: [0xBB] [status 6×32b] [0x55] RX (Host→FPGA): Command word: {opcode[31:24], addr[23:16], value[15:0]} + +USB Packet Protocol (FT2232H, 11-byte compact): + TX (FPGA→Host): + Data packet: [0xAA] [range_q 2B] [range_i 2B] [dop_re 2B] [dop_im 2B] [det 1B] [0x55] + Status packet: [0xBB] [status 6×32b] [0x55] (same 26-byte format) + RX (Host→FPGA): + Command: 4 bytes received sequentially {opcode, addr, value_hi, value_lo} """ import os @@ -38,6 +47,11 @@ HEADER_BYTE = 0xAA FOOTER_BYTE = 0x55 STATUS_HEADER_BYTE = 0xBB +# Packet sizes +DATA_PACKET_SIZE_FT601 = 35 # FT601: 1 + 16 + 16 + 1 + 1 +DATA_PACKET_SIZE_FT2232H = 11 # FT2232H: 1 + 4 + 2 + 2 + 1 + 1 +STATUS_PACKET_SIZE = 26 # Same for both: 1 + 24 + 1 + NUM_RANGE_BINS = 64 NUM_DOPPLER_BINS = 32 NUM_CELLS = NUM_RANGE_BINS * NUM_DOPPLER_BINS # 2048 @@ -198,6 +212,43 @@ class RadarProtocol: return result + @staticmethod + def parse_data_packet_compact(raw: bytes) -> Optional[Dict[str, Any]]: + """ + Parse a compact 11-byte data packet from the FT2232H byte stream. + Returns dict with keys: 'range_i', 'range_q', 'doppler_i', 'doppler_q', + 'detection', or None if invalid. + + Compact packet format (FT2232H, 11 bytes): + Byte 0: 0xAA (header) + Bytes 1-2: range_q[15:0] MSB first + Bytes 3-4: range_i[15:0] MSB first + Bytes 5-6: doppler_real[15:0] MSB first + Bytes 7-8: doppler_imag[15:0] MSB first + Byte 9: {7'b0, cfar_detection} + Byte 10: 0x55 (footer) + """ + if len(raw) < DATA_PACKET_SIZE_FT2232H: + return None + if raw[0] != HEADER_BYTE: + return None + if raw[10] != FOOTER_BYTE: + return None + + range_q = _to_signed16(struct.unpack_from(">H", raw, 1)[0]) + 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 + + return { + "range_i": range_i, + "range_q": range_q, + "doppler_i": doppler_i, + "doppler_q": doppler_q, + "detection": detection, + } + @staticmethod def parse_status_packet(raw: bytes) -> Optional[StatusResponse]: """ @@ -241,25 +292,31 @@ class RadarProtocol: return sr @staticmethod - def find_packet_boundaries(buf: bytes) -> List[Tuple[int, int, str]]: + def find_packet_boundaries(buf: bytes, + compact: bool = False) -> List[Tuple[int, int, str]]: """ Scan buffer for packet start markers (0xAA data, 0xBB status). Returns list of (start_idx, expected_end_idx, packet_type). + + Args: + buf: Raw byte buffer from USB read. + compact: If True, use 11-byte compact packets (FT2232H). + If False, use 35-byte packets (FT601, default). """ + data_size = DATA_PACKET_SIZE_FT2232H if compact else DATA_PACKET_SIZE_FT601 packets = [] i = 0 while i < len(buf): if buf[i] == HEADER_BYTE: - # Data packet: 35 bytes (all streams) - end = i + 35 + end = i + data_size if end <= len(buf): packets.append((i, end, "data")) i = end else: break elif buf[i] == STATUS_HEADER_BYTE: - # Status packet: 26 bytes (6 words + header + footer) - end = i + 26 + # Status packet: 26 bytes (same for both interfaces) + end = i + STATUS_PACKET_SIZE if end <= len(buf): packets.append((i, end, "status")) i = end @@ -415,6 +472,150 @@ class FT601Connection: return bytes(buf) +# ============================================================================ +# FT2232H USB 2.0 Connection (pyftdi, 245 Synchronous FIFO) +# ============================================================================ + +# Optional pyftdi import +try: + from pyftdi.ftdi import Ftdi as PyFtdi + PYFTDI_AVAILABLE = True +except ImportError: + PYFTDI_AVAILABLE = False + + +class FT2232HConnection: + """ + FT2232H USB 2.0 Hi-Speed FIFO bridge communication. + Uses pyftdi in 245 Synchronous FIFO mode (Channel A). + VID:PID = 0x0403:0x6010 (FTDI default for FT2232H). + """ + + VID = 0x0403 + PID = 0x6010 + + def __init__(self, mock: bool = True): + self._mock = mock + self._ftdi = None + self._lock = threading.Lock() + self.is_open = False + # Mock state + 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("FT2232H mock device opened (no hardware)") + return True + + if not PYFTDI_AVAILABLE: + log.error("pyftdi not installed — cannot open real FT2232H device") + return False + + try: + self._ftdi = PyFtdi() + url = f"ftdi://0x{self.VID:04x}:0x{self.PID:04x}/{device_index + 1}" + self._ftdi.open_from_url(url) + # Configure for 245 Synchronous FIFO mode + self._ftdi.set_bitmode(0xFF, PyFtdi.BitMode.SYNCFF) + # Set USB transfer size for throughput + self._ftdi.read_data_set_chunksize(65536) + self._ftdi.write_data_set_chunksize(65536) + # Purge buffers + self._ftdi.purge_buffers() + self.is_open = True + log.info(f"FT2232H device opened: {url}") + return True + except Exception as e: + log.error(f"FT2232H open failed: {e}") + return False + + def close(self): + if self._ftdi is not None: + try: + self._ftdi.close() + except Exception: + pass + self._ftdi = None + self.is_open = False + + def read(self, size: int = 4096) -> Optional[bytes]: + """Read raw bytes from FT2232H. 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._ftdi.read_data(size) + return bytes(data) if data else None + except Exception as e: + log.error(f"FT2232H read error: {e}") + return None + + def write(self, data: bytes) -> bool: + """Write raw bytes to FT2232H (4-byte commands).""" + if not self.is_open: + return False + + if self._mock: + log.info(f"FT2232H mock write: {data.hex()}") + return True + + with self._lock: + try: + written = self._ftdi.write_data(data) + return written == len(data) + except Exception as e: + log.error(f"FT2232H write error: {e}") + return False + + def _mock_read(self, size: int) -> bytes: + """ + Generate synthetic compact radar data packets (11-byte) for testing. + Same target simulation as FT601 mock but using compact format. + """ + time.sleep(0.05) # Simulate USB latency + self._mock_frame_num += 1 + + buf = bytearray() + num_packets = min(32, size // DATA_PACKET_SIZE_FT2232H) + for _ in range(num_packets): + rbin = self._mock_rng.randint(0, NUM_RANGE_BINS) + dbin = self._mock_rng.randint(0, 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 + + # Build compact 11-byte packet + 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)) + pkt.append(detection & 0x01) + pkt.append(FOOTER_BYTE) + + buf += pkt + + return bytes(buf) + + # ============================================================================ # Replay Connection — feed real .npy data through the dashboard # ============================================================================ @@ -579,10 +780,11 @@ class ReplayConnection: """ def __init__(self, npy_dir: str, use_mti: bool = True, - replay_fps: float = 5.0): + replay_fps: float = 5.0, compact: bool = False): self._npy_dir = npy_dir self._use_mti = use_mti self._replay_fps = max(replay_fps, 0.1) + self._compact = compact # True = FT2232H 11-byte packets self._lock = threading.Lock() self.is_open = False self._packets: bytes = b"" @@ -756,8 +958,9 @@ class ReplayConnection: det = np.zeros((NUM_RANGE_BINS, NUM_DOPPLER_BINS), dtype=bool) det_count = int(det.sum()) - log.info(f"Replay: rebuilt {NUM_CELLS} packets " - f"(MTI={'ON' if self._mti_enable else 'OFF'}, " + pkt_fmt = "compact" if self._compact else "FT601" + log.info(f"Replay: rebuilt {NUM_CELLS} packets ({pkt_fmt}, " + f"MTI={'ON' if self._mti_enable else 'OFF'}, " f"DC_notch={self._dc_notch_width}, " f"CFAR={'ON' if self._cfar_enable else 'OFF'} " f"G={self._cfar_guard} T={self._cfar_train} " @@ -767,8 +970,38 @@ class ReplayConnection: range_i = self._range_i_vec range_q = self._range_q_vec - # Pre-allocate buffer (35 bytes per packet * 2048 cells) - buf = bytearray(NUM_CELLS * 35) + if self._compact: + return self._build_packets_compact(range_i, range_q, dop_i, dop_q, det) + else: + return self._build_packets_ft601(range_i, range_q, dop_i, dop_q, det) + + def _build_packets_compact(self, range_i, range_q, dop_i, dop_q, det) -> bytes: + """Build compact 11-byte packets for FT2232H interface.""" + buf = bytearray(NUM_CELLS * DATA_PACKET_SIZE_FT2232H) + pos = 0 + for rbin in range(NUM_RANGE_BINS): + ri = int(np.clip(range_i[rbin], -32768, 32767)) + rq = int(np.clip(range_q[rbin], -32768, 32767)) + rq_bytes = struct.pack(">h", rq) + ri_bytes = struct.pack(">h", ri) + for dbin in range(NUM_DOPPLER_BINS): + di = int(np.clip(dop_i[rbin, dbin], -32768, 32767)) + dq = int(np.clip(dop_q[rbin, dbin], -32768, 32767)) + d = 1 if det[rbin, dbin] else 0 + + buf[pos] = HEADER_BYTE; pos += 1 + buf[pos:pos+2] = rq_bytes; pos += 2 + buf[pos:pos+2] = ri_bytes; pos += 2 + buf[pos:pos+2] = struct.pack(">h", di); pos += 2 + buf[pos:pos+2] = struct.pack(">h", dq); pos += 2 + buf[pos] = d; pos += 1 + buf[pos] = FOOTER_BYTE; pos += 1 + + return bytes(buf) + + def _build_packets_ft601(self, range_i, range_q, dop_i, dop_q, det) -> bytes: + """Build 35-byte packets for FT601 interface.""" + buf = bytearray(NUM_CELLS * DATA_PACKET_SIZE_FT601) pos = 0 for rbin in range(NUM_RANGE_BINS): ri = int(np.clip(range_i[rbin], -32768, 32767)) & 0xFFFF @@ -879,18 +1112,20 @@ class DataRecorder: class RadarAcquisition(threading.Thread): """ - Background thread: reads from FT601, parses packets, assembles frames, - and pushes complete frames to the display queue. + Background thread: reads from USB (FT601 or FT2232H), parses packets, + assembles frames, and pushes complete frames to the display queue. """ - def __init__(self, connection: FT601Connection, frame_queue: queue.Queue, + def __init__(self, connection, frame_queue: queue.Queue, recorder: Optional[DataRecorder] = None, - status_callback=None): + status_callback=None, + compact: bool = False): super().__init__(daemon=True) self.conn = connection self.frame_queue = frame_queue self.recorder = recorder self._status_callback = status_callback + self._compact = compact # True for FT2232H 11-byte packets self._stop_event = threading.Event() self._frame = RadarFrame() self._sample_idx = 0 @@ -900,17 +1135,23 @@ class RadarAcquisition(threading.Thread): self._stop_event.set() def run(self): - log.info("Acquisition thread started") + log.info(f"Acquisition thread started (compact={self._compact})") while not self._stop_event.is_set(): raw = self.conn.read(4096) if raw is None or len(raw) == 0: time.sleep(0.01) continue - packets = RadarProtocol.find_packet_boundaries(raw) + packets = RadarProtocol.find_packet_boundaries( + raw, compact=self._compact) for start, end, ptype in packets: if ptype == "data": - parsed = RadarProtocol.parse_data_packet(raw[start:end]) + if self._compact: + parsed = RadarProtocol.parse_data_packet_compact( + raw[start:end]) + else: + parsed = RadarProtocol.parse_data_packet( + raw[start:end]) if parsed is not None: self._ingest_sample(parsed) elif ptype == "status":