PLFM_RADAR/9_Firmware/9_2_FPGA/radar_system_top.v
Jason 4c9032187a test(fpga): PR-AD AD.3 — FT601 system-opcode TB + SUPPORT_LONG_RANGE compile guard
Guards against future drift between the two USB driver command paths
(USB_MODE=0 vs USB_MODE=1) and against 200T+long-range build breakage
in usb_data_interface.v.

tb/tb_system_opcodes_ft601.v — new (~397 LOC):
  - Sibling of tb_system_opcodes.v with radar_system_top
    #(.USB_MODE(0))
  - FT601 RX BFM: 32-bit single-word read with 5-cycle word holds
    (IDLE -> OE_ASSERT -> READING -> DEASSERT -> PROCESS)
  - 28/0 PASS, same opcode coverage as the FT2232H sibling

run_regression.sh — adds 2 entries:
  - "FT601 Driver Long-Range Compile (PR-AD AD.3)" in Phase 1:
    inline iverilog -DSUPPORT_LONG_RANGE on usb_data_interface.v;
    suppresses informational warnings (timescale / dangling / @*
    sensitive) and fails on real warnings. Catches macro creep
    that would break the 200T 20-km build before Vivado synthesis.
  - "System Opcodes FT601 (tb_system_opcodes_ft601)" in Phase 2
    (non-quick block).

radar_system_top.v — header doc block documenting the
USB_MODE x SUPPORT_LONG_RANGE 4-cell build matrix and clarifying
that the flags are orthogonal (200T+FT601 with 3-km-only DSP is a
valid bring-up baseline; 50T+FT2232H+LR is rare but must still
compile). Notes that both drivers emit the identical v2 bulk frame
and that byte-equality is enforced by tb_usb_drivers_parity.v.

Regression: 46/0/0 (44 + 1 system TB + 1 LR compile guard).
Lint: 0 errors, 2 advisory (pre-existing, unrelated).
2026-05-18 13:29:11 +05:45

1413 lines
66 KiB
Verilog
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

