Compare commits

...

10 Commits

Author SHA1 Message Date
thejayman77 7d68577c75 Ch443f/Ch443e board validation evidence (compact)
Silicon closeout evidence for the Ch443f scanout CDC hardening (commits 064484c +
4110846). Text/hashes/verdict/samples only — RBFs and framebuffer .mem dumps left
uncommitted (preserved local / on board).

Ch443f board verdict: RBF 59c6372e, fit setup +0.068 / all classes clean, fpga0
operating, CORE_ID 0x50533200 ABI 0x100. 311-epoch f52 replay DONE rc=0 zero
drops, FB byte-identical golden d0047677. Scanout diagnostic 120 samples (60 @
50ms + 60 @ non-harmonic 7ms) all identical: 0x02C=0xD1 (scan-error clear),
0x120=0x18 (qualified valid=0 / underflow=0 / read-error=0 / live pmax=0). The
two phase-sweep logs are byte-identical -> no phase-dependent CDC artifact. The
former residual "underflow" was purely the raw-readiness CDC/sync transient; the
Ch443e 4-buffer/lead-2 already fixed the real lookahead starvation. No fifth
buffer justified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 20:11:11 -04:00
thejayman77 4110846c01 Ch443f timing: preload Z-RMW eviction awaddr at fill (kill cache_dirty->awaddr cone)
The Seed-3 fit of the Ch443f scanout hardening exposed one setup family in the
EMIF (iopll_0_outclk0, 3.225 ns) domain: u_zc_emit|u_z|cache_dirty -> awaddr[*],
WNS -0.062 / TNS -0.203, skew-dominated (-0.064 clock skew; the 5-level logic
path itself would land ~+0.002 at zero skew). Not the Ch443f logic — the added
EMIF-domain logic nudged this marginal Ch357-era control path negative.

Fix (Codex-directed, minimal): the normal-eviction AW address is a pure function
of the cache line's beat, known when the line is installed. Preload it in S_FILL_C
alongside cache_beat<=pf_beat (awaddr <= ZBASE + (pf_beat<<5)), and drop the two
later awaddr assignments gated by cache_dirty (the scene_flush branch and the
dirty cache-miss branch) — those now only assert awvalid on the already-prepared
address. Clear-path awaddr assignments unchanged. No new FSM state, pipeline
stage, buffer, or protocol change.

Safe: a freshly filled line cannot be dirty before S_FILL_C installs it; the
address is stable across hits and scene flushes; after a dirty eviction the next
fill re-runs S_FILL_C; clear invalidates the cache so the first subsequent fill
overwrites the clear address before any normal eviction. This removes the control
cone rather than placing around it, so closure is seed-robust.

