ESP power saving p1

This commit is contained in:
liquidraver
2026-07-30 09:33:03 +02:00
parent 3e534e6239
commit ab4ea6d87c
12 changed files with 245 additions and 48 deletions
+27
View File
@@ -342,6 +342,33 @@ endif()
# ESP32's DRAM by ~10KB. Rather than trim WiFi/heap to an unverifiable margin,
# the classic ESP32 repeater stays CLI-only on simple boot (handled in build.sh).
# The ESP32-S3/C-series have the DRAM headroom and keep WiFi OTA.
# ========== ESP32 CPU clock ==========
# Espressif parts idle in the tens of mA (WAITI gates the core clock but leaves
# PLL/peripherals/RAM powered), so CPU frequency is a first-order term in a
# battery node's draw — unlike nRF52, which already idles at microamps. Pull
# every ESP32 build down to 80 MHz, matching what Arduino MeshCore ships.
# Needs patches/modules/hal-espressif/0002 (upstream omits 80 from its DT map).
#
# Observers are the exception and keep the SoC default (240 MHz on S3/classic,
# 160 MHz on C3/C6): they run a full WiFi + TLS + MQTT stack and are mains- or
# solar-with-a-big-panel powered, so throughput matters more than current.
# Implemented as "don't apply the overlay" rather than an override, so the
# per-SoC maximum is inherited instead of hardcoded here.
if(ZEPHCORE_PLATFORM_CONF MATCHES "esp32_common" AND NOT EXTRA_CONF_FILE MATCHES "observer")
set(ZEPHCORE_ESP32_CPU_OVERLAY
"${CMAKE_CURRENT_SOURCE_DIR}/boards/common/esp32_cpu_80mhz.overlay")
if(EXISTS ${ZEPHCORE_ESP32_CPU_OVERLAY})
if(EXTRA_DTC_OVERLAY_FILE)
set(EXTRA_DTC_OVERLAY_FILE
"${EXTRA_DTC_OVERLAY_FILE};${ZEPHCORE_ESP32_CPU_OVERLAY}" CACHE STRING "" FORCE)
else()
set(EXTRA_DTC_OVERLAY_FILE
"${ZEPHCORE_ESP32_CPU_OVERLAY}" CACHE STRING "" FORCE)
endif()
message(STATUS " ESP32 CPU: 80 MHz (observers keep the SoC maximum)")
endif()
endif()
if(ZEPHCORE_PLATFORM_CONF MATCHES "esp32_common" AND EXTRA_CONF_FILE MATCHES "repeater"
AND NOT BOARD MATCHES "/esp32/")
set(ZEPHCORE_WIFI_OTA_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/wifi_ota.conf")
@@ -0,0 +1,38 @@
/*
* SPDX-License-Identifier: MIT
* ESP32 family: run the CPU at 80 MHz.
*
* Espressif parts have no equivalent of the nRF52's "idle at microamps in
* WFI": WAITI gates the core clock but leaves the PLL, peripherals and RAM
* powered, so an idle ESP32 sits in the tens of mA and CPU frequency is a
* first-order term in that. Arduino MeshCore ships its ESP32 variants at
* 80 MHz for the same reason (ESP32_CPU_FREQ=80 in variants/heltec_v3,
* heltec_v4, heltec_tracker, rak3112, xiao_c3, ...).
*
* 80 requires patches/modules/hal-espressif/0002: upstream's DT-to-Kconfig map
* lists only the per-SoC defaults (96/120/160/240) and omits 80, so without it
* the build fails with "'CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ' undeclared". Do NOT
* reach for 96 to dodge that -- it is an ESP32-H2 frequency, rejected by
* rtc_clk_cpu_freq_mhz_to_config() on every SoC we build, and it fails at BOOT
* rather than at build time.
*
* The mesh workload is event-driven and nowhere near CPU-bound: wake on a
* packet, run some AES and LittleFS, go back to idle. Airtime dominates every
* latency that matters — the slowest presets spend seconds on air per packet.
*
* Only cpu0 is set, deliberately. clock_control_esp32.c reads exactly
* DT_INST(0, DT_CPU_COMPAT) — cpu0 — so a cpu1 property is never consulted,
* and omitting it lets this one file serve the dual-core Xtensa parts (S3,
* classic) and the single-core RISC-V parts (C3, C6) alike. Referencing
* &cpu1 here would be a dtc error on the latter.
*
* Applied automatically to non-observer ESP32 builds — see the ESP32 CPU
* clock block in CMakeLists.txt. Kept out of the per-board DTS files on
* purpose: one place to change, and it makes the observer exception a simple
* "don't include this" rather than an override that would have to know each
* SoC's maximum (240 MHz on S3/classic, 160 MHz on C3/C6).
*/
&cpu0 {
clock-frequency = <DT_FREQ_M(80)>;
};
+63
View File
@@ -0,0 +1,63 @@
# ESP32 system power management — light sleep between events.
#
# west build -b heltec_wifi_lora32_v3/esp32s3/procpu zephcore --pristine -- \
# -DEXTRA_CONF_FILE="boards/common/repeater.conf;boards/common/pm_esp32.conf"
#
# OPT-IN, and deliberately not enabled by default. Read this before shipping it.
#
# 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.
#
# REPEATERS ONLY. Do not pair this with a companion build. The Espressif HCI
# driver (drivers/bluetooth/hci/hci_esp32.c) takes no pm_policy locks, so
# nothing stops the SoC light-sleeping mid-advertising or mid-connection; the
# NXP driver does take them, Espressif's does not. repeater.conf sets
# CONFIG_BT=n, which removes the problem rather than papering over it.
#
# BOARD REQUIREMENT — DIO1 must be an RTC-capable GPIO. On ESP32-S3
# esp_sleep_is_valid_wakeup_gpio() reduces to RTC_GPIO_IS_VALID_GPIO(), i.e.
# GPIO 0-21. A board wiring DIO1 above that CANNOT wake on a received packet:
#
# OK heltec_wifi_lora32_v3 / v4 / v43, heltec_wireless_tracker(_v2)
# -> DIO1 = GPIO14
# NOT OK xiao_esp32s3 (GPIO39), station_g2 (GPIO48), thinknode_m9 (GPIO42)
#
# On a NOT-OK board the driver logs "Pin N is not wakeup capable" at boot and
# the node will light-sleep straight through inbound packets while looking
# excellent on a current meter. Check that log line before trusting any
# measurement.
#
# Also required and supplied by the paired pm_esp32.overlay:
# - &rtc_timer enabled. soc/espressif/common/power.c refuses to sleep without
# it, logging "Sleep skipped. Make sure '&rtc_timer' is enabled as a wakeup
# source." — i.e. CONFIG_PM=y alone silently does nothing.
# - &gpio0 as a wakeup-source, so the driver's PM resume hook calls
# esp_sleep_enable_gpio_wakeup().
# And patches/zephyr/0012 marks DIO1 itself with GPIO_INT_WAKEUP.
#
# 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.
#
# UNVALIDATED ON HARDWARE. Verify in this order: (1) no "not wakeup capable"
# line at boot, (2) the node still receives packets, (3) only then measure
# current. A deaf repeater is the expected failure and it is not obvious.
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
+36
View File
@@ -0,0 +1,36 @@
/*
* SPDX-License-Identifier: MIT
* Devicetree half of boards/common/pm_esp32.conf — auto-paired by
* zephcore_auto_pair_overlay() in CMakeLists.txt, so passing the .conf is
* enough; this file does not need to be listed separately.
*/
/*
* The RTC timer is the wake source for every timed sleep. Upstream leaves it
* status = "disabled" (zephyr/dts/xtensa/espressif/esp32s3/esp32s3_common.dtsi),
* and soc/espressif/common/power.c checks for it in rtc_wakeup_enable():
* without a ready rtc_timer device it logs "Sleep skipped. Make sure
* '&rtc_timer' is enabled as a wakeup source." and returns false — so
* CONFIG_PM=y on its own is a silent no-op. This is the single easiest thing
* to forget when enabling ESP32 PM.
*/
&rtc_timer {
status = "okay";
wakeup-source;
};
/*
* Arms the GPIO wake path. gpio_esp32_pm_action() calls
* esp_sleep_enable_gpio_wakeup() on PM_DEVICE_ACTION_RESUME, but only when the
* PORT is wakeup-capable — that is what pm_device_wakeup_is_capable() tests,
* and it is set by this property. Per-PIN arming is separate and comes from
* patches/zephyr/0012, which passes GPIO_INT_WAKEUP on the DIO1 configure.
* Both halves are needed: the port enables the wake source, the pin selects
* which line triggers it.
*
* gpio0 covers GPIO 0-31, which is where every RTC-capable pin lives on the
* S3 (0-21) — so this is also the only port that can carry a LoRa DIO1 wake.
*/
&gpio0 {
wakeup-source;
};
@@ -166,14 +166,6 @@
};
/* Reduce CPU clock from 240 MHz to 160 MHz — saves power, HAL minimum */
&cpu0 {
clock-frequency = <DT_FREQ_M(160)>;
};
&cpu1 {
clock-frequency = <DT_FREQ_M(160)>;
};
/* I2C sensors — auto-detected at runtime */
&i2c0 {
#include "../../common/sensors-i2c.dtsi"
@@ -164,14 +164,6 @@
};
/* Reduce CPU clock from 240 MHz to 160 MHz — saves power, HAL minimum */
&cpu0 {
clock-frequency = <DT_FREQ_M(160)>;
};
&cpu1 {
clock-frequency = <DT_FREQ_M(160)>;
};
&ledc0 {
pinctrl-0 = <&ledc0_default>;
pinctrl-names = "default";
@@ -167,14 +167,6 @@
};
/* Reduce CPU clock from 240 MHz to 160 MHz — saves power, HAL minimum */
&cpu0 {
clock-frequency = <DT_FREQ_M(160)>;
};
&cpu1 {
clock-frequency = <DT_FREQ_M(160)>;
};
&ledc0 {
pinctrl-0 = <&ledc0_default>;
pinctrl-names = "default";
@@ -214,14 +214,6 @@
status = "okay";
};
&cpu0 {
clock-frequency = <DT_FREQ_M(160)>;
};
&cpu1 {
clock-frequency = <DT_FREQ_M(160)>;
};
&i2c0 {
status = "okay";
clock-frequency = <I2C_BITRATE_FAST>;
@@ -41,14 +41,6 @@
};
/* Reduce CPU clock from 240 MHz to 160 MHz — HAL Kconfig minimum supported */
&cpu0 {
clock-frequency = <DT_FREQ_M(160)>;
};
&cpu1 {
clock-frequency = <DT_FREQ_M(160)>;
};
&usb_serial {
status = "okay";
};
@@ -66,14 +66,6 @@
* CPU-bound consumer. 240 also removes this board's only timing anomaly,
* which matters while the CPU-jitter entropy source is under investigation
* (jitter health check failed here — see memory/findings.md). */
&cpu0 {
clock-frequency = <DT_FREQ_M(240)>;
};
&cpu1 {
clock-frequency = <DT_FREQ_M(240)>;
};
/* USB Serial/JTAG stays disabled (SoC default): its PHY pads are GPIO19/20,
* and on this board GPIO20 is the keypad's I2C1 SDA. The USJ driver's init
* re-attaches the USB pad (usb_serial_jtag_ll_phy_enable_pad) and enables the
@@ -0,0 +1,38 @@
Add 80 MHz to the DT-derived CPU frequency mapping.
zephyr/Kconfig derives ESP_DEFAULT_CPU_FREQ_MHZ from the devicetree cpu0
clock-frequency via a lookup table listing 96/120/160/240. Those four values
are exactly the per-SoC DEFAULTS in Zephyr's own espressif dtsi files (H2 96,
C2 120, C3/C6 160, esp32/C5/S2/S3 240) -- the table enumerates defaults, not
capabilities, so a board that deliberately downclocks falls through it and the
symbol resolves to nothing:
clk.c:120: error: 'CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ' undeclared
sleep_modes.c:235: error: 'CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ' undeclared
80 MHz is valid on every part we build -- rtc_clk.c accepts 80/160/240 on
esp32 and esp32s3, 80/160 on C3, 80/120/160 on C6 -- and Espressif's own IDF
Kconfig.cpu offers ESP_DEFAULT_CPU_FREQ_MHZ_80. This is a gap in the Zephyr
port glue, not a limitation of the silicon or of IDF.
ZephCore runs every non-observer ESP32 build at 80 MHz for battery life
(boards/common/esp32_cpu_80mhz.overlay).
Do NOT 'fix' this by selecting 96 instead: 96 is in the table but is an
ESP32-H2 frequency, rejected by rtc_clk_cpu_freq_mhz_to_config() on all of our
SoCs, and it fails at BOOT rather than at build time.
Upstream candidate: the omission looks accidental.
---
diff --git a/zephyr/Kconfig b/zephyr/Kconfig
index e9fb2b90a8..f8793f5d59 100644
--- a/zephyr/Kconfig
+++ b/zephyr/Kconfig
@@ -105,6 +105,7 @@ config ESP_SYSTEM_RTC_EXT_XTAL_BOOTSTRAP_CYCLES
# CPU frequency in MHz derived from devicetree clock-frequency property
config ESP_DEFAULT_CPU_FREQ_MHZ
int
+ default 80 if ESP_CLK_FREQ_HZ = 80000000
default 96 if ESP_CLK_FREQ_HZ = 96000000
default 120 if ESP_CLK_FREQ_HZ = 120000000
default 160 if ESP_CLK_FREQ_HZ = 160000000
@@ -0,0 +1,43 @@
Mark DIO1 as a system wakeup source.
Without this, enabling CONFIG_PM on an ESP32 gives a node that light-sleeps
correctly and is deaf: the SX1262 asserts DIO1 on a received packet, but the
SoC is not armed to wake on that pin, so the frame is dropped and the CPU only
resumes on the next RTC timer deadline. The failure is silent and looks like a
success on a current meter, which is the worst possible shape for it.
The Zephyr ESP32 GPIO driver arms the wake in its CONFIGURE path --
gpio_esp32_config() reads (flags & GPIO_INT_WAKEUP) at drivers/gpio/gpio_esp32.c
and calls esp_sleep_enable_ext1_wakeup_io() -- not in the interrupt path, which
strips the bit. So the flag has to go on gpio_pin_configure_dt(), not on the
gpio_pin_interrupt_configure_dt() call below it.
GPIO_INT_WAKEUP is deliberately NOT part of GPIO_INT_MASK (see
include/zephyr/drivers/gpio.h), precisely so it can be passed to
gpio_pin_configure() without tripping its "Interrupt flags are not supported"
assert. Drivers that do not implement wakeup ignore the bit, so this stays a
no-op on nRF/STM32/MG24 rather than needing a per-platform guard.
Board caveat, not fixable here: on ESP32-S3 esp_sleep_is_valid_wakeup_gpio()
reduces to RTC_GPIO_IS_VALID_GPIO(), i.e. GPIO 0-21 only. Boards wiring DIO1
above that (xiao_esp32s3 GPIO39, station_g2 GPIO48, thinknode_m9 GPIO42) log
"Pin N is not wakeup capable" and cannot use light sleep with LoRa RX at all.
Heltec V3/V4/V43 and the Wireless Trackers put DIO1 on GPIO14 and are fine.
--- a/drivers/lora/native/sx126x/sx126x_hal.c
+++ b/drivers/lora/native/sx126x/sx126x_hal.c
@@ -150,7 +150,13 @@
LOG_ERR("DIO1 GPIO not ready");
return -ENODEV;
}
- ret = gpio_pin_configure_dt(&config->dio1, GPIO_INPUT);
+ /* GPIO_INT_WAKEUP: let a DIO1 assertion wake the SoC from a system
+ * low-power state, so an inbound packet is not slept through when
+ * CONFIG_PM is enabled. Outside GPIO_INT_MASK by design, so this is
+ * legal here and ignored by drivers without wakeup support.
+ */
+ ret = gpio_pin_configure_dt(&config->dio1,
+ GPIO_INPUT | GPIO_INT_WAKEUP);
if (ret < 0) {
LOG_ERR("Failed to configure DIO1 GPIO: %d", ret);
return ret;