Files
i2c-imc-skylake/test-smoke.sh
T
Simone c04e0ec5a8 i2c-imc-skylake: implement Receive Byte and Send Byte
The engine encodes an address-only transaction in command word bit 18,
which is what SMBus Receive Byte and Send Byte need and what was missing
for ee1004 to bind.

The bit is not documented for the PCU function. It was located by
structure rather than by sweeping: SMBCMD on the iMC channel function
(datasheet ref. 614073 section 3.1.8) places PNTR_SEL immediately above
WORD_ACCESS, and word access here is bit 17. Bit 17 had itself been
unknown until it turned out to be word access, and an earlier sweep of
this register covered bits 31:20 only, so 18 was untried rather than
already ruled out.

Validated against the device rather than against the register. An EEPROM
auto-increments its pointer after a read, so priming the pointer and
repeating the read with the bit set has a predicted answer: the next
byte, not the one named. Eight consecutive reads returned SPD bytes 0x01
through 0x08 in order, matching an ordinary dump. Send Byte was confirmed
on the DDR4 page-select addresses, which hold a latch and store no data,
so the page state is itself the readout.

Result: ee1004 binds on all four SPD addresses and decode-dimms decodes
all four modules with both CRCs valid on each. The part number comes off
page 1, so the page select runs end to end. The four module serial
numbers are unchanged from the baseline recorded before this series.

SMBus Quick stays unimplemented. Beyond having no obvious encoding, a
write into the 0x30-0x37 range is how an EE1004 SPD is write-protected,
permanently on many modules, so an adapter on the memory bus should not
advertise a probe that writes. i2cdetect scans in read mode.

Also fix test-smoke.sh resolving the module through modinfo, which
silently tested the installed DKMS build instead of the one just
compiled. It now prefers the local .ko and prints which it loaded.

Smoke suite: 19 passed, 0 failed. checkpatch --strict 0/0/0, in-tree W=1
and Sparse clean.
2026-07-27 01:26:07 +02:00

