ec82764bef
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>
270 lines
12 KiB
Systemverilog
270 lines
12 KiB
Systemverilog
// 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 && (®_wr_be))
|
||
? reg_wr_data[15:0] : 16'd0;
|
||
wire iop_intc_mask_wr_en =
|
||
reg_wr_en && wr_idx == OFFSET_1074_WIDX && (®_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
|