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

203 lines
8.1 KiB
Systemverilog

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