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.
676 lines
28 KiB
Verilog
676 lines
28 KiB
Verilog
`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
|