diff --git a/releasenotes/RELEASE_NOTES_1.17.7-zephcore.md b/releasenotes/RELEASE_NOTES_1.17.7-zephcore.md index 2b1aec2..49a2bf4 100644 --- a/releasenotes/RELEASE_NOTES_1.17.7-zephcore.md +++ b/releasenotes/RELEASE_NOTES_1.17.7-zephcore.md @@ -1,8 +1,8 @@ # ZephCore 1.17.7-zephcore Faster GPS fixes on three trackers, a clock that survives a reboot, a fix for USB companions that -could stop responding right after boot, more reliable detection of clock chips, and a few fixes ported -from upstream MeshCore. +could stop responding right after boot, more reliable detection of clock chips, repeaters that power +down an external flash chip they do not use, and a few fixes ported from upstream MeshCore. > [!NOTE] > A normal upgrade keeps your identity, settings, contacts and phone pairing. @@ -81,6 +81,29 @@ and the companion protocol test gives the same replies as 1.17.6. The same patch covers a second way into the same loop: a host computer going to sleep with the port open. That one is fixed from reading the code and has not been reproduced on hardware. +## Repeaters: the external flash chip is powered down + +Some boards carry a second flash chip, which companions keep contacts and channels on. Repeaters, +room servers and observers store nothing on it. Since 1.17.6 those roles no longer set the chip up at +all, and that left its control pins unconnected: on a Wio Tracker L1 repeater the chip-select line +read low, so the chip sat selected for as long as the node ran instead of idling. 1.17.4 and earlier +set the chip up at boot on every role. + +These roles now set the chip up once at start-up and put it into its deep power-down mode, with +chip-select held high. A factory `erase` wakes the chip, erases it and puts it back. Companions are +unchanged: they use the chip, so it stays in standby. + +Boards with this chip: T-Echo, T-Impulse Plus, MeshTracker X1, SenseCAP Solar, ThinkNode M1, +ThinkNode M6, Wio Tracker L1, Wio Tracker L1 Pro 1W and XIAO nRF52840. + +Checked on a Wio Tracker L1 repeater after a normal boot, after a first-boot format and after +`erase`: the chip ignores an ID request, and answers again once it is woken. The current this saves +has not been measured, and the change has not been run on the other boards. + +This was found while looking into a report of a XIAO nRF52840 repeater drawing about 4 mA more on +1.17.6 than on 1.17.4 (thanks to **Kimotu**). Whether it accounts for that difference is not +confirmed yet. + ## Also in this release - **Malformed encrypted packets are rejected earlier**, before any cryptography runs. diff --git a/zephcore/CMakeLists.txt b/zephcore/CMakeLists.txt index fd8c544..b84313b 100644 --- a/zephcore/CMakeLists.txt +++ b/zephcore/CMakeLists.txt @@ -378,6 +378,8 @@ target_sources(app PRIVATE helpers/buzzer_gate.c # QSPI bring-up probe. Self-stubs unless CONFIG_ZEPHCORE_QSPI_RDID_PROBE. helpers/qspi_probe.c + # TEMPORARY bench instrumentation. Self-stubs unless CONFIG_ZEPHCORE_POWER_DIAG. + helpers/power_diag.c # Storage shared by every role's store (the companion builds # ZephyrDataStore, the other three RepeaterDataStore): factory format, # power-safe replace and non-logging probes, the identity file, and the diff --git a/zephcore/Kconfig b/zephcore/Kconfig index 4f9cbf7..90d8d3a 100644 --- a/zephcore/Kconfig +++ b/zephcore/Kconfig @@ -867,6 +867,15 @@ config ZEPHCORE_LOOP_WAKE_STATS after a change to its scheduling. Measure on the release config with this passed by -D, so debug logging does not add wakes of its own. +config ZEPHCORE_POWER_DIAG + bool "CLI: CPU wake sources, sleep fraction, enabled peripherals (TEMPORARY)" + depends on SOC_NRF52840 + help + Bench instrumentation: `get pwr ` reports which interrupts wake the + CPU, how much of the time it sleeps, and which clocks and peripherals + stay enabled. Build with boards/common/power_diag.conf, which turns + on the user tracing hooks it counts from. + config ZEPHCORE_LORA_RETENTION_DEBUG bool "SX126x: verify the warm-start retention list on air" depends on ZEPHCORE_RADIO_NATIVE diff --git a/zephcore/adapters/datastore/ZephyrFsFormat.c b/zephcore/adapters/datastore/ZephyrFsFormat.c index abec2c1..b65232e 100644 --- a/zephcore/adapters/datastore/ZephyrFsFormat.c +++ b/zephcore/adapters/datastore/ZephyrFsFormat.c @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -46,20 +47,70 @@ static void flatten(uint8_t id, const char *tag) * boot after a UF2 update (seconds in the bootloader, rail off) did not. * Bringing the part up here, on first use, is long after the rail. Each role * reaches this on its own path, so it is idempotent: -EALREADY means an - * earlier call already ran the init, whatever the result was. */ + * earlier call already ran the init, whatever the result was. + * + * A role that does not use /ext parks the part in deep power-down + * (zephcore_fs_ext_power_down()); a format still has to reach it, so a + * suspended flash is resumed here. */ static void ext_flash_init(void) { const struct device *dev = PARTITION_DEVICE(qspi_storage_partition); + enum pm_device_state state; if (!device_is_ready(dev)) { int rc = device_init(dev); LOG_INF("%s flash init: rc=%d ready=%d", EXT_MNT_POINT, rc, (int)device_is_ready(dev)); + } else if (pm_device_state_get(dev, &state) == 0 && + state == PM_DEVICE_STATE_SUSPENDED) { + int rc = pm_device_action_run(dev, PM_DEVICE_ACTION_RESUME); + + LOG_INF("%s flash resume: rc=%d", EXT_MNT_POINT, rc); } } #endif +void zephcore_fs_ext_power_down(void) +{ +#if PARTITION_EXISTS(qspi_storage_partition) + const struct device *dev = PARTITION_DEVICE(qspi_storage_partition); + enum pm_device_state state; + int rc; + + if (device_is_ready(dev) && pm_device_state_get(dev, &state) == 0 && + state == PM_DEVICE_STATE_SUSPENDED) { + return; + } + +#if DT_NODE_EXISTS(DT_NODELABEL(qspi_lfs)) + /* A first-boot format leaves /ext mounted (zephcore_fs_format_all()). */ + if (zephcore_fs_is_mounted(EXT_MNT_POINT)) { + FS_FSTAB_DECLARE_ENTRY(DT_NODELABEL(qspi_lfs)); + fs_unmount(&FS_FSTAB_ENTRY(DT_NODELABEL(qspi_lfs))); + } +#endif + + /* The part has to be brought up to be put down: left deferred, nothing + * ever configures its pads, and a floating CS# reads low -- the flash + * sits selected for as long as the node runs (measured on a Wio Tracker + * L1 repeater). Init drives CS# and sends the part its commands; + * suspend sends Deep Power-Down and applies the sleep pinctrl state, + * which must keep a pull-up on CS#. */ + ext_flash_init(); + if (!device_is_ready(dev)) { + LOG_WRN("%s flash not ready, not powered down", EXT_MNT_POINT); + return; + } + rc = pm_device_action_run(dev, PM_DEVICE_ACTION_SUSPEND); + if (rc == 0) { + LOG_INF("%s flash in deep power-down", EXT_MNT_POINT); + } else { + LOG_WRN("%s flash power-down failed: %d", EXT_MNT_POINT, rc); + } +#endif +} + bool zephcore_fs_mount_ext(void) { #if DT_NODE_EXISTS(DT_NODELABEL(qspi_lfs)) diff --git a/zephcore/adapters/datastore/ZephyrFsFormat.h b/zephcore/adapters/datastore/ZephyrFsFormat.h index 038579b..7cccc04 100644 --- a/zephcore/adapters/datastore/ZephyrFsFormat.h +++ b/zephcore/adapters/datastore/ZephyrFsFormat.h @@ -44,6 +44,17 @@ bool zephcore_fs_format_all(bool *out_ext_mounted); */ bool zephcore_fs_mount_ext(void); +/** + * @brief Put the /ext flash into deep power-down, for a role that keeps + * nothing on it (repeater, room server, observer). + * + * Unmounts /ext if a format left it mounted, initialises the deferred flash, + * then suspends it: the part gets its Deep Power-Down command and the bus + * pads go to their sleep state. Idempotent; a no-op on boards with no QSPI. + * zephcore_fs_format_all() and zephcore_fs_mount_ext() resume the part. + */ +void zephcore_fs_ext_power_down(void); + #ifdef __cplusplus } #endif diff --git a/zephcore/app/RepeaterDataStore.cpp b/zephcore/app/RepeaterDataStore.cpp index 86e476f..b424499 100644 --- a/zephcore/app/RepeaterDataStore.cpp +++ b/zephcore/app/RepeaterDataStore.cpp @@ -36,6 +36,9 @@ bool RepeaterDataStore::begin() { LOG_INF("Created %s directory", BASE_PATH); } + /* Nothing a server role stores lives on /ext. */ + zephcore_fs_ext_power_down(); + _initialized = true; LOG_INF("RepeaterDataStore initialized at %s", BASE_PATH); return true; diff --git a/zephcore/boards/common/power_diag.conf b/zephcore/boards/common/power_diag.conf new file mode 100644 index 0000000..e807d0b --- /dev/null +++ b/zephcore/boards/common/power_diag.conf @@ -0,0 +1,14 @@ +# TEMPORARY bench instrumentation: `get pwr ` (helpers/power_diag.c) and +# `get loop.wakes`. nRF52840 only. Logging stays off, as in a release build, +# so the log thread adds no wakes of its own. +CONFIG_ZEPHCORE_POWER_DIAG=y +CONFIG_ZEPHCORE_LOOP_WAKE_STATS=y + +# The idle and ISR-entry hooks power_diag counts from. +CONFIG_TRACING=y +CONFIG_TRACING_USER=y + +# Per-thread run time. +CONFIG_THREAD_RUNTIME_STATS=y +CONFIG_THREAD_NAME=y +CONFIG_THREAD_MONITOR=y diff --git a/zephcore/boards/nrf52840/sensecap_solar/sensecap_solar_nrf52840-pinctrl.dtsi b/zephcore/boards/nrf52840/sensecap_solar/sensecap_solar_nrf52840-pinctrl.dtsi index 6724ac1..fbd8fa5 100644 --- a/zephcore/boards/nrf52840/sensecap_solar/sensecap_solar_nrf52840-pinctrl.dtsi +++ b/zephcore/boards/nrf52840/sensecap_solar/sensecap_solar_nrf52840-pinctrl.dtsi @@ -75,9 +75,15 @@ , , , - , - ; + ; low-power-enable; }; + /* CS# keeps a pull-up: the flash is in deep power-down in this + * state, and a floating CS# reads low (the part sits selected). */ + group2 { + psels = ; + low-power-enable; + bias-pull-up; + }; }; }; diff --git a/zephcore/boards/nrf52840/wio_tracker_l1/board.overlay b/zephcore/boards/nrf52840/wio_tracker_l1/board.overlay index f722612..28ed091 100644 --- a/zephcore/boards/nrf52840/wio_tracker_l1/board.overlay +++ b/zephcore/boards/nrf52840/wio_tracker_l1/board.overlay @@ -216,19 +216,9 @@ }; }; -/* QSPI sleep: CSN in the same group as the data lines, no pull-up. */ -&qspi_sleep { - /delete-node/ group2; - - group1 { - psels = , - , - , - , - , - ; - }; -}; +/* QSPI sleep: upstream's state as it is, CS# in its own group with a pull-up. + * The flash is in deep power-down in this state, and a floating CS# reads low + * (the part sits selected). */ /* ---- Peripherals ------------------------------------------------------ */ diff --git a/zephcore/boards/nrf52840/wio_tracker_l1_1w/wio_tracker_l1_1w_nrf52840-pinctrl.dtsi b/zephcore/boards/nrf52840/wio_tracker_l1_1w/wio_tracker_l1_1w_nrf52840-pinctrl.dtsi index d97d742..6155c4e 100644 --- a/zephcore/boards/nrf52840/wio_tracker_l1_1w/wio_tracker_l1_1w_nrf52840-pinctrl.dtsi +++ b/zephcore/boards/nrf52840/wio_tracker_l1_1w/wio_tracker_l1_1w_nrf52840-pinctrl.dtsi @@ -117,9 +117,15 @@ , , , - , - ; + ; low-power-enable; }; + /* CS# keeps a pull-up: the flash is in deep power-down in this + * state, and a floating CS# reads low (the part sits selected). */ + group2 { + psels = ; + low-power-enable; + bias-pull-up; + }; }; }; diff --git a/zephcore/boards/nrf52840/xiao_nrf52840/xiao_nrf52840_nrf52840-pinctrl.dtsi b/zephcore/boards/nrf52840/xiao_nrf52840/xiao_nrf52840_nrf52840-pinctrl.dtsi index 03ac62f..13abf3a 100644 --- a/zephcore/boards/nrf52840/xiao_nrf52840/xiao_nrf52840_nrf52840-pinctrl.dtsi +++ b/zephcore/boards/nrf52840/xiao_nrf52840/xiao_nrf52840_nrf52840-pinctrl.dtsi @@ -58,9 +58,15 @@ , , , - , - ; + ; low-power-enable; }; + /* CS# keeps a pull-up: the flash is in deep power-down in this + * state, and a floating CS# reads low (the part sits selected). */ + group2 { + psels = ; + low-power-enable; + bias-pull-up; + }; }; }; diff --git a/zephcore/helpers/CommonCLI.cpp b/zephcore/helpers/CommonCLI.cpp index 9ea10d6..c25bf7c 100644 --- a/zephcore/helpers/CommonCLI.cpp +++ b/zephcore/helpers/CommonCLI.cpp @@ -14,6 +14,7 @@ #endif #include #include +extern "C" void power_diag_line(int n, char *out, size_t cap); /* TEMPORARY: helpers/power_diag.c */ #include #include #include @@ -1235,6 +1236,10 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, const char* command, cha } else if (memcmp(config, "mem", 3) == 0) { memStatsLine(atoi(&config[3]), reply); #endif +#if IS_ENABLED(CONFIG_ZEPHCORE_POWER_DIAG) + } else if (memcmp(config, "pwr", 3) == 0) { + power_diag_line(atoi(&config[3]), reply, replyCap(sender_timestamp)); +#endif #if IS_ENABLED(CONFIG_ZEPHCORE_LOOP_WAKE_STATS) } else if (memcmp(config, "loop.wakes", 10) == 0) { loopWakeFormat(reply, replyCap(sender_timestamp)); diff --git a/zephcore/helpers/power_diag.c b/zephcore/helpers/power_diag.c new file mode 100644 index 0000000..089cf6f --- /dev/null +++ b/zephcore/helpers/power_diag.c @@ -0,0 +1,383 @@ +/* + * SPDX-License-Identifier: MIT + * power_diag - what wakes the CPU, how long it sleeps, and which clocks and + * peripherals stay enabled through that sleep (CONFIG_ZEPHCORE_POWER_DIAG, + * `get pwr `). nRF52840 only. TEMPORARY bench instrumentation. + * + * Counted from Zephyr's user tracing hooks: the idle hook stamps each entry to + * WFI, and the first interrupt after it closes the interval and is recorded as + * the wake source. Every interrupt is counted by IRQ number as well. + * + * Two windows are kept: since boot (or the last `get pwr -1`), and "battery", + * the last stretch with VBUS absent -- opened when USB power goes away and + * frozen when it returns, so a node can be run unplugged and read afterwards. + */ + +#include +#include +#include + +#if IS_ENABLED(CONFIG_ZEPHCORE_POWER_DIAG) && defined(CONFIG_SOC_NRF52840) + +#include + +#define NIRQ 48 + +#define REG(addr) (*(volatile uint32_t *)(addr)) + +#define POWER_BASE 0x40000000UL +#define USBREGSTATUS REG(POWER_BASE + 0x438) +#define HFCLKSTAT REG(POWER_BASE + 0x40C) + +/* Peripherals with an ENABLE register at +0x500; bit n of the masks below. */ +static const struct { + const char *name; + uint32_t base; +} periph[] = { + { "uarte0", 0x40002000UL }, { "spi0/twi0", 0x40003000UL }, + { "spi1/twi1", 0x40004000UL }, { "saadc", 0x40007000UL }, + { "qdec", 0x40012000UL }, + { "comp", 0x40013000UL }, { "pwm0", 0x4001C000UL }, + { "pdm", 0x4001D000UL }, { "pwm1", 0x40021000UL }, + { "pwm2", 0x40022000UL }, { "spi2", 0x40023000UL }, + { "i2s", 0x40025000UL }, { "usbd", 0x40027000UL }, + { "uarte1", 0x40028000UL }, { "qspi", 0x40029000UL }, + { "pwm3", 0x4002D000UL }, { "spi3", 0x4002F000UL }, +}; + +struct pd_stats { + uint32_t start_cyc; /* k_cycle_get_32() when the window opened */ + uint32_t end_cyc; /* when it was frozen (battery window only) */ + uint64_t sleep_cyc; /* cycles spent in WFI */ + uint64_t sleep_hfxo_cyc; /* ... of which with the HF crystal running */ + uint32_t wakes; /* WFI exits */ + uint32_t isrs; /* interrupt entries */ + uint32_t en_or; /* peripherals seen enabled at an idle entry */ + uint32_t en_and; /* peripherals enabled at every idle entry */ + uint32_t wake_irq[NIRQ]; + uint32_t isr_irq[NIRQ]; +}; + +static struct pd_stats boot_w; /* since boot / last reset */ +static struct pd_stats batt_w; /* last VBUS-absent stretch */ +static bool batt_open; +static bool batt_valid; +static bool vbus_last = true; +static bool started; + +static volatile bool in_idle; +static uint32_t idle_enter_cyc; +static bool idle_hfxo; + +static void window_open(struct pd_stats *w, uint32_t now) +{ + memset(w, 0, sizeof(*w)); + w->start_cyc = now; + w->en_and = 0xFFFFFFFFu; +} + +static uint32_t periph_mask(void) +{ + uint32_t m = 0; + + for (unsigned int i = 0; i < ARRAY_SIZE(periph); i++) { + if (REG(periph[i].base + 0x500) != 0) { + m |= BIT(i); + } + } + return m; +} + +void sys_trace_idle_user(void) +{ + uint32_t m = periph_mask(); + + if (!started) { + window_open(&boot_w, k_cycle_get_32()); + started = true; + } + boot_w.en_or |= m; + boot_w.en_and &= m; + if (batt_open) { + batt_w.en_or |= m; + batt_w.en_and &= m; + } + /* SRC = Xtal (bit 0) and STATE = running (bit 16) */ + idle_hfxo = (HFCLKSTAT & 0x10001u) == 0x10001u; + idle_enter_cyc = k_cycle_get_32(); + in_idle = true; +} + +static void account(struct pd_stats *w, uint32_t irq, bool woke, uint32_t slept) +{ + w->isrs++; + if (irq < NIRQ) { + w->isr_irq[irq]++; + } + if (woke) { + w->wakes++; + w->sleep_cyc += slept; + if (idle_hfxo) { + w->sleep_hfxo_cyc += slept; + } + if (irq < NIRQ) { + w->wake_irq[irq]++; + } + } +} + +void sys_trace_isr_enter_user(void) +{ + uint32_t now = k_cycle_get_32(); + uint32_t irq = __get_IPSR() - 16u; + bool woke = in_idle; + uint32_t slept = woke ? now - idle_enter_cyc : 0; + bool vbus = (USBREGSTATUS & 1u) != 0; + + in_idle = false; + + if (!started) { + window_open(&boot_w, now); + started = true; + } + if (vbus != vbus_last) { + if (!vbus) { + window_open(&batt_w, now); + batt_open = true; + batt_valid = false; + } else if (batt_open) { + batt_w.end_cyc = now; + batt_open = false; + batt_valid = true; + } + vbus_last = vbus; + } + + account(&boot_w, irq, woke, slept); + if (batt_open) { + account(&batt_w, irq, woke, slept); + } +} + +static int fmt_irqs(char *out, size_t cap, const char *tag, const uint32_t *tab) +{ + int n = snprintf(out, cap, "> %s", tag); + + for (int i = 0; i < NIRQ && n > 0 && (size_t)n < cap; i++) { + if (tab[i]) { + n += snprintf(out + n, cap - n, " %d:%u", i, (unsigned)tab[i]); + } + } + return n; +} + +static void fmt_summary(char *out, size_t cap, const char *tag, const struct pd_stats *w, + uint32_t end) +{ + uint32_t hz = sys_clock_hw_cycles_per_sec(); + uint64_t span = (uint32_t)(end - w->start_cyc); + uint32_t span_ms = (uint32_t)(span * 1000u / hz); + uint32_t sleep_ms = (uint32_t)(w->sleep_cyc * 1000u / hz); + uint32_t hfxo_ms = (uint32_t)(w->sleep_hfxo_cyc * 1000u / hz); + uint32_t awake_ppm = span ? (uint32_t)((span - MIN(span, w->sleep_cyc)) * 1000000u / span) + : 0; + + snprintf(out, cap, + "> %s span=%ums sleep=%ums awake_ppm=%u wakes=%u isrs=%u sleep_hfxo=%ums " + "en_or=%05x en_and=%05x", + tag, (unsigned)span_ms, (unsigned)sleep_ms, (unsigned)awake_ppm, + (unsigned)w->wakes, (unsigned)w->isrs, (unsigned)hfxo_ms, (unsigned)w->en_or, + (unsigned)(w->en_and & (BIT(ARRAY_SIZE(periph)) - 1u))); +} + +struct thr_walk { + int want; + int idx; + char *out; + size_t cap; +}; + +static void thr_cb(const struct k_thread *t, void *u) +{ + struct thr_walk *w = u; + k_thread_runtime_stats_t st; + const char *name; + + if (w->idx++ != w->want) { + return; + } + name = k_thread_name_get((k_tid_t)t); + if (k_thread_runtime_stats_get((k_tid_t)t, &st) != 0) { + st.execution_cycles = 0; + } + snprintf(w->out, w->cap, "> t %s prio=%d run=%ums", (name && name[0]) ? name : "?", + t->base.prio, + (unsigned)(st.execution_cycles * 1000u / sys_clock_hw_cycles_per_sec())); +} + +/* One single-lane SPI transaction on the QSPI pads (mode 0, ~250 kHz). */ +static void pd_spi(uint8_t cmd, uint8_t *rx, size_t rx_len) +{ + REG(0x5000050CUL) = BIT(25); /* CS# low */ + k_busy_wait(2); + for (size_t i = 0; i < 1 + rx_len; i++) { + uint8_t o = (i == 0) ? cmd : 0, in = 0; + + for (int b = 7; b >= 0; b--) { + if ((o >> b) & 1u) { + REG(0x50000508UL) = BIT(20); + } else { + REG(0x5000050CUL) = BIT(20); + } + k_busy_wait(2); + REG(0x50000508UL) = BIT(21); + in = (in << 1) | ((REG(0x50000510UL) >> 24) & 1u); + k_busy_wait(2); + REG(0x5000050CUL) = BIT(21); + } + if (i > 0) { + rx[i - 1] = in; + } + } + k_busy_wait(2); + REG(0x50000508UL) = BIT(25); /* CS# high */ +} + +/* One line per call: + * -1 reset the boot window 0 boot summary 1 boot wake IRQs + * 2 boot all IRQs 3 battery summary 4 battery wake IRQs + * 5 battery all IRQs 6 power/clock regs 7 peripheral enables now + * 8 GPIO 10.. threads (run time), then "end" */ +void power_diag_line(int n, char *out, size_t cap) +{ + uint32_t now = k_cycle_get_32(); + + if (n < 0) { + unsigned int key = irq_lock(); + + window_open(&boot_w, now); + irq_unlock(key); + snprintf(out, cap, "> reset"); + } else if (n == 0) { + fmt_summary(out, cap, "boot", &boot_w, now); + } else if (n == 1) { + fmt_irqs(out, cap, "wake", boot_w.wake_irq); + } else if (n == 2) { + fmt_irqs(out, cap, "isr", boot_w.isr_irq); + } else if (n == 3) { + if (batt_open) { + fmt_summary(out, cap, "batt(open)", &batt_w, now); + } else if (batt_valid) { + fmt_summary(out, cap, "batt", &batt_w, batt_w.end_cyc); + } else { + snprintf(out, cap, "> batt none"); + } + } else if (n == 4) { + fmt_irqs(out, cap, "bwake", batt_w.wake_irq); + } else if (n == 5) { + fmt_irqs(out, cap, "bisr", batt_w.isr_irq); + } else if (n == 6) { + snprintf(out, cap, + "> dcdcen=%u dcdcen0=%u mainreg=%u usbreg=%x hfclkstat=%05x " + "lfclkstat=%05x lfclksrccopy=%x radio_power=%u radio_state=%u", + (unsigned)REG(POWER_BASE + 0x578), (unsigned)REG(POWER_BASE + 0x580), + (unsigned)REG(POWER_BASE + 0x640), (unsigned)USBREGSTATUS, + (unsigned)HFCLKSTAT, (unsigned)REG(POWER_BASE + 0x418), + (unsigned)REG(POWER_BASE + 0x41C), (unsigned)REG(0x40001FFCUL), + (unsigned)REG(0x40001550UL)); + } else if (n == 7) { + int k = snprintf(out, cap, "> en"); + + for (unsigned int i = 0; i < ARRAY_SIZE(periph) && (size_t)k < cap; i++) { + uint32_t v = REG(periph[i].base + 0x500); + + if (v) { + k += snprintf(out + k, cap - k, " %s=%u", periph[i].name, + (unsigned)v); + } + } + } else if (n == 8) { + snprintf(out, cap, + "> p0 dir=%08x out=%08x in=%08x p1 dir=%08x out=%08x in=%08x", + (unsigned)REG(0x50000514UL), (unsigned)REG(0x50000504UL), + (unsigned)REG(0x50000510UL), (unsigned)REG(0x50000814UL), + (unsigned)REG(0x50000804UL), (unsigned)REG(0x50000810UL)); + } else if (n >= 100 && n < 148) { + /* Pin peek: PIN_CNF as it stands, then the level the pad sits at, + * sampled 8 times 1 ms apart with the input buffer connected for + * the read only. A floating pad reads mixed or follows a finger. */ + volatile uint32_t *cnf = (n < 132) ? ®(0x50000700UL + 4u * (n - 100)) + : ®(0x50000A00UL + 4u * (n - 132)); + uint32_t in_reg = (n < 132) ? 0x50000510UL : 0x50000810UL; + uint32_t bit = (n < 132) ? (n - 100) : (n - 132); + uint32_t saved = *cnf; + unsigned int lv = 0; + + *cnf = saved & ~BIT(1); /* INPUT = connect */ + for (int i = 0; i < 8; i++) { + k_busy_wait(1000); + lv = (lv << 1) | ((REG(in_reg) >> bit) & 1u); + } + *cnf = saved; + snprintf(out, cap, "> P%d.%02d cnf=%08x dir=%s inbuf=%s pull=%u samples=%02x", + n < 132 ? 0 : 1, (int)bit, (unsigned)saved, (saved & 1u) ? "out" : "in", + (saved & 2u) ? "off" : "on", (unsigned)((saved >> 2) & 3u), lv); + } else if (n == 200 || n == 201) { + /* Is the /ext flash in deep power-down? Bit-bang RDID (0x9F) on + * the QSPI pads as single-lane SPI: a part in DPD ignores it and + * IO1, pulled up here, reads ff ff ff. 201 sends Release (0xAB) + * first, expects the JEDEC id, then puts the part back (0xB9). + * Pads: SCK P0.21, CSN P0.25, IO0 P0.20, IO1 P0.24, IO2/IO3 + * P0.22/P0.23 held high (Wio L1, XIAO nRF52840, SenseCAP Solar). */ + static const uint8_t pins[] = { 21, 25, 20, 24, 22, 23 }; + uint32_t saved[ARRAY_SIZE(pins)]; + uint32_t saved_out = REG(0x50000504UL); + uint8_t id[3]; + + for (unsigned int i = 0; i < ARRAY_SIZE(pins); i++) { + saved[i] = REG(0x50000700UL + 4u * pins[i]); + } + REG(0x50000508UL) = BIT(25) | BIT(22) | BIT(23); /* OUTSET */ + REG(0x5000050CUL) = BIT(21) | BIT(20); /* OUTCLR */ + REG(0x50000700UL + 4u * 25) = 1u; /* out */ + REG(0x50000700UL + 4u * 21) = 1u; + REG(0x50000700UL + 4u * 20) = 1u; + REG(0x50000700UL + 4u * 22) = 1u; + REG(0x50000700UL + 4u * 23) = 1u; + REG(0x50000700UL + 4u * 24) = (3u << 2); /* in, pull-up */ + k_busy_wait(100); + if (n == 201) { + pd_spi(0xAB, NULL, 0); + k_busy_wait(200); + } + pd_spi(0x9F, id, sizeof(id)); + if (n == 201) { + pd_spi(0xB9, NULL, 0); + k_busy_wait(50); + } + for (unsigned int i = 0; i < ARRAY_SIZE(pins); i++) { + REG(0x50000700UL + 4u * pins[i]) = saved[i]; + } + REG(0x50000504UL) = saved_out; + snprintf(out, cap, "> rdid%s %02x %02x %02x", n == 201 ? " after release" : "", + id[0], id[1], id[2]); + } else { + struct thr_walk w = { n - 10, 0, out, cap }; + + out[0] = '\0'; + k_thread_foreach_unlocked(thr_cb, &w); + if (out[0] == '\0') { + snprintf(out, cap, "end"); + } + } +} + +#else + +void power_diag_line(int n, char *out, size_t cap) +{ + ARG_UNUSED(n); + snprintf(out, cap, "> unsupported"); +} + +#endif