#!/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