`timescale 1ns / 1ps
`include "radar_params.vh"
/**
* radar_system_top.v
*
* Complete Radar System Top Module
* Integrates:
* - Radar Transmitter (PLFM chirp generation)
* - Radar Receiver (ADC interface, DDC, matched filtering, Doppler processing)
* - 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: USB interface clock (100MHz FT601 or 60MHz FT2232H)
*
* ========================================================================
* BUILD MATRIX — USB_MODE × SUPPORT_LONG_RANGE
* ========================================================================
* USB_MODE | SUPPORT_LONG_RANGE | Board / Variant
* ---------+--------------------+----------------------------------------
* 1 | undefined | 50T + FT2232H — 3 km production
* 0 | defined | 200T + FT601 — 3 km + 20 km
* 0 | undefined | 200T + FT601 — 3 km only (compile-tested)
* 1 | defined | 50T + FT2232H + LR (rare; allowed)
*
* Both USB drivers (usb_data_interface.v / usb_data_interface_ft2232h.v)
* emit the identical v2 bulk frame protocol (PR-G, 56330 B/frame at the
* default 512 range × 48 doppler × 3 sub-frames). Byte-equality is asserted
* end-to-end by tb/tb_usb_drivers_parity.v (PR-AD AD.2).
*
* SUPPORT_LONG_RANGE only widens the upstream pipeline (range_bin_decimator,
* doppler_processor BRAM, cfar magnitude memory) from 512 → 4096 range bins;
* the USB wire protocol stays at 512 range bins regardless. The driver
* builds cleanly under either flag (verified by the FT601 Long-Range
* compile-only check in run_regression.sh).
* ========================================================================
*
* 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 (
// System Clocks
input wire clk_100m, // 100MHz system clock
input wire clk_120m_dac, // 120MHz DAC clock
input wire ft601_clk_in, // FT601 clock (100MHz)
input wire reset_n, // Active-low reset
// ========== TRANSMITTER INTERFACES ==========
// DAC Interface
output wire [7:0] dac_data,
output wire dac_clk,
output wire dac_sleep,
// RF Switch Control
output wire fpga_rf_switch,
// Mixer Enables
output wire rx_mixer_en,
output wire tx_mixer_en,
// ADAR1000 Beamformer Control (via level shifters)
output wire adar_tx_load_1, adar_rx_load_1,
output wire adar_tx_load_2, adar_rx_load_2,
output wire adar_tx_load_3, adar_rx_load_3,
output wire adar_tx_load_4, adar_rx_load_4,
output wire adar_tr_1, adar_tr_2, adar_tr_3, adar_tr_4,
// Level Shifter SPI Interface (STM32F7 to ADAR1000)
input wire stm32_sclk_3v3,
input wire stm32_mosi_3v3,
output wire stm32_miso_3v3,
input wire stm32_cs_adar1_3v3, stm32_cs_adar2_3v3,
input wire stm32_cs_adar3_3v3, stm32_cs_adar4_3v3,
output wire stm32_sclk_1v8,
output wire stm32_mosi_1v8,
input wire stm32_miso_1v8,
output wire stm32_cs_adar1_1v8, stm32_cs_adar2_1v8,
output wire stm32_cs_adar3_1v8, stm32_cs_adar4_1v8,
// ========== RECEIVER INTERFACES ==========
// ADC Physical Interface (LVDS)
input wire [7:0] adc_d_p, // ADC Data P (LVDS)
input wire [7:0] adc_d_n, // ADC Data N (LVDS)
input wire adc_dco_p, // Data Clock Output P (400MHz LVDS)
input wire adc_dco_n, // Data Clock Output N (400MHz LVDS)
// Audit F-0.1: AD9484 OR (overrange) LVDS pair
input wire adc_or_p,
input wire adc_or_n,
output wire adc_pwdn, // ADC Power Down
// ========== STM32 CONTROL INTERFACES ==========
// STM32 → FPGA control (PD8/PD11). PD9/PD10 (was stm32_new_elevation /
// stm32_new_azimuth) retired in PR-AB.b expanded — see commit 3 for the
// XDC + MCU GPIO scrub. stm32_beam_ready (PD8) is the MCU's per-pattern
// ready toggle for the beam-ready handshake added in PR-AB.b expanded
// commit 5; CDC-sync is owned by chirp_scheduler.v on `clk`. The
// handshake itself is gated by opcode 0x1A (host_handshake_enable);
// cold-reset default 1'b0 keeps the open-loop legacy cadence in place.
input wire stm32_beam_ready,
input wire stm32_mixers_enable,
// ========== FT601 USB 3.0 INTERFACE ==========
// Data bus
inout wire [31:0] ft601_data, // 32-bit bidirectional data bus
output wire [3:0] ft601_be, // Byte enable (4 lanes for 32-bit mode)
// Control signals
output wire ft601_txe_n, // Transmit enable (active low)
output wire ft601_rxf_n, // Receive enable (active low)
input wire ft601_txe, // Transmit FIFO empty
input wire ft601_rxf, // Receive FIFO full
output wire ft601_wr_n, // Write strobe (active low)
output wire ft601_rd_n, // Read strobe (active low)
output wire ft601_oe_n, // Output enable (active low)
output wire ft601_siwu_n, // Send immediate / Wakeup
// FIFO flags
input wire [1:0] ft601_srb, // Selected read buffer
input wire [1:0] ft601_swb, // Selected write buffer
// 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 ==========
// Live chirp-index telemetry (TX-side counter sync'd back to clk_100m)
output wire [5:0] current_chirp,
output wire new_chirp_frame,
// Doppler processing outputs (for debugging)
output wire [31:0] dbg_doppler_data,
output wire dbg_doppler_valid,
output wire [`RP_DOPPLER_BIN_WIDTH-1:0] dbg_doppler_bin,
output wire [`RP_RANGE_BIN_WIDTH_MAX-1:0] dbg_range_bin,
// System status
output wire [3:0] system_status,
// FPGA→STM32 GPIO outputs (DIG_5..DIG_7 on 50T board)
// Used by STM32 outer AGC loop to read saturation state without USB polling.
output wire gpio_dig5, // DIG_5 (H11→PD13): AGC saturation flag (1=clipping detected)
output wire gpio_dig6, // DIG_6 (G12→PD14): stretched chirp_scheduler frame_pulse (~100 ns) for STM32 dwell sync
output wire gpio_dig7 // DIG_7 (H12→PD15): control-fault OR (F-6.4 watchdog | F-1.2 CDC overrun)
);
// ============================================================================
// PARAMETERS
// ============================================================================
// System configuration
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 = 1; // 0=FT601 (32-bit, 200T), 1=FT2232H (8-bit, 50T production default)
// ============================================================================
// INTERNAL SIGNALS
// ============================================================================
// Clock and reset
wire clk_100m_buf;
wire clk_120m_dac_buf;
wire ft601_clk_buf;
wire sys_reset_n;
wire sys_reset_120m_n; // Reset synchronized to clk_120m_dac domain
wire sys_reset_ft601_n; // Reset synchronized to ft601_clk domain
// CDC: synchronized versions of async inputs for status_reg
wire stm32_mixers_enable_100m; // stm32_mixers_enable sync'd to clk_100m
wire ft601_txe_100m; // ft601_txe sync'd to clk_100m
// Transmitter internal signals
wire [7:0] tx_chirp_data;
wire tx_chirp_valid;
wire tx_chirp_done;
wire tx_new_chirp_frame; // In clk_120m_dac domain
wire tx_new_chirp_frame_sync; // Synchronized to clk_100m domain
wire [5:0] tx_current_chirp; // In clk_120m_dac domain
wire [5:0] tx_current_chirp_sync; // Synchronized to clk_100m domain
wire tx_current_chirp_sync_valid;
// PR-E: scheduler outputs from receiver_final, in clk_100m domain.
// Routed directly into radar_transmitter, which owns the 100→120 CDC.
wire [1:0] sched_wave_sel;
wire sched_chirp_pulse;
wire sched_frame_pulse;
// Receiver internal signals
wire [31:0] rx_doppler_output;
wire rx_doppler_valid;
wire [`RP_DOPPLER_BIN_WIDTH-1:0] rx_doppler_bin;
wire [`RP_RANGE_BIN_WIDTH_MAX-1:0] rx_range_bin;
wire [31:0] rx_range_profile;
wire rx_range_valid;
wire [15:0] rx_range_profile_decimated;
wire rx_range_profile_decimated_valid;
wire [15:0] rx_doppler_real;
wire [15:0] rx_doppler_imag;
wire rx_doppler_data_valid;
reg rx_detect_flag; // Threshold detection result (was rx_cfar_detection)
reg rx_detect_valid; // Detection valid pulse (was rx_cfar_valid)
// PR-G: 2-bit class register (registered alongside detect_flag for the same
// CDC-clean handoff to usb_data_interface_ft2232h). Encoding per RP_DETECT_*.
reg [`RP_DETECT_CLASS_WIDTH-1:0] rx_detect_class;
// PR-Z A6 fix (Bug A): cfar emits per-cell coordinates during ST_CFAR_CMP via
// detect_range/detect_doppler. Register them in lockstep with rx_detect_valid
// so the USB RMW write address tracks cfar's own bin counter, not doppler's
// stale (511, 47) tail-state.
reg [`RP_RANGE_BIN_WIDTH_MAX-1:0] rx_detect_range;
reg [`RP_DOPPLER_BIN_WIDTH-1:0] rx_detect_doppler;
// Frame-complete signal from Doppler processor (for CFAR)
wire rx_frame_complete;
// ADC debug tap from receiver (clk_100m domain, post-DDC)
wire [15:0] rx_dbg_adc_i;
wire [15:0] rx_dbg_adc_q;
wire rx_dbg_adc_valid;
// AGC status from receiver (for status readback and GPIO)
wire [7:0] rx_agc_saturation_count;
wire [7:0] rx_agc_peak_magnitude;
wire [3:0] rx_agc_current_gain;
// DDC overflow diagnostics (audit F-6.1) — plumbed out of receiver so the
// DDC mixer_saturation / filter_overflow ports are no longer deleted at
// the boundary. Aggregated into gpio_dig5 alongside AGC saturation.
wire rx_ddc_overflow_any;
wire [2:0] rx_ddc_saturation_count;
// MTI saturation count (audit F-6.3). OR'd into gpio_dig5 for MCU visibility.
wire [7:0] rx_mti_saturation_count;
// Range-bin decimator watchdog (audit F-6.4). 1-cycle pulse from
// range_bin_decimator (~10 ns @ 100 MHz). Drives gpio_dig7 directly so
// the MCU can see the raw fault edge.
wire rx_range_decim_watchdog;
// PR-AB.b Step 1: sticky latch over the watchdog pulse so a slow host
// status poll never misses the event. The 100 MHz → ft_clk synchronizer
// inside usb_data_interface_ft2232h cannot reliably capture a 10 ns
// pulse, so we register the level here in the source (clk) domain and
// feed the level — not the pulse — into status_words[5][5]. Cleared
// only by reset_n: the FPGA exposes no host-driven clear opcode today.
// See project_aeris10_clear_monitors_opcode_future.md for the bundled-PR
// shape that would activate a host clear here and across the DDC + AD9484
// overrange stickies in radar_receiver_final.v.
reg rx_range_decim_watchdog_sticky;
always @(posedge clk_100m_buf or negedge sys_reset_n) begin
if (!sys_reset_n)
rx_range_decim_watchdog_sticky <= 1'b0;
else if (rx_range_decim_watchdog)
rx_range_decim_watchdog_sticky <= 1'b1;
end
// CIC→FIR CDC overrun sticky (audit F-1.2). High = at least one baseband
// sample has been silently dropped between the 400 MHz CIC and 100 MHz FIR.
// Already sticky in source (ddc_400m.v:697-702), no extra latch needed.
wire rx_ddc_cic_fir_overrun;
// PR-AB.b expanded commit 5: sticky from chirp_scheduler. High = at least one
// frame fired without an MCU PD8 ack within the ~80 ms watchdog. Packed into
// status_words[4][1] so the host can spot patterns falling behind. Sticky is
// owned by the scheduler (clk_100m source domain); CDC across to ft_clk is
// done inside usb_data_interface_ft2232h alongside the other status stickies.
wire rx_beam_handshake_watchdog;
// Data packing for USB
wire [31:0] usb_range_profile;
wire usb_range_valid;
wire [15:0] usb_doppler_real;
wire [15:0] usb_doppler_imag;
wire usb_doppler_valid;
wire usb_detect_flag; // (was usb_cfar_detection) — FT601 legacy 1-bit path
wire usb_detect_valid; // (was usb_cfar_valid)
wire [`RP_DETECT_CLASS_WIDTH-1:0] usb_detect_class; // PR-G: 2-bit class for FT2232H bulk frame v2
// System status
reg [3:0] status_reg;
// USB host command outputs (Gap 4: USB Read Path)
// These are in the ft601_clk domain; CDC'd to clk_100m below
wire [31:0] usb_cmd_data;
wire usb_cmd_valid; // 1-cycle pulse in ft601_clk domain
wire [7:0] usb_cmd_opcode;
wire [7:0] usb_cmd_addr;
wire [15:0] usb_cmd_value;
// USB command decode registers (clk_100m domain, driven by CDC block below)
// Declared here (before rx_inst) so Icarus Verilog can resolve forward refs.
reg [15:0] host_detect_threshold; // (was host_cfar_threshold)
reg [5:0] host_stream_control;
// Fix 3: Digital gain control register
// [3]=direction: 0=amplify, 1=attenuate. [2:0]=shift amount 0..7.
// Default 0x00 = pass-through (no gain change).
reg [3:0] host_gain_shift;
// Gap 2: Host-configurable chirp timing registers
// chirp_scheduler (chirp-v2 PR-D) consumes these directly — no parameter
// overrides. Defaults match the legacy values so behavior is unchanged
// until the host writes them. PR-G adds the v2 medium/track/subframe regs.
reg [15:0] host_long_chirp_cycles; // Opcode 0x10 (default 3000)
reg [15:0] host_long_listen_cycles; // Opcode 0x11 (default 13700)
reg [15:0] host_guard_cycles; // Opcode 0x12 (default 17540)
reg [15:0] host_short_chirp_cycles; // Opcode 0x13 (default 100, V2)
reg [15:0] host_short_listen_cycles; // Opcode 0x14 (default 17400, V2)
reg [15:0] host_medium_chirp_cycles; // Opcode 0x17 (default 500, PR-G G2)
reg [15:0] host_medium_listen_cycles; // Opcode 0x18 (default 15600, PR-Q staggered PRI)
reg [5:0] host_chirps_per_elev; // Opcode 0x15 (default 48 = RP_CHIRPS_PER_FRAME, PR-F)
// PR-U / M-8: per-sub-frame enable mask routed end-to-end so the host knows
// which sub-frames the chirp_scheduler emitted for a given frame. Bit 0 SHORT,
// bit 1 MEDIUM, bit 2 LONG. Default 3'b111 keeps the production 3-PRI ladder.
// Mirrored into v2 frame byte 2 bits[5:3] (usb_data_interface_ft2232h.v).
reg [2:0] host_subframe_enable; // Opcode 0x19 (default RP_DEF_SUBFRAME_ENABLE = 3'b111)
// PR-AB.b expanded commit 5: opcode 0x1A enables the beam-ready handshake
// inside chirp_scheduler. Cold-reset default = 1'b0 (legacy open-loop
// cadence) so existing TBs and any production deployment without the MCU
// PD8 toggle wired up keep their old behavior. Host (GUI) writes 1 once
// the MCU has been verified to be toggling PD8 each beam pattern.
reg host_handshake_enable;
reg host_status_request; // Opcode 0xFF (self-clearing pulse)
// Fix 4: Doppler/chirps mismatch protection
// DOPPLER_FRAME_CHIRPS is the fixed chirp count expected by the staggered-PRI
// Doppler path: 48 chirps split as 16 SHORT (175 µs PRI) + 16 MEDIUM (161 µs
// PRI, PR-Q) + 16 LONG (167 µs PRI). Three distinct coprime PRIs let the
// host run 3-PRI CRT to unfold Doppler aliases (C-5 / PR-Q). If host sets
// chirps_per_elev to a different value, Doppler accumulation is corrupted.
// Clamp at command decode and flag the mismatch so the host knows.
localparam DOPPLER_FRAME_CHIRPS = `RP_CHIRPS_PER_FRAME; // 48 (PR-F); was 32
reg chirps_mismatch_error; // Set if host tried to set chirps != FFT size
// CFAR configuration registers (host-configurable via USB)
reg [3:0] host_cfar_guard; // Opcode 0x21: guard cells per side (0..8)
reg [4:0] host_cfar_train; // Opcode 0x22: training cells per side (1..16)
reg [7:0] host_cfar_alpha; // Opcode 0x23: threshold multiplier (Q4.4)
reg [7:0] host_cfar_alpha_soft; // PR-F: soft / candidate-tier multiplier (Q4.4).
// USB opcode mapping deferred to PR-G (planned 0x28).
reg [1:0] host_cfar_mode; // Opcode 0x24: 00=CA, 01=GO, 10=SO
reg host_cfar_enable; // Opcode 0x25: 1=CFAR, 0=simple threshold
// Ground clutter removal registers (host-configurable via USB)
reg host_mti_enable; // Opcode 0x26: 1=MTI active, 0=pass-through
reg [2:0] host_dc_notch_width; // Opcode 0x27: DC notch ±width bins (0=off, 1..7)
// AGC configuration registers (host-configurable via USB, opcodes 0x28-0x2C)
reg host_agc_enable; // Opcode 0x28: 0=manual gain, 1=auto AGC
reg [7:0] host_agc_target; // Opcode 0x29: target peak magnitude (default 200)
reg [3:0] host_agc_attack; // Opcode 0x2A: gain-down step on clipping (default 1)
reg [3:0] host_agc_decay; // Opcode 0x2B: gain-up step when weak (default 1)
reg [3:0] host_agc_holdoff; // Opcode 0x2C: frames to wait before gain-up (default 4)
// AUDIT-C3: ADC format register (opcode 0x33). Selects DDC sign-conversion
// to match the AD9484 SCLK/DFS strap (jumper SJ1 on Main Board).
// 2'b00 = offset-binary (default; matches SJ1 pins 1-2 bridged)
// 2'b01 = two's-complement (SJ1 pins 2-3 bridged)
// 2'b1x = reserved (treated as offset-binary)
// CSB is hard-tied HIGH on the production Main Board so SPI cannot reconfigure
// the AD9484 — see RADAR_Main_Board.sch:46719. This register is the host's
// only path to align RTL with the physical strap without a board rework.
// AUDIT-S25 (opcode 0x32): AD9484 power-down control.
// 1'b0 = ADC powered up (default; matches the historical hard-tied state)
// 1'b1 = ADC PWDN asserted (active-high per AD9484 datasheet section
// "Power-Down (PWDN)"; FPGA pin drives the AD9484 PWDN net via
// the R36/R37 divider on the Main Board).
// Lets the MCU pulse PWDN during recovery without dropping main power.
// AUDIT-C13 noted that the AD9484's CSB is hard-tied HIGH on the production
// board (no SPI access), so PWDN is the ONLY in-system reset path for the
// ADC. PWDN is a stable single-bit level driven from this clk_100m register
// straight to the I/O pad; no CDC needed (asynchronous w.r.t. ADC, which
// re-acquires its DLL on PWDN deassert).
reg host_adc_pwdn;
reg [1:0] host_adc_format;
// Board bring-up self-test registers (opcode 0x30 trigger, 0x31 readback)
reg host_self_test_trigger; // Opcode 0x30: self-clearing pulse
wire self_test_busy;
wire self_test_result_valid;
wire [4:0] self_test_result_flags; // Per-test PASS(1)/FAIL(0)
wire [7:0] self_test_result_detail; // Diagnostic detail byte
// Self-test latched results (hold until next trigger)
reg [4:0] self_test_flags_latched;
reg [7:0] self_test_detail_latched;
// Self-test ADC capture wires
wire self_test_capture_active;
wire [15:0] self_test_capture_data;
wire self_test_capture_valid;
// ============================================================================
// CLOCK BUFFERING
// ============================================================================
`ifdef SIMULATION
// In simulation (iverilog), BUFG is not available — pass-through assigns
assign clk_100m_buf = clk_100m;
assign clk_120m_dac_buf = clk_120m_dac;
assign ft601_clk_buf = ft601_clk_in;
`else
BUFG bufg_100m (
.I(clk_100m),
.O(clk_100m_buf)
);
BUFG bufg_120m (
.I(clk_120m_dac),
.O(clk_120m_dac_buf)
);
BUFG bufg_ft601 (
.I(ft601_clk_in),
.O(ft601_clk_buf)
);
`endif
// Reset synchronization (clk_100m domain)
(* ASYNC_REG = "TRUE" *) reg [1:0] reset_sync;
always @(posedge clk_100m_buf or negedge reset_n) begin
if (!reset_n) begin
reset_sync <= 2'b00;
end else begin
reset_sync <= {reset_sync[0], 1'b1};
end
end
assign sys_reset_n = reset_sync[1];
// Reset synchronization (clk_120m_dac domain)
// Ensures reset deassertion is synchronous to the DAC clock,
// preventing recovery/removal timing violations on 120 MHz FFs.
(* ASYNC_REG = "TRUE" *) reg [1:0] reset_sync_120m;
always @(posedge clk_120m_dac_buf or negedge reset_n) begin
if (!reset_n) begin
reset_sync_120m <= 2'b00;
end else begin
reset_sync_120m <= {reset_sync_120m[0], 1'b1};
end
end
assign sys_reset_120m_n = reset_sync_120m[1];
// Reset synchronization (ft601_clk domain)
// FT601 has its own asynchronous clock from the USB controller.
// All FT601-domain registers need a properly synchronized reset.
(* ASYNC_REG = "TRUE" *) reg [2:0] reset_sync_ft601; // 3-stage for better MTBF
always @(posedge ft601_clk_buf or negedge reset_n) begin
if (!reset_n) begin
reset_sync_ft601 <= 3'b000;
end else begin
reset_sync_ft601 <= {reset_sync_ft601[1:0], 1'b1};
end
end
assign sys_reset_ft601_n = reset_sync_ft601[2];
// CDC synchronizers for status_reg inputs (async -> clk_100m)
// stm32_mixers_enable is an async GPIO; ft601_txe is on ft601_clk domain
cdc_single_bit #(.STAGES(2)) cdc_mixers_en_status (
.src_clk(clk_100m_buf), // Pseudo-source for async GPIO
.dst_clk(clk_100m_buf),
.reset_n(sys_reset_n),
.src_signal(stm32_mixers_enable),
.dst_signal(stm32_mixers_enable_100m)
);
cdc_single_bit #(.STAGES(2)) cdc_ft601_txe_status (
.src_clk(ft601_clk_buf),
.dst_clk(clk_100m_buf),
.reset_n(sys_reset_n),
.src_signal(ft601_txe),
.dst_signal(ft601_txe_100m)
);
// ============================================================================
// CLOCK DOMAIN CROSSING: TRANSMITTER (120 MHz) -> SYSTEM (100 MHz)
// ============================================================================
// CDC for chirp_counter: 6-bit multi-bit Gray-code synchronizer
// Source domain is clk_120m_dac, so reset must be synchronized to that domain.
// The cdc_adc_to_processing module uses synchronous reset internally, so
// using sys_reset_120m_n (120m-synchronized) is correct for the source side.
// The destination side will sample it synchronously on dst_clk, which at worst
// delays reset deassertion by 1-2 cycles — acceptable for CDC reset.
cdc_adc_to_processing #(
.WIDTH(6),
.STAGES(3)
) cdc_chirp_counter (
.src_clk(clk_120m_dac_buf),
.dst_clk(clk_100m_buf),
.src_reset_n(sys_reset_120m_n),
.dst_reset_n(sys_reset_n),
.src_data(tx_current_chirp),
.src_valid(1'b1), // Always valid — counter updates continuously
.dst_data(tx_current_chirp_sync),
.dst_valid(tx_current_chirp_sync_valid)
);
// CDC for new_chirp_frame: toggle CDC (pulse on clk_120m -> pulse on clk_100m)
// new_chirp_frame is a 1-cycle pulse on clk_120m_dac. A level synchronizer
// at 100 MHz can miss it. Toggle CDC converts pulse -> level toggle,
// synchronizes the toggle, then detects edges to recover the pulse.
reg chirp_frame_toggle_120m;
always @(posedge clk_120m_dac_buf or negedge sys_reset_120m_n) begin
if (!sys_reset_120m_n)
chirp_frame_toggle_120m <= 1'b0;
else if (tx_new_chirp_frame)
chirp_frame_toggle_120m <= ~chirp_frame_toggle_120m;
end
wire chirp_frame_toggle_100m;
cdc_single_bit #(
.STAGES(3)
) cdc_new_chirp_frame (
.src_clk(clk_120m_dac_buf),
.dst_clk(clk_100m_buf),
.reset_n(sys_reset_n),
.src_signal(chirp_frame_toggle_120m),
.dst_signal(chirp_frame_toggle_100m)
);
reg chirp_frame_toggle_100m_prev;
always @(posedge clk_100m_buf or negedge sys_reset_n) begin
if (!sys_reset_n)
chirp_frame_toggle_100m_prev <= 1'b0;
else
chirp_frame_toggle_100m_prev <= chirp_frame_toggle_100m;
end
assign tx_new_chirp_frame_sync = chirp_frame_toggle_100m ^ chirp_frame_toggle_100m_prev;
// ============================================================================
// RADAR TRANSMITTER INSTANTIATION
// ============================================================================
radar_transmitter tx_inst (
// System Clocks
.clk_100m(clk_100m_buf),
.clk_120m_dac(clk_120m_dac_buf),
.reset_n(sys_reset_120m_n), // 120 MHz-synchronized reset for DAC-domain logic
.reset_100m_n(sys_reset_n), // 100 MHz-synchronized reset for edge detectors/CDC
// DAC Interface
.dac_data(dac_data),
.dac_clk(dac_clk),
.dac_sleep(dac_sleep),
// Mixer Enables
.rx_mixer_en(rx_mixer_en),
.tx_mixer_en(tx_mixer_en),
// Scheduler bridge (chirp-v2 PR-E): clk_100m signals from receiver_final
.sched_wave_sel(sched_wave_sel),
.sched_chirp_pulse(sched_chirp_pulse),
.sched_frame_pulse(sched_frame_pulse),
// STM32 master enable (mixers_enable)
.stm32_mixers_enable(stm32_mixers_enable),
// RF Switch Control
.fpga_rf_switch(fpga_rf_switch),
// ADAR1000 Control Interface
.adar_tx_load_1(adar_tx_load_1),
.adar_rx_load_1(adar_rx_load_1),
.adar_tx_load_2(adar_tx_load_2),
.adar_rx_load_2(adar_rx_load_2),
.adar_tx_load_3(adar_tx_load_3),
.adar_rx_load_3(adar_rx_load_3),
.adar_tx_load_4(adar_tx_load_4),
.adar_rx_load_4(adar_rx_load_4),
.adar_tr_1(adar_tr_1),
.adar_tr_2(adar_tr_2),
.adar_tr_3(adar_tr_3),
.adar_tr_4(adar_tr_4),
// Level Shifter SPI Interface
.stm32_sclk_3v3(stm32_sclk_3v3),
.stm32_mosi_3v3(stm32_mosi_3v3),
.stm32_miso_3v3(stm32_miso_3v3),
.stm32_cs_adar1_3v3(stm32_cs_adar1_3v3),
.stm32_cs_adar2_3v3(stm32_cs_adar2_3v3),
.stm32_cs_adar3_3v3(stm32_cs_adar3_3v3),
.stm32_cs_adar4_3v3(stm32_cs_adar4_3v3),
.stm32_sclk_1v8(stm32_sclk_1v8),
.stm32_mosi_1v8(stm32_mosi_1v8),
.stm32_miso_1v8(stm32_miso_1v8),
.stm32_cs_adar1_1v8(stm32_cs_adar1_1v8),
.stm32_cs_adar2_1v8(stm32_cs_adar2_1v8),
.stm32_cs_adar3_1v8(stm32_cs_adar3_1v8),
.stm32_cs_adar4_1v8(stm32_cs_adar4_1v8),
// Live chirp-index telemetry
.current_chirp(tx_current_chirp),
.new_chirp_frame(tx_new_chirp_frame)
);
// ============================================================================
// RADAR RECEIVER INSTANTIATION
// ============================================================================
radar_receiver_final rx_inst (
.clk(clk_100m_buf),
.reset_n(sys_reset_n),
// Chirp counter from transmitter (CDC-synchronized from 120 MHz domain)
.chirp_counter(tx_current_chirp_sync),
// Frame-start pulse from transmitter (CDC-synchronized toggle→pulse)
.tx_frame_start(tx_new_chirp_frame_sync),
// ADC Physical Interface
.adc_d_p(adc_d_p),
.adc_d_n(adc_d_n),
.adc_dco_p(adc_dco_p),
.adc_dco_n(adc_dco_n),
.adc_or_p(adc_or_p),
.adc_or_n(adc_or_n),
.adc_pwdn(adc_pwdn),
// Doppler Outputs
.doppler_output(rx_doppler_output),
.doppler_valid(rx_doppler_valid),
.doppler_bin(rx_doppler_bin),
.range_bin(rx_range_bin),
// Range-profile outputs
.range_profile_i_out(rx_range_profile[15:0]),
.range_profile_q_out(rx_range_profile[31:16]),
.range_profile_valid_out(rx_range_valid),
.decimated_range_mag_out(rx_range_profile_decimated),
.decimated_range_valid_out(rx_range_profile_decimated_valid),
// Gap 2: Host-configurable chirp timing
.host_long_chirp_cycles(host_long_chirp_cycles),
.host_long_listen_cycles(host_long_listen_cycles),
.host_guard_cycles(host_guard_cycles),
.host_short_chirp_cycles(host_short_chirp_cycles),
.host_short_listen_cycles(host_short_listen_cycles),
// PR-G G2: MEDIUM ladder timings (was hardcoded to RP_DEF_MEDIUM_*)
.host_medium_chirp_cycles(host_medium_chirp_cycles),
.host_medium_listen_cycles(host_medium_listen_cycles),
.host_chirps_per_elev(host_chirps_per_elev),
// PR-U / M-8: sub-frame enable mask, was tied to RP_DEF_SUBFRAME_ENABLE
// inside radar_receiver_final at the chirp_scheduler instance.
.host_subframe_enable(host_subframe_enable),
// Fix 3: digital gain control
.host_gain_shift(host_gain_shift),
// AGC configuration (opcodes 0x28-0x2C)
.host_agc_enable(host_agc_enable),
.host_agc_target(host_agc_target),
.host_agc_attack(host_agc_attack),
.host_agc_decay(host_agc_decay),
.host_agc_holdoff(host_agc_holdoff),
// PR-E: master enable for the scheduler (CDC-sync'd to clk_100m above)
.mixers_enable_100m(stm32_mixers_enable_100m),
// PR-AB.b expanded commit 5: beam-ready handshake plumbing. stm32_beam_ready
// is the raw MCU PD8 GPIO (sync'd inside chirp_scheduler on `clk`).
// host_handshake_enable is the opcode-0x1A register, sampled on clk_100m
// (quasi-static, no extra CDC). beam_handshake_watchdog_fired is sticky in
// the source domain and packed into status_words[4][1] downstream.
.stm32_beam_ready_async(stm32_beam_ready),
.host_handshake_enable(host_handshake_enable),
.beam_handshake_watchdog_fired(rx_beam_handshake_watchdog),
// CFAR: Doppler frame-complete pulse
.doppler_frame_done_out(rx_frame_complete),
// Ground clutter removal
.host_mti_enable(host_mti_enable),
.host_dc_notch_width(host_dc_notch_width),
// AUDIT-C3: ADC format select (opcode 0x33) -> DDC sign-conversion
.host_adc_format(host_adc_format),
// AUDIT-S25: ADC power-down control (opcode 0x32) -> AD9484 PWDN pin
.host_adc_pwdn(host_adc_pwdn),
// ADC debug tap (for self-test / bring-up)
.dbg_adc_i(rx_dbg_adc_i),
.dbg_adc_q(rx_dbg_adc_q),
.dbg_adc_valid(rx_dbg_adc_valid),
// AGC status outputs
.agc_saturation_count(rx_agc_saturation_count),
.agc_peak_magnitude(rx_agc_peak_magnitude),
.agc_current_gain(rx_agc_current_gain),
// DDC overflow diagnostics (audit F-6.1)
.ddc_overflow_any(rx_ddc_overflow_any),
.ddc_saturation_count(rx_ddc_saturation_count),
// MTI saturation count (audit F-6.3)
.mti_saturation_count_out(rx_mti_saturation_count),
// Range-bin decimator watchdog (audit F-6.4)
.range_decim_watchdog(rx_range_decim_watchdog),
.ddc_cic_fir_overrun(rx_ddc_cic_fir_overrun),
// PR-E: scheduler outputs forwarded to TX-side CDC bridge (clk_100m).
.sched_wave_sel_out(sched_wave_sel),
.sched_chirp_pulse_out(sched_chirp_pulse),
.sched_frame_pulse_out(sched_frame_pulse)
);
// ============================================================================
// DOPPLER DATA DECODING
// ============================================================================
// Decode 32-bit doppler output into real and imaginary parts
// Format: {doppler_q[15:0], doppler_i[15:0]}
assign rx_doppler_real = rx_doppler_output[15:0];
assign rx_doppler_imag = rx_doppler_output[31:16];
assign rx_doppler_data_valid = rx_doppler_valid;
// ============================================================================
// DC NOTCH FILTER (post-Doppler-FFT, pre-CFAR)
// ============================================================================
// Zeros out Doppler bins within ±host_dc_notch_width of DC for ALL
// 16-pt sub-frames in the chirp-v2 architecture (3 sub-frames in production).
// doppler_bin[5:0] = {sub_frame[1:0], bin[3:0]}:
// Sub-frame 0: bins 0-15, DC = bin 0, wrap = bin 15
// Sub-frame 1: bins 16-31, DC = bin 16, wrap = bin 31
// Sub-frame 2: bins 32-47, DC = bin 32, wrap = bin 47
// The DC test ignores the sub-frame field and gates on the 4-bit per-FFT bin.
wire dc_notch_active;
wire [`RP_DOPPLER_BIN_WIDTH-1:0] dop_bin_unsigned = rx_doppler_bin;
wire [3:0] bin_within_sf = dop_bin_unsigned[3:0];
// Audit S-1: ±W around DC in a 16-bin FFT covers bins {0..W, 16-W..15}
// (2W+1 total, bin 8 the only one excluded at W=7). The previous form
// `< W || > 15-W+1` missed both boundaries: at W=1 it notched only {0}
// (skipping 1 and 15); at W=7 it missed 7 and 9. Inclusive comparators
// against the 5-bit limits hit the intended set for all W ∈ {1..7}.
wire [4:0] notch_lo = {2'b00, host_dc_notch_width}; // 0..7
wire [4:0] notch_hi = 5'd16 - notch_lo; // 9..16
assign dc_notch_active = (host_dc_notch_width != 3'd0) &&
({1'b0, bin_within_sf} <= notch_lo ||
{1'b0, bin_within_sf} >= notch_hi);
// Notched Doppler data: zero I/Q when in notch zone, pass through otherwise
wire [31:0] notched_doppler_data = dc_notch_active ? 32'd0 : rx_doppler_output;
wire notched_doppler_valid = rx_doppler_valid;
wire [`RP_DOPPLER_BIN_WIDTH-1:0] notched_doppler_bin = rx_doppler_bin;
wire [`RP_RANGE_BIN_WIDTH_MAX-1:0] notched_range_bin = rx_range_bin;
// ============================================================================
// CFAR DETECTOR (replaces simple threshold detector)
// ============================================================================
// Cell-Averaging CFAR with CA/GO/SO modes. When cfg_cfar_enable=0,
// falls back to simple magnitude threshold (backward-compatible).
// See cfar_ca.v for architecture details.
wire cfar_detect_flag;
wire cfar_detect_valid;
wire [`RP_RANGE_BIN_WIDTH_MAX-1:0] cfar_detect_range;
wire [`RP_DOPPLER_BIN_WIDTH-1:0] cfar_detect_doppler;
wire [16:0] cfar_detect_magnitude;
wire [16:0] cfar_detect_threshold;
wire [15:0] cfar_detect_count;
wire cfar_busy_w;
wire [7:0] cfar_status_w;
// PR-G: 2-class adaptive detection now wired through to the USB bulk frame
// (detect_class per cell) and status packet (count_cand + threshold_soft).
wire [`RP_DETECT_CLASS_WIDTH-1:0] cfar_detect_class; // PR-G: NONE/CAND/CONFIRM
wire [16:0] cfar_detect_threshold_soft; // PR-G: soft (candidate) threshold
wire [15:0] cfar_detect_count_cand; // PR-G: per-frame candidate count
cfar_ca cfar_inst (
.clk(clk_100m_buf),
.reset_n(sys_reset_n),
// Doppler processor outputs (DC-notch filtered)
.doppler_data(notched_doppler_data),
.doppler_valid(notched_doppler_valid),
.doppler_bin_in(notched_doppler_bin),
.range_bin_in(notched_range_bin),
.frame_complete(rx_frame_complete),
// Configuration
.cfg_guard_cells(host_cfar_guard),
.cfg_train_cells(host_cfar_train),
.cfg_alpha(host_cfar_alpha),
.cfg_alpha_soft(host_cfar_alpha_soft),
.cfg_cfar_mode(host_cfar_mode),
.cfg_cfar_enable(host_cfar_enable),
.cfg_simple_threshold(host_detect_threshold),
// Detection outputs
.detect_flag(cfar_detect_flag),
.detect_class(cfar_detect_class), // PR-G: 2-bit dense to USB bulk frame
.detect_valid(cfar_detect_valid),
.detect_range(cfar_detect_range),
.detect_doppler(cfar_detect_doppler),
.detect_magnitude(cfar_detect_magnitude),
.detect_threshold(cfar_detect_threshold),
.detect_threshold_soft(cfar_detect_threshold_soft), // PR-G: status_words[6][15:0]
// Status
.detect_count(cfar_detect_count),
.detect_count_cand(cfar_detect_count_cand), // PR-G: status_words[6][31:16]
.cfar_busy(cfar_busy_w),
.cfar_status(cfar_status_w)
);
// Connect CFAR outputs to existing detection signals
// (rx_detect_flag/valid are regs — drive them from CFAR combinationally)
always @(posedge clk_100m_buf or negedge sys_reset_n) begin
if (!sys_reset_n) begin
rx_detect_flag <= 1'b0;
rx_detect_valid <= 1'b0;
rx_detect_class <= `RP_DETECT_NONE;
rx_detect_range <= {`RP_RANGE_BIN_WIDTH_MAX{1'b0}};
rx_detect_doppler <= {`RP_DOPPLER_BIN_WIDTH{1'b0}};
end else begin
rx_detect_flag <= cfar_detect_flag;
rx_detect_valid <= cfar_detect_valid;
rx_detect_class <= cfar_detect_class;
rx_detect_range <= cfar_detect_range;
rx_detect_doppler <= cfar_detect_doppler;
end
end
// ============================================================================
// BOARD BRING-UP SELF-TEST (opcode 0x30 trigger, 0x31 readback)
// ============================================================================
// Exercises key subsystems independently on first power-on.
// ADC data input is tied to real ADC data.
fpga_self_test self_test_inst (
.clk(clk_100m_buf),
.reset_n(sys_reset_n),
.trigger(host_self_test_trigger),
.busy(self_test_busy),
.result_valid(self_test_result_valid),
.result_flags(self_test_result_flags),
.result_detail(self_test_result_detail),
.adc_data_in(rx_dbg_adc_i), // Post-DDC I channel (clk_100m, 16-bit signed)
.adc_valid_in(rx_dbg_adc_valid), // DDC output valid (clk_100m)
.capture_active(self_test_capture_active),
.capture_data(self_test_capture_data),
.capture_valid(self_test_capture_valid)
);
// Latch self-test results when valid (hold until next trigger)
always @(posedge clk_100m_buf or negedge sys_reset_n) begin
if (!sys_reset_n) begin
self_test_flags_latched <= 5'b00000;
self_test_detail_latched <= 8'd0;
end else begin
if (self_test_result_valid) begin
self_test_flags_latched <= self_test_result_flags;
self_test_detail_latched <= self_test_result_detail;
end
end
end
// ============================================================================
// DATA PACKING FOR USB
// ============================================================================
// USB range profile must match the advertised 512-bin frame payload, so source it
// from the decimated range stream that feeds Doppler rather than raw MF samples.
assign usb_range_profile = {16'd0, rx_range_profile_decimated};
assign usb_range_valid = rx_range_profile_decimated_valid;
assign usb_doppler_real = rx_doppler_real;
assign usb_doppler_imag = rx_doppler_imag;
assign usb_doppler_valid = rx_doppler_valid;
assign usb_detect_flag = rx_detect_flag;
assign usb_detect_valid = rx_detect_valid;
assign usb_detect_class = rx_detect_class; // PR-G: 2-bit class to FT2232H
// ============================================================================
// USB DATA INTERFACE INSTANTIATION (parametric: FT601 or FT2232H)
// ============================================================================
generate
if (USB_MODE == 0) begin : gen_ft601
// ---- FT601 USB 3.0 (32-bit, 200T premium board) ----
// PR-AD: brought to v2 bulk parity with FT2232H driver. All status inputs
// mirror the FT2232H instantiation below so the host sees the same
// protocol regardless of board variant.
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_detect_class(usb_detect_class), // PR-G: 2-bit class (PR-AD)
.cfar_valid(usb_detect_valid),
// PR-AD bulk frame protocol inputs - same mux as FT2232H instance.
// RMW samples {range_bin_in, doppler_bin_in} when cfar_valid is high;
// mux cfar coords during CMP so the write address tracks cfar
// (not doppler's stale 511/47 idle state). Doppler-magnitude write
// path is gated on doppler_valid; rx_detect_valid is mutually
// exclusive with doppler_valid so the mux is safe in both cases.
.range_bin_in(rx_detect_valid ? rx_detect_range : notched_range_bin),
.doppler_bin_in(rx_detect_valid ? rx_detect_doppler : notched_doppler_bin),
.frame_complete(rx_frame_complete),
// 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),
// Stream control
.stream_control(host_stream_control),
// PR-U / M-8: per-frame snapshot of host_subframe_enable echoed in
// v2 frame byte 2 bits[5:3] (PR-AD parity).
.subframe_enable(host_subframe_enable),
// Status readback inputs
.status_request(host_status_request),
.status_cfar_threshold(host_detect_threshold),
.status_stream_ctrl(host_stream_control),
.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),
// M-5: MEDIUM PRI readback in status_words[7] (PR-AD parity)
.status_medium_chirp(host_medium_chirp_cycles),
.status_medium_listen(host_medium_listen_cycles),
.status_chirps_per_elev(host_chirps_per_elev),
.status_chirps_mismatch(chirps_mismatch_error),
// 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),
// AGC status readback
.status_agc_current_gain(rx_agc_current_gain),
.status_agc_peak_magnitude(rx_agc_peak_magnitude),
.status_agc_saturation_count(rx_agc_saturation_count),
.status_agc_enable(host_agc_enable),
// AUDIT-S10: control-fault flags exposed in status_words[5][6:5].
.status_range_decim_watchdog(rx_range_decim_watchdog_sticky),
.status_ddc_cic_fir_overrun(rx_ddc_cic_fir_overrun),
// PR-AB.b expanded commit 5: beam-ready handshake watchdog sticky.
.status_beam_handshake_watchdog(rx_beam_handshake_watchdog),
// PR-G: 2-tier CFAR telemetry in status_words[6] (PR-AD parity).
.status_cfar_alpha_soft(host_cfar_alpha_soft),
.status_detect_threshold_soft(cfar_detect_threshold_soft),
.status_detect_count_cand(cfar_detect_count_cand)
);
// 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_detect_class(usb_detect_class), // PR-G: 2-bit class (was 1-bit cfar_detection)
.cfar_valid(usb_detect_valid),
// Bulk frame protocol inputs
// PR-Z A6 (Bug A) fix: usb's RMW samples {range_bin_in, doppler_bin_in}
// when cfar_valid is high. cfar emits per-cell coords during CMP via
// rx_detect_range/doppler — mux them in alongside rx_detect_valid so the
// RMW write address tracks cfar (not doppler's stale 511/47 idle state).
// Doppler-magnitude write path uses the same inputs but is gated on
// doppler_valid; rx_detect_valid is mutually exclusive with doppler_valid
// (BUFFER vs CMP phases) so the mux is safe in both cases.
.range_bin_in(rx_detect_valid ? rx_detect_range : notched_range_bin),
.doppler_bin_in(rx_detect_valid ? rx_detect_doppler : notched_doppler_bin),
.frame_complete(rx_frame_complete),
// 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),
// PR-U / M-8: per-frame snapshot of host_subframe_enable echoed in
// v2 frame byte 2 bits[5:3]. Lets the host detect when an operator
// disabled a sub-frame and downgrade CRT confidence accordingly.
.subframe_enable(host_subframe_enable),
// Status readback inputs
.status_request(host_status_request),
.status_cfar_threshold(host_detect_threshold),
.status_stream_ctrl(host_stream_control),
.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),
// M-5: MEDIUM PRI readback in status_words[7]
.status_medium_chirp(host_medium_chirp_cycles),
.status_medium_listen(host_medium_listen_cycles),
.status_chirps_per_elev(host_chirps_per_elev),
.status_chirps_mismatch(chirps_mismatch_error),
// 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),
// AGC status readback
.status_agc_current_gain(rx_agc_current_gain),
.status_agc_peak_magnitude(rx_agc_peak_magnitude),
.status_agc_saturation_count(rx_agc_saturation_count),
.status_agc_enable(host_agc_enable),
// AUDIT-S10: control-fault flags exposed in status_words[5][6:5]
// for host-side observability (paired with gpio_dig7 split).
// PR-AB.b Step 1: feed the sticky version of the F-6.4 watchdog
// (level signal) so the ft_clk synchronizer cannot miss a 10 ns
// source-domain pulse. F-1.2 overrun is already sticky in source.
.status_range_decim_watchdog(rx_range_decim_watchdog_sticky),
.status_ddc_cic_fir_overrun(rx_ddc_cic_fir_overrun),
// PR-G: 2-tier CFAR telemetry (status_words[6])
.status_cfar_alpha_soft(host_cfar_alpha_soft),
.status_detect_threshold_soft(cfar_detect_threshold_soft),
.status_detect_count_cand(cfar_detect_count_cand),
// PR-AB.b expanded commit 5: beam-ready handshake watchdog sticky.
// Source domain is clk_100m; usb_data_interface_ft2232h hands it to
// ft_clk through the same status-bit CDC convention used for the
// F-6.4 watchdog and F-1.2 overrun stickies above. Packed into
// status_words[4][1] downstream — see word-4 layout note in
// usb_data_interface_ft2232h.v.
.status_beam_handshake_watchdog(rx_beam_handshake_watchdog)
);
// 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)
// ============================================================================
// cmd_valid is a 1-cycle pulse in ft601_clk. Use toggle CDC (same pattern
// as chirp_frame_toggle_120m above) to safely transfer it to clk_100m.
// cmd_data/opcode/addr/value are held stable after cmd_valid pulses, so
// we simply sample them in clk_100m when the CDC'd pulse arrives.
// Step 1: Toggle on cmd_valid pulse (ft601_clk domain)
//
// CDC INVARIANT (audit F-1.1): usb_cmd_opcode / usb_cmd_addr / usb_cmd_value
// / usb_cmd_data MUST be driven to their final values BEFORE usb_cmd_valid
// asserts, and held stable for at least (STAGES + 1) clk_100m cycles after
// (i.e., until cmd_valid_100m has pulsed in the destination domain). These
// buses cross from ft601_clk to clk_100m as quasi-static data, NOT through
// a synchronizer — only the toggle bit above is CDC'd. If a future edit
// moves the cmd_* register write to the SAME cycle as the toggle flip, or
// drops the stability hold, the clk_100m sampler at the command decoder
// will latch metastable bits and dispatch on a garbage opcode.
// The source-side FSM in usb_data_interface_ft2232h.v / usb_data_interface.v
// currently satisfies this by assigning the cmd_* buses several cycles
// before pulsing cmd_valid and leaving them stable until the next command.
reg cmd_valid_toggle_ft601;
always @(posedge ft601_clk_buf or negedge sys_reset_ft601_n) begin
if (!sys_reset_ft601_n)
cmd_valid_toggle_ft601 <= 1'b0;
else if (usb_cmd_valid)
cmd_valid_toggle_ft601 <= ~cmd_valid_toggle_ft601;
end
// Step 2: Synchronize toggle to clk_100m domain (3-stage)
wire cmd_valid_toggle_100m;
cdc_single_bit #(
.STAGES(3)
) cdc_cmd_valid (
.src_clk(ft601_clk_buf),
.dst_clk(clk_100m_buf),
.reset_n(sys_reset_n),
.src_signal(cmd_valid_toggle_ft601),
.dst_signal(cmd_valid_toggle_100m)
);
// Step 3: Edge-detect toggle to recover pulse in clk_100m domain
reg cmd_valid_toggle_100m_prev;
always @(posedge clk_100m_buf or negedge sys_reset_n) begin
if (!sys_reset_n)
cmd_valid_toggle_100m_prev <= 1'b0;
else
cmd_valid_toggle_100m_prev <= cmd_valid_toggle_100m;
end
wire cmd_valid_100m = cmd_valid_toggle_100m ^ cmd_valid_toggle_100m_prev;
// Step 4: Command decode registers in clk_100m domain
// Sample cmd_data fields when CDC'd valid pulse arrives. Data is stable
// because the read FSM holds cmd_opcode/addr/value until the next command.
// NOTE: reg declarations for host_detect_threshold, host_stream_control are
// in INTERNAL SIGNALS section above (before rx_inst) to avoid Icarus Verilog
// forward-reference errors.
always @(posedge clk_100m_buf or negedge sys_reset_n) begin
if (!sys_reset_n) begin
host_detect_threshold <= 16'd10000; // Default threshold
host_stream_control <= `RP_STREAM_CTRL_DEFAULT; // Default: all streams, mag-only mode
host_gain_shift <= 4'd0; // Default: pass-through (no gain change)
// Gap 2: chirp timing defaults (forwarded to chirp_scheduler).
// SHORT bumped to 100 cycles (1 us) for chirp-v2 SHORT waveform.
host_long_chirp_cycles <= 16'd`RP_DEF_LONG_CHIRP_CYCLES;
host_long_listen_cycles <= 16'd`RP_DEF_LONG_LISTEN_CYCLES;
host_guard_cycles <= 16'd`RP_DEF_GUARD_CYCLES;
host_short_chirp_cycles <= 16'd`RP_DEF_SHORT_CHIRP_CYCLES_V2;
host_short_listen_cycles <= 16'd`RP_DEF_SHORT_LISTEN_CYCLES_V2;
host_medium_chirp_cycles <= 16'd`RP_DEF_MEDIUM_CHIRP_CYCLES; // PR-G G2
host_medium_listen_cycles <= 16'd`RP_DEF_MEDIUM_LISTEN_CYCLES; // PR-G G2
// PR-F bumped RP_CHIRPS_PER_FRAME 32 -> 48 (3 sub-frames * 16); the
// chirps_per_elev register is echoed in status word 3 and used by host
// sanity-checking. Keep cold-reset value in lockstep with the truth.
host_chirps_per_elev <= 6'd48;
// PR-U / M-8: 3'b111 = SHORT|MEDIUM|LONG all on (production 3-PRI ladder).
host_subframe_enable <= `RP_DEF_SUBFRAME_ENABLE;
// PR-AB.b expanded commit 5: handshake disabled at cold-reset so any
// host or TB that doesn't drive PD8 keeps the legacy open-loop cadence.
// Production GUI writes 0x1A=1 after MCU has been verified.
host_handshake_enable <= 1'b0;
host_status_request <= 1'b0;
chirps_mismatch_error <= 1'b0;
// CFAR defaults (disabled by default — backward-compatible)
host_cfar_guard <= 4'd2; // 2 guard cells each side
host_cfar_train <= 5'd8; // 8 training cells each side
host_cfar_alpha <= `RP_DEF_CFAR_ALPHA; // 3.0 (Q4.4)
host_cfar_alpha_soft <= `RP_DEF_CFAR_ALPHA_SOFT; // 1.5 (Q4.4) — PR-F
host_cfar_mode <= 2'b00; // CA-CFAR
host_cfar_enable <= 1'b0; // Disabled (simple threshold)
// Ground clutter removal defaults (disabled — backward-compatible)
host_mti_enable <= 1'b0; // MTI off
host_dc_notch_width <= 3'd0; // DC notch off
// AGC defaults (disabled — backward-compatible with manual gain)
host_agc_enable <= 1'b0; // AGC off (manual gain)
host_agc_target <= 8'd200; // Target peak magnitude
host_agc_attack <= 4'd1; // 1-step gain-down on clipping
host_agc_decay <= 4'd1; // 1-step gain-up when weak
host_agc_holdoff <= 4'd4; // 4 frames before gain-up
// Self-test defaults
host_self_test_trigger <= 1'b0; // Self-test idle
// AUDIT-C3: ADC format default (offset-binary matches SJ1 default)
host_adc_format <= 2'b00;
// AUDIT-S25: AD9484 PWDN default = 0 (ADC powered up; matches the
// historical hard-tied state at radar_receiver_final.v:246 prior to
// this fix, so existing bringup behavior is preserved).
host_adc_pwdn <= 1'b0;
end else begin
host_status_request <= 1'b0; // Self-clearing pulse
host_self_test_trigger <= 1'b0; // Self-clearing pulse
if (cmd_valid_100m) begin
case (usb_cmd_opcode)
// 0x01 (RADAR_MODE) and 0x02 (TRIGGER_PULSE) retired in
// PR-AB.b expanded — single-mode FSM has no mode field.
8'h03: host_detect_threshold <= usb_cmd_value;
// PR-G: protocol v2 has a single canonical encoding
// (Manhattan-mag doppler + 2-bit dense detect). Bits [5:3] of
// host_stream_control are RESERVED — host SHOULD write 0, RTL
// ignores them. The legacy AUDIT-C9 force-clamp + INERT FLAGS
// logic was removed when v1 was retired.
8'h04: host_stream_control <= {3'b000, usb_cmd_value[2:0]};
// Gap 2: chirp timing configuration
8'h10: host_long_chirp_cycles <= usb_cmd_value;
8'h11: host_long_listen_cycles <= usb_cmd_value;
8'h12: host_guard_cycles <= usb_cmd_value;
8'h13: host_short_chirp_cycles <= usb_cmd_value;
8'h14: host_short_listen_cycles <= usb_cmd_value;
// PR-G G2: MEDIUM ladder timings
8'h17: host_medium_chirp_cycles <= usb_cmd_value;
8'h18: host_medium_listen_cycles <= usb_cmd_value;
// PR-U / M-8: sub-frame enable mask {LONG, MEDIUM, SHORT} =
// {value[2], value[1], value[0]}. Surfaced in v2 frame byte 2
// bits[5:3] so host CRT can detect mask != 3'b111 and degrade
// confidence rather than mis-attribute the SF axis.
8'h19: host_subframe_enable <= usb_cmd_value[2:0];
// PR-AB.b expanded commit 5: beam-ready handshake enable.
// value[0]=1 makes chirp_scheduler stall in S_BEAM_WAIT after
// each frame_pulse until the MCU toggles PD8 (or watchdog
// ~80 ms). value[0]=0 reverts to the legacy open-loop cadence.
8'h1A: host_handshake_enable <= usb_cmd_value[0];
8'h15: begin
// Fix 4: Clamp chirps_per_elev to the fixed Doppler frame size.
// If host requests a different value, clamp and set error flag.
if (usb_cmd_value[5:0] > DOPPLER_FRAME_CHIRPS[5:0]) begin
host_chirps_per_elev <= DOPPLER_FRAME_CHIRPS[5:0];
chirps_mismatch_error <= 1'b1;
end else if (usb_cmd_value[5:0] == 6'd0) begin
host_chirps_per_elev <= DOPPLER_FRAME_CHIRPS[5:0];
chirps_mismatch_error <= 1'b1;
end else begin
host_chirps_per_elev <= usb_cmd_value[5:0];
// Clear error only if value matches FFT size exactly
chirps_mismatch_error <= (usb_cmd_value[5:0] != DOPPLER_FRAME_CHIRPS[5:0]);
end
end
8'h16: host_gain_shift <= usb_cmd_value[3:0]; // Fix 3: digital gain
// 0x20 (RANGE_MODE) retired in PR-AB.b expanded — runtime
// 3km/20km presentation is driven by host_subframe_enable +
// per-waveform chirp/listen-cycles on a 200T build.
// CFAR configuration opcodes
8'h21: host_cfar_guard <= usb_cmd_value[3:0];
8'h22: host_cfar_train <= usb_cmd_value[4:0];
8'h23: host_cfar_alpha <= usb_cmd_value[7:0];
8'h24: host_cfar_mode <= usb_cmd_value[1:0];
8'h25: host_cfar_enable <= usb_cmd_value[0];
// Ground clutter removal opcodes
8'h26: host_mti_enable <= usb_cmd_value[0];
8'h27: host_dc_notch_width <= usb_cmd_value[2:0];
// AGC configuration opcodes
8'h28: host_agc_enable <= usb_cmd_value[0];
8'h29: host_agc_target <= usb_cmd_value[7:0];
8'h2A: host_agc_attack <= usb_cmd_value[3:0];
8'h2B: host_agc_decay <= usb_cmd_value[3:0];
8'h2C: host_agc_holdoff <= usb_cmd_value[3:0];
// PR-G: 2-tier CFAR — soft (candidate) threshold multiplier.
// Default RP_DEF_CFAR_ALPHA_SOFT = 0x18 (1.5 in Q4.4, Pfa~10⁻⁵).
8'h2D: host_cfar_alpha_soft <= usb_cmd_value[7:0];
// Board bring-up self-test opcodes
8'h30: host_self_test_trigger <= 1'b1; // Trigger self-test
8'h31: host_status_request <= 1'b1; // Self-test readback (status alias)
// 0x31: readback handled via status mechanism (latched results)
// AUDIT-S25: AD9484 power-down control (active-high). Lets MCU
// recover the ADC from a stuck state without dropping main power.
8'h32: host_adc_pwdn <= usb_cmd_value[0];
// AUDIT-C3: ADC format select (matches AD9484 SCLK/DFS strap SJ1).
8'h33: host_adc_format <= usb_cmd_value[1:0];
8'hFF: host_status_request <= 1'b1; // Gap 2: status readback
default: ;
endcase
end
end
end
// ============================================================================
// OUTPUT ASSIGNMENTS
// ============================================================================
assign current_chirp = tx_current_chirp_sync; // Use CDC-synchronized version
assign new_chirp_frame = tx_new_chirp_frame_sync; // Use CDC-synchronized version
assign dbg_doppler_data = rx_doppler_output;
assign dbg_doppler_valid = rx_doppler_valid;
assign dbg_doppler_bin = rx_doppler_bin;
assign dbg_range_bin = rx_range_bin;
// ============================================================================
// SYSTEM STATUS MONITORING
// ============================================================================
always @(posedge clk_100m_buf or negedge sys_reset_n) begin
if (!sys_reset_n) begin
status_reg <= 4'b0000;
end else begin
status_reg[0] <= stm32_mixers_enable_100m; // Mixers enabled (CDC sync'd)
status_reg[1] <= ft601_txe_100m; // USB TX ready (CDC sync'd)
status_reg[2] <= rx_doppler_valid; // Data valid
status_reg[3] <= tx_new_chirp_frame_sync; // New chirp frame (CDC-sync'd)
end
end
assign system_status = status_reg;
// ============================================================================
// FPGA→STM32 GPIO OUTPUTS (DIG_5, DIG_6, DIG_7) — AUDIT-S10 SPLIT + PR-AB.b
// ============================================================================
// AUDIT-S10: gpio_dig5 previously OR'd six unrelated flags (signal-saturation
// AND control-faults), so the MCU outer-loop AGC could not distinguish
// "I'm clipping, reduce RF gain" from "FFT chain stalled, reset me". Gain
// reduction is the wrong response for a watchdog/CDC stall; it just hides the
// stall behind a quiet receive chain. The split routes the two control-fault
// classes (audit F-6.4 range-decimator watchdog, audit F-1.2 CIC→FIR CDC
// overrun) to gpio_dig7 (PD15) so the MCU can react differently — log + reset
// FPGA — without affecting the AGC loop. Status word visibility is added in
// usb_data_interface[*_ft2232h].v so the host can graph each fault class
// regardless of MCU consumption.
//
// PR-AB.b: gpio_dig6 is reassigned from a host_agc_enable mirror to the
// chirp_scheduler frame_pulse, stretched to ~100 ns for clean STM32 EXTI
// capture on PD14. The STM32 firmware swaps HAL_Delay(BEAM_PATTERN_DWELL_MS)
// for an EXTI-driven semaphore acquire so per-pattern dwell tracks the
// actual ladder length — drift-free, mask-aware (a 2-subframe 3 km variant
// runs at the shorter cadence automatically because chirp_scheduler.frame_pulse
// fires at the wrap of whatever subframes are enabled). Outer-loop AGC moves
// to always-on in production firmware (saturation handling shouldn't be
// optional); host_agc_enable register stays for status display + bench build
// flag MCU_AGC_FORCE_DISABLED keeps a manual-gain path for ADAR1000
// calibration / level checks / RX gain sweeps.
//
// DIG_5 (PD13): Signal-chain saturation — outer-loop AGC reduces RF gain.
// Asserts on AGC clipping, DDC mixer/filter overflow, or MTI
// 2-pulse saturation (audit F-6.1/F-6.3).
// DIG_6 (PD14): Stretched chirp_scheduler frame_pulse for STM32 dwell sync.
// 1-cycle source pulse @ 100 MHz, stretched to 10 cycles
// (100 ns). Period tracks the enabled-subframe ladder
// (8.05 ms full 3-PRI, 5.38 ms SHORT+MEDIUM, 2.80 ms SHORT-only).
// DIG_7 (PD15): Control-chain fault — MCU should log + consider FPGA reset.
// Asserts on range-decimator watchdog (audit F-6.4) or CIC→FIR
// CDC overrun (audit F-1.2). PD15 stuck-high sampler in MCU
// main loop fires attemptErrorRecovery(ERROR_FPGA_DSP_STALL).
assign gpio_dig5 = (rx_agc_saturation_count != 8'd0)
| rx_ddc_overflow_any
| (rx_ddc_saturation_count != 3'd0)
| (rx_mti_saturation_count != 8'd0);
// PR-AB.b: 10-cycle stretcher on sched_frame_pulse. STM32 EXTI on PD14 needs
// ≥2 APB clocks (~12 ns @ 168 MHz APB) to latch the rising edge; 100 ns gives
// generous margin while staying well below the shortest dwell (2.80 ms =
// SHORT-only) so back-to-back pulses never overlap.
reg [3:0] frame_pulse_stretch_count;
always @(posedge clk_100m_buf or negedge sys_reset_n) begin
if (!sys_reset_n)
frame_pulse_stretch_count <= 4'd0;
else if (sched_frame_pulse)
frame_pulse_stretch_count <= 4'd10; // 10 cycles = 100 ns @ 100 MHz
else if (frame_pulse_stretch_count != 4'd0)
frame_pulse_stretch_count <= frame_pulse_stretch_count - 4'd1;
end
assign gpio_dig6 = (frame_pulse_stretch_count != 4'd0);
assign gpio_dig7 = rx_range_decim_watchdog // audit F-6.4
| rx_ddc_cic_fir_overrun; // audit F-1.2
// ============================================================================
// DEBUG AND VERIFICATION
// ============================================================================
`ifdef SIMULATION
// Simulation-only debug monitoring
reg [31:0] debug_cycle_counter;
reg [31:0] data_packet_counter;
always @(posedge clk_100m_buf) begin
debug_cycle_counter <= debug_cycle_counter + 1;
if (tx_new_chirp_frame_sync) begin
$display("[TOP] New chirp frame started at cycle %0d", debug_cycle_counter);
end
if (rx_doppler_valid) begin
data_packet_counter <= data_packet_counter + 1;
if (data_packet_counter < 10) begin
$display("[TOP] Doppler data[%0d]: bin=%0d, range=%0d, I=%0d, Q=%0d",
data_packet_counter, rx_doppler_bin, rx_range_bin,
rx_doppler_real, rx_doppler_imag);
end
end
if (data_packet_counter == 100) begin
$display("[TOP] First 100 doppler packets processed");
end
end
`endif
endmodule