Compare commits

18 Commits
Author SHA1 Message Date
Simone 9d0faba8d7 Tabella fuori dall'elenco puntato (W161)
La tabella indentata dentro il bullet CPUID -> Device ID non veniva resa da
Obsidian: spostata fuori dall'elenco con rimando e didascalia.
2026-09-09 02:25:06 +02:00
Simone f75d13dec9 Ignora STORICO.md al posto di TODO-chiusi.md (symlink verso private/)
Il file dei chiusi si chiama STORICO.md dal 2026-09-09 (Task 25 del giro
wiki-decisioni-2026-09-08); il symlink nel padre segue il nome nuovo.
2026-09-09 00:55:20 +02:00
Simone 2bc5145cf2 gitignore: aggiunto TODO-chiusi.md, symlink verso private/ come TODO.md
Task 14 (potatura chiusi) crea TODO-chiusi.md come nuovo symlink verso
private/TODO-chiusi.md: va ignorato allo stesso modo del TODO.md
esistente, il contenuto vero sta nel repo private/ senza remote.
2026-09-08 20:17:40 +02:00
Simone 5dc45f7b91 docs: sposta l'uguale fuori dagli span di codice (W158)
Dataview esegue come query ogni span di codice il cui testo inizia con
l'uguale: negli span `== 0x23`, `=m`, `=n`, l'uguale esce dal backtick di
apertura e resta subito prima.
2026-09-08 15:32:58 +02:00
Simone 01685016df docs: cross-check col reverse engineering dei binari ASUS
Confronto fra la classe gs::IntelSKLXSmbus dei binari Armoury Crate e il nostro
driver: nessuna divergenza, stessa mappa registri, registro per registro,
device PCI 0x2085 compreso.

Il valore non è una correzione ma una seconda conferma indipendente su
un'interfaccia che il datasheet Intel non documenta per la PCU function 5 — la
prima era PawnIO.Modules. Più tre dati che nei nostri doc non c'erano.

Fonte dichiarata come claim di terzi ma verificabile (raw/ e disasm/ nella
cartella non-git su partizione Windows), con due spot-check su due riscontrati
sul disassemblato primario. Nessuna modifica al codice necessaria.
2026-08-31 01:31:48 +02:00
Simone 078c1b1414 Segnali: passata del vocabolario (⚠️→🚨, 🧊→⏸️, ⛔/📌 ritirati)
Sostituzione meccanica dei segnali, nessun cambio di contenuto:
- ⚠️ → 🚨 e 🧊 → ⏸️, scelte di Simone il 2026-08-24
- ⛔/🛑 → ❌ e 📌 → ℹ️, ritirati dalla spec del 2026-08-22
- ❌ seguito da U+FE0F, se no il font di testo lo rende come una × sottile

Fatta con wiki/bin/cambia-segnale.py e wiki/bin/fix-emoji-fe0f.py, che saltano
blocchi di codice, span fra backtick e symlink, e non muovono il mtime.
Vocabolario: wiki/docs/specs/2026-08-22-segnali-e-colori.md
2026-08-17 23:01:15 +02:00
Simone a4fa04a9f9 docs: tabelle rese leggibili nel render (sweep wiki W73)
Intestazioni riempite e prosa portata fuori dalle celle, testo
spostato verbatim, mai riscritto. Criterio e dettaglio in
wiki/TODO.md, item W73.
2026-08-17 23:01:15 +02:00
Simone 39783c8a76 chore: ignore private/ invece di .private/
La cartella dei documenti interni ha perso il punto iniziale: Obsidian non
indicizza ne' le dot-dir ne' i symlink, quindi i wikilink a TODO.md,
AGENTS.md e ai doc di submission erano morti in app. Il punto era solo
estetica: la riservatezza la fa questa riga, e resta identica.

Verificato: git check-ignore copre private/, git ls-files non traccia
nessun documento interno.
2026-08-17 13:10:50 +02:00
Simone d671034e8b chore: ignore .private/, the separate history for internal documents
The internal working documents were ignored here — correctly, this repo
is headed for mainline — but that left them versioned nowhere. They now
live in .private/, a nested git repo with no remote, and stay in place in
the tree as relative symlinks. Their names are already in the ignore list
above, so nothing about this repo's contents changes.
2026-08-14 19:09:44 +02:00
Simone d1b48b2b16 tools: fixed SMBus load protocol for the poweroff-hang observation window
pre-poweroff-load.sh runs the same mixed, deterministic workload before
every poweroff, so each observation carries identical bus exposure in both
the use_ecam=1 phase and the use_ecam=0 counter-proof. It records one line
per run (use_ecam, srcversion, blocks, warns, spd_ok) and keeps the full
per-run output beside it, then powers the machine off by itself.

Two runs of 2026-08-09 are included: without them the log is a claim.
2026-08-14 17:49:56 +02:00
Simone 2c7675a104 tools: measure the ENE read-back question instead of inferring it
P2 asked whether the ENE at 0x27 has readable state. The answer had been
reached twice by elimination, and the last link in that chain was a comment
in a third party's patched OpenRGB detector, not a measurement of ours.

probe-ene-receive-byte.sh runs the experiment the driver made possible on
2026-07-27, when PNTR_SEL (BIT(18)) gave us a bare Receive Byte: a read with
no command byte on the wire cannot return a transform of a register that was
never sent. It does, however, return the last command byte the chip received
— 12/12 on both channels — so the value is still ours, not the device's. A
Receive Byte taken from 0x31 after addressing 0x27 answers 0xff instead of
the echo, which places the latch in the device and not in the iMC engine;
empty addresses NACK, so the transaction is real.

