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:
Jason
2026-04-07 19:22:16 +03:00
parent 3e737fb90e
commit 408f4d126f
9 changed files with 1119 additions and 136 deletions
@@ -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 (unusedno signal connections)
# Bank 35: VCCO = 3.3V (FT2232H USB 2.0 FIFO15 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 (ADBUS0ADBUS7)
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
+43 -15
View File
@@ -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 CLKP 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 CLKP 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;
+150 -59
View File
@@ -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)
+35 -10
View File
@@ -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" \
]
+17 -3
View File
@@ -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
+1 -1
View File
@@ -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 (FPGAHost): 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 (FPGAHost): 26 bytes
* Byte 0: 0xBB (status header)
* Bytes 1-24: 6 × 32-bit status words, MSB first
* Byte 25: 0x55 (footer)
*
* Command (HostFPGA): 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
+261 -20
View File
@@ -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":