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:
@@ -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
|
||||
@@ -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.)
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user