// 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