Ch443f: coherent Gray readiness CDC + qualified scanout underflow detector

Harden the LPDDR scanout underflow diagnostic (Codex direction). Keeps the
Ch443e 4-buffer + lead-2 fix; adds no 5th buffer.

RTL (gs_lpddr_scanout_lb):
- Replace the raw-binary next_fetch readiness sync with a reset-aware GRAY
  code. next_fetch is monotonic between frames, so one Gray bit changes per
  increment; the 2-FF-synced + decoded nf_v is always a real prior frontier
  (monotone, burst-safe), never a torn multi-bit combination. (A plain
  toggle-per-change handshake dropped bursts when two increments landed in one
  sync window; that is why the earlier attempt under-read nf_v and false-tripped.)
  fs_edge_v overrides the lone multi-bit reset transient.
- Qualify the underflow: only a miss persisting >= QUAL_CYCLES (4) sets sticky
  underflow. uf_pmax_q records the longest streak and uf_qual_q whether any
  qualified, so a host can distinguish a 1-cycle CDC lag from a real late row.
- Atomic snapshot: scan_y/nf_v/pmax/causes/vphase/line_valid all latched the
  same video cycle on the first qualified miss.

ABI: 0x120 adds [15:12]=live pmax (2-FF synced via scan_diag_pmax_i). 0x124
[29:20] now carries pmax-at-capture (was nf_s0). Bridge dst reg kept 10-bit.

SDC: scanout diag bundle source count 37 -> 31 (nf_s0[10] -> pmax[4]); new
async-in cut + max_skew/net_delay for the next_fetch Gray CDC; stage-0 cut for
the live pmax sync.

Tests: new tb_gs_scanout_cdc_qual (async-clock focused: no false event from the
readiness transition or ordinary sync latency; sub-QUAL transient does not
qualify; genuine late row qualifies with a self-consistent atomic snapshot;
frame reset + mod-4 reuse re-arm the detector). tb_gs_scanout_diag updated for
the pmax field. Tie off scan_diag_pmax_i (+ pre-existing clut_* gap from the
fog baseline) in the four .* bridge/integration TBs.

