Ch442 review fixes: fs_edge parity, dest snap_valid, sync attrs + SDC
Codex review containment/observability fixes (no behavior change, no redesign):
- scanout_lb: add !fs_edge_v to the diagnostic predicate so capture
matches the underflow latch's frame-start CLEAR priority exactly
(no capture on an fs_edge cycle the real latch suppresses).
- bridge: expose a destination snap_valid_q as 0x120[0] — set ON the
payload-capture edge, cleared on synced source-valid deassert — so
valid never leads the bundle by a cycle (was the middle sync stage).
- bridge: forced-synchronizer (SYNCHRONIZER_IDENTIFICATION FORCED) +
dont_merge/preserve on the underflow/read-error/valid chains;
preserve on the bundle capture regs (both domains).
- SDC: stage-0 async cuts on the three sync[0] inputs + the 37-bit
stable bundle hold-false-path + 2ns max_skew + 2ns net_delay, with
fail-closed src==37 / dst!=0 count checks (tile_ram_cdc idiom).
- tb_gs_scanout_diag: +fs_edge-suppression monitor (with coverage that
the coincidence is exercised), +valid-ordering monitor, +snapshot
stability after later misses, +production DUT (V_SOURCE_START=32,
stretch, linear) proving cold-start scan_y=32, +cause/phase packing.
32/32 checks pass.
- doc: production cold start is source row 32 (not 0), base+lookahead
may both assert, and one snapshot narrows but does not prove
starvation vs next_fetch CDC-lag.
Sim set all PASS: focused TB, tb_ps2_hps_bridge, scanout_lb
{binomial,hstretch,psm32_256}, complete f52 replay (FB byte-identical
Z 0/307200, COLOR 0/245760). No Quartus, board, push, or scanout
behavior change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,17 +4,20 @@
|
||||
// LPDDR_STATUS[5] scan-error bit into independently observable causes and
|
||||
// captures the FIRST raw-underflow event of each video-source-enabled session.
|
||||
//
|
||||
// Wires the REAL gs_lpddr_scanout_lb diag outputs into the REAL ps2_hps_bridge
|
||||
// diag inputs and reads back 0x120 SCAN_DIAG_STATUS / 0x124 SCAN_DIAG_FIRST over
|
||||
// the bridge AXI4-lite slave. A behavioral EMIF read responder provokes each
|
||||
// cause SEPARATELY:
|
||||
// P0 baseline : healthy reads, no active pixels -> all diag bits 0 (no false positive)
|
||||
// P1 starvation: arready held low + active row 0 -> underflow, first-failure snapshot captured
|
||||
// P2 clear : drop video source (enable=0) -> valid + underflow clear
|
||||
// P3 rrerror : SLVERR reads, NO active pixels -> read-error flag set WITHOUT underflow
|
||||
// DUT-A: real gs_lpddr_scanout_lb -> real ps2_hps_bridge, read back over AXI.
|
||||
// P1 starvation: arready held low + active row 0 -> underflow, first-failure snapshot
|
||||
// P2 clear : drop video source (enable=0) -> valid + underflow clear
|
||||
// P3 baseline : healthy reads, no active pixels -> no false positive
|
||||
// P4 rrerror : SLVERR reads, no active pixels -> read-error WITHOUT underflow
|
||||
// + concurrent monitors (Codex review):
|
||||
// M1 fs_edge suppression : raw_uf_cond must never assert on an fs_edge_v cycle
|
||||
// M2 valid ordering : dest snap_valid rises ONLY on the payload-capture edge
|
||||
// + P1 snapshot stability : later misses do NOT overwrite the first snapshot
|
||||
//
|
||||
// Three asynchronous clocks (emif / video / bridge) exercise every CDC on the
|
||||
// path: the two split live flags, and the bundled-data snapshot handshake.
|
||||
// DUT-B: production-like gs_lpddr_scanout_lb (V_SOURCE_START=32, V_STRETCH_15_TO_14,
|
||||
// V_LINEAR_FILTER) checked at its outputs directly:
|
||||
// nonzero V_SOURCE_START -> cold-start snapshot scan_y=32 (NOT 0)
|
||||
// cause-bit / filter-phase packing of diag_stat / diag_first
|
||||
`timescale 1ns/1ps
|
||||
|
||||
module tb_gs_scanout_diag;
|
||||
@@ -28,7 +31,13 @@ module tb_gs_scanout_diag;
|
||||
localparam int ROW_BEATS = 2;
|
||||
localparam int STRIDE = 64; // PSMCT32: 16 px/row, 2 beats
|
||||
|
||||
// ---- scanout_lb <-> EMIF responder ----
|
||||
int errors = 0;
|
||||
task automatic chk(input string label, input logic cond);
|
||||
if (!cond) begin $error("[scanout_diag] FAIL: %s", label); errors++; end
|
||||
else $display("[scanout_diag] ok : %s", label);
|
||||
endtask
|
||||
|
||||
// ================= DUT-A: scanout_lb + bridge =================
|
||||
logic so_enable, frame_start, in_window;
|
||||
logic [11:0] pixel_x, pixel_y;
|
||||
logic [7:0] so_r, so_g, so_b;
|
||||
@@ -55,11 +64,10 @@ module tb_gs_scanout_diag;
|
||||
.rdata(rdata), .rresp(rresp), .rlast(rlast), .rvalid(rvalid), .rready(rready)
|
||||
);
|
||||
|
||||
// ---- behavioral EMIF read responder (emif_clk) ----
|
||||
// behavioral EMIF read responder (emif_clk) — normal / starve / SLVERR
|
||||
localparam logic [1:0] M_NORMAL = 2'd0, M_STARVE = 2'd1, M_ERR = 2'd2;
|
||||
logic [1:0] resp_mode = M_NORMAL;
|
||||
logic pend; logic [1:0] pend_resp; logic [29:0] pend_addr;
|
||||
// grant AR when not starving and no response in flight
|
||||
assign arready = (resp_mode != M_STARVE) && arvalid && !pend && !rvalid;
|
||||
always_ff @(posedge emif_clk or negedge emif_rst_n) begin
|
||||
if (!emif_rst_n) begin
|
||||
@@ -68,11 +76,11 @@ module tb_gs_scanout_diag;
|
||||
if (arready) begin
|
||||
pend <= 1'b1;
|
||||
pend_addr <= araddr;
|
||||
pend_resp <= (resp_mode == M_ERR) ? 2'b10 : 2'b00; // SLVERR vs OKAY
|
||||
pend_resp <= (resp_mode == M_ERR) ? 2'b10 : 2'b00;
|
||||
end
|
||||
if (pend && !rvalid) begin
|
||||
rvalid <= 1'b1; rlast <= 1'b1; rresp <= pend_resp;
|
||||
rdata <= {8{2'b01, pend_addr}}; // deterministic nonzero payload
|
||||
rdata <= {8{2'b01, pend_addr}};
|
||||
pend <= 1'b0;
|
||||
end else if (rvalid && rready) begin
|
||||
rvalid <= 1'b0; rlast <= 1'b0;
|
||||
@@ -80,14 +88,14 @@ module tb_gs_scanout_diag;
|
||||
end
|
||||
end
|
||||
|
||||
// ---- bridge diag inputs, driven from scanout_lb (top wiring mirror) ----
|
||||
wire scan_diag_uf_i = so_underflow; // live sticky underflow alone
|
||||
wire scan_diag_rderr_nz_i = so_diag_rderr_nz; // live (rd_errs != 0)
|
||||
wire scan_diag_valid_i = so_diag_valid; // first-failure captured (held to !enable)
|
||||
wire [29:0] scan_diag_first_i = so_diag_first; // snapshot {nf_s0,nf_v,scan_y}
|
||||
wire [6:0] scan_diag_stat_i = so_diag_stat; // snapshot {vphase,line_valid,lookahead,base}
|
||||
// bridge diag inputs, driven from scanout_lb (top wiring mirror)
|
||||
wire scan_diag_uf_i = so_underflow;
|
||||
wire scan_diag_rderr_nz_i = so_diag_rderr_nz;
|
||||
wire scan_diag_valid_i = so_diag_valid;
|
||||
wire [29:0] scan_diag_first_i = so_diag_first;
|
||||
wire [6:0] scan_diag_stat_i = so_diag_stat;
|
||||
|
||||
// ---- bridge AXI4-lite slave master signals ----
|
||||
// bridge AXI4-lite slave master signals
|
||||
logic [3:0] s_axi_awid=0; logic [37:0] s_axi_awaddr=0; logic [7:0] s_axi_awlen=0;
|
||||
logic [2:0] s_axi_awsize=0; logic [1:0] s_axi_awburst=0; logic s_axi_awlock=0;
|
||||
logic [3:0] s_axi_awcache=0; logic [2:0] s_axi_awprot=0; logic s_axi_awvalid=0, s_axi_awready;
|
||||
@@ -98,8 +106,6 @@ module tb_gs_scanout_diag;
|
||||
logic [3:0] s_axi_arcache=0; logic [2:0] s_axi_arprot=0; logic s_axi_arvalid=0, s_axi_arready;
|
||||
logic [3:0] s_axi_rid; logic [127:0] s_axi_rdata; logic [1:0] s_axi_rresp; logic s_axi_rlast, s_axi_rvalid; logic s_axi_rready=0;
|
||||
|
||||
// Partial named-port bridge instance: only clk/reset/AXI-slave/scan_diag_* are
|
||||
// relevant to the 0x120/0x124 read path. All other ports feed unrelated registers.
|
||||
ps2_hps_bridge u_bridge (
|
||||
.clk(clk), .reset_n(reset_n),
|
||||
.s_axi_awid(s_axi_awid), .s_axi_awaddr(s_axi_awaddr), .s_axi_awlen(s_axi_awlen),
|
||||
@@ -120,7 +126,33 @@ module tb_gs_scanout_diag;
|
||||
.scan_diag_stat_i(scan_diag_stat_i)
|
||||
);
|
||||
|
||||
// ---- AXI read task (single-beat, lane by addr[3:2]) ----
|
||||
// ---- concurrent monitor M1: capture must never be enabled on an fs_edge cycle ----
|
||||
logic cov_fsedge_miss = 1'b0; // coverage: we actually hit fs_edge coincident with a would-be miss
|
||||
always @(posedge video_clk) if (emif_rst_n) begin
|
||||
if (dut_scan.enable && dut_scan.fs_edge_v && dut_scan.raw_uf_cond) begin
|
||||
$error("[scanout_diag] M1 FAIL: raw_uf_cond asserted on fs_edge_v cycle"); errors++;
|
||||
end
|
||||
if (dut_scan.enable && dut_scan.fs_edge_v && dut_scan.in_window &&
|
||||
(dut_scan.scan_y >= dut_scan.nf_v) && (dut_scan.scan_y < ($clog2(N_ROWS)+1)'(N_ROWS)))
|
||||
cov_fsedge_miss <= 1'b1;
|
||||
end
|
||||
|
||||
// ---- concurrent monitor M2: dest snap_valid rises ONLY on the payload-capture edge ----
|
||||
// snap_valid_q is registered, so its observed rise (T+1) reflects the capture condition
|
||||
// sampled on the PREVIOUS edge (T). Compare against that delayed condition, else the
|
||||
// check mis-times the legitimate capture. Proves 0x120[0] never leads payload capture.
|
||||
logic snapv_prev = 1'b0, cap_edge_prev = 1'b0;
|
||||
always @(posedge clk) if (reset_n) begin
|
||||
if (u_bridge.scan_diag_snap_valid_q && !snapv_prev) begin // rising edge of exposed 0x120[0]
|
||||
if (!cap_edge_prev) begin
|
||||
$error("[scanout_diag] M2 FAIL: snap_valid rose without the payload-capture edge"); errors++;
|
||||
end
|
||||
end
|
||||
snapv_prev <= u_bridge.scan_diag_snap_valid_q;
|
||||
cap_edge_prev <= u_bridge.scan_diag_valid_sync[1] && !u_bridge.scan_diag_valid_sync[2];
|
||||
end
|
||||
|
||||
// ---- AXI read (single-beat, lane by addr[3:2]) ----
|
||||
task automatic axi_read32(input logic [37:0] addr, output logic [31:0] data);
|
||||
@(posedge clk);
|
||||
s_axi_arid<=0; s_axi_araddr<=addr; s_axi_arlen<=0; s_axi_arsize<=3'd2;
|
||||
@@ -136,40 +168,56 @@ module tb_gs_scanout_diag;
|
||||
@(posedge clk); s_axi_rready<=1'b0;
|
||||
endtask
|
||||
|
||||
// sweep pixel_y across all rows (in the video domain) to advance disp_row so the
|
||||
// prefetcher loads rows; in_window stays as caller set it.
|
||||
task automatic sweep_rows(input int hold_cycles);
|
||||
for (int y = 0; y < N_ROWS; y++) begin
|
||||
@(posedge video_clk); pixel_y <= y[11:0]; pixel_x <= 12'd0;
|
||||
repeat (hold_cycles) @(posedge video_clk);
|
||||
end
|
||||
endtask
|
||||
|
||||
task automatic pulse_frame_start();
|
||||
@(posedge video_clk); frame_start <= 1'b1;
|
||||
repeat (3) @(posedge video_clk); frame_start <= 1'b0;
|
||||
repeat (3) @(posedge video_clk);
|
||||
endtask
|
||||
|
||||
int errors = 0;
|
||||
task automatic chk(input string label, input logic cond);
|
||||
if (!cond) begin $error("[scanout_diag] FAIL: %s", label); errors++; end
|
||||
else $display("[scanout_diag] ok : %s", label);
|
||||
endtask
|
||||
// ================= DUT-B: production-like (V_SOURCE_START=32, stretch, linear) =================
|
||||
localparam int NB = 48, VSS = 32;
|
||||
logic b_enable, b_frame_start, b_in_window;
|
||||
logic [11:0] b_px, b_py;
|
||||
logic [7:0] b_r, b_g, b_bb;
|
||||
logic b_line_valid, b_underflow; logic [31:0] b_rd_errs;
|
||||
logic b_diag_rderr_nz, b_diag_valid;
|
||||
logic [29:0] b_diag_first; logic [6:0] b_diag_stat;
|
||||
logic [29:0] b_araddr; logic [1:0] b_arburst; logic [6:0] b_arid;
|
||||
logic [7:0] b_arlen; logic [2:0] b_arsize; logic b_arvalid;
|
||||
gs_lpddr_scanout_lb #(.FB_BASE(30'd0), .STRIDE_BYTES(STRIDE), .ROW_BEATS(ROW_BEATS),
|
||||
.N_ROWS(NB), .PSMCT32(1'b1),
|
||||
.V_SOURCE_START(VSS), .V_STRETCH_15_TO_14(1'b1),
|
||||
.V_LINEAR_FILTER(1'b1)) dut_b (
|
||||
.axi_clk(emif_clk), .axi_rst_n(emif_rst_n), .enable(b_enable),
|
||||
.video_clk(video_clk), .frame_start(b_frame_start),
|
||||
.pixel_x(b_px), .pixel_y(b_py), .in_window(b_in_window),
|
||||
.r(b_r), .g(b_g), .b(b_bb),
|
||||
.line_valid(b_line_valid), .underflow(b_underflow), .rd_errs(b_rd_errs),
|
||||
.diag_rderr_nz(b_diag_rderr_nz), .diag_valid(b_diag_valid),
|
||||
.diag_first(b_diag_first), .diag_stat(b_diag_stat),
|
||||
.araddr(b_araddr), .arburst(b_arburst), .arid(b_arid),
|
||||
.arlen(b_arlen), .arsize(b_arsize), .arvalid(b_arvalid), .arready(1'b0), // permanently starved
|
||||
.rdata(256'd0), .rresp(2'b00), .rlast(1'b0), .rvalid(1'b0), .rready()
|
||||
);
|
||||
|
||||
logic [31:0] st, fst;
|
||||
initial begin
|
||||
so_enable=0; frame_start=0; in_window=0; pixel_x=0; pixel_y=0; resp_mode=M_NORMAL;
|
||||
b_enable=0; b_frame_start=0; b_in_window=0; b_px=0; b_py=0;
|
||||
repeat (6) @(posedge clk); reset_n=1; emif_rst_n=1;
|
||||
repeat (6) @(posedge clk);
|
||||
|
||||
// -------- P1 FIRST: cold-start row-zero starvation -> first-failure snapshot --------
|
||||
// Run before any row ever loads so line_valid is genuinely 0 (line_valid is sticky,
|
||||
// reset only by axi_rst_n) — the authentic row-zero-miss the field defect resembles.
|
||||
// -------- P1: cold-start row-zero starvation -> first-failure snapshot --------
|
||||
resp_mode=M_STARVE; so_enable=1;
|
||||
pulse_frame_start();
|
||||
@(posedge video_clk) begin in_window<=1'b1; pixel_x<=12'd0; pixel_y<=12'd0; end
|
||||
repeat (60) @(posedge video_clk); // hold active row 0 while nothing loads
|
||||
repeat (60) @(posedge video_clk);
|
||||
repeat (60) @(posedge clk);
|
||||
axi_read32(38'h120, st);
|
||||
axi_read32(38'h124, fst);
|
||||
@@ -178,40 +226,77 @@ module tb_gs_scanout_diag;
|
||||
chk("P1 rderr_nz=0", st[2]===1'b0);
|
||||
chk("P1 cause_base=1", st[3]===1'b1);
|
||||
chk("P1 cause_lookah=0", st[4]===1'b0);
|
||||
chk("P1 line_valid=0", st[5]===1'b0); // cold start: no row loaded yet
|
||||
chk("P1 snap scan_y=0", fst[9:0]===10'd0); // row-zero miss
|
||||
chk("P1 line_valid=0", st[5]===1'b0);
|
||||
chk("P1 snap scan_y=0", fst[9:0]===10'd0);
|
||||
chk("P1 snap nf_v=0", fst[19:10]===10'd0);
|
||||
chk("P1 snap nf_s0=0", fst[29:20]===10'd0);
|
||||
|
||||
// -------- P2: clear via video-source disable --------------------------------------
|
||||
// snapshot stability: later misses on a different row must NOT overwrite the first snapshot
|
||||
@(posedge video_clk) begin pixel_y<=12'd2; end
|
||||
repeat (40) @(posedge video_clk);
|
||||
repeat (20) @(posedge clk);
|
||||
axi_read32(38'h124, fst);
|
||||
chk("P1 snapshot stable (scan_y still 0)", fst[9:0]===10'd0);
|
||||
// fs_edge coincident with an active miss: pulse frame_start while in_window+miss hold.
|
||||
// Monitor M1 asserts capture stays suppressed on the fs_edge cycle.
|
||||
@(posedge video_clk) begin pixel_y<=12'd0; end
|
||||
pulse_frame_start();
|
||||
repeat (10) @(posedge video_clk);
|
||||
|
||||
// -------- P2: clear via video-source disable --------
|
||||
so_enable=0; in_window=0;
|
||||
repeat (40) @(posedge clk);
|
||||
axi_read32(38'h120, st);
|
||||
chk("P2 valid cleared", st[0]===1'b0);
|
||||
chk("P2 underflow cleared", st[1]===1'b0);
|
||||
|
||||
// -------- P3: healthy baseline (rows load, no active pixels) -> NO false positive --
|
||||
// -------- P3: healthy baseline (rows load, no active pixels) -> no false positive --
|
||||
resp_mode=M_NORMAL; so_enable=1; in_window=0;
|
||||
pulse_frame_start(); sweep_rows(24);
|
||||
repeat (40) @(posedge clk);
|
||||
axi_read32(38'h120, st);
|
||||
chk("P3 valid=0", st[0]===1'b0);
|
||||
chk("P3 underflow=0", st[1]===1'b0);
|
||||
chk("P3 rderr_nz=0", st[2]===1'b0); // OKAY reads: still zero
|
||||
chk("P3 rderr_nz=0", st[2]===1'b0);
|
||||
|
||||
// -------- P4: SLVERR reads, no active pixels -> read-error WITHOUT underflow -------
|
||||
// -------- P4: SLVERR reads, no active pixels -> read-error WITHOUT underflow --
|
||||
resp_mode=M_ERR; in_window=0;
|
||||
pulse_frame_start(); sweep_rows(24); // reload every row; each read returns SLVERR
|
||||
pulse_frame_start(); sweep_rows(24);
|
||||
repeat (60) @(posedge clk);
|
||||
axi_read32(38'h120, st);
|
||||
chk("P4 rderr_nz=1", st[2]===1'b1);
|
||||
chk("P4 valid=0", st[0]===1'b0); // in_window=0 -> underflow impossible
|
||||
chk("P4 valid=0", st[0]===1'b0);
|
||||
chk("P4 underflow=0", st[1]===1'b0);
|
||||
|
||||
// -------- P5 (DUT-B): nonzero V_SOURCE_START cold-start + packing --------
|
||||
b_enable=1;
|
||||
@(posedge video_clk) b_frame_start<=1'b1;
|
||||
repeat (3) @(posedge video_clk) b_frame_start<=1'b0;
|
||||
repeat (3) @(posedge video_clk);
|
||||
@(posedge video_clk) begin b_in_window<=1'b1; b_px<=12'd0; b_py<=12'd0; end
|
||||
repeat (80) @(posedge video_clk);
|
||||
chk("P5 DUT-B captured", b_diag_valid===1'b1);
|
||||
chk("P5 scan_y=VSS(32)", b_diag_first[9:0]===10'd32); // NOT 0: source row 32
|
||||
chk("P5 nf_v=32", b_diag_first[19:10]===10'd32);
|
||||
chk("P5 nf_s0=32", b_diag_first[29:20]===10'd32);
|
||||
chk("P5 base=1", b_diag_stat[0]===1'b1);
|
||||
chk("P5 line_valid=0", b_diag_stat[2]===1'b0);
|
||||
// cause/phase packing: outputs must byte-map the internal capture regs
|
||||
chk("P5 pack base bit", b_diag_stat[0] === dut_b.diag_base_q);
|
||||
chk("P5 pack lookahead bit",b_diag_stat[1] === dut_b.diag_look_q);
|
||||
chk("P5 pack line_valid bit",b_diag_stat[2] === dut_b.diag_lv_q);
|
||||
chk("P5 pack vphase field", b_diag_stat[6:3] === dut_b.diag_vphase_q);
|
||||
chk("P5 pack scan_y field", b_diag_first[9:0] === 10'(dut_b.diag_scan_y_q));
|
||||
chk("P5 pack nf_v field", b_diag_first[19:10] === 10'(dut_b.diag_nf_v_q));
|
||||
chk("P5 pack nf_s0 field", b_diag_first[29:20] === 10'(dut_b.diag_nf_s0_q));
|
||||
|
||||
// -------- coverage: the fs_edge-coincident-miss case was actually exercised --------
|
||||
chk("M1 coverage: fs_edge coincident with miss observed", cov_fsedge_miss===1'b1);
|
||||
|
||||
if (errors==0) $display("[tb_gs_scanout_diag] PASS");
|
||||
else $display("[tb_gs_scanout_diag] FAIL (%0d errors)", errors);
|
||||
$finish;
|
||||
end
|
||||
|
||||
initial begin #500000; $error("[tb_gs_scanout_diag] TIMEOUT"); $finish; end
|
||||
initial begin #800000; $error("[tb_gs_scanout_diag] TIMEOUT"); $finish; end
|
||||
endmodule : tb_gs_scanout_diag
|
||||
|
||||
Reference in New Issue
Block a user