// retroDE_ps2 — tb_iop_core_bios_smoke
//
// Strict-mode BIOS smoke bring-up. Boots the IOP core from 0xBFC0_0000
// through the real shared BIOS path, with `iop_core_stub`'s
// STRICT_UNSUPPORTED mode enabled so the first unsupported instruction
// the BIOS hits halts the core loudly instead of silently retiring as
// a NOP. The trap outputs and the retire trace then tell us exactly
// which opcode the core needs to grow next.
//
// The committed TB runs a synthetic BIOS-ish program whose last real
// instruction is deliberately one the core does NOT decode — `AND` (a
// SPECIAL R-type at func 0x24). That keeps CI deterministic while
// proving the strict-mode mechanism end-to-end.
//
// Swapping in a real BIOS dump:
//   Replace the hand-assembled BIOS-preload in this TB's `initial`
//   block with `$readmemh("path/to/bios.hex", u_bios.mem);` — or pass
//   an IMAGE_FILE parameter to the bios_rom_stub instance below. No
//   Sony BIOS is shipped with the repository (see
//   third_party/LICENSING.md). When a real BIOS is loaded, this TB
//   will report the first opcode the current core can't execute.
//
// BIOS layout (TB preloads via hierarchical reference):
//   0xBFC0_0000   j    0xBFC0_0100
//   0xBFC0_0004   nop (delay slot)
//   0xBFC0_0008..0xBFC0_00FF   default NOP sled (bios_rom_stub fixture)
//   0xBFC0_0100   lui   $1, 0x1F80
//   0xBFC0_0104   ori   $1, $1, 0x1520        ; DMAC_MADR
//   0xBFC0_0108   ori   $2, $0, 0x0200         ; payload base
//   0xBFC0_010C   ori   $3, $0, 0x0004
//   0xBFC0_0110   ori   $4, $0, 0x0001
//   0xBFC0_0114   addiu $6, $1, 0x000C         ; DONE_COUNT addr
//   0xBFC0_0118   sw    $2, 0($1)              ; plausible kick-off work
//   0xBFC0_011C   sw    $3, 4($1)
//   0xBFC0_0120   and   $5, $2, $3             ; UNSUPPORTED → strict trap
//   (core halts before reaching any later instruction)
//
// Acceptance:
//   - halt_o asserts exactly once
//   - trap_o asserts; trap_pc_o matches the pc of the AND instruction
//   - trap_instr_o equals the AND encoding (0x00432824)
//   - the core retired exactly 9 instructions before trapping:
//     reset J + delay NOP + 7 setup instructions
//   - the retire immediately preceding the halt has flag bit 7 (trap)
//   - no exception vectoring occurred (strict trap is a halt, not
//     an exception)
//   - the DMA subsystem was NOT successfully kicked (no DMA_DONE)

