Files
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

183 lines
6.4 KiB
Systemverilog

// retroDE_ps2 — tb_vram_stub (Ch89 audit-medium)
//
// Focused unit TB for vram_stub bounds-check correctness. The
// Ch89 audit flagged the original `(addr + 32'd3) < BYTES` guard:
// a 32-bit addr near 0xFFFF_FFFF wraps on the +3 and the
// comparison passes, indexing out of range. The fix uses
// `addr <= MAX_BASE` where MAX_BASE = BYTES - 4 (a localparam).
// This TB exercises the boundary and a high-address wrap case.
//
// Scenarios:
// 1. Write at addr 0 → read back matches.
// 2. Write at addr MAX_BASE = BYTES-4 → read back matches.
// 3. Write at addr MAX_BASE+1 → rejected; read at that addr
// returns zero.
// 4. Write at addr 0xFFFF_FFFE (near 32-bit wrap; +3 wraps to
// 0x0000_0001, which would have falsely passed the old
// `(addr+3) < BYTES` check) → rejected; read at addr 0
// stays zero (proving we did NOT alias-write into mem[0]).
`timescale 1ns/1ps
module tb_vram_stub;
localparam int unsigned BYTES = 64;
logic clk;
logic rst_n;
initial clk = 1'b0;
always #5 clk = ~clk;
logic write_en;
logic [31:0] write_addr;
logic [31:0] write_data;
logic [3:0] write_be;
logic [31:0] read_addr;
logic [31:0] read_data;
vram_stub #(.BYTES(BYTES)) u_vram (
.clk(clk), .rst_n(rst_n),
.write_en(write_en),
.write_addr(write_addr),
.write_data(write_data),
.write_be(write_be),
.write_mask(32'hFFFF_FFFF),
.read_addr(read_addr),
.read_data(read_data),
.read2_addr(32'd0),
.read2_data()
);
int errors;
task automatic do_write(input logic [31:0] addr, input logic [31:0] data);
@(negedge clk);
write_en = 1'b1;
write_addr = addr;
write_data = data;
write_be = 4'b1111;
@(posedge clk);
@(negedge clk);
write_en = 1'b0;
write_addr = 32'd0;
write_data = 32'd0;
write_be = 4'b0000;
endtask
// Halfword (PSMCT16-style) write: only the low 2 bytes of the
// 32-bit slot starting at `addr` are committed.
task automatic do_write_hw(input logic [31:0] addr, input logic [15:0] data);
@(negedge clk);
write_en = 1'b1;
write_addr = addr;
write_data = {16'd0, data};
write_be = 4'b0011;
@(posedge clk);
@(negedge clk);
write_en = 1'b0;
write_addr = 32'd0;
write_data = 32'd0;
write_be = 4'b0000;
endtask
task automatic check_read_hw(input logic [31:0] addr,
input logic [15:0] expected,
input string tag);
// Use the existing 4-byte read at a 4-byte-aligned base
// and mux the right halfword.
logic [15:0] got;
read_addr = addr & ~32'd3;
#1;
got = addr[1] ? read_data[31:16] : read_data[15:0];
if (got !== expected) begin
$error("[%s] hw read @0x%08x got 0x%04x expected 0x%04x",
tag, addr, got, expected);
errors = errors + 1;
end else begin
$display("[tb_vram_stub] %s hw read @0x%08x = 0x%04x OK", tag, addr, got);
end
endtask
task automatic check_read(input logic [31:0] addr,
input logic [31:0] expected,
input string tag);
read_addr = addr;
#1;
if (read_data !== expected) begin
$error("[%s] read @0x%08x got 0x%08x expected 0x%08x",
tag, addr, read_data, expected);
errors = errors + 1;
end else begin
$display("[tb_vram_stub] %s read @0x%08x = 0x%08x OK", tag, addr, read_data);
end
endtask
initial begin
rst_n = 1'b0;
write_en = 1'b0;
write_be = 4'b0000;
write_addr = 32'd0;
write_data = 32'd0;
read_addr = 32'd0;
errors = 0;
repeat (4) @(posedge clk);
rst_n = 1'b1;
repeat (2) @(posedge clk);
// 1. In-range write at 0.
do_write(32'd0, 32'hDEAD_BEEF);
check_read(32'd0, 32'hDEAD_BEEF, "in-range-low");
// 2. In-range write at MAX_BASE = BYTES-4 (boundary OK).
do_write(32'(BYTES - 4), 32'hCAFE_F00D);
check_read(32'(BYTES - 4), 32'hCAFE_F00D, "in-range-boundary");
// 3. Out-of-range write at MAX_BASE+1: +3 stays in 32-bit
// space (no wrap), but addr > MAX_BASE → reject.
do_write(32'(BYTES - 3), 32'h1111_2222);
check_read(32'(BYTES - 3), 32'd0, "oor-just-past-boundary");
// 4. High-address wrap case (the Ch89 audit-medium fix
// target): addr = 0xFFFF_FFFE; addr+3 wraps to
// 0x0000_0001 which is < BYTES, so the OLD guard would
// have allowed indexing. We assert the write is
// rejected (no aliasing into mem[0]) AND that the
// high-addr read returns zero.
do_write(32'hFFFF_FFFE, 32'h5555_AAAA);
check_read(32'hFFFF_FFFE, 32'd0, "oor-32-bit-wrap-read");
// mem[0] should still be the value from step 1.
check_read(32'd0, 32'hDEAD_BEEF, "no-alias-into-low-addr");
// 5. Ch95 audit-medium target — halfword write at
// BYTES-2 must be ACCEPTED (both bytes in range), and
// halfword write at BYTES-1 must be REJECTED (byte 1
// of the slot lands at BYTES, OOB).
// The pre-fix admission gate was the 4-byte
// `addr <= MAX_BASE` (= BYTES-4), which would have
// silently dropped the BYTES-2 halfword too.
// After step 2 the slot at BYTES-4 holds 0xCAFE_F00D
// (mem[60..63] = {0x0D, 0xF0, 0xFE, 0xCA}). After
// the BYTES-2 halfword write of 0x7788, mem[62..63]
// becomes {0x88, 0x77}, so the slot reads back as
// 0x7788_F00D. The rejected write at BYTES-1 must
// leave that unchanged.
do_write_hw(32'(BYTES - 2), 16'h7788);
check_read(32'(BYTES - 4), 32'h7788_F00D, "hw-in-range-boundary-slot");
do_write_hw(32'(BYTES - 1), 16'h99AA);
check_read(32'(BYTES - 4), 32'h7788_F00D, "hw-oor-leaves-slot-untouched");
if (errors == 0) $display("[tb_vram_stub] PASS");
else $display("[tb_vram_stub] FAIL");
$finish;
end
initial begin
#1000000;
$error("[tb_vram_stub] timeout");
$finish;
end
endmodule : tb_vram_stub