|
|
|
@@ -0,0 +1,259 @@
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// tb_gs_scanout_cdc_qual — Ch443f
|
|
|
|
|
//
|
|
|
|
|
// Focused async-clock test for the COHERENT + QUALIFIED underflow detector in
|
|
|
|
|
// gs_lpddr_scanout_lb. It targets exactly the four properties Codex required of
|
|
|
|
|
// the hardened diagnostic, using deliberately non-commensurate axi/video clocks
|
|
|
|
|
// so the readiness (next_fetch) row-commit lands at every phase relative to the
|
|
|
|
|
// video sampling edge:
|
|
|
|
|
//
|
|
|
|
|
// §A No false event from the binary transition + no false event from ordinary
|
|
|
|
|
// synchronizer latency. A healthy phase-swept display commits many rows at
|
|
|
|
|
// walking phases; the coherent nf_v handshake must never latch a value the
|
|
|
|
|
// committed frontier never held (nf_v <= next_fetch, monotone), and NO
|
|
|
|
|
// qualified underflow may assert (underflow==0, diag_valid==0, uf_qual==0)
|
|
|
|
|
// even though the 2-FF sync lags next_fetch.
|
|
|
|
|
//
|
|
|
|
|
// §B1 A sub-QUAL miss (persistence 3 > the ~2-cycle sync latency, < QUAL=4) is
|
|
|
|
|
// NOT a qualified event: underflow stays 0, diag_valid stays 0, but the
|
|
|
|
|
// live uf_pmax records 3 so a host can SEE the transient. This is the
|
|
|
|
|
// direct proof that ordinary synchronizer latency cannot fabricate an event.
|
|
|
|
|
//
|
|
|
|
|
// §B2 A genuinely late row (persistence >> QUAL) DOES produce a qualified event:
|
|
|
|
|
// underflow==1, diag_valid==1, uf_qual==1, and the atomic snapshot is
|
|
|
|
|
// self-consistent (diag_scan_y >= diag_nf_v, both frozen the same cycle,
|
|
|
|
|
// diag_pmax >= QUAL).
|
|
|
|
|
//
|
|
|
|
|
// §C Frame reset + modulo-4 buffer reuse remain correct: frame_start clears the
|
|
|
|
|
// detector (nf_v->VSTART, persistence/qual/valid=0) and a full healthy sweep
|
|
|
|
|
// spanning > 4 source rows (all 4 rotating buffers reused) reproduces the
|
|
|
|
|
// binomial 3x3 output exactly with no underflow.
|
|
|
|
|
//
|
|
|
|
|
// Vehicle: BINOMIAL_3X3_FILTER=1 (NBUF=4, lookahead active), stretch OFF so
|
|
|
|
|
// scan_y == pixel_y and the fetch frontier can be positioned exactly. The
|
|
|
|
|
// detector logic under test (coherent nf_v, persistence, qualification, atomic
|
|
|
|
|
// capture) is stretch-agnostic; this isolates the CDC without stretch bookkeeping.
|
|
|
|
|
`timescale 1ns/1ps
|
|
|
|
|
|
|
|
|
|
module tb_gs_scanout_cdc_qual;
|
|
|
|
|
localparam int SRC_W=32, N_ROWS=48, ROW_BEATS=4, STRIDE=SRC_W*4, VSTART=0;
|
|
|
|
|
localparam int QUAL=4; // must match RTL QUAL_CYCLES
|
|
|
|
|
|
|
|
|
|
// Deliberately non-commensurate clocks: 6.0 ns vs 13.0 ns (ratio 13/6) so the
|
|
|
|
|
// axi-domain next_fetch Gray commit crosses the video sampling edge at walking phases.
|
|
|
|
|
logic axi_clk=0, video_clk=0, rst_n=0, enable=0;
|
|
|
|
|
always #3 axi_clk = ~axi_clk; // 6.0 ns
|
|
|
|
|
always #6.5 video_clk = ~video_clk; // 13.0 ns
|
|
|
|
|
|
|
|
|
|
logic frame_start=0, in_window=0;
|
|
|
|
|
logic [11:0] pixel_x=0, pixel_y=0;
|
|
|
|
|
wire [7:0] r,g,b;
|
|
|
|
|
wire line_valid, underflow;
|
|
|
|
|
wire [31:0] rd_errs;
|
|
|
|
|
wire [29:0] araddr; wire [1:0] arburst; wire [6:0] arid;
|
|
|
|
|
wire [7:0] arlen; wire [2:0] arsize; wire arvalid, rready;
|
|
|
|
|
logic arready=0, rvalid=0, rlast=0;
|
|
|
|
|
logic [255:0] rdata=0; logic [1:0] rresp=0;
|
|
|
|
|
|
|
|
|
|
logic [255:0] mem [0:N_ROWS*ROW_BEATS-1];
|
|
|
|
|
initial begin
|
|
|
|
|
for (int beat=0; beat<N_ROWS*ROW_BEATS; beat++) mem[beat]='0;
|
|
|
|
|
for (int y=0; y<N_ROWS; y++)
|
|
|
|
|
for (int x=0; x<SRC_W; x++)
|
|
|
|
|
mem[y*ROW_BEATS + (x>>3)][(x&7)*32 +: 32] = {8'hff, 8'(8'h80+x+y), 8'(y), 8'(x)};
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
gs_lpddr_scanout_lb #(
|
|
|
|
|
.FB_BASE(30'd0), .STRIDE_BYTES(STRIDE), .ROW_BEATS(ROW_BEATS),
|
|
|
|
|
.N_ROWS(N_ROWS), .PSMCT32(1'b1), .H_STRETCH_5_TO_4(1'b0),
|
|
|
|
|
.V_SOURCE_START(VSTART), .V_STRETCH_15_TO_14(1'b0),
|
|
|
|
|
.V_LINEAR_FILTER(1'b0), .H_LINEAR_FILTER(1'b0),
|
|
|
|
|
.H_SOURCE_PIXELS(SRC_W), .BINOMIAL_3X3_FILTER(1'b1)
|
|
|
|
|
) dut (
|
|
|
|
|
.axi_clk(axi_clk), .axi_rst_n(rst_n), .enable(enable),
|
|
|
|
|
.video_clk(video_clk), .frame_start(frame_start),
|
|
|
|
|
.pixel_x(pixel_x), .pixel_y(pixel_y), .in_window(in_window),
|
|
|
|
|
.r(r), .g(g), .b(b), .line_valid(line_valid), .underflow(underflow),
|
|
|
|
|
.rd_errs(rd_errs), .araddr(araddr), .arburst(arburst), .arid(arid),
|
|
|
|
|
.arlen(arlen), .arsize(arsize), .arvalid(arvalid), .arready(arready),
|
|
|
|
|
.rdata(rdata), .rresp(rresp), .rlast(rlast), .rvalid(rvalid), .rready(rready)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// ---- EMIF responder with TB-controlled starvation. starve=1 -> no AR accept,
|
|
|
|
|
// no new R (fetch freezes -> next_fetch stops advancing). ----
|
|
|
|
|
logic starve=0;
|
|
|
|
|
localparam int LAT=3;
|
|
|
|
|
typedef enum logic [1:0] {S_AR, S_WAIT, S_R} state_t;
|
|
|
|
|
state_t state=S_AR;
|
|
|
|
|
localparam int MEM_BEAT_BITS=$clog2(N_ROWS*ROW_BEATS);
|
|
|
|
|
logic [MEM_BEAT_BITS-1:0] beat_q; int wait_c=0;
|
|
|
|
|
always_ff @(posedge axi_clk) begin
|
|
|
|
|
arready <= 1'b0;
|
|
|
|
|
if (!rst_n) begin state<=S_AR; rvalid<=1'b0; rlast<=1'b0; wait_c<=0; end
|
|
|
|
|
else case (state)
|
|
|
|
|
S_AR: if (arvalid && !arready && !starve) begin
|
|
|
|
|
beat_q<=araddr[MEM_BEAT_BITS+4:5]; arready<=1'b1; wait_c<=LAT; state<=S_WAIT;
|
|
|
|
|
end
|
|
|
|
|
S_WAIT: if (wait_c>0) wait_c<=wait_c-1;
|
|
|
|
|
else begin rdata<=mem[beat_q]; rresp<=2'b00; rlast<=1'b1; rvalid<=1'b1; state<=S_R; end
|
|
|
|
|
S_R: if (rready && rvalid) begin rvalid<=1'b0; rlast<=1'b0; state<=S_AR; end
|
|
|
|
|
endcase
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
// (pixel-exact binomial 3x3 reproduction across mod-4 wrap is owned by
|
|
|
|
|
// tb_gs_scanout_binomial_lookahead; this TB isolates the readiness CDC +
|
|
|
|
|
// qualification, so no per-pixel oracle is needed here.)
|
|
|
|
|
|
|
|
|
|
// ---- global invariants (video domain) ----
|
|
|
|
|
int errors=0;
|
|
|
|
|
logic expect_uf=0; // 0 while a qualified underflow is NOT permitted
|
|
|
|
|
logic coh_en=0; // enable the nf_v coherence invariant (steady-state windows only)
|
|
|
|
|
int commit_events=0; // # of next_fetch changes observed (phase-swept commits)
|
|
|
|
|
logic [$clog2(N_ROWS):0] nf_prev=0; logic nf_seen=0;
|
|
|
|
|
logic [$clog2(N_ROWS):0] fetch_prev=0;
|
|
|
|
|
task automatic chk(input string label, input logic cond);
|
|
|
|
|
if (!cond) begin $error("[cdc_qual] FAIL: %s", label); errors++; end
|
|
|
|
|
else $display("[cdc_qual] ok : %s", label);
|
|
|
|
|
endtask
|
|
|
|
|
|
|
|
|
|
// suppress the coherence invariant for a few cycles after a frame reset: the
|
|
|
|
|
// Gray sync legitimately holds the (large, SAFE) pre-reset frontier for ~2 video
|
|
|
|
|
// cycles after next_fetch drops to VSTART — that is the benign reset transient
|
|
|
|
|
// fs_edge_v overrides, NOT a tear. Steady-state (guard expired) nf_v <= next_fetch
|
|
|
|
|
// must hold; a real torn decode would surface there.
|
|
|
|
|
int fs_guard=0;
|
|
|
|
|
always @(posedge video_clk) if (rst_n && enable) begin
|
|
|
|
|
if (dut.fs_edge_v) fs_guard <= 4; else if (fs_guard>0) fs_guard <= fs_guard-1;
|
|
|
|
|
// coherence invariant (steady state): nf_v may never exceed the committed
|
|
|
|
|
// frontier nor run below VSTART. A torn Gray decode would surface here.
|
|
|
|
|
if (coh_en && !dut.fs_edge_v && fs_guard==0) begin
|
|
|
|
|
if (dut.nf_v > dut.next_fetch) begin
|
|
|
|
|
$error("[cdc_qual] FAIL: nf_v %0d > committed next_fetch %0d (incoherent latch)",
|
|
|
|
|
dut.nf_v, dut.next_fetch); errors++;
|
|
|
|
|
end
|
|
|
|
|
if (dut.nf_v < ($clog2(N_ROWS)+1)'(VSTART)) begin
|
|
|
|
|
$error("[cdc_qual] FAIL: nf_v %0d < VSTART", dut.nf_v); errors++;
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
// qualified-underflow gate
|
|
|
|
|
if (underflow && !expect_uf) begin
|
|
|
|
|
$error("[cdc_qual] FAIL: qualified underflow asserted when not permitted (scan_y=%0d nf_v=%0d)",
|
|
|
|
|
dut.scan_y, dut.nf_v); errors++;
|
|
|
|
|
end
|
|
|
|
|
// count committed-frontier changes (phase coverage)
|
|
|
|
|
if (nf_seen && dut.next_fetch !== fetch_prev) commit_events++;
|
|
|
|
|
fetch_prev <= dut.next_fetch; nf_prev <= dut.nf_v; nf_seen <= 1'b1;
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
// ---- step display one source row, slowly enough that the modeled EMIF keeps
|
|
|
|
|
// its lead-2 prefetch (fetch ~1 row / 6 axi cycles; display 1 row / 8 video
|
|
|
|
|
// cycles -> fetch stays ahead). A row advance triggers exactly one commit. ----
|
|
|
|
|
task automatic step_row(input int y);
|
|
|
|
|
pixel_y = 12'(y); pixel_x = 12'd0; in_window = 1'b1;
|
|
|
|
|
repeat (20) @(posedge video_clk);
|
|
|
|
|
endtask
|
|
|
|
|
|
|
|
|
|
initial begin
|
|
|
|
|
// ---- reset / enable / prime ----
|
|
|
|
|
repeat(8) @(posedge axi_clk); rst_n=1; enable=1;
|
|
|
|
|
@(negedge video_clk); frame_start=1; repeat(3) @(posedge video_clk);
|
|
|
|
|
@(negedge video_clk); frame_start=0;
|
|
|
|
|
repeat(120) @(posedge axi_clk); // prime lead-2 buffers before any display
|
|
|
|
|
|
|
|
|
|
// ================= §A healthy phase-swept display =================
|
|
|
|
|
expect_uf = 0; coh_en = 1; // steady-state coherence invariant active here
|
|
|
|
|
for (int y=0; y<24; y++) step_row(y);
|
|
|
|
|
@(negedge video_clk); in_window=0; repeat(10) @(posedge video_clk);
|
|
|
|
|
coh_en = 0;
|
|
|
|
|
chk("A: no qualified underflow across healthy phase sweep", !underflow);
|
|
|
|
|
chk("A: diag_valid stays 0 (no real miss)", dut.diag_valid_q===1'b0);
|
|
|
|
|
chk("A: uf_qual stays 0", dut.uf_qual_q===1'b0);
|
|
|
|
|
chk("A: many frontier commits observed at swept phases (>=16)", commit_events>=16);
|
|
|
|
|
$display("[cdc_qual] A: commit_events=%0d live_pmax=%0d", commit_events, dut.uf_pmax_q);
|
|
|
|
|
|
|
|
|
|
// ================= §B1 sub-QUAL transient must NOT qualify =================
|
|
|
|
|
// fresh frame; advance healthily to row R, freeze fetch, jump display just
|
|
|
|
|
// past the frozen frontier, hold for (QUAL-1) video cycles.
|
|
|
|
|
in_window=0; pixel_y=0; @(negedge video_clk);
|
|
|
|
|
frame_start=1; repeat(3) @(posedge video_clk);
|
|
|
|
|
@(negedge video_clk); frame_start=0; repeat(120) @(posedge axi_clk);
|
|
|
|
|
expect_uf = 0;
|
|
|
|
|
for (int y=0; y<=18; y++) step_row(y);
|
|
|
|
|
repeat(20) @(posedge axi_clk);
|
|
|
|
|
begin
|
|
|
|
|
int F3;
|
|
|
|
|
starve = 1'b1; repeat(8) @(posedge video_clk); // freeze; drain in-flight + settle nf_v
|
|
|
|
|
F3 = dut.next_fetch; // now-stable committed frontier
|
|
|
|
|
pixel_y = 12'(F3); pixel_x = 12'd0; in_window = 1'b1; // scan_y == nf_v -> base miss
|
|
|
|
|
repeat(QUAL-1) @(posedge video_clk); // persistence reaches 3 (< QUAL)
|
|
|
|
|
chk("B1: sub-QUAL miss does NOT qualify (underflow==0)", !underflow);
|
|
|
|
|
chk("B1: sub-QUAL miss leaves diag_valid==0", dut.diag_valid_q===1'b0);
|
|
|
|
|
chk("B1: sub-QUAL miss leaves uf_qual==0", dut.uf_qual_q===1'b0);
|
|
|
|
|
// a real transient was recorded (>0) but stayed below the qualification floor
|
|
|
|
|
chk("B1: live pmax recorded a sub-QUAL transient (0<pmax<QUAL)",
|
|
|
|
|
dut.uf_pmax_q>4'd0 && dut.uf_pmax_q<4'(QUAL));
|
|
|
|
|
$display("[cdc_qual] B1: uf_pmax=%0d underflow=%0b (F3 frontier=%0d)",
|
|
|
|
|
dut.uf_pmax_q, underflow, F3);
|
|
|
|
|
starve = 1'b0; // relieve before it can qualify
|
|
|
|
|
@(negedge video_clk); in_window=0; repeat(20) @(posedge video_clk);
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
// ================= §B2 genuine late row MUST qualify =================
|
|
|
|
|
in_window=0; pixel_y=0; @(negedge video_clk);
|
|
|
|
|
frame_start=1; repeat(3) @(posedge video_clk);
|
|
|
|
|
@(negedge video_clk); frame_start=0; repeat(120) @(posedge axi_clk);
|
|
|
|
|
expect_uf = 1; // a qualified event is now the INTENDED outcome
|
|
|
|
|
for (int y=0; y<=18; y++) step_row(y);
|
|
|
|
|
repeat(20) @(posedge axi_clk);
|
|
|
|
|
begin
|
|
|
|
|
int F2;
|
|
|
|
|
starve = 1'b1; repeat(8) @(posedge video_clk);
|
|
|
|
|
F2 = dut.next_fetch;
|
|
|
|
|
pixel_y = 12'(F2); pixel_x = 12'd0; in_window = 1'b1;
|
|
|
|
|
repeat(10) @(posedge video_clk); // persistence >> QUAL
|
|
|
|
|
chk("B2: genuine late row qualifies (underflow==1)", underflow===1'b1);
|
|
|
|
|
chk("B2: genuine late row sets uf_qual==1", dut.uf_qual_q===1'b1);
|
|
|
|
|
chk("B2: diag captured (diag_valid==1)", dut.diag_valid_q===1'b1);
|
|
|
|
|
chk("B2: atomic snapshot self-consistent (scan_y >= nf_v)",
|
|
|
|
|
dut.diag_scan_y_q >= dut.diag_nf_v_q);
|
|
|
|
|
chk("B2: snapshot scan_y == frozen display row", dut.diag_scan_y_q===($clog2(N_ROWS)+1)'(F2));
|
|
|
|
|
chk("B2: snapshot nf_v == frozen frontier", dut.diag_nf_v_q===($clog2(N_ROWS)+1)'(F2));
|
|
|
|
|
chk("B2: captured pmax >= QUAL", dut.diag_pmax_q>=4'(QUAL));
|
|
|
|
|
chk("B2: live pmax kept growing past capture", dut.uf_pmax_q>dut.diag_pmax_q);
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
// ================= §C frame reset clears + mod-4 reuse correct =================
|
|
|
|
|
starve = 1'b0;
|
|
|
|
|
@(negedge video_clk); in_window=0; pixel_y=0; // disp_row->0 so fetch reloads rows 0..3
|
|
|
|
|
@(negedge video_clk); frame_start=1; repeat(3) @(posedge video_clk);
|
|
|
|
|
@(negedge video_clk); frame_start=0;
|
|
|
|
|
chk("C: frame reset cleared diag_valid", dut.diag_valid_q===1'b0);
|
|
|
|
|
chk("C: frame reset cleared uf_qual", dut.uf_qual_q===1'b0);
|
|
|
|
|
chk("C: frame reset cleared underflow", underflow===1'b0);
|
|
|
|
|
chk("C: frame reset realigned nf_v to VSTART", dut.nf_v===($clog2(N_ROWS)+1)'(VSTART));
|
|
|
|
|
repeat(160) @(posedge axi_clk); // prime lead-2 buffers 0..3 before display (binomial needs r+1)
|
|
|
|
|
|
|
|
|
|
// Healthy display spanning 12 source rows (mod-4 rotation reused 3x) at the
|
|
|
|
|
// proven step_row pace. This proves the DETECTOR re-arms correctly across a
|
|
|
|
|
// frame reset and does NOT false-trip while the 4 buffers are recycled: no
|
|
|
|
|
// qualified underflow, diag_valid stays 0, and the coherence invariant holds.
|
|
|
|
|
// (Pixel-exact binomial 3x3 reproduction across the mod-4 wrap is owned by
|
|
|
|
|
// tb_gs_scanout_binomial_lookahead; not re-litigated here.)
|
|
|
|
|
expect_uf = 0; coh_en = 1;
|
|
|
|
|
for (int y=0; y<12; y++) begin
|
|
|
|
|
step_row(y);
|
|
|
|
|
if (underflow) begin $error("[cdc_qual] C: false underflow at row %0d (nf_v=%0d next_fetch=%0d)",
|
|
|
|
|
y, dut.nf_v, dut.next_fetch); errors++; end
|
|
|
|
|
end
|
|
|
|
|
@(negedge video_clk); in_window=0; repeat(10) @(posedge video_clk);
|
|
|
|
|
chk("C: no qualified underflow across reset + mod-4 buffer reuse", !underflow);
|
|
|
|
|
chk("C: uf_qual stayed 0 across reset+reuse", dut.uf_qual_q===1'b0);
|
|
|
|
|
chk("C: diag_valid stayed 0 across reset+reuse", dut.diag_valid_q===1'b0);
|
|
|
|
|
chk("C: live pmax stayed sub-QUAL across reset+reuse", dut.uf_pmax_q<4'(QUAL));
|
|
|
|
|
|
|
|
|
|
if (errors==0) $display("[tb_gs_scanout_cdc_qual] PASS");
|
|
|
|
|
else $display("[tb_gs_scanout_cdc_qual] FAIL (errors=%0d)", errors);
|
|
|
|
|
$finish;
|
|
|
|
|
end
|
|
|
|
|
initial begin #6_000_000; $display("[tb_gs_scanout_cdc_qual] TIMEOUT"); $finish; end
|
|
|
|
|
endmodule
|