// retroDE_ps2 — tb_sif_iop_driven_combined
//
// First IOP-driven combined control+data SIF protocol. Mirror of the
// EE-driven combined handshake (tb_sif_combined_ctrl_data) in the
// reverse direction: the IOP posts a doorbell through the real IOP
// map, the IOP DMAC ch9 moves payload IOP→EE, and an EE-side combiner
// emits the ack back ONLY after the payload has fully landed in EE RAM.
//
// Chain:
//   iop_ram_stub (source)
//     └─► iop_memory_map_stub (dma_rd_* port)
//           └─► iop_dmac_reg_stub (ch9 / SIF0 IOP→EE)
//                 └─► sif_dma_ee_ram_bridge_stub (last_seen_o drives
//                                                 payload_complete)
//                       └─► ee_memory_map_stub (bridge_wr_*)
//                             └─► ee_ram_stub (destination)
//
//   TB (as IOP CPU) also drives iop_memory_map_stub's SIF region at
//   0x1D000000 — SMCOM (cmd) + SMFLG (doorbell) writes, plus polling
//   reads of MSFLG/MSCOM to observe the EE-side ack.
//
//   sif_dma_ee_ack_peer_stub observes the mailbox's EE-side port and
//   the bridge's last_seen_o, and emits the ack (MSCOM echo + MSFLG
//   ACK) once both are true.
//
// Protocol sequence:
//   1. IOP preloads payload into IOP RAM at SRC_BASE.
//   2. IOP writes SMCOM = CAPTURED_CMD (the IOP→EE command value).
//   3. IOP writes SMFLG = CMD_PENDING_BIT (doorbell).
//   4. TB asserts that at this point the combiner has NOT fired:
//      ack_count=0, MSFLG=0. The load-bearing ordering guarantee is
//      that the doorbell alone must not produce an ack.
//   5. IOP programs DMAC ch9: MADR=SRC_BASE, BCR=BEATS.
//   6. IOP starts the DMAC (CHCR bit 0).
//   7. DMA runs. Bridge accumulates qwords, lands them in EE RAM, and
//      raises last_seen_o on the final beat.
//   8. Combiner now has both conditions true. It reads SMCOM, writes
//      MSCOM=cmd, writes MSFLG=CMD_ACK_BIT, and goes DONE.
//   9. IOP polls MSFLG via the IOP map; observes CMD_ACK_BIT.
//  10. IOP reads MSCOM via the IOP map; observes the echoed command.
//  11. EE-side readback verifies the payload landed qword-by-qword.
//
// Explicit one-shot: the combiner fires exactly once and stays DONE.
// Re-armable reverse protocol is a future wave.