Regressions green: scanout (cdc_qual, binomial_lookahead, diag, restart,
lpddr_scanout_lb x3), regbuf (r/aw/w), bridge + 3 pad integration TBs, and the
f52 top-level golden FB (Z 0/307200, COLOR 0/245760 mismatch, drops=0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-23 23:17:41 -04:00
parent 2e2c1e9ca6
commit 064484c50d
11 changed files with 418 additions and 63 deletions
+77 -47
View File
@@ -88,9 +88,10 @@ module gs_lpddr_scanout_lb #(
// Splits the single top-level scan-error bit into independently observable causes and captures
// the FIRST raw-underflow event of each video-source-enabled session as a bundled-data snapshot.
output logic diag_rderr_nz, // LIVE (rd_errs != 0), reduced+registered in the axi_clk domain (single bit; the raw counter never crosses)
output logic diag_valid, // first raw-underflow captured; held stable until video source disabled (!enable)
output logic [29:0] diag_first, // SNAPSHOT, stable while diag_valid: {nf_s0[9:0], nf_v[9:0], scan_y[9:0]}
output logic diag_valid, // Ch443f: first QUALIFIED (persistent) miss captured this frame; held until !enable
output logic [29:0] diag_first, // SNAPSHOT, stable while diag_valid: {pmax_at_capture[9:0], nf_v[9:0], scan_y[9:0]}
output logic [6:0] diag_stat, // SNAPSHOT, stable while diag_valid: {vphase[3:0], line_valid, lookahead_cause, base_cause}
output logic [3:0] diag_pmax, // Ch443f: LIVE per-frame max consecutive-miss streak (transient vs real classification)
// ---- AXI4 read channel to the EMIF user port (axi_clk, 256-bit) ----
output logic [29:0] araddr,
@@ -672,8 +673,22 @@ module gs_lpddr_scanout_lb #(
logic [2:0] fs_sync_e;
wire fs_edge_e = fs_sync_e[1] && !fs_sync_e[2]; // RISING edge only: one prefetch restart per frame_start pulse
logic [$clog2(N_ROWS):0] disp_row_s0, disp_row_limit_e;
logic [$clog2(N_ROWS):0] next_fetch; // next row to load (0..N_ROWS)
localparam int NFW = $clog2(N_ROWS)+1;
logic [NFW-1:0] next_fetch; // next row to load (0..N_ROWS)
logic [1:0] next_fetch_buf;
// Ch443f (Codex) — COHERENT readiness transfer via a reset-aware GRAY-CODED counter.
// next_fetch is monotonic between frame boundaries (it only ever +1 on a row commit), so its
// Gray code changes exactly ONE bit per increment. Crossing the Gray word through a 2-FF
// synchronizer therefore never yields a bogus intermediate — the decoded value is always some
// real prior next_fetch, monotone, never a torn multi-bit binary combination, and it does NOT
// drop bursts (a plain toggle-per-change handshake cancels when two increments land inside one
// sync window; Gray does not). The only multi-bit jump is the frame-restart reset to
// V_SOURCE_START, and that transient is overridden on the video side by fs_edge_v (reset-aware).
wire [NFW-1:0] next_fetch_gray = next_fetch ^ (next_fetch >> 1);
function automatic [NFW-1:0] gray2bin(input [NFW-1:0] g);
gray2bin[NFW-1] = g[NFW-1];
for (int i = NFW-2; i >= 0; i--) gray2bin[i] = gray2bin[i+1] ^ g[i];
endfunction
typedef enum logic [1:0] { L_IDLE, L_AR, L_R, L_C } lstate_t;
lstate_t lst;
logic [$clog2(N_ROWS):0] cur_row;
@@ -826,34 +841,29 @@ module gs_lpddr_scanout_lb #(
end
end
// underflow (sticky, video domain): an in-window pixel for line pixel_y is read
// before that row was prefetched. The axi side loads rows 0..next_fetch-1, so row
// pixel_y is ready iff pixel_y < next_fetch. next_fetch crosses axi->video synced
// (slowly-changing; a 1-off transient is harmless). Resets on vsync.
logic [$clog2(N_ROWS):0] nf_s0, nf_v;
// =============== Ch443f coherent, QUALIFIED underflow detector (video domain) ===============
// COHERENT readiness: next_fetch crosses as a reset-aware GRAY code (next_fetch_gray), 2-FF
// synced and decoded here, NOT as a raw multi-bit binary bus. Because next_fetch is monotonic
// between frames, one Gray bit changes per increment, so the synchronized+decoded nf_v is always
// a real prior frontier value (monotone, never a torn combination) and never drops burst
// increments. Reset-aware: fs_edge_v realigns nf_v to V_SOURCE_START at the frame boundary,
// overriding the one multi-bit Gray transient (the restart jump to V_SOURCE_START).
logic [NFW-1:0] nf_gray_s0, nf_gray_s1; // 2-FF sync of the axi-domain Gray code
wire [NFW-1:0] nf_v_sync = gray2bin(nf_gray_s1);
logic [NFW-1:0] nf_v;
logic underflow_v;
always_ff @(posedge video_clk) begin
nf_s0 <= next_fetch; nf_v <= nf_s0;
if (!enable || fs_edge_v) underflow_v <= 1'b0;
else if (in_window && (scan_y < ($clog2(N_ROWS)+1)'(N_ROWS)) &&
((scan_y >= nf_v) ||
((BINOMIAL_3X3_FILTER ||
(V_LINEAR_FILTER && (stretch_vphase_q != 4'd0))) &&
(scan_y + 1'b1 < ($clog2(N_ROWS)+1)'(N_ROWS)) &&
(scan_y + 1'b1 >= nf_v))))
underflow_v <= 1'b1;
end
assign underflow = underflow_v;
// ================= Ch442 A+B first-failure diagnostic (read-only) =================
// Re-express the EXACT raw-underflow predicate (identical boolean to the sticky latch
// 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.
// 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.
// QUALIFICATION: the Gray transfer removes incoherent transitions but NOT the normal ~2-clock
// sync lag. A genuine row-miss holds for the whole output line (many video cycles); an nf_v sync
// lag clears within a few cycles as nf_v catches up. Only a miss that PERSISTS >= QUAL_CYCLES is
// treated as real (-> sticky underflow / 0x02C bit5). uf_pmax_q records the longest miss streak
// this frame and uf_qual_q whether any streak qualified, so the host can DISTINGUISH a 1-cycle
// CDC lag (small pmax, uf_qual=0) from an actual late row (pmax>=QUAL, uf_qual=1).
localparam int QUAL_CYCLES = 4;
logic [3:0] uf_persist_q, uf_pmax_q;
logic uf_qual_q;
// Raw miss predicate (combinational, on the coherent nf_v). base = current row not loaded;
// lookahead = the binomial/linear filter's r+1 row not loaded. !fs_edge_v gives the frame-start
// clear priority (a miss on an fs_edge_v cycle is suppressed, matching the reset above).
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 &&
@@ -862,38 +872,58 @@ module gs_lpddr_scanout_lb #(
(scan_y + 1'b1 < ($clog2(N_ROWS)+1)'(N_ROWS)) &&
(scan_y + 1'b1 >= nf_v);
wire raw_uf_cond = uf_base_cond || uf_look_cond;
always_ff @(posedge video_clk) begin
nf_gray_s0 <= next_fetch_gray; nf_gray_s1 <= nf_gray_s0; // 2-FF Gray sync
if (!enable || fs_edge_v) begin
nf_v <= ($clog2(N_ROWS)+1)'(V_SOURCE_START);
underflow_v <= 1'b0; uf_persist_q <= 4'd0; uf_pmax_q <= 4'd0; uf_qual_q <= 1'b0;
end else begin
nf_v <= nf_v_sync; // coherent decoded frontier (monotone, burst-safe)
if (raw_uf_cond) begin
uf_persist_q <= (uf_persist_q == 4'hF) ? 4'hF : uf_persist_q + 4'd1;
if ((uf_persist_q + 4'd1) > uf_pmax_q) uf_pmax_q <= uf_persist_q + 4'd1;
end else begin
uf_persist_q <= 4'd0;
end
if (uf_persist_q >= 4'(QUAL_CYCLES)) begin underflow_v <= 1'b1; uf_qual_q <= 1'b1; end
end
end
assign underflow = underflow_v; // QUALIFIED (persistent) miss only
// ================= Ch442 A+B first-failure diagnostic (read-only) =================
// The raw_uf_cond wires above name WHICH sub-cause fired. The registers below drive ONLY
// the diag_* outputs — never the fetch FSM, pixel path, arbitration, or underflow_v.
// line_valid is axi_clk-domain; 2-FF into video_clk for coherent capture.
logic [1:0] line_valid_vsync;
// First-failure snapshot (video_clk). Ch443c (Codex): PER-FRAME — diag_valid clears on
// fs_edge_v as well as !enable, mirroring underflow_v, then records the first raw miss of
// the NEW frame. This discards the pre-display WARM-UP capture: the line-buffer reader is
// enabled by video_src_emif immediately, but the HDMI mux only switches to it at the next
// vsync, so the very first post-enable miss is not an HDMI-visible one. Clearing per frame
// makes the snapshot report the first miss AFTER the real frame boundary. The bundled
// snapshot ({nf_s0,nf_v,scan_y} + cause/line_valid/vphase) stays stable within a frame,
// so the bridge can transfer it coherently with a single synchronized valid.
// (* 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).
// Per-frame snapshot (video_clk). Ch443c: cleared on fs_edge_v (discards the pre-display
// warm-up). Ch443f: captures the first QUALIFIED miss (uf_persist_q >= QUAL) — a miss that
// outlived the sync transient — so diag_valid means a REAL (non-CDC-lag) miss occurred this
// frame; a transient-only frame leaves diag_valid=0 but a nonzero live uf_pmax. The bundled
// snapshot {pmax_at_capture, nf_v, scan_y} + {vphase, line_valid, lookahead, base} is stable
// within a frame so the bridge transfers it coherently with one synchronized valid; the live
// diag_pmax (per-frame max streak) is exposed separately so a transient (small) is visible.
// (* preserve *): keep these capture regs as named keepers for the SDC bundled-data constraint.
(* 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;
(* preserve *) logic [3:0] diag_vphase_q, diag_pmax_q;
(* preserve *) logic [$clog2(N_ROWS):0] diag_scan_y_q, diag_nf_v_q;
always_ff @(posedge video_clk) begin
line_valid_vsync <= {line_valid_vsync[0], line_valid};
if (!enable || fs_edge_v) begin
diag_valid_q <= 1'b0; // Ch443c: per-frame clear (discards pre-display warm-up capture)
end else if (!diag_valid_q && raw_uf_cond) begin
diag_valid_q <= 1'b1;
diag_valid_q <= 1'b0; // per-frame clear
end else if (!diag_valid_q && (uf_persist_q >= 4'(QUAL_CYCLES))) begin
diag_valid_q <= 1'b1; // capture the first QUALIFIED (persistent, real) miss
diag_base_q <= uf_base_cond;
diag_look_q <= uf_look_cond;
diag_lv_q <= line_valid_vsync[1];
diag_vphase_q <= stretch_vphase_q;
diag_scan_y_q <= scan_y;
diag_nf_v_q <= nf_v;
diag_nf_s0_q <= nf_s0;
diag_pmax_q <= uf_pmax_q; // persistence of this miss at qualification
end
end
assign diag_valid = diag_valid_q;
assign diag_first = {10'(diag_nf_s0_q), 10'(diag_nf_v_q), 10'(diag_scan_y_q)};
assign diag_pmax = uf_pmax_q; // LIVE per-frame max streak (transient observability)
assign diag_first = {6'd0, diag_pmax_q, 10'(diag_nf_v_q), 10'(diag_scan_y_q)};
assign diag_stat = {diag_vphase_q, diag_lv_q, diag_look_q, diag_base_q};
endmodule
+10 -1
View File
@@ -339,6 +339,7 @@ module ps2_hps_bridge (
input logic scan_diag_valid_i, // first-failure captured, held to !enable (video_clk level; bundled-data valid)
input logic [29:0] scan_diag_first_i, // SNAPSHOT {nf_s0[9:0],nf_v[9:0],scan_y[9:0]} (stable while valid -> 0x124)
input logic [6:0] scan_diag_stat_i, // SNAPSHOT {vphase[3:0],line_valid,lookahead,base} (stable while valid -> 0x120)
input logic [3:0] scan_diag_pmax_i, // Ch443f LIVE per-frame max miss streak (2-FF synced -> 0x120[15:12])
// ---- Ch322: LPDDR write-probe (HPS stages texture words) + texture-cache fill ----
// 0x040 LPDDR_WRADDR (W): set the LPDDR byte address (auto-increments +4 per data write).
@@ -657,6 +658,7 @@ module ps2_hps_bridge (
logic [1:0] scan_diag_uf_sync;
(* altera_attribute = "-name SYNCHRONIZER_IDENTIFICATION FORCED", dont_merge, preserve *)
logic [1:0] scan_diag_rderr_sync;
logic [3:0] scan_diag_pmax_s0, scan_diag_pmax_s1; // Ch443f — 2-FF sync of the live pmax magnitude
(* 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_snap_valid_q; // DEST valid: set WITH payload capture, cleared on synced source-valid deassert
@@ -954,7 +956,10 @@ module ps2_hps_bridge (
case (addr[4:2])
// 0x120 SCAN_DIAG_STATUS: [0]valid [1]underflow [2]read-error-nonzero
// [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,
// Ch443f: [15:12] = live per-frame max miss streak (pmax); [0] valid now = a
// QUALIFIED (persistent) miss captured. Classify: valid=0 + small pmax => CDC-lag
// transient; valid=1 / large pmax => real starvation.
3'h0: reg_read = {16'd0, scan_diag_pmax_s1, 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_snap_valid_q};
// 0x124 SCAN_DIAG_FIRST: [9:0]scan_y [19:10]nf_v [29:20]nf_s0.
@@ -1122,6 +1127,8 @@ module ps2_hps_bridge (
lpddr_scan_err_sync <= 2'b00;
scan_diag_uf_sync <= 2'b00;
scan_diag_rderr_sync <= 2'b00;
scan_diag_pmax_s0 <= 4'd0;
scan_diag_pmax_s1 <= 4'd0;
scan_diag_valid_sync <= 3'b000;
scan_diag_snap_valid_q<= 1'b0;
scan_diag_base_q <= 1'b0;
@@ -1167,6 +1174,8 @@ module ps2_hps_bridge (
// Ch442 — independent 2-FF syncs of the two split live flags (never the raw counter).
scan_diag_uf_sync <= {scan_diag_uf_sync[0], scan_diag_uf_i};
scan_diag_rderr_sync <= {scan_diag_rderr_sync[0], scan_diag_rderr_nz_i};
scan_diag_pmax_s0 <= scan_diag_pmax_i; // Ch443f — 2-FF sync of the small live magnitude
scan_diag_pmax_s1 <= scan_diag_pmax_s0;
// Bundled-data capture: sync the valid, and on its RISING synced edge the snapshot
// (held stable in the video domain since first-failure) is quiescent -> latch coherently.
scan_diag_valid_sync <= {scan_diag_valid_sync[1:0], scan_diag_valid_i};
+5 -3
View File
@@ -527,6 +527,7 @@ module de25_nano_psmct32_raster_demo_top (
wire scan_diag_uf_w, scan_diag_rderr_nz_w, scan_diag_valid_w;
wire [29:0] scan_diag_first_w;
wire [6:0] scan_diag_stat_w;
wire [3:0] scan_diag_pmax_w; // Ch443f — live per-frame max miss streak
// Ch320/Ch321 — LPDDR scanout frame-cache size: 256 beats (8 KiB, 64x64) by default,
// 1024 beats (32 KiB, 128x128) for the Ch321 larger-frame demo.
`ifdef GS_TILE_LPDDR128_DEMO
@@ -2681,7 +2682,7 @@ module de25_nano_psmct32_raster_demo_top (
.r(lb_r_w), .g(lb_g_w), .b(lb_b_w),
.line_valid(lb_valid_w), .underflow(lb_underflow_w), .rd_errs(lb_rd_errs_w),
.diag_rderr_nz(scan_diag_rderr_nz_w), .diag_valid(scan_diag_valid_w),
.diag_first(scan_diag_first_w), .diag_stat(scan_diag_stat_w),
.diag_first(scan_diag_first_w), .diag_stat(scan_diag_stat_w), .diag_pmax(scan_diag_pmax_w),
.araddr(lb_araddr), .arburst(lb_arburst), .arid(lb_arid),
.arlen(lb_arlen), .arsize(lb_arsize), .arvalid(lb_arvalid),
.arready(scan_ar_arready & scanout_lb_eff),
@@ -2846,7 +2847,7 @@ module de25_nano_psmct32_raster_demo_top (
assign scan_r_w=8'd0; assign scan_g_w=8'd0; assign scan_b_w=8'd0;
assign scan_cache_valid_w=1'b0; assign scan_err_w=1'b0; // no LPDDR scanout
assign scan_diag_uf_w=1'b0; assign scan_diag_rderr_nz_w=1'b0; assign scan_diag_valid_w=1'b0;
assign scan_diag_first_w=30'd0; assign scan_diag_stat_w=7'd0; // Ch442 diag tie-off (no LPDDR scanout)
assign scan_diag_first_w=30'd0; assign scan_diag_stat_w=7'd0; assign scan_diag_pmax_w=4'd0; // Ch442/443f diag tie-off (no LPDDR scanout)
assign tex_fill_done_w=1'b0; assign tex_fill_beats_w=32'd0; assign tex_fill_bytes_w=32'd0;
assign tex_rd_errs_w=32'd0; assign tex_fill_crc_w=32'd0;
assign lpddr_wr_busy_w=1'b0; assign lpddr_wr_done_w=1'b0; assign lpddr_wr_bresp_err_w=32'd0;
@@ -2868,7 +2869,7 @@ module de25_nano_psmct32_raster_demo_top (
assign scan_r_w=8'd0; assign scan_g_w=8'd0; assign scan_b_w=8'd0;
assign scan_cache_valid_w=1'b0; assign scan_err_w=1'b0; // no LPDDR scanout
assign scan_diag_uf_w=1'b0; assign scan_diag_rderr_nz_w=1'b0; assign scan_diag_valid_w=1'b0;
assign scan_diag_first_w=30'd0; assign scan_diag_stat_w=7'd0; // Ch442 diag tie-off (no LPDDR scanout)
assign scan_diag_first_w=30'd0; assign scan_diag_stat_w=7'd0; assign scan_diag_pmax_w=4'd0; // Ch442/443f diag tie-off (no LPDDR scanout)
assign tex_fill_done_w=1'b0; assign tex_fill_beats_w=32'd0; assign tex_fill_bytes_w=32'd0;
assign tex_rd_errs_w=32'd0; assign tex_fill_crc_w=32'd0;
assign lpddr_wr_busy_w=1'b0; assign lpddr_wr_done_w=1'b0; assign lpddr_wr_bresp_err_w=32'd0;
@@ -3139,6 +3140,7 @@ module de25_nano_psmct32_raster_demo_top (
.scan_diag_valid_i (scan_diag_valid_w),
.scan_diag_first_i (scan_diag_first_w),
.scan_diag_stat_i (scan_diag_stat_w),
.scan_diag_pmax_i (scan_diag_pmax_w),
// Ch322 — LPDDR write-probe (HPS stages texture words) + texture-cache fill.
.lpddr_wr_addr_o (lpddr_wr_addr_w),
.lpddr_wr_data_o (lpddr_wr_data_w),