diff --git a/.claude/HISTORY.md b/.claude/HISTORY.md index 6be9f71a..09862594 100644 --- a/.claude/HISTORY.md +++ b/.claude/HISTORY.md @@ -1,6 +1,56 @@ # HISTORY — SecuBox-DEB Migration Log *Tracking completed milestones with dates* +--- +## 2026-05-22 + +### MOCHAbin mPCIe slot J5 — EP06 GPIO investigation runbook (Issue #345) + +**Context:** PR #255's DTS patch wired the UTMI PHY to `cp0_usb3_1`, so the +mPCIe slot's USB pipe is alive (4 USB buses on gk2 vs 2 before). But +plugging an EP06-E still produces no enumeration: `lsusb` blank for the +slot, `lspci -vvv` reports `DLActive-` on the PCIe side. Suspect: the +slot's W_DISABLE# / PWR_EN / WAKE# control lines are not declared in +the DTS, so they come up in whatever default state the SoC pad config +leaves them — likely holding the modem powered-down. No MOCHAbin +schematic in the repo to pin down which CP0 GPIO is wired to J5. + +**Done:** + +- **#345** — added `scripts/probe-mpcie-gpios.sh`: empirical sweep that + drives each *unrequested* CP0 GPIO line HIGH for a few seconds and + watches `dmesg` / `lsusb` for a new USB device. Skips any line marked + `[used]` by gpioinfo, uses libgpiod (`gpioset --mode=time` so the + line auto-restores to input on release). Three modes: `--baseline` + (snapshot only, no GPIO writes — safe to run anytime), `--line + gpiochipN K` (single line, useful once a candidate is found), or no + arg (full sweep across `gpiochip1` + `gpiochip2` = the two CP0 + banks). Output to `/var/log/secubox/mpcie-probe-.log`. +- Companion `docs/hardware/mochabin-mpcie-ep06-runbook.md` documenting + the hypothesis, the procedure step-by-step, and the DTS template + (`rfkill-gpio` block patterned after `cn9132-clearfog.dts` lines + 69–122) to apply once a candidate GPIO is identified. + +**Validation:** `--baseline` smoke-tested on gk2 — produces correct +gpioinfo dump, identifies the `[used]` lines (cp0_gpio0 line 0 "reset", +line 12 "PHY reset", line 30 "shutdown", plus the SFP+ pca9554 +expander chip 3). No spare modem expected; if probing finds no +candidate the next escalation is multimeter scoping of J5 pins +2/20/24/39/41/52. + +**Incident — v0.1.0 blanket sweep crashed gk2:** running the +"all unused inputs" sweep with no modem in J5 took the board hard-down +within seconds (host became unreachable; ARP stopped responding). +`gpioinfo`'s `[used]` tag only reflects lines the kernel *requested*, +not lines that are physically wired — several unrequested CP0 GPIOs +drive critical board functions (eth switch reset, PCIe2 PERST#, +pca9554 IRQ). v0.2.0 of the script removes the blanket-sweep mode +entirely: defaults to dry-run candidate listing, requires explicit +`--commit --line CHIP N` to drive any single line, hardcodes a +`DANGER_LINES` skip-list, and holds `gpiochip2` off behind +`--allow-chip2`. Memorialised in memory +`feedback_gpio_blanket_sweep_crashed_board.md`. + --- ## 2026-05-21 diff --git a/docs/hardware/mochabin-mpcie-ep06-runbook.md b/docs/hardware/mochabin-mpcie-ep06-runbook.md new file mode 100644 index 00000000..118b30bc --- /dev/null +++ b/docs/hardware/mochabin-mpcie-ep06-runbook.md @@ -0,0 +1,169 @@ +# MOCHAbin mPCIe Slot J5 — EP06 Investigation Runbook + +**Status:** parked. Runbook ready to execute when an mPCIe modem is +reseated in J5 (the original EP06-E on hand; no spare on order). +**Related issues:** #255 (DTS UTMI PHY landed) · #345 (this runbook). + +## Symptom + +After the DTS patch in #255 wired the UTMI PHY to `cp0_usb3_1`, the +mPCIe slot J5 USB pipe is fully alive — kernel reports the controller +binds cleanly and `lsusb -t` shows 4 USB buses (vs 2 before the patch). + +But plugging a Quectel EP06-E modem into J5 still doesn't produce any +`new high-speed USB device` event in dmesg, no `2c7c:0306` in `lsusb`, +and `lspci -vvv` reports the PCIe side as `DLActive-` (no link +partner). The modem is electrically dark on both interfaces of the +slot. + +## Hypothesis + +mPCIe defines several control pins per slot that the host must drive +for the device to wake: + +| Pin | Direction | Meaning | Default | +| ------------- | ----------- | ----------------------- | -------------------------------- | +| PERST# | host→device | reset (active low) | must transition low→high at boot | +| W_DISABLE# | host→device | radio kill (active low) | must be HIGH for radio | +| WAKE# | device→host | wake event (active low) | input only | +| PWR_EN / 3.3V | rail | power | must be ON | + +In `armada-7040-mochabin.dts`: + +- `cp0_pcie2` has `reset-gpios = <&cp0_gpio1 9 GPIO_ACTIVE_LOW>` — + PERST# is declared. +- `cp0_usb3_1` has UTMI PHY but **no W_DISABLE# / PWR_EN / WAKE# + GPIOs** declared. + +The schematic for the MOCHAbin J5 slot is not in this repo, and +Globalscale only publishes a partial. So we don't know which CP0 GPIO +(if any) is wired to W_DISABLE#. The other option is that it's +hard-tied (HIGH or LOW) on the PCB. + +Reference: `cn9132-clearfog.dts` declares `rfkill-gpio` nodes for each +of its mPCIe / M.2 slots (lines 69–122). That's the pattern to copy. + +## Investigation procedure + +`scripts/probe-mpcie-gpios.sh` (v0.2.0) is a one-line-at-a-time probe. +v0.1.0 of this script ran a blanket sweep across all "unused input" +CP0 GPIOs and took gk2 hard-down on 2026-05-22 — `gpioinfo`'s +`[used]` tag only reflects what the kernel *requested*, and several +unrequested lines are physically wired to critical functions (eth +switch reset, PCIe2 PERST#, pca9554 IRQ). v0.2.0 therefore: + +- skips a hardcoded `DANGER_LINES` list (those three + future + additions); +- refuses to touch `gpiochip2` (CP0 GPIO1) entirely without + `--allow-chip2` (no per-line schematic mapping for that bank yet); +- defaults to dry-run — listing candidates without driving anything; +- requires `--commit` + `--line CHIP N` to actually drive a line HIGH. + +There is no longer a blanket-sweep mode. + +Prerequisites: + +- Run **on the MOCHAbin**, as root. +- A known-good mPCIe device seated in J5 (preferably the EP06 modem; + failing that, any mPCIe-USB device). +- Screw tightened — the slot has spring-contact retention only. + +### Step 1 — baseline (safe, no GPIO writes) + +```bash +# from the dev host +scp scripts/probe-mpcie-gpios.sh root@:/tmp/ +ssh root@ 'bash /tmp/probe-mpcie-gpios.sh --baseline' +``` + +Output goes to `/var/log/secubox/mpcie-probe-.log`. Check +the gpioinfo table to confirm the lines you expect are `[used]` (PHY +reset on cp0_gpio0 line 12, etc.) — the script will refuse to touch +those. + +### Step 2 — list candidates (dry-run, default mode) + +```bash +ssh root@ 'bash /tmp/probe-mpcie-gpios.sh' +``` + +Prints the candidate list on `gpiochip1` (CP0 GPIO0). Each entry is +of the form `gpiochip1:N` — an unrequested input that is NOT in +`DANGER_LINES`. No GPIO is driven in this mode. + +If you want candidates on `gpiochip2` too (entire bank held off by +default), add `--allow-chip2` — only do this if you can physically +power-cycle the board if something locks up. + +### Step 3 — commit a single line + +Pick one candidate from step 2, then drive it for `SETTLE_SEC=3` s +(longer with the env var if the EP06 needs more time to boot its +radio): + +```bash +ssh root@ \ + 'SETTLE_SEC=10 bash /tmp/probe-mpcie-gpios.sh --line gpiochip1 14 --commit' +``` + +The script: + +1. Snapshots `lsusb` + last `dmesg` line. +2. Drives the line output-HIGH for `SETTLE_SEC` seconds. +3. Re-snapshots. +4. Releases the line (libgpiod restores input direction on close). +5. If `lsusb` differs or new "new high-speed USB device" / + "Quectel" / `2c7c` appears in dmesg, logs `*** CHANGE DETECTED ***`. + +A successful probe gives you the GPIO number to wire into the DTS. +Iterate over candidates one at a time; if a probe locks up the board, +add the line to `DANGER_LINES` in the script before retrying. + +## Translating findings to DTS + +Once a line is confirmed, add an `rfkill-gpio` block to +`armada-7040-mochabin.dts` next to the existing `&cp0_usb3_1` / +`&cp0_pcie2` declarations. Template (replace `` and ``): + +```dts +/ { + /* J5 W_DISABLE# (mPCIe modem radio enable) */ + rfkill-mpcie-modem { + compatible = "rfkill-gpio"; + label = "mpcie modem (J5)"; + radio-type = "wwan"; + /* rfkill-gpio inverts internally; ACTIVE_HIGH = pin HIGH when + * radio is OFF, LOW when ON. Check polarity against the + * schematic before merging. */ + shutdown-gpios = <& GPIO_ACTIVE_HIGH>; + }; +}; +``` + +If the modem still doesn't enumerate after asserting W_DISABLE# HIGH, +the next suspect is a missing 3.3V rail enable. Look for an unused +GPIO that toggles `/sys/class/regulator/regulator-N/state` from +`disabled` → `enabled` when driven HIGH. + +## Disambiguating "dead modem" vs "slot not powering it" + +There's no spare EP06 coming, so we can't simply swap units. If the +GPIO sweep finds no candidate line, the remaining signal that the +modem is alive at all is: + +- **3.3V on J5 pin 2 / 24 / 39 / 41 / 52** — multimeter on the + connector with the modem unseated tells you if the slot's power rail + is live. If 3.3V is missing, the modem can't possibly enumerate + regardless of W_DISABLE#. +- **Modem LED** — the EP06-E lights an onboard status LED when it has + power. Visible through the chassis cutout near J5. + +Multimeter scoping is outside the runbook's automated scope but is the +next escalation if the sweep is inconclusive. + +## Outcome we want + +A single small DTS patch (rfkill-gpio block, maybe a regulator) that +makes EP06 enumerate at boot without any post-boot intervention. That +unblocks #236 (secubox-rbs-sensor v0.2) and the v0.4 SMS-via-EP06 +notification channel for secubox-sentinelle-gsm. diff --git a/scripts/probe-mpcie-gpios.sh b/scripts/probe-mpcie-gpios.sh new file mode 100755 index 00000000..846ebb1d --- /dev/null +++ b/scripts/probe-mpcie-gpios.sh @@ -0,0 +1,336 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LicenseRef-CMSD-1.0 +# Copyright (c) 2026 CyberMind — Gérald Kerma +# 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 <.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 # dry-run" + echo " $0 --line --commit # actually drive" + ;; + esac +} + +main "$@"