`timescale 1ns/1ps

module tb_iop_core_bios_smoke;

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

    localparam logic [31:0] RESET_VECTOR = 32'hBFC0_0000;
    localparam logic [31:0] BOOTSTRAP_VA = 32'hBFC0_0100;

    localparam int RESET_SLOT      = 0;
    localparam int BOOTSTRAP_SLOT0 = 32'h100 >> 2;

    // Expected unsupported instruction (AND $5, $2, $3):
    //   SPECIAL opcode 0x00, rs=2, rt=3, rd=5, shamt=0, func=0x24
    //   = 000000 | 00010 | 00011 | 00101 | 00000 | 100100
    //   = 0x00432824
    localparam logic [31:0] EXPECTED_TRAP_INSTR = 32'h0043_2824;

    // Slot of the AND instruction within the bootstrap
    localparam int AND_BOOTSTRAP_OFFSET = 8;
    localparam logic [31:0] EXPECTED_TRAP_PC =
        BOOTSTRAP_VA + (AND_BOOTSTRAP_OFFSET << 2);

    // MIPS opcodes
    localparam logic [5:0] OP_SPECIAL = 6'h00;
    localparam logic [5:0] OP_J       = 6'h02;
    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;

    logic clk;
    logic rst_n;

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

    // ------------------------------------------------------------------
    // IOP core — strict mode ON
    // ------------------------------------------------------------------

    logic        core_go;
    logic        core_halt;
    logic [31:0] core_pc;
    logic        core_trap;
    logic [31:0] core_trap_pc;
    logic [31:0] core_trap_instr;

    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 #(
        .STRICT_UNSUPPORTED(1'b1)
    ) 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(core_trap), .trap_pc_o(core_trap_pc),
        .trap_instr_o(core_trap_instr),
        .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 + RAM + BIOS + DMAC + INTC
    // (DMAC/INTC present mostly so existing IOP-map port tie-offs match
    //  the normal pattern; the BIOS smoke doesn't successfully kick DMA.)
    // ------------------------------------------------------------------

    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        bios_rd_en;
    logic [21:0] bios_rd_addr;
    logic [31:0] bios_rd_data;
    logic        bios_rd_valid;

    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(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(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(), .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_rd_en(bios_rd_en), .bios_rd_addr(bios_rd_addr),
        .bios_rd_data(bios_rd_data), .bios_rd_valid(bios_rd_valid),
        .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()
    );

    // Swap IMAGE_FILE here with a user-supplied Sony BIOS hex dump to
    // run a real-BIOS smoke pass. The hand-assembled content in the
    // stimulus block is only the fallback synthetic program.
    bios_rom_stub #(.SIZE_BYTES(BIOS_BYTES)) u_bios (
        .clk(clk), .rst_n(rst_n),
        .rd_en(bios_rd_en), .rd_addr(bios_rd_addr),
        .rd_data(bios_rd_data), .rd_valid(bios_rd_valid),
        .ev_valid(), .ev_subsys(), .ev_event(),
        .ev_arg0(), .ev_arg1(), .ev_arg2(), .ev_arg3(), .ev_flags()
    );

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

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

    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(), .ev_subsys(), .ev_event(),
        .ev_arg0(), .ev_arg1(), .ev_arg2(), .ev_arg3(), .ev_flags()
    );

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

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

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

    int dmac_done_events;
    int core_retired_events;
    int core_trap_events;
    int core_exception_events;
    int core_halt_events;
    int bios_reads;
    int errors;

    // Opcode histogram (by retired instruction's opcode field [31:26]).
    // Counted only on supported-instruction retires (i.e., before trap).
    int op_count_lui;
    int op_count_ori;
    int op_count_addiu;
    int op_count_sw;
    int op_count_lw;
    int op_count_j;
    int op_count_other;

    initial begin
        dmac_done_events      = 0;
        core_retired_events   = 0;
        core_trap_events      = 0;
        core_exception_events = 0;
        core_halt_events      = 0;
        bios_reads            = 0;
        op_count_lui          = 0;
        op_count_ori          = 0;
        op_count_addiu        = 0;
        op_count_sw           = 0;
        op_count_lw           = 0;
        op_count_j            = 0;
        op_count_other        = 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 && core_ev_valid) begin
            core_retired_events <= core_retired_events + 1;
            if ((core_ev_flags & 32'h08) != 32'd0)
                core_halt_events <= core_halt_events + 1;
            if ((core_ev_flags & 32'h20) != 32'd0)
                core_exception_events <= core_exception_events + 1;
            if ((core_ev_flags & 32'h80) != 32'd0)
                core_trap_events <= core_trap_events + 1;

            // Histogram: only count non-trap retires (supported ops).
            if ((core_ev_flags & 32'h80) == 32'd0) begin
                case (core_ev_arg1[31:26])
                    OP_LUI:   op_count_lui   <= op_count_lui   + 1;
                    OP_ORI:   op_count_ori   <= op_count_ori   + 1;
                    OP_ADDIU: op_count_addiu <= op_count_addiu + 1;
                    OP_SW:    op_count_sw    <= op_count_sw    + 1;
                    OP_LW:    op_count_lw    <= op_count_lw    + 1;
                    OP_J:     op_count_j     <= op_count_j     + 1;
                    default:  op_count_other <= op_count_other + 1;
                endcase
            end
        end

        if (rst_n && iop_map_ev_valid &&
            iop_map_ev_subsys == trace_pkg::SUBSYS_IOP &&
            iop_map_ev_event  == trace_pkg::EV_READ &&
            iop_map_ev_arg3[7:0] == 8'd6)                      // IOP_BIOS
            bios_reads <= bios_reads + 1;
    end

    // ------------------------------------------------------------------
    // Mnemonic helper — short decode name for a 32-bit MIPS word.
    // Used by the halt report so the iteration loop (drop in real BIOS,
    // read output, implement first missing opcode) doesn't require a
    // human disassembler on the side. Covers the whole R3000 opcode
    // space we expect real BIOS code to use, including the opcodes
    // iop_core_stub does NOT yet implement (ANDI, ADDU, SLL, JAL...).
    // ------------------------------------------------------------------

    function automatic string mips_mnemonic(input logic [31:0] word);
        logic [5:0] op, func;
        logic [4:0] rt_field;

        if (word == 32'd0) return "nop";

        op   = word[31:26];
        func = word[5:0];
        rt_field = word[20:16];

        if (op == 6'h00) begin
            // SPECIAL — R-type. func selects.
            case (func)
                6'h00: return "sll";
                6'h02: return "srl";
                6'h03: return "sra";
                6'h04: return "sllv";
                6'h06: return "srlv";
                6'h07: return "srav";
                6'h08: return "jr";
                6'h09: return "jalr";
                6'h0C: return "syscall";
                6'h0D: return "break";
                6'h10: return "mfhi";
                6'h11: return "mthi";
                6'h12: return "mflo";
                6'h13: return "mtlo";
                6'h18: return "mult";
                6'h19: return "multu";
                6'h1A: return "div";
                6'h1B: return "divu";
                6'h20: return "add";
                6'h21: return "addu";
                6'h22: return "sub";
                6'h23: return "subu";
                6'h24: return "and";
                6'h25: return "or";
                6'h26: return "xor";
                6'h27: return "nor";
                6'h2A: return "slt";
                6'h2B: return "sltu";
                default: return $sformatf("SPECIAL?(%02h)", func);
            endcase
        end else if (op == 6'h01) begin
            // REGIMM — rt selects.
            case (rt_field)
                5'h00: return "bltz";
                5'h01: return "bgez";
                5'h10: return "bltzal";
                5'h11: return "bgezal";
                default: return $sformatf("REGIMM?(%02h)", rt_field);
            endcase
        end else if (op == 6'h10) begin
            // COP0 — rs selects.
            case (word[25:21])
                5'h00: return "mfc0";
                5'h04: return "mtc0";
                5'h10: begin
                    if (func == 6'h10) return "rfe";
                    return $sformatf("COP0.CO?(func=%02h)", func);
                end
                default: return $sformatf("COP0?(rs=%02h)", word[25:21]);
            endcase
        end
        case (op)
            6'h02: return "j";
            6'h03: return "jal";
            6'h04: return "beq";
            6'h05: return "bne";
            6'h06: return "blez";
            6'h07: return "bgtz";
            6'h08: return "addi";
            6'h09: return "addiu";
            6'h0A: return "slti";
            6'h0B: return "sltiu";
            6'h0C: return "andi";
            6'h0D: return "ori";
            6'h0E: return "xori";
            6'h0F: return "lui";
            6'h20: return "lb";
            6'h21: return "lh";
            6'h22: return "lwl";
            6'h23: return "lw";
            6'h24: return "lbu";
            6'h25: return "lhu";
            6'h26: return "lwr";
            6'h28: return "sb";
            6'h29: return "sh";
            6'h2A: return "swl";
            6'h2B: return "sw";
            6'h2E: return "swr";
            default: return $sformatf("OP?(%02h)", op);
        endcase
    endfunction

    // ------------------------------------------------------------------
    // Preload helpers
    // ------------------------------------------------------------------

    task automatic bios_word(input int slot, input logic [31:0] word);
        u_bios.mem[slot] = word;
    endtask

    function automatic logic [31:0] enc_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] enc_j(input logic [31:0] target_addr);
        return {OP_J, target_addr[27:2]};
    endfunction

    function automatic logic [31:0] enc_and(input int rd, input int rs,
                                            input int rt);
        // SPECIAL opcode, func=0x24 (AND). Unsupported by iop_core_stub.
        return {OP_SPECIAL, rs[4:0], rt[4:0], rd[4:0], 5'd0, 6'h24};
    endfunction

    task automatic bios_lui(input int slot, input int rt,
                            input logic [15:0] imm);
        bios_word(slot, enc_i(OP_LUI, 0, rt, imm));
    endtask
    task automatic bios_ori(input int slot, input int rt, input int rs,
                            input logic [15:0] imm);
        bios_word(slot, enc_i(OP_ORI, rs, rt, imm));
    endtask
    task automatic bios_addiu(input int slot, input int rt, input int rs,
                              input logic [15:0] imm);
        bios_word(slot, enc_i(OP_ADDIU, rs, rt, imm));
    endtask
    task automatic bios_sw(input int slot, input int rt, input int rs,
                           input logic [15:0] offs);
        bios_word(slot, enc_i(OP_SW, rs, rt, offs));
    endtask
    task automatic bios_nop(input int slot);
        bios_word(slot, 32'd0);
    endtask
    task automatic bios_j(input int slot, input logic [31:0] target_va);
        bios_word(slot, enc_j(target_va));
    endtask
    task automatic bios_and(input int slot, input int rd, input int rs,
                            input int rt);
        bios_word(slot, enc_and(rd, rs, rt));
    endtask

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

    // BIOS image path (plusarg-driven). When +BIOS=<hexpath> is supplied
    // on the vvp command line, the TB loads that file via $readmemh into
    // u_bios.mem and skips the synthetic preload. Strict pc/instr
    // assertions are replaced with a minimal "something tripped trap_o"
    // check so we can learn what the real BIOS did.
    string bios_image_path;
    logic  using_real_bios;

    initial begin
        rst_n   = 1'b0;
        core_go = 1'b0;

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

        using_real_bios = $value$plusargs("BIOS=%s", bios_image_path);
        if (using_real_bios) begin
            $display("[tb_iop_core_bios_smoke] loading real BIOS image: %s",
                     bios_image_path);
            $readmemh(bios_image_path, u_bios.mem);
        end else begin
            // Synthetic BIOS-like bootstrap used for the deterministic
            // CI regression: reset jump + a handful of supported setup
            // ops, then an AND that the core does not support.
            bios_j  (RESET_SLOT,     BOOTSTRAP_VA);
            bios_nop(RESET_SLOT + 1);                       // delay slot

            bios_lui  (BOOTSTRAP_SLOT0 + 0,  1, 16'h1F80);
            bios_ori  (BOOTSTRAP_SLOT0 + 1,  1,  1, 16'h1520);
            bios_ori  (BOOTSTRAP_SLOT0 + 2,  2,  0, 16'h0200);
            bios_ori  (BOOTSTRAP_SLOT0 + 3,  3,  0, 16'h0004);
            bios_ori  (BOOTSTRAP_SLOT0 + 4,  4,  0, 16'h0001);
            bios_addiu(BOOTSTRAP_SLOT0 + 5,  6,  1, 16'h000C);
            bios_sw   (BOOTSTRAP_SLOT0 + 6,  2,  1, 16'h0000);
            bios_sw   (BOOTSTRAP_SLOT0 + 7,  3,  1, 16'h0004);
            bios_and  (BOOTSTRAP_SLOT0 + AND_BOOTSTRAP_OFFSET,
                                        5,  2,  3);          // UNSUPPORTED
        end

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

        while (!core_halt) @(posedge clk);
        repeat (4) @(posedge clk);

        // --------------------------------------------------------------
        // Post-halt reporting (common to both modes)
        // --------------------------------------------------------------
        $display("[tb_iop_core_bios_smoke] retired=%0d trap=%0d trap_pc=0x%08h trap_instr=0x%08h mnemonic=%s halt=%0d except=%0d done=%0d bios_reads=%0d hist(lui,ori,addiu,sw,lw,j,other)=(%0d,%0d,%0d,%0d,%0d,%0d,%0d) errors=%0d",
                 core_retired_events, core_trap_events,
                 core_trap_pc, core_trap_instr,
                 mips_mnemonic(core_trap_instr),
                 core_halt_events, core_exception_events,
                 dmac_done_events, bios_reads,
                 op_count_lui, op_count_ori, op_count_addiu,
                 op_count_sw, op_count_lw, op_count_j, op_count_other,
                 errors);

        // --------------------------------------------------------------
        // Mode-specific post-halt checks
        // --------------------------------------------------------------
        if (using_real_bios) begin
            // Real-BIOS iteration: no hardcoded trap pc/instr. Assert
            // only that the core terminated via strict trap (not via
            // SYSCALL, not via exception, not via timeout). Whatever
            // trap_instr turns out to be is the next opcode to add.
            if (!core_trap) begin
                $error("[tb_iop_core_bios_smoke] real-BIOS run did not hit a strict trap; core halted via %s",
                       (core_halt_events > 0) ? "SYSCALL" : "other");
                errors = errors + 1;
            end
            if (core_trap_events != 1) begin
                $error("[tb_iop_core_bios_smoke] expected exactly 1 trap retire, got %0d",
                       core_trap_events);
                errors = errors + 1;
            end
        end else begin
            // Synthetic-CI mode: strict, deterministic assertions.
            if (!core_trap) begin
                $error("[tb_iop_core_bios_smoke] trap_o not asserted at halt");
                errors = errors + 1;
            end
            if (core_trap_pc !== EXPECTED_TRAP_PC) begin
                $error("[tb_iop_core_bios_smoke] trap_pc mismatch: got 0x%08h expected 0x%08h",
                       core_trap_pc, EXPECTED_TRAP_PC);
                errors = errors + 1;
            end
            if (core_trap_instr !== EXPECTED_TRAP_INSTR) begin
                $error("[tb_iop_core_bios_smoke] trap_instr mismatch: got 0x%08h expected 0x%08h",
                       core_trap_instr, EXPECTED_TRAP_INSTR);
                errors = errors + 1;
            end
            if (core_trap_events != 1) begin
                $error("[tb_iop_core_bios_smoke] expected 1 trap retire, got %0d",
                       core_trap_events);
                errors = errors + 1;
            end
            // Reset J + delay NOP + 8 supported bootstrap ops + trap = 11
            if (core_retired_events != 11) begin
                $error("[tb_iop_core_bios_smoke] expected 11 retires total, got %0d",
                       core_retired_events);
                errors = errors + 1;
            end
            if (core_halt_events != 0) begin
                $error("[tb_iop_core_bios_smoke] expected 0 SYSCALL halts, got %0d",
                       core_halt_events);
                errors = errors + 1;
            end
            if (core_exception_events != 0) begin
                $error("[tb_iop_core_bios_smoke] expected 0 exception entries, got %0d",
                       core_exception_events);
                errors = errors + 1;
            end
            if (dmac_done_events != 0) begin
                $error("[tb_iop_core_bios_smoke] DMA should not have completed, got %0d",
                       dmac_done_events);
                errors = errors + 1;
            end
            if (bios_reads < 10) begin
                $error("[tb_iop_core_bios_smoke] expected >=10 BIOS reads, got %0d",
                       bios_reads);
                errors = errors + 1;
            end
        end

        if (errors == 0 &&
            core_trap && core_trap_events == 1 &&
            (using_real_bios ||
             (core_trap_pc        == EXPECTED_TRAP_PC &&
              core_trap_instr     == EXPECTED_TRAP_INSTR &&
              core_retired_events == 11)))
            $display("[tb_iop_core_bios_smoke] PASS");
        else
            $display("[tb_iop_core_bios_smoke] FAIL");

        $finish;
    end

    initial begin
        #5000000;
        // Give the iteration loop something useful even when the BIOS
        // never trips a trap (e.g., a pure NOP image).
        $display("[tb_iop_core_bios_smoke] TIMEOUT pc=0x%08h retired=%0d trap=%0d halt=%0d except=%0d done=%0d bios_reads=%0d",
                 core_pc, core_retired_events, core_trap_events,
                 core_halt_events, core_exception_events,
                 dmac_done_events, bios_reads);
        $error("[tb_iop_core_bios_smoke] timeout");
        $finish;
    end

endmodule : tb_iop_core_bios_smoke