Everything here is read-only: Receive Byte and Read Byte Data at 0x27, SPD
reads through ee1004 sysfs, and the four SPD serials compared before and
after every run.

Two engine behaviours the script has to work around, both measured:
the first transaction on a channel answers with a stale value (hence the
discarded warm-up read), and a read following a successful one usually
NACKs, which one retry always clears — the P1 policy, confirmed.

lore-thread-check.py is unrelated to the probe but shares the same motive:
lore and patchwork now sit behind an Anubis bot check, so a shell cannot
tell "not published" from "not allowed to look". The NNTP mirror can.
2026-08-10 18:49:56 +02:00
Simone d7bd8068b4 Merge branch 'ecam-accessors' into guard (ECAM page identity check) 2026-08-09 09:49:08 +02:00
Simone 1a897d7d24 i2c-imc-skylake: guard branch - verify the ECAM page before trusting it
A miscomputed page would send every engine write to some other
function's config space - the exact failure mode the ECAM path exists
to remove.  Require word 0 to read back as 8086:2085 before using the
mapping; otherwise fall back to config cycles.
2026-08-09 09:49:08 +02:00
Simone 8a85d19e50 Merge branch 'ecam-accessors' into guard
ECAM MMIO accessors (use_ecam, default on): remove the CF8/CFC SMI
window, suspected poison channel of the ws1 poweroff hang.
2026-08-09 09:48:04 +02:00
Simone 637717dad0 i2c-imc-skylake: guard branch - ECAM accessors close the CF8/CFC SMI window
A conf1 access is two I/O instructions (index on 0xCF8, data on 0xCFC)
serialised against the kernel but not against SMM: an SMI landing between
them, whose handler runs its own CF8/CFC dialogue without restoring the
index, redirects our data phase to whatever register the firmware
addressed last.  The CMD write is covered by the landing check, but the
DATA-register write has no readback, so its payload would land unnoticed
in a foreign config register.  Suspected (not proven) trigger of the ws1
poweroff hang: the wedge sits in the firmware's S5 path and the poison
channel must be invisible to the driver, which this one is.

Map the function's ECAM page from the ACPI MCFG table at probe and route
every register touch through a single MMIO access, which no SMI can
split.  use_ecam=0 (load-time) restores the config-cycle baseline for
A/B; MCFG lookup failure falls back automatically.
2026-08-09 09:48:04 +02:00
Simone 308d99c5db docs: add MESSAGGI Q&A log 2026-08-08 20:38:51 +02:00
Simone 8da98fe1c7 i2c-imc-skylake: guard branch - QUICK emulation and protect_spd
Downstream-only branch, not for submission.  Two additions on top of the
v4 driver:

Approximate SMBus Quick with a byte-data read of register 0 so that
i2cdetect and OpenRGB's DDR4 probe see the devices.  The engine cannot
do a zero-length transaction, so this is a real read against every
probed address - acceptable on a known bus, not as a generic facility,
which is why mainline does not get it.

Add protect_spd (bool, 0644, default on): reject SMBus writes to the
SPD EEPROMs (0x50-0x57) and to the DDR4 protection command addresses
SWPn/CWP (0x30-0x35) with -EPERM, and log the attempt ratelimited.
RSWP cannot be set in-system on this platform (SWPn/CWP require VHV on
SA0), so a driver-level refusal is the strongest available protection
against stray SPD writes from any i2c-dev client.  The page-select
addresses 0x36/0x37 stay writable because ee1004 needs them (Send Byte
page select); ee1004 itself never writes the EEPROM contents.  Toggle
at runtime via /sys/module/i2c_imc_skylake/parameters/protect_spd for
deliberate SPD maintenance, e.g. the documented repair recipe.

The old compat branch predates 1.0.1 (no native Receive Byte, no ee1004
instantiation, no I2C_FUNC_SMBUS_BYTE, so ee1004 cannot even bind) and
is renamed to attic/compat-stale-pre-1.0.1 so nobody builds on it.
2026-07-31 11:26:38 +02:00
Simone aa2ef6bd7b i2c-imc-skylake: downstream A/B lever for the shutdown quiescing
quiesce_on_shutdown (bool, 0644, default on) turns imc_pci_shutdown()
into a no-op when clear, byte for byte the pre-1.0.4 behaviour.  It
exists for the ws1-mint S5-hang investigation: together with furyd's
FURYD_SKIP_CLEAR it lets the serial-console session vary one factor at
a time without rebuilding or reloading.

