Initial commit: retroDE_ps2 — first-of-its-kind PS2 GS FPGA core (DE25-Nano / Agilex 5)

RTL (GS rasterizer, EE core stub, platform bridge, LPDDR4B path), sim regression
(272 TBs), docs, and tooling. Copyrighted PS2 content (BIOS, game code, GS dumps,
and all dump-derived textures/traces) is excluded via .gitignore and stays local.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-29 20:10:50 -04:00
commit ec82764bef
2462 changed files with 2174303 additions and 0 deletions
+495
View File
@@ -0,0 +1,495 @@
// retroDE_ps2 — tb_iop_dmac_via_map
//
// IOP-DMAC register lifecycle + data-path integration. Proves:
// - IOP can program DMAC ch9 through the real 0x1F80_152x window.
// - DMAC fetches real source words from IOP RAM via the map's new
// dma_rd_* master port — MADR is a live pointer, stepping by 4.
// - Transfer emits DMA_START, per-beat DMA_BEAT with correct
// src_addr + remaining-count, and DMA_DONE on the final beat.
// - Endpoint ready/valid backpressure stalls mid-stream without any
// false completion: busy stays high, no DMA_DONE fires, no beat
// events advance while ep_ready is low.
//
// Chain:
// TB (preloads payload into IOP RAM via map CPU write)
// TB (programs ch9 via map at 0x1F80_152x)
// │
// ▼
// iop_memory_map_stub ← iop_dmac_reg_stub (ch9, reads via dma_rd_*)
// │
// ▼
// iop_ram_stub
//
// TB consumes the DMAC endpoint with a manually-driven ep_ready; the
// egress bridge is deliberately not wired here — that's the full-chain
// TB's job (tb_sif_ee_landing_via_dmac). Keeping this TB minimal keeps
// the lifecycle/stall coverage independent of the landing path.
//
// Scenarios:
// 1. Architectural programming. IOP writes MADR + BCR via map; DMAC
// emits EV_DMA_CFG; busy stays low; CHCR[0] stays 0.
// 2. Normal run: ep_ready = 1, IOP writes CHCR start (bit 0). DMAC
// emits START, then emits one BEAT per accepted word with correct
// src_addr, finishes with DONE. CHCR[0] clears.
// 3. Stall: start a second transfer with ep_ready = 0. Confirm busy
// high, no new DONE, beat count unchanged across a wait window.
// 4. Recovery: ep_ready back to 1. Transfer completes.
// 5. Readback: MADR/BCR/CHCR read through the map.
//
// Key trace-visible assertions:
// - Map emits events with region=IOP_DMAC (4) on ch9 register
// accesses, region=IOP_RAM (2) on DMAC source fetches (master_id=4).
// - DMAC trace has the CFG/START/BEAT/DONE sequence with BEAT count
// equal to BCR, and src_addr stepping by 4 per beat.
`timescale 1ns/1ps
module tb_iop_dmac_via_map;
localparam int RAM_BYTES = 4 * 1024;
localparam int RAM_ADDR_W = $clog2(RAM_BYTES);
localparam logic [31:0] DMAC_CH9_BASE = 32'h1F80_1520;
localparam logic [31:0] DMAC_MADR_ADDR = DMAC_CH9_BASE | 32'h00;
localparam logic [31:0] DMAC_BCR_ADDR = DMAC_CH9_BASE | 32'h04;
localparam logic [31:0] DMAC_CHCR_ADDR = DMAC_CH9_BASE | 32'h08;
// Source address in IOP RAM for the payload — bit-aligned to a
// deterministic offset away from 0 so the TB can tell the right
// bytes moved.
localparam logic [31:0] SRC_BASE = 32'h0000_0200;
localparam int BEATS = 4; // small BCR for TB speed
logic clk;
logic rst_n;
initial clk = 1'b0;
always #5 clk = ~clk;
// ------------------------------------------------------------------
// IOP map
// ------------------------------------------------------------------
logic iop_rd_en;
logic [31:0] iop_rd_addr;
logic [31:0] iop_rd_data;
logic iop_rd_valid;
logic iop_wr_en;
logic [31:0] iop_wr_addr;
logic [31:0] iop_wr_data;
logic [3:0] iop_wr_be;
logic ram_rd_en;
logic [20:0] ram_rd_addr;
logic [31:0] ram_rd_data;
logic ram_rd_valid;
logic ram_wr_en;
logic [20:0] ram_wr_addr;
logic [31:0] ram_wr_data;
logic [3:0] ram_wr_be;
logic [7:0] ram_master_id;
logic map_dmac_rd_en;
logic [3:0] map_dmac_rd_addr;
logic [31:0] map_dmac_rd_data;
logic map_dmac_rd_valid;
logic map_dmac_wr_en;
logic [3:0] map_dmac_wr_addr;
logic [31:0] map_dmac_wr_data;
// DMAC as read master through the map
logic dma_rd_en;
logic [31:0] dma_rd_addr;
logic [7:0] dma_master_id;
logic [31:0] dma_rd_data;
logic dma_rd_valid;
logic map_ev_valid;
trace_pkg::subsys_e map_ev_subsys;
trace_pkg::event_e map_ev_event;
logic [63:0] map_ev_arg0, map_ev_arg1, map_ev_arg2, map_ev_arg3;
logic [31:0] map_ev_flags;
iop_memory_map_stub u_iop_map (
.clk(clk), .rst_n(rst_n),
.iop_rd_en(iop_rd_en), .iop_rd_addr(iop_rd_addr),
.iop_rd_data(iop_rd_data), .iop_rd_valid(iop_rd_valid),
.iop_wr_en(iop_wr_en), .iop_wr_addr(iop_wr_addr),
.iop_wr_data(iop_wr_data), .iop_wr_be(iop_wr_be),
.master_id(8'd2), // IOP_CPU
// Bridge port — unused
.bridge_wr_en(1'b0), .bridge_wr_addr(32'd0),
.bridge_wr_data(32'd0), .bridge_wr_be(4'd0),
.bridge_master_id(8'd0),
// DMA read-master port — DMAC's fetches
.dma_rd_en(dma_rd_en), .dma_rd_addr(dma_rd_addr),
.dma_master_id(dma_master_id),
.dma_rd_data(dma_rd_data), .dma_rd_valid(dma_rd_valid),
// SIF register-shell port — unused by this TB
.sif_rd_en(), .sif_rd_addr(),
.sif_rd_data(32'd0), .sif_rd_valid(1'b0),
.sif_wr_en(), .sif_wr_addr(), .sif_wr_data(),
// DMAC register-shell port
.iop_dmac_rd_en(map_dmac_rd_en), .iop_dmac_rd_addr(map_dmac_rd_addr),
.iop_dmac_rd_data(map_dmac_rd_data), .iop_dmac_rd_valid(map_dmac_rd_valid),
.iop_dmac_wr_en(map_dmac_wr_en), .iop_dmac_wr_addr(map_dmac_wr_addr),
.iop_dmac_wr_data(map_dmac_wr_data),
// IOP INTC port — unused by this TB
.iop_intc_rd_en(), .iop_intc_rd_addr(),
.iop_intc_rd_data(32'd0), .iop_intc_rd_valid(1'b0),
.iop_intc_wr_en(), .iop_intc_wr_addr(), .iop_intc_wr_data(),
.input_p1(32'd0), .input_p2(32'd0),
// BIOS ROM port — unused by this TB
.bios_rd_en(), .bios_rd_addr(),
.bios_rd_data(32'd0), .bios_rd_valid(1'b0),
// RAM downstream
.ram_rd_en(ram_rd_en), .ram_rd_addr(ram_rd_addr),
.ram_rd_data(ram_rd_data), .ram_rd_valid(ram_rd_valid),
.ram_wr_en(ram_wr_en), .ram_wr_addr(ram_wr_addr),
.ram_wr_data(ram_wr_data), .ram_wr_be(ram_wr_be),
.ram_master_id(ram_master_id),
.ev_valid(map_ev_valid), .ev_subsys(map_ev_subsys),
.ev_event(map_ev_event),
.ev_arg0(map_ev_arg0), .ev_arg1(map_ev_arg1),
.ev_arg2(map_ev_arg2), .ev_arg3(map_ev_arg3),
.ev_flags(map_ev_flags)
);
iop_ram_stub #(.SIZE_BYTES(RAM_BYTES)) u_iop_ram (
.clk(clk), .rst_n(rst_n),
.rd_en(ram_rd_en), .rd_addr(ram_rd_addr[RAM_ADDR_W-1:0]),
.rd_data(ram_rd_data), .rd_valid(ram_rd_valid),
.wr_en(ram_wr_en), .wr_addr(ram_wr_addr[RAM_ADDR_W-1:0]),
.wr_data(ram_wr_data), .wr_be(ram_wr_be),
.master_id(ram_master_id),
.ev_valid(), .ev_subsys(), .ev_event(),
.ev_arg0(), .ev_arg1(), .ev_arg2(), .ev_arg3(), .ev_flags()
);
// ------------------------------------------------------------------
// IOP DMAC (ch9) — TB drives ep_ready directly
// ------------------------------------------------------------------
logic ep_valid;
logic [31:0] ep_data;
logic ep_last;
logic ep_ready;
logic dmac_busy;
logic [31:0] dmac_done_count;
logic dmac_ev_valid;
trace_pkg::subsys_e dmac_ev_subsys;
trace_pkg::event_e dmac_ev_event;
logic [63:0] dmac_ev_arg0, dmac_ev_arg1, dmac_ev_arg2, dmac_ev_arg3;
logic [31:0] dmac_ev_flags;
iop_dmac_reg_stub u_dmac (
.clk(clk), .rst_n(rst_n),
.reg_wr_en(map_dmac_wr_en), .reg_rd_en(map_dmac_rd_en),
.reg_offset(map_dmac_wr_en ? map_dmac_wr_addr : map_dmac_rd_addr),
.reg_wr_data(map_dmac_wr_data),
.reg_rd_data(map_dmac_rd_data), .reg_rd_valid(map_dmac_rd_valid),
// DMAC → IOP map dma_rd_*
.mem_rd_en(dma_rd_en), .mem_rd_addr(dma_rd_addr),
.mem_master_id(dma_master_id),
.mem_rd_data(dma_rd_data), .mem_rd_valid(dma_rd_valid),
// Endpoint (TB drives ep_ready)
.ep_valid(ep_valid), .ep_data(ep_data), .ep_last(ep_last),
.ep_ready(ep_ready),
.irq_completion_o(),
.busy_o(dmac_busy), .done_count_o(dmac_done_count),
.ev_valid(dmac_ev_valid), .ev_subsys(dmac_ev_subsys),
.ev_event(dmac_ev_event),
.ev_arg0(dmac_ev_arg0), .ev_arg1(dmac_ev_arg1),
.ev_arg2(dmac_ev_arg2), .ev_arg3(dmac_ev_arg3),
.ev_flags(dmac_ev_flags)
);
// ------------------------------------------------------------------
// Trace sinks
// ------------------------------------------------------------------
trace_sink_stub #(.FILENAME("iop_dmac_via_map.trace"), .SINK_LABEL("iop_map"))
u_trace_map (.clk(clk), .rst_n(rst_n),
.ev_valid(map_ev_valid), .ev_subsys(map_ev_subsys),
.ev_event(map_ev_event), .ev_arg0(map_ev_arg0),
.ev_arg1(map_ev_arg1), .ev_arg2(map_ev_arg2),
.ev_arg3(map_ev_arg3), .ev_flags(map_ev_flags));
trace_sink_stub #(.FILENAME("iop_dmac_reg.trace"), .SINK_LABEL("iop_dmac"))
u_trace_dmac (.clk(clk), .rst_n(rst_n),
.ev_valid(dmac_ev_valid), .ev_subsys(dmac_ev_subsys),
.ev_event(dmac_ev_event), .ev_arg0(dmac_ev_arg0),
.ev_arg1(dmac_ev_arg1), .ev_arg2(dmac_ev_arg2),
.ev_arg3(dmac_ev_arg3), .ev_flags(dmac_ev_flags));
// ------------------------------------------------------------------
// Counters + captured beats
// ------------------------------------------------------------------
int map_dmac_events; // map events region=IOP_DMAC (reg accesses)
int map_ram_dma_events; // map events region=IOP_RAM + master_id=4
int dmac_cfg_events;
int dmac_start_events;
int dmac_beat_events;
int dmac_done_events;
int stall_cycles;
int errors;
logic [31:0] captured_beats [0:31];
int beat_capture_count;
initial begin
map_dmac_events = 0;
map_ram_dma_events = 0;
dmac_cfg_events = 0;
dmac_start_events = 0;
dmac_beat_events = 0;
dmac_done_events = 0;
stall_cycles = 0;
beat_capture_count = 0;
errors = 0;
for (int i = 0; i < 32; i++) captured_beats[i] = 32'd0;
end
always_ff @(posedge clk) begin
if (rst_n && map_ev_valid &&
map_ev_subsys == trace_pkg::SUBSYS_IOP) begin
if (map_ev_arg3[7:0] == 8'd4)
map_dmac_events <= map_dmac_events + 1;
if (map_ev_arg3[7:0] == 8'd2 && map_ev_arg2[7:0] == 8'd4)
map_ram_dma_events <= map_ram_dma_events + 1;
end
if (rst_n && dmac_ev_valid &&
dmac_ev_subsys == trace_pkg::SUBSYS_DMAC) begin
case (dmac_ev_event)
trace_pkg::EV_DMA_CFG: dmac_cfg_events <= dmac_cfg_events + 1;
trace_pkg::EV_DMA_START: dmac_start_events <= dmac_start_events + 1;
trace_pkg::EV_DMA_BEAT: dmac_beat_events <= dmac_beat_events + 1;
trace_pkg::EV_DMA_DONE: dmac_done_events <= dmac_done_events + 1;
default: ;
endcase
end
if (rst_n && dmac_busy && !ep_ready)
stall_cycles <= stall_cycles + 1;
// Capture beats as they're accepted at the endpoint.
if (rst_n && ep_valid && ep_ready && beat_capture_count < 32) begin
captured_beats[beat_capture_count] <= ep_data;
beat_capture_count <= beat_capture_count + 1;
end
end
// ------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------
task automatic iop_write(input logic [31:0] addr, input logic [31:0] data);
@(negedge clk);
iop_wr_en = 1'b1;
iop_wr_addr = addr;
iop_wr_data = data;
iop_wr_be = 4'b1111;
@(negedge clk);
iop_wr_en = 1'b0;
iop_wr_addr = 32'd0;
iop_wr_data = 32'd0;
iop_wr_be = 4'd0;
endtask
task automatic iop_read_expect(input logic [31:0] addr,
input logic [31:0] expected,
input string label);
@(negedge clk);
iop_rd_en = 1'b1;
iop_rd_addr = addr;
@(negedge clk);
iop_rd_en = 1'b0;
iop_rd_addr = 32'd0;
if (iop_rd_data !== expected || iop_rd_valid !== 1'b1) begin
$error("[tb_iop_dmac_via_map] IOP read %s at 0x%08h: got 0x%08h valid=%0b expected 0x%08h",
label, addr, iop_rd_data, iop_rd_valid, expected);
errors = errors + 1;
end
endtask
// ------------------------------------------------------------------
// Stimulus
// ------------------------------------------------------------------
logic [31:0] payload [0:BEATS-1];
int prev_done_count;
int prev_beat_count;
int stall_observed;
initial begin
rst_n = 1'b0;
iop_rd_en = 1'b0;
iop_rd_addr = 32'd0;
iop_wr_en = 1'b0;
iop_wr_addr = 32'd0;
iop_wr_data = 32'd0;
iop_wr_be = 4'd0;
ep_ready = 1'b1;
prev_done_count = 0;
prev_beat_count = 0;
stall_observed = 0;
// Four distinctive words so beat capture is unambiguous.
payload[0] = 32'hA5A5_0000;
payload[1] = 32'hDEAD_BEEF;
payload[2] = 32'hCAFE_F00D;
payload[3] = 32'h1234_5678;
repeat (4) @(posedge clk);
rst_n = 1'b1;
repeat (2) @(posedge clk);
// --------------------------------------------------------------
// Preload: TB writes the payload words into IOP RAM through the
// CPU-side port of the map. Uses the RAM region (phys 0x00...),
// 4-byte stride starting at SRC_BASE.
// --------------------------------------------------------------
for (int i = 0; i < BEATS; i++) begin
iop_write(SRC_BASE + (i << 2), payload[i]);
end
// --------------------------------------------------------------
// Scenario 1: architectural programming
// --------------------------------------------------------------
iop_write(DMAC_MADR_ADDR, SRC_BASE);
iop_write(DMAC_BCR_ADDR, 32'(BEATS));
if (dmac_busy !== 1'b0) begin
$error("[tb_iop_dmac_via_map] unexpected busy after programming (no start)");
errors = errors + 1;
end
if (dmac_done_count != 0) begin
$error("[tb_iop_dmac_via_map] done_count nonzero before any start: %0d",
dmac_done_count);
errors = errors + 1;
end
// --------------------------------------------------------------
// Scenario 2: normal flow with ep_ready high throughout
// --------------------------------------------------------------
ep_ready = 1'b1;
iop_write(DMAC_CHCR_ADDR, 32'h0000_0001);
// Allow enough cycles for the full transfer: each beat takes at
// least 2 cycles (fetch + send), plus a few cycles of startup +
// wind-down. 6*BEATS is generous.
repeat (6 * BEATS) @(posedge clk);
if (dmac_done_count != 1) begin
$error("[tb_iop_dmac_via_map] done_count expected 1 after normal flow, got %0d",
dmac_done_count);
errors = errors + 1;
end
if (beat_capture_count < BEATS) begin
$error("[tb_iop_dmac_via_map] captured %0d beats, expected %0d",
beat_capture_count, BEATS);
errors = errors + 1;
end
// Payload order check
for (int i = 0; i < BEATS; i++) begin
if (captured_beats[i] !== payload[i]) begin
$error("[tb_iop_dmac_via_map] beat[%0d] got 0x%08h expected 0x%08h",
i, captured_beats[i], payload[i]);
errors = errors + 1;
end
end
// --------------------------------------------------------------
// Scenario 3: mid-stream stall
// --------------------------------------------------------------
prev_done_count = dmac_done_count;
prev_beat_count = dmac_beat_events;
// Re-program same source+length (transfer already cleared CHCR.0).
iop_write(DMAC_MADR_ADDR, SRC_BASE);
iop_write(DMAC_BCR_ADDR, 32'(BEATS));
// Start with ep_ready low from the beginning.
ep_ready = 1'b0;
iop_write(DMAC_CHCR_ADDR, 32'h0000_0001);
// Wait window with backpressure held. DMAC should reach
// ACTIVE_SEND and wait there — busy high, no DONE.
repeat (10) @(posedge clk);
if (dmac_busy !== 1'b1) begin
$error("[tb_iop_dmac_via_map] busy should be high during mid-stream stall");
errors = errors + 1;
end
if (dmac_done_count != prev_done_count) begin
$error("[tb_iop_dmac_via_map] DMA_DONE fired during stall: %0d→%0d",
prev_done_count, dmac_done_count);
errors = errors + 1;
end
stall_observed = stall_cycles;
if (stall_observed < 5) begin
$error("[tb_iop_dmac_via_map] expected >=5 stall cycles, got %0d", stall_observed);
errors = errors + 1;
end
// --------------------------------------------------------------
// Scenario 4: recovery
// --------------------------------------------------------------
ep_ready = 1'b1;
repeat (6 * BEATS) @(posedge clk);
if (dmac_done_count != 2) begin
$error("[tb_iop_dmac_via_map] done_count expected 2 after recovery, got %0d",
dmac_done_count);
errors = errors + 1;
end
// --------------------------------------------------------------
// Scenario 5: readback via map
// --------------------------------------------------------------
iop_read_expect(DMAC_MADR_ADDR, SRC_BASE, "MADR readback");
iop_read_expect(DMAC_BCR_ADDR, 32'(BEATS), "BCR readback");
iop_read_expect(DMAC_CHCR_ADDR, 32'h0000_0000, "CHCR readback (start cleared)");
repeat (4) @(posedge clk);
// --------------------------------------------------------------
$display("[tb_iop_dmac_via_map] map_dmac=%0d map_ram_dma=%0d cfg=%0d start=%0d beat=%0d done=%0d stall_cy=%0d errors=%0d",
map_dmac_events, map_ram_dma_events,
dmac_cfg_events, dmac_start_events, dmac_beat_events,
dmac_done_events, stall_observed, errors);
// Two transfers × BEATS beats each = 2*BEATS expected DMA_BEAT events
if (dmac_beat_events != 2 * BEATS)
$error("expected %0d EV_DMA_BEAT events, got %0d",
2 * BEATS, dmac_beat_events);
if (dmac_start_events != 2)
$error("expected 2 EV_DMA_START, got %0d", dmac_start_events);
if (dmac_done_events != 2)
$error("expected 2 EV_DMA_DONE, got %0d", dmac_done_events);
if (map_ram_dma_events < BEATS)
$error("expected >=%0d map RAM reads by DMA master, got %0d",
BEATS, map_ram_dma_events);
if (errors == 0 &&
dmac_beat_events == 2 * BEATS &&
dmac_start_events == 2 &&
dmac_done_events == 2 &&
map_ram_dma_events >= BEATS)
$display("[tb_iop_dmac_via_map] PASS");
else
$display("[tb_iop_dmac_via_map] FAIL");
$finish;
end
initial begin
#200000;
$error("[tb_iop_dmac_via_map] timeout");
$finish;
end
endmodule : tb_iop_dmac_via_map