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