Stage-7 ADC chain audit. The MMCM SIMULATION model in adc_clk_mmcm.v takes 4096 DCO cycles (~10 µs at 400 MHz) to assert mmcm_locked. The existing TB waited only ~5 cycles after each reset deassert, so the gated reset_n_400m never released and adc_data_valid_400m stayed low for every test group past the initial reset checks. Expose mmcm_locked as a new module output on ad9484_interface_400m (real path) and ad9484_interface_400m_stub (sim path; tied high one DCO cycle after reset deassert since the stub has no MMCM). Connect it through to tb_ad9484_xsim.v and add a `wait_for_adc_ready` task that waits on the lock signal plus 6 DCO cycles for the 2-FF lock-sync, 2-FF reset-sync, and pipeline drain. Apply the task at each of the five reset cycles in the testbench. radar_receiver_final.v: tie the new port off (.mmcm_locked()) — host status pipeline doesn't surface lock yet, deferred for a future status-word widening. Local iverilog regression (36/0/7) clean. xsim verification of the xsim-only TB itself is pending (remote Vivado host).
240 lines
10 KiB
Verilog
240 lines
10 KiB
Verilog
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 (SDR, like data — see
|
||
// AUDIT-C4 note below; an earlier comment incorrectly described this as
|
||
// DDR). 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,
|
||
// Audit F-7.4: MMCM lock indicator. Surfaces the internal mmcm_locked
|
||
// wire so testbenches (and, in future, the host status pipeline) can
|
||
// gate on a stable jitter-cleaned 400 MHz clock instead of guessing at
|
||
// a fixed wait. Combinational MMCME2 output — synchronize before use
|
||
// outside the clk_400m domain.
|
||
output wire mmcm_locked
|
||
);
|
||
|
||
// 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, drives IOB primitives only (in this
|
||
// module: the IOB-packed input FF on each data bit and the OR pair).
|
||
// Eliminates the hold violation that a BUFG-clocked capture would
|
||
// suffer from BUFG insertion delay.
|
||
//
|
||
// BUFG: Global clock buffer for fabric logic (downstream processing and
|
||
// the BUFIO→BUFG re-register stage). ~4 ns insertion delay, fine
|
||
// for fabric-to-fabric paths.
|
||
// ============================================================================
|
||
wire adc_dco_bufio; // Near-zero delay — drives IOB IFFs 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.
|
||
// Audit F-7.4: mmcm_locked is now exposed as a module output — see the
|
||
// port-list comment above.
|
||
wire mmcm_locked_int;
|
||
|
||
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_int)
|
||
);
|
||
assign adc_dco_bufg = adc_dco_buffered;
|
||
assign mmcm_locked = mmcm_locked_int;
|
||
|
||
// AUDIT-C4 (2026-05-01): AD9484 outputs SDR LVDS (datasheet p.5: "Output
|
||
// (LVDS—SDR)"; p.16: "data outputs are valid on the rising edge of DCO").
|
||
// One new sample per DCO period (DCO=fs=400 MHz); data is held stable for
|
||
// the full period. The chip has no DDR mode and no SPI access (CSB tied
|
||
// high on the production board, see Main_Board.sch:46719) so no register
|
||
// can change this.
|
||
//
|
||
// Capture on the FALLING DCO edge — 1.25 ns inside AD9484's stable data
|
||
// window. The rising DCO edge coincides with the AD9484 data transition
|
||
// (tSKEW = ±70 ps vs typical IFF setup ~150 ps), which would be
|
||
// functionally metastable; the falling edge has ~0.4 ns of setup margin
|
||
// against tPD = 0.85 ns. IOB=TRUE forces the FF into the input I/O block,
|
||
// giving near-zero clock-to-Q insertion delay matched to BUFIO.
|
||
//
|
||
// Previous (broken) behaviour: an IDDR captured both edges and a `dco_phase`
|
||
// FSM alternated Q1/Q2 in an attempt to demux a "DDR" stream. Because the
|
||
// chip is SDR, both edges represent the same sample, and the alternation
|
||
// produced approximately [s_{-1}, s_1, s_1, s_3, s_3, …] — odd-sample
|
||
// duplication with even-sample loss, equivalent to decimate-by-2 +
|
||
// ZOH upsample-by-2. The downstream 120 MHz IF then folded to ~80 MHz
|
||
// and corrupted the DDC. See git log for the audit trail.
|
||
(* IOB = "TRUE" *) reg [7:0] adc_data_iff;
|
||
|
||
always @(negedge adc_dco_bufio) begin
|
||
adc_data_iff <= adc_data;
|
||
end
|
||
|
||
// ============================================================================
|
||
// Re-register the IFF output into the BUFG domain
|
||
// adc_data_iff is stable from one falling DCO edge to the next (full DCO
|
||
// period of validity), so the rising-edge BUFG capture has ample margin.
|
||
// BUFIO and BUFG are derived from the same source (adc_dco), so they are
|
||
// frequency-matched.
|
||
// ============================================================================
|
||
// 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. The fabric BUFG-clocked re-register intentionally lives outside
|
||
// the IOB — packing it back into the IOB column was tried in earlier
|
||
// builds and rejected (the BUFG clock can't share the ILOGIC clock mux
|
||
// with a BUFIO-domain capture, and a pblock around the IDDR column
|
||
// pulled fanout into the I/O region and caused router congestion on 51
|
||
// unrelated paths).
|
||
reg [7:0] adc_data_iff_bufg;
|
||
|
||
always @(posedge adc_dco_buffered) begin
|
||
adc_data_iff_bufg <= adc_data_iff;
|
||
end
|
||
|
||
// SDR output: one sample per BUFG cycle (400 MHz BUFG = 400 MSPS)
|
||
reg [7:0] adc_data_400m_reg;
|
||
reg adc_data_valid_400m_reg;
|
||
|
||
// ── 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;
|
||
end else begin
|
||
adc_data_400m_reg <= adc_data_iff_bufg;
|
||
adc_data_valid_400m_reg <= 1'b1;
|
||
end
|
||
end
|
||
|
||
assign adc_data_400m = adc_data_400m_reg;
|
||
assign adc_data_valid_400m = adc_data_valid_400m_reg;
|
||
|
||
// ============================================================================
|
||
// Audit F-0.1 / AUDIT-C4: AD9484 OR (overrange) capture
|
||
// OR is an SDR LVDS pair (per AD9484 datasheet — the chip outputs SDR,
|
||
// not DDR; comment in earlier revision was wrong). One assertion per DCO
|
||
// period, valid on the rising DCO edge, held stable for the rest of the
|
||
// period. Captured at the falling DCO edge in the same IFF style as the
|
||
// data path. Downstream stickifies 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)
|
||
);
|
||
|
||
(* IOB = "TRUE" *) reg adc_or_iff;
|
||
always @(negedge adc_dco_bufio) begin
|
||
adc_or_iff <= adc_or_raw;
|
||
end
|
||
|
||
reg adc_or_iff_bufg;
|
||
always @(posedge adc_dco_buffered) begin
|
||
adc_or_iff_bufg <= adc_or_iff;
|
||
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_iff_bufg;
|
||
end
|
||
assign adc_overrange_400m = adc_overrange_r;
|
||
|
||
endmodule |