Files
retroDE_ps2/rtl/ee/ee_bootstrap_mmio_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

270 lines
12 KiB
Systemverilog
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// retroDE_ps2 — ee_bootstrap_mmio_stub
//
// Latched-register-file stub for the EE "bootstrap MMIO" window at
// physical `0x1F80_0000 - 0x1F80_FFFF` (64 KiB). Covers the real
// PS2 MCH (memory controller), SBUS gateway, and RDRAM init
// registers the BIOS touches very early in boot. This is the
// narrowest thing that closes the poisoned-dataflow hole found by
// chapter 7.99: before this module existed, the EE map returned
// `0xDEADBEEF` for every CPU read in this window, and the BIOS
// laundered that poison into a data structure whose later
// traversal wedged the core forever.
//
// Semantics (deliberately simple, not architecturally accurate):
// - Full window is a 16 KiB word-addressed register file; all
// registers reset/init to 0.
// - Writes latch per-byte: for each `wr_be[i]` that is asserted,
// `regs[addr[15:2]][8*i +: 8] <= wr_data[8*i +: 8]`. Untouched
// byte lanes preserve their existing value. This makes SB/SH
// write-through-this-window safe — prior chapters added SB/SH
// for BIOS progress, and without be-aware latching a sub-word
// store here would clobber the other three (or two) bytes.
// - Reads return the currently-latched value, one-cycle latency,
// matching the rest of the stub ecosystem.
// - No side effects, no per-register behavior (no ready-bit
// auto-set, no interrupt generation, no state machines).
//
// That keeps BIOS read/modify/write sequences self-consistent:
// if the BIOS reads reg X, ORs a bit, writes back, it sees the
// merged value on the next read. It does NOT emulate real
// hardware semantics (e.g. status bits that flip on their own,
// interrupt latches, FIFO behavior). If the BIOS tripwire-depends
// on any of that, it will reveal itself the same way the 0x14B4
// linked-list wedge did — via a new diagnostic signal, handled
// in a future chapter.
//
// Trace:
// Per-access event on SUBSYS_MEM with the region tag
// `REGION_EE_MISC_MMIO = 9`. arg0 is the 16-bit offset within
// the window (not the full 32-bit address — the map's own
// trace already carries the full address; the stub's finer
// trace carries the offset so downstream analysis can see
// which register was touched without having to mask). arg1 is
// the data (write data, or the value being returned on read).
// arg3 is the region constant. flags bit 0 = write.
//
// Size cost: 16384 × 32 bits ≈ 64 KiB of sim memory. Negligible.
`timescale 1ns/1ps
module ee_bootstrap_mmio_stub
import trace_pkg::*;
#(
// Ch202 — narrow "ready" return for offset 0x1814. Pre-Ch201 the
// window returned the latched register value (which initialises to
// 0); the BIOS at PC=0xBFC4FB04..FB30 polls this address waiting
// for ($read & $mask) != 0 and our zero return left it spinning.
// Default = 32'hFFFFFFFF satisfies any non-zero mask the BIOS may
// hold in $a0 — wider than a real PS2 GPUSTAT (typical idle =
// 0x1C00_0000), but the BIOS has not been observed to USE the
// value beyond the bit-test so the wider satisfaction is safe.
// A future chapter can narrow this if a side-effect is observed.
parameter logic [31:0] MMIO_1814_RDY_VALUE = 32'hFFFF_FFFF,
// Ch258 — IOP DMAC PCR realism stub. The IOP DMAC Priority Control
// Register lives at phys 0x1F8010F0 (= EE kseg1 0xBF8010F0). Real
// PS1/IOP hardware resets this to 0x07654321 (priority 1 for ch0,
// 2 for ch1, ... 7 for ch6, with bit[31:24]=0x07 as the enable
// mask). Ch218 observer captured BIOS reading this address three
// times during the Ch215 longjmp treadmill (PC=0xbfc4d2cc /
// 0xbfc4d2dc / 0xbfc4d350), all returning 0 from our latched-zero
// stub. Whether the zero return is the cause of the treadmill or
// an incidental noise read is open — Ch258's job is to flip the
// PCR to its real reset value and re-observe.
//
// This is a REALISM STUB, not a fix. We are not modelling the
// IOP DMA channel priority semantics; we are just declining to
// return poison-zero for a named hardware register with a known
// reset value. If BIOS escapes the Ch215 treadmill after this
// change, great. If it does not, Ch258 closes with "PCR was not
// the gate" and we name the next observed blocker.
parameter logic [31:0] MMIO_10F0_PCR_VALUE = 32'h0765_4321
)
(
input logic clk,
input logic rst_n,
// Write port
input logic reg_wr_en,
input logic [15:0] reg_wr_addr,
input logic [31:0] reg_wr_data,
input logic [3:0] reg_wr_be,
// Read port — 1-cycle latency, matches rest of stub ecosystem
input logic reg_rd_en,
input logic [15:0] reg_rd_addr,
output logic [31:0] reg_rd_data,
output logic reg_rd_valid,
// Ch259 / Ch260 — DIAGNOSTIC source-injection port for the named
// IOP INTC view at 0x1F801070/0x1F801074. DEFAULT IS ZERO in every
// existing instantiation (tb_ee_bootstrap_mmio.sv and
// tb_ee_core_bios_smoke.sv both tie this to 16'd0 unless the
// BIOS-long TB's +IOP_INTC_BOOT_SRC plusarg overrides it).
//
// When non-zero, each set bit is ORed into I_STAT every cycle so
// the assertion survives W1C clears (matches the "real device
// asserts the line until serviced" shape, not a one-shot pulse).
//
// This port exists ONLY as a controlled diagnostic knob. Ch259
// closed the BIOS-mmio-probe arc with the finding that single
// synthetic source bits do not break the Ch215 treadmill — the
// multi-state IOP/SBUS/kernel activity is needed instead. Any
// future use of this port should be similarly scoped (TB-driven,
// documented intent, default-zero on instantiation).
input logic [15:0] iop_intc_inject_src_i,
// 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 WORDS = 16384; // 64 KiB / 4
localparam logic [63:0] REGION_EE_MISC_MMIO = 64'd9;
logic [31:0] regs [0:WORDS-1];
initial begin
for (int i = 0; i < WORDS; i++) regs[i] = 32'd0;
end
logic [13:0] wr_idx;
logic [13:0] rd_idx;
assign wr_idx = reg_wr_addr[15:2];
assign rd_idx = reg_rd_addr[15:2];
// Per-byte write latch — honors reg_wr_be so SB/SH through this
// window preserves the untouched byte lanes instead of clobbering
// the whole 32-bit register.
always_ff @(posedge clk) begin
if (rst_n && reg_wr_en) begin
if (reg_wr_be[0]) regs[wr_idx][ 7: 0] <= reg_wr_data[ 7: 0];
if (reg_wr_be[1]) regs[wr_idx][15: 8] <= reg_wr_data[15: 8];
if (reg_wr_be[2]) regs[wr_idx][23:16] <= reg_wr_data[23:16];
if (reg_wr_be[3]) regs[wr_idx][31:24] <= reg_wr_data[31:24];
end
end
// Read — 1-cycle latency. Ch202: offset 0x1814 ignores the latched
// register and returns MMIO_1814_RDY_VALUE so the BIOS bit-test
// poll satisfies (read & mask) != 0 on the first read. Writes to
// 0x1814 still latch into regs[]; a future chapter can promote
// 0x1814 to a true read-write register if BIOS-write semantics
// matter, but the current observed behavior is read-only-status.
// Ch258 adds the same shape for offset 0x10F0 (IOP DMAC PCR).
// Ch259 promotes 0x1070 (IOP INTC I_STAT) and 0x1074 (I_MASK)
// OUT of the anonymous regfile into named INTC behavior — W1C
// on STAT writes, plain-write on MASK writes, sticky source
// injection from `iop_intc_inject_src_i`. Matches the existing
// `rtl/intc/intc_stub.sv` shape exactly so the EE-side view of
// the IOP INTC behaves like the IOP-side view does.
localparam logic [13:0] OFFSET_1814_WIDX = 14'h0605; // 0x1814 >> 2 (1541)
localparam logic [13:0] OFFSET_10F0_WIDX = 14'h043C; // 0x10F0 >> 2 (1084)
localparam logic [13:0] OFFSET_1070_WIDX = 14'h041C; // 0x1070 >> 2 (1052)
localparam logic [13:0] OFFSET_1074_WIDX = 14'h041D; // 0x1074 >> 2 (1053)
// Ch259 — named IOP INTC state. Independent of the anonymous
// regs[] (writes to 0x1070/0x1074 still update regs[] via the
// generic per-byte latch above, but reads bypass it for these
// offsets, matching the Ch202/Ch258 override pattern).
logic [15:0] iop_intc_stat_q;
logic [15:0] iop_intc_mask_q;
wire [15:0] iop_intc_stat_w1c_mask =
(reg_wr_en && wr_idx == OFFSET_1070_WIDX && (&reg_wr_be))
? reg_wr_data[15:0] : 16'd0;
wire iop_intc_mask_wr_en =
reg_wr_en && wr_idx == OFFSET_1074_WIDX && (&reg_wr_be);
always_ff @(posedge clk) begin
if (!rst_n) begin
iop_intc_stat_q <= 16'd0;
iop_intc_mask_q <= 16'd0;
end else begin
// I_STAT: W1C of cleared bits, OR'd with sticky injection.
// Assertion-wins on same-cycle W1C+source collision —
// matches `intc_stub.sv` lines ~102-110 so we don't
// swallow an interrupt that's still held.
iop_intc_stat_q <= (iop_intc_stat_q & ~iop_intc_stat_w1c_mask)
| iop_intc_inject_src_i;
if (iop_intc_mask_wr_en)
iop_intc_mask_q <= reg_wr_data[15:0];
end
end
wire [31:0] iop_intc_stat_read = {16'd0, iop_intc_stat_q | iop_intc_inject_src_i};
wire [31:0] iop_intc_mask_read = {16'd0, iop_intc_mask_q};
always_ff @(posedge clk) begin
if (!rst_n) begin
reg_rd_data <= 32'd0;
reg_rd_valid <= 1'b0;
end else begin
reg_rd_valid <= reg_rd_en;
if (reg_rd_en) begin
if (rd_idx == OFFSET_1814_WIDX)
reg_rd_data <= MMIO_1814_RDY_VALUE;
else if (rd_idx == OFFSET_10F0_WIDX)
reg_rd_data <= MMIO_10F0_PCR_VALUE;
else if (rd_idx == OFFSET_1070_WIDX)
reg_rd_data <= iop_intc_stat_read;
else if (rd_idx == OFFSET_1074_WIDX)
reg_rd_data <= iop_intc_mask_read;
else
reg_rd_data <= regs[rd_idx];
end
end
end
// Trace emission — one event per cycle, write wins on same-cycle
// collision (mirrors the rd/wr_en mutual-exclusion at the map level;
// this is defensive for mechanical safety).
always_ff @(posedge clk) begin
if (!rst_n) begin
ev_valid <= 1'b0;
ev_subsys <= SUBSYS_MEM;
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 (reg_wr_en) begin
ev_valid <= 1'b1;
ev_subsys <= SUBSYS_MEM;
ev_event <= EV_WRITE;
ev_arg0 <= {48'd0, reg_wr_addr};
ev_arg1 <= {32'd0, reg_wr_data};
ev_arg2 <= 64'd0;
ev_arg3 <= REGION_EE_MISC_MMIO;
ev_flags <= 32'h0000_0001;
end else if (reg_rd_en) begin
ev_valid <= 1'b1;
ev_subsys <= SUBSYS_MEM;
ev_event <= EV_READ;
ev_arg0 <= {48'd0, reg_rd_addr};
ev_arg1 <= (rd_idx == OFFSET_1814_WIDX)
? {32'd0, MMIO_1814_RDY_VALUE}
: (rd_idx == OFFSET_10F0_WIDX)
? {32'd0, MMIO_10F0_PCR_VALUE}
: (rd_idx == OFFSET_1070_WIDX)
? {32'd0, iop_intc_stat_read}
: (rd_idx == OFFSET_1074_WIDX)
? {32'd0, iop_intc_mask_read}
: {32'd0, regs[rd_idx]};
ev_arg2 <= 64'd0;
ev_arg3 <= REGION_EE_MISC_MMIO;
ev_flags <= 32'd0;
end else begin
ev_valid <= 1'b0;
end
end
endmodule : ee_bootstrap_mmio_stub