Files
thejayman77 ec82764bef 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>
2026-06-29 20:10:50 -04:00

496 lines
20 KiB
Systemverilog
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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