// retroDE_ps2 — tb_ps2_hps_bridge (Ch173/Ch174/Ch176)
// ============================================================================
// Focused unit-test TB for ps2_hps_bridge. Drives the AXI4 slave port
// directly from the testbench, runs read transactions at each register
// address in the Ch173/Ch174 map, and verifies the response data.
//
// The bridge is self-contained — the AXI master in this TB is a tiny
// SV task `axi_read32(addr)` that follows the same single-beat, 128-bit
// data, byte-lane-via-{addr[3:2]} contract used by qsys_top.hps2fpga.
// No qsys or full-system involvement; the test is "for this AXI input,
// does the bridge produce the right read response."
//
// What we prove:
//   1. Identity reads:
//        - CORE_ID         @ 0x000 = 0x50533200
//        - ABI_VERSION     @ 0x004 = 0x00000100
//        - CORE_CAPS       @ 0x00C = 0x00102800 (Ch246: osd_cols=40, osd_rows=16)
//   2. CORE_STATUS bits track the synchronized status inputs.
//   3. CORE_CTRL/CORE_PULSE @ 0x010/0x014 read 0 (shared-ABI stubs).
//   4. Ch174: FRAME_COUNT increments on each *edge* (rising or falling)
//      of `frame_toggle`. Sticky `frame_seen` rising does NOT advance
//      FRAME_COUNT — it only drives CORE_STATUS bit 3.
//   5. Ch174: DMA_DONE_COUNT increments on each edge of `dma_done_toggle`,
//      same pattern.
//   6. RASTER_OVERFLOW_COUNT counts every cycle the input stays high.
//   7. Reads outside the mapped window return 0.
//   8. Writes to read-only addresses are ACK'd with OKAY and ignored
//      (Ch176: only CORE_CTRL @ 0x010 and CORE_PULSE @ 0x014 actually
//      take effect; all other addresses are still read-only).
//   9. Ch176: CORE_CTRL @ 0x010 latches the three ABI-shaped bits
//      (RESET / ROM_LOADED / PAUSE), readback returns the latched
//      value, and bit 0 drives the `core_reset_req` output port.
//  10. Ch176: CORE_PULSE @ 0x014 self-clears (always reads 0). A
//      write with HDMI_CLR (bit 3) high zeros the diagnostic
//      counters (FRAME_COUNT / DMA_DONE_COUNT / RASTER_OVERFLOW_COUNT)
//      synchronously in the bridge clock domain. Writes that DON'T
//      set HDMI_CLR leave the counters alone.
//  11. Ch222: INPUT_P1 / INPUT_P2 / INPUT_P1_RAW @ 0x040/0x044/0x048
//      are HPS-visible write/read latches. Reset clears all three.
//      Each register is independently writable (writes to one don't
//      touch the others). Existing CTRL/STATUS/counter regs and
//      `core_reset_req` are unaffected by these writes — proves the
//      Ch222 decode is additive, not destructive.
//  12. Ch223: OSD_CTRL / OSD_CFG0 / OSD_CFG1 @ 0x100/0x110/0x114 are
//      HPS-visible write/read latches (the splash backend's writes
//      now hit real registers instead of being silently dropped).
//      Reset clears all three. Reads at the unmapped slots inside
//      the OSD window (0x104 / 0x108 / 0x10C / 0x118 / 0x11C)
//      return 0; writes there don't bleed into the mapped latches.
//      Other regs (Ch222 inputs + CTRL/STATUS/PULSE/counters) are
//      unchanged across the OSD writes.
//  13. Ch224: OSD_STATUS @ 0x104 always reads 0 (no FSM source);
//      OSD_TRIGGER @ 0x108 is a W1C-shape sink — reads 0, writes
//      accepted-and-ignored. W1C patterns (0xFFFFFFFF, single-bit,
//      `OSD_TRIG_ACTION | row<<8`) all leave the register at 0 and
//      don't disturb OSD_CTRL/CFG0/CFG1. 0x10C also stays read-zero
//      (OSD_INPUT reserved). 0x120 is just past the OSD window and
//      reads 0 (the audit-requested out-of-window sentinel).
//  14. Ch225: VIDEO_STATUS @ 0x060 and HDMI_DIAG @ 0x064 are
//      read-only diagnostic views into the synchronized status
//      signals. Reset values trace the live inputs; flipping
//      frame_toggle raises VIDEO_STATUS[1] (scanout_alive) via the
//      frame_count!=0 derivation; pulsing raster_overflow raises +
//      drops VIDEO_STATUS[2] live. Writes are accepted-and-ignored.
//      Ch222–Ch224 latches are unchanged across the Ch225 writes.
//  15. Ch226: DS2 stub @ 0x0F0 / 0x0F4. DS2_STATUS = 0x00000004
//      (sibling-ABI [0]=connected=0, [1]=error=0, plus PS2-local
//      [2]=input_latches_valid=1). DS2_BUTTONS mirrors INPUT_P1.
//      Both are read-only — writes ack and don't mutate. Existing
//      Ch222 input latches and Ch223 OSD latches are unaffected
//      by Ch226 reads or write-attempts. §7 also picks up the
//      0x0E0 / 0x0F8 audit sentinels (just-before / just-after
//      DS2) confirming the widened 256-byte window's decode is
//      tight.
//  16. Ch227: tile RAM storage @ 0x1000..0x1FFF, 1024 × 32-bit.
//      Writes land in the in-bridge memory; reads return the last
//      value written at the same byte offset. Boundary sentinels
//      at 0x0FFC (just below window) and 0x2000 (just above)
//      read 0. Other regs (CTRL/STATUS/inputs/OSD/DS2) are
//      unchanged across the tile writes. Reset semantics: tile
//      RAM is *retained* across warm reset (sim's `initial` block
//      pre-zeros it; the TB writes, resets, and verifies the
//      written values survive).
// ============================================================================

