PLFM_RADAR/9_Firmware/9_2_FPGA/ad9484_interface_400m.v
Jason 8865e9a0ef fix(fpga): pre-bringup RTL hardening + test-suite hardening
RTL (P0 pre-bringup findings R-1/R-2/R-3/R-5/R-6):

- mti_canceller: add use_long_chirp input and waveform-boundary mute
  so the long->short transition in mode 01 no longer subtracts across
  heterogeneous waveforms (R-1). Prev buffer is overwritten in-flight
  at the boundary so the next same-waveform chirp subtracts cleanly.
- ad9484_interface_400m: 2FF sync of mmcm_locked into the 400 MHz
  domain before gating reset_n_gated (R-6).
- cic_decimator_4x_enhanced: correct max_fanout narrative (R-3).
- ad9484_interface_400m: strip stale pblock comment, note 3.0 ns
  max_delay instead (R-2).
- mti_canceller / doppler_processor: 200T-20km WARNING banners
  flagging the broken 4096-bin path (R-5). 9-bit BRAM address aliases
  silently until rewritten.
- adc_clk_mmcm.xdc: relax set_max_delay from 2.700 -> 3.000 ns,
  closes WNS with headroom on 50T build.
- radar_receiver_final: wire use_long_chirp into mti_inst.

Architecture-bump finalization (2048-pt range FFT, 512 range bins,
32 Doppler bins -> 16384 output cells per frame):

- tb/cosim/radar_scene.py: FFT_SIZE 1024 -> 2048, RANGE_BINS 64 -> 512.
- tb/gen_mf_golden_ref.py: N 1024 -> 2048.
- Regenerate all affected hex goldens (MF cases 1-4, Doppler inputs
  + py goldens, receiver integration golden_doppler.mem 2048 -> 16384).
- tb_radar_receiver_final: widen range_bin_out 6 -> 9 bits, bump
  GOLDEN_ENTRIES 2048 -> 16384, expand bitmaps/arrays to 512 bins,
  update all check messages and thresholds.
- tb_mti_canceller, tb_fullchain_mti_cfar_realdata: tie/pass
  use_long_chirp so compile still works after RTL port add.

Test-suite hardening (coverage audit findings):

- tb_mti_canceller T12: 10 new assertions exercising R-1 waveform-
  boundary mute across a long/long/short/short/long sequence. Catches
  a regression that re-enables subtraction across the boundary.
