secubox-deb/scripts/probe-mpcie-gpios.sh
CyberMind ff990ba112
docs(mochabin): EP06 mPCIe GPIO investigation runbook + hardened probe script (closes #345) (#359)
* docs(mochabin): add EP06 mPCIe GPIO investigation runbook (ref #345)

scripts/probe-mpcie-gpios.sh: empirical CP0 GPIO sweep that drives
each unrequested line HIGH for a few seconds and watches dmesg/lsusb
for new USB device. Skips [used] lines, uses libgpiod gpioset
--mode=time so the line auto-restores to input on release. Modes:
--baseline (snapshot only, safe), --line gpiochipN K (single line),
no arg (full sweep gpiochip1+gpiochip2).

docs/hardware/mochabin-mpcie-ep06-runbook.md: hypothesis, procedure,
and DTS rfkill-gpio template patterned after cn9132-clearfog.dts
lines 69-122. Apply once a candidate GPIO is identified.

--baseline smoke-tested on gk2 — sweep deferred until spare EP06
arrives (no mPCIe device in J5 = no signal regardless of which line
drives W_DISABLE#).

* docs(mochabin): drop EP06 spare assumption from runbook (ref #345)

No spare EP06 on order; the existing EP06-E will be used for the
sweep when re-seated in J5. Replaces "deferred until spare" framing
with a multimeter-scoping fallback for the inconclusive case.

* fix(mpcie): v0.2.0 probe-mpcie-gpios.sh — drop blanket sweep, danger-list, dry-run default (ref #345)

The v0.1.0 "drive every unused-input CP0 GPIO HIGH for 3s one-at-a-time"
sweep took gk2 hard-down within seconds (host became unreachable; ARP
stopped responding). gpioinfo's [used] tag only reflects lines the
kernel *requested* — several unrequested CP0 GPIOs are physically
wired to critical board functions (eth switch reset, PCIe2 PERST#,
pca9554 IRQ) and driving them HIGH wedged the board.

v0.2.0:
- No blanket-sweep mode. Removed.
- Default mode = dry-run candidate listing only. No GPIO is driven.
- --commit --line CHIP N required to actually drive a single line.
- Hardcoded DANGER_LINES (gpiochip1:1, gpiochip1:9, gpiochip1:27)
  skipped unconditionally, even with --commit.
- gpiochip2 entirely held off without --allow-chip2 (no per-line
  schematic mapping for that bank).
- Hard refusals are vocal (REFUSED: ... reason) before exit.

Runbook updated to walk through the new dry-run → --commit flow.
HISTORY documents the incident + the v0.2.0 mitigations.
Lesson saved in memory feedback_gpio_blanket_sweep_crashed_board.md.

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
2026-05-22 17:50:33 +02:00

337 lines
10 KiB
Bash
Executable File

#!/usr/bin/env bash
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
# Source-Disclosed License — All rights reserved except as expressly granted.
# See LICENCE-CMSD-1.0.md for terms.
#
# SecuBox-Deb :: probe-mpcie-gpios.sh
#
# Hardware investigation runbook for issue #345 — find which CP0 GPIO drives
# W_DISABLE# / PERST# / PWR_EN on the MOCHAbin mPCIe slot J5.
#
# Background: the kernel DTS patch from #255 wired the UTMI PHY to
# cp0_usb3_1 (the mPCIe USB controller) — so the SoC USB pipe is alive
# on the slot, but plugging an EP06 modem still doesn't enumerate. The
# slot's control lines (W_DISABLE#, PWR_EN, possibly WAKE#) are NOT
# declared in the current DTS, so they come up in whatever default state
# the SoC pad config leaves them in — which may be holding the modem
# powered-down. This runbook drives carefully-selected CP0 GPIO lines
# HIGH one at a time, watching for a new USB enumeration event.
#
# Prerequisites: run ON THE BOARD (gk2), as root, with a known-good
# mPCIe device (EP06 modem or any mPCIe-USB device) physically seated
# in J5 and the screw tightened.
#
# Safety (v0.2.0): gpioinfo's "[used]" tag only reflects lines the
# kernel *requested* — some unrequested lines on the MOCHAbin are
# physically wired to critical functions (eth switch reset, USB hub
# power-enable, pca9554 IRQ, etc.) and driving them HIGH crashed
# gk2 on the v0.1.0 blanket sweep (incident 2026-05-22). v0.2.0
# therefore:
# 1. Skips known-dangerous lines unconditionally (DANGER_LINES).
# 2. Defaults to --dry-run (lists candidates without driving them).
# 3. Requires --commit AND --line for the actual probe — no
# blanket sweep mode anymore.
# 4. Refuses to touch gpiochip2 (CP0 GPIO1) entirely without an
# explicit --allow-chip2 flag (we have no per-line schematic
# mapping for that bank yet).
set -euo pipefail
readonly MODULE="probe-mpcie-gpios"
readonly VERSION="0.2.0"
# Lines known (from DTS inspection) to be wired to critical board
# functions even when gpioinfo reports them as "unused input". Format:
# "chip:line". Driving any of these would risk a board crash.
readonly -a DANGER_LINES=(
# gpiochip1 = cp0_gpio0
"gpiochip1:1" # mpp1 — cp0_switch_pins gpio function (Topaz reset)
"gpiochip1:9" # mpp9 — cp0_pcie_reset_pins (PCIe2 PERST#); driving it can crash the slot
"gpiochip1:27" # mpp27 — pca9554_int (SFP+ I/O expander IRQ); IRQ storm risk
# gpiochip2 = cp0_gpio1 — entire bank held off by default
# (no per-line schematic; require --allow-chip2 to probe)
)
readonly LOG_DIR="/var/log/secubox"
readonly LOG_FILE="${LOG_DIR}/mpcie-probe-$(date +%Y%m%d-%H%M%S).log"
readonly SETTLE_SEC="${SETTLE_SEC:-3}"
# Which gpiochips to probe. gpiochip1 = cp0_gpio0, gpiochip2 = cp0_gpio1.
# These are the two big CP0 banks on the Armada-7040. AP807 (gpiochip0)
# is system-controller / never wired to slot J5, so we skip it. SFP+
# expander (gpiochip3) and CP210x (gpiochip4) are obviously unrelated.
readonly CHIPS="${CHIPS:-gpiochip1 gpiochip2}"
usage() {
cat <<EOF
${MODULE} v${VERSION} — probe one CP0 GPIO line to find mPCIe J5 W_DISABLE#
usage:
$0 snapshot + list dry-run candidates (default)
$0 --baseline snapshot only — no candidate listing
$0 --line gpiochipN K dry-run a single line (refuses if danger-listed)
$0 --line gpiochipN K --commit actually drive the line HIGH for SETTLE_SEC
$0 --allow-chip2 ... required to touch any gpiochip2 line
$0 --help
env vars:
SETTLE_SEC=N seconds to hold the line HIGH (default 3)
output:
${LOG_DIR}/mpcie-probe-<timestamp>.log
WARNING — read this before --commit:
v0.1.0 of this script ran a blanket sweep across all "unused input"
CP0 GPIOs. That took gk2 hard-down on 2026-05-22 — some unrequested
lines are physically wired to critical functions (eth switch reset,
PCIe2 PERST#, pca9554 IRQ, etc.) even though the kernel never
requests them. v0.2.0 therefore refuses to run a blanket sweep at
all, and the default mode just lists candidates without driving
anything.
When you do --commit a single line, make sure you can physically
reach the MOCHAbin to power-cycle it if the board locks up.
prerequisites: run as root on the MOCHAbin, with an mPCIe device
(preferably the EP06 modem) physically seated in J5 and the retention
screw tightened.
A successful probe shows a new "usb 2-1.X: new high-speed USB device"
line in the post-dmesg diff AND a new device in lsusb.
EOF
}
# True if "chip:line" matches an entry in DANGER_LINES (case-sensitive).
is_danger_line() {
local key="$1"
local entry
for entry in "${DANGER_LINES[@]}"; do
[[ "$entry" == "$key" ]] && return 0
done
return 1
}
require_root() {
if [[ $EUID -ne 0 ]]; then
echo "error: must run as root (need gpiochip + dmesg access)" >&2
exit 1
fi
}
require_libgpiod() {
for cmd in gpiodetect gpioinfo gpioset; do
if ! command -v "$cmd" >/dev/null; then
echo "error: $cmd not found — install libgpiod (apt install gpiod)" >&2
exit 1
fi
done
}
setup_log() {
mkdir -p "${LOG_DIR}"
exec > >(tee -a "${LOG_FILE}") 2>&1
echo "=== ${MODULE} v${VERSION}$(date -Is) ==="
echo "log: ${LOG_FILE}"
}
snapshot_baseline() {
echo
echo "=== baseline ==="
echo "--- kernel ---"
uname -a
echo "--- gpiodetect ---"
gpiodetect
echo "--- gpioinfo (probed chips: ${CHIPS}) ---"
for chip in ${CHIPS}; do
gpioinfo "$chip"
done
echo "--- lsusb -t ---"
lsusb -t
echo "--- lsusb ---"
lsusb
echo "--- lspci -tv ---"
lspci -tv 2>/dev/null || echo "(lspci not installed)"
echo "--- recent dmesg (last 60s) ---"
dmesg -T --since '60 sec ago' 2>/dev/null | tail -30 || dmesg | tail -30
}
# gpioinfo line format:
# " line 14: unnamed unused input active-high "
# " line 0: unnamed \"reset\" output active-low [used]"
# A "candidate" is: unnamed + unused + input + NOT in DANGER_LINES.
line_is_candidate() {
local chip="$1"
local info="$2"
local n
if [[ "$info" =~ line\ +([0-9]+): ]]; then
n="${BASH_REMATCH[1]}"
else
return 1
fi
[[ "$info" != *"[used]"* ]] || return 1
[[ "$info" == *"input"* ]] || return 1
[[ "$info" == *"unused"* ]] || return 1
is_danger_line "${chip}:${n}" && return 1
return 0
}
list_candidates() {
local chip="$1"
echo
echo "=== candidates on ${chip} ==="
while IFS= read -r line; do
if line_is_candidate "${chip}" "${line}"; then
# Extract line number for the summary
[[ "$line" =~ line\ +([0-9]+): ]] && echo " ${chip}:${BASH_REMATCH[1]}"
fi
done < <(gpioinfo "${chip}")
}
probe_line_commit() {
local chip="$1"
local n="$2"
# Hard gate — never drive a danger-listed line, even with --commit.
if is_danger_line "${chip}:${n}"; then
echo "REFUSED: ${chip}:${n} is in DANGER_LINES — driving it" \
"risks crashing the board (see header comment)." >&2
exit 1
fi
# gpiochip2 entirely off-limits without --allow-chip2.
if [[ "${chip}" == "gpiochip2" && "${ALLOW_CHIP2:-0}" != "1" ]]; then
echo "REFUSED: gpiochip2 (cp0_gpio1) requires --allow-chip2 — no" \
"per-line schematic mapping for that bank yet." >&2
exit 1
fi
echo
echo "── probe ${chip} line ${n} (COMMIT, HIGH for ${SETTLE_SEC}s) ──"
local pre_lsusb pre_dmesg_tail
pre_lsusb=$(lsusb | sort)
pre_dmesg_tail=$(dmesg | tail -1)
# Drive HIGH for SETTLE_SEC seconds. gpioset releases the line
# (libgpiod restores it to input on close).
timeout "$((SETTLE_SEC + 2))" gpioset --mode=time --sec="${SETTLE_SEC}" \
"${chip}" "${n}=1" &
local gpio_pid=$!
sleep "$((SETTLE_SEC - 1))"
local post_lsusb post_dmesg
post_lsusb=$(lsusb | sort)
post_dmesg=$(dmesg | awk -v marker="${pre_dmesg_tail}" '
BEGIN { capture=0 }
index($0, marker) { capture=1; next }
capture { print }
')
wait "${gpio_pid}" 2>/dev/null || true
local lsusb_diff
lsusb_diff=$(diff <(echo "${pre_lsusb}") <(echo "${post_lsusb}") || true)
if [[ -n "${lsusb_diff}" ]] || echo "${post_dmesg}" | grep -qiE 'new (high|full|low|super)-speed usb|quectel|2c7c'; then
echo " *** CHANGE DETECTED ***"
echo " --- lsusb diff ---"
echo "${lsusb_diff:-(no diff)}"
echo " --- new dmesg ---"
printf ' %s\n' "${post_dmesg}"
else
echo " no change"
fi
}
probe_line_dryrun() {
local chip="$1"
local n="$2"
echo
echo "── DRY-RUN ${chip}:${n} ──"
if is_danger_line "${chip}:${n}"; then
echo " REFUSED: in DANGER_LINES"
return
fi
if [[ "${chip}" == "gpiochip2" && "${ALLOW_CHIP2:-0}" != "1" ]]; then
echo " REFUSED: gpiochip2 requires --allow-chip2"
return
fi
# Verify the line is currently unrequested and an input.
local info
info=$(gpioinfo "${chip}" 2>/dev/null | awk -v n="${n}" '$2 == n":"')
if [[ -z "${info}" ]]; then
echo " ERROR: line ${n} not found on ${chip}"
return
fi
if [[ "${info}" == *"[used]"* ]]; then
echo " REFUSED: line is currently [used] by the kernel"
return
fi
echo " OK: --commit would drive ${chip}:${n} HIGH for ${SETTLE_SEC}s"
}
main() {
require_root
require_libgpiod
local mode="default"
local chip=""
local n=""
local commit=0
while [[ $# -gt 0 ]]; do
case "$1" in
--help|-h)
usage; exit 0 ;;
--baseline)
mode="baseline"; shift ;;
--line)
[[ $# -ge 3 ]] || { usage; exit 1; }
mode="line"; chip="$2"; n="$3"; shift 3 ;;
--commit)
commit=1; shift ;;
--allow-chip2)
ALLOW_CHIP2=1; shift ;;
*)
usage; exit 1 ;;
esac
done
setup_log
case "${mode}" in
baseline)
snapshot_baseline
;;
line)
snapshot_baseline
if [[ "${commit}" == "1" ]]; then
probe_line_commit "${chip}" "${n}"
else
probe_line_dryrun "${chip}" "${n}"
echo
echo "(dry-run — pass --commit to actually drive the line)"
fi
;;
default)
snapshot_baseline
list_candidates gpiochip1
if [[ "${ALLOW_CHIP2:-0}" == "1" ]]; then
list_candidates gpiochip2
else
echo
echo "=== gpiochip2 candidates suppressed ==="
echo " pass --allow-chip2 to list (no per-line schematic yet)"
fi
echo
echo "=== next step ==="
echo " pick a candidate, then:"
echo " $0 --line <chip> <line> # dry-run"
echo " $0 --line <chip> <line> --commit # actually drive"
;;
esac
}
main "$@"