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,49 @@
|
||||
# rtl/intc
|
||||
|
||||
Interrupt controller scaffolding. Matches `docs/contracts/intc.md`.
|
||||
|
||||
## Current contents
|
||||
|
||||
- `intc_stub.sv` — generic PS2-style INTC register shell.
|
||||
Register-visible INTC_STAT / INTC_MASK (offsets parameterized) plus a
|
||||
16-source injection port `irq_src[15:0]`. The aggregate output
|
||||
`cpu_irq` is polarity-neutral: the same module is instantiated both
|
||||
as the EE INTC and as the IOP INTC (with appropriate offsets and a
|
||||
different set of wired sources in each case).
|
||||
|
||||
## Register semantics
|
||||
|
||||
- `INTC_STAT` (offset is a parameter; default 0x00): W1C on writes;
|
||||
sticky until cleared. `irq_src` sets bits on each cycle they're
|
||||
observed; same-cycle inject-over-W1C collisions keep the pending bit
|
||||
— interrupts are never silently swallowed.
|
||||
- `INTC_MASK` (offset is a parameter; default 0x10): plain write-to-set.
|
||||
Real PS2 uses XOR/toggle semantics on mask writes; stub uses plain
|
||||
write for simplicity. Escalate if a BIOS trace demands it.
|
||||
|
||||
## Instantiation conventions
|
||||
|
||||
- **EE INTC**: default offsets (STAT=0x00, MASK=0x10). Instantiated
|
||||
stand-alone in most benches; the EE memory map does not route INTC
|
||||
addresses yet (deferred).
|
||||
- **IOP INTC**: parameterized to STAT=0x70, MASK=0x74 to match real
|
||||
PS2 IOP INTC placement. Reached through `iop_memory_map_stub` at
|
||||
physical address 0x1F80_1070+ (region id = 5).
|
||||
|
||||
## Wired sources (current)
|
||||
|
||||
- EE INTC bit 0 = EE DMAC completion (`dmac_reg_stub.irq_completion_o`).
|
||||
- IOP INTC bit 0 = IOP DMAC ch9 completion
|
||||
(`iop_dmac_reg_stub.irq_completion_o`).
|
||||
|
||||
Both are one-cycle pulses driven from the respective DMAC's `S_DONE`
|
||||
state. The INTC latches them into its own pending bit; software (the
|
||||
TB, for now) reads STAT through the architectural register port and
|
||||
acks with a W1C write.
|
||||
|
||||
## Scope boundary
|
||||
|
||||
Module is side-neutral by design. Source-routing from other real
|
||||
subsystems (timers, GIF/GS, IPU, SPU2, bridge `last_seen_o`) is the
|
||||
next natural expansion. Re-arm / re-assertion ordering is already
|
||||
proven in the integration benches.
|
||||
@@ -0,0 +1,189 @@
|
||||
// retroDE_ps2 — intc_stub
|
||||
//
|
||||
// Generic PS2-style interrupt controller shell. Register-visible
|
||||
// status/mask behaviour plus a 16-source injection port; the same
|
||||
// module is reusable as either the EE-side or IOP-side INTC by picking
|
||||
// the appropriate address offsets and instantiating with different
|
||||
// sources. The aggregate output `cpu_irq` is side-neutral.
|
||||
//
|
||||
// Contract refs:
|
||||
// docs/stub_module_plan.md (Wave 1, item 7)
|
||||
// docs/contracts/intc.md
|
||||
//
|
||||
// Register layout (Wave 1):
|
||||
// offset 0x000: INTC_STAT read: current pending, write: W1C
|
||||
// offset 0x010: INTC_MASK read: current mask, write: plain set
|
||||
//
|
||||
// Real PS2 INTC_MASK uses write-to-toggle (XOR) semantics. Wave 1 uses
|
||||
// plain write semantics for stub simplicity; toggle semantics are a
|
||||
// Wave 2+ concern if BIOS traces demand them.
|
||||
//
|
||||
// Injection:
|
||||
// irq_src[i] high on any cycle latches bit i in INTC_STAT. Sticky until
|
||||
// cleared by a W1C write. Sixteen sources are exposed (matches real PS2
|
||||
// INTC source count); testbenches drive whichever they need.
|
||||
//
|
||||
// Trace payload schema (per stub plan):
|
||||
// INTC IRQ arg0=source_bitmap arg1=masked arg2=pending arg3=ack
|
||||
// one event per cycle max. Priority if multiple triggers coincide:
|
||||
// ack (STAT W1C) > new assertion > mask write.
|
||||
// ack arg3=1 when the event is a W1C ack, 0 otherwise.
|
||||
// flags bit 0 = register write (vs. source-driven assertion)
|
||||
|
||||
`timescale 1ns/1ps
|
||||
|
||||
module intc_stub
|
||||
import trace_pkg::*;
|
||||
#(
|
||||
parameter logic [7:0] INTC_STAT_OFFSET = 8'h00,
|
||||
parameter logic [7:0] INTC_MASK_OFFSET = 8'h10
|
||||
) (
|
||||
input logic clk,
|
||||
input logic rst_n,
|
||||
|
||||
// Register port
|
||||
input logic reg_wr_en,
|
||||
input logic reg_rd_en,
|
||||
input logic [7:0] reg_addr,
|
||||
input logic [31:0] reg_wr_data,
|
||||
output logic [31:0] reg_rd_data,
|
||||
output logic reg_rd_valid,
|
||||
|
||||
// Synthetic interrupt sources
|
||||
input logic [15:0] irq_src,
|
||||
|
||||
// Aggregate interrupt line to whichever CPU side this INTC serves
|
||||
// (EE or IOP). Named generically because this module is reused on
|
||||
// both sides.
|
||||
output logic cpu_irq,
|
||||
|
||||
// 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
|
||||
);
|
||||
|
||||
logic [15:0] intc_stat;
|
||||
logic [15:0] intc_mask;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Register reads (1-cycle latency, matches bios_rom_stub pattern)
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
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_addr)
|
||||
INTC_STAT_OFFSET: reg_rd_data <= {16'd0, intc_stat};
|
||||
INTC_MASK_OFFSET: reg_rd_data <= {16'd0, intc_mask};
|
||||
default: reg_rd_data <= 32'd0;
|
||||
endcase
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Pending/mask update + synthetic injection
|
||||
// - W1C on INTC_STAT clears bits where write_data has 1.
|
||||
// - Plain write on INTC_MASK replaces current mask.
|
||||
// - irq_src sets bits in INTC_STAT (sticky).
|
||||
// - If W1C and irq_src collide on the same cycle and same bit, the
|
||||
// assertion wins — we don't want to swallow an interrupt.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
logic [15:0] stat_w1c_mask;
|
||||
logic [15:0] stat_inject;
|
||||
logic mask_wr;
|
||||
|
||||
assign stat_w1c_mask = (reg_wr_en && (reg_addr == INTC_STAT_OFFSET))
|
||||
? reg_wr_data[15:0] : 16'd0;
|
||||
assign stat_inject = irq_src;
|
||||
assign mask_wr = reg_wr_en && (reg_addr == INTC_MASK_OFFSET);
|
||||
|
||||
always_ff @(posedge clk) begin
|
||||
if (!rst_n) begin
|
||||
intc_stat <= 16'd0;
|
||||
intc_mask <= 16'd0;
|
||||
end else begin
|
||||
intc_stat <= (intc_stat & ~stat_w1c_mask) | stat_inject;
|
||||
if (mask_wr) intc_mask <= reg_wr_data[15:0];
|
||||
end
|
||||
end
|
||||
|
||||
assign cpu_irq = |(intc_stat & intc_mask);
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Trace
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
logic [15:0] new_assertions;
|
||||
logic [15:0] bits_acked;
|
||||
logic had_ack;
|
||||
logic had_assertion;
|
||||
logic had_mask_wr;
|
||||
|
||||
// "new_assertions" = bits becoming pending this cycle that weren't pending
|
||||
// before. Combinational on the pre-edge state.
|
||||
assign new_assertions = stat_inject & ~intc_stat;
|
||||
assign bits_acked = stat_w1c_mask & intc_stat;
|
||||
assign had_ack = |bits_acked;
|
||||
assign had_assertion = |new_assertions;
|
||||
assign had_mask_wr = mask_wr;
|
||||
|
||||
always_ff @(posedge clk) begin
|
||||
if (!rst_n) begin
|
||||
ev_valid <= 1'b0;
|
||||
ev_subsys <= SUBSYS_INTC;
|
||||
ev_event <= EV_IRQ;
|
||||
ev_arg0 <= 64'd0;
|
||||
ev_arg1 <= 64'd0;
|
||||
ev_arg2 <= 64'd0;
|
||||
ev_arg3 <= 64'd0;
|
||||
ev_flags <= 32'd0;
|
||||
end else if (had_ack) begin
|
||||
// arg1/arg2 must reflect the post-update state. The state
|
||||
// update preserves simultaneous stat_inject over W1C clears
|
||||
// (see always_ff above), so if inject and ack collide on the
|
||||
// same bit, that bit stays pending. arg0 still reports what
|
||||
// software tried to ack, regardless of whether it took effect.
|
||||
ev_valid <= 1'b1;
|
||||
ev_subsys <= SUBSYS_INTC;
|
||||
ev_event <= EV_IRQ;
|
||||
ev_arg0 <= {48'd0, bits_acked};
|
||||
ev_arg1 <= {48'd0, ((intc_stat & ~stat_w1c_mask) | stat_inject) & intc_mask};
|
||||
ev_arg2 <= {48'd0, (intc_stat & ~stat_w1c_mask) | stat_inject};
|
||||
ev_arg3 <= 64'd1; // ack = 1
|
||||
ev_flags <= 32'h0000_0001;
|
||||
end else if (had_assertion) begin
|
||||
ev_valid <= 1'b1;
|
||||
ev_subsys <= SUBSYS_INTC;
|
||||
ev_event <= EV_IRQ;
|
||||
ev_arg0 <= {48'd0, new_assertions};
|
||||
ev_arg1 <= {48'd0, (intc_stat | stat_inject) & intc_mask};
|
||||
ev_arg2 <= {48'd0, (intc_stat | stat_inject)};
|
||||
ev_arg3 <= 64'd0; // ack = 0
|
||||
ev_flags <= 32'd0;
|
||||
end else if (had_mask_wr) begin
|
||||
ev_valid <= 1'b1;
|
||||
ev_subsys <= SUBSYS_INTC;
|
||||
ev_event <= EV_IRQ;
|
||||
ev_arg0 <= 64'd0;
|
||||
ev_arg1 <= {48'd0, intc_stat & reg_wr_data[15:0]};
|
||||
ev_arg2 <= {48'd0, intc_stat};
|
||||
ev_arg3 <= 64'd0;
|
||||
ev_flags <= 32'h0000_0001;
|
||||
end else begin
|
||||
ev_valid <= 1'b0;
|
||||
end
|
||||
end
|
||||
|
||||
endmodule : intc_stub
|
||||
Reference in New Issue
Block a user