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%)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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" \
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user