Files
retroDE_ps2/sim/tb/gif_gs/tb_gs_scanout_restart.sv
T
thejayman77 846eee06b6 Ch443c: per-frame scanout diagnostic + frame-restart latency fix
The Ch443 board A+B diagnostic captured a WARM-UP miss, not a displayed-
frame miss: the line-buffer reader is enabled by video_src_emif
immediately, but the HDMI mux only switches to it at the next vsync, and
the diagnostic (cleared only on !enable) froze that pre-display capture.
scan_y=32/nf=32 was the expected pre-display warm-up, and line_valid is
sticky so it only meant SOME row had loaded, not that row 32 was valid.

Two fixes (all accepted Ch443 timing repairs kept: AW buffer, F_SETTLE +
drain multicycle, tile max-skew, monolithic tex_mem):

1. Per-frame diagnostic: clear diag_valid_q on fs_edge_v as well as
   !enable (mirrors underflow_v). Discards the warm-up capture and
   records the first miss, if any, AFTER the real frame boundary.

2. Frame-restart latency: in L_R, after the single-beat response is
   accepted, if fs_pending || fs_edge_e, abandon the remainder of the
   obsolete row -- no beat commit, no next old-row AR, no publish/
   increment -- and return to L_IDLE, which restarts at V_SOURCE_START.
   Protocol-safe (the accepted AXI transaction is complete); removes up
   to a full row of restart latency during vertical blanking, so the
   restarted prefetch leads the first displayed row.

New tb_gs_scanout_restart proves: mid-fetch frame-start accepts the
in-flight response, issues NO further old-row AR, restarts at row 32,
loads rows 32/33 before active consumption, and no post-restart
underflow. Regressions green: scanout_lb {,_binomial,_hstretch,
_psm32_256,_fb}, scanout_diag (per-frame), ps2_hps_bridge, and the
complete f52 replay BYTE-IDENTICAL (Z 0/307200, COLOR 0/245760).

Also commits the previously-untracked Ch443 board A+B evidence
(docs/hardware/ch443_board_validation/: verdict, 3-session raw, board
FB, RBF sha). No Quartus/board/push from here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 18:10:55 -04:00

147 lines
7.2 KiB
Systemverilog

