Files
retroDE_ps2/sim/tb/gif_gs/tb_gs_lpddr_scanout.sv
thejayman77 ec82764bef Initial commit: retroDE_ps2 — first-of-its-kind PS2 GS FPGA core (DE25-Nano / Agilex 5)
RTL (GS rasterizer, EE core stub, platform bridge, LPDDR4B path), sim regression
(272 TBs), docs, and tooling. Copyrighted PS2 content (BIOS, game code, GS dumps,
and all dump-derived textures/traces) is excluded via .gitignore and stays local.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 20:10:50 -04:00

148 lines
6.5 KiB
Systemverilog

// ============================================================================
// tb_gs_lpddr_scanout — Ch320 Brick 1
//
// Preloads an AXI read-slave with a known 64x64 PSMCT16 frame (pixel n carries
// the value n), pulses frame_start, waits for the cache to fill, then presents
// PCRTC-style vram_read_addr values and checks the decoded r/g/b match the
// known frame (5->8 bit-replication) and that out-of-range reads return black.
// ============================================================================
`timescale 1ns/1ps
module tb_gs_lpddr_scanout;
logic axi_clk = 0, video_clk = 0;
always #2 axi_clk = ~axi_clk; // ~250 MHz
always #10 video_clk = ~video_clk; // 50 MHz
logic axi_rst_n;
logic enable, frame_start;
logic [31:0] vram_read_addr;
logic [7:0] r, g, b;
logic cache_valid;
logic [31:0] rd_beats, rd_errs;
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 #(.FB_BASE(30'd0), .CACHE_BEATS(256)) dut (
.axi_clk(axi_clk), .axi_rst_n(axi_rst_n),
.enable(enable), .frame_start(frame_start),
.video_clk(video_clk), .vram_read_addr(vram_read_addr),
.r(r), .g(g), .b(b),
.cache_valid(cache_valid), .rd_beats(rd_beats), .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)
);
// ---- known frame: pixel n (16-bit) carries value n ----
function automatic [255:0] beat_data(input [7:0] beat);
integer i;
for (i = 0; i < 16; i = i + 1)
beat_data[16*i +: 16] = (beat * 16 + i); // pixel index = value
endfunction
function automatic [15:0] pixel_val(input [31:0] byte_addr);
pixel_val = byte_addr[16:1]; // pixel index = addr>>1
endfunction
// ---- AXI read-slave model ----
localparam [1:0] S_IDLE=0, S_LAT=1, S_RESP=2;
logic [1:0] sst; logic [2:0] sdly; logic [7:0] sbeat;
always_ff @(posedge axi_clk or negedge axi_rst_n) begin
if (!axi_rst_n) begin
sst<=S_IDLE; arready<=0; rvalid<=0; rlast<=0; rdata<='0; rresp<=2'b00; sdly<=0; sbeat<=0;
end else begin
arready <= 1'b0;
case (sst)
S_IDLE: if (arvalid) begin
arready <= 1'b1; sbeat <= araddr[12:5]; sdly <= 3'd3; sst <= S_LAT;
end
S_LAT: if (sdly==0) begin
rdata<=beat_data(sbeat); rresp<=2'b00; rlast<=1'b1; rvalid<=1'b1; sst<=S_RESP;
end else sdly <= sdly-1;
S_RESP: if (rready) begin rvalid<=0; rlast<=0; sst<=S_IDLE; end
default: sst <= S_IDLE;
endcase
end
end
// ---- checks ----
integer errors = 0;
task automatic check_pixel(input [31:0] addr);
logic [15:0] px; logic [4:0] r5,g5,b5; logic [7:0] er,eg,eb;
begin
vram_read_addr = addr;
@(posedge video_clk); @(posedge video_clk); // registered (sync) read latency
px = pixel_val(addr);
r5 = px[4:0]; g5 = px[9:5]; b5 = px[14:10];
er = {r5, r5[4:2]}; eg = {g5, g5[4:2]}; eb = {b5, b5[4:2]};
if (r!==er || g!==eg || b!==eb) begin
errors = errors + 1;
$display("[tb] PIXEL MISMATCH @0x%0h: got(%0h,%0h,%0h) exp(%0h,%0h,%0h)",
addr, r, g, b, er, eg, eb);
end
end
endtask
task automatic check_black(input [31:0] addr);
begin
vram_read_addr = addr;
@(posedge video_clk); @(posedge video_clk);
if (r!==8'd0 || g!==8'd0 || b!==8'd0) begin
errors = errors + 1;
$display("[tb] EXPECTED BLACK @0x%0h: got(%0h,%0h,%0h)", addr, r, g, b);
end
end
endtask
// Ch352 (Codex) — register-contract witness: while disabled (the de25 top gates this with video_src=0),
// the scanout reader must issue ZERO AXI AR traffic. Proves video_src=0 -> no LPDDR scanout reads.
logic saw_ar_dis; initial saw_ar_dis=0;
always_ff @(posedge axi_clk) if (!enable && arvalid) saw_ar_dis <= 1'b1;
initial begin
enable=0; frame_start=0; vram_read_addr=0;
axi_rst_n=0; repeat(8) @(posedge axi_clk); axi_rst_n=1;
repeat(4) @(posedge video_clk);
// out-of-range before any fill -> black (cache_valid=0)
check_black(32'h0000_0000);
// pulse frame_start while DISABLED — must NOT trigger any AR (contract: video_src=0 => no reads)
@(posedge video_clk); frame_start=1'b1; @(posedge video_clk); frame_start=1'b0;
repeat(40) @(posedge axi_clk);
if (saw_ar_dis) begin errors=errors+1; $display("[tb] AR issued while disabled (video_src=0 contract violated)"); end
// trigger a frame fill
enable = 1'b1;
@(posedge video_clk); frame_start = 1'b1; @(posedge video_clk); frame_start = 1'b0;
// wait for the cache to load
begin integer to; to=0;
while (!cache_valid && to<100000) begin @(posedge axi_clk); to=to+1; end
if (!cache_valid) begin errors=errors+1; $display("[tb] cache_valid TIMEOUT"); end
end
if (rd_beats !== 32'd256) begin errors=errors+1; $display("[tb] rd_beats=%0d exp 256", rd_beats); end
if (rd_errs !== 32'd0) begin errors=errors+1; $display("[tb] rd_errs=%0d exp 0", rd_errs); end
// in-range pixels across beats + halfword lanes
check_pixel(32'h0000_0000); // pixel 0, beat 0 lane 0
check_pixel(32'h0000_0002); // pixel 1
check_pixel(32'h0000_001E); // pixel 15, beat 0 lane 15
check_pixel(32'h0000_0020); // pixel 16, beat 1 lane 0
check_pixel(32'h0000_0080); // pixel 64 (row 1, col 0)
check_pixel(32'h0000_1FFE); // pixel 4095, last in-range
// out-of-range -> black
check_black(32'h0000_2000); // byte 8192 = first past the frame
$display("[tb_gs_lpddr_scanout] errors=%0d", errors);
if (errors==0) $display("[tb_gs_lpddr_scanout] PASS");
else $display("[tb_gs_lpddr_scanout] FAIL");
$finish;
end
initial begin
#500000; $display("[tb_gs_lpddr_scanout] TIMEOUT"); $display("[tb_gs_lpddr_scanout] FAIL"); $finish;
end
endmodule