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
+151
View File
@@ -0,0 +1,151 @@
# rtl/iop
IOP subsystem. Matches `docs/contracts/iop.md`.
## Current contents
- `iop_ram_stub.sv` — 32-bit IOP RAM primitive. Default 16 KiB,
parameterizable. Read + write ports with byte-enable granularity,
one-cycle read latency, caller-provided `master_id` for trace
attribution. Emits trace events under `SUBSYS_IOP`.
- `iop_memory_map_stub.sv` — IOP-side address decode. CPU-side port uses
kseg0/kseg1 stripping (`phys = iop_addr[28:0]`). Second write-master
port for DMA bridges (`bridge_wr_*`), physical addressing. Regions now
decoded:
- IOP RAM (phys 0x00000000-0x001FFFFF) → `iop_ram_stub`
- SIF registers (phys 0x1D000000 block) → SIF register shell
(`sif_mailbox_stub` IOP side) via `sif_rd_*` / `sif_wr_*` ports
- IOP DMAC channel 9 (phys 0x1F801520-0x1F80152F) → IOP DMAC
register shell via `iop_dmac_rd_*` / `iop_dmac_wr_*` ports
- IOP INTC (phys 0x1F801070-0x1F80107F) → `intc_stub` (IOP-side
instance) via `iop_intc_rd_*` / `iop_intc_wr_*` ports
- Shared BIOS ROM (phys 0x1FC00000-0x1FFFFFFF, 4 MiB) →
`bios_rom_stub` via `bios_rd_*` port. kseg1 aliasing makes
`0xBFC0_0000` reset fetches land here transparently. BIOS is
read-only; writes to this window trace as UNMAPPED.
- everything else → UNMAPPED with deterministic 0xDEADBEEF
Future regions (other DMAC channels, IOP timers, SPU2) reserved in
comments. Arbitration between CPU and bridge writes on RAM path:
CPU wins on same-cycle collision. SIF, DMAC, INTC, and BIOS are
separate ports and don't contend with RAM.
- `iop_fetch_stub.sv` — minimal sequential 32-bit fetcher. Mirrors
`ee_fetch_stub` in shape: PC-incrementing, no decode, no branches, no
exceptions. Default `RESET_VECTOR` is in IOP RAM (0x00000000), NOT in
BIOS space — explicitly non-BIOS boot. Emits `IOP RESET` once and
`IOP IFETCH` per response. First execution-visible IOP traffic in the
project; fetches route through `iop_memory_map_stub`.
- `iop_core_stub.sv` — **real instruction-decoding IOP core with
minimal COP0, asynchronous interrupt exception entry, and the
architectural MIPS reset vector.** Tiny MIPS R3000 subset,
multi-cycle FSM, speaks the same map / DMAC / INTC protocol as every
previous engine. Default `PC_RESET = 0xBFC0_0000` (kseg1 into the
shared BIOS window; override with a parameter for RAM-only tests).
Supported opcodes:
LUI, ORI, ADDIU, LW, SW, BEQ, BNE, J, JR (SPECIAL func 0x08),
NOP (any other SPECIAL func / unknown opcode), SYSCALL
(SPECIAL func 0x0C, halts), **MFC0 / MTC0 / RFE** (COP0 opcode 0x10).
32-entry register file with `$0` hardwired.
**COP0 subset:** Status (IE/KU triple stack + IM), Cause (ExcCode +
IP reflecting cpu_irq), EPC. Exception entry is sampled at clean
instruction-retire boundaries: if a delay slot is outstanding, the
exception defers until the delay slot resolves. On entry: push IE/KU
stack, ExcCode=0, save EPC=next_pc, PC←EXC_VECTOR (parameter).
**Branch delay slot** honoured from day one; taken-branch and
delay-slot retires are both flagged in the trace.
**Strict mode:** `STRICT_UNSUPPORTED` parameter (default 0). When
set, unsupported opcodes halt the core and latch the offending
pc/instr word into `trap_o` / `trap_pc_o` / `trap_instr_o` instead
of silently retiring as NOPs. The canonical NOP (`instr == 32'h0`,
SLL $0,$0,0) is always treated as a real NOP. Retire trace flag
bit 7 marks strict-trap retires. Used by the BIOS smoke TB; other
benches leave it off for backwards compatibility.
Deferred: BD bit in Cause, nested interrupts, syscall/break
exception dispatch, R-type ALU/shifts/HI-LO.
- `iop_exec_stub.sv` — **RAM-backed IOP execution primitive (bridge
module).**
Not a MIPS core, not an ISA decoder. A tiny FSM sequencer that fetches
its micro-ops from IOP RAM through the real `iop_memory_map_stub`
CPU-side port — the same way a future instruction-fetching CPU will.
The control program is no longer RTL-resident; it lives as data in
RAM that someone (a TB, eventually a BIOS loader) preloads before
pulsing `go_i`.
**Five opcodes**: `HALT`, `WRITE(addr, data)`, `READ(addr)`,
`WAIT_IRQ`, `BNE(target_pc, expected)` — branch if the last READ's
result does not equal `expected`, enabling real loops.
Op layout in RAM: 16 bytes per op (`pc<<4` addressing). Word 0 is
the opcode (low 4 bits), word 1 is addr or branch target, word 2 is
data or expected value, word 3 is reserved. `SCRIPT_BASE` is a
parameter (default 0x0000_0400).
Takes `cpu_irq` from the IOP INTC; `WAIT_IRQ` genuinely blocks until
a real interrupt asserts. One trace event per op completion with
flag bits marking WAIT_IRQ exit (bit 1), HALT entry (bit 2), and
BNE taken (bit 3). When a real MIPS decode primitive eventually
arrives, it replaces this module while keeping the same map / DMA /
INTC hookup verbatim.
- `iop_dmac_reg_stub.sv` — IOP DMAC for one SIF-facing channel
(CHANNEL=9, PATH_ID=9, MASTER_ID=4). Register surface (low-byte
offsets): MADR @ 0x00, BCR @ 0x04, CHCR @ 0x08, DONE_COUNT @ 0x0C
(read-only monotonic counter); start bit is CHCR[0].
Real data path: on start, DMAC latches MADR/BCR, then steps through
IDLE → FETCH_WAIT → ACTIVE_SEND → DONE per beat, sourcing 32-bit words
from IOP RAM through the map's `dma_rd_*` port (src_addr stepping by
4 per beat). Endpoint is a word-granularity ready/valid/last stream
with `ep_ready` back-pressure — no false completion under stall.
Emits DMA_CFG on register writes, DMA_START on arm, DMA_BEAT per
accepted beat (with src_addr + remaining count), DMA_DONE on the
final beat. `done_count_o` is a monotonic visible counter.
`irq_completion_o` is a one-cycle pulse on S_DONE — wired into the
IOP INTC as source bit 0 so software can observe channel completion.
Only reachable through the real IOP map at 0x1F80_1520.
## Explicit non-goals (current step)
- Full MIPS R3000 ISA coverage (the core is still a narrow subset;
strict-mode halts on the first unsupported opcode so the BIOS tells
us what to grow next)
- Full 2 MiB RAM sizing (stub defaults stay small for sim speed; the map
window is 2 MiB and truncates at the connection to the smaller stub)
- IOP I/O beyond the currently decoded regions (DMAC ch9 / INTC / BIOS);
SPU2, timers, and other peripherals are not wired yet
- IOP DMAC channels other than ch9 (SIF0 IOP→EE)
- Real Sony BIOS execution (the smoke TB's synthetic bootstrap is the
current committed content; swapping in a user-supplied dump is a
drop-in exercise that will reveal the next missing opcode)
## Scope boundary
This directory owns IOP CPU execution, IOP-local RAM/I/O decode, IOP
interrupt intake, IOP DMAC channels, and BIOS-side IOP boot sequencing
behavior (per `docs/contracts/iop.md`).
The IOP side now runs a MIPS R3000 subset from an architecturally
correct BIOS reset vector, with precise interrupt exception entry and
a RAM-resident ISR. The project has crossed five architectural seams:
1. TB-orchestrated → fabric-orchestrated (scripted exec stub)
2. RTL-resident → RAM-resident control (exec stub reads ops from RAM)
3. Micro-op bridge → real ISA decode (iop_core_stub)
4. Polled completion → asynchronous exception-driven control flow
(COP0 + cpu_irq)
5. TB-preloaded RAM as reset source → BIOS ROM at 0xBFC0_0000
(shared BIOS wired through the IOP map; hand-assembled bootstraps
prove the seam before any real Sony BIOS is attempted)
Each seam preserved every prior module — only where code comes from
evolved.
## Planned next increments
These are possibilities, not commitments — order will be decided per the
next architectural question:
- **BIOS-driven core growth:** point `tb_iop_core_bios_smoke` at a
user-supplied BIOS dump (swap the TB's synthetic preload for
`$readmemh` into `u_bios.mem`), observe the first unsupported
opcode, add it to `iop_core_stub`, repeat. Expected near-term
additions: ANDI, ADDU/SUBU, SLL/SRL/SRA, JAL, SLT(U). Do not add
speculatively; let the BIOS trace drive the order.
- Core exception growth as the BIOS path demands it: BD bit in
Cause, nested interrupts, syscall/break exception dispatch.
- Other IOP DMAC channels (CDVD / SPU2 / DEV9 / SIF1-2 / SIO2).
- IOP map expansion: remaining IOP I/O (0x1F800000), SPU2
(0x1F900000).
+711
View File
@@ -0,0 +1,711 @@
// retroDE_ps2 — iop_core_stub
//
// Minimal MIPS R3000 subset for the IOP side, now with real interrupt
// exception entry. The engine sits where `iop_exec_stub` sat, drives
// `iop_memory_map_stub`'s CPU-side port for ifetch and data accesses,
// and finally *uses* `cpu_irq` from the IOP INTC instead of ignoring it.
//
// Wave 1 (decode): LUI/ORI/ADDIU/LW/SW/BEQ/BNE/J/NOP/SYSCALL, honest
// branch delay slots. Programs polled INTC_STAT through the real map.
//
// Wave 2 (this module revision): minimal COP0 + asynchronous interrupt
// exception entry. cpu_irq becomes a real vectoring event when
// enabled through Status. Mainline no longer needs to touch INTC_STAT;
// an ISR at the exception vector handles acknowledgement.
//
// Intentionally still NOT a full R3000:
// - No TLB / cache / HI/LO / R-type ALU / shifts / mul / div.
// - No syscall / break exception *handling* beyond SYSCALL-as-halt.
// - No BD bit in Cause for branch-delay exceptions (we simply
// refuse to take exceptions between a taken branch and its delay
// slot — see "delay-slot rule" below).
// - No kernel/user mode enforcement: KU state exists on the stack
// for forward compatibility but nothing in the core consults it.
//
// Supported opcodes (MIPS encoding):
// SPECIAL (opcode = 0x00):
// func 0x08 (JR) — pc <= rs_val; has delay slot.
// func 0x0C (SYSCALL) — halt_o asserts; FSM stops fetching.
// any other func — treated as NOP (incl. SLL $0,$0,0).
// 0x02 J — jump; has delay slot.
// 0x04 BEQ / 0x05 BNE — conditional branch; has delay slot.
// 0x09 ADDIU — no overflow trap.
// 0x0D ORI / 0x0F LUI — logical immediate / upper load.
// 0x10 COP0:
// rs 0x00 (MFC0) — rt <= COP0[rd]
// rs 0x04 (MTC0) — COP0[rd] <= rt
// rs 0x10, func 0x10 (RFE)
// — shift IE/KU stack right (pop)
// 0x23 LW / 0x2B SW — word memory access.
// Anything else — treated as NOP.
//
// COP0 register surface (subset):
// 12 Status [0]=IEc [1]=KUc [2]=IEp [3]=KUp [4]=IEo [5]=KUo
// [15:8]=IM (bit 10 = IM2 gates the HW interrupt
// wired to cpu_irq)
// 13 Cause [6:2]=ExcCode, [15:8]=IP. IP[2] reflects cpu_irq.
// Software may write Cause but we only latch SW
// interrupt pending bits IP[1:0] — not load-bearing
// in the first TB.
// 14 EPC saved PC on exception entry.
//
// Exception entry semantics:
// Sampled at *instruction-retire boundaries*, never mid-fetch or
// mid-memory. An exception is taken iff all of the following hold
// at the retire boundary:
// - Status.IEc == 1 (master interrupts enabled)
// - Cause.IP[i] & Status.IM[i] (any unmasked pending source)
// - new_branch_pending == 0 (delay slot already resolved)
// On entry:
// EPC <= next_pc (the pc that would have been
// fetched next; branch_target
// if a delay slot just resolved,
// pc+4 otherwise)
// Cause.ExcCode <= 5'h00 (Int exception)
// Status stack pushes left:
// IEo <= IEp; IEp <= IEc; IEc <= 0
// KUo <= KUp; KUp <= KUc; KUc <= 0
// pc <= EXC_VECTOR (fixed, parameter)
// branch_pending <= 0 (any pending control flow is
// canceled; EPC captured it)
//
// RFE semantics (pop stack, one level):
// IEc <= IEp; IEp <= IEo
// KUc <= KUp; KUp <= KUo
// (IEo, KUo left intact — matches impl-defined R3000 behaviour
// for non-nested use)
//
// Trace (SUBSYS_IOP, EV_IFETCH one-per-retire as before):
// flags bit 0 = SW (write) (unchanged)
// flags bit 1 = LW (read) (unchanged)
// flags bit 2 = branch / jump taken (unchanged)
// flags bit 3 = SYSCALL (halt) (unchanged)
// flags bit 4 = this instruction was in a delay slot
// flags bit 5 = exception taken at the end of this instruction
// (EPC saved = next_pc, PC redirected to EXC_VECTOR)
// flags bit 6 = RFE retired (IE stack popped)
// flags bit 7 = strict trap (unsupported instruction halted the core)
//
// Strict mode (STRICT_UNSUPPORTED parameter):
// Default is 0 (lenient) to preserve every prior bench's regression
// behaviour — any instruction the core doesn't actively decode retires
// as a NOP. When STRICT_UNSUPPORTED=1, the core instead halts on the
// first unsupported opcode it encounters, latches the offending PC +
// instruction word into trap_pc_o / trap_instr_o, asserts trap_o, and
// emits a retire trace with flag bit 7 set. Intended for real-BIOS
// smoke bring-up — "the first missing opcode is the one the core
// needs to grow next." The canonical NOP (32'h0000_0000 =
// SLL $0,$0,0) is always treated as a NOP regardless of strict mode.
`timescale 1ns/1ps
module iop_core_stub
import trace_pkg::*;
#(
// Architectural MIPS R3000 reset vector (kseg1 into the shared BIOS
// window). kseg1 strip in iop_memory_map_stub maps this to physical
// 0x1FC0_0000, which the map now routes to bios_rom_stub.
// Tests that don't have a BIOS image must override PC_RESET.
parameter logic [31:0] PC_RESET = 32'hBFC0_0000,
parameter logic [31:0] EXC_VECTOR = 32'h0000_0080,
// See header comment "Strict mode". Default 0 preserves existing
// regression behaviour; BIOS-oriented benches should set to 1.
parameter bit STRICT_UNSUPPORTED = 1'b0
) (
input logic clk,
input logic rst_n,
input logic go_i,
output logic map_rd_en,
output logic [31:0] map_rd_addr,
input logic [31:0] map_rd_data,
input logic map_rd_valid,
output logic map_wr_en,
output logic [31:0] map_wr_addr,
output logic [31:0] map_wr_data,
output logic [3:0] map_wr_be,
input logic cpu_irq,
output logic halt_o,
output logic [31:0] pc_o,
// Strict-mode trap reporting. `trap_o` rises the cycle the core
// halts on an unsupported instruction; `trap_pc_o` / `trap_instr_o`
// latch the offending fetch. All three stay stable after the halt.
output logic trap_o,
output logic [31:0] trap_pc_o,
output logic [31:0] trap_instr_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
);
// ------------------------------------------------------------------
// Opcode / func / COP0 rs constants
// ------------------------------------------------------------------
localparam logic [5:0] OP_SPECIAL = 6'h00;
localparam logic [5:0] OP_J = 6'h02;
localparam logic [5:0] OP_BEQ = 6'h04;
localparam logic [5:0] OP_BNE = 6'h05;
localparam logic [5:0] OP_ADDIU = 6'h09;
localparam logic [5:0] OP_ORI = 6'h0D;
localparam logic [5:0] OP_LUI = 6'h0F;
localparam logic [5:0] OP_COP0 = 6'h10;
localparam logic [5:0] OP_LW = 6'h23;
localparam logic [5:0] OP_SW = 6'h2B;
localparam logic [5:0] FUNC_JR = 6'h08;
localparam logic [5:0] FUNC_SYSCALL = 6'h0C;
localparam logic [5:0] FUNC_RFE = 6'h10;
localparam logic [4:0] COP0_RS_MF = 5'h00;
localparam logic [4:0] COP0_RS_MT = 5'h04;
localparam logic [4:0] COP0_RS_CO = 5'h10;
localparam logic [4:0] COP0_REG_STATUS = 5'd12;
localparam logic [4:0] COP0_REG_CAUSE = 5'd13;
localparam logic [4:0] COP0_REG_EPC = 5'd14;
// ------------------------------------------------------------------
// FSM state
// ------------------------------------------------------------------
typedef enum logic [3:0] {
S_IDLE = 4'd0,
S_IFETCH_REQ = 4'd1,
S_IFETCH_WAIT = 4'd2,
S_EXECUTE = 4'd3,
S_MEM_REQ = 4'd4,
S_MEM_WAIT = 4'd5,
S_MEM_WRITE = 4'd6,
S_HALT = 4'd7
} state_e;
state_e state;
// Architectural state
logic [31:0] pc;
logic [31:0] instr;
logic [31:0] regfile [0:31];
// Branch delay-slot tracking
logic branch_pending;
logic [31:0] branch_target;
logic instr_in_delay_slot;
// COP0 — Status (IE/KU triple stack + IM)
logic status_iec, status_iep, status_ieo;
logic status_kuc, status_kup, status_kuo;
logic [7:0] status_im;
// COP0 — Cause / EPC
logic [4:0] cause_exc_code;
logic [7:0] cause_ip_sw; // software-writable pending bits (IP[1:0])
logic [31:0] epc;
// Combinational composition of IP. IP[2] mirrors cpu_irq directly;
// higher sources are not wired in the current scope.
logic [7:0] cause_ip;
always_comb begin
cause_ip = 8'd0;
cause_ip[1:0] = cause_ip_sw[1:0];
cause_ip[2] = cpu_irq;
end
// Composed Status word (for MFC0) and Cause word (for MFC0)
logic [31:0] status_word;
logic [31:0] cause_word;
always_comb begin
status_word = 32'd0;
status_word[0] = status_iec;
status_word[1] = status_kuc;
status_word[2] = status_iep;
status_word[3] = status_kup;
status_word[4] = status_ieo;
status_word[5] = status_kuo;
status_word[15:8] = status_im;
cause_word = 32'd0;
cause_word[6:2] = cause_exc_code;
cause_word[15:8] = cause_ip;
end
// ------------------------------------------------------------------
// Decode — combinational extraction from `instr`
// ------------------------------------------------------------------
logic [5:0] opcode;
logic [4:0] rs_idx;
logic [4:0] rt_idx;
logic [4:0] rd_idx;
logic [5:0] func;
logic [15:0] imm16;
logic [25:0] imm26;
logic [31:0] imm_sx;
logic [31:0] imm_zx;
logic [31:0] branch_offset;
logic [31:0] branch_tgt;
logic [31:0] j_tgt;
logic [31:0] rs_val;
logic [31:0] rt_val;
logic [31:0] ea;
assign opcode = instr[31:26];
assign rs_idx = instr[25:21];
assign rt_idx = instr[20:16];
assign rd_idx = instr[15:11];
assign imm16 = instr[15:0];
assign imm26 = instr[25:0];
assign func = instr[5:0];
assign imm_sx = {{16{imm16[15]}}, imm16};
assign imm_zx = {16'd0, imm16};
assign branch_offset = {{14{imm16[15]}}, imm16, 2'b00};
assign branch_tgt = pc + 32'd4 + branch_offset;
assign j_tgt = {pc[31:28], imm26, 2'b00};
assign rs_val = (rs_idx == 5'd0) ? 32'd0 : regfile[rs_idx];
assign rt_val = (rt_idx == 5'd0) ? 32'd0 : regfile[rt_idx];
assign ea = rs_val + imm_sx;
// Instruction classification
logic is_special, is_syscall, is_jr;
logic is_cop0, is_mfc0, is_mtc0, is_rfe;
logic is_nop_class;
logic is_lui, is_ori, is_addiu, is_lw, is_sw, is_beq, is_bne, is_j;
logic is_branch, is_jump;
logic branch_taken;
logic is_taken_branch_or_jump;
assign is_special = (opcode == OP_SPECIAL);
assign is_syscall = is_special && (func == FUNC_SYSCALL);
assign is_jr = is_special && (func == FUNC_JR);
assign is_cop0 = (opcode == OP_COP0);
assign is_mfc0 = is_cop0 && (rs_idx == COP0_RS_MF);
assign is_mtc0 = is_cop0 && (rs_idx == COP0_RS_MT);
assign is_rfe = is_cop0 && (rs_idx == COP0_RS_CO) && (func == FUNC_RFE);
assign is_lui = (opcode == OP_LUI);
assign is_ori = (opcode == OP_ORI);
assign is_addiu = (opcode == OP_ADDIU);
assign is_lw = (opcode == OP_LW);
assign is_sw = (opcode == OP_SW);
assign is_beq = (opcode == OP_BEQ);
assign is_bne = (opcode == OP_BNE);
assign is_j = (opcode == OP_J);
assign is_branch = is_beq || is_bne;
assign is_jump = is_j || is_jr;
assign branch_taken = (is_beq && (rs_val == rt_val)) ||
(is_bne && (rs_val != rt_val));
assign is_taken_branch_or_jump = branch_taken || is_jump;
// "NOP class" = anything we don't actively decode. In lenient mode
// these retire as a NOP; in strict mode the core halts on them
// (see `is_unsupported` / `strict_trap` below).
assign is_nop_class = (is_special && !is_syscall && !is_jr)
|| (is_cop0 && !is_mfc0 && !is_mtc0 && !is_rfe)
|| (!is_special && !is_cop0
&& !is_lui && !is_ori && !is_addiu
&& !is_lw && !is_sw && !is_beq && !is_bne
&& !is_j);
// The canonical NOP is the all-zero instruction word
// (SLL $0,$0,0). It is always treated as a NOP even in strict mode
// so the bios_rom_stub default NOP sled doesn't look like a field
// of traps.
logic is_nop_instr;
logic is_unsupported;
logic strict_trap;
assign is_nop_instr = (instr == 32'd0);
assign is_unsupported = is_nop_class && !is_nop_instr;
assign strict_trap = STRICT_UNSUPPORTED && is_unsupported;
// ALU writeback value (for LUI/ORI/ADDIU)
logic [31:0] alu_wb;
always_comb begin
if (is_lui) alu_wb = {imm16, 16'd0};
else if (is_ori) alu_wb = rs_val | imm_zx;
else if (is_addiu) alu_wb = rs_val + imm_sx;
else alu_wb = 32'd0;
end
// MFC0 source value (selected by rd_idx)
logic [31:0] cop0_read_val;
always_comb begin
unique case (rd_idx)
COP0_REG_STATUS: cop0_read_val = status_word;
COP0_REG_CAUSE: cop0_read_val = cause_word;
COP0_REG_EPC: cop0_read_val = epc;
default: cop0_read_val = 32'd0;
endcase
end
// Taken-branch / jump target selection
logic [31:0] taken_target;
always_comb begin
if (is_jr) taken_target = rs_val;
else if (is_j) taken_target = j_tgt;
else taken_target = branch_tgt;
end
// ------------------------------------------------------------------
// Trace book-keeping (captured at retire)
// ------------------------------------------------------------------
logic [31:0] retired_pc;
logic [31:0] retired_instr;
logic [31:0] retired_arg2;
logic [31:0] retired_arg3;
logic retired_flag_write;
logic retired_flag_read;
logic retired_flag_branch;
logic retired_flag_halt;
logic retired_flag_in_delay;
logic retired_flag_except;
logic retired_flag_rfe;
logic retired_flag_trap;
logic retire_pulse;
// ------------------------------------------------------------------
// Map-port drive (combinational on state)
// ------------------------------------------------------------------
always_comb begin
map_rd_en = 1'b0;
map_rd_addr = 32'd0;
map_wr_en = 1'b0;
map_wr_addr = 32'd0;
map_wr_data = 32'd0;
map_wr_be = 4'd0;
case (state)
S_IFETCH_REQ: begin
map_rd_en = 1'b1;
map_rd_addr = pc;
end
S_MEM_REQ: begin
map_rd_en = 1'b1;
map_rd_addr = ea;
end
S_MEM_WRITE: begin
map_wr_en = 1'b1;
map_wr_addr = ea;
map_wr_data = rt_val;
map_wr_be = 4'b1111;
end
default: ;
endcase
end
// ------------------------------------------------------------------
// Retire helper — applies pc advance, branch queuing, and
// exception entry at a clean instruction boundary.
//
// Inputs (implicit from decoded state):
// - is_taken_branch_or_jump, taken_target
// - branch_pending (current, pre-advance)
// - branch_target (the pending target if any)
// - Status / Cause state for exception gating
//
// Outputs (all registered on this clock edge):
// - pc, branch_pending, branch_target
// - epc, status/cause on exception
// - retired_flag_except set when exception fires
// ------------------------------------------------------------------
task automatic retire_advance;
logic [31:0] next_pc;
logic new_branch_pending;
logic [31:0] new_branch_target;
logic irq_pending_masked;
logic exception_now;
next_pc = branch_pending ? branch_target : pc + 32'd4;
new_branch_pending = is_taken_branch_or_jump;
new_branch_target = taken_target;
irq_pending_masked = |(cause_ip & status_im);
exception_now = !new_branch_pending
&& status_iec
&& irq_pending_masked;
if (exception_now) begin
epc <= next_pc;
cause_exc_code <= 5'h00; // Int exception code
status_ieo <= status_iep;
status_iep <= status_iec;
status_iec <= 1'b0;
status_kuo <= status_kup;
status_kup <= status_kuc;
status_kuc <= 1'b0;
pc <= EXC_VECTOR;
branch_pending <= 1'b0;
retired_flag_except <= 1'b1;
end else begin
pc <= next_pc;
branch_pending <= new_branch_pending;
if (new_branch_pending) branch_target <= new_branch_target;
retired_flag_except <= 1'b0;
end
endtask
// ------------------------------------------------------------------
// Main FSM
// ------------------------------------------------------------------
always_ff @(posedge clk) begin
if (!rst_n) begin
state <= S_IDLE;
pc <= PC_RESET;
instr <= 32'd0;
branch_pending <= 1'b0;
branch_target <= 32'd0;
instr_in_delay_slot <= 1'b0;
// COP0 reset state: interrupts disabled, mask cleared.
status_iec <= 1'b0;
status_iep <= 1'b0;
status_ieo <= 1'b0;
status_kuc <= 1'b0;
status_kup <= 1'b0;
status_kuo <= 1'b0;
status_im <= 8'd0;
cause_exc_code <= 5'd0;
cause_ip_sw <= 8'd0;
epc <= 32'd0;
retire_pulse <= 1'b0;
retired_pc <= 32'd0;
retired_instr <= 32'd0;
retired_arg2 <= 32'd0;
retired_arg3 <= 32'd0;
retired_flag_write <= 1'b0;
retired_flag_read <= 1'b0;
retired_flag_branch <= 1'b0;
retired_flag_halt <= 1'b0;
retired_flag_in_delay <= 1'b0;
retired_flag_except <= 1'b0;
retired_flag_rfe <= 1'b0;
retired_flag_trap <= 1'b0;
trap_o <= 1'b0;
trap_pc_o <= 32'd0;
trap_instr_o <= 32'd0;
for (int i = 0; i < 32; i++) regfile[i] <= 32'd0;
end else begin
retire_pulse <= 1'b0;
case (state)
S_IDLE: begin
if (go_i) state <= S_IFETCH_REQ;
end
S_IFETCH_REQ: state <= S_IFETCH_WAIT;
S_IFETCH_WAIT: begin
if (map_rd_valid) begin
instr <= map_rd_data;
instr_in_delay_slot <= branch_pending;
state <= S_EXECUTE;
end
end
S_EXECUTE: begin
// Defaults for retire bookkeeping
retired_pc <= pc;
retired_instr <= instr;
retired_arg2 <= 32'd0;
retired_arg3 <= 32'd0;
retired_flag_write <= 1'b0;
retired_flag_read <= 1'b0;
retired_flag_branch <= is_taken_branch_or_jump;
retired_flag_halt <= 1'b0;
retired_flag_in_delay <= instr_in_delay_slot;
retired_flag_except <= 1'b0;
retired_flag_rfe <= 1'b0;
retired_flag_trap <= 1'b0;
if (is_syscall) begin
// SYSCALL halts the core unconditionally; no
// exception vectoring in this scope.
retired_flag_halt <= 1'b1;
retire_pulse <= 1'b1;
state <= S_HALT;
end else if (strict_trap) begin
// Unsupported instruction under strict mode.
// Halt and latch the offending fetch; no pc
// advance, no regfile write, no COP0 side
// effect. Trap output stays asserted for the
// TB to inspect after halt_o rises.
retired_flag_trap <= 1'b1;
retire_pulse <= 1'b1;
trap_o <= 1'b1;
trap_pc_o <= pc;
trap_instr_o <= instr;
state <= S_HALT;
end else if (is_lw) begin
state <= S_MEM_REQ;
end else if (is_sw) begin
state <= S_MEM_WRITE;
end else begin
// ALU / branch / COP0 / NOP: retire in this
// cycle. Handle per-op writebacks and COP0
// side effects, then advance pc.
if ((is_lui || is_ori || is_addiu) && (rt_idx != 5'd0))
regfile[rt_idx] <= alu_wb;
if (is_mfc0 && (rt_idx != 5'd0))
regfile[rt_idx] <= cop0_read_val;
if (is_mtc0) begin
unique case (rd_idx)
COP0_REG_STATUS: begin
status_iec <= rt_val[0];
status_kuc <= rt_val[1];
status_iep <= rt_val[2];
status_kup <= rt_val[3];
status_ieo <= rt_val[4];
status_kuo <= rt_val[5];
status_im <= rt_val[15:8];
end
COP0_REG_CAUSE: begin
// Only the software IP[1:0] bits
// are writable; ExcCode is normally
// written by the core on exception
// entry, but allow SW override too
// since the minimal scope doesn't
// dispatch on ExcCode.
cause_exc_code <= rt_val[6:2];
cause_ip_sw[1:0] <= rt_val[9:8];
end
COP0_REG_EPC: epc <= rt_val;
default: ;
endcase
end
if (is_rfe) begin
status_iec <= status_iep;
status_iep <= status_ieo;
status_kuc <= status_kup;
status_kup <= status_kuo;
retired_flag_rfe <= 1'b1;
end
// Trace payload for ALU / branch / COP0 / NOP
if (is_mfc0) begin
retired_arg2 <= {27'd0, rd_idx};
retired_arg3 <= cop0_read_val;
end else if (is_mtc0) begin
retired_arg2 <= {27'd0, rd_idx};
retired_arg3 <= rt_val;
end else if (is_taken_branch_or_jump) begin
retired_arg2 <= taken_target;
retired_arg3 <= 32'd0;
end else if (is_lui || is_ori || is_addiu) begin
retired_arg3 <= alu_wb;
end
retire_pulse <= 1'b1;
retire_advance();
state <= S_IFETCH_REQ;
end
end
S_MEM_REQ: state <= S_MEM_WAIT;
S_MEM_WAIT: begin
if (map_rd_valid) begin
if (rt_idx != 5'd0) regfile[rt_idx] <= map_rd_data;
retired_pc <= pc;
retired_instr <= instr;
retired_arg2 <= ea;
retired_arg3 <= map_rd_data;
retired_flag_write <= 1'b0;
retired_flag_read <= 1'b1;
retired_flag_branch <= 1'b0;
retired_flag_halt <= 1'b0;
retired_flag_in_delay <= instr_in_delay_slot;
retired_flag_rfe <= 1'b0;
retire_pulse <= 1'b1;
retire_advance();
state <= S_IFETCH_REQ;
end
end
S_MEM_WRITE: begin
retired_pc <= pc;
retired_instr <= instr;
retired_arg2 <= ea;
retired_arg3 <= rt_val;
retired_flag_write <= 1'b1;
retired_flag_read <= 1'b0;
retired_flag_branch <= 1'b0;
retired_flag_halt <= 1'b0;
retired_flag_in_delay <= instr_in_delay_slot;
retired_flag_rfe <= 1'b0;
retire_pulse <= 1'b1;
retire_advance();
state <= S_IFETCH_REQ;
end
S_HALT: state <= S_HALT;
default: state <= S_IDLE;
endcase
end
end
assign halt_o = (state == S_HALT);
assign pc_o = pc;
// ------------------------------------------------------------------
// Trace emission — one event per retire
// ------------------------------------------------------------------
always_ff @(posedge clk) begin
if (!rst_n) begin
ev_valid <= 1'b0;
ev_subsys <= SUBSYS_IOP;
ev_event <= EV_IFETCH;
ev_arg0 <= 64'd0;
ev_arg1 <= 64'd0;
ev_arg2 <= 64'd0;
ev_arg3 <= 64'd0;
ev_flags <= 32'd0;
end else if (retire_pulse) begin
ev_valid <= 1'b1;
ev_subsys <= SUBSYS_IOP;
ev_event <= EV_IFETCH;
ev_arg0 <= {32'd0, retired_pc};
ev_arg1 <= {32'd0, retired_instr};
ev_arg2 <= {32'd0, retired_arg2};
ev_arg3 <= {32'd0, retired_arg3};
ev_flags <= {24'd0,
retired_flag_trap,
retired_flag_rfe,
retired_flag_except,
retired_flag_in_delay,
retired_flag_halt,
retired_flag_branch,
retired_flag_read,
retired_flag_write};
end else begin
ev_valid <= 1'b0;
end
end
endmodule : iop_core_stub
+319
View File
@@ -0,0 +1,319 @@
// retroDE_ps2 — iop_dmac_reg_stub
//
// IOP DMAC channel 9 (SIF0 IOP→EE) with a real, bounded data path.
// Upgraded from the earlier register+lifecycle shell: MADR is a real
// source pointer into IOP RAM, BCR is a real word count, and the
// state machine pulls 32-bit beats out of IOP RAM through the IOP map
// and emits them on a word-granularity endpoint with ready/valid/last
// handshake. Mirrors the EE DMAC shape (dmac_reg_stub) at 32-bit width.
//
// Contract refs:
// docs/contracts/iop.md (IOP DMAC ownership)
//
// Register surface (per-channel, low-byte offset):
// 0x00 MADR — real source address in IOP physical space
// 0x04 BCR — transfer length in 32-bit beats
// 0x08 CHCR — channel control; bit[0] is the start bit
// 0x0C DONE_COUNT — monotonic completion counter (read-only; writes
// are accepted but ignored). Software reads this
// to distinguish "nth completion" without needing
// to count interrupts externally.
// Other offsets: writes accepted but ignored; reads return 0.
//
// Memory master interface (to iop_memory_map_stub's dma_rd_* port):
// mem_rd_en / mem_rd_addr issue the request (one cycle)
// mem_rd_valid / mem_rd_data return the word one cycle later
// mem_master_id drives the map trace attribution (convention: 4)
//
// Endpoint (to sif_dma_ee_ram_bridge_stub or similar 32-bit sink):
// ep_valid / ep_data[31:0] / ep_last
// ep_ready is the backpressure signal — when low, the state machine
// holds in ACTIVE_SEND with the current beat. No false completion.
//
// State machine:
// IDLE → FETCH_WAIT on CHCR start
// FETCH_WAIT → ACTIVE_SEND on mem_rd_valid (word latched)
// ACTIVE_SEND → FETCH_WAIT on endpoint accept with more beats left
// → DONE on endpoint accept for the final beat
// DONE → IDLE next cycle (clears CHCR.start)
//
// Source stepping: src_addr = madr_latched + (beat_index * 4).
//
// Trace payload schema (SUBSYS_DMAC):
// DMA_CFG arg0=channel arg1=chcr arg2=madr arg3=bcr flags=reg_offset
// DMA_START arg0=channel arg1=bcr arg2=madr arg3=path_id
// DMA_BEAT arg0=channel arg1=beat_index arg2=src_addr arg3=remaining
// DMA_DONE arg0=channel arg1=beats arg2=completion_code arg3=path_id
// completion_code 0 = OK.
`timescale 1ns/1ps
module iop_dmac_reg_stub
import trace_pkg::*;
#(
parameter logic [3:0] CHANNEL = 4'd9, // SIF0 (IOP → EE)
parameter logic [3:0] PATH_ID = 4'd9,
parameter logic [7:0] MASTER_ID = 8'd4 // for dma_rd trace attribution
) (
input logic clk,
input logic rst_n,
// IOP-side register access (from the memory map's iop_dmac_* port)
input logic reg_wr_en,
input logic reg_rd_en,
input logic [3:0] reg_offset,
input logic [31:0] reg_wr_data,
output logic [31:0] reg_rd_data,
output logic reg_rd_valid,
// Memory read master (to iop_memory_map_stub dma_rd_* port)
output logic mem_rd_en,
output logic [31:0] mem_rd_addr,
output logic [7:0] mem_master_id,
input logic [31:0] mem_rd_data,
input logic mem_rd_valid,
// Endpoint (word-granularity stream to SIF egress bridge)
output logic ep_valid,
output logic [31:0] ep_data,
output logic ep_last,
input logic ep_ready,
// Completion pulse — one cycle high when the channel reaches S_DONE.
// Intended as an IOP INTC source; latching is the interrupt
// controller's responsibility.
output logic irq_completion_o,
// Status
output logic busy_o,
output logic [31:0] done_count_o,
// 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 [3:0] MADR_OFFSET = 4'h0;
localparam logic [3:0] BCR_OFFSET = 4'h4;
localparam logic [3:0] CHCR_OFFSET = 4'h8;
localparam logic [3:0] DONE_COUNT_OFFSET = 4'hC;
typedef enum logic [1:0] {
S_IDLE = 2'd0,
S_FETCH_WAIT = 2'd1,
S_ACTIVE_SEND = 2'd2,
S_DONE = 2'd3
} state_e;
logic [31:0] madr;
logic [31:0] bcr;
logic [31:0] chcr;
state_e state;
logic [31:0] madr_latched;
logic [31:0] bcr_latched;
logic [31:0] beat_index;
logic [31:0] beat_payload;
logic start_pulse;
assign start_pulse = reg_wr_en && (reg_offset == CHCR_OFFSET)
&& reg_wr_data[0] && !chcr[0];
// ------------------------------------------------------------------
// Register file
// ------------------------------------------------------------------
always_ff @(posedge clk) begin
if (!rst_n) begin
madr <= 32'd0;
bcr <= 32'd0;
chcr <= 32'd0;
end else begin
if (reg_wr_en) begin
case (reg_offset)
MADR_OFFSET: madr <= reg_wr_data;
BCR_OFFSET: bcr <= reg_wr_data;
CHCR_OFFSET: chcr <= reg_wr_data;
default: ;
endcase
end
if (state == S_DONE) chcr[0] <= 1'b0;
end
end
// ------------------------------------------------------------------
// Register read (1-cycle latency, matches rest of stub ecosystem)
// ------------------------------------------------------------------
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
case (reg_offset)
MADR_OFFSET: reg_rd_data <= madr;
BCR_OFFSET: reg_rd_data <= bcr;
CHCR_OFFSET: reg_rd_data <= chcr;
DONE_COUNT_OFFSET: reg_rd_data <= done_count_o;
default: reg_rd_data <= 32'd0;
endcase
end
end
end
// ------------------------------------------------------------------
// Transfer state machine
// ------------------------------------------------------------------
logic [31:0] src_addr;
assign src_addr = madr_latched + (beat_index << 2); // 4 bytes/beat
logic beat_accepted;
assign beat_accepted = ep_valid && ep_ready;
// Pulse mem_rd_en for one cycle whenever we first enter FETCH_WAIT.
logic prev_state_fw;
always_ff @(posedge clk) begin
if (!rst_n) prev_state_fw <= 1'b0;
else prev_state_fw <= (state == S_FETCH_WAIT);
end
logic entering_fw;
assign entering_fw = (state == S_FETCH_WAIT) && !prev_state_fw;
assign mem_rd_en = entering_fw;
assign mem_rd_addr = src_addr;
assign mem_master_id = MASTER_ID;
// Drive endpoint only in ACTIVE_SEND with the latched payload.
assign ep_valid = (state == S_ACTIVE_SEND);
assign ep_data = beat_payload;
assign ep_last = (state == S_ACTIVE_SEND) &&
(beat_index + 32'd1 == bcr_latched);
always_ff @(posedge clk) begin
if (!rst_n) begin
state <= S_IDLE;
madr_latched <= 32'd0;
bcr_latched <= 32'd0;
beat_index <= 32'd0;
beat_payload <= 32'd0;
end else begin
unique case (state)
S_IDLE: begin
if (start_pulse) begin
state <= S_FETCH_WAIT;
madr_latched <= madr;
bcr_latched <= bcr;
beat_index <= 32'd0;
end
end
S_FETCH_WAIT: begin
if (mem_rd_valid) begin
beat_payload <= mem_rd_data;
state <= S_ACTIVE_SEND;
end
end
S_ACTIVE_SEND: begin
if (beat_accepted) begin
if (beat_index + 32'd1 == bcr_latched) begin
state <= S_DONE;
end else begin
beat_index <= beat_index + 32'd1;
state <= S_FETCH_WAIT;
end
end
end
S_DONE: begin
state <= S_IDLE;
end
default: state <= S_IDLE;
endcase
end
end
assign busy_o = (state != S_IDLE);
assign irq_completion_o = (state == S_DONE);
// ------------------------------------------------------------------
// Trace emission — one event per cycle. Priority:
// DONE > BEAT > START > CFG (register write)
// ------------------------------------------------------------------
logic prev_in_transfer;
always_ff @(posedge clk) begin
if (!rst_n) prev_in_transfer <= 1'b0;
else prev_in_transfer <= (state != S_IDLE);
end
logic enter_start;
assign enter_start = (state == S_FETCH_WAIT) && !prev_in_transfer;
logic enter_done;
assign enter_done = (state == S_DONE);
always_ff @(posedge clk) begin
if (!rst_n) begin
ev_valid <= 1'b0;
ev_subsys <= SUBSYS_DMAC;
ev_event <= EV_DMA_CFG;
ev_arg0 <= 64'd0;
ev_arg1 <= 64'd0;
ev_arg2 <= 64'd0;
ev_arg3 <= 64'd0;
ev_flags <= 32'd0;
done_count_o <= 32'd0;
end else if (enter_done) begin
ev_valid <= 1'b1;
ev_subsys <= SUBSYS_DMAC;
ev_event <= EV_DMA_DONE;
ev_arg0 <= {60'd0, CHANNEL};
ev_arg1 <= {32'd0, beat_index + 32'd1}; // beats completed
ev_arg2 <= 64'd0; // completion OK
ev_arg3 <= {60'd0, PATH_ID};
ev_flags <= 32'd0;
done_count_o <= done_count_o + 32'd1;
end else if (beat_accepted) begin
ev_valid <= 1'b1;
ev_subsys <= SUBSYS_DMAC;
ev_event <= EV_DMA_BEAT;
ev_arg0 <= {60'd0, CHANNEL};
ev_arg1 <= {32'd0, beat_index};
ev_arg2 <= {32'd0, src_addr};
ev_arg3 <= {32'd0, bcr_latched - beat_index - 32'd1};
ev_flags <= 32'd0;
end else if (enter_start) begin
ev_valid <= 1'b1;
ev_subsys <= SUBSYS_DMAC;
ev_event <= EV_DMA_START;
ev_arg0 <= {60'd0, CHANNEL};
ev_arg1 <= {32'd0, bcr_latched};
ev_arg2 <= {32'd0, madr_latched};
ev_arg3 <= {60'd0, PATH_ID};
ev_flags <= 32'd0;
end else if (reg_wr_en) begin
ev_valid <= 1'b1;
ev_subsys <= SUBSYS_DMAC;
ev_event <= EV_DMA_CFG;
ev_arg0 <= {60'd0, CHANNEL};
ev_arg1 <= {32'd0, (reg_offset == CHCR_OFFSET) ? reg_wr_data : chcr};
ev_arg2 <= {32'd0, (reg_offset == MADR_OFFSET) ? reg_wr_data : madr};
ev_arg3 <= {32'd0, (reg_offset == BCR_OFFSET) ? reg_wr_data : bcr};
ev_flags <= {28'd0, reg_offset};
end else begin
ev_valid <= 1'b0;
end
end
endmodule : iop_dmac_reg_stub
+320
View File
@@ -0,0 +1,320 @@
// retroDE_ps2 — iop_exec_stub
//
// First RAM-backed IOP execution primitive. Micro-op fetch now comes
// from IOP RAM through the real `iop_memory_map_stub` CPU-side port —
// the same way a future MIPS-class CPU would fetch instructions. The
// control program is no longer RTL-resident; it lives as data in RAM
// that someone (a TB, eventually a BIOS / loader path) preloads before
// pulsing `go_i`.
//
// NOT a MIPS core, NOT an ISA decoder. A tiny FSM sequencer over a
// five-opcode micro-op ISA, designed as the bridge between "testbench
// choreographs everything" and a real instruction-fetching CPU. When
// the real CPU arrives, it replaces this module but keeps the same
// map / DMA / INTC hookup verbatim.
//
// Contract refs:
// docs/contracts/iop.md (IOP-local programming model)
//
// Opcodes (encoded in word 0 low nibble):
// OP_HALT 0x0 — terminal; halt_o rises, no further accesses.
// OP_WRITE 0x1 — pulse map CPU write with (addr, data). pc++
// OP_READ 0x2 — pulse map CPU read; latch into last_read_data.
// pc++
// OP_WAIT_IRQ 0x3 — block until cpu_irq==1. pc++
// OP_BNE 0x4 — if last_read_data != expected, pc <= target;
// else pc++.
// target is in word1[7:0]; expected is in word2.
//
// Micro-op layout in RAM (16 bytes per op, little-endian word order):
// +0 word 0: {28'd0, opcode[3:0]}
// +4 word 1: addr (for WRITE/READ) or target_pc in low 8 bits (for BNE)
// +8 word 2: data (for WRITE) or expected value (for BNE); unused for
// READ/WAIT_IRQ/HALT
// +12 word 3: reserved for future opcodes
//
// Fetch sequence: three map reads per op (words 0/1/2). Word 3 is
// skipped to save a cycle. Each read has one-cycle latency via the
// map — so a full fetch is ~6 cycles, after which dispatch takes one
// more cycle. Negligible in the current scope; swap the engine for a
// real CPU later and the instruction width stops mattering.
//
// Trace payload (SUBSYS_IOP, EV_IFETCH, emitted on each op completion):
// arg0 = pc value of the op that just completed
// arg1 = opcode
// arg2 = addr (0 for WAIT_IRQ/HALT)
// arg3 = data written, data read back, expected (for BNE), or 0
// flags bit 0 = 1 → write-flavour op
// flags bit 1 = 1 → WAIT_IRQ just exited (IRQ observed)
// flags bit 2 = 1 → HALT entered
// flags bit 3 = 1 → BNE branch taken (pc changed to target, not +1)
`timescale 1ns/1ps
module iop_exec_stub
import trace_pkg::*;
#(
parameter logic [31:0] SCRIPT_BASE = 32'h0000_0400
) (
input logic clk,
input logic rst_n,
input logic go_i,
// Drive the IOP memory map's CPU-side port. Both ifetch reads and
// the script's own WRITE/READ ops flow through here.
output logic map_rd_en,
output logic [31:0] map_rd_addr,
input logic [31:0] map_rd_data,
input logic map_rd_valid,
output logic map_wr_en,
output logic [31:0] map_wr_addr,
output logic [31:0] map_wr_data,
output logic [3:0] map_wr_be,
input logic cpu_irq,
output logic halt_o,
output logic [7:0] pc_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
);
localparam logic [3:0] OP_HALT = 4'h0;
localparam logic [3:0] OP_WRITE = 4'h1;
localparam logic [3:0] OP_READ = 4'h2;
localparam logic [3:0] OP_WAIT_IRQ = 4'h3;
localparam logic [3:0] OP_BNE = 4'h4;
typedef enum logic [3:0] {
S_IDLE = 4'd0,
S_IF0_REQ = 4'd1,
S_IF0_WAIT = 4'd2,
S_IF1_REQ = 4'd3,
S_IF1_WAIT = 4'd4,
S_IF2_REQ = 4'd5,
S_IF2_WAIT = 4'd6,
S_DECODE = 4'd7,
S_WRITE = 4'd8,
S_READ_REQ = 4'd9,
S_READ_WAIT = 4'd10,
S_WAIT_IRQ = 4'd11,
S_BNE = 4'd12,
S_HALT = 4'd13
} state_e;
state_e state;
logic [7:0] pc;
logic [3:0] cur_opcode;
logic [31:0] cur_addr;
logic [31:0] cur_data;
logic [31:0] last_read_data;
// Op-completion event triggers (one-cycle pulses)
logic ev_op_done;
logic ev_wait_irq_exit;
logic ev_enter_halt;
logic ev_bne_taken;
// Address for the next ifetch word: SCRIPT_BASE + pc*16 + word_offset
logic [31:0] ifetch_base;
assign ifetch_base = SCRIPT_BASE + {20'd0, pc, 4'd0}; // pc << 4
// ------------------------------------------------------------------
// Map-port drive (combinational on state)
// ------------------------------------------------------------------
always_comb begin
map_wr_en = 1'b0;
map_wr_addr = 32'd0;
map_wr_data = 32'd0;
map_wr_be = 4'd0;
map_rd_en = 1'b0;
map_rd_addr = 32'd0;
case (state)
S_IF0_REQ: begin
map_rd_en = 1'b1;
map_rd_addr = ifetch_base + 32'd0;
end
S_IF1_REQ: begin
map_rd_en = 1'b1;
map_rd_addr = ifetch_base + 32'd4;
end
S_IF2_REQ: begin
map_rd_en = 1'b1;
map_rd_addr = ifetch_base + 32'd8;
end
S_WRITE: begin
map_wr_en = 1'b1;
map_wr_addr = cur_addr;
map_wr_data = cur_data;
map_wr_be = 4'b1111;
end
S_READ_REQ: begin
map_rd_en = 1'b1;
map_rd_addr = cur_addr;
end
default: ;
endcase
end
// ------------------------------------------------------------------
// State machine
// ------------------------------------------------------------------
always_ff @(posedge clk) begin
if (!rst_n) begin
state <= S_IDLE;
pc <= 8'd0;
cur_opcode <= 4'd0;
cur_addr <= 32'd0;
cur_data <= 32'd0;
last_read_data <= 32'd0;
ev_op_done <= 1'b0;
ev_wait_irq_exit <= 1'b0;
ev_enter_halt <= 1'b0;
ev_bne_taken <= 1'b0;
end else begin
ev_op_done <= 1'b0;
ev_wait_irq_exit <= 1'b0;
ev_enter_halt <= 1'b0;
ev_bne_taken <= 1'b0;
case (state)
S_IDLE: begin
if (go_i) begin
pc <= 8'd0;
state <= S_IF0_REQ;
end
end
S_IF0_REQ: state <= S_IF0_WAIT;
S_IF0_WAIT: if (map_rd_valid) begin
cur_opcode <= map_rd_data[3:0];
state <= S_IF1_REQ;
end
S_IF1_REQ: state <= S_IF1_WAIT;
S_IF1_WAIT: if (map_rd_valid) begin
cur_addr <= map_rd_data;
state <= S_IF2_REQ;
end
S_IF2_REQ: state <= S_IF2_WAIT;
S_IF2_WAIT: if (map_rd_valid) begin
cur_data <= map_rd_data;
state <= S_DECODE;
end
S_DECODE: begin
case (cur_opcode)
OP_HALT: begin
state <= S_HALT;
ev_enter_halt <= 1'b1;
end
OP_WRITE: state <= S_WRITE;
OP_READ: state <= S_READ_REQ;
OP_WAIT_IRQ: state <= S_WAIT_IRQ;
OP_BNE: state <= S_BNE;
default: state <= S_HALT; // unknown opcode → safe stop
endcase
end
S_WRITE: begin
pc <= pc + 8'd1;
state <= S_IF0_REQ;
ev_op_done <= 1'b1;
end
S_READ_REQ: state <= S_READ_WAIT;
S_READ_WAIT: if (map_rd_valid) begin
last_read_data <= map_rd_data;
pc <= pc + 8'd1;
state <= S_IF0_REQ;
ev_op_done <= 1'b1;
end
S_WAIT_IRQ: begin
if (cpu_irq) begin
pc <= pc + 8'd1;
state <= S_IF0_REQ;
ev_op_done <= 1'b1;
ev_wait_irq_exit <= 1'b1;
end
end
S_BNE: begin
// target_pc = cur_addr[7:0], expected = cur_data
if (last_read_data != cur_data) begin
pc <= cur_addr[7:0];
ev_bne_taken <= 1'b1;
end else begin
pc <= pc + 8'd1;
end
state <= S_IF0_REQ;
ev_op_done <= 1'b1;
end
S_HALT: state <= S_HALT;
default: state <= S_IDLE;
endcase
end
end
assign halt_o = (state == S_HALT);
assign pc_o = pc;
// ------------------------------------------------------------------
// Trace emission. One event per op completion + one on HALT entry.
// ------------------------------------------------------------------
always_ff @(posedge clk) begin
if (!rst_n) begin
ev_valid <= 1'b0;
ev_subsys <= SUBSYS_IOP;
ev_event <= EV_IFETCH;
ev_arg0 <= 64'd0;
ev_arg1 <= 64'd0;
ev_arg2 <= 64'd0;
ev_arg3 <= 64'd0;
ev_flags <= 32'd0;
end else if (ev_enter_halt) begin
ev_valid <= 1'b1;
ev_subsys <= SUBSYS_IOP;
ev_event <= EV_IFETCH;
ev_arg0 <= {56'd0, pc};
ev_arg1 <= {60'd0, cur_opcode};
ev_arg2 <= 64'd0;
ev_arg3 <= 64'd0;
ev_flags <= 32'h0000_0004; // halt marker
end else if (ev_op_done) begin
ev_valid <= 1'b1;
ev_subsys <= SUBSYS_IOP;
ev_event <= EV_IFETCH;
ev_arg0 <= {56'd0, pc};
ev_arg1 <= {60'd0, cur_opcode};
ev_arg2 <= {32'd0, cur_addr};
ev_arg3 <= (cur_opcode == OP_READ)
? {32'd0, map_rd_data}
: {32'd0, cur_data};
ev_flags <= {28'd0,
ev_bne_taken,
1'b0, // (was halt; halt has its own path above)
ev_wait_irq_exit,
(cur_opcode == OP_WRITE)};
end else begin
ev_valid <= 1'b0;
end
end
endmodule : iop_exec_stub
+128
View File
@@ -0,0 +1,128 @@
// retroDE_ps2 — iop_fetch_stub
//
// Minimal IOP-side sequential fetcher. Mirrors ee_fetch_stub in shape and
// discipline — just the smallest honest primitive that produces visible
// IOP-side execution-flow traffic. Not a CPU. Explicitly NOT a BIOS boot
// stub: the default reset vector lives in IOP RAM, not in BIOS space.
//
// Contract refs:
// docs/contracts/iop.md (IOP CPU execution, required debug
// visibility: PC stream)
//
// Behavior:
// - On reset, PC = RESET_VECTOR (default 0x00000000, the low end of
// IOP RAM).
// - Each cycle while `enable` is high: issue a 32-bit read at PC,
// advance PC += 4. No decode, no branches, no exceptions, no FPU.
// - Responses return 1 cycle later via rd_valid/rd_data from the
// map. The issued address is latched (pc_d1) so trace lines pair
// address with data.
//
// Non-goals:
// - full decode
// - branch / exception / interrupt handling
// - real IOP R3000 pipeline timing
// - BIOS fetch (use a BIOS-pointing RESET_VECTOR param override if
// needed, but that's intentionally not the default)
//
// Trace payload schema (matches ee_fetch_stub structure under SUBSYS_IOP):
// IOP RESET arg0=reset_vector
// IOP IFETCH arg0=pc arg1=data arg2=resp_kind arg3=-
// resp_kind: 0=OK (only path in this scope)
`timescale 1ns/1ps
module iop_fetch_stub
import trace_pkg::*;
#(
parameter logic [31:0] RESET_VECTOR = 32'h0000_0000
) (
input logic clk,
input logic rst_n,
input logic enable,
// Map-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 issued-address shadow (same pattern as ee_fetch_stub):
// 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_IOP;
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_IOP;
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_IOP;
ev_event <= EV_IFETCH;
ev_arg0 <= {32'd0, pc_d1};
ev_arg1 <= {32'd0, rd_data};
ev_arg2 <= 64'd0;
ev_arg3 <= 64'd0;
ev_flags <= 32'd0;
end else begin
ev_valid <= 1'b0;
end
end
endmodule : iop_fetch_stub
+652
View File
@@ -0,0 +1,652 @@
// retroDE_ps2 — iop_memory_map_stub
//
// IOP-side memory map. Gives IOP-visible addresses architectural meaning.
// Wave 3 first-pass scope is deliberately narrow: only the IOP-RAM window
// is routed; every other address decodes as UNMAPPED with deterministic
// fault data. SIF registers, IOP I/O, SPU2, CDVD, and IOP-side BIOS are
// all intentionally deferred — slots noted in comments so the map can
// grow without re-shaping its interface.
//
// Contract refs:
// docs/contracts/iop.md (IOP-local address decode)
// docs/contracts/memory.md (IOP RAM lives at phys 0x00000000-
// 0x001FFFFF, 2 MiB)
//
// Address semantics:
// IOP CPU side is MIPS R3000-class. kseg0/kseg1 aliases
// (0x80000000/0xA0000000 mirrors of 0x00000000) are modelled via
// `phys = iop_addr[28:0]`, consistent with how ee_memory_map_stub
// strips kseg for EE fetches. Physical window decode then works on
// the low 29 bits.
//
// Bridge-side write port (Wave 3 addition): the SIF-to-IOP-RAM bridge
// writes directly at physical offsets — no kseg strip. The map decodes
// its address bits directly against the same region rules.
//
// DMA read-master port (Wave 3 reverse-direction addition): the IOP
// DMAC (ch9 SIF0 egress, and any other future channel) fetches source
// bytes through this port. Physical addressing; RAM-only decode in
// current scope. Caller provides its own master_id (convention: 4 =
// IOP_DMAC).
//
// Arbitration (Wave 3 scope):
// Two potential write masters on the RAM path: the IOP CPU port and
// the bridge port. Two potential read masters on the RAM path: the
// IOP CPU port and the DMA read master. Collisions within the same
// cycle are not expected in the current TBs (CPU programming / readback
// phases are separate from DMA transfer phases). Policy if they ever
// collide: CPU wins. Documented here rather than hidden in priority
// ordering; RAM port is mux'd accordingly.
//
// Region decode (current):
// - IOP RAM window: phys[28:21] == 8'b00000000
// (0x0000_0000 - 0x001F_FFFF, 2 MiB)
// → route to iop_ram_stub (offset phys[20:0])
// - SIF registers (IOP side): phys[28:24] == 5'b11101
// (0x1D00_0000 block) → route to the SIF
// register shell with offset phys[7:0]. The
// mailbox stub's register surface covers
// offsets 0x00/0x10/0x20/0x30.
// - IOP DMAC channel 9: phys[28:4] == 25'h01F8_0152
// (0x1F80_1520 - 0x1F80_152F, 16 bytes)
// → route to iop_dmac_reg_stub with 4-bit
// offset phys[3:0]. Channel 9 is SIF0
// (IOP→EE) in the real PS2 DMAC map; other
// channels are intentionally not decoded.
// - IOP INTC: phys[28:4] == 25'h01F8_0107
// (0x1F80_1070 - 0x1F80_107F, 16 bytes)
// → route to intc_stub with 8-bit offset
// phys[7:0]. Matches the real PS2 IOP INTC
// placement (I_STAT / I_MASK).
// - Shared BIOS ROM: phys[28:22] == 7'b1111111
// (0x1FC0_0000 - 0x1FFF_FFFF, 4 MiB)
// → route to bios_rom_stub with 22-bit
// offset phys[21:0]. kseg1 aliasing maps
// 0xBFC0_0000 fetches to this window via
// the standard [28:0] strip. The IOP core
// reset vector normally points here.
// Writes to BIOS decode as UNMAPPED
// (read-only ROM).
// - everything else: UNMAPPED, reads return 32'hDEADBEEF
//
// Future regions (reserved in comments, not wired):
// - Other IOP DMAC channels: 0x1F80_1080-0x1F80_156F (partial block)
// - IOP timers / SIO: elsewhere in 0x1F80_0000 block
// - SPU2: 0x1F90_0000 block
//
// Trace semantics (matches ee_memory_map_stub's request-routing pattern):
// Map-layer events describe routing (what was asked for, where it was
// sent). Arg1 is 0 when the request is routed to a backing store that
// will emit its own delivery event; 0xDEADBEEF on unmapped reads; the
// actual write data on unmapped writes (so the TB can see what software
// tried to write).
//
// Latency assumption (mirrors ee_memory_map_stub note):
// Assumes fixed one-cycle backing-store latency. `ram_rd_valid` is not
// consulted — the map asserts its own `iop_rd_valid` one cycle after
// request unconditionally. All Wave 3 backing stubs honour that. If a
// later backing store introduces wait states, the map must grow proper
// response handshaking.
//
// Trace payload schema:
// IOP READ arg0=addr arg1=0 arg2=master_id arg3=region_id
// IOP WRITE arg0=addr arg1=wr_data arg2=master_id arg3=region_id
// IOP UNMAPPED arg0=addr arg1=0xDEADBEEF arg2=master_id arg3=0xFF
// region_id: 2 = IOP_RAM, 3 = SIF_REGS, 4 = IOP_DMAC, 5 = IOP_INTC,
// 6 = IOP_BIOS
// master_id: 2 = IOP_CPU, 3 = SIF bridge (writes), 4 = IOP_DMAC (reads)
// flags bit 0: 1 = write, 0 = read
`timescale 1ns/1ps
module iop_memory_map_stub
import trace_pkg::*;
(
input logic clk,
input logic rst_n,
// ------------------------------------------------------------------
// IOP CPU-side request interface (32-bit data, virtual address)
// ------------------------------------------------------------------
input logic iop_rd_en,
input logic [31:0] iop_rd_addr,
output logic [31:0] iop_rd_data,
output logic iop_rd_valid,
input logic iop_wr_en,
input logic [31:0] iop_wr_addr,
input logic [31:0] iop_wr_data,
input logic [3:0] iop_wr_be,
// Caller-provided master id for trace attribution. Conventional:
// 0 = TB direct, 2 = IOP CPU (once a fetch stub exists).
input logic [7:0] master_id,
// ------------------------------------------------------------------
// Bridge-side write port (Wave 3). Physical addresses; no kseg strip.
// Used by sif_dma_iop_ram_bridge_stub and similar DMA-side masters.
// Caller provides its own master_id (convention: 3 = SIF bridge).
// ------------------------------------------------------------------
input logic bridge_wr_en,
input logic [31:0] bridge_wr_addr,
input logic [31:0] bridge_wr_data,
input logic [3:0] bridge_wr_be,
input logic [7:0] bridge_master_id,
// ------------------------------------------------------------------
// DMA read-master port (Wave 3). Physical addressing; intended for
// IOP DMAC ch9 reads out of IOP RAM. One-cycle read latency, same
// pipeline shape as the CPU read. Caller provides its own master_id
// (convention: 4 = IOP_DMAC).
// ------------------------------------------------------------------
input logic dma_rd_en,
input logic [31:0] dma_rd_addr,
input logic [7:0] dma_master_id,
output logic [31:0] dma_rd_data,
output logic dma_rd_valid,
// ------------------------------------------------------------------
// Downstream to iop_ram_stub.
// Address presented as a 21-bit offset within the 2 MiB IOP RAM
// window; consumers may truncate to match their backing-store width.
// ------------------------------------------------------------------
output logic ram_rd_en,
output logic [20:0] ram_rd_addr,
input logic [31:0] ram_rd_data,
input logic ram_rd_valid,
output logic ram_wr_en,
output logic [20:0] ram_wr_addr,
output logic [31:0] ram_wr_data,
output logic [3:0] ram_wr_be,
output logic [7:0] ram_master_id,
// ------------------------------------------------------------------
// Downstream to the SIF register shell (sif_mailbox_stub IOP-side
// port). Low byte of the physical address is presented; writes go
// out with the CPU's data/be; reads come back with 1-cycle latency
// consistent with the rest of the stub ecosystem.
// ------------------------------------------------------------------
output logic sif_rd_en,
output logic [7:0] sif_rd_addr,
input logic [31:0] sif_rd_data,
input logic sif_rd_valid,
output logic sif_wr_en,
output logic [7:0] sif_wr_addr,
output logic [31:0] sif_wr_data,
// ------------------------------------------------------------------
// Downstream to the IOP DMAC register shell (channel 9). 4-bit
// offset; data path uses the CPU write data. Read returns with
// one-cycle latency like the rest of the stub ecosystem.
// ------------------------------------------------------------------
output logic iop_dmac_rd_en,
output logic [3:0] iop_dmac_rd_addr,
input logic [31:0] iop_dmac_rd_data,
input logic iop_dmac_rd_valid,
output logic iop_dmac_wr_en,
output logic [3:0] iop_dmac_wr_addr,
output logic [31:0] iop_dmac_wr_data,
// ------------------------------------------------------------------
// Downstream to the IOP INTC register shell (intc_stub reused).
// 8-bit offset passed downstream; read returns with one-cycle
// latency consistent with the rest of the stub ecosystem.
// ------------------------------------------------------------------
output logic iop_intc_rd_en,
output logic [7:0] iop_intc_rd_addr,
input logic [31:0] iop_intc_rd_data,
input logic iop_intc_rd_valid,
output logic iop_intc_wr_en,
output logic [7:0] iop_intc_wr_addr,
output logic [31:0] iop_intc_wr_data,
// ------------------------------------------------------------------
// Downstream to bios_rom_stub (shared BIOS window).
// 22-bit byte offset within the 4 MiB window. Writes are never
// forwarded (BIOS is ROM); the map routes any bios-window write
// attempt to the UNMAPPED trace event instead.
// ------------------------------------------------------------------
output logic bios_rd_en,
output logic [21:0] bios_rd_addr,
input logic [31:0] bios_rd_data,
input logic bios_rd_valid,
// ------------------------------------------------------------------
// Ch234 — bridge-clock-domain pad bitmaps from ps2_hps_bridge
// (INPUT_P1/P2 latches @ 0x040/0x044). Sync'd into the IOP clock
// by the internal `sio2_input_stub` instance below. TBs that
// don't exercise the pad path can tie both ports to `32'd0`.
// ------------------------------------------------------------------
input logic [31:0] input_p1,
input logic [31:0] input_p2,
// ------------------------------------------------------------------
// 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 [63:0] REGION_IOP_RAM = 64'd2;
localparam logic [63:0] REGION_SIF_REGS = 64'd3;
localparam logic [63:0] REGION_IOP_DMAC = 64'd4;
localparam logic [63:0] REGION_IOP_INTC = 64'd5;
localparam logic [63:0] REGION_IOP_BIOS = 64'd6;
localparam logic [63:0] REGION_PAD_IO = 64'd7; // Ch234
localparam logic [63:0] REGION_UNMAPPED = 64'hFF;
localparam logic [28:0] DMAC_CH9_BASE = 29'h1F80_1520;
localparam logic [28:0] IOP_INTC_BASE = 29'h1F80_1070;
// Ch234 — retroDE-local pad I/O window (256 bytes), deliberately
// OUTSIDE the real SIO2 range (0x1F80_8200..0x1F80_82FF) so a
// faithful SIO2 emulation chapter can land later without collision.
localparam logic [28:0] PAD_IO_BASE = 29'h1F80_8500;
// ------------------------------------------------------------------
// Region decode (combinational, shared for read + write)
// ------------------------------------------------------------------
// CPU-side decode (virtual → physical via kseg strip)
logic [28:0] rd_phys_addr;
logic [28:0] wr_phys_addr;
logic rd_is_ram;
logic rd_is_sif;
logic rd_is_dmac;
logic rd_is_intc;
logic rd_is_bios;
logic rd_is_pad; // Ch234
logic cpu_wr_is_ram;
logic cpu_wr_is_sif;
logic cpu_wr_is_dmac;
logic cpu_wr_is_intc;
logic cpu_wr_is_bios;
logic cpu_wr_is_pad; // Ch234
logic [20:0] rd_ram_offset;
logic [20:0] cpu_wr_ram_offset;
assign rd_phys_addr = iop_rd_addr[28:0];
assign wr_phys_addr = iop_wr_addr[28:0];
assign rd_is_ram = (rd_phys_addr[28:21] == 8'd0);
assign rd_is_sif = (rd_phys_addr[28:24] == 5'b11101);
assign rd_is_dmac = (rd_phys_addr[28:4] == DMAC_CH9_BASE[28:4]);
assign rd_is_intc = (rd_phys_addr[28:4] == IOP_INTC_BASE[28:4]);
assign rd_is_bios = (rd_phys_addr[28:22] == 7'b1111111);
// Ch234 — pad I/O region is 256 bytes at PAD_IO_BASE, so the
// decode is bits [28:8] (= 21 high bits of the 29-bit phys addr).
assign rd_is_pad = (rd_phys_addr[28:8] == PAD_IO_BASE[28:8]);
assign cpu_wr_is_ram = (wr_phys_addr[28:21] == 8'd0);
assign cpu_wr_is_sif = (wr_phys_addr[28:24] == 5'b11101);
assign cpu_wr_is_dmac = (wr_phys_addr[28:4] == DMAC_CH9_BASE[28:4]);
assign cpu_wr_is_intc = (wr_phys_addr[28:4] == IOP_INTC_BASE[28:4]);
assign cpu_wr_is_bios = (wr_phys_addr[28:22] == 7'b1111111);
assign cpu_wr_is_pad = (wr_phys_addr[28:8] == PAD_IO_BASE[28:8]);
assign rd_ram_offset = rd_phys_addr[20:0];
assign cpu_wr_ram_offset = wr_phys_addr[20:0];
// Bridge-side decode (physical, no strip). Bridge writes are routed
// to IOP RAM only — no SIF destination from the bridge side yet.
logic bridge_wr_is_ram;
logic [20:0] bridge_wr_ram_offset;
assign bridge_wr_is_ram = (bridge_wr_addr[28:21] == 8'd0);
assign bridge_wr_ram_offset = bridge_wr_addr[20:0];
// DMA-side read decode (physical, no strip). Scope covers RAM only.
logic dma_rd_is_ram;
logic [20:0] dma_rd_ram_offset;
assign dma_rd_is_ram = (dma_rd_addr[28:21] == 8'd0);
assign dma_rd_ram_offset = dma_rd_addr[20:0];
// RAM routing. Ch261 — DMA wins the port on CPU+DMA collision; the
// CPU's read address is latched into a one-entry pending slot and
// serviced on the next RAM cycle that the DMA does not consume.
// Pre-Ch261 the comment above this block was "CPU read wins over
// DMA read on same-cycle collision" but the silent consequence was
// the DMA path sampling `ram_rd_data` from the CPU's address —
// silent DMA data corruption. The Ch261 SIF-landing TB found it.
//
// Single-entry slot is sufficient because every existing CPU
// client of this map is request-then-wait-for-valid (no second
// outstanding read in flight): exec stub, iop_core_stub, fetch
// stub all stall in their own wait state until `iop_rd_valid`
// asserts. A sim-only overflow assertion below catches any future
// client that breaks that assumption.
logic cpu_rd_hit;
logic dma_rd_hit;
logic cpu_dma_collision;
assign cpu_rd_hit = iop_rd_en && rd_is_ram;
assign dma_rd_hit = dma_rd_en && dma_rd_is_ram;
assign cpu_dma_collision = cpu_rd_hit && dma_rd_hit;
// One-entry deferred CPU-RAM-read slot.
logic cpu_pend_valid;
logic [20:0] cpu_pend_addr;
// Service priority (mutually exclusive):
// serve_dma — DMA wins the bus any cycle it asks
// serve_cpu_def — deferred CPU read services on the next non-DMA cycle
// serve_cpu_now — live CPU read services when neither of the above fires
logic serve_dma;
logic serve_cpu_def;
logic serve_cpu_now;
assign serve_dma = dma_rd_hit;
assign serve_cpu_def = !dma_rd_hit && cpu_pend_valid;
assign serve_cpu_now = !dma_rd_hit && !cpu_pend_valid && cpu_rd_hit;
assign ram_rd_en = serve_dma || serve_cpu_def || serve_cpu_now;
assign ram_rd_addr = serve_dma ? dma_rd_ram_offset
: serve_cpu_def ? cpu_pend_addr
: rd_ram_offset;
// Slot update: latch on collision, clear on service.
always_ff @(posedge clk) begin
if (!rst_n) begin
cpu_pend_valid <= 1'b0;
cpu_pend_addr <= 21'd0;
end else begin
if (cpu_dma_collision && !cpu_pend_valid) begin
cpu_pend_valid <= 1'b1;
cpu_pend_addr <= rd_ram_offset;
end else if (serve_cpu_def) begin
cpu_pend_valid <= 1'b0;
end
end
end
`ifndef SYNTHESIS
// Overflow detector: a second CPU+DMA collision while the slot is
// already pending means we'd drop the new CPU read silently. The
// current set of CPU clients can't trigger this (single outstanding
// read each), but future producers should fail loudly here.
always_ff @(posedge clk) begin
if (rst_n && cpu_dma_collision && cpu_pend_valid) begin
$error("[iop_memory_map_stub] Ch261 deferred-CPU-slot overflow: cpu_dma_collision while cpu_pend_valid (live addr=0x%05h pending addr=0x%05h)",
rd_ram_offset, cpu_pend_addr);
end
end
`endif
// SIF register-shell routing. Low byte of the physical address is
// presented downstream (mailbox uses 8-bit offsets).
assign sif_rd_en = iop_rd_en && rd_is_sif;
assign sif_rd_addr = rd_phys_addr[7:0];
assign sif_wr_en = iop_wr_en && cpu_wr_is_sif;
assign sif_wr_addr = wr_phys_addr[7:0];
assign sif_wr_data = iop_wr_data;
// IOP DMAC ch9 routing. Low 4 bits of the physical address select
// among MADR / BCR / CHCR (and any other in-block offsets).
assign iop_dmac_rd_en = iop_rd_en && rd_is_dmac;
assign iop_dmac_rd_addr = rd_phys_addr[3:0];
assign iop_dmac_wr_en = iop_wr_en && cpu_wr_is_dmac;
assign iop_dmac_wr_addr = wr_phys_addr[3:0];
assign iop_dmac_wr_data = iop_wr_data;
// IOP INTC routing. Low byte of the physical address selects
// INTC_STAT (0x00) or INTC_MASK (0x10).
assign iop_intc_rd_en = iop_rd_en && rd_is_intc;
assign iop_intc_rd_addr = rd_phys_addr[7:0];
assign iop_intc_wr_en = iop_wr_en && cpu_wr_is_intc;
assign iop_intc_wr_addr = wr_phys_addr[7:0];
assign iop_intc_wr_data = iop_wr_data;
// Ch234 — pad-I/O region wiring. The map owns a single internal
// `sio2_input_stub` instance; the bridge's INPUT_P1/P2 latches
// flow into it directly. `pad_rd_*` / `pad_wr_*` are the
// map↔stub handshake (4-bit word offset within the 256-byte
// region, captured from phys_addr[5:2]).
wire pad_rd_en;
wire [3:0] pad_rd_addr;
wire [31:0] pad_rd_data;
wire pad_rd_valid;
wire pad_wr_en;
wire [3:0] pad_wr_addr;
wire [31:0] pad_wr_data;
assign pad_rd_en = iop_rd_en && rd_is_pad;
assign pad_rd_addr = rd_phys_addr[5:2];
assign pad_wr_en = iop_wr_en && cpu_wr_is_pad;
assign pad_wr_addr = wr_phys_addr[5:2];
assign pad_wr_data = iop_wr_data;
sio2_input_stub u_sio2_input (
.clk (clk),
.rst_n (rst_n),
.input_p1 (input_p1),
.input_p2 (input_p2),
.rd_en (pad_rd_en),
.rd_addr (pad_rd_addr),
.rd_data (pad_rd_data),
.rd_valid (pad_rd_valid),
.wr_en (pad_wr_en),
.wr_addr (pad_wr_addr),
.wr_data (pad_wr_data)
);
// BIOS ROM routing. 22-bit byte offset within the 4 MiB window.
// No write path — BIOS is read-only.
assign bios_rd_en = iop_rd_en && rd_is_bios;
assign bios_rd_addr = rd_phys_addr[21:0];
// Write-path arbitration for the RAM side: CPU wins on same-cycle
// collision. Neither TB nor current design exercises collision;
// priority is defensive. SIF writes are a separate port and don't
// contend with RAM writes.
logic cpu_wr_hit;
logic bridge_wr_hit;
assign cpu_wr_hit = iop_wr_en && cpu_wr_is_ram;
assign bridge_wr_hit = bridge_wr_en && bridge_wr_is_ram;
assign ram_wr_en = cpu_wr_hit || bridge_wr_hit;
assign ram_wr_addr = cpu_wr_hit ? cpu_wr_ram_offset : bridge_wr_ram_offset;
assign ram_wr_data = cpu_wr_hit ? iop_wr_data : bridge_wr_data;
assign ram_wr_be = cpu_wr_hit ? iop_wr_be : bridge_wr_be;
assign ram_master_id = cpu_wr_hit ? master_id : bridge_master_id;
// ------------------------------------------------------------------
// Read response pipeline
// cycle N : iop_rd_en high, request routed downstream (or unmapped)
// cycle N+1: iop_rd_valid high, data from RAM or fault
// ------------------------------------------------------------------
logic rd_pending;
logic rd_was_ram;
logic rd_was_sif;
logic rd_was_dmac;
logic rd_was_intc;
logic rd_was_bios;
logic rd_was_pad; // Ch234
// Ch261 — rd_pending only pulses when the CPU read is ACTUALLY
// serviced this cycle. Three cases:
// 1. Non-RAM CPU read: always serviced (separate decode paths,
// no arbitration). Pulse rd_pending normally.
// 2. RAM CPU read, no collision: serviced this cycle (serve_cpu_now
// fires above). Pulse rd_pending.
// 3. RAM CPU read in collision: deferred (cpu_pend_valid latches).
// Do NOT pulse rd_pending — iop_rd_valid stays low until the
// deferred read finally fires (serve_cpu_def).
// 4. Deferred RAM read finally serviced (serve_cpu_def): pulse
// rd_pending with rd_was_ram=1; the data arrives next cycle.
always_ff @(posedge clk) begin
if (!rst_n) begin
rd_pending <= 1'b0;
rd_was_ram <= 1'b0;
rd_was_sif <= 1'b0;
rd_was_dmac <= 1'b0;
rd_was_intc <= 1'b0;
rd_was_bios <= 1'b0;
rd_was_pad <= 1'b0;
end else if (serve_cpu_def) begin
// Deferred RAM read serviced this cycle — data next cycle.
rd_pending <= 1'b1;
rd_was_ram <= 1'b1;
rd_was_sif <= 1'b0;
rd_was_dmac <= 1'b0;
rd_was_intc <= 1'b0;
rd_was_bios <= 1'b0;
rd_was_pad <= 1'b0;
end else if (iop_rd_en && !(rd_is_ram && cpu_dma_collision)) begin
// Normal read path: live RAM read with no collision, OR
// any non-RAM CPU read (decoded by rd_is_*, routed via
// independent paths so no arbitration concern).
rd_pending <= 1'b1;
rd_was_ram <= rd_is_ram;
rd_was_sif <= rd_is_sif;
rd_was_dmac <= rd_is_dmac;
rd_was_intc <= rd_is_intc;
rd_was_bios <= rd_is_bios;
rd_was_pad <= rd_is_pad;
end else begin
// Collision-deferred OR idle cycle. CPU waits for deferred
// read to fire; iop_rd_valid stays low.
rd_pending <= 1'b0;
end
end
assign iop_rd_valid = rd_pending;
assign iop_rd_data = rd_was_ram ? ram_rd_data
: rd_was_sif ? sif_rd_data
: rd_was_dmac ? iop_dmac_rd_data
: rd_was_intc ? iop_intc_rd_data
: rd_was_bios ? bios_rd_data
: rd_was_pad ? pad_rd_data
: 32'hDEADBEEF;
// ------------------------------------------------------------------
// DMA read response pipeline (separate from CPU pipeline). Ch261 —
// CPU+DMA collision is now handled cleanly by the deferred-CPU-slot
// above: DMA wins the port immediately, CPU's read is latched and
// serviced on the next non-DMA cycle. DMA always gets its own word
// on its expected timing; no silent corruption.
// ------------------------------------------------------------------
logic dma_rd_pending;
logic dma_rd_was_ram;
always_ff @(posedge clk) begin
if (!rst_n) begin
dma_rd_pending <= 1'b0;
dma_rd_was_ram <= 1'b0;
end else begin
dma_rd_pending <= dma_rd_en;
if (dma_rd_en) dma_rd_was_ram <= dma_rd_is_ram;
end
end
assign dma_rd_valid = dma_rd_pending;
assign dma_rd_data = dma_rd_was_ram ? ram_rd_data : 32'hDEADBEEF;
// ------------------------------------------------------------------
// Trace emission — one event per cycle. Priority:
// CPU read > CPU write > DMA read > bridge write
// Masters are expected to be sequenced in TBs; priority is defensive
// for the rare collision case.
// ------------------------------------------------------------------
always_ff @(posedge clk) begin
if (!rst_n) begin
ev_valid <= 1'b0;
ev_subsys <= SUBSYS_IOP;
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 (iop_rd_en) begin
ev_valid <= 1'b1;
ev_subsys <= SUBSYS_IOP;
if (rd_is_ram) begin
ev_event <= EV_READ;
ev_arg1 <= 64'd0;
ev_arg3 <= REGION_IOP_RAM;
end else if (rd_is_sif) begin
ev_event <= EV_READ;
ev_arg1 <= 64'd0;
ev_arg3 <= REGION_SIF_REGS;
end else if (rd_is_dmac) begin
ev_event <= EV_READ;
ev_arg1 <= 64'd0;
ev_arg3 <= REGION_IOP_DMAC;
end else if (rd_is_intc) begin
ev_event <= EV_READ;
ev_arg1 <= 64'd0;
ev_arg3 <= REGION_IOP_INTC;
end else if (rd_is_bios) begin
ev_event <= EV_READ;
ev_arg1 <= 64'd0;
ev_arg3 <= REGION_IOP_BIOS;
end else if (rd_is_pad) begin
ev_event <= EV_READ;
ev_arg1 <= 64'd0;
ev_arg3 <= REGION_PAD_IO;
end else begin
ev_event <= EV_UNMAPPED;
ev_arg1 <= 64'hDEADBEEF;
ev_arg3 <= REGION_UNMAPPED;
end
ev_arg0 <= {32'd0, iop_rd_addr};
ev_arg2 <= {56'd0, master_id};
ev_flags <= 32'd0;
end else if (iop_wr_en) begin
ev_valid <= 1'b1;
ev_subsys <= SUBSYS_IOP;
if (cpu_wr_is_ram) begin
ev_event <= EV_WRITE;
ev_arg3 <= REGION_IOP_RAM;
end else if (cpu_wr_is_sif) begin
ev_event <= EV_WRITE;
ev_arg3 <= REGION_SIF_REGS;
end else if (cpu_wr_is_dmac) begin
ev_event <= EV_WRITE;
ev_arg3 <= REGION_IOP_DMAC;
end else if (cpu_wr_is_intc) begin
ev_event <= EV_WRITE;
ev_arg3 <= REGION_IOP_INTC;
end else if (cpu_wr_is_pad) begin
ev_event <= EV_WRITE;
ev_arg3 <= REGION_PAD_IO;
end else begin
ev_event <= EV_UNMAPPED;
ev_arg3 <= REGION_UNMAPPED;
end
ev_arg0 <= {32'd0, iop_wr_addr};
ev_arg1 <= {32'd0, iop_wr_data};
ev_arg2 <= {56'd0, master_id};
ev_flags <= 32'h0000_0001;
end else if (dma_rd_en) begin
ev_valid <= 1'b1;
ev_subsys <= SUBSYS_IOP;
ev_event <= dma_rd_is_ram ? EV_READ : EV_UNMAPPED;
ev_arg0 <= {32'd0, dma_rd_addr};
ev_arg1 <= dma_rd_is_ram ? 64'd0 : 64'hDEADBEEF;
ev_arg2 <= {56'd0, dma_master_id};
ev_arg3 <= dma_rd_is_ram ? REGION_IOP_RAM : REGION_UNMAPPED;
ev_flags <= 32'd0;
end else if (bridge_wr_en) begin
ev_valid <= 1'b1;
ev_subsys <= SUBSYS_IOP;
ev_event <= bridge_wr_is_ram ? EV_WRITE : EV_UNMAPPED;
ev_arg0 <= {32'd0, bridge_wr_addr};
ev_arg1 <= {32'd0, bridge_wr_data};
ev_arg2 <= {56'd0, bridge_master_id};
ev_arg3 <= bridge_wr_is_ram ? REGION_IOP_RAM : REGION_UNMAPPED;
ev_flags <= 32'h0000_0001;
end else begin
ev_valid <= 1'b0;
end
end
endmodule : iop_memory_map_stub
+153
View File
@@ -0,0 +1,153 @@
// retroDE_ps2 — iop_ram_stub
//
// First narrow IOP-side primitive. 32-bit IOP-RAM stub, architecturally
// honest to the IOP's R3000-class 32-bit bus. NOT an IOP CPU — this is
// pure memory. No fetch, no execution, no BIOS bring-up. Future IOP-side
// work (fetch stub, IOP memory map, DMAC routing) can build on top of it.
//
// Contract refs:
// docs/contracts/iop.md (IOP-local RAM/I/O decode)
// docs/contracts/memory.md (2 MiB IOP RAM in the PS2 memory map)
//
// Scope:
// - read/write 32-bit data
// - byte-enable granularity on writes
// - one-cycle read latency (matches existing stub ecosystem)
// - caller-provided master_id for trace attribution
// - trace events tagged as SUBSYS_IOP so IOP-side memory traffic is
// distinct from EE MEM events even when both are active
//
// Explicit non-goals (Wave 3 IOP first step):
// - IOP CPU execution
// - full 2 MiB sizing (default is 16 KiB — plenty for stub tests)
// - integration into any IOP memory map yet
// - connection to SIF receive path (intentional: kept independent so
// future bridging is explicit, not accidental)
//
// Trace payload schema:
// IOP READ arg0=addr arg1=data arg2=master_id arg3=region_id
// IOP WRITE arg0=addr arg1=data arg2=master_id arg3=region_id
// master_id : caller-provided (e.g. 0 = TB direct, future: 2 = IOP CPU,
// 3 = SIF bridge, etc.)
// region_id : 2 = IOP_RAM (constant for this module)
// flags[0] : 1 = write, 0 = read
`timescale 1ns/1ps
module iop_ram_stub
import trace_pkg::*;
#(
parameter int SIZE_BYTES = 16 * 1024, // 16 KiB default
parameter string IMAGE_FILE = ""
) (
input logic clk,
input logic rst_n,
// Read port
input logic rd_en,
input logic [$clog2(SIZE_BYTES)-1:0] rd_addr,
output logic [31:0] rd_data,
output logic rd_valid,
// Write port
input logic wr_en,
input logic [$clog2(SIZE_BYTES)-1:0] wr_addr,
input logic [31:0] wr_data,
input logic [3:0] wr_be,
// Caller-provided master id for trace attribution
input logic [7:0] master_id,
// 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 ADDR_WIDTH = $clog2(SIZE_BYTES);
localparam int WORD_COUNT = SIZE_BYTES / 4;
localparam int WORD_INDEX_WIDTH = ADDR_WIDTH - 2;
localparam logic [63:0] REGION_IOP_RAM = 64'd2;
logic [31:0] mem [0:WORD_COUNT-1];
initial begin
if (IMAGE_FILE != "") begin
$display("[iop_ram_stub] loading image: %0s", IMAGE_FILE);
$readmemh(IMAGE_FILE, mem);
end else begin
for (int i = 0; i < WORD_COUNT; i++) mem[i] = 32'd0;
$display("[iop_ram_stub] zero-initialised (%0d words / %0d bytes)",
WORD_COUNT, SIZE_BYTES);
end
end
logic [WORD_INDEX_WIDTH-1:0] rd_word_idx;
logic [WORD_INDEX_WIDTH-1:0] wr_word_idx;
assign rd_word_idx = rd_addr[ADDR_WIDTH-1:2];
assign wr_word_idx = wr_addr[ADDR_WIDTH-1:2];
// ------------------------------------------------------------------
// Read + write (one-cycle latency)
// ------------------------------------------------------------------
always_ff @(posedge clk) begin
if (!rst_n) begin
rd_data <= 32'd0;
rd_valid <= 1'b0;
end else begin
rd_valid <= rd_en;
if (rd_en) rd_data <= mem[rd_word_idx];
if (wr_en) begin
for (int b = 0; b < 4; b++) begin
if (wr_be[b]) mem[wr_word_idx][b*8 +: 8] <= wr_data[b*8 +: 8];
end
end
end
end
// ------------------------------------------------------------------
// Trace emission — read wins on same-cycle collision (single-port
// RAM wouldn't see that anyway in Wave 3).
// ------------------------------------------------------------------
always_ff @(posedge clk) begin
if (!rst_n) begin
ev_valid <= 1'b0;
ev_subsys <= SUBSYS_IOP;
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 (rd_en) begin
ev_valid <= 1'b1;
ev_subsys <= SUBSYS_IOP;
ev_event <= EV_READ;
ev_arg0 <= {{(64-ADDR_WIDTH){1'b0}}, rd_addr};
ev_arg1 <= {32'd0, mem[rd_word_idx]};
ev_arg2 <= {56'd0, master_id};
ev_arg3 <= REGION_IOP_RAM;
ev_flags <= 32'd0;
end else if (wr_en) begin
ev_valid <= 1'b1;
ev_subsys <= SUBSYS_IOP;
ev_event <= EV_WRITE;
ev_arg0 <= {{(64-ADDR_WIDTH){1'b0}}, wr_addr};
ev_arg1 <= {32'd0, wr_data};
ev_arg2 <= {56'd0, master_id};
ev_arg3 <= REGION_IOP_RAM;
ev_flags <= 32'h0000_0001;
end else begin
ev_valid <= 1'b0;
end
end
endmodule : iop_ram_stub
+204
View File
@@ -0,0 +1,204 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (c) 2025-2026 retroDE contributors
// ============================================================================
// sio2_input_stub — Ch234 retroDE-local IOP-readable pad input stub
// ============================================================================
// **Not real SIO2.** A deliberately minimal MMIO surface that translates
// the Ch222 HPS-written `INPUT_P1`/`INPUT_P2` controller bitmaps into a
// Sony-format 16-bit digital pad word, exposed as IOP-readable
// registers in the retroDE-local I/O window
// `0x1F80_8500..0x1F80_85FF`. Real SIO2 emulation (`0x1F80_8200..0x1F80_82FF`,
// FIFO, command/response, IOP DMAC channel 11) is intentionally deferred
// — see `docs/contracts/sio2_pad.md` for the reconnaissance + scoping.
//
// **Register surface** (offsets relative to PAD_IO_BASE = 0x1F80_8500):
//
// 0x500 PAD_P1_STATE (RO) [15:0] = Sony 16-bit pad word for P1
// [31:16] = 0
// 0x504 PAD_P2_STATE (RO) Same shape, sourced from `input_p2`.
// 0x508 PAD_STATUS (RO) [0] = pad path present/valid = 1
// [31:1] = 0
// other reserved reads return 32'd0; writes accepted-and-ignored.
//
// **Sony pad word format (Sony "digital mode" / type 0x41 response,
// bytes 3 and 4 of the libpad/padman struct):**
//
// pad_byte3 (D-pad / start / select / sticks; active-low, 0 = pressed):
// bit 7 LEFT bit 6 DOWN bit 5 RIGHT bit 4 UP
// bit 3 START bit 2 R3 bit 1 L3 bit 0 SELECT
//
// pad_byte4 (face / shoulder buttons; active-low):
// bit 7 □ square bit 6 × cross bit 5 ○ circle bit 4 △ triangle
// bit 3 R1 bit 2 L1 bit 1 R2 bit 0 L2
//
// PAD_P1_STATE[7:0] = pad_byte3
// PAD_P1_STATE[15:8] = pad_byte4
//
// **INPUT_P1 → Sony mapping** (per `docs/contracts/sio2_pad.md`,
// SNES-style 32-bit retroDE bitmap folded onto Sony names by spatial
// face-button layout — matches the convention coco2 / a2600 already use):
//
// INPUT_P1[ 0] JOY_RIGHT → Sony RIGHT (byte3.5)
// INPUT_P1[ 1] JOY_LEFT → Sony LEFT (byte3.7)
// INPUT_P1[ 2] JOY_DOWN → Sony DOWN (byte3.6)
// INPUT_P1[ 3] JOY_UP → Sony UP (byte3.4)
// INPUT_P1[ 4] JOY_START → Sony START (byte3.3)
// INPUT_P1[ 5] JOY_SELECT → Sony SELECT (byte3.0)
// INPUT_P1[ 6] JOY_Y → Sony △ triangle (byte4.4)
// INPUT_P1[ 7] JOY_B → Sony × cross (byte4.6)
// INPUT_P1[ 8] JOY_X → Sony □ square (byte4.7)
// INPUT_P1[ 9] JOY_A → Sony ○ circle (byte4.5)
// INPUT_P1[10] JOY_L → Sony L1 (byte4.2)
// INPUT_P1[11] JOY_R → Sony R1 (byte4.3)
// INPUT_P1[12] JOY_L2 → Sony L2 (byte4.0)
// INPUT_P1[13] JOY_R2 → Sony R2 (byte4.1)
// INPUT_P1[14] JOY_L3 → Sony L3 (byte3.1)
// INPUT_P1[15] JOY_R3 → Sony R3 (byte3.2)
// INPUT_P1[16] JOY_OSD → not forwarded (retrodesd consumes it)
//
// retroDE bitmap is **active-high** (1 = pressed); Sony word is
// **active-low** (0 = pressed). The two `pad_byteN` assigns invert
// per-bit and reorder.
//
// **CDC contract.** `input_p1`/`input_p2` are bridge-clock-domain
// signals (CLOCK2_50). This module runs on the IOP/design clock.
// The 2-FF synchronizer chain inside is the standard retroDE
// single-bit sync; tearing between bits during a partial-write
// settling window is theoretically possible but practically
// vanishingly rare (retrodesd writes the whole 32-bit latch at
// one bridge edge ≤ 1 kHz; the IOP-side read is a small window
// against millions of bridge cycles). A future chapter can promote
// this to "snapshot CDC" (latch + 2-sample coherency) if tearing
// ever becomes observable.
//
// In the focused TB and single-clock sim setups, the 2-FF sync is
// a no-op functionally and adds 2 cycles of read latency from
// input change to readable register update.
// ============================================================================
`timescale 1ns/1ps
module sio2_input_stub (
input logic clk, // IOP / design clock
input logic rst_n,
// Bridge-clock-domain inputs (sync'd internally).
input logic [31:0] input_p1,
input logic [31:0] input_p2,
// IOP map read port. `rd_addr` is the 4-bit word offset within
// the PAD I/O region (so 0x500 → addr 0x0, 0x504 → 0x1, etc.).
input logic rd_en,
input logic [3:0] rd_addr,
output logic [31:0] rd_data,
output logic rd_valid,
// IOP map write port. Writes are accepted-and-ignored.
input logic wr_en,
input logic [3:0] wr_addr,
input logic [31:0] wr_data,
// Ch250 — surface the post-translation Sony 16-bit pad words for
// fabric consumers that don't go through the IOP read memory map.
// The synth top uses `p1_sony_word_o` bits to drive status LEDs as
// a hardware proof that `bridge_input_p1_raw` actually reaches a
// live fabric consumer. (Ch241 noted those wires terminated at
// unconnected nets that Quartus elided; Ch250 ends that.) Bits
// are still active-LOW per Sony's wire-format convention. Both
// outputs are parallel taps of the same internal logic that feeds
// the 0x500/0x504 read responses — no functional change to the
// existing IOP-side path.
output logic [15:0] p1_sony_word_o,
output logic [15:0] p2_sony_word_o
);
// -----------------------------------------------------------------
// 2-FF sync of each P1/P2 bit into the IOP clock domain.
// -----------------------------------------------------------------
logic [31:0] p1_sync_0, p1_sync_1;
logic [31:0] p2_sync_0, p2_sync_1;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
p1_sync_0 <= 32'd0;
p1_sync_1 <= 32'd0;
p2_sync_0 <= 32'd0;
p2_sync_1 <= 32'd0;
end else begin
p1_sync_0 <= input_p1;
p1_sync_1 <= p1_sync_0;
p2_sync_0 <= input_p2;
p2_sync_1 <= p2_sync_0;
end
end
wire [31:0] p1_q = p1_sync_1;
wire [31:0] p2_q = p2_sync_1;
// -----------------------------------------------------------------
// Sony pad-word translation. Each `pad_byteN` is the *active-low*
// Sony byte; inversion folds the active-high retroDE bitmap.
// Bit positions per `docs/contracts/sio2_pad.md`:
// byte3 = {LEFT, DOWN, RIGHT, UP, START, R3, L3, SELECT} (MSB→LSB)
// byte4 = {□, ×, ○, △, R1, L1, R2, L2}
// -----------------------------------------------------------------
function automatic logic [15:0] sony_word(input logic [31:0] joy);
logic [7:0] byte3;
logic [7:0] byte4;
// byte3 MSB→LSB: LEFT[1], DOWN[2], RIGHT[0], UP[3], START[4], R3[15], L3[14], SELECT[5]
byte3 = ~{joy[1], joy[2], joy[0], joy[3], joy[4], joy[15], joy[14], joy[5]};
// byte4 MSB→LSB: SQUARE[8], CROSS[7], CIRCLE[9], TRIANGLE[6], R1[11], L1[10], R2[13], L2[12]
byte4 = ~{joy[8], joy[7], joy[9], joy[6], joy[11], joy[10], joy[13], joy[12]};
sony_word = {byte4, byte3};
endfunction
wire [15:0] p1_word = sony_word(p1_q);
wire [15:0] p2_word = sony_word(p2_q);
// Ch250 — surface the post-translation Sony words to fabric.
assign p1_sony_word_o = p1_word;
assign p2_sony_word_o = p2_word;
// -----------------------------------------------------------------
// Register address constants (word-aligned within the PAD I/O
// region; address bits [3:2] passed in as `rd_addr[1:0]`).
// 0x500 → rd_addr = 4'h0 PAD_P1_STATE
// 0x504 → rd_addr = 4'h1 PAD_P2_STATE
// 0x508 → rd_addr = 4'h2 PAD_STATUS
// -----------------------------------------------------------------
localparam logic [3:0] OFF_P1_STATE = 4'h0;
localparam logic [3:0] OFF_P2_STATE = 4'h1;
localparam logic [3:0] OFF_STATUS = 4'h2;
// -----------------------------------------------------------------
// Read response. Combinational lookup + 1-cycle valid pipeline
// (matches the rest of the IOP map peripherals).
// -----------------------------------------------------------------
logic [31:0] rd_data_c;
always_comb begin
unique case (rd_addr)
OFF_P1_STATE: rd_data_c = {16'd0, p1_word};
OFF_P2_STATE: rd_data_c = {16'd0, p2_word};
OFF_STATUS: rd_data_c = {31'd0, 1'b1};
default: rd_data_c = 32'd0;
endcase
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rd_data <= 32'd0;
rd_valid <= 1'b0;
end else begin
rd_valid <= rd_en;
if (rd_en)
rd_data <= rd_data_c;
end
end
// -----------------------------------------------------------------
// Writes are accepted-and-ignored. We tie `wr_*` to a placeholder
// wire so lint tools don't flag them as unused.
// -----------------------------------------------------------------
// verilator lint_off UNUSED
wire _wr_unused = &{1'b0, wr_en, wr_addr, wr_data, 1'b0};
// verilator lint_on UNUSED
endmodule : sio2_input_stub