4358bc328b
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 from27dfd0bare 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>
166 lines
8.1 KiB
Systemverilog
166 lines
8.1 KiB
Systemverilog
// ============================================================================
|
|
// tb_gs_texture_cache.sv (Ch322 Brick 1)
|
|
//
|
|
// Proves the prefilled texture cache: fill an 8x8 PSMCT32 texture (256 B = 8
|
|
// single-beat 256-bit reads) from a tiny EMIF-ish read model into the on-chip
|
|
// RAM, then verify:
|
|
// (1) fill_done asserts, fill_beats=8, fill_bytes=256, rd_errs=0
|
|
// (2) every cached word == the source word (sampler-side 1-cycle reads)
|
|
// (3) the read is 1-cycle REGISTERED (data lands the cycle AFTER tex_rd_en)
|
|
// (4) a forced non-OKAY rresp increments rd_errs
|
|
// ============================================================================
|
|
`timescale 1ns/1ps
|
|
|
|
module tb_gs_texture_cache;
|
|
localparam int TEX_BYTES = 256;
|
|
localparam int N_BEATS = 8;
|
|
localparam int TEX_WORDS = TEX_BYTES/4; // 64
|
|
localparam [29:0] LPDDR_TEX_BASE = 30'h0010_0000;
|
|
localparam [31:0] TEX_VRAM_BASE = 32'd2048;
|
|
|
|
logic axi_clk = 0, sample_clk = 0, axi_rst_n = 0;
|
|
always #5 axi_clk = ~axi_clk; // 100 MHz-ish
|
|
always #7 sample_clk = ~sample_clk; // asynchronous, slower
|
|
|
|
// fill control
|
|
logic fill_start;
|
|
logic fill_done;
|
|
logic [31:0] fill_beats, fill_bytes, rd_errs;
|
|
|
|
// AXI read channel
|
|
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;
|
|
|
|
// sampler read port
|
|
logic tex_rd_en;
|
|
logic [31:0] tex_rd_addr, tex_rd_data;
|
|
logic tex_ready;
|
|
|
|
// 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;
|
|
|
|
gs_texture_cache #(
|
|
.LPDDR_TEX_BASE(LPDDR_TEX_BASE), .TEX_VRAM_BASE(TEX_VRAM_BASE),
|
|
.TEX_BYTES(TEX_BYTES), .N_BEATS(N_BEATS)
|
|
) dut (
|
|
.axi_clk(axi_clk), .axi_rst_n(axi_rst_n),
|
|
.fill_start(fill_start), .fill_done(fill_done),
|
|
.fill_beats(fill_beats), .fill_bytes(fill_bytes), .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),
|
|
.sample_clk(sample_clk), .tex_rd_en(tex_rd_en),
|
|
.tex_rd_addr(tex_rd_addr), .tex_rd_data(tex_rd_data), .tex_ready(tex_ready)
|
|
);
|
|
|
|
// ---- single-beat EMIF read model (arlen=0 only), ~ a few cycles latency ----
|
|
// returns 8 words per 256-bit beat from the golden source, indexed by araddr.
|
|
typedef enum logic [1:0] { S_IDLE, S_WAIT, S_DATA } sstate_t;
|
|
sstate_t sst;
|
|
logic [29:0] beat_addr; logic [3:0] dly; int beat_idx;
|
|
always_ff @(posedge axi_clk) begin
|
|
if (!axi_rst_n) begin
|
|
sst <= S_IDLE; arready <= 1'b0; rvalid <= 1'b0; rlast <= 1'b0;
|
|
rresp <= 2'b00; rdata <= '0; dly <= '0;
|
|
end else begin
|
|
arready <= 1'b0; rvalid <= 1'b0; rlast <= 1'b0;
|
|
case (sst)
|
|
S_IDLE: if (arvalid) begin
|
|
beat_addr <= araddr; arready <= 1'b1;
|
|
beat_idx <= (araddr - LPDDR_TEX_BASE) >> 5; // 32 B/beat
|
|
dly <= 4'd3; sst <= S_WAIT;
|
|
end
|
|
S_WAIT: if (dly==0) sst <= S_DATA; else dly <= dly - 1'b1;
|
|
S_DATA: if (rready) begin
|
|
for (int w=0; w<8; w++) rdata[w*32 +: 32] <= tex_word(beat_idx*8 + w);
|
|
rresp <= (beat_idx == err_beat) ? 2'b10 : 2'b00; // SLVERR on chosen beat
|
|
rvalid <= 1'b1; rlast <= 1'b1;
|
|
sst <= S_IDLE;
|
|
end
|
|
endcase
|
|
end
|
|
end
|
|
|
|
int errors = 0;
|
|
task automatic check(input bit cond, input string msg);
|
|
if (!cond) begin errors++; $display("[texcache] FAIL: %s", msg); end
|
|
endtask
|
|
|
|
// sampler 1-cycle read helper (sample_clk domain)
|
|
task automatic read_word(input int widx, output logic [31:0] got);
|
|
@(posedge sample_clk);
|
|
tex_rd_addr <= TEX_VRAM_BASE + widx*4; tex_rd_en <= 1'b1;
|
|
@(posedge sample_clk);
|
|
tex_rd_en <= 1'b0; // data for THIS address lands now (1-cycle registered)
|
|
@(posedge sample_clk); // sample the registered output
|
|
got = tex_rd_data;
|
|
endtask
|
|
|
|
logic [31:0] got;
|
|
initial begin
|
|
fill_start = 0; tex_rd_en = 0; tex_rd_addr = 0;
|
|
repeat (4) @(posedge axi_clk); axi_rst_n = 1;
|
|
repeat (2) @(posedge axi_clk);
|
|
|
|
// ---- pass 1: clean fill ----
|
|
// fill_start is an EDGE/TOGGLE: flip once (0->1) and HOLD — a 0->1->0 pulse would
|
|
// trigger a second fill on the falling edge. Pass 2's reset re-creates the edge.
|
|
@(posedge axi_clk); fill_start <= 1'b1;
|
|
// wait for fill_done
|
|
begin int g=0; while (!fill_done && g<2000) begin @(posedge axi_clk); g++; end end
|
|
check(fill_done, "fill_done never asserted");
|
|
check(fill_beats == N_BEATS, $sformatf("fill_beats=%0d exp %0d", fill_beats, N_BEATS));
|
|
check(fill_bytes == TEX_BYTES, $sformatf("fill_bytes=%0d exp %0d", fill_bytes, TEX_BYTES));
|
|
check(rd_errs == 32'd0, $sformatf("rd_errs=%0d exp 0", rd_errs));
|
|
|
|
// wait for tex_ready to cross into sample_clk
|
|
begin int g=0; while (!tex_ready && g<200) begin @(posedge sample_clk); g++; end end
|
|
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);
|
|
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",
|
|
fill_done, fill_beats, fill_bytes, rd_errs, TEX_WORDS, errors);
|
|
|
|
// ---- pass 2: rd_errs exercise (reset, force SLVERR on beat 3) ----
|
|
// The reset clears the cache's fill_start edge-sync; with fill_start still held 1
|
|
// from pass 1, the post-reset re-sync re-creates the edge and re-fills.
|
|
axi_rst_n = 0; err_beat = 3; repeat (3) @(posedge axi_clk); axi_rst_n = 1;
|
|
repeat (2) @(posedge axi_clk);
|
|
begin int g=0; while (!fill_done && g<2000) begin @(posedge axi_clk); g++; end end
|
|
check(rd_errs == 32'd1, $sformatf("forced-err rd_errs=%0d exp 1", rd_errs));
|
|
$display("[texcache] err-inject: rd_errs=%0d (exp 1)", rd_errs);
|
|
|
|
if (errors==0) $display("[tb_gs_texture_cache] PASS");
|
|
else $display("[tb_gs_texture_cache] FAIL (%0d errors)", errors);
|
|
$finish;
|
|
end
|
|
|
|
initial begin #500000; $display("[tb_gs_texture_cache] TIMEOUT"); $finish; end
|
|
endmodule
|