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