image.health: the watchdog's proof is WDOG1's own WCR, not the sysfs state

The sysfs state of watchdog0 says whether a process holds the device, and
none does: the kernel's core feeds the boot-armed hardware. The check
reads WCR through /dev/mem (WDE set, a 60 s period) and expects the state
to read inactive. Bench: PASS on image 20260903003213 with WCR 0x771f. The
BRINGUP facts bullet says the same.
This commit is contained in:
ScottW514
2026-09-02 20:46:48 -04:00
parent 5d2aa41a1c
commit 7c45642f05
2 changed files with 42 additions and 8 deletions
+6 -2
View File
@@ -1264,8 +1264,12 @@ is committed.
115200, the strings present in the fielded binary); the same adapter on the 115200, the strings present in the fielded binary); the same adapter on the
SoC console pins shows U-Boot at every normal boot and a console in the SoC console pins shows U-Boot at every normal boot and a console in the
recovery image after a button-hold recovery from power-off. An oddity of recovery image after a button-hold recovery from power-off. An oddity of
the watchdog reboot, recorded, not an open item. The kernel config now the watchdog reboot, recorded, not an open item. The kernel config carries
carries CONFIG_WATCHDOG_SYSFS so the state is readable without /dev/mem. CONFIG_WATCHDOG_SYSFS (identity, timeout, bootstatus). The sysfs `state`
reads `inactive`: it says whether a process holds the device, which none
does, the kernel's core feeds the hardware. The proof that WDOG1 is armed
is its own WCR (WDE set, a 60 s period), which `image.health` reads through
/dev/mem.
## Next work ## Next work
Open items only. Anything closed is in `CAMPAIGN-LOG.md`. Open items (bugs, Open items only. Anything closed is in `CAMPAIGN-LOG.md`. Open items (bugs,
+36 -6
View File
@@ -1,7 +1,9 @@
"""image.* - the post-flash health of the running image (always-required).""" """image.* - the post-flash health of the running image (always-required)."""
import glob import glob
import gzip import gzip
import mmap
import os import os
import struct
import re import re
import stat import stat
@@ -9,6 +11,23 @@ from ..catalog import test
from .. import hw from .. import hw
WDOG1_BASE = 0x020BC000 # i.MX6 WDOG1; WCR is the 16-bit word at offset 0
def _wdog1_wcr():
"""WDOG1's control register, read through /dev/mem (MMIO stays readable
under STRICT_DEVMEM), or None."""
try:
with open("/dev/mem", "r+b") as f:
m = mmap.mmap(f.fileno(), 4096, offset=WDOG1_BASE)
try:
return struct.unpack_from("<H", m, 0)[0]
finally:
m.close()
except (OSError, ValueError):
return None
def _read(path, default=None): def _read(path, default=None):
try: try:
with open(path, "r", encoding="utf-8", errors="replace") as f: with open(path, "r", encoding="utf-8", errors="replace") as f:
@@ -107,17 +126,28 @@ def image_health(ctx):
ev[opt] = val ev[opt] = val
ctx.log("%s=%s", opt, val) ctx.log("%s=%s", opt, val)
ctx.check(val in want, "%s=%s, expected one of %s", opt, val, want) ctx.check(val in want, "%s=%s, expected one of %s", opt, val, want)
# The boot-armed hardware watchdog, as the kernel's core sees it: U-Boot # The boot-armed hardware watchdog: U-Boot arms WDOG1 at 60 s and the
# arms WDOG1 at 60 s and the core keeps it fed (no userspace opens it). # kernel's core keeps it fed (no userspace opens it, so the sysfs state
# The sysfs view needs CONFIG_WATCHDOG_SYSFS; bootstatus carries the # reads "inactive": that attribute says whether a process holds the
# last reset's cause (32 = the watchdog's timeout). # device, not whether the hardware runs). The hardware's own word is
# WCR: WDE set, and WT giving the period. The sysfs view needs
# CONFIG_WATCHDOG_SYSFS; bootstatus carries the last reset's cause (32 =
# the watchdog's timeout).
wd = {k: (_read("/sys/class/watchdog/watchdog0/" + k) or "").strip() wd = {k: (_read("/sys/class/watchdog/watchdog0/" + k) or "").strip()
for k in ("identity", "state", "timeout", "bootstatus")} for k in ("identity", "state", "timeout", "bootstatus")}
wcr = _wdog1_wcr()
wd["wcr"] = ("%#06x" % wcr) if wcr is not None else None
wd["wde"] = bool(wcr & 0x4) if wcr is not None else None
wd["period_s"] = ((wcr >> 8) + 1) / 2.0 if wcr is not None else None
ev["watchdog"] = wd ev["watchdog"] = wd
ctx.log("watchdog0: %s", wd) ctx.log("watchdog0: %s", wd)
ctx.check(wd["state"] == "active", "watchdog0 is %r, expected active (armed by the bootloader, " ctx.check(wd["identity"], "watchdog0 has no sysfs view (CONFIG_WATCHDOG_SYSFS)")
"fed by the kernel core)", wd["state"] or None)
ctx.check(wd["timeout"] == "60", "watchdog0 timeout %r, expected 60 s", wd["timeout"] or None) ctx.check(wd["timeout"] == "60", "watchdog0 timeout %r, expected 60 s", wd["timeout"] or None)
ctx.check(wcr is not None, "WDOG1 WCR unreadable through /dev/mem")
ctx.check(wd["wde"], "WDOG1 is not enabled (WCR %s): nothing resets a hung kernel", wd["wcr"])
ctx.check(wd["period_s"] == 60.0, "WDOG1 period %s s (WCR %s), expected 60", wd["period_s"], wd["wcr"])
ctx.check(wd["state"] == "inactive", "watchdog0 is %r: a process holds the device, which the image "
"does not do", wd["state"] or None)
rel = _read("/proc/sys/kernel/osrelease", "").strip() rel = _read("/proc/sys/kernel/osrelease", "").strip()
ev["kernel_release"] = rel ev["kernel_release"] = rel
mods = manifest.platform.get("kernel_modules") or [] mods = manifest.platform.get("kernel_modules") or []