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>
This commit is contained in:
2026-06-29 20:10:50 -04:00
commit ec82764bef
2462 changed files with 2174303 additions and 0 deletions
+326
View File
@@ -0,0 +1,326 @@
#!/bin/sh
# retroDE_ps2 status block — Ch219 operator helper.
#
# Reads the retroDE ABI v1.0 + ps2-specific diagnostic registers
# exposed by `ps2_hps_bridge` on the hps2fpga bridge and prints a
# one-screen status block. Run from the HPS Linux on the DE25-Nano
# board after `core_loader.sh load /home/terasic/cores/retroDE_ps2.core.rbf`.
#
# Uses `busybox devmem` rather than `devmem2` to avoid a known
# devmem2 access-size quirk on `0x?4`-suffixed offsets (the older
# devmem2 build throws "Bus error" on ABI_VERSION @ 0x40000004 and
# DMA_DONE_COUNT @ 0x40000024 — busybox devmem reads them cleanly).
#
# Usage:
# ./ps2_status.sh # one-shot snapshot
# ./ps2_status.sh --delta # take two snapshots 500ms apart, show counter deltas
#
# Exits 0 if CORE_ID matches "PS2\0" and CORE_STATUS bit [5]
# (hdmi_i2c_error) is clear, else nonzero. Suitable for automation.
set -u
BASE="${PS2_BRIDGE_BASE:-0x40000000}"
DEVMEM="${DEVMEM:-busybox devmem}"
MODE="${1:-one-shot}"
# Register offsets (matches retroDE ABI v1.0 + ps2 diagnostics; see
# rtl/platform/ps2_hps_bridge.sv and docs/hardware/de25_nano_bringup.md).
OFF_CORE_ID=0x000
OFF_ABI_VERSION=0x004
OFF_CORE_STATUS=0x008
OFF_CORE_CAPS=0x00C
OFF_CORE_CTRL=0x010
OFF_CORE_PULSE=0x014
OFF_FRAME_COUNT=0x020
OFF_DMA_DONE=0x024
OFF_RASTER_OF=0x028
# Ch222 input latches + Ch226 DS2 stub (added to the snapshot in Ch236
# so operators can confirm the input-latch landing on real silicon).
OFF_INPUT_P1=0x040
OFF_INPUT_P2=0x044
OFF_INPUT_P1_RAW=0x048
OFF_DS2_STATUS=0x0F0
OFF_DS2_BUTTONS=0x0F4
# Ch245 — OSD registers (sibling-ABI offsets, now wired to the
# platform osd_overlay + osd_menu_fsm).
OFF_OSD_CTRL=0x100
OFF_OSD_STATUS=0x104
OFF_OSD_TRIGGER=0x108
OFF_OSD_CFG0=0x110
OFF_OSD_CFG1=0x114
read_reg() {
# $1 = offset (hex string with 0x prefix)
# Returns the 32-bit value as 0x%08x via stdout.
local addr=$(printf "0x%08x" $(( BASE + $1 )) )
$DEVMEM "$addr" w
}
bit_set() {
# $1 = value, $2 = bit index → echo "1" if bit set, "0" if not.
local v=$1 b=$2
if [ $(( (v >> b) & 1 )) -eq 1 ]; then echo "1"; else echo "0"; fi
}
print_status() {
local label=$1
local id abi caps status ctrl frame dma raster
id=$(read_reg $OFF_CORE_ID)
abi=$(read_reg $OFF_ABI_VERSION)
caps=$(read_reg $OFF_CORE_CAPS)
status=$(read_reg $OFF_CORE_STATUS)
ctrl=$(read_reg $OFF_CORE_CTRL)
frame=$(read_reg $OFF_FRAME_COUNT)
dma=$(read_reg $OFF_DMA_DONE)
raster=$(read_reg $OFF_RASTER_OF)
printf "retroDE_ps2 core status [%s] @ %s\n" "$label" "$(date)"
printf "================================================\n"
printf " CORE_ID : %s " "$id"
if [ "$id" = "0x50533200" ]; then
printf '"PS2\\0" ✓ ps2 fabric loaded\n'
else
printf " ✗ wrong/no fabric loaded (expected 0x50533200)\n"
fi
printf " ABI_VERSION : %s " "$abi"
if [ "$abi" = "0x00000100" ]; then
printf "(v1.0) ✓ retroDE ABI v1.0\n"
else
printf " ✗ unexpected ABI\n"
fi
# Ch246 — CORE_CAPS now advertises OSD geometry per the sibling-ABI
# bit layout (matches nes_hps_bridge.sv). Decode the fields so
# operators can spot a misadvertisement at a glance.
local caps_cols caps_rows caps_2p caps_analog caps_savestates caps_save
caps_save=$(( caps & 0x1 ))
caps_savestates=$(( (caps >> 1) & 0x1 ))
caps_2p=$(( (caps >> 2) & 0x1 ))
caps_analog=$(( (caps >> 3) & 0x1 ))
caps_cols=$(( (caps >> 8) & 0xff ))
caps_rows=$(( (caps >> 16) & 0x1f ))
printf " CORE_CAPS : %s (osd=%dx%d save=%d ss=%d 2p=%d analog=%d)\n" \
"$caps" "$caps_cols" "$caps_rows" "$caps_save" "$caps_savestates" "$caps_2p" "$caps_analog"
printf "\n"
# CORE_STATUS bit decode
local sv=$((status))
printf " CORE_STATUS : %s\n" "$status"
printf " [0] loaded : %s\n" "$(bit_set $sv 0)"
# Ch251 inverted core_halt semantics: pre-Ch251 the bootlet halted
# after a single paint (lit = healthy); the animated demo now loops
# forever (unlit = healthy, lit = EE stuck on an unexpected SYSCALL).
printf " [1] core_halt : %s " "$(bit_set $sv 1)"
if [ "$(bit_set $sv 1)" = "0" ]; then
printf "✓ Ch251 animated demo running (expected)\n"
else
printf "✗ EE halted — pre-Ch251 sentinel, or animated loop crashed/SYSCALL'd\n"
fi
printf " [2] dma_done_seen : %s\n" "$(bit_set $sv 2)"
printf " [3] frame_seen : %s (lit = PCRTC delivered ≥1 frame)\n" "$(bit_set $sv 3)"
printf " [4] hdmi_init_done : %s (lit = ADV7513 LUT walk complete)\n" "$(bit_set $sv 4)"
printf " [5] hdmi_i2c_error : %s " "$(bit_set $sv 5)"
if [ "$(bit_set $sv 5)" = "0" ]; then printf "✓ no I²C NACKs\n"; else printf "✗ NACK watchdog LATCHED — see runbook triage\n"; fi
printf " [6] raster_overflow : %s " "$(bit_set $sv 6)"
if [ "$(bit_set $sv 6)" = "0" ]; then printf "✓ raster healthy\n"; else printf "✗ raster FIFO overflowed — Ch172 backpressure broken?\n"; fi
printf "\n"
# CORE_CTRL bit decode
local cv=$((ctrl))
printf " CORE_CTRL : %s\n" "$ctrl"
printf " [0] reset : %s (1 = PS2 design held in reset)\n" "$(bit_set $cv 0)"
printf " [1] rom_loaded : %s (ABI-shape latch, no functional effect yet)\n" "$(bit_set $cv 1)"
printf " [2] pause : %s (ABI-shape latch, no functional effect yet)\n" "$(bit_set $cv 2)"
printf "\n"
printf " Counters:\n"
printf " FRAME_COUNT : %s (advances at ~60Hz once PCRTC is alive)\n" "$frame"
printf " DMA_DONE_COUNT : %s\n" "$dma"
printf " RASTER_OVERFLOW_COUNT : %s (should be 0 under Ch172 backpressure)\n" "$raster"
printf "\n"
# Ch222 / Ch226 / Ch235 input-latch readbacks. These prove the
# HPS-to-bridge half of the input path is alive on real silicon.
# PS2-fabric consumption (Ch234 sio2_input_stub via IOP) is sim-
# validated by tb_bridge_iop_pad_input but not wired into the
# synth top yet — so non-zero values here mean "the bridge latch
# landed" rather than "an in-fabric consumer saw it."
local p1 p2 p1raw ds2s ds2b
p1=$(read_reg $OFF_INPUT_P1)
p2=$(read_reg $OFF_INPUT_P2)
p1raw=$(read_reg $OFF_INPUT_P1_RAW)
ds2s=$(read_reg $OFF_DS2_STATUS)
ds2b=$(read_reg $OFF_DS2_BUTTONS)
printf " Input latches (Ch222) + DS2 mirror (Ch226):\n"
printf " INPUT_P1 : %s\n" "$p1"
printf " INPUT_P2 : %s\n" "$p2"
printf " INPUT_P1_RAW : %s\n" "$p1raw"
# Ch248 — DS2_STATUS is now driven by the platform ds2_controller
# (via the bridge's new ds2_connected_i / ds2_error_i input ports).
# Bit layout: [0]=connected, [1]=error, [2]=1 (PS2-local legacy bit).
local ds2_conn ds2_err
ds2_conn=$(( $ds2s & 0x1 ))
ds2_err=$(( ($ds2s >> 1) & 0x1 ))
printf " DS2_STATUS : %s ([0]=connected=%d [1]=error=%d [2]=reserved=1)\n" \
"$ds2s" "$ds2_conn" "$ds2_err"
# DS2_BUTTONS is now the live wired-controller bitmap (Ch248); the
# Ch226 INPUT_P1 mirror was removed because it blocked retrodesd's
# ds2_poll_thread from seeing real button state.
printf " DS2_BUTTONS : %s (live ds2_controller readback — Ch248)\n" "$ds2b"
if [ "$ds2_conn" = "1" ]; then
printf " → controller plugged in; ds2_poll_thread should be writing INPUT_P1/RAW from this.\n"
else
printf " → controller unplugged (DS2_STATUS[0]=0). Plug a DS2 wired pad into\n"
printf " the DE25 GPIO header (CLK/CMD/DATA/ATTN = PIN_H16/Y1/C2/P1) to wake the path.\n"
fi
# Ch245 — platform OSD register surface. Sibling-ABI layout; the
# shared retroDE_splash overlay + menu FSM is wired to these.
local octrl ostatus otrig ocfg0 ocfg1
octrl=$(read_reg $OFF_OSD_CTRL)
ostatus=$(read_reg $OFF_OSD_STATUS)
otrig=$(read_reg $OFF_OSD_TRIGGER)
ocfg0=$(read_reg $OFF_OSD_CFG0)
ocfg1=$(read_reg $OFF_OSD_CFG1)
printf "\n OSD registers (Ch245 platform OSD):\n"
printf " OSD_CTRL : %s ([0]=enable [2]=force_open [3]=force_close,self-clear)\n" "$octrl"
printf " OSD_STATUS : %s ([0]=osd_active [12:8]=cursor_row)\n" "$ostatus"
printf " OSD_TRIGGER : %s ([4]=A [5]=B [6]=down [7]=up [16]=open [12:8]=row)\n" "$otrig"
printf " OSD_CFG0 : %s ([5:0]=cols [12:8]=rows [23:16]=origin_x_chars [31:24]=origin_y_chars)\n" "$ocfg0"
printf " OSD_CFG1 : %s ([4:0]=menu_first_row [12:8]=menu_last_row [23:16]=cursor_attr)\n" "$ocfg1"
# Decode CFG0 fields for human readability.
local cols rows origx origy origx_pix origy_pix
cols=$(( ocfg0 & 0x3f ))
rows=$(( (ocfg0 >> 8 ) & 0x1f ))
origx=$(((ocfg0 >> 16) & 0xff ))
origy=$(((ocfg0 >> 24) & 0xff ))
origx_pix=$(( origx * 16 ))
origy_pix=$(( origy * 16 ))
printf " → cols=%d rows=%d origin=(%d,%d) chars = (%d,%d) px at 2× scale\n" \
"$cols" "$rows" "$origx" "$origy" "$origx_pix" "$origy_pix"
}
print_delta() {
# Ch253 — two snapshots 2 s apart so a Ch251.5 ~1 Hz heartbeat
# toggle is captured reliably regardless of phase (was 500 ms in
# Ch219 when the bootlet was one-shot and counter Δ didn't matter
# for blink rate).
local f1 d1 r1 f2 d2 r2
f1=$(read_reg $OFF_FRAME_COUNT)
d1=$(read_reg $OFF_DMA_DONE)
r1=$(read_reg $OFF_RASTER_OF)
sleep 2
f2=$(read_reg $OFF_FRAME_COUNT)
d2=$(read_reg $OFF_DMA_DONE)
r2=$(read_reg $OFF_RASTER_OF)
local df dd dr
df=$(( f2 - f1 ))
dd=$(( d2 - d1 ))
dr=$(( r2 - r1 ))
printf "\n Counter Δ over 2 s:\n"
printf " FRAME_COUNT : %s → %s Δ=%d (≈ 120 if PCRTC is alive @ 60 Hz)\n" "$f1" "$f2" "$df"
printf " DMA_DONE_COUNT : %s → %s Δ=%d (validated band 02 per 2 s — liveness cue, not a clock)\n" "$d1" "$d2" "$dd"
printf " RASTER_OVERFLOW_COUNT : %s → %s Δ=%d (MUST stay 0 — Ch172 backpressure)\n" "$r1" "$r2" "$dr"
# Ch253 — Ch251+ animated-demo health verdict. Rolls up the
# individual signals into a single PASS/FAIL block so the
# operator can run `ps2_status.sh --delta` and know in <3 s
# whether the field unit is healthy.
local status_final halt_bit i2c_bit raster_bit ds2_conn_bit
status_final=$(read_reg $OFF_CORE_STATUS)
halt_bit=$(bit_set $((status_final)) 1)
i2c_bit=$(bit_set $((status_final)) 5)
raster_bit=$(bit_set $((status_final)) 6)
ds2_conn_bit=$(( $(read_reg $OFF_DS2_STATUS) & 0x1 ))
local verdict_ok=1
printf "\n Ch251+ animated-demo health verdict:\n"
if [ "$df" -ge 100 ] && [ "$df" -le 140 ]; then
printf " [ ✓ ] PCRTC alive (FRAME_COUNT Δ=%d ≈ 120 @ 60 Hz)\n" "$df"
else
printf " [ ✗ ] PCRTC stalled/off-rate (FRAME_COUNT Δ=%d, expected 100140)\n" "$df"
verdict_ok=0
fi
if [ "$raster_bit" = "0" ] && [ "$dr" -eq 0 ]; then
printf " [ ✓ ] Raster healthy (RASTER_OVERFLOW_COUNT stable + bit[6] clear)\n"
else
printf " [ ✗ ] Raster overflowing (Δ=%d / status bit[6]=%s — Ch172 broken?)\n" "$dr" "$raster_bit"
verdict_ok=0
fi
# Ch253 / Ch254 — this counter is a LIVENESS CUE, not a precision
# timer. The bootlet ties DMAC DONE 1:1 to the visible heartbeat
# toggle, so DMA_DONE Δ over 2 s is a faithful proxy for the blink.
# Hardware-validated Δ band is {0, 1, 2}; ~1.2 s fixed per-iter
# overhead (DMAC drain + 17-SPRITE GS rasterize) puts the cadence
# around 0.5 Hz with natural ±0.5 s jitter — see bake.py comment
# block and de25_nano_bringup.md Ch254 for the empirical model.
# Δ=0 is a 2 s phase-miss not a failure; rerun protocol.
if [ "$dd" -ge 1 ] && [ "$dd" -le 2 ]; then
printf " [ ✓ ] DMAC repaint liveness (DMA_DONE Δ=%d over 2 s; bootlet animating)\n" "$dd"
elif [ "$dd" -eq 0 ]; then
printf " [ ? ] DMAC repaint window miss (Δ=0 — likely 2 s phase miss; rerun. Persistent Δ=0 = bootlet stuck)\n"
# Don't fail the verdict on a single Δ=0; rerun is the right
# protocol. Persistent stuck-state shows up as the operator
# running --delta twice in a row and getting Δ=0 both times.
else
printf " [ ? ] DMAC repaint off-rate (Δ=%d, validated band is 02 over 2 s — investigate)\n" "$dd"
verdict_ok=0
fi
if [ "$halt_bit" = "0" ]; then
printf " [ ✓ ] EE core not halted (CORE_STATUS[1]=0)\n"
else
printf " [ ✗ ] EE halted (CORE_STATUS[1]=1 — animated loop crashed/SYSCALL)\n"
verdict_ok=0
fi
if [ "$i2c_bit" = "0" ]; then
printf " [ ✓ ] HDMI I²C clean (CORE_STATUS[5]=0)\n"
else
printf " [ ✗ ] HDMI I²C NACKed (CORE_STATUS[5]=1 — see runbook triage)\n"
verdict_ok=0
fi
if [ "$ds2_conn_bit" = "1" ]; then
printf " [ ✓ ] DS2 controller plugged (DS2_STATUS[0]=1)\n"
else
printf " [ — ] DS2 controller unplugged (informational — Select+Start menu needs a pad)\n"
fi
if [ "$verdict_ok" -eq 1 ]; then
printf "\n ──> Ch251+ field health: PASS\n"
else
printf "\n ──> Ch251+ field health: FAIL (see flags above)\n"
fi
# Build-time profile reminder. The script cannot read VRAM_ENABLE_READ2
# at runtime (it is baked into the bitstream); the sim-side tripwire
# in vram_bram_stub.sv catches a bad profile during iverilog/verilator
# elaboration, but Quartus does not honour it (the guard sits inside
# `translate_off`). Real synth-side protection is the explicit
# parameter override in de25_nano_psmct32_raster_demo_top.sv.
printf "\n Profile note: hardware build assumes VRAM_ENABLE_READ2=0 (Ch252).\n"
printf " Re-enabling read2 at VRAM_BYTES=512 KiB will blow the\n"
printf " Agilex 5 M20K budget at fit time. See\n"
printf " docs/decisions/0006-vram-roadmap.md for triggers.\n"
}
print_status "snapshot"
if [ "$MODE" = "--delta" ]; then
print_delta
fi
# Exit status: 0 if CORE_ID + clean I²C, nonzero otherwise.
id_ok=$(read_reg $OFF_CORE_ID)
status_check=$(read_reg $OFF_CORE_STATUS)
i2c_err=$(bit_set $((status_check)) 5)
if [ "$id_ok" = "0x50533200" ] && [ "$i2c_err" = "0" ]; then
exit 0
else
exit 1
fi