330 lines
9.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# Read-only smoke test for i2c-imc-skylake.
set -euo pipefail
PASS=0
FAIL=0
CH0=""
CH1=""
CH0_ADDR=""
CH1_ADDR=""
if ((EUID == 0)); then
SUDO=()
else
SUDO=(sudo -n)
if ! "${SUDO[@]}" true 2>/dev/null; then
echo "Run as root or configure non-interactive sudo." >&2
exit 2
fi
fi
# Prefer the module built in this directory. Falling straight through to
# modinfo would silently test whichever build is installed, which on a machine
# with a DKMS package is not the one just compiled.
_default_module()
{
local local_build
local_build=$(dirname "$(readlink -f "$0")")/i2c-imc-skylake.ko
if [[ -r "$local_build" ]]; then
printf '%s\n' "$local_build"
else
modinfo -n i2c-imc-skylake
fi
}
MODULE_PATH=${MODULE_PATH:-$(_default_module)}
echo "testing module: $MODULE_PATH"
if [[ ! -r "$MODULE_PATH" ]]; then
echo "Module not found: $MODULE_PATH" >&2
exit 2
fi
_ok()
{
echo "PASS: $*"
((PASS++)) || true
}
_fail()
{
echo "FAIL: $*"
((FAIL++)) || true
}
_module_loaded()
{
lsmod | awk '$1 == "i2c_imc_skylake" { found = 1 } END { exit !found }'
}
_bus()
{
local channel=$1
i2cdetect -l 2>/dev/null | awk -v channel="$channel" '
tolower($0) ~ /imc.*channel/ && $0 ~ "channel " channel {
sub(/^i2c-/, "", $1)
print $1
exit
}'
}
_find_pci_device()
{
local device path vendor
for path in /sys/bus/pci/devices/*; do
[[ -r "$path/vendor" && -r "$path/device" ]] || continue
read -r vendor < "$path/vendor"
read -r device < "$path/device"
if [[ "$vendor" == "0x8086" && "$device" == "0x2085" ]]; then
printf '%s\n' "$path"
return 0
fi
done
return 1
}
_refresh_buses()
{
CH0=$(_bus 0)
CH1=$(_bus 1)
}
_scan_spd()
{
local bus=$1 result_var=$2
local address address_hex device readable="" value
local found=false
echo "--- SPD scan bus $bus (read-only BYTE_DATA) ---"
for address in {80..87}; do
printf -v address_hex '0x%02x' "$address"
printf -v device '%d-%04x' "$bus" "$address"
if [[ -L "/sys/bus/i2c/devices/$device/driver" ]]; then
echo " $address_hex present (bound to $(basename "$(readlink "/sys/bus/i2c/devices/$device/driver")"))"
found=true
continue
fi
if value=$("${SUDO[@]}" i2cget -y "$bus" "$address_hex" 0x00 2>/dev/null); then
echo " $address_hex present (reg0=$value)"
found=true
readable=${readable:-$address_hex}
fi
done
printf -v "$result_var" '%s' "$readable"
$found
}
_word_test()
{
local bus=$1 address=$2
local byte0 byte1 expected word
if ! byte0=$("${SUDO[@]}" i2cget -y "$bus" "$address" 0x00 2>/dev/null) ||
! byte1=$("${SUDO[@]}" i2cget -y "$bus" "$address" 0x01 2>/dev/null) ||
! word=$("${SUDO[@]}" i2cget -y "$bus" "$address" 0x00 w 2>/dev/null); then
return 1
fi
expected=$((byte0 | (byte1 << 8)))
((word == expected))
}
# SMBus Receive Byte reads from the device's own pointer, so on an EEPROM a
# sequence of them walks forward through the array. Prime the pointer with an
# ordinary read, then check the next reads reproduce an ordinary dump of the
# following bytes. A transaction that still named a register would return the
# same byte every time.
_receive_byte_test()
{
local bus=$1 address=$2
local offset expected got
"${SUDO[@]}" i2cget -y "$bus" "$address" 0x00 >/dev/null 2>&1 || return 1
for offset in 0x01 0x02 0x03 0x04; do
got=$("${SUDO[@]}" i2cget -y "$bus" "$address" 2>/dev/null) || return 1
expected=$("${SUDO[@]}" i2cget -y "$bus" "$address" "$offset" 2>/dev/null) || return 1
((got == expected)) || return 1
# The explicit read above moved the pointer to $offset + 1, which
# is where the next Receive Byte should pick up.
done
}
# SMBus Send Byte against the DDR4 page-select addresses. Selecting page 1 puts
# the upper half of the SPD in the 256-byte window, so byte 0x00 stops being
# the JEDEC 0x23; selecting page 0 brings it back. The page state is the
# readout, so the test verifies itself, and it ends on page 0 either way.
_send_byte_test()
{
local bus=$1 address=$2
local base flipped restored
base=$("${SUDO[@]}" i2cget -y "$bus" "$address" 0x00 2>/dev/null) || return 1
"${SUDO[@]}" i2cset -y "$bus" 0x37 0x00 c 2>/dev/null
flipped=$("${SUDO[@]}" i2cget -y "$bus" "$address" 0x00 2>/dev/null)
"${SUDO[@]}" i2cset -y "$bus" 0x36 0x00 c 2>/dev/null
restored=$("${SUDO[@]}" i2cget -y "$bus" "$address" 0x00 2>/dev/null)
[[ "$flipped" != "$base" && "$restored" == "$base" ]]
}
_nack_test()
{
local bus=$1
local trace
command -v strace >/dev/null || return 1
trace=$("${SUDO[@]}" strace -qq -f -e trace=ioctl \
i2cget -y "$bus" 0x77 0x00 2>&1 || true)
grep -q '= -1 ENXIO' <<< "$trace"
}
_stress_reader()
{
local bus=$1 address=$2
local iteration
for iteration in {1..100}; do
"${SUDO[@]}" i2cget -y "$bus" "$address" 0x02 >/dev/null 2>&1 || return 1
done
}
_load_unsafe()
{
"${SUDO[@]}" insmod "$MODULE_PATH" allow_unsafe_access=1
}
DMESG_START=$("${SUDO[@]}" dmesg | wc -l)
"${SUDO[@]}" modprobe i2c-dev
_module_loaded && "${SUDO[@]}" rmmod i2c-imc-skylake
_load_unsafe
echo "=== adapter discovery ==="
_refresh_buses
[[ -n "$CH0" ]] && _ok "ch0 adapter at i2c-$CH0" || _fail "ch0 adapter not found"
[[ -n "$CH1" ]] && _ok "ch1 adapter at i2c-$CH1" || _fail "ch1 adapter not found"
if [[ -n "$CH0" ]]; then
_scan_spd "$CH0" CH0_ADDR && _ok "SPD device present on ch0" || _fail "no SPD device on ch0"
fi
if [[ -n "$CH1" ]]; then
_scan_spd "$CH1" CH1_ADDR && _ok "SPD device present on ch1" || _fail "no SPD device on ch1"
fi
echo "=== transfer semantics ==="
if [[ -n "$CH0_ADDR" ]]; then
_word_test "$CH0" "$CH0_ADDR" && _ok "WORD_DATA byte order on ch0" || _fail "WORD_DATA test on ch0"
fi
if [[ -n "$CH1_ADDR" ]]; then
_word_test "$CH1" "$CH1_ADDR" && _ok "WORD_DATA byte order on ch1" || _fail "WORD_DATA test on ch1"
fi
if [[ -n "$CH0_ADDR" ]]; then
_receive_byte_test "$CH0" "$CH0_ADDR" &&
_ok "Receive Byte follows the EEPROM pointer on ch0" ||
_fail "Receive Byte test on ch0"
fi
if [[ -n "$CH1_ADDR" ]]; then
_receive_byte_test "$CH1" "$CH1_ADDR" &&
_ok "Receive Byte follows the EEPROM pointer on ch1" ||
_fail "Receive Byte test on ch1"
fi
if [[ -n "$CH0_ADDR" ]]; then
_send_byte_test "$CH0" "$CH0_ADDR" &&
_ok "Send Byte selects the SPD page on ch0" ||
_fail "Send Byte test on ch0"
fi
if [[ -n "$CH1_ADDR" ]]; then
_send_byte_test "$CH1" "$CH1_ADDR" &&
_ok "Send Byte selects the SPD page on ch1" ||
_fail "Send Byte test on ch1"
fi
if [[ -n "$CH0" ]]; then
_nack_test "$CH0" && _ok "absent-device ENXIO on ch0" || _fail "expected ENXIO at address 0x77 on ch0"
fi
if [[ -n "$CH1" ]]; then
_nack_test "$CH1" && _ok "absent-device ENXIO on ch1" || _fail "expected ENXIO at address 0x77 on ch1"
fi
if [[ -n "$CH0_ADDR" && -n "$CH1_ADDR" ]]; then
_stress_reader "$CH0" "$CH0_ADDR" &
pid0=$!
_stress_reader "$CH1" "$CH1_ADDR" &
pid1=$!
stress_ok=true
wait "$pid0" || stress_ok=false
wait "$pid1" || stress_ok=false
$stress_ok && _ok "concurrent 100-read stress on both channels" || _fail "concurrent read stress"
fi
echo "=== safe default and targeted udev event ==="
PCI_DEVICE=$(_find_pci_device) || {
_fail "PCI device 8086:2085 not found"
PCI_DEVICE=""
}
if [[ -n "$PCI_DEVICE" ]]; then
"${SUDO[@]}" rmmod i2c-imc-skylake
"${SUDO[@]}" insmod "$MODULE_PATH"
_refresh_buses
if [[ -z "$CH0" && -z "$CH1" ]]; then
_ok "default-off gate rejected bind without opt-in"
else
_fail "driver bound without allow_unsafe_access=1"
fi
_module_loaded && "${SUDO[@]}" rmmod i2c-imc-skylake
if "${SUDO[@]}" udevadm trigger --action=add "$PCI_DEVICE" &&
"${SUDO[@]}" udevadm settle --timeout=5; then
_ok "targeted PCI udev event completed"
else
_fail "targeted PCI udev event failed"
fi
_module_loaded && "${SUDO[@]}" rmmod i2c-imc-skylake
_load_unsafe
fi
echo "=== load/unload x20 ==="
for iteration in {1..20}; do
"${SUDO[@]}" rmmod i2c-imc-skylake
_load_unsafe
echo " cycle $iteration OK"
done
_ok "20 load/unload cycles completed"
echo "=== kernel messages produced by this test ==="
NEW_DMESG=$("${SUDO[@]}" dmesg | tail -n "+$((DMESG_START + 1))")
RELEVANT_DMESG=$(grep -Ei 'i2c-imc-skylake [0-9a-f]{4}:|BUG:|WARNING:|Oops:|KASAN:|lockdep' <<< "$NEW_DMESG" || true)
printf '%s\n' "$RELEVANT_DMESG"
if grep -Eiq 'BUG:|WARNING:|Oops:|KASAN:|lockdep' <<< "$NEW_DMESG"; then
_fail "kernel warning/oops detected"
else
_ok "no kernel warning/oops detected"
fi
# The driver compares the command registers before and after every transfer.
# A hit here means firmware, a BMC or CLTT is driving the same engine, which
# invalidates every other result in this run.
if grep -q 'another master is using the engine' <<< "$NEW_DMESG"; then
_fail "foreign master detected on the engine; do not use this system"
else
_ok "no foreign master detected during the test"
fi
# Likewise for a command that never reached the register: that path exists
# because the done bits are latched and a stale DONE must not be mistaken for
# a fresh completion.
if grep -q 'did not reach the register' <<< "$NEW_DMESG"; then
_fail "a command never reached the register; config access is unreliable"
else
_ok "every command reached the register"
fi
echo "=== result: $PASS passed, $FAIL failed ==="
((FAIL == 0))