Files
retroDE_ps2/rtl/sif/sif_mailbox_stub.sv
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

231 lines
8.8 KiB
Systemverilog

// retroDE_ps2 — sif_mailbox_stub
//
// Minimal EE↔IOP subsystem-interface mailbox shell. First stub on the SIF
// track. Standalone unit — does not yet integrate with any live IOP core.
// Testbenches drive both the EE-side port and the IOP-side port directly,
// playing both roles, to prove the register semantics without requiring
// a full dual-CPU bring-up.
//
// Contract refs:
// docs/stub_module_plan.md (Wave 2, item 10)
// docs/contracts/sif.md (mailbox/flag-only stub is allowed here)
//
// Register surface (offsets within the SIF block):
// 0x00 MSCOM — 32-bit mailbox, conventionally EE→IOP
// 0x10 SMCOM — 32-bit mailbox, conventionally IOP→EE
// 0x20 MSFLG — 32-bit flag word, conventionally EE-owned for set,
// IOP-owned for clear (directional semantics deferred)
// 0x30 SMFLG — 32-bit flag word, conventionally IOP-owned for set,
// EE-owned for clear (directional semantics deferred)
//
// Wave 2 scope intentionally does NOT enforce direction or set/clear
// semantics. Both ports can read and write any register with plain
// replace-on-write. The trace records which side initiated each access
// (side_id in arg2) so future-wave work can layer directional rules on
// top without changing the storage model.
//
// Port semantics:
// Each side (EE / IOP) has an independent register port:
// wr_en, rd_en, addr[7:0], wr_data[31:0], rd_data[31:0], rd_valid
// Reads have 1-cycle latency to match the existing stub ecosystem.
//
// Write arbitration (per-register):
// - EE and IOP writes to *different* registers on the same cycle both
// land. Storage is not serialized across independent registers.
// - EE and IOP writes to the *same* register on the same cycle: EE
// wins, IOP write is dropped that cycle.
// - Trace is limited to one event per cycle by the shared trace bus
// (priority EE > IOP). An IOP write that lands silently when EE is
// driving a different register will not be traced this wave — future
// waves can add a second trace output port if that becomes a gap.
//
// Trace payload schema (SUBSYS_SIF, existing EV_READ/EV_WRITE codes):
// SIF WRITE arg0=offset arg1=data arg2=side_id arg3=0 flags[0]=1
// SIF READ arg0=offset arg1=data arg2=side_id arg3=0 flags[0]=0
// side_id: 0 = EE, 1 = IOP
//
// Trace priority on same cycle: EE write > IOP write > EE read > IOP read.
// In practice TBs drive at most one operation per cycle.
`timescale 1ns/1ps
module sif_mailbox_stub
import trace_pkg::*;
(
input logic clk,
input logic rst_n,
// EE-side register port
input logic ee_wr_en,
input logic ee_rd_en,
input logic [7:0] ee_addr,
input logic [31:0] ee_wr_data,
output logic [31:0] ee_rd_data,
output logic ee_rd_valid,
// IOP-side register port
input logic iop_wr_en,
input logic iop_rd_en,
input logic [7:0] iop_addr,
input logic [31:0] iop_wr_data,
output logic [31:0] iop_rd_data,
output logic iop_rd_valid,
// Trace
output logic ev_valid,
output subsys_e ev_subsys,
output event_e ev_event,
output logic [63:0] ev_arg0,
output logic [63:0] ev_arg1,
output logic [63:0] ev_arg2,
output logic [63:0] ev_arg3,
output logic [31:0] ev_flags
);
localparam logic [7:0] MSCOM_OFF = 8'h00;
localparam logic [7:0] SMCOM_OFF = 8'h10;
localparam logic [7:0] MSFLG_OFF = 8'h20;
localparam logic [7:0] SMFLG_OFF = 8'h30;
localparam logic [63:0] SIDE_EE = 64'd0;
localparam logic [63:0] SIDE_IOP = 64'd1;
// ------------------------------------------------------------------
// Register file
// ------------------------------------------------------------------
logic [31:0] mscom;
logic [31:0] smcom;
logic [31:0] msflg;
logic [31:0] smflg;
function automatic logic [31:0] select_reg(input logic [7:0] offset,
input logic [31:0] mscom_v,
input logic [31:0] smcom_v,
input logic [31:0] msflg_v,
input logic [31:0] smflg_v);
case (offset)
MSCOM_OFF: select_reg = mscom_v;
SMCOM_OFF: select_reg = smcom_v;
MSFLG_OFF: select_reg = msflg_v;
SMFLG_OFF: select_reg = smflg_v;
default: select_reg = 32'hDEAD_BEEF;
endcase
endfunction
// Per-register write arbitration: EE wins on same-register collision,
// but writes to different registers land independently.
logic ee_hits_mscom, ee_hits_smcom, ee_hits_msflg, ee_hits_smflg;
logic iop_hits_mscom, iop_hits_smcom, iop_hits_msflg, iop_hits_smflg;
assign ee_hits_mscom = ee_wr_en && (ee_addr == MSCOM_OFF);
assign ee_hits_smcom = ee_wr_en && (ee_addr == SMCOM_OFF);
assign ee_hits_msflg = ee_wr_en && (ee_addr == MSFLG_OFF);
assign ee_hits_smflg = ee_wr_en && (ee_addr == SMFLG_OFF);
assign iop_hits_mscom = iop_wr_en && (iop_addr == MSCOM_OFF);
assign iop_hits_smcom = iop_wr_en && (iop_addr == SMCOM_OFF);
assign iop_hits_msflg = iop_wr_en && (iop_addr == MSFLG_OFF);
assign iop_hits_smflg = iop_wr_en && (iop_addr == SMFLG_OFF);
always_ff @(posedge clk) begin
if (!rst_n) begin
mscom <= 32'd0;
smcom <= 32'd0;
msflg <= 32'd0;
smflg <= 32'd0;
end else begin
if (ee_hits_mscom) mscom <= ee_wr_data;
else if (iop_hits_mscom) mscom <= iop_wr_data;
if (ee_hits_smcom) smcom <= ee_wr_data;
else if (iop_hits_smcom) smcom <= iop_wr_data;
if (ee_hits_msflg) msflg <= ee_wr_data;
else if (iop_hits_msflg) msflg <= iop_wr_data;
if (ee_hits_smflg) smflg <= ee_wr_data;
else if (iop_hits_smflg) smflg <= iop_wr_data;
end
end
// ------------------------------------------------------------------
// Reads (1-cycle latency, both ports independent)
// ------------------------------------------------------------------
always_ff @(posedge clk) begin
if (!rst_n) begin
ee_rd_data <= 32'd0;
ee_rd_valid <= 1'b0;
iop_rd_data <= 32'd0;
iop_rd_valid <= 1'b0;
end else begin
ee_rd_valid <= ee_rd_en;
if (ee_rd_en)
ee_rd_data <= select_reg(ee_addr, mscom, smcom, msflg, smflg);
iop_rd_valid <= iop_rd_en;
if (iop_rd_en)
iop_rd_data <= select_reg(iop_addr, mscom, smcom, msflg, smflg);
end
end
// ------------------------------------------------------------------
// Trace emission — priority EE_wr > IOP_wr > EE_rd > IOP_rd.
// Reads emit with the data that will be delivered next cycle, keeping
// the trace line self-consistent.
// ------------------------------------------------------------------
always_ff @(posedge clk) begin
if (!rst_n) begin
ev_valid <= 1'b0;
ev_subsys <= SUBSYS_SIF;
ev_event <= EV_READ;
ev_arg0 <= 64'd0;
ev_arg1 <= 64'd0;
ev_arg2 <= 64'd0;
ev_arg3 <= 64'd0;
ev_flags <= 32'd0;
end else if (ee_wr_en) begin
ev_valid <= 1'b1;
ev_subsys <= SUBSYS_SIF;
ev_event <= EV_WRITE;
ev_arg0 <= {56'd0, ee_addr};
ev_arg1 <= {32'd0, ee_wr_data};
ev_arg2 <= SIDE_EE;
ev_arg3 <= 64'd0;
ev_flags <= 32'h0000_0001;
end else if (iop_wr_en) begin
ev_valid <= 1'b1;
ev_subsys <= SUBSYS_SIF;
ev_event <= EV_WRITE;
ev_arg0 <= {56'd0, iop_addr};
ev_arg1 <= {32'd0, iop_wr_data};
ev_arg2 <= SIDE_IOP;
ev_arg3 <= 64'd0;
ev_flags <= 32'h0000_0001;
end else if (ee_rd_en) begin
ev_valid <= 1'b1;
ev_subsys <= SUBSYS_SIF;
ev_event <= EV_READ;
ev_arg0 <= {56'd0, ee_addr};
ev_arg1 <= {32'd0, select_reg(ee_addr, mscom, smcom, msflg, smflg)};
ev_arg2 <= SIDE_EE;
ev_arg3 <= 64'd0;
ev_flags <= 32'd0;
end else if (iop_rd_en) begin
ev_valid <= 1'b1;
ev_subsys <= SUBSYS_SIF;
ev_event <= EV_READ;
ev_arg0 <= {56'd0, iop_addr};
ev_arg1 <= {32'd0, select_reg(iop_addr, mscom, smcom, msflg, smflg)};
ev_arg2 <= SIDE_IOP;
ev_arg3 <= 64'd0;
ev_flags <= 32'd0;
end else begin
ev_valid <= 1'b0;
end
end
endmodule : sif_mailbox_stub