PLFM_RADAR/9_Firmware/9_2_FPGA/radar_system_top.v
Jason ada170ef1f feat(fpga,mcu,gui): PR-AB.b — drift-free dwell sync via DIG_6 frame_pulse + AGC always-on policy
FPGA (Phase 1+2):
- gpio_dig6 (PD14) now carries chirp_scheduler frame_pulse, FPGA-stretched
  to ~100 ns so the STM32 EXTI on PD14 can latch reliably.
- gpio_dig7 (PD15) returns to its pre-PR-AB.b role: control-fault OR
  (range_decim_watchdog | CDC overrun); MCU stuck-high sampler unchanged.
- rx_range_decim_watchdog gains a sticky in source clock domain so a slow
  status poll cannot miss a 1-cycle assertion (Phase 1).
- New tb_dig6_frame_pulse.v (13 checks); tb_status_words_stickies.v extended
  with DIG_7 fault-OR coverage (14 checks); retired tb_audit_s10_gpio_split.v.
- Port comments in radar_system_top.v / _50t.v and XDC roles refreshed.

MCU (Phase 3):
- PD14 reconfigured to GPIO_MODE_IT_RISING + GPIO_PULLDOWN; new
  EXTI15_10_IRQHandler in stm32f7xx_it.c dispatches to HAL_GPIO_EXTI_Callback
  that bumps a volatile g_frame_pulse_count.
- runRadarPulseSequence dwell loop replaces 3x HAL_Delay(8) with
  waitForFramePulse(20) — per-pattern dwell now tracks the actual mask-aware
  ladder length (drift-free, mask-aware), with a 20 ms timeout safety net.
- AGC outer loop is ALWAYS-ON in production (compile-time policy); bench
  builds compile the body out via -DMCU_AGC_FORCE_DISABLED. The runtime
  enable/debounce + DIG_6 polling that previously gated AGC are removed.
- main.h adds FPGA_FRAME_PULSE_* aliases pointing at FPGA_DIG6_*.

GUI (Phase 4):
- Settings tab gains a Bench / Diagnostics group with a BENCH-MODE checkbox
  (off by default, persisted via QSettings).
- AGC group header swaps between a green "AGC: ALWAYS-ON" badge (production)
  and Enable/Disable AGC buttons (bench), pinned to the top of the group.
  The redundant 0/1 spinbox row for opcode 0x28 is removed — buttons send
  the same opcode and cannot accept invalid input.
- Both the FPGA Control AGC Status box and the AGC Monitor strip share a
  helper that honours bench-mode in production (always shows ALWAYS-ON in
  green so the two views never disagree with the badge).
- _add_fpga_param_row uses setFixedWidth on label and Set button + explicit
  stretch=1 on the hint, so all rows align column-wise whether they sit
  directly in a QVBoxLayout or inside a wrapper QWidget.

Regression: FPGA 42/0/0 (PR-M.4 baseline) - MCU 34/34 - GPS extended 51/51
- GUI v7 150/150 - BENCH-MODE flip behaviorally verified.
Hardware-blocked steps deferred: bench-scope verify (PD14 dwell pulse,
counter advance, PD15 stuck-high recovery still triggers).

Closes #182.
2026-05-07 13:29:48 +05:45

1357 lines
61 KiB
Verilog

`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)
*
* 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 ==========
// Chirp/Beam Control (toggle signals from STM32)
input wire stm32_new_chirp,
input wire stm32_new_elevation,
input wire stm32_new_azimuth,
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 ==========
// Beam position tracking
output wire [5:0] current_elevation,
output wire [5:0] current_azimuth,
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_elevation;
wire [5:0] tx_current_azimuth;
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;
// 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 [1:0] host_radar_mode;
reg host_trigger_pulse;
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)
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
// Range-mode register (opcode 0x20)
// Controls chirp type selection in the mode controller:
// 2'b00 = 3 km mode (all short chirps — long blind zone > max range)
// 2'b01 = Long-range (dual chirp: first half long, second half short)
// 2'b10 = Reserved
// 2'b11 = Reserved
reg [1:0] host_range_mode;
// 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 Control Interface (chirp moved to scheduler — only beam-step here)
.stm32_new_elevation(stm32_new_elevation),
.stm32_new_azimuth(stm32_new_azimuth),
.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),
// Beam Position Tracking
.current_elevation(tx_current_elevation),
.current_azimuth(tx_current_azimuth),
.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),
.host_mode(host_radar_mode),
.host_trigger(host_trigger_pulse),
.host_range_mode(host_range_mode),
// 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),
// STM32 toggle signals for the RX scheduler (mode 00 pass-through).
// Raw GPIO inputs — chirp_scheduler's edge detectors handle debouncing.
.stm32_new_chirp_rx(stm32_new_chirp),
.stm32_new_elevation_rx(stm32_new_elevation),
.stm32_new_azimuth_rx(stm32_new_azimuth),
// PR-E: master enable for the scheduler (CDC-sync'd to clk_100m above)
.mixers_enable_100m(stm32_mixers_enable_100m),
// 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) ----
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),
// 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),
.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)
);
// 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_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_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)
);
// 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_radar_mode, host_trigger_pulse,
// 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_radar_mode <= 2'b01; // Default: auto-scan
host_trigger_pulse <= 1'b0;
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;
host_status_request <= 1'b0;
chirps_mismatch_error <= 1'b0;
host_range_mode <= 2'b00; // Default: 3 km mode (all short chirps)
// 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_trigger_pulse <= 1'b0; // Self-clearing pulse
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)
8'h01: host_radar_mode <= usb_cmd_value[1:0];
8'h02: host_trigger_pulse <= 1'b1;
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];
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
8'h20: host_range_mode <= usb_cmd_value[1:0]; // Range mode
// 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_elevation = tx_current_elevation;
assign current_azimuth = tx_current_azimuth;
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