PLFM_RADAR/9_Firmware/9_2_FPGA/radar_receiver_final.v
Jason 4989c33aa6 PR-AB.b expanded commit 5: Beam-ready handshake (RTL + MCU + GUI)
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.
2026-05-11 12:07:08 +05:45

676 lines
28 KiB
Verilog
Raw Blame History

This file contains ambiguous Unicode characters

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

`timescale 1ns / 1ps
`include "radar_params.vh"
module radar_receiver_final (
input wire clk, // 100MHz
input wire reset_n,
// ADC Physical Interface (LVDS Inputs)
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,
// Chirp counter from transmitter (for matched filter indexing)
input wire [5:0] chirp_counter,
// Frame-start pulse from transmitter (CDC-synchronized, 1 clk_100m cycle)
input wire tx_frame_start,
output wire [31:0] doppler_output,
output wire doppler_valid,
output wire [`RP_DOPPLER_BIN_WIDTH-1:0] doppler_bin,
output wire [`RP_RANGE_BIN_WIDTH_MAX-1:0] range_bin, // 9-bit
// Raw matched-filter output (debug/bring-up)
output wire signed [15:0] range_profile_i_out,
output wire signed [15:0] range_profile_q_out,
output wire range_profile_valid_out,
// Decimated 512-bin range profile (for USB bulk frames / downstream consumers)
output wire [15:0] decimated_range_mag_out,
output wire decimated_range_valid_out,
// Host command inputs (CDC-synchronized in radar_system_top.v)
// Gap 2: Host-configurable chirp timing
input wire [15:0] host_long_chirp_cycles,
input wire [15:0] host_long_listen_cycles,
input wire [15:0] host_guard_cycles,
input wire [15:0] host_short_chirp_cycles,
input wire [15:0] host_short_listen_cycles,
// PR-G G2: MEDIUM ladder timings (was hardcoded to RP_DEF_MEDIUM_*)
input wire [15:0] host_medium_chirp_cycles,
input wire [15:0] host_medium_listen_cycles,
input wire [5:0] host_chirps_per_elev,
// PR-U / M-8: sub-frame enable mask {LONG, MEDIUM, SHORT}. Was tied to
// RP_DEF_SUBFRAME_ENABLE here at the chirp_scheduler instance; routed
// through radar_system_top opcode 0x19 so the host owns the mask.
input wire [2:0] host_subframe_enable,
// Digital gain control (Fix 3: between DDC output and matched filter)
// [3]=direction: 0=amplify(left shift), 1=attenuate(right shift)
// [2:0]=shift amount: 0..7 bits. Default 0 = pass-through.
input wire [3:0] host_gain_shift,
// AGC configuration (opcodes 0x28-0x2C, active only when agc_enable=1)
input wire host_agc_enable, // 0x28: 0=manual, 1=auto AGC
input wire [7:0] host_agc_target, // 0x29: target peak magnitude
input wire [3:0] host_agc_attack, // 0x2A: gain-down step on clipping
input wire [3:0] host_agc_decay, // 0x2B: gain-up step when weak
input wire [3:0] host_agc_holdoff, // 0x2C: frames before gain-up
// PR-E: master mixers_enable in clk_100m domain — gates the scheduler
// so it stays in S_IDLE until the operator turns the radar on.
input wire mixers_enable_100m,
// PR-AB.b expanded commit 5: beam-ready handshake plumbing.
// stm32_beam_ready_async is the raw MCU PD8 toggle (CDC-synchronized
// inside chirp_scheduler on `clk`). host_handshake_enable gates whether
// the scheduler stalls in S_BEAM_WAIT after each frame_pulse.
// beam_handshake_watchdog_fired is sticky (cleared only on reset_n) and
// surfaces in status_words[4][1] at the top level.
input wire stm32_beam_ready_async,
input wire host_handshake_enable,
output wire beam_handshake_watchdog_fired,
// CFAR integration: expose Doppler frame_complete to top level
output wire doppler_frame_done_out,
// Ground clutter removal controls
input wire host_mti_enable, // 1=MTI active, 0=pass-through
input wire [2:0] host_dc_notch_width, // DC notch: zero Doppler bins within ±width of DC
// AUDIT-C3: AD9484 sign-conversion select (opcode 0x33). Selects DDC
// sign-conversion to match the SCLK/DFS strap (SJ1) on the Main Board.
// 2'b00 = offset-binary (default), 2'b01 = two's-complement.
input wire [1:0] host_adc_format,
// AUDIT-S25: AD9484 power-down control (opcode 0x32). Active-high per
// AD9484 datasheet ("Power-Down (PWDN)" section). 1'b0 = ADC powered up
// (default), 1'b1 = PWDN asserted. Lets the MCU recover the ADC from a
// stuck state without dropping main power. Pin drives AD9484 PWDN net via
// the R36/R37 divider on the Main Board (CMOS thresholds, no level
// translation needed). Stable single-bit level — no CDC needed.
input wire host_adc_pwdn,
// ADC raw data tap (clk_100m domain, post-DDC, for self-test / debug)
output wire [15:0] dbg_adc_i, // DDC output I (16-bit signed, 100 MHz)
output wire [15:0] dbg_adc_q, // DDC output Q (16-bit signed, 100 MHz)
output wire dbg_adc_valid, // DDC output valid (100 MHz)
// AGC status outputs (for status readback / STM32 outer loop)
output wire [7:0] agc_saturation_count, // Per-frame clipped sample count
output wire [7:0] agc_peak_magnitude, // Per-frame peak (upper 8 bits)
output wire [3:0] agc_current_gain, // Effective gain_shift encoding
// DDC overflow diagnostics (audit F-6.1 — previously deleted at boundary).
// Not yet plumbed into the USB status packet (protocol contract is frozen);
// exposed here for gpio aggregation and ILA mark_debug visibility.
output wire ddc_overflow_any,
output wire [2:0] ddc_saturation_count,
// MTI 2-pulse canceller saturation count (audit F-6.3).
output wire [7:0] mti_saturation_count_out,
// Range-bin decimator watchdog (audit F-6.4 — previously tied off
// with an ILA-only note). A high pulse here means the decimator
// FSM has not seen the expected number of input samples within
// its timeout window, i.e. the upstream FIR/CDC has stalled.
output wire range_decim_watchdog,
// Audit F-1.2: sticky CIC→FIR CDC overrun flag. Asserts on the first
// silent sample drop between the 400 MHz CIC output and the 100 MHz
// FIR input; stays high until the next reset. OR'd into the GPIO
// diagnostic bit at the top level.
output wire ddc_cic_fir_overrun,
// chirp_scheduler outputs exposed for the TX-side CDC bridge (PR-E).
// sched_chirp_pulse: 1-cycle pulse on clk that announces "begin chirp now"
// sched_wave_sel: waveform identity rail valid alongside chirp_pulse
// sched_frame_pulse: 1-cycle pulse on frame boundary (chirp_counter wrap)
output wire [1:0] sched_wave_sel_out,
output wire sched_chirp_pulse_out,
output wire sched_frame_pulse_out
);
// ========== INTERNAL SIGNALS ==========
// chirp_counter is an input port (driven by the transmitter — bug NEW-1).
// chirp_scheduler emits the canonical wave_sel rail and 1-cycle chirp_pulse;
// no use_long_chirp shim and no mc_new_*-toggle XOR converters.
wire [1:0] wave_sel;
wire chirp_pulse;
wire subframe_pulse; // unused on RX in PR-D; doppler picks up in PR-F
wire frame_pulse; // unused on RX in PR-D; PR-F doppler driver
wire [5:0] sched_chirp_counter;
wire [1:0] sched_subframe_id;
wire [15:0] sched_cfg_chirp_cycles, sched_cfg_listen_cycles, sched_cfg_guard_cycles;
wire [1:0] segment_request;
wire mem_request;
wire [15:0] ref_i, ref_q;
wire mem_ready;
wire [15:0] adc_i_scaled, adc_q_scaled;
wire adc_valid_sync;
// Gain-controlled signals (between DDC output and matched filter)
wire signed [15:0] gc_i, gc_q;
wire gc_valid;
wire [7:0] gc_saturation_count; // Diagnostic: per-frame clipped sample counter
wire [7:0] gc_peak_magnitude; // Diagnostic: per-frame peak magnitude
wire [3:0] gc_current_gain; // Diagnostic: effective gain_shift
// Reference signal for the processing chain (carries SHORT/MEDIUM/LONG ref
// depending on wave_sel — selected by chirp_reference_rom).
wire [15:0] ref_chirp_real, ref_chirp_imag;
// ========== DOPPLER PROCESSING SIGNALS ==========
wire [31:0] range_data_32bit;
wire range_data_valid;
wire new_chirp_frame;
// Doppler processor outputs
wire [31:0] doppler_spectrum;
wire doppler_spectrum_valid;
wire [4:0] doppler_bin_out;
wire doppler_processing;
// frame_complete from doppler_processor is a LEVEL signal (high whenever
// state == S_IDLE && !frame_buffer_full). Downstream consumers (USB FT2232H,
// AGC, CFAR) expect a single-cycle PULSE. Convert here at the source so all
// consumers are safe.
wire doppler_frame_done_level; // raw level from doppler_processor
reg doppler_frame_done_prev;
wire doppler_frame_done; // rising-edge pulse (1 clk cycle)
// [RX-E FIX] doppler_frame_done_level is HIGH at reset (state==S_IDLE,
// frame_buffer_full==0). Initializing prev to 1'b0 produces a spurious
// rising-edge pulse on cycle 1, before any real frame has been processed,
// which causes a stale AGC gain update and a phantom CFAR tick. Initialize
// prev to 1'b1 so the first edge fires only after the doppler processor
// actually exits idle for a real frame and returns.
always @(posedge clk or negedge reset_n) begin
if (!reset_n)
doppler_frame_done_prev <= 1'b1;
else
doppler_frame_done_prev <= doppler_frame_done_level;
end
assign doppler_frame_done = doppler_frame_done_level & ~doppler_frame_done_prev;
assign doppler_frame_done_out = doppler_frame_done;
// ========== RANGE BIN DECIMATOR SIGNALS ==========
wire signed [15:0] decimated_range_i;
wire signed [15:0] decimated_range_q;
wire decimated_range_valid;
wire [`RP_RANGE_BIN_WIDTH_MAX-1:0] decimated_range_bin; // 9-bit
// ========== MTI CANCELLER SIGNALS ==========
wire signed [15:0] mti_range_i;
wire signed [15:0] mti_range_q;
wire mti_range_valid;
wire [`RP_RANGE_BIN_WIDTH_MAX-1:0] mti_range_bin; // 9-bit
wire mti_first_chirp;
// ========== MODULE INSTANTIATIONS ==========
// 0. Chirp scheduler (chirp-v2 PR-D) — single source of truth for waveform
// identity and inter-chirp timing on the RX side. Replaces the legacy
// radar_mode_controller. SHORT/MEDIUM/LONG ladders + sub-frame walking
// + host-cued track dwell with watchdog scan-fallback.
//
// PR-G G2: MEDIUM chirp/listen are now host-configurable via opcodes
// 0x17/0x18, plumbed through host_medium_*_cycles. Track-mode + subframe-
// enable + debug-wave selectors remain pinned to radar_params defaults
// (single-mode SHORT track is the only HW-tested track variant; per-mode
// track waveform selection is a future feature, not part of PR-G).
// host_chirps_per_elev (legacy) is intentionally not wired here — the V2
// sub-frame structure uses RP_DEF_CHIRPS_PER_SUBFRAME (16) and is fixed.
chirp_scheduler sched (
.mixers_enable(mixers_enable_100m),
.clk(clk),
.reset_n(reset_n),
// PR-U / M-8: routed from radar_system_top opcode 0x19 (was the
// RP_DEF_SUBFRAME_ENABLE constant — host had no way to mask sub-frames).
.host_subframe_enable(host_subframe_enable),
.host_short_chirp_cycles (host_short_chirp_cycles),
.host_short_listen_cycles(host_short_listen_cycles),
// PR-G G2: MEDIUM now flows from radar_system_top opcodes 0x17/0x18.
.host_medium_chirp_cycles (host_medium_chirp_cycles),
.host_medium_listen_cycles(host_medium_listen_cycles),
.host_long_chirp_cycles (host_long_chirp_cycles),
.host_long_listen_cycles(host_long_listen_cycles),
.host_guard_cycles(host_guard_cycles),
.host_chirps_per_subframe(6'd`RP_DEF_CHIRPS_PER_SUBFRAME),
// PR-AB.b expanded commit 5: beam-ready handshake. CDC-sync happens
// inside chirp_scheduler so the raw async GPIO can be routed directly.
.beam_ready_async(stm32_beam_ready_async),
.host_handshake_enable(host_handshake_enable),
.wave_sel(wave_sel),
.chirp_pulse(chirp_pulse),
.subframe_pulse(subframe_pulse),
.frame_pulse(frame_pulse),
.chirp_counter(sched_chirp_counter),
.subframe_id(sched_subframe_id),
.cfg_chirp_cycles (sched_cfg_chirp_cycles),
.cfg_listen_cycles(sched_cfg_listen_cycles),
.cfg_guard_cycles (sched_cfg_guard_cycles),
.beam_handshake_watchdog_fired(beam_handshake_watchdog_fired)
);
// PR-E: forward scheduler pulses + wave_sel to the TX-side CDC bridge in
// radar_system_top. The transmitter does its own clk_100m → clk_120m_dac
// crossing via cdc_async_fifo + toggle CDC.
assign sched_wave_sel_out = wave_sel;
assign sched_chirp_pulse_out = chirp_pulse;
assign sched_frame_pulse_out = frame_pulse;
wire clk_400m;
// NOTE: lvds_to_cmos_400m removed — ad9484_interface_400m now provides
// the buffered 400MHz DCO clock via adc_dco_bufg, avoiding duplicate
// IBUFDS instantiations on the same LVDS clock pair.
// 1. ADC + CDC + Digital Gain
// CMOS Output Interface (400MHz Domain)
wire [7:0] adc_data_cmos; // 8-bit ADC data (CMOS, from ad9484_interface_400m)
wire adc_valid; // Data valid signal
// AUDIT-S25: ADC power-down driven by host_adc_pwdn (opcode 0x32). Default
// 0 keeps the ADC powered up — same behavior as the previous hard-tied 1'b0.
// Set to 1 to assert AD9484 PWDN; see port comment for full design notes.
assign adc_pwdn = host_adc_pwdn;
wire adc_overrange_400m;
// Audit F-7.4: mmcm_locked exposed as a port for sim-side gating; not
// surfaced to the host status pipeline yet (deferred for a future status
// widening). Leave unconnected here.
ad9484_interface_400m adc (
.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),
.sys_clk(clk),
.reset_n(reset_n),
.adc_data_400m(adc_data_cmos),
.adc_data_valid_400m(adc_valid),
.adc_dco_bufg(clk_400m),
.adc_overrange_400m(adc_overrange_400m),
.mmcm_locked()
);
// Audit F-0.1 / F-7.7: stickify the 400 MHz OR pulse, then CDC to clk_100m
// via 2FF. Single-bit, low→high-only once latched, so a 2FF sync is
// sufficient for a GPIO-class diagnostic.
//
// Cleared only by reset_n: the FPGA exposes no host-driven clear opcode
// today, so any path that wants the sticky to drop has to go through a
// full chip reset. A real host clear is a future bundled PR (opcode +
// ft_clk→clk_400m CDC + matching plumbing for the DDC stickies). See
// project_aeris10_clear_monitors_opcode_future.md.
reg adc_overrange_sticky_400m;
always @(posedge clk_400m or negedge reset_n) begin
if (!reset_n)
adc_overrange_sticky_400m <= 1'b0;
else if (adc_overrange_400m)
adc_overrange_sticky_400m <= 1'b1;
end
(* ASYNC_REG = "TRUE" *) reg [1:0] adc_overrange_sync_100m;
always @(posedge clk or negedge reset_n) begin
if (!reset_n)
adc_overrange_sync_100m <= 2'b00;
else
adc_overrange_sync_100m <= {adc_overrange_sync_100m[0], adc_overrange_sticky_400m};
end
wire adc_overrange_100m = adc_overrange_sync_100m[1];
// NOTE: The cdc_adc_to_processing instance that was here used src_clk=dst_clk=clk_400m
// (same clock domain — no crossing). Gray-code CDC on same-clock with fast-changing
// ADC data corrupts samples because Gray coding only guarantees safe transfer of
// values that change by 1 LSB at a time. The real 400MHz→100MHz CDC crossing is
// handled inside ddc_400m_enhanced via CIC decimation + CDC_FIR instances.
// Removed: cdc_adc_to_processing instance. ADC data now goes directly to DDC.
// 2. DDC Input Interface
wire signed [17:0] ddc_out_i;
wire signed [17:0] ddc_out_q;
wire ddc_valid_i;
wire ddc_valid_q;
// DDC diagnostic signals (audit F-6.1 — all outputs previously unconnected)
wire [1:0] ddc_status_w;
wire [7:0] ddc_diagnostics_w;
wire ddc_mixer_saturation;
wire ddc_filter_overflow;
(* mark_debug = "true" *) wire ddc_mixer_saturation_dbg = ddc_mixer_saturation;
(* mark_debug = "true" *) wire ddc_filter_overflow_dbg = ddc_filter_overflow;
(* mark_debug = "true" *) wire [7:0] ddc_diagnostics_dbg = ddc_diagnostics_w;
ddc_400m_enhanced ddc(
.clk_400m(clk_400m), // 400MHz clock from ADC DCO
.clk_100m(clk), // 100MHz system clock //used by the 2 FIR
.reset_n(reset_n),
.adc_data(adc_data_cmos), // ADC data at 400MHz (direct from ADC interface)
.adc_data_valid_i(adc_valid), // Valid at 400MHz
.adc_data_valid_q(adc_valid), // Valid at 400MHz
.adc_format(host_adc_format), // AUDIT-C3: opcode 0x33 selects offset-binary vs 2C
.baseband_i(ddc_out_i), // I output at 100MHz
.baseband_q(ddc_out_q), // Q output at 100MHz
.baseband_valid_i(ddc_valid_i), // Valid at 100MHz
.baseband_valid_q(ddc_valid_q),
// RX DDC mixer is always enabled — asymmetric vs the TX path which CDCs
// stm32_mixers_enable into clk_120m_dac (radar_transmitter.v:175-183).
// Counter-UAS RX has no operational scenario where the digital DDC NCO
// should be quiesced while the system is running; tie-1 is intentional.
.mixers_enable(1'b1),
// Diagnostics (audit F-6.1) — previously all unconnected
.ddc_status(ddc_status_w),
.ddc_diagnostics(ddc_diagnostics_w),
.mixer_saturation(ddc_mixer_saturation),
.filter_overflow(ddc_filter_overflow),
// Test/debug inputs — explicit tie-low (were floating)
.test_mode(2'b00),
.test_phase_inc(16'h0000),
// No host-driven clear/force opcode exists today; both ports tied 1'b0.
// See project_aeris10_clear_monitors_opcode_future.md for the bundled-PR
// shape required to activate them (opcode + CDC + matching top-level hook).
.force_saturation(1'b0),
.reset_monitors(1'b0),
.debug_sample_count(),
.debug_internal_i(),
.debug_internal_q(),
.cdc_cic_fir_overrun(ddc_cic_fir_overrun)
);
// Audit F-0.1: AD9484 overrange aggregated here so a single gpio_dig bit
// covers DDC-internal saturation, FIR overflow, AND raw ADC clipping.
assign ddc_overflow_any = ddc_mixer_saturation | ddc_filter_overflow | adc_overrange_100m;
assign ddc_saturation_count = ddc_diagnostics_w[7:5];
// Audit M-10: in production AD9484 produces a single 8-bit stream that the
// DDC mixes into matched I/Q paths with symmetric pipelines, so ddc_valid_i
// and ddc_valid_q rise on the same cycle and `data_sync_error` cannot fire
// by construction. The check is retained inside ddc_input_interface for the
// standalone tb_ddc_input_interface unit-test (which intentionally drives
// valid_i ≠ valid_q) — left unconnected here.
ddc_input_interface ddc_if (
.clk(clk),
.reset_n(reset_n),
.ddc_i(ddc_out_i),
.ddc_q(ddc_out_q),
.valid_i(ddc_valid_i),
.valid_q(ddc_valid_q),
.adc_i(adc_i_scaled),
.adc_q(adc_q_scaled),
.adc_valid(adc_valid_sync),
.data_sync_error() // unconnected — see M-10 note above
);
// 2b. Digital Gain Control with AGC
// Host-configurable power-of-2 shift between DDC output and matched filter.
// Default gain_shift=0, agc_enable=0 → pass-through (no behavioral change).
// When agc_enable=1: auto-adjusts gain per frame based on peak/saturation.
rx_gain_control gain_ctrl (
.clk(clk),
.reset_n(reset_n),
.data_i_in(adc_i_scaled),
.data_q_in(adc_q_scaled),
.valid_in(adc_valid_sync),
.gain_shift(host_gain_shift),
// AGC configuration
.agc_enable(host_agc_enable),
.agc_target(host_agc_target),
.agc_attack(host_agc_attack),
.agc_decay(host_agc_decay),
.agc_holdoff(host_agc_holdoff),
// Frame boundary from Doppler processor
.frame_boundary(doppler_frame_done),
// Outputs
.data_i_out(gc_i),
.data_q_out(gc_q),
.valid_out(gc_valid),
.saturation_count(gc_saturation_count),
.peak_magnitude(gc_peak_magnitude),
.current_gain(gc_current_gain)
);
// 3. Chirp reference ROM (chirp-v2 PR-C)
wire [10:0] sample_addr_from_chain;
chirp_reference_rom chirp_rom (
.clk(clk),
.reset_n(reset_n),
.wave_sel(wave_sel),
.segment_select(segment_request),
.mem_request(mem_request),
.sample_addr(sample_addr_from_chain),
.ref_i(ref_i),
.ref_q(ref_q),
.mem_ready(mem_ready)
);
// 4. [RX-B FIX, Option A 2026-04-23] Reference chirp wired to MF chain with
// a single-FF alignment delay. Previously ran through `latency_buffer` with
// LATENCY=3187 — that module is a count-N-valid-pulses-then-prime FIFO,
// not a true cycle delay. It needed ~2 frames of mem_request pulses before
// any ref reached the chain (so frame 1 saw all-zero ref → noise output).
// Removed in favour of a direct-wire path with one FF.
//
// Why the 1-FF stage: multi_segment ST_PROCESSING latches `adc_data` through
// one register stage (`fft_input_i <= buf_rdata_i`) before it reaches the
// chain. The ref path from chirp_reference_rom is combinational into the
// chain. Without compensation, ref leads sig by 1 cycle → autocorrelation
// peak at bin 1 instead of bin 0 (verified in tb/tb_rxb_fullchain_latency.v
// against fft_engine.v synthesis path: peak/mean ratio ~80× confirms clean
// correlation; peak position fixed to bin 0 by this register stage).
reg [15:0] ref_chirp_real_d, ref_chirp_imag_d;
always @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
ref_chirp_real_d <= 16'd0;
ref_chirp_imag_d <= 16'd0;
end else begin
ref_chirp_real_d <= ref_i;
ref_chirp_imag_d <= ref_q;
end
end
assign ref_chirp_real = ref_chirp_real_d;
assign ref_chirp_imag = ref_chirp_imag_d;
// 5. Dual Chirp Matched Filter
wire signed [15:0] range_profile_i;
wire signed [15:0] range_profile_q;
wire range_valid;
// Expose matched filter output to top level for USB range profile
assign range_profile_i_out = range_profile_i;
assign range_profile_q_out = range_profile_q;
assign range_profile_valid_out = range_valid;
// Manhattan magnitude: |I| + |Q|, saturated to 16 bits
wire [15:0] abs_mti_i = mti_range_i[15] ? (~mti_range_i + 16'd1) : mti_range_i;
wire [15:0] abs_mti_q = mti_range_q[15] ? (~mti_range_q + 16'd1) : mti_range_q;
wire [16:0] manhattan_sum = {1'b0, abs_mti_i} + {1'b0, abs_mti_q};
assign decimated_range_mag_out = manhattan_sum[16] ? 16'hFFFF : manhattan_sum[15:0];
assign decimated_range_valid_out = mti_range_valid;
matched_filter_multi_segment mf_dual (
.clk(clk),
.reset_n(reset_n),
.ddc_i({{2{gc_i[15]}}, gc_i}),
.ddc_q({{2{gc_q[15]}}, gc_q}),
.ddc_valid(gc_valid),
.wave_sel(wave_sel),
.chirp_counter(chirp_counter),
.chirp_pulse(chirp_pulse),
.ref_chirp_real(ref_chirp_real), // 1-FF aligned ref (RX-B fix)
.ref_chirp_imag(ref_chirp_imag),
.segment_request(segment_request),
.mem_request(mem_request),
.sample_addr_out(sample_addr_from_chain),
.mem_ready(mem_ready),
.pc_i_w(range_profile_i),
.pc_q_w(range_profile_q),
.pc_valid_w(range_valid)
);
// ========== CRITICAL: RANGE BIN DECIMATOR ==========
// Convert 2048 range bins to 512 bins for Doppler
range_bin_decimator #(
.INPUT_BINS(`RP_FFT_SIZE), // 2048
.OUTPUT_BINS(`RP_MAX_OUTPUT_BINS), // 512 (50T) / 4096 (200T) [RX-D]
.DECIMATION_FACTOR(`RP_DECIMATION_FACTOR) // 4
) range_decim (
.clk(clk),
.reset_n(reset_n),
.range_i_in(range_profile_i),
.range_q_in(range_profile_q),
.range_valid_in(range_valid),
.range_i_out(decimated_range_i),
.range_q_out(decimated_range_q),
.range_valid_out(decimated_range_valid),
.range_bin_index(decimated_range_bin),
.decimation_mode(2'b01), // Peak detection mode
.start_bin(11'd0),
.watchdog_timeout(range_decim_watchdog) // Audit F-6.4 — plumbed out
);
// ========== MTI CANCELLER (Ground Clutter Removal) ==========
// 2-pulse canceller: subtracts previous chirp from current chirp.
// H(z) = 1 - z^{-1} → null at DC Doppler, removes stationary clutter.
// When host_mti_enable=0: transparent pass-through.
mti_canceller #(
.NUM_RANGE_BINS(`RP_MAX_OUTPUT_BINS), // 512 (50T) / 4096 (200T) [RX-D]
.DATA_WIDTH(`RP_DATA_WIDTH) // 16
) mti_inst (
.clk(clk),
.reset_n(reset_n),
.range_i_in(decimated_range_i),
.range_q_in(decimated_range_q),
.range_valid_in(decimated_range_valid),
.range_bin_in(decimated_range_bin),
.range_i_out(mti_range_i),
.range_q_out(mti_range_q),
.range_valid_out(mti_range_valid),
.range_bin_out(mti_range_bin),
.mti_enable(host_mti_enable),
.wave_sel(wave_sel),
.mti_first_chirp(mti_first_chirp),
.mti_saturation_count(mti_saturation_count_out)
);
// ========== FRAME SYNC FROM TRANSMITTER ==========
// [FPGA-001 FIXED] Use the authoritative new_chirp_frame signal from the
// transmitter (via plfm_chirp_controller_enhanced), CDC-synchronized to
// clk_100m in radar_system_top. Previous code tried to derive frame
// boundaries from chirp_counter == 0, but that counter comes from the
// transmitter path (plfm_chirp_controller_enhanced) which does NOT wrap
// at chirps_per_elev — it overflows to N and only wraps at 6-bit rollover
// (64). This caused frame pulses at half the expected rate for N=32.
reg tx_frame_start_prev;
reg new_frame_pulse;
always @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
tx_frame_start_prev <= 1'b0;
new_frame_pulse <= 1'b0;
end else begin
new_frame_pulse <= 1'b0;
// Edge detect: tx_frame_start is a toggle-CDC derived pulse that
// may be 1 clock wide. Capture rising edge for clean 1-cycle pulse.
if (tx_frame_start && !tx_frame_start_prev) begin
new_frame_pulse <= 1'b1;
end
tx_frame_start_prev <= tx_frame_start;
end
end
assign new_chirp_frame = new_frame_pulse;
// ========== DATA PACKING FOR DOPPLER ==========
// Use MTI-filtered data (or pass-through if MTI disabled)
assign range_data_32bit = {mti_range_q, mti_range_i};
assign range_data_valid = mti_range_valid;
// ========== DOPPLER PROCESSOR ==========
doppler_processor_optimized #(
.DOPPLER_FFT_SIZE(`RP_DOPPLER_FFT_SIZE), // 16
.RANGE_BINS(`RP_MAX_OUTPUT_BINS), // 512 (50T) / 4096 (200T) [RX-D]
.CHIRPS_PER_FRAME(`RP_CHIRPS_PER_FRAME), // 48 (PR-F: 3 sub-frames * 16)
.CHIRPS_PER_SUBFRAME(`RP_CHIRPS_PER_SUBFRAME) // 16
) doppler_proc (
.clk(clk),
.reset_n(reset_n),
.range_data(range_data_32bit),
.data_valid(range_data_valid),
.new_chirp_frame(new_chirp_frame),
// Outputs
.doppler_output(doppler_output),
.doppler_valid(doppler_valid),
.doppler_bin(doppler_bin),
.range_bin(range_bin),
// Status
.processing_active(doppler_processing),
.frame_complete(doppler_frame_done_level),
.status()
);
// ========== OUTPUT CONNECTIONS ==========
// doppler_output, doppler_valid, doppler_bin, range_bin are directly
// connected to doppler_proc ports above
// ========== STATUS ==========
// ========== DEBUG AND VERIFICATION ==========
reg [31:0] frame_counter;
reg [5:0] chirps_in_current_frame;
always @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
frame_counter <= 0;
chirps_in_current_frame <= 0;
end else begin
// Count chirps in current frame
if (range_data_valid && decimated_range_bin == 0) begin
// First range bin of a chirp
chirps_in_current_frame <= chirps_in_current_frame + 1;
end
// Detect frame completion
if (new_chirp_frame) begin
frame_counter <= frame_counter + 1;
`ifdef SIMULATION
$display("[TOP] Frame %0d started. Previous frame had %0d chirps",
frame_counter, chirps_in_current_frame);
`endif
chirps_in_current_frame <= 0;
end
end
end
// ========== ADC DEBUG TAP (for self-test / bring-up) ==========
assign dbg_adc_i = adc_i_scaled;
assign dbg_adc_q = adc_q_scaled;
assign dbg_adc_valid = adc_valid_sync;
// ========== AGC STATUS OUTPUTS ==========
assign agc_saturation_count = gc_saturation_count;
assign agc_peak_magnitude = gc_peak_magnitude;
assign agc_current_gain = gc_current_gain;
endmodule