mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-10-15 16:49:54 +00:00
ESP32 light sleep (devdocs/lld/13-power-management.md)
Light sleep on the S3 repeaters never worked as designed, for two
independent reasons:
- No S3 repeater ever slept. Every one carries WiFi OTA, the WiFi
driver starts WiFi at boot (NULL mode), and a started WiFi library
holds its modem PM lock. Measured on the XIAO: zero sleep entries
with WiFi compiled in. helpers/pm_esp32_wake.c now stops WiFi at
boot on PM builds; `start ota` and a station connect start it again
(verified: AP up, sleep blocked for the session, sleeping after
`stop ota`).
- DIO1 was not a wake source. GPIO_INT_WAKEUP means EXT1 in Zephyr's
ESP32 driver: RTC pads only, one trigger polarity for all pins on
the S3, and pads left on the RTC mux after every sleep. Since 1.17.1
pm_esp32.overlay also armed the active-low user button; gpio-keys
configures first, so the driver refused the active-high DIO1.
The new helper arms the radio IRQ line and the user button (sw0) as
per-pin level GPIO wakes (ESP-IDF gpio_wakeup_enable, any pad) for the
length of each sleep, masks their CPU interrupt while in level mode,
and re-raises the interrupt a sleep swallowed through status_w1ts, so
the radio's dio1_isr and gpio-keys run as usual. EXT1 is no longer
used: patch 0003 configures DIO1 as a plain input again and drops the
d7cc4cb pad-restore notifier (preamble section rewritten). Non-RTC
DIO1 boards (XIAO GPIO39, Station G2 GPIO48) can now light-sleep.
Each light sleep is also bounded by the next esp_timer alarm
(pm_policy_next_custom_ticks, CONFIG_PM_CUSTOM_TICKS_HOOK), as ESP-IDF
does; the BLE controller's modem-sleep wake timer lives there.
Light sleep isolates every pad that is not held, RTC or digital, while
the radio keeps receiving. Holds (ESP32_GPIO_SLEEP_HOLD_EN, inert
without PM) now live in each board's own files: NSS and RESET on all
PM-capable boards, VFEM_Ctrl (GPIO7) and Vext on V4/V4.3/V4 R8/WTv2,
CPS on V4, RXEN on the XIAO. The shared overlay keeps only &rtc_timer
and the GPIO wake source; the wrong RTC_PERIPH explanations are gone.
Also:
- `powersaving` (upstream's command and replies) gates light sleep;
light-sleep builds default to on, and a stored 0 without the new
pwr_sav_set marker (the field did nothing before) becomes on once.
`powersaving off` persists and keeps the node (and its USB) awake.
- `get pm`: asleep %, sleeps, wake causes, per-pin wake counts.
- The console window reopens on every user-button press.
- XIAO ESP32-S3: GPIO21 is both the kit's user button and the board's
led0; the heartbeat drove it low and every sleep woke as a button
press. led0 is deleted, the button gets a pull-up.
- Station G2: button active-low with pull-up, as upstream reads it
(was active-high). Untested on hardware.
Verified on the XIAO ESP32-S3 as a light-sleep repeater on the live
mesh (repeat off, 2 dBm) against a T1000-E on the same channel: awake
20 min, ratio of floods heard 0.95; asleep 30.5 min (96.6 % of
uptime), 119 vs 119, ratio 1.00. TX: sent == flood + direct. Heltec
boards: build-only (no unit on the bench).
Companion light sleep stays off by default; the esp_timer bound makes
it possible. Spike on the XIAO (pm_esp32.conf by hand, BLE only): a
held connection survives, 35 min with a poll every minute and no
miss, the SoC asleep ~91 % between connection events, LoRa RX 130 vs
~130 on the T1000-E. Pairing while asleep fails; USB CDC and UI still
need locks before companions can ship with it.
Flash wear (storage)
The companion marked contacts dirty on is_new, but upstream's is_new
means "not in contacts[]": auto-add declined the node (type filter,
hop limit, full table). Every advert from such a node forced a 5 s
full rewrite of an unchanged contacts3, while a contact that really
was just added only got the liveness deadline. onAdvertRecv now
snapshots the stored record first. is_new never dirties. An addition
or a changed name, type or position flushes in 5 s, and a plain
re-advert waits the liveness deadline, now 1 h instead of 10 min.
A new weak hook, zephcore_persist_before_off(), flushes the deferred
writes before every clean reboot and power-off. It is called from
zephcore_power_off(), the ZephyrBoard reboot, bootloader and BLE-OTA
paths, the UI save-and-restart, the joystick restart and wifi_ota.
Off the main thread it posts MESH_EVENT_PERSIST and waits up to 5 s.
Before this, only CMD_REBOOT flushed.
The repeater saved its ACL on every admin login. It now saves only
when the role or secret changed; the secret is the ECDH of two fixed
keys, so re-logins store nothing new. The room server likewise skips
the lazy write unless the role, sync_since or secret changed.
zephcore_prefs_json_save() compares the serialised JSON with the file
and skips an identical rewrite. Not tested on hardware.
Builds and tests: role_builds.sh 10/10; the ESP32 PM set on the
combined tree (five Heltec repeaters, V4 R8, Station G2, XIAO repeater
and companion) 9/9, repeaters at 69-71 % dram0; board_manifest check
clean; host tests quick 114/114. heltec_companion dram0 at 99.80 %
(776 B left, +88 B for both changes together).
122 lines
4.3 KiB
C
122 lines
4.3 KiB
C
/*
|
|
* SPDX-License-Identifier: MIT
|
|
* ESP32 light-sleep console guards.
|
|
*
|
|
* Two independent problems sit between ESP32 light sleep and a usable serial
|
|
* console. This file solves both; neither fix substitutes for the other.
|
|
*
|
|
* 1. TRUNCATED OUTPUT. soc/espressif/common/power.c calls
|
|
* esp_light_sleep_start() with no wait for the console UART to drain — the
|
|
* TX-idle flush that ESP-IDF performs lives behind its own console Kconfig,
|
|
* which is not part of a Zephyr build. Sleeping with bytes still in the
|
|
* FIFO cuts the line mid-character. A pm_notifier's state_entry callback
|
|
* runs immediately before pm_state_set() (subsys/pm/pm.c), which is exactly
|
|
* the right moment to poll the UART to idle.
|
|
*
|
|
* 2. UNREACHABLE INPUT. Nothing arms a UART wake source — Zephyr's PM path
|
|
* never calls esp_sleep_enable_uart_wakeup(), and on the boards this runs on
|
|
* the console RX pad (GPIO44 on an S3 uart0) is outside the RTC range that
|
|
* could carry a GPIO wake instead. Characters typed at a sleeping node are
|
|
* therefore dropped, and no amount of TX handling changes that.
|
|
*
|
|
* The answer here is a console window rather than a wake source: sleep is
|
|
* blocked outright for ZEPHCORE_PM_BOOT_AWAKE_MS after boot, and again
|
|
* after every press of the user button (helpers/pm_esp32_wake.c), so a
|
|
* node is reachable for that long whenever someone is standing at it.
|
|
* On Heltec V3 a terminal that asserts DTR on open also resets the board
|
|
* through its USB bridge, which opens the window too. Boards that console
|
|
* on the S3's own USB Serial/JTAG (V4, V4.3, Wireless Tracker V2, XIAO,
|
|
* Station G2) do not reset on open, and their USB device detaches on every
|
|
* sleep; the button is the way back in, or `powersaving off` over LoRa.
|
|
*
|
|
* Remote admin over LoRa is unaffected by any of this: the radio's IRQ line
|
|
* wakes the SoC on a received packet (helpers/pm_esp32_wake.c).
|
|
*/
|
|
|
|
#include <zephyr/kernel.h>
|
|
#include <zephyr/init.h>
|
|
#include <zephyr/pm/pm.h>
|
|
|
|
#include <esp_rom_serial_output.h>
|
|
|
|
#include "pm_sleep_guard.h"
|
|
|
|
#include <zephyr/logging/log.h>
|
|
LOG_MODULE_REGISTER(zephcore_pm, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL);
|
|
|
|
/* ========== 1. Flush the console before every sleep ========== */
|
|
|
|
static void pm_console_flush(enum pm_state state)
|
|
{
|
|
if (state != PM_STATE_STANDBY) {
|
|
return;
|
|
}
|
|
|
|
/* Polls a status register until the shifter empties — no locking, so it
|
|
* is safe in this context (called with the scheduler locked). Bounded
|
|
* by the FIFO depth at the configured baud: ~11 ms worst case for a full
|
|
* 128-byte FIFO at 115200, and normally microseconds. */
|
|
esp_rom_output_tx_wait_idle(CONFIG_ZEPHCORE_PM_CONSOLE_UART_NUM);
|
|
}
|
|
|
|
static struct pm_notifier console_notifier = {
|
|
.state_entry = pm_console_flush,
|
|
};
|
|
|
|
/* ========== 2. Keep the node awake for a console window ========== */
|
|
|
|
/* 1 while the window holds its sleep lock. The lock is taken only on the
|
|
* 0 -> 1 edge and released only on 1 -> 0, so re-opening an open window just
|
|
* moves its deadline. */
|
|
static atomic_t window_locked;
|
|
|
|
static void console_window_expired(struct k_work *work)
|
|
{
|
|
ARG_UNUSED(work);
|
|
|
|
if (atomic_cas(&window_locked, 1, 0)) {
|
|
zc_pm_unblock_sleep();
|
|
LOG_INF("console window closed — light sleep enabled "
|
|
"(press the user button to reopen it)");
|
|
}
|
|
}
|
|
|
|
static K_WORK_DELAYABLE_DEFINE(console_window_work, console_window_expired);
|
|
|
|
void zc_pm_console_window_open(void)
|
|
{
|
|
#if CONFIG_ZEPHCORE_PM_BOOT_AWAKE_MS > 0
|
|
if (atomic_cas(&window_locked, 0, 1)) {
|
|
zc_pm_block_sleep();
|
|
}
|
|
k_work_reschedule(&console_window_work,
|
|
K_MSEC(CONFIG_ZEPHCORE_PM_BOOT_AWAKE_MS));
|
|
#endif
|
|
}
|
|
|
|
uint32_t zc_pm_console_window_remaining_ms(void)
|
|
{
|
|
if (!atomic_get(&window_locked)) {
|
|
return 0;
|
|
}
|
|
return (uint32_t)k_ticks_to_ms_floor64(
|
|
k_work_delayable_remaining_get(&console_window_work));
|
|
}
|
|
|
|
static int pm_console_init(void)
|
|
{
|
|
pm_notifier_register(&console_notifier);
|
|
|
|
/* Opened here, at POST_KERNEL, so there is no stretch at startup in
|
|
* which the node could sleep before the guard is in place. */
|
|
zc_pm_console_window_open();
|
|
LOG_INF("light sleep deferred %d ms (console configuration window)",
|
|
CONFIG_ZEPHCORE_PM_BOOT_AWAKE_MS);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* POST_KERNEL: needs the kernel work queue, and must be in place before the
|
|
* application can idle long enough to sleep. */
|
|
SYS_INIT(pm_console_init, POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT);
|