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:
2026-06-29 20:10:50 -04:00
commit ec82764bef
2462 changed files with 2174303 additions and 0 deletions
+91
View File
@@ -0,0 +1,91 @@
# rtl/sif
EE↔IOP subsystem interface. Matches `docs/contracts/sif.md`.
## Current contents
- `sif_mailbox_stub.sv` — minimal four-register mailbox/flag shell
(MSCOM / SMCOM / MSFLG / SMFLG). Independent EE-side and IOP-side register
ports. Directional set/clear semantics deferred; this phase only proves
that both sides observe consistent storage and that side-of-origin is
trace-visible. Per-register write arbitration: EE wins on same-register
collision, independent writes to different registers coexist.
- `sif_dma_stub.sv` — receive-side DMA endpoint. Accepts qwords from a
DMAC channel's `ep_*` port into a small internal buffer (default DEPTH=8).
Capacity-safe: `in_ready` drops when `rx_count >= DEPTH`, `full_o`
exposed for testbench observation. TB-controlled `stall_in` input for
explicit stall testing. Read port for payload verification. No consume
path yet — once full, stays full. NOT an IOP — purely a bounded receive
buffer with trace emission per accepted beat.
- `sif_mailbox_peer_stub.sv` — tiny active peer used in integration tests
to play "the IOP side" of a specific mailbox protocol. Re-armable
command-echo state machine (poll MSFLG → read MSCOM → write SMCOM →
write SMFLG → wait for TB to clear MSFLG → repeat). Refuses to re-fire
while the doorbell bit stays high, so lifecycle is explicit. Exposes
`ack_count_o` for testbench synchronisation.
Explicitly NOT an IOP core: no code execution, no BIOS bring-up, no
implicit flag clearing (re-arm is the TB's responsibility). Kept under
`rtl/sif/` precisely so it does not get misread as IOP maturity progress.
- `sif_dma_iop_ram_bridge_stub.sv` — width-adapting bridge from a 128-bit
SIF DMA endpoint to 32-bit IOP-side writes. Splits each incoming qword
into four 32-bit writes at consecutive physical addresses from
`DEST_BASE_ADDR`. Little-endian unpacking. Drives the IOP memory map's
bridge-write port (`bridge_wr_*`). In-ready drops while the bridge is
flushing a qword — natural backpressure to the DMAC.
- `sif_dma_ack_peer_stub.sv` — protocol combiner for the first combined
control+data SIF milestone. Observes a mailbox doorbell (MSFLG pending
bit) AND `sif_dma_stub.last_seen` (payload completion); only emits the
ack sequence (SMCOM=cmd + SMFLG=ACK) once both are true. Composes two
existing SIF primitives; does not fatten the plain mailbox peer with
DMA awareness.
Explicitly NOT an IOP.
- `sif_dma_ee_ram_bridge_stub.sv` — width-adapting bridge from a 32-bit
SIF DMA endpoint (IOP→EE egress) to 128-bit EE-side writes. Mirror of
`sif_dma_iop_ram_bridge_stub` in the other direction: accumulates four
consecutive 32-bit beats into a qword (little-endian), then issues
one write through the EE memory map's bridge write port. Drops
`in_ready` during the one-cycle emit for natural back-pressure.
Handles partial-quad on `in_last` via byte-enable masking. Exposes
`last_seen_o` — a level-held latch that rises when the final beat of
a transfer is accepted, so EE-side protocol combiners can gate on
"payload fully landed."
- `sif_dma_ee_ack_peer_stub.sv` — protocol combiner for the first
IOP-driven combined control+data SIF milestone. Polarity mirror of
`sif_dma_ack_peer_stub`: observes the mailbox's EE side for an IOP
doorbell (SMFLG pending bit), gates on
`sif_dma_ee_ram_bridge_stub.last_seen_o`, and only then reads SMCOM
and echoes MSCOM + MSFLG=ACK back IOP-ward. One-shot. Explicitly NOT
an EE core — purely a composition of two existing SIF primitives.
## Current status
The SIF seam is feature-complete for staged bring-up in both directions.
Storage, active peer, lifecycle/re-arm, negative-path, EE→IOP DMA, three
classes of backpressure (start / mid-transfer / full-stop), EE-driven
combined control+data gating, a reverse-direction (IOP→EE) data path
with its own stall semantics, AND the matching IOP-driven combined
control+data handshake are all proven end-to-end. Further SIF-only work
would be symmetry-chasing rather than unlocking new architectural
questions.
## Deferred follow-ons (not gaps)
These are known extension points, intentionally not pursued yet:
- **Re-armable combined control+data handshakes.** Both directions are
currently one-shot; re-arm mostly composes pieces already proven
separately. Nice-to-have.
- **Directional write-ownership + flag set/clear semantics.** Currently
both sides of the mailbox can write any register with plain replace
semantics; real PS2 has directional set/W1C rules.
- **Real EE↔IOP coordination.** Arrives once an IOP-side execution
primitive exists that can observe SIF as "IOP behaviour," not as a
peer stub.
## Scope boundary
This directory owns the SIF register shell and DMA-visible coordination.
It does **not** own:
- IOP CPU execution (`rtl/iop/`, not yet created)
- EE-side addressing / kseg stripping for SIF registers (memory-map work)
- Interrupt routing to INTC on SIF transitions (Wave 3)
+217
View File
@@ -0,0 +1,217 @@
// retroDE_ps2 — boot_install_agent_stub (Ch55 / Ch56)
//
// Minimal external producer that streams a coordinated low-RAM handler
// image into EE RAM through the SIF EE-RAM bridge. Emits 32-bit beats
// on a ready/valid handshake compatible with sif_dma_ee_ram_bridge_stub.
//
// NOT an IOP, NOT a full boot firmware. This is the thinnest possible
// stand-in for "whatever on real PS2 populates EE useg [0x80..0x1FF]
// with exception-entry + safe-return stubs before the EE starts
// faulting" (IOP→EE SIF DMA, BootROM/CDVD handoff, etc.). The point
// is to validate the transport path and the coordinated-install
// thesis, not to model the producer's identity.
//
// Payload source (Ch56):
// USE_IMAGE_FILE=0 (default) — built-in Ch54 image, hardcoded below
// USE_IMAGE_FILE=1 — $readmemh(IMAGE_FILE, payload) once
// at sim start, expects TOTAL_WORDS
// hex words
// Transport (timing, handshake, trace) is identical across both modes.
//
// Built-in image (USE_IMAGE_FILE=0):
// word[0..3] → AdES handler at useg 0x80..0x8C:
// MFC0 $26, $14 (32'h401A7000)
// ADDIU $26, $26, 4 (32'h275A0004)
// JR $26 (32'h03400008)
// RFE (32'h42000010)
// word[4..95] → 46× (JR $31; NOP) safe-return pairs covering
// useg 0x90..0x1FC.
//
// Downstream contract (matches sif_dma_ee_ram_bridge_stub upstream):
// out_valid / out_data[31:0] / out_last / out_ready
// out_last asserted on the final word. One-beat-per-cycle while
// out_ready is high.
//
// Trace:
// SUBSYS_SIF / EV_DMA_START once on go.
// SUBSYS_SIF / EV_DMA_BEAT per accepted beat.
// arg0 = word index, arg1 = word data, arg2 = MASTER_ID,
// arg3 = TOTAL_WORDS, flags bit0 = out_last.
// SUBSYS_SIF / EV_DMA_DONE once on completion.
`timescale 1ns/1ps
module boot_install_agent_stub
import trace_pkg::*;
#(
parameter int TOTAL_WORDS = 96,
parameter logic [7:0] MASTER_ID = 8'd6, // install agent
parameter bit USE_IMAGE_FILE = 1'b0, // 0: built-in ROM, 1: $readmemh
parameter string IMAGE_FILE = ""
) (
input logic clk,
input logic rst_n,
input logic go_i,
output logic out_valid,
output logic [31:0] out_data,
output logic out_last,
input logic out_ready,
output logic busy_o,
output logic done_o,
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
);
// ------------------------------------------------------------------
// Payload ROM
// ------------------------------------------------------------------
// The 4-word exception-return stub at [0..3] plus (JR $31; NOP)
// pairs filling the rest requires TOTAL_WORDS to be even and at
// least 4. Guard explicitly (Ch55 audit low-1): odd values would
// walk past the array end in the pair loop.
initial begin
if (TOTAL_WORDS < 4 || (TOTAL_WORDS & 1) != 0) begin
$fatal(1, "boot_install_agent_stub: TOTAL_WORDS must be even and >= 4, got %0d",
TOTAL_WORDS);
end
end
logic [31:0] payload [0:TOTAL_WORDS-1];
initial begin
if (USE_IMAGE_FILE) begin
if (IMAGE_FILE == "") begin
$fatal(1, "boot_install_agent_stub: USE_IMAGE_FILE=1 but IMAGE_FILE is empty");
end
$readmemh(IMAGE_FILE, payload);
end else begin
payload[0] = 32'h401A7000; // MFC0 $26, $14
payload[1] = 32'h275A0004; // ADDIU $26, $26, 4
payload[2] = 32'h03400008; // JR $26
payload[3] = 32'h42000010; // RFE (delay slot)
for (int i = 4; i < TOTAL_WORDS; i = i + 2) begin
payload[i] = 32'h03E00008; // JR $31
payload[i + 1] = 32'h00000000; // NOP
end
end
end
// ------------------------------------------------------------------
// Streaming FSM
// ------------------------------------------------------------------
typedef enum logic [1:0] {
S_IDLE = 2'd0,
S_STREAM = 2'd1,
S_DONE = 2'd2
} state_e;
state_e state;
logic [31:0] idx; // next word to emit
logic accept_beat;
assign accept_beat = out_valid && out_ready;
assign out_valid = (state == S_STREAM);
assign out_data = (state == S_STREAM) ? payload[idx[$clog2(TOTAL_WORDS)-1:0]]
: 32'd0;
assign out_last = (state == S_STREAM) && (idx == TOTAL_WORDS - 1);
assign busy_o = (state == S_STREAM);
assign done_o = (state == S_DONE);
always_ff @(posedge clk) begin
if (!rst_n) begin
state <= S_IDLE;
idx <= 32'd0;
end else begin
unique case (state)
S_IDLE: begin
if (go_i) begin
state <= S_STREAM;
idx <= 32'd0;
end
end
S_STREAM: begin
if (accept_beat) begin
if (idx == TOTAL_WORDS - 1) state <= S_DONE;
else idx <= idx + 32'd1;
end
end
S_DONE: ; // terminal
default: state <= S_IDLE;
endcase
end
end
// ------------------------------------------------------------------
// Trace
// ------------------------------------------------------------------
// START fires combinationally on the cycle the caller pulses go_i
// while we're still in S_IDLE. That cycle has out_valid=0 and
// accept_beat=0, so the event doesn't compete with a BEAT event
// in the priority if-else below (the bug pre-fix: flopping
// go_latched delayed START onto the same cycle as beat 0, dropping
// one of the two).
logic go_pulse;
assign go_pulse = (state == S_IDLE) && go_i;
logic done_edge;
state_e state_prev;
always_ff @(posedge clk) begin
if (!rst_n) state_prev <= S_IDLE;
else state_prev <= state;
end
assign done_edge = (state == S_DONE) && (state_prev != S_DONE);
always_ff @(posedge clk) begin
if (!rst_n) begin
ev_valid <= 1'b0;
ev_subsys <= SUBSYS_SIF;
ev_event <= EV_DMA_START;
ev_arg0 <= 64'd0;
ev_arg1 <= 64'd0;
ev_arg2 <= 64'd0;
ev_arg3 <= 64'd0;
ev_flags <= 32'd0;
end else if (go_pulse) begin
ev_valid <= 1'b1;
ev_subsys <= SUBSYS_SIF;
ev_event <= EV_DMA_START;
ev_arg0 <= 64'd0;
ev_arg1 <= 64'd0;
ev_arg2 <= {56'd0, MASTER_ID};
ev_arg3 <= 64'(TOTAL_WORDS);
ev_flags <= 32'd0;
end else if (accept_beat) begin
ev_valid <= 1'b1;
ev_subsys <= SUBSYS_SIF;
ev_event <= EV_DMA_BEAT;
ev_arg0 <= {32'd0, idx};
ev_arg1 <= {32'd0, out_data};
ev_arg2 <= {56'd0, MASTER_ID};
ev_arg3 <= 64'(TOTAL_WORDS);
ev_flags <= {31'd0, out_last};
end else if (done_edge) begin
ev_valid <= 1'b1;
ev_subsys <= SUBSYS_SIF;
ev_event <= EV_DMA_DONE;
ev_arg0 <= 64'(TOTAL_WORDS);
ev_arg1 <= 64'd0;
ev_arg2 <= {56'd0, MASTER_ID};
ev_arg3 <= 64'(TOTAL_WORDS);
ev_flags <= 32'd0;
end else begin
ev_valid <= 1'b0;
end
end
endmodule : boot_install_agent_stub
+181
View File
@@ -0,0 +1,181 @@
// retroDE_ps2 — sif_dma_ack_peer_stub
//
// Protocol combiner for the first combined control+data SIF milestone.
// Observes a mailbox command doorbell on one seam and the SIF DMA receive
// endpoint's payload-complete indication on the other; only issues the
// mailbox ack sequence once BOTH are true.
//
// Explicitly NOT an IOP. This module has no code execution, no bus master,
// and no capability beyond composing two existing SIF primitives. Kept
// under `rtl/sif/` with the other SIF scaffolding.
//
// Contract refs:
// docs/contracts/sif.md
//
// Layering:
// sif_mailbox_stub — storage primitive
// sif_mailbox_peer_stub — mailbox-only active peer (no DMA awareness)
// sif_dma_stub — data-plane receive endpoint
// sif_dma_ack_peer_stub — THIS module. Wires the two together.
//
// Protocol (one-shot):
// 1. EE writes MSCOM = cmd
// 2. EE writes MSFLG = CMD_PENDING_BIT (request doorbell)
// 3. DMAC transfers bounded payload into sif_dma_stub
// 4. sif_dma_stub asserts last_seen once the final beat arrives
// 5. this peer observes (MSFLG & CMD_PENDING_BIT) AND last_seen
// 6. peer reads MSCOM
// 7. peer writes SMCOM = cmd
// 8. peer writes SMFLG = CMD_ACK_BIT
// 9. terminal DONE (one-shot for this milestone)
//
// The peer does NOT clear MSFLG or SMFLG — lifecycle is the TB's
// responsibility, consistent with sif_mailbox_peer_stub's guardrail.
//
// Ports connect to:
// obs_* → sif_mailbox_stub iop_rd_* (peer reads MSFLG, then MSCOM)
// resp_* → sif_mailbox_stub iop_wr_* (peer writes SMCOM, then SMFLG)
// payload_complete ← sif_dma_stub.last_seen
`timescale 1ns/1ps
module sif_dma_ack_peer_stub
#(
parameter logic [7:0] MSCOM_OFF = 8'h00,
parameter logic [7:0] SMCOM_OFF = 8'h10,
parameter logic [7:0] MSFLG_OFF = 8'h20,
parameter logic [7:0] SMFLG_OFF = 8'h30,
parameter logic [31:0] CMD_PENDING_BIT = 32'h0000_0001,
parameter logic [31:0] CMD_ACK_BIT = 32'h0000_0002
) (
input logic clk,
input logic rst_n,
// Mailbox observation (IOP-side read port)
output logic obs_rd_en,
output logic [7:0] obs_rd_addr,
input logic [31:0] obs_rd_data,
input logic obs_rd_valid,
// Mailbox response (IOP-side write port)
output logic resp_wr_en,
output logic [7:0] resp_wr_addr,
output logic [31:0] resp_wr_data,
// Payload completion indication from sif_dma_stub (level)
input logic payload_complete,
// Status
output logic done_o,
output logic [31:0] ack_count_o
);
typedef enum logic [2:0] {
S_POLL_REQ = 3'd0, // pulse rd_en for MSFLG
S_POLL_WAIT = 3'd1, // wait for rd_valid, gate on BOTH conditions
S_MSCOM_REQ = 3'd2, // pulse rd_en for MSCOM
S_MSCOM_WAIT = 3'd3, // wait for rd_valid, latch cmd
S_WRITE_SMCOM = 3'd4, // drive wr_en, addr=SMCOM, data=cmd
S_WRITE_SMFLG = 3'd5, // drive wr_en, addr=SMFLG, data=ACK
S_DONE = 3'd6 // terminal (one-shot for this milestone)
} state_e;
state_e state;
logic [31:0] latched_cmd;
// ------------------------------------------------------------------
// State machine — advance to MSCOM_REQ only when MSFLG pending is set
// AND payload_complete is observed simultaneously. This is the
// load-bearing guarantee of the whole combiner.
// ------------------------------------------------------------------
always_ff @(posedge clk) begin
if (!rst_n) begin
state <= S_POLL_REQ;
latched_cmd <= 32'd0;
end else begin
unique case (state)
S_POLL_REQ: state <= S_POLL_WAIT;
S_POLL_WAIT: begin
if (obs_rd_valid) begin
if (((obs_rd_data & CMD_PENDING_BIT) != 32'd0) &&
payload_complete)
state <= S_MSCOM_REQ;
else
state <= S_POLL_REQ; // keep polling
end
end
S_MSCOM_REQ: state <= S_MSCOM_WAIT;
S_MSCOM_WAIT: begin
if (obs_rd_valid) begin
latched_cmd <= obs_rd_data;
state <= S_WRITE_SMCOM;
end
end
S_WRITE_SMCOM: state <= S_WRITE_SMFLG;
S_WRITE_SMFLG: state <= S_DONE;
S_DONE: state <= S_DONE;
default: state <= S_POLL_REQ;
endcase
end
end
// ------------------------------------------------------------------
// Output drive (combinational, one-hot on state)
// ------------------------------------------------------------------
always_comb begin
obs_rd_en = 1'b0;
obs_rd_addr = 8'd0;
resp_wr_en = 1'b0;
resp_wr_addr = 8'd0;
resp_wr_data = 32'd0;
unique case (state)
S_POLL_REQ: begin
obs_rd_en = 1'b1;
obs_rd_addr = MSFLG_OFF;
end
S_MSCOM_REQ: begin
obs_rd_en = 1'b1;
obs_rd_addr = MSCOM_OFF;
end
S_WRITE_SMCOM: begin
resp_wr_en = 1'b1;
resp_wr_addr = SMCOM_OFF;
resp_wr_data = latched_cmd;
end
S_WRITE_SMFLG: begin
resp_wr_en = 1'b1;
resp_wr_addr = SMFLG_OFF;
resp_wr_data = CMD_ACK_BIT;
end
default: ;
endcase
end
// ------------------------------------------------------------------
// Ack bookkeeping
// ------------------------------------------------------------------
always_ff @(posedge clk) begin
if (!rst_n) begin
ack_count_o <= 32'd0;
done_o <= 1'b0;
end else if (state == S_WRITE_SMFLG) begin
// S_WRITE_SMFLG is a single-cycle state: unconditionally
// transitions to S_DONE on the next edge, so this observes
// exactly one completion.
ack_count_o <= ack_count_o + 32'd1;
done_o <= 1'b1;
end
end
endmodule : sif_dma_ack_peer_stub
+185
View File
@@ -0,0 +1,185 @@
// retroDE_ps2 — sif_dma_ee_ack_peer_stub
//
// Protocol combiner for the first reverse-direction (IOP→EE) combined
// control+data SIF milestone. Mirror of sif_dma_ack_peer_stub with
// polarity swapped to observe the IOP's doorbell from the EE side and
// respond back IOP-ward through the mailbox.
//
// Explicitly NOT an EE. This module has no code execution, no bus
// master, no capability beyond composing two existing SIF primitives.
// Kept under `rtl/sif/` with the other protocol scaffolding so it
// does not get misread as EE maturity progress.
//
// Contract refs:
// docs/contracts/sif.md
//
// Layering:
// sif_mailbox_stub — storage primitive
// sif_dma_ee_ram_bridge_stub — reverse-direction data-plane landing
// sif_dma_ee_ack_peer_stub — THIS module. Ties them together on
// the EE side.
//
// Protocol (one-shot, reverse direction):
// 1. IOP writes SMCOM = cmd (what the IOP wants to say)
// 2. IOP writes SMFLG = CMD_PENDING_BIT (doorbell IOP→EE)
// 3. IOP DMAC ch9 transfers bounded payload through the SIF egress
// bridge, which lands qwords in EE RAM. Bridge's last_seen_o rises
// on the final beat and stays high.
// 4. this peer observes (SMFLG & CMD_PENDING_BIT) AND payload_complete
// 5. peer reads SMCOM (captures the command)
// 6. peer writes MSCOM = cmd (echo back IOP-ward)
// 7. peer writes MSFLG = CMD_ACK_BIT (ack back IOP-ward)
// 8. terminal DONE (one-shot for this milestone)
//
// The peer does NOT clear SMFLG or MSFLG — lifecycle is the TB's
// responsibility, consistent with sif_mailbox_peer_stub's guardrail.
//
// Ordering guarantee: the load-bearing behaviour is that the peer will
// not advance to the ack write sequence unless BOTH the doorbell AND
// payload_complete are observed simultaneously. The milestone is about
// verifying that the ack is gated on data arriving, not just control.
//
// Ports connect to:
// obs_* → sif_mailbox_stub ee_rd_* (peer reads SMFLG, then SMCOM)
// resp_* → sif_mailbox_stub ee_wr_* (peer writes MSCOM, then MSFLG)
// payload_complete ← sif_dma_ee_ram_bridge_stub.last_seen_o
`timescale 1ns/1ps
module sif_dma_ee_ack_peer_stub
#(
parameter logic [7:0] MSCOM_OFF = 8'h00,
parameter logic [7:0] SMCOM_OFF = 8'h10,
parameter logic [7:0] MSFLG_OFF = 8'h20,
parameter logic [7:0] SMFLG_OFF = 8'h30,
parameter logic [31:0] CMD_PENDING_BIT = 32'h0000_0001,
parameter logic [31:0] CMD_ACK_BIT = 32'h0000_0002
) (
input logic clk,
input logic rst_n,
// Mailbox observation (EE-side read port)
output logic obs_rd_en,
output logic [7:0] obs_rd_addr,
input logic [31:0] obs_rd_data,
input logic obs_rd_valid,
// Mailbox response (EE-side write port)
output logic resp_wr_en,
output logic [7:0] resp_wr_addr,
output logic [31:0] resp_wr_data,
// Payload completion indication from sif_dma_ee_ram_bridge_stub (level)
input logic payload_complete,
// Status
output logic done_o,
output logic [31:0] ack_count_o
);
typedef enum logic [2:0] {
S_POLL_REQ = 3'd0, // pulse rd_en for SMFLG
S_POLL_WAIT = 3'd1, // wait for rd_valid, gate on BOTH conditions
S_SMCOM_REQ = 3'd2, // pulse rd_en for SMCOM
S_SMCOM_WAIT = 3'd3, // wait for rd_valid, latch cmd
S_WRITE_MSCOM = 3'd4, // drive wr_en, addr=MSCOM, data=cmd
S_WRITE_MSFLG = 3'd5, // drive wr_en, addr=MSFLG, data=ACK
S_DONE = 3'd6 // terminal (one-shot for this milestone)
} state_e;
state_e state;
logic [31:0] latched_cmd;
// ------------------------------------------------------------------
// State machine — advance to SMCOM_REQ only when SMFLG pending is set
// AND payload_complete is observed simultaneously. This is the
// load-bearing guarantee of the whole combiner.
// ------------------------------------------------------------------
always_ff @(posedge clk) begin
if (!rst_n) begin
state <= S_POLL_REQ;
latched_cmd <= 32'd0;
end else begin
unique case (state)
S_POLL_REQ: state <= S_POLL_WAIT;
S_POLL_WAIT: begin
if (obs_rd_valid) begin
if (((obs_rd_data & CMD_PENDING_BIT) != 32'd0) &&
payload_complete)
state <= S_SMCOM_REQ;
else
state <= S_POLL_REQ; // keep polling
end
end
S_SMCOM_REQ: state <= S_SMCOM_WAIT;
S_SMCOM_WAIT: begin
if (obs_rd_valid) begin
latched_cmd <= obs_rd_data;
state <= S_WRITE_MSCOM;
end
end
S_WRITE_MSCOM: state <= S_WRITE_MSFLG;
S_WRITE_MSFLG: state <= S_DONE;
S_DONE: state <= S_DONE;
default: state <= S_POLL_REQ;
endcase
end
end
// ------------------------------------------------------------------
// Output drive (combinational, one-hot on state)
// ------------------------------------------------------------------
always_comb begin
obs_rd_en = 1'b0;
obs_rd_addr = 8'd0;
resp_wr_en = 1'b0;
resp_wr_addr = 8'd0;
resp_wr_data = 32'd0;
unique case (state)
S_POLL_REQ: begin
obs_rd_en = 1'b1;
obs_rd_addr = SMFLG_OFF;
end
S_SMCOM_REQ: begin
obs_rd_en = 1'b1;
obs_rd_addr = SMCOM_OFF;
end
S_WRITE_MSCOM: begin
resp_wr_en = 1'b1;
resp_wr_addr = MSCOM_OFF;
resp_wr_data = latched_cmd;
end
S_WRITE_MSFLG: begin
resp_wr_en = 1'b1;
resp_wr_addr = MSFLG_OFF;
resp_wr_data = CMD_ACK_BIT;
end
default: ;
endcase
end
// ------------------------------------------------------------------
// Ack bookkeeping
// ------------------------------------------------------------------
always_ff @(posedge clk) begin
if (!rst_n) begin
ack_count_o <= 32'd0;
done_o <= 1'b0;
end else if (state == S_WRITE_MSFLG) begin
ack_count_o <= ack_count_o + 32'd1;
done_o <= 1'b1;
end
end
endmodule : sif_dma_ee_ack_peer_stub
+202
View File
@@ -0,0 +1,202 @@
// retroDE_ps2 — sif_dma_ee_ram_bridge_stub
//
// Width-adapting bridge from a 32-bit SIF DMA endpoint (IOP→EE egress)
// to the 128-bit EE memory map. Accumulates four incoming 32-bit beats
// into a qword and issues one qword write through ee_memory_map_stub's
// bridge write port.
//
// Mirror of sif_dma_iop_ram_bridge_stub, but in the other direction
// (words → qword, EE-side landing).
//
// Contract refs:
// docs/contracts/sif.md (DMA-linked data movement endpoints)
// docs/contracts/memory.md (EE RAM is 128-bit qword-aligned)
//
// Handshake (upstream, from DMAC ep_* port or equivalent):
// in_valid / in_data[31:0] / in_last / in_ready
// Bridge asserts in_ready while it's accumulating (up to the 3rd beat
// of a quad, inclusive). It drops in_ready during the one-cycle emit
// that follows the 4th beat, so the DMAC naturally stalls with
// back-pressure for a single cycle between qwords.
//
// Handshake (downstream, to ee_memory_map_stub bridge-write port):
// bridge_wr_en / bridge_wr_addr[31:0] / bridge_wr_data[127:0] /
// bridge_wr_be[15:0] / bridge_master_id[7:0]
//
// Data layout (little-endian):
// beat 0 → bridge_wr_data[31:0]
// beat 1 → bridge_wr_data[63:32]
// beat 2 → bridge_wr_data[95:64]
// beat 3 → bridge_wr_data[127:96]
// qword address advances DEST_BASE_ADDR by 16 per emit.
//
// Partial quad on in_last:
// If `in_last` arrives before the 4th beat of a quad, the bridge
// emits the partial qword with wr_be masked to cover only the bytes
// that were actually accepted. Not exercised by the current TB (BCR
// is chosen to be a multiple of 4), but kept defensively.
//
// Payload-complete indication (last_seen_o):
// Level-held output, set when `in_last && accept_beat` fires on the
// upstream handshake. Intended for EE-side protocol combiners that
// need to gate an ack on "payload fully moved" independently of when
// the IOP posted a control doorbell. Latch stays high until reset —
// this mirrors sif_dma_stub.last_seen.
//
// Parameters:
// DEST_BASE_ADDR — byte offset where the first qword lands. Advances
// by 16 per emit for the life of the transfer.
// MASTER_ID — bridge's identity for MEM / EE-map trace attribution
// (default 5 = SIF EE-side bridge).
//
// Non-goals:
// - multiple in-flight qwords
// - arbitration against other bridge writers on the EE map's write path
`timescale 1ns/1ps
module sif_dma_ee_ram_bridge_stub
#(
parameter logic [31:0] DEST_BASE_ADDR = 32'h0000_0000,
parameter logic [7:0] MASTER_ID = 8'd5
) (
input logic clk,
input logic rst_n,
// Upstream (DMAC endpoint side)
input logic in_valid,
input logic [31:0] in_data,
input logic in_last,
output logic in_ready,
// Downstream (EE map bridge-write port)
output logic bridge_wr_en,
output logic [31:0] bridge_wr_addr,
output logic [127:0] bridge_wr_data,
output logic [15:0] bridge_wr_be,
output logic [7:0] bridge_master_id,
// Payload-complete indication (level, latched). Consumers gate on
// "full payload landed" without needing to count beats.
output logic last_seen_o,
// Ch239 — single-cycle "rewind" pulse. When asserted (and the
// bridge is idle in S_ACCUM with no beat in flight), the running
// `wr_offset` returns to 0 so the NEXT emit lands at
// DEST_BASE_ADDR. Lets a producer that wants single-slot buffer
// semantics (e.g. a libpad-style pad packet) overwrite the same
// 16-byte slot on every transfer instead of streaming forward.
// Existing producers that don't need this leave it tied to 1'b0
// and the bridge keeps its streaming behaviour exactly as before.
// Pulse must be asserted between transfers; firing mid-transfer
// (`state==S_EMIT` or `pos != 0`) is illegal and logged as a
// sim-only `$error` (no defensive RTL gating — keeps the path
// single-purpose). See `docs/contracts/sio2_pad.md` Ch239.
input logic rewind_i = 1'b0
);
typedef enum logic [0:0] {
S_ACCUM = 1'b0,
S_EMIT = 1'b1
} state_e;
state_e state;
logic [127:0] acc_data;
logic [15:0] acc_be;
logic [1:0] pos; // 0..3 within qword
logic [31:0] wr_offset; // running byte offset
assign in_ready = (state == S_ACCUM);
assign bridge_master_id = MASTER_ID;
logic accept_beat;
assign accept_beat = in_valid && in_ready;
// ------------------------------------------------------------------
// Accumulator / state machine
// ------------------------------------------------------------------
always_ff @(posedge clk) begin
if (!rst_n) begin
state <= S_ACCUM;
acc_data <= 128'd0;
acc_be <= 16'd0;
pos <= 2'd0;
wr_offset <= 32'd0;
end else begin
// Ch239 — between-transfer rewind. Resets only the
// streaming offset; `acc_data`/`acc_be`/`pos` are
// already 0 after every emit's tail. Misuse (rewind
// pulse during a transfer) is reported via sim $error
// below; the RTL still applies the rewind because the
// guard would otherwise hide producer-side bugs.
if (rewind_i) wr_offset <= 32'd0;
unique case (state)
S_ACCUM: begin
if (accept_beat) begin
// Place the incoming word in slot `pos` and mark
// its four bytes enabled.
acc_data[pos*32 +: 32] <= in_data;
acc_be[pos*4 +: 4] <= 4'b1111;
if (pos == 2'd3 || in_last) begin
state <= S_EMIT;
end else begin
pos <= pos + 2'd1;
end
end
end
S_EMIT: begin
// Single-cycle emit; bridge_wr_en is combinationally
// tied to state. Advance qword offset, reset slot /
// accumulator for the next quad. The Ch239 rewind
// above runs first, so a `rewind_i` pulse coincident
// with an emit cycle leaves wr_offset at 0 (no +16
// increment) — but that combination is the illegal
// "rewind mid-transfer" case and the $error below
// catches it for the producer to fix.
wr_offset <= wr_offset + 32'd16;
acc_data <= 128'd0;
acc_be <= 16'd0;
pos <= 2'd0;
state <= S_ACCUM;
end
default: state <= S_ACCUM;
endcase
end
end
`ifndef SYNTHESIS
// Misuse detector — `rewind_i` while a transfer is in flight is
// a producer-side bug. Caught here so the path stays clean.
always_ff @(posedge clk) begin
if (rst_n && rewind_i && (state != S_ACCUM || pos != 2'd0)) begin
$error("[sif_dma_ee_ram_bridge_stub] illegal rewind_i mid-transfer (state=%0d pos=%0d)",
state, pos);
end
end
`endif
// ------------------------------------------------------------------
// Downstream write-port drive (combinational on state)
// ------------------------------------------------------------------
assign bridge_wr_en = (state == S_EMIT);
assign bridge_wr_addr = DEST_BASE_ADDR + wr_offset;
assign bridge_wr_data = acc_data;
assign bridge_wr_be = acc_be;
// ------------------------------------------------------------------
// last_seen_o: set once the upstream asserts in_last on a beat that
// is actually accepted. Level-held until reset.
// ------------------------------------------------------------------
always_ff @(posedge clk) begin
if (!rst_n) last_seen_o <= 1'b0;
else if (accept_beat && in_last) last_seen_o <= 1'b1;
end
endmodule : sif_dma_ee_ram_bridge_stub
+139
View File
@@ -0,0 +1,139 @@
// retroDE_ps2 — sif_dma_iop_ram_bridge_stub
//
// Width-adapting bridge from a 128-bit SIF DMA endpoint to the 32-bit
// IOP memory map. Splits each incoming qword into four 32-bit writes at
// consecutive physical addresses starting from DEST_BASE_ADDR.
//
// First real coupling between the SIF data-plane and the IOP side. NOT an
// IOP, not a DMAC, not a peer — just a width/ordering adapter.
//
// Contract refs:
// docs/contracts/sif.md (DMA-linked data movement endpoints)
// docs/contracts/iop.md (IOP-local RAM/I/O decode; writes land
// through the IOP memory map)
//
// Handshake (upstream, from DMAC ep_* port or equivalent):
// in_valid / in_data[127:0] / in_last / in_ready
// Bridge asserts in_ready only while idle. During the four-write
// expansion of a qword, in_ready drops — natural backpressure onto
// whatever's producing qwords.
//
// Handshake (downstream, to iop_memory_map_stub's bridge-write port):
// bridge_wr_en / bridge_wr_addr[31:0] / bridge_wr_data[31:0] /
// bridge_wr_be[3:0] / bridge_master_id[7:0]
// Addresses are physical (no kseg stripping) — the IOP map must treat
// this port's addresses differently from its CPU-side port.
//
// Data layout:
// Little-endian unpacking: in_data[31:0] -> DEST_BASE+0
// in_data[63:32] -> DEST_BASE+4
// in_data[95:64] -> DEST_BASE+8
// in_data[127:96] -> DEST_BASE+12
// Subsequent qwords append: DEST_BASE+16, +20, +24, +28, ...
//
// Parameters:
// DEST_BASE_ADDR — where the bridge starts writing. Persistent across
// the life of the transfer; would become a register in
// a later wave where software programs the target.
// MASTER_ID — bridge's identity in MEM / IOP traces (default 3,
// distinct from EE IFETCH=0, DMAC=1, IOP_CPU=2).
//
// Non-goals:
// - multiple in-flight qwords
// - ack back upstream beyond in_ready / in_last observation
// - byte-enable variation per write (all writes are full 32-bit)
// - arbitration against other masters on the map's write path
`timescale 1ns/1ps
module sif_dma_iop_ram_bridge_stub
#(
parameter logic [31:0] DEST_BASE_ADDR = 32'h0000_0000,
parameter logic [7:0] MASTER_ID = 8'd3
) (
input logic clk,
input logic rst_n,
// Upstream (DMAC endpoint side)
input logic in_valid,
input logic [127:0] in_data,
input logic in_last,
output logic in_ready,
// Downstream (IOP map bridge-write port)
output logic bridge_wr_en,
output logic [31:0] bridge_wr_addr,
output logic [31:0] bridge_wr_data,
output logic [3:0] bridge_wr_be,
output logic [7:0] bridge_master_id
);
typedef enum logic [1:0] {
S_IDLE = 2'd0,
S_WRITE = 2'd1
} state_e;
state_e state;
logic [127:0] latched_qword;
logic [1:0] beat_index; // 0..3 across the 4 writes
logic [31:0] wr_offset; // running byte offset
assign in_ready = (state == S_IDLE);
assign bridge_master_id = MASTER_ID;
logic accept_new_qword;
assign accept_new_qword = in_valid && in_ready;
// ------------------------------------------------------------------
// State machine
// ------------------------------------------------------------------
always_ff @(posedge clk) begin
if (!rst_n) begin
state <= S_IDLE;
latched_qword <= 128'd0;
beat_index <= 2'd0;
wr_offset <= 32'd0;
end else begin
unique case (state)
S_IDLE: begin
if (accept_new_qword) begin
latched_qword <= in_data;
beat_index <= 2'd0;
state <= S_WRITE;
end
end
S_WRITE: begin
// Each cycle here drives one 32-bit write. After the
// fourth, go idle. wr_offset advances per write.
wr_offset <= wr_offset + 32'd4;
if (beat_index == 2'd3) begin
state <= S_IDLE;
beat_index <= 2'd0;
end else begin
beat_index <= beat_index + 2'd1;
end
end
default: state <= S_IDLE;
endcase
end
end
// ------------------------------------------------------------------
// Downstream write-port drive (combinational on state)
// ------------------------------------------------------------------
// Indexed part-select picks the 32-bit slice for the current beat.
// Avoids the constant-select-in-always_comb pattern that trips
// portability warnings on some simulators.
logic [31:0] beat_data;
assign beat_data = latched_qword[beat_index*32 +: 32];
assign bridge_wr_en = (state == S_WRITE);
assign bridge_wr_addr = DEST_BASE_ADDR + wr_offset;
assign bridge_wr_data = beat_data;
assign bridge_wr_be = 4'b1111; // full-word writes only
endmodule : sif_dma_iop_ram_bridge_stub
+154
View File
@@ -0,0 +1,154 @@
// retroDE_ps2 — sif_dma_stub
//
// Minimal SIF DMA receive-side endpoint. First data-plane step on the SIF
// seam. NOT an IOP — this is a bounded receive buffer that accepts qwords
// from a DMAC channel and exposes them to the TB via a small read port.
// No IOP CPU, no live peer logic, no directional policy beyond "incoming
// qwords land in sequential slots."
//
// Contract refs:
// docs/contracts/sif.md (DMA-linked data movement endpoints)
//
// Receive interface (connects to DMAC's ep_* endpoint):
// in_valid / in_data / in_last / in_ready
// One-cycle accept per beat when in_ready is high. in_last observed
// alongside the final qword of a transfer.
//
// Read interface (TB-side verification):
// rd_en pulses with rd_idx; rd_data / rd_valid return the stored qword
// one cycle later.
//
// Stall input:
// stall_in (level) forces in_ready low while asserted. Used by the
// negative-path test to prove that a not-ready receiver does not let
// the DMAC spuriously complete.
//
// Buffer:
// Small internal array (DEPTH qwords). Full detection is tracked from
// `rx_count`: once `rx_count >= DEPTH` the buffer is full and `in_ready`
// drops so the DMAC stalls in ACTIVE_SEND. No silent wrap. There is no
// consume path yet — once full, the buffer stays full (intentional for
// the current scope). `full_o` is exposed for testbench observation.
//
// Trace:
// One SIF EV_WRITE per accepted beat (one event per cycle).
// arg0 = slot index into the receive buffer
// arg1 = data[63:0] (low half)
// arg2 = source id (hard-wired to 8'd1 = DMAC for Wave 3)
// arg3 = 0
// flags bit 0 = in_last value for this beat
// flags bit 1 = 1 (distinguishes DMA-receive writes from mailbox writes
// if both subsystems are ever instantiated together)
`timescale 1ns/1ps
module sif_dma_stub
import trace_pkg::*;
#(
parameter int DEPTH = 8 // max qwords buffered
) (
input logic clk,
input logic rst_n,
// DMAC-facing receive
input logic in_valid,
input logic [127:0] in_data,
input logic in_last,
output logic in_ready,
// TB verification read port
input logic rd_en,
input logic [$clog2(DEPTH)-1:0] rd_idx,
output logic [127:0] rd_data,
output logic rd_valid,
// Negative-path control
input logic stall_in,
// Status
output logic [31:0] rx_count, // monotonic accepted-beat count
output logic last_seen, // sticky: in_last observed
output logic full_o, // buffer full, in_ready=0
// 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 int IDX_W = $clog2(DEPTH);
logic [127:0] buf_mem [0:DEPTH-1];
logic [IDX_W-1:0] wr_ptr;
logic beat_accepted;
assign full_o = (rx_count >= DEPTH);
assign in_ready = !stall_in && !full_o;
assign beat_accepted = in_valid && in_ready;
// ------------------------------------------------------------------
// Receive path
// ------------------------------------------------------------------
always_ff @(posedge clk) begin
if (!rst_n) begin
wr_ptr <= '0;
rx_count <= 32'd0;
last_seen <= 1'b0;
for (int i = 0; i < DEPTH; i++) buf_mem[i] <= 128'd0;
end else if (beat_accepted) begin
buf_mem[wr_ptr] <= in_data;
wr_ptr <= wr_ptr + IDX_W'(1);
rx_count <= rx_count + 32'd1;
if (in_last) last_seen <= 1'b1;
end
end
// ------------------------------------------------------------------
// Read port (1-cycle latency)
// ------------------------------------------------------------------
always_ff @(posedge clk) begin
if (!rst_n) begin
rd_data <= 128'd0;
rd_valid <= 1'b0;
end else begin
rd_valid <= rd_en;
if (rd_en) rd_data <= buf_mem[rd_idx];
end
end
// ------------------------------------------------------------------
// Trace — one event per accepted beat
// ------------------------------------------------------------------
always_ff @(posedge clk) begin
if (!rst_n) begin
ev_valid <= 1'b0;
ev_subsys <= SUBSYS_SIF;
ev_event <= EV_WRITE;
ev_arg0 <= 64'd0;
ev_arg1 <= 64'd0;
ev_arg2 <= 64'd0;
ev_arg3 <= 64'd0;
ev_flags <= 32'd0;
end else if (beat_accepted) begin
ev_valid <= 1'b1;
ev_subsys <= SUBSYS_SIF;
ev_event <= EV_WRITE;
ev_arg0 <= {{(64-IDX_W){1'b0}}, wr_ptr};
ev_arg1 <= in_data[63:0];
ev_arg2 <= 64'd1; // DMAC
ev_arg3 <= 64'd0;
ev_flags <= {30'd0, 1'b1, in_last}; // bit1=DMA, bit0=in_last
end else begin
ev_valid <= 1'b0;
end
end
endmodule : sif_dma_stub
+184
View File
@@ -0,0 +1,184 @@
// retroDE_ps2 — sif_mailbox_peer_stub
//
// Re-armable active peer for the SIF mailbox. Second step on the two-actor
// coordination track (A'' — lifecycle). Observes one mailbox/flag pattern
// and responds with a known acknowledgement pattern. NOT an IOP — does not
// execute code, does not boot anything, does not claim to be a CPU.
//
// Contract refs:
// docs/contracts/sif.md (mailbox/flag-only SIF stub)
// docs/stub_module_plan.md (Wave 2 SIF track)
//
// Canonical command-echo protocol:
// 1. EE writes MSCOM = cmd
// 2. EE writes MSFLG = CMD_PENDING_BIT (doorbell rising edge)
// 3. peer polls MSFLG; when it sees CMD_PENDING_BIT set AND it has not
// already responded to the current request, it reads MSCOM
// 4. peer writes SMCOM = <the cmd it just read>
// 5. peer writes SMFLG = CMD_ACK_BIT
// 6. peer latches `responded` and resumes polling; it will NOT respond
// again until the TB (or EE) clears CMD_PENDING_BIT in MSFLG
// 7. when the peer observes CMD_PENDING_BIT cleared, `responded` clears
// and the next rising edge of CMD_PENDING_BIT triggers a fresh echo
//
// The peer still does NOT clear any mailbox state itself. Re-arm is the
// TB's responsibility; the peer just refuses to double-fire while the
// doorbell bit is still high.
//
// Ports connect directly to sif_mailbox_stub's IOP-side register port:
// obs_* → mailbox iop_rd_* (peer reads MSFLG then MSCOM)
// resp_* → mailbox iop_wr_* (peer writes SMCOM then SMFLG)
//
// All peer activity is visible through the mailbox's own trace output
// (side_id=IOP=1). The peer does not emit its own trace; `ack_count_o`
// provides a testbench synchronisation point.
`timescale 1ns/1ps
module sif_mailbox_peer_stub
#(
parameter logic [7:0] MSCOM_OFF = 8'h00,
parameter logic [7:0] SMCOM_OFF = 8'h10,
parameter logic [7:0] MSFLG_OFF = 8'h20,
parameter logic [7:0] SMFLG_OFF = 8'h30,
parameter logic [31:0] CMD_PENDING_BIT = 32'h0000_0001,
parameter logic [31:0] CMD_ACK_BIT = 32'h0000_0002
) (
input logic clk,
input logic rst_n,
// Observation — connects to mailbox iop_rd_*
output logic obs_rd_en,
output logic [7:0] obs_rd_addr,
input logic [31:0] obs_rd_data,
input logic obs_rd_valid,
// Response — connects to mailbox iop_wr_*
output logic resp_wr_en,
output logic [7:0] resp_wr_addr,
output logic [31:0] resp_wr_data,
// Status
output logic done_o, // latched high after the first ack
output logic [31:0] ack_count_o // monotonic count of completed acks
);
typedef enum logic [2:0] {
S_POLL_REQ = 3'd0, // drive rd_en for MSFLG
S_POLL_WAIT = 3'd1, // wait for obs_rd_valid, decide
S_MSCOM_REQ = 3'd2, // drive rd_en for MSCOM
S_MSCOM_WAIT = 3'd3, // wait for obs_rd_valid, latch cmd
S_WRITE_SMCOM = 3'd4, // drive wr_en, addr=SMCOM, data=cmd
S_WRITE_SMFLG = 3'd5 // drive wr_en, addr=SMFLG, data=ACK
} state_e;
state_e state;
logic [31:0] latched_cmd;
logic responded; // peer has already acked the current
// doorbell assertion; suppresses re-fire
// until the doorbell is observed low
// ------------------------------------------------------------------
// State machine
// ------------------------------------------------------------------
always_ff @(posedge clk) begin
if (!rst_n) begin
state <= S_POLL_REQ;
latched_cmd <= 32'd0;
responded <= 1'b0;
end else begin
unique case (state)
S_POLL_REQ: state <= S_POLL_WAIT;
S_POLL_WAIT: begin
if (obs_rd_valid) begin
if (responded) begin
// Waiting for the TB to clear CMD_PENDING_BIT
// before we arm again.
if ((obs_rd_data & CMD_PENDING_BIT) == 32'd0)
responded <= 1'b0;
state <= S_POLL_REQ;
end else begin
if ((obs_rd_data & CMD_PENDING_BIT) != 32'd0)
state <= S_MSCOM_REQ;
else
state <= S_POLL_REQ;
end
end
end
S_MSCOM_REQ: state <= S_MSCOM_WAIT;
S_MSCOM_WAIT: begin
if (obs_rd_valid) begin
latched_cmd <= obs_rd_data;
state <= S_WRITE_SMCOM;
end
end
S_WRITE_SMCOM: state <= S_WRITE_SMFLG;
S_WRITE_SMFLG: begin
responded <= 1'b1; // refuse to re-fire until MSFLG
// clears
state <= S_POLL_REQ;
end
default: state <= S_POLL_REQ;
endcase
end
end
// ------------------------------------------------------------------
// Output drive (combinational, one-hot on state)
// ------------------------------------------------------------------
always_comb begin
obs_rd_en = 1'b0;
obs_rd_addr = 8'd0;
resp_wr_en = 1'b0;
resp_wr_addr = 8'd0;
resp_wr_data = 32'd0;
unique case (state)
S_POLL_REQ: begin
obs_rd_en = 1'b1;
obs_rd_addr = MSFLG_OFF;
end
S_MSCOM_REQ: begin
obs_rd_en = 1'b1;
obs_rd_addr = MSCOM_OFF;
end
S_WRITE_SMCOM: begin
resp_wr_en = 1'b1;
resp_wr_addr = SMCOM_OFF;
resp_wr_data = latched_cmd;
end
S_WRITE_SMFLG: begin
resp_wr_en = 1'b1;
resp_wr_addr = SMFLG_OFF;
resp_wr_data = CMD_ACK_BIT;
end
default: ;
endcase
end
// ------------------------------------------------------------------
// Ack bookkeeping
// ------------------------------------------------------------------
always_ff @(posedge clk) begin
if (!rst_n) begin
ack_count_o <= 32'd0;
done_o <= 1'b0;
end else if (state == S_WRITE_SMFLG) begin
// S_WRITE_SMFLG is a single-cycle state: the state machine
// unconditionally transitions to S_POLL_REQ on the next edge,
// so this branch is observed exactly once per completed ack.
ack_count_o <= ack_count_o + 32'd1;
done_o <= 1'b1;
end
end
endmodule : sif_mailbox_peer_stub
+230
View File
@@ -0,0 +1,230 @@
// 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