Files
retroDE_ps2/sim/tb/platform/tb_ps2_hps_bridge.sv
T
thejayman77 064484c50d Ch443f: coherent Gray readiness CDC + qualified scanout underflow detector
Harden the LPDDR scanout underflow diagnostic (Codex direction). Keeps the
Ch443e 4-buffer + lead-2 fix; adds no 5th buffer.

RTL (gs_lpddr_scanout_lb):
- Replace the raw-binary next_fetch readiness sync with a reset-aware GRAY
  code. next_fetch is monotonic between frames, so one Gray bit changes per
  increment; the 2-FF-synced + decoded nf_v is always a real prior frontier
  (monotone, burst-safe), never a torn multi-bit combination. (A plain
  toggle-per-change handshake dropped bursts when two increments landed in one
  sync window; that is why the earlier attempt under-read nf_v and false-tripped.)
  fs_edge_v overrides the lone multi-bit reset transient.
- Qualify the underflow: only a miss persisting >= QUAL_CYCLES (4) sets sticky
  underflow. uf_pmax_q records the longest streak and uf_qual_q whether any
  qualified, so a host can distinguish a 1-cycle CDC lag from a real late row.
- Atomic snapshot: scan_y/nf_v/pmax/causes/vphase/line_valid all latched the
  same video cycle on the first qualified miss.

ABI: 0x120 adds [15:12]=live pmax (2-FF synced via scan_diag_pmax_i). 0x124
[29:20] now carries pmax-at-capture (was nf_s0). Bridge dst reg kept 10-bit.

SDC: scanout diag bundle source count 37 -> 31 (nf_s0[10] -> pmax[4]); new
async-in cut + max_skew/net_delay for the next_fetch Gray CDC; stage-0 cut for
the live pmax sync.

Tests: new tb_gs_scanout_cdc_qual (async-clock focused: no false event from the
readiness transition or ordinary sync latency; sub-QUAL transient does not
qualify; genuine late row qualifies with a self-consistent atomic snapshot;
frame reset + mod-4 reuse re-arm the detector). tb_gs_scanout_diag updated for
the pmax field. Tie off scan_diag_pmax_i (+ pre-existing clut_* gap from the
fog baseline) in the four .* bridge/integration TBs.

Regressions green: scanout (cdc_qual, binomial_lookahead, diag, restart,
lpddr_scanout_lb x3), regbuf (r/aw/w), bridge + 3 pad integration TBs, and the
f52 top-level golden FB (Z 0/307200, COLOR 0/245760 mismatch, drops=0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 23:17:41 -04:00

1367 lines
73 KiB
Systemverilog
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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.
// Ch222Ch224 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)
// Ch442 — scanout A+B diagnostic inputs; tied off (.* matches by name).
logic scan_diag_uf_i = 1'b0, scan_diag_rderr_nz_i = 1'b0, scan_diag_valid_i = 1'b0;
logic [29:0] scan_diag_first_i = 30'd0;
logic [6:0] scan_diag_stat_i = 7'd0;
logic [3:0] scan_diag_pmax_i = 4'd0;
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);
// Ch222Ch224 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