`timescale 1ns/1ps

module tb_sif_iop_driven_combined;

    localparam int IOP_RAM_BYTES  = 4 * 1024;
    localparam int IOP_RAM_ADDR_W = $clog2(IOP_RAM_BYTES);
    localparam int EE_RAM_BYTES   = 4 * 1024;
    localparam int EE_RAM_ADDR_W  = $clog2(EE_RAM_BYTES);

    // IOP DMAC ch9 window (architectural addresses through the map)
    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;

    // SIF register window at 0x1D00_0000 (offsets match mailbox layout)
    localparam logic [31:0] SIF_BASE    = 32'h1D00_0000;
    localparam logic [31:0] SIF_MSCOM   = SIF_BASE | 32'h00;
    localparam logic [31:0] SIF_SMCOM   = SIF_BASE | 32'h10;
    localparam logic [31:0] SIF_MSFLG   = SIF_BASE | 32'h20;
    localparam logic [31:0] SIF_SMFLG   = SIF_BASE | 32'h30;

    localparam logic [31:0] SRC_BASE     = 32'h0000_0200;
    localparam logic [31:0] EE_DEST_BASE = 32'h0000_0100;
    localparam int          BEATS        = 16;
    localparam int          QWORDS       = BEATS / 4;

    localparam logic [31:0] CAPTURED_CMD     = 32'hDEAD_C0DE;
    localparam logic [31:0] CMD_PENDING_BIT  = 32'h0000_0001;
    localparam logic [31:0] CMD_ACK_BIT      = 32'h0000_0002;

    logic clk;
    logic rst_n;

    initial clk = 1'b0;
    always #5 clk = ~clk;

    // ------------------------------------------------------------------
    // IOP map + IOP RAM
    // ------------------------------------------------------------------

    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        iop_ram_rd_en;
    logic [20:0] iop_ram_rd_addr;
    logic [31:0] iop_ram_rd_data;
    logic        iop_ram_rd_valid;
    logic        iop_ram_wr_en;
    logic [20:0] iop_ram_wr_addr;
    logic [31:0] iop_ram_wr_data;
    logic [3:0]  iop_ram_wr_be;
    logic [7:0]  iop_ram_master_id;

    // IOP map → mailbox IOP-side
    logic        map_sif_rd_en;
    logic [7:0]  map_sif_rd_addr;
    logic [31:0] map_sif_rd_data;
    logic        map_sif_rd_valid;
    logic        map_sif_wr_en;
    logic [7:0]  map_sif_wr_addr;
    logic [31:0] map_sif_wr_data;

    // IOP map → DMAC ch9 register port
    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;

    // IOP DMAC dma_rd_* to IOP 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                iop_map_ev_valid;
    trace_pkg::subsys_e  iop_map_ev_subsys;
    trace_pkg::event_e   iop_map_ev_event;
    logic [63:0]         iop_map_ev_arg0, iop_map_ev_arg1, iop_map_ev_arg2, iop_map_ev_arg3;
    logic [31:0]         iop_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_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_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_rd_en(map_sif_rd_en), .sif_rd_addr(map_sif_rd_addr),
        .sif_rd_data(map_sif_rd_data), .sif_rd_valid(map_sif_rd_valid),
        .sif_wr_en(map_sif_wr_en), .sif_wr_addr(map_sif_wr_addr),
        .sif_wr_data(map_sif_wr_data),
        .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_rd_en(iop_ram_rd_en), .ram_rd_addr(iop_ram_rd_addr),
        .ram_rd_data(iop_ram_rd_data), .ram_rd_valid(iop_ram_rd_valid),
        .ram_wr_en(iop_ram_wr_en), .ram_wr_addr(iop_ram_wr_addr),
        .ram_wr_data(iop_ram_wr_data), .ram_wr_be(iop_ram_wr_be),
        .ram_master_id(iop_ram_master_id),
        .ev_valid(iop_map_ev_valid), .ev_subsys(iop_map_ev_subsys),
        .ev_event(iop_map_ev_event),
        .ev_arg0(iop_map_ev_arg0), .ev_arg1(iop_map_ev_arg1),
        .ev_arg2(iop_map_ev_arg2), .ev_arg3(iop_map_ev_arg3),
        .ev_flags(iop_map_ev_flags)
    );

    iop_ram_stub #(.SIZE_BYTES(IOP_RAM_BYTES)) u_iop_ram (
        .clk(clk), .rst_n(rst_n),
        .rd_en(iop_ram_rd_en), .rd_addr(iop_ram_rd_addr[IOP_RAM_ADDR_W-1:0]),
        .rd_data(iop_ram_rd_data), .rd_valid(iop_ram_rd_valid),
        .wr_en(iop_ram_wr_en), .wr_addr(iop_ram_wr_addr[IOP_RAM_ADDR_W-1:0]),
        .wr_data(iop_ram_wr_data), .wr_be(iop_ram_wr_be),
        .master_id(iop_ram_master_id),
        .ev_valid(), .ev_subsys(), .ev_event(),
        .ev_arg0(), .ev_arg1(), .ev_arg2(), .ev_arg3(), .ev_flags()
    );

    // ------------------------------------------------------------------
    // IOP DMAC ch9
    // ------------------------------------------------------------------

    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),
        .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),
        .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)
    );

    // ------------------------------------------------------------------
    // SIF egress bridge with last_seen_o
    // ------------------------------------------------------------------

    logic        bridge_in_ready;
    logic        bridge_last_seen;

    assign ep_ready = bridge_in_ready;       // no TB stall injection here

    logic        eebr_wr_en;
    logic [31:0] eebr_wr_addr;
    logic [127:0] eebr_wr_data;
    logic [15:0] eebr_wr_be;
    logic [7:0]  eebr_master_id;

    sif_dma_ee_ram_bridge_stub #(
        .DEST_BASE_ADDR(EE_DEST_BASE),
        .MASTER_ID(8'd5)
    ) u_ee_bridge (
        .clk(clk), .rst_n(rst_n),
        .in_valid(ep_valid), .in_data(ep_data), .in_last(ep_last),
        .in_ready(bridge_in_ready),
        .bridge_wr_en(eebr_wr_en), .bridge_wr_addr(eebr_wr_addr),
        .bridge_wr_data(eebr_wr_data), .bridge_wr_be(eebr_wr_be),
        .bridge_master_id(eebr_master_id),
        .last_seen_o(bridge_last_seen)
    );

    // ------------------------------------------------------------------
    // EE map + EE RAM
    // ------------------------------------------------------------------

    logic        ee_dmac_rd_en;
    logic [31:0] ee_dmac_rd_addr;
    logic [127:0] ee_dmac_rd_data;
    logic        ee_dmac_rd_valid;

    logic        ee_map_ram_rd_en;
    logic [24:0] ee_map_ram_rd_addr;
    logic [127:0] ee_ram_rd_data;
    logic        ee_ram_rd_valid;

    logic        ee_map_ram_wr_en;
    logic [24:0] ee_map_ram_wr_addr;
    logic [127:0] ee_map_ram_wr_data;
    logic [15:0] ee_map_ram_wr_be;
    logic [7:0]  ee_map_ram_master_id;

    logic                ee_map_ev_valid;
    trace_pkg::subsys_e  ee_map_ev_subsys;
    trace_pkg::event_e   ee_map_ev_event;
    logic [63:0]         ee_map_ev_arg0, ee_map_ev_arg1, ee_map_ev_arg2, ee_map_ev_arg3;
    logic [31:0]         ee_map_ev_flags;

    ee_memory_map_stub u_ee_map (
        .clk(clk), .rst_n(rst_n),
        .ee_rd_en(1'b0), .ee_rd_addr(32'd0),
        .ee_rd_data(), .ee_rd_valid(),
        .ee_wr_en(1'b0), .ee_wr_addr(32'd0),
        .ee_wr_data(32'd0), .ee_wr_be(4'd0),
        .dmac_rd_en(ee_dmac_rd_en), .dmac_rd_addr(ee_dmac_rd_addr),
        .dmac_rd_data(ee_dmac_rd_data), .dmac_rd_valid(ee_dmac_rd_valid),
        .bios_rd_en(), .bios_rd_addr(),
        .bios_rd_data(32'd0), .bios_rd_valid(1'b0),
        .ram_rd_en(ee_map_ram_rd_en), .ram_rd_addr(ee_map_ram_rd_addr),
        .ram_rd_data(ee_ram_rd_data), .ram_rd_valid(ee_ram_rd_valid),
        .bridge_wr_en(eebr_wr_en), .bridge_wr_addr(eebr_wr_addr),
        .bridge_wr_data(eebr_wr_data), .bridge_wr_be(eebr_wr_be),
        .bridge_master_id(eebr_master_id),
        .ram_wr_en(ee_map_ram_wr_en), .ram_wr_addr(ee_map_ram_wr_addr),
        .ram_wr_data(ee_map_ram_wr_data), .ram_wr_be(ee_map_ram_wr_be),
        .ram_master_id(ee_map_ram_master_id),
        .ee_dmac_ch2_wr_en(), .ee_dmac_ch2_wr_addr(), .ee_dmac_ch2_wr_data(),
        .ee_dmac_ch2_rd_en(), .ee_dmac_ch2_rd_addr(),
        .ee_dmac_ch2_rd_data(32'd0), .ee_dmac_ch2_rd_valid(1'b0),
        .ee_intc_wr_en(), .ee_intc_wr_addr(), .ee_intc_wr_data(),
        .ee_intc_rd_en(), .ee_intc_rd_addr(),
        .ee_intc_rd_data(32'd0), .ee_intc_rd_valid(1'b0),
        .ee_misc_mmio_wr_en(), .ee_misc_mmio_wr_addr(), .ee_misc_mmio_wr_data(), .ee_misc_mmio_wr_be(),
        .ee_misc_mmio_rd_en(), .ee_misc_mmio_rd_addr(),
        .ee_misc_mmio_rd_data(32'd0), .ee_misc_mmio_rd_valid(1'b0),
        .ee_biu_wr_en(), .ee_biu_wr_addr(), .ee_biu_wr_data(), .ee_biu_wr_be(),
        .ee_biu_rd_en(), .ee_biu_rd_addr(),
        .ee_biu_rd_data(32'd0), .ee_biu_rd_valid(1'b0),
        .ev_valid(ee_map_ev_valid), .ev_subsys(ee_map_ev_subsys),
        .ev_event(ee_map_ev_event),
        .ev_arg0(ee_map_ev_arg0), .ev_arg1(ee_map_ev_arg1),
        .ev_arg2(ee_map_ev_arg2), .ev_arg3(ee_map_ev_arg3),
        .ev_flags(ee_map_ev_flags)
    );

    ee_ram_stub #(.SIZE_BYTES(EE_RAM_BYTES)) u_ee_ram (
        .clk(clk), .rst_n(rst_n),
        .rd_en(ee_map_ram_rd_en), .rd_addr(ee_map_ram_rd_addr[EE_RAM_ADDR_W-1:0]),
        .rd_data(ee_ram_rd_data), .rd_valid(ee_ram_rd_valid),
        .wr_en(ee_map_ram_wr_en), .wr_addr(ee_map_ram_wr_addr[EE_RAM_ADDR_W-1:0]),
        .wr_data(ee_map_ram_wr_data), .wr_be(ee_map_ram_wr_be),
        .master_id(ee_map_ram_master_id),
        .ev_valid(), .ev_subsys(), .ev_event(),
        .ev_arg0(), .ev_arg1(), .ev_arg2(), .ev_arg3(), .ev_flags()
    );

    // ------------------------------------------------------------------
    // SIF mailbox
    //   - IOP-side: driven by iop_memory_map_stub.sif_*
    //   - EE-side:  driven by the combiner (muxed rd/wr on a shared port)
    // ------------------------------------------------------------------

    logic        comb_obs_rd_en;
    logic [7:0]  comb_obs_rd_addr;
    logic [31:0] comb_obs_rd_data;
    logic        comb_obs_rd_valid;
    logic        comb_resp_wr_en;
    logic [7:0]  comb_resp_wr_addr;
    logic [31:0] comb_resp_wr_data;

    logic        mbx_ee_wr_en;
    logic        mbx_ee_rd_en;
    logic [7:0]  mbx_ee_addr;
    logic [31:0] mbx_ee_wr_data;
    logic [31:0] mbx_ee_rd_data;
    logic        mbx_ee_rd_valid;

    // Combiner share of the EE mailbox port: write wins over read on
    // the address mux (they don't overlap in the combiner's state
    // machine — writes in S_WRITE_*, reads in S_POLL_WAIT/S_SMCOM_WAIT).
    assign mbx_ee_wr_en   = comb_resp_wr_en;
    assign mbx_ee_rd_en   = comb_obs_rd_en;
    assign mbx_ee_addr    = comb_resp_wr_en ? comb_resp_wr_addr : comb_obs_rd_addr;
    assign mbx_ee_wr_data = comb_resp_wr_data;
    assign comb_obs_rd_data  = mbx_ee_rd_data;
    assign comb_obs_rd_valid = mbx_ee_rd_valid;

    logic                mbx_ev_valid;
    trace_pkg::subsys_e  mbx_ev_subsys;
    trace_pkg::event_e   mbx_ev_event;
    logic [63:0]         mbx_ev_arg0, mbx_ev_arg1, mbx_ev_arg2, mbx_ev_arg3;
    logic [31:0]         mbx_ev_flags;

    sif_mailbox_stub u_mailbox (
        .clk(clk), .rst_n(rst_n),
        .ee_wr_en(mbx_ee_wr_en), .ee_rd_en(mbx_ee_rd_en),
        .ee_addr(mbx_ee_addr), .ee_wr_data(mbx_ee_wr_data),
        .ee_rd_data(mbx_ee_rd_data), .ee_rd_valid(mbx_ee_rd_valid),
        // IOP side driven by the IOP map's sif_* downstream
        .iop_wr_en(map_sif_wr_en), .iop_rd_en(map_sif_rd_en),
        .iop_addr(map_sif_wr_en ? map_sif_wr_addr : map_sif_rd_addr),
        .iop_wr_data(map_sif_wr_data),
        .iop_rd_data(map_sif_rd_data), .iop_rd_valid(map_sif_rd_valid),
        .ev_valid(mbx_ev_valid), .ev_subsys(mbx_ev_subsys),
        .ev_event(mbx_ev_event),
        .ev_arg0(mbx_ev_arg0), .ev_arg1(mbx_ev_arg1),
        .ev_arg2(mbx_ev_arg2), .ev_arg3(mbx_ev_arg3),
        .ev_flags(mbx_ev_flags)
    );

    // ------------------------------------------------------------------
    // Combiner
    // ------------------------------------------------------------------

    logic        comb_done;
    logic [31:0] comb_ack_count;

    sif_dma_ee_ack_peer_stub u_combiner (
        .clk(clk), .rst_n(rst_n),
        .obs_rd_en(comb_obs_rd_en), .obs_rd_addr(comb_obs_rd_addr),
        .obs_rd_data(comb_obs_rd_data), .obs_rd_valid(comb_obs_rd_valid),
        .resp_wr_en(comb_resp_wr_en), .resp_wr_addr(comb_resp_wr_addr),
        .resp_wr_data(comb_resp_wr_data),
        .payload_complete(bridge_last_seen),
        .done_o(comb_done),
        .ack_count_o(comb_ack_count)
    );

    // ------------------------------------------------------------------
    // Trace sinks
    // ------------------------------------------------------------------

    trace_sink_stub #(.FILENAME("iop_driven_iop_map.trace"), .SINK_LABEL("iop_map"))
        u_trace_iop_map (.clk(clk), .rst_n(rst_n),
            .ev_valid(iop_map_ev_valid), .ev_subsys(iop_map_ev_subsys),
            .ev_event(iop_map_ev_event), .ev_arg0(iop_map_ev_arg0),
            .ev_arg1(iop_map_ev_arg1), .ev_arg2(iop_map_ev_arg2),
            .ev_arg3(iop_map_ev_arg3), .ev_flags(iop_map_ev_flags));

    trace_sink_stub #(.FILENAME("iop_driven_ee_map.trace"), .SINK_LABEL("ee_map"))
        u_trace_ee_map (.clk(clk), .rst_n(rst_n),
            .ev_valid(ee_map_ev_valid), .ev_subsys(ee_map_ev_subsys),
            .ev_event(ee_map_ev_event), .ev_arg0(ee_map_ev_arg0),
            .ev_arg1(ee_map_ev_arg1), .ev_arg2(ee_map_ev_arg2),
            .ev_arg3(ee_map_ev_arg3), .ev_flags(ee_map_ev_flags));

    trace_sink_stub #(.FILENAME("iop_driven_dmac.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));

    trace_sink_stub #(.FILENAME("iop_driven_mailbox.trace"), .SINK_LABEL("sif_mbx"))
        u_trace_mbx (.clk(clk), .rst_n(rst_n),
            .ev_valid(mbx_ev_valid), .ev_subsys(mbx_ev_subsys),
            .ev_event(mbx_ev_event), .ev_arg0(mbx_ev_arg0),
            .ev_arg1(mbx_ev_arg1), .ev_arg2(mbx_ev_arg2),
            .ev_arg3(mbx_ev_arg3), .ev_flags(mbx_ev_flags));

    // ------------------------------------------------------------------
    // Counters
    // ------------------------------------------------------------------

    int dmac_start_events;
    int dmac_beat_events;
    int dmac_done_events;
    int ee_map_ram_writes;
    int errors;

    initial begin
        dmac_start_events = 0;
        dmac_beat_events  = 0;
        dmac_done_events  = 0;
        ee_map_ram_writes = 0;
        errors            = 0;
    end

    always_ff @(posedge clk) begin
        if (rst_n && dmac_ev_valid &&
            dmac_ev_subsys == trace_pkg::SUBSYS_DMAC) begin
            case (dmac_ev_event)
                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 && ee_map_ev_valid &&
            ee_map_ev_subsys == trace_pkg::SUBSYS_MEM &&
            ee_map_ev_event  == trace_pkg::EV_WRITE &&
            ee_map_ev_arg3[7:0] == 8'd1)
            ee_map_ram_writes <= ee_map_ram_writes + 1;
    end

    // ------------------------------------------------------------------
    // Helpers (IOP-perspective via map)
    // ------------------------------------------------------------------

    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(input logic [31:0] addr,
                            output logic [31:0] data);
        @(negedge clk);
        iop_rd_en   = 1'b1;
        iop_rd_addr = addr;
        @(negedge clk);
        iop_rd_en   = 1'b0;
        iop_rd_addr = 32'd0;
        data = iop_rd_data;
    endtask

    task automatic ee_dmac_read_qword(input logic [31:0] addr,
                                      output logic [127:0] data);
        @(negedge clk);
        ee_dmac_rd_en   = 1'b1;
        ee_dmac_rd_addr = addr;
        @(negedge clk);
        ee_dmac_rd_en   = 1'b0;
        ee_dmac_rd_addr = 32'd0;
        @(posedge clk);
        data = ee_dmac_rd_data;
    endtask

    // ------------------------------------------------------------------
    // Stimulus
    // ------------------------------------------------------------------

    logic [31:0] payload [0:BEATS-1];
    logic [31:0] pre_dma_ack_count;
    logic [31:0] msflg_val;
    logic [31:0] mscom_val;
    logic [127:0] expected_qw;
    logic [127:0] got_qw;

    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;
        ee_dmac_rd_en   = 1'b0;
        ee_dmac_rd_addr = 32'd0;

        for (int i = 0; i < BEATS; i++) payload[i] = 32'hCAFE_0000 | i[31:0];

        repeat (4) @(posedge clk);
        rst_n = 1'b1;
        repeat (2) @(posedge clk);

        // Phase 1: preload IOP RAM.
        for (int i = 0; i < BEATS; i++) begin
            iop_write(SRC_BASE + (i << 2), payload[i]);
        end

        // Phase 2: IOP posts SMCOM (command value) via IOP map.
        iop_write(SIF_SMCOM, CAPTURED_CMD);

        // Phase 3: IOP rings doorbell (SMFLG = CMD_PENDING_BIT).
        iop_write(SIF_SMFLG, CMD_PENDING_BIT);

        // Phase 4: ordering guarantee — give the combiner time to poll
        // SMFLG. Even though SMFLG is set, payload_complete is NOT yet
        // set, so the combiner must NOT advance past S_POLL_WAIT.
        repeat (20) @(posedge clk);
        pre_dma_ack_count = comb_ack_count;
        if (pre_dma_ack_count != 32'd0) begin
            $error("[tb_sif_iop_driven_combined] combiner acked before DMA (count=%0d)",
                   pre_dma_ack_count);
            errors = errors + 1;
        end
        if (comb_done) begin
            $error("[tb_sif_iop_driven_combined] combiner flagged done before DMA");
            errors = errors + 1;
        end
        iop_read(SIF_MSFLG, msflg_val);
        if (msflg_val !== 32'd0) begin
            $error("[tb_sif_iop_driven_combined] MSFLG nonzero pre-DMA: 0x%08h", msflg_val);
            errors = errors + 1;
        end

        // Phase 5: program DMAC ch9 and start.
        iop_write(DMAC_MADR_ADDR, SRC_BASE);
        iop_write(DMAC_BCR_ADDR,  32'(BEATS));
        iop_write(DMAC_CHCR_ADDR, 32'h0000_0001);

        // Phase 6: wait for DMA completion + combiner ack.
        repeat (10 * BEATS) @(posedge clk);

        if (dmac_done_count != 1) begin
            $error("[tb_sif_iop_driven_combined] DMA done_count expected 1, got %0d",
                   dmac_done_count);
            errors = errors + 1;
        end
        if (comb_ack_count != 32'd1) begin
            $error("[tb_sif_iop_driven_combined] combiner ack_count expected 1, got %0d",
                   comb_ack_count);
            errors = errors + 1;
        end
        if (!comb_done) begin
            $error("[tb_sif_iop_driven_combined] combiner done_o not asserted");
            errors = errors + 1;
        end

        // Phase 7: IOP polls MSFLG through the real map, expects ACK.
        iop_read(SIF_MSFLG, msflg_val);
        if ((msflg_val & CMD_ACK_BIT) == 32'd0) begin
            $error("[tb_sif_iop_driven_combined] MSFLG missing ACK bit: 0x%08h", msflg_val);
            errors = errors + 1;
        end

        // Phase 8: IOP reads MSCOM through the real map, expects echo.
        iop_read(SIF_MSCOM, mscom_val);
        if (mscom_val !== CAPTURED_CMD) begin
            $error("[tb_sif_iop_driven_combined] MSCOM echo got 0x%08h expected 0x%08h",
                   mscom_val, CAPTURED_CMD);
            errors = errors + 1;
        end

        // Phase 9: EE-side payload readback via ee_map.dmac_rd_*.
        for (int q = 0; q < QWORDS; q++) begin
            expected_qw = { payload[q*4 + 3], payload[q*4 + 2],
                            payload[q*4 + 1], payload[q*4 + 0] };
            ee_dmac_read_qword(EE_DEST_BASE + (q << 4), got_qw);
            if (got_qw !== expected_qw) begin
                $error("[tb_sif_iop_driven_combined] qw[%0d] got 0x%032h expected 0x%032h",
                       q, got_qw, expected_qw);
                errors = errors + 1;
            end
        end

        repeat (4) @(posedge clk);

        $display("[tb_sif_iop_driven_combined] start=%0d beat=%0d done=%0d ee_map_writes=%0d pre_dma_ack=%0d ack=%0d errors=%0d",
                 dmac_start_events, dmac_beat_events, dmac_done_events,
                 ee_map_ram_writes, pre_dma_ack_count, comb_ack_count, errors);

        if (dmac_start_events != 1)
            $error("expected 1 DMA_START, got %0d", dmac_start_events);
        if (dmac_beat_events != BEATS)
            $error("expected %0d DMA_BEAT, got %0d", BEATS, dmac_beat_events);
        if (dmac_done_events != 1)
            $error("expected 1 DMA_DONE, got %0d", dmac_done_events);
        if (ee_map_ram_writes != QWORDS)
            $error("expected %0d EE map writes, got %0d",
                   QWORDS, ee_map_ram_writes);

        if (errors == 0 &&
            dmac_start_events == 1 &&
            dmac_beat_events  == BEATS &&
            dmac_done_events  == 1 &&
            ee_map_ram_writes == QWORDS &&
            pre_dma_ack_count == 32'd0 &&
            comb_ack_count    == 32'd1)
            $display("[tb_sif_iop_driven_combined] PASS");
        else
            $display("[tb_sif_iop_driven_combined] FAIL");

        $finish;
    end

    initial begin
        #500000;
        $error("[tb_sif_iop_driven_combined] timeout");
        $finish;
    end

endmodule : tb_sif_iop_driven_combined
