// retroDE_ps2 — tb_iop_core_basic
//
// First real-decode IOP test. A hand-assembled MIPS R3000 program
// resides in IOP RAM at PROGRAM_BASE. The iop_core_stub fetches,
// decodes, and executes it through the same map / DMAC / INTC plumbing
// the earlier chapters built — no micro-op bridge involved.
//
// The program drives two DMAC ch9 transactions back-to-back. Completion
// is observed by POLLING INTC_STAT through the real memory map (per
// Codex's scope note: defer IRQ-as-exception; prove the decode slice
// with polled completion first). Each transaction ends with a W1C
// INTC_STAT ack through the real register path. The loop-tail BNE
// checks DMAC's readable DONE_COUNT and branches back until it has
// observed two completions. SYSCALL halts the core.
//
// Hand-assembled program (22 instructions, PROGRAM_BASE = 0x0000_0100):
//
//   slot  addr      mnemonic                ; comment
//   ----  --------  ------------------      ; -----------------------------
//    0   0x0100   lui   $1, 0x1F80
//    1   0x0104   ori   $1, $1, 0x1520      ; $1 = DMAC_MADR_ADDR
//    2   0x0108   ori   $2, $0, 0x0200      ; $2 = PAYLOAD_BASE
//    3   0x010C   ori   $3, $0, 0x0004      ; $3 = BCR value
//    4   0x0110   ori   $4, $0, 0x0001      ; $4 = start-bit / ack / mask
//    5   0x0114   lui   $5, 0x1F80
//    6   0x0118   ori   $5, $5, 0x1070      ; $5 = INTC_STAT_ADDR
//    7   0x011C   addiu $6, $5, 0x0004      ; $6 = INTC_MASK_ADDR
//    8   0x0120   addiu $7, $1, 0x000C     ; $7 = DONE_COUNT_ADDR
//    9   0x0124   ori   $8, $0, 0x0002      ; $8 = expected DONE_COUNT (2)
//   10   0x0128   sw    $4,  0($6)          ; INTC_MASK = 1
//   --- loop: ---
//   11   0x012C   sw    $2,  0($1)          ; DMAC_MADR = payload
//   12   0x0130   sw    $3,  4($1)          ; DMAC_BCR  = 4
//   13   0x0134   sw    $4,  8($1)          ; DMAC_CHCR = 1 (start)
//   --- poll: ---
//   14   0x0138   lw    $9,  0($5)          ; $9 = INTC_STAT
//   15   0x013C   beq   $9,  $0, poll       ; spin until STAT != 0
//   16   0x0140   nop                        ; delay slot
//   17   0x0144   sw    $4,  0($5)          ; INTC_STAT = 1 (W1C ack)
//   18   0x0148   lw    $10, 0($7)          ; $10 = DONE_COUNT
//   19   0x014C   bne   $10, $8, loop       ; loop if DONE_COUNT != 2
//   20   0x0150   nop                        ; delay slot
//   21   0x0154   syscall                    ; halt
//
// Acceptance:
//   - halt_o asserts exactly once
//   - 2 DMA_DONE events, 2 INTC assertions, 2 INTC acks
//   - final CORE pc_o parks at the SYSCALL address (no further fetches)
//   - trace shows genuine delay-slot retires on the delay-slot NOPs

