diff --git a/docs/evidence/2026-08-10-ene-artefact-map.log b/docs/evidence/2026-08-10-ene-artefact-map.log new file mode 100644 index 0000000..d837fcd --- /dev/null +++ b/docs/evidence/2026-08-10-ene-artefact-map.log @@ -0,0 +1,62 @@ +== P2 Receive Byte probe at 0x27 — 2026-08-10T18:27:12+02:00 + driver: /lib/modules/7.0.0-28-generic/updates/dkms/i2c-imc-skylake.ko.zst + rounds per channel: 8 + +-- SPD baseline + ch0.dimm0=50E4333A + ch0.dimm1=1FE43236 + ch1.dimm0=50E434BA + ch1.dimm1=1EE433B5 + +-- ch0 (i2c-1) — artefact map, read_byte_data(reg) + reg value N>>2 N>>3 N-1 closer + 0x00 0x00 0x00 0x00 0xff NACK + 0x01 0x00 0x00 0x00 0x00 0x00 + 0x02 0x00 0x00 0x00 0x01 NACK + 0x03 0x00 0x00 0x00 0x02 0x00 + 0x04 0x01 0x01 0x00 0x03 NACK + 0x08 0x02 0x02 0x01 0x07 NACK + 0x0f 0x03 0x03 0x01 0x0e 0x03 + 0x10 0x04 0x04 0x02 0x0f NACK + 0x11 0x04 0x04 0x02 0x10 0x04 + 0x20 0x08 0x08 0x04 0x1f NACK + 0x21 0x02 0x08 0x04 0x20 NACK + 0x40 0x10 0x10 0x08 0x3f NACK + 0x41 0x10 0x10 0x08 0x40 0x10 + 0x80 0x20 0x20 0x10 0x7f NACK + 0xa0 0x28 0x28 0x14 0x9f NACK + 0xa1 0x28 0x28 0x14 0xa0 0xe8 + 0xaf 0xeb 0x2b 0x15 0xae 0xeb + 0xe1 0xf8 0x38 0x1c 0xe0 0xf8 + 0xec 0xfb 0x3b 0x1d 0xeb NACK + 0xff 0x3f 0x3f 0x1f 0xfe 0xff + +-- ch1 (i2c-2) — artefact map, read_byte_data(reg) + reg value N>>2 N>>3 N-1 closer + 0x00 0x00 0x00 0x00 0xff NACK + 0x01 0x00 0x00 0x00 0x00 0x00 + 0x02 0x00 0x00 0x00 0x01 NACK + 0x03 0x00 0x00 0x00 0x02 0x00 + 0x04 0x01 0x01 0x00 0x03 NACK + 0x08 0x02 0x02 0x01 0x07 NACK + 0x0f 0x03 0x03 0x01 0x0e 0x03 + 0x10 0x04 0x04 0x02 0x0f NACK + 0x11 0x02 0x04 0x02 0x10 NACK + 0x20 0x02 0x08 0x04 0x1f NACK + 0x21 0x08 0x08 0x04 0x20 0x08 + 0x40 0x10 0x10 0x08 0x3f NACK + 0x41 0x10 0x10 0x08 0x40 0x10 + 0x80 0x20 0x20 0x10 0x7f NACK + 0xa0 0x28 0x28 0x14 0x9f NACK + 0xa1 0x28 0x28 0x14 0xa0 0xe8 + 0xaf 0xfa 0x2b 0x15 0xae 0xeb + 0xe1 0xf8 0x38 0x1c 0xe0 0xf8 + 0xec 0xfb 0x3b 0x1d 0xeb NACK + 0xff 0x0f 0x3f 0x1f 0xfe 0xff + +-- SPD after + ch0.dimm0=50E4333A + ch0.dimm1=1FE43236 + ch1.dimm0=50E434BA + ch1.dimm1=1EE433B5 +SPD integrity: OK (4/4 serials unchanged) diff --git a/docs/evidence/2026-08-10-ene-receive-byte-addrsweep.log b/docs/evidence/2026-08-10-ene-receive-byte-addrsweep.log new file mode 100644 index 0000000..930713a --- /dev/null +++ b/docs/evidence/2026-08-10-ene-receive-byte-addrsweep.log @@ -0,0 +1,38 @@ +== P2 Receive Byte probe at 0x27 — 2026-08-10T18:29:29+02:00 + driver: /lib/modules/7.0.0-28-generic/updates/dkms/i2c-imc-skylake.ko.zst + rounds per channel: 8 + +-- SPD baseline + ch0.dimm0=50E4333A + ch0.dimm1=1FE43236 + ch1.dimm0=50E434BA + ch1.dimm1=1EE433B5 + +-- ch0 (i2c-1) — receive_byte across addresses + 0x27 0xe1 0xe1 0xe1 + 0x30 0xff 0xff 0xff + 0x31 0xff 0xff 0xff + 0x34 0xff 0xff 0xff + 0x35 0xff 0xff 0xff + 0x36 ERR:Error: Could not set address to 0x36: Device or resource busy ERR:Error: Could not set address to 0x36: Device or resource busy ERR:Error: Could not set address to 0x36: Device or resource busy + 0x37 ERR:Error: Could not set address to 0x37: Device or resource busy ERR:Error: Could not set address to 0x37: Device or resource busy ERR:Error: Could not set address to 0x37: Device or resource busy + 0x2a NACK NACK NACK + 0x2b NACK NACK NACK + +-- ch1 (i2c-2) — receive_byte across addresses + 0x27 0xe1 0xe1 0xe1 + 0x30 0xff 0xff 0xff + 0x31 0xff 0xff 0xff + 0x34 0xff 0xff 0xff + 0x35 0xff 0xff 0xff + 0x36 ERR:Error: Could not set address to 0x36: Device or resource busy ERR:Error: Could not set address to 0x36: Device or resource busy ERR:Error: Could not set address to 0x36: Device or resource busy + 0x37 ERR:Error: Could not set address to 0x37: Device or resource busy ERR:Error: Could not set address to 0x37: Device or resource busy ERR:Error: Could not set address to 0x37: Device or resource busy + 0x2a NACK NACK NACK + 0x2b NACK NACK NACK + +-- SPD after + ch0.dimm0=50E4333A + ch0.dimm1=1FE43236 + ch1.dimm0=50E434BA + ch1.dimm1=1EE433B5 +SPD integrity: OK (4/4 serials unchanged) diff --git a/docs/evidence/2026-08-10-ene-receive-byte-latch.log b/docs/evidence/2026-08-10-ene-receive-byte-latch.log new file mode 100644 index 0000000..9976ada --- /dev/null +++ b/docs/evidence/2026-08-10-ene-receive-byte-latch.log @@ -0,0 +1,40 @@ +== P2 Receive Byte probe at 0x27 — 2026-08-10T18:30:45+02:00 + driver: /lib/modules/7.0.0-28-generic/updates/dkms/i2c-imc-skylake.ko.zst + rounds per channel: 8 + +-- SPD baseline + ch0.dimm0=50E4333A + ch0.dimm1=1FE43236 + ch1.dimm0=50E434BA + ch1.dimm1=1EE433B5 + +-- ch0 (i2c-1) — does receive_byte echo the last command byte? + probe_reg read_byte_data receive_byte verdict + 0x33 0x0c 0x33 ECHOES the command byte + 0x55 0x15 0x55 ECHOES the command byte + 0x77 0x1d 0x77 ECHOES the command byte + 0xa5 0x29 0xa5 ECHOES the command byte + 0x0c 0xc1 0x0c ECHOES the command byte + 0x33 0x0c 0x33 ECHOES the command byte + cross-address: command byte to 0x27, Receive Byte from 0x31 + 0x27 reg 0x5a -> 0x16 then 0x31 receive_byte -> 0xff + 0x27 reg 0x3c -> 0x0f then 0x31 receive_byte -> 0xff + +-- ch1 (i2c-2) — does receive_byte echo the last command byte? + probe_reg read_byte_data receive_byte verdict + 0x33 0x0c 0x33 ECHOES the command byte + 0x55 0x15 0x55 ECHOES the command byte + 0x77 0x1d 0x77 ECHOES the command byte + 0xa5 0x29 0xa5 ECHOES the command byte + 0x0c 0xc3 0x0c ECHOES the command byte + 0x33 0x0c 0x33 ECHOES the command byte + cross-address: command byte to 0x27, Receive Byte from 0x31 + 0x27 reg 0x5a -> 0x16 then 0x31 receive_byte -> 0xff + 0x27 reg 0x3c -> 0x0f then 0x31 receive_byte -> 0xff + +-- SPD after + ch0.dimm0=50E4333A + ch0.dimm1=1FE43236 + ch1.dimm0=50E434BA + ch1.dimm1=1EE433B5 +SPD integrity: OK (4/4 serials unchanged) diff --git a/docs/evidence/2026-08-10-ene-receive-byte.log b/docs/evidence/2026-08-10-ene-receive-byte.log new file mode 100644 index 0000000..eff59a2 --- /dev/null +++ b/docs/evidence/2026-08-10-ene-receive-byte.log @@ -0,0 +1,47 @@ +== P2 Receive Byte probe at 0x27 — 2026-08-10T18:28:35+02:00 + driver: /lib/modules/7.0.0-28-generic/updates/dkms/i2c-imc-skylake.ko.zst + rounds per channel: 12 + +-- SPD baseline + ch0.dimm0=50E4333A + ch0.dimm1=1FE43236 + ch1.dimm0=50E434BA + ch1.dimm1=1EE433B5 + +-- ch0 (i2c-1) + control read_byte_data(0x10) = 0x04 [after 1 retry] (poison-close read: NACK) + round 1 receive_byte = 0x10 retry: - (closer: 0x10 ) spd-live: 50E4333A + round 2 receive_byte = 0x10 retry: - (closer: 0x10 ) spd-live: 50E4333A + round 3 receive_byte = 0x10 retry: - (closer: 0x10 ) spd-live: 50E4333A + round 4 receive_byte = 0x10 retry: - (closer: 0x10 ) spd-live: 50E4333A + round 5 receive_byte = 0x10 retry: - (closer: 0x10 ) spd-live: 50E4333A + round 6 receive_byte = 0x10 retry: - (closer: 0x10 ) spd-live: 50E4333A + round 7 receive_byte = 0x10 retry: - (closer: 0x10 ) spd-live: 50E4333A + round 8 receive_byte = 0x10 retry: - (closer: 0x10 ) spd-live: 50E4333A + round 9 receive_byte = 0x10 retry: - (closer: 0x10 ) spd-live: 50E4333A + round 10 receive_byte = 0x10 retry: - (closer: 0x10 ) spd-live: 50E4333A + round 11 receive_byte = 0x10 retry: - (closer: 0x10 ) spd-live: 50E4333A + round 12 receive_byte = 0x10 retry: - (closer: 0x10 ) spd-live: 50E4333A + +-- ch1 (i2c-2) + control read_byte_data(0x10) = 0x04 [after 1 retry] (poison-close read: NACK) + round 1 receive_byte = 0x10 retry: - (closer: 0x10 ) spd-live: 50E4333A + round 2 receive_byte = 0x10 retry: - (closer: 0x10 ) spd-live: 50E4333A + round 3 receive_byte = 0x10 retry: - (closer: 0x10 ) spd-live: 50E4333A + round 4 receive_byte = 0x10 retry: - (closer: 0x10 ) spd-live: 50E4333A + round 5 receive_byte = 0x10 retry: - (closer: 0x10 ) spd-live: 50E4333A + round 6 receive_byte = 0x10 retry: - (closer: 0x10 ) spd-live: 50E4333A + round 7 receive_byte = 0x10 retry: - (closer: 0x10 ) spd-live: 50E4333A + round 8 receive_byte = 0x10 retry: - (closer: 0x10 ) spd-live: 50E4333A + round 9 receive_byte = 0x10 retry: - (closer: 0x10 ) spd-live: 50E4333A + round 10 receive_byte = 0x10 retry: - (closer: 0x10 ) spd-live: 50E4333A + round 11 receive_byte = 0x10 retry: - (closer: 0x10 ) spd-live: 50E4333A + round 12 receive_byte = 0x10 retry: - (closer: 0x10 ) spd-live: 50E4333A + +-- SPD after + ch0.dimm0=50E4333A + ch0.dimm1=1FE43236 + ch1.dimm0=50E434BA + ch1.dimm1=1EE433B5 + +SPD integrity: OK (4/4 serials unchanged) diff --git a/tools/README.md b/tools/README.md index 637d1ff..b133862 100644 --- a/tools/README.md +++ b/tools/README.md @@ -13,6 +13,19 @@ raw numbers. Do not paraphrase them. | `dump-pcu-config.sh` | Which PCU registers move on their own? Is there a TSOD polling control? | no | | `probe-cf8-write.sh` | Is the CF8/CFC write really dropped, and does it raise an SMI? | **yes**, requires `--i-understand` | +Not part of the submission: `pre-poweroff-load.sh` — fixed, mixed SMBus load +to run before every poweroff of the A5 hang observation window (TODO.md E1), +identical in the `use_ecam=1` phase and the `use_ecam=0` counter-proof. +3-minute blocks: software effect stream (~2760 writes/s) → rapid hw/sw switch +burst → hardware effect → full SPD dump via ee1004 + serial integrity check. +Fully automatic: saves run context + any driver warnings to a per-run file in +`tools/pre-poweroff-runs/`, appends one summary line to +`pre-poweroff-load.log`, then — **only with `--poweroff`** — powers the +machine off (15 s grace, Ctrl-C aborts); without the flag it just reports and +exits. Run: `sudo ./tools/pre-poweroff-load.sh [minutes] [--poweroff]` +(default 30; re-execs under sudo by itself). The one exception: an SPD serial +mismatch saves everything and does NOT power off — investigate first. + ## 1. `watch-engine.sh` — foreign activity (read-only) Samples the six per-channel registers and reports every change that this @@ -133,3 +146,50 @@ shown the encoding works. A channel found on page 1 is not a fault of this driver: `ee1004`, and OpenRGB's `DDR4DirectAccessor::set_page()`, both select a page and leave it selected. + +## 8. `lore-thread-check.py` — "la patch è stata pubblicata? qualcuno ha risposto?" + +Niente hardware: interroga l'archivio **NNTP** di lore, `nntp.lore.kernel.org:119`, +e dice (a) se un message-id è nell'archivio della lista, con gli header veri, e +(b) quali articoli successivi appartengono allo stesso thread. Sola lettura, il +server rifiuta il posting comunque. + +```bash +./tools/lore-thread-check.py 20260808212414.217511-1-simone.chifari@gmail.com +./tools/lore-thread-check.py --group org.kernel.vger.linux-kernel +``` + +Esce `0` se il messaggio è archiviato, `1` se non c'è, `2` su errore. + +> ⚠️ Esiste perché dal browser o con `curl` la risposta è ingannevole: +> `lore.kernel.org` e `patchwork.kernel.org` stanno dietro il bot-check **Anubis** +> e restituiscono 403 o la pagina *"Making sure you're not a bot!"* al posto del +> messaggio — che si legge facilmente come "la patch non è mai stata pubblicata". +> Il mirror NNTP non ha il challenge. Per patchwork non esiste equivalente: lì +> serve un browser vero. + +## 9. `probe-ene-receive-byte.sh` — la Receive Byte a 0x27 (read-only) + +L'esperimento di `TODO.md` §P2, e le sue varianti diagnostiche. Nessuna +scrittura, su nessun indirizzo: solo Receive Byte e Read Byte Data verso `0x27` +e letture SPD dai nodi sysfs di `ee1004`. Le quattro seriali SPD sono +confrontate prima e dopo a ogni run e una differenza è errore fatale. + +```bash +sudo systemctl stop furyd # lo script rifiuta di partire con furyd attivo +sudo ./tools/probe-ene-receive-byte.sh --rounds 12 # l'esperimento +sudo ./tools/probe-ene-receive-byte.sh --latch-test # eco del command byte + cross-address +sudo ./tools/probe-ene-receive-byte.sh --map # ri-deriva l'artefatto N>>2 +sudo ./tools/probe-ene-receive-byte.sh --addr-sweep # stessa read su altri indirizzi +sudo systemctl start furyd +``` + +Esito del 2026-08-10 (log in `docs/evidence/2026-08-10-ene-*.log`): la Receive +Byte restituisce l'**eco dell'ultimo command byte** mandato a quel chip (12/12, +due canali), il latch è del device e non dell'engine (da `0x31` torna `0xff`), +gli indirizzi vuoti NACKano. **L'ENE non ha stato leggibile** — ora misurato. + +> ⚠️ Due trappole, valgono per qualunque misura su questo engine: la **prima** +> transazione di un canale restituisce un valore stale (per questo lo script fa +> un read di warm-up scartato), e la lettura successiva a una riuscita NACKa +> spesso — un solo retry la pulisce sempre, come dice P1. diff --git a/tools/lore-thread-check.py b/tools/lore-thread-check.py new file mode 100755 index 0000000..04d04ed --- /dev/null +++ b/tools/lore-thread-check.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +"""Check on lore.kernel.org whether a posting was archived, and list its replies. + +Why NNTP and not the web UI: since mid-2026 lore.kernel.org (and +patchwork.kernel.org) sit behind an Anubis proof-of-work bot check, so plain +HTTP from curl or a script gets the "Making sure you're not a bot!" +interstitial instead of the message -- HTTP 403 without a browser User-Agent, +HTTP 200 with the interstitial body if one is set. The NNTP mirror on +nntp.lore.kernel.org:119 serves the same archive with no challenge, so it is +the reliable way to answer "was my patch actually published, and did anyone +answer" from a shell. + +Read-only: it opens the group, asks for the article head, and scans the +overview records after it. It never posts (the server refuses posting anyway). + +Usage: + ./tools/lore-thread-check.py [--group org.kernel.vger.linux-i2c] + +The message-id is given without angle brackets, e.g. + + ./tools/lore-thread-check.py 20260808212414.217511-1-simone.chifari@gmail.com + +Exit status: 0 if the message is in the archive, 1 if it is not, 2 on error. +""" + +import argparse +import socket +import sys + +DEFAULT_GROUP = "org.kernel.vger.linux-i2c" +HOST = "nntp.lore.kernel.org" +PORT = 119 +HEADERS_OF_INTEREST = ( + "from", "to", "cc", "subject", "date", "message-id", + "in-reply-to", "references", "x-mailing-list", +) + + +class Nntp: + def __init__(self, host, port, timeout=60): + self.sock = socket.create_connection((host, port), timeout=timeout) + self.fp = self.sock.makefile("rb") + self.banner = self._line() + + def _line(self): + return self.fp.readline().decode("utf-8", "replace").rstrip("\r\n") + + def cmd(self, text): + self.sock.sendall((text + "\r\n").encode()) + return self._line() + + def block(self): + out = [] + while True: + line = self._line() + if line == ".": + return out + out.append(line[1:] if line.startswith("..") else line) + + def close(self): + try: + self.cmd("QUIT") + finally: + self.sock.close() + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("msgid", help="message-id without angle brackets") + ap.add_argument("--group", default=DEFAULT_GROUP, + help="lore NNTP group (default: %(default)s)") + ap.add_argument("--scan", type=int, default=0, metavar="N", + help="also scan the N articles before the message " + "(default: only the ones after it)") + args = ap.parse_args() + + msgid = args.msgid.strip().strip("<>") + + try: + nntp = Nntp(HOST, PORT) + except OSError as exc: + print(f"cannot reach {HOST}:{PORT}: {exc}", file=sys.stderr) + return 2 + + try: + nntp.cmd("MODE READER") + resp = nntp.cmd("GROUP " + args.group) + if not resp.startswith("211"): + print(f"GROUP {args.group}: {resp}", file=sys.stderr) + return 2 + _, _, low, high, _ = resp.split(None, 4) + + resp = nntp.cmd(f"HEAD <{msgid}>") + if not resp.startswith("221"): + print(f"NOT in {args.group}: <{msgid}> ({resp})") + return 1 + + article = resp.split()[1] + print(f"archived: article {article} of {args.group} (range {low}-{high})") + for line in nntp.block(): + if line[:1] in (" ", "\t"): + continue + if line.split(":", 1)[0].lower() in HEADERS_OF_INTEREST: + print(" ", line) + + first = max(int(low), int(article) - args.scan) + resp = nntp.cmd(f"XOVER {first}-{high}") + if not resp.startswith("224"): + print(f"XOVER: {resp}", file=sys.stderr) + return 2 + + replies = [] + for line in nntp.block(): + f = line.split("\t") + if len(f) < 6: + continue + num, subject, sender, date, this_id, refs = f[:6] + if num == article: + continue + if msgid in refs or msgid in this_id: + replies.append((num, date, sender, subject)) + + print(f"\nsame thread, articles {first}-{high}: {len(replies)}") + for num, date, sender, subject in replies: + print(f" {num} | {date} | {sender} | {subject}") + if not replies: + print(" (none -- no reply and no follow-up patch of this thread)") + return 0 + finally: + nntp.close() + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/probe-ene-receive-byte.sh b/tools/probe-ene-receive-byte.sh new file mode 100755 index 0000000..6116fc7 --- /dev/null +++ b/tools/probe-ene-receive-byte.sh @@ -0,0 +1,334 @@ +#!/usr/bin/env bash +# +# P2 — Receive Byte at the ENE LED controller (0x27), both iMC channels. +# +# The question. Reads of 0x27 with a command byte return a deterministic +# artefact *of the command byte we send*: even register N -> N>>2, odd N -> +# N-1 (furypy research, 2026-06-10/12). That measurement is why "the ENE is +# write-only" was concluded by elimination. A Receive Byte sends no command +# byte at all (SMBus Read Byte, PNTR_SEL = BIT(18), added to this driver on +# 2026-07-27), so whatever comes back cannot be a transform of a register that +# never went on the wire. +# +# NACK, or a constant 0x00 -> write-only is a property of the ENE slave, +# measured directly instead of inferred. +# a stable value not derivable from the command word +# -> device data; the read-back question is open +# again and the follow-up is a known-value +# write via fury, then Receive Byte. +# +# READ-ONLY. Every transaction is a read: Receive Byte and Read Byte Data at +# 0x27, and SPD reads through the bound ee1004 sysfs nodes. Nothing is written +# to any address, so the DDR4 page latch cannot move and no LED state changes. +# +# Design constraints, from TODO.md P1/P2: +# * NACK poison: a read of 0x27 poisons the *next* read on the same channel +# (~deterministic back-to-back; one retry clears it; the state is consumed +# by any next transaction). Every measured read is therefore followed by a +# discarded closing read, and the discarded result is reported separately +# so it is never mistaken for a result. +# * Control the artefact predicts: Read Byte Data of register 0x10 must +# return 0x04 (N>>2). If it does not, the artefact changed and the whole +# comparison is void — the script says so and stops. +# * SPD integrity: the four serials are read before and after and compared. +# A mismatch is a hard failure. +# * furyd must not be driving the bus during the run (it holds its own lock +# and this script deliberately does not take it). +# +# Usage: sudo ./tools/probe-ene-receive-byte.sh [--rounds N] [--allow-furyd] + +set -u + +cd "$(dirname "$0")/.." +# shellcheck source=tools/common.sh +. tools/common.sh + +ENE_ADDR=0x27 +CONTROL_REG=0x10 +CONTROL_EXPECT=0x04 +BUSES=("1:ch0" "2:ch1") +SPD_NODES=( + "ch0.dimm0:/sys/bus/i2c/devices/1-0050/eeprom" + "ch0.dimm1:/sys/bus/i2c/devices/1-0052/eeprom" + "ch1.dimm0:/sys/bus/i2c/devices/2-0050/eeprom" + "ch1.dimm1:/sys/bus/i2c/devices/2-0052/eeprom" +) +rounds=8 +allow_furyd=0 +map_only=0 +force=0 +addr_sweep=0 +latch_test=0 +MAP_REGS=(0x00 0x01 0x02 0x03 0x04 0x08 0x0f 0x10 0x11 0x20 0x21 0x40 0x41 + 0x80 0xa0 0xa1 0xaf 0xe1 0xec 0xff) + +while (($#)); do + case $1 in + --rounds) + rounds=${2:?--rounds needs a number} + shift 2 + ;; + --addr-sweep) + # Same Receive Byte, other addresses with known read-only semantics — + # tells a device answer apart from an engine-wide constant. + addr_sweep=1 + shift + ;; + --latch-test) + # Decisive test for "the Receive Byte answers with the last command + # byte that went on the wire": send a Read Byte Data of a chosen + # register, then a Receive Byte, and see whether it echoes that + # register. Read-only. + latch_test=1 + shift + ;; + --map) + # Re-derive the command-word artefact on this driver instead of + # assuming the 2026-06 mapping. Read Byte Data only, read-only. + map_only=1 + shift + ;; + --force) + # Run the Receive Byte even if the artefact control disagrees with + # CONTROL_EXPECT — for when --map has established what the current + # artefact is and the comparison is being made against that instead. + force=1 + shift + ;; + --allow-furyd) + allow_furyd=1 + shift + ;; + -h | --help) + sed -n '3,36p' "$0" + exit 0 + ;; + *) + die "unknown argument: $1" + ;; + esac +done + +require_root +require_cmd i2cget i2cdetect +driver_is_loaded || die "i2c_imc_skylake is not loaded" + +if ((!allow_furyd)) && systemctl is-active --quiet furyd; then + die "furyd is active and would share the bus. Stop it for the run + (systemctl stop furyd; systemctl start furyd afterwards), or pass + --allow-furyd if you accept the interleaving." +fi + +# SPD serial number: DDR4 bytes 325-328 (0x145-0x148), big-endian in the dump. +spd_serial() +{ + local node=$1 + od -An -tx1 -j 325 -N 4 "$node" 2>/dev/null | tr -d ' \n' | tr 'a-f' 'A-F' +} + +snapshot_spd() +{ + local entry name node + for entry in "${SPD_NODES[@]}"; do + name=${entry%%:*} + node=${entry#*:} + printf '%s=%s\n' "$name" "$(spd_serial "$node")" + done +} + +# read_ene [reg] -> prints "value" or "NACK"/"ERR:"; never fails +# the script, because a refusal is an answer here. +read_ene() +{ + local bus=$1 reg=${2:-} out rc + if [[ -n $reg ]]; then + out=$(i2cget -y "$bus" "$ENE_ADDR" "$reg" 2>&1) + else + out=$(i2cget -y "$bus" "$ENE_ADDR" 2>&1) + fi + rc=$? + if ((rc == 0)); then + printf '%s\n' "$out" + return + fi + case $out in + *"Read failed"*) printf 'NACK\n' ;; + *) printf 'ERR:%s\n' "${out//$'\n'/ }" ;; + esac +} + +echo "== P2 Receive Byte probe at $ENE_ADDR — $(date -Is)" +echo " driver: $(modinfo -F filename i2c-imc-skylake 2>/dev/null || echo '?')" +echo " rounds per channel: $rounds" +echo + +echo "-- SPD baseline" +spd_before=$(snapshot_spd) +printf '%s\n' "$spd_before" | sed 's/^/ /' +[[ -n $spd_before ]] || die "no SPD serials read — is ee1004 bound?" +echo + +if ((latch_test)); then + for entry in "${BUSES[@]}"; do + bus=${entry%%:*} + label=${entry##*:} + echo "-- $label (i2c-$bus) — does receive_byte echo the last command byte?" + echo " probe_reg read_byte_data receive_byte verdict" + for reg in 0x33 0x55 0x77 0xa5 0x0c 0x33; do + # The byte-data read is the thing that puts a command byte on the + # wire. One retry each, per the P1 poison policy. + bd=$(read_ene "$bus" "$reg") + [[ $bd == NACK ]] && bd=$(read_ene "$bus" "$reg") + rb=$(read_ene "$bus") + [[ $rb == NACK ]] && rb=$(read_ene "$bus") + verdict="no" + [[ $rb == "$(printf '0x%02x' "$((reg))")" ]] && verdict="ECHOES the command byte" + printf ' %-10s %-15s %-13s %s\n' "$reg" "$bd" "$rb" "$verdict" + done + # Where does the latch live: in the ENE chip (a register pointer it + # keeps) or in the iMC engine (its own last command byte)? Put a + # command byte on the wire towards 0x27, then take the Receive Byte + # from a *different* address that also ACKs (RPS0 at 0x31). An echo + # there means the latch is the engine's and says nothing about any + # chip; 0xff (what RPS answers on its own) means it is per-device. + echo " cross-address: command byte to 0x27, Receive Byte from 0x31" + for reg in 0x5a 0x3c; do + bd=$(read_ene "$bus" "$reg") + [[ $bd == NACK ]] && bd=$(read_ene "$bus" "$reg") + rb=$(ENE_ADDR=0x31 read_ene "$bus") + [[ $rb == NACK ]] && rb=$(ENE_ADDR=0x31 read_ene "$bus") + printf ' 0x27 reg %-6s -> %-6s then 0x31 receive_byte -> %s\n' \ + "$reg" "$bd" "$rb" + done + read_ene "$bus" >/dev/null + echo + done + echo "-- SPD after" + spd_after=$(snapshot_spd) + printf '%s\n' "$spd_after" | sed 's/^/ /' + [[ $spd_before == "$spd_after" ]] && + echo "SPD integrity: OK (4/4 serials unchanged)" || + { echo "SPD integrity: !! MISMATCH"; exit 4; } + exit 0 +fi + +if ((addr_sweep)); then + # Is a stable Receive Byte answer at 0x27 device data, or the same thing + # every address says on this engine? Compare against addresses whose + # Receive Byte semantics are known and read-only: RPSn (0x30/0x31/0x34/ + # 0x35, JEDEC Read Protection Status — ACK = unprotected), RPA (0x36/0x37, + # Read Page Address) and an address with nothing on it. + for entry in "${BUSES[@]}"; do + bus=${entry%%:*} + label=${entry##*:} + echo "-- $label (i2c-$bus) — receive_byte across addresses" + for addr in 0x27 0x30 0x31 0x34 0x35 0x36 0x37 0x2a 0x2b; do + a1=$(ENE_ADDR=$addr read_ene "$bus") + a2=$(ENE_ADDR=$addr read_ene "$bus") + a3=$(ENE_ADDR=$addr read_ene "$bus") + printf ' %-6s %-6s %-6s %-6s\n' "$addr" "$a1" "$a2" "$a3" + done + echo + done + echo "-- SPD after" + spd_after=$(snapshot_spd) + printf '%s\n' "$spd_after" | sed 's/^/ /' + [[ $spd_before == "$spd_after" ]] && + echo "SPD integrity: OK (4/4 serials unchanged)" || + { echo "SPD integrity: !! MISMATCH"; exit 4; } + exit 0 +fi + +if ((map_only)); then + for entry in "${BUSES[@]}"; do + bus=${entry%%:*} + label=${entry##*:} + echo "-- $label (i2c-$bus) — artefact map, read_byte_data(reg)" + echo " reg value N>>2 N>>3 N-1 closer" + for reg in "${MAP_REGS[@]}"; do + val=$(read_ene "$bus" "$reg") + closer=$(read_ene "$bus" "$reg") + n=$((reg)) + printf ' %-6s %-7s 0x%02x 0x%02x 0x%02x %s\n' \ + "$reg" "$val" "$((n >> 2))" "$((n >> 3))" \ + "$(((n - 1) & 0xff))" "$closer" + done + read_ene "$bus" >/dev/null + echo + done + echo "-- SPD after" + spd_after=$(snapshot_spd) + printf '%s\n' "$spd_after" | sed 's/^/ /' + [[ $spd_before == "$spd_after" ]] && + echo "SPD integrity: OK (4/4 serials unchanged)" || + { echo "SPD integrity: !! MISMATCH"; exit 4; } + exit 0 +fi + +for entry in "${BUSES[@]}"; do + bus=${entry%%:*} + label=${entry##*:} + echo "-- $label (i2c-$bus)" + + # 1. The artefact control. A warm-up read first: measured 2026-08-10, the + # very first transaction on a channel after another process (furyd) + # stopped touching it answers with a stale value — reg 0x10 returned + # 0x02 as the session's first read and 0x04 on every later one, on both + # channels (docs/evidence/2026-08-10-ene-artefact-map.log). Same shape + # as the P1 poison: the engine hands back what the *previous* + # transaction left. So the control only means something warm. + # + # The retry is P1's documented client policy, not a fudge: a read of + # 0x27 poisons the *next* read on the same channel and one retry always + # clears it. Both attempts are printed so the poison stays visible. + read_ene "$bus" 0x00 >/dev/null + ctl=$(read_ene "$bus" "$CONTROL_REG") + ctl_retry="" + if [[ $ctl == NACK ]]; then + ctl_retry=$(read_ene "$bus" "$CONTROL_REG") + ctl=$ctl_retry + fi + ctl_close=$(read_ene "$bus" "$CONTROL_REG") + printf ' control read_byte_data(%s) = %-6s%s (poison-close read: %s)\n' \ + "$CONTROL_REG" "$ctl" \ + "$([[ -n $ctl_retry ]] && echo ' [after 1 retry]')" "$ctl_close" + if [[ $ctl != "$CONTROL_EXPECT" ]]; then + echo " !! artefact differs from the 2026-06 mapping:" + echo " expected $CONTROL_EXPECT (N>>2) on reg $CONTROL_REG, got $ctl." + if ((!force)); then + echo " Run --map to re-derive it, then --force to compare" + echo " against the current artefact instead. Stopping." + exit 3 + fi + echo " --force given: continuing against the current artefact." + fi + + # 2. The experiment itself. + for ((i = 1; i <= rounds; i++)); do + val=$(read_ene "$bus") + retry="" + [[ $val == NACK ]] && retry=$(read_ene "$bus") + closer=$(read_ene "$bus") + spd_live=$(spd_serial "${SPD_NODES[0]#*:}") + printf ' round %-2d receive_byte = %-6s retry: %-6s (closer: %-6s) spd-live: %s\n' \ + "$i" "$val" "${retry:--}" "$closer" "${spd_live:-READ-FAILED}" + done + + # 3. Leave the channel clean: one more discarded read consumes any + # poison left by the last measured one. + read_ene "$bus" >/dev/null + echo +done + +echo "-- SPD after" +spd_after=$(snapshot_spd) +printf '%s\n' "$spd_after" | sed 's/^/ /' +echo + +if [[ $spd_before == "$spd_after" ]]; then + echo "SPD integrity: OK (4/4 serials unchanged)" +else + echo "SPD integrity: !! MISMATCH — investigate before anything else" + diff <(printf '%s\n' "$spd_before") <(printf '%s\n' "$spd_after") + exit 4 +fi