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>
This commit is contained in:
2026-07-22 11:19:09 -04:00
parent 194f45bd05
commit 27dfd0b0cf
8 changed files with 316 additions and 15 deletions
+36 -7
View File
@@ -85,7 +85,17 @@ 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.
(* ramstyle = "M20K" *) logic [31:0] tex_mem [0:TEX_WORDS-1];
// Ch443 (Codex): four 65536x8 byte-width banks with INDEPENDENT preserved write-
// address launch registers, replacing the single 65536x32 logical RAM. The single
// drain_idx_q fanning across the whole 128-M20K macro was the -0.370 ns setup family
// (drain_idx_q[4] -> tex_mem ram_block*~reg0). Splitting the WIDTH gives each address
// tree ~32 M20Ks; total M20K count is unchanged (4x32 == 128). Selector structure,
// one-shot fill sequence, and the 1-cycle registered read latency are all preserved;
// the read reconstructs the 32-bit word by concatenation.
(* ramstyle = "M20K" *) logic [7:0] tex_mem_b0 [0:TEX_WORDS-1];
(* ramstyle = "M20K" *) logic [7:0] tex_mem_b1 [0:TEX_WORDS-1];
(* ramstyle = "M20K" *) logic [7:0] tex_mem_b2 [0:TEX_WORDS-1];
(* ramstyle = "M20K" *) logic [7:0] tex_mem_b3 [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;
@@ -103,7 +113,10 @@ module gs_texture_cache #(
// so reset values are unobservable; keeping them out of the 4k-fanout EMIF calibration reset removes that reset
// from the duplicated RAM-address launch registers at 310 MHz.
logic [31:0] drain_word_q;
logic [WIDX_BITS-1:0] drain_idx_q;
// Ch443: four independent, preserved, non-merged write-address launch registers —
// one per byte bank. dont_merge is REQUIRED: they hold identical values, so without
// it Quartus would re-merge them into a single high-fanout register, undoing the fix.
(* preserve, dont_merge *) logic [WIDX_BITS-1:0] drain_idx_q0, drain_idx_q1, drain_idx_q2, drain_idx_q3;
// fill_start is an EDGE/TOGGLE (bridge toggles it on each arm), CDC-synced here so the
// cache is RE-FILLABLE: each arm reloads the texture (lets the HPS re-stage a different
// texture without a board reset). 3-FF sync + edge-detect, like the read/write probes.
@@ -154,9 +167,13 @@ module gs_texture_cache #(
fst <= F_WRITE;
end
F_WRITE: begin
// COMMIT half: registered word -> M20K; CRC over the word actually committed.
tex_mem[drain_idx_q] <= drain_word_q;
fill_crc <= fill_crc + drain_word_q; // sum32 over the words written
// COMMIT half: registered word -> four byte banks (each with its own
// preserved address reg); CRC over the FULL word actually committed.
tex_mem_b0[drain_idx_q0] <= drain_word_q[7:0];
tex_mem_b1[drain_idx_q1] <= drain_word_q[15:8];
tex_mem_b2[drain_idx_q2] <= drain_word_q[23:16];
tex_mem_b3[drain_idx_q3] <= drain_word_q[31:24];
fill_crc <= fill_crc + drain_word_q; // sum32 over the words written (unchanged)
if (fill_lane == 3'd7) begin
fill_beats <= fill_beats + 32'd1;
fill_bytes <= fill_bytes + 32'd32;
@@ -198,7 +215,10 @@ 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_q0 <= fill_word_idx; // four identical loads; kept separate by dont_merge
drain_idx_q1 <= fill_word_idx;
drain_idx_q2 <= fill_word_idx;
drain_idx_q3 <= fill_word_idx;
end
end
@@ -207,9 +227,18 @@ module gs_texture_cache #(
// present (tex_rd_addr) when tex_rd_en, data lands next cycle.
wire [31:0] word_off = (tex_rd_addr - TEX_VRAM_BASE) >> 2;
wire [WIDX_BITS-1:0] rd_word = word_off[WIDX_BITS-1:0];
// Ch443: read each byte bank, register the four bytes, reconstruct the 32-bit word
// by concatenation. Identical 1-cycle registered latency to the pre-split read.
logic [7:0] tex_rd_b0, tex_rd_b1, tex_rd_b2, tex_rd_b3;
always_ff @(posedge sample_clk) begin
if (tex_rd_en) tex_rd_data <= tex_mem[rd_word];
if (tex_rd_en) begin
tex_rd_b0 <= tex_mem_b0[rd_word];
tex_rd_b1 <= tex_mem_b1[rd_word];
tex_rd_b2 <= tex_mem_b2[rd_word];
tex_rd_b3 <= tex_mem_b3[rd_word];
end
end
assign tex_rd_data = {tex_rd_b3, tex_rd_b2, tex_rd_b1, tex_rd_b0};
// fill_done -> sample_clk (2-FF). The read mux only goes live once warm.
logic [1:0] done_sync;