From 194f45bd05c95cb6ae89f93b84d84b3269bdcefb Mon Sep 17 00:00:00 2001 From: thejayman77 Date: Tue, 21 Jul 2026 23:50:04 -0400 Subject: [PATCH] Ch442 review fixes: fs_edge parity, dest snap_valid, sync attrs + SDC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../scanout_status_bit5_proposal.md | 21 +++ rtl/gif_gs/gs_lpddr_scanout_lb.sv | 14 +- rtl/platform/ps2_hps_bridge.sv | 22 ++- sim/tb/gif_gs/tb_gs_scanout_diag.sv | 177 +++++++++++++----- .../de25_nano_psmct32_raster_demo_top.sdc | 38 ++++ 5 files changed, 217 insertions(+), 55 deletions(-) diff --git a/docs/hardware/ch441_board_validation/scanout_status_bit5_proposal.md b/docs/hardware/ch441_board_validation/scanout_status_bit5_proposal.md index bd9b494..0585091 100644 --- a/docs/hardware/ch441_board_validation/scanout_status_bit5_proposal.md +++ b/docs/hardware/ch441_board_validation/scanout_status_bit5_proposal.md @@ -72,3 +72,24 @@ from "memory wrong" (write path — already ruled out for this scene by the re-d (A) + (B) together are minimal, add no wide arithmetic, and already partition the three causes for the observed persistent 0xF1. (D) is the follow-on if (A)/(B) point at the scanout path rather than a status artifact. Selection is deferred to owner/Codex. + +## Interpreting 0x120 / 0x124 on the production f52 scene (Ch442, Codex review) + +The A+B diagnostic is IMPLEMENTED (Ch442, RTL + SDC + focused TB, sim-only). Read the +registers with these caveats: + +- **Cold start is source row 32, NOT 0.** The SH3 DISPLAY2 profile runs `V_SOURCE_START=32` + (DISPFB2.DBY=32) with the 15:14 vertical map, so `scan_y` begins at 32. A row-zero-miss + therefore reads `0x124` `scan_y≈32` / `nf_v≈32` — **not** zero. (Zero only appears in + synthetic `V_SOURCE_START=0` configs; the focused TB checks both.) +- **Both cause bits may assert.** With the vertical linear/binomial filter active, a miss can + satisfy the base term (`scan_y>=nf_v`) AND the lookahead term (`scan_y+1>=nf_v`) at the same + vphase, so `0x120[3]` and `0x120[4]` can both be 1. Treat them as "which rows were short," + not mutually exclusive. +- **One snapshot NARROWS, it does not PROVE.** A single first-failure `nf_v`/`nf_s0` pair + distinguishes a real AXI error (`0x120[2]=1`) from an underflow (`[1]=1,[2]=0`), and locates + the first short row — but by itself it does **not** definitively separate a sustained + starvation from a transient `next_fetch` CDC-lag false positive. That separation needs the + later gray-code/2-cycle-agreement work (deferred), or repeated captures across frames. + +`bit5` at `0x02C` is unchanged; `0x118/0x11C` remain reserved. diff --git a/rtl/gif_gs/gs_lpddr_scanout_lb.sv b/rtl/gif_gs/gs_lpddr_scanout_lb.sv index eb97de2..9fa33ba 100644 --- a/rtl/gif_gs/gs_lpddr_scanout_lb.sv +++ b/rtl/gif_gs/gs_lpddr_scanout_lb.sv @@ -798,7 +798,11 @@ module gs_lpddr_scanout_lb #( // above) as combinational wires so the capture can name WHICH sub-cause fired. These // wires and the registers below drive ONLY the diag_* outputs — never the fetch FSM, // the pixel path, arbitration, or underflow_v. The existing latch block is untouched. - wire uf_in_range = enable && in_window && (scan_y < ($clog2(N_ROWS)+1)'(N_ROWS)); + // NOTE the `!fs_edge_v` term: the sticky latch gives frame-start CLEAR priority + // (`if (!enable || fs_edge_v) ... else if (predicate)`), so on an fs_edge_v cycle it + // never sets. Mirror that exactly here or the diagnostic could capture a frame-start + // coincident miss the real latch suppresses. + wire uf_in_range = enable && !fs_edge_v && in_window && (scan_y < ($clog2(N_ROWS)+1)'(N_ROWS)); wire uf_base_cond = uf_in_range && (scan_y >= nf_v); wire uf_look_cond = uf_in_range && (BINOMIAL_3X3_FILTER || @@ -814,9 +818,11 @@ module gs_lpddr_scanout_lb #( // so it names the first failure of the whole enabled session. The bundled snapshot // ({nf_s0,nf_v,scan_y} + cause/line_valid/vphase) stays stable while diag_valid is high, // so the bridge can transfer it coherently with a single synchronized valid. - logic diag_valid_q, diag_base_q, diag_look_q, diag_lv_q; - logic [3:0] diag_vphase_q; - logic [$clog2(N_ROWS):0] diag_scan_y_q, diag_nf_v_q, diag_nf_s0_q; + // (* preserve *): keep these capture regs as named keepers so the SDC bundled-data + // hold-false/max-skew/net-delay constraint can bind to them (they must not be merged). + (* preserve *) logic diag_valid_q, diag_base_q, diag_look_q, diag_lv_q; + (* preserve *) logic [3:0] diag_vphase_q; + (* preserve *) logic [$clog2(N_ROWS):0] diag_scan_y_q, diag_nf_v_q, diag_nf_s0_q; always_ff @(posedge video_clk) begin line_valid_vsync <= {line_valid_vsync[0], line_valid}; if (!enable) begin diff --git a/rtl/platform/ps2_hps_bridge.sv b/rtl/platform/ps2_hps_bridge.sv index ec8c73e..a46b39c 100644 --- a/rtl/platform/ps2_hps_bridge.sv +++ b/rtl/platform/ps2_hps_bridge.sv @@ -651,11 +651,19 @@ module ps2_hps_bridge ( assign lpddr_video_src_o = lpddr_video_src_q; assign lpddr_scanout_lb_o = lpddr_scanout_lb_q; // Ch442 A+B diagnostic — independently synced live flags + bundled-data snapshot capture. - logic [1:0] scan_diag_uf_sync, scan_diag_rderr_sync; + // Forced-synchronizer + dont_merge/preserve on each async chain so Quartus identifies the + // synchronizers (MTBF) and never retimes/merges them; the SDC cuts the async into stage 0. + (* altera_attribute = "-name SYNCHRONIZER_IDENTIFICATION FORCED", dont_merge, preserve *) + logic [1:0] scan_diag_uf_sync; + (* altera_attribute = "-name SYNCHRONIZER_IDENTIFICATION FORCED", dont_merge, preserve *) + logic [1:0] scan_diag_rderr_sync; + (* altera_attribute = "-name SYNCHRONIZER_IDENTIFICATION FORCED", dont_merge, preserve *) logic [2:0] scan_diag_valid_sync; // 3-deep so the RISING synced edge latches the snapshot - logic scan_diag_base_q, scan_diag_look_q, scan_diag_lv_q; - logic [3:0] scan_diag_vphase_q; - logic [9:0] scan_diag_scan_y_q, scan_diag_nf_v_q, scan_diag_nf_s0_q; + logic scan_diag_snap_valid_q; // DEST valid: set WITH payload capture, cleared on synced source-valid deassert + // (* preserve *): keep the bundle dest regs as named keepers for the SDC bundled-data constraint. + (* preserve *) logic scan_diag_base_q, scan_diag_look_q, scan_diag_lv_q; + (* preserve *) logic [3:0] scan_diag_vphase_q; + (* preserve *) logic [9:0] scan_diag_scan_y_q, scan_diag_nf_v_q, scan_diag_nf_s0_q; // Ch322 — LPDDR write-probe + texture-cache fill registers + return-path CDC. logic [31:0] lpddr_wr_addr_q; // "next write" pointer (auto-increments) logic [31:0] lpddr_wr_addr_present_q;// the address that goes WITH the current data word @@ -948,7 +956,7 @@ module ps2_hps_bridge ( // [3]cause_base(scan_y>=nf_v) [4]cause_lookahead [5]line_valid [11:8]filter vphase. 3'h0: reg_read = {20'd0, scan_diag_vphase_q, 2'd0, scan_diag_lv_q, scan_diag_look_q, scan_diag_base_q, - scan_diag_rderr_sync[1], scan_diag_uf_sync[1], scan_diag_valid_sync[1]}; + scan_diag_rderr_sync[1], scan_diag_uf_sync[1], scan_diag_snap_valid_q}; // 0x124 SCAN_DIAG_FIRST: [9:0]scan_y [19:10]nf_v [29:20]nf_s0. 3'h1: reg_read = {2'd0, scan_diag_nf_s0_q, scan_diag_nf_v_q, scan_diag_scan_y_q}; default: reg_read = 32'd0; @@ -1115,6 +1123,7 @@ module ps2_hps_bridge ( scan_diag_uf_sync <= 2'b00; scan_diag_rderr_sync <= 2'b00; scan_diag_valid_sync <= 3'b000; + scan_diag_snap_valid_q<= 1'b0; scan_diag_base_q <= 1'b0; scan_diag_look_q <= 1'b0; scan_diag_lv_q <= 1'b0; @@ -1169,6 +1178,9 @@ module ps2_hps_bridge ( scan_diag_look_q <= scan_diag_stat_i[1]; scan_diag_lv_q <= scan_diag_stat_i[2]; scan_diag_vphase_q <= scan_diag_stat_i[6:3]; + scan_diag_snap_valid_q <= 1'b1; // valid asserts ON the payload-capture edge, never before + end else if (!scan_diag_valid_sync[1]) begin + scan_diag_snap_valid_q <= 1'b0; // clear once the synced source valid has deasserted (!enable) end // Ch322 — texture-fill / write-probe status sync (emif_clk -> clk). tex_fill_done_sync <= {tex_fill_done_sync[0], tex_fill_done_i}; diff --git a/sim/tb/gif_gs/tb_gs_scanout_diag.sv b/sim/tb/gif_gs/tb_gs_scanout_diag.sv index 10f75e8..39f5855 100644 --- a/sim/tb/gif_gs/tb_gs_scanout_diag.sv +++ b/sim/tb/gif_gs/tb_gs_scanout_diag.sv @@ -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 diff --git a/synth/de25_nano/top_psmct32_raster_demo/de25_nano_psmct32_raster_demo_top.sdc b/synth/de25_nano/top_psmct32_raster_demo/de25_nano_psmct32_raster_demo_top.sdc index 2494964..15365a2 100644 --- a/synth/de25_nano/top_psmct32_raster_demo/de25_nano_psmct32_raster_demo_top.sdc +++ b/synth/de25_nano/top_psmct32_raster_demo/de25_nano_psmct32_raster_demo_top.sdc @@ -353,6 +353,44 @@ set osd_dst_n [get_collection_size $osd_cfg_dst] if { $osd_src_n == 0 } { post_message -type warning "Ch357 SDC WARN: osd_cfg src pattern matched 0 keepers (renamed? osd_cfg false-path NOT applied)" } if { $osd_dst_n == 0 } { post_message -type warning "Ch357 SDC WARN: osd_cfg dst pattern matched 0 keepers (renamed? osd_cfg false-path NOT applied)" } set_false_path -from $osd_cfg_src -to $osd_cfg_dst + +# ============================================================================ +# Ch442 (Codex) — scanout A+B diagnostic CDC. gs_lpddr_scanout_lb (u_lpddr_scan_lb) +# drives the diagnostic into ps2_hps_bridge (u_hps_bridge) across async domains: +# (a) THREE single-bit synchronizer chains — underflow (video_clk), read-error +# (emif_clk), valid (video_clk) — 2/3-FF (* FORCED, dont_merge, preserve *). +# Cut the async INTO stage 0 only (setup+hold); the sync[0]->sync[1..] MTBF +# leg stays timed. Nonzero-check WARN (the whole path is absent in non-LPDDR +# profiles), not fatal. +# (b) the 37-bit STABLE first-failure bundle {scan_y[10], nf_v[10], nf_s0[10], +# vphase[4], base, lookahead, line_valid}. It is written once per enabled +# session and held quiescent until the bridge captures it on the synced-valid +# edge -> the raw HOLD check is false; cut HOLD only + bound bit spread +# (set_max_skew 2ns) + bound net arrival (set_net_delay -max 2ns), same recipe +# as the tile_ram_cdc bundle above. FATAL count checks so a rename can't +# silently orphan the exception (src==37 exact; dst nonzero). +foreach s {scan_diag_uf_sync scan_diag_rderr_sync scan_diag_valid_sync} { + set s0 [get_keepers -nowarn "*u_hps_bridge|$s\[0\]"] + if { [get_collection_size $s0] == 0 } { + post_message -type warning "Ch442 SDC WARN: $s\[0\] matched 0 keepers (renamed / no LPDDR scanout in profile) — stage-0 cut NOT applied" + } else { + set_false_path -to $s0 + } +} +set scan_diag_src [get_keepers -nowarn {*u_lpddr_scan_lb|diag_scan_y_q[*] *u_lpddr_scan_lb|diag_nf_v_q[*] *u_lpddr_scan_lb|diag_nf_s0_q[*] *u_lpddr_scan_lb|diag_vphase_q[*] *u_lpddr_scan_lb|diag_base_q *u_lpddr_scan_lb|diag_look_q *u_lpddr_scan_lb|diag_lv_q}] +set scan_diag_dst [get_keepers -nowarn {*u_hps_bridge|scan_diag_scan_y_q[*] *u_hps_bridge|scan_diag_nf_v_q[*] *u_hps_bridge|scan_diag_nf_s0_q[*] *u_hps_bridge|scan_diag_vphase_q[*] *u_hps_bridge|scan_diag_base_q *u_hps_bridge|scan_diag_look_q *u_hps_bridge|scan_diag_lv_q}] +set scan_diag_src_n [get_collection_size $scan_diag_src] +set scan_diag_dst_n [get_collection_size $scan_diag_dst] +if { $scan_diag_src_n == 0 } { + post_message -type info "Ch442 SDC: scanout diagnostic CDC inactive in this profile (0 src -> no LPDDR scanout)" +} else { + if { $scan_diag_src_n != 37 } { error "Ch442 SDC FATAL: scanout diag bundle source count $scan_diag_src_n != 37 (10+10+10+4+1+1+1; renamed/optimized? hold cut + net_delay would orphan)" } + if { $scan_diag_dst_n == 0 } { error "Ch442 SDC FATAL: scanout diag bundle dest u_hps_bridge|scan_diag_*_q EMPTY (renamed? hold cut would orphan)" } + set_false_path -hold -from $scan_diag_src -to $scan_diag_dst + set_max_skew -from $scan_diag_src -to $scan_diag_dst 2.0 + set_net_delay -max -from $scan_diag_src -to $scan_diag_dst 2.0 + post_message -type info "Ch442 SDC: scanout diag bundle hold-false-path + 2ns max_skew + 2ns net_delay ($scan_diag_src_n src -> $scan_diag_dst_n dst)" +} post_message -type info "Ch357 SDC: osd_cfg refclk->design_clk synchronizer false-path ($osd_src_n src -> $osd_dst_n stage0 dst)" # Ch357 (Codex) — OSD MENU-STATE synchronizers (same quasi-static class as osd_cfg): cursor_row + osd_active cross