- tb_fir_lowpass: replace tautological check(1'b1, ...) on coefficient
  symmetry with a real hierarchical check coeff[k]===coeff[31-k];
  replace always-pass overflow check with a well-driven (not X/Z)
  assertion on filter_overflow.
- tb_matched_filter_processing_chain: replace three always-pass peak-
  bin placeholders with peak-to-mean-|out| > 2x ratio checks (catches
  flat/zero output that the old tautologies silently accepted).
- tb_cdc_modules M2: replace always-pass narrow-pulse check with a
  well-defined-output assertion on the synchronizer.
- tb_nco_400m: replace always-pass freq-switch check with a swing +
  no-X assertion across 200 post-switch samples.
- tb_system_e2e G12.1: replace check(1, ...) with test_num > 20 so
  it catches a stalled TB that skipped prior groups.
- tb_multiseg_cosim TEST 4: replace always-pass placeholder with a
  bitmap that asserts segment_request visited all 4 values.
- tb_mf_chain_synth and tb_fullchain_mti_cfar_realdata: add DEPRECATED
  headers plus \$fatal guards (ifndef ALLOW_STALE_*) so they cannot
  be silently re-enabled in CI with stale 1024-bin goldens against
  current 2048-pt RTL.

Regression: 32 passed, 0 failed. MTI TB grew 30 -> 39 checks;
receiver integration grew 17 -> 18 checks with 16384/16384 golden
match at tolerance +/- 2 LSB.
2026-04-22 13:23:38 +05:45

250 lines
9.3 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.

module ad9484_interface_400m (
// ADC Physical Interface (LVDS)
input wire [7:0] adc_d_p, // ADC Data P
input wire [7:0] adc_d_n, // ADC Data N
input wire adc_dco_p, // Data Clock Output P (400MHz)
input wire adc_dco_n, // Data Clock Output N (400MHz)
// Audit F-0.1: AD9484 OR (overrange) LVDS pair, DDR like data.
// Routed on the 50T main board to bank 14 pins M6/N6. Asserts for any
// sample whose absolute value exceeds full-scale.
input wire adc_or_p,
input wire adc_or_n,
// System Interface
input wire sys_clk, // 100MHz system clock (for control only)
input wire reset_n,
// Output at 400MHz domain
output wire [7:0] adc_data_400m, // ADC data at 400MHz
output wire adc_data_valid_400m, // Valid at 400MHz
output wire adc_dco_bufg, // Buffered 400MHz DCO clock for downstream use
// Audit F-0.1: OR flag, clk_400m domain. High on any sample in the
// current 400 MHz cycle where the ADC reports overrange.
output wire adc_overrange_400m
);
// LVDS to single-ended conversion
wire [7:0] adc_data;
wire adc_dco;
// IBUFDS for each data bit
// NOTE: IOSTANDARD and DIFF_TERM are set via XDC constraints, not RTL
// parameters, to support multiple FPGA targets with different bank voltages:
// - XC7A200T (FBG484): Bank 14 VCCO = 2.5V → LVDS_25
// - XC7A50T (FTG256): Bank 14 VCCO = 3.3V → LVDS_33
genvar i;
generate
for (i = 0; i < 8; i = i + 1) begin : data_buffers
IBUFDS #(
.DIFF_TERM("FALSE"), // Overridden by XDC DIFF_TERM property
.IOSTANDARD("DEFAULT") // Overridden by XDC IOSTANDARD property
) ibufds_data (
.O(adc_data[i]),
.I(adc_d_p[i]),
.IB(adc_d_n[i])
);
end
endgenerate
// IBUFDS for DCO
IBUFDS #(
.DIFF_TERM("FALSE"), // Overridden by XDC DIFF_TERM property
.IOSTANDARD("DEFAULT") // Overridden by XDC IOSTANDARD property
) ibufds_dco (
.O(adc_dco),
.I(adc_dco_p),
.IB(adc_dco_n)
);
// ============================================================================
// Clock buffering strategy for source-synchronous ADC interface:
//
// BUFIO: Near-zero insertion delay, can only drive IOB primitives (IDDR).
// Used for IDDR clocking to match the data path delay through IBUFDS.
// This eliminates the hold violation caused by BUFG insertion delay.
//
// BUFG: Global clock buffer for fabric logic (downstream processing).
// Has ~4 ns insertion delay but that's fine for fabric-to-fabric paths.
// ============================================================================
wire adc_dco_bufio; // Near-zero delay — drives IDDR only
wire adc_dco_buffered; // BUFG output — drives fabric logic
BUFIO bufio_dco (
.I(adc_dco),
.O(adc_dco_bufio)
);
// MMCME2 jitter-cleaning wrapper replaces the direct BUFG.
// The PLL feedback loop attenuates input jitter from ~50 ps to ~20-30 ps,
// reducing clock uncertainty and improving WNS on the 400 MHz CIC path.
wire mmcm_locked;
adc_clk_mmcm mmcm_inst (
.clk_in (adc_dco), // 400 MHz from IBUFDS output
.reset_n (reset_n),
.clk_400m_out (adc_dco_buffered), // Jitter-cleaned 400 MHz on BUFG
.mmcm_locked (mmcm_locked)
);
assign adc_dco_bufg = adc_dco_buffered;
// IDDR for capturing DDR data
wire [7:0] adc_data_rise; // Data on rising edge (BUFIO domain)
wire [7:0] adc_data_fall; // Data on falling edge (BUFIO domain)
genvar j;
generate
for (j = 0; j < 8; j = j + 1) begin : iddr_gen
IDDR #(
.DDR_CLK_EDGE("SAME_EDGE_PIPELINED"),
.INIT_Q1(1'b0),
.INIT_Q2(1'b0),
.SRTYPE("SYNC")
) iddr_inst (
.Q1(adc_data_rise[j]), // Rising edge data
.Q2(adc_data_fall[j]), // Falling edge data
.C(adc_dco_bufio), // BUFIO clock (near-zero insertion delay)
.CE(1'b1),
.D(adc_data[j]),
.R(1'b0),
.S(1'b0)
);
end
endgenerate
// ============================================================================
// Re-register IDDR outputs into BUFG domain
// IDDR with SAME_EDGE_PIPELINED produces outputs stable for a full clock cycle.
// BUFIO and BUFG are derived from the same source (adc_dco), so they are
// frequency-matched. This single register stage transfers from IOB (BUFIO)
// to fabric (BUFG) with guaranteed timing.
// ============================================================================
// Timing on the BUFIO→BUFG CDC edge is governed by a 3.000 ns
// set_max_delay in constraints/adc_clk_mmcm.xdc (1.2× the 2.500 ns period),
// which leaves the placer free and still fits inside the ADC data-valid
// window. IOB=TRUE and a pblock around the IDDR column were both tried
// and rejected: IOB packing fails because the BUFG clock on these
// capture FFs can't share the ILOGIC clock mux with the BUFIO-clocked
// IDDR, and the pblock pulled fanout logic into the I/O region and
// triggered router congestion on 51 unrelated paths.
reg [7:0] adc_data_rise_bufg;
reg [7:0] adc_data_fall_bufg;
always @(posedge adc_dco_buffered) begin
adc_data_rise_bufg <= adc_data_rise;
adc_data_fall_bufg <= adc_data_fall;
end
// Combine rising and falling edge data to get 400MSPS stream
reg [7:0] adc_data_400m_reg;
reg adc_data_valid_400m_reg;
reg dco_phase;
// ── Reset synchronizer ────────────────────────────────────────
// reset_n comes from the 100 MHz sys_clk domain. Assertion (going low)
// is asynchronous and safe — the FFs enter reset instantly. De-assertion
// (going high) must be synchronised to adc_dco_buffered to avoid
// metastability. This is the classic "async assert, sync de-assert" pattern.
//
// mmcm_locked gates de-assertion: the 400 MHz domain stays in reset until
// the MMCM PLL has locked and the jitter-cleaned clock is stable.
// mmcm_locked is a combinational MMCME2 output and can glitch; sync it
// into the 400 MHz domain with a 2-FF chain before using it in the
// async-reset branch below so a LOCKED blip doesn't asynchronously
// re-reset the domain. The chain is itself async-reset by the raw
// reset_n so it forces reset_n_gated=0 at power-up (no valid adc_dco
// edges exist yet to clock the sync chain).
(* ASYNC_REG = "TRUE" *) reg [1:0] mmcm_locked_sync_400m;
always @(posedge adc_dco_buffered or negedge reset_n) begin
if (!reset_n)
mmcm_locked_sync_400m <= 2'b00;
else
mmcm_locked_sync_400m <= {mmcm_locked_sync_400m[0], mmcm_locked};
end
wire mmcm_locked_400m = mmcm_locked_sync_400m[1];
(* ASYNC_REG = "TRUE" *) reg [1:0] reset_sync_400m;
wire reset_n_400m;
wire reset_n_gated = reset_n & mmcm_locked_400m;
always @(posedge adc_dco_buffered or negedge reset_n_gated) begin
if (!reset_n_gated)
reset_sync_400m <= 2'b00; // async assert (or MMCM not locked)
else
reset_sync_400m <= {reset_sync_400m[0], 1'b1}; // sync de-assert
end
assign reset_n_400m = reset_sync_400m[1];
always @(posedge adc_dco_buffered or negedge reset_n_400m) begin
if (!reset_n_400m) begin
adc_data_400m_reg <= 8'b0;
adc_data_valid_400m_reg <= 1'b0;
dco_phase <= 1'b0;
end else begin
dco_phase <= ~dco_phase;
if (dco_phase) begin
// Output falling edge data (completes the 400MSPS stream)
adc_data_400m_reg <= adc_data_fall_bufg;
end else begin
// Output rising edge data
adc_data_400m_reg <= adc_data_rise_bufg;
end
adc_data_valid_400m_reg <= 1'b1; // Always valid when ADC is running
end
end
assign adc_data_400m = adc_data_400m_reg;
assign adc_data_valid_400m = adc_data_valid_400m_reg;
// ============================================================================
// Audit F-0.1: AD9484 OR (overrange) capture
// OR is a DDR LVDS pair (same as data). Buffer it, capture both edges with an
// IDDR in the BUFIO domain, then OR the two phases into a single clk_400m
// flag. Register once for stability. No latching — downstream is expected to
// stickify in its own domain.
// ============================================================================
wire adc_or_raw;
IBUFDS #(
.DIFF_TERM("FALSE"),
.IOSTANDARD("DEFAULT")
) ibufds_or (
.O(adc_or_raw),
.I(adc_or_p),
.IB(adc_or_n)
);
wire adc_or_rise;
wire adc_or_fall;
IDDR #(
.DDR_CLK_EDGE("SAME_EDGE_PIPELINED"),
.INIT_Q1(1'b0),
.INIT_Q2(1'b0),
.SRTYPE("SYNC")
) iddr_or (
.Q1(adc_or_rise),
.Q2(adc_or_fall),
.C(adc_dco_bufio),
.CE(1'b1),
.D(adc_or_raw),
.R(1'b0),
.S(1'b0)
);
reg adc_or_rise_bufg;
reg adc_or_fall_bufg;
always @(posedge adc_dco_buffered) begin
adc_or_rise_bufg <= adc_or_rise;
adc_or_fall_bufg <= adc_or_fall;
end
reg adc_overrange_r;
always @(posedge adc_dco_buffered or negedge reset_n_400m) begin
if (!reset_n_400m)
adc_overrange_r <= 1'b0;
else
adc_overrange_r <= adc_or_rise_bufg | adc_or_fall_bufg;
end
assign adc_overrange_400m = adc_overrange_r;
endmodule