Files
ZephCore/zephcore/boards/common/pm_esp32.conf
T
liquidraver d2d786517b power & storage: ESP32 light sleep that sleeps and wakes; less flash wear
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).
2026-09-26 16:49:17 +02:00

80 lines
4.5 KiB
Plaintext

# ESP32 system power management — light sleep between events.
#
# AUTO-ENABLED for repeater builds on boards whose zephcore.yml declares
# `light_sleep: true` (cmake/zephcore_board.cmake, CMakeLists.txt). Passing it
# by hand is supported for testing another board:
#
# west build -b xiao_esp32s3/esp32s3/procpu zephcore --pristine --sysbuild -- # -DEXTRA_CONF_FILE="boards/common/repeater.conf;boards/common/pm_esp32.conf"
#
# What it buys: Espressif parts have no equivalent of the nRF52's "idle at
# microamps in WFI" — WAITI gates the core clock but leaves PLL, peripherals
# and RAM powered, so an idle ESP32 sits in the tens of mA. Light sleep is
# sub-mA. On a repeater that is the difference between the MCU dominating the
# power budget and the radio dominating it.
#
# THE PIECES
# - pm_esp32.overlay: &rtc_timer as the timed wake source (without it power.c
# logs "Sleep skipped" and CONFIG_PM silently does nothing) and &gpio0
# wakeup-source, which turns the ESP32 GPIO wake source on.
# - helpers/pm_esp32_wake.c: the LoRa IRQ line and the user button (sw0)
# become level wakes on any pad for the length of each sleep, and the
# interrupt a sleep swallowed is re-raised on wake. Also the `powersaving`
# switch and the `get pm` counters. GPIO_INT_WAKEUP (EXT1) is NOT used:
# RTC pads only, one polarity for all pins on the S3, pads left on the RTC
# mux after a sleep (devdocs/lld/13-power-management.md, F-1/F-2).
# - helpers/pm_esp32_console.c: flushes the console UART before every sleep,
# and holds a console window (ZEPHCORE_PM_BOOT_AWAKE_MS, 10 min) after boot
# and after every button press.
# - Pin holds, in each board's DTS/overlay: light sleep isolates every pad
# that is not held (input and output off, no pulls), RTC pad or not, while
# the radio keeps receiving. NSS, RESET and any MCU-driven RF switch, FEM
# enable or FEM supply carry ESP32_GPIO_SLEEP_HOLD_EN (inert without PM).
#
# CONSOLE: outside the console window, characters typed at a sleeping node are
# dropped. On boards that console over the S3's USB Serial/JTAG (V4, V4.3,
# Wireless Tracker V2, XIAO, Station G2) the HAL disables the USJ pad for every
# sleep, so the USB device detaches and reattaches continuously. Press the user
# button to reopen the window, or send `powersaving off` (persisted) over LoRa;
# `powersaving on` allows sleep again. Light-sleep builds default to on.
#
# WiFi: the ESP32 WiFi driver starts WiFi at boot (NULL mode), and a started
# WiFi library holds its modem lock, so NO S3 repeater (they all carry WiFi OTA)
# ever light-slept before 2026-09-26. helpers/pm_esp32_wake.c stops WiFi at
# boot; `start ota` (AP) and a station connect start it again. While WiFi is in
# use a pm_policy lock is held — the OTA session for its duration
# (adapters/ota/wifi_ota.c), an uplink or observer station permanently
# (adapters/wifi/ZephyrWiFiStation.c).
#
# REPEATERS ONLY for now. Companions keep BLE up; making the controller's
# modem sleep coexist with system light sleep is the open question in LLD 13.
#
# CHECKING A BOARD: `get pm` shows the share of time asleep, the sleep count and
# wake causes (t = RTC timer, g = GPIO, o = other) and per-pin wake counts.
# A deaf node is the failure to rule out first: radio wakes should track the
# packets it hears, and its receive counters should track a non-sleeping
# neighbour's.
#
# Kernel time is safe across sleep: the Xtensa/ESP32 timer drivers implement
# the LPM hooks (esp32_lptim_hook_on_lpm_entry/exit) that read the RTC counter
# and announce elapsed ticks, so k_uptime_get() and every maintenance deadline
# stay correct.
CONFIG_PM=y
# The RTC timer is a COUNTER driver. Enabling the DT node alone is not enough:
# without the counter subsystem drivers/counter is never built, no device
# object is emitted, and power.c's DEVICE_DT_GET_OR_NULL(rtc_timer) fails to
# link ("undefined reference to __device_dts_ord_NN"). COUNTER_RTC_ESP32 is
# then default y off the enabled DT node.
CONFIG_COUNTER=y
# System-managed device PM stays off. prj.conf pins it off globally so that
# turning on CONFIG_PM cannot silently start suspending devices from the idle
# thread — that is the trap that historically killed GPS. Device suspends stay
# explicit (pm_device_action_run from the main thread).
CONFIG_PM_DEVICE_SYSTEM_MANAGED=n
# Bound each light sleep by the next esp_timer alarm as well as the kernel's
# next timeout (helpers/pm_esp32_wake.c, pm_policy_next_custom_ticks()).
CONFIG_PM_CUSTOM_TICKS_HOOK=y