# top_psmct32_raster_demo on DE25-Nano (current as of Ch165)

This directory is the **DE25-Nano synthesis project** for
[`rtl/top/de25_nano_psmct32_raster_demo_top.sv`](../../../rtl/top/de25_nano_psmct32_raster_demo_top.sv)
(the Ch149 board wrapper, retargeted in **Ch159** to instantiate
the BRAM-backed `top_psmct32_raster_demo_bram` instead of the
Ch146 legacy `top_psmct32_raster_demo`). The directory grew
across these chapters:

| Chapter | Added                                                                                              |
|---------|----------------------------------------------------------------------------------------------------|
| Ch148   | `files.f` (RTL filelist, Ch123 dep tree only) + this README.                                       |
| Ch149   | (RTL only — `de25_nano_psmct32_raster_demo_top.sv` lives in `rtl/top/`).                           |
| Ch150   | `de25_nano_psmct32_raster_demo_top.qsf` + `.sdc` — minimal Quartus scaffold.                       |
| Ch151   | (RTL only — `de25_nano_pll_stub.sv` + PLL/lock-gated-reset rework of board top).                   |
| Ch152   | `build_quartus.sh` + `parse_reports.py` — first real Quartus compile (fit FAILED).                 |
| Ch159   | Board-top swap to `top_psmct32_raster_demo_bram` (Ch155-Ch158 BRAM/normalize/PCRTC stack); fit + STA succeed. `baseline_ch152/` snapshots the prior fit-failed reports for diff. |
| Ch160   | SDC retarget from 50 MHz → 30 MHz (down-clock profile); `build_quartus.sh` runs `quartus_asm` on clean STA → first `.sof` bitstream produced. `baseline_ch159/` snapshots the 50 MHz timing-miss reports. |
| Ch161   | Real Quartus IOPLL `.ip` commit (50 MHz refclk → 30 MHz outclk_0) + `USE_PLL_IP=1` macro + `ip/` symlink in build_quartus.sh; SDC restored to 20 ns CLOCK2_50 (the IP's auto-generated SDC handles the post-PLL clock). The `.sof` now genuinely runs at 30 MHz on hardware. `baseline_ch160/` snapshots the SDC-profile-only state. |
| Ch162   | `STRIP_HW_DIVIDER` parameter on `ee_core_stub` removes the auto-inferred 32-bit DIVU divider from the synth path on hardware builds (default off in sim preserves every existing TB). Fmax 30.74 → **33.6 MHz** (+9.4 %); fit ALMs −892, registers −734. New critical path: PCRTC magnification divider in `gs_pcrtc_stub` (`hwin_rel / hmag_factor`). `baseline_ch161/` snapshots the pre-strip state. |
| Ch163   | `STRIP_PCRTC_MAG_DIV` parameter on `gs_pcrtc_stub` strips the PCRTC magnification dividers (constant divisor 1 when MAGH=MAGV=0). Fmax 33.6 → **81.83 MHz** at 30 MHz target (+143 %), then PLL `.ip` retuned 30 MHz → **50 MHz** outclk_0; STA closes at 50 MHz with +7.500 ns setup slack and Fmax 80.0 MHz. **First .sof that genuinely runs at 50 MHz on the DE25-Nano.** `baseline_ch162/` + `baseline_ch163_30mhz/` snapshots the milestones. |
| Ch164   | First video-PHY shim — pins HDMI_TX_CLK + HDMI_TX_D[23:0] + HS/VS/DE on the DE25-Nano ADV7513 (pinout sourced from `retroDE_nes`). Wrapper drives them combinationally from VIDEO_* (R in MSBs of HDMI_TX_D). HDMI_TX_CLK = design_clk = 50 MHz post-PLL. ADV7513 I²C wake-up FSM still deferred (Ch165) so a real monitor stays dark — but pixels are now off-chip on the HDMI connector. STA stays clean (+7.536 ns slack); pins 17 → 45. `baseline_ch163_50mhz/` snapshots the pre-shim state. |
| Ch165   | ADV7513 I²C wake-up FSM (Terasic-derived; ported from `retroDE_splash/rtl/platform/`). Adds 4 control pins (`HDMI_I2C_SCL/SDA` open-drain bus + `HDMI_TX_INT` interrupt + `HDMI_MCLK` audio reference); `LED[3] = ~hdmi_init_done`. The 38-entry LUT walks ADV7513 register writes (power-up + HPD override + AVI InfoFrame + HDMI mode select), turning the chip from standby into "transmitting RGB on the HDMI port". Pins 45 → 49; STA setup slack +7.198 ns; `.sof` clean. **First .sof that should drive a real HDMI monitor.** `baseline_ch164/` snapshots the pre-wake-up state. |

## Ch163 strip-PCRTC-divider + 50 MHz close state

The full journey from Ch152's fit failure to a real 50 MHz
bitstream:

| Metric                            | Ch152 (50 MHz)    | Ch159 (50 MHz)        | Ch161 (real PLL @ 30 MHz) | Ch162 (strip EE div, 30 MHz)        | **Ch163 (strip both, 50 MHz)**       |
|-----------------------------------|-------------------|-----------------------|---------------------------|-------------------------------------|--------------------------------------|
| Fit status                        | **FAILED**        | Successful (30,364)   | Successful (30,898)       | Successful (30,006)                 | **Successful (27,543)**              |
| Fit RAM blocks                    | 6                 | 14                    | 14                        | 14                                  | 14                                   |
| Fit PLLs                          | 0                 | 0                     | 1                         | 1                                   | **1 (50 MHz outclk0)**               |
| Setup slack worst (design domain) | (did not run)     | −6.950 ns             | +0.565 ns @ 30 MHz        | +3.567 ns @ 30 MHz                  | **+7.500 ns @ 50 MHz**               |
| **Fmax (design domain)**          | (did not run)     | 37.11 MHz             | 30.74 MHz                 | 33.6 MHz                            | **80.0 MHz**                         |
| `.sof` produced                   | (skipped)         | (skipped)             | yes — 30 MHz on hardware  | yes — Fmax 33.6 MHz / 30 MHz target | **yes — 50 MHz on hardware**         |

Ch163 lands in two stages:

- **Stage A**: with `STRIP_PCRTC_MAG_DIV=1` on the board top
  but the PLL still at 30 MHz output, Fmax jumps from 33.6 MHz
  (Ch162) to **81.83 MHz** at the 30 MHz target — +143 %, well
  past 50 MHz.
- **Stage B**: retune `pll.ip` from 30 MHz → 50 MHz output
  (`gui_output_clock_frequency0` = 50.0; `gui_output_clock_frequency_ps0`
  = 20000.0), `quartus_ipgenerate` regenerates the .qip / synth
  files. CLOCK2_50 stays at the physical 50 MHz period in the
  SDC; the IOPLL's auto-generated SDC declares the new
  outclk_0. Quartus rebuild → STA closes at 50 MHz with
  **+7.500 ns** of setup slack and Fmax **80.0 MHz**.

The Stage B `.sof` is the first bitstream that genuinely runs
at 50 MHz on the DE25-Nano. The Ch161 PLL hardware-real
contract carries through; the IOPLL takes 50 MHz CLOCK2_50 in
and emits 50 MHz outclk_0, so the chip-internal clock
distribution still goes through the dedicated IOPLL clock
network even at the 1:1 frequency relation.

## Ch162 strip-divider state (vs Ch161 / Ch160 / Ch159 / Ch152 baselines)

| Metric                            | Ch152 (50 MHz)    | Ch159 (50 MHz)        | Ch160 (30 MHz profile)    | Ch161 (real PLL @ 30 MHz)         | Ch162 (real PLL + strip divider)    |
|-----------------------------------|-------------------|-----------------------|---------------------------|-----------------------------------|-------------------------------------|
| Fit status                        | **FAILED**        | Successful (30,364)   | Successful (31,056)       | Successful (30,898)               | Successful (30,006)                 |
| Fit registers                     | 121,176           | 39,085                | 37,381                    | 37,352                            | 36,618                              |
| Fit RAM blocks                    | 6                 | 14                    | 14                        | 14                                | 14                                  |
| Fit PLLs                          | 0                 | 0                     | 0                         | 1                                 | 1                                   |
| Setup slack worst (design domain) | (did not run)     | −6.950 ns             | +0.805 ns                 | +0.565 ns @ `iopll_0_outclk0`     | **+3.567 ns** @ `iopll_0_outclk0`   |
| **Fmax (design domain)**          | (did not run)     | 37.11 MHz             | 30.74 MHz                 | 30.74 MHz                         | **33.6 MHz** (+9.4 %)               |
| `.sof` produced                   | (skipped)         | (skipped)             | yes (profile only)        | yes — 30 MHz on hardware          | **yes — 33.6 MHz Fmax / 30 MHz target** |

Stripping the EE-core 32-bit DIVU divider freed +3 ns of setup
margin and 892 ALMs / 734 registers. The new STA worst path
moves to the **PCRTC magnification divider** in
[`gs_pcrtc_stub.sv`](../../../rtl/gif_gs/gs_pcrtc_stub.sv)
(`vram_x_unshift = hwin_rel / hmag_factor` and the matching y
form). **Ch163 gates that divider** via `STRIP_PCRTC_MAG_DIV`
and retunes the PLL to 50 MHz — see the Ch163 section above
for the milestone numbers.

## Ch161 real-PLL state (vs Ch160 / Ch159 / Ch152 baselines)

| Metric                            | Ch152 (50 MHz)    | Ch159 (50 MHz)            | Ch160 (30 MHz profile)    | Ch161 (real PLL @ 30 MHz)         |
|-----------------------------------|-------------------|---------------------------|---------------------------|-----------------------------------|
| Fit status                        | **FAILED**        | Successful (30,364)       | Successful (31,056)       | Successful (30,898)               |
| Fit registers                     | 121,176           | 39,085                    | 37,381                    | 37,352                            |
| Fit RAM blocks                    | 6                 | 14                        | 14                        | 14                                |
| **Fit PLLs**                      | 0                 | 0                         | 0                         | **1** (real IOPLL)                |
| Setup slack worst (design domain) | (did not run)     | −6.950 ns                 | +0.805 ns                 | **+0.565 ns** @ `iopll_0_outclk0` |
| Fmax (design domain)              | (did not run)     | 37.11 MHz                 | 30.74 MHz                 | **30.74 MHz**                     |
| `.sof` produced                   | (skipped)         | (skipped)                 | yes (profile only)        | **yes — 30 MHz on hardware**      |

The Ch161 `.sof` is the first bitstream that genuinely runs at
the constrained frequency on the real DE25-Nano: the IOPLL
takes the 50 MHz CLOCK2_50 input and divides to 30 MHz inside
the chip, so the entire design downstream of `u_pll.outclk_0`
operates at 30 MHz. Critical path was the EE core's auto-
generated 64-bit DIVU divider — **closed in Ch162** via
`STRIP_HW_DIVIDER`. New critical path (Ch162 onward): the PCRTC
magnification divider in `gs_pcrtc_stub.sv` (`hwin_rel /
hmag_factor`); see the Ch162 section above.

## Ch160 down-clock + first .sof state (vs Ch159 / Ch152 baselines)

| Metric                        | Ch152 (50 MHz)            | Ch159 (50 MHz)            | Ch160 (30 MHz)            |
|-------------------------------|---------------------------|---------------------------|---------------------------|
| Fit status                    | **FAILED** (155k / 331 %) | Successful (30,364 / 65 %) | Successful (31,056 / 66 %) |
| STA setup slack worst         | (did not run)             | −6.950 ns                 | **+0.805 ns**             |
| Fmax                          | (did not run)             | 37.11 MHz                 | 30.74 MHz                 |
| `quartus_asm` (.sof produced) | (skipped — fit failed)    | (skipped — STA missed)    | **Successful**            |

The Ch160 SDC profile retargets CLOCK2_50 from 20.000 ns
(50 MHz) to 33.333 ns (30 MHz) so the fitter has positive
slack on every report. `quartus_asm` now runs on every clean
build, so a real `.sof` bitstream lands in
[`output_files/de25_nano_psmct32_raster_demo_top.sof`](output_files/de25_nano_psmct32_raster_demo_top.sof).
Worst-case path is the EE core's auto-generated 64-bit divider
(actually the Ch43 DIVU divider, dead code in the PSMCT32 demo
since the bootlet doesn't execute DIVU); **closed in Ch162**
via `STRIP_HW_DIVIDER`. Programming the Ch160 `.sof` onto a
real board where CLOCK2_50 is still wired straight through
gives a 50 MHz chip
clock that may setup-violate the divider path; Ch161+'s
PLL-IP commit fixes that.

## Ch159 fit-success state (vs Ch152 baseline)

`make quartus_compile` runs the full syn/fit/sta flow. With
the Ch159 BRAM-backed top:

| Metric                            | Ch152 (vram_stub)             | Ch159 (vram_bram_stub)        |
|-----------------------------------|-------------------------------|-------------------------------|
| Synthesis status                  | Successful                    | Successful                    |
| Synthesis ALMs estimate           | 199,103 / 46,800 (425% over)  | **22,704 / 46,800 (49%)**     |
| Synthesis registers               | 101,457                       | **36,008**                    |
| **Fit status**                    | **FAILED** (155k / 331% over) | **Successful** (30,364 / 65%) |
| Fit registers                     | 121,176                       | **39,085**                    |
| Fit RAM blocks                    | 6 / 358                       | **14 / 358**                  |
| **STA status**                    | **DID NOT RUN**               | **Successful** (12 warnings)  |
| Setup slack worst (CLOCK2_50)     | n/a                           | −6.950 ns                     |
| Fmax                              | n/a                           | 37.11 MHz                     |

The headline numbers: **synth ALMs −88.6 %, fit registers
−67.7 %, +8 RAM blocks** (the `vram_bram_stub` 8 KiB dual-port
shape that Ch154 exp_c forecast). Fit and STA both run through
to completion. Setup slack is negative at the 50 MHz CLOCK2_50
constraint — Fmax is currently 37.11 MHz, so timing closure
(PLL down-clock or critical-path pipelining) is the **Ch160+**
surface; Ch159 deliberately stops at "fits and reaches STA."

The full chapter narrative lives in
[`docs/contracts/gif_gs.md`](../../../docs/contracts/gif_gs.md)
under "Board-top swap to BRAM wrapper + Quartus fit recovery
(Ch159)". The Ch152 baseline reports are preserved under
[`baseline_ch152/`](baseline_ch152/) for diff and audit.

Ch150's QSF/SDC pin clock + reset + LED I/O for a real Quartus
build, but **`VIDEO_R/G/B/HSYNC/VSYNC/DE` are intentionally left
as virtual pins** (`VIRTUAL_PIN ON` in the QSF) — they will not
toggle real package pins until the PHY shim chapter (Ch151+)
maps them to a video output. Without virtualization, Quartus
would auto-place an unassigned top-level output on an arbitrary
package pin.

## Files

| File                                              | Purpose                                                        |
|---------------------------------------------------|----------------------------------------------------------------|
| `files.f`                                         | Synthesis filelist — Ch123 dep tree + Ch149 board wrapper.     |
| `de25_nano_psmct32_raster_demo_top.qsf`           | **Ch150** — Quartus pin assignments + IO standards + macros.   |
| `de25_nano_psmct32_raster_demo_top.sdc`           | **Ch150** — clock + reset-sync + IO false-path constraints.    |
| `README.md`                                       | This file.                                                     |

The Ch150 `.qsf` + `.sdc` together are PHY-light: clock / reset /
LED pins are pinned and constrained, but video pins
(`VIDEO_R/G/B/HSYNC/VSYNC/DE`) are **not** pin-assigned. The PHY
shim chapter (Ch151+) maps them to a real connector.

To validate the Quartus scaffold (.qsf + .sdc + filelist +
fixtures) without launching Quartus:
```
make -C sim top_psmct32_raster_demo_quartus_scaffold_check
```

## Top module

**Set Quartus's top-level entity to `de25_nano_psmct32_raster_demo_top`**
(in [`rtl/top/de25_nano_psmct32_raster_demo_top.sv`](../../../rtl/top/de25_nano_psmct32_raster_demo_top.sv)).
This is the Ch149 board-shaped wrapper, retargeted in **Ch159**
to instantiate `top_psmct32_raster_demo_bram` (the BRAM-backed
inner module from Ch155+, carrying every Ch155-Ch158 fix). It
adds the DE25-Nano-specific plumbing — Terasic-canonical port
names, reset-release sequencer, `core_go` pulse generator, and
active-low LED status mapping.

The legacy Ch146 inner module `top_psmct32_raster_demo` is kept
on the project file list for back-compat with sim TBs that
still target it; on Quartus only the actually-instantiated top
is elaborated. **Do NOT set the top entity to either of the
inner modules** (`top_psmct32_raster_demo` or
`top_psmct32_raster_demo_bram`) — that bypasses every board
adapter and exposes the inner module's `clk` / `rst_n` /
`core_go` / `r/g/b/hsync/vsync/de` / status outputs directly,
which is useful for sim and lint but not for an FPGA build.

### Ch149 board wrapper ports (`de25_nano_psmct32_raster_demo_top`)

| Port                                | Direction | Width | Role                                                       |
|-------------------------------------|-----------|-------|------------------------------------------------------------|
| `CLOCK0_50` / `CLOCK1_50` / `CLOCK2_50` | input | 1 ea. | DE25-Nano 50 MHz oscillators. Only `CLOCK2_50` is used.    |
| `KEY[1:0]`                          | input     | 2     | Active-LOW push buttons. `KEY[0]` = soft reset.            |
| `SW[3:0]`                           | input     | 4     | DIP switches; placeholder, unused.                         |
| `LED[7:0]`                          | output    | 8     | Active-LOW. `LED[2:0]` = status, `LED[7:3]` = OFF.         |
| `VIDEO_R/G/B`                       | output    | 8 ea. | Raw 8-bit RGB; PHY shim deferred to next chapter.          |
| `VIDEO_HSYNC/VSYNC/DE`              | output    | 1 ea. | Raw video timing; PHY shim deferred.                       |

LED mapping (active-low; signal asserted lights its LED):

| LED       | Polarity-corrected source |
|-----------|---------------------------|
| `LED[0]`  | `~core_halt`              |
| `LED[1]`  | `~dma_done_seen`          |
| `LED[2]`  | `~frame_seen`             |
| `LED[7:3]`| tied HIGH (OFF)           |

### Inner module ports (`top_psmct32_raster_demo_bram`, Ch155+)

For sim / lint use only. The board wrapper above adapts these
to the DE25-Nano signal names; a custom integration could
re-adapt them for a different board. The legacy Ch146 inner
module `top_psmct32_raster_demo` exposes the same external
port shape (just with a `vram_stub`-backed implementation
instead of `vram_bram_stub`).

| Port            | Direction | Width | Role                                                          |
|-----------------|-----------|-------|---------------------------------------------------------------|
| `clk`           | input     | 1     | Single clock domain (see "Clock plan" below).                 |
| `rst_n`         | input     | 1     | Active-low synchronous reset (see "Reset plan").              |
| `core_go`       | input     | 1     | Pulsed high to start the EE bootlet (see "core_go strategy"). |
| `r/g/b`         | output    | 8 ea. | 8-bit RGB scanout (PCRTC).                                    |
| `hsync/vsync/de`| output    | 1 ea. | Standard video timing.                                        |
| `core_halt`     | output    | 1     | High once the EE has SYSCALL'd.                               |
| `dma_done_seen` | output    | 1     | Sticky: DMAC ch2 fired EV_DMA_DONE.                           |
| `frame_seen`    | output    | 1     | Sticky: PCRTC end-of-frame fired ≥1 frame.                    |

## Required preprocessor macros

Both must be set on the synthesis tool (NOT as module generics —
they're `\`define` macros per the Ch146 iverilog-12 string-
parameter forwarding workaround):

| Macro                                              | Value                                       |
|----------------------------------------------------|---------------------------------------------|
| `TOP_PSMCT32_RASTER_DEMO_BIOS_IMAGE_FILE`          | Absolute path to `bios.mem`                 |
| `TOP_PSMCT32_RASTER_DEMO_PAYLOAD_IMAGE_FILE`       | Absolute path to `payload.mem`              |

For Quartus: see the `set_global_assignment -name VERILOG_MACRO`
example in [`docs/contracts/gif_gs.md`](../../../docs/contracts/gif_gs.md)
under "Synthesis-facing macros".

## Required `.mem` fixtures

Both files must exist before synthesis (Quartus's `$readmemh`
runs at elaboration; if the file is missing you get a silent
zero-init that produces no payload at all):

| File                                       | Size                  | Produced by |
|--------------------------------------------|-----------------------|-------------|
| `sim/data/top_psmct32_raster_demo/bios.mem` | 1024 32-bit words      | `bake.py`   |
| `sim/data/top_psmct32_raster_demo/payload.mem` | 256 128-bit qwords | `bake.py`   |

To produce them:
```
make -C sim top_psmct32_raster_demo_mem
```

To verify both files exist + match expected sizes (and the
synth filelist resolves cleanly):
```
make -C sim top_psmct32_raster_demo_synth_check
```

## DE25-Nano board topology (current as of Ch151)

Live wiring (no longer "assumptions" — the board top
implements it):

### Clock plan

- DE25-Nano's `CLOCK2_50` (50 MHz, PIN_BF23) is the only board
  oscillator the design uses. CLOCK0_50 / CLOCK1_50 are pin-
  assigned in the QSF for completeness but the wrapper ties
  them off internally.
- **Ch151**: CLOCK2_50 → `de25_nano_pll_stub` (sim default,
  pass-through) or Quartus IOPLL `pll` (synth, when
  `USE_PLL_IP` is defined) → `design_clk`. Everything in the
  design (EE core, GIF, GS, PCRTC, `core_go` sequencer, all
  three status sticky-bits) is clocked on `design_clk`.
- The PCRTC's H/V counters are parameter-driven (default 16×8)
  and do NOT model real CRTC timing — a first hardware build
  can run at any sane clock. The PLL chapter (Ch152+) will
  commit a real `outclk_0` frequency.
- No clock gating, no derived clocks, no CDCs in the design.

### Reset plan

- **Ch151**: the reset bridge async-asserts on
  `(ninit_done | ~pll_locked)` and synchronously deasserts on
  `design_clk` through a 2-stage shift register. Both
  conditions must clear before the design leaves reset:
  1. `ninit_done` falls when FPGA initialization completes
     (real Terasic `reset_release` IP under
     `\`ifdef USE_TERASIC_RESET_RELEASE_IP`, else inline
     16-cycle stub).
  2. `pll_locked` rises when the PLL acquires lock (32-cycle
     stub warm-up; real IP timing depends on the configured
     output frequency).
- `KEY[0]` (active-low push button) is sampled synchronously
  through the same 2-stage register — async-assert is reserved
  for the FPGA-init / PLL-lock signals.
- The Ch146 wrapper's `rst_n` input is active-low and sampled
  inside `always_ff @(posedge clk) if (!rst_n)` — i.e.
  **synchronous** reset despite active-low polarity.

### `core_go` strategy

`ee_core_stub.go_i` is sampled **level-sensitively** in the
core's `S_IDLE` state (see [`rtl/ee/ee_core_stub.sv:812-813`](../../../rtl/ee/ee_core_stub.sv#L812-L813)
— `if (go_i) state <= S_IFETCH_REQ`). There is no edge
synchronizer; the core enters `S_IDLE` after reset and
transitions to `S_IFETCH_REQ` on the first cycle `go_i` is
high. Two equivalent hardware paths:

1. **Tie `core_go` high** at the top level. After `rst_n`
   deasserts, the core enters `S_IDLE`, sees `go_i == 1`, and
   immediately starts fetching from `0xBFC0_0000`. Simplest
   wiring.
2. **Reset-release sequencer**: a small board-level FSM that
   waits N cycles after `rst_n` deasserts (giving the PLL +
   BRAM init time to settle) then drives `core_go` high. Held-
   high or one-cycle pulse both work — only the rising-into-
   S_IDLE matters.

Option 2 is the **recommended hardware path** because the
deliberate post-reset wait avoids fetching during the BRAM
`$readmemh` settle window. A board-level synchronizer also
gives a clean place to debounce the user push-button if
`core_go` is also user-controllable.

### Video output path

- DE25-Nano video output is **TBD** in this chapter — the next
  chapter will wire the top's `r/g/b/hsync/vsync/de` to the
  board's chosen video PHY. Likely candidates:
  - **HDMI** via on-board ADV7513 (or equivalent). Needs an I²C
    config sequence, which adds a small bring-up state machine.
  - **VGA** via on-board ADV7123 / passive DAC. Simpler — just
    pin-wire the 8-bit RGB + sync signals.
  - **Direct GPIO** for an external PMOD video adapter
    (simplest path for a first bring-up).
- The Ch146 wrapper's PCRTC outputs are 8-bit RGB at one cycle
  per pixel — compatible with all three options after a clock-
  rate adjustment.

### LEDs / status

The Ch146 wrapper exposes three sticky status bits the DE25-
Nano user LEDs can show:

| LED            | Signal           | Meaning                                |
|----------------|------------------|----------------------------------------|
| User LED 0     | `core_halt`      | EE has SYSCALL'd → bootlet finished.   |
| User LED 1     | `dma_done_seen`  | DMAC ch2 completed transfer.           |
| User LED 2     | `frame_seen`     | PCRTC has scanned out ≥1 frame.        |

All three latch high within ~10 ms of `rst_n` deasserting on a
typical 50 MHz clock. The actual order is **`frame_seen` first**
(the PCRTC starts scanning empty frames as soon as reset
deasserts), then `core_halt` (after the EE bootlet runs to
SYSCALL), then `dma_done_seen` (after the DMAC channel-2
transfer completes). The Ch146 wrapper's `frame_seen` is a
"PCRTC alive" indicator — it doesn't gate on whether the frame
holds rendered content. If you see `core_halt` low, the bootlet
hung; if `dma_done_seen` is low, the DMAC didn't deliver the
GIF payload; if `frame_seen` is low, the PCRTC isn't scanning
out (PLL not locked or reset stuck asserted).

## What's still NOT in this chapter

Landed in Ch165–Ch167:

- **Accelerated I²C bring-up TB** — `sim/tb/top/tb_hdmi_i2c_wake_smoke.sv`
  instantiates `I2C_HDMI_Config` with `CLK_Freq=2 / I2C_Freq=1` so
  the LUT walks in microseconds, asserts (1) the 38-entry walk reaches
  `LUT_SIZE-1`, (2) `READY` rises, (3) `HDMI_TX_INT` retriggers the
  walk, (4) SDA never resolves to `'x` (no driver conflict), (5)
  Ch166 NACK watchdog stays low on the healthy bus and rises
  (sticky) when `mI2C_ACK` is forced HIGH, and **(6, Ch167) every
  one of the 38 transactions on the wire matches the FSM-intent
  payload byte-for-byte**. The byte-sequence lock uses a pullup +
  phase-aware slave-ACK bus model so released SDA bits resolve to
  1; a decoder samples SDA on each SCL rising edge between START
  and STOP and assembles each transaction as a 24-bit
  `{dev_addr, reg, data}` tuple, which is then compared against
  `u_dut.mI2C_DATA[23:0]` snapshots taken on `mI2C_GO` rising
  edges. Every transaction's dev_addr is asserted to be 8'h72
  (ADV7513 write address). At the production divider the LUT
  walk takes ~125 ms (controller-clock period ~100 µs × 33
  phases/byte × 38 bytes), so observing it inside the 5 ms board
  TB is impractical.

Deferred to Ch168+:

- Proper `set_output_delay` constraints for HDMI_TX_* once
  the ADV7513 setup/hold window is locked from the bring-up
  datasheet pass — replaces Ch164's `set_false_path -to`.
- Make the rendered pattern bigger than Ch123's 16×8 SPRITE
  so there's something visible to admire on a real screen.
- xfer-side T4 coverage TB (open from Ch157+).
- `useg_shadow_mem` BRAM-shape forensics.

(Ch161 made the PLL hardware-real at 30 MHz. Ch162 retired
the EE-core hardware-divider critical path via
`STRIP_HW_DIVIDER`. Ch163 retired the PCRTC magnification
divider via `STRIP_PCRTC_MAG_DIV` and retuned the PLL to
50 MHz outclk_0 — the `.sof` now genuinely runs at 50 MHz
on the DE25-Nano. Ch164 added the HDMI pin shim — pixels
reach the DE25-Nano HDMI connector pins. Ch165 ported the
ADV7513 wake-up FSM from Terasic's reference design — the
chip is now configured to transmit RGB on the HDMI port,
so the .sof should drive a real monitor for the first time.)

The point of Ch149 was that the **design is now board-shaped** —
DE25-Nano signal names, Terasic-style reset-release sequencer,
and active-low LED status mapping. Ch161 made the **PLL
hardware-real**; Ch162+ makes the **timing closure
hardware-real** at 50 MHz.

## Ch149 additions

| Artifact                                         | Purpose                                                  |
|--------------------------------------------------|----------------------------------------------------------|
| `rtl/top/de25_nano_psmct32_raster_demo_top.sv`   | DE25-Nano board wrapper: CLOCK0/1/2_50 + KEY[1:0] + SW[3:0] + LED[7:0] + raw video pins. |
| `sim/tb/top/tb_de25_nano_psmct32_raster_demo_top.sv` | Smoke TB: drives CLOCK2_50 + KEY[0] release, asserts core_go pulses exactly once, all 3 status LEDs latch, VIDEO_DE rises. |

The board top instantiates `top_psmct32_raster_demo_bram` (Ch155+
BRAM-backed inner module, retargeted in **Ch159** from the Ch146
legacy `top_psmct32_raster_demo`) and adds:

- **`ninit_done` source**. Default-off `\`ifdef
  USE_TERASIC_RESET_RELEASE_IP` swaps in Terasic's `reset_release`
  IP (from
  `DE25_Nano_ResourceCD/Demonstration/FPGA/Board_Info_RTL/reset_release/`).
  When the macro is undefined (sim default), an inline 16-cycle
  counter mimics the IP's "high until BRAM init completes" shape.
- **Reset synchronizer**. `KEY[0]` (active-low button) and
  `ninit_done` feed an async-assert/sync-deassert 2-stage shift
  register clocked by CLOCK2_50, mirroring the retroDE_nes
  pattern. Output is the design's `core_rst_n`.
- **`core_go` sequencer**. After `core_rst_n` deasserts, waits 16
  cycles then drives `core_go` high for one cycle. Matches the
  recommended hardware path documented above (the EE core's
  `go_i` is sampled level-sensitively in S_IDLE, so a single
  pulse is sufficient).
- **LED polarity**. DE25-Nano LEDs are active-LOW (LED HIGH =
  OFF); the three status outputs are inverted before driving
  the pins. `LED[7:3]` are tied HIGH (OFF).
