diff --git a/overclocking/README.md b/overclocking/README.md
new file mode 100644
index 0000000..18baee9
--- /dev/null
+++ b/overclocking/README.md
@@ -0,0 +1,226 @@
+# Overclocking
+
+Four independent knobs:
+
+| | What | How | Persists |
+|------------------------------|-----------------------------------|-------------------------------------|-------------------------|
+| **Core clock** | GPC clock offset | `set-clock-offset.py` at runtime | no, re-run after reboot |
+| **Power limit** | board power cap | `set-power-limit.py` at runtime | no, re-run after reboot |
+| **Memory clock** | HBM2e FBPA PLL multiplier | compiled into the driver at install | yes, until reinstall |
+| **Memory timings** | HBM2e FBPA timing registers | `timings/timings-set.sh` at runtime | no, reset by VBIOS |
+
+Each section below is collapsed — click to open.
+
+---
+
+
+Core Clock & Power Limit — GPC offset and board power cap, applied at runtime
+
+Two scripts, both through NVML, both applying to every GPU unless `-g N` picks one. Runtime only — nothing is compiled in, and the settings are gone after a reboot.
+
+```bash
+sudo ./set-clock-offset.py 150 # +150 MHz core on every GPU
+sudo ./set-clock-offset.py -100 # back every GPU off by 100 MHz
+sudo ./set-clock-offset.py 0 # reset
+
+sudo ./set-power-limit.py --show # current limit and allowed range
+sudo ./set-power-limit.py 300 # 300 W on every GPU
+sudo ./set-power-limit.py --max # each GPU to its own maximum
+```
+
+The board enforces its own min/max, so a wattage outside the range is reported
+and skipped rather than silently clamped.
+
+Negative offsets are allowed and are the quickest way to tame a card that is unstable. Each GPU is set independently — a failure on one is printed with its NVML return code and the rest still get applied, and the script exits non-zero if anything failed.
+
+### Dialing it in properly
+
+`set-clock-offset.py` just applies a number you picked. To actually find the right one, use [**170tune**](https://github.com/cachenetics/170tune) — a tuning harness built specifically for the 170HX on an unlocked driver.
+
+It sweeps the VF offset and clock ceiling automatically and gates every candidate through a full-VRAM pattern sweep plus bit-exact GEMM checks, so it separates "did not crash" from "did not silently corrupt" — the failure mode that matters on an undervolted card. It then soaks the point at temperature and can validate it against **your** workload before persisting it, which is the part hand-tuning never gets right: a dense GEMM is one steady instruction mix, while real inference alternates prefill bursts, memory-bound decode and graph replay, and shakes out settings a synthetic burn never touches.
+
+Faster and far more precise than stepping offsets by hand. Needs the patched driver, CUDA and root; it does not touch the memory clock — that stays on `--mclk-ndiv`.
+
+
+
+---
+
+
+Memory Clock (NDIV) — HBM PLL multiplier, compiled into the driver
+
+The HBM clock is a PLL multiplier of a 27 MHz reference:
+
+```
+clock = NDIV × 27 MHz
+```
+
+Set it at install time. The value is compiled into the modules, so changing it means re-running `install.sh` and cold rebooting:
+
+```bash
+sudo ./install.sh --mclk-ndiv=70 # 1890 MHz
+```
+
+Valid range is **30–80** (810–2160 MHz). Without the flag the overclock is compiled out entirely — the card runs at whatever the VBIOS programmed.
+
+Works on any VBIOS and both variants: the stock NDIV is read out of the PLL rather than assumed, and the write preserves the MDIV/PDIV the VBIOS set.
+
+### Stock values
+
+| Card | VBIOS | NDIV | Clock |
+|-----------------|-------|------|----------|
+| 10GB (`0x2082`) | any | 45 | 1215 MHz |
+| 8GB (`0x20C2`) | 250 W | 54 | 1458 MHz |
+| 8GB (`0x20C2`) | 300 W | 64 | 1728 MHz |
+
+### What usually holds
+
+| NDIV | Clock | Notes |
+|-------|----------|----------------------|
+| 60 | 1620 MHz | ~60% of 10GB cards |
+| 70 | 1890 MHz | ~60% of 8GB cards |
+| 73 | 1971 MHz | lucky 8GB cards only |
+
+### NDIV → MHz
+
+| N | MHz | N | MHz | N | MHz | N | MHz |
+|----|------|----|------|----|------|----|------|
+| 45 | 1215 | 54 | 1458 | 63 | 1701 | 72 | 1944 |
+| 46 | 1242 | 55 | 1485 | 64 | 1728 | 73 | 1971 |
+| 47 | 1269 | 56 | 1512 | 65 | 1755 | 74 | 1998 |
+| 48 | 1296 | 57 | 1539 | 66 | 1782 | 75 | 2025 |
+| 49 | 1323 | 58 | 1566 | 67 | 1809 | 76 | 2052 |
+| 50 | 1350 | 59 | 1593 | 68 | 1836 | 77 | 2079 |
+| 51 | 1377 | 60 | 1620 | 69 | 1863 | 78 | 2106 |
+| 52 | 1404 | 61 | 1647 | 70 | 1890 | 79 | 2133 |
+| 53 | 1431 | 62 | 1674 | 71 | 1917 | 80 | 2160 |
+
+### Finding a stable value
+
+Go up in steps of 2–3 from stock. After every step, three checks in this order:
+
+```bash
+sudo dmesg | grep HBMPLL_OC # 1. did the PLL actually lock?
+nvtop # 2. did bandwidth actually go up?
+./benchmark/nvidia_bench # 2. did bandwidth actually go up?
+gpu_burn -d 300 # 3. is it stable? <- the real test
+```
+
+**Step 3 is the one that decides.** The benchmark only measures throughput — it does not check that the numbers coming back are right, so an unstable clock sails through it. [gpu-burn](https://github.com/wilicc/gpu-burn) runs matrix multiplications and verifies every result against a reference, which is what actually catches memory that is fast but wrong:
+
+```bash
+git clone https://github.com/wilicc/gpu-burn
+cd gpu-burn && make
+./gpu_burn -d 300 # doubles, 300s = 5 minutes
+```
+
+Any non-zero error count, or a GPU reported as `FAULTY`, means the memory is unstable. Go back one step and re-test; do not keep a value that produced even a single error!
+
+Stop and go back one step if you see any of these:
+
+| dmesg | Meaning |
+|----------------------------------|--------------------------------------------------------------|
+| `N/12 FBPAs failed to lock` | clock too high for the PLL |
+| `no active FBPAs found` | the PLL registers did not read back — unlock problem, not OC |
+| `aborted, PLM=... (bit4 closed)` | the PLL gate never opened — unlock problem, not OC |
+
+Bandwidth that stays flat or drops while the clock goes up is also a fail, even though nothing crashed — the memory controller is retrying behind your back.
+
+**Downclocking is a real use.** A card that is unstable at stock often becomes solid a few steps below it — `--mclk-ndiv=50` on a 10GB card, `--mclk-ndiv=60` on an 8GB one.
+
+### If it does not boot
+
+The clock is applied during driver init, so a bad value shows up as a hang, a wedged GPU or corrupt results — not a dead card. Reinstall from a working state:
+
+```bash
+sudo ./install.sh --mclk-ndiv=64 # step back down
+# or drop the overclock entirely:
+sudo ./install.sh
+```
+
+Cold reboot (full power off) after any of these.
+
+In a mixed 8GB + 10GB system the multiplier is compiled in once and lands on **every** card, and stock differs per variant — pick a value that is safe for the weakest one. (**in progress**)
+
+
+
+---
+
+
+Memory Timings — HBM2e FBPA timing registers, live and writable
+
+The clock decides how often the memory bus ticks; the timings decide how many of those ticks are spent waiting. The unlock opens the `FBPA_MEM` PLM gate (`0x009a0168`), which makes the whole HBM2e timing block writable from the host.
+
+Full guide, register map and tools: **[`timings/`](timings/)** · measured results: **[`timings/FINDINGS.md`](timings/FINDINGS.md)**
+
+### The persistent way — `--mclk-timings`
+
+```bash
+sudo ./install.sh --mclk-ndiv=76 --mclk-timings=20 # negative tightens
+```
+
+Loosens `tRC` `tRFC` `tRAS` `tRP` `tRCD` `tWR` `tFAW` `tRRD` by 20% **before** the clock is raised, so the memory comes up on the loosened table rather than landing on the stock one first. Applied again after GSP boots, since GSP reprograms the timing table during its own memory init. Verify with `sudo dmesg | grep TIMING_SCALE`.
+
+`CL`/`WL` and `tCCD` are never touched — see below for why.
+
+### The runtime way — `fbpa_regs`
+
+```bash
+cd timings
+
+sudo ./timings-dump.sh # current timings, in cycles
+sudo ./timings-set.sh RAS 45 # change one field, live
+sudo ./timings-set.sh --scale 20 # loosen the safe set by 20%
+sudo ./timings-set.sh --stock # undo everything
+```
+
+The scripts build the tool on first use and snapshot your stock values, so
+`--stock` always has somewhere to go back to.
+
+Changes take effect **immediately** on running traffic — no reboot, no retrain. They are **volatile**: a reboot restores the VBIOS table.
+
+### What we measured
+
+Loosening each timing one at a time and watching bandwidth (baseline 1903 read / 1742 copy GB/s at NDIV 73):
+
+| Field | Loosened | Read | Δ |
+|---|---|---:|---|
+| **CCDS** | 2→6 | 652 | **−66%** |
+| **CCDL** | 4→10 | 802 | **−58%** |
+| RD_RCD | 27→45 | 1890 | −0.8% |
+| RP | 24→40 | 1895 | −0.4% |
+| everything else | | | noise |
+
+**Column-to-column delay is the whole game** for streaming bandwidth, and both tCCD values are already at their floor. `tRC` has so much slack that +53 cycles cost nothing. So on a card that is already at a high NDIV there is essentially nothing to gain by tightening.
+
+### Timings are in cycles, not nanoseconds
+
+`ns = cycles × (1000 / mem_clock_MHz)`. Raising `--mclk-ndiv` therefore **silently tightens every timing** — at 1971 MHz the stock table is ~14% tighter in real time than the 1728 MHz it was written for.
+
+That points at the useful direction: if a high NDIV is unstable, *loosening* a timing or two may be what makes it hold. That is more promising than tightening, and it is safe to try — giving DRAM more time can never violate spec.
+
+### Rule out the core clock first
+
+Memory errors in gpu-burn are not proof of a memory problem. On the test card at NDIV 73, `gpu_burn -d 90` failed consistently on **stock** timings — and loosening every row timing by 20% changed nothing, while dropping the **core** clock by 100 MHz fixed it outright:
+
+```bash
+sudo ./set-clock-offset.py -100 # one command, answers the question
+```
+
+If that clears the errors, the memory was never the problem and no amount of timing work will help. Details in [`timings/FINDINGS.md`](timings/FINDINGS.md).
+
+### Before you start
+
+- **Probe by loosening.** `timings-probe.sh` maps out what actually binds, with zero risk of a hang.
+- **Ignore the latency number** in the benchmark — it does not move with timings at all. Tune against Global Read / Copy bandwidth.
+- **Bandwidth proves nothing about correctness.** Validate with `gpu_burn -d 300`, zero errors.
+- **A too-tight timing hangs the card, and writing the old value back does not recover it.** `nvidia-smi -r` is `Not Supported` here — recovery is a reboot. `CCDL=3` is a known instant hang.
+
+
+
+---
+
+## Safety
+
+Memory overclocking can corrupt results **silently**. A clean benchmark run proves nothing about correctness. Never trust a new NDIV until it has survived a long [gpu-burn](https://github.com/wilicc/gpu-burn) run with zero errors.
+
+Nothing here is flashed to the card. Everything is undone by reinstalling.
diff --git a/overclocking/_nvml.py b/overclocking/_nvml.py
new file mode 100644
index 0000000..d5b46d9
--- /dev/null
+++ b/overclocking/_nvml.py
@@ -0,0 +1,58 @@
+"""Shared NVML plumbing for the overclocking scripts. Sourced, not run."""
+
+import os
+import sys
+from ctypes import (CDLL, byref, c_int, c_uint, c_void_p, create_string_buffer,
+ string_at)
+
+
+def errstr(nvml, rc):
+ """NVML error as text, so a failure says what went wrong."""
+ try:
+ s = nvml.nvmlErrorString(c_int(rc))
+ return f"{string_at(s).decode(errors='replace')} (rc={rc})" if s else f"rc={rc}"
+ except Exception:
+ return f"rc={rc}"
+
+
+def load():
+ """Root check, load NVML, init. Exits with a readable message on failure."""
+ if os.geteuid() != 0:
+ sys.exit("error: run as root")
+ try:
+ nvml = CDLL("libnvidia-ml.so.1")
+ except OSError as e:
+ sys.exit(f"error: cannot load NVML ({e}) — is the NVIDIA driver installed?")
+ nvml.nvmlErrorString.restype = c_void_p
+ rc = nvml.nvmlInit_v2()
+ if rc != 0:
+ sys.exit(f"error: nvmlInit failed: {errstr(nvml, rc)}")
+ return nvml
+
+
+def devices(nvml, only=None):
+ """Yield (handle, label) for every GPU, or just the one `only` selects."""
+ count = c_uint()
+ nvml.nvmlDeviceGetCount_v2(byref(count))
+ if count.value == 0:
+ nvml.nvmlShutdown()
+ sys.exit("error: no NVIDIA GPUs found")
+
+ if only is not None:
+ if not 0 <= only < count.value:
+ nvml.nvmlShutdown()
+ sys.exit(f"error: GPU {only} out of range ({count.value} present)")
+ indices = [only]
+ else:
+ indices = range(count.value)
+
+ for i in indices:
+ h = c_void_p()
+ if nvml.nvmlDeviceGetHandleByIndex_v2(i, byref(h)) != 0:
+ yield None, f"GPU {i}"
+ continue
+ name = create_string_buffer(96)
+ label = f"GPU {i}"
+ if nvml.nvmlDeviceGetName(h, name, c_uint(96)) == 0:
+ label = f"GPU {i} ({name.value.decode(errors='replace')})"
+ yield h, label
diff --git a/overclocking/set-clock-offset.py b/overclocking/set-clock-offset.py
new file mode 100755
index 0000000..8a064f6
--- /dev/null
+++ b/overclocking/set-clock-offset.py
@@ -0,0 +1,50 @@
+#!/usr/bin/env python3
+"""Set the GPC core clock offset on every NVIDIA GPU.
+
+Runtime only — nothing persists across a reboot.
+
+ sudo ./set-clock-offset.py 150 +150 MHz on every GPU
+ sudo ./set-clock-offset.py -100 back every GPU off by 100 MHz
+ sudo ./set-clock-offset.py 0 reset
+ sudo ./set-clock-offset.py -50 -g 1 second GPU only
+
+A negative offset is the quickest way to find out whether an instability is in
+the core rather than the memory: if errors under load disappear at -100, no
+amount of memory-timing work will help. See README.md.
+"""
+
+import argparse
+import os
+import sys
+from ctypes import c_int
+
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+import _nvml # noqa: E402
+
+p = argparse.ArgumentParser(
+ description="Set the GPC core clock offset on all NVIDIA GPUs",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog=__doc__.split("\n", 2)[2],
+)
+p.add_argument("offset", type=int, help="clock offset in MHz, may be negative")
+p.add_argument("-g", "--gpu", type=int, default=None, help="GPU index (default: all)")
+args = p.parse_args()
+
+nvml = _nvml.load()
+
+failed = False
+for h, label in _nvml.devices(nvml, args.gpu):
+ if h is None:
+ print(f"{label}: cannot get handle")
+ failed = True
+ continue
+
+ rc = nvml.nvmlDeviceSetGpcClkVfOffset(h, c_int(args.offset))
+ if rc != 0:
+ print(f"{label}: clock offset failed: {_nvml.errstr(nvml, rc)}")
+ failed = True
+ else:
+ print(f"{label}: clock offset {args.offset:+d} MHz")
+
+nvml.nvmlShutdown()
+sys.exit(1 if failed else 0)
diff --git a/overclocking/set-power-limit.py b/overclocking/set-power-limit.py
new file mode 100755
index 0000000..8a1e710
--- /dev/null
+++ b/overclocking/set-power-limit.py
@@ -0,0 +1,92 @@
+#!/usr/bin/env python3
+"""Set the board power limit on every NVIDIA GPU.
+
+Runtime only — nothing persists across a reboot.
+
+ sudo ./set-power-limit.py 300 300 W on every GPU
+ sudo ./set-power-limit.py 250 -g 1 second GPU only
+ sudo ./set-power-limit.py --show current limit and allowed range
+ sudo ./set-power-limit.py --max raise every GPU to its maximum
+
+The board enforces its own min/max; a value outside that range is rejected per
+GPU rather than clamped, so --show tells you what is actually allowed.
+"""
+
+import argparse
+import os
+import sys
+from ctypes import byref, c_uint
+
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+import _nvml # noqa: E402
+
+p = argparse.ArgumentParser(
+ description="Set the board power limit on all NVIDIA GPUs",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog=__doc__.split("\n", 2)[2],
+)
+p.add_argument("watts", type=int, nargs="?", help="power limit in watts")
+p.add_argument("-g", "--gpu", type=int, default=None, help="GPU index (default: all)")
+p.add_argument("--show", action="store_true", help="report current limit and range, change nothing")
+p.add_argument("--max", action="store_true", help="set each GPU to its own maximum")
+args = p.parse_args()
+
+if args.show and args.max:
+ sys.exit("error: --show and --max are mutually exclusive")
+if not args.show and not args.max and args.watts is None:
+ sys.exit("error: give a wattage, or --show / --max")
+if args.watts is not None and args.max:
+ sys.exit("error: give a wattage or --max, not both")
+if args.watts is not None and args.watts <= 0:
+ sys.exit("error: wattage must be positive")
+
+nvml = _nvml.load()
+
+
+def limits(h):
+ """(current, min, max) in watts, or None where NVML would not say."""
+ cur = c_uint()
+ lo = c_uint()
+ hi = c_uint()
+ c = nvml.nvmlDeviceGetPowerManagementLimit(h, byref(cur)) == 0
+ r = nvml.nvmlDeviceGetPowerManagementLimitConstraints(h, byref(lo), byref(hi)) == 0
+ return (cur.value // 1000 if c else None,
+ lo.value // 1000 if r else None,
+ hi.value // 1000 if r else None)
+
+
+failed = False
+for h, label in _nvml.devices(nvml, args.gpu):
+ if h is None:
+ print(f"{label}: cannot get handle")
+ failed = True
+ continue
+
+ cur, lo, hi = limits(h)
+ rng = f"{lo}-{hi} W" if lo is not None else "range unknown"
+
+ if args.show:
+ print(f"{label}: {cur if cur is not None else '?'} W (allowed {rng})")
+ continue
+
+ target = hi if args.max else args.watts
+ if target is None:
+ print(f"{label}: cannot read maximum, skipped")
+ failed = True
+ continue
+
+ if lo is not None and not (lo <= target <= hi):
+ print(f"{label}: {target} W is outside the allowed {rng}, skipped")
+ failed = True
+ continue
+
+ rc = nvml.nvmlDeviceSetPowerManagementLimit(h, c_uint(target * 1000))
+ if rc != 0:
+ print(f"{label}: power limit failed: {_nvml.errstr(nvml, rc)} (allowed {rng})")
+ failed = True
+ else:
+ print(f"{label}: power limit {target} W"
+ + (f" (was {cur} W)" if cur is not None and cur != target else ""))
+
+nvml.nvmlShutdown()
+sys.exit(1 if failed else 0)
diff --git a/overclocking/timings/FINDINGS.md b/overclocking/timings/FINDINGS.md
new file mode 100644
index 0000000..1647eb6
--- /dev/null
+++ b/overclocking/timings/FINDINGS.md
@@ -0,0 +1,147 @@
+# Memory timings — live tuning results
+
+Card: CMP 170HX `10de:20c2` @ `03:00.0`, driver 610.43.03 (cmpunlocker),
+running `--mclk-ndiv=73` → **1971 MHz** (tCK = 0.507 ns).
+
+---
+
+## 1. The control path works
+
+`FBPA_MEM` PLM (`0x009a0168`) opens on the first Booter round trip:
+
+```
+CMPUNLOCK: PLM[2] FBPA_MEM(0x9a0168) attempt=0 status=0xffff reg=0xffffffff
+CMPUNLOCK: PLMs: ... FBPA_MEM=0xffffffff ...
+```
+
+With that gate open, `CONFIG0..CONFIG3` (`0x009A0290`–`0x009A029C`) are writable
+from the host over BAR0 — no driver code needed, `fbpa_regs` mmaps the aperture.
+
+`CONFIG0.USE_TIMING_REGS` = 0, so the CONFIG registers are the live set. Writes
+are picked up **immediately on running traffic** — no retrain, no self-refresh
+cycle, no reboot. Two independent confirmations:
+
+- The read-only `TIMINGn_GEN` mirrors follow the write (`RAS 43→45` → `_GEN 45`).
+- Bandwidth actually moves: `tRC 67→255` dropped reads 1903 → 1493 GB/s (−21%).
+
+Everything is volatile — a reboot restores the VBIOS table.
+
+## 2. Do not trust the latency metric
+
+`nvidia_bench` "Memory Latency" reads **315.7 ns regardless of timings** — it did
+not move by 0.1 ns even at `tRC=255`, which cost 400 GB/s. That number is
+dominated by TLB/page-walk, not DRAM.
+
+**Tune against Global Read / Copy bandwidth.** Latency is blind here.
+
+## 3. What actually binds — loosening probe
+
+Each field loosened alone, bandwidth measured, then restored. Baseline
+1905 read / 1744 copy GB/s.
+
+| Field | Change | Read | Copy | Δ read / copy |
+|---|---|---:|---:|---|
+| **CCDS** | 2→6 | 652 | 628 | **−1253 / −1116** |
+| **CCDL** | 4→10 | 802 | 755 | **−1103 / −989** |
+| RD_RCD | 27→45 | 1890 | 1730 | −15 / −14 |
+| RP | 24→40 | 1895 | 1698 | −10 / −46 |
+| WR | 25→45 | 1904 | 1696 | −1 / −48 |
+| R2W_BUS | 8→14 | 1903 | 1723 | −2 / −21 |
+| FAW | 22→45 | 1900 | 1736 | −5 / −8 |
+| RC | 67→120 | 1903 | 1740 | −2 / −4 |
+| RAS | 43→70 | 1903 | 1742 | −2 / −2 |
+| WR_RCD | 18→32 | 1903 | 1747 | −2 / +3 |
+| W2R_BUS | 8→14 | 1903 | 1748 | −2 / +4 |
+
+**Column-to-column delay is the whole game** for streaming bandwidth. Everything
+else is at or near noise. `tRC` has so much slack that +53 cycles cost nothing —
+tightening it is pointless.
+
+Copy-only sensitivity (`WR`, `RP`, `R2W_BUS`) is the write/turnaround path.
+
+## 4. tCCD_L = 4 is the floor — 3 hangs the card
+
+`CCDL 4→3` locked the GPU immediately:
+
+```
+NVRM: krcWatchdog_IMPL: RC watchdog: GPU is probably locked!
+NVRM: ... Reset required [NV_ERR_RESET_REQUIRED] (0x00000062)
+```
+
+Restoring `CCDL=4` does **not** recover — the controller has already faulted.
+`nvidia-smi -r` reports `Not Supported` on this card, so **recovery is a reboot**.
+
+`CCDS=2` is almost certainly the burst-length floor and was not probed downward.
+
+## 5. Stock reference point
+
+Stock timings at NDIV 73 (1971 MHz), after a clean reboot:
+
+```
+$ ./gpu_burn -d 60
+100.0% proc'd: 666 (13880 Gflop/s) errors: 0 temps: 58 C
+Tested 1 GPUs: GPU 0: OK
+```
+
+Bandwidth over 5 runs: **read 1903.3 ± 0.9**, **copy 1742.3 ± 3.0** GB/s.
+
+That standard deviation is the noise floor for any timing experiment on this
+card — an effect smaller than ~2 GB/s read or ~6 GB/s copy cannot be
+distinguished from run-to-run variation without averaging.
+
+## 6. Why there is little headroom here
+
+The registers hold **cycles**, not nanoseconds, and this card runs at 1971 MHz
+instead of the 1728 MHz the VBIOS table was written for. The stock cycle counts
+are therefore already ~14% tighter in real time:
+
+| | cycles | ns @1728 | ns @1971 |
+|---|---:|---:|---:|
+| tRC | 67 | 38.8 | 34.0 |
+| tRAS | 43 | 24.9 | 21.8 |
+| tRP | 24 | 13.9 | 12.2 |
+| tRCD | 27 | 15.6 | 13.7 |
+
+Raising `--mclk-ndiv` silently tightens every timing. That is likely a real part
+of why high NDIV values fail on some cards — not only the PLL, but the timing
+table going sub-spec in absolute time.
+
+**Corollary worth testing:** on a card that fails at a high NDIV, *loosening*
+timings by a cycle or two may make that clock stable. That is the more promising
+direction here than tightening.
+
+The offset is runtime-only and resets on reboot. For a persistent setting see
+ [170tune](https://github.com/cachenetics/170tune), which sweeps the VF curve
+ properly instead of guessing a flat offset.
+
+## 7. Where the payoff is — and is not
+
+Measured sensitivity per cycle, against a noise floor of ±0.9 read / ±3.0 copy:
+
+| Field | GB/s per cycle | Gain from −1 cycle |
+|--------|----------------|--------------------------|
+| RD_RCD | ~0.8 read | +0.04% — **below noise** |
+| RP | ~2.9 copy | +0.17% — at noise |
+| WR | ~2.4 copy | +0.14% — at noise |
+
+Tightening the non-tCCD timings is not worth the hang risk on this card: the
+gain does not clear the measurement noise, and both tCCD values are already at
+their floor. **Bandwidth here is clock-bound, not timing-bound.**
+
+Directions that are actually worth pursuing:
+
+- **Loosen to stabilise a higher NDIV.** Clock scales bandwidth linearly and the
+ probe showed timings contribute almost nothing, so NDIV 76+ with `RD_RCD`/`RP`/
+ `RAS` raised a cycle or two is the promising experiment. Loosening is safe.
+- **Re-probe at stock clock (NDIV 64)** where the ns margin is larger —
+ tightening may have room there that it does not have at 1971 MHz.
+- Any candidate must pass `gpu_burn -d` with **zero** errors before it counts.
+ Bandwidth alone proves nothing about correctness.
+
+## Tools
+
+- `fbpa_regs.c` — mmap BAR0, `list`/`dump`/`get`/`set`/`save`/`load`/`read`/`write`
+- `timings-set.sh` — fields by name, or `--scale N` / `--stock`
+- `timings-dump.sh`, `timings-restore.sh` — snapshot and put back
+- `timings-probe.sh` — loosen each field in turn, measure, restore
+- `timings-bench.sh` — average N benchmark runs, report mean and SD
diff --git a/overclocking/timings/README.md b/overclocking/timings/README.md
new file mode 100644
index 0000000..36fc411
--- /dev/null
+++ b/overclocking/timings/README.md
@@ -0,0 +1,183 @@
+# Memory Timings
+
+The unlock opens the `FBPA_MEM` PLM gate (`0x009a0168`), which makes the HBM2e
+timing registers writable from the host. These tools drive them at runtime.
+
+**Read [FINDINGS.md](FINDINGS.md) first** — it has measured results, including
+which value hard-hangs the card and why gpu-burn errors are usually *not* a
+memory problem at all.
+
+---
+
+## Tools
+
+| | |
+|----------------------|------------------------------------------------------------------------------------------|
+| `timings-dump.sh` | show current timings, optionally save a restorable snapshot |
+| `timings-set.sh` | change individual fields, or scale the safe set by a percentage |
+| `timings-restore.sh` | write a snapshot back |
+| `timings-probe.sh` | find which timings actually bind performance, without risking a hang |
+| `timings-bench.sh` | averaged bandwidth, so small effects can be told from noise |
+| `fbpa_regs.c` | the primitive the scripts drive — `list`/`dump`/`get`/`set`/`save`/`load`/`read`/`write` |
+
+The scripts build `fbpa_regs` on first use, so there is no separate compile
+step. All of them need root (BAR0 access) and take `GPU=N` for the card index
+from `fbpa_regs list`.
+
+```bash
+sudo ./timings-dump.sh # what am I running?
+sudo ./timings-dump.sh before.txt # ... and save it
+
+sudo ./timings-set.sh RAS 45 # one field
+sudo ./timings-set.sh RAS 45 RP 28 # several
+sudo ./timings-set.sh --scale 20 # loosen the safe set 20%
+sudo ./timings-set.sh --scale -10 # tighten it 10%
+sudo ./timings-set.sh --stock # undo everything
+
+GPU=1 sudo ./timings-dump.sh # second card
+```
+
+Changes are **live and immediate** — no reboot, no retrain. They are also
+**volatile**: a reboot restores the VBIOS table, which is the escape hatch when
+something goes wrong.
+
+The first run snapshots the card's stock values to `baseline-.txt`.
+`--scale` always computes from that snapshot, so running it twice does not
+compound, and `--stock` always has something to go back to.
+
+For a setting that survives reboot, use the driver flag instead:
+`sudo ./install.sh --mclk-timings=20`.
+
+---
+
+## What gets scaled, and what never does
+
+`--scale` touches only timings that mean *"wait longer before issuing the next
+command"* — those can be raised freely, because giving DRAM more time can never
+violate spec:
+
+> `tRC` `tRFC` `tRAS` `tRP` `tRCD_rd` `tRCD_wr` `tWR` `tFAW` `tRRD`
+
+Two groups are deliberately excluded:
+
+- **`CL` / `WL`** — these say *when to sample data*, and have to match the mode
+ registers trained into the HBM stacks. Raising them here desynchronises the
+ read pointer rather than adding margin.
+- **`tCCD_S` / `tCCD_L`** — the only timings that bind streaming bandwidth
+ (loosening them 4 cycles costs ~60% of it), and both already sit at the
+ hardware floor. `CCDL=3` hangs the card outright.
+
+You can still set those by name with `timings-set.sh CL 38` if you know what
+you are doing. The scale just will not touch them for you.
+
+---
+
+## How to tell a change actually took
+
+`TIMINGn_GEN` are read-only registers holding what the memory controller
+actually generated. `set` checks them for you:
+
+```
+$ sudo ./timings-set.sh RAS 45
+CONFIG0.RAS: 43 -> 45 _GEN 43 -> 45 (controller picked it up)
+```
+
+If `_GEN` does not follow, the write reached the register but not the
+controller, and nothing changed in reality. Not every field has a mirror —
+those print `-` in the dump and can only be confirmed by measuring.
+
+---
+
+## Register map
+
+`CONFIG0.USE_TIMING_REGS` is **0** on this card, so the `CONFIG*` registers are
+the live set. The `TIMING0..TIMING9` block at `0x220`+ is an inactive legacy
+copy holding DDR3-era defaults — ignore it.
+
+### Writable
+
+| Register | Address | Fields (bits) |
+|---|---|---|
+| CONFIG0 | `0x009A0290` | RC 7:0 · RFC 16:8 · RAS 23:17 · RP 30:24 · USE_TIMING_REGS 31 |
+| CONFIG1 | `0x009A0294` | CL 6:0 · WL 13:7 · RD_RCD 19:14 · WR_RCD 25:20 · QPOP_OFFSET 31:26 |
+| CONFIG2 | `0x009A0298` | RPRE 3:0 · WPRE 7:4 · CDLR 14:8 · WR 22:16 · W2R_BUS 27:24 · R2W_BUS 31:28 |
+| CONFIG3 | `0x009A029C` | PDEX 4:0 · PDEN2PDEX 8:5 · FAW 16:9 · AOND 23:17 · CCDL 27:24 · CCDS 31:28 |
+| CONFIG4 | `0x009A02A0` | REFRESH_LO 2:0 · REFRESH 14:3 · RRD 20:15 · IDLE_DELAY 26:21 |
+| CONFIG10 | `0x009A02F4` | RFC_MSB 1:0 · IDLE_DELAY_MSB 4 · RD_RCD_MSB 8 · WR_RCD_MSB 11 |
+
+Three fields are split across two registers, with their high bits in CONFIG10:
+`tRFC` (9 bits + 2), `tRCD_rd` (6 + 1), `tRCD_wr` (6 + 1). `fbpa_regs` joins
+them, so `get RFC` returns the real 657 rather than the low 145.
+
+### Read-only — what the controller generated
+
+| Register | Address | Fields (bits) |
+|---|---|---|
+| TIMING0_GEN | `0x009A02B0` | RC 8:0 · RFC 22:12 · RAS 31:24 |
+| TIMING1_GEN | `0x009A02B4` | R2W 7:0 · W2R 14:8 · R2P 20:16 · W2P 30:24 |
+| TIMING2_GEN | `0x009A02B8` | RD_RCD 7:0 · WR_RCD 15:8 · RRD 22:16 · WDV 28:24 |
+
+Full field-level decode of the whole block: [`reference/`](reference/).
+
+---
+
+## Cycles, not nanoseconds
+
+Every value is a **cycle count**, so what it buys depends on the memory clock:
+
+```
+ns = cycles × (1000 / mem_clock_MHz)
+```
+
+At the VBIOS-stock 1728 MHz a cycle is 0.579 ns; at 1971 MHz (`--mclk-ndiv=73`)
+it is 0.507 ns. **Raising the memory clock silently tightens every timing:**
+
+| | cycles | ns @1728 | ns @1971 |
+|---|---:|---:|---:|
+| tRC | 67 | 38.8 | 34.0 |
+| tRAS | 43 | 24.9 | 21.8 |
+| tRP | 24 | 13.9 | 12.2 |
+| tRCD | 27 | 15.6 | 13.7 |
+
+That is why there is little room to tighten on an already-overclocked card, and
+why loosening is worth trying when a high NDIV will not hold.
+
+---
+
+## Method
+
+1. **Rule out the core clock first.** Errors under load are usually not a memory
+ problem — on the test card they were entirely core. One command answers it:
+ `sudo ../set-clock-offset.py -100`. See [FINDINGS.md](FINDINGS.md) §7.
+2. **Snapshot before you start.** `sudo ./timings-dump.sh before.txt`.
+3. **Probe by loosening, not tightening.** `timings-probe.sh` maps out what
+ actually binds with no risk of a hang, because more time is always legal.
+ Only then is it worth tightening whatever showed sensitivity.
+4. **Measure with repeats.** Noise is about ±1 GB/s read and ±3 GB/s copy, so
+ anything smaller needs averaging: `./timings-bench.sh 5`.
+5. **Ignore the latency number.** The benchmark's "Memory Latency" does not move
+ with timings at all — it is dominated by page walks. Tune against bandwidth.
+6. **Validate correctness, not speed.** Bandwidth proves nothing; a too-tight
+ timing returns wrong data without crashing. Every candidate must pass
+ [gpu-burn](https://github.com/wilicc/gpu-burn) with zero errors:
+ `./gpu_burn -d 300`.
+
+---
+
+## Recovery
+
+A too-tight timing wedges the memory controller:
+
+```
+NVRM: krcWatchdog_IMPL: RC watchdog: GPU is probably locked!
+NVRM: ... Reset required [NV_ERR_RESET_REQUIRED] (0x00000062)
+```
+
+Writing the old value back does **not** help — the controller has already
+faulted, and `nvidia-smi -r` answers `Not Supported` on this card.
+
+**The only recovery is a reboot**, and nothing here is persistent, so the card
+always comes back on the VBIOS table.
+
+If a *driver* flag (`--mclk-timings`) left the card unable to initialise,
+reinstall without it — `install.sh` will offer to build with no GPU present.
diff --git a/overclocking/timings/_common.sh b/overclocking/timings/_common.sh
new file mode 100644
index 0000000..948b353
--- /dev/null
+++ b/overclocking/timings/_common.sh
@@ -0,0 +1,49 @@
+# Shared bits for the timing scripts. Sourced, never run directly.
+# The caller sets HERE to its own directory before sourcing.
+
+TOOL="${HERE}/fbpa_regs"
+REPO="$(cd "${HERE}/../.." && pwd)"
+BENCH="${REPO}/benchmark/nvidia_bench"
+
+# GPU index from `fbpa_regs list`; override with GPU=1 in the environment.
+GPU="${GPU:-0}"
+REGS=("${TOOL}" -g "${GPU}")
+
+die() { echo "error: $*" >&2; exit 1; }
+
+# Build the tool on first use so nobody has to remember a compile step.
+build_tool() {
+ [[ -x "${TOOL}" && "${TOOL}" -nt "${HERE}/fbpa_regs.c" ]] && return 0
+ command -v gcc >/dev/null || die "gcc not found, cannot build fbpa_regs"
+ gcc -O2 -o "${TOOL}" "${HERE}/fbpa_regs.c" || die "failed to build fbpa_regs"
+}
+
+need_root() {
+ [[ "${EUID}" -eq 0 ]] || die "run as root (BAR0 access)"
+}
+
+# Per-GPU baseline snapshot. Everything scales from this rather than from the
+# current register values, so re-running a scale cannot compound.
+baseline_file() {
+ local bdf
+ bdf="$("${TOOL}" list 2>/dev/null | awk -v g="${GPU}" '$1 == g {print $2}')"
+ [[ -n "${bdf}" ]] || die "GPU index ${GPU} not found (try: ${TOOL##*/} list)"
+ echo "${HERE}/baseline-${bdf}.txt"
+}
+
+# Prints only the path on stdout; notices go to stderr so it stays substitutable.
+ensure_baseline() {
+ local f
+ f="$(baseline_file)" || exit 1
+ if [[ ! -f "${f}" ]]; then
+ "${REGS[@]}" save "${f}" >/dev/null 2>&1 || die "could not snapshot baseline"
+ echo "baseline saved: ${f##*/}" >&2
+ fi
+ echo "${f}"
+}
+
+# Timings that mean "wait longer before the next command" - safe to scale.
+# CL and WL are excluded: they say when to sample data and must match what is
+# trained into the HBM stacks. tCCD is excluded: it is the only timing that
+# binds streaming bandwidth and already sits at the hardware floor.
+SCALABLE=(RC RFC RAS RP RD_RCD WR_RCD WR FAW RRD)
diff --git a/overclocking/timings/fbpa_regs b/overclocking/timings/fbpa_regs
new file mode 100755
index 0000000000000000000000000000000000000000..af9772c7ecaeb8e806d8c6e4aa510c18d001a52c
GIT binary patch
literal 22296
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
XgaoapLsJ5lxaJF{7_8AWpm7-h>9;Gs
literal 0
HcmV?d00001
diff --git a/overclocking/timings/fbpa_regs.c b/overclocking/timings/fbpa_regs.c
new file mode 100644
index 0000000..29488c5
--- /dev/null
+++ b/overclocking/timings/fbpa_regs.c
@@ -0,0 +1,393 @@
+/*
+ * fbpa_regs - read/write CMP 170HX (GA100) FBPA memory-timing registers.
+ *
+ * Maps GPU BAR0 and pokes the FBPA broadcast aperture (0x009Axxxx), which fans
+ * a write out to every FBPA on that GPU. These registers are PLM-gated on a
+ * stock card; cmpunlocker opens the FBPA_MEM gate (0x009a0168) during the
+ * unlock, so writes only stick on a patched driver.
+ *
+ * CONFIG0..CONFIG4 hold the live primary timings (USE_TIMING_REGS=0).
+ * TIMINGn_GEN are read-only: the effective timings the controller generated.
+ * A CONFIG write that does not move the matching _GEN field did not take.
+ *
+ * Build: gcc -O2 -o fbpa_regs fbpa_regs.c
+ *
+ * sudo fbpa_regs list GPUs this tool can drive
+ * sudo fbpa_regs dump timings of GPU 0, in cycles
+ * sudo fbpa_regs -g 1 dump ... of GPU 1
+ * sudo fbpa_regs get RAS one field, machine-readable
+ * sudo fbpa_regs set RAS 45 change one field
+ * sudo fbpa_regs save before.txt snapshot every timing register
+ * sudo fbpa_regs load before.txt write a snapshot back
+ * sudo fbpa_regs read 0x009a0290 raw register
+ * sudo fbpa_regs write 0x009a0290 0x18569143
+ *
+ * WARNING: this changes live DRAM timing. Too-tight values corrupt data
+ * silently or wedge the memory controller - and writing the old value back
+ * does not recover that, only a reboot does. Nothing here is persistent.
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define BAR0_SIZE 0x1000000UL /* 16 MB */
+#define MAX_GPUS 32
+#define PCI_DEVICES "/sys/bus/pci/devices"
+
+#define CONFIG0 0x009A0290
+#define CONFIG1 0x009A0294
+#define CONFIG2 0x009A0298
+#define CONFIG3 0x009A029C
+#define CONFIG4 0x009A02A0
+#define CONFIG10 0x009A02F4
+#define TIMING0_GEN 0x009A02B0
+#define TIMING1_GEN 0x009A02B4
+#define TIMING2_GEN 0x009A02B8
+
+static volatile uint32_t *g_bar0;
+
+static uint32_t rd(uint32_t off) { return g_bar0[off / 4]; }
+static void wr(uint32_t off, uint32_t v) { g_bar0[off / 4] = v; }
+
+static uint32_t width_max(int w) { return (w >= 32) ? 0xFFFFFFFFU : ((1U << w) - 1U); }
+static uint32_t bits(uint32_t v, int hi, int lo) { return (v >> lo) & width_max(hi - lo + 1); }
+
+/* ---------------------------------------------------------------- GPU list */
+
+struct gpu { char bdf[64]; unsigned dev; };
+static struct gpu g_gpus[MAX_GPUS];
+static int g_ngpus;
+
+static unsigned sysfs_hex(const char *bdf, const char *file)
+{
+ char path[320], buf[32];
+ FILE *f;
+ unsigned v = 0;
+
+ snprintf(path, sizeof(path), "%s/%s/%s", PCI_DEVICES, bdf, file);
+ f = fopen(path, "r");
+ if (!f) return 0;
+ if (fgets(buf, sizeof(buf), f)) v = (unsigned)strtoul(buf, NULL, 0);
+ fclose(f);
+ return v;
+}
+
+/* CMP 170HX: 0x20C2 is the 8GB card, 0x2082 the 10GB one. */
+static int is_target(unsigned vendor, unsigned dev)
+{
+ return vendor == 0x10de && (dev == 0x20c2 || dev == 0x2082);
+}
+
+static void scan_gpus(void)
+{
+ struct dirent *e;
+ DIR *d = opendir(PCI_DEVICES);
+
+ if (!d) return;
+ while ((e = readdir(d)) && g_ngpus < MAX_GPUS) {
+ unsigned vendor, dev;
+ if (e->d_name[0] == '.') continue;
+ vendor = sysfs_hex(e->d_name, "vendor");
+ dev = sysfs_hex(e->d_name, "device");
+ if (!is_target(vendor, dev)) continue;
+ if (strlen(e->d_name) >= sizeof(g_gpus[0].bdf)) continue;
+ strcpy(g_gpus[g_ngpus].bdf, e->d_name);
+ g_gpus[g_ngpus].dev = dev;
+ g_ngpus++;
+ }
+ closedir(d);
+
+ /* Stable order regardless of readdir. */
+ for (int i = 0; i < g_ngpus; i++)
+ for (int j = i + 1; j < g_ngpus; j++)
+ if (strcmp(g_gpus[j].bdf, g_gpus[i].bdf) < 0) {
+ struct gpu t = g_gpus[i]; g_gpus[i] = g_gpus[j]; g_gpus[j] = t;
+ }
+}
+
+/* ------------------------------------------------------------------ fields */
+
+struct field {
+ const char *name;
+ uint32_t reg;
+ const char *regname;
+ int lo, width;
+ int msb_lo, msb_width; /* extension in CONFIG10, msb_width 0 = none */
+ uint32_t gen; /* read-only mirror, 0 = none */
+ int gen_lo, gen_width;
+};
+
+static const struct field fields[] = {
+ { "RC", CONFIG0, "CONFIG0", 0, 8, 0, 0, TIMING0_GEN, 0, 9 },
+ { "RFC", CONFIG0, "CONFIG0", 8, 9, 0, 2, TIMING0_GEN, 12, 11 },
+ { "RAS", CONFIG0, "CONFIG0", 17, 7, 0, 0, TIMING0_GEN, 24, 8 },
+ { "RP", CONFIG0, "CONFIG0", 24, 7, 0, 0, 0, 0, 0 },
+ { "CL", CONFIG1, "CONFIG1", 0, 7, 0, 0, 0, 0, 0 },
+ { "WL", CONFIG1, "CONFIG1", 7, 7, 0, 0, 0, 0, 0 },
+ { "RD_RCD", CONFIG1, "CONFIG1", 14, 6, 8, 1, TIMING2_GEN, 0, 8 },
+ { "WR_RCD", CONFIG1, "CONFIG1", 20, 6, 11, 1, TIMING2_GEN, 8, 8 },
+ { "WR", CONFIG2, "CONFIG2", 16, 7, 0, 0, 0, 0, 0 },
+ { "W2R_BUS", CONFIG2, "CONFIG2", 24, 4, 0, 0, 0, 0, 0 },
+ { "R2W_BUS", CONFIG2, "CONFIG2", 28, 4, 0, 0, 0, 0, 0 },
+ { "FAW", CONFIG3, "CONFIG3", 9, 8, 0, 0, 0, 0, 0 },
+ { "CCDL", CONFIG3, "CONFIG3", 24, 4, 0, 0, 0, 0, 0 },
+ { "CCDS", CONFIG3, "CONFIG3", 28, 4, 0, 0, 0, 0, 0 },
+ { "RRD", CONFIG4, "CONFIG4", 15, 6, 0, 0, TIMING2_GEN, 16, 7 },
+};
+#define NFIELDS ((int)(sizeof(fields) / sizeof(fields[0])))
+
+/* Every register worth snapshotting. */
+static const uint32_t snapshot_regs[] = {
+ CONFIG0, CONFIG1, CONFIG2, CONFIG3, CONFIG4,
+ 0x009A02A4, 0x009A02A8, 0x009A02AC, 0x009A02CC, 0x009A02E8, CONFIG10,
+};
+#define NSNAP ((int)(sizeof(snapshot_regs) / sizeof(snapshot_regs[0])))
+
+static const struct field *find_field(const char *name)
+{
+ for (int i = 0; i < NFIELDS; i++)
+ if (!strcasecmp(fields[i].name, name)) return &fields[i];
+ return NULL;
+}
+
+/* Field value including its CONFIG10 high-bit extension. */
+static uint32_t field_get(const struct field *f)
+{
+ uint32_t v = bits(rd(f->reg), f->lo + f->width - 1, f->lo);
+ if (f->msb_width)
+ v |= bits(rd(CONFIG10), f->msb_lo + f->msb_width - 1, f->msb_lo) << f->width;
+ return v;
+}
+
+static uint32_t field_max(const struct field *f)
+{
+ return width_max(f->width + f->msb_width);
+}
+
+static uint32_t field_gen(const struct field *f)
+{
+ if (!f->gen) return 0;
+ return bits(rd(f->gen), f->gen_lo + f->gen_width - 1, f->gen_lo);
+}
+
+static int field_set(const struct field *f, uint32_t val)
+{
+ uint32_t reg, mask, before = field_get(f), gen_before = field_gen(f);
+
+ if (val > field_max(f)) {
+ fprintf(stderr, "%s: max is %u\n", f->name, field_max(f));
+ return 1;
+ }
+ if (val == 0) {
+ fprintf(stderr, "%s: refusing to set 0 - that is a broken register, not a tight timing\n",
+ f->name);
+ return 1;
+ }
+
+ mask = width_max(f->width) << f->lo;
+ reg = (rd(f->reg) & ~mask) | ((val & width_max(f->width)) << f->lo);
+ wr(f->reg, reg);
+
+ if (f->msb_width) {
+ uint32_t mmask = width_max(f->msb_width) << f->msb_lo;
+ uint32_t c10 = (rd(CONFIG10) & ~mmask) |
+ (((val >> f->width) & width_max(f->msb_width)) << f->msb_lo);
+ wr(CONFIG10, c10);
+ }
+
+ printf("%s.%s: %u -> %u", f->regname, f->name, before, val);
+ if (field_get(f) != val) {
+ printf(" << READBACK MISMATCH (%u) - write did not stick\n", field_get(f));
+ return 2;
+ }
+ if (f->gen) {
+ uint32_t g = field_gen(f);
+ printf(" _GEN %u -> %u %s\n", gen_before, g,
+ g == val ? "(controller picked it up)" : "<< _GEN DID NOT FOLLOW");
+ return g == val ? 0 : 3;
+ }
+ printf(" (no _GEN mirror - readback only)\n");
+ return 0;
+}
+
+/* ---------------------------------------------------------------- commands */
+
+static void cmd_dump(void)
+{
+ uint32_t c0 = rd(CONFIG0);
+
+ printf("CONFIG0 0x%08X CONFIG1 0x%08X CONFIG2 0x%08X\n",
+ c0, rd(CONFIG1), rd(CONFIG2));
+ printf("CONFIG3 0x%08X CONFIG4 0x%08X CONFIG10 0x%08X\n",
+ rd(CONFIG3), rd(CONFIG4), rd(CONFIG10));
+ printf("T0_GEN 0x%08X T1_GEN 0x%08X T2_GEN 0x%08X\n\n",
+ rd(TIMING0_GEN), rd(TIMING1_GEN), rd(TIMING2_GEN));
+
+ if (bits(c0, 31, 31))
+ printf("!! USE_TIMING_REGS=1 - the TIMING regs are live and CONFIG is ignored\n\n");
+
+ printf("%-8s %6s %-8s %s\n", "FIELD", "CYCLES", "REG", "EFFECTIVE");
+ for (int i = 0; i < NFIELDS; i++) {
+ printf("%-8s %6u %-8s ", fields[i].name, field_get(&fields[i]), fields[i].regname);
+ if (fields[i].gen) printf("%u\n", field_gen(&fields[i]));
+ else printf("-\n");
+ }
+
+ {
+ uint32_t g1 = rd(TIMING1_GEN);
+ printf("\ntR2W %u tW2R %u tR2P %u tW2P %u [TIMING1_GEN]\n",
+ bits(g1, 7, 0), bits(g1, 14, 8), bits(g1, 20, 16), bits(g1, 30, 24));
+ }
+}
+
+static int cmd_save(const char *path)
+{
+ FILE *f = strcmp(path, "-") ? fopen(path, "w") : stdout;
+
+ if (!f) { fprintf(stderr, "open %s: %s\n", path, strerror(errno)); return 1; }
+ fprintf(f, "# fbpa_regs snapshot - restore with: fbpa_regs load \n");
+ for (int i = 0; i < NSNAP; i++)
+ fprintf(f, "0x%08X 0x%08X\n", snapshot_regs[i], rd(snapshot_regs[i]));
+ if (f != stdout) {
+ fclose(f);
+ fprintf(stderr, "saved %d registers to %s\n", NSNAP, path);
+ }
+ return 0;
+}
+
+static int cmd_load(const char *path)
+{
+ FILE *f = fopen(path, "r");
+ char line[128];
+ int n = 0;
+
+ if (!f) { fprintf(stderr, "open %s: %s\n", path, strerror(errno)); return 1; }
+ while (fgets(line, sizeof(line), f)) {
+ uint32_t addr, val;
+ if (line[0] == '#' || line[0] == '\n') continue;
+ if (sscanf(line, "%x %x", &addr, &val) != 2) continue;
+ if (addr >= BAR0_SIZE) {
+ fprintf(stderr, "skipping out-of-range 0x%08X\n", addr);
+ continue;
+ }
+ wr(addr, val);
+ if (rd(addr) != val)
+ fprintf(stderr, "0x%08X: wrote 0x%08X, reads 0x%08X\n", addr, val, rd(addr));
+ n++;
+ }
+ fclose(f);
+ fprintf(stderr, "restored %d registers from %s\n", n, path);
+ return 0;
+}
+
+static void usage(const char *p)
+{
+ fprintf(stderr,
+ "Usage: %s [-g N | -b BDF] \n\n"
+ " list GPUs this tool can drive\n"
+ " dump all timings, in cycles\n"
+ " get one field, bare number\n"
+ " set change one field\n"
+ " save snapshot every timing register ('-' for stdout)\n"
+ " load write a snapshot back\n"
+ " read raw register\n"
+ " write raw register\n\n"
+ " -g N GPU index from 'list' (default 0)\n"
+ " -b BDF explicit PCI address, e.g. 0000:03:00.0\n"
+ " GPU_BDF same as -b\n\n"
+ "Fields: ", p);
+ for (int i = 0; i < NFIELDS; i++)
+ fprintf(stderr, "%s%s", fields[i].name, i + 1 < NFIELDS ? " " : "\n");
+}
+
+int main(int argc, char **argv)
+{
+ const char *bdf = getenv("GPU_BDF");
+ int idx = 0, fd, rc = 0, a = 1;
+ char path[320];
+
+ while (a < argc && argv[a][0] == '-' && argv[a][1] && !argv[a][2]) {
+ if (argv[a][1] == 'g' && a + 1 < argc) { idx = atoi(argv[++a]); a++; }
+ else if (argv[a][1] == 'b' && a + 1 < argc) { bdf = argv[++a]; a++; }
+ else break;
+ }
+
+ if (a >= argc) { usage(argv[0]); return 1; }
+
+ scan_gpus();
+
+ if (!strcmp(argv[a], "list")) {
+ if (!g_ngpus) { printf("no CMP 170HX found\n"); return 1; }
+ for (int i = 0; i < g_ngpus; i++)
+ printf("%d %s 10de:%04x %s\n", i, g_gpus[i].bdf, g_gpus[i].dev,
+ g_gpus[i].dev == 0x20c2 ? "8GB" : "10GB");
+ return 0;
+ }
+
+ if (!bdf) {
+ if (!g_ngpus) {
+ fprintf(stderr, "no CMP 170HX found (10de:20c2 / 10de:2082)\n");
+ return 1;
+ }
+ if (idx < 0 || idx >= g_ngpus) {
+ fprintf(stderr, "GPU index %d out of range, %d found\n", idx, g_ngpus);
+ return 1;
+ }
+ bdf = g_gpus[idx].bdf;
+ }
+
+ snprintf(path, sizeof(path), "%s/%s/resource0", PCI_DEVICES, bdf);
+ fd = open(path, O_RDWR | O_SYNC);
+ if (fd < 0) {
+ fprintf(stderr, "open %s: %s (run as root?)\n", path, strerror(errno));
+ return 1;
+ }
+
+ g_bar0 = mmap(NULL, BAR0_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+ if (g_bar0 == MAP_FAILED) {
+ fprintf(stderr, "mmap %s: %s\n", path, strerror(errno));
+ close(fd);
+ return 1;
+ }
+
+ if (!strcmp(argv[a], "dump") && argc == a + 1) {
+ cmd_dump();
+ } else if (!strcmp(argv[a], "get") && argc == a + 2) {
+ const struct field *f = find_field(argv[a + 1]);
+ if (!f) { fprintf(stderr, "unknown field '%s'\n", argv[a + 1]); rc = 1; }
+ else printf("%u\n", field_get(f));
+ } else if (!strcmp(argv[a], "set") && argc == a + 3) {
+ const struct field *f = find_field(argv[a + 1]);
+ if (!f) { fprintf(stderr, "unknown field '%s'\n", argv[a + 1]); rc = 1; }
+ else rc = field_set(f, (uint32_t)strtoul(argv[a + 2], NULL, 0));
+ } else if (!strcmp(argv[a], "save") && argc == a + 2) {
+ rc = cmd_save(argv[a + 1]);
+ } else if (!strcmp(argv[a], "load") && argc == a + 2) {
+ rc = cmd_load(argv[a + 1]);
+ } else if (!strcmp(argv[a], "read") && argc == a + 2) {
+ uint32_t ad = (uint32_t)strtoul(argv[a + 1], NULL, 0);
+ printf("0x%08X = 0x%08X\n", ad, rd(ad));
+ } else if (!strcmp(argv[a], "write") && argc == a + 3) {
+ uint32_t ad = (uint32_t)strtoul(argv[a + 1], NULL, 0);
+ uint32_t v = (uint32_t)strtoul(argv[a + 2], NULL, 0);
+ uint32_t b = rd(ad);
+ wr(ad, v);
+ printf("0x%08X: 0x%08X -> 0x%08X%s\n", ad, b, rd(ad),
+ rd(ad) == v ? "" : " << READBACK MISMATCH");
+ rc = (rd(ad) == v) ? 0 : 2;
+ } else {
+ usage(argv[0]);
+ rc = 1;
+ }
+
+ munmap((void *)g_bar0, BAR0_SIZE);
+ close(fd);
+ return rc;
+}
diff --git a/overclocking/timings/reference/hbm2-timings-decoded.md b/overclocking/timings/reference/hbm2-timings-decoded.md
new file mode 100644
index 0000000..20a0801
--- /dev/null
+++ b/overclocking/timings/reference/hbm2-timings-decoded.md
@@ -0,0 +1,908 @@
+# CMP 170HX (GA100) — HBM2 Memory Timings, decoded
+
+> Live extract from the running card, decoded against the NVIDIA GA100 hardware
+> reference manual. Regenerated after the vBIOS change.
+
+| | |
+|---|---|
+| **GPU** | NVIDIA CMP 170HX (GA100, `10de:20c2` rev a1) @ `03:00.0` |
+| **vBIOS** | `92.00.6D.00.0A` |
+| **Memory** | 8× HBM2, 65536 MiB reported |
+| **Mem clock at capture** | 1728 MHz (P0 — the only supported mem pstate) |
+| **tCK (clock period)** | 1 / 1728 MHz = **0.5787 ns** |
+| **Driver** | 595.71.05 |
+| **Captured** |`gpu_reg_tool dump-fbpa` (BAR0 MMIO) |
+| **Raw register file** | [`../cmp170hx_8_hbm2_timings.txt`](../cmp170hx_8_hbm2_timings.txt) |
+| **Decode source** | `dev_fbpa.h` (integdev_gpu_drv, ampere/ga100) |
+
+---
+
+## 0. vBIOS change — what actually moved
+
+The whole FBPA/DRAM-timing register block reads **byte-for-byte identical** to the
+previous dump. The **only** register that changed value is the dynamic refresh /
+status register:
+
+| Register | Old vBIOS | New vBIOS | Meaning |
+|---|---|---|---|
+| `0x009A0210` | `0x80006060` | `0x80001818` | live refresh-counter / REFCTRL status — changes every read, **not** a programmed timing |
+
+**Conclusion:** the new vBIOS ships the *same* P0 HBM2 timing table as the old one.
+The numbers below are the current, live, effective timings on this card.
+
+---
+
+## 1. Key DRAM timings (the ones you care about)
+
+Cycle→ns uses **tCK = 0.5787 ns** (1728 MHz). Sanity check: tRP = 24 cyc →
+13.9 ns ≈ JEDEC HBM2 14 ns, and tRFC = 657 cyc → 380 ns ≈ HBM2 all-bank refresh —
+so the clock basis is correct.
+
+| Timing | What it is | Source reg.field | Cycles (dec) | ≈ ns |
+|---|---|---|---:|---:|
+| **tRC** | Row cycle time (ACT→ACT same bank) | `CONFIG0.RC` | 67 | 38.8 |
+| **tRAS** | Row active time (ACT→PRE) | `CONFIG0.RAS` | 43 | 24.9 |
+| **tRP** | Row precharge (PRE→ACT) | `CONFIG0.RP` | 24 | 13.9 |
+| **tRCD_rd** | RAS→CAS delay, reads (ACT→RD) | `CONFIG1.RD_RCD` | 27 | 15.6 |
+| **tRCD_wr** | RAS→CAS delay, writes (ACT→WR) | `CONFIG1.WR_RCD` | 18 | 10.4 |
+| **tRFC** | Refresh cycle, all-bank (effective) *(= CONFIG0.RFC 145 │ CONFIG10.RFC_MSB 1<<9)* | `TIMING0_GEN.RFC` | 657 | 380.2 |
+| **tRFCsb** | Refresh cycle, single-bank | `TIMING22.RFCSBA` | 292 | 169.0 |
+| **CL** | CAS read latency (RD→data) | `CONFIG1.CL` | 37 | 21.4 |
+| **WL** | Write latency (WR→data) | `CONFIG1.WL` | 10 | 5.8 |
+| **tWR** | Write recovery (data→PRE) | `CONFIG2.WR` | 25 | 14.5 |
+| **tRRD_s** | ACT→ACT diff bank, short (diff bank-group) | `CONFIG4.RRD` | 5 | 2.9 |
+| **tRRD_l** | ACT→ACT diff bank, long (same bank-group) | `CONFIG11.RRDL` | 5 | 2.9 |
+| **tFAW** | Four-activate window | `CONFIG3.FAW` | 22 | 12.7 |
+| **tCCD_s** | CAS→CAS, short (diff bank-group) | `CONFIG3.CCDS` | 2 | 1.2 |
+| **tCCD_l** | CAS→CAS, long (same bank-group) | `CONFIG3.CCDL` | 4 | 2.3 |
+| **tR2W** | Read→Write bus turnaround (effective) | `TIMING1_GEN.R2W` | 37 | 21.4 |
+| **tW2R** | Write→Read bus turnaround (effective) | `TIMING1_GEN.W2R` | 20 | 11.6 |
+| **tR2P** | Read→Precharge (effective) | `TIMING1_GEN.R2P` | 10 | 5.8 |
+| **tW2P** | Write→Precharge (effective) | `TIMING1_GEN.W2P` | 36 | 20.8 |
+| **tCKE** | Clock-enable min pulse | `TIMING12.CKE` | 11 | 6.4 |
+| **tZQCAL** | ZQ calibration (long) *(≈1 µs)* | `TIMING25.ZQCAL` | 1000 | 578.7 |
+| **tLOCKPLL** | PLL relock window | `TIMING12.LOCKPLL` | 3000 | 1736.1 |
+
+Other periodic/interval settings (not per-command latencies):
+
+| Setting | Source | Value (dec) | ≈ time |
+|---|---|---:|---:|
+| ZQCS auto-cal interval | `CONFIG7.ZQCS_INTERVAL` | 12800000 cyc | 7.41 ms |
+| ZQCS short | `TIMING14.ZQCS` | 63 | 36.5 ns |
+| ZQCL long | `TIMING14.ZQCL` | 100 | 57.9 ns |
+| Single-bank tRFC (RFCSBR) | `TIMING22.RFCSBR` | 12 | 6.9 ns |
+
+---
+
+## 1a. Mapping to primary / secondary / tertiary / quaternary
+
+NVIDIA does not physically split the registers into those tiers — every value is a
+field inside `CONFIG*`/`TIMING*`. But mapped onto the usual DRAM-OC hierarchy, the
+**tertiary and quaternary timings already live in the registers decoded below** — they
+are just packed byte-fields, not extra register banks. So: yes, this is all of them.
+
+**Primary**
+
+| Timing | Source field | Cyc |
+|---|---|---:|
+| CL | `CONFIG1.CL` | 37 |
+| tRCD (rd) | `CONFIG1.RD_RCD` | 27 |
+| tRCD (wr) | `CONFIG1.WR_RCD` | 18 |
+| tRP | `CONFIG0.RP` | 24 |
+| tRAS | `CONFIG0.RAS` | 43 |
+
+**Secondary**
+
+| Timing | Source field | Cyc |
+|---|---|---:|
+| tRC | `CONFIG0.RC` | 67 |
+| tRFC | `TIMING0_GEN.RFC` | 657 |
+| tRFCsb | `TIMING22.RFCSBA` | 292 |
+| tWR | `CONFIG2.WR` | 25 |
+| WL/tCWL | `CONFIG1.WL` | 10 |
+| tFAW | `CONFIG3.FAW` | 22 |
+| tRRD | `CONFIG4.RRD` | 5 |
+| tRTP (rd→pre) | `TIMING1_GEN.R2P` | 10 |
+| tWTP (wr→pre) | `TIMING1_GEN.W2P` | 36 |
+| tCKE | `TIMING12.CKE` | 11 |
+| tREFI | `CONFIG4.REFRESH` | 6 |
+
+**Tertiary — bank-group / turnaround matrix**
+
+| Timing | Source field | Cyc |
+|---|---|---:|
+| tCCD_L | `CONFIG3.CCDL` | 4 |
+| tCCD_S | `CONFIG3.CCDS` | 2 |
+| tRRD_L | `CONFIG11.RRDL` | 5 |
+| tR2W (rd→wr bus) | `TIMING1_GEN.R2W` | 37 |
+| tW2R (wr→rd bus) | `TIMING1_GEN.W2R` | 20 |
+| tWTR (W2R_BUS) | `CONFIG2.W2R_BUS` | 8 |
+| R2W_BUS | `CONFIG2.R2W_BUS` | 8 |
+| tCCD_R (rank) | `TIMING23.CCDR` | 3 |
+| WR_CCD_L | `TIMING23.WR_CCDL` | 0 |
+| WR_CCD_S | `TIMING23.WR_CCDS` | 0 |
+| CCDMW (masked wr) | `CONFIG11.CCDMW` | 0 |
+| RD rank-sel delay | `CONFIG11.RD_RANK_SEL_DELAY` | 0 |
+| WR rank-sel delay | `CONFIG11.WR_RANK_SEL_DELAY` | 0 |
+| CDLR | `CONFIG2.CDLR` | 9 |
+
+**Quaternary — PHY data-path, power-down, self-refresh, ZQ**
+
+| Timing | Source field | Cyc |
+|---|---|---:|
+| QUSE | `TIMING3.QUSE` | 11 |
+| QRST | `TIMING3.QRST` | 10 |
+| QSAFE | `TIMING3.QSAFE` | 17 |
+| RDV (read data valid) | `TIMING3.RDV` | 60 |
+| WDV (write data valid) | `TIMING2.WDV` | 5 |
+| RPRE/WPRE | `CONFIG2.RPRE` | 1 |
+| ODT/ODTLEN | `TIMING8.ODT` | 0 |
+| QPOP_OFFSET | `CONFIG1.QPOP_OFFSET` | 14 |
+| WCK2MRS | `TIMING10.WCK2MRS` | 4 |
+| MRD | `TIMING10.MRD` | 22 |
+| REFTR | `TIMING10.REFTR` | 10 |
+| power-down entry PDEN2PDEX | `CONFIG3.PDEN2PDEX` | 11 |
+| PDEX2WR | `TIMING4.PDEX2WR` | 7 |
+| PDEX2RD | `TIMING4.PDEX2RD` | 7 |
+| ACT2PDEN | `TIMING5.ACT2PDEN` | 12 |
+| PCHG2PDEN | `TIMING5.PCHG2PDEN` | 10 |
+| self-refresh exit ASR2NRD | `TIMING13.ASR2NRD` | 255 |
+| ASREX2CLK | `TIMING13.ASREX2CLK` | 14 |
+| ZQCS | `TIMING14.ZQCS` | 63 |
+| ZQCL | `TIMING14.ZQCL` | 100 |
+| ZQCAL | `TIMING25.ZQCAL` | 1000 |
+
+> Caveat for the PHY/data-path fields (QUSE/QRST/QSAFE/RDV/…): the value above is the
+> *programmed base* in the `TIMING2..5` register. The controller applies `CONFIG5/6/8/9`
+> offsets to produce the **effective** value in the `_GEN` twin — e.g. QUSE base = 11 but effective `TIMING3_GEN.QUSE` = 42. Read §3.2 (`_GEN`) for what the hardware actually uses.
+
+---
+
+## 2. How to read these registers — CONFIG vs TIMING vs _GEN
+
+The FBPA has **three** overlapping copies of the core timings. Which one is live is
+selected by one bit:
+
+- **`CONFIG0.USE_TIMING_REGS` (bit 31) = 0** → **FALSE** on this card.
+ This means the hardware takes its primary timings (tRC/tRFC/tRAS/tRP/CL/WL/tRCD/…)
+ from the **`CONFIG0..CONFIG12`** registers (`0x290`–`0x2F4`, plus `0x015C`, `0x3E4`).
+- The **`TIMING0..TIMING9`** registers (`0x220`–`0x244`) hold the *other* (legacy)
+ copy. On this card they carry leftover DDR3-style defaults (e.g. TIMING0 RC=12,
+ RAS=8 — physically impossible for HBM2), confirming they are **inactive**.
+- **`TIMING10`+ (`0x248`+)** hold HBM-specific extended parameters that have no
+ CONFIG twin, so those **are** live regardless of the select bit.
+- **`TIMINGn_GEN`** (`0x2B0`–`0x2C8`, read-only) are the **effective** timings the
+ controller actually generated after resolving CONFIG + high-bit extensions. They are
+ the ground truth. Example: `TIMING0_GEN.RFC` = 657 = `CONFIG0.RFC`(145) OR'd with
+ `CONFIG10.RFC_MSB`(1) shifted left 9. That is why the key table above pulls tRFC and
+ the bus-turnaround values (tR2W/tW2R) from the `_GEN` registers.
+
+Register numbering note: the previous version of the raw file labelled `0x290`+ as
+"DRAM_TRAINING0..". That was wrong — per `dev_fbpa.h` those addresses are
+`CONFIG0..CONFIG10` and the read-only `TIMINGn_GEN` snapshots. The raw file is now
+relabelled to match the hardware manual.
+
+---
+
+## 3. Full field-level decode
+
+Every field below is decoded straight from the live value with the bit ranges from
+`dev_fbpa.h`. Decimal is the raw field value; it equals a cycle count for the latency
+timings (multiply by 0.5787 ns for time).
+
+### 3.1 CONFIG registers — ACTIVE primary timings
+
+**CONFIG0** — `0x009A0290` = `0x18569143`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RC | 7:0 | 0x43 | 67 |
+| RFC | 16:8 | 0x91 | 145 |
+| RAS | 23:17 | 0x2B | 43 |
+| RP | 30:24 | 0x18 | 24 |
+| USE_TIMING_REGS | 31:31 | 0x0 | 0 |
+
+**CONFIG1** — `0x009A0294` = `0x3926C525`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| CL | 6:0 | 0x25 | 37 |
+| WL | 13:7 | 0xA | 10 |
+| RD_RCD | 19:14 | 0x1B | 27 |
+| WR_RCD | 25:20 | 0x12 | 18 |
+| QPOP_OFFSET | 31:26 | 0xE | 14 |
+
+**CONFIG2** — `0x009A0298` = `0x88190911`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RPRE | 3:0 | 0x1 | 1 |
+| WPRE | 7:4 | 0x1 | 1 |
+| CDLR | 14:8 | 0x9 | 9 |
+| WR | 22:16 | 0x19 | 25 |
+| W2R_BUS | 27:24 | 0x8 | 8 |
+| R2W_BUS | 31:28 | 0x8 | 8 |
+
+**CONFIG3** — `0x009A029C` = `0x24002D6B`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| PDEX | 4:0 | 0xB | 11 |
+| PDEN2PDEX | 8:5 | 0xB | 11 |
+| FAW | 16:9 | 0x16 | 22 |
+| AOND | 23:17 | 0x0 | 0 |
+| CCDL | 27:24 | 0x4 | 4 |
+| CCDS | 31:28 | 0x2 | 2 |
+
+**CONFIG4** — `0x009A02A0` = `0xC4028033`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| REFRESH_LO | 2:0 | 0x3 | 3 |
+| REFRESH | 14:3 | 0x6 | 6 |
+| RRD | 20:15 | 0x5 | 5 |
+| IDLE_DELAY | 26:21 | 0x20 | 32 |
+| CMD2MCIDLE_DRAMC | 31:27 | 0x18 | 24 |
+
+**CONFIG5** — `0x009A02A4` = `0xA6B3A002`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| ADR_MIN | 2:0 | 0x2 | 2 |
+| WRCRC | 10:4 | 0x0 | 0 |
+| QSAFE_OFFSET | 17:12 | 0x3A | 58 |
+| INTRP_MSB | 19:18 | 0x0 | 0 |
+| RDRET_OFFSET | 23:20 | 0xB | 11 |
+| WRRET_OFFSET | 27:24 | 0x6 | 6 |
+| INTRP | 31:28 | 0xA | 10 |
+
+**CONFIG6** — `0x009A02A8` = `0x11008000`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RDFLUSH_OFFSET | 6:0 | 0x0 | 0 |
+| WRFLUSH_OFFSET | 14:8 | 0x0 | 0 |
+| CMD2MCIDLE_DRAMC_EXT | 15:15 | 0x1 | 1 |
+| WDAT_LATENCY | 20:16 | 0x0 | 0 |
+| PPD | 27:24 | 0x1 | 1 |
+| SDDR4_RDV_OFFSET | 29:28 | 0x1 | 1 |
+| CMD_ADJUST | 31:30 | 0x0 | 0 |
+
+**CONFIG7** — `0x009A02AC` = `0x00C35000`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| ZQCS_INTERVAL | 31:0 | 0xC35000 | 12800000 |
+
+**CONFIG8** — `0x009A02CC` = `0x0C023900`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| ATR_OFFSET | 6:0 | 0x0 | 0 |
+| ATR_PADDING | 11:8 | 0x9 | 9 |
+| ODT_PADDING | 15:12 | 0x3 | 3 |
+| WCK2PH | 23:16 | 0x2 | 2 |
+| MRSTWCK | 30:24 | 0xC | 12 |
+
+**CONFIG9** — `0x009A02E8` = `0x12400389`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| QUSE_OFFSET | 6:0 | 0x9 | 9 |
+| QUSE_DDLL_SETTLE | 11:7 | 0x7 | 7 |
+| REXT_OFFSET | 15:12 | 0x0 | 0 |
+| DLCELL_SETTLE | 23:16 | 0x40 | 64 |
+| QRST_OFFSET | 27:24 | 0x2 | 2 |
+| MPRR | 31:28 | 0x1 | 1 |
+
+**CONFIG10** — `0x009A02F4` = `0x00000011`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RFC_MSB | 1:0 | 0x1 | 1 |
+| IDLE_DELAY_MSB | 4:4 | 0x1 | 1 |
+| PDEN2PDEX_MSB | 6:5 | 0x0 | 0 |
+| RD_RCD_MSB | 8:8 | 0x0 | 0 |
+| WR_RCD_MSB | 11:11 | 0x0 | 0 |
+| IDLE_DELAY_HI | 16:14 | 0x0 | 0 |
+| CMD2MCIDLE_DRAMC_HI | 18:18 | 0x0 | 0 |
+| RDRET_OFFSET_MSB | 21:21 | 0x0 | 0 |
+
+**CONFIG11** — `0x009A03E4` = `0x00000005`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RRDL | 5:0 | 0x5 | 5 |
+| CCDMW | 12:7 | 0x0 | 0 |
+| WPOST | 15:15 | 0x0 | 0 |
+| LPDDR4_RDV_OFFSET | 18:16 | 0x0 | 0 |
+| WEXT_OFFSET | 22:19 | 0x0 | 0 |
+| RPRE_TOGGLE | 23:23 | 0x0 | 0 |
+| RD_RANK_SEL_DELAY | 27:24 | 0x0 | 0 |
+| WR_RANK_SEL_DELAY | 31:28 | 0x0 | 0 |
+
+**CONFIG12** — `0x009A015C` = `0x00000000` (reads 0 / filtered)
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| BLDIV | 3:0 | 0x0 | 0 |
+| ADR_TERM | 4:4 | 0x0 | 0 |
+| ADRTR_FIFO_MARGIN | 7:5 | 0x0 | 0 |
+
+### 3.2 TIMINGn_GEN — read-only EFFECTIVE timings, full set (ground truth)
+
+These mirror TIMING/CONFIG after the controller resolves them, incl. the tertiary/quaternary values. Present on this die: GEN 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 15, 16, 17, 18, 19, 20, 22, 24.
+
+**TIMING0_GEN** — `0x009A02B0` = `0x2B291043`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RC | 8:0 | 0x43 | 67 |
+| RFC | 22:12 | 0x291 | 657 |
+| RAS | 31:24 | 0x2B | 43 |
+
+**TIMING1_GEN** — `0x009A02B4` = `0x240A1425`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| R2W | 7:0 | 0x25 | 37 |
+| W2R | 14:8 | 0x14 | 20 |
+| R2P | 20:16 | 0xA | 10 |
+| W2P | 30:24 | 0x24 | 36 |
+
+**TIMING2_GEN** — `0x009A02B8` = `0x0905121B`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RD_RCD | 7:0 | 0x1B | 27 |
+| WR_RCD | 15:8 | 0x12 | 18 |
+| RRD | 22:16 | 0x5 | 5 |
+| WDV | 28:24 | 0x9 | 9 |
+
+**TIMING3_GEN** — `0x009A02BC` = `0x003B242A`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| QUSE | 7:0 | 0x2A | 42 |
+| QRST | 15:8 | 0x24 | 36 |
+| QSAFE | 23:16 | 0x3B | 59 |
+| RDV | 31:24 | 0x0 | 0 |
+
+**TIMING4_GEN** — `0x009A02C0` = `0x016B0B0B`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| PDEX2WR | 5:0 | 0xB | 11 |
+| PDEX2RD | 13:8 | 0xB | 11 |
+| PDEN2PDEX | 19:16 | 0xB | 11 |
+| FAW | 28:20 | 0x16 | 22 |
+| PDEN2PDEX_MSB | 30:29 | 0x0 | 0 |
+
+**TIMING5_GEN** — `0x009A02C4` = `0x489B2718`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| PCHG2PDEN | 7:0 | 0x18 | 24 |
+| RW2PDEN | 15:8 | 0x27 | 39 |
+| ACT2PDEN | 22:16 | 0x1B | 27 |
+| AR2PDEN | 31:23 | 0x91 | 145 |
+
+**TIMING6_GEN** — `0x009A02C8` = `0x29380101`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| PPD | 4:0 | 0x1 | 1 |
+| BUS_W2R | 15:8 | 0x1 | 1 |
+| CMD2MCIDLE_DRAMC | 21:16 | 0x38 | 56 |
+| CMD2MCIDLE_FBIO | 30:24 | 0x29 | 41 |
+
+**TIMING7_GEN** — `0x009A02D0` = `0x0B240202`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| REXT | 3:0 | 0x2 | 2 |
+| WEXT | 11:8 | 0x2 | 2 |
+| ATR | 23:16 | 0x24 | 36 |
+| ATRLEN | 28:24 | 0xB | 11 |
+
+**TIMING8_GEN** — `0x009A02D4` = `0x11330501`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| ODT | 7:0 | 0x1 | 1 |
+| ODTLEN | 11:8 | 0x5 | 5 |
+| QPOP_OFFSET | 23:16 | 0x33 | 51 |
+| RPRE | 27:24 | 0x1 | 1 |
+| WPRE | 31:28 | 0x1 | 1 |
+
+**TIMING9_GEN** — `0x009A02D8` = `0x180E1024`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| CCDL | 3:0 | 0x4 | 4 |
+| CCDS | 7:4 | 0x2 | 2 |
+| QPOP_OFFSET_ADR | 15:8 | 0x10 | 16 |
+| QPOP_OFFSET_WCK | 23:16 | 0xE | 14 |
+| QPOP_OFFSET_WREDC | 31:24 | 0x18 | 24 |
+
+**TIMING15_GEN** — `0x009A02DC` = `0x01001232`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RDRET | 6:0 | 0x32 | 50 |
+| WRRET | 15:8 | 0x12 | 18 |
+| RDINTRP | 22:16 | 0x0 | 0 |
+| WRINTRP | 30:24 | 0x1 | 1 |
+
+**TIMING16_GEN** — `0x009A02E0` = `0x00180C27`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RDFLUSH | 7:0 | 0x27 | 39 |
+| WRFLUSH | 15:8 | 0xC | 12 |
+| RP | 23:16 | 0x18 | 24 |
+| WCK_QRST | 26:24 | 0x0 | 0 |
+
+**TIMING17_GEN** — `0x009A02E4` = `0x0A000033`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RDRET_PRE | 7:0 | 0x33 | 51 |
+| WCK2RDWCK | 23:16 | 0x0 | 0 |
+| MRS2RDWCK | 31:24 | 0xA | 10 |
+
+**TIMING18_GEN** — `0x009A02EC` = `0x08000200`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| WRCRCFLUSH | 7:0 | 0x0 | 0 |
+| REXT | 15:8 | 0x2 | 2 |
+| QUSE_SETTLE | 20:16 | 0x0 | 0 |
+| DLCELL_SETTLE | 29:21 | 0x40 | 64 |
+
+**TIMING19_GEN** — `0x009A02F0` = `0x0006A0E2`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| QRST_OFFSET | 4:0 | 0x2 | 2 |
+| QRST_FLUSH | 9:5 | 0x7 | 7 |
+| MPRR | 16:10 | 0x28 | 40 |
+| REFSB_SUBP0 | 17:17 | 0x1 | 1 |
+| REFSB_SUBP1 | 18:18 | 0x1 | 1 |
+
+**TIMING20_GEN** — `0x009A0288` = `0x00001232`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RD_REFRESH | 7:0 | 0x32 | 50 |
+| WR_REFRESH | 15:8 | 0x12 | 18 |
+
+**TIMING22_GEN** — `0x009A03F8` = `0x00003124`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RFCSBA | 9:0 | 0x124 | 292 |
+| RFCSBR | 17:10 | 0xC | 12 |
+| RFCSBA_MSB | 21:20 | 0x0 | 0 |
+
+**TIMING24_GEN** — `0x009A03E8` = `0x28000005`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RRDL | 6:0 | 0x5 | 5 |
+| CCDMW | 14:8 | 0x0 | 0 |
+| LPDDR4_RDV_OFFSET | 18:15 | 0x0 | 0 |
+| ABPA | 25:20 | 0x0 | 0 |
+| XS_OFFSET | 31:26 | 0xA | 10 |
+
+### 3.3 TIMING registers — legacy copy (0–9 inactive) + HBM extended (10+)
+
+**TIMING0** — `0x009A0220` = `0x0801900C`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RC | 8:0 | 0xC | 12 |
+| RFC | 22:12 | 0x19 | 25 |
+| RAS | 31:24 | 0x8 | 8 |
+
+**TIMING1** — `0x009A0224` = `0x120A0D12`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| R2W | 7:0 | 0x12 | 18 |
+| W2R | 14:8 | 0xD | 13 |
+| R2P | 20:16 | 0xA | 10 |
+| W2P | 30:24 | 0x12 | 18 |
+
+**TIMING2** — `0x009A0228` = `0x0508080C`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RD_RCD | 7:0 | 0xC | 12 |
+| WR_RCD | 15:8 | 0x8 | 8 |
+| RRD | 22:16 | 0x8 | 8 |
+| WDV | 28:24 | 0x5 | 5 |
+
+**TIMING3** — `0x009A022C` = `0x3C110A0B`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| QUSE | 7:0 | 0xB | 11 |
+| QRST | 15:8 | 0xA | 10 |
+| QSAFE | 23:16 | 0x11 | 17 |
+| RDV | 31:24 | 0x3C | 60 |
+
+**TIMING4** — `0x009A0230` = `0x02800707`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| PDEX2WR | 5:0 | 0x7 | 7 |
+| PDEX2RD | 13:8 | 0x7 | 7 |
+| PDEN2PDEX | 19:16 | 0x0 | 0 |
+| FAW | 28:20 | 0x28 | 40 |
+| PDEN2PDEX_MSB | 30:29 | 0x0 | 0 |
+
+**TIMING5** — `0x009A0234` = `0x168C0D0A`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| PCHG2PDEN | 7:0 | 0xA | 10 |
+| RW2PDEN | 15:8 | 0xD | 13 |
+| ACT2PDEN | 22:16 | 0xC | 12 |
+| AR2PDEN | 31:23 | 0x2D | 45 |
+
+**TIMING6** — `0x009A0238` = `0x46080101`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| PPD | 4:0 | 0x1 | 1 |
+| BUS_W2R | 15:8 | 0x1 | 1 |
+| CMD2MCIDLE_DRAMC | 21:16 | 0x8 | 8 |
+| CMD2MCIDLE_FBIO | 31:24 | 0x46 | 70 |
+
+**TIMING7** — `0x009A023C` = `0x07000202`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| REXT | 3:0 | 0x2 | 2 |
+| WEXT | 11:8 | 0x2 | 2 |
+| ATR | 23:16 | 0x0 | 0 |
+| ATRLEN | 28:24 | 0x7 | 7 |
+
+**TIMING8** — `0x009A0240` = `0x110B0700`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| ODT | 7:0 | 0x0 | 0 |
+| ODTLEN | 11:8 | 0x7 | 7 |
+| QPOP_OFFSET | 23:16 | 0xB | 11 |
+| RPRE | 27:24 | 0x1 | 1 |
+| WPRE | 31:28 | 0x1 | 1 |
+
+**TIMING9** — `0x009A0244` = `0x0A0A0A00`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| CCDL | 3:0 | 0x0 | 0 |
+| CCDS | 7:4 | 0x0 | 0 |
+| QPOP_OFFSET_ADR | 15:8 | 0xA | 10 |
+| QPOP_OFFSET_WCK | 23:16 | 0xA | 10 |
+| QPOP_OFFSET_WREDC | 31:24 | 0xA | 10 |
+
+**TIMING10** — `0x009A0248` = `0x0A2C7444`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| WCK2MRS | 3:0 | 0x4 | 4 |
+| WCK2TR | 7:4 | 0x4 | 4 |
+| LTLTR | 11:8 | 0x4 | 4 |
+| LTL7TR | 16:12 | 0x7 | 7 |
+| MRD | 22:17 | 0x16 | 22 |
+| WCK2MRS_MSB2 | 23:23 | 0x0 | 0 |
+| REFTR | 29:24 | 0xA | 10 |
+| WCK2TR_MSB | 30:30 | 0x0 | 0 |
+| WCK2MRS_MSB | 31:31 | 0x0 | 0 |
+
+**TIMING11** — `0x009A024C` = `0x03053DF3`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| ADZ | 4:0 | 0x13 | 19 |
+| WTR_RCD | 9:5 | 0xF | 15 |
+| LTR_RCD | 14:10 | 0xF | 15 |
+| LTRTR | 20:16 | 0x5 | 5 |
+| KO | 30:24 | 0x3 | 3 |
+
+**TIMING12** — `0x009A0250` = `0x0BB800B1`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RDCRC | 3:0 | 0x1 | 1 |
+| CKE | 9:4 | 0xB | 11 |
+| LOCKPLL | 29:16 | 0xBB8 | 3000 |
+
+**TIMING13** — `0x009A0254` = `0x02BAFF4E`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| ASREX2CLK | 4:0 | 0xE | 14 |
+| ASREX2CLK_MSB | 5:5 | 0x0 | 0 |
+| ASR2NRD_MSB | 6:6 | 0x1 | 1 |
+| ASR2ASREX_MSB | 7:7 | 0x0 | 0 |
+| ASR2NRD | 15:8 | 0xFF | 255 |
+| ASR2ASREX | 31:16 | 0x2BA | 698 |
+
+**TIMING14** — `0x009A0258` = `0x0000643F`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| ZQCS | 6:0 | 0x3F | 63 |
+| ZQCL | 17:8 | 0x64 | 100 |
+
+**TIMING15** — `0x009A025C` = `0x08080F0F`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RDRET | 6:0 | 0xF | 15 |
+| WRRET | 15:8 | 0xF | 15 |
+| RDINTRP | 22:16 | 0x8 | 8 |
+| WRINTRP | 30:24 | 0x8 | 8 |
+
+**TIMING16** — `0x009A0260` = `0x00001F1F`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RDFLUSH | 7:0 | 0x1F | 31 |
+| WRFLUSH | 15:8 | 0x1F | 31 |
+| RP | 23:16 | 0x0 | 0 |
+| WCK_QRST | 27:24 | 0x0 | 0 |
+
+**TIMING17** — `0x009A0264` = `0x00000000` (reads 0 / filtered)
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RDRET_PRE | 7:0 | 0x0 | 0 |
+| WCK2RDWCK | 23:16 | 0x0 | 0 |
+| MRS2RDWCK | 31:24 | 0x0 | 0 |
+
+**TIMING18** — `0x009A0268` = `0x03FF1F1F`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| WRCRCFLUSH | 7:0 | 0x1F | 31 |
+| REXT | 15:8 | 0x1F | 31 |
+| QUSE_SETTLE | 20:16 | 0x1F | 31 |
+| DLCELL_SETTLE | 29:21 | 0x1F | 31 |
+
+**TIMING19** — `0x009A026C` = `0x00007FE2`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| QRST_OFFSET | 4:0 | 0x2 | 2 |
+| QRST_FLUSH | 9:5 | 0x1F | 31 |
+| MPRR | 16:10 | 0x1F | 31 |
+
+**TIMING20** — `0x009A028C` = `0x00001F1F`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RD_REFRESH | 7:0 | 0x1F | 31 |
+| WR_REFRESH | 15:8 | 0x1F | 31 |
+
+**TIMING21** — `0x009A0390` = `0x00C0052D`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| REFSB | 0:0 | 0x1 | 1 |
+| REFSB_FORCE_FULL | 1:1 | 0x0 | 0 |
+| REFSB_DUAL_REQUEST | 2:2 | 0x1 | 1 |
+| REFSB_DELAYED_THRESHOLD | 5:3 | 0x5 | 5 |
+| REFSB_FULL_REF_PERIOD | 19:8 | 0x5 | 5 |
+| REFSB_DISPATCH_PERIOD | 31:22 | 0x3 | 3 |
+
+**TIMING22** — `0x009A0394` = `0x00003124`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RFCSBA | 9:0 | 0x124 | 292 |
+| RFCSBR | 17:10 | 0xC | 12 |
+| RFCSBA_MSB | 21:20 | 0x0 | 0 |
+
+**TIMING23** — `0x009A039C` = `0x00000003`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| CCDR | 3:0 | 0x3 | 3 |
+| RD_RANK_SEL_DELAY | 10:4 | 0x0 | 0 |
+| WR_RANK_SEL_DELAY | 17:11 | 0x0 | 0 |
+| WR_CCDL | 27:24 | 0x0 | 0 |
+| WR_CCDS | 31:28 | 0x0 | 0 |
+
+**TIMING24** — `0x009A03E0` = `0x28000008`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RRDL | 6:0 | 0x8 | 8 |
+| CCDMW | 14:8 | 0x0 | 0 |
+| LPDDR4_RDV_OFFSET | 18:15 | 0x0 | 0 |
+| ABPA | 25:20 | 0x0 | 0 |
+| XS_OFFSET | 31:26 | 0xA | 10 |
+
+**TIMING25** — `0x009A03EC` = `0x400803E8`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| ZQCAL | 11:0 | 0x3E8 | 1000 |
+| MRRW | 21:13 | 0x40 | 64 |
+| MRRWL | 30:22 | 0x100 | 256 |
+
+### 3.4 Other timing-bearing register families (not field-decoded here)
+
+Beyond the command-timing block above, the FBPA carries a few more groups that also
+hold timing/latency values. They are captured as raw values in the `.txt`; decode them
+on request.
+
+| Group | Addr range | Live sample | What it is |
+|---|---|---|---|
+| **MRS / mode registers** | `0x009A0300`–`0x3FC` | `MR0..MR15` writes | the DRAM-side mode registers — the HBM stack's *own* CL/WR/drive/refresh config, sent over the bus. Complements the controller-side timings. |
+| **ASR (auto self-refresh)** | `0x009A02F8` / `0x2FC` | `0x0A000080` / `0x00000002` | self-refresh wakeup + entry timing (`ASR_WAKEUP`, `DRAM_ASR`). |
+| **Training timing** | `0x009A0970`, `0x09C8`, `0x2050` | `0x006E0600`, `0x0000000A` | PHY/DLL training windows (`TRAINING_TIMING`, `_TIMING2`, `_TIMING3`). Not per-command latencies. |
+| **REFMPLL / DRAMPLL** | `0x009A0E20`+ | PLL cfg | memory-clock PLL coefficients — set the tCK the above cycle counts are measured in. |
+
+These are *fixed* by the memory type/training, not knobs you would tune for latency the
+way the primary–quaternary set is.
+
+### 3.5 MRS — HBM2 mode registers (the DRAM stack's own view)
+
+Each `GENERIC_MRSn` register is a *command*: `[31:30]SUBP · [29:28]CH · [25:20]BA=MR#`
+`· [19:18]RANK · [16:0]ADR=MR payload`. The payload is what the HBM die stores. Two
+values cross-check the controller side: **MR3 RAS = CONFIG0.RAS**, **MR1 WR = CONFIG2.WR**.
+
+| MR | Reg | Value | Payload | Decoded (NVIDIA HBM fields) |
+|---|---|---|---:|---|
+| MR0 | `0x009A0300` | `0x00000003` | 0x0003 | RDBI=1, WDBI=1 (data-bus inversion rd/wr) |
+| MR1 | `0x009A0330` | `0x00100099` | 0x0099 | **WR=25**, DRV=4 (write-recovery, drive strength) |
+| MR2 | `0x009A0334` | `0x00200019` | 0x0019 | WL_code=1, RL_code=3 (DRAM latency codes, not raw cyc) |
+| MR3 | `0x009A0338` | `0x003000EB` | 0x00EB | **RAS=43**, [7:6]=3 |
+| MR4 | `0x009A033C` | `0x00400030` | 0x0030 | ECC/parity/vendor misc |
+| MR5 | `0x009A0340` | `0x00500000` | 0x0000 | default (payload 0) |
+| MR6 | `0x009A0344` | `0x00600000` | 0x0000 | default (payload 0) |
+| MR7 | `0x009A0348` | `0x00700000` | 0x0000 | default (payload 0) |
+| MR8 | `0x009A0354` | `0x00800000` | 0x0000 | default (payload 0) |
+| MR9 | `0x009A0358` | `0x00900000` | 0x0000 | default (payload 0) |
+| MR14 | `0x009A035C` | `0x00E00000` | 0x0000 | default (payload 0) |
+| MR15 | `0x009A034C` | `0x00F00000` | 0x0000 | default (payload 0) |
+
+`MR5..MR15` carry payload 0 on this card (defaults). Note MR2's RL/WL are *encoded
+codes* the die maps to real latency per its datasheet — that is why they read 3/1, not
+the controller's CL=37/WL=10.
+
+---
+
+## 4. When these timings apply in boot — and how to change them
+
+### The apply moment: VBIOS devinit (POST), before the driver
+
+1. Power-on → PCIe link up.
+2. **VBIOS devinit tables** run on the PMU/devinit engine at high privilege. This is
+ where the FBPA `CONFIG/TIMING` registers are written from the VBIOS memory-tuning
+ tables, `MRS` commands are issued to the HBM stacks, HBM **training** runs
+ (address / WCK / read / write), DLLs calibrate, and the controller computes the
+ effective `_GEN` values.
+3. Normal operation. **This card exposes a single mem pstate (1728 MHz)** → the timings
+ are **never re-derived at runtime**; there is no mclk-switch to reprogram them.
+4. OS driver loads → it *reads* these; it does not reprogram the base timings.
+
+So the timings are latched **once, at POST**, long before the driver — and then frozen.
+
+### Why a runtime host poke does not work
+
+- **Write-locked.** `NV_PFB_FBPA_MEM_PRIV_LEVEL_MASK` (`0x009A0168`) = `0xFFFFFFCF` → WRITE_PROTECTION `[7:4]` = `0xC` = only privilege **levels 2–3**
+ may write. A BAR0 write from the CPU (level 0) is silently dropped —
+ `gpu_reg_tool write 0x009A0290 …` will **not** stick.
+- Even from a privileged writer, a live controller needs a **self-refresh wrapper**:
+ `SELF_REF`=ENABLED → reprogram `CONFIG/TIMING` (+ re-issue `MRS` for DRAM-side
+ values) → optionally retrain → exit self-refresh. Poking a running controller
+ without that either no-ops or corrupts memory.
+- Anything set at runtime is **lost on reboot**.
+
+### The practical options
+
+| Method | Applies at | Survives reboot | Needs |
+|---|---|---|---|
+| **Edit VBIOS memory-timing tables + reflash** (realistic path) | next POST/devinit | yes | nvflash, VBIOS timing edit, recovery plan |
+| **Privileged Falcon at runtime** | immediately | no | code at priv level ≥2 (PMU/GSP/SEC2 — the GSP-bypass chain), self-refresh+retrain wrapper |
+| Host BAR0 write | — | — | blocked by PLM — does not work |
+
+**Bottom line:** change the values in the **VBIOS timing tables and reflash** so devinit
+applies (and trains) them at the next boot. A runtime change is only possible from a
+level-2 Falcon and would be wiped on reboot anyway. Toggling `USE_TIMING_REGS`
+(CONFIG vs TIMING source) does not help — both copies sit behind the same PLM lock.
+
+---
+
+## 4a. Bandwidth ceiling & the disabled stacks
+
+### The ceiling is set by width × clock — timings only affect efficiency
+
+```
+active FBPAs = 16 of 24 (CSTATUS: 8 read 0xBADF = floorswept)
+bus width = 16 × 256 bit = 4096 bit
+data rate = 2 × 1728 MHz = 3.456 Gbps/pin (HBM2e, DDR)
+─────────────────────────────────────────────────────────────
+theoretical peak = 3.456 × 4096/8 = 1769 GB/s ← hard ceiling
+```
+
+Measured (`mem_bench`, 1 GB): read **1617** (91% of peak), write 1394 (79%),
+copy 1514, triad 1585, DMA D2D 1593 GB/s. Read is already ~91% of the wall.
+
+**Timings cannot cross 1769 GB/s** — they only recover part of the ~9% gap (refresh
+`tRFC`/`tREFI` is the biggest lever; row `tRC`/`tRP`/`tRAS` help random access; write
+turnaround helps copy/triad). Realistic tuned ceiling ≈ **1680–1720 GB/s read**.
+**2 TB/s is above the physical wall at 4096-bit — unreachable by timings.**
+
+### The only route to 2 TB/s is a wider bus — and part of it may be recoverable
+
+Reading this card's floorsweep fuses:
+
+| Fuse | Addr | Value | FBPAs |
+|---|---|---|---|
+| `FUSE_OPT_FBPA_DISABLE` | `0x00820368` | `0x00C0330C` | {2,3, 8,9, 12,13, 22,23} off (8) |
+| `FUSE_OPT_FBPA_DEFECTIVE` | `0x008205D0` | `0x00C03000` | {12,13, 22,23} bad silicon (4) |
+| **DISABLE − DEFECTIVE** | | | **{2,3, 8,9} — off but NOT defective (4)** |
+
+So 4 FBPAs (1024-bit) are disabled for SKU segmentation, not because they're broken.
+If they could be brought back: 16→20 FBPA = **5120-bit** → 3.456 × 5120/8 =
+**2211 GB/s** theoretical → ~2010 GB/s at 91% → **2 TB/s becomes physically possible.**
+
+### Why it is still a long shot
+
+| Lock | Value | Effect |
+|---|---|---|
+| `FUSE_EN_SW_OVERRIDE` | `0x00820040` = `0x0` | the CTRL_OPT SW-override path is **fuse-disabled** — `CTRL_OPT_FBPA` writes are ignored |
+| `FUSE_DIS_SW_OVR` | `0x00820084` = `0x1` | likely **latches** EN_SW_OVERRIDE off, so even an HS Falcon can't enable the override |
+| `FUSE_MEM_LOCKED` / `FUSE_FBPA_MEM_WR_SEC` | `0x1` | memory config write-locked |
+
+This is the same wall that defeated the compute unlock. Even if the override took,
+re-enabling an FBPA is a **devinit-time** operation (FB re-init with the new mask +
+full HBM training on the recovered channels + MMU/CFG1 remap), not a runtime poke.
+
+### The cheap, decisive experiment (via the SEC2/HS chain)
+
+The HBM **IEEE1500** test port is host-readable here (`I1500_INSTR 0x009A3CB4`=0x0F,
+`I1500_DATA 0x009A3CBC`=live, not `0xBADF`), so the port is not fully locked. Plan:
+
+1. **Probe** channels {2,3,8,9} over per-FBPA IEEE1500 (`0x00900000 + fbpa*0x4000 +
+ 0x3CB4`): drive `WIR` = DEVICE_ID / run `MBIST`. If a die answers → the stack is
+ physically present and (per DEFECTIVE=0) good → recovery is worth pursuing.
+2. **Write-probe** the floorsweep override (HS): try clearing {2,3,8,9} in the FS path.
+ If `EN_SW_OVERRIDE`=0 + `DIS_SW_OVR`=1 truly lock it → blocked, hard stop.
+3. If it takes → patch devinit to init with 20 FBPA + retrain → measure.
+
+**Odds:** the non-defective 4 FBPAs are real and make 2 TB/s *physically* possible; the
+`DIS_SW_OVR=1` override lock is the make-or-break, and the prior (from the compute
+unlock) is that it holds. Probing (step 1) is cheap and non-destructive; actual
+re-enable (steps 2–3) is a research project gated by that fuse.
+
+---
+
+## 5. Re-extracting after a future vBIOS flash
+
+```bash
+# dumps the whole FBPA + NV_PFB block from BAR0 (needs root)
+cd /home/aboba/nvidia-unlock/chain-a-v2
+sudo ./gpu_reg_tool dump-fbpa /tmp/fbpa_new.txt
+# single register:
+sudo ./gpu_reg_tool read 0x009A0290
+```
+
+The tool mmaps `/sys/bus/pci/devices/0000:03:00.0/resource0`; if the card ever moves
+bus address, update `SYSFS_BAR0` in `gpu_reg_tool.c`.
+
+## 6. Sources
+
+- **Register map & bitfields:** `....../dev_fbpa.h`
+- **Live values:** `gpu_reg_tool dump-fbpa` (BAR0 MMIO)
+- **Cross-reference:** envytools `rnndb/memory/gf100_pbfb.xml` (older layout; offsets
+ differ from GA100 — GA100 timings start at `0x220`, Fermi at `0x290`)
+- **Card context:** `timings/fuse-reference-table.md`
diff --git a/overclocking/timings/reference/hbm2-timings-earlier-capture.md b/overclocking/timings/reference/hbm2-timings-earlier-capture.md
new file mode 100644
index 0000000..31b99da
--- /dev/null
+++ b/overclocking/timings/reference/hbm2-timings-earlier-capture.md
@@ -0,0 +1,719 @@
+
+## 1. Key DRAM timings (the ones you care about)
+
+Cycle→ns uses **tCK = 0.5787 ns** (1728 MHz). Sanity check: tRP = 24 cyc →
+13.9 ns ≈ JEDEC HBM2 14 ns, and tRFC = 657 cyc → 380 ns ≈ HBM2 all-bank refresh —
+so the clock basis is correct.
+
+| Timing | What it is | Source reg.field | Cycles (dec) | ≈ ns |
+|---|---|---|---:|---:|
+| **tRC** | Row cycle time (ACT→ACT same bank) | `CONFIG0.RC` | 67 | 38.8 |
+| **tRAS** | Row active time (ACT→PRE) | `CONFIG0.RAS` | 43 | 24.9 |
+| **tRP** | Row precharge (PRE→ACT) | `CONFIG0.RP` | 24 | 13.9 |
+| **tRCD_rd** | RAS→CAS delay, reads (ACT→RD) | `CONFIG1.RD_RCD` | 27 | 15.6 |
+| **tRCD_wr** | RAS→CAS delay, writes (ACT→WR) | `CONFIG1.WR_RCD` | 18 | 10.4 |
+| **tRFC** | Refresh cycle, all-bank (effective) *(= CONFIG0.RFC 145 │ CONFIG10.RFC_MSB 1<<9)* | `TIMING0_GEN.RFC` | 657 | 380.2 |
+| **tRFCsb** | Refresh cycle, single-bank | `TIMING22.RFCSBA` | 292 | 169.0 |
+| **CL** | CAS read latency (RD→data) | `CONFIG1.CL` | 37 | 21.4 |
+| **WL** | Write latency (WR→data) | `CONFIG1.WL` | 10 | 5.8 |
+| **tWR** | Write recovery (data→PRE) | `CONFIG2.WR` | 25 | 14.5 |
+| **tRRD_s** | ACT→ACT diff bank, short (diff bank-group) | `CONFIG4.RRD` | 5 | 2.9 |
+| **tRRD_l** | ACT→ACT diff bank, long (same bank-group) | `CONFIG11.RRDL` | 5 | 2.9 |
+| **tFAW** | Four-activate window | `CONFIG3.FAW` | 22 | 12.7 |
+| **tCCD_s** | CAS→CAS, short (diff bank-group) | `CONFIG3.CCDS` | 2 | 1.2 |
+| **tCCD_l** | CAS→CAS, long (same bank-group) | `CONFIG3.CCDL` | 4 | 2.3 |
+| **tR2W** | Read→Write bus turnaround (effective) | `TIMING1_GEN.R2W` | 37 | 21.4 |
+| **tW2R** | Write→Read bus turnaround (effective) | `TIMING1_GEN.W2R` | 20 | 11.6 |
+| **tR2P** | Read→Precharge (effective) | `TIMING1_GEN.R2P` | 10 | 5.8 |
+| **tW2P** | Write→Precharge (effective) | `TIMING1_GEN.W2P` | 36 | 20.8 |
+| **tCKE** | Clock-enable min pulse | `TIMING12.CKE` | 11 | 6.4 |
+| **tZQCAL** | ZQ calibration (long) *(≈1 µs)* | `TIMING25.ZQCAL` | 1000 | 578.7 |
+| **tLOCKPLL** | PLL relock window | `TIMING12.LOCKPLL` | 3000 | 1736.1 |
+
+Other periodic/interval settings (not per-command latencies):
+
+| Setting | Source | Value (dec) | ≈ time |
+|---------------------------|-------------------------|-------------:|--------:|
+| ZQCS auto-cal interval | `CONFIG7.ZQCS_INTERVAL` | 12800000 cyc | 7.41 ms |
+| ZQCS short | `TIMING14.ZQCS` | 63 | 36.5 ns |
+| ZQCL long | `TIMING14.ZQCL` | 100 | 57.9 ns |
+| Single-bank tRFC (RFCSBR) | `TIMING22.RFCSBR` | 12 | 6.9 ns |
+
+---
+
+## 1a. Mapping to primary / secondary / tertiary / quaternary
+
+NVIDIA does not physically split the registers into those tiers — every value is a
+field inside `CONFIG*`/`TIMING*`. But mapped onto the usual DRAM-OC hierarchy, the
+**tertiary and quaternary timings already live in the registers decoded below** — they
+are just packed byte-fields, not extra register banks. So: yes, this is all of them.
+
+**Primary**
+
+| Timing | Source field | Cyc |
+|-----------|------------------|----:|
+| CL | `CONFIG1.CL` | 37 |
+| tRCD (rd) | `CONFIG1.RD_RCD` | 27 |
+| tRCD (wr) | `CONFIG1.WR_RCD` | 18 |
+| tRP | `CONFIG0.RP` | 24 |
+| tRAS | `CONFIG0.RAS` | 43 |
+
+**Secondary**
+
+| Timing | Source field | Cyc |
+|---------------|-------------------|----:|
+| tRC | `CONFIG0.RC` | 67 |
+| tRFC | `TIMING0_GEN.RFC` | 657 |
+| tRFCsb | `TIMING22.RFCSBA` | 292 |
+| tWR | `CONFIG2.WR` | 25 |
+| WL/tCWL | `CONFIG1.WL` | 10 |
+| tFAW | `CONFIG3.FAW` | 22 |
+| tRRD | `CONFIG4.RRD` | 5 |
+| tRTP (rd→pre) | `TIMING1_GEN.R2P` | 10 |
+| tWTP (wr→pre) | `TIMING1_GEN.W2P` | 36 |
+| tCKE | `TIMING12.CKE` | 11 |
+| tREFI | `CONFIG4.REFRESH` | 6 |
+
+**Tertiary — bank-group / turnaround matrix**
+
+| Timing | Source field | Cyc |
+|-------------------|------------------------------|----:|
+| tCCD_L | `CONFIG3.CCDL` | 4 |
+| tCCD_S | `CONFIG3.CCDS` | 2 |
+| tRRD_L | `CONFIG11.RRDL` | 5 |
+| tR2W (rd→wr bus) | `TIMING1_GEN.R2W` | 37 |
+| tW2R (wr→rd bus) | `TIMING1_GEN.W2R` | 20 |
+| tWTR (W2R_BUS) | `CONFIG2.W2R_BUS` | 8 |
+| R2W_BUS | `CONFIG2.R2W_BUS` | 8 |
+| tCCD_R (rank) | `TIMING23.CCDR` | 3 |
+| WR_CCD_L | `TIMING23.WR_CCDL` | 0 |
+| WR_CCD_S | `TIMING23.WR_CCDS` | 0 |
+| CCDMW (masked wr) | `CONFIG11.CCDMW` | 0 |
+| RD rank-sel delay | `CONFIG11.RD_RANK_SEL_DELAY` | 0 |
+| WR rank-sel delay | `CONFIG11.WR_RANK_SEL_DELAY` | 0 |
+| CDLR | `CONFIG2.CDLR` | 9 |
+
+**Quaternary — PHY data-path, power-down, self-refresh, ZQ**
+
+| Timing | Source field | Cyc |
+|----------------------------|-----------------------|-----:|
+| QUSE | `TIMING3.QUSE` | 11 |
+| QRST | `TIMING3.QRST` | 10 |
+| QSAFE | `TIMING3.QSAFE` | 17 |
+| RDV (read data valid) | `TIMING3.RDV` | 60 |
+| WDV (write data valid) | `TIMING2.WDV` | 5 |
+| RPRE/WPRE | `CONFIG2.RPRE` | 1 |
+| ODT/ODTLEN | `TIMING8.ODT` | 0 |
+| QPOP_OFFSET | `CONFIG1.QPOP_OFFSET` | 14 |
+| WCK2MRS | `TIMING10.WCK2MRS` | 4 |
+| MRD | `TIMING10.MRD` | 22 |
+| REFTR | `TIMING10.REFTR` | 10 |
+| power-down entry PDEN2PDEX | `CONFIG3.PDEN2PDEX` | 11 |
+| PDEX2WR | `TIMING4.PDEX2WR` | 7 |
+| PDEX2RD | `TIMING4.PDEX2RD` | 7 |
+| ACT2PDEN | `TIMING5.ACT2PDEN` | 12 |
+| PCHG2PDEN | `TIMING5.PCHG2PDEN` | 10 |
+| self-refresh exit ASR2NRD | `TIMING13.ASR2NRD` | 255 |
+| ASREX2CLK | `TIMING13.ASREX2CLK` | 14 |
+| ZQCS | `TIMING14.ZQCS` | 63 |
+| ZQCL | `TIMING14.ZQCL` | 100 |
+| ZQCAL | `TIMING25.ZQCAL` | 1000 |
+
+> Caveat for the PHY/data-path fields (QUSE/QRST/QSAFE/RDV/…): the value above is the
+> *programmed base* in the `TIMING2..5` register. The controller applies `CONFIG5/6/8/9`
+> offsets to produce the **effective** value in the `_GEN` twin — e.g. QUSE base = 11 but effective `TIMING3_GEN.QUSE` = 42. Read §3.2 (`_GEN`) for what the hardware actually uses.
+
+---
+
+## 2. How to read these registers — CONFIG vs TIMING vs _GEN
+
+The FBPA has **three** overlapping copies of the core timings. Which one is live is
+selected by one bit:
+
+- **`CONFIG0.USE_TIMING_REGS` (bit 31) = 0** → **FALSE** on this card.
+ This means the hardware takes its primary timings (tRC/tRFC/tRAS/tRP/CL/WL/tRCD/…)
+ from the **`CONFIG0..CONFIG12`** registers (`0x290`–`0x2F4`, plus `0x015C`, `0x3E4`).
+- The **`TIMING0..TIMING9`** registers (`0x220`–`0x244`) hold the *other* (legacy)
+ copy. On this card they carry leftover DDR3-style defaults (e.g. TIMING0 RC=12,
+ RAS=8 — physically impossible for HBM2), confirming they are **inactive**.
+- **`TIMING10`+ (`0x248`+)** hold HBM-specific extended parameters that have no
+ CONFIG twin, so those **are** live regardless of the select bit.
+- **`TIMINGn_GEN`** (`0x2B0`–`0x2C8`, read-only) are the **effective** timings the
+ controller actually generated after resolving CONFIG + high-bit extensions. They are
+ the ground truth. Example: `TIMING0_GEN.RFC` = 657 = `CONFIG0.RFC`(145) OR'd with
+ `CONFIG10.RFC_MSB`(1) shifted left 9. That is why the key table above pulls tRFC and
+ the bus-turnaround values (tR2W/tW2R) from the `_GEN` registers.
+
+Register numbering note: the previous version of the raw file labelled `0x290`+ as
+"DRAM_TRAINING0..". That was wrong — per `dev_fbpa.h` those addresses are
+`CONFIG0..CONFIG10` and the read-only `TIMINGn_GEN` snapshots. The raw file is now
+relabelled to match the hardware manual.
+
+---
+
+## 3. Full field-level decode
+
+Every field below is decoded straight from the live value with the bit ranges from
+`dev_fbpa.h`. Decimal is the raw field value; it equals a cycle count for the latency
+timings (multiply by 0.5787 ns for time).
+
+### 3.1 CONFIG registers — ACTIVE primary timings
+
+**CONFIG0** — `0x009A0290` = `0x18569143`
+
+| Field | Bits | Hex | Dec |
+|-----------------|-------|-----:|----:|
+| RC | 7:0 | 0x43 | 67 |
+| RFC | 16:8 | 0x91 | 145 |
+| RAS | 23:17 | 0x2B | 43 |
+| RP | 30:24 | 0x18 | 24 |
+| USE_TIMING_REGS | 31:31 | 0x0 | 0 |
+
+**CONFIG1** — `0x009A0294` = `0x3926C525`
+
+| Field | Bits | Hex | Dec |
+|-------------|-------|-----:|----:|
+| CL | 6:0 | 0x25 | 37 |
+| WL | 13:7 | 0xA | 10 |
+| RD_RCD | 19:14 | 0x1B | 27 |
+| WR_RCD | 25:20 | 0x12 | 18 |
+| QPOP_OFFSET | 31:26 | 0xE | 14 |
+
+**CONFIG2** — `0x009A0298` = `0x88190911`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RPRE | 3:0 | 0x1 | 1 |
+| WPRE | 7:4 | 0x1 | 1 |
+| CDLR | 14:8 | 0x9 | 9 |
+| WR | 22:16 | 0x19 | 25 |
+| W2R_BUS | 27:24 | 0x8 | 8 |
+| R2W_BUS | 31:28 | 0x8 | 8 |
+
+**CONFIG3** — `0x009A029C` = `0x24002D6B`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| PDEX | 4:0 | 0xB | 11 |
+| PDEN2PDEX | 8:5 | 0xB | 11 |
+| FAW | 16:9 | 0x16 | 22 |
+| AOND | 23:17 | 0x0 | 0 |
+| CCDL | 27:24 | 0x4 | 4 |
+| CCDS | 31:28 | 0x2 | 2 |
+
+**CONFIG4** — `0x009A02A0` = `0xC4028033`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| REFRESH_LO | 2:0 | 0x3 | 3 |
+| REFRESH | 14:3 | 0x6 | 6 |
+| RRD | 20:15 | 0x5 | 5 |
+| IDLE_DELAY | 26:21 | 0x20 | 32 |
+| CMD2MCIDLE_DRAMC | 31:27 | 0x18 | 24 |
+
+**CONFIG5** — `0x009A02A4` = `0xA6B3A002`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| ADR_MIN | 2:0 | 0x2 | 2 |
+| WRCRC | 10:4 | 0x0 | 0 |
+| QSAFE_OFFSET | 17:12 | 0x3A | 58 |
+| INTRP_MSB | 19:18 | 0x0 | 0 |
+| RDRET_OFFSET | 23:20 | 0xB | 11 |
+| WRRET_OFFSET | 27:24 | 0x6 | 6 |
+| INTRP | 31:28 | 0xA | 10 |
+
+**CONFIG6** — `0x009A02A8` = `0x11008000`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RDFLUSH_OFFSET | 6:0 | 0x0 | 0 |
+| WRFLUSH_OFFSET | 14:8 | 0x0 | 0 |
+| CMD2MCIDLE_DRAMC_EXT | 15:15 | 0x1 | 1 |
+| WDAT_LATENCY | 20:16 | 0x0 | 0 |
+| PPD | 27:24 | 0x1 | 1 |
+| SDDR4_RDV_OFFSET | 29:28 | 0x1 | 1 |
+| CMD_ADJUST | 31:30 | 0x0 | 0 |
+
+**CONFIG7** — `0x009A02AC` = `0x00C35000`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| ZQCS_INTERVAL | 31:0 | 0xC35000 | 12800000 |
+
+**CONFIG8** — `0x009A02CC` = `0x0C023900`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| ATR_OFFSET | 6:0 | 0x0 | 0 |
+| ATR_PADDING | 11:8 | 0x9 | 9 |
+| ODT_PADDING | 15:12 | 0x3 | 3 |
+| WCK2PH | 23:16 | 0x2 | 2 |
+| MRSTWCK | 30:24 | 0xC | 12 |
+
+**CONFIG9** — `0x009A02E8` = `0x12400389`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| QUSE_OFFSET | 6:0 | 0x9 | 9 |
+| QUSE_DDLL_SETTLE | 11:7 | 0x7 | 7 |
+| REXT_OFFSET | 15:12 | 0x0 | 0 |
+| DLCELL_SETTLE | 23:16 | 0x40 | 64 |
+| QRST_OFFSET | 27:24 | 0x2 | 2 |
+| MPRR | 31:28 | 0x1 | 1 |
+
+**CONFIG10** — `0x009A02F4` = `0x00000011`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RFC_MSB | 1:0 | 0x1 | 1 |
+| IDLE_DELAY_MSB | 4:4 | 0x1 | 1 |
+| PDEN2PDEX_MSB | 6:5 | 0x0 | 0 |
+| RD_RCD_MSB | 8:8 | 0x0 | 0 |
+| WR_RCD_MSB | 11:11 | 0x0 | 0 |
+| IDLE_DELAY_HI | 16:14 | 0x0 | 0 |
+| CMD2MCIDLE_DRAMC_HI | 18:18 | 0x0 | 0 |
+| RDRET_OFFSET_MSB | 21:21 | 0x0 | 0 |
+
+**CONFIG11** — `0x009A03E4` = `0x00000005`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RRDL | 5:0 | 0x5 | 5 |
+| CCDMW | 12:7 | 0x0 | 0 |
+| WPOST | 15:15 | 0x0 | 0 |
+| LPDDR4_RDV_OFFSET | 18:16 | 0x0 | 0 |
+| WEXT_OFFSET | 22:19 | 0x0 | 0 |
+| RPRE_TOGGLE | 23:23 | 0x0 | 0 |
+| RD_RANK_SEL_DELAY | 27:24 | 0x0 | 0 |
+| WR_RANK_SEL_DELAY | 31:28 | 0x0 | 0 |
+
+**CONFIG12** — `0x009A015C` = `0x00000000` (reads 0 / filtered)
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| BLDIV | 3:0 | 0x0 | 0 |
+| ADR_TERM | 4:4 | 0x0 | 0 |
+| ADRTR_FIFO_MARGIN | 7:5 | 0x0 | 0 |
+
+### 3.2 TIMINGn_GEN — read-only EFFECTIVE timings, full set (ground truth)
+
+These mirror TIMING/CONFIG after the controller resolves them, incl. the tertiary/quaternary values. Present on this die: GEN 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 15, 16, 17, 18, 19, 20, 22, 24.
+
+**TIMING0_GEN** — `0x009A02B0` = `0x2B291043`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RC | 8:0 | 0x43 | 67 |
+| RFC | 22:12 | 0x291 | 657 |
+| RAS | 31:24 | 0x2B | 43 |
+
+**TIMING1_GEN** — `0x009A02B4` = `0x240A1425`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| R2W | 7:0 | 0x25 | 37 |
+| W2R | 14:8 | 0x14 | 20 |
+| R2P | 20:16 | 0xA | 10 |
+| W2P | 30:24 | 0x24 | 36 |
+
+**TIMING2_GEN** — `0x009A02B8` = `0x0905121B`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RD_RCD | 7:0 | 0x1B | 27 |
+| WR_RCD | 15:8 | 0x12 | 18 |
+| RRD | 22:16 | 0x5 | 5 |
+| WDV | 28:24 | 0x9 | 9 |
+
+**TIMING3_GEN** — `0x009A02BC` = `0x003B242A`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| QUSE | 7:0 | 0x2A | 42 |
+| QRST | 15:8 | 0x24 | 36 |
+| QSAFE | 23:16 | 0x3B | 59 |
+| RDV | 31:24 | 0x0 | 0 |
+
+**TIMING4_GEN** — `0x009A02C0` = `0x016B0B0B`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| PDEX2WR | 5:0 | 0xB | 11 |
+| PDEX2RD | 13:8 | 0xB | 11 |
+| PDEN2PDEX | 19:16 | 0xB | 11 |
+| FAW | 28:20 | 0x16 | 22 |
+| PDEN2PDEX_MSB | 30:29 | 0x0 | 0 |
+
+**TIMING5_GEN** — `0x009A02C4` = `0x489B2718`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| PCHG2PDEN | 7:0 | 0x18 | 24 |
+| RW2PDEN | 15:8 | 0x27 | 39 |
+| ACT2PDEN | 22:16 | 0x1B | 27 |
+| AR2PDEN | 31:23 | 0x91 | 145 |
+
+**TIMING6_GEN** — `0x009A02C8` = `0x29380101`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| PPD | 4:0 | 0x1 | 1 |
+| BUS_W2R | 15:8 | 0x1 | 1 |
+| CMD2MCIDLE_DRAMC | 21:16 | 0x38 | 56 |
+| CMD2MCIDLE_FBIO | 30:24 | 0x29 | 41 |
+
+**TIMING7_GEN** — `0x009A02D0` = `0x0B240202`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| REXT | 3:0 | 0x2 | 2 |
+| WEXT | 11:8 | 0x2 | 2 |
+| ATR | 23:16 | 0x24 | 36 |
+| ATRLEN | 28:24 | 0xB | 11 |
+
+**TIMING8_GEN** — `0x009A02D4` = `0x11330501`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| ODT | 7:0 | 0x1 | 1 |
+| ODTLEN | 11:8 | 0x5 | 5 |
+| QPOP_OFFSET | 23:16 | 0x33 | 51 |
+| RPRE | 27:24 | 0x1 | 1 |
+| WPRE | 31:28 | 0x1 | 1 |
+
+**TIMING9_GEN** — `0x009A02D8` = `0x180E1024`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| CCDL | 3:0 | 0x4 | 4 |
+| CCDS | 7:4 | 0x2 | 2 |
+| QPOP_OFFSET_ADR | 15:8 | 0x10 | 16 |
+| QPOP_OFFSET_WCK | 23:16 | 0xE | 14 |
+| QPOP_OFFSET_WREDC | 31:24 | 0x18 | 24 |
+
+**TIMING15_GEN** — `0x009A02DC` = `0x01001232`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RDRET | 6:0 | 0x32 | 50 |
+| WRRET | 15:8 | 0x12 | 18 |
+| RDINTRP | 22:16 | 0x0 | 0 |
+| WRINTRP | 30:24 | 0x1 | 1 |
+
+**TIMING16_GEN** — `0x009A02E0` = `0x00180C27`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RDFLUSH | 7:0 | 0x27 | 39 |
+| WRFLUSH | 15:8 | 0xC | 12 |
+| RP | 23:16 | 0x18 | 24 |
+| WCK_QRST | 26:24 | 0x0 | 0 |
+
+**TIMING17_GEN** — `0x009A02E4` = `0x0A000033`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RDRET_PRE | 7:0 | 0x33 | 51 |
+| WCK2RDWCK | 23:16 | 0x0 | 0 |
+| MRS2RDWCK | 31:24 | 0xA | 10 |
+
+**TIMING18_GEN** — `0x009A02EC` = `0x08000200`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| WRCRCFLUSH | 7:0 | 0x0 | 0 |
+| REXT | 15:8 | 0x2 | 2 |
+| QUSE_SETTLE | 20:16 | 0x0 | 0 |
+| DLCELL_SETTLE | 29:21 | 0x40 | 64 |
+
+**TIMING19_GEN** — `0x009A02F0` = `0x0006A0E2`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| QRST_OFFSET | 4:0 | 0x2 | 2 |
+| QRST_FLUSH | 9:5 | 0x7 | 7 |
+| MPRR | 16:10 | 0x28 | 40 |
+| REFSB_SUBP0 | 17:17 | 0x1 | 1 |
+| REFSB_SUBP1 | 18:18 | 0x1 | 1 |
+
+**TIMING20_GEN** — `0x009A0288` = `0x00001232`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RD_REFRESH | 7:0 | 0x32 | 50 |
+| WR_REFRESH | 15:8 | 0x12 | 18 |
+
+**TIMING22_GEN** — `0x009A03F8` = `0x00003124`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RFCSBA | 9:0 | 0x124 | 292 |
+| RFCSBR | 17:10 | 0xC | 12 |
+| RFCSBA_MSB | 21:20 | 0x0 | 0 |
+
+**TIMING24_GEN** — `0x009A03E8` = `0x28000005`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RRDL | 6:0 | 0x5 | 5 |
+| CCDMW | 14:8 | 0x0 | 0 |
+| LPDDR4_RDV_OFFSET | 18:15 | 0x0 | 0 |
+| ABPA | 25:20 | 0x0 | 0 |
+| XS_OFFSET | 31:26 | 0xA | 10 |
+
+### 3.3 TIMING registers — legacy copy (0–9 inactive) + HBM extended (10+)
+
+**TIMING0** — `0x009A0220` = `0x0801900C`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RC | 8:0 | 0xC | 12 |
+| RFC | 22:12 | 0x19 | 25 |
+| RAS | 31:24 | 0x8 | 8 |
+
+**TIMING1** — `0x009A0224` = `0x120A0D12`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| R2W | 7:0 | 0x12 | 18 |
+| W2R | 14:8 | 0xD | 13 |
+| R2P | 20:16 | 0xA | 10 |
+| W2P | 30:24 | 0x12 | 18 |
+
+**TIMING2** — `0x009A0228` = `0x0508080C`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RD_RCD | 7:0 | 0xC | 12 |
+| WR_RCD | 15:8 | 0x8 | 8 |
+| RRD | 22:16 | 0x8 | 8 |
+| WDV | 28:24 | 0x5 | 5 |
+
+**TIMING3** — `0x009A022C` = `0x3C110A0B`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| QUSE | 7:0 | 0xB | 11 |
+| QRST | 15:8 | 0xA | 10 |
+| QSAFE | 23:16 | 0x11 | 17 |
+| RDV | 31:24 | 0x3C | 60 |
+
+**TIMING4** — `0x009A0230` = `0x02800707`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| PDEX2WR | 5:0 | 0x7 | 7 |
+| PDEX2RD | 13:8 | 0x7 | 7 |
+| PDEN2PDEX | 19:16 | 0x0 | 0 |
+| FAW | 28:20 | 0x28 | 40 |
+| PDEN2PDEX_MSB | 30:29 | 0x0 | 0 |
+
+**TIMING5** — `0x009A0234` = `0x168C0D0A`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| PCHG2PDEN | 7:0 | 0xA | 10 |
+| RW2PDEN | 15:8 | 0xD | 13 |
+| ACT2PDEN | 22:16 | 0xC | 12 |
+| AR2PDEN | 31:23 | 0x2D | 45 |
+
+**TIMING6** — `0x009A0238` = `0x46080101`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| PPD | 4:0 | 0x1 | 1 |
+| BUS_W2R | 15:8 | 0x1 | 1 |
+| CMD2MCIDLE_DRAMC | 21:16 | 0x8 | 8 |
+| CMD2MCIDLE_FBIO | 31:24 | 0x46 | 70 |
+
+**TIMING7** — `0x009A023C` = `0x07000202`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| REXT | 3:0 | 0x2 | 2 |
+| WEXT | 11:8 | 0x2 | 2 |
+| ATR | 23:16 | 0x0 | 0 |
+| ATRLEN | 28:24 | 0x7 | 7 |
+
+**TIMING8** — `0x009A0240` = `0x110B0700`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| ODT | 7:0 | 0x0 | 0 |
+| ODTLEN | 11:8 | 0x7 | 7 |
+| QPOP_OFFSET | 23:16 | 0xB | 11 |
+| RPRE | 27:24 | 0x1 | 1 |
+| WPRE | 31:28 | 0x1 | 1 |
+
+**TIMING9** — `0x009A0244` = `0x0A0A0A00`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| CCDL | 3:0 | 0x0 | 0 |
+| CCDS | 7:4 | 0x0 | 0 |
+| QPOP_OFFSET_ADR | 15:8 | 0xA | 10 |
+| QPOP_OFFSET_WCK | 23:16 | 0xA | 10 |
+| QPOP_OFFSET_WREDC | 31:24 | 0xA | 10 |
+
+**TIMING10** — `0x009A0248` = `0x0A2C7444`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| WCK2MRS | 3:0 | 0x4 | 4 |
+| WCK2TR | 7:4 | 0x4 | 4 |
+| LTLTR | 11:8 | 0x4 | 4 |
+| LTL7TR | 16:12 | 0x7 | 7 |
+| MRD | 22:17 | 0x16 | 22 |
+| WCK2MRS_MSB2 | 23:23 | 0x0 | 0 |
+| REFTR | 29:24 | 0xA | 10 |
+| WCK2TR_MSB | 30:30 | 0x0 | 0 |
+| WCK2MRS_MSB | 31:31 | 0x0 | 0 |
+
+**TIMING11** — `0x009A024C` = `0x03053DF3`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| ADZ | 4:0 | 0x13 | 19 |
+| WTR_RCD | 9:5 | 0xF | 15 |
+| LTR_RCD | 14:10 | 0xF | 15 |
+| LTRTR | 20:16 | 0x5 | 5 |
+| KO | 30:24 | 0x3 | 3 |
+
+**TIMING12** — `0x009A0250` = `0x0BB800B1`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RDCRC | 3:0 | 0x1 | 1 |
+| CKE | 9:4 | 0xB | 11 |
+| LOCKPLL | 29:16 | 0xBB8 | 3000 |
+
+**TIMING13** — `0x009A0254` = `0x02BAFF4E`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| ASREX2CLK | 4:0 | 0xE | 14 |
+| ASREX2CLK_MSB | 5:5 | 0x0 | 0 |
+| ASR2NRD_MSB | 6:6 | 0x1 | 1 |
+| ASR2ASREX_MSB | 7:7 | 0x0 | 0 |
+| ASR2NRD | 15:8 | 0xFF | 255 |
+| ASR2ASREX | 31:16 | 0x2BA | 698 |
+
+**TIMING14** — `0x009A0258` = `0x0000643F`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| ZQCS | 6:0 | 0x3F | 63 |
+| ZQCL | 17:8 | 0x64 | 100 |
+
+**TIMING15** — `0x009A025C` = `0x08080F0F`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RDRET | 6:0 | 0xF | 15 |
+| WRRET | 15:8 | 0xF | 15 |
+| RDINTRP | 22:16 | 0x8 | 8 |
+| WRINTRP | 30:24 | 0x8 | 8 |
+
+**TIMING16** — `0x009A0260` = `0x00001F1F`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RDFLUSH | 7:0 | 0x1F | 31 |
+| WRFLUSH | 15:8 | 0x1F | 31 |
+| RP | 23:16 | 0x0 | 0 |
+| WCK_QRST | 27:24 | 0x0 | 0 |
+
+**TIMING17** — `0x009A0264` = `0x00000000` (reads 0 / filtered)
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RDRET_PRE | 7:0 | 0x0 | 0 |
+| WCK2RDWCK | 23:16 | 0x0 | 0 |
+| MRS2RDWCK | 31:24 | 0x0 | 0 |
+
+**TIMING18** — `0x009A0268` = `0x03FF1F1F`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| WRCRCFLUSH | 7:0 | 0x1F | 31 |
+| REXT | 15:8 | 0x1F | 31 |
+| QUSE_SETTLE | 20:16 | 0x1F | 31 |
+| DLCELL_SETTLE | 29:21 | 0x1F | 31 |
+
+**TIMING19** — `0x009A026C` = `0x00007FE2`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| QRST_OFFSET | 4:0 | 0x2 | 2 |
+| QRST_FLUSH | 9:5 | 0x1F | 31 |
+| MPRR | 16:10 | 0x1F | 31 |
+
+**TIMING20** — `0x009A028C` = `0x00001F1F`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RD_REFRESH | 7:0 | 0x1F | 31 |
+| WR_REFRESH | 15:8 | 0x1F | 31 |
+
+**TIMING21** — `0x009A0390` = `0x00C0052D`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| REFSB | 0:0 | 0x1 | 1 |
+| REFSB_FORCE_FULL | 1:1 | 0x0 | 0 |
+| REFSB_DUAL_REQUEST | 2:2 | 0x1 | 1 |
+| REFSB_DELAYED_THRESHOLD | 5:3 | 0x5 | 5 |
+| REFSB_FULL_REF_PERIOD | 19:8 | 0x5 | 5 |
+| REFSB_DISPATCH_PERIOD | 31:22 | 0x3 | 3 |
+
+**TIMING22** — `0x009A0394` = `0x00003124`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RFCSBA | 9:0 | 0x124 | 292 |
+| RFCSBR | 17:10 | 0xC | 12 |
+| RFCSBA_MSB | 21:20 | 0x0 | 0 |
+
+**TIMING23** — `0x009A039C` = `0x00000003`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| CCDR | 3:0 | 0x3 | 3 |
+| RD_RANK_SEL_DELAY | 10:4 | 0x0 | 0 |
+| WR_RANK_SEL_DELAY | 17:11 | 0x0 | 0 |
+| WR_CCDL | 27:24 | 0x0 | 0 |
+| WR_CCDS | 31:28 | 0x0 | 0 |
+
+**TIMING24** — `0x009A03E0` = `0x28000008`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| RRDL | 6:0 | 0x8 | 8 |
+| CCDMW | 14:8 | 0x0 | 0 |
+| LPDDR4_RDV_OFFSET | 18:15 | 0x0 | 0 |
+| ABPA | 25:20 | 0x0 | 0 |
+| XS_OFFSET | 31:26 | 0xA | 10 |
+
+**TIMING25** — `0x009A03EC` = `0x400803E8`
+
+| Field | Bits | Hex | Dec |
+|---|---|---:|---:|
+| ZQCAL | 11:0 | 0x3E8 | 1000 |
+| MRRW | 21:13 | 0x40 | 64 |
+| MRRWL | 30:22 | 0x100 | 256 |
+
+### 3.4 Other timing-bearing register families (not field-decoded here)
+
+Beyond the command-timing block above, the FBPA carries a few more groups that also
+hold timing/latency values. They are captured as raw values in the `.txt`; decode them
+on request.
+
+| Group | Addr range | Live sample | What it is |
+|---|---|---|---|
+| **MRS / mode registers** | `0x009A0300`–`0x3FC` | `MR0..MR15` writes | the DRAM-side mode registers — the HBM stack's *own* CL/WR/drive/refresh config, sent over the bus. Complements the controller-side timings. |
+| **ASR (auto self-refresh)** | `0x009A02F8` / `0x2FC` | `0x0A000080` / `0x00000002` | self-refresh wakeup + entry timing (`ASR_WAKEUP`, `DRAM_ASR`). |
+| **Training timing** | `0x009A0970`, `0x09C8`, `0x2050` | `0x006E0600`, `0x0000000A` | PHY/DLL training windows (`TRAINING_TIMING`, `_TIMING2`, `_TIMING3`). Not per-command latencies. |
+| **REFMPLL / DRAMPLL** | `0x009A0E20`+ | PLL cfg | memory-clock PLL coefficients — set the tCK the above cycle counts are measured in. |
+
+
diff --git a/overclocking/timings/reference/raw-register-dump.txt b/overclocking/timings/reference/raw-register-dump.txt
new file mode 100644
index 0000000..66120f4
--- /dev/null
+++ b/overclocking/timings/reference/raw-register-dump.txt
@@ -0,0 +1,588 @@
+# Format: REGISTER_ADDR = VALUE ; notes
+
+# ============================================================
+# FBPA Memory Config + DRAM Timing Registers (0x009A0200+)
+# THIS IS THE TIMING AREA - key registers for HBM2 overclocking
+# ============================================================
+0x009A020C = 0x00001000 ; FBPA_CFG (RAMTYPE/subp)
+0x009A0210 = 0x80001818 ; FBPA_REF/REFCTRL status (DYNAMIC - refresh counters, varies)
+
+# --- Core DRAM Timing Parameters (packed byte fields) ---
+0x009A0218 = 0xC0000001 ; FBPA_REFCTRL / cal ctrl
+0x009A021C = 0x00000001 ; FBPA_REFCTRL1
+0x009A0220 = 0x0801900C ; TIMING0 (INACTIVE: legacy DDR3 set; USE_TIMING_REGS=0) RC/RFC/RAS
+0x009A0224 = 0x120A0D12 ; TIMING1 R2W/W2R/R2P/W2P (legacy set)
+0x009A0228 = 0x0508080C ; TIMING2 RD_RCD/WR_RCD/RRD/WDV (legacy set)
+0x009A022C = 0x3C110A0B ; TIMING3 QUSE/QRST/QSAFE/RDV
+0x009A0230 = 0x02800707 ; TIMING4 PDEX2WR/PDEX2RD/PDEN2PDEX/FAW
+0x009A0234 = 0x168C0D0A ; TIMING5 PCHG2PDEN/RW2PDEN/ACT2PDEN/AR2PDEN
+0x009A0238 = 0x46080101 ; TIMING6 PPD/BUS_W2R/CMD2MCIDLE_DRAMC/CMD2MCIDLE_FBIO
+0x009A023C = 0x07000202 ; TIMING7 REXT/WEXT/ATR/ATRLEN
+0x009A0240 = 0x110B0700 ; TIMING8 ODT/ODTLEN/QPOP_OFFSET/RPRE/WPRE
+0x009A0244 = 0x0A0A0A00 ; TIMING9 CCDL/CCDS/QPOP_OFFSET_ADR/WCK/WREDC
+0x009A0248 = 0x0A2C7444 ; TIMING10 WCK2MRS/WCK2TR/LTLTR/LTL7TR/MRD/REFTR (HBM, ACTIVE)
+0x009A024C = 0x03053DF3 ; TIMING11 ADZ/WTR_RCD/LTR_RCD/LTRTR/KO (ACTIVE)
+0x009A0250 = 0x0BB800B1 ; TIMING12 RDCRC/CKE/LOCKPLL(=3000) (ACTIVE)
+0x009A0254 = 0x02BAFF4E ; TIMING13 ASREX2CLK/ASR2NRD/ASR2ASREX (self-refresh exit)
+0x009A0258 = 0x0000643F ; TIMING14 ZQCS(=63)/ZQCL(=100) (ZQ calibration)
+0x009A025C = 0x08080F0F ; TIMING15 RDRET/WRRET/RDINTRP/WRINTRP
+0x009A0260 = 0x00001F1F ; TIMING16 RDFLUSH/WRFLUSH/RP/WCK_QRST
+0x009A0268 = 0x03FF1F1F ; TIMING18 WRCRCFLUSH/REXT/QUSE_SETTLE/DLCELL_SETTLE
+0x009A026C = 0x00007FE2 ; TIMING19 QRST_OFFSET/QRST_FLUSH/MPRR
+0x009A0288 = 0x00001232 ; TIMING (0x288) refresh-related
+0x009A028C = 0x00001F1F ; TIMING20 RD_REFRESH(=31)/WR_REFRESH(=31)
+
+# --- Extended Timing / Training Parameters ---
+0x009A0290 = 0x18569143 ; CONFIG0 *** ACTIVE tRC/tRFC/tRAS/tRP *** (USE_TIMING_REGS bit31)
+0x009A0294 = 0x3926C525 ; CONFIG1 *** ACTIVE CL/WL/RD_RCD/WR_RCD/QPOP_OFFSET ***
+0x009A0298 = 0x88190911 ; CONFIG2 *** ACTIVE RPRE/WPRE/CDLR/tWR/W2R_BUS/R2W_BUS ***
+0x009A029C = 0x24002D6B ; CONFIG3 *** ACTIVE PDEX/PDEN2PDEX/tFAW/AOND/CCDL/CCDS ***
+0x009A02A0 = 0xC4028033 ; CONFIG4 *** ACTIVE REFRESH/tRRD/IDLE_DELAY/CMD2MCIDLE ***
+0x009A02A4 = 0xA6B3A002 ; CONFIG5 ADR_MIN/WRCRC/QSAFE_OFFSET/RDRET/WRRET/INTRP
+0x009A02A8 = 0x11008000 ; CONFIG6 RDFLUSH/WRFLUSH/WDAT_LATENCY/PPD/CMD_ADJUST
+0x009A02AC = 0x00C35000 ; CONFIG7 ZQCS_INTERVAL (32-bit)
+0x009A02B0 = 0x2B291043 ; TIMING0_GEN (RO effective) RC/RFC/RAS <- what HW actually uses
+0x009A02B4 = 0x240A1425 ; TIMING1_GEN (RO effective) R2W/W2R/R2P/W2P
+0x009A02B8 = 0x0905121B ; TIMING2_GEN (RO effective) RD_RCD/WR_RCD/RRD/WDV
+0x009A02BC = 0x003B242A ; TIMING3_GEN (RO effective)
+0x009A02C0 = 0x016B0B0B ; TIMING4_GEN (RO effective)
+0x009A02C4 = 0x489B2718 ; TIMING5_GEN (RO effective)
+0x009A02C8 = 0x29380101 ; TIMING6_GEN (RO effective)
+0x009A02CC = 0x0C023900 ; CONFIG8 ATR_OFFSET/ATR_PADDING/ODT_PADDING/WCK2PH/MRSTWCK
+0x009A02D0 = 0x0B240202 ; TIMING_GEN / cfg (RO)
+0x009A02D4 = 0x11330501 ; TIMING_GEN / cfg (RO)
+0x009A02D8 = 0x180E1024 ; TIMING_GEN / cfg (RO)
+0x009A02DC = 0x01001232 ; TIMING_GEN / cfg (RO)
+0x009A02E0 = 0x00180C27 ; TIMING_GEN / cfg (RO)
+0x009A02E4 = 0x0A000033 ; TIMING_GEN / cfg (RO)
+0x009A02E8 = 0x12400389 ; CONFIG9 QUSE_OFFSET/QUSE_DDLL_SETTLE/DLCELL_SETTLE/QRST/MPRR
+0x009A02EC = 0x08000200 ; cfg/gen (RO)
+0x009A02F0 = 0x0006A0E2 ; cfg/gen (RO)
+0x009A02F4 = 0x00000011 ; CONFIG10 RFC_MSB/IDLE_DELAY_MSB/RD_RCD_MSB/WR_RCD_MSB (high bits)
+0x009A02F8 = 0x0A000080 ; cfg/gen (RO)
+0x009A02FC = 0x00000002 ; cfg/gen (RO)
+
+# ============================================================
+# FBPA Mode Registers / MRS (0x009A0300+)
+# ============================================================
+0x009A0300 = 0x00000003
+0x009A0304 = 0x00100000
+0x009A030C = 0x00100000
+0x009A0320 = 0x00200000
+0x009A0324 = 0x00300000
+0x009A0328 = 0x00200000
+0x009A032C = 0x00300000
+0x009A0330 = 0x00100099
+0x009A0334 = 0x00200019
+0x009A0338 = 0x003000EB
+0x009A033C = 0x00400030
+0x009A0340 = 0x00500000
+0x009A0344 = 0x00600000
+0x009A0348 = 0x00700000
+0x009A034C = 0x00F00000
+0x009A0350 = 0x004906D1
+0x009A0354 = 0x00800000
+0x009A0358 = 0x00900000
+0x009A035C = 0x00E00000
+0x009A0360 = 0x00600000
+0x009A0364 = 0x00600000
+0x009A0368 = 0x00900000
+0x009A036C = 0x00900000
+0x009A0370 = 0x00E00000
+0x009A0374 = 0x00E00000
+0x009A0378 = 0x00400000
+0x009A037C = 0x00500000
+0x009A0380 = 0x00600000
+0x009A0384 = 0x007F0FF0
+0x009A038C = 0x000000A7
+0x009A0390 = 0x00C0052D ; TIMING21 REFSB (single-bank refresh) enable/period
+0x009A0394 = 0x00003124 ; TIMING22 RFCSBA(=292)/RFCSBR(=12) single-bank tRFC
+0x009A039C = 0x00000003 ; TIMING23 CCDR/RD_RANK_SEL/WR_RANK_SEL/WR_CCDL/WR_CCDS
+0x009A03A0 = 0x00000083
+0x009A03A4 = 0x00000040
+0x009A03AC = 0x00000300
+0x009A03E0 = 0x28000008 ; TIMING24 RRDL/CCDMW/ABPA/XS_OFFSET
+0x009A03E4 = 0x00000005 ; CONFIG11 RRDL/CCDMW/WPOST/WEXT_OFFSET/RANK_SEL_DELAY
+0x009A03E8 = 0x28000005
+0x009A03EC = 0x400803E8 ; TIMING25 ZQCAL(=1000)/MRRW(=64)/MRRWL(=256)
+0x009A03F0 = 0xFFFFFF8F
+0x009A03F8 = 0x00003124
+0x009A03FC = 0x00000002
+
+# ============================================================
+# FBPA Controller Config (0x009A0400+)
+# ============================================================
+0x009A0400 = 0x0000171F
+0x009A0410 = 0x30018504
+0x009A0420 = 0x00000143
+0x009A0430 = 0x00000141
+0x009A0440 = 0x22E4FF40
+0x009A0444 = 0x0466443F
+0x009A0448 = 0x120C0606
+0x009A0450 = 0x00480802
+0x009A0454 = 0x00001002
+0x009A0460 = 0x20638646
+0x009A0464 = 0x79CC9CCF
+0x009A0468 = 0xB9C40E40
+0x009A046C = 0x00011FFF
+0x009A04B8 = 0x00000028
+0x009A04C0 = 0x0000100A
+0x009A04C4 = 0x4924924B
+0x009A04C8 = 0x00000001
+0x009A04CC = 0x000000FF
+
+# ============================================================
+# FBPA FBIO / PHY Config (0x009A0500+)
+# ============================================================
+0x009A0500 = 0x80200015
+0x009A0504 = 0x0A000000
+0x009A0508 = 0x80000001
+0x009A050C = 0x00000111
+0x009A0518 = 0x0007E1F8
+0x009A0554 = 0xFFFFFFCF
+0x009A0560 = 0x000FF000
+0x009A0570 = 0x000003E8 ; (1000 decimal - could be refresh interval)
+0x009A0574 = 0x000002EE ; (750 decimal)
+0x009A0584 = 0x00004000
+0x009A0590 = 0x00000D07
+0x009A0594 = 0x00000101
+0x009A0598 = 0x00000003
+
+# ============================================================
+# FBPA PHY / DLL / Training (0x009A0900+)
+# ============================================================
+0x009A08FC = 0xFFFFFFCF
+0x009A0970 = 0x006E0600
+0x009A0978 = 0x88327F09
+0x009A097C = 0x10000000
+0x009A0988 = 0x2001FF00
+0x009A098C = 0x003FC140
+0x009A0994 = 0x00000005
+0x009A0998 = 0x00013500
+0x009A099C = 0x000001F0
+0x009A09A0 = 0x05012858
+0x009A09A4 = 0x05011F00
+0x009A09A8 = 0x00010100
+0x009A09B0 = 0x05513F41
+0x009A09C0 = 0x00000060
+0x009A09C4 = 0x00000060
+0x009A09C8 = 0x0000000A
+0x009A09CC = 0x80180080
+0x009A09D0 = 0x0001FF00
+0x009A09D4 = 0x05017F80
+0x009A09D8 = 0x2010F000
+0x009A09DC = 0x20017F00
+0x009A09E0 = 0x20000000
+0x009A09E4 = 0x00001113
+0x009A09E8 = 0x000001F0
+0x009A09EC = 0x05012858
+0x009A09F0 = 0x05513F41
+0x009A09F4 = 0x0001FF00
+0x009A09F8 = 0x050128D8
+0x009A09FC = 0x00030000
+0x009A0A04 = 0x04000F00
+0x009A0A08 = 0x000080FF
+0x009A0A0C = 0x3F004000
+0x009A0A18 = 0x010FF000
+0x009A0A1C = 0x007F0000
+
+# --- PHY DQ/DQS training values (0x88 repeated = default/untrained) ---
+0x009A0A20 = 0x88888888
+0x009A0A24 = 0x88888888
+0x009A0A28 = 0x88888888
+0x009A0A2C = 0x88888888
+0x009A0A30 = 0x88888888
+0x009A0A34 = 0x88888888
+0x009A0A38 = 0x88888888
+0x009A0A3C = 0x88888888
+0x009A0A40 = 0x88888888
+0x009A0A44 = 0x88888888
+0x009A0A48 = 0x88888888
+0x009A0A4C = 0x88888888
+0x009A0A50 = 0x88888888
+0x009A0A54 = 0x88888888
+0x009A0A58 = 0x88888888
+0x009A0A5C = 0x88888888
+0x009A0A60 = 0x88888888
+0x009A0A64 = 0x88888888
+0x009A0A68 = 0x00008888
+0x009A0A6C = 0x00008888
+
+# --- PHY Config (0x009A0AD0+) ---
+0x009A0AD8 = 0x010720C0
+0x009A0ADC = 0x00042020
+0x009A0AE0 = 0x00080008
+0x009A0AE4 = 0x00001F10
+0x009A0AE8 = 0x05011F00
+0x009A0AEC = 0x11700300
+0x009A0AF4 = 0x0000E420
+0x009A0AF8 = 0x000F3355
+0x009A0AFC = 0x00000100
+
+# ============================================================
+# NV_PFB Range (Memory Controller, 0x00100000+)
+# From separate dump section
+# ============================================================
+
+# (NV_PFB memory-controller registers, live values below)
+0x00100000 = 0x0FFFFFF0
+0x00100004 = 0x0FFFFFF0
+0x00100008 = 0x08049248
+0x0010000C = 0x0000FFF0
+0x00100010 = 0x0000FFF0
+0x00100014 = 0x00000009
+0x00100400 = 0xBADF1100
+0x00100404 = 0xBADF1100
+0x00100408 = 0xBADF1100
+0x0010040C = 0xBADF1100
+0x00100410 = 0xBADF1100
+0x00100414 = 0xBADF1100
+0x00100418 = 0xBADF1100
+0x0010041C = 0xBADF1100
+0x00100420 = 0xBADF1100
+0x00100424 = 0xBADF1100
+0x00100428 = 0xBADF1100
+0x0010042C = 0xBADF1100
+0x00100430 = 0xBADF1100
+0x00100434 = 0xBADF1100
+0x00100438 = 0xBADF1100
+0x0010043C = 0xBADF1100
+0x00100440 = 0xBADF1100
+0x00100444 = 0xBADF1100
+0x00100448 = 0xBADF1100
+0x0010044C = 0xBADF1100
+0x00100450 = 0xBADF1100
+0x00100454 = 0xBADF1100
+0x00100458 = 0xBADF1100
+0x0010045C = 0xBADF1100
+0x00100460 = 0xBADF1100
+0x00100464 = 0xBADF1100
+0x00100468 = 0xBADF1100
+0x0010046C = 0xBADF1100
+0x00100470 = 0xBADF1100
+0x00100474 = 0xBADF1100
+0x00100478 = 0xBADF1100
+0x0010047C = 0xBADF1100
+0x00100480 = 0xBADF1100
+0x00100484 = 0xBADF1100
+0x00100488 = 0xBADF1100
+0x0010048C = 0xBADF1100
+0x00100490 = 0xBADF1100
+0x00100494 = 0xBADF1100
+0x00100498 = 0xBADF1100
+0x0010049C = 0xBADF1100
+0x001004A0 = 0xBADF1100
+0x001004A4 = 0xBADF1100
+0x001004A8 = 0xBADF1100
+0x001004AC = 0xBADF1100
+0x001004B0 = 0xBADF1100
+0x001004B4 = 0xBADF1100
+0x001004B8 = 0xBADF1100
+0x001004BC = 0xBADF1100
+0x001004C0 = 0xBADF1100
+0x001004C4 = 0xBADF1100
+0x001004C8 = 0xBADF1100
+0x001004CC = 0xBADF1100
+0x001004D0 = 0xBADF1100
+0x001004D4 = 0xBADF1100
+0x001004D8 = 0xBADF1100
+0x001004DC = 0xBADF1100
+0x001004E0 = 0xBADF1100
+0x001004E4 = 0xBADF1100
+0x001004E8 = 0xBADF1100
+0x001004EC = 0xBADF1100
+0x001004F0 = 0xBADF1100
+0x001004F4 = 0xBADF1100
+0x001004F8 = 0xBADF1100
+0x001004FC = 0xBADF1100
+0x00100500 = 0xBADF1100
+0x00100504 = 0xBADF1100
+0x00100508 = 0xBADF1100
+0x0010050C = 0xBADF1100
+0x00100510 = 0xBADF1100
+0x00100514 = 0xBADF1100
+0x00100518 = 0xBADF1100
+0x0010051C = 0xBADF1100
+0x00100520 = 0xBADF1100
+0x00100524 = 0xBADF1100
+0x00100528 = 0xBADF1100
+0x0010052C = 0xBADF1100
+0x00100530 = 0xBADF1100
+0x00100534 = 0xBADF1100
+0x00100538 = 0xBADF1100
+0x0010053C = 0xBADF1100
+0x00100540 = 0xBADF1100
+0x00100544 = 0xBADF1100
+0x00100548 = 0xBADF1100
+0x0010054C = 0xBADF1100
+0x00100550 = 0xBADF1100
+0x00100554 = 0xBADF1100
+0x00100558 = 0xBADF1100
+0x0010055C = 0xBADF1100
+0x00100560 = 0xBADF1100
+0x00100564 = 0xBADF1100
+0x00100568 = 0xBADF1100
+0x0010056C = 0xBADF1100
+0x00100570 = 0xBADF1100
+0x00100574 = 0xBADF1100
+0x00100578 = 0xBADF1100
+0x0010057C = 0xBADF1100
+0x00100580 = 0xBADF1100
+0x00100584 = 0xBADF1100
+0x00100588 = 0xBADF1100
+0x0010058C = 0xBADF1100
+0x00100590 = 0xBADF1100
+0x00100594 = 0xBADF1100
+0x00100598 = 0xBADF1100
+0x0010059C = 0xBADF1100
+0x001005A0 = 0xBADF1100
+0x001005A4 = 0xBADF1100
+0x001005A8 = 0xBADF1100
+0x001005AC = 0xBADF1100
+0x001005B0 = 0xBADF1100
+0x001005B4 = 0xBADF1100
+0x001005B8 = 0xBADF1100
+0x001005BC = 0xBADF1100
+0x001005C0 = 0xBADF1100
+0x001005C4 = 0xBADF1100
+0x001005C8 = 0xBADF1100
+0x001005CC = 0xBADF1100
+0x001005D0 = 0xBADF1100
+0x001005D4 = 0xBADF1100
+0x001005D8 = 0xBADF1100
+0x001005DC = 0xBADF1100
+0x001005E0 = 0xBADF1100
+0x001005E4 = 0xBADF1100
+0x001005E8 = 0xBADF1100
+0x001005EC = 0xBADF1100
+0x001005F0 = 0xBADF1100
+0x001005F4 = 0xBADF1100
+0x001005F8 = 0xBADF1100
+0x001005FC = 0xBADF1100
+0x00100600 = 0xBADF1100
+0x00100604 = 0xBADF1100
+0x00100608 = 0xBADF1100
+0x0010060C = 0xBADF1100
+0x00100610 = 0xBADF1100
+0x00100614 = 0xBADF1100
+0x00100618 = 0xBADF1100
+0x0010061C = 0xBADF1100
+0x00100620 = 0xBADF1100
+0x00100624 = 0xBADF1100
+0x00100628 = 0xBADF1100
+0x0010062C = 0xBADF1100
+0x00100630 = 0xBADF1100
+0x00100634 = 0xBADF1100
+0x00100638 = 0xBADF1100
+0x0010063C = 0xBADF1100
+0x00100640 = 0xBADF1100
+0x00100644 = 0xBADF1100
+0x00100648 = 0xBADF1100
+0x0010064C = 0xBADF1100
+0x00100650 = 0xBADF1100
+0x00100654 = 0xBADF1100
+0x00100658 = 0xBADF1100
+0x0010065C = 0xBADF1100
+0x00100660 = 0xBADF1100
+0x00100664 = 0xBADF1100
+0x00100668 = 0xBADF1100
+0x0010066C = 0xBADF1100
+0x00100670 = 0xBADF1100
+0x00100674 = 0xBADF1100
+0x00100678 = 0xBADF1100
+0x0010067C = 0xBADF1100
+0x00100680 = 0xBADF1100
+0x00100684 = 0xBADF1100
+0x00100688 = 0xBADF1100
+0x0010068C = 0xBADF1100
+0x00100690 = 0xBADF1100
+0x00100694 = 0xBADF1100
+0x00100698 = 0xBADF1100
+0x0010069C = 0xBADF1100
+0x001006A0 = 0xBADF1100
+0x001006A4 = 0xBADF1100
+0x001006A8 = 0xBADF1100
+0x001006AC = 0xBADF1100
+0x001006B0 = 0xBADF1100
+0x001006B4 = 0xBADF1100
+0x001006B8 = 0xBADF1100
+0x001006BC = 0xBADF1100
+0x001006C0 = 0xBADF1100
+0x001006C4 = 0xBADF1100
+0x001006C8 = 0xBADF1100
+0x001006CC = 0xBADF1100
+0x001006D0 = 0xBADF1100
+0x001006D4 = 0xBADF1100
+0x001006D8 = 0xBADF1100
+0x001006DC = 0xBADF1100
+0x001006E0 = 0xBADF1100
+0x001006E4 = 0xBADF1100
+0x001006E8 = 0xBADF1100
+0x001006EC = 0xBADF1100
+0x001006F0 = 0xBADF1100
+0x001006F4 = 0xBADF1100
+0x001006F8 = 0xBADF1100
+0x001006FC = 0xBADF1100
+0x00100700 = 0xBADF1100
+0x00100704 = 0xBADF1100
+0x00100708 = 0xBADF1100
+0x0010070C = 0xBADF1100
+0x00100710 = 0xBADF1100
+0x00100714 = 0xBADF1100
+0x00100718 = 0xBADF1100
+0x0010071C = 0xBADF1100
+0x00100720 = 0xBADF1100
+0x00100724 = 0xBADF1100
+0x00100728 = 0xBADF1100
+0x0010072C = 0xBADF1100
+0x00100730 = 0xBADF1100
+0x00100734 = 0xBADF1100
+0x00100738 = 0xBADF1100
+0x0010073C = 0xBADF1100
+0x00100740 = 0xBADF1100
+0x00100744 = 0xBADF1100
+0x00100748 = 0xBADF1100
+0x0010074C = 0xBADF1100
+0x00100750 = 0xBADF1100
+0x00100754 = 0xBADF1100
+0x00100758 = 0xBADF1100
+0x0010075C = 0xBADF1100
+0x00100760 = 0xBADF1100
+0x00100764 = 0xBADF1100
+0x00100768 = 0xBADF1100
+0x0010076C = 0xBADF1100
+0x00100770 = 0xBADF1100
+0x00100774 = 0xBADF1100
+0x00100778 = 0xBADF1100
+0x0010077C = 0xBADF1100
+0x00100780 = 0xBADF1100
+0x00100784 = 0xBADF1100
+0x00100788 = 0xBADF1100
+0x0010078C = 0xBADF1100
+0x00100790 = 0xBADF1100
+0x00100794 = 0xBADF1100
+0x00100798 = 0xBADF1100
+0x0010079C = 0xBADF1100
+0x001007A0 = 0xBADF1100
+0x001007A4 = 0xBADF1100
+0x001007A8 = 0xBADF1100
+0x001007AC = 0xBADF1100
+0x001007B0 = 0xBADF1100
+0x001007B4 = 0xBADF1100
+0x001007B8 = 0xBADF1100
+0x001007BC = 0xBADF1100
+0x001007C0 = 0xBADF1100
+0x001007C4 = 0xBADF1100
+0x001007C8 = 0xBADF1100
+0x001007CC = 0xBADF1100
+0x001007D0 = 0xBADF1100
+0x001007D4 = 0xBADF1100
+0x001007D8 = 0xBADF1100
+0x001007DC = 0xBADF1100
+0x001007E0 = 0xBADF1100
+0x001007E4 = 0xBADF1100
+0x001007E8 = 0xBADF1100
+0x001007EC = 0xBADF1100
+0x001007F0 = 0xBADF1100
+0x001007F4 = 0xBADF1100
+0x001007F8 = 0xBADF1100
+0x001007FC = 0xBADF1100
+0x00100800 = 0x00000010
+0x00100A00 = 0xF0000080
+0x00100A04 = 0x6E00A000
+0x00100A08 = 0x70481718
+0x00100A20 = 0x00000048
+0x00100A24 = 0x00000087
+0x00100A3C = 0x00000003
+0x00100B10 = 0xFFFFFF8F
+0x00100B18 = 0x001FFFFF
+0x00100B20 = 0x00000001
+0x00100B24 = 0x001FFFFF
+0x00100B38 = 0xFFFFFF8F
+0x00100B40 = 0x0001001F
+0x00100B44 = 0x0000200F
+0x00100B50 = 0x0000000E
+0x00100B54 = 0x00000008
+0x00100B58 = 0x00000002
+0x00100B5C = 0x00000002
+0x00100B7C = 0xBADF5108
+0x00100B80 = 0xBADF5108
+0x00100B84 = 0xFFFFFF88
+0x00100B88 = 0x00000402
+0x00100B8C = 0x0003F5E0
+0x00100B90 = 0x00000603
+0x00100B98 = 0x00000003
+0x00100B9C = 0xFFFFFFCF
+0x00100C00 = 0xFFE00000
+0x00100C04 = 0x11111311
+0x00100C08 = 0x11011111
+0x00100C0C = 0x11101111
+0x00100C10 = 0x011AFB50
+0x00100C14 = 0x00040000
+0x00100C24 = 0x11111010
+0x00100C28 = 0x00011111
+0x00100C2C = 0x00111001
+0x00100C30 = 0x00011111
+0x00100C34 = 0x05A00000
+0x00100C38 = 0x003FF3FF
+0x00100C4C = 0x00000049
+0x00100C54 = 0xFF0FFFFF
+0x00100C58 = 0x0F0FFFFF
+0x00100C5C = 0xFFF00FFF
+0x00100C60 = 0xFF0FFFFF
+0x00100C64 = 0x0F0FFFFF
+0x00100C68 = 0xFFF00FFF
+0x00100C6C = 0x11249249
+0x00100C70 = 0x00248249
+0x00100C74 = 0x0000007A
+0x00100C7C = 0x00010400
+0x00100C80 = 0x08018021
+0x00100C98 = 0x00004042
+0x00100CA0 = 0x49A40A0A
+0x00100CA8 = 0x00249249
+0x00100CAC = 0x00000001
+0x00100CB0 = 0x40000040
+0x00100CB8 = 0x00104000
+0x00100CBC = 0x00010001
+0x00100CC0 = 0x00003000
+0x00100CD0 = 0x1FFFF000
+0x00100CD4 = 0x08000880
+0x00100CD8 = 0x1FFFF000
+0x00100CDC = 0x0004CB8F
+0x00100CE0 = 0x0000020B
+0x00100CFC = 0xFF053977
+0x00100D10 = 0x00004042
+0x00100D14 = 0x00200020
+0x00100D30 = 0x00004042
+0x00100D3C = 0x00000042
+0x00100D48 = 0x00004042
+0x00100D70 = 0x44444FFE
+0x00100D74 = 0x0FF358FF
+0x00100D7C = 0x10A00000
+0x00100E28 = 0x00008000
+0x00100E34 = 0xE0018061
+0x00100E38 = 0x00328000
+0x00100E3C = 0x00008000
+0x00100E48 = 0x40004400
+0x00100E4C = 0x11443000
+0x00100E50 = 0xB75C50BA
+0x00100E54 = 0x75399000
+0x00100E58 = 0x4D917D30
+0x00100E5C = 0x14000694
+0x00100E7C = 0x00012204
+0x00100E90 = 0x1002709D
+0x00100EA4 = 0x2000107F
+0x00100EAC = 0x0004CBCF
+0x00100EB4 = 0xA0FFF000
+0x00100EB8 = 0x0004CB8F
+0x00100EBC = 0xFFFFFBFF
+0x00100EC0 = 0x04001410
+0x00100ECC = 0x800FFFFF
+0x00100ED0 = 0x00000036
+0x00100ED4 = 0x00000800
+0x00100EDC = 0x00000080
+0x00100EE0 = 0x00000086
+0x00100EE4 = 0x00840085
+0x00100EE8 = 0x00400083
+0x00100EF8 = 0x60000003
+0x00100EFC = 0x0004CB8F
diff --git a/overclocking/timings/timings-bench.sh b/overclocking/timings/timings-bench.sh
new file mode 100755
index 0000000..afc58be
--- /dev/null
+++ b/overclocking/timings/timings-bench.sh
@@ -0,0 +1,35 @@
+#!/bin/bash
+#
+# Average N benchmark runs, so an effect smaller than the run-to-run noise
+# can be told apart from it.
+#
+# timings-bench.sh 3 runs
+# timings-bench.sh 5 5 runs
+#
+# Prints: read_mean read_sd copy_mean copy_sd (GB/s)
+#
+# Note: the benchmark's "Memory Latency" figure does not move with timings at
+# all - it is dominated by page walks. Tune against these bandwidth numbers.
+#
+set -euo pipefail
+HERE="$(cd "$(dirname "$0")" && pwd)"
+. "${HERE}/_common.sh"
+
+[[ -x "${BENCH}" ]] || die "benchmark not found at ${BENCH} (see benchmark/README or build it)"
+N="${1:-3}"
+
+for ((i = 0; i < N; i++)); do
+ "${BENCH}" 2>/dev/null | awk '
+ /Global Read Bandwidth/ { r=$4 }
+ /Global Copy Bandwidth/ { c=$4 }
+ END { print r, c }'
+done | awk '
+{ r[NR]=$1; c[NR]=$2; sr+=$1; sc+=$2 }
+END {
+ n=NR
+ if (n == 0) { print "no benchmark output"; exit 1 }
+ mr=sr/n; mc=sc/n
+ for (i=1; i<=n; i++) { vr+=(r[i]-mr)^2; vc+=(c[i]-mc)^2 }
+ printf "read %.1f +/- %.1f copy %.1f +/- %.1f GB/s (%d runs)\n",
+ mr, (n>1?sqrt(vr/(n-1)):0), mc, (n>1?sqrt(vc/(n-1)):0), n
+}'
diff --git a/overclocking/timings/timings-dump.sh b/overclocking/timings/timings-dump.sh
new file mode 100755
index 0000000..3415ba9
--- /dev/null
+++ b/overclocking/timings/timings-dump.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+#
+# Snapshot the DRAM timings.
+#
+# timings-dump.sh print the decoded table
+# timings-dump.sh out.txt also write a restorable snapshot
+# GPU=1 timings-dump.sh second card
+#
+# The written file is restorable with: timings-restore.sh out.txt
+#
+set -euo pipefail
+HERE="$(cd "$(dirname "$0")" && pwd)"
+. "${HERE}/_common.sh"
+
+need_root
+build_tool
+
+"${REGS[@]}" dump
+
+if [[ $# -ge 1 ]]; then
+ "${REGS[@]}" save "$1"
+ echo "restore with: ${0##*/} -> timings-restore.sh $1"
+fi
diff --git a/overclocking/timings/timings-probe.sh b/overclocking/timings/timings-probe.sh
new file mode 100755
index 0000000..7812f48
--- /dev/null
+++ b/overclocking/timings/timings-probe.sh
@@ -0,0 +1,65 @@
+#!/bin/bash
+#
+# Find which timings actually bind performance on this card.
+#
+# Each field is loosened on its own, bandwidth is measured, then the field is
+# put back. Loosening only ever grants the DRAM more time, so this maps the
+# card out with no risk of a hang - unlike tightening, which is how you find
+# the floor the hard way.
+#
+# timings-probe.sh probe every field
+# timings-probe.sh RP WR probe just these
+# GPU=1 timings-probe.sh second card
+#
+set -euo pipefail
+HERE="$(cd "$(dirname "$0")" && pwd)"
+. "${HERE}/_common.sh"
+
+need_root
+build_tool
+[[ -x "${BENCH}" ]] || die "benchmark not found at ${BENCH}"
+
+# field:multiplier - how far to loosen each one for the probe
+PROBES=(
+ RC:1.8 RFC:1.2 RAS:1.6 RP:1.6 RD_RCD:1.6 WR_RCD:1.7
+ WR:1.8 FAW:2.0 RRD:2.0 W2R_BUS:1.7 R2W_BUS:1.7 CCDL:2.5 CCDS:3.0
+)
+
+measure() {
+ "${BENCH}" 2>/dev/null | awk '
+ /Global Read Bandwidth/ { r=$4 }
+ /Global Copy Bandwidth/ { c=$4 }
+ END { printf "%.0f %.0f", r, c }'
+}
+
+ensure_baseline >/dev/null
+read -r BR BC < <(measure)
+printf 'baseline read %s copy %s GB/s\n\n' "${BR}" "${BC}"
+printf '%-8s %-12s %6s %6s %s\n' FIELD CHANGE READ COPY 'DELTA read/copy'
+
+for entry in "${PROBES[@]}"; do
+ f="${entry%%:*}"
+ mult="${entry##*:}"
+
+ # Only probe what the caller asked for, if they asked.
+ if [[ $# -gt 0 ]]; then
+ want=0
+ for a in "$@"; do [[ "${a^^}" == "${f}" ]] && want=1; done
+ (( want )) || continue
+ fi
+
+ stock="$("${REGS[@]}" get "${f}" 2>/dev/null)" || continue
+ loose="$(awk -v s="${stock}" -v m="${mult}" 'BEGIN{printf "%d", s*m}')"
+ (( loose <= stock )) && loose=$(( stock + 1 ))
+
+ "${REGS[@]}" set "${f}" "${loose}" >/dev/null 2>&1 || { echo "${f}: set failed"; continue; }
+ read -r R C < <(measure)
+ "${REGS[@]}" set "${f}" "${stock}" >/dev/null 2>&1
+
+ printf '%-8s %-12s %6s %6s %+d / %+d\n' \
+ "${f}" "${stock}->${loose}" "${R}" "${C}" "$((R - BR))" "$((C - BC))"
+done
+
+echo
+echo "restored:"
+"${REGS[@]}" dump | sed -n '/^FIELD/,$p'
diff --git a/overclocking/timings/timings-restore.sh b/overclocking/timings/timings-restore.sh
new file mode 100755
index 0000000..281b1db
--- /dev/null
+++ b/overclocking/timings/timings-restore.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+#
+# Write a saved timing snapshot back to the card.
+#
+# timings-restore.sh out.txt a file from timings-dump.sh
+# timings-restore.sh the baseline taken on first use
+# GPU=1 timings-restore.sh second card
+#
+set -euo pipefail
+HERE="$(cd "$(dirname "$0")" && pwd)"
+. "${HERE}/_common.sh"
+
+need_root
+build_tool
+
+if [[ $# -ge 1 ]]; then
+ file="$1"
+ [[ -f "${file}" ]] || die "no such snapshot: ${file}"
+else
+ file="$(baseline_file)"
+ [[ -f "${file}" ]] || die "no baseline snapshot yet — pass a file, or run timings-set.sh first"
+fi
+
+"${REGS[@]}" load "${file}"
+"${REGS[@]}" dump | sed -n '/^FIELD/,$p'
diff --git a/overclocking/timings/timings-set.sh b/overclocking/timings/timings-set.sh
new file mode 100755
index 0000000..6463060
--- /dev/null
+++ b/overclocking/timings/timings-set.sh
@@ -0,0 +1,78 @@
+#!/bin/bash
+#
+# Set DRAM timings: individual fields, or a percentage scale.
+#
+# timings-set.sh RAS 45 one field
+# timings-set.sh RAS 45 RP 28 several at once
+# timings-set.sh --scale 20 loosen the safe set by 20%
+# timings-set.sh --scale -10 tighten it by 10%
+# timings-set.sh --stock back to the baseline snapshot
+# GPU=1 timings-set.sh --scale 20 second card
+#
+# --scale always computes from the baseline snapshot taken the first time this
+# script runs, so applying it twice does not compound.
+#
+# Scaled by --scale: tRC tRFC tRAS tRP tRCD tWR tFAW tRRD.
+# Never scaled: CL and WL (they must match what is trained into the HBM
+# stacks) and tCCD (the only timing that binds bandwidth, already at its floor).
+#
+# Tightening can corrupt data silently or wedge the memory controller, and
+# writing the old value back does not recover it - only a reboot does.
+#
+set -euo pipefail
+HERE="$(cd "$(dirname "$0")" && pwd)"
+. "${HERE}/_common.sh"
+
+usage() { sed -n '2,/^set -e/p' "$0" | sed 's/^# \{0,1\}//; $d'; exit "${1:-0}"; }
+[[ $# -eq 0 || "${1:-}" == "-h" || "${1:-}" == "--help" ]] && usage
+
+need_root
+build_tool
+
+case "$1" in
+--stock)
+ base="$(baseline_file)"
+ [[ -f "${base}" ]] || die "no baseline snapshot yet — nothing to restore"
+ "${REGS[@]}" load "${base}"
+ "${REGS[@]}" dump | sed -n '/^FIELD/,$p'
+ ;;
+
+--scale)
+ [[ $# -eq 2 ]] || die "--scale takes one percentage, e.g. --scale 20"
+ pct="$2"
+ [[ "${pct}" =~ ^-?[0-9]+$ ]] || die "percentage must be an integer (got '${pct}')"
+ (( pct >= -50 && pct <= 50 )) || die "percentage must be between -50 and 50"
+
+ base="$(ensure_baseline)"
+
+ (( pct < 0 )) && echo "tightening by ${pct#-}% — validate with gpu-burn before trusting it"
+
+ # Always start from the baseline so repeated runs are idempotent.
+ "${REGS[@]}" load "${base}" >/dev/null 2>&1
+
+ for f in "${SCALABLE[@]}"; do
+ stock="$("${REGS[@]}" get "${f}")" || continue
+ target=$(( stock + (stock * pct) / 100 ))
+ (( target < 1 )) && target=1
+ if (( target == stock )); then
+ printf '%-8s %4d unchanged\n' "${f}" "${stock}"
+ continue
+ fi
+ printf '%-8s %4d -> %-4d ' "${f}" "${stock}" "${target}"
+ "${REGS[@]}" set "${f}" "${target}" >/dev/null 2>&1 && echo "ok" || echo "FAILED"
+ done
+ ;;
+
+-*)
+ die "unknown option '$1' (see --help)"
+ ;;
+
+*)
+ (( $# % 2 == 0 )) || die "fields come in NAME VALUE pairs"
+ ensure_baseline >/dev/null
+ while [[ $# -ge 2 ]]; do
+ "${REGS[@]}" set "$1" "$2"
+ shift 2
+ done
+ ;;
+esac