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>
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
// 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
|
||||
Reference in New Issue
Block a user