test(fpga): PR-X.1 F-7.4 — gate tb_ad9484_xsim on MMCM lock (closes PR-N #86)

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).
This commit is contained in:
Jason
2026-05-05 11:59:30 +05:45
parent 83cbc91d8b
commit 6738f12e54
4 changed files with 78 additions and 43 deletions

View File

@@ -21,7 +21,13 @@ module ad9484_interface_400m (
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
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
@@ -80,15 +86,18 @@ BUFIO bufio_dco (
// 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;
// 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)
.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").

View File

@@ -301,6 +301,9 @@ wire adc_valid; // Data valid signal
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),
@@ -313,7 +316,8 @@ ad9484_interface_400m adc (
.adc_data_400m(adc_data_cmos),
.adc_data_valid_400m(adc_valid),
.adc_dco_bufg(clk_400m),
.adc_overrange_400m(adc_overrange_400m)
.adc_overrange_400m(adc_overrange_400m),
.mmcm_locked()
);
// Audit F-0.1: stickify the 400 MHz OR pulse, then CDC to clk_100m via 2FF.

View File

@@ -40,7 +40,11 @@ module ad9484_interface_400m (
output wire [7:0] adc_data_400m,
output wire adc_data_valid_400m,
output wire adc_dco_bufg,
output wire adc_overrange_400m
output wire adc_overrange_400m,
// Audit F-7.4: stub has no MMCM, but must mirror the real module's
// port shape. Tie the lock indicator HIGH after reset deassert so
// testbenches that gate on it (e.g. tb_ad9484_xsim) don't stall.
output wire mmcm_locked
);
// Pass-through clock (no BUFG needed in simulation)
@@ -74,4 +78,16 @@ always @(posedge adc_dco_p or negedge reset_n) begin
end
assign adc_overrange_400m = adc_overrange_400m_reg;
// Audit F-7.4: mock the real MMCM lock indicator. The synthesizable
// path takes ~4096 DCO cycles to lock; here we just track reset_n so
// callers see "locked" within one DCO edge of reset deassert.
reg mmcm_locked_stub;
always @(posedge adc_dco_p or negedge reset_n) begin
if (!reset_n)
mmcm_locked_stub <= 1'b0;
else
mmcm_locked_stub <= 1'b1;
end
assign mmcm_locked = mmcm_locked_stub;
endmodule

View File

@@ -38,6 +38,17 @@ module tb_ad9484_xsim;
wire [7:0] adc_data_400m;
wire adc_data_valid_400m;
wire adc_dco_bufg;
// Audit F-7.4: mmcm_locked exposed by ad9484_interface_400m so the TB
// can wait deterministically for the MMCM SIM model to lock (~4096
// DCO cycles) instead of guessing at a fixed 5-cycle delay.
wire mmcm_locked;
// Audit F-7.4: AD9484 OR (overrange) pair TB doesn't drive overrange,
// so tie complementary pair to a quiescent low/high so IBUFDS sees
// valid differential input.
reg adc_or_p = 1'b0;
wire adc_or_n = ~adc_or_p;
wire adc_overrange_400m;
// Differential complements
assign adc_d_n = ~adc_d_p;
@@ -59,13 +70,29 @@ module tb_ad9484_xsim;
.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 (sys_clk),
.reset_n (reset_n),
.adc_data_400m (adc_data_400m),
.adc_data_valid_400m(adc_data_valid_400m),
.adc_dco_bufg (adc_dco_bufg)
.adc_dco_bufg (adc_dco_bufg),
.adc_overrange_400m(adc_overrange_400m),
.mmcm_locked (mmcm_locked)
);
// F-7.4: gate post-reset waiting on the MMCM lock indicator
// The MMCM SIMULATION model takes 4096 DCO cycles (~10 µs at 400 MHz)
// to assert mmcm_locked. The interface's reset synchronizer then
// requires 2 more cycles for the 2-FF lock sync, plus 2 cycles for the
// 2-FF reset sync. Allow a couple more for the data pipeline to fill.
task wait_for_adc_ready;
begin
wait (mmcm_locked === 1'b1);
repeat (6) @(posedge adc_dco_p);
end
endtask
// Check task
task check;
input cond;
@@ -134,40 +161,18 @@ module tb_ad9484_xsim;
#(DCO_PERIOD * 0.3); // mid-cycle
reset_n = 1;
// valid should NOT assert immediately (needs 2 sync stages)
// F-7.4: valid stays 0 while the MMCM is locking. Verifying it
// immediately after deassert covers the 2-FF reset-sync window
// and is also vacuously true throughout the MMCM lock phase.
@(posedge adc_dco_p); #0.1;
// After 1 dco cycle: reset_sync_400m[0] = 1, [1] still = 0
// So reset_n_400m should still be 0
check(adc_data_valid_400m === 1'b0,
"valid stays 0 for 1 cycle after reset de-assert (sync stage 1)");
@(posedge adc_dco_p); #0.1;
// After 2 dco cycles: reset_sync_400m = 2'b11, reset_n_400m = 1
// But the data pipeline has its own 1-cycle delay
// So valid might assert this cycle or next
// Wait one more cycle for pipeline
@(posedge adc_dco_p); #0.1;
// By now (3 dco cycles after reset de-assert), valid should be 1
// Allow one more for IDDR pipeline
begin : wait_valid
reg saw_valid;
saw_valid = 0;
for (i = 0; i < 5; i = i + 1) begin
@(posedge adc_dco_p); #0.1;
if (adc_data_valid_400m) begin
saw_valid = 1;
$display(" valid asserted %0d dco cycles after reset de-assert", i + 4);
disable wait_valid;
end
end
if (!saw_valid) begin
$display(" [WARN] valid did not assert within 8 dco cycles");
end
end
// Wait for the MMCM SIM model to lock and the 2-FF reset-sync
// pipeline to drain, then confirm the data pipeline starts producing.
wait_for_adc_ready();
check(adc_data_valid_400m === 1'b1,
"valid asserts after reset sync pipeline completes");
"valid asserts after MMCM lock + reset sync pipeline completes");
//
// TEST GROUP 4: Data capture via IDDR
@@ -179,8 +184,9 @@ module tb_ad9484_xsim;
adc_d_p = 8'h00;
#100;
reset_n = 1;
// Wait for reset sync pipeline
repeat (5) @(posedge adc_dco_p);
// F-7.4: every reset cycle re-arms the MMCM lock countdown, so
// wait for lock + sync drain before driving the test pattern.
wait_for_adc_ready();
// Feed a known pattern on rising edges
// IDDR in SAME_EDGE_PIPELINED mode captures:
@@ -232,7 +238,7 @@ module tb_ad9484_xsim;
reset_n = 0;
#100;
reset_n = 1;
repeat (5) @(posedge adc_dco_p);
wait_for_adc_ready(); // F-7.4: wait for MMCM lock + sync drain
// Feed incrementing pattern: 0, 1, 2, ... on each half-cycle
begin : seq_test
@@ -280,15 +286,15 @@ module tb_ad9484_xsim;
// De-assert and verify sync pipeline
#30;
reset_n = 1;
// Should NOT be valid yet (2-stage sync)
// Should NOT be valid yet (2-stage sync + MMCM lock countdown)
@(posedge adc_dco_p); #0.1;
check(adc_data_valid_400m === 1'b0,
"valid stays 0 during reset sync de-assertion");
// Wait for full pipeline
repeat (5) @(posedge adc_dco_p); #0.1;
// F-7.4: wait for MMCM lock + sync drain, then confirm reassert.
wait_for_adc_ready();
check(adc_data_valid_400m === 1'b1,
"valid reasserts after sync pipeline completes");
"valid reasserts after MMCM lock + sync pipeline completes");
//
// TEST GROUP 7: ADC power-down output
@@ -317,7 +323,7 @@ module tb_ad9484_xsim;
adc_d_p = 8'h00;
#100;
reset_n = 1;
repeat (8) @(posedge adc_dco_p);
wait_for_adc_ready(); // F-7.4: wait for MMCM lock + sync drain
begin : sdr_ramp
reg [7:0] ramp_value;