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,265 @@
|
||||
// retroDE_ps2 — tb_sif_negative_path
|
||||
//
|
||||
// Negative-path coordination proof for the SIF mailbox seam. Exercises
|
||||
// malformed or incomplete lifecycle usage and verifies that the peer
|
||||
// refuses to false-ack while staying available to recover when the TB
|
||||
// eventually does the right thing.
|
||||
//
|
||||
// Matches the SIF contract's "mismatched or stalled handshakes" debug-
|
||||
// visibility requirement (docs/contracts/sif.md).
|
||||
//
|
||||
// Three phases:
|
||||
//
|
||||
// Phase A — no doorbell: after reset, no EE activity for a long window.
|
||||
// Peer must stay silent. ack_count_o == 0, done_o == 0.
|
||||
//
|
||||
// Phase B — doorbell stuck high: one legitimate exchange, then the TB
|
||||
// fails to re-arm. MSFLG stays asserted; TB even re-writes the same
|
||||
// bit to probe for spurious re-fire. Peer must respond exactly once.
|
||||
//
|
||||
// Phase C — proper re-arm recovers: TB finally clears MSFLG (and SMFLG
|
||||
// for cleanliness), then issues a second distinct command. Peer
|
||||
// must echo the new command, not stale state.
|
||||
//
|
||||
// Key assertions:
|
||||
// - ack_count stays at 0 through Phase A's quiet window
|
||||
// - ack_count stays at 1 through Phase B's stuck-doorbell window
|
||||
// - ack_count advances to 2 only after proper re-arm + new doorbell
|
||||
// - SMCOM at end of Phase C holds cmd2, not stale cmd1
|
||||
|
||||
`timescale 1ns/1ps
|
||||
|
||||
module tb_sif_negative_path;
|
||||
|
||||
logic clk;
|
||||
logic rst_n;
|
||||
|
||||
initial clk = 1'b0;
|
||||
always #5 clk = ~clk;
|
||||
|
||||
localparam logic [7:0] MSCOM = 8'h00;
|
||||
localparam logic [7:0] SMCOM = 8'h10;
|
||||
localparam logic [7:0] MSFLG = 8'h20;
|
||||
localparam logic [7:0] SMFLG = 8'h30;
|
||||
localparam logic [31:0] CMD_PENDING_BIT = 32'h0000_0001;
|
||||
localparam logic [31:0] CMD_ACK_BIT = 32'h0000_0002;
|
||||
localparam logic [31:0] CMD1_VALUE = 32'hCAFE_CAFE;
|
||||
localparam logic [31:0] CMD2_VALUE = 32'h1234_5678;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Mailbox + peer
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
logic ee_wr_en, ee_rd_en;
|
||||
logic [7:0] ee_addr;
|
||||
logic [31:0] ee_wr_data;
|
||||
logic [31:0] ee_rd_data;
|
||||
logic ee_rd_valid;
|
||||
|
||||
logic peer_rd_en;
|
||||
logic [7:0] peer_rd_addr;
|
||||
logic [31:0] peer_rd_data;
|
||||
logic peer_rd_valid;
|
||||
|
||||
logic peer_wr_en;
|
||||
logic [7:0] peer_wr_addr_w;
|
||||
logic [31:0] peer_wr_data_w;
|
||||
|
||||
logic peer_done;
|
||||
logic [31:0] peer_ack_count;
|
||||
|
||||
logic ev_valid;
|
||||
trace_pkg::subsys_e ev_subsys;
|
||||
trace_pkg::event_e ev_event;
|
||||
logic [63:0] ev_arg0, ev_arg1, ev_arg2, ev_arg3;
|
||||
logic [31:0] ev_flags;
|
||||
|
||||
sif_mailbox_stub u_mailbox (
|
||||
.clk(clk), .rst_n(rst_n),
|
||||
.ee_wr_en(ee_wr_en), .ee_rd_en(ee_rd_en),
|
||||
.ee_addr(ee_addr), .ee_wr_data(ee_wr_data),
|
||||
.ee_rd_data(ee_rd_data), .ee_rd_valid(ee_rd_valid),
|
||||
.iop_wr_en(peer_wr_en), .iop_rd_en(peer_rd_en),
|
||||
.iop_addr(peer_wr_en ? peer_wr_addr_w : peer_rd_addr),
|
||||
.iop_wr_data(peer_wr_data_w),
|
||||
.iop_rd_data(peer_rd_data), .iop_rd_valid(peer_rd_valid),
|
||||
.ev_valid(ev_valid), .ev_subsys(ev_subsys),
|
||||
.ev_event(ev_event),
|
||||
.ev_arg0(ev_arg0), .ev_arg1(ev_arg1),
|
||||
.ev_arg2(ev_arg2), .ev_arg3(ev_arg3),
|
||||
.ev_flags(ev_flags)
|
||||
);
|
||||
|
||||
sif_mailbox_peer_stub u_peer (
|
||||
.clk(clk), .rst_n(rst_n),
|
||||
.obs_rd_en(peer_rd_en), .obs_rd_addr(peer_rd_addr),
|
||||
.obs_rd_data(peer_rd_data), .obs_rd_valid(peer_rd_valid),
|
||||
.resp_wr_en(peer_wr_en), .resp_wr_addr(peer_wr_addr_w),
|
||||
.resp_wr_data(peer_wr_data_w),
|
||||
.done_o(peer_done), .ack_count_o(peer_ack_count)
|
||||
);
|
||||
|
||||
trace_sink_stub #(.FILENAME("sif_negative_path.trace"), .SINK_LABEL("sif"))
|
||||
u_trace_sif (
|
||||
.clk(clk), .rst_n(rst_n),
|
||||
.ev_valid(ev_valid), .ev_subsys(ev_subsys),
|
||||
.ev_event(ev_event),
|
||||
.ev_arg0(ev_arg0), .ev_arg1(ev_arg1),
|
||||
.ev_arg2(ev_arg2), .ev_arg3(ev_arg3),
|
||||
.ev_flags(ev_flags)
|
||||
);
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// EE-side helpers
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
int errors;
|
||||
|
||||
initial errors = 0;
|
||||
|
||||
task automatic ee_write(input logic [7:0] addr, input logic [31:0] data);
|
||||
@(negedge clk);
|
||||
ee_wr_en = 1'b1;
|
||||
ee_addr = addr;
|
||||
ee_wr_data = data;
|
||||
@(negedge clk);
|
||||
ee_wr_en = 1'b0;
|
||||
ee_addr = 8'd0;
|
||||
ee_wr_data = 32'd0;
|
||||
endtask
|
||||
|
||||
task automatic ee_read_expect(input logic [7:0] addr, input logic [31:0] expected,
|
||||
input string label);
|
||||
@(negedge clk);
|
||||
ee_rd_en = 1'b1;
|
||||
ee_addr = addr;
|
||||
@(negedge clk);
|
||||
ee_rd_en = 1'b0;
|
||||
ee_addr = 8'd0;
|
||||
if (ee_rd_data !== expected || ee_rd_valid !== 1'b1) begin
|
||||
$error("[tb_sif_negative_path] EE read %s: got 0x%08h valid=%0b expected 0x%08h",
|
||||
label, ee_rd_data, ee_rd_valid, expected);
|
||||
errors = errors + 1;
|
||||
end
|
||||
endtask
|
||||
|
||||
task automatic wait_for_ack(input int target, input string label);
|
||||
int spin;
|
||||
spin = 0;
|
||||
while (peer_ack_count < target && spin < 200) begin
|
||||
@(posedge clk);
|
||||
spin = spin + 1;
|
||||
end
|
||||
if (peer_ack_count < target) begin
|
||||
$error("[tb_sif_negative_path] peer did not reach ack_count=%0d (%s) within %0d cycles",
|
||||
target, label, spin);
|
||||
errors = errors + 1;
|
||||
end
|
||||
endtask
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Phase markers (for $display traceability)
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
int phase_a_ack_observed;
|
||||
int phase_b_ack_observed;
|
||||
int phase_c_ack_observed;
|
||||
|
||||
initial begin
|
||||
rst_n = 1'b0;
|
||||
ee_wr_en = 1'b0;
|
||||
ee_rd_en = 1'b0;
|
||||
ee_addr = 8'd0;
|
||||
ee_wr_data = 32'd0;
|
||||
phase_a_ack_observed = 0;
|
||||
phase_b_ack_observed = 0;
|
||||
phase_c_ack_observed = 0;
|
||||
|
||||
repeat (4) @(posedge clk);
|
||||
rst_n = 1'b1;
|
||||
|
||||
// ---- Phase A: no doorbell ----
|
||||
// Peer must stay silent throughout this window.
|
||||
repeat (80) @(posedge clk);
|
||||
phase_a_ack_observed = peer_ack_count;
|
||||
if (peer_ack_count !== 32'd0) begin
|
||||
$error("[tb_sif_negative_path] Phase A: peer fired without a doorbell (ack_count=%0d)",
|
||||
peer_ack_count);
|
||||
errors = errors + 1;
|
||||
end
|
||||
if (peer_done !== 1'b0) begin
|
||||
$error("[tb_sif_negative_path] Phase A: done_o asserted without any ack");
|
||||
errors = errors + 1;
|
||||
end
|
||||
|
||||
// ---- Phase B: legitimate exchange, then stuck doorbell ----
|
||||
ee_write(MSCOM, CMD1_VALUE);
|
||||
ee_write(MSFLG, CMD_PENDING_BIT);
|
||||
wait_for_ack(1, "Phase B first ack");
|
||||
repeat (2) @(posedge clk);
|
||||
|
||||
ee_read_expect(SMCOM, CMD1_VALUE, "Phase B SMCOM after cmd1");
|
||||
ee_read_expect(SMFLG, CMD_ACK_BIT, "Phase B SMFLG after cmd1");
|
||||
|
||||
// Now leave MSFLG stuck high. TB even probes by re-writing MSFLG
|
||||
// with the same bit set — redundant from the storage view, but a
|
||||
// common real-world mistake. Peer must not re-fire.
|
||||
ee_write(MSFLG, CMD_PENDING_BIT); // same value, no transition
|
||||
repeat (40) @(posedge clk);
|
||||
ee_write(MSFLG, CMD_PENDING_BIT | 32'h4); // extra unrelated bit on
|
||||
repeat (40) @(posedge clk);
|
||||
|
||||
phase_b_ack_observed = peer_ack_count;
|
||||
if (peer_ack_count !== 32'd1) begin
|
||||
$error("[tb_sif_negative_path] Phase B: peer re-fired while doorbell stayed high (count=%0d)",
|
||||
peer_ack_count);
|
||||
errors = errors + 1;
|
||||
end
|
||||
|
||||
// ---- Phase C: proper re-arm + fresh command ----
|
||||
ee_write(MSFLG, 32'd0); // clear doorbell
|
||||
ee_write(SMFLG, 32'd0); // TB also clears its
|
||||
// own response flag
|
||||
repeat (6) @(posedge clk); // let peer observe
|
||||
// MSFLG low → clears
|
||||
// responded
|
||||
|
||||
ee_write(MSCOM, CMD2_VALUE);
|
||||
ee_write(MSFLG, CMD_PENDING_BIT);
|
||||
wait_for_ack(2, "Phase C second ack");
|
||||
repeat (2) @(posedge clk);
|
||||
|
||||
ee_read_expect(SMCOM, CMD2_VALUE, "Phase C SMCOM (must be cmd2, not stale cmd1)");
|
||||
ee_read_expect(SMFLG, CMD_ACK_BIT, "Phase C SMFLG after cmd2");
|
||||
|
||||
phase_c_ack_observed = peer_ack_count;
|
||||
if (peer_ack_count !== 32'd2) begin
|
||||
$error("[tb_sif_negative_path] Phase C: expected ack_count=2, got %0d",
|
||||
peer_ack_count);
|
||||
errors = errors + 1;
|
||||
end
|
||||
|
||||
repeat (4) @(posedge clk);
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
$display("[tb_sif_negative_path] phase_a_ack=%0d phase_b_ack=%0d phase_c_ack=%0d errors=%0d",
|
||||
phase_a_ack_observed, phase_b_ack_observed, phase_c_ack_observed, errors);
|
||||
|
||||
if (errors == 0 &&
|
||||
phase_a_ack_observed == 0 &&
|
||||
phase_b_ack_observed == 1 &&
|
||||
phase_c_ack_observed == 2)
|
||||
$display("[tb_sif_negative_path] PASS");
|
||||
else
|
||||
$display("[tb_sif_negative_path] FAIL");
|
||||
|
||||
$finish;
|
||||
end
|
||||
|
||||
initial begin
|
||||
#400000;
|
||||
$error("[tb_sif_negative_path] timeout");
|
||||
$finish;
|
||||
end
|
||||
|
||||
endmodule : tb_sif_negative_path
|
||||
Reference in New Issue
Block a user