# 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.