`timescale 1ns/1ps

module tb_ps2_hps_bridge;

    logic clk;
    logic reset_n;
    initial clk = 1'b0;
    always #5 clk = ~clk;     // 100 MHz TB clock

    // ---- Status inputs ----
    logic core_halt;
    logic dma_done_seen;
    logic frame_seen;
    logic hdmi_init_done;
    logic hdmi_i2c_error;
    logic raster_overflow;
    // ---- Ch174 event-toggle counter inputs ----
    logic frame_toggle;
    logic dma_done_toggle;
    // ---- Ch176 outputs ----
    logic core_reset_req;
    // ---- Ch229 tile-write broadcast outputs (unused in this TB; the
    //      Ch229 receiver-side coverage lives in tb_tile_ram_cdc) ----
    logic        tile_wr_toggle;
    logic [9:0]  tile_wr_index;
    logic [31:0] tile_wr_data;
    // ---- Ch235 INPUT_P1/P2 broadcast outputs (unused here; the
    //      Ch235 integration TB lives in sim/tb/integration) ----
    logic [31:0] input_p1_o;
    logic [31:0] input_p2_o;
    logic [31:0] input_p1_raw_o;

    // ---- Ch245 platform OSD register surface (consumed by the
    //      top-level platform OSD migration; this focused bridge TB
    //      only verifies register read/write semantics).
    logic [31:0] osd_ctrl_o;
    logic [31:0] osd_cfg0_o;
    logic [31:0] osd_cfg1_o;
    logic        osd_active_i               = 1'b0;
    logic [4:0]  osd_cursor_row_i           = 5'd0;
    logic        osd_set_trigger_i          = 1'b0;
    logic        osd_back_trigger_i         = 1'b0;
    logic        osd_scroll_down_trigger_i  = 1'b0;
    logic        osd_scroll_up_trigger_i    = 1'b0;
    logic        osd_open_trigger_i         = 1'b0;
    logic [4:0]  osd_trigger_row_i          = 5'd0;

    // ---- Ch248 DS2 wired-controller inputs (TB-driven). The bridge
    //      surfaces these on DS2_STATUS/DS2_BUTTONS reads instead of
    //      the Ch226 hardcoded stub.
    logic [31:0] ds2_buttons_i              = 32'd0;
    logic        ds2_connected_i            = 1'b0;
    logic        ds2_error_i                = 1'b0;

    // ---- AXI master signals driven by the TB ----
    logic [3:0]   s_axi_awid;
    logic [37:0]  s_axi_awaddr;
    logic [7:0]   s_axi_awlen;
    logic [2:0]   s_axi_awsize;
    logic [1:0]   s_axi_awburst;
    logic         s_axi_awlock;
    logic [3:0]   s_axi_awcache;
    logic [2:0]   s_axi_awprot;
    logic         s_axi_awvalid;
    logic         s_axi_awready;
    logic [127:0] s_axi_wdata;
    logic [15:0]  s_axi_wstrb;
    logic         s_axi_wlast;
    logic         s_axi_wvalid;
    logic         s_axi_wready;
    logic [3:0]   s_axi_bid;
    logic [1:0]   s_axi_bresp;
    logic         s_axi_bvalid;
    logic         s_axi_bready;
    logic [3:0]   s_axi_arid;
    logic [37:0]  s_axi_araddr;
    logic [7:0]   s_axi_arlen;
    logic [2:0]   s_axi_arsize;
    logic [1:0]   s_axi_arburst;
    logic         s_axi_arlock;
    logic [3:0]   s_axi_arcache;
    logic [2:0]   s_axi_arprot;
    logic         s_axi_arvalid;
    logic         s_axi_arready;
    logic [3:0]   s_axi_rid;
    logic [127:0] s_axi_rdata;
    logic [1:0]   s_axi_rresp;
    logic         s_axi_rlast;
    logic         s_axi_rvalid;
    logic         s_axi_rready;

    // Ch318 — LPDDR test control/status nets (matched by the .* below).
    logic        lpddr_arm_o, lpddr_canary_o;
    logic        lpddr_ctrl_commit_o;   // Ch352 — control-commit toggle for the EMIF-domain snapshot
    logic [31:0] lpddr_fb_base_o;
    logic [31:0] lpddr_bytes_i, lpddr_bursts_i, lpddr_bresp_err_i, lpddr_fifo_ovf_i;
    logic        lpddr_idle_i;
    // Ch319 — read-probe nets.
    logic [31:0] lpddr_rd_addr_o; logic lpddr_rd_pulse_o;
    logic [31:0] lpddr_rd_data_i; logic lpddr_rd_done_i;
    // Ch320 — scanout source-select + status nets.
    logic        lpddr_video_src_o;
    logic        lpddr_scanout_lb_o;   // Ch321 — line-buffer vs frame-cache select
    logic        lpddr_scan_valid_i, lpddr_scan_err_i;
    // Ch322 — write-probe + texture-cache fill nets (matched by the .* below).
    logic [31:0] lpddr_wr_addr_o, lpddr_wr_data_o; logic lpddr_wr_pulse_o;
    logic        lpddr_wr_busy_i, lpddr_wr_done_i; logic [31:0] lpddr_wr_bresp_err_i;
    logic        tex_fill_start_o, tex_fill_done_i;
    logic [31:0] tex_fill_beats_i, tex_fill_bytes_i, tex_rd_errs_i, tex_fill_crc_i;
    logic [31:0] tex_cache_hits_i, tex_bram_hits_i;
    // Ch323 — tile COLOR+Z spill/reload counters (read-only regs at 0x080..0x09C).
    logic [31:0] spill_color_beats_i, spill_z_beats_i, reload_color_beats_i, reload_z_beats_i;
    logic [31:0] reload_rd_errs_i, spill_color_errs_i, spill_z_errs_i;
    logic        spill_color_ovf_i, spill_z_ovf_i;
    logic [1:0]  diag_ctrl_o;  // Ch323 diag DIAG_CTRL output
    logic [31:0] color_spill_awaddr_i=0, color_spill_wdata0_i=0;
    logic [31:0] dbg_c_first_awaddr_i=0, dbg_c_first_wdata0_i=0, dbg_c_first_wstrb_i=0, dbg_c_last_wstrb_i=0, dbg_c_beat_count_i=0;
    logic [31:0] dbg_c_emit_count_i=0, dbg_c_push_count_i=0, dbg_c_pop_count_i=0; // Ch323 pipeline-split (.* tie-off)
    logic [31:0] dbg_z_beat_count_i=0, dbg_z_emit_count_i=0, dbg_z_push_count_i=0, dbg_z_pop_count_i=0; // Ch324 (.* tie-off)
    logic [31:0] dbg_m_first_awaddr_i=0, dbg_m_first_wdata0_i=0, dbg_m_first_wstrb_i=0;
    // Ch323 diag — upstream tile-spill/reload event counters (0x0A0..0x0BC).
    logic [31:0] ev_tp_flush_i, ev_tp_zflush_i, ev_tp_reload_i, ev_tp_render_i;
    logic [31:0] ev_flush_emit_i, ev_zflush_emit_i, ev_reload_start_i, ev_reload_ready_i;
    // Ch330 Brick 4 — feeder control ports (.* match). Outputs observed by the feeder test;
    // status inputs driven by the test.
    logic        feeder_stg_we_tgl_o, feeder_go_tgl_o;
    logic [11:0] feeder_stg_waddr_o;
    logic [63:0] feeder_stg_wdata_o;
    logic        feeder_ready_i   = 1'b0;
    logic [15:0] feeder_records_i = 16'd0;
    logic [31:0] feeder_waits_i   = 32'd0;
    logic        frame_drained_i  = 1'b0;   // Ch353 — new bridge diagnostic input; tied off (.* matches by name)
    logic        clear_done_i     = 1'b0;   // Ch357 — persistent-Z preclear ack (driven by the Ch357 status test)
    logic [31:0] frag_drops_i     = 32'd0;  // Ch357 — persistent-Z drop count (driven by the Ch357 status test)
    // Ch367 -- runtime CLUT staging ports (the data-bank CDC itself is
    // covered in tb_clut_stage_cdc; this TB pins the bridge ABI/decode).
    logic [7:0]  clut_stage_waddr_o;
    logic [31:0] clut_stage_wdata_o;
    logic        clut_stage_we_o, clut_commit_tgl_o;
    logic [31:0] clut_expected_crc_o;
    logic        clut_busy_i = 1'b0, clut_done_tgl_i = 1'b0;
    logic [31:0] clut_crc_i = 32'd0;
    integer      clut_stage_writes;
    logic [7:0]  clut_stage_last_addr;
    logic [31:0] clut_stage_last_data;

    always_ff @(posedge clk or negedge reset_n) begin
        if (!reset_n) begin
            clut_stage_writes    <= 0;
            clut_stage_last_addr <= 8'd0;
            clut_stage_last_data <= 32'd0;
        end else if (clut_stage_we_o) begin
            clut_stage_writes    <= clut_stage_writes + 1;
            clut_stage_last_addr <= clut_stage_waddr_o;
            clut_stage_last_data <= clut_stage_wdata_o;
        end
    end

    // Ch355 — model the feeder staging RAM exactly as the on-board feeder does: the bridge emits a COMMIT strobe
    // (feeder_stg_we_tgl_o toggles) with waddr/wdata for each staged word. Capture it here to verify that a
    // register-write stream lands every word at the correct slot (the board bug put word0 at slot 1).
    logic [63:0] model_stg [0:63]; logic mstg_tgl_q;
    always_ff @(posedge clk or negedge reset_n) begin
        if (!reset_n) mstg_tgl_q <= 1'b0;
        else begin
            mstg_tgl_q <= feeder_stg_we_tgl_o;
            if (feeder_stg_we_tgl_o !== mstg_tgl_q) model_stg[feeder_stg_waddr_o] <= feeder_stg_wdata_o;
        end
    end

    ps2_hps_bridge u_dut (
        .clk           (clk),
        .reset_n       (reset_n),
        .h2f_reset     (1'b0),
        .core_halt     (core_halt),
        .dma_done_seen (dma_done_seen),
        .frame_seen    (frame_seen),
        .hdmi_init_done(hdmi_init_done),
        .hdmi_i2c_error(hdmi_i2c_error),
        .raster_overflow(raster_overflow),
        .frame_toggle   (frame_toggle),
        .dma_done_toggle(dma_done_toggle),
        .core_reset_req (core_reset_req),
        .*
    );

    int errors;

    // ----------------------------------------------------------------
    // AXI master tasks. Single-beat, 32-bit-lane reads/writes.
    // Address bits [3:2] pick which 32-bit slot inside the 128-bit
    // response carries the register value.
    // ----------------------------------------------------------------
    task automatic axi_read32(input logic [37:0] addr, output logic [31:0] data);
        // Issue AR
        @(posedge clk);
        s_axi_arid    <= 4'd0;
        s_axi_araddr  <= addr;
        s_axi_arlen   <= 8'd0;
        s_axi_arsize  <= 3'd2;
        s_axi_arburst <= 2'b01;
        s_axi_arlock  <= 1'b0;
        s_axi_arcache <= 4'd0;
        s_axi_arprot  <= 3'd0;
        s_axi_arvalid <= 1'b1;
        s_axi_rready  <= 1'b1;
        wait (s_axi_arready);
        @(posedge clk);
        s_axi_arvalid <= 1'b0;

        // Wait for R
        wait (s_axi_rvalid);
        // Lane select: addr[3:2] picks which 32-bit slot.
        case (addr[3:2])
            2'b00:   data = s_axi_rdata[31:0];
            2'b01:   data = s_axi_rdata[63:32];
            2'b10:   data = s_axi_rdata[95:64];
            default: data = s_axi_rdata[127:96];
        endcase
        @(posedge clk);
        s_axi_rready <= 1'b0;
    endtask

    task automatic axi_write32(input logic [37:0] addr, input logic [31:0] data);
        // Issue AW + W together, then wait B.
        @(posedge clk);
        s_axi_awid    <= 4'd0;
        s_axi_awaddr  <= addr;
        s_axi_awlen   <= 8'd0;
        s_axi_awsize  <= 3'd2;
        s_axi_awburst <= 2'b01;
        s_axi_awlock  <= 1'b0;
        s_axi_awcache <= 4'd0;
        s_axi_awprot  <= 3'd0;
        s_axi_awvalid <= 1'b1;
        case (addr[3:2])
            2'b00:   s_axi_wdata <= {96'd0,         data};
            2'b01:   s_axi_wdata <= {64'd0,         data, 32'd0};
            2'b10:   s_axi_wdata <= {32'd0,         data, 64'd0};
            default: s_axi_wdata <= {data,          96'd0};
        endcase
        s_axi_wstrb  <= 16'h000F << ({addr[3:2], 2'b00});
        s_axi_wlast  <= 1'b1;
        s_axi_wvalid <= 1'b1;
        s_axi_bready <= 1'b1;
        wait (s_axi_awready);
        @(posedge clk);
        s_axi_awvalid <= 1'b0;
        wait (s_axi_wready);
        @(posedge clk);
        s_axi_wvalid <= 1'b0;
        wait (s_axi_bvalid);
        @(posedge clk);
        s_axi_bready <= 1'b0;
    endtask

    // ----------------------------------------------------------------
    // Raise a sticky status-signal level and HOLD it (real hardware
    // sticky rises once and stays — the inner wrapper's
    // frame_seen_q / dma_done_seen_q never fall back to 0).
    // Drives the CORE_STATUS bit, NOT the Ch174 counter (those use
    // separate `*_toggle` inputs).
    // ----------------------------------------------------------------
    task automatic set_dma_done_seen();
        @(posedge clk); dma_done_seen <= 1'b1;
        repeat (5) @(posedge clk);
    endtask
    task automatic set_frame_seen();
        @(posedge clk); frame_seen <= 1'b1;
        repeat (5) @(posedge clk);
    endtask

    // ----------------------------------------------------------------
    // Ch174 — flip an event toggle once. The toggle stays at its new
    // value (no return); the bridge XOR-detects each edge as one event.
    // Holds the new level long enough for the 2-FF synchronizer to
    // settle and the edge detector to fire on the next clk.
    // ----------------------------------------------------------------
    task automatic flip_frame_toggle();
        @(posedge clk); frame_toggle <= ~frame_toggle;
        repeat (8) @(posedge clk);
    endtask
    task automatic flip_dma_done_toggle();
        @(posedge clk); dma_done_toggle <= ~dma_done_toggle;
        repeat (8) @(posedge clk);
    endtask

    // ----------------------------------------------------------------
    // Assertion helper.
    // ----------------------------------------------------------------
    task automatic check_eq(input string label, input logic [31:0] got, input logic [31:0] expected);
        if (got !== expected) begin
            $error("[%s] got 0x%08x expected 0x%08x", label, got, expected);
            errors = errors + 1;
        end
    endtask

    logic [31:0] rd;

    initial begin
        errors          = 0;
        // Default AXI bus state.
        s_axi_arvalid   = 1'b0;
        s_axi_awvalid   = 1'b0;
        s_axi_wvalid    = 1'b0;
        s_axi_rready    = 1'b0;
        s_axi_bready    = 1'b0;
        // Status defaults.
        core_halt       = 1'b0;
        dma_done_seen   = 1'b0;
        frame_seen      = 1'b0;
        hdmi_init_done  = 1'b0;
        hdmi_i2c_error  = 1'b0;
        raster_overflow = 1'b0;
        frame_toggle    = 1'b0;
        dma_done_toggle = 1'b0;
        // Ch318 LPDDR status inputs (fixed test values).
        lpddr_bytes_i     = 32'h0000_2000;   // 8192
        lpddr_bursts_i    = 32'd256;
        lpddr_bresp_err_i = 32'd0;
        lpddr_fifo_ovf_i  = 32'd0;
        lpddr_idle_i      = 1'b1;
        lpddr_rd_data_i   = 32'd0;   // Ch319 read-probe return path (TB models the probe)
        lpddr_rd_done_i   = 1'b0;
        lpddr_scan_valid_i = 1'b0;   // Ch320 scanout status (TB models the scanout reader)
        lpddr_scan_err_i   = 1'b0;
        lpddr_wr_busy_i    = 1'b0;   // Ch322 write-probe + texture-cache fill status
        lpddr_wr_done_i    = 1'b0;
        lpddr_wr_bresp_err_i = 32'd0;
        tex_fill_done_i    = 1'b0;
        tex_fill_beats_i   = 32'd0;
        tex_fill_bytes_i   = 32'd0;
        tex_rd_errs_i      = 32'd0;
        tex_cache_hits_i   = 32'd0;
        tex_bram_hits_i    = 32'd0;
        spill_color_beats_i  = 32'd0; spill_z_beats_i   = 32'd0;
        reload_color_beats_i = 32'd0; reload_z_beats_i  = 32'd0;
        reload_rd_errs_i     = 32'd0; spill_color_errs_i= 32'd0; spill_z_errs_i = 32'd0;
        spill_color_ovf_i    = 1'b0;  spill_z_ovf_i     = 1'b0;
        ev_tp_flush_i = 0; ev_tp_zflush_i = 0; ev_tp_reload_i = 0; ev_tp_render_i = 0;
        ev_flush_emit_i = 0; ev_zflush_emit_i = 0; ev_reload_start_i = 0; ev_reload_ready_i = 0;
        reset_n         = 1'b0;
        repeat (10) @(posedge clk);
        reset_n = 1'b1;
        repeat (4) @(posedge clk);

        // --------------------------------------------------------
        // 1. Identity reads.
        // --------------------------------------------------------
        axi_read32(38'h000, rd); check_eq("CORE_ID",     rd, 32'h50533200);
        axi_read32(38'h004, rd); check_eq("ABI_VERSION", rd, 32'h00000100);
        axi_read32(38'h00C, rd); check_eq("CORE_CAPS",   rd, 32'h00102800);

        // --------------------------------------------------------
        // 2. CORE_STATUS at reset — only `loaded` bit set.
        // --------------------------------------------------------
        axi_read32(38'h008, rd); check_eq("CORE_STATUS@rst", rd, 32'h00000001);

        // Raise core_halt, wait for sync to propagate (~3 clk),
        // read again — bit 1 should be HIGH.
        @(posedge clk); core_halt = 1'b1;
        repeat (5) @(posedge clk);
        axi_read32(38'h008, rd);
        check_eq("CORE_STATUS@halt", rd, 32'h00000003);  // loaded | core_halt

        // Raise hdmi_init_done. Should set bit 4 too.
        @(posedge clk); hdmi_init_done = 1'b1;
        repeat (5) @(posedge clk);
        axi_read32(38'h008, rd);
        check_eq("CORE_STATUS@hdmi_init", rd, 32'h00000013);  // loaded | halt | hdmi_init

        // hdmi_i2c_error bit 5.
        @(posedge clk); hdmi_i2c_error = 1'b1;
        repeat (5) @(posedge clk);
        axi_read32(38'h008, rd);
        check_eq("CORE_STATUS@hdmi_err", rd, 32'h00000033);  // + hdmi_i2c_error

        // --------------------------------------------------------
        // 3. CORE_CTRL / CORE_PULSE — Ch173 stubs, read 0 (shared
        //    retroDE ABI reserves 0x010/0x014; writable in Ch174+).
        // --------------------------------------------------------
        axi_read32(38'h010, rd); check_eq("CORE_CTRL@rst",  rd, 32'd0);
        axi_read32(38'h014, rd); check_eq("CORE_PULSE@rst", rd, 32'd0);

        // --------------------------------------------------------
        // 3b. Ch318 — LPDDR test registers (RW control + R status).
        // --------------------------------------------------------
        axi_read32(38'h018, rd); check_eq("LPDDR_CTRL@rst (arm=0,canary=1)", rd, 32'h00000002);
        axi_read32(38'h01C, rd); check_eq("LPDDR_FB_BASE@rst", rd, 32'h80000000);
        axi_write32(38'h018, 32'h00000001);                       // arm=1, canary=0
        axi_read32(38'h018, rd); check_eq("LPDDR_CTRL after wr", rd, 32'h00000001);
        check_eq("lpddr_arm_o",    {31'd0, lpddr_arm_o},    32'd1);
        check_eq("lpddr_canary_o", {31'd0, lpddr_canary_o}, 32'd0);
        axi_write32(38'h01C, 32'h8000_0000);                      // restore safe base
        axi_read32(38'h01C, rd); check_eq("LPDDR_FB_BASE RW", rd, 32'h80000000);
        check_eq("lpddr_fb_base_o", lpddr_fb_base_o, 32'h80000000);
        axi_read32(38'h02C, rd); check_eq("LPDDR_STATUS (idle,noerr)", rd, 32'h00000001);
        axi_read32(38'h030, rd); check_eq("LPDDR_BYTES",  rd, 32'h0000_2000);
        axi_read32(38'h034, rd); check_eq("LPDDR_BURSTS", rd, 32'd256);

        // 3c. Ch319 — LPDDR4B read-probe (0x03C). WRITE sets the byte address +
        //     triggers a read; STATUS[3]=rd_pending stays high until the probe
        //     (modeled here in the emif domain) toggles done; then 0x03C reads
        //     back the returned word. Exercises the full return-path CDC.
        axi_write32(38'h03C, 32'h0000_0040);                      // request read @ byte 0x40
        check_eq("lpddr_rd_addr_o", lpddr_rd_addr_o, 32'h0000_0040);
        axi_read32(38'h02C, rd); check_eq("rd_pending after trigger", {31'd0, rd[3]}, 32'd1);
        lpddr_rd_data_i = 32'hCAFE_F00D;                          // probe returns a word ...
        lpddr_rd_done_i = ~lpddr_rd_done_i;                       // ... then toggles done
        repeat (6) @(posedge clk);                                // 2-FF sync + latch settle
        axi_read32(38'h02C, rd); check_eq("rd_pending after done", {31'd0, rd[3]}, 32'd0);
        axi_read32(38'h03C, rd); check_eq("LPDDR_RDATA", rd, 32'hCAFE_F00D);

        // 3d. Ch320 — video-source select (0x018[2]) + scanout status (0x02C[4]/[5]).
        axi_write32(38'h018, 32'h00000004);                       // video_src=1 (arm=0,canary=0)
        check_eq("lpddr_video_src_o", {31'd0, lpddr_video_src_o}, 32'd1);
        axi_read32(38'h018, rd); check_eq("LPDDR_CTRL[2] video_src", {31'd0, rd[2]}, 32'd1);
        axi_write32(38'h018, 32'h0000000C);                       // Ch321: video_src=1 + scanout_lb=1
        check_eq("lpddr_scanout_lb_o", {31'd0, lpddr_scanout_lb_o}, 32'd1);
        axi_read32(38'h018, rd); check_eq("LPDDR_CTRL[3] scanout_lb", {31'd0, rd[3]}, 32'd1);
        axi_write32(38'h018, 32'h00000004);                       // back to video_src=1, frame-cache
        lpddr_scan_valid_i = 1'b1; lpddr_scan_err_i = 1'b0;       // model: cache loaded, no errors
        repeat (5) @(posedge clk);
        axi_read32(38'h02C, rd);
        check_eq("STATUS[4] scan_cache_valid", {31'd0, rd[4]}, 32'd1);
        check_eq("STATUS[5] scan_rd_err",      {31'd0, rd[5]}, 32'd0);
        axi_write32(38'h018, 32'h00000002);                       // restore default (BRAM scanout)

        // --------------------------------------------------------
        // 4. Ch174 — FRAME_COUNT @ 0x020 advances on `frame_toggle`
        //    edges (rising OR falling), NOT on the sticky
        //    `frame_seen` rise. Sticky drives CORE_STATUS bit 3
        //    only; counter uses the event-toggle CDC primitive.
        // --------------------------------------------------------
        axi_read32(38'h020, rd); check_eq("FRAME_COUNT@rst", rd, 32'd0);

        // Rising the sticky `frame_seen` MUST NOT increment FRAME_COUNT.
        // This is the Ch174 CDC-safety regression: if a future
        // refactor accidentally rewires the counter onto the sticky
        // source, the count would jump to 1 here and this check fires.
        set_frame_seen();
        axi_read32(38'h020, rd); check_eq("FRAME_COUNT@stickyrise", rd, 32'd0);

        // But the sticky DOES set CORE_STATUS bit 3 (frame_seen).
        axi_read32(38'h008, rd);
        check_eq("CORE_STATUS@frame_seen", rd, 32'h0000003B);
            // loaded(0) | halt(1) | frame_seen(3) | hdmi_init(4) | hdmi_err(5)

        // Now flip the toggle once — counter advances by 1.
        flip_frame_toggle();
        axi_read32(38'h020, rd); check_eq("FRAME_COUNT@toggle1", rd, 32'd1);

        // Two more flips → +2 (both the rising and falling toggle
        // edge each count as one event; 2 flips = 2 edges = 2 events).
        flip_frame_toggle();
        flip_frame_toggle();
        axi_read32(38'h020, rd); check_eq("FRAME_COUNT@toggle3", rd, 32'd3);

        // --------------------------------------------------------
        // 5. Ch174 — DMA_DONE_COUNT @ 0x024 advances on
        //    `dma_done_toggle` edges, not on sticky `dma_done_seen`.
        // --------------------------------------------------------
        axi_read32(38'h024, rd); check_eq("DMA_DONE_COUNT@rst", rd, 32'd0);

        set_dma_done_seen();
        axi_read32(38'h024, rd); check_eq("DMA_DONE_COUNT@stickyrise", rd, 32'd0);

        // Sticky set CORE_STATUS bit 2 (dma_done_seen).
        axi_read32(38'h008, rd);
        check_eq("CORE_STATUS@dma_done", rd, 32'h0000003F);
            // loaded(0) | halt(1) | dma_done(2) | frame_seen(3) | hdmi_init(4) | hdmi_err(5)

        // Toggle path increments the counter.
        flip_dma_done_toggle();
        flip_dma_done_toggle();
        axi_read32(38'h024, rd); check_eq("DMA_DONE_COUNT@toggle2", rd, 32'd2);

        // --------------------------------------------------------
        // 6. RASTER_OVERFLOW_COUNT @ 0x028 counts every cycle high.
        // --------------------------------------------------------
        axi_read32(38'h028, rd); check_eq("RASTER_OVERFLOW_COUNT@rst", rd, 32'd0);

        @(posedge clk); raster_overflow = 1'b1;
        // 2-FF sync delays the count start by ~2 clks; hold high for
        // a known window and check the count grows by approximately
        // that window minus the sync delay.
        repeat (20) @(posedge clk);
        raster_overflow = 1'b0;
        repeat (5) @(posedge clk);
        axi_read32(38'h028, rd);
        // Allow some slack — sync delay + read-pipeline timing.
        if (rd < 32'd15 || rd > 32'd25) begin
            $error("[RASTER_OVERFLOW_COUNT] got %0d, expected ~18..22", rd);
            errors = errors + 1;
        end

        // --------------------------------------------------------
        // 7. Read outside the mapped window → returns 0.
        //    Sentinels probe both window boundaries:
        //      - 0x200: past every decoded region; truly OOB.
        //      - 0x120: first byte past the OSD window (Ch223).
        //      - 0x0F8: between DS2_BUTTONS and OSD_CTRL — inside
        //               the Ch226-widened 256-byte window but
        //               unmapped (CoCo2-specific DS2_ANALOG slot
        //               that PS2 does not implement).
        //      - 0x0E0: just before DS2_STATUS — inside the widened
        //               window but unmapped, confirms the upper
        //               half of the window decodes tightly.
        //      - 0x074: an undecoded word in the lower 128 B; falls
        //               through case-default. (0x078/0x07C are now the
        //               Ch322 TEX_CACHE_HITS/TEX_BRAM_HITS counters.)
        //    (Ch222 mapped 0x040/0x044/0x048; Ch223 mapped 0x100/
        //    0x110/0x114; Ch226 mapped 0x0F0/0x0F4, so prior probes
        //    avoid those addresses.)
        // --------------------------------------------------------
        // --------------------------------------------------------
        // 8b. Ch323 — tile COLOR+Z spill/reload counters (R) at 0x080..0x09C.
        //     Drive distinct sentinels and read each back through the regmap.
        // --------------------------------------------------------
        spill_color_beats_i  = 32'h0000_0101; spill_z_beats_i = 32'h0000_0202;
        reload_color_beats_i = 32'h0000_0303; reload_z_beats_i = 32'h0000_0404;
        reload_rd_errs_i     = 32'h0000_0505; spill_color_errs_i = 32'h0000_0606;
        spill_z_errs_i       = 32'h0000_0707; spill_color_ovf_i = 1'b1; spill_z_ovf_i = 1'b1;
        repeat (2) @(posedge clk);
        axi_read32(38'h080, rd); check_eq("SPILL_COLOR_WR_BEATS@0x080", rd, 32'h0000_0101);
        axi_read32(38'h084, rd); check_eq("SPILL_Z_WR_BEATS@0x084",     rd, 32'h0000_0202);
        axi_read32(38'h088, rd); check_eq("RELOAD_COLOR_BEATS@0x088",   rd, 32'h0000_0303);
        axi_read32(38'h08C, rd); check_eq("RELOAD_Z_BEATS@0x08C",       rd, 32'h0000_0404);
        axi_read32(38'h090, rd); check_eq("RELOAD_RD_ERRS@0x090",       rd, 32'h0000_0505);
        axi_read32(38'h094, rd); check_eq("SPILL_COLOR_WR_ERRS@0x094",  rd, 32'h0000_0606);
        axi_read32(38'h098, rd); check_eq("SPILL_Z_WR_ERRS@0x098",      rd, 32'h0000_0707);
        axi_read32(38'h09C, rd); check_eq("SPILL_OVF@0x09C",            rd, 32'h0000_0003);

        // 8c. Ch323 diag — upstream event counters at 0x0A0..0x0BC.
        ev_tp_flush_i=32'h0A0; ev_tp_zflush_i=32'h0A4; ev_tp_reload_i=32'h0A8; ev_tp_render_i=32'h0AC;
        ev_flush_emit_i=32'h0B0; ev_zflush_emit_i=32'h0B4; ev_reload_start_i=32'h0B8; ev_reload_ready_i=32'h0BC;
        repeat (2) @(posedge clk);
        axi_read32(38'h0A0, rd); check_eq("EV_TP_FLUSH@0x0A0",    rd, 32'h0A0);
        axi_read32(38'h0A4, rd); check_eq("EV_TP_ZFLUSH@0x0A4",   rd, 32'h0A4);
        axi_read32(38'h0A8, rd); check_eq("EV_TP_RELOAD@0x0A8",   rd, 32'h0A8);
        axi_read32(38'h0AC, rd); check_eq("EV_TP_RENDER@0x0AC",   rd, 32'h0AC);
        axi_read32(38'h0B0, rd); check_eq("EV_FLUSH_EMIT@0x0B0",  rd, 32'h0B0);
        axi_read32(38'h0B4, rd); check_eq("EV_ZFLUSH_EMIT@0x0B4", rd, 32'h0B4);
        axi_read32(38'h0B8, rd); check_eq("EV_RELOAD_START@0x0B8",rd, 32'h0B8);
        axi_read32(38'h0BC, rd); check_eq("EV_RELOAD_READY@0x0BC",rd, 32'h0BC);

        axi_read32(38'h200, rd); check_eq("oob_read@0x200", rd, 32'd0);
        axi_read32(38'h120, rd); check_eq("oob_read@0x120", rd, 32'd0);
        axi_read32(38'h0F8, rd); check_eq("oob_read@0x0F8", rd, 32'd0);
        axi_read32(38'h0E0, rd); check_eq("oob_read@0x0E0", rd, 32'd0);
        axi_read32(38'h074, rd); check_eq("oob_read@0x074", rd, 32'd0);

        // --------------------------------------------------------
        // 9. Ch176 — CORE_CTRL @ 0x010 is writable + readable.
        //    Bit 0 (RESET) drives core_reset_req; bits 1/2 are
        //    pure latches; bits[31:3] are ignored on write.
        // --------------------------------------------------------
        // Round-trip latch check: write 0x06 (PAUSE | ROM_LOADED,
        // RESET clear). Read back exactly 0x06; core_reset_req=0.
        axi_write32(38'h010, 32'h00000006);
        axi_read32(38'h010, rd); check_eq("CORE_CTRL@latch_06", rd, 32'h00000006);
        repeat (3) @(posedge clk);
        check_eq("core_reset_req@latch_06", core_reset_req, 1'b0);

        // Assert RESET: write 0x05 (RESET | ROM_LOADED). Read back
        // 0x05; core_reset_req=1 within ~1 clk of the write accept.
        axi_write32(38'h010, 32'h00000005);
        axi_read32(38'h010, rd); check_eq("CORE_CTRL@reset_held", rd, 32'h00000005);
        check_eq("core_reset_req@assert", core_reset_req, 1'b1);

        // Ignore-high-bits check: write 0xFFFFFFFF. Only bits[2:0]
        // survive; readback = 0x07.
        axi_write32(38'h010, 32'hFFFFFFFF);
        axi_read32(38'h010, rd); check_eq("CORE_CTRL@hibits_dropped", rd, 32'h00000007);

        // Release reset: write 0x00. core_reset_req goes low.
        axi_write32(38'h010, 32'h00000000);
        axi_read32(38'h010, rd); check_eq("CORE_CTRL@released", rd, 32'h00000000);
        check_eq("core_reset_req@release", core_reset_req, 1'b0);

        // --------------------------------------------------------
        // 10. Ch176 — CORE_PULSE @ 0x014. Self-clearing; always
        //     reads 0. HDMI_CLR (bit 3) zeros the diagnostic
        //     counters. Other bits are ACK'd-and-ignored.
        // --------------------------------------------------------
        // Re-arm the counters so we have non-zero values to clear.
        // (Section 6 left RASTER_OVERFLOW_COUNT in the 15..25
        // range; sections 4-5 left FRAME_COUNT=3, DMA_DONE_COUNT=2.)
        // First confirm the pre-clear state.
        axi_read32(38'h020, rd); check_eq("FRAME_COUNT@pre_clr",      rd, 32'd3);
        axi_read32(38'h024, rd); check_eq("DMA_DONE_COUNT@pre_clr",   rd, 32'd2);
        // RASTER_OVERFLOW_COUNT already validated as nonzero in §6.

        // Write a no-op pulse (bits[2:0] set but not HDMI_CLR).
        // Counters should be untouched.
        axi_write32(38'h014, 32'h00000007);
        axi_read32(38'h014, rd); check_eq("CORE_PULSE@self_clear_noop", rd, 32'd0);
        axi_read32(38'h020, rd); check_eq("FRAME_COUNT@noop_pulse",     rd, 32'd3);
        axi_read32(38'h024, rd); check_eq("DMA_DONE_COUNT@noop_pulse",  rd, 32'd2);

        // Now fire HDMI_CLR (bit 3 = 0x08). All three counters
        // synchronously zero in the bridge clock domain.
        axi_write32(38'h014, 32'h00000008);
        axi_read32(38'h014, rd); check_eq("CORE_PULSE@self_clear_hdmi", rd, 32'd0);
        axi_read32(38'h020, rd); check_eq("FRAME_COUNT@hdmi_clr",      rd, 32'd0);
        axi_read32(38'h024, rd); check_eq("DMA_DONE_COUNT@hdmi_clr",   rd, 32'd0);
        axi_read32(38'h028, rd); check_eq("RASTER_OVERFLOW_COUNT@hdmi_clr", rd, 32'd0);

        // After clear, the toggle path still advances the counter
        // — proves HDMI_CLR didn't break the increment logic.
        flip_frame_toggle();
        axi_read32(38'h020, rd); check_eq("FRAME_COUNT@post_clr_resume", rd, 32'd1);

        // Final HDMI_CLR to leave counters at 0 for section 8's
        // FRAME_COUNT-write-ignored check.
        axi_write32(38'h014, 32'h00000008);

        // --------------------------------------------------------
        // 8. Writes to read-only addresses are ACK'd with OKAY and
        //    don't modify state. Ch176: this now means everything
        //    EXCEPT CORE_CTRL @ 0x010 and CORE_PULSE @ 0x014.
        // --------------------------------------------------------
        axi_write32(38'h000, 32'hDEAD_BEEF);
        axi_read32(38'h000, rd); check_eq("CORE_ID@write-ignored",     rd, 32'h50533200);

        axi_write32(38'h020, 32'hDEAD_BEEF);
        axi_read32(38'h020, rd); check_eq("FRAME_COUNT@write-ignored", rd, 32'd0);
            // FRAME_COUNT was zeroed by the trailing HDMI_CLR in §10.

        // --------------------------------------------------------
        // 11. Ch222 — INPUT_P1 / INPUT_P2 / INPUT_P1_RAW write/read
        //     latches at 0x040 / 0x044 / 0x048. HPS-visible state
        //     only; reset clears; independent latches; CTRL/STATUS/
        //     counter regs unchanged.
        //
        // Entry conditions inherited from prior sections:
        //   - reset_n = 1 since the very first deassertion.
        //   - core_halt, dma_done_seen, frame_seen, hdmi_init_done,
        //     hdmi_i2c_error are all asserted HIGH and stable.
        //   - CORE_CTRL = 0 (§9 last write released it).
        //   - FRAME_COUNT / DMA_DONE_COUNT / RASTER_OVERFLOW_COUNT all
        //     0 (§10 trailing HDMI_CLR + §8 write-ignored check).
        //   - raster_overflow input is LOW (deasserted in §6).
        // --------------------------------------------------------
        // Reset state — all three latches read back as 0.
        axi_read32(38'h040, rd); check_eq("INPUT_P1@rst",         rd, 32'd0);
        axi_read32(38'h044, rd); check_eq("INPUT_P2@rst",         rd, 32'd0);
        axi_read32(38'h048, rd); check_eq("INPUT_P1_RAW@rst",     rd, 32'd0);

        // Round-trip P1; siblings remain 0.
        axi_write32(38'h040, 32'hDEADBEEF);
        axi_read32(38'h040, rd); check_eq("INPUT_P1@rt",          rd, 32'hDEADBEEF);
        axi_read32(38'h044, rd); check_eq("INPUT_P2@p1_indep",    rd, 32'd0);
        axi_read32(38'h048, rd); check_eq("INPUT_P1_RAW@p1_indep",rd, 32'd0);

        // Round-trip P2; P1 sticks, P1_RAW still 0.
        axi_write32(38'h044, 32'hCAFEBABE);
        axi_read32(38'h044, rd); check_eq("INPUT_P2@rt",          rd, 32'hCAFEBABE);
        axi_read32(38'h040, rd); check_eq("INPUT_P1@p2_indep",    rd, 32'hDEADBEEF);
        axi_read32(38'h048, rd); check_eq("INPUT_P1_RAW@p2_indep",rd, 32'd0);

        // Round-trip P1_RAW; P1 + P2 stick.
        axi_write32(38'h048, 32'h12345678);
        axi_read32(38'h048, rd); check_eq("INPUT_P1_RAW@rt",      rd, 32'h12345678);
        axi_read32(38'h040, rd); check_eq("INPUT_P1@raw_indep",   rd, 32'hDEADBEEF);
        axi_read32(38'h044, rd); check_eq("INPUT_P2@raw_indep",   rd, 32'hCAFEBABE);

        // Decode-tightness: 0x070 is an undecoded slot inside the 128-byte
        // side-effect window (0x04C..0x06C are now Ch322 LPDDR write-probe /
        // texture-fill regs). Writes must not bleed into the input latches,
        // and reads must return 0.
        axi_write32(38'h070, 32'hFEEDF00D);
        axi_read32(38'h070, rd); check_eq("oob_in_window@0x070",   rd, 32'd0);
        axi_read32(38'h048, rd); check_eq("INPUT_P1_RAW@070_nobleed", rd, 32'h12345678);
        axi_read32(38'h040, rd); check_eq("INPUT_P1@070_nobleed",  rd, 32'hDEADBEEF);
        axi_read32(38'h044, rd); check_eq("INPUT_P2@070_nobleed",  rd, 32'hCAFEBABE);

        // Existing regs unchanged.
        axi_read32(38'h010, rd); check_eq("CORE_CTRL@post_ch222",  rd, 32'd0);
        check_eq("core_reset_req@post_ch222", core_reset_req,      1'b0);
        // CORE_STATUS still driven by held-high inputs: loaded(0) |
        // halt(1) | dma_done(2) | frame_seen(3) | hdmi_init(4) |
        // hdmi_err(5) = 0x3F.
        axi_read32(38'h008, rd); check_eq("CORE_STATUS@post_ch222",rd, 32'h0000003F);
        axi_read32(38'h014, rd); check_eq("CORE_PULSE@post_ch222", rd, 32'd0);
        axi_read32(38'h020, rd); check_eq("FRAME_COUNT@post_ch222",rd, 32'd0);
        axi_read32(38'h024, rd); check_eq("DMA_DONE_COUNT@post_ch222", rd, 32'd0);
        axi_read32(38'h028, rd); check_eq("RASTER_OVERFLOW_COUNT@post_ch222", rd, 32'd0);

        // Counters still advance via toggle inputs after Ch222 writes.
        flip_frame_toggle();
        axi_read32(38'h020, rd); check_eq("FRAME_COUNT@post_ch222_inc", rd, 32'd1);

        // Reset clears all three latches (and the rest of the bridge,
        // but only the Ch222 regs are asserted here — the other regs
        // already have separate coverage in §1..§10).
        @(posedge clk); reset_n = 1'b0;
        repeat (4) @(posedge clk);
        reset_n = 1'b1;
        repeat (4) @(posedge clk);
        axi_read32(38'h040, rd); check_eq("INPUT_P1@reset_clr",    rd, 32'd0);
        axi_read32(38'h044, rd); check_eq("INPUT_P2@reset_clr",    rd, 32'd0);
        axi_read32(38'h048, rd); check_eq("INPUT_P1_RAW@reset_clr",rd, 32'd0);

        // --------------------------------------------------------
        // 12. Ch223 — OSD compatibility sink @ 0x100 / 0x110 / 0x114.
        //     Store-and-readback latches; reset clears; unmapped
        //     slots inside the OSD window read 0 and don't accept
        //     register state. retrodesd's splash backend now writes
        //     these without being dropped on the side-effect map.
        //
        // Entry state inherited from the §11 trailing reset:
        //   - reset_n = 1, all latches and counters at 0.
        //   - Status inputs (core_halt etc.) still asserted; the
        //     2-FF synchronizer re-fills within ~2 clks of reset
        //     release, so CORE_STATUS reads back 0x3F.
        //   - frame_toggle and dma_done_toggle at their last value
        //     (from §11 last flip); no further edges produced here.
        // --------------------------------------------------------
        // Reset state — all three OSD latches read 0.
        axi_read32(38'h100, rd); check_eq("OSD_CTRL@rst",     rd, 32'd0);
        axi_read32(38'h110, rd); check_eq("OSD_CFG0@rst",     rd, 32'd0);
        axi_read32(38'h114, rd); check_eq("OSD_CFG1@rst",     rd, 32'd0);

        // Round-trip OSD_CTRL; siblings stay 0.
        axi_write32(38'h100, 32'hA5A5A5A5);
        axi_read32(38'h100, rd); check_eq("OSD_CTRL@rt",      rd, 32'hA5A5A5A5);
        axi_read32(38'h110, rd); check_eq("OSD_CFG0@ctrl_indep", rd, 32'd0);
        axi_read32(38'h114, rd); check_eq("OSD_CFG1@ctrl_indep", rd, 32'd0);

        // Round-trip OSD_CFG0; OSD_CTRL sticks, OSD_CFG1 still 0.
        axi_write32(38'h110, 32'h11223344);
        axi_read32(38'h110, rd); check_eq("OSD_CFG0@rt",      rd, 32'h11223344);
        axi_read32(38'h100, rd); check_eq("OSD_CTRL@cfg0_indep",  rd, 32'hA5A5A5A5);
        axi_read32(38'h114, rd); check_eq("OSD_CFG1@cfg0_indep",  rd, 32'd0);

        // Round-trip OSD_CFG1; OSD_CTRL + OSD_CFG0 stick.
        axi_write32(38'h114, 32'h55667788);
        axi_read32(38'h114, rd); check_eq("OSD_CFG1@rt",      rd, 32'h55667788);
        axi_read32(38'h100, rd); check_eq("OSD_CTRL@cfg1_indep",  rd, 32'hA5A5A5A5);
        axi_read32(38'h110, rd); check_eq("OSD_CFG0@cfg1_indep",  rd, 32'h11223344);

        // Decode-tightness inside the OSD window. 0x104/0x108/0x10C
        // are reserved-for-Ch224; 0x118/0x11C are reserved-zero.
        // Writes to any of those addresses must not bleed into the
        // mapped CTRL/CFG0/CFG1 latches, and reads must return 0.
        axi_read32(38'h104, rd); check_eq("oob_in_osd@0x104_rst", rd, 32'd0);
        axi_read32(38'h108, rd); check_eq("oob_in_osd@0x108_rst", rd, 32'd0);
        axi_read32(38'h10C, rd); check_eq("oob_in_osd@0x10C_rst", rd, 32'd0);
        axi_read32(38'h118, rd); check_eq("oob_in_osd@0x118_rst", rd, 32'd0);
        axi_read32(38'h11C, rd); check_eq("oob_in_osd@0x11C_rst", rd, 32'd0);

        axi_write32(38'h104, 32'hFEEDF00D);
        axi_write32(38'h108, 32'hFEEDF00D);
        axi_write32(38'h10C, 32'hFEEDF00D);
        axi_write32(38'h118, 32'hFEEDF00D);
        axi_write32(38'h11C, 32'hFEEDF00D);
        axi_read32(38'h104, rd); check_eq("oob_in_osd@0x104", rd, 32'd0);
        axi_read32(38'h108, rd); check_eq("oob_in_osd@0x108", rd, 32'd0);
        axi_read32(38'h10C, rd); check_eq("oob_in_osd@0x10C", rd, 32'd0);
        axi_read32(38'h118, rd); check_eq("oob_in_osd@0x118", rd, 32'd0);
        axi_read32(38'h11C, rd); check_eq("oob_in_osd@0x11C", rd, 32'd0);
        // Mapped OSD latches unchanged by the tight-decode writes.
        axi_read32(38'h100, rd); check_eq("OSD_CTRL@oob_nobleed",  rd, 32'hA5A5A5A5);
        axi_read32(38'h110, rd); check_eq("OSD_CFG0@oob_nobleed",  rd, 32'h11223344);
        axi_read32(38'h114, rd); check_eq("OSD_CFG1@oob_nobleed",  rd, 32'h55667788);

        // Other regs unchanged across the Ch223 writes. The toggle
        // synchronizers re-fill after the §11 reset and can emit a
        // single phantom edge in FRAME_COUNT / DMA_DONE_COUNT before
        // steady state — that's a CDC-refill artifact, not a Ch223
        // bug. Issue an HDMI_CLR pulse here to zero the counters
        // after the refill has settled; then prove the subsequent
        // OSD writes don't disturb them.
        axi_write32(38'h014, 32'h00000008);  // CORE_PULSE.HDMI_CLR
        repeat (4) @(posedge clk);

        axi_read32(38'h040, rd); check_eq("INPUT_P1@post_ch223",      rd, 32'd0);
        axi_read32(38'h044, rd); check_eq("INPUT_P2@post_ch223",      rd, 32'd0);
        axi_read32(38'h048, rd); check_eq("INPUT_P1_RAW@post_ch223",  rd, 32'd0);
        axi_read32(38'h010, rd); check_eq("CORE_CTRL@post_ch223",     rd, 32'd0);
        axi_read32(38'h008, rd); check_eq("CORE_STATUS@post_ch223",   rd, 32'h0000003F);
        axi_read32(38'h014, rd); check_eq("CORE_PULSE@post_ch223",    rd, 32'd0);
        axi_read32(38'h020, rd); check_eq("FRAME_COUNT@post_ch223",   rd, 32'd0);
        axi_read32(38'h024, rd); check_eq("DMA_DONE_COUNT@post_ch223",rd, 32'd0);
        axi_read32(38'h028, rd); check_eq("RASTER_OVERFLOW_COUNT@post_ch223", rd, 32'd0);

        // Reset clears all three OSD latches.
        @(posedge clk); reset_n = 1'b0;
        repeat (4) @(posedge clk);
        reset_n = 1'b1;
        repeat (4) @(posedge clk);
        axi_read32(38'h100, rd); check_eq("OSD_CTRL@reset_clr",  rd, 32'd0);
        axi_read32(38'h110, rd); check_eq("OSD_CFG0@reset_clr",  rd, 32'd0);
        axi_read32(38'h114, rd); check_eq("OSD_CFG1@reset_clr",  rd, 32'd0);

        // --------------------------------------------------------
        // 13. Ch224 — OSD_STATUS / OSD_TRIGGER / OSD_INPUT contracts.
        //
        // OSD_STATUS @ 0x104: always-zero source. No FSM driving it
        //   in PS2 today; reads must return 0 regardless of any
        //   writes that landed at that address.
        // OSD_TRIGGER @ 0x108: W1C-shape sink. The contract sibling
        //   cores expose is "FSM sets ACTION/BACK/SCROLL bits,
        //   retrodesd reads, processes, writes-1-to-clear". With no
        //   FSM source, bits never go HIGH, so reads always return 0
        //   and *any* W1C pattern (including 0xFFFFFFFF and the real
        //   OSD_TRIG_ACTION|row<<8 shape retrodesd writes) is a
        //   no-op against an already-zero register.
        // OSD_INPUT @ 0x10C: reserved-zero. Sibling cores use this
        //   as a debug-override; PS2 keeps it dormant.
        //
        // Existing OSD_CTRL/CFG0/CFG1 latches must survive every
        // poke at 0x104/0x108/0x10C.
        // --------------------------------------------------------
        // First arm OSD_CTRL/CFG0/CFG1 to known non-zero values so
        // the "unchanged" checks below have signal to lose.
        axi_write32(38'h100, 32'hA5A5A5A5);
        axi_write32(38'h110, 32'h11223344);
        axi_write32(38'h114, 32'h55667788);

        // OSD_STATUS — always 0, any write is a no-op.
        axi_read32(38'h104, rd); check_eq("OSD_STATUS@rst",          rd, 32'd0);
        axi_write32(38'h104, 32'hFFFFFFFF);
        axi_read32(38'h104, rd); check_eq("OSD_STATUS@write_all",    rd, 32'd0);
        axi_write32(38'h104, 32'h00000001);  // would be the "active" bit if FSM existed
        axi_read32(38'h104, rd); check_eq("OSD_STATUS@write_active", rd, 32'd0);

        // OSD_TRIGGER — W1C sink. All shapes (broad, real W1C
        // mask, action+row encoding) read back 0.
        axi_read32(38'h108, rd); check_eq("OSD_TRIGGER@rst",         rd, 32'd0);
        axi_write32(38'h108, 32'hFFFFFFFF);
        axi_read32(38'h108, rd); check_eq("OSD_TRIGGER@w1c_all",     rd, 32'd0);
        // OSD_TRIG_ACTION = 1<<4 = 0x00000010; ROW=3 → 0x00000300; combined.
        axi_write32(38'h108, 32'h00000310);
        axi_read32(38'h108, rd); check_eq("OSD_TRIGGER@w1c_action_row3", rd, 32'd0);
        // OSD_TRIG_BACK = 1<<5 = 0x00000020.
        axi_write32(38'h108, 32'h00000020);
        axi_read32(38'h108, rd); check_eq("OSD_TRIGGER@w1c_back",    rd, 32'd0);

        // OSD_INPUT — reserved-zero, any write is a no-op.
        axi_read32(38'h10C, rd); check_eq("OSD_INPUT@rst",           rd, 32'd0);
        axi_write32(38'h10C, 32'hDEADBEEF);
        axi_read32(38'h10C, rd); check_eq("OSD_INPUT@write",         rd, 32'd0);

        // OSD_CTRL/CFG0/CFG1 unchanged by every Ch224 poke above.
        axi_read32(38'h100, rd); check_eq("OSD_CTRL@post_ch224",  rd, 32'hA5A5A5A5);
        axi_read32(38'h110, rd); check_eq("OSD_CFG0@post_ch224",  rd, 32'h11223344);
        axi_read32(38'h114, rd); check_eq("OSD_CFG1@post_ch224",  rd, 32'h55667788);

        // Audit-tail: 0x120 is the first byte past the OSD window
        // and reads 0 (the decode guard `addr[37:5] == 33'h08`
        // rejects it). Confirms the Ch223 window boundary is tight.
        axi_read32(38'h120, rd); check_eq("OSD_window_tail@0x120", rd, 32'd0);

        // --------------------------------------------------------
        // 14. Ch225 — VIDEO_STATUS @ 0x060 + HDMI_DIAG @ 0x064.
        //     Read-only diagnostic views; writes accepted-and-ignored.
        //
        // Entry state (inherited from §13):
        //   - core_halt = dma_done_seen = frame_seen = hdmi_init_done
        //     = hdmi_i2c_error = 1 (held HIGH since §2/§4/§5).
        //   - raster_overflow = 0 (deasserted in §6).
        //   - frame_count = dma_done_count = 1 (the §12 trailing
        //     reset re-fired the toggle synchronizer phantom edge
        //     that the §12 HDMI_CLR previously absorbed; §13 didn't
        //     re-clear). scanout_alive is therefore already SET at
        //     entry — useful, since the test then exercises both
        //     transitions (HIGH → HDMI_CLR → LOW → toggle → HIGH).
        //   - OSD_CTRL=0xA5A5A5A5, OSD_CFG0=0x11223344,
        //     OSD_CFG1=0x55667788 (from §13's arming write).
        //
        // Expected entry reads:
        //   VIDEO_STATUS[3:0] = {dma_done(1), raster_of(0),
        //                        scanout_alive(1), frame_seen(1)}
        //                     = 4'b1011 = 0xB
        //   HDMI_DIAG[1:0]    = {hdmi_err(1), hdmi_init(1)} = 0x3
        // --------------------------------------------------------
        axi_read32(38'h060, rd); check_eq("VIDEO_STATUS@entry", rd, 32'h0000000B);
        axi_read32(38'h064, rd); check_eq("HDMI_DIAG@entry",    rd, 32'h00000003);

        // scanout_alive falling edge: HDMI_CLR zeros frame_count.
        axi_write32(38'h014, 32'h00000008);
        repeat (4) @(posedge clk);
        axi_read32(38'h020, rd); check_eq("FRAME_COUNT@ch225_clr", rd, 32'd0);
        axi_read32(38'h060, rd); check_eq("VIDEO_STATUS@scanout_lo", rd, 32'h00000009);
            // [0]=frame_seen | [3]=dma_done, [1]=0 = 0x9

        // scanout_alive rising edge: one toggle flip bumps frame_count
        // to 1, scanout_alive re-rises.
        flip_frame_toggle();
        axi_read32(38'h020, rd); check_eq("FRAME_COUNT@ch225_inc",   rd, 32'd1);
        axi_read32(38'h060, rd); check_eq("VIDEO_STATUS@scanout_hi", rd, 32'h0000000B);

        // raster_overflow live tracking: pulse HIGH, then LOW.
        @(posedge clk); raster_overflow = 1'b1;
        repeat (5) @(posedge clk);
        axi_read32(38'h060, rd); check_eq("VIDEO_STATUS@raster_hi", rd, 32'h0000000F);
            // [0..3] all set = 0xF

        @(posedge clk); raster_overflow = 1'b0;
        repeat (5) @(posedge clk);
        axi_read32(38'h060, rd); check_eq("VIDEO_STATUS@raster_lo", rd, 32'h0000000B);

        // Writes to 0x060 / 0x064 are accepted-and-ignored; the
        // read-only diagnostic values must remain driven by status.
        axi_write32(38'h060, 32'hFFFFFFFF);
        axi_read32(38'h060, rd); check_eq("VIDEO_STATUS@write_all", rd, 32'h0000000B);
        axi_write32(38'h060, 32'h00000000);
        axi_read32(38'h060, rd); check_eq("VIDEO_STATUS@write_zero",rd, 32'h0000000B);

        axi_write32(38'h064, 32'hFFFFFFFF);
        axi_read32(38'h064, rd); check_eq("HDMI_DIAG@write_all",    rd, 32'h00000003);
        axi_write32(38'h064, 32'hDEADBEEF);
        axi_read32(38'h064, rd); check_eq("HDMI_DIAG@write_pat",    rd, 32'h00000003);

        // Ch222–Ch224 latches unchanged across the Ch225 writes.
        axi_read32(38'h040, rd); check_eq("INPUT_P1@post_ch225",    rd, 32'd0);
        axi_read32(38'h044, rd); check_eq("INPUT_P2@post_ch225",    rd, 32'd0);
        axi_read32(38'h048, rd); check_eq("INPUT_P1_RAW@post_ch225",rd, 32'd0);
        axi_read32(38'h100, rd); check_eq("OSD_CTRL@post_ch225",    rd, 32'hA5A5A5A5);
        axi_read32(38'h110, rd); check_eq("OSD_CFG0@post_ch225",    rd, 32'h11223344);
        axi_read32(38'h114, rd); check_eq("OSD_CFG1@post_ch225",    rd, 32'h55667788);

        // --------------------------------------------------------
        // 15. Ch248 — DS2 wired-controller live readback at 0x0F0 / 0x0F4.
        //     Replaces the Ch226 hardcoded stub. The bridge now
        //     composes DS2_STATUS from the (ds2_connected_i,
        //     ds2_error_i) ports driven by the platform
        //     `ds2_controller` in the top, and DS2_BUTTONS is the
        //     ds2_buttons_i input (NOT a mirror of INPUT_P1).
        //
        //     DS2_STATUS bit layout (sibling-ABI + PS2-local):
        //       [0] connected   — from ds2_connected_i
        //       [1] error       — from ds2_error_i
        //       [2] reserved    — 1 (PS2-local legacy bit kept stable)
        //       [31:3] reserved — 0
        //
        //     DS2_BUTTONS [31:0] = ds2_buttons_i (controller's
        //     decoded button bitmap). Writes to 0x0F0/0x0F4 are
        //     still accepted-and-ignored — both are read-only.
        // --------------------------------------------------------
        // Entry state: TB-driven inputs all at reset defaults
        // (ds2_buttons_i=0, ds2_connected_i=0, ds2_error_i=0).
        axi_read32(38'h0F0, rd); check_eq("DS2_STATUS@unplugged",   rd, 32'h00000004);
        axi_read32(38'h0F4, rd); check_eq("DS2_BUTTONS@unplugged",  rd, 32'd0);

        // Plug in the controller (no errors).
        ds2_connected_i = 1'b1;
        @(posedge clk);
        axi_read32(38'h0F0, rd); check_eq("DS2_STATUS@connected",   rd, 32'h00000005);

        // Drive a button bitmap.
        ds2_buttons_i = 32'hF00DBABE;
        @(posedge clk);
        axi_read32(38'h0F4, rd); check_eq("DS2_BUTTONS@live",       rd, 32'hF00DBABE);

        // INPUT_P1 mirror behavior is gone — writing INPUT_P1 must
        // NOT change DS2_BUTTONS now (Ch248 cuts the Ch226 mirror).
        axi_write32(38'h040, 32'h13579BDF);
        axi_read32(38'h0F4, rd); check_eq("DS2_BUTTONS@p1_no_mirror", rd, 32'hF00DBABE);

        // INPUT_P2 / INPUT_P1_RAW writes don't touch DS2_BUTTONS either.
        axi_write32(38'h044, 32'hDEADC0DE);
        axi_write32(38'h048, 32'h0BADF00D);
        axi_read32(38'h0F4, rd); check_eq("DS2_BUTTONS@p2raw_no_mirror", rd, 32'hF00DBABE);

        // Update the button bitmap → readback follows.
        ds2_buttons_i = 32'h12345678;
        @(posedge clk);
        axi_read32(38'h0F4, rd); check_eq("DS2_BUTTONS@update",     rd, 32'h12345678);

        // Comms-error condition: error=1 → DS2_STATUS[1]=1.
        ds2_error_i = 1'b1;
        @(posedge clk);
        axi_read32(38'h0F0, rd); check_eq("DS2_STATUS@error",       rd, 32'h00000007);
        ds2_error_i = 1'b0;
        @(posedge clk);

        // Disconnect → status drops connected bit but ds2_buttons_i
        // remains whatever the controller last reported (the
        // controller-RTL latches it; the bridge passes through).
        ds2_connected_i = 1'b0;
        @(posedge clk);
        axi_read32(38'h0F0, rd); check_eq("DS2_STATUS@disc",        rd, 32'h00000004);

        // Writes to 0x0F0 / 0x0F4 are accepted-and-ignored.
        axi_write32(38'h0F0, 32'hFFFFFFFF);
        axi_read32(38'h0F0, rd); check_eq("DS2_STATUS@write_all",   rd, 32'h00000004);
        axi_write32(38'h0F4, 32'hFFFFFFFF);
        axi_read32(38'h0F4, rd); check_eq("DS2_BUTTONS@write_all",  rd, 32'h12345678);

        // Other regs unchanged across the Ch248 reads (sibling-ABI
        // additivity unchanged from Ch226).
        axi_read32(38'h040, rd); check_eq("INPUT_P1@post_ch248",    rd, 32'h13579BDF);
        axi_read32(38'h044, rd); check_eq("INPUT_P2@post_ch248",    rd, 32'hDEADC0DE);
        axi_read32(38'h048, rd); check_eq("INPUT_P1_RAW@post_ch248",rd, 32'h0BADF00D);
        axi_read32(38'h100, rd); check_eq("OSD_CTRL@post_ch248",    rd, 32'hA5A5A5A5);
        axi_read32(38'h110, rd); check_eq("OSD_CFG0@post_ch248",    rd, 32'h11223344);
        axi_read32(38'h114, rd); check_eq("OSD_CFG1@post_ch248",    rd, 32'h55667788);
        axi_read32(38'h010, rd); check_eq("CORE_CTRL@post_ch248",   rd, 32'd0);

        // Reset clears INPUT_P1 latch; DS2 inputs are TB-side so they
        // stay at whatever the TB last drove. Set them to a known
        // unplugged state across the reset.
        ds2_buttons_i = 32'd0;
        ds2_connected_i = 1'b0;
        ds2_error_i = 1'b0;
        @(posedge clk); reset_n = 1'b0;
        repeat (4) @(posedge clk);
        reset_n = 1'b1;
        repeat (4) @(posedge clk);
        axi_read32(38'h0F0, rd); check_eq("DS2_STATUS@reset",       rd, 32'h00000004);
        axi_read32(38'h0F4, rd); check_eq("DS2_BUTTONS@reset",      rd, 32'd0);

        // --------------------------------------------------------
        // 16. Ch227 — tile RAM storage @ 0x1000..0x1FFF.
        //
        // Entry state (inherited from §15 trailing reset):
        //   - Tile RAM was sim-initialized to 0 by the bridge's
        //     `initial` block; no writes have hit it yet, so every
        //     slot reads 0.
        //   - INPUT_P1 = 0 (cleared by §15 reset), but INPUT_P2
        //     and INPUT_P1_RAW survive at 0xDEADC0DE / 0x0BADF00D
        //     after §15's pre-reset arms… *unless* they were also
        //     cleared by the reset. Ch222 latches DO reset (§11
        //     reset path showed it), so the §15 reset cleared them
        //     too.
        //   - OSD_CTRL/CFG0/CFG1 = 0 (cleared by §15 reset).
        //   - Counters = 0 with potential 1-edge sync-refill from
        //     the §15 reset — irrelevant for tile-RAM testing.
        // --------------------------------------------------------
        // Reset state: every probed tile slot reads 0.
        axi_read32(38'h1000, rd); check_eq("TILE_RAM@base_rst",     rd, 32'd0);
        axi_read32(38'h1800, rd); check_eq("TILE_RAM@mid_rst",      rd, 32'd0);
        axi_read32(38'h1FFC, rd); check_eq("TILE_RAM@end_rst",      rd, 32'd0);

        // Round-trip at base / middle / end.
        axi_write32(38'h1000, 32'hCAFEF00D);
        axi_read32(38'h1000, rd); check_eq("TILE_RAM@base_rt",      rd, 32'hCAFEF00D);

        axi_write32(38'h1800, 32'h12345678);
        axi_read32(38'h1800, rd); check_eq("TILE_RAM@mid_rt",       rd, 32'h12345678);

        axi_write32(38'h1FFC, 32'hDEADBEEF);
        axi_read32(38'h1FFC, rd); check_eq("TILE_RAM@end_rt",       rd, 32'hDEADBEEF);

        // Independent-slot check: each write hit only its target.
        axi_read32(38'h1000, rd); check_eq("TILE_RAM@base_indep",   rd, 32'hCAFEF00D);
        axi_read32(38'h1800, rd); check_eq("TILE_RAM@mid_indep",    rd, 32'h12345678);
        axi_read32(38'h1FFC, rd); check_eq("TILE_RAM@end_indep",    rd, 32'hDEADBEEF);

        // Adjacent-slot tightness: a slot we never wrote stays 0.
        axi_read32(38'h1004, rd); check_eq("TILE_RAM@base+4_zero",  rd, 32'd0);
        axi_read32(38'h17FC, rd); check_eq("TILE_RAM@mid-4_zero",   rd, 32'd0);
        axi_read32(38'h1FF8, rd); check_eq("TILE_RAM@end-4_zero",   rd, 32'd0);

        // Boundary sentinels: 0x0FFC is the last word *below* the
        // tile window (between OSD at 0x11C and tile at 0x1000), and
        // 0x2000 is the first word *above* the window. Both are
        // outside every decoded region.
        axi_read32(38'h0FFC, rd); check_eq("TILE_RAM_bound_lo@0x0FFC", rd, 32'd0);
        axi_read32(38'h2000, rd); check_eq("TILE_RAM_bound_hi@0x2000", rd, 32'd0);

        // Writes to those boundary addresses must not bleed into the
        // tile RAM. Use distinct patterns to make a bleed visible.
        axi_write32(38'h0FFC, 32'hAAAA5555);
        axi_write32(38'h2000, 32'h5555AAAA);
        axi_read32(38'h0FFC, rd); check_eq("TILE_RAM_bound_lo_w@0x0FFC", rd, 32'd0);
        axi_read32(38'h2000, rd); check_eq("TILE_RAM_bound_hi_w@0x2000", rd, 32'd0);
        // Inside-window slots near the boundaries didn't pick up the
        // boundary writes either.
        axi_read32(38'h1000, rd); check_eq("TILE_RAM@base_nobleed",  rd, 32'hCAFEF00D);
        axi_read32(38'h1FFC, rd); check_eq("TILE_RAM@end_nobleed",   rd, 32'hDEADBEEF);

        // Other regs unchanged across the Ch227 writes.
        axi_read32(38'h008, rd); check_eq("CORE_STATUS@post_ch227", rd, 32'h0000003F);
        axi_read32(38'h010, rd); check_eq("CORE_CTRL@post_ch227",   rd, 32'd0);
        axi_read32(38'h040, rd); check_eq("INPUT_P1@post_ch227",    rd, 32'd0);
        axi_read32(38'h100, rd); check_eq("OSD_CTRL@post_ch227",    rd, 32'd0);
        axi_read32(38'h110, rd); check_eq("OSD_CFG0@post_ch227",    rd, 32'd0);
        axi_read32(38'h114, rd); check_eq("OSD_CFG1@post_ch227",    rd, 32'd0);
        axi_read32(38'h0F0, rd); check_eq("DS2_STATUS@post_ch227",  rd, 32'h00000004);

        // Reset retention: tile RAM is *retained* across warm reset
        // (no sibling precedent for clearing, and the bridge owns
        // the storage). Pulse reset and verify the previously
        // written tile slots still hold their values.
        @(posedge clk); reset_n = 1'b0;
        repeat (4) @(posedge clk);
        reset_n = 1'b1;
        repeat (4) @(posedge clk);
        axi_read32(38'h1000, rd); check_eq("TILE_RAM@base_post_rst", rd, 32'hCAFEF00D);
        axi_read32(38'h1800, rd); check_eq("TILE_RAM@mid_post_rst",  rd, 32'h12345678);
        axi_read32(38'h1FFC, rd); check_eq("TILE_RAM@end_post_rst",  rd, 32'hDEADBEEF);

        // --------------------------------------------------------
        // Ch330 Brick 4 — feeder staging-write + retrigger regmap.
        // --------------------------------------------------------
        begin
            logic we_b4, go_b4;
            we_b4 = feeder_stg_we_tgl_o; go_b4 = feeder_go_tgl_o;
            // write staging[5] = 0x12345678_CAFEF00D via ADDR/LO/HI
            axi_write32(38'h0D8, 32'd5);            // FEEDER_STG_ADDR = 5
            axi_write32(38'h0DC, 32'hCAFEF00D);     // FEEDER_STG_LO
            axi_write32(38'h0E4, 32'h12345678);     // FEEDER_STG_HI -> commit + addr++
            repeat (2) @(posedge clk);
            check_eq("FEEDER_we_toggled",  {31'd0, (feeder_stg_we_tgl_o ^ we_b4)}, 32'd1);
            check_eq("FEEDER_waddr",       {20'd0, feeder_stg_waddr_o}, 32'd5);
            check_eq("FEEDER_wdata_lo",    feeder_stg_wdata_o[31:0],  32'hCAFEF00D);
            check_eq("FEEDER_wdata_hi",    feeder_stg_wdata_o[63:32], 32'h12345678);
            axi_read32(38'h0DC, rd); check_eq("FEEDER_addr_autoinc", rd, 32'd6);  // advanced 5->6

            // a second word WITHOUT re-writing ADDR streams into staging[6]
            axi_write32(38'h0DC, 32'h0000AAAA);
            axi_write32(38'h0E4, 32'h0000BBBB);     // commit -> waddr should be 6
            repeat (2) @(posedge clk);
            check_eq("FEEDER_waddr_stream", {20'd0, feeder_stg_waddr_o}, 32'd6);
            axi_read32(38'h0DC, rd); check_eq("FEEDER_addr_autoinc2", rd, 32'd7);

            // GO pulse toggles
            axi_write32(38'h0E8, 32'd1);            // FEEDER_GO bit0
            repeat (2) @(posedge clk);
            check_eq("FEEDER_go_toggled", {31'd0, (feeder_go_tgl_o ^ go_b4)}, 32'd1);
            // GO with bit0=0 must NOT toggle
            go_b4 = feeder_go_tgl_o;
            axi_write32(38'h0E8, 32'd0);
            repeat (2) @(posedge clk);
            check_eq("FEEDER_go_no_toggle", {31'd0, (feeder_go_tgl_o ^ go_b4)}, 32'd0);

            // status readbacks
            feeder_ready_i = 1'b1; feeder_records_i = 16'd4; feeder_waits_i = 32'd7;
            repeat (3) @(posedge clk);               // let ready sync through
            axi_read32(38'h0D8, rd); check_eq("FEEDER_status_ready", rd[0], 1'b1);
            axi_read32(38'h0E4, rd); check_eq("FEEDER_records", rd, 32'd4);
            axi_read32(38'h0E8, rd); check_eq("FEEDER_waits",   rd, 32'd7);
        end

        // --------------------------------------------------------
        // Ch355 — BRIDGE FEEDER-STAGING address path (closes the gap that hid the board bug: the multi-texture TB
        // staged via $readmemh and never exercised the real 0x0D8 set-addr / 0x0DC-lo / 0x0E4-hi-commit protocol).
        // Stage two complete lists EXCLUSIVELY through bridge register writes; verify every word (esp word 0), the
        // final address, and the records readback; plus a negative test that reproduces the exact board bug.
        // --------------------------------------------------------
        begin
            logic [63:0] la [0:9]; logic [63:0] lb [0:5]; logic [31:0] rd;
            la[0]=64'h0000000100000044; la[1]=64'hDEADBEEF00000001; la[2]=64'h1111111122222222; la[3]=64'h3333333344444444;
            la[4]=64'h5555555566666666; la[5]=64'h7777777788888888; la[6]=64'h99999999000000AA; la[7]=64'hAAAAAAAABBBBBBBB;
            la[8]=64'hCCCCCCCCDDDDDDDD; la[9]=64'hEEEEEEEEFFFFFFFF;
            lb[0]=64'h0000000100000032; lb[1]=64'h0102030405060708; lb[2]=64'h1122334455667788; lb[3]=64'h99AABBCCDDEEFF00;
            lb[4]=64'hFEDCBA9876543210; lb[5]=64'hCAFEBABE0BADF00D;

            // ---- SCENE A: reset addr to 0 (0x0D8=0), stream {lo(0x0DC), hi(0x0E4)} per word ----
            for (int i=0;i<64;i++) model_stg[i]=64'd0;
            axi_write32(38'h0D8, 32'd0);
            for (int i=0;i<10;i++) begin axi_write32(38'h0DC, la[i][31:0]); axi_write32(38'h0E4, la[i][63:32]); end
            repeat(8) @(posedge clk);
            for (int i=0;i<10;i++) check_eq($sformatf("bridge_stgA[%0d]",i), model_stg[i][31:0], la[i][31:0]);
            if (model_stg[0] !== la[0]) begin $error("[bridge_stg] A: WORD0 not at slot 0 (0x0D8 addr-reset broken)"); errors=errors+1; end
            axi_read32(38'h0DC, rd); check_eq("A staged_addr (=nwords)", rd, 32'd10);

            // ---- NEGATIVE test: 0x0D8 SETS the addr to the written value; writing 1 mis-slots word0 to slot 1 (the
            //      EXACT board failure that produced 0 rendered beats). This pins the register semantics. ----
            for (int i=0;i<64;i++) model_stg[i]=64'd0;
            axi_write32(38'h0D8, 32'd1);
            axi_write32(38'h0DC, la[0][31:0]); axi_write32(38'h0E4, la[0][63:32]);
            repeat(8) @(posedge clk);
            if (model_stg[0][31:0] !== 32'd0) begin $error("[bridge_stg] neg: slot0 should stay empty when addr set to 1"); errors=errors+1; end
            if (model_stg[1] !== la[0])       begin $error("[bridge_stg] neg: word0 must land at slot 1 when 0x0D8=1 (bug repro)"); errors=errors+1; end

            // ---- SCENE B: re-stage a DIFFERENT list after resetting addr to 0 (A -> B, no shortcut) ----
            for (int i=0;i<64;i++) model_stg[i]=64'd0;
            axi_write32(38'h0D8, 32'd0);
            for (int i=0;i<6;i++) begin axi_write32(38'h0DC, lb[i][31:0]); axi_write32(38'h0E4, lb[i][63:32]); end
            repeat(8) @(posedge clk);
            for (int i=0;i<6;i++) check_eq($sformatf("bridge_stgB[%0d]",i), model_stg[i][31:0], lb[i][31:0]);
            if (model_stg[0] !== lb[0]) begin $error("[bridge_stg] B: WORD0 not at slot 0"); errors=errors+1; end
            axi_read32(38'h0DC, rd); check_eq("B staged_addr (=nwords)", rd, 32'd6);

            // ---- records_emitted readback (0x0E4 R reflects the feeder count; board expected 68/scene) ----
            feeder_records_i = 16'd68; repeat(4) @(posedge clk);
            axi_read32(38'h0E4, rd); check_eq("records_emitted readback", rd, 32'd68);
            $display("[bridge_stg] OK: two lists staged via bridge writes; word0@slot0, addr=nwords, addr-set semantics pinned, records=68");
        end

        // --------------------------------------------------------
        // Ch357 — persistent-Z observability status: clear_done (0x02C[7]), drop snapshot (0x0EC),
        // sticky drops-nonzero (0x02C[8]). These are the host's fail-closed gates for the Z fit.
        // --------------------------------------------------------
        begin
            frame_drained_i = 1'b0; clear_done_i = 1'b0; frag_drops_i = 32'd0; repeat(4) @(posedge clk);
            axi_read32(38'h02C, rd); check_eq("Ch357 clear_done idle 0x02C[7]=0", {31'd0, rd[7]}, 32'd0);
            axi_read32(38'h0EC, rd); check_eq("Ch357 FRAG_DROPS idle=0",           rd,             32'd0);
            axi_read32(38'h02C, rd); check_eq("Ch357 drops_nz idle 0x02C[8]=0",    {31'd0, rd[8]}, 32'd0);
            // clear_done asserts -> synced high at 0x02C[7]
            clear_done_i = 1'b1; repeat(4) @(posedge clk);
            axi_read32(38'h02C, rd); check_eq("Ch357 clear_done synced 0x02C[7]=1", {31'd0, rd[7]}, 32'd1);
            // drop snapshot latches ONLY while drained (quiescent). Drained + drops=0 -> snapshot 0, sticky 0.
            frame_drained_i = 1'b1; frag_drops_i = 32'd0; repeat(4) @(posedge clk);
            axi_read32(38'h0EC, rd); check_eq("Ch357 FRAG_DROPS drained,0",         rd,             32'd0);
            axi_read32(38'h02C, rd); check_eq("Ch357 drops_nz still 0 0x02C[8]=0",  {31'd0, rd[8]}, 32'd0);
            // inject a drop while drained -> snapshot captures the count, sticky trips
            frag_drops_i = 32'd5; repeat(4) @(posedge clk);
            axi_read32(38'h0EC, rd); check_eq("Ch357 FRAG_DROPS snapshot=5",        rd,             32'd5);
            axi_read32(38'h02C, rd); check_eq("Ch357 drops_nz sticky set 0x02C[8]=1",{31'd0, rd[8]},32'd1);
            // sticky is FAIL-CLOSED: persists even after drops clear and frame_drained drops
            frag_drops_i = 32'd0; frame_drained_i = 1'b0; repeat(4) @(posedge clk);
            axi_read32(38'h02C, rd); check_eq("Ch357 drops_nz sticky persists 0x02C[8]=1", {31'd0, rd[8]}, 32'd1);
            clear_done_i = 1'b0;
            $display("[bridge_ch357] OK: clear_done sync + drop snapshot + fail-closed sticky-nonzero pinned");
        end

        // --------------------------------------------------------
        // Ch367 -- runtime CLUT residency ABI. The 1 KiB palette window
        // is intentionally outside every legacy side-effect window. Pin
        // the address/data write pulse, expected-sum round trip, busy
        // gate, commit toggle, and done/result readback independently of
        // the actual dual-clock copier.
        // --------------------------------------------------------
        begin
            logic commit_before;
            integer writes_before;
            axi_write32(38'h200, 32'h11223344);
            @(posedge clk);
            check_eq("CLUT_STAGE data@0", clut_stage_last_data, 32'h11223344);
            check_eq("CLUT_STAGE addr@0", {24'd0, clut_stage_last_addr}, 32'd0);
            writes_before = clut_stage_writes;
            axi_write32(38'h400, 32'h55667788);
            @(posedge clk);
            check_eq("CLUT_STAGE write@128", clut_stage_writes, writes_before + 1);
            check_eq("CLUT_STAGE data@128", clut_stage_last_data, 32'h55667788);
            check_eq("CLUT_STAGE addr@128", {24'd0, clut_stage_last_addr}, 32'd128);
            writes_before = clut_stage_writes;
            axi_write32(38'h5FC, 32'hAABBCCDD);
            @(posedge clk);
            check_eq("CLUT_STAGE write@255", clut_stage_writes, writes_before + 1);
            check_eq("CLUT_STAGE data@255", clut_stage_last_data, 32'hAABBCCDD);
            check_eq("CLUT_STAGE addr@255", {24'd0, clut_stage_last_addr}, 32'd255);

            axi_write32(38'h1E4, 32'h89ABCDEF);
            axi_read32(38'h1E4, rd); check_eq("CLUT expected sum", rd, 32'h89ABCDEF);
            commit_before = clut_commit_tgl_o;
            axi_write32(38'h1E0, 32'h1);
            repeat(2) @(posedge clk);
            check_eq("CLUT commit toggle", clut_commit_tgl_o ^ commit_before, 1'b1);

            clut_busy_i = 1'b1; repeat(3) @(posedge clk);
            axi_read32(38'h1E0, rd); check_eq("CLUT busy status", rd[0], 1'b1);
            commit_before = clut_commit_tgl_o;
            axi_write32(38'h1E0, 32'h1);
            repeat(2) @(posedge clk);
            check_eq("CLUT busy blocks commit", clut_commit_tgl_o ^ commit_before, 1'b0);

            clut_busy_i = 1'b0; clut_crc_i = 32'h89ABCDEF; clut_done_tgl_i = ~clut_done_tgl_i;
            repeat(4) @(posedge clk);
            axi_read32(38'h1E0, rd); check_eq("CLUT done status", rd[1], 1'b1);
            axi_read32(38'h1E8, rd); check_eq("CLUT result sum", rd, 32'h89ABCDEF);
            $display("[bridge_clut] OK: palette window, commit/busy gate, done/result ABI pinned");
        end

        // --------------------------------------------------------
        // Done.
        // --------------------------------------------------------
        $display("[tb_ps2_hps_bridge] errors=%0d", errors);
        if (errors == 0) $display("[tb_ps2_hps_bridge] PASS");
        else             $display("[tb_ps2_hps_bridge] FAIL");
        $finish;
    end

    initial begin
        #2_000_000;
        $error("[tb_ps2_hps_bridge] TIMEOUT");
        $finish;
    end

endmodule : tb_ps2_hps_bridge
