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