Downstream only, like the guard branch: a parameter whose only purpose
is A/B-testing one machine is not upstream material.  Keep this commit
out of the mainline v5 respin.
2026-07-31 11:25:44 +02:00
16 changed files with 2316 additions and 6 deletions
+8
View File
@@ -42,7 +42,15 @@ Thumbs.db
patches/
# Internal working documents (not for public repo)
# Dal 2026-08-14 i file qui sotto sono symlink verso private/, repo git a parte
# e senza remote: erano ignorati e quindi non versionati da nessuna parte.
# Vedi private/README.md. I nomi restano ignorati identici a prima.
# 2026-08-17: la cartella era `.private/`, rinominata senza punto perche' Obsidian
# non indicizza ne' i symlink ne' le dot-dir, quindi i wikilink ai doc interni
# erano morti (wiki/TODO.md W60). Il punto era estetica, l'ignore fa il lavoro.
private/
TODO.md
STORICO.md
docs/archive-submission-analysis.md
README.local.md
AGENTS.md
+1060
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1 +1 @@
1.0.4
1.0.5-guard1
@@ -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)
@@ -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)
@@ -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)
@@ -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)
+163
View File
@@ -0,0 +1,163 @@
# Cross-check col reverse engineering vendor ASUS (`aura-re`) — 2026-08-26
> **Esito: nessuna divergenza. Il binario ASUS usa la stessa mappa registri del driver,
> registro per registro.** Il valore di questo confronto non è una correzione: è la
> **seconda conferma indipendente** su un'interfaccia che il datasheet Intel non documenta
> per la PCU function 5 — la prima era PawnIO.Modules (`SmbusIntelSkylakeIMC.p`,
> `TODO.md` §Notes) — più tre dati che nei nostri doc non c'erano.
| | |
|---|---|
| **Fonte** | `/media/simone/OS/Users/Simone/Desktop/aura-re` — analisi statica dei binari Armoury Crate, prodotta il **2026-08-26** (cartella non-git, sulla partizione Windows) |
| **Grado di fiducia** | claim di terzi, ma **verificabile**: `raw/` ha i binari, `disasm/` il disassemblato completo |
| **Spot-check fatto** | 2 su 2 riscontrati sul disassemblato primario, non sul solo testo — vedi §4 |
| **Ricaduta sul codice** | **nessuna modifica necessaria** |
---
## 1. Confermato — mappa registri identica
Confronto fra `aura-re/03-smbus-backends.md` (classe `gs::IntelSKLXSmbus`) e
`i2c-imc-skylake.c`.
| Elemento | ASUS `gs::IntelSKLXSmbus` | Nostro driver | Esito |
|---|---|---|---|
| Device PCI | DID `0x2085` | `PCU_DEVICE 0x2085` (`:95`) | identico |
| Canale 0 — CMD / STAT / DATA | `0x9C` / `0xA8` / `0xB4` | `:110-112` | identico |
| Canale 1 — CMD / STAT / DATA | `0xA0` / `0xAC` / `0xB8` | `:113-115` | identico |
| CMD `[7:0]` | BA, offset nel device | `GENMASK(15,0)` low byte (`:132`) | identico |
| CMD `[14:8]` | SA, indirizzo 7 bit | `:130-131` | identico |
| CMD `[15]` | WRT, 1 = scrittura | `WRITE_OPERATION BIT(15)` (`:154`) | identico |
| CMD `[17]` | WORD, accesso 16 bit | `WORD_BIT BIT(17)` (`:138`) | identico |
| CMD `[19]` | TRIGGER | `GO_BIT BIT(19)` (`:136`) | identico |
| CMD `[20]` | TSOD poll *(loro: `[inferenza]`)* | `TSOD_ACTIVE_BIT BIT(20)` (`:137`) | concorde |
| STATUS, letto come **byte** | `[0]` BUSY `[1]` ERROR `[2]` read done `[3]` write done | `:164-167` | identico |
| DATA — scrittura byte | `[23:16]` | `raw << 16` con `raw` a 8 bit (`:571-572`) | identico |
| DATA — scrittura word | `[31:16]` | `swab16(*value) << 16` (`:571-572`) | identico |
| DATA — lettura | `[15:0]` | `raw & 0xffff` / `& 0xff` (`:599-600`) | identico |
> **Perché conta.** La cover letter dice che il datasheet Intel (rif. 614073 §3.1.8)
> descrive un'altra interfaccia, sulla channel function, e **non** copre la function 5.
> Il HAL ASUS pilota la function 5 esattamente con la nostra codifica: la mappa non è più
> solo "misurata sul mio hardware".
## 2. Differenze di condotta — nessuna è un difetto nostro
- **Bit 29** — ASUS **non lo scrive mai**: legge il CMD e lo maschera con
`and dword ptr [r8], 0xfffe0000`, cioè conserva tutti i bit ≥ 17 e ricostruisce solo
`[16:0]`. Il bit 29 sopravvive perché **lo ha già messo il firmware**. Noi lo scriviamo
esplicitamente (`COMMAND_PREFIX`, `:155`) e abbiamo misurato che azzerandolo la
transazione fallisce con ERROR. Le due osservazioni sono coerenti; è un secondo dato per
la domanda "cos'è il bit 29" se un reviewer la fa.
- **Gate TSOD** — ASUS si limita a **rifiutare** la transazione se `CMD bit20 == 1`
(funzione `0x14001D720`, chiamata una sola volta via `std::call_once`). Noi facciamo di
più: check di `SMBCNTL.TSOD_POLL_EN` a probe (`:783`) **e** azzeramento + ripristino del
bit 20 su tutti i canali per ogni transfer (`imc_prepare_xfer`, `:517-548`). Il nostro
è strettamente più cauto — niente da importare.
- **`PNTR_SEL` (bit 18)** — ASUS non lo usa affatto. La scoperta del 27/07 resta solo
nostra: nessuna conferma, ma nemmeno contraddizione.
- **Timeout** — ASUS: 1000 iterazioni con attesa di ~237 µs (`0xED`) ≈ 237 ms.
Noi: poll 10 µs, timeout 50 ms idle / 200 ms GO / 50 ms done. Nessuna azione.
## 3. Dati nuovi, che nei nostri doc non c'erano
- 🚨 **Tabella CPUID → Device ID del vendor** (`0x140001510`, tab. in
`03-smbus-backends.md`):
→ tabella «cpuid e i device id cercati» qui sotto, fuori dall'elenco (W161)
| CPUID `>>4` | Famiglia | DID cercato |
|---|---|---|
| `0x306F` | Haswell-E / EP | `0x2FA8` |
| `0x406F` | Broadwell-E / EP | `0x6FA8` |
| `0x5065` | Skylake-X / Cascade Lake-X | `0x2085` |
**Tabella — cpuid e i device id cercati**
- **Il vendor conferma che i due layout non sono intercambiabili.** Il ramo
`gs::IntelImcSmbus` (Haswell/Broadwell) usa `SMBSTAT 0x180` / `SMBCMD 0x184` /
`SMBCNTL 0x188` con TRIGGER a `[31]`, PNTR_SEL a `[30]`, WORD a `[29]`, WRITE a `[27]` —
**identico a `i2c-imc` di Lutomirski**; il ramo SKL-X è la codifica compatta che usiamo
noi. È l'argomento tecnico del perché serve un driver nuovo e non un'estensione di
`i2c-imc` (fonte: `aura-re/08-domande-aperte.md`, §"`IntelImcSmbus` contro `i2c-imc`").
- **ENE a `0x27`: il percorso vendor è write-only**, e ora lo si legge nel codice ASUS,
non in un commento di OpenRGB. Sessione `0xE1=0x01` + `0xE4=0x09`, colori
`0xEC`/`0xED`/`0xEE`, commit `0xDD=0x64`, **50 µs di attesa attiva fra una scrittura e
l'altra**, `Sleep(1 ms)` attorno all'apertura/chiusura, chiusura `0xE1=0x02` poi `0x03`.
Corrobora **P2** (chiusa il 10/08) da una fonte indipendente.
- **Gli indirizzi RGB dei moduli vengono *assegnati*, non sono di fabbrica**
(`EneRGBSDK::Dram_AssignAddress`, base `0x48`; `GskillLogic::ChangeAddress`). Quindi
`0x27` è "l'indirizzo dopo l'inizializzazione", non un dato di targa — da tenere
presente in ogni futura misura sull'ENE.
## 4. Spot-check sul disassemblato primario
Due claim presi a campione e riscontrati sul `.asm`, non sul testo del doc:
```
disasm/AacKingstonDramHal_x64.asm:337
0x000140001550 b885200000 mov eax, 0x2085 # tabella CPUID->DID, voce SKL-X
disasm/AacKingstonDramHal_x64.asm:33438
0x00014001d29f 4181200000feff and dword ptr [r8], 0xfffe0000 # ReadByte: conserva
# i bit >=17 (29 e 20)
```
## 5. 🚨 Vincolo: questo materiale non va su LKML
Deriva dal disassemblato di binari ASUS proprietari. Le affermazioni della patch e della
cover letter restano fondate **sulle nostre misure su hardware**, che ci sono già e bastano.
Il cross-check serve internamente (e in `docs/submission/review-answers.md`, gitignored),
non come citazione nel thread upstream.
## 6. Ricadute fuori da questo progetto
- **furyrs** — Kingston FURY: RGB a `0x58` (DDR4, base `0x57`+1) e `0x60` (DDR5, base
`0x5F`+1); mode register `0x08`: `0x53` `'S'` = controllo software, `0x44` `'D'` =
default; detect = ReadWord `0x05`/`0x06`/`0x88`/`0x26` con 20 ms fra i passi. Detect
del modulo RGB via SPD: manufacturer `0x0198` in `SPD[0x140/0x141]`, flag RGB =
**`SPD[0x168] bit 4`** (pagina 1, SPA1 `0x37`). Fonte: `aura-re/04-dram-kingston.md`.
- **tuf-x299-aura** — i LED della mobo **non stanno sull'i801**: stanno sul controller
SMBus del Super I/O **Nuvoton** (`0x2E`/`0x2F`, LDN `0x0B`, base da `0x62`/`0x63`), con
controller **AURA82** a `0x9C` 8 bit = **`0x4E` 7 bit** e pacchetti da 65 byte con marker
`0xEC`. Spiegherebbe sia lo scan i801 muto sia OpenRGB a zero controller.
Fonte: `aura-re/05-mobo-aura.md`, `06-porting-linux.md`.
## 7. Cosa ho già letto nel disassemblato (per non rifarlo)
Analisi mia del 26/08 sulla vtable di `gs::IntelSKLXSmbus`
(`disasm/AacKingstonDramHal_x64.rtti.txt:17-30`, 13 slot, base `gs::Smbus` ne ha 11 con
7 `_purecall`):
| Slot | VA | Cosa fa davvero | Ricaduta sul driver |
|---|---|---|---|
| 1-4 | `0x14001D310` / `D240` / `D3E0` / `D4A0` | read/write byte e word | già nostri |
| 6 | `0x14001CD60` | **block read emulata**: ciclo che chiama lo slot 4 (`call [rax+0x20]` = ReadWord) e impila 2 byte per giro in uno `std::vector` | **niente block hardware** |
| 7 | `0x14001CEB0` | **block write emulata**: ciclo che chiama lo slot 3 (`call [rax+0x18]` = WriteWord), impacchettando 2 byte per giro | idem |
| 8 | `0x14001D560` | write "nuda": `CMD &= 0xfffc00ff` (azzera `[17:8]`, **conserva BA `[7:0]`, bit 18, 20, 29**), poi `SA` in `[14:8]`, `WRT` in `[15]`, `GO` in `[19]`; nessun dato messo in DATA; indirizzo = `arg1 + arg2` a 8 bit; chiude con `WaitDone(isWrite=1)`. È il **page-select SPD** (SPA0/SPA1) che serve a leggere gli offset > `0xFF` | equivalente al nostro Send Byte, ma **senza** usare `PNTR_SEL` |
| 12 | `0x14001D5D0` | `slot % 6` → indice in tabella a `0x14007F938`, passo 4 | mappa slot logico → indirizzo |
Costruttore (`0x14001A3C5`-`0x14001A496`): crea **esattamente due** istanze, con
`(status, cmd, data)` = `(0xA8, 0x9C, 0xB4)` e `(0xAC, 0xA0, 0xB8)`. 🚨 **Nessun altro
offset del config space della function 5 viene toccato** in questo percorso: niente
registro nascosto da aggiungere al driver.
**Conclusione: dal materiale letto finora non esce nessuna feature hardware che ci
manchi.** Le domande che potrebbero ancora cambiare il driver sono in `TODO.md` §V,
item `V3`.
## 8. 🚨 Un claim di `aura-re` è sbagliato — verificato sul primario
`aura-re/06-porting-linux.md` §"Percorso 1" dice che *«esiste già un'implementazione Linux
funzionante: OpenRGB ha un driver SMBus per il Super I/O Nuvoton»* e cita
`i2c_smbus/i2c_smbus_nuvoton_nct6775.cpp`. **Quel file non esiste.** Su
`hw/OpenRGB-src` a `master` `8c19428b` (2026-08-08) l'unico backend Nuvoton è
`i2c_smbus/MacOS/i2c_smbus_nct6775.cpp` — **solo macOS**, esattamente come dice già
`hw/tuf-x299-aura/TODO.md` **B2**. Su Linux la strada resta il modulo out-of-tree
`i2c-nct6775` (`openrgb-dkms`) che espone un adapter i2c, non un backend interno di
OpenRGB: meccanismo diverso, e in `tuf-x299-aura` è bloccato per decisione di Simone
finché `W27` è aperta.
Morale operativa: `aura-re` è forte dove disassembla (i claim con indirizzo VA reggono lo
spot-check), debole dove riassume l'ecosistema Linux. Prendere i primi, verificare i
secondi.
+188 -5
View File
@@ -25,6 +25,41 @@
* The two channels are independent SMBus buses (DIMMs 1,2 on ch0; DIMMs 3,4 on
* ch1), hence two i2c_adapter instances exposed as separate /dev/i2c-* nodes.
*
* DOWNSTREAM BRANCH (guard). Not what is submitted upstream. Two local
* additions:
*
* - SMBus Quick is approximated with a byte-data read of register 0 so
* that i2cdetect can scan these buses. The engine always puts a
* register byte on the bus, so Quick cannot be implemented faithfully;
* a read against a known bus is acceptable here, not as a generic
* kernel facility.
*
* - protect_spd (default on) rejects SMBus writes to the SPD EEPROMs
* (0x50-0x57) and to the DDR4 protection command addresses SWPn/CWP
* (0x30-0x35) with -EPERM. RSWP cannot be set in-system on this
* platform (it needs VHV on SA0), so this is the strongest available
* protection against stray SPD writes. The page-select addresses
* 0x36/0x37 stay writable: ee1004 needs them for its Send Byte page
* select. Toggle at runtime via
* /sys/module/i2c_imc_skylake/parameters/protect_spd for deliberate
* SPD maintenance.
*
* - use_ecam (default on): reach the engine registers through a private
* ECAM (MMCONFIG) mapping instead of the kernel's legacy config
* cycles. For standard config space the kernel uses conf1: two I/O
* instructions, index on 0xCF8 and data on 0xCFC. pci_config_lock
* serialises that pair against the rest of the kernel but not against
* SMM: an SMI landing between the two, whose handler runs its own
* CF8/CFC dialogue without restoring the index, silently redirects
* the data phase to whatever register the firmware addressed last.
* A misdirected read only perturbs one poll iteration and the CMD
* write is covered by the landing check below, but the DATA-register
* write has no readback, so its payload would land unnoticed in a
* foreign config register. A single MMIO access cannot be split by
* an SMI, so the window does not exist on the ECAM path. Suspected
* (not proven) trigger of the ws1 poweroff hang. Falls back to
* config cycles when MCFG has no entry for the device.
*
* Bus arbitration / concurrency with firmware:
* The driver cannot coordinate with SMM, a BMC, or the iMC's own closed-loop
* thermal throttling (CLTT), and the PCI ID alone does not prove that none of
@@ -44,6 +79,7 @@
* to be arbitrated against.
*/
#include <linux/acpi.h>
#include <linux/bits.h>
#include <linux/delay.h>
#include <linux/i2c.h>
@@ -148,6 +184,32 @@ module_param(allow_unsafe_access, bool, 0444);
MODULE_PARM_DESC(allow_unsafe_access,
"allow access without firmware/SMM arbitration (unsafe)");
/*
* Runtime-writable so the handback can be A/B tested against the previous
* do-nothing shutdown without rebuilding or reloading: with the parameter
* clear, imc_pci_shutdown() is a no-op, byte for byte the pre-1.0.4
* behaviour.
*/
static bool quiesce_on_shutdown = true;
module_param(quiesce_on_shutdown, bool, 0644);
MODULE_PARM_DESC(quiesce_on_shutdown,
"hand the engine back to the firmware quiesced at shutdown");
static bool protect_spd = true;
module_param(protect_spd, bool, 0644);
MODULE_PARM_DESC(protect_spd,
"reject SMBus writes to SPD addresses 0x30-0x35 and 0x50-0x57 (default on)");
/*
* Load-time only: the accessors pick their path from s->ecam, captured at
* probe. Flipping this at runtime would race in-flight transfers for no
* benefit; the A/B lever is a reboot away, like the pre-ECAM baseline.
*/
static bool use_ecam = true;
module_param(use_ecam, bool, 0444);
MODULE_PARM_DESC(use_ecam,
"reach the engine via an ECAM mapping instead of CF8/CFC config cycles (default on)");
struct imc_chan {
unsigned int cmd, data, stat;
int idx; /* channel index 0 or 1 */
@@ -161,6 +223,8 @@ static const struct imc_chan imc_chans[2] = {
/* one driver state object, shared by both per-channel adapters */
struct imc_smbus {
struct pci_dev *pdev; /* config space carrying the engine */
void __iomem *ecam; /* this function's config page, or NULL
to fall back to config cycles */
struct device *dev; /* &pdev->dev, for dev_*() logging */
struct mutex lock; /* serialises all SMBus transactions */
/* needed: both channels share one engine */
@@ -176,9 +240,69 @@ struct imc_xfer_state {
};
/*
* Register accessors. A config-space failure is recorded in s->io_err and
* reported once, at the end of the transfer, so the transaction logic does not
* have to check a return value on every register touch. A failed read yields
* Locate this function's configuration page in the ECAM window described by
* the ACPI MCFG table and map it. The arch code keeps its own MCFG mapping
* for extended config space but exposes no way to route standard config
* space through it, hence the private lookup. Mapping the page a second
* time is harmless: it is device MMIO, not RAM.
*/
static void __iomem *imc_map_ecam(struct pci_dev *pdev)
{
struct acpi_table_mcfg *mcfg;
struct acpi_mcfg_allocation *alloc;
void __iomem *map = NULL;
unsigned int bus = pdev->bus->number;
int i, n;
if (ACPI_FAILURE(acpi_get_table(ACPI_SIG_MCFG, 0,
(struct acpi_table_header **)&mcfg)))
return NULL;
n = (mcfg->header.length - sizeof(*mcfg)) / sizeof(*alloc);
alloc = (struct acpi_mcfg_allocation *)(mcfg + 1);
for (i = 0; i < n; i++, alloc++) {
resource_size_t addr;
if (alloc->pci_segment != pci_domain_nr(pdev->bus) ||
bus < alloc->start_bus_number ||
bus > alloc->end_bus_number)
continue;
addr = alloc->address +
((resource_size_t)(bus - alloc->start_bus_number) << 20) +
((resource_size_t)pdev->devfn << 12);
map = devm_ioremap(&pdev->dev, addr, SZ_4K);
break;
}
acpi_put_table(&mcfg->header);
if (!map)
return NULL;
/*
* A wrong page here would make every engine write land on some other
* function - the exact failure mode this path exists to remove.
* Word 0 is the one register whose value is known in advance; require
* it before trusting the mapping with writes.
*/
if (readl(map) != (PCU_DEVICE << 16 | PCI_VENDOR_ID_INTEL)) {
dev_warn(&pdev->dev,
"ECAM page does not identify as %04x:%04x; not using it\n",
PCI_VENDOR_ID_INTEL, PCU_DEVICE);
devm_iounmap(&pdev->dev, map);
return NULL;
}
return map;
}
/*
* Register accessors. On the ECAM path a register touch is a single MMIO
* access; it has no failure channel, and the function sits on the CPU die,
* so it cannot vanish and produce the all-ones of a master abort. On the
* config-cycle fallback a failure is recorded in s->io_err and reported
* once, at the end of the transfer, so the transaction logic does not have
* to check a return value on every register touch. A failed read yields
* all-ones, which no polling loop mistakes for a completed transaction.
*/
static u32 imc_reg(struct imc_smbus *s, unsigned int off)
@@ -186,6 +310,9 @@ static u32 imc_reg(struct imc_smbus *s, unsigned int off)
u32 val;
int ret;
if (s->ecam)
return readl(s->ecam + off);
ret = pci_read_config_dword(s->pdev, off, &val);
if (ret) {
s->io_err = pcibios_err_to_errno(ret);
@@ -199,6 +326,11 @@ static void imc_set(struct imc_smbus *s, unsigned int off, u32 val)
{
int ret;
if (s->ecam) {
writel(val, s->ecam + off);
return;
}
ret = pci_write_config_dword(s->pdev, off, val);
if (ret)
s->io_err = pcibios_err_to_errno(ret);
@@ -503,6 +635,25 @@ static s32 imc_smbus_xfer(struct i2c_adapter *adap, u16 addr,
if (addr > 0x7f)
return -EINVAL;
/*
* Downstream-only, see the note at the top of the file: approximate
* Quick with a byte-data read of register 0 so that i2cdetect can
* scan. Read rather than write, so a probe of an unknown address
* cannot store anything.
*/
if (size == I2C_SMBUS_QUICK) {
mutex_lock(&s->lock);
s->io_err = 0;
ret = imc_access(s, c, addr, 0, I2C_SMBUS_READ,
I2C_SMBUS_BYTE_DATA, &val);
if (s->io_err)
ret = s->io_err;
mutex_unlock(&s->lock);
dev_dbg(s->dev, "ch%d QUICK addr=%02x (ret %d)\n",
c->idx, addr, ret);
return ret;
}
if (size != I2C_SMBUS_BYTE && size != I2C_SMBUS_BYTE_DATA &&
size != I2C_SMBUS_WORD_DATA)
return -EOPNOTSUPP;
@@ -515,6 +666,22 @@ static s32 imc_smbus_xfer(struct i2c_adapter *adap, u16 addr,
*/
reg = size == I2C_SMBUS_BYTE ? 0 : command;
/*
* Downstream-only: refuse writes to the SPD EEPROMs and to the DDR4
* protection command addresses. Send Byte writes to 0x50-0x57 are
* caught too; on an EE1004 they would only move the address pointer,
* but there is no caller that needs them. 0x36/0x37 (page select)
* stay writable.
*/
if (protect_spd && read_write == I2C_SMBUS_WRITE &&
((addr >= 0x50 && addr <= 0x57) ||
(addr >= 0x30 && addr <= 0x35))) {
dev_warn_ratelimited(s->dev,
"ch%d write to addr 0x%02x reg 0x%02x blocked by protect_spd\n",
c->idx, addr, reg);
return -EPERM;
}
mutex_lock(&s->lock);
s->io_err = 0;
@@ -577,8 +744,8 @@ static s32 imc_smbus_xfer(struct i2c_adapter *adap, u16 addr,
static u32 imc_func(struct i2c_adapter *adap)
{
return I2C_FUNC_SMBUS_BYTE | I2C_FUNC_SMBUS_BYTE_DATA |
I2C_FUNC_SMBUS_WORD_DATA;
return I2C_FUNC_SMBUS_QUICK | I2C_FUNC_SMBUS_BYTE |
I2C_FUNC_SMBUS_BYTE_DATA | I2C_FUNC_SMBUS_WORD_DATA;
}
static const struct i2c_algorithm imc_algo = {
@@ -713,6 +880,9 @@ static void imc_pci_shutdown(struct pci_dev *pdev)
if (!s)
return;
if (!quiesce_on_shutdown)
return;
/* refuse new transfers, then drain the one possibly in flight */
for (i = 0; i < ARRAY_SIZE(s->adap); i++)
i2c_mark_adapter_suspended(&s->adap[i]);
@@ -764,6 +934,19 @@ static int imc_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
if (ret)
return ret;
/*
* Before the first register touch, so that even the pristine-word
* snapshot below goes through the SMI-safe path.
*/
if (use_ecam) {
s->ecam = imc_map_ecam(pdev);
if (!s->ecam)
dev_warn(&pdev->dev,
"no usable MCFG entry; falling back to CF8/CFC config cycles\n");
}
dev_dbg(&pdev->dev, "engine access via %s\n",
s->ecam ? "ECAM MMIO" : "CF8/CFC config cycles");
/*
* Pristine command words: the state the firmware left in the engine at
* boot, captured before this driver's first transfer. They are handed
+60
View File
@@ -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 [email protected]
./tools/lore-thread-check.py <msgid> --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.
+135
View File
@@ -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 <message-id> [--group org.kernel.vger.linux-i2c]
The message-id is given without angle brackets, e.g.
./tools/lore-thread-check.py [email protected]
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())
+2
View File
@@ -0,0 +1,2 @@
2026-08-09 11:01 use_ecam=Y srcversion=C1EC295E03F1D2060EBBB3B blocks=10 warns=0 spd_ok=1
2026-08-09 12:49 use_ecam=Y srcversion=C1EC295E03F1D2060EBBB3B blocks=10 warns=0 spd_ok=1
+154
View File
@@ -0,0 +1,154 @@
#!/bin/bash
# pre-poweroff-load.sh - fixed SMBus load protocol for the A5 hang observation
# window (TODO.md E1), fully automatic: runs the load, saves observations,
# powers the machine off.
#
# Mixed, deterministic workload so that every poweroff in the window carries
# the same, high and *diverse* bus exposure - both in the use_ecam=1 phase and
# in the use_ecam=0 counter-proof phase. Each 3-minute block:
#
# 1. software effect (~2760 SMBus writes/s, steady stream) ~100 s
# 2. rapid hw/sw switch burst (6 alternations back-to-back:
# one-shot config-write bursts + apply latches) ~20 s
# 3. hardware effect (engine-side, bus quiet) ~30 s
# 4. full SPD dump of all 4 DIMMs via ee1004 (reads +
# page-selects) + serial integrity check ~30 s
#
# Effects rotate through fixed lists, so run N of phase A and run N of phase B
# execute the identical sequence.
#
# End of run: kernel-journal warnings and run context are saved to a per-run
# file in tools/pre-poweroff-runs/, one summary line is appended to
# pre-poweroff-load.log, and - only with --poweroff - the machine POWERS OFF
# (15 s grace, Ctrl-C to abort). The last software effect is left running into
# the poweroff on purpose: exposure should cover the shutdown path too; the
# systemd boot scene restores the normal LED state at next boot.
#
# The one exception: an SPD serial mismatch saves everything and does NOT
# power off - investigate before shutting down.
#
# Usage: sudo ./pre-poweroff-load.sh [minutes] [--poweroff]
# (default 30 minutes, in 3-min blocks; re-execs itself under sudo if
# started as a plain user; powers off at the end ONLY with --poweroff)
set -eu
[ "$(id -u)" -eq 0 ] || exec sudo "$0" "$@"
MINUTES=30
POWEROFF=0
for arg in "$@"; do
case "$arg" in
--poweroff) POWEROFF=1 ;;
*) MINUTES="$arg" ;;
esac
done
BLOCKS=$(((MINUTES + 2) / 3))
DIR="$(cd "$(dirname "$0")" && pwd)"
LOG="$DIR/pre-poweroff-load.log"
RUNDIR="$DIR/pre-poweroff-runs"
START="$(date '+%Y-%m-%d %H:%M:%S')"
RUNFILE="$RUNDIR/$(date '+%Y%m%d-%H%M').log"
ANIMS=(aurora wave_rainbow plasma ripple gradient_pulse fire)
FX=(respiro arcobaleno cometa battito ciclico lampeggio)
# SPD EEPROMs at 0x50/0x52 (ee1004); i2c bus numbers depend on module load
# order, so discover them instead of hardcoding bus IDs.
mapfile -t EEPROMS < <(ls /sys/bus/i2c/devices/*-005[02]/eeprom 2>/dev/null | sort)
[ "${#EEPROMS[@]}" -eq 4 ] || { echo "ERROR: expected 4 SPD EEPROMs, found ${#EEPROMS[@]}"; exit 1; }
MODE="$(cat /sys/module/i2c_imc_skylake/parameters/use_ecam 2>/dev/null || echo '?')"
SRCV="$(cat /sys/module/i2c_imc_skylake/srcversion 2>/dev/null || echo '?')"
mkdir -p "$RUNDIR"
spd_serials() {
local e
for e in "${EEPROMS[@]}"; do
dd if="$e" bs=1 skip=325 count=4 2>/dev/null | xxd -p
done
}
echo "== pre-poweroff load: ${BLOCKS} blocks (~$((BLOCKS * 3)) min), use_ecam=${MODE}, srcversion=${SRCV}"
if [ "$POWEROFF" -eq 1 ]; then
echo "== POWERS OFF at the end. Run file: ${RUNFILE}"
else
echo "== no poweroff at the end (use --poweroff). Run file: ${RUNFILE}"
fi
REF_SERIALS="$(spd_serials)"
echo "== SPD serial reference: $(echo "$REF_SERIALS" | tr '\n' ' ')"
SPD_BAD=0
for ((b = 0; b < BLOCKS; b++)); do
anim="${ANIMS[b % ${#ANIMS[@]}]}"
fx="${FX[b % ${#FX[@]}]}"
echo "-- block $((b + 1))/${BLOCKS}: anim=${anim} fx=${fx}"
furyrs anim "$anim"
sleep 100
# rapid hw/sw alternation burst
for ((i = 0; i < 3; i++)); do
furyrs effect "${FX[(b + i) % ${#FX[@]}]}"
sleep 2
furyrs anim "${ANIMS[(b + i) % ${#ANIMS[@]}]}"
sleep 2
done
furyrs effect "$fx"
sleep 30
# full SPD dump (reads + ee1004 page-selects) + integrity check
for e in "${EEPROMS[@]}"; do
dd if="$e" bs=64 2>/dev/null >/dev/null
done
if [ "$(spd_serials)" != "$REF_SERIALS" ]; then
SPD_BAD=1
echo "!! SPD SERIAL MISMATCH in block $((b + 1)) - stopping, will NOT power off"
break
fi
done
# leave a steady software effect running into the poweroff
furyrs anim "${ANIMS[0]}"
# Only warning-and-up: the dev_dbg per-transfer traffic is not an anomaly.
WARNS="$(journalctl -k -p warning --since "$START" --no-pager 2>/dev/null | grep -i 'i2c-imc' || true)"
N="$(printf '%s' "$WARNS" | grep -c . || true)"
{
echo "start: ${START}"
echo "end: $(date '+%Y-%m-%d %H:%M:%S')"
echo "use_ecam: ${MODE}"
echo "srcversion: ${SRCV}"
echo "blocks: ${BLOCKS}"
echo "spd_ok: $((1 - SPD_BAD))"
echo "spd_serials_ref:"
echo "$REF_SERIALS"
echo "driver_warnings: ${N}"
[ "$N" -gt 0 ] && echo "$WARNS"
} >"$RUNFILE"
echo "$(date '+%Y-%m-%d %H:%M') use_ecam=${MODE} srcversion=${SRCV} blocks=${BLOCKS} warns=${N} spd_ok=$((1 - SPD_BAD))" >>"$LOG"
[ -n "${SUDO_UID:-}" ] && chown "$SUDO_UID:${SUDO_GID:-$SUDO_UID}" "$RUNFILE" "$LOG" "$RUNDIR"
sync
if [ "$SPD_BAD" -ne 0 ]; then
echo "== SPD integrity FAILED - observations in ${RUNFILE} - NOT powering off."
exit 1
fi
if [ "$N" -gt 0 ]; then
echo "== ${N} driver warning(s) - SIGNAL, saved in ${RUNFILE}:"
echo "$WARNS"
else
echo "== clean run, no driver warnings."
fi
if [ "$POWEROFF" -eq 1 ]; then
echo "== powering off in 15 s (Ctrl-C to abort) ..."
sleep 15
systemctl poweroff
else
echo "== done, NOT powering off (pass --poweroff to shut down at the end)."
fi
+12
View File
@@ -0,0 +1,12 @@
start: 2026-08-09 10:38:00
end: 2026-08-09 11:01:56
use_ecam: Y
srcversion: C1EC295E03F1D2060EBBB3B
blocks: 10
spd_ok: 1
spd_serials_ref:
50e4333a
1fe43236
50e434ba
1ee433b5
driver_warnings: 0
+12
View File
@@ -0,0 +1,12 @@
start: 2026-08-09 12:25:06
end: 2026-08-09 12:49:02
use_ecam: Y
srcversion: C1EC295E03F1D2060EBBB3B
blocks: 10
spd_ok: 1
spd_serials_ref:
50e4333a
1fe43236
50e434ba
1ee433b5
driver_warnings: 0
+334
View File
@@ -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 <bus> [reg] -> prints "value" or "NACK"/"ERR:<text>"; 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