Files
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

240 lines
8.3 KiB
Systemverilog

// retroDE_ps2 — tb_intc_stub
//
// Verifies intc_stub: register mask/set/clear behavior, synthetic interrupt
// injection, and trace on state transitions. Explicitly covers the
// ack+assert collision corner case — simultaneous W1C and irq_src on the
// same bit must keep the bit pending and have arg1/arg2 reflect the
// post-update state (regression guard for the fix flagged in Codex's
// audit on 2026-04-16).
//
// Plan ref: docs/stub_module_plan.md (Initial testbench plan).
`timescale 1ns/1ps
module tb_intc_stub;
logic clk;
logic rst_n;
initial clk = 1'b0;
always #5 clk = ~clk;
// ------------------------------------------------------------------
// DUT and trace sink
// ------------------------------------------------------------------
logic reg_wr_en, reg_rd_en;
logic [7:0] reg_addr;
logic [31:0] reg_wr_data;
logic [31:0] reg_rd_data;
logic reg_rd_valid;
logic [15:0] irq_src;
logic ee_irq;
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;
intc_stub u_intc (
.clk(clk), .rst_n(rst_n),
.reg_wr_en(reg_wr_en), .reg_rd_en(reg_rd_en), .reg_addr(reg_addr),
.reg_wr_data(reg_wr_data), .reg_rd_data(reg_rd_data),
.reg_rd_valid(reg_rd_valid),
.irq_src(irq_src), .cpu_irq(ee_irq),
.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)
);
trace_sink_stub #(.FILENAME("intc.trace"), .SINK_LABEL("intc")) u_trace_intc (
.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)
);
// ------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------
// Event counters. Three distinct categories, distinguished using
// intc_stub's own semantics:
// source_assertion: source-driven (flags[0]==0, arg3==0)
// register_write: mask-write or STAT W1C without ack effect
// (flags[0]==1, arg3==0 — i.e., mask-write branch)
// ack: STAT W1C that actually cleared bits (arg3==1)
//
// The regression guard specifically needs source_assertion counted as
// source-driven, not contaminated by mask writes. Previously all non-
// ack events were lumped together (flagged by Codex audit 2026-04-16).
int errors;
int source_assertion_events;
int register_write_events;
int ack_events;
initial begin
errors = 0;
source_assertion_events = 0;
register_write_events = 0;
ack_events = 0;
end
always_ff @(posedge clk) begin
if (rst_n && ev_valid && ev_event == trace_pkg::EV_IRQ) begin
if (ev_arg3 == 64'd1) begin
ack_events <= ack_events + 1;
end else if (ev_flags[0]) begin
register_write_events <= register_write_events + 1;
end else begin
source_assertion_events <= source_assertion_events + 1;
end
end
end
task automatic write_reg(input logic [7:0] offset, input logic [31:0] data);
@(negedge clk);
reg_wr_en = 1'b1;
reg_addr = offset;
reg_wr_data = data;
@(negedge clk);
reg_wr_en = 1'b0;
reg_addr = 8'd0;
reg_wr_data = 32'd0;
endtask
task automatic expect_ee_irq(input logic want, input string label);
if (ee_irq !== want) begin
$error("[tb_intc_stub] %s: ee_irq=%0b expected=%0b", label, ee_irq, want);
errors = errors + 1;
end
endtask
// ------------------------------------------------------------------
// Stimulus
// ------------------------------------------------------------------
initial begin
rst_n = 1'b0;
reg_wr_en = 1'b0;
reg_rd_en = 1'b0;
reg_addr = 8'd0;
reg_wr_data = 32'd0;
irq_src = 16'd0;
repeat (4) @(posedge clk);
rst_n = 1'b1;
repeat (2) @(posedge clk);
// Post-reset: no interrupts should be pending, no line asserted.
expect_ee_irq(1'b0, "post-reset quiet");
// Enable mask for bits [3:0].
write_reg(8'h10, 32'h0000_000F);
repeat (2) @(posedge clk);
// Inject a pulse on bit 1 and verify ee_irq asserts.
@(negedge clk);
irq_src = 16'h0002;
@(negedge clk);
irq_src = 16'h0000;
repeat (2) @(posedge clk);
expect_ee_irq(1'b1, "after bit1 assertion");
// W1C the bit and verify ee_irq drops.
write_reg(8'h00, 32'h0000_0002);
repeat (2) @(posedge clk);
expect_ee_irq(1'b0, "after bit1 ack");
// Mask-gated: inject bit 8 while mask has only [3:0] enabled.
// ee_irq should stay low even though intc_stat latches.
@(negedge clk);
irq_src = 16'h0100;
@(negedge clk);
irq_src = 16'h0000;
repeat (2) @(posedge clk);
expect_ee_irq(1'b0, "masked bit8 holds line low");
// Widen mask to include bit 8; line should now assert.
write_reg(8'h10, 32'h0000_010F);
repeat (2) @(posedge clk);
expect_ee_irq(1'b1, "after widening mask");
write_reg(8'h00, 32'h0000_0100);
repeat (2) @(posedge clk);
expect_ee_irq(1'b0, "after bit8 ack");
// ------------------------------------------------------------------
// Regression guard: simultaneous ack-and-inject on a *pre-asserted*
// bit. Real-hardware contract: the assertion wins, intc_stat bit
// stays pending, and the ack-path trace must show the bit still set
// in arg1/arg2 (post-update state). Covers the audit fix from
// 2026-04-16 where arg1 was dropping stat_inject.
// ------------------------------------------------------------------
// Pre-assert bit 2 via a pulse on irq_src.
@(negedge clk);
irq_src = 16'h0004;
@(negedge clk);
irq_src = 16'h0000;
repeat (2) @(posedge clk);
if ((u_intc.intc_stat[2]) !== 1'b1) begin
$error("[tb_intc_stub] setup for collision test failed: bit2 not latched");
errors = errors + 1;
end
// Collision: W1C bit 2 AND re-inject bit 2 on the same cycle.
@(negedge clk);
reg_wr_en = 1'b1;
reg_addr = 8'h00;
reg_wr_data = 32'h0000_0004;
irq_src = 16'h0004;
@(posedge clk);
#1;
if ((u_intc.intc_stat[2]) !== 1'b1) begin
$error("[tb_intc_stub] collision: bit2 dropped when inject+ack coincided");
errors = errors + 1;
end
@(negedge clk);
reg_wr_en = 1'b0;
reg_wr_data = 32'd0;
irq_src = 16'h0000;
repeat (2) @(posedge clk);
// Clean up: a plain W1C now should finally clear bit 2.
write_reg(8'h00, 32'h0000_0004);
repeat (2) @(posedge clk);
if ((u_intc.intc_stat[2]) !== 1'b0) begin
$error("[tb_intc_stub] post-collision cleanup: bit2 still pending");
errors = errors + 1;
end
// ------------------------------------------------------------------
$display("[tb_intc_stub] source_assertion=%0d register_write=%0d ack=%0d errors=%0d",
source_assertion_events, register_write_events, ack_events, errors);
if (source_assertion_events < 3)
$error("[tb_intc_stub] expected at least 3 source-driven assertion events, got %0d",
source_assertion_events);
if (ack_events < 2)
$error("[tb_intc_stub] expected at least 2 ack events, got %0d", ack_events);
if (errors == 0 && source_assertion_events >= 3 && ack_events >= 2)
$display("[tb_intc_stub] PASS");
else
$display("[tb_intc_stub] FAIL");
$finish;
end
initial begin
#100000;
$error("[tb_intc_stub] timeout");
$finish;
end
endmodule : tb_intc_stub