mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-08-29 14:58:16 +00:00
ESP power saving p1
This commit is contained in:
@@ -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)>;
|
||||
};
|
||||
@@ -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
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user