Files
retroDE_ps2/sim/tb/platform/tb_ps2_hps_bridge.sv
T
thejayman77 ec82764bef Initial commit: retroDE_ps2 — first-of-its-kind PS2 GS FPGA core (DE25-Nano / Agilex 5)
RTL (GS rasterizer, EE core stub, platform bridge, LPDDR4B path), sim regression
(272 TBs), docs, and tooling. Copyrighted PS2 content (BIOS, game code, GS dumps,
and all dump-derived textures/traces) is excluded via .gitignore and stays local.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 20:10:50 -04:00

1202 lines
63 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;
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
// --------------------------------------------------------
// 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