qspi power down on repeaters

This commit is contained in:
liquidraver
2026-10-06 13:27:58 +02:00
parent 108b96d36c
commit e016fe1760
13 changed files with 531 additions and 22 deletions
+25 -2
View File
@@ -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.
+2
View File
@@ -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
+9
View File
@@ -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 <n>` 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
+52 -1
View File
@@ -9,6 +9,7 @@
#include <zephyr/devicetree.h>
#include <zephyr/device.h>
#include <zephyr/fs/fs.h>
#include <zephyr/pm/device.h>
#include <zephyr/storage/flash_map.h>
#include <zephyr/logging/log.h>
@@ -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))
@@ -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
+3
View File
@@ -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;
+14
View File
@@ -0,0 +1,14 @@
# TEMPORARY bench instrumentation: `get pwr <n>` (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
@@ -75,9 +75,15 @@
<NRF_PSEL(QSPI_IO0, 0, 20)>,
<NRF_PSEL(QSPI_IO1, 0, 24)>,
<NRF_PSEL(QSPI_IO2, 0, 22)>,
<NRF_PSEL(QSPI_IO3, 0, 23)>,
<NRF_PSEL(QSPI_CSN, 0, 25)>;
<NRF_PSEL(QSPI_IO3, 0, 23)>;
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 = <NRF_PSEL(QSPI_CSN, 0, 25)>;
low-power-enable;
bias-pull-up;
};
};
};
@@ -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 = <NRF_PSEL(QSPI_SCK, 0, 21)>,
<NRF_PSEL(QSPI_IO0, 0, 20)>,
<NRF_PSEL(QSPI_IO1, 0, 24)>,
<NRF_PSEL(QSPI_IO2, 0, 22)>,
<NRF_PSEL(QSPI_IO3, 0, 23)>,
<NRF_PSEL(QSPI_CSN, 0, 25)>;
};
};
/* 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 ------------------------------------------------------ */
@@ -117,9 +117,15 @@
<NRF_PSEL(QSPI_IO0, 0, 20)>,
<NRF_PSEL(QSPI_IO1, 0, 24)>,
<NRF_PSEL(QSPI_IO2, 0, 22)>,
<NRF_PSEL(QSPI_IO3, 0, 23)>,
<NRF_PSEL(QSPI_CSN, 0, 25)>;
<NRF_PSEL(QSPI_IO3, 0, 23)>;
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 = <NRF_PSEL(QSPI_CSN, 0, 25)>;
low-power-enable;
bias-pull-up;
};
};
};
@@ -58,9 +58,15 @@
<NRF_PSEL(QSPI_IO0, 0, 20)>,
<NRF_PSEL(QSPI_IO1, 0, 24)>,
<NRF_PSEL(QSPI_IO2, 0, 22)>,
<NRF_PSEL(QSPI_IO3, 0, 23)>,
<NRF_PSEL(QSPI_CSN, 0, 25)>;
<NRF_PSEL(QSPI_IO3, 0, 23)>;
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 = <NRF_PSEL(QSPI_CSN, 0, 25)>;
low-power-enable;
bias-pull-up;
};
};
};
+5
View File
@@ -14,6 +14,7 @@
#endif
#include <helpers/MeshTimeSync.h>
#include <helpers/LoopWakeStats.h>
extern "C" void power_diag_line(int n, char *out, size_t cap); /* TEMPORARY: helpers/power_diag.c */
#include <helpers/time_sync.h>
#include <helpers/TxtDataHelpers.h>
#include <helpers/AdvertDataHelpers.h>
@@ -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));
+383
View File
@@ -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 <n>`). 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 <zephyr/kernel.h>
#include <stdio.h>
#include <string.h>
#if IS_ENABLED(CONFIG_ZEPHCORE_POWER_DIAG) && defined(CONFIG_SOC_NRF52840)
#include <cmsis_core.h>
#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) ? &REG(0x50000700UL + 4u * (n - 100))
: &REG(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