`timescale 1ns/1ps

module tb_iop_core_basic;

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

    localparam logic [31:0] PROGRAM_BASE = 32'h0000_0100;
    localparam logic [31:0] PAYLOAD_BASE = 32'h0000_0200;

    // Program slot numbers (kept symbolic for readability)
    localparam int SLOT_LOOP = 11;
    localparam int SLOT_POLL = 14;
    localparam int SLOT_BEQ  = 15;
    localparam int SLOT_BNE  = 19;
    localparam int SLOT_HALT = 21;

    // MIPS opcodes (high 6 bits)
    localparam logic [5:0] OP_SPECIAL = 6'h00;
    localparam logic [5:0] OP_J       = 6'h02;
    localparam logic [5:0] OP_BEQ     = 6'h04;
    localparam logic [5:0] OP_BNE     = 6'h05;
    localparam logic [5:0] OP_ADDIU   = 6'h09;
    localparam logic [5:0] OP_ORI     = 6'h0D;
    localparam logic [5:0] OP_LUI     = 6'h0F;
    localparam logic [5:0] OP_LW      = 6'h23;
    localparam logic [5:0] OP_SW      = 6'h2B;
    localparam logic [5:0] FUNC_SYSCALL = 6'h0C;

    logic clk;
    logic rst_n;

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

    // ------------------------------------------------------------------
    // IOP core — drives the IOP map CPU-side port for ifetch and LW/SW
    // ------------------------------------------------------------------

    logic        core_go;
    logic        core_halt;
    logic [31:0] core_pc;

    logic        map_cpu_rd_en;
    logic [31:0] map_cpu_rd_addr;
    logic [31:0] map_cpu_rd_data;
    logic        map_cpu_rd_valid;

    logic        map_cpu_wr_en;
    logic [31:0] map_cpu_wr_addr;
    logic [31:0] map_cpu_wr_data;
    logic [3:0]  map_cpu_wr_be;

    logic        iop_cpu_irq;

    logic                core_ev_valid;
    trace_pkg::subsys_e  core_ev_subsys;
    trace_pkg::event_e   core_ev_event;
    logic [63:0]         core_ev_arg0, core_ev_arg1, core_ev_arg2, core_ev_arg3;
    logic [31:0]         core_ev_flags;

    iop_core_stub #(.PC_RESET(PROGRAM_BASE)) u_core (
        .clk(clk), .rst_n(rst_n),
        .go_i(core_go),
        .map_rd_en(map_cpu_rd_en), .map_rd_addr(map_cpu_rd_addr),
        .map_rd_data(map_cpu_rd_data), .map_rd_valid(map_cpu_rd_valid),
        .map_wr_en(map_cpu_wr_en), .map_wr_addr(map_cpu_wr_addr),
        .map_wr_data(map_cpu_wr_data), .map_wr_be(map_cpu_wr_be),
        .cpu_irq(iop_cpu_irq),
        .halt_o(core_halt), .pc_o(core_pc),
        .trap_o(), .trap_pc_o(), .trap_instr_o(),
        .ev_valid(core_ev_valid), .ev_subsys(core_ev_subsys),
        .ev_event(core_ev_event),
        .ev_arg0(core_ev_arg0), .ev_arg1(core_ev_arg1),
        .ev_arg2(core_ev_arg2), .ev_arg3(core_ev_arg3),
        .ev_flags(core_ev_flags)
    );

    // ------------------------------------------------------------------
    // IOP map + IOP RAM (TB preloads via bridge_wr_*)
    // ------------------------------------------------------------------

    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;

    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;

    logic        map_intc_rd_en;
    logic [7:0]  map_intc_rd_addr;
    logic [31:0] map_intc_rd_data;
    logic        map_intc_rd_valid;
    logic        map_intc_wr_en;
    logic [7:0]  map_intc_wr_addr;
    logic [31:0] map_intc_wr_data;

    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        tb_br_wr_en;
    logic [31:0] tb_br_wr_addr;
    logic [31:0] tb_br_wr_data;
    logic [3:0]  tb_br_wr_be;

    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(map_cpu_rd_en), .iop_rd_addr(map_cpu_rd_addr),
        .iop_rd_data(map_cpu_rd_data), .iop_rd_valid(map_cpu_rd_valid),
        .iop_wr_en(map_cpu_wr_en), .iop_wr_addr(map_cpu_wr_addr),
        .iop_wr_data(map_cpu_wr_data), .iop_wr_be(map_cpu_wr_be),
        .master_id(8'd2),
        .bridge_wr_en(tb_br_wr_en), .bridge_wr_addr(tb_br_wr_addr),
        .bridge_wr_data(tb_br_wr_data), .bridge_wr_be(tb_br_wr_be),
        .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(), .sif_rd_addr(),
        .sif_rd_data(32'd0), .sif_rd_valid(1'b0),
        .sif_wr_en(), .sif_wr_addr(), .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_rd_en(map_intc_rd_en), .iop_intc_rd_addr(map_intc_rd_addr),
        .iop_intc_rd_data(map_intc_rd_data), .iop_intc_rd_valid(map_intc_rd_valid),
        .iop_intc_wr_en(map_intc_wr_en), .iop_intc_wr_addr(map_intc_wr_addr),
        .iop_intc_wr_data(map_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 — endpoint drained (ep_ready=1); payload on the floor
    // ------------------------------------------------------------------

    logic        ep_valid;
    logic [31:0] ep_data;
    logic        ep_last;
    logic        ep_ready;
    assign ep_ready = 1'b1;

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

    // ------------------------------------------------------------------
    // IOP INTC
    // ------------------------------------------------------------------

    logic [15:0] iop_irq_src;
    assign iop_irq_src = {15'd0, dmac_irq_completion};

    logic                intc_ev_valid;
    trace_pkg::subsys_e  intc_ev_subsys;
    trace_pkg::event_e   intc_ev_event;
    logic [63:0]         intc_ev_arg0, intc_ev_arg1, intc_ev_arg2, intc_ev_arg3;
    logic [31:0]         intc_ev_flags;

    intc_stub #(
        .INTC_STAT_OFFSET(8'h70),
        .INTC_MASK_OFFSET(8'h74)
    ) u_intc (
        .clk(clk), .rst_n(rst_n),
        .reg_wr_en(map_intc_wr_en), .reg_rd_en(map_intc_rd_en),
        .reg_addr(map_intc_wr_en ? map_intc_wr_addr : map_intc_rd_addr),
        .reg_wr_data(map_intc_wr_data),
        .reg_rd_data(map_intc_rd_data), .reg_rd_valid(map_intc_rd_valid),
        .irq_src(iop_irq_src), .cpu_irq(iop_cpu_irq),
        .ev_valid(intc_ev_valid), .ev_subsys(intc_ev_subsys),
        .ev_event(intc_ev_event),
        .ev_arg0(intc_ev_arg0), .ev_arg1(intc_ev_arg1),
        .ev_arg2(intc_ev_arg2), .ev_arg3(intc_ev_arg3),
        .ev_flags(intc_ev_flags)
    );

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

    trace_sink_stub #(.FILENAME("iop_core_basic_core.trace"), .SINK_LABEL("iop_core"))
        u_trace_core (.clk(clk), .rst_n(rst_n),
            .ev_valid(core_ev_valid), .ev_subsys(core_ev_subsys),
            .ev_event(core_ev_event), .ev_arg0(core_ev_arg0),
            .ev_arg1(core_ev_arg1), .ev_arg2(core_ev_arg2),
            .ev_arg3(core_ev_arg3), .ev_flags(core_ev_flags));

    trace_sink_stub #(.FILENAME("iop_core_basic_map.trace"), .SINK_LABEL("iop_map"))
        u_trace_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_core_basic_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_core_basic_intc.trace"), .SINK_LABEL("iop_intc"))
        u_trace_intc (.clk(clk), .rst_n(rst_n),
            .ev_valid(intc_ev_valid), .ev_subsys(intc_ev_subsys),
            .ev_event(intc_ev_event), .ev_arg0(intc_ev_arg0),
            .ev_arg1(intc_ev_arg1), .ev_arg2(intc_ev_arg2),
            .ev_arg3(intc_ev_arg3), .ev_flags(intc_ev_flags));

    // ------------------------------------------------------------------
    // Observers
    // ------------------------------------------------------------------

    int dmac_done_events;
    int intc_assert_events;
    int intc_ack_events;
    int core_retired_events;
    int core_branch_taken_events;
    int core_halt_events;
    int core_delay_slot_events;
    int errors;

    initial begin
        dmac_done_events         = 0;
        intc_assert_events       = 0;
        intc_ack_events          = 0;
        core_retired_events      = 0;
        core_branch_taken_events = 0;
        core_halt_events         = 0;
        core_delay_slot_events   = 0;
        errors                   = 0;
    end

    always_ff @(posedge clk) begin
        if (rst_n && dmac_ev_valid &&
            dmac_ev_subsys == trace_pkg::SUBSYS_DMAC &&
            dmac_ev_event  == trace_pkg::EV_DMA_DONE)
            dmac_done_events <= dmac_done_events + 1;

        if (rst_n && intc_ev_valid &&
            intc_ev_subsys == trace_pkg::SUBSYS_INTC &&
            intc_ev_event  == trace_pkg::EV_IRQ) begin
            if (intc_ev_arg3 == 64'd0 && (intc_ev_flags & 32'h1) == 32'd0)
                intc_assert_events <= intc_assert_events + 1;
            else if (intc_ev_arg3 == 64'd1)
                intc_ack_events <= intc_ack_events + 1;
        end

        if (rst_n && core_ev_valid) begin
            core_retired_events <= core_retired_events + 1;
            if ((core_ev_flags & 32'h4) != 32'd0)
                core_branch_taken_events <= core_branch_taken_events + 1;
            if ((core_ev_flags & 32'h8) != 32'd0)
                core_halt_events <= core_halt_events + 1;
            if ((core_ev_flags & 32'h10) != 32'd0)
                core_delay_slot_events <= core_delay_slot_events + 1;
        end
    end

    // ------------------------------------------------------------------
    // Preload helpers (bridge_wr_* → IOP RAM)
    // ------------------------------------------------------------------

    task automatic bridge_write(input logic [31:0] addr, input logic [31:0] data);
        @(negedge clk);
        tb_br_wr_en   = 1'b1;
        tb_br_wr_addr = addr;
        tb_br_wr_data = data;
        tb_br_wr_be   = 4'b1111;
        @(negedge clk);
        tb_br_wr_en   = 1'b0;
        tb_br_wr_addr = 32'd0;
        tb_br_wr_data = 32'd0;
        tb_br_wr_be   = 4'd0;
    endtask

    task automatic emit_instr(input int slot, input logic [31:0] word);
        bridge_write(PROGRAM_BASE + (slot << 2), word);
    endtask

    // Assembler helpers (MIPS 32-bit encoding)
    function automatic logic [31:0] encode_i(input logic [5:0] op,
                                             input int rs,
                                             input int rt,
                                             input logic [15:0] imm);
        return {op, rs[4:0], rt[4:0], imm};
    endfunction

    function automatic logic [31:0] encode_j(input logic [5:0] op,
                                             input logic [25:0] imm26);
        return {op, imm26};
    endfunction

    // Branch offset: MIPS imm16 is the word-count from the delay-slot PC
    // to the target. For slot-indexed programs: target_slot - slot - 1.
    function automatic logic [15:0] branch_imm(input int branch_slot,
                                               input int target_slot);
        int diff;
        diff = target_slot - branch_slot - 1;
        return diff[15:0];
    endfunction

    task automatic emit_lui(input int slot, input int rt,
                            input logic [15:0] imm);
        emit_instr(slot, encode_i(OP_LUI, 0, rt, imm));
    endtask

    task automatic emit_ori(input int slot, input int rt, input int rs,
                            input logic [15:0] imm);
        emit_instr(slot, encode_i(OP_ORI, rs, rt, imm));
    endtask

    task automatic emit_addiu(input int slot, input int rt, input int rs,
                              input logic [15:0] imm);
        emit_instr(slot, encode_i(OP_ADDIU, rs, rt, imm));
    endtask

    task automatic emit_lw(input int slot, input int rt, input int rs,
                           input logic [15:0] offs);
        emit_instr(slot, encode_i(OP_LW, rs, rt, offs));
    endtask

    task automatic emit_sw(input int slot, input int rt, input int rs,
                           input logic [15:0] offs);
        emit_instr(slot, encode_i(OP_SW, rs, rt, offs));
    endtask

    task automatic emit_beq(input int slot, input int rs, input int rt,
                            input int target_slot);
        emit_instr(slot, encode_i(OP_BEQ, rs, rt, branch_imm(slot, target_slot)));
    endtask

    task automatic emit_bne(input int slot, input int rs, input int rt,
                            input int target_slot);
        emit_instr(slot, encode_i(OP_BNE, rs, rt, branch_imm(slot, target_slot)));
    endtask

    task automatic emit_nop(input int slot);
        emit_instr(slot, 32'd0);
    endtask

    task automatic emit_syscall(input int slot);
        emit_instr(slot, 32'h0000_000C);
    endtask

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

    initial begin
        rst_n         = 1'b0;
        core_go       = 1'b0;
        tb_br_wr_en   = 1'b0;
        tb_br_wr_addr = 32'd0;
        tb_br_wr_data = 32'd0;
        tb_br_wr_be   = 4'd0;

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

        // DMA payload (four distinctive words; contents don't matter for
        // the test, they just have to exist in RAM so the DMAC can fetch)
        bridge_write(PAYLOAD_BASE + 32'h00, 32'hFEED_0000);
        bridge_write(PAYLOAD_BASE + 32'h04, 32'hFEED_0001);
        bridge_write(PAYLOAD_BASE + 32'h08, 32'hFEED_0002);
        bridge_write(PAYLOAD_BASE + 32'h0C, 32'hFEED_0003);

        // Hand-assembled program
        emit_lui  ( 0, 1, 16'h1F80);                 // lui   $1, 0x1F80
        emit_ori  ( 1, 1, 1, 16'h1520);              // ori   $1, $1, 0x1520
        emit_ori  ( 2, 2, 0, 16'h0200);              // ori   $2, $0, 0x0200
        emit_ori  ( 3, 3, 0, 16'h0004);              // ori   $3, $0, 0x0004
        emit_ori  ( 4, 4, 0, 16'h0001);              // ori   $4, $0, 0x0001
        emit_lui  ( 5, 5, 16'h1F80);                 // lui   $5, 0x1F80
        emit_ori  ( 6, 5, 5, 16'h1070);              // ori   $5, $5, 0x1070
        emit_addiu( 7, 6, 5, 16'h0004);              // addiu $6, $5, 0x0004
        emit_addiu( 8, 7, 1, 16'h000C);              // addiu $7, $1, 0x000C
        emit_ori  ( 9, 8, 0, 16'h0002);              // ori   $8, $0, 0x0002
        emit_sw   (10, 4, 6, 16'h0000);              // sw    $4, 0($6)        ; mask=1
        // loop:
        emit_sw   (SLOT_LOOP + 0, 2, 1, 16'h0000);   // sw    $2, 0($1)
        emit_sw   (SLOT_LOOP + 1, 3, 1, 16'h0004);   // sw    $3, 4($1)
        emit_sw   (SLOT_LOOP + 2, 4, 1, 16'h0008);   // sw    $4, 8($1)
        // poll:
        emit_lw   (SLOT_POLL,     9, 5, 16'h0000);   // lw    $9, 0($5)
        emit_beq  (SLOT_BEQ,      9, 0, SLOT_POLL);  // beq   $9, $0, poll
        emit_nop  (SLOT_BEQ + 1);                    // nop (delay slot)
        emit_sw   (SLOT_BEQ + 2,  4, 5, 16'h0000);   // sw    $4, 0($5)        ; W1C ack
        emit_lw   (SLOT_BEQ + 3, 10, 7, 16'h0000);   // lw    $10, 0($7)       ; DONE_COUNT
        emit_bne  (SLOT_BNE,     10, 8, SLOT_LOOP);  // bne   $10, $8, loop
        emit_nop  (SLOT_BNE + 1);                    // nop (delay slot)
        emit_syscall(SLOT_HALT);                     // syscall (halt)

        // Kick the core
        @(negedge clk);
        core_go = 1'b1;
        @(negedge clk);
        core_go = 1'b0;

        // Wait for halt. Polling-spin + two full DMAs + some ifetch
        // overhead; generous timeout.
        while (!core_halt) @(posedge clk);
        repeat (4) @(posedge clk);

        // --------------------------------------------------------------
        // Post-halt checks
        // --------------------------------------------------------------
        if (dmac_done_events != 2) begin
            $error("[tb_iop_core_basic] expected 2 DMA_DONE, got %0d",
                   dmac_done_events);
            errors = errors + 1;
        end
        if (intc_assert_events != 2) begin
            $error("[tb_iop_core_basic] expected 2 INTC asserts, got %0d",
                   intc_assert_events);
            errors = errors + 1;
        end
        if (intc_ack_events != 2) begin
            $error("[tb_iop_core_basic] expected 2 INTC acks, got %0d",
                   intc_ack_events);
            errors = errors + 1;
        end
        if (core_halt_events != 1) begin
            $error("[tb_iop_core_basic] expected 1 halt event, got %0d",
                   core_halt_events);
            errors = errors + 1;
        end
        // Load-bearing delay-slot invariant: every taken branch/jump
        // must produce exactly one delay-slot retire on the instruction
        // that follows it in fetch order. Count should match exactly.
        if (core_delay_slot_events != core_branch_taken_events) begin
            $error("[tb_iop_core_basic] delay-slot count mismatch: %0d delay retires vs %0d taken branches (should be equal)",
                   core_delay_slot_events, core_branch_taken_events);
            errors = errors + 1;
        end
        if (core_branch_taken_events < 2) begin
            // Minimum: BNE taken once (loop-back after first DMA) plus
            // at least one BEQ taken during polling (DMA takes multiple
            // core cycles to complete). Looser bound; timing-dependent.
            $error("[tb_iop_core_basic] expected >=2 taken branches, got %0d",
                   core_branch_taken_events);
            errors = errors + 1;
        end
        // Final pc should park at the syscall instruction
        if (core_pc !== PROGRAM_BASE + (SLOT_HALT << 2)) begin
            $error("[tb_iop_core_basic] final pc mismatch: got 0x%08h expected 0x%08h",
                   core_pc, PROGRAM_BASE + (SLOT_HALT << 2));
            errors = errors + 1;
        end

        $display("[tb_iop_core_basic] retired=%0d branches_taken=%0d delay_slots=%0d halt=%0d done=%0d asserts=%0d acks=%0d errors=%0d",
                 core_retired_events, core_branch_taken_events,
                 core_delay_slot_events, core_halt_events,
                 dmac_done_events, intc_assert_events, intc_ack_events,
                 errors);

        if (errors == 0 &&
            dmac_done_events   == 2 &&
            intc_assert_events == 2 &&
            intc_ack_events    == 2 &&
            core_halt_events   == 1)
            $display("[tb_iop_core_basic] PASS");
        else
            $display("[tb_iop_core_basic] FAIL");

        $finish;
    end

    initial begin
        #5000000;
        $error("[tb_iop_core_basic] timeout");
        $finish;
    end

endmodule : tb_iop_core_basic
