Files
retroDE_ps2/sim/tb/iop/tb_sio2_input_stub.sv
T
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

238 lines
9.0 KiB
Systemverilog

// retroDE_ps2 — tb_sio2_input_stub (Ch234)
// ============================================================================
// Focused unit-test TB for sio2_input_stub. Instantiates the stub
// directly (no IOP map) and drives synthetic input_p1/input_p2 bitmaps,
// then reads back PAD_P1_STATE / PAD_P2_STATE / PAD_STATUS through the
// stub's IOP-map-style read port.
//
// Verifies:
// 1. Reset: stub responds 32'd0 for any read.
// 2. No buttons (INPUT_P1 = INPUT_P2 = 0) → Sony word = 0xFFFF.
// 3. Single buttons (each of the 16 retroDE bits) map to the right
// Sony pad bit (active-low: pressed = bit cleared).
// 4. Combos (multiple buttons) → combined active-low
// pattern.
// 5. P1 and P2 are independent (writing P1 doesn't change P2 readback
// and vice versa).
// 6. PAD_STATUS reads `32'h0000_0001` (bit 0 = present/valid).
// 7. Writes accepted-and-ignored (no mutation of any state).
// 8. Out-of-range word offsets inside the region read 0.
//
// CDC note: this TB drives a single clock, so the stub's 2-FF
// synchronizer is functionally a 2-cycle delay. The TB inserts
// `repeat (3) @(posedge clk)` after every INPUT_P1/P2 change to let
// the sync settle + the read pipeline produce valid data.
// ============================================================================
`timescale 1ns/1ps
module tb_sio2_input_stub;
logic clk;
logic rst_n;
initial clk = 1'b0;
always #5 clk = ~clk;
logic [31:0] input_p1;
logic [31:0] input_p2;
logic rd_en;
logic [3:0] rd_addr;
wire [31:0] rd_data;
wire rd_valid;
logic wr_en;
logic [3:0] wr_addr;
logic [31:0] wr_data;
sio2_input_stub u_dut (
.clk (clk),
.rst_n (rst_n),
.input_p1 (input_p1),
.input_p2 (input_p2),
.rd_en (rd_en),
.rd_addr (rd_addr),
.rd_data (rd_data),
.rd_valid (rd_valid),
.wr_en (wr_en),
.wr_addr (wr_addr),
.wr_data (wr_data)
);
int errors;
task automatic check_eq(input string label,
input logic [31:0] got,
input logic [31:0] expected);
if (got !== expected) begin
$error("[%s] got 0x%08x expected 0x%08x", label, got, expected);
errors = errors + 1;
end
endtask
// Issue a read at the given word offset and sample rd_data the
// cycle rd_valid asserts.
task automatic do_read(input logic [3:0] addr, output logic [31:0] data);
@(posedge clk);
rd_en <= 1'b1;
rd_addr <= addr;
@(posedge clk);
rd_en <= 1'b0;
wait (rd_valid);
data = rd_data;
@(posedge clk);
endtask
// Apply new pad state and let the 2-FF sync settle.
task automatic apply_pads(input logic [31:0] new_p1, input logic [31:0] new_p2);
@(posedge clk);
input_p1 <= new_p1;
input_p2 <= new_p2;
repeat (4) @(posedge clk); // 2-FF sync + slack
endtask
// Sony pad-word expectation helper. Mirrors the stub's sony_word()
// function — bit-for-bit copy from `docs/contracts/sio2_pad.md`.
function automatic logic [15:0] expected_sony(input logic [31:0] joy);
logic [7:0] b3, b4;
b3 = ~{joy[1], joy[2], joy[0], joy[3], joy[4], joy[15], joy[14], joy[5]};
b4 = ~{joy[8], joy[7], joy[9], joy[6], joy[11], joy[10], joy[13], joy[12]};
expected_sony = {b4, b3};
endfunction
logic [31:0] rd;
logic [31:0] pat;
initial begin
errors = 0;
rst_n = 1'b0;
rd_en = 1'b0;
rd_addr = 4'd0;
wr_en = 1'b0;
wr_addr = 4'd0;
wr_data = 32'd0;
input_p1 = 32'd0;
input_p2 = 32'd0;
repeat (4) @(posedge clk);
rst_n = 1'b1;
repeat (4) @(posedge clk);
// ----------------------------------------------------------
// §1. Reset state — all pad bits LOW → Sony word 0xFFFF.
// STATUS = 1.
// ----------------------------------------------------------
do_read(4'h0, rd); check_eq("rst_P1_word", rd, 32'h0000_FFFF);
do_read(4'h1, rd); check_eq("rst_P2_word", rd, 32'h0000_FFFF);
do_read(4'h2, rd); check_eq("rst_STATUS", rd, 32'h0000_0001);
// Out-of-range reads inside the 4-bit addr field → 0.
do_read(4'h3, rd); check_eq("rst_oob_3", rd, 32'd0);
do_read(4'hF, rd); check_eq("rst_oob_F", rd, 32'd0);
// ----------------------------------------------------------
// §2. Single-button mapping — for each of the 16 retroDE
// bits, press only that bit and verify the Sony word
// has exactly the corresponding bit cleared.
// ----------------------------------------------------------
for (int i = 0; i < 16; i = i + 1) begin
pat = (32'd1 << i);
apply_pads(pat, 32'd0);
do_read(4'h0, rd);
check_eq($sformatf("single_bit_%0d_P1", i),
rd, {16'd0, expected_sony(pat)});
do_read(4'h1, rd);
check_eq($sformatf("single_bit_%0d_P2_unaffected", i),
rd, 32'h0000_FFFF);
end
// ----------------------------------------------------------
// §3. JOY_OSD (bit 16) is intentionally NOT forwarded —
// retrodesd consumes it before the bridge. Pressing it
// should leave the Sony word at 0xFFFF.
// ----------------------------------------------------------
apply_pads(32'h0001_0000, 32'd0);
do_read(4'h0, rd); check_eq("OSD_bit_not_forwarded",
rd, 32'h0000_FFFF);
// ----------------------------------------------------------
// §4. Combos. The classic "Konami code" final two — Start +
// Select pressed together → byte3 bits 0 and 3 both
// cleared, all other bits HIGH (released).
// ----------------------------------------------------------
pat = (32'd1 << 4) | (32'd1 << 5); // START | SELECT
apply_pads(pat, 32'd0);
do_read(4'h0, rd);
check_eq("combo_START_SELECT", rd, {16'd0, expected_sony(pat)});
// All face + D-pad pressed at once. Both bytes have visible
// clears.
pat = (32'd1 << 6) | (32'd1 << 7) | (32'd1 << 8) | (32'd1 << 9)
| (32'd1 << 0) | (32'd1 << 1) | (32'd1 << 2) | (32'd1 << 3);
apply_pads(pat, 32'd0);
do_read(4'h0, rd);
check_eq("combo_face_plus_dpad", rd, {16'd0, expected_sony(pat)});
// ----------------------------------------------------------
// §5. P1/P2 independence. Different patterns on each
// channel, verify reads track each port independently.
// ----------------------------------------------------------
apply_pads(32'hAAAA_5555, 32'h5555_AAAA);
do_read(4'h0, rd);
check_eq("indep_P1_word",
rd, {16'd0, expected_sony(32'hAAAA_5555)});
do_read(4'h1, rd);
check_eq("indep_P2_word",
rd, {16'd0, expected_sony(32'h5555_AAAA)});
// ----------------------------------------------------------
// §6. Writes accepted-and-ignored. Drive write strobes at
// each of the three mapped addresses, then verify the
// readback values are unchanged.
// ----------------------------------------------------------
@(posedge clk);
wr_en <= 1'b1;
wr_addr <= 4'h0;
wr_data <= 32'hF00DBABE;
@(posedge clk);
wr_addr <= 4'h1;
wr_data <= 32'hCAFEF00D;
@(posedge clk);
wr_addr <= 4'h2;
wr_data <= 32'h12345678;
@(posedge clk);
wr_en <= 1'b0;
repeat (2) @(posedge clk);
do_read(4'h0, rd);
check_eq("wr_ignored_P1", rd, {16'd0, expected_sony(32'hAAAA_5555)});
do_read(4'h1, rd);
check_eq("wr_ignored_P2", rd, {16'd0, expected_sony(32'h5555_AAAA)});
do_read(4'h2, rd);
check_eq("wr_ignored_STATUS", rd, 32'h0000_0001);
// ----------------------------------------------------------
// §7. Clearing both pads returns to all-released = 0xFFFF.
// ----------------------------------------------------------
apply_pads(32'd0, 32'd0);
do_read(4'h0, rd); check_eq("clr_P1", rd, 32'h0000_FFFF);
do_read(4'h1, rd); check_eq("clr_P2", rd, 32'h0000_FFFF);
do_read(4'h2, rd); check_eq("clr_STATUS", rd, 32'h0000_0001);
// ----------------------------------------------------------
// Done.
// ----------------------------------------------------------
$display("[tb_sio2_input_stub] errors=%0d", errors);
if (errors == 0) $display("[tb_sio2_input_stub] PASS");
else $display("[tb_sio2_input_stub] FAIL");
$finish;
end
initial begin
#5_000_000;
$error("[tb_sio2_input_stub] TIMEOUT");
$finish;
end
endmodule : tb_sio2_input_stub