# Ch318 — LPDDR framebuffer write/readback: board bring-up

ONE bitstream. All test controls are **runtime**, via HPS bridge registers — no rebuild
to go disabled → canary → full. Defaults are safe: **arm OFF, canary ON, base 0x80000000**.
The booted core writes nothing to LPDDR until the HPS arms it.

## Runnable script
`docs/hardware/ps2_lpddr_test.sh` (same style as `ps2_status.sh`; bridge base defaults to
`0x40000000`, `busybox devmem`):
```
./ps2_lpddr_test.sh            # read-only LPDDR status (safe)
./ps2_lpddr_test.sh --canary   # arm canary, verify 32 B vs expected, PASS/FAIL, auto-disarm
./ps2_lpddr_test.sh --full     # arm full frame, hash 8 KiB vs expected md5, PASS/FAIL, auto-disarm
./ps2_lpddr_test.sh --disarm   # force disarm (LPDDR_CTRL=0x2)
```
The manual register/`dd` reference below is what the script automates.

## Build
QSF (already set): `GS_TILE_PSMCT16FB_DEMO=1` + `GS_LPDDR_FB=1` (plus the usual
`GS_RMW_DEMO`). Build/load the `.rbf` once. That's the only build.

## HPS bridge register map (new in Ch318)
Offsets are relative to the **PS2 HPS-bridge base** — the same base `retrodesd` already
uses to reach `CORE_ID`/`OSD_CTRL`/`INPUT_P1` on this core (the HPS2FPGA bridge window).
32-bit accesses.

| Offset | Name          | R/W | Meaning |
|--------|---------------|-----|---------|
| 0x018  | LPDDR_CTRL    | RW  | bit0 = **arm** (1 = permit AXI writes), bit1 = **canary** (1 = write only the 32-byte top line). Reset = 0x2 (disarmed, canary). |
| 0x01C  | LPDDR_FB_BASE | RW  | LPDDR byte base address. Reset = 0x8000_0000. |
| 0x02C  | LPDDR_STATUS  | R   | bit0 = idle, bit1 = bresp error seen, bit2 = FIFO overflow seen. |
| 0x030  | LPDDR_BYTES   | R   | total bytes written. |
| 0x034  | LPDDR_BURSTS  | R   | total 32-byte bursts issued. |

The framebuffer itself is read from **physical LPDDR 0x8000_0000** (the `f2sdram` AXI
address is the HPS physical address — the qsys slave maps a flat 4 GiB), which is the
`reserved` region from `/proc/iomem` (below Linux System RAM at 0x82000000 — safe).

## Canary test (32-byte write, deterministic)
1. Confirm defaults: read `LPDDR_CTRL` (expect 0x2), `LPDDR_FB_BASE` (expect 0x8000_0000).
2. Baseline: `sudo dd if=/dev/mem bs=1 skip=2147483648 count=32 2>/dev/null | hexdump -C`
3. Arm in canary mode: write `LPDDR_CTRL = 0x3` (arm=1, canary=1).
4. Re-read the 32 bytes (same `dd`). Expect the top scanline (PSMCT16 green = 0x8200):
   `00 82 00 82 00 82 00 82  00 82 00 82 00 82 00 82` (×2 lines = 32 bytes).
5. Optional: read `LPDDR_BURSTS` (advancing) + `LPDDR_STATUS` (bit1/bit2 = 0).
   PASS = bytes changed baseline → the `00 82` pattern (fabric reached LPDDR at the
   expected physical address). Then **disarm: write LPDDR_CTRL = 0x2**.

## Full-frame test (8 KiB)
1. Arm full: write `LPDDR_CTRL = 0x1` (arm=1, canary=0).
2. `sudo dd if=/dev/mem bs=4096 skip=524288 count=2 2>/dev/null | md5sum`
   Expect **`3b12baffc00bb6419fa66272c75b2cc7`** (the exact sim image).
3. Confirm `LPDDR_STATUS` bits 1,2 = 0 (no bresp/FIFO errors). Disarm when done (0x2).

## Notes
- `0x80000000` = 2147483648 bytes; `skip=524288` blocks × 4096 = same address.
- Never read/write `0x82000000–0xBFFFFFFF` (live Linux RAM).
- If a hardened kernel blocks `/dev/mem` to the reserved region, use the same
  `devmem`/mmap path the existing runtime uses; if a readback looks stale, it's CPU
  caching of that address — read uncached.
- Scanout from LPDDR is Ch319 — start only after this write/readback passes.