Regressions green: z_rmw/zc_emit/axi_master_elastic/zbuffer/z_flush_writer/
tile_zflush, full Ch443f scanout set, sh3_zint (Z 0/53760, COLOR 0/11092),
sh3_zrop (errors=0), and f52 byte-identity (Z 0/307200, COLOR 0/245760, drops=0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 10:19:23 -04:00
thejayman77 064484c50d 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>
2026-07-23 23:17:41 -04:00
thejayman77 2e2c1e9ca6 Ch443e: binomial 4th line buffer + prefetch lead-2 (fix lookahead underflow)
The Ch443d board per-frame diagnostic identified the real displayed-frame
failure as a BINOMIAL vertical-lookahead starvation: displaying source
row r while interpolating r-1/r/r+1, the prefetch led by only one row
(next_fetch <= disp_row+1), so the lookahead row r+1 was still in flight
when the 3x3 filter read it (board: scan_y=33, nf_v=34, cause_lookahead=1,
read-error=0, deterministic every frame).

Fix (BINOMIAL_3X3_FILTER only; legacy 2/3-buffer, lead-1 paths unchanged):
- Add a 4th rotating line buffer (lb3) with its own RAM-local write/read/
  cache registers. The 3x3 filter needs r-1/r/r+1 resident (3 buffers), so
  leading by 2 (fetch r+2 while displaying r) without overwriting r-1
  requires a 4th buffer.
- Prefetch lead-2 for binomial: disp_row_limit_e = disp_row+2. The in-flight
  r+2 lands in the 4th buffer (b+2 mod 4), always distinct from prev/cur/next
  (b-1/b/b+1 mod 4), so it never clobbers a row being read.
- Modulo-4 rotation everywhere: V_SOURCE_BUF%4, stretch_buf_q, next_fetch_buf,
  reset alignment at V_SOURCE_START, and the read-cache prev/cur/next case
  extended to 4 branches with (b-1)/b/(b+1) mod 4 selection + first/last-row
  clamps preserved.

New tb_gs_scanout_binomial_lookahead reproduces the board condition under
realistic EMIF latency (LAT=7) + backpressure and proves: NO binomial
lookahead underflow, correct 3x3 output across modulo-4 wrap + clamps (full
oracle, 1280 px), and coverage that mid-frame rows past V_SOURCE_START+1
with vphase!=0 were exercised under prefetch pressure.

All green: binomial (4-buffer, identical output), lookahead (new),
scanout_lb {,_hstretch,_psm32_256,_fb}, scanout_restart, scanout_diag,
ps2_hps_bridge, and the complete f52 replay BYTE-IDENTICAL (Z 0/307200,
COLOR 0/245760). Also commits the Ch443d board evidence that identified
this defect. No Quartus/board/push from here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 15:24:37 -04:00
thejayman77 0c9b2bf5cd QSF: set fitter placement SEED 3 (bounded sweep winner)
RTL frozen at 6319d7c. The AW/W/R buffers + F_SETTLE drain multicycle
removed every structural EMIF-handshake -> FSM setup family; the residual
was a placement/route-marginal EMIF path (zc_emit QUAD_WIDTH4 Z-request
FIFO read, ~-0.087 ns: 3.132 ns data / -0.100 ns skew vs 3.225 ns period).

A bounded, Codex-authorized 4-seed fit+STA sweep (AGGRESSIVE AREA kept,
no asm) resolved it by placement:
  seed 2  EMIF -0.087  (10 violated)
  seed 3  EMIF +0.132  (0 violated, ALL classes >=0)   <- winner
  seed 4  EMIF +0.062  (0 violated, under +0.100 margin)
  seed 5  EMIF -0.130  (3 violated)

SEED 3 is the best complete result: EMIF setup +0.132 (>= +0.100),
design +5.561, hold/recovery/removal/MPW all nonnegative, no AWREADY /
texture-fill R-return / texcache drain / scanout-diagnostic violations,
RAM 317/358, ALM 85%. This is a fitter placement seed only -- no RTL or
optimization-mode change. Per-seed reports preserved under
synth/.../seed_sweep/ (not committed; build artifacts).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 12:31:31 -04:00
thejayman77 6319d7ca85 Ch443d: registered AXI R buffer (texcache fill) + drop fill_data_q reset
Closes the last currently-visible EMIF-handshake -> FSM setup family the
Ch443c fit exposed (-0.043/-0.022/-0.005 ns), the read-response analogue
of the AW/W buffers.

- gs_axi_r_regbuf: one-entry FULLY-registered AXI R buffer (the R twin of
  gs_axi_w_regbuf). Buffers the complete {rdata,rresp,rlast}; u_rready =
  !full only (NO combinational dependence on the texture FSM's d_rready);
  captures on u_rvalid && u_rready; d_rvalid = full with the payload held
  stable until d_rvalid && d_rready; resets only . Inserted between
  read-arbiter s2 and gs_texture_cache (u_texf_rbuf). The arbiter is
  unchanged -- it completes its R transaction into the buffer, which then
  owns delivery to the fill FSM. Cuts EMIF rvalid/rdata -> fst.F_R.
- gs_texture_cache: drop the unobservable fill_data_q reset. F_DRAIN (its
  only reader) is reachable only after F_R loads it, so the reset value is
  never observed; removing it kills the separate lock_sync|dreg[1] ->
  fill_data_q[80] setup path (-0.005 ns).
- New tb_gs_axi_r_regbuf: exactly-once/in-order, randomized responses +
  stalls, full backpressure, the full && d_rready no-fall-through case,
  {rdata,rresp,rlast} stability, reset-while-empty AND reset-while-full.

All green: r-buffer TB, texture_cache, texture_psmt8_clut, scanout_lb,
scanout_restart, scanout_diag, ps2_hps_bridge, rd_arb, and the complete
f52 replay BYTE-IDENTICAL (Z 0/307200, COLOR 0/245760). No Quartus/board/
push from here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 10:03:01 -04:00
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
thejayman77 4358bc328b Ch443b: revert texture 4-bank split; 2-cycle drain (F_SETTLE + multicycle)
The four-bank tex_mem split (27dfd0b) closed the -0.370 EMIF drain
write-address fanout but the owner GUI fit showed the fitter SCATTERED
the banks, pushing the DESIGN-clock sampler read cone
(ras_v0_x -> perspective-UV -> texel addr -> tex_mem portbaddr, the
design's fundamental ~40ns critical path) to -2.208 ns. Net worse.

Codex's call (Option 1 + honest write-side multicycle), implemented:
- Restore the MONOLITHIC 65536x32 tex_mem, recovering the clean 25 MHz
  read-cone placement. Sampler/read-address path stays fully timed
  (Ch439g); nothing about it is relaxed.
- Make the EMIF drain write genuinely two-cycle: new F_SETTLE state
  between F_DRAIN and F_WRITE. drain_idx_q/drain_word_q are loaded in
  F_DRAIN, HELD unchanged through F_SETTLE (the load block gates on
  F_DRAIN), and the RAM write + CRC happen at the later F_WRITE edge.
- SDC: fail-closed 2-cycle-setup / 1-cycle-hold multicycle from ONLY
  u_texcache|drain_idx_q[*] to tex_mem (a 6.45 ns EMIF window for the
  drain write-address). Scoped -from the drain regs, so the sampler
  read path (different launch regs) is untouched. HALTs if tex_mem is
  present but drain_idx_q renamed.

The AW buffer (gs_axi_aw_regbuf) and the tile-CDC max-skew 2.5 relax
from 27dfd0b are KEPT unchanged (both closed their families in the fit).

Verified: tb_gs_texture_cache (monolithic + F_SETTLE, distinct-per-byte-
lane + full-word, 0 errors), aw/w regbuf, texture_psmt8_clut,
scanout_diag, ps2_hps_bridge, and the complete f52 replay BYTE-IDENTICAL
(Z 0/307200, COLOR 0/245760). No Quartus/board/push from here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 15:11:46 -04:00
thejayman77 27dfd0b0cf Ch443: combined timing repair for both Ch442 setup families
Ch442's fit exposed two failing setup families (plus a stale max-skew).
Attack both structurally; no reseeding.

Family 1 - AWREADY -> Z-FSM (-0.410): gen_p2c_ff[23] (EMIF AWREADY)
reached u_zc_emit|u_z's S_SFLUSH_AW/S_FILL_R next-state combinationally.
Ch441 registered only the W channel; add the AW twin:
- new gs_axi_aw_regbuf (one-entry fully-registered AW buffer), inserted
  in zc_emit between u_z's AW output and the arbiter s2 AW port. The FSM
  now sees registered occupancy, never EMIF's combinational AWREADY.

Family 2 - texcache drain_idx_q -> tex_mem (-0.370, x7): a single index
fanned across the whole 65536x32, 128-M20K macro. Split by WIDTH into
four 65536x8 banks, each with its own (* preserve, dont_merge *) write-
address launch register; write the four byte lanes together in F_WRITE;
reconstruct the sample word by concatenating four registered read bytes.
Selector structure and 1-cycle read latency unchanged; total M20Ks
unchanged (4x32 == 128); fill_crc still sums the full 32-bit word.

Max-skew: relax ONLY the Ch357 tile-write CDC set_max_skew 2.0 -> 2.5
(quasi-static bundle, >=2 dclk stability window); retain set_net_delay
2.0 (the real arrival bound). SDC comment updated.

Tests: new tb_gs_axi_aw_regbuf (AW scoreboard: exactly-once/order/no-
combinational-AWREADY-bypass/stable-while-stalled); tb_gs_texture_cache
strengthened to distinct-per-byte-bank data + per-bank + full-word
checks. All pass: aw/w regbuf, texture_cache, texture_psmt8_clut,
scanout_diag, ps2_hps_bridge, and the complete f52 replay BYTE-IDENTICAL
(Z 0/307200, COLOR 0/245760).

No Quartus, board, or push from here. Ready for one owner GUI fit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 11:19:09 -04:00
thejayman77 194f45bd05 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>
2026-07-21 23:50:04 -04:00
36 changed files with 616254 additions and 156 deletions
@@ -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.
@@ -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
@@ -0,0 +1,8 @@
sample 1: 0x02C=0x000000F1 0x120=0x00000D33 0x124=0x02208821
sample 2: 0x02C=0x000000F1 0x120=0x00000D33 0x124=0x02208821
sample 3: 0x02C=0x000000F1 0x120=0x00000D33 0x124=0x02208821
sample 4: 0x02C=0x000000F1 0x120=0x00000D33 0x124=0x02208821
sample 5: 0x02C=0x000000F1 0x120=0x00000D33 0x124=0x02208821
sample 6: 0x02C=0x000000F1 0x120=0x00000D30 0x124=0x02208821
sample 7: 0x02C=0x000000F1 0x120=0x00000D33 0x124=0x02208821
sample 8: 0x02C=0x000000F1 0x120=0x00000D33 0x124=0x02208821
@@ -0,0 +1,44 @@
# Ch443d board verdict — per-frame scanout diagnostic (zsrt139f52, 2026-07-23)
Image: `retroDE_ps2.ch443d.core.rbf` SHA `87c382f1...` (26.1 GUI compile of `0c9b2bf` =
`6319d7c` RTL + SEED 3). Non-canonical candidate; Ch441 restored after capture.
## Fit + render (accepted)
Seed-3 GUI compile: EMIF setup +0.132, all timing classes >=0, 0 violated, no
AWREADY/texcache-fill/drain/scanout-diag families, RAM 317/358. Board f52 replay
byte-identical to golden (FB `d0047677`, texture CRC `0x13cfe390`, 0 drops).
## Per-frame diagnostic (8 samples over many displayed frames, stable)
| reg | raw | decode |
|-----|-----|--------|
| 0x02C | `0xF1` | scan underflow (bit5)=1, bresp=0 |
| 0x120 | `0x0D33` | valid=1, **underflow=1**, **read-error=0**, cause_base=**0**, **cause_lookahead=1**, line_valid=1, **vphase=13** |
| 0x124 | `0x02208821` | **scan_y=33, nf_v=34, nf_s0=34** |
## VERDICT — the REAL displayed-frame failure (NOT the warm-up, NOT a read error)
The Ch443c per-frame clear + restart-latency fix worked as intended: the pre-display
warm-up capture (previously scan_y=32/nf=32/base) is GONE. The diagnostic now reports the
true steady-state failure, and it is a **vertical LINEAR-FILTER LOOKAHEAD starvation**:
- scan_y=33 is resident (33 < nf_v=34 -> base cause correctly 0).
- At vphase=13 the V_LINEAR_FILTER interpolates between source rows 33 and 34, so it needs
the LOOKAHEAD row scan_y+1 = 34. But nf_v=34 means next_fetch is AT row 34 -> row 34 is
being fetched, NOT yet resident -> `scan_y+1 >= nf_v` -> lookahead cause fires -> underflow.
- read-error=0: not an AXI error. Deterministic every frame: structural, not a CDC glitch.
Root cause: the prefetch throttle leads by exactly ONE row (`next_fetch <= disp_row+1`), but
the vertical linear filter needs the lookahead row (disp_row+1) FULLY resident. The prefetch
is one row short of the filter's requirement, so displaying row 33 while row 34 is still
loading races the filter's read of row 34.
## Open question + fix direction (for Codex)
- Leading by 2 (`disp_row+2`) would make the lookahead row resident in time, but V_LINEAR uses
only 2 line buffers (parity) -- disp_row and disp_row+2 share a buffer, so leading by 2
needs a 3rd line buffer (as BINOMIAL already has) or a different prefetch/latency structure.
- The FB (memory) is byte-perfect; whether this lookahead underflow is HDMI-VISIBLE (a subtle
artifact at inter-row filter boundaries) needs an actual HDMI capture -- the flag is
conservative (fires when the lookahead row's fetch is in-flight, which may still complete
before the specific pixels are read).
Evidence: `ch443d_perframe_diag_8samples.txt`, `sh3_zsrt139f52_ch443d_board_fb.mem`,
`ch443d_rbf.sha256`.
@@ -0,0 +1 @@
87c382f1c1664fd049f1546f6ec250cc6f7d23a3d3475aa2ebad9194086aa73e docs/hardware/ch443d_board_validation/retroDE_ps2.ch443d.core.rbf
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,6 @@
sample 1: 0x02C=0x000000D1 0x120=0x00000733 0x124=0x02A0A027
sample 2: 0x02C=0x000000D1 0x120=0x00000D33 0x124=0x03E0F03B
sample 3: 0x02C=0x000000D1 0x120=0x00000130 0x124=0x0421003F
sample 4: 0x02C=0x000000D1 0x120=0x00000D30 0x124=0x06A1A067
sample 5: 0x02C=0x000000F1 0x120=0x00000B30 0x124=0x0DA360D7
sample 6: 0x02C=0x000000D1 0x120=0x00000B30 0x124=0x0AA2A0A7
@@ -0,0 +1 @@
bef7d7bfe83ddcc6368105cd8f46594ef0407a2cdc8c2139a3b26ef0adfef7ff docs/hardware/ch443e_board_validation/retroDE_ps2.ch443e.core.rbf
@@ -0,0 +1,57 @@
# Ch443f board validation
Date: 2026-07-24
Candidate:
- Git commit: `4110846` (`Ch443f timing: preload Z-RMW eviction awaddr at fill`)
- RBF: `retroDE_ps2.ch443f.4110846.core.rbf`
- SHA-256: `59c6372e1cfeba62dccdce860f7ab365255ef32a77c3f7b13909b4c7cff16a87`
- Quartus: 26.1.0 Build 110, Seed 3
Fit acceptance:
- Setup: `+0.068 ns`
- Hold: `0.000 ns`
- Recovery: `+1.080 ns`
- Removal: `+0.003 ns`
- Minimum pulse width: `+0.200 ns`
- RAM: `322 / 358`
- Zero violated paths
Board load:
- Remote candidate hash matched the local artifact.
- `core_loader.sh` reported `Fabric loaded successfully`.
- `fpga0` state: `operating`
- `CORE_ID`: `0x50533200`
- ABI: `0x00000100`
- Board canonical `/home/terasic/cores/retroDE_ps2.core.rbf` was not modified and
remained SHA-256 `56e45346338907eb6c307320757774ac65efa56dabb644906a5600f5ea73a982`.
Functional replay:
- Complete 311-epoch `sh3_zsrt139f52` replay finished `DONE rc=0`.
- Every epoch passed texture CRC, drain, and zero-fragment-drop gates.
- Framebuffer SHA-256:
`d0047677371a0f6e4e319458926f604a8599c92baaf34d09b8e1fd452e31662b`.
- The board framebuffer was byte-identical to the retained Ch443e/golden
framebuffer.
Scanout diagnostic:
- 60 samples at 50 ms spacing plus 60 samples at non-harmonic 7 ms spacing.
- All 120 observations were identical:
- `0x02C = 0x000000D1`: scan-error bit 5 clear.
- `0x120 = 0x00000018`: qualified valid=0, underflow=0, read-error=0,
live `pmax=0`.
- `0x124 = 0x00408020`: stale snapshot payload; ignored because valid=0.
- No fifth line buffer is justified. The Ch443e four-buffer/lead-2 change fixed
the real lookahead starvation; the remaining pre-Ch443f indication was caused
by the raw readiness CDC / synchronization transient.
Disposition:
- Ch443f is timing-clean, renderer-correct, and scanout-clean on silicon.
- Board left running the Ch443f candidate.
- Canonical artifact left untouched pending an explicit promotion instruction.
@@ -0,0 +1,5 @@
59c6372e1cfeba62dccdce860f7ab365255ef32a77c3f7b13909b4c7cff16a87 retroDE_ps2.ch443f.4110846.core.rbf
d0047677371a0f6e4e319458926f604a8599c92baaf34d09b8e1fd452e31662b sh3_zsrt139f52_ch443f_board_fb.mem
6b336f86b5fc2e3636fa6fa602065041063611df51963b4a5978a130a517a5c9 sh3_zsrt139f52_ch443f_board_replay.log
c41cbfc3d12d8594244895f415a9179223f618e54797f4c3c316ed542bd74a9c ch443f_scan_diag_60samples.log
c41cbfc3d12d8594244895f415a9179223f618e54797f4c3c316ed542bd74a9c ch443f_scan_diag_60samples_phase_sweep.log
@@ -0,0 +1,60 @@
sample 01: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 02: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 03: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 04: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 05: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 06: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 07: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 08: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 09: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 10: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 11: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 12: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 13: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 14: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 15: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 16: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 17: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 18: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 19: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 20: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 21: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 22: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 23: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 24: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 25: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 26: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 27: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 28: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 29: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 30: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 31: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 32: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 33: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 34: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 35: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 36: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 37: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 38: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 39: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 40: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 41: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 42: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 43: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 44: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 45: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 46: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 47: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 48: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 49: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 50: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 51: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 52: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 53: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 54: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 55: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 56: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 57: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 58: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 59: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
sample 60: 0x02C=0x000000D1 0x120=0x00000018 0x124=0x00408020
+88
View File
@@ -0,0 +1,88 @@
// retroDE_ps2 — gs_axi_aw_regbuf (Ch443)
//
// Fully-registered ONE-ENTRY AXI AW-channel (write-address) buffer. The AW twin
// of gs_axi_w_regbuf. Inserted between the Z RMW master's (gs_lpddr_z_rmw) AW
// OUTPUT and the write arbiter's s2 AW INPUT to cut the combinational path
// EMIF gen_p2c_ff[23] (AWREADY) -> wr_arb s2_awready -> gs_lpddr_z_rmw
// next-state (`st`, S_SFLUSH_AW / S_FILL_R) — the -0.410 ns EMIF setup family.
//
// Ch441 registered the W channel (gs_axi_w_regbuf u_z_wbuf) but left AW running
// straight from the FSM to the arbiter, so EMIF AWREADY still reached the FSM
// combinationally. This buffer applies the identical structural cut to AW.
//
// CONTRACT (identical to gs_axi_w_regbuf, must hold exactly):
// - FULLY REGISTERED, not a fall-through skid: u_awready depends ONLY on the
// registered occupancy `full`, never on d_awready. Downstream (EMIF) AWREADY
// therefore can never propagate combinationally back into the upstream FSM.
// (i.e. NOT `u_awready = !full || d_awready`.)
// - Buffers the complete {AWADDR, AWLEN, AWSIZE, AWBURST} payload and holds it
// stable downstream until the arbiter accepts it.
// - Exactly-once: an address accepted upstream is delivered downstream exactly
// once. One in flight (the Z RMW issues single-beat writes, AWLEN=0).
// - W and B channels are NOT touched here (W is separately buffered by
// gs_axi_w_regbuf; B passes through). AXI permits AW and W in either order,
// and each one-entry buffer holds its beat until the arbiter accepts it, so
// the AW/W pair still reaches the slave together.
`timescale 1ns/1ps
module gs_axi_aw_regbuf #(
parameter int ADDR_W = 32,
parameter int LEN_W = 8,
parameter int SIZE_W = 3,
parameter int BURST_W = 2
) (
input logic clk,
input logic rst_n,
// upstream — from the Z RMW master's AW output
input logic [ADDR_W-1:0] u_awaddr,
input logic [LEN_W-1:0] u_awlen,
input logic [SIZE_W-1:0] u_awsize,
input logic [BURST_W-1:0] u_awburst,
input logic u_awvalid,
output logic u_awready,
// downstream — to the write arbiter's s2 AW input
output logic [ADDR_W-1:0] d_awaddr,
output logic [LEN_W-1:0] d_awlen,
output logic [SIZE_W-1:0] d_awsize,
output logic [BURST_W-1:0] d_awburst,
output logic d_awvalid,
input logic d_awready
);
logic full;
logic [ADDR_W-1:0] awaddr_q;
logic [LEN_W-1:0] awlen_q;
logic [SIZE_W-1:0] awsize_q;
logic [BURST_W-1:0] awburst_q;
// Upstream ready = registered occupancy ONLY (no d_awready term) -> EMIF AWREADY
// never reaches the upstream FSM combinationally.
assign u_awready = !full;
// Downstream presents the held address, stable until the arbiter accepts it.
assign d_awvalid = full;
assign d_awaddr = awaddr_q;
assign d_awlen = awlen_q;
assign d_awsize = awsize_q;
assign d_awburst = awburst_q;
// One-entry register. Accept an offered upstream address only while empty;
// release only when the arbiter accepts the held address. When full and
// accepted in the same cycle, u_awready is still 0 (full is registered), so
// the next address waits one cycle -> a swap/drop/dup is impossible. Payload
// registers deliberately have no reset (qualified by `full`/d_awvalid; the
// writer cannot present an address until reset releases).
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
full <= 1'b0;
end else if (!full) begin
if (u_awvalid) begin
full <= 1'b1;
awaddr_q <= u_awaddr;
awlen_q <= u_awlen;
awsize_q <= u_awsize;
awburst_q <= u_awburst;
end
end else begin
if (d_awready) full <= 1'b0;
end
end
endmodule : gs_axi_aw_regbuf
+78
View File
@@ -0,0 +1,78 @@
// retroDE_ps2 — gs_axi_r_regbuf (Ch443d)
//
// Fully-registered ONE-ENTRY AXI R-channel (read-response) buffer. The R twin of
// gs_axi_w_regbuf / gs_axi_aw_regbuf. Inserted between the read arbiter's s2 R
// OUTPUT and the texture-cache fill FSM's R INPUT to cut the combinational path
// EMIF gen_p2c_ff[*] (rvalid/rdata) -> gs_texture_cache F_R next-state (`fst`)
// + fill_data_q capture — the -0.043 ns / -0.022 ns EMIF setup family.
//
// CONTRACT (per Codex review, identical shape to gs_axi_w_regbuf):
// - FULLY REGISTERED, not a fall-through skid: u_rready depends ONLY on the
// registered occupancy `full`, never on the texture FSM's downstream d_rready.
// EMIF RVALID therefore can never propagate combinationally into the fill FSM.
// (i.e. NOT `u_rready = !full || d_rready`.)
// - Buffers the COMPLETE {RDATA, RRESP, RLAST} payload and holds it stable on the
// downstream side until the texture FSM accepts it.
// - Capture only on u_rvalid && u_rready; exactly-once, in-order (one in flight —
// the texture fill issues single-beat reads, ARLEN=0/RLAST=1, but RRESP/RLAST
// are preserved regardless).
// - Downstream VALID is `full`; payload held stable until d_rvalid && d_rready.
// - Reset only `full`; payload registers deliberately unreset (qualified by full).
// - The arbiter is unchanged: it completes its R transaction when the response is
// accepted into this buffer (s2_rready = u_rready = !full); the buffer then owns
// delivery to the texture FSM.
`timescale 1ns/1ps
module gs_axi_r_regbuf #(
parameter int RDATA_W = 256,
parameter int RRESP_W = 2
) (
input logic clk,
input logic rst_n,
// upstream — from the read arbiter's s2 R output (EMIF read return)
input logic [RDATA_W-1:0] u_rdata,
input logic [RRESP_W-1:0] u_rresp,
input logic u_rlast,
input logic u_rvalid,
output logic u_rready,
// downstream — to the texture-cache fill FSM's R input
output logic [RDATA_W-1:0] d_rdata,
output logic [RRESP_W-1:0] d_rresp,
output logic d_rlast,
output logic d_rvalid,
input logic d_rready
);
logic full;
logic [RDATA_W-1:0] rdata_q;
logic [RRESP_W-1:0] rresp_q;
logic rlast_q;
// Upstream ready = registered occupancy ONLY (no d_rready term) -> EMIF RVALID
// never reaches the texture fill FSM combinationally.
assign u_rready = !full;
// Downstream presents the held response, stable until the texture FSM accepts it.
assign d_rvalid = full;
assign d_rdata = rdata_q;
assign d_rresp = rresp_q;
assign d_rlast = rlast_q;
// One-entry register. Accept an offered upstream response only while empty;
// release only when the texture FSM accepts the held response. When full and
// accepted in the same cycle, u_rready is still 0 (full is registered), so the
// next response waits one cycle -> a swap/drop/dup is impossible. Payload
// registers deliberately have no reset (qualified by `full`/d_rvalid).
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
full <= 1'b0;
end else if (!full) begin
if (u_rvalid) begin
full <= 1'b1;
rdata_q <= u_rdata;
rresp_q <= u_rresp;
rlast_q <= u_rlast;
end
end else begin
if (d_rready) full <= 1'b0;
end
end
endmodule : gs_axi_r_regbuf
+173 -82
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,
@@ -116,10 +117,16 @@ module gs_lpddr_scanout_lb #(
assign arsize = 3'b101; // 32 bytes
// Two line buffers for legacy/linear scanout; Ch438 enables a third so the
// previous, current, and next source rows are resident simultaneously.
// previous, current, and next source rows are resident simultaneously. Ch443e
// adds a FOURTH buffer for BINOMIAL only: the 3x3 filter needs r-1/r/r+1 resident
// (3 buffers), so to lead the prefetch by 2 (fetch r+2 while displaying r) without
// overwriting r-1 (still needed) a 4th rotating buffer is required. Non-binomial
// configs leave lb3 unused (pruned) and keep the legacy 2/3-buffer, lead-1 behavior.
localparam int NBUF = BINOMIAL_3X3_FILTER ? 4 : 3; // buffers actually rotated
logic [255:0] lb0 [0:ROW_BEATS-1];
logic [255:0] lb1 [0:ROW_BEATS-1];
logic [255:0] lb2 [0:ROW_BEATS-1];
logic [255:0] lb3 [0:ROW_BEATS-1];
// ================= video side (video_clk) =================
// No miss-prone request toggle. The video side just exposes the current
@@ -133,7 +140,7 @@ module gs_lpddr_scanout_lb #(
// "disp_buf" lags by one cycle and corrupts col 0 of each line.
logic [$clog2(N_ROWS):0] stretch_src_y_q;
logic [3:0] stretch_vphase_q;
localparam int V_SOURCE_BUF = V_SOURCE_START % 3;
localparam int V_SOURCE_BUF = V_SOURCE_START % NBUF;
logic [1:0] stretch_buf_q;
logic in_window_v_q;
always_ff @(posedge video_clk) begin
@@ -157,8 +164,8 @@ module gs_lpddr_scanout_lb #(
else begin
stretch_src_y_q <= stretch_src_y_q + 1'b1;
stretch_vphase_q <= stretch_vphase_q - 1'b1;
stretch_buf_q <= (stretch_buf_q == 2'd2) ? 2'd0
: stretch_buf_q + 1'b1;
stretch_buf_q <= (stretch_buf_q == 2'(NBUF-1)) ? 2'd0
: stretch_buf_q + 1'b1;
end
end
end
@@ -236,11 +243,12 @@ module gs_lpddr_scanout_lb #(
logic [255:0] lb0_cache0_q, lb0_cache1_q;
logic [255:0] lb1_cache0_q, lb1_cache1_q;
logic [255:0] lb2_cache0_q, lb2_cache1_q;
logic [255:0] lb3_cache0_q, lb3_cache1_q; // Ch443e — binomial 4th buffer
logic blank_prime_q;
logic [RB_BITS-1:0] active_beat_q;
logic [RB_BITS-1:0] video_rd_addr_q, video_rd_tag_q;
logic video_rd_req_q, video_rd_valid_q;
logic [255:0] lb0_video_rd_q, lb1_video_rd_q, lb2_video_rd_q;
logic [255:0] lb0_video_rd_q, lb1_video_rd_q, lb2_video_rd_q, lb3_video_rd_q;
// Keep the inferred line-buffer read ports canonical: exactly one
// unconditional registered address and one registered data output
@@ -250,6 +258,7 @@ module gs_lpddr_scanout_lb #(
lb0_video_rd_q <= lb0[video_rd_addr_q];
lb1_video_rd_q <= lb1[video_rd_addr_q];
lb2_video_rd_q <= lb2[video_rd_addr_q];
lb3_video_rd_q <= lb3[video_rd_addr_q]; // Ch443e — binomial 4th buffer read port
video_rd_tag_q <= video_rd_addr_q;
video_rd_valid_q <= video_rd_req_q;
end
@@ -263,10 +272,12 @@ module gs_lpddr_scanout_lb #(
lb0_cache1_q <= lb0_video_rd_q;
lb1_cache1_q <= lb1_video_rd_q;
lb2_cache1_q <= lb2_video_rd_q;
lb3_cache1_q <= lb3_video_rd_q;
end else begin
lb0_cache0_q <= lb0_video_rd_q;
lb1_cache0_q <= lb1_video_rd_q;
lb2_cache0_q <= lb2_video_rd_q;
lb3_cache0_q <= lb3_video_rd_q;
end
end
@@ -297,31 +308,35 @@ module gs_lpddr_scanout_lb #(
// opposite cache slot still contains the preceding
// beat; by the time lane seven needs x+1 it contains
// the newly fetched successor beat.
// Ch443e — modulo-4 rotation across FOUR buffers. Current row is
// buffer scan_buf3; prev=(b-1)%4, next=(b+1)%4; the 4th buffer
// (b+2)%4 is being prefetched (row r+2, lead-2) and is not read here.
// First row clamps prev->cur; last row clamps next->cur.
case (scan_buf3)
2'd0: begin
2'd0: begin // cur=lb0 prev=lb3 next=lb1
prv_word_q <= (scan_y <= ($clog2(N_ROWS)+1)'(V_SOURCE_START))
? (col_beat[0] ? lb0_cache1_q : lb0_cache0_q)
: (col_beat[0] ? lb2_cache1_q : lb2_cache0_q);
: (col_beat[0] ? lb3_cache1_q : lb3_cache0_q);
cur_word_q <= col_beat[0] ? lb0_cache1_q : lb0_cache0_q;
nxt_word_q <= (scan_y + 1'b1 >= ($clog2(N_ROWS)+1)'(N_ROWS))
? (col_beat[0] ? lb0_cache1_q : lb0_cache0_q)
: (col_beat[0] ? lb1_cache1_q : lb1_cache0_q);
prv_left_px_q <= (scan_y <= ($clog2(N_ROWS)+1)'(V_SOURCE_START))
? (col_beat[0] ? lb0_cache0_q[255:224] : lb0_cache1_q[255:224])
: (col_beat[0] ? lb2_cache0_q[255:224] : lb2_cache1_q[255:224]);
: (col_beat[0] ? lb3_cache0_q[255:224] : lb3_cache1_q[255:224]);
cur_left_px_q <= col_beat[0] ? lb0_cache0_q[255:224] : lb0_cache1_q[255:224];
nxt_left_px_q <= (scan_y + 1'b1 >= ($clog2(N_ROWS)+1)'(N_ROWS))
? (col_beat[0] ? lb0_cache0_q[255:224] : lb0_cache1_q[255:224])
: (col_beat[0] ? lb1_cache0_q[255:224] : lb1_cache1_q[255:224]);
prv_look_px_q <= (scan_y <= ($clog2(N_ROWS)+1)'(V_SOURCE_START))
? (col_beat[0] ? lb0_cache0_q[31:0] : lb0_cache1_q[31:0])
: (col_beat[0] ? lb2_cache0_q[31:0] : lb2_cache1_q[31:0]);
: (col_beat[0] ? lb3_cache0_q[31:0] : lb3_cache1_q[31:0]);
cur_look_px_q <= col_beat[0] ? lb0_cache0_q[31:0] : lb0_cache1_q[31:0];
nxt_look_px_q <= (scan_y + 1'b1 >= ($clog2(N_ROWS)+1)'(N_ROWS))
? (col_beat[0] ? lb0_cache0_q[31:0] : lb0_cache1_q[31:0])
: (col_beat[0] ? lb1_cache0_q[31:0] : lb1_cache1_q[31:0]);
end
2'd1: begin
2'd1: begin // cur=lb1 prev=lb0 next=lb2
prv_word_q <= (scan_y <= ($clog2(N_ROWS)+1)'(V_SOURCE_START))
? (col_beat[0] ? lb1_cache1_q : lb1_cache0_q)
: (col_beat[0] ? lb0_cache1_q : lb0_cache0_q);
@@ -344,27 +359,50 @@ module gs_lpddr_scanout_lb #(
? (col_beat[0] ? lb1_cache0_q[31:0] : lb1_cache1_q[31:0])
: (col_beat[0] ? lb2_cache0_q[31:0] : lb2_cache1_q[31:0]);
end
default: begin
2'd2: begin // cur=lb2 prev=lb1 next=lb3
prv_word_q <= (scan_y <= ($clog2(N_ROWS)+1)'(V_SOURCE_START))
? (col_beat[0] ? lb2_cache1_q : lb2_cache0_q)
: (col_beat[0] ? lb1_cache1_q : lb1_cache0_q);
cur_word_q <= col_beat[0] ? lb2_cache1_q : lb2_cache0_q;
nxt_word_q <= (scan_y + 1'b1 >= ($clog2(N_ROWS)+1)'(N_ROWS))
? (col_beat[0] ? lb2_cache1_q : lb2_cache0_q)
: (col_beat[0] ? lb0_cache1_q : lb0_cache0_q);
: (col_beat[0] ? lb3_cache1_q : lb3_cache0_q);
prv_left_px_q <= (scan_y <= ($clog2(N_ROWS)+1)'(V_SOURCE_START))
? (col_beat[0] ? lb2_cache0_q[255:224] : lb2_cache1_q[255:224])
: (col_beat[0] ? lb1_cache0_q[255:224] : lb1_cache1_q[255:224]);
cur_left_px_q <= col_beat[0] ? lb2_cache0_q[255:224] : lb2_cache1_q[255:224];
nxt_left_px_q <= (scan_y + 1'b1 >= ($clog2(N_ROWS)+1)'(N_ROWS))
? (col_beat[0] ? lb2_cache0_q[255:224] : lb2_cache1_q[255:224])
: (col_beat[0] ? lb0_cache0_q[255:224] : lb0_cache1_q[255:224]);
: (col_beat[0] ? lb3_cache0_q[255:224] : lb3_cache1_q[255:224]);
prv_look_px_q <= (scan_y <= ($clog2(N_ROWS)+1)'(V_SOURCE_START))
? (col_beat[0] ? lb2_cache0_q[31:0] : lb2_cache1_q[31:0])
: (col_beat[0] ? lb1_cache0_q[31:0] : lb1_cache1_q[31:0]);
cur_look_px_q <= col_beat[0] ? lb2_cache0_q[31:0] : lb2_cache1_q[31:0];
nxt_look_px_q <= (scan_y + 1'b1 >= ($clog2(N_ROWS)+1)'(N_ROWS))
? (col_beat[0] ? lb2_cache0_q[31:0] : lb2_cache1_q[31:0])
: (col_beat[0] ? lb3_cache0_q[31:0] : lb3_cache1_q[31:0]);
end
default: begin // cur=lb3 prev=lb2 next=lb0
prv_word_q <= (scan_y <= ($clog2(N_ROWS)+1)'(V_SOURCE_START))
? (col_beat[0] ? lb3_cache1_q : lb3_cache0_q)
: (col_beat[0] ? lb2_cache1_q : lb2_cache0_q);
cur_word_q <= col_beat[0] ? lb3_cache1_q : lb3_cache0_q;
nxt_word_q <= (scan_y + 1'b1 >= ($clog2(N_ROWS)+1)'(N_ROWS))
? (col_beat[0] ? lb3_cache1_q : lb3_cache0_q)
: (col_beat[0] ? lb0_cache1_q : lb0_cache0_q);
prv_left_px_q <= (scan_y <= ($clog2(N_ROWS)+1)'(V_SOURCE_START))
? (col_beat[0] ? lb3_cache0_q[255:224] : lb3_cache1_q[255:224])
: (col_beat[0] ? lb2_cache0_q[255:224] : lb2_cache1_q[255:224]);
cur_left_px_q <= col_beat[0] ? lb3_cache0_q[255:224] : lb3_cache1_q[255:224];
nxt_left_px_q <= (scan_y + 1'b1 >= ($clog2(N_ROWS)+1)'(N_ROWS))
? (col_beat[0] ? lb3_cache0_q[255:224] : lb3_cache1_q[255:224])
: (col_beat[0] ? lb0_cache0_q[255:224] : lb0_cache1_q[255:224]);
prv_look_px_q <= (scan_y <= ($clog2(N_ROWS)+1)'(V_SOURCE_START))
? (col_beat[0] ? lb3_cache0_q[31:0] : lb3_cache1_q[31:0])
: (col_beat[0] ? lb2_cache0_q[31:0] : lb2_cache1_q[31:0]);
cur_look_px_q <= col_beat[0] ? lb3_cache0_q[31:0] : lb3_cache1_q[31:0];
nxt_look_px_q <= (scan_y + 1'b1 >= ($clog2(N_ROWS)+1)'(N_ROWS))
? (col_beat[0] ? lb3_cache0_q[31:0] : lb3_cache1_q[31:0])
: (col_beat[0] ? lb0_cache0_q[31:0] : lb0_cache1_q[31:0]);
end
endcase
@@ -635,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;
@@ -656,9 +708,9 @@ module gs_lpddr_scanout_lb #(
// cycle per beat and produced sustained line-buffer underflow on hardware.
// Data/address registers intentionally have no reset; the reset write-
// enables qualify them.
logic [255:0] lb0_wdata_q, lb1_wdata_q, lb2_wdata_q;
logic [RB_BITS-1:0] lb0_waddr_q, lb1_waddr_q, lb2_waddr_q;
logic lb0_we_q, lb1_we_q, lb2_we_q;
logic [255:0] lb0_wdata_q, lb1_wdata_q, lb2_wdata_q, lb3_wdata_q;
logic [RB_BITS-1:0] lb0_waddr_q, lb1_waddr_q, lb2_waddr_q, lb3_waddr_q;
logic lb0_we_q, lb1_we_q, lb2_we_q, lb3_we_q; // Ch443e — binomial 4th buffer
// Ch442 diag: registered single-bit reduction of the read-error counter (emif domain).
// Diagnostic-only; nothing downstream reads it, so it cannot perturb the fetch FSM.
logic rd_err_nz_q;
@@ -668,14 +720,14 @@ module gs_lpddr_scanout_lb #(
if (!axi_rst_n) begin
fs_sync_e <= 3'd0;
disp_row_s0 <= ($clog2(N_ROWS)+1)'(V_SOURCE_START);
disp_row_limit_e <= ($clog2(N_ROWS)+1)'(V_SOURCE_START + 1);
disp_row_limit_e <= ($clog2(N_ROWS)+1)'(V_SOURCE_START + (BINOMIAL_3X3_FILTER ? 2 : 1));
next_fetch <= ($clog2(N_ROWS)+1)'(V_SOURCE_START);
next_fetch_buf <= BINOMIAL_3X3_FILTER ? 2'(V_SOURCE_BUF)
: {1'b0, 1'(V_SOURCE_START)};
lst <= L_IDLE; araddr <= '0; arvalid <= 1'b0; rready <= 1'b0;
cur_row <= '0; cur_buf <= 2'd0; beat <= '0;
line_valid <= 1'b0; rd_errs <= 32'd0; fs_pending <= 1'b0;
lb0_we_q <= 1'b0; lb1_we_q <= 1'b0; lb2_we_q <= 1'b0;
lb0_we_q <= 1'b0; lb1_we_q <= 1'b0; lb2_we_q <= 1'b0; lb3_we_q <= 1'b0;
rd_err_nz_q <= 1'b0;
end else begin
fs_sync_e <= {fs_sync_e[1:0], frame_start};
@@ -684,7 +736,10 @@ module gs_lpddr_scanout_lb #(
// second CDC stage, but removes disp_row -> (+1) -> compare -> araddr
// enable from one 310 MHz cycle (the post-alpha fit's -0.125 ns family).
// The extra bit represents N_ROWS exactly on the final display row.
disp_row_limit_e <= disp_row_s0 + 1'b1;
// Ch443e — BINOMIAL leads by 2 (fetch disp_row+2 while displaying disp_row) so
// the 3x3 filter's lookahead row (disp_row+1) is FULLY resident before it is read;
// the 4th rotating buffer holds the in-flight disp_row+2. Legacy stays lead-1.
disp_row_limit_e <= disp_row_s0 + (BINOMIAL_3X3_FILTER ? 2'd2 : 2'd1);
// Ch439c — RAM-local response pipeline. Commit the response
// captured on the preceding cycle while the AXI FSM advances to
// (or waits for) the next single-beat read. This keeps the
@@ -694,9 +749,11 @@ module gs_lpddr_scanout_lb #(
if (lb0_we_q) lb0[lb0_waddr_q] <= lb0_wdata_q;
if (lb1_we_q) lb1[lb1_waddr_q] <= lb1_wdata_q;
if (lb2_we_q) lb2[lb2_waddr_q] <= lb2_wdata_q;
if (lb3_we_q) lb3[lb3_waddr_q] <= lb3_wdata_q;
lb0_we_q <= 1'b0;
lb1_we_q <= 1'b0;
lb2_we_q <= 1'b0;
lb3_we_q <= 1'b0;
// Ch442 diag: register the read-error-nonzero flag (emif domain). One cycle of lag
// vs rd_errs is irrelevant for a stuck-nonzero indicator; keeps a clean launch flop.
rd_err_nz_q <= (rd_errs != 32'd0);
@@ -729,31 +786,41 @@ 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;
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;
lb3_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
// 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.
beat <= beat + 1'b1;
araddr <= araddr + 30'd32;
arvalid <= 1'b1;
lst <= L_AR;
// 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
2'd3: begin lb3_wdata_q <= rdata; lb3_waddr_q <= beat[RB_BITS-1:0]; lb3_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 (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
// 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;
lst <= L_AR;
end
end
end
end
@@ -763,7 +830,7 @@ module gs_lpddr_scanout_lb #(
line_valid <= 1'b1;
next_fetch <= next_fetch + 1'b1; // rows 0..next_fetch are now loaded
if (BINOMIAL_3X3_FILTER)
next_fetch_buf <= (next_fetch_buf == 2'd2) ? 2'd0
next_fetch_buf <= (next_fetch_buf == 2'd3) ? 2'd0 // Ch443e — mod-4 rotation
: next_fetch_buf + 1'b1;
else
next_fetch_buf <= {1'b0, ~next_fetch_buf[0]};
@@ -774,31 +841,30 @@ 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.
wire uf_in_range = enable && in_window && (scan_y < ($clog2(N_ROWS)+1)'(N_ROWS));
// 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 &&
(BINOMIAL_3X3_FILTER ||
@@ -806,33 +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). 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,
// 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;
// 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, 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) begin
diag_valid_q <= 1'b0;
end else if (!diag_valid_q && raw_uf_cond) begin
diag_valid_q <= 1'b1;
if (!enable || fs_edge_v) begin
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
+8 -2
View File
@@ -256,7 +256,7 @@ module gs_lpddr_z_rmw #(
// scene-end flush (ordered after all fragments): push the dirty line, keep it cached (persist across
// epochs). Waits for every pipeline stage to drain so nothing is stranded.
if (scene_flush && !d_valid && !rmw_valid && !cmp_valid && cache_valid && cache_dirty) begin
awaddr<=ZBASE + (cache_beat<<5); awvalid<=1'b1;
awvalid<=1'b1; // Ch443f — awaddr already prepared at S_FILL_C install
wdata<=cache_data; wvalid<=1'b1; wlast<=1'b1; st<=S_SFLUSH_AW;
end
// STAGE 2 — barrel READ of the target lane into the RMW register (hit), or promote+fill (miss). Fires
@@ -272,7 +272,7 @@ module gs_lpddr_z_rmw #(
// MISS: promote the decoded fragment to pf_*, backpressure, flush-if-dirty then fill
pf_x<=d_x; pf_y<=d_y; pf_zq<=d_zq; pf_zmsk<=d_zmsk; pf_ztst<=d_ztst; pf_beat<=d_beat; pf_lane<=d_lane;
if (cache_valid && cache_dirty) begin
awaddr<=ZBASE + (cache_beat<<5); awvalid<=1'b1;
awvalid<=1'b1; // Ch443f — awaddr already prepared at S_FILL_C install
wdata<=cache_data; wvalid<=1'b1; wlast<=1'b1; st<=S_FLUSH_AW;
end else begin
araddr<=ZBASE + (d_beat<<5); arvalid<=1'b1; st<=S_FILL_AR;
@@ -318,6 +318,12 @@ module gs_lpddr_z_rmw #(
// RMW stage (barrel READ of the target lane off the just-read beat). Stage 3 does the GEQUAL + 16-way
// lane write next cycle in S_RUN (a guaranteed hit). Same result as the old single-cycle fill, staged.
cache_data<=z_rd_q; cache_beat<=pf_beat; cache_valid<=1'b1; cache_dirty<=1'b0;
// Ch443f — PRELOAD the line's normal-eviction AW address at install. The address
// is a pure function of this line's beat (== cache_beat after this cycle) and is
// stable across every hit/flush until the next fill re-installs it. Preparing it
// here removes the 5-level cache_dirty -> awaddr control cone that was the EMIF
// setup leader; the eviction branches below now only assert awvalid.
awaddr<=ZBASE + (pf_beat<<5);
rmw_valid<=1'b1; rmw_dz<=z_rd_q[pf_lane*16 +: 16];
rmw_lane<=pf_lane; rmw_zq<=pf_zq; rmw_zmsk<=pf_zmsk; rmw_ztst<=pf_ztst; rmw_x<=pf_x; rmw_y<=pf_y;
st<=S_RUN;
+14 -2
View File
@@ -155,6 +155,10 @@ module gs_lpddr_zc_emit #(
// ports (write arbiter s2). This cuts the combinational EMIF WREADY -> z_rmw
// FSM path. AW and B pass straight through (z_awaddr.../z_bvalid... unchanged).
logic [255:0] zi_wdata; logic [31:0] zi_wstrb; logic zi_wlast, zi_wvalid, zi_wready;
// Ch443: the Z RMW master's AW output ALSO goes through a one-entry registered
// buffer (gs_axi_aw_regbuf u_z_awbuf, below) so the FSM sees a REGISTERED awready
// (buffer occupancy), never EMIF's combinational AWREADY (gen_p2c_ff[23]).
logic [31:0] zi_awaddr; logic [7:0] zi_awlen; logic [2:0] zi_awsize; logic [1:0] zi_awburst; logic zi_awvalid, zi_awready;
gs_lpddr_z_rmw #(.ZBASE(ZBASE), .FB_PXW(FB_PXW), .FB_H(FB_H), .Z_CLEAR(Z_CLEAR)) u_z (
.clk(axi_clk), .rst_n(axi_rst_n), .enable(enable), .clear_start(clear_start), .clear_done(clear_done),
.scene_flush(z_sflush), .z_drained(z_drained),
@@ -163,20 +167,28 @@ module gs_lpddr_zc_emit #(
.p_valid(z_pvalid), .p_ready(z_pready), .p_pass(z_ppass), .p_x(z_px), .p_y(z_py), .p_zq(z_pzq),
.araddr(z_araddr), .arlen(z_arlen), .arsize(z_arsize), .arburst(z_arburst), .arvalid(z_arvalid), .arready(z_arready),
.rdata(z_rdata), .rresp(z_rresp), .rlast(z_rlast), .rvalid(z_rvalid), .rready(z_rready),
.awaddr(z_awaddr), .awlen(z_awlen), .awsize(z_awsize), .awburst(z_awburst), .awvalid(z_awvalid), .awready(z_awready),
.awaddr(zi_awaddr), .awlen(zi_awlen), .awsize(zi_awsize), .awburst(zi_awburst), .awvalid(zi_awvalid), .awready(zi_awready),
.wdata(zi_wdata), .wstrb(zi_wstrb), .wlast(zi_wlast), .wvalid(zi_wvalid), .wready(zi_wready),
.bvalid(z_bvalid), .bready(z_bready), .bresp(z_bresp),
.beats_read(z_beats_read), .beats_written(z_beats_written), .bresp_err(bresp_err), .idle(z_idle)
);
// Ch441: one-entry fully-registered W-channel buffer between the Z RMW master
// (u_z) and the write arbiter s2 (via the z_w* ports). Breaks the combinational
// EMIF WREADY -> z_rmw FSM path. AW/B untouched; the arbiter's bready_q still
// EMIF WREADY -> z_rmw FSM path. B untouched; the arbiter's bready_q still
// arms on the real EMIF m_wvalid && m_wready && m_wlast (buffer is upstream).
gs_axi_w_regbuf #(.WDATA_W(256), .WSTRB_W(32)) u_z_wbuf (
.clk(axi_clk), .rst_n(axi_rst_n),
.u_wdata(zi_wdata), .u_wstrb(zi_wstrb), .u_wlast(zi_wlast), .u_wvalid(zi_wvalid), .u_wready(zi_wready),
.d_wdata(z_wdata), .d_wstrb(z_wstrb), .d_wlast(z_wlast), .d_wvalid(z_wvalid), .d_wready(z_wready)
);
// Ch443: one-entry fully-registered AW-channel buffer, the AW twin of u_z_wbuf.
// Cuts the combinational EMIF AWREADY -> z_rmw FSM next-state (S_SFLUSH_AW/
// S_FILL_R) path — the -0.410 ns setup family Ch442's fit exposed.
gs_axi_aw_regbuf #(.ADDR_W(32), .LEN_W(8), .SIZE_W(3), .BURST_W(2)) u_z_awbuf (
.clk(axi_clk), .rst_n(axi_rst_n),
.u_awaddr(zi_awaddr), .u_awlen(zi_awlen), .u_awsize(zi_awsize), .u_awburst(zi_awburst), .u_awvalid(zi_awvalid), .u_awready(zi_awready),
.d_awaddr(z_awaddr), .d_awlen(z_awlen), .d_awsize(z_awsize), .d_awburst(z_awburst), .d_awvalid(z_awvalid), .d_awready(z_awready)
);
// A fragment carries ztest: when ztest=0 it must ALWAYS pass. Feed the RMW a zmsk so it never writes Z for a
// non-Z fragment, and force its zq to max so GEQUAL always passes. (All scheduler draws are ztest=1.)
// (Handled at feed below via the always-pass override on the pass decision.)
+26 -3
View File
@@ -85,10 +85,22 @@ module gs_texture_cache #(
// 1x 65536x32 (HERE) -> latch each AXI beat, drain 8 lanes over 8 axi_clk cycles.
// Serializing the fill removes the multi-bank/multi-write geometry while preserving the sampler's
// one-cycle registered 32-bit read. The one-shot fill is still tiny compared with board startup.
// Ch443 (Codex): MONOLITHIC 65536x32 texture RAM. A four-bank width split was tried
// to relieve the -0.370 ns EMIF drain write-address fanout, but it scattered the banks
// and pushed the DESIGN-clock sampler read cone (ras_v0_x -> perspective-UV -> texel
// addr -> tex_mem portbaddr) to -2.208 ns. The monolithic RAM restores that clean 25MHz
// placement; the drain write-address is instead given a FUNCTIONALLY HONEST 2-cycle
// window (F_SETTLE state + a drain-only multicycle SDC exception), never touching the
// sampler-facing read port.
(* ramstyle = "M20K" *) logic [31:0] tex_mem [0:TEX_WORDS-1];
// ================= fill side (axi_clk) =================
typedef enum logic [2:0] { F_IDLE, F_AR, F_R, F_DRAIN, F_WRITE, F_DONE } fstate_t;
// Ch443: F_SETTLE inserted between F_DRAIN and F_WRITE. drain_idx_q/drain_word_q are
// loaded at the F_DRAIN edge and held unchanged through F_SETTLE; the RAM write-enable
// asserts only at the later F_WRITE edge. The drain address is thus functionally
// required 2 cycles after launch -> a fail-closed 2-cycle-setup/1-cycle-hold multicycle
// (drain_idx_q -> tex_mem only) is honest, giving the EMIF write-address a 6.45 ns window.
typedef enum logic [2:0] { F_IDLE, F_AR, F_R, F_DRAIN, F_SETTLE, F_WRITE, F_DONE } fstate_t;
fstate_t fst;
logic [$clog2(N_BEATS):0] beat; // 0..N_BEATS
logic [255:0] fill_data_q;
@@ -114,7 +126,11 @@ module gs_texture_cache #(
if (!axi_rst_n) begin
fst <= F_IDLE; araddr <= '0; arvalid <= 1'b0; rready <= 1'b0;
beat <= '0; fill_done <= 1'b0; fill_beats <= 32'd0; fill_bytes <= 32'd0;
rd_errs <= 32'd0; fs_sync <= 3'd0; fill_data_q <= '0;
rd_errs <= 32'd0; fs_sync <= 3'd0;
// Ch443d (Codex): fill_data_q is deliberately UNRESET. F_DRAIN (its only reader)
// is reachable only after F_R has loaded it from the buffered read response, so
// its reset value is unobservable. Dropping the reset removes the separate
// lock_sync_inst|dreg[1] -> fill_data_q[80] setup path (-0.005 ns).
fill_lane <= 3'd0; fill_word_base <= '0; fill_crc <= 32'd0;
end else begin
fs_sync <= {fs_sync[1:0], fill_start};
@@ -151,6 +167,13 @@ module gs_texture_cache #(
end
end
F_DRAIN: begin
// LOAD half only (see the separate load block). Advance to F_SETTLE so
// the just-loaded address/word are held one extra cycle before the write.
fst <= F_SETTLE;
end
F_SETTLE: begin
// HOLD: drain_idx_q/drain_word_q are NOT reloaded (load block gates on
// F_DRAIN), so they stay stable across this edge. No RAM write here.
fst <= F_WRITE;
end
F_WRITE: begin
@@ -198,7 +221,7 @@ module gs_texture_cache #(
always_ff @(posedge axi_clk) begin
if (fst == F_DRAIN) begin
drain_word_q <= fill_data_q[fill_lane*32 +: 32];
drain_idx_q <= fill_word_idx;
drain_idx_q <= fill_word_idx; // held through F_SETTLE (block gates on F_DRAIN), written at F_WRITE
end
end
+27 -6
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).
@@ -651,11 +652,20 @@ 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;
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_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
@@ -946,9 +956,12 @@ 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_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;
@@ -1114,7 +1127,10 @@ 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;
scan_diag_look_q <= 1'b0;
scan_diag_lv_q <= 1'b0;
@@ -1158,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};
@@ -1169,6 +1187,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};
+21 -6
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
@@ -1991,6 +1992,9 @@ module de25_nano_psmct32_raster_demo_top (
wire [29:0] texf_ar_araddr; wire [1:0] texf_ar_arburst; wire [6:0] texf_ar_arid;
wire [7:0] texf_ar_arlen; wire [2:0] texf_ar_arsize; wire texf_ar_arvalid, texf_ar_arready;
wire [255:0] texf_r_rdata; wire [1:0] texf_r_rresp; wire texf_r_rlast, texf_r_rvalid, texf_r_rready;
// Ch443d — read-arbiter s2 R output feeds a one-entry registered R buffer (texf_ri_* =
// buffer upstream, from the arbiter); the buffer drives texf_r_* into the texture fill FSM.
wire [255:0] texf_ri_rdata; wire [1:0] texf_ri_rresp; wire texf_ri_rlast, texf_ri_rvalid, texf_ri_rready;
EMIF_Qsys u_emif_lpddr4b (
.iopll_refclk_clk (CLOCK2_50),
@@ -2678,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),
@@ -2719,8 +2723,8 @@ module de25_nano_psmct32_raster_demo_top (
.s2_araddr(texf_ar_araddr), .s2_arburst(texf_ar_arburst), .s2_arid(texf_ar_arid),
.s2_arlen(texf_ar_arlen), .s2_arsize(texf_ar_arsize), .s2_arvalid(texf_ar_arvalid),
.s2_arready(texf_ar_arready),
.s2_rdata(texf_r_rdata), .s2_rresp(texf_r_rresp), .s2_rlast(texf_r_rlast),
.s2_rvalid(texf_r_rvalid), .s2_rready(texf_r_rready),
.s2_rdata(texf_ri_rdata), .s2_rresp(texf_ri_rresp), .s2_rlast(texf_ri_rlast),
.s2_rvalid(texf_ri_rvalid), .s2_rready(texf_ri_rready),
.s3_araddr(reload_ar_araddr), .s3_arburst(reload_ar_arburst), .s3_arid(reload_ar_arid),
.s3_arlen(reload_ar_arlen), .s3_arsize(reload_ar_arsize), .s3_arvalid(reload_ar_arvalid),
.s3_arready(reload_ar_arready),
@@ -2740,6 +2744,16 @@ module de25_nano_psmct32_raster_demo_top (
// (one-shot before raster, armed by the bridge); sample side on design_clk, tapping
// u_demo's texel-fetch request and returning the texel at the existing 1-cycle latency.
`ifdef GS_LPDDR_TEX
// Ch443d — one-entry fully-registered R buffer between read-arbiter s2 and the
// texture fill FSM. Cuts the combinational EMIF RVALID/RDATA -> gs_texture_cache
// F_R next-state (`fst`) path (the -0.043/-0.022 ns EMIF setup family). The arbiter
// completes its R transaction into this buffer (s2_rready = !full); the buffer owns
// delivery to the fill FSM. B/AR/W untouched.
gs_axi_r_regbuf #(.RDATA_W(256), .RRESP_W(2)) u_texf_rbuf (
.clk(emif_clk), .rst_n(emif_reset_n),
.u_rdata(texf_ri_rdata), .u_rresp(texf_ri_rresp), .u_rlast(texf_ri_rlast), .u_rvalid(texf_ri_rvalid), .u_rready(texf_ri_rready),
.d_rdata(texf_r_rdata), .d_rresp(texf_r_rresp), .d_rlast(texf_r_rlast), .d_rvalid(texf_r_rvalid), .d_rready(texf_r_rready)
);
gs_texture_cache #(
.LPDDR_TEX_BASE(TEX_LPDDR_BASE), .TEX_VRAM_BASE(TEXC_VRAM_BASE), .TEX_BYTES(TEXC_BYTES), .N_BEATS(TEXC_NBEATS)
) u_texcache (
@@ -2758,7 +2772,7 @@ module de25_nano_psmct32_raster_demo_top (
// no texture cache — tie read-port-2 inert (arvalid=0, rready=1 drains).
assign texf_ar_araddr=30'd0; assign texf_ar_arburst=2'b01; assign texf_ar_arid=7'd4;
assign texf_ar_arlen=8'd0; assign texf_ar_arsize=3'b101; assign texf_ar_arvalid=1'b0;
assign texf_r_rready=1'b1;
assign texf_ri_rready=1'b1; // Ch443d — drain the arbiter s2 R directly (no R buffer / texcache in this profile)
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;
`ifndef GS_TILE_SPILL
@@ -2833,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;
@@ -2855,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;
@@ -3126,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),
+49 -1
View File
@@ -586,6 +586,8 @@ RTL_SRCS := \
$(RTL_ROOT)/gif_gs/gs_lpddr_axi_master.sv \
$(RTL_ROOT)/gif_gs/gs_lpddr_z_rmw.sv \
$(RTL_ROOT)/gif_gs/gs_axi_w_regbuf.sv \
$(RTL_ROOT)/gif_gs/gs_axi_aw_regbuf.sv \
$(RTL_ROOT)/gif_gs/gs_axi_r_regbuf.sv \
$(RTL_ROOT)/gif_gs/gs_lpddr_zc_emit.sv \
$(RTL_ROOT)/gif_gs/gs_lpddr_rd_probe.sv \
$(RTL_ROOT)/gif_gs/gs_lpddr_scanout.sv \
@@ -1153,6 +1155,24 @@ tb_gs_axi_w_regbuf: dirs
@echo "=== run tb_gs_axi_w_regbuf ==="
@cd $(TRACE_DIR) && $(VVP) $(BUILD_DIR)/tb_gs_axi_w_regbuf.vvp
tb_gs_axi_aw_regbuf: dirs
@echo "=== build tb_gs_axi_aw_regbuf ==="
$(IVERILOG) $(IVERILOG_FLGS) \
-o $(BUILD_DIR)/tb_gs_axi_aw_regbuf.vvp \
-s tb_gs_axi_aw_regbuf \
$(RTL_SRCS) $(TB_ROOT)/gif_gs/tb_gs_axi_aw_regbuf.sv
@echo "=== run tb_gs_axi_aw_regbuf ==="
@cd $(TRACE_DIR) && $(VVP) $(BUILD_DIR)/tb_gs_axi_aw_regbuf.vvp
tb_gs_axi_r_regbuf: dirs
@echo "=== build tb_gs_axi_r_regbuf ==="
$(IVERILOG) $(IVERILOG_FLGS) \
-o $(BUILD_DIR)/tb_gs_axi_r_regbuf.vvp \
-s tb_gs_axi_r_regbuf \
$(RTL_SRCS) $(TB_ROOT)/gif_gs/tb_gs_axi_r_regbuf.sv
@echo "=== run tb_gs_axi_r_regbuf ==="
@cd $(TRACE_DIR) && $(VVP) $(BUILD_DIR)/tb_gs_axi_r_regbuf.vvp
tb_gs_grad_divider: dirs
@echo "=== build tb_gs_grad_divider ==="
$(IVERILOG) $(IVERILOG_FLGS) \
@@ -1261,6 +1281,15 @@ tb_gs_lpddr_scanout_lb_binomial: dirs
@echo "=== run tb_gs_lpddr_scanout_lb_binomial ==="
@cd $(TRACE_DIR) && $(VVP) $(BUILD_DIR)/tb_gs_lpddr_scanout_lb_binomial.vvp
tb_gs_scanout_binomial_lookahead: dirs
@echo "=== build tb_gs_scanout_binomial_lookahead ==="
$(IVERILOG) $(IVERILOG_FLGS) \
-o $(BUILD_DIR)/tb_gs_scanout_binomial_lookahead.vvp \
-s tb_gs_scanout_binomial_lookahead \
$(RTL_SRCS) $(TB_ROOT)/gif_gs/tb_gs_scanout_binomial_lookahead.sv
@echo "=== run tb_gs_scanout_binomial_lookahead ==="
@cd $(TRACE_DIR) && $(VVP) $(BUILD_DIR)/tb_gs_scanout_binomial_lookahead.vvp
tb_gs_scanout_diag: dirs
@echo "=== build tb_gs_scanout_diag ==="
$(IVERILOG) $(IVERILOG_FLGS) \
@@ -1270,6 +1299,24 @@ 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_cdc_qual: dirs
@echo "=== build tb_gs_scanout_cdc_qual ==="
$(IVERILOG) $(IVERILOG_FLGS) \
-o $(BUILD_DIR)/tb_gs_scanout_cdc_qual.vvp \
-s tb_gs_scanout_cdc_qual \
$(RTL_SRCS) $(TB_ROOT)/gif_gs/tb_gs_scanout_cdc_qual.sv
@echo "=== run tb_gs_scanout_cdc_qual ==="
@cd $(TRACE_DIR) && $(VVP) $(BUILD_DIR)/tb_gs_scanout_cdc_qual.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) \
@@ -6282,7 +6329,8 @@ run: tb_top_psmct32_sh3_zs640c12_cap tb_top_psmct32_sh3_zint640c12
run: tb_top_psmct32_sh3_zs640b24_cap tb_top_psmct32_sh3_zint640b24
.PHONY: tb_top_psmct32_sh3_zs640c24c_cap tb_top_psmct32_sh3_zint640c24c sh3_zs640c24c_fixture sh3_zs640motionabc_bootlet
run: tb_top_psmct32_sh3_zs640c24c_cap tb_top_psmct32_sh3_zint640c24c tb_gs_axi_w_regbuf
run: tb_top_psmct32_sh3_zs640c24c_cap tb_top_psmct32_sh3_zint640c24c tb_gs_axi_w_regbuf tb_gs_axi_aw_regbuf tb_gs_axi_r_regbuf
run: tb_gs_lpddr_scanout_lb tb_gs_scanout_binomial_lookahead tb_gs_scanout_diag tb_gs_scanout_restart tb_gs_scanout_cdc_qual
run: tb_ee_fetch tb_gs tb_intc tb_platform_video tb_bgcolor_via_dma tb_sif_mailbox \
tb_sif_command_echo tb_sif_command_echo_rearm tb_sif_negative_path \
+138
View File
@@ -0,0 +1,138 @@
// retroDE_ps2 — tb_gs_axi_aw_regbuf (Ch443)
//
// AW twin of tb_gs_axi_w_regbuf. Focused scoreboard for the one-entry fully-
// registered AXI AW buffer. Verifies:
// (1) EXACTLY-ONCE, IN-ORDER delivery with payload integrity {AWADDR,AWLEN,
// AWSIZE,AWBURST} under randomized upstream offer + downstream backpressure
// (a drop, dup, or reorder trips the sequence scoreboard).
// (2) The NO-COMBINATIONAL-BYPASS contract: u_awready === !full every cycle, so a
// fall-through `u_awready = !full || d_awready` (which would leak downstream
// AWREADY back upstream into the Z FSM) is caught. A directed phase forces the
// full && d_awready case.
// (3) Downstream payload held STABLE while d_awvalid && !d_awready.
// (4) Full drain leaves the buffer empty with equal produced/consumed counts.
`timescale 1ns/1ps
module tb_gs_axi_aw_regbuf;
localparam int AW = 32, LN = 8, SZ = 3, BR = 2;
logic clk = 0; always #5 clk = ~clk; // 100 MHz
logic rst_n;
logic [AW-1:0] u_awaddr; logic [LN-1:0] u_awlen; logic [SZ-1:0] u_awsize; logic [BR-1:0] u_awburst; logic u_awvalid, u_awready;
logic [AW-1:0] d_awaddr; logic [LN-1:0] d_awlen; logic [SZ-1:0] d_awsize; logic [BR-1:0] d_awburst; logic d_awvalid; logic d_awready;
gs_axi_aw_regbuf #(.ADDR_W(AW), .LEN_W(LN), .SIZE_W(SZ), .BURST_W(BR)) dut (
.clk(clk), .rst_n(rst_n),
.u_awaddr(u_awaddr), .u_awlen(u_awlen), .u_awsize(u_awsize), .u_awburst(u_awburst), .u_awvalid(u_awvalid), .u_awready(u_awready),
.d_awaddr(d_awaddr), .d_awlen(d_awlen), .d_awsize(d_awsize), .d_awburst(d_awburst), .d_awvalid(d_awvalid), .d_awready(d_awready)
);
int errors; initial errors = 0;
// distinct payload per sequence value (fills all fields)
function automatic logic [AW-1:0] mk_addr(input logic [31:0] s); mk_addr = (s ^ 32'hCAFE_0000) | 32'd4; endfunction
function automatic logic [LN-1:0] mk_len (input logic [31:0] s); mk_len = s[7:0]; endfunction
function automatic logic [SZ-1:0] mk_size(input logic [31:0] s); mk_size = s[2:0] ^ 3'd5; endfunction
function automatic logic [BR-1:0] mk_brst(input logic [31:0] s); mk_brst = s[1:0] | 2'b01; endfunction
// LFSR backpressure on both sides
logic [15:0] ul = 16'hACE1, dl = 16'h1357;
always_ff @(posedge clk) begin
ul <= {ul[14:0], ul[15]^ul[13]^ul[12]^ul[10]};
dl <= {dl[14:0], dl[15]^dl[13]^dl[12]^dl[10]};
end
logic force_ready, force_stall, prod_freeze;
assign d_awready = force_ready ? 1'b1 : (force_stall ? 1'b0 : (dl[0] | dl[3]));
// AXI-legal producer: assert u_awvalid with STABLE payload until accepted.
logic [31:0] wr_seq;
logic pending;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin wr_seq <= 0; pending <= 1'b0; end
else if (u_awvalid && u_awready) begin
wr_seq <= wr_seq + 1;
pending <= (ul[0] | ul[3]) && !prod_freeze;
end
else if (!pending) pending <= (ul[0] | ul[3]) && !prod_freeze;
end
assign u_awvalid = pending;
assign u_awaddr = mk_addr(wr_seq);
assign u_awlen = mk_len(wr_seq);
assign u_awsize = mk_size(wr_seq);
assign u_awburst = mk_brst(wr_seq);
// (1) downstream scoreboard: exactly-once, in-order, payload-correct
logic [31:0] rd_seq;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) rd_seq <= 0;
else if (d_awvalid && d_awready) begin
if (d_awaddr !== mk_addr(rd_seq) || d_awlen !== mk_len(rd_seq) ||
d_awsize !== mk_size(rd_seq) || d_awburst !== mk_brst(rd_seq)) begin
if (errors < 20) $error("[awbuf] drop/dup/reorder/payload at seq %0d: addr %h len %h size %h burst %b",
rd_seq, d_awaddr, d_awlen, d_awsize, d_awburst);
errors++;
end
rd_seq <= rd_seq + 1;
end
end
// (2) NO combinational downstream-ready bypass: u_awready must equal !full.
always_ff @(posedge clk) if (rst_n) begin
if (u_awready !== !dut.full) begin
if (errors < 20) $error("[awbuf] u_awready(%b) != !full(%b) — combinational bypass?", u_awready, dut.full);
errors++;
end
end
logic saw_full_and_ready; initial saw_full_and_ready = 1'b0;
always_ff @(posedge clk) if (rst_n && dut.full && d_awready) saw_full_and_ready <= 1'b1;
// (3) while stalled, the SAME beat must still be presented.
logic [AW-1:0] hold_a; logic [LN-1:0] hold_l; logic [SZ-1:0] hold_s; logic [BR-1:0] hold_b; logic hold_v;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin hold_v <= 1'b0; hold_a <= '0; hold_l <= '0; hold_s <= '0; hold_b <= '0; end
else begin
if (hold_v) begin
if (!d_awvalid) begin
if (errors < 20) $error("[awbuf] d_awvalid deasserted while stalled"); errors++;
end else if (d_awaddr !== hold_a || d_awlen !== hold_l || d_awsize !== hold_s || d_awburst !== hold_b) begin
if (errors < 20) $error("[awbuf] downstream {awaddr,awlen,awsize,awburst} changed while stalled"); errors++;
end
end
hold_v <= d_awvalid && !d_awready;
hold_a <= d_awaddr; hold_l <= d_awlen; hold_s <= d_awsize; hold_b <= d_awburst;
end
end
initial begin
rst_n = 0; force_ready = 0; force_stall = 0; prod_freeze = 0;
repeat (6) @(posedge clk); rst_n = 1;
@(posedge clk);
if (d_awvalid !== 1'b0) begin $error("[awbuf] not empty after reset"); errors++; end
// Phase 1: randomized offer + backpressure
repeat (20000) @(posedge clk);
// Phase 2 (directed): stall downstream so the buffer fills and STAYS full
// (u_awready must read 0 = !full), then hold full while d_awready=1.
force_stall = 1; repeat (200) @(posedge clk);
force_stall = 0; force_ready = 1; repeat (200) @(posedge clk);
force_ready = 0;
// Phase 3: freeze producer, drain fully
prod_freeze = 1; force_ready = 1;
begin int g; g = 0; while ((wr_seq !== rd_seq) && g < 4000) begin @(posedge clk); g++; end end
repeat (10) @(posedge clk);
if (d_awvalid !== 1'b0) begin $error("[awbuf] not empty after drain (d_awvalid=%b)", d_awvalid); errors++; end
if (wr_seq !== rd_seq) begin $error("[awbuf] count mismatch: in %0d out %0d", wr_seq, rd_seq); errors++; end
if (wr_seq < 32'd2000) begin $error("[awbuf] too few transfers (%0d) — not meaningful", wr_seq); errors++; end
if (!saw_full_and_ready) begin $error("[awbuf] coverage: full && d_awready never observed — no-bypass case unexercised"); errors++; end
$display("[tb_gs_axi_aw_regbuf] in=%0d out=%0d errors=%0d", wr_seq, rd_seq, errors);
if (errors == 0) $display("[tb_gs_axi_aw_regbuf] PASS");
else $display("[tb_gs_axi_aw_regbuf] FAIL");
$finish;
end
initial begin #2000000; $error("[tb_gs_axi_aw_regbuf] TIMEOUT"); $finish; end
endmodule : tb_gs_axi_aw_regbuf
+145
View File
@@ -0,0 +1,145 @@
// retroDE_ps2 — tb_gs_axi_r_regbuf (Ch443d)
//
// Focused scoreboard for the one-entry fully-registered AXI R buffer (the R twin of
// tb_gs_axi_w_regbuf). Verifies:
// (1) EXACTLY-ONCE, IN-ORDER delivery with payload integrity {RDATA,RRESP,RLAST}
// under randomized upstream responses + downstream backpressure (a drop, dup,
// or reorder trips the sequence scoreboard).
// (2) NO-COMBINATIONAL-BYPASS: u_rready === !full every cycle, so a fall-through
// `u_rready = !full || d_rready` (which would leak the texture FSM's downstream
// READY back upstream into EMIF RVALID) is caught. A directed phase forces the
// full && d_rready case.
// (3) Downstream payload held STABLE while d_rvalid && !d_rready.
// (4) Reset while empty AND while full both leave the buffer empty.
`timescale 1ns/1ps
module tb_gs_axi_r_regbuf;
localparam int RD = 256, RS = 2;
logic clk = 0; always #5 clk = ~clk; // 100 MHz
logic rst_n;
logic [RD-1:0] u_rdata; logic [RS-1:0] u_rresp; logic u_rlast, u_rvalid, u_rready;
logic [RD-1:0] d_rdata; logic [RS-1:0] d_rresp; logic d_rlast, d_rvalid; logic d_rready;
gs_axi_r_regbuf #(.RDATA_W(RD), .RRESP_W(RS)) dut (
.clk(clk), .rst_n(rst_n),
.u_rdata(u_rdata), .u_rresp(u_rresp), .u_rlast(u_rlast), .u_rvalid(u_rvalid), .u_rready(u_rready),
.d_rdata(d_rdata), .d_rresp(d_rresp), .d_rlast(d_rlast), .d_rvalid(d_rvalid), .d_rready(d_rready)
);
int errors; initial errors = 0;
// distinct nonzero payload per sequence value
function automatic logic [RD-1:0] mk(input logic [31:0] s);
mk = {s^32'h1234ABCD, s+32'd7, ~s, s^32'h55AA55AA, s+32'd2, s^32'hF0F0F0F0, s+32'd9, s};
endfunction
function automatic logic [RS-1:0] mk_resp(input logic [31:0] s); mk_resp = s[1:0]; endfunction // 0..3 incl SLVERR
function automatic logic mk_last(input logic [31:0] s); mk_last = s[2]; endfunction
// LFSR backpressure both sides
logic [15:0] ul = 16'hACE1, dl = 16'h1357;
always_ff @(posedge clk) begin
ul <= {ul[14:0], ul[15]^ul[13]^ul[12]^ul[10]};
dl <= {dl[14:0], dl[15]^dl[13]^dl[12]^dl[10]};
end
logic force_ready, force_stall, prod_freeze;
assign d_rready = force_ready ? 1'b1 : (force_stall ? 1'b0 : (dl[0] | dl[3]));
// AXI-legal producer: assert u_rvalid with STABLE payload until accepted.
logic [31:0] wr_seq; logic pending;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin wr_seq <= 0; pending <= 1'b0; end
else if (u_rvalid && u_rready) begin
wr_seq <= wr_seq + 1;
pending <= (ul[0] | ul[3]) && !prod_freeze;
end
else if (!pending) pending <= (ul[0] | ul[3]) && !prod_freeze;
end
assign u_rvalid = pending;
assign u_rdata = mk(wr_seq);
assign u_rresp = mk_resp(wr_seq);
assign u_rlast = mk_last(wr_seq);
// (1) downstream scoreboard: exactly-once, in-order, payload-correct
logic [31:0] rd_seq;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) rd_seq <= 0;
else if (d_rvalid && d_rready) begin
if (d_rdata !== mk(rd_seq) || d_rresp !== mk_resp(rd_seq) || d_rlast !== mk_last(rd_seq)) begin
if (errors < 20) $error("[rbuf] drop/dup/reorder/payload at seq %0d: rdata %h resp %h last %b",
rd_seq, d_rdata, d_rresp, d_rlast);
errors++;
end
rd_seq <= rd_seq + 1;
end
end
// (2) NO combinational downstream-ready bypass: u_rready must equal !full.
always_ff @(posedge clk) if (rst_n) begin
if (u_rready !== !dut.full) begin
if (errors < 20) $error("[rbuf] u_rready(%b) != !full(%b) — combinational bypass?", u_rready, dut.full);
errors++;
end
end
logic saw_full_and_ready; initial saw_full_and_ready = 1'b0;
always_ff @(posedge clk) if (rst_n && dut.full && d_rready) saw_full_and_ready <= 1'b1;
// (3) while stalled, the SAME beat must still be presented.
logic [RD-1:0] hold_d; logic [RS-1:0] hold_r; logic hold_l, hold_v;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin hold_v <= 1'b0; hold_d <= '0; hold_r <= '0; hold_l <= 1'b0; end
else begin
if (hold_v) begin
if (!d_rvalid) begin
if (errors < 20) $error("[rbuf] d_rvalid deasserted while stalled"); errors++;
end else if (d_rdata !== hold_d || d_rresp !== hold_r || d_rlast !== hold_l) begin
if (errors < 20) $error("[rbuf] downstream {rdata,rresp,rlast} changed while stalled"); errors++;
end
end
hold_v <= d_rvalid && !d_rready;
hold_d <= d_rdata; hold_r <= d_rresp; hold_l <= d_rlast;
end
end
initial begin
rst_n = 0; force_ready = 0; force_stall = 0; prod_freeze = 0;
repeat (6) @(posedge clk); rst_n = 1;
@(posedge clk);
if (d_rvalid !== 1'b0) begin $error("[rbuf] not empty after reset (empty case)"); errors++; end
// Phase 1: reset while FULL — briefly run, stall to fill, then pulse reset. Done
// EARLY so the later count/coverage checks accumulate after it (reset zeroes wr_seq).
force_stall = 1; repeat (40) @(posedge clk);
if (dut.full !== 1'b1) begin $error("[rbuf] expected full before reset-while-full case"); errors++; end
rst_n = 0; repeat (3) @(posedge clk); rst_n = 1; force_stall = 0;
@(posedge clk);
if (d_rvalid !== 1'b0) begin $error("[rbuf] not empty after reset (full case)"); errors++; end
// Phase 2: randomized responses + backpressure (accumulate transfers)
repeat (20000) @(posedge clk);
// Phase 3 (directed): stall downstream so the buffer fills and STAYS full
// (u_rready must read 0 = !full), then hold full while d_rready=1 — the
// distinguishing full && d_rready case a fall-through skid would mishandle.
force_stall = 1; repeat (200) @(posedge clk);
force_stall = 0; force_ready = 1; repeat (200) @(posedge clk);
force_ready = 0;
// Phase 4: freeze producer, drain fully
prod_freeze = 1; force_ready = 1;
begin int g; g = 0; while ((wr_seq !== rd_seq) && g < 4000) begin @(posedge clk); g++; end end
repeat (10) @(posedge clk);
if (d_rvalid !== 1'b0) begin $error("[rbuf] not empty after drain (d_rvalid=%b)", d_rvalid); errors++; end
if (wr_seq !== rd_seq) begin $error("[rbuf] count mismatch: in %0d out %0d", wr_seq, rd_seq); errors++; end
if (wr_seq < 32'd2000) begin $error("[rbuf] too few transfers (%0d) — not meaningful", wr_seq); errors++; end
if (!saw_full_and_ready) begin $error("[rbuf] coverage: full && d_rready never observed — no-bypass case unexercised"); errors++; end
$display("[tb_gs_axi_r_regbuf] in=%0d out=%0d errors=%0d", wr_seq, rd_seq, errors);
if (errors == 0) $display("[tb_gs_axi_r_regbuf] PASS");
else $display("[tb_gs_axi_r_regbuf] FAIL");
$finish;
end
initial begin #2500000; $error("[tb_gs_axi_r_regbuf] TIMEOUT"); $finish; end
endmodule : tb_gs_axi_r_regbuf
@@ -0,0 +1,144 @@
// ============================================================================
// tb_gs_scanout_binomial_lookahead — Ch443e
//
// Reproduces the board's binomial vertical-lookahead underflow (per-frame diag:
// scan_y=33, next_fetch=34, cause_lookahead=1) under REALISTIC EMIF read latency
// and backpressure, and proves the Ch443e fix (4th rotating buffer + prefetch
// lead-2, modulo-4) eliminates it:
// - NO binomial lookahead underflow across the whole displayed frame;
// - correct previous/current/next 3x3 output across modulo-4 wrap (full oracle);
// - first/last-row clamping + 15:14 stretch-phase transitions correct;
// - coverage: mid-frame source rows past V_SOURCE_START+1 with vphase!=0 are
// actually displayed while the prefetch is under latency pressure.
// Production-like: V_SOURCE_START=32, V_STRETCH_15_TO_14, BINOMIAL, PSMCT32.
`timescale 1ns/1ps
module tb_gs_scanout_binomial_lookahead;
localparam int SRC_W=64, OUT_W=80, OUT_H=16;
localparam int STRIDE=320, ROW_BEATS=10, N_ROWS=48, VSTART=32;
logic axi_clk=0, video_clk=0, rst_n=0, enable=0;
always #2 axi_clk=~axi_clk; // fast EMIF
always #10 video_clk=~video_clk; // video
logic frame_start=0, in_window=0;
logic [11:0] pixel_x=0, pixel_y=0;
wire [7:0] r,g,b;
wire line_valid, underflow;
wire [31:0] rd_errs;
wire [29:0] araddr; wire [1:0] arburst; wire [6:0] arid;
wire [7:0] arlen; wire [2:0] arsize; wire arvalid, rready;
logic arready=0, rvalid=0, rlast=0;
logic [255:0] rdata=0; logic [1:0] rresp=0;
logic [255:0] mem [0:N_ROWS*ROW_BEATS-1];
initial begin
for (int beat=0; beat<N_ROWS*ROW_BEATS; beat++) mem[beat]='0;
for (int y=0; y<N_ROWS; y++)
for (int x=0; x<SRC_W; x++)
mem[y*ROW_BEATS + (x>>3)][(x&7)*32 +: 32] = {8'hff, 8'(8'h80+x+y), 8'(y), 8'(x)};
end
gs_lpddr_scanout_lb #(
.FB_BASE(30'd0), .STRIDE_BYTES(STRIDE), .ROW_BEATS(ROW_BEATS),
.N_ROWS(N_ROWS), .PSMCT32(1'b1), .H_STRETCH_5_TO_4(1'b1),
.V_SOURCE_START(VSTART), .V_STRETCH_15_TO_14(1'b1),
.V_LINEAR_FILTER(1'b0), .H_LINEAR_FILTER(1'b0),
.H_SOURCE_PIXELS(SRC_W), .BINOMIAL_3X3_FILTER(1'b1)
) dut (
.axi_clk(axi_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), .araddr(araddr), .arburst(arburst), .arid(arid),
.arlen(arlen), .arsize(arsize), .arvalid(arvalid), .arready(arready),
.rdata(rdata), .rresp(rresp), .rlast(rlast), .rvalid(rvalid), .rready(rready)
);
// ---- REALISTIC EMIF responder: AR accept, then R after LAT cycles, with
// periodic extra backpressure. Stresses the prefetch so a lead-1 design
// would starve the binomial lookahead (as the board did). ----
localparam int LAT=7;
typedef enum logic [1:0] {S_AR, S_WAIT, S_R} state_t;
state_t state=S_AR;
localparam int MEM_BEAT_BITS=$clog2(N_ROWS*ROW_BEATS);
logic [MEM_BEAT_BITS-1:0] beat_q;
int wait_c=0; logic [3:0] bp=0;
always_ff @(posedge axi_clk) begin
arready <= 1'b0;
bp <= bp + 1'b1;
if (!rst_n) begin state<=S_AR; rvalid<=1'b0; rlast<=1'b0; wait_c<=0; end
else case (state)
S_AR: if (arvalid && !arready) begin
beat_q<=araddr[MEM_BEAT_BITS+4:5]; arready<=1'b1; wait_c<=LAT + (bp[1:0]); state<=S_WAIT;
end
S_WAIT: begin
if (wait_c>0) wait_c<=wait_c-1;
else begin rdata<=mem[beat_q]; rresp<=2'b00; rlast<=1'b1; rvalid<=1'b1; state<=S_R; end
end
S_R: if (rready && rvalid) begin rvalid<=1'b0; rlast<=1'b0; state<=S_AR; end
endcase
end
// ---- golden binomial oracle (division only in TB) ----
function automatic int source_x(input int ox); source_x=(ox*4)/5; endfunction
function automatic int source_y(input int oy); source_y=VSTART+(oy*14)/15; endfunction
function automatic int clamp_x(input int x); clamp_x=(x<0)?0:((x>=SRC_W)?SRC_W-1:x); endfunction
function automatic int clamp_y(input int y); clamp_y=(y<VSTART)?VSTART:((y>=N_ROWS)?N_ROWS-1:y); endfunction
function automatic int sample(input int ch, input int x, input int y);
int xx,yy; begin xx=clamp_x(x); yy=clamp_y(y);
case (ch) 0:sample=xx; 1:sample=yy; default:sample=8'h80+xx+yy; endcase end
endfunction
function automatic int hbin(input int ch, input int x, input int y);
hbin=(sample(ch,x-1,y)+2*sample(ch,x,y)+sample(ch,x+1,y)+2)/4; endfunction
function automatic int binomial(input int ch, input int x, input int y);
binomial=(hbin(ch,x,y-1)+2*hbin(ch,x,y)+hbin(ch,x,y+1)+2)/4; endfunction
int errors=0, checked=0;
logic check_en=0, uflow_sticky=0, cov_midframe_vphase=0;
logic [11:0] x_d, y_d; logic in_d;
always_ff @(posedge video_clk) begin
x_d<=pixel_x; y_d<=pixel_y; in_d<=in_window;
if (rst_n && enable && underflow) uflow_sticky<=1'b1; // ANY underflow at ANY time fails
// coverage: a mid-frame source row (> VSTART+1) is on display with vphase!=0
if (rst_n && enable && in_window && dut.scan_y > ($clog2(N_ROWS)+1)'(VSTART+1)
&& dut.stretch_vphase_q != 4'd0)
cov_midframe_vphase<=1'b1;
if (check_en && in_d) begin
int sx,sy,er,eg,eb;
sx=source_x(x_d); sy=source_y(y_d);
er=binomial(0,sx,sy); eg=binomial(1,sx,sy); eb=binomial(2,sx,sy);
checked++;
if (r!==8'(er)||g!==8'(eg)||b!==8'(eb)) begin
if (errors<32) $display("[binlook] out=(%0d,%0d) src=(%0d,%0d) got=%02x/%02x/%02x exp=%02x/%02x/%02x",
x_d,y_d,sx,sy,r,g,b,8'(er),8'(eg),8'(eb));
errors++;
end
end
end
initial begin
repeat(8) @(posedge axi_clk); rst_n=1; enable=1;
frame_start=1; repeat(3) @(posedge video_clk); frame_start=0;
repeat(400) @(posedge axi_clk); // let the lead-2 prefetch prime rows 32,33,34,35
repeat(3) @(posedge video_clk);
check_en=1;
for (int y=0; y<OUT_H; y++) begin
for (int x=0; x<OUT_W; x++) begin
@(negedge video_clk); pixel_x=12'(x); pixel_y=12'(y); in_window=1;
@(posedge video_clk);
end
@(negedge video_clk); in_window=0;
repeat(25) @(posedge video_clk); // horizontal blank between output lines
end
check_en=0;
$display("[binlook] checked=%0d errors=%0d underflow(sticky)=%0b rd_errs=%0d cov_midframe_vphase=%0b",
checked,errors,uflow_sticky,rd_errs,cov_midframe_vphase);
if (checked==OUT_W*OUT_H && errors==0 && !uflow_sticky && rd_errs==0 && cov_midframe_vphase)
$display("[tb_gs_scanout_binomial_lookahead] PASS");
else
$display("[tb_gs_scanout_binomial_lookahead] FAIL");
$finish;
end
initial begin #4_000_000; $display("[tb_gs_scanout_binomial_lookahead] TIMEOUT"); $finish; end
endmodule
+259
View File
@@ -0,0 +1,259 @@
// ============================================================================
// tb_gs_scanout_cdc_qual — Ch443f
//
// Focused async-clock test for the COHERENT + QUALIFIED underflow detector in
// gs_lpddr_scanout_lb. It targets exactly the four properties Codex required of
// the hardened diagnostic, using deliberately non-commensurate axi/video clocks
// so the readiness (next_fetch) row-commit lands at every phase relative to the
// video sampling edge:
//
// §A No false event from the binary transition + no false event from ordinary
// synchronizer latency. A healthy phase-swept display commits many rows at
// walking phases; the coherent nf_v handshake must never latch a value the
// committed frontier never held (nf_v <= next_fetch, monotone), and NO
// qualified underflow may assert (underflow==0, diag_valid==0, uf_qual==0)
// even though the 2-FF sync lags next_fetch.
//
// §B1 A sub-QUAL miss (persistence 3 > the ~2-cycle sync latency, < QUAL=4) is
// NOT a qualified event: underflow stays 0, diag_valid stays 0, but the
// live uf_pmax records 3 so a host can SEE the transient. This is the
// direct proof that ordinary synchronizer latency cannot fabricate an event.
//
// §B2 A genuinely late row (persistence >> QUAL) DOES produce a qualified event:
// underflow==1, diag_valid==1, uf_qual==1, and the atomic snapshot is
// self-consistent (diag_scan_y >= diag_nf_v, both frozen the same cycle,
// diag_pmax >= QUAL).
//
// §C Frame reset + modulo-4 buffer reuse remain correct: frame_start clears the
// detector (nf_v->VSTART, persistence/qual/valid=0) and a full healthy sweep
// spanning > 4 source rows (all 4 rotating buffers reused) reproduces the
// binomial 3x3 output exactly with no underflow.
//
// Vehicle: BINOMIAL_3X3_FILTER=1 (NBUF=4, lookahead active), stretch OFF so
// scan_y == pixel_y and the fetch frontier can be positioned exactly. The
// detector logic under test (coherent nf_v, persistence, qualification, atomic
// capture) is stretch-agnostic; this isolates the CDC without stretch bookkeeping.
`timescale 1ns/1ps
module tb_gs_scanout_cdc_qual;
localparam int SRC_W=32, N_ROWS=48, ROW_BEATS=4, STRIDE=SRC_W*4, VSTART=0;
localparam int QUAL=4; // must match RTL QUAL_CYCLES
// Deliberately non-commensurate clocks: 6.0 ns vs 13.0 ns (ratio 13/6) so the
// axi-domain next_fetch Gray commit crosses the video sampling edge at walking phases.
logic axi_clk=0, video_clk=0, rst_n=0, enable=0;
always #3 axi_clk = ~axi_clk; // 6.0 ns
always #6.5 video_clk = ~video_clk; // 13.0 ns
logic frame_start=0, in_window=0;
logic [11:0] pixel_x=0, pixel_y=0;
wire [7:0] r,g,b;
wire line_valid, underflow;
wire [31:0] rd_errs;
wire [29:0] araddr; wire [1:0] arburst; wire [6:0] arid;
wire [7:0] arlen; wire [2:0] arsize; wire arvalid, rready;
logic arready=0, rvalid=0, rlast=0;
logic [255:0] rdata=0; logic [1:0] rresp=0;
logic [255:0] mem [0:N_ROWS*ROW_BEATS-1];
initial begin
for (int beat=0; beat<N_ROWS*ROW_BEATS; beat++) mem[beat]='0;
for (int y=0; y<N_ROWS; y++)
for (int x=0; x<SRC_W; x++)
mem[y*ROW_BEATS + (x>>3)][(x&7)*32 +: 32] = {8'hff, 8'(8'h80+x+y), 8'(y), 8'(x)};
end
gs_lpddr_scanout_lb #(
.FB_BASE(30'd0), .STRIDE_BYTES(STRIDE), .ROW_BEATS(ROW_BEATS),
.N_ROWS(N_ROWS), .PSMCT32(1'b1), .H_STRETCH_5_TO_4(1'b0),
.V_SOURCE_START(VSTART), .V_STRETCH_15_TO_14(1'b0),
.V_LINEAR_FILTER(1'b0), .H_LINEAR_FILTER(1'b0),
.H_SOURCE_PIXELS(SRC_W), .BINOMIAL_3X3_FILTER(1'b1)
) dut (
.axi_clk(axi_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), .araddr(araddr), .arburst(arburst), .arid(arid),
.arlen(arlen), .arsize(arsize), .arvalid(arvalid), .arready(arready),
.rdata(rdata), .rresp(rresp), .rlast(rlast), .rvalid(rvalid), .rready(rready)
);
// ---- EMIF responder with TB-controlled starvation. starve=1 -> no AR accept,
// no new R (fetch freezes -> next_fetch stops advancing). ----
logic starve=0;
localparam int LAT=3;
typedef enum logic [1:0] {S_AR, S_WAIT, S_R} state_t;
state_t state=S_AR;
localparam int MEM_BEAT_BITS=$clog2(N_ROWS*ROW_BEATS);
logic [MEM_BEAT_BITS-1:0] beat_q; int wait_c=0;
always_ff @(posedge axi_clk) begin
arready <= 1'b0;
if (!rst_n) begin state<=S_AR; rvalid<=1'b0; rlast<=1'b0; wait_c<=0; end
else case (state)
S_AR: if (arvalid && !arready && !starve) begin
beat_q<=araddr[MEM_BEAT_BITS+4:5]; arready<=1'b1; wait_c<=LAT; state<=S_WAIT;
end
S_WAIT: if (wait_c>0) wait_c<=wait_c-1;
else begin rdata<=mem[beat_q]; rresp<=2'b00; rlast<=1'b1; rvalid<=1'b1; state<=S_R; end
S_R: if (rready && rvalid) begin rvalid<=1'b0; rlast<=1'b0; state<=S_AR; end
endcase
end
// (pixel-exact binomial 3x3 reproduction across mod-4 wrap is owned by
// tb_gs_scanout_binomial_lookahead; this TB isolates the readiness CDC +
// qualification, so no per-pixel oracle is needed here.)
// ---- global invariants (video domain) ----
int errors=0;
logic expect_uf=0; // 0 while a qualified underflow is NOT permitted
logic coh_en=0; // enable the nf_v coherence invariant (steady-state windows only)
int commit_events=0; // # of next_fetch changes observed (phase-swept commits)
logic [$clog2(N_ROWS):0] nf_prev=0; logic nf_seen=0;
logic [$clog2(N_ROWS):0] fetch_prev=0;
task automatic chk(input string label, input logic cond);
if (!cond) begin $error("[cdc_qual] FAIL: %s", label); errors++; end
else $display("[cdc_qual] ok : %s", label);
endtask
// suppress the coherence invariant for a few cycles after a frame reset: the
// Gray sync legitimately holds the (large, SAFE) pre-reset frontier for ~2 video
// cycles after next_fetch drops to VSTART — that is the benign reset transient
// fs_edge_v overrides, NOT a tear. Steady-state (guard expired) nf_v <= next_fetch
// must hold; a real torn decode would surface there.
int fs_guard=0;
always @(posedge video_clk) if (rst_n && enable) begin
if (dut.fs_edge_v) fs_guard <= 4; else if (fs_guard>0) fs_guard <= fs_guard-1;
// coherence invariant (steady state): nf_v may never exceed the committed
// frontier nor run below VSTART. A torn Gray decode would surface here.
if (coh_en && !dut.fs_edge_v && fs_guard==0) begin
if (dut.nf_v > dut.next_fetch) begin
$error("[cdc_qual] FAIL: nf_v %0d > committed next_fetch %0d (incoherent latch)",
dut.nf_v, dut.next_fetch); errors++;
end
if (dut.nf_v < ($clog2(N_ROWS)+1)'(VSTART)) begin
$error("[cdc_qual] FAIL: nf_v %0d < VSTART", dut.nf_v); errors++;
end
end
// qualified-underflow gate
if (underflow && !expect_uf) begin
$error("[cdc_qual] FAIL: qualified underflow asserted when not permitted (scan_y=%0d nf_v=%0d)",
dut.scan_y, dut.nf_v); errors++;
end
// count committed-frontier changes (phase coverage)
if (nf_seen && dut.next_fetch !== fetch_prev) commit_events++;
fetch_prev <= dut.next_fetch; nf_prev <= dut.nf_v; nf_seen <= 1'b1;
end
// ---- step display one source row, slowly enough that the modeled EMIF keeps
// its lead-2 prefetch (fetch ~1 row / 6 axi cycles; display 1 row / 8 video
// cycles -> fetch stays ahead). A row advance triggers exactly one commit. ----
task automatic step_row(input int y);
pixel_y = 12'(y); pixel_x = 12'd0; in_window = 1'b1;
repeat (20) @(posedge video_clk);
endtask
initial begin
// ---- reset / enable / prime ----
repeat(8) @(posedge axi_clk); rst_n=1; enable=1;
@(negedge video_clk); frame_start=1; repeat(3) @(posedge video_clk);
@(negedge video_clk); frame_start=0;
repeat(120) @(posedge axi_clk); // prime lead-2 buffers before any display
// ================= §A healthy phase-swept display =================
expect_uf = 0; coh_en = 1; // steady-state coherence invariant active here
for (int y=0; y<24; y++) step_row(y);
@(negedge video_clk); in_window=0; repeat(10) @(posedge video_clk);
coh_en = 0;
chk("A: no qualified underflow across healthy phase sweep", !underflow);
chk("A: diag_valid stays 0 (no real miss)", dut.diag_valid_q===1'b0);
chk("A: uf_qual stays 0", dut.uf_qual_q===1'b0);
chk("A: many frontier commits observed at swept phases (>=16)", commit_events>=16);
$display("[cdc_qual] A: commit_events=%0d live_pmax=%0d", commit_events, dut.uf_pmax_q);
// ================= §B1 sub-QUAL transient must NOT qualify =================
// fresh frame; advance healthily to row R, freeze fetch, jump display just
// past the frozen frontier, hold for (QUAL-1) video cycles.
in_window=0; pixel_y=0; @(negedge video_clk);
frame_start=1; repeat(3) @(posedge video_clk);
@(negedge video_clk); frame_start=0; repeat(120) @(posedge axi_clk);
expect_uf = 0;
for (int y=0; y<=18; y++) step_row(y);
repeat(20) @(posedge axi_clk);
begin
int F3;
starve = 1'b1; repeat(8) @(posedge video_clk); // freeze; drain in-flight + settle nf_v
F3 = dut.next_fetch; // now-stable committed frontier
pixel_y = 12'(F3); pixel_x = 12'd0; in_window = 1'b1; // scan_y == nf_v -> base miss
repeat(QUAL-1) @(posedge video_clk); // persistence reaches 3 (< QUAL)
chk("B1: sub-QUAL miss does NOT qualify (underflow==0)", !underflow);
chk("B1: sub-QUAL miss leaves diag_valid==0", dut.diag_valid_q===1'b0);
chk("B1: sub-QUAL miss leaves uf_qual==0", dut.uf_qual_q===1'b0);
// a real transient was recorded (>0) but stayed below the qualification floor
chk("B1: live pmax recorded a sub-QUAL transient (0<pmax<QUAL)",
dut.uf_pmax_q>4'd0 && dut.uf_pmax_q<4'(QUAL));
$display("[cdc_qual] B1: uf_pmax=%0d underflow=%0b (F3 frontier=%0d)",
dut.uf_pmax_q, underflow, F3);
starve = 1'b0; // relieve before it can qualify
@(negedge video_clk); in_window=0; repeat(20) @(posedge video_clk);
end
// ================= §B2 genuine late row MUST qualify =================
in_window=0; pixel_y=0; @(negedge video_clk);
frame_start=1; repeat(3) @(posedge video_clk);
@(negedge video_clk); frame_start=0; repeat(120) @(posedge axi_clk);
expect_uf = 1; // a qualified event is now the INTENDED outcome
for (int y=0; y<=18; y++) step_row(y);
repeat(20) @(posedge axi_clk);
begin
int F2;
starve = 1'b1; repeat(8) @(posedge video_clk);
F2 = dut.next_fetch;
pixel_y = 12'(F2); pixel_x = 12'd0; in_window = 1'b1;
repeat(10) @(posedge video_clk); // persistence >> QUAL
chk("B2: genuine late row qualifies (underflow==1)", underflow===1'b1);
chk("B2: genuine late row sets uf_qual==1", dut.uf_qual_q===1'b1);
chk("B2: diag captured (diag_valid==1)", dut.diag_valid_q===1'b1);
chk("B2: atomic snapshot self-consistent (scan_y >= nf_v)",
dut.diag_scan_y_q >= dut.diag_nf_v_q);
chk("B2: snapshot scan_y == frozen display row", dut.diag_scan_y_q===($clog2(N_ROWS)+1)'(F2));
chk("B2: snapshot nf_v == frozen frontier", dut.diag_nf_v_q===($clog2(N_ROWS)+1)'(F2));
chk("B2: captured pmax >= QUAL", dut.diag_pmax_q>=4'(QUAL));
chk("B2: live pmax kept growing past capture", dut.uf_pmax_q>dut.diag_pmax_q);
end
// ================= §C frame reset clears + mod-4 reuse correct =================
starve = 1'b0;
@(negedge video_clk); in_window=0; pixel_y=0; // disp_row->0 so fetch reloads rows 0..3
@(negedge video_clk); frame_start=1; repeat(3) @(posedge video_clk);
@(negedge video_clk); frame_start=0;
chk("C: frame reset cleared diag_valid", dut.diag_valid_q===1'b0);
chk("C: frame reset cleared uf_qual", dut.uf_qual_q===1'b0);
chk("C: frame reset cleared underflow", underflow===1'b0);
chk("C: frame reset realigned nf_v to VSTART", dut.nf_v===($clog2(N_ROWS)+1)'(VSTART));
repeat(160) @(posedge axi_clk); // prime lead-2 buffers 0..3 before display (binomial needs r+1)
// Healthy display spanning 12 source rows (mod-4 rotation reused 3x) at the
// proven step_row pace. This proves the DETECTOR re-arms correctly across a
// frame reset and does NOT false-trip while the 4 buffers are recycled: no
// qualified underflow, diag_valid stays 0, and the coherence invariant holds.
// (Pixel-exact binomial 3x3 reproduction across the mod-4 wrap is owned by
// tb_gs_scanout_binomial_lookahead; not re-litigated here.)
expect_uf = 0; coh_en = 1;
for (int y=0; y<12; y++) begin
step_row(y);
if (underflow) begin $error("[cdc_qual] C: false underflow at row %0d (nf_v=%0d next_fetch=%0d)",
y, dut.nf_v, dut.next_fetch); errors++; end
end
@(negedge video_clk); in_window=0; repeat(10) @(posedge video_clk);
chk("C: no qualified underflow across reset + mod-4 buffer reuse", !underflow);
chk("C: uf_qual stayed 0 across reset+reuse", dut.uf_qual_q===1'b0);
chk("C: diag_valid stayed 0 across reset+reuse", dut.diag_valid_q===1'b0);
chk("C: live pmax stayed sub-QUAL across reset+reuse", dut.uf_pmax_q<4'(QUAL));
if (errors==0) $display("[tb_gs_scanout_cdc_qual] PASS");
else $display("[tb_gs_scanout_cdc_qual] FAIL (errors=%0d)", errors);
$finish;
end
initial begin #6_000_000; $display("[tb_gs_scanout_cdc_qual] TIMEOUT"); $finish; end
endmodule
+132 -47
View File
@@ -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);
chk("P1 snap pmax qualified (>=QUAL)", fst[29:20] >= 10'd4);
// -------- 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 pmax qualified (>=QUAL)", b_diag_first[29:20] >= 10'd4);
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 pmax field", b_diag_first[29:20] === 10'(dut_b.diag_pmax_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
+146
View File
@@ -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
+20 -3
View File
@@ -37,8 +37,17 @@ module tb_gs_texture_cache;
logic [31:0] tex_rd_addr, tex_rd_data;
logic tex_ready;
// golden source: 64 words. tex_word(i) = 0xC0DE_0000 | i (distinct per lane).
function automatic [31:0] tex_word(input int i); tex_word = 32'hC0DE_0000 | i[31:0]; endfunction
// golden source. Ch443: every byte lane carries DISTINCT data — different per index
// AND different across the four banks (distinct XOR keys + distinct bases) — so the
// four-bank tex_mem split is genuinely exercised: a bank swap, misroute, or dropped
// bank produces a wrong reconstructed byte that the full-word + per-bank checks catch.
// (The old 0xC0DE_0000|i had constant bytes 2/3, which a bank fault could hide.)
function automatic [31:0] tex_word(input int i);
tex_word = { 8'(((i*4 + 3) & 32'hFF) ^ 32'hA3),
8'(((i*4 + 2) & 32'hFF) ^ 32'h5C),
8'(((i*4 + 1) & 32'hFF) ^ 32'h91),
8'(((i*4 + 0) & 32'hFF) ^ 32'h2E) };
endfunction
// force a bad rresp on a chosen beat to exercise rd_errs (set <0 to disable)
int err_beat = -1;
@@ -122,9 +131,17 @@ module tb_gs_texture_cache;
check(tex_ready, "tex_ready never synced");
// ---- verify every word via the sampler 1-cycle read port ----
// Full reconstructed word AND per-byte-lane pinpoint. The distinct-per-byte golden
// exercises all four byte lanes of the monolithic 32-bit word (Ch443).
for (int i=0; i<TEX_WORDS; i++) begin
logic [31:0] exp;
read_word(i, got);
check(got == tex_word(i), $sformatf("word[%0d]=%08x exp %08x", i, got, tex_word(i)));
exp = tex_word(i); // temp: iverilog forbids part-select on a function-call result
check(got == exp, $sformatf("word[%0d]=%08x exp %08x", i, got, exp));
check(got[7:0] === exp[7:0], $sformatf("lane0 byte[%0d]=%02x exp %02x", i, got[7:0], exp[7:0]));
check(got[15:8] === exp[15:8], $sformatf("lane1 byte[%0d]=%02x exp %02x", i, got[15:8], exp[15:8]));
check(got[23:16] === exp[23:16], $sformatf("lane2 byte[%0d]=%02x exp %02x", i, got[23:16], exp[23:16]));
check(got[31:24] === exp[31:24], $sformatf("lane3 byte[%0d]=%02x exp %02x", i, got[31:24], exp[31:24]));
end
$display("[texcache] clean: fill_done=%0d beats=%0d bytes=%0d rd_errs=%0d words_checked=%0d errors=%0d",
@@ -176,6 +176,11 @@ module tb_bridge_iop_pad_input;
logic scan_diag_uf_i = 1'b0, scan_diag_rderr_nz_i = 1'b0, scan_diag_valid_i = 1'b0;
logic [29:0] scan_diag_first_i = 30'd0;
logic [6:0] scan_diag_stat_i = 7'd0;
logic [3:0] scan_diag_pmax_i = 4'd0;
// Pre-existing clut_stage/commit bridge outputs (fog-baseline ba74bbd); nets so .* binds.
wire [7:0] clut_stage_waddr_o; wire [31:0] clut_stage_wdata_o; wire clut_stage_we_o;
wire clut_commit_tgl_o; wire [31:0] clut_expected_crc_o;
logic clut_busy_i = 1'b0, clut_done_tgl_i = 1'b0; logic [31:0] clut_crc_i = 32'd0;
ps2_hps_bridge u_bridge (
.clk (bclk),
.reset_n (breset_n),
@@ -256,6 +256,11 @@ module tb_ee_pad_buffer_branch;
logic scan_diag_uf_i = 1'b0, scan_diag_rderr_nz_i = 1'b0, scan_diag_valid_i = 1'b0;
logic [29:0] scan_diag_first_i = 30'd0;
logic [6:0] scan_diag_stat_i = 7'd0;
logic [3:0] scan_diag_pmax_i = 4'd0;
// Pre-existing clut_stage/commit bridge outputs (fog-baseline ba74bbd); nets so .* binds.
wire [7:0] clut_stage_waddr_o; wire [31:0] clut_stage_wdata_o; wire clut_stage_we_o;
wire clut_commit_tgl_o; wire [31:0] clut_expected_crc_o;
logic clut_busy_i = 1'b0, clut_done_tgl_i = 1'b0; logic [31:0] clut_crc_i = 32'd0;
ps2_hps_bridge u_bridge (
.clk (bclk),
.reset_n (breset_n),
@@ -207,6 +207,11 @@ module tb_pad_state_via_sif_to_ee;
logic scan_diag_uf_i = 1'b0, scan_diag_rderr_nz_i = 1'b0, scan_diag_valid_i = 1'b0;
logic [29:0] scan_diag_first_i = 30'd0;
logic [6:0] scan_diag_stat_i = 7'd0;
logic [3:0] scan_diag_pmax_i = 4'd0;
// Pre-existing clut_stage/commit bridge outputs (fog-baseline ba74bbd); nets so .* binds.
wire [7:0] clut_stage_waddr_o; wire [31:0] clut_stage_wdata_o; wire clut_stage_we_o;
wire clut_commit_tgl_o; wire [31:0] clut_expected_crc_o;
logic clut_busy_i = 1'b0, clut_done_tgl_i = 1'b0; logic [31:0] clut_crc_i = 32'd0;
ps2_hps_bridge u_bridge (
.clk (bclk),
.reset_n (breset_n),
+1
View File
@@ -219,6 +219,7 @@ module tb_ps2_hps_bridge;
logic scan_diag_uf_i = 1'b0, scan_diag_rderr_nz_i = 1'b0, scan_diag_valid_i = 1'b0;
logic [29:0] scan_diag_first_i = 30'd0;
logic [6:0] scan_diag_stat_i = 7'd0;
logic [3:0] scan_diag_pmax_i = 4'd0;
logic clear_done_i = 1'b0; // Ch357 — persistent-Z preclear ack (driven by the Ch357 status test)
logic [31:0] frag_drops_i = 32'd0; // Ch357 — persistent-Z drop count (driven by the Ch357 status test)
// Ch367 -- runtime CLUT staging ports (the data-bank CDC itself is
@@ -995,8 +995,14 @@ set_global_assignment -name OPTIMIZATION_MODE "AGGRESSIVE AREA"
# block cluster -> shorter routing. Surgical (no global area/placement disruption); ~a few ALMs.
set_instance_assignment -name MAX_FANOUT 3 -to "u_zc_emit|u_req|raddr_hi1_q*"
set_global_assignment -name AUTO_RESOURCE_SHARING ON
# Ch357 (Codex) — SEED left at default (1): seed 1 gave -0.019 (quasi-static EMIF-lock ONLY, all real datapaths met);
# seed 2 was worse (-0.155, exposed a different marginal path). The last sliver is closed via the scoped false-path in the SDC.
# Ch357 (Codex) — historical: at that netlist seed 1 gave -0.019 (quasi-static EMIF-lock only), seed 2 was worse.
# Ch443d (Codex) — after the AW/W/R buffers + F_SETTLE drain multicycle froze the functional netlist at 6319d7c,
# the residual failure was a placement/route-marginal EMIF path (zc_emit QUAD_WIDTH4 Z-req FIFO read, ~-0.087,
# 3.132ns data / -0.100 skew vs 3.225ns period). A bounded 4-seed sweep (fit+STA, AGGRESSIVE AREA kept) resolved it:
# seed2 -0.087 | seed3 +0.132 (0 violated, ALL classes >=0) | seed4 +0.062 | seed5 -0.130.
# SEED 3 is the reproducibility setting: EMIF setup +0.132 (>= +0.100 margin), no AWREADY/texcache-fill/drain/
# scanout-diag violations, RAM 317/358, ALM 85%. This is a fitter placement seed, NOT an RTL/timing-mode change.
set_global_assignment -name SEED 3
set_global_assignment -name STRATIXV_CONFIGURATION_SCHEME "ACTIVE SERIAL X4"
set_global_assignment -name ACTIVE_SERIAL_CLOCK AS_FREQ_125MHZ
set_global_assignment -name USE_CONF_DONE SDM_IO16
@@ -1061,6 +1067,8 @@ set_global_assignment -name SYSTEMVERILOG_FILE rtl/gif_gs/gs_tile_reload.sv
set_global_assignment -name SYSTEMVERILOG_FILE rtl/gif_gs/gs_lpddr_map_pkg.sv
set_global_assignment -name SYSTEMVERILOG_FILE rtl/gif_gs/gs_lpddr_z_rmw.sv
set_global_assignment -name SYSTEMVERILOG_FILE rtl/gif_gs/gs_axi_w_regbuf.sv
set_global_assignment -name SYSTEMVERILOG_FILE rtl/gif_gs/gs_axi_aw_regbuf.sv
set_global_assignment -name SYSTEMVERILOG_FILE rtl/gif_gs/gs_axi_r_regbuf.sv
set_global_assignment -name SYSTEMVERILOG_FILE rtl/gif_gs/gs_lpddr_zc_emit.sv
# ----------------------------------------------------------------------------
# Design RTL added during the EE/qbert chapters (Ch287+) that reached the demo
@@ -336,9 +336,16 @@ if { $tile_src_n == 0 && $tile_dst_n == 0 } {
if { $tile_src_n != 42 } { error "Ch357 SDC FATAL: tile_ram_cdc source tile_wr_index+tile_wr_data count $tile_src_n != 42 (10+32; renamed/optimized? hold cut + net_delay would orphan)" }
if { $tile_dst_n == 0 } { error "Ch357 SDC FATAL: tile_ram_cdc dest u_tile_cdc|shadow_mem EMPTY (renamed? hold cut would orphan)" }
set_false_path -hold -from $tile_cdc_src -to $tile_cdc_dst
set_max_skew -from $tile_cdc_src -to $tile_cdc_dst 2.0
# Ch443 (Codex): max_skew bound 2.0 -> 2.5. The Ch442 fit placed this quasi-static OSD
# tile bundle at 2.050 ns actual skew, failing the round-number 2.0 by 50 ps. The CDC
# contract guarantees >= 2 design-clock periods of bus stability before the synchronized
# toggle samples (same recipe as the tile hold-false + net_delay below), so 2.5 ns stays
# FAR inside the real functional window and avoids engineering to a 50 ps margin. The
# set_net_delay ARRIVAL bound is UNCHANGED at 2.0 ns (it bounds the actual requirement:
# each bit must arrive within the stability window; skew only bounds bit-to-bit spread).
set_max_skew -from $tile_cdc_src -to $tile_cdc_dst 2.5
set_net_delay -max -from $tile_cdc_src -to $tile_cdc_dst 2.0
post_message -type info "Ch357 SDC: tile_ram_cdc hold-false-path + 2ns max_skew + 2ns net_delay ($tile_src_n src -> $tile_dst_n shadow_mem dst)"
post_message -type info "Ch357/443 SDC: tile_ram_cdc hold-false-path + 2.5ns max_skew + 2ns net_delay ($tile_src_n src -> $tile_dst_n shadow_mem dst)"
}
# Ch357 (Codex) — osd_cfg QUASI-STATIC config CDC. osd_cfg{0,1}_q (bridge, refclk/fabric domain) cross into
@@ -353,6 +360,73 @@ 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 31-src STABLE first-failure bundle {scan_y[10], nf_v[10], pmax[4],
# vphase[4], base, lookahead, line_valid} (Ch443f: nf_s0[10] -> pmax[4];
# diag_first[29:20] now carries the qualified-miss persistence max, upper 6
# bits const 0 so the bridge dst reg stays 10-bit / 37-keeper). 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==31 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
}
}
# Ch443f: live per-frame pmax magnitude (4-bit bus) is a plain advisory 2-FF sync
# (video_clk uf_pmax_q -> bridge scan_diag_pmax_s0[*]); cut the async INTO stage 0
# only, leaving s0->s1 timed. Bus bits may be momentarily incoherent — harmless,
# it feeds 0x120[15:12] read across many frames, not the coherent capture bundle.
set pmax_s0 [get_keepers -nowarn {*u_hps_bridge|scan_diag_pmax_s0[*]}]
if { [get_collection_size $pmax_s0] == 0 } {
post_message -type warning "Ch443f SDC WARN: scan_diag_pmax_s0 matched 0 keepers (renamed / no LPDDR scanout) — stage-0 cut NOT applied"
} else {
set_false_path -to $pmax_s0
}
# Ch443f: coherent readiness GRAY code (axi next_fetch -> next_fetch_gray comb ->
# video nf_gray_s0[*]). Async into stage 0 -> cut setup+hold into nf_gray_s0; the
# s0->s1 MTBF leg stays timed. Gray changes ONE bit per increment, but bound the
# source->stage0 bit spread (max_skew + net_delay 2ns) so the lone multi-bit event
# (frame-restart reset to V_SOURCE_START, which fs_edge_v overrides on the video
# side anyway) cannot spread across more than one video sample. FATAL count guard.
set nfg_dst [get_keepers -nowarn {*u_lpddr_scan_lb|nf_gray_s0[*]}]
set nfg_src [get_keepers -nowarn {*u_lpddr_scan_lb|next_fetch[*]}]
if { [get_collection_size $nfg_dst] == 0 } {
post_message -type info "Ch443f SDC: next_fetch Gray readiness CDC inactive in this profile (0 stage-0 dst)"
} else {
if { [get_collection_size $nfg_src] == 0 } { error "Ch443f SDC FATAL: nf_gray_s0 present but u_lpddr_scan_lb|next_fetch[*] matched 0 keepers (renamed? Gray CDC source orphaned)" }
set_false_path -to $nfg_dst
set_max_skew -from $nfg_src -to $nfg_dst 2.0
set_net_delay -max -from $nfg_src -to $nfg_dst 2.0
post_message -type info "Ch443f SDC: next_fetch Gray readiness CDC async-in cut + 2ns max_skew/net_delay ([get_collection_size $nfg_src] src -> [get_collection_size $nfg_dst] stage-0 dst)"
}
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_pmax_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 != 31 } { error "Ch442/443f SDC FATAL: scanout diag bundle source count $scan_diag_src_n != 31 (10+10+4+4+1+1+1 = scan_y+nf_v+pmax+vphase+base+look+lv; 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
@@ -446,6 +520,28 @@ if { $tdn == 0 } { error "Ch439g SDC FATAL: tex_mem destination collection empty
set_false_path -from $ch439_bili_taps -to $tex_mem_dst
post_message -type info "Ch439g SDC: impossible bilinear-tap -> tex_mem packed-DSP arc cut ($ch439_bili_taps_n src -> $tdn dst); all non-tap texture-address launches remain timed"
# Ch443 (Codex) — texture-cache EMIF DRAIN write-address multicycle. The fill FSM now holds
# drain_idx_q/drain_word_q stable across F_DRAIN -> F_SETTLE -> F_WRITE and asserts the RAM
# write-enable ONLY at the later F_WRITE edge, so the drain address is functionally required
# 2 EMIF cycles after launch. Give ONLY drain_idx_q -> tex_mem a 2-cycle setup / 1-cycle hold
# multicycle (a 6.45 ns window for the -0.370 ns drain write-address fanout that closing the
# four-bank split had opened). SCOPED -from the drain registers, so it does NOT touch the
# sampler/read-address path (ras_v0_x / walker / UV / perspective -> tex_mem portbaddr) — that
# launches from DIFFERENT registers and stays fully timed (Ch439g above). Reuses the tex_mem
# destination collection $tex_mem_dst / $tdn defined for the Ch357/Ch439g exceptions.
# Fail-closed: if tex_mem exists but drain_idx_q[*] is gone (renamed), HALT — never silently
# omit the exception (which would let the drain path refail setup unconstrained).
set tex_drain_src [get_keepers -nowarn {*u_texcache|drain_idx_q[*]}]
set tex_drain_src_n [get_collection_size $tex_drain_src]
if { $tdn == 0 } {
post_message -type info "Ch443 SDC: texture drain multicycle inactive (no tex_mem in this profile)"
} else {
if { $tex_drain_src_n == 0 } { error "Ch443 SDC FATAL: tex_mem present but u_texcache|drain_idx_q[*] matched 0 keepers (renamed? drain multicycle orphaned -> write-address would silently refail setup)" }
set_multicycle_path -setup -end 2 -from $tex_drain_src -to $tex_mem_dst
set_multicycle_path -hold -end 1 -from $tex_drain_src -to $tex_mem_dst
post_message -type info "Ch443 SDC: texture drain_idx_q -> tex_mem 2-cycle setup / 1-cycle hold multicycle ($tex_drain_src_n src -> $tdn dst)"
}
# Ch358 (Codex) — the SAME quasi-static EMIF calibration-ready signal also reaches the scanout LINE-BUFFER RAMs
# (26.1 fit at 640x480: lock_sync_inst|dreg[1] -> u_lpddr_scan_lb|lb0/lb1, 10 endpoints at -0.010ns). These are
# startup/reset-derived line-buffer control paths, NOT runtime scanout data (the line buffers are inactive until