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