Files
2026-10-22 14:01:22 +02:00

349 lines
11 KiB
C

/*
* SPDX-License-Identifier: MIT
* ESP32 light sleep: wake pins, the `powersaving` switch and `get pm`.
*
* WHY NOT EXT1. Zephyr's ESP32 GPIO driver turns GPIO_INT_WAKEUP into an EXT1
* wake, and that failed three ways (devdocs/lld/13-power-management.md, F-1/F-2):
* EXT1 takes RTC pads only (GPIO 0-21 on the S3, so not the XIAO's DIO1 on
* GPIO39); on the S3 it has a single trigger polarity for all its pins, so an
* active-low button and an active-high DIO1 cannot both be armed — the button,
* configured first, won and DIO1 was silently refused; and before every sleep
* the HAL moves each EXT1 pad to the RTC mux and nothing moves it back.
*
* WHAT THIS DOES INSTEAD. ESP-IDF's GPIO wake (gpio_wakeup_enable()) works on
* any pad in light sleep, each pin with its own level. The LoRa IRQ line and
* the user button (DT alias sw0) become level wakes for the length of each
* sleep and go back to their normal edge interrupts afterwards:
*
* entry (IRQs locked): save the pin's interrupt type and enable, mask its CPU
* interrupt, arm a level wake at the pin's active level, keep the pad's
* input live through the sleep (sleep-select off).
* exit (IRQs still locked; the idle thread unlocks them after the kernel
* clock is corrected): disarm, restore the saved type, clear the status
* the level mode latched, unmask, and if the pin is at its active level,
* re-raise its interrupt through status_w1ts.
*
* The pin's interrupt stays masked while it is in level mode: a level
* interrupt on a line the radio holds high until ClearIrqStatus would storm
* if interrupts ran before the type is restored. Since the 2026-10-22 Zephyr
* pin they do not (LLD 13 section 14); the mask keeps that true on a Zephyr
* version whose post_ops re-enables them. The re-raise is the missed edge: the level that
* woke us was reached while the pin was in level mode, so no edge will come.
* The driver's own ISR then runs the normal callbacks (the radio's dio1_isr,
* gpio-keys) once interrupts are unlocked. A duplicate is harmless: both only
* queue work that re-reads the hardware.
*
* The ESP32 GPIO wake source itself (ESP_SLEEP_WAKEUP_GPIO) is switched on by
* `&gpio0 { wakeup-source; }` in boards/common/pm_esp32.overlay.
*/
#include <zephyr/kernel.h>
#include <zephyr/init.h>
#include <zephyr/device.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/pm/pm.h>
#include <zephyr/sys/util.h>
#include <driver/gpio.h>
#include <hal/gpio_ll.h>
#include <soc/gpio_struct.h>
#include <esp_sleep.h>
#include <esp_timer.h>
#include <soc/soc_caps.h>
#if defined(CONFIG_WIFI_ESP32)
#include <esp_wifi.h>
#include <esp_private/wifi.h>
#endif
/* The WiFi MAC keeps its own beacon timer (TSF) and wakes the SoC with it. */
#if defined(CONFIG_WIFI_ESP32) && SOC_WIFI_HW_TSF
#define ZC_PM_WIFI_TSF 1
#endif
#include <stdio.h>
#include <string.h>
#include "pm_sleep_guard.h"
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(zephcore_pm_wake, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL);
/* ========== Wake pins from devicetree ========== */
#define ZC_LORA_NODE DT_ALIAS(lora0)
#if DT_NODE_HAS_PROP(ZC_LORA_NODE, dio1_gpios)
#define ZC_WAKE_RADIO_PROP dio1_gpios
#elif DT_NODE_HAS_PROP(ZC_LORA_NODE, irq_gpios)
#define ZC_WAKE_RADIO_PROP irq_gpios
#endif
#define ZC_BTN_NODE DT_ALIAS(sw0)
#define ZC_WAKE_HAS_BUTTON DT_NODE_HAS_PROP(ZC_BTN_NODE, gpios)
struct wake_pin {
const char *name;
struct gpio_dt_spec spec;
uint8_t num; /* SoC GPIO number, 0-48 */
bool active_low;
uint8_t saved_type;
uint8_t saved_ena;
uint32_t hits; /* sleeps that ended with this pin active */
};
static struct wake_pin wake_pins[] = {
#ifdef ZC_WAKE_RADIO_PROP
{ .name = "radio", .spec = GPIO_DT_SPEC_GET(ZC_LORA_NODE, ZC_WAKE_RADIO_PROP) },
#endif
#if ZC_WAKE_HAS_BUTTON
{ .name = "btn", .spec = GPIO_DT_SPEC_GET(ZC_BTN_NODE, gpios) },
#endif
};
/* ========== Counters (`get pm`) ========== */
static struct {
uint32_t entries; /* times the idle path chose light sleep */
uint32_t slept; /* ... and the SoC actually slept */
uint32_t wake_timer;
uint32_t wake_gpio;
uint32_t wake_wifi; /* the WiFi MAC, for a beacon */
uint32_t wake_other;
uint64_t asleep_us;
} stats;
static int64_t entry_us;
/* power.c skips the sleep (and returns at once) when the time left to the
* kernel deadline is under min-residency after its own lead time; a real light
* sleep always lasts longer than this. Only used to tell the two apart for
* the counters. */
#define SLEPT_MIN_US 500
/* ========== Sleep entry / exit ========== */
static void wake_state_entry(enum pm_state state)
{
if (state != PM_STATE_STANDBY) {
return;
}
stats.entries++;
entry_us = esp_timer_get_time();
ARRAY_FOR_EACH_PTR(wake_pins, p) {
p->saved_type = GPIO.pin[p->num].int_type;
p->saved_ena = GPIO.pin[p->num].int_ena;
GPIO.pin[p->num].int_ena = 0;
(void)gpio_wakeup_enable(p->num, p->active_low ? GPIO_INTR_LOW_LEVEL
: GPIO_INTR_HIGH_LEVEL);
gpio_ll_sleep_sel_dis(&GPIO, p->num);
}
}
static void wake_state_exit(enum pm_state state)
{
if (state != PM_STATE_STANDBY) {
return;
}
int64_t dt = esp_timer_get_time() - entry_us;
bool slept = dt >= SLEPT_MIN_US;
uint32_t causes = slept ? esp_sleep_get_wakeup_causes() : 0;
if (slept) {
stats.slept++;
stats.asleep_us += (uint64_t)dt;
if (causes & BIT(ESP_SLEEP_WAKEUP_GPIO)) {
stats.wake_gpio++;
} else if (causes & BIT(ESP_SLEEP_WAKEUP_TIMER)) {
stats.wake_timer++;
} else if (causes & BIT(ESP_SLEEP_WAKEUP_WIFI)) {
stats.wake_wifi++;
} else {
stats.wake_other++;
}
}
ARRAY_FOR_EACH_PTR(wake_pins, p) {
uint32_t bit = BIT(p->num & 31);
(void)gpio_wakeup_disable(p->num);
gpio_ll_sleep_sel_en(&GPIO, p->num);
GPIO.pin[p->num].int_type = p->saved_type;
if (p->num < 32) {
GPIO.status_w1tc = bit;
} else {
GPIO.status1_w1tc.val = bit;
}
GPIO.pin[p->num].int_ena = p->saved_ena;
bool active = gpio_ll_get_level(&GPIO, p->num) == (p->active_low ? 0 : 1);
if (!active) {
continue;
}
if (slept) {
p->hits++;
}
if (p->saved_ena != 0 && p->saved_type != 0) {
if (p->num < 32) {
GPIO.status_w1ts = bit;
} else {
GPIO.status1_w1ts.val = bit;
}
}
}
}
static struct pm_notifier wake_notifier = {
.state_entry = wake_state_entry,
.state_exit = wake_state_exit,
};
/* ========== Button: re-open the console window ========== */
#if ZC_WAKE_HAS_BUTTON
static struct gpio_callback button_cb;
static void button_pressed(const struct device *port, struct gpio_callback *cb,
gpio_port_pins_t pins)
{
ARG_UNUSED(port);
ARG_UNUSED(cb);
ARG_UNUSED(pins);
zc_pm_console_window_open();
}
#endif
/* ========== powersaving ========== */
static atomic_t powersave_blocked;
void zc_pm_set_powersaving(bool on)
{
if (on) {
if (atomic_cas(&powersave_blocked, 1, 0)) {
zc_pm_unblock_sleep();
}
} else if (atomic_cas(&powersave_blocked, 0, 1)) {
zc_pm_block_sleep();
}
}
int zc_pm_format_stats(char *buf, size_t len)
{
uint64_t up_ms = (uint64_t)k_uptime_get();
uint32_t pct10 = up_ms ? (uint32_t)(stats.asleep_us / up_ms) : 0; /* 0.1 % units */
uint32_t win_s = zc_pm_console_window_remaining_ms() / 1000U;
int n = snprintf(buf, len,
"> %s asleep %u.%u%% sleeps %u/%u wake t%u g%u w%u o%u",
atomic_get(&powersave_blocked) ? "off" : "on",
pct10 / 10U, pct10 % 10U,
stats.slept, stats.entries,
stats.wake_timer, stats.wake_gpio, stats.wake_wifi, stats.wake_other);
ARRAY_FOR_EACH_PTR(wake_pins, p) {
if (n > 0 && (size_t)n < len) {
n += snprintf(buf + n, len - n, " %s%u", p->name, p->hits);
}
}
if (n > 0 && (size_t)n < len && win_s > 0) {
n += snprintf(buf + n, len - n, " win %us", win_s);
}
return (n > 0 && (size_t)n < len) ? n : (int)strlen(buf);
}
/* ========== WiFi station ========== */
#if defined(ZC_PM_WIFI_TSF)
static atomic_t wifi_station_started;
#endif
void zc_pm_wifi_station_started(void)
{
#if defined(ZC_PM_WIFI_TSF)
atomic_set(&wifi_station_started, 1);
#endif
}
/* ========== esp_timer deadlines bound the sleep ========== */
#if defined(CONFIG_PM_CUSTOM_TICKS_HOOK)
/* power.c sizes each light sleep from the kernel's next timeout only. Work
* scheduled on esp_timer (its own systimer alarm, which does not wake the SoC)
* would otherwise be slept through — the BLE controller's modem-sleep wake
* timer ("btSlp") is the one that matters: miss it and the controller misses
* its connection event. ESP-IDF's own tickless idle bounds the sleep the same
* way (esp_pm/pm_impl.c).
*
* A WiFi station in power save gives up the WiFi library's sleep lock between
* beacons a moment before the MAC's beacon timer (TSF) goes idle. No sleep in
* that moment, as in ESP-IDF: 0 ticks means "an event is due now"
* (devdocs/lld/13-power-management.md, section 12). */
int64_t pm_policy_next_custom_ticks(void)
{
#if defined(ZC_PM_WIFI_TSF)
if (atomic_get(&wifi_station_started) && esp_wifi_internal_is_tsf_active()) {
return 0;
}
#endif
int64_t next = esp_timer_get_next_alarm_for_wake_up();
if (next == INT64_MAX) {
return -1;
}
int64_t delta = next - esp_timer_get_time();
if (delta <= 0) {
return 0;
}
return MIN((int64_t)k_us_to_ticks_floor64((uint64_t)delta), (int64_t)INT32_MAX);
}
#endif
/* ========== Init ========== */
static int pm_wake_init(void)
{
ARRAY_FOR_EACH_PTR(wake_pins, p) {
if (!gpio_is_ready_dt(&p->spec)) {
LOG_ERR("wake pin %s: port not ready", p->name);
return -ENODEV;
}
p->num = p->spec.pin;
#if DT_NODE_EXISTS(DT_NODELABEL(gpio1))
if (p->spec.port == DEVICE_DT_GET(DT_NODELABEL(gpio1))) {
p->num += 32;
}
#endif
p->active_low = (p->spec.dt_flags & GPIO_ACTIVE_LOW) != 0;
LOG_INF("light-sleep wake: %s GPIO%u (%s)", p->name, p->num,
p->active_low ? "low" : "high");
}
#if defined(CONFIG_WIFI_ESP32)
/* The ESP32 WiFi driver starts WiFi at boot, in NULL mode, "to enable
* coexistence". In NULL mode the WiFi radio stays on, the WiFi library
* holds its modem PM lock for as long as it is
* (pm_policy_state_all_lock_get via esp_pm) and the SoC never
* light-sleeps: measured on the XIAO repeater, zero sleep entries with
* WiFi compiled in, sleeps without it. Stop it here. Nothing is lost:
* the driver's AP enable (`start ota`) and STA connect (uplink, WiFi
* companion) both call esp_wifi_start() themselves. */
(void)esp_wifi_stop();
#endif
#if ZC_WAKE_HAS_BUTTON
gpio_init_callback(&button_cb, button_pressed, BIT(wake_pins[ARRAY_SIZE(wake_pins) - 1].spec.pin));
(void)gpio_add_callback(wake_pins[ARRAY_SIZE(wake_pins) - 1].spec.port, &button_cb);
#endif
pm_notifier_register(&wake_notifier);
return 0;
}
/* APPLICATION: after the radio and gpio-keys have configured their pins, so
* the interrupt types saved at the first sleep are theirs. */
SYS_INIT(pm_wake_init, APPLICATION, 0);