// ============================================================================ // 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