// retroDE_ps2 — tb_gs_scanout_restart (Ch443c)
//
// Focused test for the frame-restart latency repair in gs_lpddr_scanout_lb's AXI
// prefetch FSM. On a vsync (frame_start) that lands MID-ROW, the FSM must, after the
// current single-beat response is accepted, abandon the remainder of the now-obsolete
// row (no next old-row AR, no publish/increment) and restart the prefetch at
// V_SOURCE_START. Proves:
// (1) the in-flight response is accepted normally (no deadlock);
// (2) NO further old-row AR issues after the restart trigger;
// (3) the restart's first AR targets row V_SOURCE_START (=32);
// (4) rows 32 and 33 become resident before the first active (in_window) consumption;
// (5) no post-restart underflow on the displayed frame.
`timescale 1ns/1ps
module tb_gs_scanout_restart;
logic emif_clk = 0; always #2.5 emif_clk = ~emif_clk; // 200 MHz
logic video_clk = 0; always #4 video_clk = ~video_clk; // 125 MHz
logic rst_n;
localparam int N_ROWS = 48, VSS = 32, ROW_BEATS = 2, STRIDE = 64;
localparam int ROW32_ADDR = VSS * STRIDE; // 2048
localparam int ROW33_ADDR = (VSS+1) * STRIDE; // 2112
logic enable, frame_start, in_window;
logic [11:0] pixel_x, pixel_y;
logic [7:0] r,g,b; logic line_valid, underflow; logic [31:0] rd_errs;
logic diag_rderr_nz, diag_valid; logic [29:0] diag_first; logic [6:0] diag_stat;
logic [29:0] araddr; logic [1:0] arburst; logic [6:0] arid;
logic [7:0] arlen; logic [2:0] arsize; logic arvalid, arready;
logic [255:0] rdata; logic [1:0] rresp; logic rlast, rvalid, rready;
gs_lpddr_scanout_lb #(.FB_BASE(30'd0), .STRIDE_BYTES(STRIDE), .ROW_BEATS(ROW_BEATS),
.N_ROWS(N_ROWS), .PSMCT32(1'b1), .V_SOURCE_START(VSS)) dut (
.axi_clk(emif_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),
.diag_rderr_nz(diag_rderr_nz), .diag_valid(diag_valid), .diag_first(diag_first), .diag_stat(diag_stat),
.araddr(araddr), .arburst(arburst), .arid(arid), .arlen(arlen), .arsize(arsize),
.arvalid(arvalid), .arready(arready), .rdata(rdata), .rresp(rresp),
.rlast(rlast), .rvalid(rvalid), .rready(rready)
);
// behavioral EMIF responder: grant AR, return OKAY R one cycle later (fast).
logic pend; logic [29:0] pend_addr;
assign arready = arvalid && !pend && !rvalid;
always_ff @(posedge emif_clk or negedge rst_n) begin
if (!rst_n) begin pend<=0; rvalid<=0; rlast<=0; rresp<=0; rdata<=0; end
else begin
if (arready) begin pend<=1; pend_addr<=araddr; end
if (pend && !rvalid) begin rvalid<=1; rlast<=1; rresp<=0; rdata<={8{2'b01,pend_addr}}; pend<=0; end
else if (rvalid && rready) begin rvalid<=0; rlast<=0; end
end
end
int errors = 0;
task automatic chk(input string s, input logic c);
if (!c) begin $error("[restart] FAIL: %s", s); errors++; end
else $display("[restart] ok : %s", s);
endtask
// ---- monitors (emif domain) ----
// capture, at the restart trigger, whether we were mid-fetch and which row.
logic restart_seen, restart_midfetch;
logic [$clog2(N_ROWS):0] row_at_restart;
// track the FIRST araddr issued (arvalid rising) AFTER the restart trigger.
logic arv_prev, armed_first_ar, got_first_ar;
logic [29:0] first_ar_after_restart;
// flag ANY old-row AR after restart (araddr not equal to a fresh row-32-onwards fetch
// before we've seen the row-32 restart AR).
always_ff @(posedge emif_clk or negedge rst_n) begin
if (!rst_n) begin
restart_seen<=0; restart_midfetch<=0; row_at_restart<=0;
arv_prev<=0; armed_first_ar<=0; got_first_ar<=0; first_ar_after_restart<=0;
end else begin
arv_prev <= arvalid;
// restart trigger = fs_edge_e inside the DUT
// lstate_t = { L_IDLE=0, L_AR=1, L_R=2, L_C=3 }
if (dut.fs_edge_e && !restart_seen) begin
restart_seen <= 1'b1;
restart_midfetch <= (dut.lst == 2'd1) || (dut.lst == 2'd2); // L_AR or L_R
row_at_restart <= dut.cur_row;
armed_first_ar <= 1'b1;
end
// first arvalid rising edge after the restart trigger
if (armed_first_ar && arvalid && !arv_prev && !got_first_ar) begin
got_first_ar <= 1'b1;
first_ar_after_restart<= araddr;
end
end
end
task automatic pulse_fs();
@(posedge video_clk) frame_start <= 1'b1;
repeat (3) @(posedge video_clk); frame_start <= 1'b0;
repeat (2) @(posedge video_clk);
endtask
initial begin
rst_n=0; enable=0; frame_start=0; in_window=0; pixel_x=0; pixel_y=VSS[11:0];
repeat (8) @(posedge emif_clk); rst_n=1;
repeat (6) @(posedge emif_clk);
// ---- warm the prefetch: enable, first frame, advance disp_row so rows load ----
enable=1;
pulse_fs();
// climb pixel_y from VSS upward (in_window=0 -> no underflow) so the prefetch
// actively fetches rows 32,33,34,... Inject the mid-row restart partway through.
for (int y=VSS; y<VSS+8; y++) begin
@(posedge video_clk) pixel_y <= y[11:0];
repeat (6) @(posedge video_clk);
if (y == VSS+4) begin
// MID-ROW vsync: assert frame_start while the FSM is actively fetching.
@(posedge video_clk) frame_start <= 1'b1;
repeat (3) @(posedge video_clk); frame_start <= 1'b0;
end
end
repeat (40) @(posedge emif_clk);
// ---- (1)+(2)+(3): restart abandoned the old row and re-fetched from row 32 ----
chk("restart trigger observed", restart_seen === 1'b1);
chk("restart landed mid-fetch (L_AR/L_R)", restart_midfetch === 1'b1);
chk("first AR after restart = row 32", first_ar_after_restart === 30'(ROW32_ADDR));
// ---- (4): rows 32 and 33 resident before active consumption ----
// advance disp_row to let the restarted prefetch reach next_fetch >= 34.
for (int y=VSS; y<VSS+4; y++) begin
@(posedge video_clk) pixel_y <= y[11:0];
repeat (10) @(posedge video_clk);
end
repeat (40) @(posedge emif_clk);
chk("next_fetch advanced past rows 32,33 (>=34)", dut.next_fetch >= ($clog2(N_ROWS)+1)'(VSS+2));
chk("line_valid set (rows resident)", line_valid === 1'b1);
// ---- (5): now consume actively at row 32 -> NO underflow ----
@(posedge video_clk) begin in_window <= 1'b1; pixel_y <= VSS[11:0]; pixel_x <= 12'd0; end
repeat (60) @(posedge video_clk);
chk("no post-restart underflow at row 32", underflow === 1'b0);
if (errors==0) $display("[tb_gs_scanout_restart] PASS");
else $display("[tb_gs_scanout_restart] FAIL (%0d errors)", errors);
$finish;
end
initial begin #400000; $error("[tb_gs_scanout_restart] TIMEOUT"); $finish; end
endmodule : tb_gs_scanout_restart