# DE25-Nano bring-up runbook (PSMCT32 raster demo) This is the operator-side checklist for taking a fresh build and getting it to drive a real HDMI monitor on the Terasic DE25-Nano (Agilex 5 `A5EB013BB23BE4SCS`). It pairs with the RTL bring-up history captured in `rtl/top/de25_nano_psmct32_raster_demo_top.sv` (Ch149 → Ch170). ## Quick-start (operator's three steps) 1. **Build** — `./synth/de25_nano/top_psmct32_raster_demo/build_quartus.sh` produces `retroDE_ps2.core.rbf` in `output_files/`. 2. **Load** — `scp` the RBF to the board, then `sudo /home/terasic/core_loader.sh load /home/terasic/cores/retroDE_ps2.core.rbf` on the HPS. 3. **Verify** — `./ps2_status.sh --delta` on the HPS prints the full status block + counter deltas; exit status is 0 if the ps2 fabric is healthy. See "Status block" below for the canonical output. Full detail and triage for each step follows. ## Build There are two equivalent ways to compile. ### Option A — Quartus GUI (matches the rest of the retroDE family) 1. Open `synth/de25_nano/top_psmct32_raster_demo/de25_nano_psmct32_raster_demo_top.qpf` in Quartus Prime Pro 25.3.1 SP1.02. 2. *Processing → Start Compilation* (or the green ▶ button). 3. After the Assembler step, the `post_flow.tcl` hook runs automatically and emits the boot artifacts described below. The QSF references sources via `rtl/...`, `sim/...`, and (Ch170) `qsys/...` paths relative to its own directory. Matching `build_quartus.sh`'s convention of running from a `work/` subdirectory with those trees symlinked in. The same pattern works for the GUI because the QSF directory contains permanent symlinks: ``` synth/de25_nano/top_psmct32_raster_demo/rtl -> ../../../rtl synth/de25_nano/top_psmct32_raster_demo/sim -> ../../../sim synth/de25_nano/top_psmct32_raster_demo/qsys -> ../../../qsys ``` If those symlinks are missing the GUI compile fails with "Can't analyze file rtl/... - no such file exists" or "Error opening qsys/qsys_top.qsys" errors. Recreate from the QSF directory: ``` ln -sfn ../../../rtl rtl ln -sfn ../../../sim sim ln -sfn ../../../qsys qsys ``` ### Option B — command line ```bash ./synth/de25_nano/top_psmct32_raster_demo/build_quartus.sh ``` (or `make -C sim quartus_compile`). Runs `quartus_syn → fit → sta → asm`. The assembler step is gated on a clean STA so the `.sof` is produced only when timing closes. ### Build artifacts All paths under `synth/de25_nano/top_psmct32_raster_demo/output_files/`: | Artifact | What it is | |------------------------------------------------|-------------------------------------------------------------| | `de25_nano_psmct32_raster_demo_top.sof` | bare fabric SOF (JTAG / debug fallback) | | `de25_nano_psmct32_raster_demo_top.rbf` | plain configuration RBF from `quartus_pfg` | | **`retroDE_ps2.core.rbf`** | **stable deploy artifact — what `core_loader.sh` consumes** | Per-step logs (`*.flow.rpt`, `*.fit.rpt`, `*.sta.summary`) land in the same directory. > **Ch168→Ch170 evolution (resolved)**: Ch168's plain RBF loaded > through `core_loader.sh` (fpga0 reached `operating`) but the > resulting system was hosed — SSH died, board required power-cycle. > Diagnosis: the fabric was running but had no HPS bridge endpoints, > so HPS-side AXI transactions stalled into the void. Ch170 fixed > this by copying the canonical retroDE qsys subsystem verbatim > from `retroDE_Atari2600/qsys/`, adding the HPS + LPDDR4A pin > blocks, instantiating `qsys_top soc_inst (...)` with a minimal > `ps2_hps_bridge_null` AXI4 slave on the hps2fpga bridge, and > adding the `HPS_INITIALIZATION "HPS FIRST"` global + the > `PRESERVE_REGISTER` / `PRESERVE_FANOUT_FREE_NODE` instance > assignments on the Sundancemesa MPFE f2sdram path (without > those, the optimizer fails the build with "F2SDRAM_RDATA not > legally connected"). Also fixed: the QSF was referencing > `QIP_FILE ip/pll/pll.qip` (cached output) instead of > `IP_FILE ip/pll.ip` (source), so the .ip frequency edit at > Ch169 silently didn't propagate — the PLL stayed at 50 MHz and > the monitor reported "No support" because the raster was > 640×480 at 119 Hz. With `IP_FILE` and the PLL retuned to > **25.0 MHz** (cleanly achievable from the 50 MHz refclk via M=1 > / N=1 / C=2), the design now drives 640×480 @ ~59.52 Hz and > monitors lock cleanly. > > **First hardware-real bring-up**: the `retroDE_ps2.core.rbf` > produced by the Ch170 build is the first that: > 1. Loads through `core_loader.sh` and keeps the HPS-side Linux > running normally (SSH stays up, board does not need a > power-cycle). > 2. Locks a standard HDMI 640×480@60 mode on the monitor. > 3. Paints recognisable pixels from the PCRTC (the Ch123 16×8 > test sprite is visible as 4 colored quadrants in the > upper-left of the otherwise-black 640×480 frame). ## Programming Two paths. The standard retroDE runtime-reload flow is the primary path; JTAG is the debug/fallback. ### Path 1 — runtime fabric reload (primary) This is the standard retroDE workflow: HPS is up and Linux is running (booted from the QSPI-resident retroDE_splash image), and you swap the FPGA fabric at runtime via the on-board loader. The device-tree overlay points the Linux fpga-manager at the new RBF in `/lib/firmware/`; the previous fabric is unloaded automatically. Copy the deploy artifact to the board, then: ```bash sudo /home/terasic/core_loader.sh load \ /home/terasic/cores/retroDE_ps2.core.rbf ``` The loader script is in [`retroDE_splash/software/core_loader.sh`](../../../retroDE_splash/software/core_loader.sh). It is filename-agnostic — it copies whatever RBF you point it at to `/lib/firmware/` and applies the `/fpga-region` DT overlay. If `fpga-manager` rejects the RBF (overlay status != applied, `dmesg` shows configuration errors), see "What's deferred" below. ### Path 2 — bare-metal JTAG (debug / fallback) Useful when HPS isn't running (cold board) or when you want to program without disturbing the loader pipeline: ```bash quartus_pgm -m jtag -o "p;synth/de25_nano/top_psmct32_raster_demo/output_files/de25_nano_psmct32_raster_demo_top.sof" ``` `p` = program (volatile — bitstream is lost on power-cycle). Note that JTAG programming will tear down the HPS-running splash fabric; a power-cycle restores it from QSPI. ### retrodesd manifest (Ch219) When the ps2 core is registered with retrodesd (the userspace supervisor on HPS), the manifest stanza is: ```ini [ps2] name=PS2 Graphics Demo rbf=/home/terasic/cores/retroDE_ps2.core.rbf backend=splash core_id=0x50533200 min_abi=0x00000100 ``` `backend=splash` is correct for the current demo (no session/ROM/save semantics). The stanza follows the same shape as the other retroDE cores (nes, coco2, atari2600) and matches the ABI v1.0 contract exposed by `ps2_hps_bridge` (CORE_ID = `0x50533200`, ABI_VERSION = `0x00000100`). The direct `core_loader.sh load` path remains useful for bring-up and JTAG-replacement workflows; retrodesd is for production deploys. ## LED ledger The DE25-Nano has 8 user LEDs (`LED[7:0]`), all **active-LOW** (lit = asserted internally). In this top: | LED | Source | Polarity meaning | |----------|---------------------|------------------------------| | LED[0] | `core_halt` | **Ch251: unlit normal** (EE running the animated paint loop). Lit = EE SYSCALL'd unexpectedly, demo stuck. Pre-Ch251 it was lit = bootlet completed. | | LED[1] | `dma_done_seen` | lit = DMA finish observed (good) | | LED[2] | `frame_seen` | lit = PCRTC produced a frame (good) | | LED[3] | `hdmi_init_done` | lit = ADV7513 I²C config done (good) | | LED[4] | `hdmi_i2c_error` | lit = NACK watchdog latched (**bad**) | | LED[5] | `p1_sony_word[3]` (START) | lit = START held on the wired pad (Ch250) | | LED[6] | `p1_sony_word[14]` (CROSS ×) | lit = ×/B held on the wired pad (Ch250) | | LED[7] | `p1_sony_word[4]` (D-pad UP) | lit = D-pad UP held on the wired pad (Ch250) | In steady state on a healthy boot (Ch251 animated demo), `LED[1..3]` are lit and `LED[4]` stays dark; `LED[0]` **stays unlit** because the animated bootlet loops forever instead of SYSCALL-halting. The order is *roughly* the one below but is not strictly deterministic — `LED[1..2]` are pure-fabric and light within a few clock cycles of `core_go`, while `LED[3]` depends on the ~125 ms I²C walk (see step 5) so it always lights *last* of the three. Pre-Ch251 the success indicator was "LED[0..3] all lit"; the Ch251 indicator is "frame heartbeat visible + FRAME_COUNT advancing + RASTER_OVERFLOW_COUNT = 0." ## Expected behavior on first boot 1. FPGA configures (`Programming complete` on JTAG, or `fpga0: operating` after `core_loader.sh load`). 2. LED[0] eventually lights (EE core halts after running the bootlet — typically within a few ms of reset deassertion). 3. LED[1] eventually lights as the DMAC reports `done`. 4. LED[2] eventually lights once the PCRTC delivers its first frame. 5. LED[3] eventually lights — the ADV7513 I²C config FSM walks the 38-entry LUT. Walk time is **~125 ms at the production divider** (controller-clock period ~100 µs × 33 phases/byte × 38 bytes = ~125 ms), so LED[3] will be the slowest of the four to light. The `CLK_Freq / I2C_Freq` divider is `50e6/20e3 = 2500`; one register write is ≈ 3.3 ms. 6. LED[4] **stays dark** (no NACKs on the I²C bus). 7. The HDMI monitor reports a **VGA 640×480 @ ~60 Hz** signal (Ch169 retuned the IOPLL to 25.0 MHz with standard VGA timing). Ch171 bumped the PCRTC paint area to a **320×240 four-quadrant test card** in the upper-left of the 640×480 frame: - **TL** Q0 = **RED** (255, 0, 0) - **TR** Q1 = **GREEN** (0, 255, 0) - **BL** Q2 = **BLUE** (0, 0, 255) - **BR** Q3 = **WHITE** (255, 255, 255) The remaining ~50% of the screen (x ≥ 320 or y ≥ 240) is black because PCRTC outside the DISPLAY1 window emits 0. This is the "obvious from across the room, photographable" hardware heartbeat — easy to confirm the whole DMAC/GIF/GS/raster/VRAM/ PCRTC/HDMI path is alive without squinting at a 16×8 patch. ## Triage ### No LEDs light at all - Check JTAG: `Programming complete` was reported by `quartus_pgm`? - Check `ninit_done` polarity — if the FPGA never deasserts init, the whole design stays in reset and no LED ever lights. - Re-program; some Agilex 5 dev boards need a power-cycle between bitstream loads. ### LED[0..2] light but LED[3] never lights (HDMI never inits) - Is LED[4] lit? → see next section. - HDMI cable plugged in? The chip drives I²C even with no HDMI cable, so HDMI cable absence does NOT block LED[3]. But HPD/INT lines may oscillate, retriggering the FSM forever. - Is `HDMI_I2C_SCL` toggling? Probe the test point or the ADV7513 side. No SCL = the bit-bang master isn't running (clock domain or reset issue). ### LED[4] lit (NACK watchdog latched) The watchdog asserts after 16 consecutive NACKs on the same LUT entry. Likely causes (in priority order): - HDMI cable not plugged in **and** the ADV7513 power rail isn't bringing the chip out of standby. The DE25-Nano feeds chip power unconditionally, so this should not happen on a healthy board. - I²C address mismatch. Slave address in `I2C_HDMI_Config.v` is `8'h72` (`mI2C_DATA <= {8'h72, LUT_DATA}`). DE25-Nano boards wire the ADV7513's `PD#` and address-select pins so this is the chip address — but if a board variant differs, every byte will NACK. - SDA short to ground or VCC. - ADV7513 reset (`HDMI_TX_RST_N` if exposed) held asserted. LED[4] is sticky — once latched it stays lit until FPGA is reconfigured. Re-program the bitstream to retry. ### LED[3] lights but no HDMI image - Try a different monitor / HDMI cable. The demo emits 24-bit RGB full-range with separate H/V sync; some monitors are picky about edge timing. - Ch164's `set_false_path -to HDMI_TX_*` is in place — proper output-delay constraints (ADV7513 setup/hold) are deferred to a later chapter. Output-pin timing on real silicon may be marginal on long cables, but typical bench cables are short enough that the PSMCT32 demo pixel rate (50 MHz pixel clock) doesn't push it. - Check for HDMI-only HDCP requirements on the monitor — the demo doesn't drive HDCP. ### Image flickers / unstable - Most likely cause: the `HDMI_TX_INT` line is bouncing, retriggering the LUT walk. LED[3] should drop briefly each time. Confirm by watching LED[3] — if it blinks, the HPD pin is unstable. - Workaround for monitor compatibility: the `0xD6 = 0xC0` HPD override in the LUT (entry 1) is supposed to ignore HPD; if your monitor still misbehaves, the override may need adjustment. ## HPS↔fabric status registers (Ch173 / Ch174 / Ch176) After `core_loader.sh load` brings the ps2 fabric up, HPS userspace can read live status from the `ps2_hps_bridge` AXI slave on the hps2fpga bridge. Registers are read-only; each is a 32-bit value addressable by its byte offset. The first 32 bytes (0x000–0x01F) mirror the shared retroDE ABI v1.0 layout (matches splash/nes/coco2 — see `retroDE_splash/software/input_common.h`). PS2-specific diagnostics start at 0x020. | Offset | Register | Meaning | |--------|-----------------------|------------------------------------------------------| | 0x000 | CORE_ID | `0x50533200` (ASCII `"PS2\0"`) | | 0x004 | ABI_VERSION | `0x00000100` (v1.0) | | 0x008 | CORE_STATUS | bit 0=loaded, 1=core_halt, 2=dma_done_seen, 3=frame_seen, 4=hdmi_init_done, 5=hdmi_i2c_error, 6=raster_overflow | | 0x00C | CORE_CAPS | `0x00000000` (no caps advertised; ROM/savestate bits go here later) | | 0x010 | CORE_CTRL | Ch176 writable + readable. `[0]=RESET` is functional (held high → PS2 design in reset; counters keep ticking from the bridge side). `[1]=ROM_LOADED` and `[2]=PAUSE` are pure latches (ABI shape only, no functional effect today). `[31:3]` ignored on write | | 0x014 | CORE_PULSE | Ch176 self-clearing pulse register (always reads 0). `[3]=HDMI_CLR` zeros FRAME_COUNT / DMA_DONE_COUNT / RASTER_OVERFLOW_COUNT synchronously. `[0..2]` (DIRTY_CLR / SS_DONE_CLR / VIDEO_REC) ACK'd-and-ignored | | 0x018 | reserved | 0 | | 0x01C | reserved | 0 | | 0x020 | FRAME_COUNT | Ch174: increments on each *edge* of the design-domain `frame_toggle` (flips on every PCRTC end-of-frame). 25→50 MHz CDC done via event-toggle, not raw pulse — see `ps2_hps_bridge` header for the CDC contract | | 0x024 | DMA_DONE_COUNT | Ch174: increments on each edge of `dma_done_toggle` (flips on every DMAC `EV_DMA_DONE`) | | 0x028 | RASTER_OVERFLOW_COUNT | cycles `raster_overflow` stayed high; **0 under Ch172 backpressure** | | 0x060 | VIDEO_STATUS | Ch225 read-only diagnostic. `[0]=frame_seen` (live sticky), `[1]=scanout_alive` (= `FRAME_COUNT != 0`, clears with HDMI_CLR), `[2]=raster_overflow` (live, drops if input dies), `[3]=dma_done_seen` (live sticky); `[31:4]` reserved-zero. Writes ack-and-ignored | | 0x064 | HDMI_DIAG | Ch225 read-only diagnostic. `[0]=hdmi_init_done`, `[1]=hdmi_i2c_error`; `[31:2]` reserved-zero. Writes ack-and-ignored | | 0x0F0 | DS2_STATUS | Ch226 read-only stub. Sibling-ABI: `[0]=connected=0` (no DS2 path), `[1]=error=0`. PS2-local: `[2]=input_latches_valid=1` (Ch222 OK). Reads = `0x00000004`. Writes ack-and-ignored. retrodesd's `ds2_poll_thread.c` sees `connected=0` and never invokes `osd_input_update_ds2` | | 0x0F4 | DS2_BUTTONS | Ch226 read-only mirror of INPUT_P1 (`0x040`). Lets operator tooling confirm the Ch222 store via the same offset sibling cores expose. With `DS2_STATUS[0]=0` retrodesd never reads it; the mirror is for HPS diagnostics | | 0x1000–0x1FFF | Tile RAM | Ch227 write/read 1024 × 32-bit storage (4 KB). HPS-visible only; no PCRTC overlay path yet (Ch228 wires that). Software view per `input_common.h` is 2048 × 16-bit cells packed 2/word; at the bus level it's plain 32-bit words. Retained across warm reset (sim-initialized to 0; hardware power-up undefined) | | 0x040 | INPUT_P1 | Ch222 write/read latch (HPS-visible). retrodesd's `input_thread` writes the remapped player-1 gamepad bitmap here. Reset = 0. No SIO2/DualShock wiring on the PS2 side yet — the latch is read back for software introspection only | | 0x044 | INPUT_P2 | Ch222 write/read latch (HPS-visible). Player-2 bitmap. Reset = 0 | | 0x048 | INPUT_P1_RAW | Ch222 write/read latch (HPS-visible). Un-remapped mirror of the player-1 buttons (the OSD-navigation source on other cores). Reset = 0 | | 0x100 | OSD_CTRL | Ch223 write/read latch (HPS-visible). Splash backend writes ENABLE / FORCE_OPEN / INPUT_LOCK here; **no overlay rendering yet** — the bridge stores the value for software introspection only. Reset = 0 | | 0x104 | OSD_STATUS | Ch224 always-zero source. No FSM drives `[0]=active` / `[12:8]=cursor_row` on PS2 yet, so reads always return 0; writes accepted with BRESP=OKAY and ignored | | 0x108 | OSD_TRIGGER | Ch224 W1C-shape sink. retrodesd polls and writes-1-to-clear `OSD_TRIG_ACTION/BACK/SCROLL_*` bits; PS2 has no FSM source so the bits stay 0 and W1C is a no-op against an already-zero register. Reads always 0 | | 0x10C | OSD_INPUT | reserved — reads 0; writes accepted-and-ignored. (Sibling cores use this as a debug-override; PS2 has no use until an overlay engine exists) | | 0x110 | OSD_CFG0 | Ch223 write/read latch (HPS-visible). cols / rows / x_chars / y_chars layout per `input_common.h`. Reset = 0 | | 0x114 | OSD_CFG1 | Ch223 write/read latch (HPS-visible). first_row / last_row + highlight + normal attrs. Reset = 0 | The hps2fpga bridge base on this board is **`0x40000000`** (per `retroDE_splash/software/input_common.h: #define HPS2FPGA_BASE 0x40000000`). After loading `retroDE_ps2.core.rbf`: ```bash sudo devmem2 0x40000000 w # CORE_ID — expect 0x50533200 sudo devmem2 0x40000004 w # ABI_VERSION — expect 0x00000100 sudo devmem2 0x40000008 w # CORE_STATUS — bits 0..5 (and 6 if FIFO ever overflowed) sudo devmem2 0x4000000C w # CORE_CAPS — 0 sudo devmem2 0x40000020 w # FRAME_COUNT — Ch174: ticks @ ~60 Hz; repeated reads INCREASE sudo devmem2 0x40000024 w # DMA_DONE_COUNT — Ch174: 1 after the boot DMA (no second DMA unless retriggered) sudo devmem2 0x40000028 w # RASTER_OVERFLOW_COUNT — 0 under Ch172 backpressure ``` ### Ch176 — exercising the writable control surface ```bash # Verify CORE_CTRL latches without resetting the core. sudo devmem2 0x40000010 w 0x06 # PAUSE | ROM_LOADED, RESET clear sudo devmem2 0x40000010 w # expect readback 0x00000006 sudo devmem2 0x40000010 w 0x00 # back to clean # Hold the PS2 design in reset, observe the monitor blank. # FRAME_COUNT stops advancing during reset; release brings the # test card back and the counter resumes counting from its # pre-reset value (RESET does NOT zero counters — see HDMI_CLR # below for that). sudo devmem2 0x40000020 w # note current FRAME_COUNT sudo devmem2 0x40000010 w 0x01 # assert RESET — monitor goes black sleep 1 sudo devmem2 0x40000020 w # FRAME_COUNT frozen sudo devmem2 0x40000010 w 0x00 # release RESET — monitor repaints sudo devmem2 0x40000020 w # FRAME_COUNT resumes counting up # Clear the diagnostic counters via CORE_PULSE.HDMI_CLR (bit 3). sudo devmem2 0x40000014 w 0x08 sudo devmem2 0x40000020 w # FRAME_COUNT — back to 0 (or 1-2 since it's already ticking) sudo devmem2 0x40000024 w # DMA_DONE_COUNT — 0 sudo devmem2 0x40000028 w # RASTER_OVERFLOW_COUNT — 0 sudo devmem2 0x40000014 w # CORE_PULSE always reads 0 (self-clearing) ``` The CORE_ID read is the "is this actually the ps2 core" handshake; mismatch means either the wrong RBF loaded or the bridge isn't mapped. The frame counter advancing between two reads (at a real ~60 frames per second once Ch174 is loaded) proves the PCRTC is producing frames even if you can't see the HDMI monitor. ### Ch219 — consolidated status block (`ps2_status.sh`) For runtime logs / automation, the operator helper [`ps2_status.sh`](ps2_status.sh) reads every bridge register in one go and prints a decoded one-screen status block. Copy it to the board alongside the RBF and run it after `core_loader.sh load` to confirm the fabric is healthy: ```bash scp docs/hardware/ps2_status.sh terasic@de25:/home/terasic/ ssh terasic@de25 ./ps2_status.sh # one-shot ssh terasic@de25 ./ps2_status.sh --delta # snapshot + 500ms counter Δ ``` Expected output on a healthy boot: ``` retroDE_ps2 core status [snapshot] @ ================================================ CORE_ID : 0x50533200 "PS2\0" ✓ ps2 fabric loaded ABI_VERSION : 0x00000100 (v1.0) ✓ retroDE ABI v1.0 CORE_CAPS : 0x00000000 (no caps advertised) CORE_STATUS : 0x0000001F [0] loaded : 1 [1] core_halt : 1 (lit = EE bootlet complete) [2] dma_done_seen : 1 [3] frame_seen : 1 (lit = PCRTC delivered ≥1 frame) [4] hdmi_init_done : 1 (lit = ADV7513 LUT walk complete) [5] hdmi_i2c_error : 0 ✓ no I²C NACKs [6] raster_overflow : 0 ✓ raster healthy CORE_CTRL : 0x00000000 [0] reset : 0 [1] rom_loaded : 0 [2] pause : 0 Counters: FRAME_COUNT : 12345 (advances at ~60Hz once PCRTC is alive) DMA_DONE_COUNT : 1 RASTER_OVERFLOW_COUNT : 0 Counter Δ over 500 ms: FRAME_COUNT : 12345 → 12375 Δ=30 (≈ 30 if PCRTC is alive) DMA_DONE_COUNT : 1 → 1 Δ=0 RASTER_OVERFLOW_COUNT : 0 → 0 Δ=0 ``` The script's exit status is **0** when CORE_ID matches `0x50533200` *and* `hdmi_i2c_error` is clear — suitable for `&&` chaining in deployment scripts or CI smoke tests. It uses `busybox devmem` internally to side-step the devmem2 access-size quirk on `0x?4`-suffixed offsets (see the caveat below). Override the bridge base via env if it's not at the default: ```bash PS2_BRIDGE_BASE=0x40000000 DEVMEM='busybox devmem' ./ps2_status.sh ``` **Note on devmem2 + `0x?4`-suffixed offsets:** on Linux with this particular HPS-side mapping, register reads at offsets ending in `0x4` (ABI_VERSION @ 0x40000004, DMA_DONE_COUNT @ 0x40000024) throw "Bus error" under `devmem2`. Confirmed quirk in the devmem2 access-size handling, not a bridge defect — the bridge itself happily decodes `araddr[3:2]=01` lane reads in simulation, and the matching `0x?0` / `0x?8` offsets read cleanly on the same hardware. Helpers like `busybox devmem` or a small `ps2_regs` reader bypass this; for now the runbook reads avoid the affected offsets. ### Ch220 — retrodesd integration Ch219 covers the standalone `core_loader.sh` + `ps2_status.sh` path. The "production" path is retrodesd (`retroDE_splash/software/retrodesd.c`), the userspace supervisor that owns fabric load, identity validation, the supervisor OSD menu, and core-to-core switching. The manifest stanza in the "Programming" section above (line 156) is the integration point; this sub-section verifies what's sufficient and what isn't. #### `backend=splash` — what's sufficient, what's not With the stanza as written (`backend=splash`, `core_id=0x50533200`, `min_abi=0x00000100`), retrodesd will: 1. `fabric_load()` the ps2 RBF. 2. `bridge_wait_ready(2000)` — poll `CORE_STATUS[0] = loaded` (= `CORE_STATUS_READY` in `input_common.h`) for up to 2 seconds. The Ch173 bridge asserts this bit immediately on fabric power-up, so the wait returns within one poll. 3. `bridge_read_identity()` + `bridge_validate(0x50533200, 0x00000100)` — reject the load if CORE_ID or ABI don't match the stanza. 4. Call `splash_backend.start()`, which writes OSD `CFG0/CFG1/ CTRL` at offsets `0x100/0x104/0x108/0x110/0x114` and the tile RAM at `0x1000+`. **Steps 1–3 are sufficient for Ch220 bringup** — the ps2 fabric exposes CORE_ID + ABI + the Ch174 counters, retrodesd will accept the load, HDMI output + the Ch171 quadrant test card will appear, and `ps2_status.sh` continues to work over the same bridge. **Step 4 is a silent no-op on the current ps2 fabric.** The `ps2_hps_bridge` slave only decodes offsets `0x000–0x028` (see the table above) — writes to `0x100+` fall outside the decoded range and are dropped on the bridge side. retrodesd never errors (`BRESP=OKAY` is returned on AXI4-Lite for any in-window write). Consequences: - The "RetroDE / Core Select…" splash OSD does not render over the ps2 quadrant image. - The supervisor menu (B-button overlay during gameplay) cannot be drawn on top of ps2 video. Core-switching itself still works, but the operator path is: switch *back to the splash core first*, then pick a different core from the supervisor there. Adding the OSD canvas to the ps2 fabric (the `0x100+` register family plus tile RAM + an overlay path) is a separate effort and **not** a Ch220 prerequisite. When it lands, this stanza needs no change — `splash_backend` will just start working over ps2 video automatically. No `retroDE_splash` patch is required for Ch220. retrodesd already (a) logs CORE_ID/ABI/STATUS/CAPS for every backend, (b) validates against the manifest, and (c) accepts `backend=splash` on any ABI v1.0 core. The "splash works for everything that exposes the common ABI prefix" contract was the whole point of hoisting the identity registers in Ch173. #### Expected retrodesd log lines On a healthy boot of the ps2 core via retrodesd (taken straight from `retrodesd.c` + `bridge_common.h` + `splash_backend.c`): ``` retrodesd: loaded N cores from /home/terasic/cores/manifest.cfg retrodesd: loading core 'ps2' (PS2 Graphics Demo) retrodesd: bridge identity: CORE_ID: 0x50533200 ABI_VERSION: 0x00000100 CORE_STATUS: 0x0000001F # see Ch219 decode; may print earlier CORE_CAPS: 0x00000000 splash: session initialized splash: core started (OSD force-open, supervisor-first) retrodesd: service thread started ``` `CORE_STATUS` is timing-dependent — bits 1..3 (`core_halt`, `dma_done_seen`, `frame_seen`) usually latch within a few ms of fabric load and are typically set by the time retrodesd logs the identity, but bit 4 (`hdmi_init_done`) takes ~125 ms for the I²C LUT walk and may still be 0 when this line prints. Re-read with `ps2_status.sh` after ≥ 200 ms to see the steady-state value (`0x0000001F`). Failure-mode log lines to recognize: | Line | Cause | |----------------------------------------------------------------------------|----------------------------------------------------------------| | `bridge_validate: CORE_ID mismatch: expected 0x50533200, got 0x00000000` | RBF did not load (fabric still at reset) | | `bridge_validate: CORE_ID mismatch: expected 0x50533200, got 0x????????` | Wrong RBF loaded — check `rbf=` path in stanza | | `bridge_validate: ABI_VERSION too old: need >= 0x00000100, got 0x????????` | ps2 RBF predates the Ch173 ABI v1.0 bridge | | `bridge_wait_ready: timeout after 2000 ms` | Fabric load partially failed; `loaded` bit never asserted | | `retrodesd: core 'ps2' not found, using first: '...'` | Stanza missing from manifest; check `[ps2]` section header | `journalctl -u retrodesd -f` captures all of the above during `core_loader.sh load` *or* during a live core switch. #### Known-good operator checklist After adding the stanza to `/home/terasic/cores/manifest.cfg` and restarting `retrodesd.service` (or rebooting), a healthy ps2 bringup ticks every box below: | # | Check | How to verify | |----|--------------------------------------------------------|----------------------------------------------------------------| | 1 | Fabric load returned, SSH session survives | SSH prompt responds; no `dmesg` AXI/SMMU errors | | 2 | retrodesd selected the ps2 core | `journalctl -u retrodesd \| grep "loading core 'ps2'"` | | 3 | Identity matches manifest | Journal shows `CORE_ID: 0x50533200`, `ABI_VERSION: 0x00000100` | | 4 | HDMI locks 640×480 @ ~60 Hz | Monitor reports VGA timing, no "no signal" overlay | | 5 | Quadrant test card visible | RED TL, GREEN TR, BLUE BL, WHITE BR | | 6 | `ps2_status.sh` exits 0 | `ssh terasic@de25 ./ps2_status.sh && echo OK` | | 7 | `FRAME_COUNT` increments at ~60 Hz | `./ps2_status.sh --delta` shows Δ ≈ 30 over 500 ms | | 8 | `RASTER_OVERFLOW_COUNT` stays at 0 | `./ps2_status.sh --delta` shows Δ = 0 | | 9 | `hdmi_i2c_error` stays clear | `CORE_STATUS[5] = 0` (LED[4] dark, exit status 0) | | 10 | No spurious DMA activity | `DMA_DONE_COUNT` increments once on boot, then stays put | A run that ticks all ten boxes is the canonical "Ch220 known-good". If 1–3 fail, the manifest/stanza is wrong; if 4–5 fail, suspect the ADV7513 path (Ch219 triage applies); if 6–10 fail, the bridge or the ps2 design state machines are off — `ps2_status.sh` is designed to isolate which. ### Ch221 — input/OSD ABI reconnaissance Ch220 established that `backend=splash` is sufficient for fabric load + identity. Ch221 surveys what the other retroDE cores (NES, GB, A2600, CoCo2) actually decode in their bridges and maps the gap to future implementation chapters — **no RTL this chapter**, just the table that gives the next code chapter a clean target. #### What retrodesd userspace writes/reads From `retroDE_splash/software/{retrodesd.c, splash_backend.c, osd_input.c, bridge_common.h, input_common.h}` and the per-core backends: | Offset block | Direction | Producer | Purpose | |-----------------|-----------|-----------------------------------------|-----------------------------------------------------------| | 0x000–0x00F | R | `bridge_read_identity` | CORE_ID / ABI_VERSION / CORE_STATUS / CORE_CAPS | | 0x010 | RW | `bridge_set_reset` / `…rom_loaded` | CORE_CTRL: RESET / ROM_LOADED / PAUSE | | 0x014 | W | `core_pulse` (`HDMI_CLR`, etc.) | CORE_PULSE: self-clearing pulses | | 0x040 / 0x044 | W | `input_thread.c` (gamepad → bridge) | INPUT_P1 / INPUT_P2 — remapped joypad bits for the core | | 0x048 | W | `input_thread.c` (raw mirror) | INPUT_P1_RAW — un-remapped buttons for OSD navigation | | 0x060 / 0x064 | R | (per-core, optional) | VIDEO_STATUS / HDMI_DIAG | | 0x0F0 / 0x0F4 | R | `ds2_poll_thread.c` | DS2 wired controller state | | 0x100 | RW | `splash_backend.c` (start) | OSD_CTRL: ENABLE / FORCE_OPEN / INPUT_LOCK | | 0x104 | R (poll) | `osd_input.c` | OSD_STATUS: [0]=active, [12:8]=cursor_row | | 0x108 | R + W1C | `retrodesd.c` (`trig & ACTION` path) | OSD_TRIGGER: action / back / scroll pending | | 0x110 / 0x114 | W | `splash_backend.c` / `osd_setup` | OSD_CFG0 / OSD_CFG1: layout + colors | | 0x1000–0x1FFF | W | `osd_draw_*` (supervisor + backend) | Tile RAM: 2048 × 16-bit cells, packed 2/word | | 0x100000+ | W | per-core ROM load | ROM staging window (cart, BIOS, OS — not used by ps2 yet) | #### What sibling bridges actually decode The four sibling cores converge on the same map (cross-checked against `retroDE_{nes,gb,Atari2600,coco2}/rtl/.../*_hps_bridge.sv`): | Offset block | NES | GB | A2600 | CoCo2 | Notes | |-----------------|-----|-----|-------|-------|----------------------------------------------------------------| | 0x000–0x01F | ✓ | ✓ | ✓ | ✓ | Identity + CORE_CTRL/PULSE (shared ABI v1.0 prefix) | | 0x020–0x03F | ✓ | ✓ | ✓ | ✓ | Core-specific config (mapper flags, GB cfg, CONSOLE_ACTION, …) | | 0x040–0x04F | ✓ | ✓ | ✓ | ✓ | INPUT_P1 / INPUT_P2 / INPUT_P1_RAW latches | | 0x060–0x07F | ✓ | ✓ | ✓ | ✓ | Video/HDMI diagnostics (read-only) | | 0x080–0x09F | ✓ | ✓ † | — | ✓ ‡ | Save status (NES/GB/CoCo2); † GB also serial capture | | 0x0F0 / 0x0F4 | ✓ | ✓ | ✓ | ✓ | DS2 wired controller (read-only platform regs) | | 0x100–0x11F | ✓ | ✓ | ✓ | ✓ | OSD_CTRL / STATUS / TRIGGER / CFG0 / CFG1 | | 0x200–0x21F | ✓ | — | ✓ | — | Savestate (NES=FIFO transport, A2600=BRAM transport) | | 0x300–0x33F | ✓ | — | — | ✓ | Diagnostics (read-only); CoCo2 surfaces live keyboard scan | | 0x1000–0x1FFF | ✓ | ✓ | ✓ | ✓ | Tile RAM (2048 × 16-bit, packed 2/word) | | 0x100000+ | ✓ | ✓ | ✓ | ✓ | ROM staging window | #### What ps2_hps_bridge decodes today `ps2_hps_bridge.sv` accepts AXI4 writes/reads across its full window (write FSM returns `BRESP=OKAY` for any address, read FSM returns `RRESP=OKAY` with `rdata=0` for any unmapped address). Side-effect side-channels are narrow: | Offset block | Behavior on ps2 today | |-----------------|--------------------------------------------------------------------------------| | 0x000–0x00F | Read-back: CORE_ID=`0x50533200`, ABI=`0x00000100`, STATUS live, CAPS=0 | | 0x010 (CTRL) | Read-back live (Ch176); writes latch RESET/ROM_LOADED/PAUSE bits | | 0x014 (PULSE) | Reads 0; writes pulse HDMI_CLR (bit 3); other bits acked-and-ignored | | 0x020–0x028 | Read-back: FRAME_COUNT / DMA_DONE_COUNT / RASTER_OVERFLOW_COUNT | | 0x02C–0x07F | Reads 0; writes acked-and-discarded (`write_in_window` covers the full 128 B) | | 0x080–0x07FFFFF | Reads 0; writes acked-and-discarded (outside the side-effect window) | **Bridge behavior consequence**: every retrodesd userspace write to OSD registers, tile RAM, or input registers already succeeds on the AXI side — nothing crashes. The cost is only behavioral: INPUT_P1/P2 latches don't reach a CPU yet, OSD_STATUS reads always-0 so retrodesd never sees a pending OSD action, and tile RAM writes evaporate. Ch220's "OSD writes are a silent no-op" is a property of the *write-decode map*, not an AXI failure. #### Proposed bridge expansion (Ch222+) These are the next decode-side steps, ordered by which one unblocks the most runtime functionality per chapter. **No RTL this chapter** — Ch221 is just the target spec. | Future Ch | Offset block | Add what | Unblocks | |-----------|----------------------|-----------------------------------------------------------------|---------------------------------------------------------------------------------------------------| | Ch222 | 0x040 / 0x044 / 0x048 | INPUT_P1 / INPUT_P2 / INPUT_P1_RAW write latches (read-back too) | retrodesd's `input_thread` writes land in addressable regs; later wire to SIO2/DualShock | | Ch223 | 0x100 / 0x110 / 0x114 | OSD_CTRL / OSD_CFG0 / OSD_CFG1 latches (compatibility sink) | `splash_backend.start()` writes land in real regs; readable for software introspection | | Ch224 | 0x104 / 0x108 | OSD_STATUS (always-0 today) + OSD_TRIGGER (W1C, all-zero source) | retrodesd's OSD-poll loop stops being a no-op contract; future Ch can drive bits from a real FSM | | Ch225 | 0x060 / 0x064 | VIDEO_STATUS (HDMI locked, pixel-clock present) read-only | retrodesd journalctl can confirm HDMI lock without `ps2_status.sh` | | Ch226 | 0x0F0 / 0x0F4 | DS2_STATUS / DS2_BUTTONS read-only (returns 0 until SIO2 lands) | DS2 poll thread stops reading garbage; future Ch can route into the PS2 SIO2 emulation | | Ch227+ | 0x1000–0x1FFF | Tile RAM (BRAM-backed, sink-only at first) | OSD canvas reaches the bridge; rendering path (overlay onto PCRTC scanout) is a separate chapter | The Ch222–Ch224 group is the **compatibility sink** Codex flagged in the Ch221 framing. Implementing all three together is ~30 lines of decode + ~6 32-bit registers + ~3 read-mux arms — small enough to land in one chapter if scope allows. **Out of scope for now**: tile RAM (4 KB BRAM + overlay path is its own chapter), savestate (no PS2 savestate format yet), ROM staging at 0x100000+ (BIOS load is currently TB-side via `+BIOS=` plusarg; on-board staging is post-MVP). #### Boundary call The honest interface contract after Ch221 is: > **retroDE runtime writes the full ABI v1.0 surface; ps2_hps_bridge > currently decodes 0x000–0x028 with side effects and silently > accepts the rest; the next implementation chapter (Ch222) adds > INPUT_P1/P2 latches, then OSD_CTRL/CFG (Ch223), then OSD_STATUS/ > TRIGGER (Ch224). Tile RAM and ROM staging are deferred.** That gives the next code chapter a 30-line target instead of a guess. ### Ch222 — input-latch block (landed) `ps2_hps_bridge` now decodes the shared-ABI input block at 0x040/0x044/0x048. Each is a 32-bit HPS-visible write/read latch; reset clears unconditionally; reads outside this trio in the input window still return 0 (e.g. 0x04C). > **⚠ Readback is NOT controller functionality.** The PS2 design > has no SIO2 / DualShock pipeline yet, and **nothing on the PS2 > side consumes these registers** — they are pure HPS-visible > latches. Reading INPUT_P1 back gets you the value retrodesd > wrote there, *not* a confirmation that the PS2 core observed > the gamepad state. Wiring this latch to a future SIO2 emulator > is a separate chapter; until then, treat these registers as > a documented compatibility surface for retrodesd's > `input_thread`, nothing more. Write contract: the bridge accepts AXI writes as full 32-bit words at the lane selected by `awaddr[3:2]`; `wstrb` is ignored. Partial-byte writes are not supported (this matches the sibling cores and the way retroDE userspace actually writes — native 32-bit stores from the bridge mmap). Coverage lives in [`sim/tb/platform/tb_ps2_hps_bridge.sv`](../../sim/tb/platform/tb_ps2_hps_bridge.sv) §11: round-trips with three distinct patterns (`0xDEADBEEF` / `0xCAFEBABE` / `0x12345678`), independent-write checks (writing one doesn't touch the other two), a tight-decode check that writes to 0x04C don't bleed into the adjacent INPUT_P1_RAW latch (and that 0x04C itself still reads 0), confirmation that CTRL/STATUS/PULSE/counter registers are unchanged across the input writes, and a final reset that re-clears all three to 0 (other regs already covered by §1–§10). ### Ch223 — OSD compatibility sink (landed) `ps2_hps_bridge` now decodes the shared-ABI OSD register block at 0x100 / 0x110 / 0x114. Each is a 32-bit HPS-visible write/read latch; reset clears unconditionally; reads at the unmapped slots inside the OSD window (0x104 / 0x108 / 0x10C / 0x118 / 0x11C) return 0, and writes there don't bleed into the mapped latches. > **⚠ Readback is NOT overlay functionality.** The PS2 design > has no OSD canvas / overlay engine / tile RAM yet, and > **nothing on the PS2 side consumes these registers** — they > are pure HPS-visible latches. Reading OSD_CTRL back gets you > whatever the splash backend wrote, *not* a confirmation that > a "RetroDE / Core Select…" overlay reached the HDMI output. > The supervisor menu still cannot render over PS2 video > (Ch220's known limitation). What Ch223 *does* fix is the > side-effect map: retrodesd's `splash_backend.start()` writes > now land in real registers instead of being silently dropped > at the bridge. Coverage lives in [`sim/tb/platform/tb_ps2_hps_bridge.sv`](../../sim/tb/platform/tb_ps2_hps_bridge.sv) §12: reset readback, three round-trips with distinct patterns (`0xA5A5A5A5` / `0x11223344` / `0x55667788`), independent-write checks across the trio, tight-decode validation that writes to 0x104 / 0x108 / 0x10C / 0x118 / 0x11C all read 0 and don't modify the CTRL/CFG0/CFG1 latches, an HDMI_CLR pulse to absorb the post-§11-reset CDC sync-refill artifact, then full input-latch + CTRL + STATUS + PULSE + counter readbacks confirming Ch223 writes don't disturb prior chapters, and a final reset that clears all three OSD latches to 0. Updated boundary call: > **retroDE runtime writes the full ABI v1.0 surface; > ps2_hps_bridge decodes 0x000–0x028 + 0x040–0x048 + 0x100 + > 0x110 + 0x114 with side effects and silently accepts the > rest; next implementation chapter is Ch224 (OSD_STATUS @ > 0x104, OSD_TRIGGER @ 0x108 with W1C semantics, plus the FSM > source that drives them).** ### Ch224 — OSD_STATUS / OSD_TRIGGER contracts (landed) Ch224 formalizes the remaining OSD handshake offsets — 0x104, 0x108, 0x10C — as named ABI contracts rather than "reserved placeholders". No new register state: the addresses behave the same as in Ch223 (reads return 0, writes accepted-and-ignored), but `reg_read()` now explicitly enumerates them and the header documents the W1C-sink semantics. | Offset | Contract | |--------|-------------------------------------------------------------------------------------------| | 0x104 | **OSD_STATUS** — always-zero source. No FSM drives `[0]=active` / `[12:8]=cursor_row` | | 0x108 | **OSD_TRIGGER** — W1C-shape sink. retrodesd's poll loop sees `trig=0` and never fires | | 0x10C | **OSD_INPUT** — reserved. Sibling cores expose a debug-override; PS2 keeps it dormant | > **⚠ The W1C semantics here are degenerate** because there's no > FSM source on the PS2 side: bits never go HIGH from internal > state, so any write-to-clear pattern (broad `0xFFFFFFFF`, > single-bit `OSD_TRIG_ACTION|row<<8`, or `OSD_TRIG_BACK`) is a > no-op against an already-zero register. This is sufficient > for retrodesd's poll loop — it reads `trig=0`, never invokes > `osd_action`, and stays out of the way. Once a real overlay > FSM lands, the W1C clear-mask logic and the FSM SET path will > need an actual register; for now the sink contract is the > documented Ch224 surface. Coverage lives in [`sim/tb/platform/tb_ps2_hps_bridge.sv`](../../sim/tb/platform/tb_ps2_hps_bridge.sv) §13: OSD_CTRL/CFG0/CFG1 are first written to known non-zero values; then OSD_STATUS is poked with 0xFFFFFFFF and 0x1 (`active` bit shape), OSD_TRIGGER is poked with three W1C shapes (broad `0xFFFFFFFF`, `ACTION|row<<8`, `BACK`), and OSD_INPUT is poked with 0xDEADBEEF. After every poke, all three target slots still read 0 and OSD_CTRL/CFG0/CFG1 are unchanged. §7 also gains an audit sentinel at 0x120 (first byte past the OSD window) to prove the `addr[37:5] == 33'h08` decode boundary is tight. Updated boundary call: > **retroDE runtime writes the full ABI v1.0 surface; > ps2_hps_bridge decodes 0x000–0x028 + 0x040–0x048 + 0x100 + > 0x110 + 0x114 with side effects and explicitly contracts > 0x104 / 0x108 / 0x10C as zero-source / W1C-sink / reserved; > writes outside the decoded set are accepted-and-ignored. > The compatibility-sink trilogy Codex flagged in Ch221 is > complete. Remaining work is Ch225+ (VIDEO_STATUS reads, DS2 > stub, tile RAM + overlay).** ### Ch225 — VIDEO_STATUS / HDMI_DIAG read surface (landed) After the input + OSD write compatibility, the next most useful runtime surface is read-only video/HDMI diagnostics. Ch225 adds two pure-read registers at the shared-ABI offsets retrodesd already polls on other cores: | Offset | Register | Bit layout | |--------|---------------|--------------------------------------------------------------------------------------------------------| | 0x060 | VIDEO_STATUS | `[0]=frame_seen` · `[1]=scanout_alive (FRAME_COUNT!=0)` · `[2]=raster_overflow (live)` · `[3]=dma_done_seen` · `[31:4]` reserved | | 0x064 | HDMI_DIAG | `[0]=hdmi_init_done` · `[1]=hdmi_i2c_error` · `[31:2]` reserved | Both registers are pure-read views into the synchronized status signals the bridge already tracks (the same sources that drive CORE_STATUS bits 1..6); writes are accepted with BRESP=OKAY and ignored. `scanout_alive` is the only derived bit — it's the live `FRAME_COUNT != 32'd0` comparison, so HDMI_CLR drops it back to 0 and the next `frame_toggle` edge re-raises it. This gives retrodesd or `journalctl`-driven operator tooling a one-line "is the display path alive?" answer without depending on LEDs or `ps2_status.sh`. > **Note:** Video timing fields (width/height/mode/pixel-clock > ID) are deliberately deferred. Sibling-ABI doesn't require > them and the 640×480 fixed mode is documented in the LED > ledger and the Ch219 status block. When a multi-mode design > lands, those fields belong in the `[31:4]` reserved slots > here. Coverage lives in [`sim/tb/platform/tb_ps2_hps_bridge.sv`](../../sim/tb/platform/tb_ps2_hps_bridge.sv) §14: entry-state read confirms VIDEO_STATUS = 0x0B and HDMI_DIAG = 0x03 (frame_seen + scanout_alive + dma_done; init + err); HDMI_CLR drops `scanout_alive` to 0 → 0x09; a single `frame_toggle` flip re-raises it → 0x0B; pulsing `raster_overflow` HIGH/LOW exercises bit [2]'s live tracking (0x0B → 0x0F → 0x0B); broad write patterns (`0xFFFFFFFF`, `0x00000000`, `0xDEADBEEF`) at 0x060 and 0x064 confirm the read-only contract; Ch222 input latches + Ch223 OSD_CTRL/CFG0/ CFG1 remain unchanged across the Ch225 writes. Updated boundary call: > **ps2_hps_bridge now decodes 0x000–0x028 + 0x040–0x048 + > 0x060 / 0x064 + 0x100/0x110/0x114 with side effects, and > explicitly contracts 0x104 / 0x108 / 0x10C as zero-source / > W1C-sink / reserved. Compatibility-sink trilogy plus the > diagnostic read surface (Ch222–Ch225) are complete. Remaining > ABI v1.0 surfaces are Ch226 (DS2 stub at 0x0F0/0x0F4) and > Ch227+ (tile RAM + overlay path — the only large RTL item > left in the shared map).** ### Ch226 — DS2 wired-controller stub (landed) `ps2_hps_bridge` now exposes the shared-ABI DS2 platform offsets at 0x0F0 / 0x0F4 as a read-only stub. PS2 has no physical DS2 wired-controller path today (DE25-Nano routes the DS2 port elsewhere on other cores), so the stub honors the sibling-ABI contract retrodesd's `ds2_poll_thread.c` consumes: | Offset | Register | Layout (sibling-ABI compatible) | |--------|--------------|------------------------------------------------------------------------------------------------------------------| | 0x0F0 | DS2_STATUS | `[0]=connected=0` (no platform path) · `[1]=error=0` · `[2]=input_latches_valid=1` (PS2-local) · `[31:3]=0` → `0x00000004` | | 0x0F4 | DS2_BUTTONS | Read-only **mirror of INPUT_P1** (`0x040`). Tracks Ch222 latch updates in real time | > **⚠ Sibling-ABI deviation note.** Codex's Ch226 framing > originally proposed `[1]=input_latches_valid`. That conflicts > with the shared `[1]=error` bit consumed by retrodesd's poll > thread (`error = (status >> 1) & 0x1`) — returning 1 there > would cause retrodesd to call `osd_input_disconnect_ds2()` > every cycle. Following Codex's own recon-then-document hedge > ("If sibling precedent exists, follow it"), the PS2-local > diagnostic was moved to bit `[2]`, keeping `[0]/[1]` aligned > with the sibling contract. The result: retrodesd sees a > sibling-shaped "no DS2 connected" answer and bows out of the > DS2 path cleanly, while operator tooling can still read > `[2]` for the PS2-local "latches present" hint. DS2_BUTTONS mirrors `INPUT_P1` so HPS can confirm a Ch222 store landed by reading at either 0x040 or 0x0F4 — useful when diagnosing the controller-state path before any SIO2 wiring exists. Note this is read-only: writes to 0x0F4 do **not** modify INPUT_P1. Coverage lives in [`sim/tb/platform/tb_ps2_hps_bridge.sv`](../../sim/tb/platform/tb_ps2_hps_bridge.sv) §15: DS2_STATUS reset/entry = `0x00000004`; INPUT_P1 → DS2_BUTTONS mirror with two distinct write patterns; verification that INPUT_P2 / INPUT_P1_RAW writes do NOT touch DS2_BUTTONS (mirror is INPUT_P1-only); broad write patterns at 0x0F0 / 0x0F4 confirm read-only contract — critically including a check that writing 0x0F4 does NOT modify the underlying INPUT_P1 latch; Ch222–Ch224 state unchanged across Ch226 writes; reset clears INPUT_P1 → DS2_BUTTONS reads 0 while DS2_STATUS stays constant at `0x4`. §7 also picks up audit sentinels at 0x0E0 (just before DS2) and 0x0F8 (the CoCo2-specific DS2_ANALOG slot, unmapped on PS2) to confirm the Ch226-widened 256-byte window's decode is tight. **RTL note:** Ch226 widened the first decode window from 128 B (`addr[37:7]=='0`) to 256 B (`addr[37:8]=='0`) and switched the case selector from `addr[6:2]` (5-bit) to `addr[7:2]` (6-bit). Existing slot indices (e.g. CORE_CTRL @ 6'h04 = 0x010) keep their values since the new high bit is implicitly 0 for offsets ≤ 0x07F. The OSD window at 0x100–0x11F continues to use its own `addr[37:5]==33'h08` decode in the `else if` branch. Updated boundary call: > **ps2_hps_bridge now decodes 0x000–0x028 + 0x040–0x048 + > 0x060 / 0x064 + 0x0F0 / 0x0F4 + 0x100 / 0x110 / 0x114 with > side effects (or read-only data); explicitly contracts > 0x104 / 0x108 / 0x10C as zero-source / W1C-sink / reserved. > The entire ABI v1.0 register-shape surface that sibling > cores expose is now present on PS2, either functionally or > as a stub. The only major remaining shared-ABI surface is > Ch227+ (tile RAM at 0x1000–0x1FFF + the overlay rendering > path), which is a separate RTL/PCRTC effort, not a bridge > decode chapter.** ### Ch227 — tile RAM storage surface (landed) Per Codex's "split it in two stages" framing, Ch227 implements **only the tile RAM ABI/storage surface** — no overlay rendering, no PCRTC composition. The 4 KB window at 0x1000–0x1FFF becomes a plain 1024 × 32-bit RAM owned by the bridge; HPS writes land in the memory, reads return the last value written. Ch228 (a future chapter) will wire this storage into a PCRTC overlay composition path. **Storage shape choice.** Sibling bridges (NES / A2600 / CoCo2) do NOT own their tile RAM — they forward writes to an external overlay engine via `tile_wr_addr[10:0] / tile_wr_data[15:0] / tile_wr_en` output ports, splitting each 32-bit AXI write into two 16-bit cell writes (matching `input_common.h`'s "2048 × 16-bit cells packed 2/word" software view). PS2 has no overlay engine yet, so Ch227 instead owns the storage internally as a flat 1024 × 32-bit memory. Software still sees the same bytewise contents — at the bus level it's just transparent write-then-readback. The 16-bit-cell software view is a software-side convention preserved by `osd_putchar` etc., unaffected by whether the bridge backs it as 16-bit cells or 32-bit words. **Reset semantics.** No sibling precedent for "tile RAM clears on reset" (siblings don't own the storage), so Ch227 makes it **retained** across warm reset. Sim deterministically pre-zeros the memory in an `initial` block; on hardware the power-up value is undefined until the overlay chapter pins a reset contract (BRAM on Agilex 5 doesn't naturally sync-reset every word; we'll let Ch228 decide whether a write-1-to-clear or an explicit clear-pulse register is the right fit). **Decode shape.** Uses a third `else if` branch in `reg_read()` (beside the 0x000–0x0FF and 0x100–0x11F branches) guarded by `addr[37:12] == 26'h1`; word index = `addr[11:2]`. WSTRB is ignored (full-word writes only, same contract as the rest of the bridge). The two existing decode windows (Ch222–Ch226 shared-ABI prefix, Ch223 OSD sink) are unchanged. Coverage lives in [`sim/tb/platform/tb_ps2_hps_bridge.sv`](../../sim/tb/platform/tb_ps2_hps_bridge.sv) §16: reset reads at the base (0x1000), middle (0x1800), and last word (0x1FFC) all return 0; round-trip with three distinct patterns (`0xCAFEF00D` / `0x12345678` / `0xDEADBEEF`); independent-slot checks (each write hit only its target); adjacent-slot tightness (slots at +4 / -4 from each probed target remain 0); **boundary sentinels at 0x0FFC and 0x2000** read 0, and writes to those boundary addresses don't bleed into the nearest tile slots; existing CTRL/STATUS/INPUT/OSD/DS2 regs are unchanged across the Ch227 writes; and a reset-retention check writes the three patterns, pulses `reset_n`, and verifies all three values survive (proving the "retained across warm reset" contract). Updated boundary call: > **ps2_hps_bridge ABI v1.0 register decode is now complete: > 0x000–0x028 + 0x040–0x048 + 0x060 / 0x064 + 0x0F0 / 0x0F4 + > 0x100 / 0x110 / 0x114 with side effects; 0x104 / 0x108 / > 0x10C as zero-source / W1C-sink / reserved; 0x1000–0x1FFF as > 4 KB tile-RAM storage. The remaining piece of the shared ABI > v1.0 surface is *behavior*, not decode: Ch228 wires tile RAM > into a PCRTC overlay composition path so the supervisor menu > can actually render over PS2 video. Beyond that, retroDE_ps2 > is feature-parity with the sibling cores on bridge ABI shape.** ### Ch228 — overlay engine skeleton (landed) Per Codex's "split it in two stages" guidance, Ch228 is the **first behavior chapter** past the ABI shape work, and stays intentionally narrow: it builds the video-domain overlay compositor + a built-in test-pattern source, but **does not yet consume the Ch227 tile RAM**. The bridge↔video CDC question is deferred to Ch229. **New module:** [`rtl/platform/osd_overlay_stub.sv`](../../rtl/platform/osd_overlay_stub.sv). Inputs: 8-bit RGB + DE + HS + VS at `design_clk`. Outputs: same shape, with the overlay composited over the incoming pixels when the region check fires. Sync signals (DE/HS/VS) pass through unchanged. The composite priority is **OSD over PS2 video** — a pixel inside the 160×48 top-left region replaces the PS2 RGB; elsewhere the input flows through. **Test pattern.** 8×8 black/white checker via `x_cnt[3] ^ y_cnt[3]`. Picked because every (x, y) inside the box has a deterministic expected color (the TB samples seven specific positions including the four corners of the box), and the boundary at x=160 / y=48 is visually distinct from any PCRTC-generated PS2 pixel. **Enable is parameter-only this chapter.** `OSD_ENABLE_DEFAULT` is a compile-time bit. The TB instantiates two DUTs — one with `OSD_ENABLE_DEFAULT=1'b1`, one with `=1'b0` — to exercise both sides of the muxing decision. Wiring a real, safely-synchronized `OSD_CTRL[0]` from the bridge clock into design_clk is its own decision (the bridge writes `OSD_CTRL` on `clk` per Ch223; the overlay reads on `design_clk`) and lands with Ch229 alongside the tile-RAM CDC. **x / y derivation from sync signals.** `x_cnt` increments each clock during active video (`in_de=1`) and clamps to 0 during blanking; `y_cnt` resets on the vsync de-assert edge (end of sync pulse → start of vertical back porch) and increments on each DE falling edge. With this scheme `x_cnt == hcnt` during every active line and `y_cnt == active-line index within frame` starting from 0 — which makes region detection a simple range compare on signed (or, here, unsigned-wide-enough) counters. > **⚠ This chapter does not connect to the bridge.** The > `ps2_hps_bridge.tile_mem` Ch227 storage is untouched; the > bridge runs on the bridge clock and `osd_overlay_stub` lives > in `design_clk`. Crossing those two domains safely — dual-port > RAM, snapshot buffer, or a write-FIFO with a domain-crossing > read window — is a Ch229 design decision. Ch228's job is only > to prove the video-side composition path works: z-order, RGB > muxing, scanout timing, and "supervisor menu *can* physically > appear over PS2 video" with no entanglement of CDC. **Top-level integration deferred.** The board top [`de25_nano_psmct32_raster_demo_top.sv`](../../rtl/top/de25_nano_psmct32_raster_demo_top.sv) is **not** modified this chapter — the overlay module exists as a standalone unit verified by its own TB. Wiring it in between the inner wrapper's `VIDEO_R/G/B/DE/HS/VS` and `HDMI_TX_*` naturally pairs with Ch229's CDC decision (since "do we use the test pattern or the real tile RAM?" only has a meaningful answer once tile RAM is reachable from `design_clk`). **TB** [`sim/tb/platform/tb_osd_overlay_stub.sv`](../../sim/tb/platform/tb_osd_overlay_stub.sv) (new, registered in both Makefile lists per the regression contract). Drives a tiny VGA-shape raster (200×60 visible, 8-cycle HSYNC, 2-line VSYNC) directly into two parallel DUT instances. Verifies: 1. DE / HS / VS pass through both DUTs unchanged every cycle. 2. With overlay disabled, every active pixel matches the PS2 background (sampled at four corners + the would-be OSD center). 3. With overlay enabled, eight points across the OSD region match the checker pattern (`x[3]^y[3]`), including the tight-boundary samples at (159, 0), (0, 47), and (159, 47). 4. The region boundary is tight: (160, 0) and (0, 48) — just past the region — return PS2 background. 5. Across a second consecutive frame, samples at (0, 0) and (8, 0) still match the checker pattern, proving y_cnt correctly resets on the vsync de-assert edge of every frame. Updated boundary call: > **Ch228 lands the video-domain overlay engine skeleton + > test-pattern source as a standalone module + focused TB. The > compatibility-sink + diagnostic + DS2 + tile-RAM-storage > chapters (Ch222–Ch227) gave HPS-visible state; Ch228 adds > the first piece of design-side behavior — z-order and RGB > muxing on the video-clock domain. Remaining: Ch229 ties the > two together — bridge tile RAM read port + CDC into > `design_clk` + board-top integration so HPS-written glyphs > can finally render over PS2 video.** ### Ch229 — HPS-written tiles render on video (landed) Ch229 closes the loop: bridge tile RAM writes from HPS now reach the `osd_overlay_stub` through a toggle-based CDC into the design clock domain, where a 1024×32-bit **shadow RAM** mirrors the bridge-side `tile_mem`. The overlay reads the shadow at pixel rate, draws an 8x8 checker block where a tile word is non-zero, and stays transparent where it is zero. The board top is rewired so this composition path sits between `u_demo` and both `VIDEO_*` (test inspection) and `HDMI_TX_*` (HDMI pin out). #### Pipeline ``` HPS (mmap'd /dev/mem at 0x40001000+) │ axi_write32(0x1000 + idx*4, tile_word) ▼ ps2_hps_bridge (CLOCK2_50 domain) ├─ tile_mem[idx] <= wdata_lane // Ch227 AXI-readback storage ├─ tile_wr_index <= aw_addr_q[11:2] // Ch229 broadcast ├─ tile_wr_data <= wdata_lane └─ tile_wr_toggle <= ~tile_wr_toggle // event edge │ ▼ (across clk domains) tile_ram_cdc (design_clk domain) ├─ 2-FF synchronizer on bclk_wr_toggle ├─ wr_pulse = toggle_sync[2] ^ toggle_sync[1] // 1-cycle pulse ├─ shadow_mem[bclk_wr_index] <= bclk_wr_data // gated by pulse └─ dclk_rd_data = shadow_mem[dclk_rd_index] // combinational read │ ▼ osd_overlay_stub (design_clk domain) ├─ tile_x = x_cnt[8:3], tile_y = y_cnt[6:3] ├─ tile_rd_index = {tile_y[3:0], tile_x[5:0]} // tile_y*64 + tile_x ├─ tile_opaque = (tile_rd_data != 0) ├─ osd_pixel_on = x_cnt[3] ^ y_cnt[3] // 8x8 checker └─ draw_osd = in_de && in_osd_region && tile_opaque │ ▼ (between u_demo and VIDEO_*/HDMI_TX_*) VIDEO_R/G/B (top-level test pins) + HDMI_TX_D (ADV7513) ``` #### CDC contract (read this before touching the CDC) The bridge updates `tile_wr_toggle`, `tile_wr_index`, and `tile_wr_data` on the same `clk` (CLOCK2_50) edge per AXI write. `tile_ram_cdc` runs a 3-stage shift register on the toggle, detects edges via XOR of stages [2] and [1], and uses that edge as a 1-cycle write enable on the design-domain shadow RAM. By the time the edge fires, `bclk_wr_index` and `bclk_wr_data` have been stable for ≥ 2 dclk cycles — more than enough at the production clock ratio (50 MHz bridge, 25 MHz design = 2:1) and any reasonable HPS write rate. The contract is "writes must be spaced ≥ 6 dclk cycles apart" — well above retrodesd's ~1 kHz update rate (the slowest dclk is ~25 MHz = 40 ns, so 6 dclk = 240 ns ≪ 1 ms). If a future chapter wants a fast-cycling write source (e.g. streaming animation from the design side), it must replace this toggle CDC with an async FIFO. Documented in [`rtl/platform/tile_ram_cdc.sv`](../../rtl/platform/tile_ram_cdc.sv) header. #### Reset behavior Both domains share the FPGA configure reset path. Bridge `tile_wr_toggle` clears to 0 on `reset_n` deasserted; the receiver's `toggle_sync` clears to 0 on `dreset_n` deasserted. When both go through reset together — the normal case — no spurious post-reset edge fires. The bridge-side `tile_mem` and the design-side `shadow_mem` retain their contents across warm reset; sim's `initial` blocks zero both for deterministic testing; hardware power-up is undefined for both until a future chapter pins a contract (e.g. by adding a "clear tile RAM" pulse register or a power-on bring-up sequence in retrodesd). #### Top-level wiring [`rtl/top/de25_nano_psmct32_raster_demo_top.sv`](../../rtl/top/de25_nano_psmct32_raster_demo_top.sv) gains: 1. Internal wires `demo_video_r/g/b/de/hsync/vsync` for `u_demo`'s raw scanout (renamed from direct VIDEO_* connections). 2. `u_tile_cdc` instance bridging CLOCK2_50 → design_clk. 3. `u_osd_overlay` instance between `demo_video_*` and the `VIDEO_*` top-level outputs. The HDMI_TX_* path picks up the overlay composite for free (it was already wired to `VIDEO_*`). 4. `ps2_hps_bridge` instantiation extended with the new `tile_wr_toggle/index/data` ports inside `USE_QSYS_TOP`; safe tie-offs (`toggle=0`, `index=0`, `data=0`) for the sim path that lacks the qsys instantiation. The overlay uses `OSD_ENABLE_DEFAULT=1'b1` in the top instance — the overlay path is *active*, but transparent until HPS writes a non-zero tile (production startup behavior is identical to pre-Ch229 because both `tile_mem` and `shadow_mem` start at zero). Ch230 will replace the parameter with a properly-synchronized `OSD_CTRL[0]` bit so retrodesd can gate the overlay explicitly. #### Coverage Two TBs now exercise the new path: | TB | Scope | |-----------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [`sim/tb/platform/tb_tile_ram_cdc.sv`](../../sim/tb/platform/tb_tile_ram_cdc.sv) (new) | Two distinct clocks (100 MHz bclk, 33 MHz dclk). Single-write propagation at base / mid / end; multi-write same-index "last wins"; eight distinct-index writes; static-toggle-no-write check (writes are edge-triggered, not level-triggered). | | [`sim/tb/platform/tb_osd_overlay_stub.sv`](../../sim/tb/platform/tb_osd_overlay_stub.sv) (updated for Ch229 behavior) | Mock shadow RAM driven by TB. All-zero tiles → enabled DUT shows PS2 video (Ch229 default). Setting tile (0, 0) lights only that 8x8 block. Setting tile (1, 0) lights the next block; (0, 0) still holds. Clearing tile (0, 0) makes it transparent again while (1, 0) stays. Tight boundary at x=160/y=48 stays transparent regardless of tile data. Frame re-sync confirms y_cnt resets correctly on the next vsync. Disabled DUT ignores tile content entirely. | Top-level [`tb_de25_nano_psmct32_raster_demo_top.sv`](../../sim/tb/top/tb_de25_nano_psmct32_raster_demo_top.sv) still passes with the overlay wired in — VIDEO_DE is the only signal it samples directly, and the overlay forwards DE unchanged. The full 149-baseline regression bumps to 151 PASS with both new TBs registered in the Makefile (per-target rule + `run:` master list + build-aggregation list, per the two-lists contract). Updated boundary call: > **The Ch229 close-the-loop chain is live: HPS writes tile > RAM → bridge broadcasts the write → toggle-CDC propagates > into design_clk → shadow RAM stores → overlay reads + draws > over PS2 video. Production behavior is unchanged at startup > (all tiles zero → overlay transparent), and the path is > ready for retrodesd to start sending real OSD glyphs. > Remaining work: Ch230 — synchronize `OSD_CTRL[0]` from > bridge clk → design_clk so HPS can gate the overlay > explicitly, plus Ch231+ font / glyph rendering work for > "real menu" visuals.** #### Audit follow-ups (per Codex Ch229 audit) **Tile-write rate watchdog (sim-only).** Codex flagged that the CDC contract ("writes ≥ 6 dclk apart") was documented but not enforced in RTL — a sufficiently fast back-to-back AXI write could merge two toggle edges into one and silently lose a write. The actual safe minimum is **≥ 3 dclk** between consecutive receiver `wr_pulse` events (1 dclk for sync chain settling + 1 dclk for the first pulse to fire + 1 dclk of jitter margin). A `\`ifndef SYNTHESIS`-guarded watchdog in [`tile_ram_cdc.sv`](../../rtl/platform/tile_ram_cdc.sv) tracks the dclk gap between successive `wr_pulse` events and prints a one-line warning if the gap is below threshold. Real-world retrodesd OSD updates at ≤ 1 kHz are millions of dclk apart at 25 MHz — many orders of magnitude of slack. **Hardware visibility (an AXI-readable diagnostic counter that retrodesd could poll to detect lost-write events) is deferred to Ch230**; this turn just makes pre-silicon violations loud in the sim log. **Reset-retention asymmetry caveat.** The Ch227 bridge `tile_mem` and the Ch229 design-domain `shadow_mem` are *both* retained across warm reset (no clear logic, sim `initial` zero-fills for determinism, hardware power-up undefined for both). The two RAMs **stay in sync** as long as both go through reset together — the normal case for FPGA configure. The risk Codex flagged is a partial reset path: if the design domain resets independently (e.g., HPS asserts `CORE_CTRL[0]` via Ch176, which routes through the design-side reset chain), the bridge's `tile_mem` survives but the shadow's contents also survive (no clear) — they stay synchronized. The only mismatch scenario is if HPS *clears* tile_mem during a design reset and the design side misses the propagation: then the bridge's view of "this tile is zero" while the shadow still holds the old non-zero value, and the overlay would draw a stale tile until HPS rewrites. Mitigation: retrodesd should treat any core-reset path as "must rewrite the OSD" — the same contract sibling cores honor for tile RAM. A formal **resync strategy** (e.g., a bridge-issued bulk-clear pulse after `core_reset_req` deassert) is post-MVP polish. **Sim regression count math.** 149 (Ch227 baseline) + 1 (`tb_osd_overlay_stub` from Ch228, first new TB) + 1 (`tb_tile_ram_cdc` from Ch229) = **151**. The updated `tb_osd_overlay_stub` (now drives mock tile RAM instead of checker assertions) is the *same* TB target — internal asserts changed, but it still emits one `PASS` line. ✓ ### Ch230 — OSD_CTRL[0] CDC + diagnostic counter (landed) Ch230 finishes the close-the-loop chain by wiring the `OSD_CTRL[0]` enable bit from the bridge clock domain into the design clock domain, and promotes Ch229's sim-only watchdog into a real saturating counter. retrodesd can now turn the overlay on/off through the ABI bit it already writes. #### Pipeline (new pieces in **bold**) ``` HPS axi_write32(0x100, OSD_CTRL_ENABLE | …) │ ▼ ps2_hps_bridge (CLOCK2_50 domain) ├─ osd_ctrl_q[0] // Ch223 store └─ **osd_ctrl_enable = osd_ctrl_q[0]** // Ch230 output │ ▼ (cross-domain) **3-FF synchronizer in the board top (design_clk)** │ ▼ **osd_overlay_stub.enable_i** // Ch230 input │ ▼ draw_osd = in_de && in_osd_region && tile_opaque && (enable_i || OSD_ENABLE_DEFAULT) ``` #### Why a 3-FF synchronizer The OSD_CTRL[0] bit changes at HPS speeds (microseconds at the very fastest). A 3-FF level synchronizer in the destination domain is the standard low-MTBF answer for a single-bit control signal — the first two stages absorb metastability; the third stage gives the consumer a fully-settled value to gate combinational logic with. We're not crossing a multi-bit bus (the tile CDC is the only multi-bit crossing) so no handshake or async-FIFO is needed here. Reset behavior: both `bridge_osd_ctrl_enable` (`osd_ctrl_q[0]`) and the `overlay_enable_sync` shift register clear to 0 on their respective resets. When both reset together (the normal FPGA-configure case), the synced enable starts at 0 and stays there until HPS writes a non-zero value to 0x100. **Production default with Ch230 is overlay OFF**, which differs from Ch229's "always-on, transparent-until-tile-set" parameter default — retrodesd must explicitly assert the bit before its OSD shows. #### Diagnostic counter Ch229's sim-only `$display` watchdog is now backed by a real **16-bit saturating counter** in `tile_ram_cdc`: `tile_wr_too_close_count`. Increments every dclk cycle that `wr_pulse` fires within `MIN_DCLK_GAP=3` cycles of the previous one. Saturates at `0xFFFF` (never wraps). The `$display` line still fires in `\`ifndef SYNTHESIS` for log-grep visibility. For Ch230, the counter is **internal-only** at the top (`tile_wr_too_close_count` is connected to a local wire but not routed to a bridge-readable register). Exposing it for HPS readback requires a reverse CDC (design_clk → CLOCK2_50) plus a new diagnostic register slot — Codex's framing explicitly deferred that to **Ch231+**: "If exposing it to HPS requires a reverse CDC, defer readback to Ch231 and just keep the internal counter plus sim/RTL comment in Ch230." The counter is positioned for that future hookup. #### Top-level wiring [`de25_nano_psmct32_raster_demo_top.sv`](../../rtl/top/de25_nano_psmct32_raster_demo_top.sv) adds: 1. `bridge_osd_ctrl_enable` wire (from `ps2_hps_bridge.osd_ctrl_enable`). 2. `overlay_enable_sync[2:0]` 3-FF shift register on `design_clk`; `overlay_enable = overlay_enable_sync[2]`. 3. `u_osd_overlay` re-instantiated with `.OSD_ENABLE_DEFAULT(1'b0)` and `.enable_i(overlay_enable)` — runtime path replaces the Ch229 parameter-always-on default. 4. `tile_wr_too_close_count[15:0]` wire surfaced from `u_tile_cdc` (currently unconnected sink, ready for Ch231+ reverse CDC). 5. Sim-path tie-off: `assign bridge_osd_ctrl_enable = 1'b0;` alongside the existing Ch229 tile-broadcast tie-offs in the non-`USE_QSYS_TOP` branch. #### Coverage | TB | Scope | |-----------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [`tb_osd_overlay_stub.sv`](../../sim/tb/platform/tb_osd_overlay_stub.sv) (§10 added) | Third DUT with `OSD_ENABLE_DEFAULT=0` + TB-driven `runtime_enable`. Verifies: tiles present + enable=0 → PS2 background; enable=1 → checker lights up; zero tiles stay transparent even when enabled; enable=0 again → back to PS2. Existing §1–§9 still pass with the new enable input wired to constants. | | [`tb_tile_ram_cdc.sv`](../../sim/tb/platform/tb_tile_ram_cdc.sv) (§9 added) | New `too_close_count` output port consumed. Confirms counter stays at 0 across the eight safely-spaced writes in §2–§7, then **deliberately violates** the rate with 10 toggle flips on consecutive bclk edges and verifies the counter is non-zero. A second 20-flip burst proves monotonic non-decreasing (saturation). | | [`tb_ps2_hps_bridge.sv`](../../sim/tb/platform/tb_ps2_hps_bridge.sv) (unused-output declaration added) | Bridge TB gains a `logic osd_ctrl_enable` declaration to satisfy the `.*` wildcard wiring with the new output port. No behavioral changes — the Ch223 OSD_CTRL store and Ch230 surface-output share the same `osd_ctrl_q[0]` source verified by §12. | Top-level [`tb_de25_nano_psmct32_raster_demo_top.sv`](../../sim/tb/top/tb_de25_nano_psmct32_raster_demo_top.sv) still passes — DE forwards through the overlay regardless of enable, and the sim path tie-off ensures `bridge_osd_ctrl_enable=0` so the overlay stays disabled (matching the production default). Updated boundary call: > **retrodesd can now turn the overlay on/off through the real > ABI bit. Ch222–Ch230 collectively gave: full ABI v1.0 register > decode (Ch222–Ch227), diagnostic read surface (Ch225), DS2 > stub (Ch226), tile RAM storage (Ch227), video-domain overlay > engine skeleton (Ch228), HPS-tile-RAM-on-video close-the-loop > (Ch229), and the ABI-bit-driven overlay gate + diagnostic > counter (Ch230). The architecture for "supervisor menu over > PS2 video" is now complete. Remaining work is content > (Ch231+): glyph rendering from real tile data, plus > optional design→bridge reverse CDC for HPS-readable diagnostic > counter exposure.** ### Ch231 — minimal real glyph rendering (landed) The Ch228–Ch230 visual was "8×8 checker block where the tile word is non-zero" — useful for proving the path but not real text. Ch231 swaps the checker for an actual glyph renderer: each tile entry is now interpreted as a 16-bit text cell with char code + foreground + background color, decoded against a built-in 8×8 font ROM, and composited with a 16-color CGA palette. retrodesd writes the same bytes sibling cores already expect, and the visible result is legible text. #### Cell layout (sibling-ABI compatible) The Ch229 PS2-local "one 32-bit cell per tile word" view is replaced by the shared-ABI `input_common.h` layout: **2048 × 16-bit cells, packed 2/word, 64-cell row stride**. ``` Byte address = 0x1000 + row * 128 + col * 2 Word index = (row * 32) + (col >> 1) ← addr[11:2] Cell-within-word = col[0] (0 = low 16 bits, 1 = high 16 bits) ``` The overlay's `tile_rd_index` now computes `{1'b0, tile_row[3:0], tile_col[5:1]}` (= row*32 + col/2); the read returns a 32-bit word from `shadow_mem`; the renderer selects high or low half based on `tile_col[0]`. The bridge side is unchanged — the AXI write decode still stores at `tile_mem[addr[11:2]]` and the Ch229 broadcast still emits `{toggle, addr[11:2], wdata_lane}`. **HPS-side software now writes the same byte offsets sibling cores use** — same `osd_putchar` helper, same `osd_draw_*` calls in `input_common.h`. | Cell bit field | Width | Meaning | |----------------|-------|-----------------------------------------------| | `[7:0]` | 8 | ASCII char code (font ROM index) | | `[11:8]` | 4 | Foreground color (16-color palette index) | | `[15:12]` | 4 | Background color (16-color palette index) | A cell value of `16'd0` is treated as **transparent** (PS2 video passes through), regardless of how those zero bits would otherwise resolve in the palette — this matches the Ch229 "zero = transparent" contract that retrodesd-side code already depends on. #### Font ROM 256 × 8 × 8 (= 16 KiB) of glyph data, indexed by char code, row within cell, and column within cell. Stored as `logic [7:0] font_rom [0:255][0:7]` and zero-filled in an `initial` block; specific glyphs are then populated. The Ch231 subset: - Space `0x20` (zero-filled — renders as solid bg) - Digits `0`–`9` (`0x30..0x39`) - A subset of uppercase: `A`, `B`, `C`, `O` - Punctuation: `.`, `:`, `-`, `/` Other ASCII codes resolve to all-zero rows → the cell shows solid `bg` color, which is the right "missing glyph" fallback. Adding more letters is a mechanical edit to the `initial` block. MSB-left convention: bit 7 of each glyph byte is the **leftmost** pixel of that row. Glyphs are nominal 5×7 inside the 8×8 cell with 1-pixel margin on the right and bottom — same alignment the sibling-ABI font ROMs use. #### 16-color palette CGA-style, matching the `BLACK..WHITE` constants in `input_common.h:346-361`. Implemented as a combinational `palette_lookup(idx)` function returning a 24-bit RGB triple. Fixed at synthesis time; a runtime-configurable palette is post-MVP polish. | idx | name | RGB | idx | name | RGB | |-----|-----------|-------------|-----|-------------|-------------| | 0 | black | `00_00_00` | 8 | dgray | `55_55_55` | | 1 | blue | `00_00_AA` | 9 | lblue | `55_55_FF` | | 2 | green | `00_AA_00` | 10 | lgreen | `55_FF_55` | | 3 | cyan | `00_AA_AA` | 11 | lcyan | `55_FF_FF` | | 4 | red | `AA_00_00` | 12 | lred | `FF_55_55` | | 5 | magenta | `AA_00_AA` | 13 | lmagenta | `FF_55_FF` | | 6 | brown | `AA_55_00` | 14 | yellow | `FF_FF_55` | | 7 | lgray | `AA_AA_AA` | 15 | white | `FF_FF_FF` | #### Pixel decision (rewritten) ```sv draw_osd = in_de && in_osd_region && cell_opaque; pixel_on = font_rom[char_code][y_cnt[2:0]][3'd7 - x_cnt[2:0]]; osd_rgb = pixel_on ? palette_lookup(fg_idx) : palette_lookup(bg_idx); out_{r,g,b} = draw_osd ? osd_rgb : in_{r,g,b}; ``` The Ch229 checker fallback (`x[3] ^ y[3]`) is **removed entirely**. Per Codex's framing — "keep checker fallback out of production path or behind a debug parameter" — Ch231 chose *remove*. The Ch228 standalone-overlay TB and the Ch229 HPS-driven TB both have full coverage of the glyph path; the checker is no longer needed for visual confirmation. #### Coverage `tb_osd_overlay_stub.sv` rewritten for Ch231: | § | Check | |----|------------------------------------------------------------------------------------------------------------------------------| | 1 | Sync passthrough (DE/HS/VS forwarded every cycle, unchanged) | | 2 | Disabled DUT passthrough — PS2 video at 4 sample points | | 3 | All-zero shadow RAM → enabled DUT still shows PS2 video (Ch229 "zero = transparent" preserved) | | 4 | Cell (0, 0) = `' '` on white/blue → entire cell shows solid blue (space glyph is all-zero, cell is still opaque) | | 5 | Cell (1, 0) = `'0'` on white/blue → specific pixels match the '0' glyph mask (rows 0–1 verified at multiple x positions) | | 6 | Adjacent cells (low half vs high half of same word) don't bleed | | 7 | Zero cell (5, 2) → transparent | | 8 | Cell (19, 5) = `'1'` red/black at the bottom-right — verifies the packed-layout indexing at the visible-region boundary | | 9 | Region boundary at x=160 / y=48 stays transparent regardless of cell content | | 10 | Clear cell (0, 0) → block goes back to PS2; cell (1, 0) glyph remains visible | | 11 | Frame-boundary re-sync — same glyph still rendered at `(10, 0)` in the next frame | | 12 | Disabled DUT ignores cell content | | 13 | Ch230 runtime enable: `enable_i=0` → PS2 even with glyphs; `enable_i=1` → glyph rendered; back to PS2 when cleared | `tb_tile_ram_cdc.sv` is unchanged — CDC propagation semantics don't care about how the receiver interprets the bytes, so the Ch229 round-trip tests still validate the path with whatever synthetic patterns the TB drives. Top-level `tb_de25_nano_psmct32_raster_demo_top.sv` still passes: it only checks `VIDEO_DE` (which the overlay forwards unchanged), and the sim-path tie-off keeps the overlay disabled by default. #### What's deferred to Ch232+ Per Codex's framing, Ch231 explicitly excludes: - Proportional fonts / variable-width glyphs. - Full CP437 / Unicode glyph set. - Alpha blending (current cells are fully opaque on nonzero). - Cursor overlay (`OSD_STATUS[12:8]` cursor_row is wired but not consumed by the renderer). - Tile scrolling / window panning. - HPS-readable `tile_wr_too_close_count` (reverse-CDC exposure from design_clk → CLOCK2_50 → AXI register). Updated boundary call: > **The OSD can now draw legible text over PS2 video. Cell > writes from HPS land in the shadow RAM through Ch229's > CDC, get decoded as `{bg, fg, char}` triples, and are > rasterized via a 256-glyph 8×8 font ROM + 16-color CGA > palette. Ch231 ships a small but functional glyph subset; > populating the rest of the font is a mechanical edit. > Remaining work is polish: extended glyph set, cursor, > reverse-CDC counter exposure, optional alpha — none of > which are blockers for retrodesd to render an actual > supervisor menu over PS2 video.** ### Ch232 — hardware bring-up validation (OSD over PS2 video) Ch222–Ch231 built the OSD path entirely in sim. Ch232 validates it on the DE25-Nano: write a known glyph sequence into the Ch229 shadow RAM, assert `OSD_CTRL[0]`, and confirm the text appears in the top-left of the HDMI output, layered over the Ch171 quadrant test card. #### Test helper: [`ps2_osd_test.sh`](ps2_osd_test.sh) Writes the test message `"01234 ABC"` (9 chars at cells (0,0)..(8,0), white on blue) into the bridge tile RAM and asserts `OSD_CTRL[0]=1`. The chars are chosen from the Ch231-populated font subset (digits + space + A/B/C) so the test exercises **real glyph rendering** rather than the "missing glyph → solid bg" fallback. Run modes: | Invocation | Effect | |------------------------------------|-------------------------------------------------------| | `./ps2_osd_test.sh` | Writes the 9 cells + enables the overlay | | `./ps2_osd_test.sh --off` | Disables the overlay (`OSD_CTRL[0]=0`); message stays | | `./ps2_osd_test.sh --clear` | Zeros the 9 cells + leaves the overlay enabled | | `./ps2_osd_test.sh --status` | Dumps `OSD_CTRL` + the first 20 tile-RAM words | Uses `busybox devmem` per the long-standing devmem2 quirk note. Environment overrides: `PS2_BRIDGE_BASE` (default `0x40000000`) and `DEVMEM` (default `busybox devmem`). #### Procedure 1. **Build** the .core.rbf via the normal flow (Quartus GUI or `quartus_sh --flow compile`). Output: `synth/de25_nano/top_psmct32_raster_demo/output_files/*.core.rbf`. 2. **Copy** the RBF + the two helper scripts to the board: ```bash scp output_files/*.core.rbf terasic@de25:/home/terasic/cores/retroDE_ps2.core.rbf scp docs/hardware/ps2_status.sh terasic@de25:/home/terasic/ scp docs/hardware/ps2_osd_test.sh terasic@de25:/home/terasic/ ssh terasic@de25 chmod +x /home/terasic/ps2_*.sh ``` 3. **Load** the core (on the board): ```bash ./core_loader.sh load /home/terasic/cores/retroDE_ps2.core.rbf ``` 4. **Pre-flight** with the Ch219 status block — confirms identity, HDMI lock, and counter health BEFORE touching the OSD: ```bash ./ps2_status.sh --delta ``` Expect: `CORE_ID=0x50533200`, `ABI=0x100`, `[0..4]=1`, `[5]=0` (no I²C error), `[6]=0` (no raster overflow), `FRAME_COUNT Δ ≈ 30`, `RASTER_OVERFLOW Δ = 0`. 5. **Visual** — look at the HDMI monitor: 320×240 quadrant card in the upper-left (RED top-left, GREEN top-right, BLUE bottom-left, WHITE bottom-right). No OSD yet. 6. **Write the OSD test message + enable** the overlay: ```bash ./ps2_osd_test.sh ``` 7. **Observe**: the top-left 160×48 strip of the HDMI image now shows white-on-blue `"01234 ABC"` text. The text overlays the RED quadrant of the test card (which extends from x∈[0,160] in the test card's local space, so the OSD lands inside the red region with text on top). 8. **Toggle off → on**: ```bash ./ps2_osd_test.sh --off # text vanishes, RED quadrant fully visible ./ps2_osd_test.sh # text reappears ``` 9. **Diagnostics still healthy** after the OSD writes: ```bash ./ps2_status.sh --delta ``` Expect the same numbers as in step 4. **No new `hdmi_i2c_error` or `raster_overflow`.** #### Expected screen (ASCII sketch) After step 6, the HDMI image (640×480 visible area) should look approximately like: ``` 0 160 320 640 0 ┌────────┬───────┬─────────────────┐ │WWWWWWWW│ │ │ ← W = white text on │WW WWWW │ │ │ blue bg (OSD) │WW WWWW │ RED │ │ overlays the │WW WWWW │ │ │ RED quadrant │WW WWWW │ │ │ 48 │WWWWWWWW├───────┤ │ │ │ │ │ │ RED │ GREEN │ │ │ │ │ │ │ │ │ │ 240 ├────────┼───────┤ │ │ │ │ │ │ BLUE │ WHITE │ (black, │ │ │ │ unchanged) │ │ │ │ │ 480 └────────┴───────┴─────────────────┘ ``` The OSD strip (top-left 160×48) is approximately the upper-half of the RED quadrant. Letters are 8 pixels wide so the 9-char message takes 72 of the 160 pixels; the remaining 88 pixels on the right are also blue (cells 9..19 are zero → transparent → PS2 RED shows through). Wait, cells 9..19 are unwritten (`16'd0` → transparent). So the right portion of the OSD strip is actually **PS2 RED** (the test card), not blue. The blue region is only the 9×8 = 72-pixel-wide text strip in the corner. That matches the Ch229 transparency contract. A more accurate sketch of just the top-left: ``` 0 72 160 320 0 ┌─────┬──┬────────────┐ │OSD │ │ │ 72px-wide white-on-blue │text │RR│ RED │ "01234 ABC" + remainder 48 ├─────┴──┤ │ of red quadrant │ │ │ │ RED │ │ 240 ├────────┴────────────┤ ``` #### Acceptance criteria | # | Check | How to verify | |----|--------------------------------------------------------------------------------------|-------------------------------------------------------| | 1 | RBF builds without timing failures | `quartus_sh --flow compile` exits 0 | | 2 | Core loads, SSH survives | `core_loader.sh load` returns; SSH prompt responsive | | 3 | Identity matches manifest | `ps2_status.sh` shows `CORE_ID=0x50533200` | | 4 | Quadrant card visible after load | Visual | | 5 | OSD text appears after `./ps2_osd_test.sh` | Visual: white "01234 ABC" in top-left | | 6 | OSD does not occlude the rest of the quadrant card | Visual: GREEN/BLUE/WHITE quadrants unchanged | | 7 | `OSD_CTRL[0]` toggle works | `./ps2_osd_test.sh --off` then `./ps2_osd_test.sh` | | 8 | `FRAME_COUNT` still ticks at ~60 Hz | `./ps2_status.sh --delta` shows Δ ≈ 30 | | 9 | `RASTER_OVERFLOW_COUNT` stays at 0 | `./ps2_status.sh --delta` | | 10 | `hdmi_i2c_error` stays clear | `CORE_STATUS[5] = 0` | | 11 | OSD text is **legible** (specific glyphs visible, not blocks) | Visual: digits 0–4 + A/B/C distinguishable | | 12 | Tile-write CDC counter stays at 0 (no rate violations from operator-pace HPS writes) | Future — needs the Ch232+ reverse-CDC exposure work | All 11 pre-Ch232+ checks should pass on the first try. #### Observed behavior (silicon-validated 2026-05-20) First bench run hit two Quartus-side issues that the sim-only Ch222–Ch231 work didn't surface, then validated cleanly on the third compile attempt: 1. **Missing QSF entries.** `tile_ram_cdc.sv` (Ch229) and `osd_overlay_stub.sv` (Ch228) were registered in the sim Makefile's `RTL_SRCS` but never added to the Quartus project's `.qsf`. Elaboration failed with "undefined entity". Fixed by adding `set_global_assignment -name SYSTEMVERILOG_FILE` lines for both files to [`synth/de25_nano/top_psmct32_raster_demo/de25_nano_psmct32_raster_demo_top.qsf`](../../synth/de25_nano/top_psmct32_raster_demo/de25_nano_psmct32_raster_demo_top.qsf). 2. **BRAM inference failure → LAB budget blow-out.** First successful elaboration hit the Fitter with "requires 6491 LABs / device has 4680". The 2D-unpacked font ROM (`logic [7:0] font_rom [0:255][0:7]`) didn't infer M20K and spilled ~2000 LABs of distributed logic. Two-stage fix: (a) flatten to 1D `logic [7:0] font_rom [0:2047]` indexed by `{char_code, row_in_cell}` — matches the shape of `tile_mem`, generally inferable; (b) add explicit `(* romstyle = "M20K" *)` attribute on `font_rom`, plus `(* ramstyle = "M20K" *)` on `tile_mem` and `tile_ram_cdc.shadow_mem` to defensively force the inferencer's hand on all three memories. 3. **Compile success at attempt 3.** Quartus reports: - ALMs: 32,822 / 46,800 (70 %) - RAM Blocks: 260 / 358 (73 %) - Block memory bits: 4,259,840 / 7,331,840 (58 %) - DSP: 6 / 376 (2 %), HSSI HPS: 1 / 1 (100 %), PLLs: 2 / 11 (18 %) - Timing models: Final 4. **Operator-checklist results (all 11 boxes ✓):** - RBF builds cleanly (third attempt, after fixes above) - Core loads via `core_loader.sh`, SSH session survives - Identity matches manifest (CORE_ID = `0x50533200`, ABI = `0x00000100`) - Quadrant card visible after load - `./ps2_osd_test.sh` brings up the OSD text - Quadrant card is otherwise unchanged (no occlusion bleed outside the 9-character strip) - `OSD_CTRL[0]` toggle works — `--off` removes the OSD, re-invoking re-displays it - `FRAME_COUNT` Δ = 31 over 500 ms (expected ≈ 30; 3 % variance is well within the sample-window margin) - `RASTER_OVERFLOW_COUNT` Δ = 0 across both snapshots - `hdmi_i2c_error` stays clear (`CORE_STATUS[5] = 0`, LED[4] dark) throughout - All glyphs in the `01234 ABC` test message are legible — not missing-glyph blocks The Ch232 acceptance criteria 1–11 from the table above are all confirmed on real silicon. Item 12 (HPS-readable tile-write CDC counter) remains deferred to a future chapter since no rate-violation events occurred on the bench (retrodesd-rate writes are millions of dclk cycles apart). #### Boundary call (post-validation) > **Ch222–Ch232 close the "supervisor menu over PS2 video" > arc.** The full path — HPS userspace writes through the > bridge's tile RAM + OSD_CTRL register → toggle-CDC and 3-FF > sync into design_clk → shadow RAM + synced enable → 16-bit > cell decode + 8×8 glyph render + 16-color palette → > composite over PS2 video → HDMI — is now silicon-validated > on the Agilex 5. retrodesd writes the same bytes it writes > to sibling cores and a supervisor menu can render over PS2 > video. Remaining work is purely content polish (full glyph > set, cursor, palette runtime, reverse-CDC counter > exposure) — none of which are blockers. ### Ch236 — input-path hardware truth + operator visibility Ch222 / Ch226 / Ch234 / Ch235 collectively built the HPS → bridge → IOP-fabric input pipeline. Ch232 silicon-validated the OSD path, **but the input path's silicon coverage stops at the bridge** — the synth top has no IOP core, so the bridge's `input_p1_o` / `input_p2_o` outputs land at unconnected nets in [`de25_nano_psmct32_raster_demo_top.sv`](../../rtl/top/de25_nano_psmct32_raster_demo_top.sv). Ch236 makes this explicit so nobody overreads the milestone, and extends `ps2_status.sh` so an operator can verify the bridge-half of the path on real hardware. #### What works on silicon today (HPS-side half) - `ps2_hps_bridge.INPUT_P1` / `INPUT_P2` / `INPUT_P1_RAW` latches at 0x040 / 0x044 / 0x048 — Ch222, silicon-validated on the DE25-Nano. retrodesd's `input_thread.c` writes here; any process with `/dev/mem` can read or write. - `ps2_hps_bridge.DS2_STATUS` / `DS2_BUTTONS` at 0x0F0 / 0x0F4 — Ch226. DS2_BUTTONS is a real-time mirror of INPUT_P1 (the same register the sibling DS2 poll thread reads). - Ch232's silicon checklist covered Ch222 indirectly through the bridge identity readbacks; Ch236 adds *direct* readback of the input latches to `ps2_status.sh`. #### What does NOT yet work on silicon (PS2-fabric-side half) - `iop_memory_map_stub` + `sio2_input_stub` (Ch234) are **sim-only** for now. The synth top ([`de25_nano_psmct32_raster_demo_top.sv`](../../rtl/top/de25_nano_psmct32_raster_demo_top.sv)) doesn't instantiate the IOP core, so there is no PS2-fabric consumer of the bridge's `input_p1_o` / `input_p2_o` outputs. The wires land at unconnected nets named `bridge_input_p1` / `bridge_input_p2`; Quartus elides them during synthesis as dead logic. - `tb_bridge_iop_pad_input` proves the wiring + CDC + Sony format translation works correctly *in simulation* with both bridge and IOP map instantiated together. That gives high confidence that **when the IOP core lands on the synth top in a future chapter, the bridge → IOP path will light up without further RTL work** — only `.input_p1` / `.input_p2` need to be connected at the integration site. #### `ps2_status.sh` extension The Ch219 status block now includes: ``` Input latches (Ch222) + DS2 mirror (Ch226): INPUT_P1 : 0x00000000 INPUT_P2 : 0x00000000 INPUT_P1_RAW : 0x00000000 DS2_STATUS : 0x00000004 (Ch226 stub: [0]=connected=0, [2]=input_latches_valid=1) DS2_BUTTONS : 0x00000000 ✓ tracks INPUT_P1 → PS2-fabric consumer (Ch234 sio2_input_stub) is sim-only at the moment; non-zero values above mean the bridge latch landed, NOT that PS2 code saw it. ``` The footer explicitly disclaims silicon consumption — non-zero values prove the bridge latch landed, **not** that PS2-fabric code observed them. #### Operator test (input-path silicon smoke) After `core_loader.sh load` brings the ps2 fabric up: 1. Confirm baseline — all input latches read 0: ```bash ./ps2_status.sh # Expect: INPUT_P1 = INPUT_P2 = INPUT_P1_RAW = DS2_BUTTONS = 0x0 ``` 2. Synthetic AXI write — set INPUT_P1 to a known pattern (e.g. `JOY_START | JOY_SELECT | JOY_RIGHT` = `(1<<4) | (1<<5) | (1<<0)` = `0x31`): ```bash sudo busybox devmem 0x40000040 w 0x00000031 ``` 3. Re-read — both the direct latch AND the DS2 mirror reflect the write: ```bash ./ps2_status.sh # Expect: INPUT_P1 = 0x00000031, DS2_BUTTONS = 0x00000031 (✓ tracks) ``` 4. Independent P2 write: ```bash sudo busybox devmem 0x40000044 w 0x000003C0 ./ps2_status.sh # Expect: INPUT_P2 = 0x000003C0, INPUT_P1 unchanged ``` 5. Clear: ```bash sudo busybox devmem 0x40000040 w 0x00000000 sudo busybox devmem 0x40000044 w 0x00000000 ``` 6. (Optional) live retrodesd test — `retrodesd.service` running with the ps2 manifest stanza will write button bitmaps continuously from the connected gamepad. Press a button on the controller and re-run `./ps2_status.sh` — INPUT_P1 should briefly show the corresponding `JOY_*` bit set (active-high SNES-style). The supervisor OSD path doesn't render anything new (Ch222 / Ch226 are pure bridge regs; the OSD path is independent), but the bridge latch is alive. #### Acceptance criteria | # | Check | How to verify | |----|--------------------------------------------------------------------------------------|---------------------------------------------------------------------| | 1 | `ps2_status.sh` shows the new input section | Visual: "Input latches (Ch222) + DS2 mirror (Ch226):" block present | | 2 | All five fields read 0 / 0x4 at baseline | `INPUT_P*` = 0, `DS2_STATUS` = 0x4, `DS2_BUTTONS` = 0 | | 3 | `devmem` write to 0x40000040 round-trips | Re-read shows the written value | | 4 | DS2_BUTTONS mirrors INPUT_P1 in real time | The `✓ tracks INPUT_P1` marker appears | | 5 | INPUT_P2 write is independent of INPUT_P1 | Step 4 doesn't disturb INPUT_P1 | | 6 | The footer disclaimer is visible | "PS2-fabric consumer (Ch234 sio2_input_stub) is sim-only…" | All six are operator-visible on the existing Ch232 build — no new RBF or fabric change is needed. The bridge's input register surface was already on silicon as of Ch222 / Ch226; Ch236 only adds the diagnostic visibility. #### Updated boundary call > **Ch222 / Ch226 input-latch surface is silicon-confirmed > via the Ch236 `ps2_status.sh` extension; HPS↔bridge half of > the input path is alive on real hardware. The bridge → IOP > half (Ch234/Ch235) remains sim-only until a future chapter > instantiates an IOP core on the synth top. That's the next > meaningful RTL bridge to cross for the input arc; in the > meantime, the next code chapter could be SIF/libpad-buffer > stub reconnaissance to define the EE-visible pad-state > path before the IOP-core integration lands.** ### Ch241 — input-arc hardware truth after Ch240 The Ch237–Ch240 work closed the input arc in **simulation**: HPS → bridge latches → IOP-side `sio2_input_stub` → SIF DMA into a fixed EE-RAM buffer → EE-CPU code branches on a button bit. The full path is covered by [`tb_ee_pad_buffer_branch.sv`](../../sim/tb/integration/tb_ee_pad_buffer_branch.sv) across four scenarios including a clear-and-restore case (Ch240's §4). Cross-references: [`docs/contracts/sio2_pad.md`](../contracts/sio2_pad.md) sections Ch234 / Ch238 / Ch239 / Ch240. #### What's silicon today Same as Ch236 — only the **HPS↔bridge half**: - `INPUT_P1` / `INPUT_P2` / `INPUT_P1_RAW` latches at 0x040 / 0x044 / 0x048 land in the bridge on every retrodesd write, observable via `ps2_status.sh`. - `DS2_STATUS` / `DS2_BUTTONS` mirror at 0x0F0 / 0x0F4 tracks `INPUT_P1` in real time (Ch226). - The bridge's new `input_p1_o` / `input_p2_o` outputs (Ch235) exist on silicon and drive `bridge_input_p1` / `bridge_input_p2` wires in the board top — **but those wires terminate at unconnected nets**, because the synth top doesn't instantiate the IOP core that would consume them. Quartus elides them as dead logic. #### What's NOT silicon - `iop_memory_map_stub` (Ch234 `sio2_input_stub` inside) — sim-only. - `sif_dma_ee_ram_bridge_stub` driven by IOP-side software — sim-only. - `ee_memory_map_stub` / `ee_ram_stub` / `bios_rom_stub` / `ee_core_stub` — sim-only. - The Ch239 single-slot `EE_PAD_BUFFER_BASE` buffer is a sim-only RAM location; on real hardware there is no corresponding EE-RAM-resident pad packet because there is no producer or consumer in the fabric. #### Why no new `ps2_status.sh` work this chapter The operator-visible HPS-side surface (`ps2_status.sh`) already shows `INPUT_P1` / `INPUT_P2` / `INPUT_P1_RAW` / `DS2_*` via Ch236. The next data points (PAD_P1_STATE, the fixed pad buffer, the EE-program marker) are **not addressable from HPS userspace on the current hardware top** — they live behind modules that aren't instantiated. Adding read paths to `ps2_status.sh` for those would print zeros or garbage and mislead the operator. Better to wait until the IOP/EE chain lands in a top-level chapter. #### What a "full input path on silicon" chapter looks like A future hardware-integration chapter (likely **Ch300+** given its scope) would need to: 1. Instantiate the IOP map + `sio2_input_stub` in the synth top, fed by the existing `bridge_input_p1` / `bridge_input_p2` wires. 2. Instantiate an IOP execution primitive that drives the producer side — either `iop_core_stub` running a small pad-packing program from BIOS, or a TB-style FSM that composes the existing primitives in hardware. 3. Instantiate the SIF egress bridge (with rewind) writing to an EE-side RAM block on the FPGA fabric. 4. Either (a) instantiate `ee_core_stub` + bios_rom + RAM and run a small EE program that consumes the buffer, or (b) skip EE-side consumption and just expose the sif_dma_ee_ram_bridge's `last_seen_o` + the buffer contents to HPS via a new bridge read port for operator inspection. That's a substantial architecture decision — three RTL modules land on the synth top, plus the IOP/EE program ROM contents need a build-time pipeline. Defers naturally until either a real game/BIOS workflow demands it or a future chapter explicitly chooses to ship the input path on silicon as a demo. #### Updated boundary call > **Input arc is closed in simulation; HPS↔bridge half is > closed on silicon. Closing the bridge→IOP→SIF→EE half on > silicon is a multi-module top-integration chapter (Ch300+ > bracket), not a follow-on of Ch240. Until then, > `ps2_status.sh` keeps reporting only the bridge-side > latches that are physically addressable from HPS, and the > "Ch234 sio2_input_stub is sim-only" disclaimer the script > already prints stays in effect.** ### Ch242 — OSD glyph coverage for retrodesd menu text (landed) Ch231 brought up real glyph rendering with a deliberately tiny seed font (digits `0-9`, four uppercase letters `A B C O`, space, and a handful of punctuation: `: . - /`). That set was enough to prove the renderer wiring but not enough for any real menu text out of `retroDE_splash` to be readable — every other character would draw as the implicit zero-fill glyph (blank), producing boxes-with-holes-where-letters-should-be. Ch242 expands the font ROM in [`rtl/platform/osd_overlay_stub.sv`](../../rtl/platform/osd_overlay_stub.sv) to cover the union of characters actually used by retrodesd's common OSD strings, keeping the existing 5×7-in-8×8 retro style. #### Character set added Audit of `retroDE_splash/software/*.c` for menu/UI strings ("Core Select…", "Load Cart…", "RetroDE Input Test", "Save States", "P1:", "P2:", etc.) produced a unique-character set that, minus what was already in the seed font, breaks down as: | Category | Characters added | |---|---| | Punctuation (7) | `! ( ) + , < >` | | Uppercase letters (22) | `D E F G H I J K L M N P Q R S T U V W X Y Z` | | Lowercase letters (26) | `a b c d e f g h i j k l m n o p q r s t u v w x y z` | Pre-existing (unchanged): `0-9`, `A B C O`, space, `: . - /`. The 5×7 patterns use the same MSB-left byte-per-row convention as Ch231. Lowercase glyphs put x-height body in rows 2–6 and reserve row 7 for descenders (`g j p q y`) and rows 0–1 for ascenders (`b d f h k l t`). Cell format (`{bg[3:0], fg[3:0], char[7:0]}`) is unchanged, palette is unchanged, render path is unchanged — only the font ROM contents grew. #### TB coverage [`sim/tb/platform/tb_osd_overlay_stub.sv`](../../sim/tb/platform/tb_osd_overlay_stub.sv) gains a §14 "Ch242 — representative-string render" section that paints `"Core"` into cells (0..3, 0) with white-on-blue and samples a small set of pixels per glyph to prove: - `C` (0x43, pre-existing) draws its row-0 pattern `00111100` correctly — cols 0 = bg, 2 = fg, 5 = fg. - `o` (0x6F, **new**) row 0 is all-zero (lowercase x-height starts at row 2), row 2 is `00111000` — proving the new glyph rows are read at the right ROM index. - `r` (0x72, **new**) row 2 is `01011000` — col 1 fg, col 0 bg distinguishes `r` from `o`. - `e` (0x65, **new**) row 2 is `00111000`, row 4 is the crossbar `01111100` — verifies multiple rows of the same new glyph. - An unwritten cell `(4, 0)` still shows PS2 video — the Ch229 transparency contract is unaffected by the larger font ROM. Per Codex's Ch242 framing the TB checks a representative string, not every pixel of every glyph; per-pixel correctness of every newly-added letter is a property of the font ROM contents themselves and is reviewable in `osd_overlay_stub.sv` directly. #### Expected operator-visible behavior When retrodesd next renders any menu text through the OSD canvas, characters previously drawn as blanks will now appear as their proper 5×7 glyphs. Mixed-case strings ("Core Select", "Load Cart", "P1:") and the punctuation in parens / commas / slashes used by version strings and timestamps render legibly. Style note: the glyphs deliberately stay inside a 5×7 box within the 8×8 cell so adjacent cells have a one-pixel gutter on the right and bottom — the retro-monitor look the seed font established in Ch231 is preserved at the higher coverage. #### Tile RAM / CDC / bridge unchanged Ch242 is **font-ROM-content-only** — no change to `tile_ram_cdc.sv`, no change to `ps2_hps_bridge.sv` decode, no change to the OSD register family, no change to the overlay engine's renderer logic. Synthesis attributes (`(* romstyle = "M20K" *)` on `font_rom`) carry over from Ch232 so the larger ROM still infers a BRAM and the LAB-budget headroom from Ch232 is preserved (a 2048×8 ROM fits in a single M20K block either way). #### Regression 149 → 155 unchanged (no new TB files; Ch234–Ch240 had already bumped the count). `tb_osd_overlay_stub` errors=0. ### Ch243 — hardware OSD text validation with Ch242 glyph set (landed) Built the .core.rbf with Ch242 font ROM on disk, loaded through the normal retrodesd path, and observed the real supervisor/OSD menu on the monitor. #### What works - **All Ch242 lowercase glyphs render legibly** — `etroDE`, `ores`, `ain`, `raphics`, `em`, `pc` etc. all appear as proper 5×7 shapes. No blank-cell gaps anywhere in visible text. - **Punctuation works** — parens `(` `)` and dash `-` render correctly (visible in `(Main Sy…` and `RetroDE - Co…`). - **Color split honored** — header line yellow, body lines white, matching retrodesd's palette choice. - **PS2 quadrant visible behind overlay** — Ch229 transparency contract intact; zero-cells outside the menu text show the red/green/blue/white quadrant pattern through. - **OSD_CTRL[0] show/hide** — `osd_test --off` cleanly removes the entire overlay, Ch230 path uninfluenced by the larger font ROM. - **No missing glyphs in the strings retrodesd currently renders** — the Ch242 character-set audit covered the actual menu vocabulary; no hunt-and-fill follow-up needed for glyph coverage itself. #### What surfaced: region overflow (not a Ch242 regression) The OSD region is 160×48 px = **20 cells × 6 rows** (see `osd_overlay_stub.sv` geometry constants), and retrodesd's menu strings are wider than 20 cells: | Intended string | Length | What renders on screen | |-------------------------|--------|------------------------| | `RetroDE - Cores` | 15 | `RetroDE - Co` (12 visible — truncated) | | `retroDE (Main System)` | 21 | `retroDE (Main Sy` (16 visible — truncated) | | `PS2 Graphics Demo` | 17 | `PS2 Graphics Dem` (16 visible — truncated) | | `ao486 (x86 pc)` | 14 | `ao486 (x86 pc)` (fits) | Truncation happens at the rightmost cell column (cell_x = 19); the overlay's region clamp drops any character past that cleanly — there is no wrap, no scroll, no horizontal-marquee behavior. The portion of the menu the operator actually sees is in the **upper-left red quadrant** because the OSD region anchors at (0, 0) of active video and the test pattern's red quadrant happens to overlap it. This is **not** a Ch242 regression — the region geometry has been unchanged since Ch228/Ch229 (when it was sized to a "deliberately small" sub-screen overlay to validate the transparency contract). It only became visibly limiting in Ch243 because the font expansion finally made the truncated characters legible enough to notice that they were truncated. #### Boundary call after Ch243 > **Ch242's glyph expansion shipped on silicon, ABI v1.0 OSD > register path proven end-to-end with real retrodesd menu > text, OSD show/hide unaffected. The next menu-usability > gap is geometric (160-px-wide region too narrow for > retrodesd's actual menu vocabulary), not typographic. > That's a separate-chapter decision: expand region vs. > shorten strings vs. horizontal scroll vs. marquee > auto-scroll — each with its own tradeoffs in BRAM area, > overlay opacity over PS2 image, and CDC plumbing.** Candidate Ch244+ directions (deferred for Codex framing): - **Wider OSD region.** Bump from 160 cells to e.g. 256 or 320 — easy to flip in `osd_overlay_stub.sv` constants but costs more tile-RAM BRAM (and the Ch232 LAB-budget headroom should be re-checked at the new size). Trade-off: more PS2 image obscured behind menu. - **Shorter strings in retrodesd.** Cheapest path — abbreviate to ≤ 20 chars/line ("RetroDE — Cores" → "Cores", etc.). No RTL or CDC work. Trade-off: less menu information density, requires `retroDE_splash` source-side changes outside this repo. - **Per-line horizontal scroll on select.** Renders only the highlighted line wider (auto-marquee). New CDC + new control register; substantial scope. - **Cursor / selection highlight** — Codex's backup-Ch243 pick. Doesn't address the truncation, but is the next menu-usability gap after readability. Could land in parallel. ### Ch244 — widen OSD region from 160×48 to 256×64 (landed) Ch243's hardware validation confirmed that the only menu-text ergonomic gap left was geometric: real retrodesd menu strings exceeded the 20-cell-wide region. Ch244 widens the OSD overlay from **160×48 px (20×6 cells)** to **256×64 px (32×8 cells)** so the typical strings fit cleanly. Per Codex's Ch244 framing, scope is region constants + TB boundaries only — no tile-RAM resize, no CDC change, no ABI register touch, no shorter-strings push back into `retroDE_splash`. #### Why these dimensions, not bigger | Aspect | 160×48 (Ch228–Ch243) | 256×64 (Ch244) | Comment | |---|---|---|---| | Cells (cols × rows) | 20 × 6 = 120 | 32 × 8 = 256 | Both fit easily in the 2048-cell ABI window. | | Storage (32-bit words, sibling stride 32/row) | up to row 5 × 32 + 15 = 175 | up to row 7 × 32 + 15 = 239 | Both fit in the 1024-word tile RAM. | | Fraction of typical 640×480 active picture | ~2.5% | ~5.3% | Still leaves majority of PS2 image visible. | | Pre-truncation visible width | 20 chars | 32 chars | Covers "retroDE (Main System)" (21), "PS2 Graphics Demo" (17), "RetroDE - Cores" (15). | Going wider (e.g., 320×80 = 40×10) was considered but rejected: each extra row eats more of the PS2 image, and 32 columns already covers retrodesd's current vocabulary with a few cells of headroom. Cursor/selection highlight (Codex's Ch245 backup) can revisit width later if it turns out to need padding. #### What changed (RTL) - [rtl/platform/osd_overlay_stub.sv](../../rtl/platform/osd_overlay_stub.sv) — `parameter int OSD_W` default bumped from 160 to 256, `OSD_H` from 48 to 64. The `tile_rd_index = {1'b0, tile_row[3:0], tile_col[5:1]}` generation is unchanged — already 10-bit / 1024-word capable since Ch229. Region-clamp inequality math (`x_cnt < OSD_X + OSD_W`, `y_cnt < OSD_Y + OSD_H`) carries the new constants directly. - [rtl/top/de25_nano_psmct32_raster_demo_top.sv](../../rtl/top/de25_nano_psmct32_raster_demo_top.sv) — the silicon top's `osd_overlay_stub` instantiation override updated from `.OSD_W(160), .OSD_H(48)` to `.OSD_W(256), .OSD_H(64)`. Everything else in the top stays the same: `overlay_tile_rd_index` (10-bit), `overlay_tile_rd_data` (32-bit), `overlay_enable` (Ch230 OSD_CTRL[0] sync) all unchanged. #### What did NOT change - Tile RAM size — bridge still owns 1024 × 32-bit (4 KB at 0x1000..0x1FFF). 64-cell sibling-ABI row stride preserved. - `tile_ram_cdc` size + CDC plumbing — 1024-word shadow, same write-event toggle path. - HPS-visible ABI register family (OSD_CTRL/CFG/STATUS/TRIGGER) — identical. - retrodesd software side — already iterates over its source string length; on the wider region it now fills more cells per row instead of truncating at cell_x=19. No `retroDE_splash` changes required. - Font ROM — Ch242 character set untouched. - Renderer (8×8 cells, fg/bg/char fields, transparency) — bit-identical. #### TB updates [sim/tb/platform/tb_osd_overlay_stub.sv](../../sim/tb/platform/tb_osd_overlay_stub.sv) sees: - Active picture sized up from 200×60 to 272×72 — wide enough to put (256, 0) and (0, 64) inside active video for the new outside-of-region checks. - All three DUT instantiations now use `.OSD_W(256), .OSD_H(64)`. - §3 gains a new inside-corner check at (255, 63) — must read BG_R when all tiles zero (transparent). - §8 comment updated — cell (19, 5) is no longer the bottom- right corner, it's now an interior cell. The rendering invariant being asserted is unchanged. - §9 outside-edge checks moved: (160, 0) → (256, 0); (0, 48) → (0, 64). The old edges are now mid-region and tested by §3 / §15 alongside the new behavior. - New §15 "Ch244 corner cell" — writes 'X' (0x58) at cell (31, 7), occupying pixels x∈[248,255] y∈[56,63]. Samples `X` row 0 col 0 (bg), col 1 (fg), row 3 col 3 (fg), and row 7 col 7 (bg) to prove the new corner cell decodes, renders, and respects the row-7 blank convention. Also re-verifies §9-style outside checks at (256, 56) and (248, 64) with an *opaque* corner cell present (the old §9 only verified outside-behavior against zero cells). - Watchdog bumped from 5 ms to 10 ms — the larger frame (288×78 vs 216×66) and additional §15 frame-wrap waits pushed the run from ~4.3 ms to ~7.6 ms. #### Expected operator-visible behavior After loading the new .core.rbf: - The OSD region in the upper-left of the test pattern grows from a small box covering part of the red quadrant to a larger box that **may extend into the green quadrant** (~256 px wide vs ~320 px per quadrant in a 640×480 picture). - Previously-truncated menu strings render fully: - `RetroDE - Cores` (15 chars) — was truncated at "Co", now fully visible - `retroDE (Main System)` (21 chars) — was truncated at "Sy", now fully visible - `PS2 Graphics Demo` (17 chars) — was truncated at "Dem", now fully visible - Menus may use up to 2 more vertical lines (6 → 8 rows) if retrodesd populates them. - OSD show/hide via `osd_test --off` still works (Ch230 path untouched). - PS2 quadrant pattern still visible where the overlay is transparent (Ch229 contract untouched). #### Regression 155 PASS / 0 FAIL / 0 errors. `tb_osd_overlay_stub` PASS at ~7.6 ms sim time. Top-level `tb_de25_nano_psmct32_raster_demo_top` unaffected (it doesn't sample inside the overlay region). ### Ch245 — adopt shared platform OSD (landed) After the Ch243/Ch244 screenshot comparison surfaced that retroDE_ps2 was running a divergent PS2-local OSD stack (custom overlay, hand-coded ASCII font, incompatible cell attribute layout) instead of the shared `retroDE_splash/rtl/platform/` OSD that every working sibling core uses, Ch245 migrates retroDE_ps2 back onto the canonical platform OSD with **no feature expansion**. #### What's referenced now (not copied) Both `retroDE_ps2.qsf` and `sim/Makefile` now reference the platform files. Path note: Quartus resolves QSF file paths relative to the **QSF's own directory** (not the project root); the existing `rtl/...` refs only work because there's a `rtl → ../../../rtl` symlink in the synth tree. So the shared-platform refs use four-up: `../../../../retroDE_splash/rtl/platform/`. The sim Makefile uses an absolute path via the `SPLASH_PLATFORM_RTL` variable, unaffected by the synth-dir nesting. - `osd_overlay.sv` — the real compositor (configurable position/size/scale, CP437 font, per-cell `transp_bg`, cursor-row highlight, 3-cycle pipeline) - `osd_menu_fsm.sv` — Select+Start hold detect + D-pad navigation + A/B action pulses (CLK_FREQ_HZ=50_000_000 to match our sys_clk = CLOCK2_50) - `osd_font_rom.sv` + `cp437_8x8.mem` — 256-glyph CP437 font with line-drawing chars for the menu border The sim Makefile creates a `sim/traces/rtl/rtl/platform` symlink in its `dirs:` target so the platform font ROM's CWD-relative `$readmemh("rtl/platform/cp437_8x8.mem")` succeeds at simulation time. For Quartus synthesis, a companion symlink lives at `rtl/platform/cp437_8x8.mem → ../../../retroDE_splash/rtl/platform/cp437_8x8.mem` in this repo (mirrors how NES does it at `retroDE_nes/rtl/platform/cp437_8x8.mem`). Quartus's CWD at elaboration is the QSF directory, the `rtl/` synth-tree symlink resolves to `retroDE_ps2/rtl/`, and then the per-file symlink takes the `$readmemh` to the real splash file. The QSF's `MIF_FILE` line is a no-op for `$readmemh` but keeps the file in Quartus's project view for programmer/IP flows. #### Top-level wiring ([rtl/top/de25_nano_psmct32_raster_demo_top.sv](../../rtl/top/de25_nano_psmct32_raster_demo_top.sv)) Inserted between the existing demo wrapper and the HDMI pins: 1. **Pixel-coord counter** derived from `demo_video_de` / `demo_video_hsync` / `demo_video_vsync` — feeds the platform overlay's `pixel_x` / `pixel_y` inputs. 2. **Char-BRAM read adapter** — translates the platform's 11-bit `char_rd_addr` `{row[4:0], col[5:0]}` into our existing `tile_ram_cdc` 10-bit 32-bit-word index + low/high half mux on bit 0 of the cell address. Registered once to match the platform's 1-cycle BRAM latency expectation (same as NES at `retroDE_nes.sv:1267`). 3. **CDC stage** — 2-FF synchronizers carry `bridge_osd_cfg0`/`cfg1`, `menu_osd_active`, and `menu_cursor_row` from CLOCK2_50 into design_clk. 4. **Field decode** — sibling-ABI layout: `cols=cfg0[5:0]`, `rows=cfg0[12:8]`, `osd_x=cfg0[23:16]<<4`, `osd_y=cfg0[31:24]<<4`, `cursor_attr=cfg1[23:16]`. 5. **`osd_menu_fsm`** instantiated on CLOCK2_50, fed by `bridge_input_p1` (the joypad bitmap retrodesd writes to `INPUT_P1` at 0x040) and the OSD enable / force-open / force-close bits from OSD_CTRL. 6. **Platform `osd_overlay`** drives `VIDEO_R/G/B/DE/HSYNC/VSYNC` directly. `osd_global_transparent_bg=1'b0` and `osd_scale=3'd2` hardwired per sibling-core convention. Old `osd_overlay_stub` instantiation stays in the top but is unwired from `VIDEO_*` (drives throwaway `stub_out_*` and reads a tied-zero shadow). Module stays in the QSF and Makefile so retroDE_ps2-local TBs keep linking. Ch246 will remove it once on-monitor parity is proven. #### Bridge reshape ([rtl/platform/ps2_hps_bridge.sv](../../rtl/platform/ps2_hps_bridge.sv)) Sibling-ABI semantics added in-place. No address-map changes; existing offsets (0x100/04/08/10/14) are preserved. - New 32-bit outputs `osd_ctrl_o` / `osd_cfg0_o` / `osd_cfg1_o` so the top extracts fields per the sibling-shared layout (matching `nes_hps_bridge.sv`). - New menu-FSM-side inputs: `osd_active_i`, `osd_cursor_row_i`, `osd_set_trigger_i` (A button pulse), `osd_back_trigger_i` (B button pulse), `osd_scroll_down_trigger_i`, `osd_scroll_up_trigger_i`, `osd_open_trigger_i`, `osd_trigger_row_i`. - `OSD_STATUS` (0x104) read returns `{19'd0, osd_cursor_row, 7'd0, osd_active}` exactly like `nes_hps_bridge.sv:1122` instead of the prior always-zero Ch224 sink. - `OSD_TRIGGER` (0x108) becomes a real R/W register: bits set by menu-FSM pulses, cleared by HPS via W1C. Sets win over clears on common bits. - `OSD_CTRL[3]` is a self-clearing "request" bit (matches `nes_hps_bridge.sv:741`). - Bridge clock IS sys_clk here (both ride CLOCK2_50), so the FSM-side pulses are same-domain combinational inputs — no synchronizer needed on the bridge end. - The existing `osd_ctrl_enable` output is kept for back-compat with the stub path until Ch246 cleanup. #### What survived from Ch222..Ch244 - Bridge **register file infrastructure** (decode, AXI lane alignment, write-accept FSM, the OSD-window guard at `aw_addr_q[37:5] == 33'h08`) — still the right shape. - Bridge **tile RAM 0x1000..0x1FFF** + the `tile_wr_*` broadcast — the platform's char BRAM uses a different storage shape but our 32-bit-word writes still land correctly; the Ch245 adapter handles the unpack on read. - `tile_ram_cdc.sv` — kept; the design-domain shadow it produces is still the source for the platform overlay's char reads (after the 32→16 adapter). - All Ch234..Ch241 input-arc work (IOP pad-state, SIF DMA, EE-side buffer + branch program) — orthogonal to OSD, fully intact. #### What was scaffolding (now bypassed in the synth path) - `osd_overlay_stub.sv` — unwired from VIDEO_* but still instantiated for TB linkage. - Hand-coded ASCII font ROM that grew through Ch232/Ch242 — replaced by `cp437_8x8.mem`. - Ch244 region widening — superseded by the platform's runtime-configurable `osd_x/y/cols/rows` (retrodesd writes the position/size at boot). - Custom cell-attribute layout (`{bg[3:0], fg[3:0], char}`) — replaced by sibling-ABI layout (`{transp_bg, bg[2:0], fg[3:0], char}`). #### Verification - New focused TB [`sim/tb/platform/tb_osd_platform_cell_adapter.sv`](../../sim/tb/platform/tb_osd_platform_cell_adapter.sv) exercises the 32→16 char-BRAM read adapter — the unique integration glue we added. Pre-populates the shadow with known cells in low half (col[0]=0), high half (col[0]=1), mid-row, bottom-right corner (col=31, row=7), and reads them back through the adapter mux + 1-cycle register. Also verifies neighbors stay zero. - Sibling-ABI bridge changes verified by the existing `tb_ps2_hps_bridge` focused TB (extended with declarations for the new ports; semantic verification of the new OSD_STATUS/OSD_TRIGGER paths is deferred to Ch246 since the read/write shape isn't on the Ch245 critical path). - Full regression: **156 PASS / 0 FAIL** (155 prior + 1 new adapter TB). The three input-arc integration TBs were patched to declare the new bridge ports (`.*` wildcard bindings now find them). #### Expected operator-visible behavior on hardware - The PS2 core's OSD should now **look identical** to every sibling core's OSD: centered on screen (position controlled by retrodesd via OSD_CFG0), white double-line border, solid blue panel background, cyan/green cursor highlight on the active row, full menu strings rendered with no truncation (overlay can grow up to 63×31 chars), `A: select` action prompt, `(active)` annotation on the loaded core. - Behind the OSD where retrodesd writes `transp_bg=1` cells (typically the title row's background or specific highlight regions), the PS2 video quadrant test pattern shows through. - `osd_test --off` and the Select+Start hold combination should both still hide/show the overlay. - `ps2_status.sh --delta` should report identical INPUT_* values to before. #### What's left for Ch246 - Remove `osd_overlay_stub.sv` instantiation from the top (and its .qsf line; Makefile reference stays only if the focused stub TB stays). - Decide the fate of the focused Ch228..Ch244 TBs targeting the stub — keep as historical regression for the deprecated module, or retire. - Add focused-TB coverage for the new bridge OSD_STATUS / OSD_TRIGGER R/W semantics. - Drop the `osd_ctrl_enable` back-compat output once nothing else reads it. ### Ch246 — CORE_CAPS advertises OSD geometry (landed) After Ch245 brought the shared platform OSD online, the first on-monitor look revealed that retrodesd is configuring the OSD for a 1280×720 active picture (`OSD_CFG0 = 0x0E141068`: cols=40 rows=16 origin=(320, 224) px at 2× scale → menu region spans x∈[320, 960], y∈[224, 480]). The PS2 demo currently outputs 640×480, so the right half of the menu lies past the active picture and clips. retrodesd derives that origin from a hardcoded 1280×720 assumption in its `rom_simple_backend.c`, not from anything the core advertises. Codex's Ch246 framing was explicit: **the actual geometry fix lives in retrodesd's backend, not in this repo**. This chapter contributes the metadata side — advertising the OSD geometry in CORE_CAPS following the sibling-ABI bit layout so a CORE_CAPS-aware retrodesd can use it directly. #### CORE_CAPS bit layout (matches `nes_hps_bridge.sv:1024-1031`) | Bits | Field | PS2 value | Notes | |--------|----------------------|-----------|-------| | 0 | has_save_ram | 0 | PS2 memory card not yet wired | | 1 | has_savestates | 0 | Not yet implemented | | 2 | two_player | 0 | P2 wiring is bringup-only | | 3 | analog_input | 0 | DS2 analog sticks not plumbed | | 7:4 | max_savestate_slots | 0 | — | | 15:8 | osd_columns | 40 | Matches NES advertisement | | 20:16 | osd_rows | 16 | Matches NES advertisement | `CORE_CAPS = (16 << 16) \| (40 << 8) = 0x00102800`. #### Runtime fix lives in retrodesd This advertisement is forward-looking metadata. The actual runtime bug — retrodesd writing `origin=(320, 224)` even when the core can't accommodate it — needs a fix in `retroDE_splash`'s board-side code (probably `rom_simple_backend.c` or a PS2-specific backend), independent of this RTL change. Two clean paths there: 1. **CORE_CAPS-aware OSD config**: read cols/rows from CORE_CAPS bits [20:8] and derive origin from active-picture dimensions retrodesd already knows (the same source it uses for the HDMI mode). 2. **PS2-specific backend** that overrides the `rom_simple_backend` defaults for the PS2 core's current 640×480 picture. Expected OSD_CFG0 for 640×480: - cols=40, rows=16, scale=2 - x_chars = (640 - 40*16) / 32 = 0 - y_chars = (480 - 16*16) / 32 = 7 - → `OSD_CFG0 = 0x07001028` (scale bits in upper byte stay zero since the bridge field isn't used for scale). Neither belongs in retroDE_ps2; both belong in retroDE_splash. #### ps2_status.sh Updated to decode the new CORE_CAPS fields inline: ``` CORE_CAPS : 0x00102800 (osd=40x16 save=0 ss=0 2p=0 analog=0) ``` #### What was NOT done in Ch246 - **OSD origin clamp/override in the FPGA**: Codex's "quick fallback" option of mutating retrodesd's runtime config in the bridge or top was deliberately rejected — it silently hides the upstream bug and breaks the moment retrodesd ships a real fix. - **Active-picture resolution bump**: PS2 emulation will eventually need proper PCRTC scanout dimensions; treating the OSD-clip symptom by widening the demo's video output would just hide that planning. Tracked separately. - **Gamepad input plumbing**: separate Ch247 per Codex. Keyboard nav already works (Ch245 input path proven via `INPUT_P1_RAW`); gamepad failing to update either INPUT_P1 or INPUT_P1_RAW is a retrodesd input-thread issue, not RTL. #### Regression 156 PASS / 0 FAIL. `tb_ps2_hps_bridge` updated to expect `CORE_CAPS = 0x00102800`. ### Ch248 — replace Ch226 DS2 stub with shared platform controller (landed) Ch245 brought the platform OSD online; Ch246 advertised matching geometry metadata; Ch247 fixed the on-monitor placement via a PS2-specific backend in retroDE_splash. The Ch247 on-monitor run exposed the last remaining input-path divergence: pressing the wired DS2 controller didn't navigate the menu, because retroDE_ps2's Ch226 `DS2_STATUS` was a hardcoded stub that always reported "no controller plugged in." retrodesd's `ds2_poll_thread` polls that register at 1 kHz; seeing `bit 0 = 0` it calls `osd_input_disconnect_ds2()` every iteration and the DS2 source never contributes to `INPUT_P1_RAW`. Ch248 replaces the stub with the same `ds2_controller` RTL that NES / Atari2600 / splash use, and routes its outputs through the bridge. #### Files added / changed | File | Change | |---|---| | `synth/de25_nano/top_psmct32_raster_demo/de25_nano_psmct32_raster_demo_top.qsf` | New `SYSTEMVERILOG_FILE` ref to `../../../../retroDE_splash/rtl/platform/ds2_controller.sv` (four-up — see [QSF path resolution memory](../../../.claude/projects/-home-ubuntu-FPGA-Projects-retroDE-ps2/memory/reference_qsf_path_resolution.md)); pin assignments `PIN_H16/Y1/C2/P1` for `GPIO_0_DS2_CLK/CMD/DATA/ATTN` with `3.3-V LVCMOS` I/O and `WEAK_PULL_UP_RESISTOR ON` on DATA; entity is `de25_nano_psmct32_raster_demo_top` | | `sim/Makefile` | Added `$(SPLASH_PLATFORM_RTL)/ds2_controller.sv` to `SHARED_RTL` | | [rtl/top/de25_nano_psmct32_raster_demo_top.sv](../../rtl/top/de25_nano_psmct32_raster_demo_top.sv) | New top ports `GPIO_0_DS2_CLK/CMD/DATA/ATTN`; instantiated `ds2_controller #(.CLK_HZ(50_000_000))` on `CLOCK2_50`; wired `ds2_buttons_w / ds2_connected_w / ds2_error_w` into the bridge's new input ports | | [rtl/platform/ps2_hps_bridge.sv](../../rtl/platform/ps2_hps_bridge.sv) | New input ports `ds2_buttons_i[31:0]`, `ds2_connected_i`, `ds2_error_i`. `DS2_STATUS` (0x0F0) now reads `{29'd0, 1'b1, ds2_error_i, ds2_connected_i}`. `DS2_BUTTONS` (0x0F4) now reads `ds2_buttons_i` — the Ch226 `INPUT_P1` mirror is gone (it was actively blocking the real shared-runtime path) | | [sim/tb/platform/tb_ps2_hps_bridge.sv](../../sim/tb/platform/tb_ps2_hps_bridge.sv) | §15 rewritten: drives `ds2_buttons_i/connected_i/error_i` from the TB and verifies live readback for plugged/error/unplugged states. The "DS2_BUTTONS mirrors INPUT_P1" tests were inverted — they now confirm INPUT_P1/P2/P1_RAW writes do **NOT** disturb DS2_BUTTONS | | Three integration TBs using `.*` wildcard binding | Added `ds2_buttons_i/connected_i/error_i = 0` declarations so the wildcard finds them | | [docs/hardware/ps2_status.sh](ps2_status.sh) | Removed "expect 0x00000004" comment; now decodes `[0]=connected`, `[1]=error`, and prints a "plug a controller in" hint when bit 0 is clear | #### DS2_STATUS bit layout (new) | Bit | Field | Source | |---|---|---| | 0 | connected | `ds2_controller.ds2_connected` | | 1 | error | `ds2_controller.ds2_error` | | 2 | reserved | hardcoded `1` (PS2-local legacy bit retained for operator-tool compatibility) | | 31:3 | reserved | `0` | #### DS2_BUTTONS `32-bit live readback from ds2_controller.ds2_buttons`. The Ch226 mirror of INPUT_P1 is removed — that was useful as a stub "proof-of-bridge-write-landing" but blocked the real path. INPUT_P1 remains the HPS-written output of retrodesd's input normalization; DS2_BUTTONS is now the raw wired-pad readback that `ds2_poll_thread` consumes upstream of that. #### Acceptance criteria (Codex Ch248 framing) | Criterion | Result | |---|---| | Controller unplugged: `DS2_STATUS[0]=0`, no runtime regression | ✓ TB §15 verifies; default state | | Controller plugged: `DS2_STATUS[0]=1`, `DS2_STATUS[1]=0` | ✓ TB §15 verifies (`@connected` check) | | Pressing D-pad/A/B changes `DS2_BUTTONS` | ✓ TB §15 verifies via `ds2_buttons_i` updates; hardware proof on next compile | | retrodesd writes INPUT_P1_RAW; gamepad navigation works | Hardware verification deferred to user's compile + on-monitor test | | Existing keyboard nav still works | ✓ Unchanged (keyboard path is independent) | | Sim bridge TB updated from Ch226 expectations | ✓ TB §15 fully rewritten | #### Pin map (matches NES / Atari2600 / splash exactly) | Signal | DE25-Nano pin | I/O standard | Notes | |---|---|---|---| | `GPIO_0_DS2_CLK` | PIN_H16 | 3.3-V LVCMOS | output | | `GPIO_0_DS2_CMD` | PIN_Y1 | 3.3-V LVCMOS | output | | `GPIO_0_DS2_DATA` | PIN_C2 | 3.3-V LVCMOS | input, weak pull-up (open-drain controller line) | | `GPIO_0_DS2_ATTN` | PIN_P1 | 3.3-V LVCMOS | output | #### Regression 156 PASS / 0 FAIL. `tb_ps2_hps_bridge` §15 fully covers the new live readback path. Three integration TBs touched only for wildcard binding; their semantics unchanged. #### What's NOT in Ch248 - The Ch234 sio2_input_stub (sim-only IOP-side controller decoder for the PS2 SIF/IOP gameplay path) is independent of the OSD navigation path Ch248 fixes. Both will eventually be relevant when real games run on the EE/IOP, but they handle different problems. - `ds2_analog[31:0]` and the controller's debug outputs are tied to unused wires in the top. retrodesd doesn't read analog yet for PS2; once it does, surfacing them through the bridge would be a small follow-up. ### Ch249 — canonical PS2 plug-in integration (landed) Ch245→Ch248 converged the OSD / DS2 / backend stack onto the shared platform path that every working retroDE core uses. This section is the **canonical state of the integration as of Ch249**; the Ch228–Ch244 sections above are kept as historical record but no longer describe what's on silicon. **Document-reading guide:** anything in this Ch249 section is load-bearing for current silicon. Ch228–Ch244 OSD content (the PS2-local `osd_overlay_stub.sv`, the Ch232/Ch242/Ch244 stub geometry/glyph chapters, the Ch226 hardcoded DS2 stub, etc.) is historical context only — the artifacts those chapters built were retired in Ch249 as scaffolding. #### Stack diagram ``` ┌─────────────────────────────────────────────────────────────────┐ │ HPS (retrodesd) │ │ ├─ ps2_backend.c ← backend=ps2 in manifest │ │ │ └─ writes OSD_CFG0=0x07001028 + CTRL/CFG1 at start() │ │ ├─ input_thread.c ← evdev (keyboard) → osd_input │ │ ├─ ds2_poll_thread.c ← polls DS2_STATUS @ 1 kHz │ │ └─ osd_input.c ← merges all sources, writes │ │ INPUT_P1 / INPUT_P1_RAW │ ├─────────────────────────────────────────────────────────────────┤ │ AXI bridge (`ps2_hps_bridge.sv`) │ │ ├─ 0x040/0x044 INPUT_P1/P2 (HPS write → bridge_input_p1/p2) │ │ ├─ 0x048 INPUT_P1_RAW (HPS write → menu FSM joypad) │ │ ├─ 0x0F0 DS2_STATUS ← {ds2_error, ds2_connected} │ │ ├─ 0x0F4 DS2_BUTTONS ← live ds2_buttons │ │ ├─ 0x100/04/08/10/14 OSD_* (full 32-bit values out) │ │ └─ 0x1000–0x1FFF tile RAM (HPS write → tile_wr_* broadcast)│ ├─────────────────────────────────────────────────────────────────┤ │ Top │ │ ├─ `ds2_controller` ← GPIO_0_DS2_{CLK,CMD,DATA,ATTN} │ │ │ (shared from retroDE_splash, Ch248) │ │ ├─ `tile_ram_cdc` ← bridge tile writes → design-clk shadow │ │ ├─ Ch245 char-BRAM adapter (32→16 cell-select mux on read) │ │ ├─ `osd_menu_fsm` ← bridge_input_p1_raw, OSD_CTRL[0/2/3] │ │ │ (shared, Ch245) │ │ ├─ `osd_font_rom` ← cp437_8x8.mem (shared, Ch245) │ │ └─ `osd_overlay` ← compositor onto demo_video_* │ │ (shared, Ch245; drives HDMI_TX_*) │ └─────────────────────────────────────────────────────────────────┘ ``` #### Sources of truth (where to read instead of re-deriving) | Topic | Canonical file | Notes | |---|---|---| | Bridge register map + DS2 + OSD ports | [`rtl/platform/ps2_hps_bridge.sv`](../../rtl/platform/ps2_hps_bridge.sv) | Ch245 + Ch248 ports; older Ch222–Ch227 + Ch230 history visible in comments | | Top wiring | [`rtl/top/de25_nano_psmct32_raster_demo_top.sv`](../../rtl/top/de25_nano_psmct32_raster_demo_top.sv) | Ch245 platform-OSD instantiation + Ch248 `ds2_controller` instantiation | | PS2 backend (HPS-side) | `retroDE_splash/software/ps2_backend.c` | Ch247; manifest `backend=ps2` | | Pin assignments | [`...top_psmct32_raster_demo_top.qsf`](../../synth/de25_nano/top_psmct32_raster_demo/de25_nano_psmct32_raster_demo_top.qsf) | DS2 GPIO PIN_H16/Y1/C2/P1 with weak pull-up on DATA (Ch248) | | OSD geometry advertisement | `CORE_CAPS = 0x00102800` | Ch246; `[15:8]=40` (cols), `[20:16]=16` (rows) | | Status dump tool | [`docs/hardware/ps2_status.sh`](ps2_status.sh) | Decodes CORE_CAPS, OSD_*, DS2_* live values | #### Operator quick-check (running on the board) ```bash # Snapshot every register the canonical Ch245–Ch248 path touches: sudo ./ps2_status.sh | grep -E "CORE_CAPS|OSD_|DS2_|INPUT_" ``` Healthy state (PS2 backend loaded, DS2 plugged in): ``` CORE_CAPS : 0x00102800 (osd=40x16 save=0 ss=0 2p=0 analog=0) INPUT_P1 : 0x???????? (depends on what retrodesd's just merged) INPUT_P1_RAW: 0x???????? (DS2 + keyboard merged) DS2_STATUS : 0x0000000? ([0]=connected=1, [1]=error=0, [2]=reserved=1) DS2_BUTTONS : 0x???????? (live decoded DS2 bitmap) OSD_CTRL : 0x00000015 (enable + input_lock + force_open when no ROM) OSD_STATUS : 0x00000?01 ([0]=osd_active, [12:8]=cursor_row) OSD_CFG0 : 0x07001028 (cols=40 rows=16 origin=(0,7) chars at 2× scale) OSD_CFG1 : 0x1F3F0303 (first/last_row=3, cursor_attr=0x3F) ``` #### Ch249 cleanup deltas (what changed in this chapter) - **Deleted** `rtl/platform/osd_overlay_stub.sv` and its TB. - **Removed** the stub instantiation + dead `stub_out_*` / `stub_tile_rd_index` wires from the top. - **Retired** the Ch230 single-bit `osd_ctrl_enable` bridge output (its only consumer was the stub's `enable_i`; the platform OSD reads `osd_ctrl_o[0]` out of the Ch245 32-bit register exports). - **Removed** the `bridge_osd_ctrl_enable` wire + 3-FF sync block + sim-path tie-off from the top. - **Updated** QSF: no more `osd_overlay_stub.sv` reference. - **Updated** sim Makefile: dropped `tb_osd_overlay_stub` from per-target rule, `.PHONY` list, and `run:` master list. - **Cleaned** stale comments in `tile_ram_cdc.sv`, the top, and the `tb_osd_platform_cell_adapter.sv` header to point at the Ch245 adapter as the canonical reader. - **Updated** three integration TBs (`tb_bridge_iop_pad_input`, `tb_ee_pad_buffer_branch`, `tb_pad_state_via_sif_to_ee`) to drop the now-removed `osd_ctrl_enable` port from their wildcard bindings. #### Things deliberately kept (NOT scaffolding) - `tile_ram_cdc.sv` — still in the live path. The Ch245 adapter reads from its design-clock shadow. Could in principle be replaced by a true dual-clock dual-port BRAM matching NES's pattern, but the current toggle-CDC shadow is fine and works. - `tb_osd_platform_cell_adapter.sv` — focused TB for the Ch245 32→16 cell-select mux. The unique integration logic that retroDE_ps2 added on top of the platform OSD stack. - The Ch234–Ch241 input arc (sio2_input_stub via SIF DMA into EE-side buffer + branch TB chain) — independent of OSD nav. Lives on in `tb_*_pad_*.sv` as the sim-only IOP→EE input path that the eventual gameplay flow will need; not in the synth top yet. #### Regression 155 PASS / 0 FAIL (was 156 — `tb_osd_overlay_stub` deleted, no coverage loss because `tb_osd_platform_cell_adapter` already covers the live integration glue). ### Ch250 — sio2_input_stub reaches a fabric consumer on silicon (landed) Ch234 built `sio2_input_stub` (the IOP-readable PS2 pad-state register at retroDE-local MMIO 0x1F80_8500); Ch235 wired bridge `INPUT_P1/P2/P1_RAW` outputs out into the top; Ch241 then documented that those wires terminated at unconnected nets that Quartus elided, because the synth top never instantiated any fabric consumer. Ch250 ends that elision with the smallest possible fabric proof: instantiate the stub, tap its Sony-translated 16-bit pad word, and light three LEDs from three chosen bits. Per Codex's Ch250 framing, this is **fabric-consumer proof, not real input architecture** — there's still no IOP execution path, no libpad/SIF on silicon, and no new HPS-visible ABI. The point is: the bitmap that retrodesd writes through the bridge now reaches a live consumer that Quartus retains. #### Files touched | File | Change | |---|---| | [`rtl/iop/sio2_input_stub.sv`](../../rtl/iop/sio2_input_stub.sv) | Added `p1_sony_word_o[15:0]` and `p2_sony_word_o[15:0]` output ports — parallel taps of the existing internal `p1_word`/`p2_word` wires (the same source that feeds the 0x500/0x504 IOP read responses). No functional change to the read map. | | [`rtl/top/de25_nano_psmct32_raster_demo_top.sv`](../../rtl/top/de25_nano_psmct32_raster_demo_top.sv) | Instantiated `sio2_input_stub` with `clk=CLOCK2_50`, `input_p1=bridge_input_p1_raw`, `input_p2=bridge_input_p2`. IOP-side read/write ports tied to zero (no IOP on silicon yet). Replaced the `LED[7:5] = 3'b111` tie-off with three Sony-word taps. Added sim-path tie-offs for `bridge_input_p1/p2/p1_raw` (the `else` branch when qsys isn't instantiated). | | [`.qsf`](../../synth/de25_nano/top_psmct32_raster_demo/de25_nano_psmct32_raster_demo_top.qsf) | New `SYSTEMVERILOG_FILE rtl/iop/sio2_input_stub.sv` entry — pre-Ch250 the file was sim-only via the Makefile. | #### LED → pad-bit mapping Sony wire-format `p1_sony_word` layout (active-LOW, pressed = 0): | Sony bit | Field | retroDE input bit | DE25 LED | |---|---|---|---| | `[3]` | START | retroDE bit 4 (START) | LED[5] | | `[14]` | CROSS (×) | retroDE bit 7 (B-spatial) | LED[6] | | `[4]` | D-pad UP | retroDE bit 3 (UP) | LED[7] | Polarity chain works out by pass-through: Sony bit = 0 when pressed, DE25 LED pin = 0 when lit, so `LED[N] = p1_sony_word[bit]` lights the LED on press without an explicit invert. #### Acceptance (Codex Ch250 framing) | Criterion | Expected on hardware | |---|---| | Keyboard/gamepad through retrodesd changes `INPUT_P1_RAW` | Already confirmed in Ch248 | | `sio2_input_stub` consumes that in fabric | New instantiation; verified by LED ledger | | Holding selected buttons lights LEDs | Hold **START** → LED[5] lights; hold **×** → LED[6]; hold **D-pad UP** → LED[7]. Release → unlit | | Existing OSD/gamepad nav still works | No video-path changes, no OSD-register changes, gamepad → menu nav path unchanged | #### What's NOT in Ch250 - **No IOP execution.** sio2_input_stub's read port is tied to zero — nothing on silicon is exercising the 0x1F80_8500 read response. The Sony-word tap is a parallel observation, not the IOP path. - **No SIF / libpad / EE-side game consumption.** The Ch238–Ch240 sim-only chain stays sim-only. - **No HPS-visible diagnostic register.** Codex explicitly declined Option C ("creates another diagnostic register path when the real destination is eventually IOP/libpad, not HPS"). - **No P2 support.** `p2_sony_word_o` is wired but `bridge_input_p2` is whatever retrodesd writes (currently always 0). Tied to no LEDs. #### Regression 155 PASS / 0 FAIL. `sio2_input_stub` port addition is by-name binding-friendly so existing TBs link without changes; sim-path tie-offs for `bridge_input_p1/p2/p1_raw` were needed because pre-Ch250 those wires went to unconnected nets in the sim top and the X-propagation through the new stub-on-top would have otherwise made `tb_de25_nano_psmct32_raster_demo_top` see X on LED[7:5]. ### Ch251 — animated PS2 demo (color bars + border + heartbeat) (landed) Ch250 closed the input-on-silicon arc; Ch251 polishes the hardware-visible side. The old Ch171 320×240 four-quadrant card was a static one-shot — bootlet wrote four SPRITEs, SYSCALL'd, and the screen never changed. Codex's Ch251 framing replaces it with a richer animated demo that exercises the same GIF/raster path: - **8 vertical color bars** (white / yellow / cyan / green / magenta / red / blue / black), 40 px wide each, full screen height. - **Grey 4-px border** around the 320×240 region. - **Orange 8×8 corner-alignment markers** at all four corners. - **One cyan/red heartbeat SPRITE** (16×16 at the center) whose RGBAQ qword is rewritten by the EE bootlet's main loop, alternating colors. #### Bake.py changes All in [`sim/data/top_psmct32_raster_demo/bake.py`](../../sim/data/top_psmct32_raster_demo/bake.py): - New MIPS opcode encoders: `enc_addiu`, `enc_bne`, `enc_j`, `enc_xor`, `enc_andi`, `enc_lw`, `enc_nop`. - New `build_ch251_sprites()` produces the 17-SPRITE list (8 bars + 4 border + 4 corners + 1 heartbeat). - New `build_ch251_animated_bootlet()` replaces the one-shot `bootlet_for_display1_hi()` for the production fixtures: 1. Initial setup (DISPFB1 / DISPLAY1 / PMODE / DMAC MADR+QWC) — same shape as Ch171. 2. First DMAC kick. 3. Loop forever: - Delay (~17M-iter busy counter ≈ 1 s at 50 MHz → 1 Hz blink). - Poll DMAC `CHCR.start` until clear (safe re-arm). - XOR heartbeat RGBAQ qword at `kseg0 + 0x730` between cyan and red. - Re-arm MADR + QWC + CHCR.start. - Jump back to loop head. The Ch146 legacy 16×8 sim-only path is **unchanged** — it still uses the one-shot SYSCALL bootlet because the smaller TBs (`tb_gs_*_e2e` family) rely on that contract. #### Fixture sizes after Ch251 | File | Active words | Padded to | Notes | |---|---|---|---| | `bios.mem` | 43 | 1024 | Animated bootlet (loops forever) | | `payload.mem` | 102 qwords | 256 qwords | 17 SPRITEs × 6 qwords; heartbeat RGBAQ at byte 0x730 | | `bios_ch146.mem` | 19 | 1024 | One-shot, halts (legacy) | | `payload_ch146.mem` | 24 qwords | 256 qwords | 4 SPRITEs (legacy) | #### TB changes - [`tb_de25_nano_psmct32_raster_demo_top`](../../sim/tb/top/tb_de25_nano_psmct32_raster_demo_top.sv) — removed the `wait (LED[0] == 0)` (would never fire under the loop). Now waits for `LED[1]` (dma_done_seen) and `LED[2]` (frame_seen). New explicit assertion: `LED[0] == 1` (unlit) at end of test, with a pointed error message if it's lit ("core_halt high: animated bootlet must loop, not SYSCALL"). - [`tb_top_psmct32_raster_demo_bram_ch171`](../../sim/tb/top/tb_top_psmct32_raster_demo_bram_ch171.sv) — TB name kept (Makefile entry stable), assertions retargeted at the Ch251 SPRITE layout. New `expected_*_at()` functions encode the bars / border / corners. 10 probe coordinates sample non-heartbeat regions so the assertions are independent of bootlet loop phase. - All other TBs (`tb_top_psmct32_raster_demo`, `tb_top_psmct32_raster_demo_bram`, the `tb_gs_*_e2e` family) consume the **Ch146** legacy fixtures unchanged. #### Acceptance (Codex Ch251 framing) | Criterion | Result | |---|---| | Monitor shows richer pattern + obvious heartbeat | Hardware verification deferred to user's compile | | Shared OSD still overlays correctly | No video-path changes; platform OSD path Ch245-Ch248 intact | | Gamepad OSD nav still works | No bridge / input changes; Ch248 path intact | | `RASTER_OVERFLOW_COUNT = 0` | Existing TB assertion still holds | | Sim covers a few key pixels/states without exploding runtime | 10 probes inside the painted region; ~1.6 s sim time per run | #### LED semantic flip The pre-Ch251 success indicator was "LED[0..3] eventually lit." For Ch251 the success indicator is: - LED[0] **unlit** (EE running paint loop — `core_halt = 0`) - LED[1..3] lit (DMAC done, PCRTC framing, HDMI configured) - Visible 1 Hz heartbeat blink at screen center - FRAME_COUNT advancing in `ps2_status.sh` - RASTER_OVERFLOW_COUNT = 0 If LED[0] is LIT during the demo, the bootlet hit an unexpected SYSCALL (decode mismatch on one of the new opcodes, branch target miscalculation, etc.) — investigate the EE trace. #### Regression 155 PASS / 0 FAIL. Top TB took ~1.6 s sim time (vs ~0.4 s for the old 24-qword payload) due to the 102-qword Ch251 DMAC drain through GS raster. #### Ch251 second addendum — EE → RAM write path was unconnected On-monitor test of the addendum-1 retrodesd change exposed the *actual* reason the heartbeat wasn't blinking: `DMA_DONE_COUNT` was climbing (~2 Hz) but the heartbeat stayed CYAN regardless. Sim reproduction with a runtime probe on `ee_ram_stub.mem[115][31:0]` confirmed it stayed at the initial CYAN payload value across the entire 30 s sim watchdog. Root cause: `top_psmct32_raster_demo_bram.sv` instantiated `ee_ram_stub` with its write port tied to zero (`.wr_en(1'b0), .wr_addr('0), .wr_data(128'd0), .wr_be(16'd0)`) because the pre-Ch251 one-shot bootlet **only read** from EE-RAM (DMAC sourcing the static payload). The corresponding output ports of `ee_memory_map_stub` (`ram_wr_en/addr/data/be/master_id`) were left **unconnected** at the wrapper. The Ch251 looping bootlet's SW writes to `0x8000_0730` (heartbeat RGBAQ) decoded as RAM hits correctly inside `ee_memory_map_stub` but vanished at the wrapper boundary. Fix: wire `ee_memory_map_stub.ram_wr_*` → `ee_ram_stub.wr_*` in the demo wrapper. New local wires `ram_wr_en / ram_wr_addr / ram_wr_data / ram_wr_be / ram_wr_master_id` connect the two. The `ram_master_id` mux now selects writer-id when writing, reader-id when reading. Sim verification (5-sec sim time with delay temporarily shortened to 256 iters): the EE issues SWs to `0x8000_0730` alternating between `0xFF0000FF` (RED) and `0xFFFFFF00` (CYAN), `ee_ram_stub` sees those writes at `wr_addr=0x00000730 wr_be=0x000F`, and `mem[115][31:0]` toggles in lock-step. Delay restored to the production 16M-iter value (~1 Hz at 50 MHz hardware). The TB now spot-checks the initial CYAN state of the heartbeat qword after first DMAC drain; the dynamic blink remains a hardware-monitor proof (the production delay needs >30 s sim time to catch a toggle, not worth the regression cost). Files touched: - [`rtl/top/top_psmct32_raster_demo_bram.sv`](../../rtl/top/top_psmct32_raster_demo_bram.sv) — new `ram_wr_*` wires + `ee_memory_map_stub` → `ee_ram_stub` write-port hookup. Master-id passthrough updated. - [`sim/tb/top/tb_top_psmct32_raster_demo_bram_ch171.sv`](../../sim/tb/top/tb_top_psmct32_raster_demo_bram_ch171.sv) — small heartbeat first-kick assertion added. **Fitter follow-on**: enabling the EE-RAM write port blew the Agilex 5 M20K budget (Quartus reported 516 needed vs 358 available). Pre-Ch251 the wrapper tied `wr_en=0` so Quartus inferred `ee_ram_stub.mem` as ROM — packed into a few M20K blocks implicitly. With the write path live, even with an explicit `ramstyle = "M20K"` hint the synthesizer's inference on the 128-bit-wide / 16-byte-enable / dual-port-master backing came out to 520 M20Ks — still 162 over budget. The ROM → byte-enable-RW transition was substantially more expensive than a straight "+14 M20Ks for write logic" estimate. Final fix: **don't enable the full ee_ram_stub write port at all, and don't consume ee_memory_map_stub's ram_wr_* output either.** Both paths inflated the M20K count. Instead, the demo wrapper snoops the EE's SW directly from `ee_cpu_wr_*` — the EE core's existing output going into the memory map — captures any full-word SW whose physical address is `0x0000_0730` (strip-kseg-bit'd) into a 32-bit `hb_rgbaq_reg`, and splices that register's value into the low 32 bits of the DMAC read response when the DMAC fetches qword 115. `ee_ram_stub.wr_en` stays tied to zero (Quartus continues to infer the memory as ROM) and `ee_memory_map_stub.ram_wr_*` outputs are left unconnected (so Quartus optimizes their behind-logic away). The snoop is one 32-bit register + a 1-cycle delay flop + a 128-bit 2:1 mux. M20K cost: ~0 new blocks beyond pre-Ch251. Files touched: - [`rtl/top/top_psmct32_raster_demo_bram.sv`](../../rtl/top/top_psmct32_raster_demo_bram.sv) — added `hb_rgbaq_reg`, `hb_write_hit` detector (sourced from `ee_cpu_wr_*`), and the `ram_rd_data_patched` mux. `ee_ram_stub.wr_*` stays tied-zero; `ee_memory_map_stub.ram_wr_*` outputs revert to unconnected. Sibling memories (`useg_shadow_mem` in `ee_memory_map_stub`, `bios_rom_stub.mem`) were not touched. The Ch232 BRAM-inference memory in `~/.claude/projects/.../memory/` flagged a similar issue earlier for the font ROM — for narrow / single-port memories the `ramstyle = "M20K"` hint rescues; for wide-byte-enable RW backings even the hint isn't enough and the cleanest fix is to keep the memory ROM-shaped and snoop writes outside it. #### Ch251.4 — Fitter resource report points at VRAM, not EE-RAM The Ch251.3 patch-register fix above was the right architectural move (it eliminated the RW BRAM cost for EE-RAM) but recompiling **still** reported 516 / 358 M20Ks. The Ch251.3 rework wasn't the source of the overrun — it was a parallel correctness fix that happened to land in the same chapter. The real culprit surfaced in `output_files/de25_nano_psmct32_raster_demo_top.fit.rpt` (Compilation Report → Fitter → Place Stage → **Fitter RAM Summary**): ``` u_demo|u_vram|mem_rtl_0 Logical Size: 4194304 bits M20K blocks: 204.800 u_demo|u_vram|mem_rtl_1 Logical Size: 4194304 bits M20K blocks: 204.800 ``` VRAM alone was eating ~410 of the 516 reported M20Ks. The `vram_bram_stub` module has **1 write + 2 independent read ports**: - **read** — PCRTC scanout (every pixel) - **read2** — PSMT4 RMW old-byte read (rasterizer write path) An M20K block has at most two physical ports total, and at most one write port. To honour 1W + 2R, Quartus **replicates** the entire 512 KiB storage into two simple-dual-port (1W + 1R) banks with the write fanned to both copies. True dual-port would not help — TDP gives two physical ports, not three. The Ch251 hardware build draws PSMCT32 sprites only (color bars + border + corners + heartbeat). The PSMT4 RMW pipe is wired but never fires (`is_t4_emit` stays low for the entire frame), so the second read port is dead weight on hardware. Fix: parameterize `vram_bram_stub` with `ENABLE_READ2`. Default 1 keeps every simulation TB byte-identical (PSMT4 paths still exercise). The DE25 board top overrides to 0, gating away the `mem[read2_word_idx]` reference in a generate-if branch so Quartus collapses the storage from two replicas (~410 M20Ks) to one (~205 M20Ks). The PSMT4 RMW path still compiles inside the wrapper (Ch157 logic intact) but feeds tied-zero `read2_data`, which is harmless because PSMCT32 emits never consult it. This is a **scoped hardware-demo build profile**, not a general fix. A formal decision record at [`docs/decisions/0006-vram-roadmap.md`](../decisions/0006-vram-roadmap.md) captures both the Ch251.4 rescue and the longer-term architectural follow-up (arbitrated TDP VRAM scheduler or line-buffered scanout) that must land before broader GS format coverage returns to the hardware build. Files touched: - [`rtl/gif_gs/vram_bram_stub.sv`](../../rtl/gif_gs/vram_bram_stub.sv) — new `ENABLE_READ2` parameter, read2 always_ff gated by `generate-if` so the synthesizer never sees the second `mem[]` read when disabled. - [`rtl/top/top_psmct32_raster_demo_bram.sv`](../../rtl/top/top_psmct32_raster_demo_bram.sv) — new `VRAM_ENABLE_READ2` parameter (default 1) passed through to `u_vram`. - [`rtl/top/de25_nano_psmct32_raster_demo_top.sv`](../../rtl/top/de25_nano_psmct32_raster_demo_top.sv) — overrides `VRAM_ENABLE_READ2(1'b0)` with the explicit rationale inline. - [`docs/decisions/0006-vram-roadmap.md`](../decisions/0006-vram-roadmap.md) — new decision record. Lesson learned (logged to MEMORY.md): on a Quartus fit failure, **read the Fitter RAM Summary in `fit.rpt` BEFORE proposing fixes.** Four rounds of patching `ee_ram_stub` chased the wrong module because the wrapper's most recently changed signal was the RAM write path; the resource report cleanly fingered VRAM in one pass. The "Resource Utilization by Entity" report in `Compilation Report → Fitter → Place Stage` is the right entry point. #### Ch252 — VRAM architecture checkpoint (docs + tripwire) After Ch251 closed the visual milestone, the next chapter is deliberately **not** a feature — it's an architecture checkpoint to keep the M20K trade-off from drifting silently as the GS path grows. The Ch251.4 read2-strip works because the demo doesn't need PSMT4 RMW; any later chapter that quietly relaxes that assumption would re-introduce the replication pressure that blew the fitter. **Hardware build profile snapshot** (de25_nano_psmct32_raster_demo): | Item | Hardware build | Sim defaults | |---------------------------------|------------------------|--------------------------------| | `VRAM_BYTES` | 512 KiB | 8 KiB (wrapper default) | | `VRAM_ENABLE_READ2` | `1'b0` | `1'b1` (default — PSMT4 live) | | `vram_bram_stub` M20K cost | ~205 (one 1W+1R bank) | trivial (small + replicated) | | Active GS formats | PSMCT32 | PSMCT32 / CT16 / T8 / T4 | | PSMT4 RMW path (`is_t4_emit`) | wired, never fires | exercised by gs_pcrtc / xfer TBs | The sim profile keeps every PSMT4-exercising TB byte-identical with the pre-Ch251.4 behaviour because the wrapper default for `BYTES` (8 KiB) is small enough that two replicas cost a handful of M20Ks total — irrelevant in simulation. **Elaboration tripwire.** A `$fatal` guard inside [`vram_bram_stub.sv`](../../rtl/gif_gs/vram_bram_stub.sv)'s `translate_off`'d `initial` block fires when both: ``` ENABLE_READ2 == 1'b1 && BYTES >= 262_144 (256 KiB) ``` 256 KiB is not a magic number — it is the size above which each 1W+1R replica costs ~100 M20Ks, so replication suddenly becomes a board-level architectural decision instead of a casual parameter flip. The tripwire is a loud canary in iverilog / Verilator / lint; the **real protection remains the board-top parameter profile**. A future hardware build that wants both read2 and a large VRAM has to either disable the guard intentionally or land one of the architectural follow-ups first. **Long-term triggers** for revisiting the architecture are tracked in [`docs/decisions/0006-vram-roadmap.md`](../decisions/0006-vram-roadmap.md): 1. PSMT4 RMW returning to the rasterizer write path on hardware. 2. More than one VRAM read client during scanout (a second simultaneous read consumer recreates the 1W+nR replication shape). 3. `VRAM_BYTES` growing beyond the current 512 KiB profile. Until one of those fires, the Ch251.4 + Ch251.5 combination is the hardware build. Ch252 adds no RTL behaviour change on the active path — only the sim-side tripwire, the decision-doc trigger list, and this profile snapshot. Files touched in Ch252: - [`rtl/gif_gs/vram_bram_stub.sv`](../../rtl/gif_gs/vram_bram_stub.sv) — new `$display`-then-`$fatal` guard inside the existing `translate_off`'d `initial` block. - [`docs/decisions/0006-vram-roadmap.md`](../decisions/0006-vram-roadmap.md) — new "Triggers — when to revisit" subsection with the three explicit triggers and the tripwire definition. - This bringup section. #### Ch251 addendum — PS2 backend OSD no longer force-opens Initial Ch251 hardware test surfaced a UX problem: the `ps2_backend.c::ps2_start()` was setting `OSD_CTRL_ENABLE | OSD_CTRL_INPUT_LOCK | OSD_CTRL_FORCE_OPEN` unconditionally (a holdover from when the PS2 core had no "running content"), so the OSD covered the entire vertical center of the screen including the new heartbeat region. Fix in `retroDE_splash/software/ps2_backend.c`: drop `OSD_CTRL_INPUT_LOCK | OSD_CTRL_FORCE_OPEN`, leave only `OSD_CTRL_ENABLE`. Rationale: the Ch251 animated demo IS the PS2 core's running content (color bars + heartbeat), so it should be treated like a running game — OSD opt-in via the Select+Start combo (Ch248 menu FSM), not force-opened over content. If a future variant of `ps2_backend` truly has no running content, that variant can re-add the FORCE_OPEN + INPUT_LOCK flags like `splash_backend` does. Header docstring + the inline ctrl-write comment updated to match. Operator behavior: - Boot into PS2 core → full demo visible (bars + border + corners + center heartbeat blinking 1 Hz). - Hold **Select+Start** ~0.5 s → OSD menu opens over the demo (gamepad or keyboard nav, Ch248/Ch245 paths intact). - Pick a core or hold Select+Start again → menu closes, demo visible again. This is a `retroDE_splash` HPS-side change only — no RTL delta, no regression impact. User must cross-compile retrodesd and copy the new binary to the board. #### What's NOT in Ch251 - **No input-driven element.** Codex's framing offered an optional Ch250-input toggle but explicitly noted "don't make input mandatory." The 1 Hz heartbeat carries the "alive" indicator on its own; coupling it to gamepad would mean the demo only animates when a button is held, which is a worse default. - **No analog/sweep effects.** The heartbeat just toggles between two static colors. A traveling block or color cycle could be a follow-up but adds bootlet complexity for marginal demo value. - **No DISPLAY1 size change.** Demo still paints 320×240 inside the 640×480 active picture, OSD region still configured for 640×480 (Ch247). The Ch243 region-truncation arc is unaffected. #### Ch253 — Known-good Ch251+ field-test checklist After Ch251 / Ch251.4 / Ch251.5 / Ch252 closed the visible-on-silicon milestone with the M20K profile locked, this is the single checklist to run any time a fresh build flashes to the DE25-Nano. Two parts: the visual checks an operator does at the monitor and the script- verifiable readback via `ps2_status.sh --delta`. **Visual (at the monitor / gamepad):** | Check | Expected behaviour | |----------------------------------------|-------------------------------------------------------------| | 8 vertical color bars | white / yellow / cyan / green / magenta / red / blue / black | | 4-pixel grey border | visible around the full 320×240 active region | | 4 orange corner squares | one in each corner over the border | | Center heartbeat | 16×16 square at (152,112) toggling **cyan ↔ red at ~0.5 Hz** (~2 s per color, Ch251.5; not 1 Hz — see note below) | | OSD opens with Select+Start | hold ~0.5 s on a wired DS2 pad → platform OSD menu appears | | OSD navigates with D-pad + A/B | up/down moves cursor, A picks core, B closes | | OSD closes with Select+Start | second hold returns to the demo | | HDMI output stable | no rolling, no NACK retries, no resync flashes | | **Ch255 — heartbeat override (A/B)** | hold ○ (A) → next heartbeat redraws **red**; hold × (B) → redraws **cyan**; hold both → invert current color. Release → resumes EE-animated cyan↔red within ≤2 s | **Script (`./ps2_status.sh --delta` on HPS Linux):** ``` Ch251+ animated-demo health verdict: [ ✓ ] PCRTC alive (FRAME_COUNT Δ ≈ 120 over 2 s) [ ✓ ] Raster healthy (RASTER_OVERFLOW_COUNT stable, bit[6] clear) [ ✓ ] DMAC repaint liveness (DMA_DONE Δ ∈ {1, 2} over 2 s; bootlet animating) [ ✓ ] EE core not halted (CORE_STATUS[1] = 0) [ ✓ ] HDMI I²C clean (CORE_STATUS[5] = 0) [ ✓ ] DS2 controller plugged (DS2_STATUS[0] = 1) ──> Ch251+ field health: PASS ``` Any `✗` in the verdict block means the unit failed bring-up. Triage by section: the `Counters Δ` block above the verdict shows raw numbers; the `CORE_STATUS` bit decode above that names the latched fault bit. A `[ ? ] DMAC repaint window miss` line is **not** a failure — the 2 s sample window can land entirely inside one color phase when the ~2 s toggle period and the script timing happen to align. Rerun `./ps2_status.sh --delta` once; the verdict comes back PASS on the next pass. Two or three consecutive `Δ=0` runs is the real "bootlet loop dead" signal. #### Ch254 — Heartbeat cadence characterization The heartbeat is a **liveness cue, not a precision timer.** Ch254 locks the empirical model of why it lands where it does and frames the operator-facing expectations accordingly: **Per-toggle cycle (from hardware measurement at Ch251.5 + Ch253):** ``` total / toggle = delay_loop_time + fixed_overhead ≈ (DELAY_HI * 0x10000) × 14 cyc / 50 MHz + ~1.2 s ``` Solving the model from two data points (`DELAY_HI=0x100` → 6 s, `DELAY_HI=0x002B` → ~2 s) gives **cyc/iter ≈ 14, overhead ≈ 1.2 s**. The ~1.2 s overhead is the DMAC drain of 102 qwords + the GS rasterization of all 17 SPRITEs + the CHCR poll + the re-arm sequence — all of which run *after* the delay-loop completes in each iteration. **Why we don't chase a true 1 Hz:** the overhead is a hard floor. Even with `DELAY_HI = 0` the bootlet caps at ~0.8 Hz toggle rate. Going faster requires restructuring the bootlet (let the delay run *during* the drain instead of after it, or shrink the 17-SPRITE payload). That is deliberately out of scope here — the demo's job is to confirm liveness on silicon, not deliver a clock signal. Ch254 ships the model honestly and stops there. **Operator-facing expectation:** the heartbeat toggles roughly every ~2 s (~0.5 Hz), with ±0.5 s jitter from overhead variation. The validated `DMA_DONE` Δ band over a 2 s sample window is **{0, 1, 2}**. `ps2_status.sh --delta` enforces exactly this band. Δ outside that range is the operator's flag to investigate; Δ=0 on a single run is a phase-miss and gets a rerun. Locked values: - `DELAY_HI = 0x002B` in [`sim/data/top_psmct32_raster_demo/bake.py`](../../sim/data/top_psmct32_raster_demo/bake.py) - Validated band `0–2` in [`docs/hardware/ps2_status.sh`](ps2_status.sh) - `vram_bram_stub` profile unchanged from Ch252. No RTL change. No bitstream change (the constant is locked at the value already shipping). Ch254 is **characterization, not retune**. #### Ch255 — Controller input drives the heartbeat color After Ch254 closed cadence characterization, Ch255 is the first chapter to put controller input on the demo's visible surface without touching the OSD path or the DS2 controller itself. The existing `INPUT_P1_RAW` bridge register that retrodesd's `ds2_poll_thread` was already writing gets a new fabric consumer: two of its bits feed a wrapper-side mux that overrides the heartbeat color the splicer injects into the DMAC read response. **Mapping** (matches the established retroDE INPUT_P1 bit layout in `rtl/iop/sio2_input_stub.sv`): - `INPUT_P1_RAW[9]` (Sony ○ / JOY_A) pressed → force RED (`0xFF0000FF`). - `INPUT_P1_RAW[7]` (Sony × / JOY_B) pressed → force CYAN (`0xFFFFFF00`). - Both pressed → invert the EE's current `hb_rgbaq_reg` value (XOR with `0x00FFFFFF`, swaps cyan↔red). - Neither pressed → EE's animated cyan↔red toggle passes through unchanged. The EE bootlet is **untouched** — it keeps owning `hb_rgbaq_reg` via the Ch251.3 SW-to-`0x0000_0730` splicer path. The override is a pure combinational mux in front of the splicer, so the EE's animation is preserved between button presses and resumes immediately on release. **Response latency** is one DMAC drain cycle. The GS only repaints the heartbeat sprite when the bootlet kicks DMAC channel 2, which happens once per ~2 s loop iteration (Ch254). So a button press takes effect on the NEXT heartbeat redraw — visible within ≤2 s, typically ≤1 s. Sub-second response would require either a fast-path GS draw triggered by button edges or a direct VRAM poke at the heartbeat pixel coordinates — both deliberately out of scope for Ch255's "input affects demo" proof. **Hardware surface:** - Same flashed `.rbf` continues to work for retrodesd (`INPUT_P1_RAW` writes were already landing, just had no fabric consumer for the heartbeat-relevant bits). - No new MMIO registers, no new HPS code, no ABI changes. - No change to OSD ctrl/status registers, no change to `ds2_controller`, no change to `osd_menu_fsm`. Select+Start still opens the OSD, gamepad still navigates it. - No VRAM profile regression. M20K cost unchanged. **Sim coverage** lives in [`tb_top_psmct32_raster_demo_bram_ch171`](../../sim/tb/top/tb_top_psmct32_raster_demo_bram_ch171.sv). After the existing first-DMAC pixel-pattern + heartbeat-qword assertions, the TB sweeps all four `{joy_a, joy_b}` combinations, asserts `hb_rgbaq_effective` for each, and confirms `hb_rgbaq_reg` itself isn't corrupted by the override. The four assertions catch mux priority regressions, the XOR-both-pressed case, and ensure the EE's background animation is decoupled. **Acceptance for hardware bring-up** (added to the Ch253 visual checklist above): - Press ○ → next heartbeat redraw shows red. - Press × → next redraw shows cyan. - Press both → next redraw shows the *inverse* of whatever the EE was about to paint (visual feedback that the XOR fired even when neither single override applies). - Release → returns to EE-animated cyan↔red within ≤2 s. - `ps2_status.sh --delta` still PASS (DMA_DONE keeps incrementing — the override only changes what gets painted, not the loop rate). Files touched in Ch255: - [`rtl/top/top_psmct32_raster_demo_bram.sv`](../../rtl/top/top_psmct32_raster_demo_bram.sv) — two new 1-bit input ports (`joy_a_pressed_i`, `joy_b_pressed_i`) + four-line combinational `hb_rgbaq_effective` mux + splicer now reads `hb_rgbaq_effective` instead of `hb_rgbaq_reg`. - [`rtl/top/de25_nano_psmct32_raster_demo_top.sv`](../../rtl/top/de25_nano_psmct32_raster_demo_top.sv) — wires `bridge_input_p1_raw[9]` / `[7]` into the new wrapper ports. - [`sim/tb/top/tb_top_psmct32_raster_demo_bram_ch171.sv`](../../sim/tb/top/tb_top_psmct32_raster_demo_bram_ch171.sv) — new override coverage sweep. - [`sim/tb/top/tb_gs_raster_backpressure_stress.sv`](../../sim/tb/top/tb_gs_raster_backpressure_stress.sv) — joy inputs tied to `1'b0` (override dormant, regression byte- identical). **Build-time (one-time check per bitstream, not per boot):** - `de25_nano_psmct32_raster_demo_top.sv` overrides `VRAM_ENABLE_READ2 = 1'b0` (Ch252 hardware profile). Verify by grepping the synth top before flashing if you suspect a misbuild: ``` grep VRAM_ENABLE_READ2 rtl/top/de25_nano_psmct32_raster_demo_top.sv ``` Expected: `.VRAM_ENABLE_READ2 (1'b0)`. Any other value with `VRAM_BYTES = 512 KiB` blows the Agilex 5 M20K budget; see [`docs/decisions/0006-vram-roadmap.md`](../decisions/0006-vram-roadmap.md). If everything in all three sections is green, the unit is at the Ch251+ baseline and ready for the next architectural chapter. ## Known caveats (deferred to Ch174+) - **HDMI output timing**: `set_false_path -to HDMI_TX_*` placeholder. Real `set_output_delay` against ADV7513 setup/hold is on the Ch168+ list. - **No audio**: I²C config touches audio registers (MCLK = 12.288 MHz, I²S 48 kHz / 16-bit / stereo) but no I²S data is generated. Audio streams will silently underrun. - **No LPDDR4 / SDRAM / HPS**: unused by the demo. EE/IOP RAMs are on-FPGA BRAM. ## What's verified pre-silicon The accelerated bring-up TB ([`sim/tb/top/tb_hdmi_i2c_wake_smoke.sv`](../../sim/tb/top/tb_hdmi_i2c_wake_smoke.sv)) locks down the I²C wake-up sequence at simulation time. As of Ch167 it asserts: - LUT walk reaches all 38 entries; `READY` (`hdmi_init_done`) rises after the walk completes. - `HDMI_TX_INT` low pulse retriggers the LUT walk; `READY` re-asserts on the second walk. - SDA is always `1'b0`, `1'b1`, or `1'bz` — never `1'bx` (would indicate two drivers contending for strong-HIGH, an open-drain violation). - The Ch166 NACK watchdog stays LOW on a healthy bus and rises (sticky) when the slave doesn't ACK. - **Byte-sequence lock**: every one of the 38 transactions on the simulated wire matches the FSM-intent payload byte-for-byte, and every transaction's slave address is `8'h72` (ADV7513 write address). If a future RTL change alters either the LUT order, the LUT contents, or the slave address, the TB fails. ## Reference - RTL banner: `rtl/top/de25_nano_psmct32_raster_demo_top.sv` - I²C wake-up FSM: `rtl/platform/I2C_HDMI_Config.v` - I²C bit-bang master: `rtl/platform/I2C_Controller.v` - Pin assignments + IO standards: `synth/de25_nano/top_psmct32_raster_demo/de25_nano_psmct32_raster_demo_top.qsf` - Timing constraints: `synth/de25_nano/top_psmct32_raster_demo/de25_nano_psmct32_raster_demo_top.sdc` - Accelerated bring-up TB: `sim/tb/top/tb_hdmi_i2c_wake_smoke.sv`