PLFM_RADAR/9_Firmware/9_2_FPGA/radar_system_top.v
Jason 049f7b5d14 fix(fpga): PR-Q.1 — stagger MEDIUM PRI 175→161 µs for 3-PRI Doppler CRT (C-5)
Bumps RP_DEF_MEDIUM_LISTEN_CYCLES 17000 → 15600 so MEDIUM PRI = 161 µs,
distinct from SHORT (175 µs) and LONG (167 µs). Three coprime PRIs let
the host run 3-PRI Chinese-Remainder unfolding on Doppler aliases beyond
the per-sub-frame ±~41 m/s unambiguous range — closes the FPGA half of
audit C-5 (PR-F Doppler ambiguity unfolding).

Stagger choice (proposal B):
  SHORT  175 µs — chirp 1   + listen 174
  MEDIUM 161 µs — chirp 5   + listen 156   (PR-Q, was 175)
  LONG   167 µs — chirp 30  + listen 137

In 3 km mode LONG is blind (4500 m blind zone) → SHORT-vs-MEDIUM
(Δ=14 µs / 8 %) is the operative pair; in 20 km mode MEDIUM-vs-LONG
(Δ=6 µs / 4 %) carries the long-range slice that has SNR for both.
Listens picked to differ by ≥5 % so the alias resolver is robust against
the 5.1 m/s/bin Doppler quantization.

Architecture is unchanged — chirp_scheduler.v already takes per-waveform
host_*_listen_cycles. doppler_processor.v / cfar_ca.v are PRI-agnostic
and just tag Doppler outputs with sub_frame ID; host-side CRT lives in
v7/processing.py (PR-Q.5, follow-on).

Files:
  radar_params.vh:240        RP_DEF_MEDIUM_LISTEN_CYCLES 17000 → 15600
  radar_params.vh:217-228    block comment: stagger rationale + Δ math
  radar_system_top.v:273     port-list comment: default 17000 → 15600
  radar_system_top.v:278-282 staggered-PRI block comment: 3-ladder PRI
  doppler_processor.v:25-30  reference v7/processing.py CRT unfolder
  tb/tb_radar_receiver_final.v:199-202  list MEDIUM=15600 in real-values

Validation: full iverilog regression 42 PASS / 0 FAIL / 1 SKIP (pre-
existing scipy availability) — same baseline as post-PR-O.8. No TB
default-value asserts touched (tb_system_opcodes / tb_usb_protocol_v2
both use literal 16500 for opcode 0x18 round-trip).

Follow-on: PR-Q.2 (cosim T_PRI_MEDIUM align + golden regen), PR-Q.4-7
(v7 GUI 3-PRI CRT unfolder + AMBIGUOUS confidence display), PR-Q.8
(memory close-out). MCU executeChirpSequence is live but PRI-agnostic
in production mode 2'b01 (FPGA auto-scan) — pre-existing 2-ladder
staleness vs chirp-v2 3-ladder, defer to PR-H or dedicated MCU PR.
2026-05-02 13:37:06 +05:45

1255 lines
55 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): AGC enable flag (mirrors host_agc_enable)
output wire gpio_dig7 // DIG_7 (H12→PD15): reserved (tied low)
);
// ============================================================================
// 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;
// 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). High = decimator stalled.
wire rx_range_decim_watchdog;
// 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.
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 32)
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),
// 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];
assign dc_notch_active = (host_dc_notch_width != 3'd0) &&
(bin_within_sf < {1'b0, host_dc_notch_width} ||
bin_within_sf > (4'd15 - {1'b0, host_dc_notch_width} + 4'd1));
// 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;
end else begin
rx_detect_flag <= cfar_detect_flag;
rx_detect_valid <= cfar_detect_valid;
rx_detect_class <= cfar_detect_class;
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)
.status_range_decim_watchdog(rx_range_decim_watchdog),
.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
.range_bin_in(notched_range_bin),
.doppler_bin_in(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),
// 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)
.status_range_decim_watchdog(rx_range_decim_watchdog),
.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
host_chirps_per_elev <= 6'd32;
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;
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
// ============================================================================
// 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.
//
// 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): AGC enable mirror (host_agc_enable) — single source of truth.
// 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). MCU consumption is a tracked
// follow-up; until then the host telemetry path covers it.
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);
assign gpio_dig6 = host_agc_enable;
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