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>
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
SESSION 1
|
||||
0x02C=0x000000F1
|
||||
0x120=0x0000003B
|
||||
0x124=0x02008020
|
||||
SESSION 2
|
||||
0x02C=0x000000F1
|
||||
0x120=0x0000003B
|
||||
0x124=0x02008020
|
||||
SESSION 3
|
||||
0x02C=0x000000F1
|
||||
0x120=0x0000003B
|
||||
0x124=0x02008020
|
||||
DONE
|
||||
@@ -0,0 +1,45 @@
|
||||
# Ch443 board A+B diagnostic — verdict (zsrt139f52, 2026-07-22)
|
||||
|
||||
Image: `retroDE_ps2.ch443.core.rbf` SHA `d15deb42c99212d49715335d460357dc3f2487e14cf49bd60c7ad63ba52b9990`
|
||||
(26.1.0 Build 110 GUI fit of commit 4358bc3; non-canonical diagnostic candidate). Canonical
|
||||
`retroDE_ps2.core.rbf` untouched; Ch441 restored after capture.
|
||||
|
||||
## Fit acceptance (all met)
|
||||
Timing: 0 violated setup/hold paths, EMIF setup +0.177; AWREADY family absent; drain multicycle
|
||||
bound (`Ch443 SDC: texture drain_idx_q -> tex_mem 2-cycle setup / 1-cycle hold (16 src -> 2053 dst)`);
|
||||
monolithic **128-M20K** `tex_mem`; RAM 317/358 (89%), ALM 85%.
|
||||
|
||||
## Board render
|
||||
f52 replay `DONE rc=0`, zero drops, texture CRC `0x13cfe390` == expected, FB **byte-identical to
|
||||
golden** (SHA `d0047677…`, sum32 `0xaad0b94d`). The AW buffer + F_SETTLE 2-cycle drain + monolithic
|
||||
tex_mem are all functionally correct on silicon.
|
||||
|
||||
## A+B diagnostic — 3 independent source-enable sessions, BIT-IDENTICAL each time
|
||||
| reg | raw | decode |
|
||||
|-----|-----|--------|
|
||||
| 0x02C LPDDR_STATUS | `0x000000F1` | idle, scan_cache_valid, **scan_rd_err(bit5)=1**, frame_drained, clear_done |
|
||||
| 0x120 SCAN_DIAG_STATUS | `0x0000003B` | valid=1, **underflow=1**, **read-error-nonzero=0**, cause_base=1, cause_lookahead=1, line_valid=1, vphase=0 |
|
||||
| 0x124 SCAN_DIAG_FIRST | `0x02008020` | **scan_y=32, nf_v=32, nf_s0=32** |
|
||||
|
||||
## VERDICT
|
||||
The `LPDDR_STATUS[5]` `0xF1` symptom is definitively a **line-buffer prefetch UNDERFLOW, NOT an AXI
|
||||
read error** — `0x120` shows underflow=1 with read-error-nonzero=**0**. This resolves the three-way
|
||||
ambiguity Codex flagged: it is cause #1 (starvation-class), not cause #2 (AXI read errors).
|
||||
|
||||
The first failure is pinned to **scan_y = 32 = V_SOURCE_START** (SH3 DISPFB2.DBY=32, the first
|
||||
displayed source row) — exactly the production cold-start row-32 miss predicted. `nf_v = nf_s0 = 32`
|
||||
= `next_fetch` at its frame-start reset value, i.e. the prefetch had not advanced past row 32 when
|
||||
row 32 was first displayed. Both cause bits assert (base + lookahead), consistent with the vertical
|
||||
filter. line_valid=1 confirms the EMIF read path works — this is a first-displayed-row ordering race,
|
||||
not total starvation. Deterministic across 3 independent sessions ⇒ a reproducible STRUCTURAL
|
||||
first-row prefetch boundary, not a random glitch.
|
||||
|
||||
## Caveat (per Codex) + next
|
||||
A single snapshot NARROWS but does not by itself PROVE sustained starvation vs a `next_fetch` CDC-lag
|
||||
at reset (nf_v=32 is exactly the reset value). What is now PROVEN: (a) not an AXI read error; (b) the
|
||||
first miss is the cold-start row-32 boundary; (c) deterministic. Definitive starvation-vs-CDC
|
||||
separation needs the deferred follow-up (gray-code `next_fetch` across the sync, or a 2-cycle
|
||||
compare-agreement, or multi-frame captures). The FB (memory) is byte-perfect; whether this scanout
|
||||
underflow produces visible HDMI top-row corruption needs an actual HDMI capture (not a memory dump).
|
||||
|
||||
Evidence: `ch443_ab_3session_raw.txt`, `sh3_zsrt139f52_ch443_board_fb.mem`, `ch443_rbf.sha256`.
|
||||
@@ -0,0 +1 @@
|
||||
d15deb42c99212d49715335d460357dc3f2487e14cf49bd60c7ad63ba52b9990 docs/hardware/ch443_board_validation/retroDE_ps2.ch443.core.rbf
|
||||
File diff suppressed because it is too large
Load Diff
@@ -729,27 +729,34 @@ module gs_lpddr_scanout_lb #(
|
||||
end
|
||||
L_R: begin
|
||||
if (rvalid) begin
|
||||
// Capture directly into the selected RAM-local port
|
||||
// stage. L_C commits it on the following cycle.
|
||||
rready <= 1'b0;
|
||||
if (rresp != 2'b00) rd_errs <= rd_errs + 32'd1; // accepted read; a non-OKAY response is real
|
||||
lb0_we_q <= 1'b0;
|
||||
lb1_we_q <= 1'b0;
|
||||
lb2_we_q <= 1'b0;
|
||||
if (fs_pending || fs_edge_e) begin
|
||||
// Ch443c (Codex): a vsync restart is pending. This single-beat AXI
|
||||
// transaction is COMPLETE (rvalid accepted), so it is protocol-safe to
|
||||
// ABANDON the remainder of this now-obsolete row: do NOT commit the beat,
|
||||
// do NOT issue the next old-row AR, and do NOT publish/increment next_fetch.
|
||||
// Return to L_IDLE, where fs_pending restarts the prefetch at V_SOURCE_START.
|
||||
// This removes up to a full row of restart latency during vertical blanking
|
||||
// (the reason the warm-up first-fetch could lag the first displayed row).
|
||||
lst <= L_IDLE;
|
||||
end else begin
|
||||
// Capture directly into the selected RAM-local port stage; L_C commits.
|
||||
case (cur_buf)
|
||||
2'd1: begin lb1_wdata_q <= rdata; lb1_waddr_q <= beat[RB_BITS-1:0]; lb1_we_q <= 1'b1; end
|
||||
2'd2: begin lb2_wdata_q <= rdata; lb2_waddr_q <= beat[RB_BITS-1:0]; lb2_we_q <= 1'b1; end
|
||||
default: begin lb0_wdata_q <= rdata; lb0_waddr_q <= beat[RB_BITS-1:0]; lb0_we_q <= 1'b1; end
|
||||
endcase
|
||||
if (rresp != 2'b00) rd_errs <= rd_errs + 32'd1;
|
||||
rready <= 1'b0;
|
||||
if (beat == ROW_BEATS-1) begin
|
||||
// The local register captures this last response
|
||||
// now; L_C flushes it into RAM on the next edge.
|
||||
lst <= L_C;
|
||||
end else begin
|
||||
// Previous behavior inserted L_C here and lost one
|
||||
// EMIF clock per beat. The RAM-local stage commits
|
||||
// independently above, so immediately issue the
|
||||
// next read just as the pre-Ch439 FSM did.
|
||||
// RAM-local stage commits independently above, so
|
||||
// immediately issue the next single-beat read.
|
||||
beat <= beat + 1'b1;
|
||||
araddr <= araddr + 30'd32;
|
||||
arvalid <= 1'b1;
|
||||
@@ -757,6 +764,7 @@ module gs_lpddr_scanout_lb #(
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
L_C: begin
|
||||
// The global pipeline commit above writes the final beat
|
||||
// on this edge. Publish the completed row only now.
|
||||
@@ -813,10 +821,13 @@ module gs_lpddr_scanout_lb #(
|
||||
|
||||
// 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). diag_valid holds from the FIRST raw underflow
|
||||
// until the video source drops (!enable) — NOT cleared per frame (unlike underflow_v),
|
||||
// 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,
|
||||
// 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).
|
||||
@@ -825,8 +836,8 @@ module gs_lpddr_scanout_lb #(
|
||||
(* 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
|
||||
diag_valid_q <= 1'b0;
|
||||
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_base_q <= uf_base_cond;
|
||||
|
||||
@@ -1280,6 +1280,15 @@ tb_gs_scanout_diag: dirs
|
||||
@echo "=== run tb_gs_scanout_diag ==="
|
||||
@cd $(TRACE_DIR) && $(VVP) $(BUILD_DIR)/tb_gs_scanout_diag.vvp
|
||||
|
||||
tb_gs_scanout_restart: dirs
|
||||
@echo "=== build tb_gs_scanout_restart ==="
|
||||
$(IVERILOG) $(IVERILOG_FLGS) \
|
||||
-o $(BUILD_DIR)/tb_gs_scanout_restart.vvp \
|
||||
-s tb_gs_scanout_restart \
|
||||
$(RTL_SRCS) $(TB_ROOT)/gif_gs/tb_gs_scanout_restart.sv
|
||||
@echo "=== run tb_gs_scanout_restart ==="
|
||||
@cd $(TRACE_DIR) && $(VVP) $(BUILD_DIR)/tb_gs_scanout_restart.vvp
|
||||
|
||||
tb_gs_texture_cache: dirs
|
||||
@echo "=== build tb_gs_texture_cache ==="
|
||||
$(IVERILOG) $(IVERILOG_FLGS) \
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
// 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
|
||||
Reference in New Issue
Block a user