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:
@@ -0,0 +1,222 @@
|
||||
# rtl/ee
|
||||
|
||||
Emotion Engine-side RTL. Matches `docs/contracts/ee.md`.
|
||||
|
||||
## Current contents
|
||||
|
||||
- `ee_fetch_stub.sv` — minimal sequential fetcher from the early waves.
|
||||
On reset, PC = BIOS reset vector (0xBFC00000). Each cycle while
|
||||
`enable` is high, issues a read at PC and advances PC += 4. No
|
||||
decode, no branches, no exceptions. Emits `EV_RESET` once at reset
|
||||
exit and `EV_IFETCH` for each returned response. Retained for the
|
||||
Milestone-B golden-reference comparison.
|
||||
- `ee_core_stub.sv` — **first real EE instruction-decoding core.**
|
||||
Structural mirror of `iop_core_stub`: same multi-cycle FSM, same
|
||||
R3000 subset (LUI/ORI/ADDIU/LW/SW/BEQ/BNE/J/JR/NOP/SYSCALL/MFC0/MTC0/
|
||||
RFE), same branch-delay-slot discipline, same minimal COP0 +
|
||||
exception entry, same `STRICT_UNSUPPORTED` trap gate. Separate file
|
||||
from the IOP core because the EE is fundamentally an R5900 and will
|
||||
eventually need 64-bit registers, COP1/COP2, VU-side plumbing the
|
||||
IOP will never grow. Emits traces under `SUBSYS_EE` (vs.
|
||||
`SUBSYS_IOP` for the IOP core).
|
||||
|
||||
## Current status
|
||||
|
||||
The EE side has a first real execution primitive (`ee_core_stub`) and
|
||||
runs hand-assembled bootstraps from the shared BIOS ROM window. The
|
||||
IOP side is ahead — it has DMAC ch9 data path, real interrupt
|
||||
exception entry, BIOS reset, and strict-mode BIOS smoke bring-up. The
|
||||
EE side's next natural growth (in roughly this order) is:
|
||||
|
||||
1. ~~CPU-side LW/SW to EE RAM.~~ **Done** (`tb_ee_core_memops`). EE
|
||||
memory map now routes CPU 32-bit reads and writes into the 128-bit
|
||||
`ee_ram_stub` with lane-select on reads and byte-enable masking on
|
||||
writes. CPU wins over DMAC on same-cycle RAM-read collisions and
|
||||
over the SIF egress bridge on RAM-write collisions.
|
||||
2. ~~EE DMAC register access from the core.~~ **Done**
|
||||
(`tb_ee_core_dmac`, `tb_ee_core_dmac_poll`). Chapter 3 added the
|
||||
write-side: EE map decodes a CPU write at `phys[28:12] ==
|
||||
17'h1_000A` (0x1000_A000-0x1000_AFFF, ch2 GIF) and routes it
|
||||
through a new `ee_dmac_ch2_wr_*` port into `dmac_reg_stub`. The
|
||||
EE core programs MADR/QWC/CHCR via SW; the DMAC fetches from EE
|
||||
RAM through the map's `dmac_rd_*` port and completes with real
|
||||
DMA_START/BEAT/DONE events. Chapter 4 added the read-side:
|
||||
`dmac_reg_stub` grew a `reg_rd_*` surface (CHCR/MADR/QWC/TADR +
|
||||
DONE_COUNT monotonic counter at 0x40), and the EE map forwards
|
||||
CPU reads in the same DMAC window via a new `ee_dmac_ch2_rd_*`
|
||||
port. The core polls CHCR.start until the DMAC clears it, then
|
||||
reads DONE_COUNT and writes the witness to RAM — no more fixed
|
||||
NOP padding.
|
||||
3. ~~EE INTC + exception entry.~~ **Done** (`tb_ee_core_dmac_intc`).
|
||||
EE map now decodes the EE INTC register window at `phys[28:12] ==
|
||||
17'h1_000F` (0x1000_F000/0x1000_F010 for STAT/MASK) and carries
|
||||
both directions through new `ee_intc_{wr,rd}_*` ports. An
|
||||
`intc_stub` instance on the EE side latches
|
||||
`dmac_reg_stub.irq_completion_o` and drives `ee_core_stub.cpu_irq`
|
||||
(which feeds `cause_ip[2]`). Bootstrap enables interrupts
|
||||
(Status = IEc | IM[2]), programs INTC_MASK, kicks the DMAC, and
|
||||
waits on DONE_COUNT; a RAM-resident ISR at `EXC_VECTOR=0x80` acks
|
||||
INTC_STAT via W1C, MFC0 EPC, JR + RFE. Core takes exactly one
|
||||
exception + one RFE, strictly after DMA_DONE.
|
||||
4. ~~EE-side strict BIOS smoke.~~ **Done** (`tb_ee_core_bios_smoke`).
|
||||
EE mirror of the IOP smoke harness: `ee_core_stub` instantiated
|
||||
with `STRICT_UNSUPPORTED=1'b1`; synthetic CI bootstrap ends in an
|
||||
`AND` (SPECIAL func 0x24) that the core doesn't decode, so
|
||||
`trap_o`/`trap_pc_o`/`trap_instr_o` fire and halt the core loudly.
|
||||
Swap in a real BIOS via `make tb_ee_core_bios_smoke
|
||||
BIOS=/path/to/bios.hex` (plusarg-driven `$readmemh` into
|
||||
`u_bios.mem`, same convention as the IOP target). Output line
|
||||
includes an inline mnemonic decoder so the iteration loop (drop
|
||||
in BIOS, read output, add the missing opcode) works without a
|
||||
separate disassembler.
|
||||
5. **Widen the core opcode set, driven by real-BIOS smoke.** The
|
||||
iteration loop is live: drop a BIOS dump in via
|
||||
`make tb_ee_core_bios_smoke BIOS=...`, read `trap_instr` +
|
||||
`mnemonic` from the output, implement the op, re-run. Progress
|
||||
so far (each step landed a dedicated coverage TB and kept
|
||||
full_checks green):
|
||||
- **SLTI / SLTIU** (I-type compare, opcodes 0x0A / 0x0B). First
|
||||
real-BIOS trip at 0xBFC0_0008. TB: `tb_ee_core_slti`.
|
||||
- **ADDI** (opcode 0x08). Implemented as ADDIU (no overflow
|
||||
trap — real BIOS doesn't emit ADDI where overflow could
|
||||
actually happen). TB: `tb_ee_core_addi`.
|
||||
- **ANDI** (opcode 0x0C, zero-extended). TB: `tb_ee_core_andi`.
|
||||
- **AND / OR / XOR / NOR** (SPECIAL R-type logic family, func
|
||||
0x24-0x27; destination = rd). Batched because they share the
|
||||
R-type ALU plumbing. TB: `tb_ee_core_rtype_logic`.
|
||||
- **SB** (opcode 0x28, byte store with lane broadcast +
|
||||
one-hot byte-enable on the map write bus). TB:
|
||||
`tb_ee_core_sb`. Unlocked a 1500-instruction stretch
|
||||
(retired=180 → 1704).
|
||||
- **LB** (opcode 0x20, sign-extended byte load via
|
||||
`map_rd_data` lane extraction + 24-bit sign-extend in
|
||||
`S_MEM_WAIT`). TB: `tb_ee_core_lb`.
|
||||
- **JAL** (opcode 0x03, jump-and-link; writes `$31 = pc+8`).
|
||||
TB: `tb_ee_core_jal`.
|
||||
- **ADDU / SUBU** (SPECIAL R-type arith, func 0x21 / 0x23).
|
||||
Batched, share R-type ALU. TB: `tb_ee_core_rtype_addu`.
|
||||
Codex pre-approved the grouping.
|
||||
- **SLT / SLTU** (SPECIAL R-type compare, func 0x2A / 0x2B).
|
||||
Batched with the R-type ALU; register-form pair of
|
||||
SLTI/SLTIU. TB: `tb_ee_core_slt`. Unlocked a 5700-
|
||||
instruction stretch (retired=1717 → 7385).
|
||||
- **LH / LHU** (opcodes 0x21 / 0x25, halfword load with sign-
|
||||
and zero-extension respectively). Batched — same lane-
|
||||
extraction plumbing, differ only in fill semantics. Halfword
|
||||
addressing uses `ea[1]` (ea[0] must be zero for aligned
|
||||
access). TBs: `tb_ee_core_lh`, `tb_ee_core_lhu` (each
|
||||
covers both halfword lanes + the fill discipline for
|
||||
negative high-lane values). Unlocked retired=7385 → 8207.
|
||||
- **SLL / SRL / SRA** (SPECIAL R-type shifts, func 0x00 /
|
||||
0x02 / 0x03). Batched per Codex pre-approval. Destination
|
||||
= rd, operand = rt, shift amount = `shamt` (bits [10:6]).
|
||||
SRA uses `$signed(rt_val) >>> shamt` for arithmetic right
|
||||
shift (sign fill); SRL uses `rt_val >> shamt` (zero fill).
|
||||
SLL $0,$0,0 is the canonical NOP encoding and flows through
|
||||
this path harmlessly — the rd_idx=0 writeback guard blocks
|
||||
any phantom write. TB: `tb_ee_core_shift` (critical probes:
|
||||
SRL vs SRA on the same negative input to catch sign-vs-zero
|
||||
fill bugs). Unlocked a **12,000-instruction stretch**
|
||||
(retired=8207 → 20327).
|
||||
- **SH** (opcode 0x29, halfword store). Store-side mate to
|
||||
LH/LHU; same lane-broadcast + byte-enable idiom as SB but
|
||||
at halfword granularity via `ea[1]`. 2-of-4 byte-enable
|
||||
(`4'b0011` for low lane, `4'b1100` for high lane) preserves
|
||||
the non-addressed halfword. TB: `tb_ee_core_sh` — two
|
||||
chained probes with register values that have distinctive
|
||||
upper halves (0xCAFE_FACE, 0x1234_5678). If the byte-enable
|
||||
is wrong or the full register leaks into the map_wr_data
|
||||
bus, the preservation check catches it (RAM word ends up
|
||||
0x5678_FACE after both stores; wrong behavior would corrupt
|
||||
the non-addressed halfword). Unlocked a **56,000-
|
||||
instruction stretch** (retired=20327 → 76406) once the
|
||||
RAM-size infra issue was also fixed in the same chapter
|
||||
— see next bullet.
|
||||
- **Real-BIOS RAM size (chapter 7.9 infra fix).** Before this
|
||||
chapter, `tb_ee_core_bios_smoke` used only 4 KiB of EE RAM
|
||||
— fine for the synthetic CI program (which never writes
|
||||
beyond the first qword), but destructive once the real
|
||||
BIOS copies a large chunk of itself into RAM and jumps
|
||||
there. Addresses beyond 4 KiB silently aliased into the
|
||||
same window, producing 156k "retires" that were actually
|
||||
the core executing a scrambled mix of overwritten bytes,
|
||||
with no trap ever firing because whatever happened to land
|
||||
at the aliased offset decoded to something supported.
|
||||
Bumped `EE_RAM_BYTES` in the bench to 4 MiB (real PS2 has
|
||||
32 MiB; 4 MiB covers BIOS init comfortably without
|
||||
ballooning sim memory). After the fix, real-BIOS smoke
|
||||
runs honestly and trapped on JALR at 0xBFC5_29E8.
|
||||
- **JALR** (SPECIAL func 0x09, register-indirect call). Target
|
||||
is `rs_val` (same path as JR); link address pc+8 is written
|
||||
to `rd_idx`. Unlike JAL's hardcoded `$31`, JALR's link
|
||||
destination is explicit in the instruction, and `rd==0` is
|
||||
a valid encoding that suppresses the link write. TB:
|
||||
`tb_ee_core_jalr` — two probes: canonical `jalr $31, $rs`
|
||||
(what the BIOS used) plus `jalr $20, $rs` with the return
|
||||
via `jr $20` to prove the rd field is honored and not
|
||||
accidentally hardcoded to $31. Unlocked retired=76406 →
|
||||
84112 and the BIOS fully jumped into RAM-resident code
|
||||
(next trap_pc is `0x0000_060C`, a RAM address, not BIOS).
|
||||
- **ADD / SUB** (SPECIAL R-type, func 0x20 / 0x22). Batched
|
||||
per Codex's guidance — same pragmatic policy as ADDI vs
|
||||
ADDIU: this core does not model the Arithmetic Overflow
|
||||
exception, so ADD behaves as ADDU and SUB behaves as SUBU.
|
||||
Merged into the existing `rs_val + rt_val` / `rs_val - rt_val`
|
||||
arms of `rtype_alu_wb`. TB: `tb_ee_core_add_sub` — four
|
||||
probes including INT_MAX+1 wrap, which documents the
|
||||
deferred-exception policy (the wrap is the *expected*
|
||||
outcome, so the TB will fail loudly if overflow trapping
|
||||
ever lands without the TB being updated).
|
||||
- **COP0 Count (reg 9)** — first machine-state chapter after
|
||||
the iter-14 transition. Free-running 32-bit counter that
|
||||
increments every clock and resets to 0. Exposed read-only
|
||||
through MFC0 $9. MTC0 $9 silently dropped (no reset-to-value
|
||||
yet; revisit if BIOS depends on it). TB:
|
||||
`tb_ee_core_cop0_count` — two probes covering consecutive-
|
||||
MFC0 advance and a canonical `while (now < target)` poll
|
||||
that must exit.
|
||||
- **Enhanced bios_smoke PC sampler** with `peek_instr(addr)`
|
||||
helper (hierarchical read through `u_bios.mem` / `u_ee_ram.mem`)
|
||||
and a parallel `retired_history` array. Timeout now reports
|
||||
the instruction and retired count at each sample, not just
|
||||
pc. Timeout window bumped 5 ms → 20 ms for BIOS runway.
|
||||
- **Sampler pointer snapshots + 80 ms timeout.** After the
|
||||
instruction-aware sampler showed the loop was a linked-list
|
||||
walk (not a hardware wait), Codex directed "extend timeout
|
||||
first, then add pointer snapshots only if still stuck".
|
||||
Timeout bumped 20 ms → 80 ms: retired grew linearly to
|
||||
2.46 M, still 100% in the same loop (≈350k iterations — way
|
||||
beyond any plausible BIOS list length). Added `u_core.regfile[5]`
|
||||
and `[6]` hierarchical snapshots at each sample. Finding:
|
||||
- `$5` (sentinel) = `0x00000974` — plausible low-RAM pointer
|
||||
- `$6` (current) = `0xDEADBEEF` — **the EE map's unmapped-
|
||||
read poison value**.
|
||||
The cycle is self-perpetuating: `lw $2, 0($6)` with
|
||||
`$6 = 0xDEADBEEF` reads address 0xDEADBEEF, which is
|
||||
unmapped, returning 0xDEADBEEF; the `bne $2, $0` stays
|
||||
taken forever. The real root cause is an **earlier** BIOS
|
||||
read from an unmapped address that poisoned a data structure
|
||||
— the traversal followed the poisoned pointer and locked in.
|
||||
- *(next-move call is with Codex: add an unmapped-read tracer
|
||||
to find the first bad address, implement whatever peripheral
|
||||
the BIOS was reading, change the poison value to 0 so the
|
||||
loop exits and exposes further BIOS progress, or something
|
||||
else.)*
|
||||
- **Bench-drift note (chapter 7.5):** the synthetic BIOS smoke
|
||||
sentinel was originally AND; once AND was added to the
|
||||
R-type ALU, the synthetic test silently stopped tripping
|
||||
and started timing out. Codex caught it; sentinel is now
|
||||
BREAK (SPECIAL func 0x0D). See project memory for the full
|
||||
post-mortem. Lesson: avoid using real opcodes as
|
||||
"unsupported sentinels" in test benches.
|
||||
|
||||
## Scope boundary
|
||||
|
||||
This directory owns EE CPU execution and its immediate coprocessors
|
||||
(COP0 minimum; eventually COP1 FPU and COP2 VU macro mode). It does
|
||||
**not** own:
|
||||
|
||||
- memory map / address decode — that's `rtl/memory/ee_memory_map_stub.sv`.
|
||||
- interrupt controller — that's `rtl/intc/` (generic; the same
|
||||
`intc_stub` module already serves the IOP side).
|
||||
- DMAC, VIF/VU, GIF/GS — separate directories.
|
||||
@@ -0,0 +1,142 @@
|
||||
// retroDE_ps2 — ee_biu_mmio_stub
|
||||
//
|
||||
// Narrow latched-register-file stub for the EE Bus Interface Unit /
|
||||
// cache-control window at virtual `0xFFFE_0000 - 0xFFFE_0FFF`
|
||||
// (physical `0x1FFE_0000 - 0x1FFE_0FFF` after kseg1-stripping).
|
||||
// Architecturally this is the R5900's privileged BIU/control
|
||||
// register space — the same place the BIOS writes CACHE-control
|
||||
// and BIU-config values during boot.
|
||||
//
|
||||
// Chapter 9: chapter 8 closed the 0x1F80_xxxx hole. The first-
|
||||
// unmapped observer in tb_ee_core_bios_smoke then showed the next
|
||||
// unmapped event was a WRITE at 0xFFFE_0130 (pc=0xBFC0_21BC,
|
||||
// cycle 808). Multiple more writes to that same offset fire later
|
||||
// with values 0xCC4, 0xCC0, 0x1E988, 0xC04, 0x3202_000F —
|
||||
// classic cache/BIU config dance. Without a stub, these writes
|
||||
// land as UNMAPPED events; the first one reads back to this stub
|
||||
// would return 0xDEADBEEF and re-poison the pointer chain chapter
|
||||
// 8 just cleaned up.
|
||||
//
|
||||
// Codex's call for chapter 9: give this its own dedicated stub
|
||||
// with its own region tag, NOT a broad "everything else" fallback.
|
||||
// Keep architecturally distinct surfaces distinct. If the BIOS
|
||||
// later touches 0x1FA0_0000 (next unmapped in the observer), that
|
||||
// will be its own chapter, not folded in here.
|
||||
//
|
||||
// Semantics (same shape as ee_bootstrap_mmio_stub):
|
||||
// - 4 KiB window = 1024 × 32-bit latched registers, zero-init.
|
||||
// - Writes latch per-byte: for each `wr_be[i]`, byte[i] of the
|
||||
// addressed register updates; untouched lanes preserve their
|
||||
// prior value. Makes SB/SH through the window safe.
|
||||
// - Reads return currently-latched value, one-cycle latency.
|
||||
// - No side effects. BIOS read-modify-write sequences stay
|
||||
// self-consistent.
|
||||
//
|
||||
// Size cost: 1024 × 32 bits = 4 KiB sim memory. Negligible.
|
||||
//
|
||||
// Trace: per-access event on SUBSYS_MEM with region tag
|
||||
// `REGION_EE_BIU = 10` (distinct from REGION_EE_MISC_MMIO=9 so
|
||||
// post-run analysis can separate the two windows).
|
||||
|
||||
`timescale 1ns/1ps
|
||||
|
||||
module ee_biu_mmio_stub
|
||||
import trace_pkg::*;
|
||||
(
|
||||
input logic clk,
|
||||
input logic rst_n,
|
||||
|
||||
// Write port — 12-bit offset within the 4 KiB window
|
||||
input logic reg_wr_en,
|
||||
input logic [11:0] reg_wr_addr,
|
||||
input logic [31:0] reg_wr_data,
|
||||
input logic [3:0] reg_wr_be,
|
||||
|
||||
// Read port — 1-cycle latency
|
||||
input logic reg_rd_en,
|
||||
input logic [11:0] reg_rd_addr,
|
||||
output logic [31:0] reg_rd_data,
|
||||
output logic reg_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 int WORDS = 1024; // 4 KiB / 4
|
||||
localparam logic [63:0] REGION_EE_BIU = 64'd10;
|
||||
|
||||
logic [31:0] regs [0:WORDS-1];
|
||||
|
||||
initial begin
|
||||
for (int i = 0; i < WORDS; i++) regs[i] = 32'd0;
|
||||
end
|
||||
|
||||
logic [9:0] wr_idx;
|
||||
logic [9:0] rd_idx;
|
||||
assign wr_idx = reg_wr_addr[11:2];
|
||||
assign rd_idx = reg_rd_addr[11:2];
|
||||
|
||||
// Per-byte write latch
|
||||
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
|
||||
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) reg_rd_data <= regs[rd_idx];
|
||||
end
|
||||
end
|
||||
|
||||
// Trace — write wins same-cycle collision (defensive; map enforces
|
||||
// mutual exclusion)
|
||||
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 <= {52'd0, reg_wr_addr};
|
||||
ev_arg1 <= {32'd0, reg_wr_data};
|
||||
ev_arg2 <= {60'd0, reg_wr_be};
|
||||
ev_arg3 <= REGION_EE_BIU;
|
||||
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 <= {52'd0, reg_rd_addr};
|
||||
ev_arg1 <= {32'd0, regs[rd_idx]};
|
||||
ev_arg2 <= 64'd0;
|
||||
ev_arg3 <= REGION_EE_BIU;
|
||||
ev_flags <= 32'd0;
|
||||
end else begin
|
||||
ev_valid <= 1'b0;
|
||||
end
|
||||
end
|
||||
|
||||
endmodule : ee_biu_mmio_stub
|
||||
@@ -0,0 +1,269 @@
|
||||
// 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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,128 @@
|
||||
// retroDE_ps2 — ee_fetch_stub
|
||||
//
|
||||
// Minimal sequential-fetch stand-in for the R5900. Wave 1 scope only: enough
|
||||
// to drive ee_memory_map_stub → bios_rom_stub for Milestone B.
|
||||
//
|
||||
// Contract refs:
|
||||
// docs/stub_module_plan.md (Wave 1, item 4)
|
||||
// docs/contracts/ee.md
|
||||
//
|
||||
// Behavior:
|
||||
// - On reset, PC = RESET_VECTOR (default 0xBFC00000, the MIPS BIOS
|
||||
// reset vector in kseg1).
|
||||
// - Each cycle while `enable` is high: issue a read at PC, advance
|
||||
// PC += 4. No decode, no branches, no exceptions, no retirement
|
||||
// fidelity (all out-of-scope per plan).
|
||||
// - Responses return 1 cycle later via rd_valid/rd_data from the
|
||||
// memory map. The issued address is latched so the trace line can
|
||||
// pair address with data.
|
||||
//
|
||||
// Non-goals for this wave (stub plan, explicit):
|
||||
// - full decode,
|
||||
// - exceptions beyond deterministic fault handling,
|
||||
// - FPU/MMI behavior,
|
||||
// - instruction retirement fidelity.
|
||||
//
|
||||
// Trace payload schema (per stub plan):
|
||||
// EE RESET arg0=reset_vector
|
||||
// EE IFETCH arg0=pc arg1=data arg2=resp_kind arg3=-
|
||||
// resp_kind: 0=OK (only path in Wave 1)
|
||||
|
||||
`timescale 1ns/1ps
|
||||
|
||||
module ee_fetch_stub
|
||||
import trace_pkg::*;
|
||||
#(
|
||||
parameter logic [31:0] RESET_VECTOR = 32'hBFC00000
|
||||
) (
|
||||
input logic clk,
|
||||
input logic rst_n,
|
||||
input logic enable,
|
||||
|
||||
// Memory-facing fetch port
|
||||
output logic rd_en,
|
||||
output logic [31:0] rd_addr,
|
||||
input logic [31:0] rd_data,
|
||||
input logic 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
|
||||
);
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// PC and one-cycle issued-address shadow
|
||||
//
|
||||
// pc is the address being issued THIS cycle (rd_addr)
|
||||
// pc_d1 is the address whose response arrives THIS cycle on rd_valid
|
||||
//
|
||||
// pc_d1 only advances alongside pc when enable is high, so it stays
|
||||
// aligned with the in-flight request.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
logic [31:0] pc;
|
||||
logic [31:0] pc_d1;
|
||||
|
||||
always_ff @(posedge clk) begin
|
||||
if (!rst_n) begin
|
||||
pc <= RESET_VECTOR;
|
||||
pc_d1 <= RESET_VECTOR;
|
||||
end else if (enable) begin
|
||||
pc_d1 <= pc;
|
||||
pc <= pc + 32'd4;
|
||||
end
|
||||
end
|
||||
|
||||
assign rd_en = enable;
|
||||
assign rd_addr = pc;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Trace
|
||||
// - Single EV_RESET pulse at reset exit.
|
||||
// - EV_IFETCH one cycle after each rd_valid response.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
logic reset_emit_pending;
|
||||
|
||||
always_ff @(posedge clk) begin
|
||||
if (!rst_n) begin
|
||||
ev_valid <= 1'b0;
|
||||
ev_subsys <= SUBSYS_EE;
|
||||
ev_event <= EV_RESET;
|
||||
ev_arg0 <= 64'd0;
|
||||
ev_arg1 <= 64'd0;
|
||||
ev_arg2 <= 64'd0;
|
||||
ev_arg3 <= 64'd0;
|
||||
ev_flags <= 32'd0;
|
||||
reset_emit_pending <= 1'b1;
|
||||
end else if (reset_emit_pending) begin
|
||||
ev_valid <= 1'b1;
|
||||
ev_subsys <= SUBSYS_EE;
|
||||
ev_event <= EV_RESET;
|
||||
ev_arg0 <= {32'd0, RESET_VECTOR};
|
||||
ev_arg1 <= 64'd0;
|
||||
ev_arg2 <= 64'd0;
|
||||
ev_arg3 <= 64'd0;
|
||||
ev_flags <= 32'd0;
|
||||
reset_emit_pending <= 1'b0;
|
||||
end else if (rd_valid) begin
|
||||
ev_valid <= 1'b1;
|
||||
ev_subsys <= SUBSYS_EE;
|
||||
ev_event <= EV_IFETCH;
|
||||
ev_arg0 <= {32'd0, pc_d1};
|
||||
ev_arg1 <= {32'd0, rd_data};
|
||||
ev_arg2 <= 64'd0; // resp_kind: 0 = OK
|
||||
ev_arg3 <= 64'd0;
|
||||
ev_flags <= 32'd0;
|
||||
end else begin
|
||||
ev_valid <= 1'b0;
|
||||
end
|
||||
end
|
||||
|
||||
endmodule : ee_fetch_stub
|
||||
Reference in New Issue
Block a user