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,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).
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user