mirror of
https://github.com/meshtastic/firmware.git
synced 2026-11-07 14:54:44 -05:00
doLightSleep() skips a light sleep when the LoRa IRQ line is already asserted, and bounds that to LORA_IRQ_SKIP_SLEEP_MAX consecutive skips so a line stuck high cannot stop the node sleeping for good. But the counter was reset on the sleep it finally allowed, so the bound never stuck: a genuinely stuck-high line produced five skips, one sleep, five skips, one sleep, for as long as it stayed high. Each of those skipped attempts is not free. It runs waitEnterSleep() and the full notifyLightSleep / notifyLightSleepEnd observer pass, which detaches, arms and restores every wake pin - the path #12100 and #12131's interrupt storm live on - so the loop costs far more than the sleep it is protecting. Reset the counter only when the line is actually read low, which is the condition that says the radio is clearing its IRQs again. Nothing changes for the transient case the skip exists for: the line goes low as soon as the main loop services the packet, and the next sleep clears the count. Claude-Session: https://claude.ai/code/session_01GTiifXrVc4YdonmLRCeWhw Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
762 lines
28 KiB
C++
762 lines
28 KiB
C++
#include "configuration.h"
|
|
|
|
#if !MESHTASTIC_EXCLUDE_GPS
|
|
#include "GPS.h"
|
|
#endif
|
|
|
|
#include "Default.h"
|
|
#include "MeshRadio.h"
|
|
#include "MeshService.h"
|
|
#include "NodeDB.h"
|
|
#include "PowerMon.h"
|
|
#include "RadioLibInterface.h"
|
|
#include "TransmitHistory.h"
|
|
#include "detect/LoRaRadioType.h"
|
|
#include "error.h"
|
|
#include "main.h"
|
|
#include "modules/StatusLEDModule.h"
|
|
#include "sleep.h"
|
|
#include "target_specific.h"
|
|
|
|
#ifdef ARCH_ESP32
|
|
#include "esp_pm.h"
|
|
#if HAS_WIFI
|
|
#include "mesh/wifi/WiFiAPClient.h"
|
|
#endif
|
|
#include "rom/rtc.h"
|
|
#include <RadioLib.h>
|
|
#include <driver/rtc_io.h>
|
|
#include <driver/uart.h>
|
|
|
|
esp_sleep_source_t wakeCause; // the reason we booted this time
|
|
#endif
|
|
#include "Throttle.h"
|
|
|
|
#ifdef USE_PCA95X5
|
|
#include PCA95X5_INC
|
|
extern PCA95X5_CLS io;
|
|
#endif
|
|
|
|
#ifdef HAS_PPM
|
|
#include <XPowersLib.h>
|
|
extern XPowersPPM *PPM;
|
|
#endif
|
|
|
|
#ifndef INCLUDE_vTaskSuspend
|
|
#define INCLUDE_vTaskSuspend 0
|
|
#endif
|
|
|
|
/// Called to ask any observers if they want to veto sleep. Return 1 to veto or 0 to allow sleep to happen
|
|
Observable<void *> preflightSleep;
|
|
|
|
/// Called to tell observers we are now entering (deep) sleep and you should prepare. Must return 0
|
|
Observable<void *> notifyDeepSleep;
|
|
|
|
/// Called to tell observers we are rebooting ASAP. Must return 0
|
|
Observable<void *> notifyReboot;
|
|
|
|
#ifdef ARCH_ESP32
|
|
/// Called to tell observers that light sleep is about to begin
|
|
Observable<void *> notifyLightSleep;
|
|
|
|
/// Called to tell observers that light sleep has just ended, and why it ended
|
|
Observable<esp_sleep_wakeup_cause_t> notifyLightSleepEnd;
|
|
#endif
|
|
|
|
// deep sleep support
|
|
RTC_DATA_ATTR int bootCount = 0;
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Application
|
|
// -----------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Control CPU core speed (80MHz vs 240MHz)
|
|
*
|
|
* We leave CPU at full speed during init, but once loop is called switch to low speed (for a 50% power savings)
|
|
*
|
|
*/
|
|
void setCPUFast(bool on)
|
|
{
|
|
#if defined(ARCH_ESP32) && HAS_WIFI && !HAS_TFT && !defined(T_LORA_PAGER) && !defined(T_DECK)
|
|
|
|
if (isWifiAvailable()) {
|
|
/*
|
|
*
|
|
* There's a newly introduced bug in the espressif framework where WiFi is
|
|
* unstable when the frequency is less than 240MHz.
|
|
*
|
|
* This mostly impacts WiFi AP mode but we'll bump the frequency for
|
|
* all WiFi use cases.
|
|
* (Added: Dec 23, 2021 by Jm Casler)
|
|
*/
|
|
#ifndef CONFIG_IDF_TARGET_ESP32C3
|
|
LOG_DEBUG("Set CPU to 240MHz because WiFi is in use");
|
|
setCpuFrequencyMhz(240);
|
|
#endif
|
|
return;
|
|
}
|
|
|
|
// The Heltec LORA32 V1 runs at 26 MHz base frequency and doesn't react well to switching to 80 MHz...
|
|
#if !defined(ARDUINO_HELTEC_WIFI_LORA_32) && !defined(CONFIG_IDF_TARGET_ESP32C3)
|
|
setCpuFrequencyMhz(on ? 240 : 80);
|
|
#endif
|
|
|
|
#endif
|
|
}
|
|
|
|
// Perform power on init that we do on each wake from deep sleep
|
|
void initDeepSleep()
|
|
{
|
|
#ifdef ARCH_ESP32
|
|
bootCount++;
|
|
const char *reason;
|
|
wakeCause = esp_sleep_get_wakeup_cause();
|
|
|
|
switch (wakeCause) {
|
|
case ESP_SLEEP_WAKEUP_EXT0:
|
|
reason = "ext0 RTC_IO";
|
|
break;
|
|
case ESP_SLEEP_WAKEUP_EXT1:
|
|
reason = "ext1 RTC_CNTL";
|
|
break;
|
|
case ESP_SLEEP_WAKEUP_TIMER:
|
|
reason = "timer";
|
|
break;
|
|
case ESP_SLEEP_WAKEUP_TOUCHPAD:
|
|
reason = "touchpad";
|
|
break;
|
|
case ESP_SLEEP_WAKEUP_ULP:
|
|
reason = "ULP program";
|
|
break;
|
|
default:
|
|
reason = "reset";
|
|
break;
|
|
}
|
|
/*
|
|
Not using yet because we are using wake on all buttons being low
|
|
|
|
wakeButtons = esp_sleep_get_ext1_wakeup_status(); // If one of these buttons is set it was the reason we woke
|
|
if (wakeCause == ESP_SLEEP_WAKEUP_EXT1 && !wakeButtons) // we must have been using the 'all buttons rule for waking' to
|
|
support busted boards, assume button one was pressed wakeButtons = ((uint64_t)1) << buttons.gpios[0];
|
|
*/
|
|
|
|
#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
|
|
// If we booted because our timer ran out or the user pressed reset, send those as fake events
|
|
RESET_REASON hwReason = rtc_get_reset_reason(0);
|
|
|
|
#ifdef CONFIG_IDF_TARGET_ESP32P4
|
|
if (hwReason == BROWN_OUT_RESET)
|
|
reason = "brownout";
|
|
else if (hwReason == HP_CORE_HP_WDT_RESET)
|
|
reason = "taskWatchdog";
|
|
else if (hwReason == HP_CORE_LP_WDT_RESET)
|
|
reason = "intWatchdog";
|
|
else if (hwReason == CHIP_LP_WDT_RESET)
|
|
reason = "chipWatchdog";
|
|
else if (hwReason == SUPER_WDT_RESET)
|
|
reason = "superWatchdog";
|
|
else if (hwReason == HP_SYS_HP_WDT_RESET)
|
|
reason = "systemWatchdog";
|
|
else if (hwReason == HP_SYS_LP_WDT_RESET)
|
|
reason = "systemLowPowerWatchdog";
|
|
#else
|
|
if (hwReason == RTCWDT_BROWN_OUT_RESET)
|
|
reason = "brownout";
|
|
else if (hwReason == RTCWDT_RTC_RESET)
|
|
reason = "rtcWatchdog";
|
|
else if (hwReason == TG0WDT_SYS_RESET)
|
|
reason = "taskWatchdog";
|
|
else if (hwReason == TG1WDT_SYS_RESET)
|
|
reason = "intWatchdog";
|
|
#endif
|
|
LOG_INFO("Booted, wake cause %d (boot count %d), reset_reason=%s", wakeCause, bootCount, reason);
|
|
#endif
|
|
|
|
#if SOC_RTCIO_HOLD_SUPPORTED
|
|
// If waking from sleep, release any and all RTC GPIOs
|
|
if (wakeCause != ESP_SLEEP_WAKEUP_UNDEFINED) {
|
|
LOG_DEBUG("Disable any holds on RTC IO pads");
|
|
for (uint8_t i = 0; i <= GPIO_NUM_MAX; i++) {
|
|
if (rtc_gpio_is_valid_gpio((gpio_num_t)i))
|
|
rtc_gpio_hold_dis((gpio_num_t)i);
|
|
|
|
// ESP32 (original)
|
|
else if (GPIO_IS_VALID_OUTPUT_GPIO((gpio_num_t)i))
|
|
gpio_hold_dis((gpio_num_t)i);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#endif
|
|
}
|
|
|
|
bool doPreflightSleep(bool deepSleep)
|
|
{
|
|
// Observers only get a void*: non-NULL means the hardware (radio) is about to be powered
|
|
// down (deep sleep / shutdown), NULL means a light sleep where the radio keeps running
|
|
static const bool deepSleepFlag = true;
|
|
if (preflightSleep.notifyObservers(deepSleep ? (void *)&deepSleepFlag : NULL) != 0)
|
|
return false; // vetoed
|
|
else
|
|
return true;
|
|
}
|
|
|
|
/// Tell devices we are going to sleep and wait for them to handle things
|
|
static void waitEnterSleep(bool skipPreflight, bool deepSleep)
|
|
{
|
|
if (!skipPreflight) {
|
|
uint32_t now = millis();
|
|
while (!doPreflightSleep(deepSleep)) {
|
|
delay(100); // Kinda yucky - wait until radio says say we can shutdown (finished in process sends/receives)
|
|
|
|
if (!Throttle::isWithinTimespanMs(now,
|
|
THIRTY_SECONDS_MS)) { // If we wait too long just report an error and go to sleep
|
|
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_SLEEP_ENTER_WAIT);
|
|
assert(0); // FIXME - for now we just restart, need to fix bug #167
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Code that still needs to be moved into notifyObservers
|
|
console->flush(); // send all our characters before we stop cpu clock
|
|
setBluetoothEnable(false); // has to be off before calling light sleep
|
|
}
|
|
|
|
void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveNodeDb = false)
|
|
{
|
|
if (INCLUDE_vTaskSuspend && (msecToWake == portMAX_DELAY)) {
|
|
LOG_INFO("Enter deep sleep forever");
|
|
} else {
|
|
LOG_INFO("Enter deep sleep for %u seconds", msecToWake / 1000);
|
|
}
|
|
|
|
// not using wifi yet, but once we are this is needed to shutoff the radio hw
|
|
// esp_wifi_stop();
|
|
waitEnterSleep(skipPreflight, true);
|
|
|
|
#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_BLUETOOTH
|
|
// Full shutdown of bluetooth hardware
|
|
if (nimbleBluetooth)
|
|
nimbleBluetooth->deinit();
|
|
#endif
|
|
|
|
#ifdef ARCH_ESP32
|
|
if (!shouldLoraWake(msecToWake))
|
|
notifyDeepSleep.notifyObservers(NULL);
|
|
#else
|
|
notifyDeepSleep.notifyObservers(NULL);
|
|
#endif
|
|
|
|
powerMon->setState(meshtastic_PowerMon_State_CPU_DeepSleep);
|
|
if (screen)
|
|
screen->doDeepSleep(); // datasheet says this will draw only 10ua
|
|
|
|
if (!skipSaveNodeDb) {
|
|
nodeDB->saveToDisk();
|
|
}
|
|
|
|
// Persist broadcast transmit times so throttle survives reboot
|
|
if (transmitHistory)
|
|
transmitHistory->saveToDisk();
|
|
|
|
#ifdef PIN_POWER_EN
|
|
digitalWrite(PIN_POWER_EN, LOW);
|
|
pinMode(PIN_POWER_EN, INPUT); // power off peripherals
|
|
#endif
|
|
|
|
#ifdef RAK_WISMESH_TAP_V2
|
|
digitalWrite(SDCARD_CS, LOW);
|
|
#endif
|
|
|
|
#if defined(TRACKER_T1000_E) || defined(MESH_TRACKER_X1)
|
|
digitalWrite(GPS_VRTC_EN, LOW);
|
|
digitalWrite(PIN_GPS_RESET, LOW);
|
|
digitalWrite(GPS_SLEEP_INT, LOW);
|
|
digitalWrite(GPS_RTC_INT, LOW);
|
|
#ifdef GPS_RESETB_OUT
|
|
pinMode(GPS_RESETB_OUT, OUTPUT);
|
|
digitalWrite(GPS_RESETB_OUT, LOW);
|
|
#endif
|
|
|
|
#ifdef BUZZER_EN_PIN
|
|
digitalWrite(BUZZER_EN_PIN, LOW);
|
|
#endif
|
|
|
|
#ifdef PIN_DRV_EN
|
|
digitalWrite(PIN_DRV_EN, LOW);
|
|
#endif
|
|
|
|
#ifdef PIN_3V3_EN
|
|
digitalWrite(PIN_3V3_EN, LOW);
|
|
#endif
|
|
#ifdef PIN_WD_EN
|
|
digitalWrite(PIN_WD_EN, LOW);
|
|
#endif
|
|
#endif
|
|
statusLEDModule->setPowerLED(false);
|
|
#ifdef RESET_OLED
|
|
digitalWrite(RESET_OLED, 1); // put the display in reset before killing its power
|
|
#endif
|
|
|
|
#if defined(VEXT_ENABLE)
|
|
digitalWrite(VEXT_ENABLE, !VEXT_ON_VALUE); // turn on the display power
|
|
#endif
|
|
|
|
#ifdef ARCH_ESP32
|
|
if (shouldLoraWake(msecToWake)) {
|
|
enableLoraInterrupt();
|
|
}
|
|
#ifdef BUTTON_PIN
|
|
// Avoid leakage through button pin
|
|
if (GPIO_IS_VALID_OUTPUT_GPIO(BUTTON_PIN)) {
|
|
#ifdef BUTTON_NEED_PULLUP
|
|
pinMode(BUTTON_PIN, INPUT_PULLUP);
|
|
#else
|
|
pinMode(BUTTON_PIN, INPUT);
|
|
#endif
|
|
// A held pad ignores ext1_wakeup_prepare()'s re-route to RTC, so never hold the pin we wake on.
|
|
if (config.device.button_gpio && config.device.button_gpio != BUTTON_PIN)
|
|
gpio_hold_en((gpio_num_t)BUTTON_PIN);
|
|
}
|
|
#endif
|
|
#ifdef SENSECAP_INDICATOR
|
|
// Portexpander definition does not pass GPIO_IS_VALID_OUTPUT_GPIO
|
|
pinMode(LORA_CS, OUTPUT);
|
|
digitalWrite(LORA_CS, HIGH);
|
|
gpio_hold_en((gpio_num_t)LORA_CS);
|
|
#elif defined(ELECROW_PANEL)
|
|
// Elecrow panels do not use LORA_CS, do nothing
|
|
#else
|
|
if (GPIO_IS_VALID_OUTPUT_GPIO(LORA_CS)) {
|
|
// LoRa CS (RADIO_NSS) needs to stay HIGH, even during deep sleep
|
|
pinMode(LORA_CS, OUTPUT);
|
|
digitalWrite(LORA_CS, HIGH);
|
|
gpio_hold_en((gpio_num_t)LORA_CS);
|
|
}
|
|
#endif
|
|
#endif
|
|
|
|
#ifdef HAS_PPM
|
|
if (PPM) {
|
|
// BQ25896 PMIC shutdown is a hard power-off state.
|
|
// Only use it for "sleep forever" / explicit shutdown, because timed deep sleep
|
|
// must remain wakeable by RTC timer.
|
|
if (msecToWake == portMAX_DELAY) {
|
|
LOG_INFO("PPM shutdown");
|
|
console->flush();
|
|
PPM->shutdown();
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#ifdef HAS_PMU
|
|
if (pmu_found && PMU) {
|
|
// Obsolete comment: from back when we we used to receive lora packets while CPU was in deep sleep.
|
|
// We no longer do that, because our light-sleep current draws are low enough and it provides fast start/low cost
|
|
// wake. We currently use deep sleep only for 'we want our device to actually be off - because our battery is
|
|
// critically low'. So in deep sleep we DO shut down power to LORA (and when we boot later we completely reinit it)
|
|
//
|
|
// No need to turn this off if the power draw in sleep mode really is just 0.2uA and turning it off would
|
|
// leave floating input for the IRQ line
|
|
// If we want to leave the radio receiving in would be 11.5mA current draw, but most of the time it is just waiting
|
|
// in its sequencer (true?) so the average power draw should be much lower even if we were listening for packets
|
|
// all the time.
|
|
PMU->setChargingLedMode(XPOWERS_CHG_LED_OFF);
|
|
|
|
uint8_t model = PMU->getChipModel();
|
|
if (model == XPOWERS_AXP2101) {
|
|
if (HW_VENDOR == meshtastic_HardwareModel_TBEAM) {
|
|
// t-beam v1.2 radio power channel
|
|
PMU->disablePowerOutput(XPOWERS_ALDO2); // lora radio power channel
|
|
} else if (HW_VENDOR == meshtastic_HardwareModel_LILYGO_TBEAM_S3_CORE ||
|
|
HW_VENDOR == meshtastic_HardwareModel_T_WATCH_S3 || HW_VENDOR == meshtastic_HardwareModel_T_WATCH_ULTRA) {
|
|
PMU->disablePowerOutput(XPOWERS_ALDO3); // lora radio power channel
|
|
}
|
|
} else if (model == XPOWERS_AXP192) {
|
|
// t-beam v1.1 radio power channel
|
|
PMU->disablePowerOutput(XPOWERS_LDO2); // lora radio power channel
|
|
}
|
|
if (msecToWake == portMAX_DELAY) {
|
|
LOG_INFO("PMU shutdown");
|
|
console->flush();
|
|
PMU->shutdown();
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#if !MESHTASTIC_EXCLUDE_I2C && defined(ARCH_ESP32) && defined(I2C_SDA)
|
|
// Added by https://github.com/meshtastic/firmware/pull/4418
|
|
// Possibly to support Heltec Capsule Sensor?
|
|
Wire.end();
|
|
pinMode(I2C_SDA, ANALOG);
|
|
pinMode(I2C_SCL, ANALOG);
|
|
#endif
|
|
|
|
console->flush();
|
|
cpuDeepSleep(msecToWake);
|
|
}
|
|
|
|
#ifdef ARCH_ESP32
|
|
/**
|
|
* The GPIO an SX126x/SX128x/LR part raises its IRQ on, or -1 if there is no usable pin.
|
|
*
|
|
* Nearly every variant spells it LORA_DIO1, but an SX1280 board may define SX128X_DIO1 on a different
|
|
* pin (t-eth-elite, tlora_t3s3_v1) or as the only one of the two (tlora_v2_1_18, betafpv_2400_tx_micro),
|
|
* because that is the name RadioInterface hands to the driver. Resolve it per radio rather than per
|
|
* macro, so arming, polling and teardown below cannot pick different pins. RF95 keeps RF95_IRQ.
|
|
*/
|
|
static int loraWakeDio1Pin()
|
|
{
|
|
#if defined(SX128X_DIO1) && (SX128X_DIO1 != RADIOLIB_NC)
|
|
if (radioType == SX1280_RADIO)
|
|
return (int)SX128X_DIO1;
|
|
#endif
|
|
#if defined(LORA_DIO1) && (LORA_DIO1 != RADIOLIB_NC)
|
|
return (int)LORA_DIO1;
|
|
#endif
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Is the LoRa IRQ line already asserted right now?
|
|
*
|
|
* DIO1 (SX126x/SX128x) and RF95_IRQ are active-high, so a HIGH level means the radio has already
|
|
* latched an interrupt (typically an RX_DONE). If that edge lands in the small window between
|
|
* enableLoraInterrupt() and esp_light_sleep_start(), the DIO ISR fires and detachInterrupt() clears
|
|
* the per-pin GPIO wake enable as a side effect - the node would then sleep with the packet latched
|
|
* but the wake source disarmed, stranding it for the rest of the sleep chunk (up to 30s). Reading the
|
|
* raw line just before sleeping lets us skip that sleep and let the main loop service the packet.
|
|
* Boards where DIO1 is a virtual/expander pin never reach here - doLightSleep() returns early on
|
|
* LORA_DIO1_EXTENDED_IO before this is called.
|
|
*/
|
|
static bool loraWakeIrqAsserted()
|
|
{
|
|
#if defined(RF95_IRQ) && (RF95_IRQ != RADIOLIB_NC)
|
|
if (radioType == RF95_RADIO)
|
|
return gpio_get_level((gpio_num_t)RF95_IRQ) == 1;
|
|
#endif
|
|
const int dio1 = loraWakeDio1Pin();
|
|
if (dio1 >= 0 && radioType != RF95_RADIO)
|
|
return gpio_get_level((gpio_num_t)dio1) == 1;
|
|
return false;
|
|
}
|
|
|
|
// The radio ISR is rising-edge and stays attached with the RX readout task. As a level wake source it would fire
|
|
// nonstop, starving the readout task that clears the IRQ, so mask it while armed and restore the edge type after.
|
|
static void armLoraWakePin(gpio_num_t pin)
|
|
{
|
|
gpio_intr_disable(pin);
|
|
gpio_wakeup_enable(pin, GPIO_INTR_HIGH_LEVEL);
|
|
}
|
|
|
|
static void restoreLoraIrqPin(gpio_num_t pin)
|
|
{
|
|
gpio_wakeup_disable(pin); // leaves the level type in place
|
|
gpio_set_intr_type(pin, GPIO_INTR_POSEDGE);
|
|
gpio_intr_enable(pin);
|
|
// The edge that woke us was consumed by the sleep; deliver an IRQ that is already pending.
|
|
if (gpio_get_level(pin) == 1 && RadioLibInterface::instance)
|
|
RadioLibInterface::instance->pollMissedIrqs();
|
|
}
|
|
|
|
// Consecutive sleeps skipped because the LoRa IRQ was already asserted. A line stuck high would
|
|
// otherwise stop the node sleeping for good, so give up on skipping once it stops looking transient.
|
|
static uint8_t loraIrqSkippedSleeps = 0;
|
|
#define LORA_IRQ_SKIP_SLEEP_WARN 3
|
|
#define LORA_IRQ_SKIP_SLEEP_MAX 5
|
|
|
|
/**
|
|
* enter light sleep (preserves ram but stops everything about CPU).
|
|
*
|
|
* Returns (after restoring hw state) when the user presses a button or we get a LoRa interrupt
|
|
*/
|
|
esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more reasonable default
|
|
{
|
|
// LOG_DEBUG("Enter light sleep");
|
|
|
|
// LORA_DIO1 is an extended IO pin (on an I/O expander). Setting it as a wake-up pin will cause problems,
|
|
// such as the device not entering light sleep. Boards opt in with LORA_DIO1_EXTENDED_IO in their variant.
|
|
#if defined(LORA_DIO1_EXTENDED_IO)
|
|
return ESP_SLEEP_WAKEUP_TIMER;
|
|
#endif
|
|
|
|
waitEnterSleep(false, false);
|
|
notifyLightSleep.notifyObservers(NULL); // Button interrupts are detached here
|
|
|
|
uint64_t sleepUsec = sleepMsec * 1000LL;
|
|
|
|
// NOTE! ESP docs say we must disable bluetooth and wifi before light sleep
|
|
|
|
#if SOC_PM_SUPPORT_RTC_PERIPH_PD
|
|
// We want RTC peripherals to stay on
|
|
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
|
|
#endif
|
|
|
|
#if defined(BUTTON_PIN) && defined(BUTTON_NEED_PULLUP)
|
|
gpio_pullup_en((gpio_num_t)BUTTON_PIN);
|
|
#endif
|
|
|
|
#ifdef SERIAL0_RX_GPIO
|
|
// We treat the serial port as a GPIO for a fast/low power way of waking, if we see a rising edge that means
|
|
// someone started to send something
|
|
|
|
// gpio 3 is RXD for serialport 0 on ESP32
|
|
// Send a few Z characters to wake the port
|
|
|
|
// this doesn't work on TBEAMs when the USB is depowered (causes bogus interrupts)
|
|
// So we disable this "wake on serial" feature - because now when a TBEAM (only) has power connected it
|
|
// never tries to go to sleep if the user is using the API
|
|
// gpio_wakeup_enable((gpio_num_t)SERIAL0_RX_GPIO, GPIO_INTR_LOW_LEVEL);
|
|
|
|
// doesn't help - I think the USB-UART chip losing power is pulling the signal low
|
|
// gpio_pullup_en((gpio_num_t)SERIAL0_RX_GPIO);
|
|
|
|
// alas - can only work if using the refclock, which is limited to about 9600 bps
|
|
// assert(uart_set_wakeup_threshold(UART_NUM_0, 3) == ESP_OK);
|
|
// assert(esp_sleep_enable_uart_wakeup(0) == ESP_OK);
|
|
#endif
|
|
#ifdef ROTARY_PRESS
|
|
// The enableLoraInterrupt() method is using ext0_wakeup, so we are forced to use GPIO wakeup
|
|
gpio_wakeup_enable((gpio_num_t)ROTARY_PRESS, GPIO_INTR_LOW_LEVEL);
|
|
#endif
|
|
#ifdef KB_INT
|
|
#if KB_INT_WAKE_ON_HIGH
|
|
gpio_wakeup_enable((gpio_num_t)KB_INT, GPIO_INTR_HIGH_LEVEL);
|
|
#else
|
|
gpio_wakeup_enable((gpio_num_t)KB_INT, GPIO_INTR_LOW_LEVEL);
|
|
#endif // KB_INT_WAKE_ON_HIGH
|
|
#endif // KB_INT
|
|
#ifdef BOARD_PCA9535_INT
|
|
// Side-key interrupt line from PCA9535 expander (active low).
|
|
// Mask the CPU interrupt: the level type would storm an ISR that cannot clear the expander's latched INT.
|
|
gpio_intr_disable((gpio_num_t)BOARD_PCA9535_INT);
|
|
gpio_wakeup_enable((gpio_num_t)BOARD_PCA9535_INT, GPIO_INTR_LOW_LEVEL);
|
|
#endif
|
|
#ifdef BUTTON_PIN
|
|
gpio_num_t pin = (gpio_num_t)(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN);
|
|
gpio_wakeup_enable(pin, GPIO_INTR_LOW_LEVEL);
|
|
#endif
|
|
#if defined(INPUTDRIVER_TWO_WAY_ROCKER_BTN) || defined(INPUTDRIVER_ENCODER_BTN)
|
|
#if defined(INPUTDRIVER_TWO_WAY_ROCKER_BTN)
|
|
#define INPUTDRIVER_WAKE_BTN_PIN INPUTDRIVER_TWO_WAY_ROCKER_BTN
|
|
#else
|
|
#define INPUTDRIVER_WAKE_BTN_PIN INPUTDRIVER_ENCODER_BTN
|
|
#endif
|
|
// Most of these switches idle high and pull to ground, but not all. Arming the wrong level on a
|
|
// button that idles low means the wake condition is already true, and light sleep ends the
|
|
// instant it begins. Defaults to low, so only a board that says otherwise changes behaviour.
|
|
#if defined(INPUTDRIVER_ENCODER_BTN_ACTIVE_LOW) && !INPUTDRIVER_ENCODER_BTN_ACTIVE_LOW
|
|
gpio_wakeup_enable((gpio_num_t)INPUTDRIVER_WAKE_BTN_PIN, GPIO_INTR_HIGH_LEVEL);
|
|
#else
|
|
gpio_wakeup_enable((gpio_num_t)INPUTDRIVER_WAKE_BTN_PIN, GPIO_INTR_LOW_LEVEL);
|
|
#endif
|
|
#endif
|
|
#if defined(WAKE_ON_TOUCH)
|
|
gpio_wakeup_enable((gpio_num_t)SCREEN_TOUCH_INT, GPIO_INTR_LOW_LEVEL);
|
|
#endif
|
|
#ifdef MOTION_WAKE_INT_PIN
|
|
// Only arm motion wake when the user asked for it, otherwise every tilt costs a wakeup.
|
|
if (config.display.wake_on_tap_or_motion)
|
|
gpio_wakeup_enable((gpio_num_t)MOTION_WAKE_INT_PIN,
|
|
MOTION_WAKE_INT_ACTIVE_HIGH ? GPIO_INTR_HIGH_LEVEL : GPIO_INTR_LOW_LEVEL);
|
|
#endif
|
|
enableLoraInterrupt();
|
|
#ifdef PMU_IRQ
|
|
// wake due to PMU can happen repeatedly if there is no battery installed or the battery fills
|
|
if (pmu_found)
|
|
gpio_wakeup_enable((gpio_num_t)PMU_IRQ, GPIO_INTR_LOW_LEVEL); // pmu irq
|
|
#endif
|
|
|
|
auto res = esp_sleep_enable_gpio_wakeup();
|
|
if (res != ESP_OK) {
|
|
LOG_ERROR("esp_sleep_enable_gpio_wakeup result %d", res);
|
|
}
|
|
assert(res == ESP_OK);
|
|
res = esp_sleep_enable_timer_wakeup(sleepUsec);
|
|
if (res != ESP_OK) {
|
|
LOG_ERROR("esp_sleep_enable_timer_wakeup result %d", res);
|
|
}
|
|
assert(res == ESP_OK);
|
|
|
|
console->flush();
|
|
const bool loraIrqAsserted = loraWakeIrqAsserted();
|
|
const bool loraIrqPending = loraIrqAsserted && loraIrqSkippedSleeps < LORA_IRQ_SKIP_SLEEP_MAX;
|
|
if (loraIrqPending) {
|
|
// A LoRa IRQ is already pending (see loraWakeIrqAsserted). Don't sleep on top of it - return so
|
|
// the main loop services the packet now instead of stranding it until the next wake.
|
|
if (++loraIrqSkippedSleeps >= LORA_IRQ_SKIP_SLEEP_WARN)
|
|
LOG_WARN("LoRa IRQ still pending after %u skipped sleeps", loraIrqSkippedSleeps);
|
|
else
|
|
LOG_INFO("LoRa IRQ pending, skip light sleep to service packet");
|
|
res = ESP_OK;
|
|
} else {
|
|
// Either nothing pending, or the line has stayed high past LORA_IRQ_SKIP_SLEEP_MAX and the main
|
|
// loop is clearly not going to clear it. Never sleeping costs more than one stranded packet.
|
|
//
|
|
// Only reset the count once the line is seen low again. Resetting on every sleep we allow made a
|
|
// stuck-high line give LORA_IRQ_SKIP_SLEEP_MAX skips, one sleep, repeat, forever - and each
|
|
// skipped attempt still runs waitEnterSleep() and the whole notifyLightSleep/End pass, which
|
|
// detaches, arms and restores every wake pin. Once we have given up, stay given up.
|
|
if (!loraIrqAsserted)
|
|
loraIrqSkippedSleeps = 0;
|
|
res = esp_light_sleep_start();
|
|
}
|
|
if (res != ESP_OK) {
|
|
LOG_ERROR("esp_light_sleep_start result %d", res);
|
|
}
|
|
// commented out because it's not that crucial;
|
|
// if it sporadically happens the node will go into light sleep during the next round
|
|
// assert(res == ESP_OK);
|
|
#ifdef ROTARY_PRESS
|
|
gpio_wakeup_disable((gpio_num_t)ROTARY_PRESS);
|
|
#endif
|
|
#ifdef KB_INT
|
|
gpio_wakeup_disable((gpio_num_t)KB_INT);
|
|
#endif
|
|
#ifdef BOARD_PCA9535_INT
|
|
gpio_wakeup_disable((gpio_num_t)BOARD_PCA9535_INT);
|
|
#endif
|
|
#ifdef BUTTON_PIN
|
|
// Disable wake-on-button interrupt. Re-attach normal button-interrupts
|
|
gpio_wakeup_disable(pin);
|
|
#endif
|
|
#ifdef PMU_IRQ
|
|
// Undo the arming above. PMU_IRQ was the only light-sleep wake pin left armed after the sleep, so
|
|
// its GPIO_INTR_LOW_LEVEL type and wake-enable bit stayed in the GPIO registers indefinitely. A
|
|
// panic or software reset is CPU-only and leaves those registers intact, so the next boot's first
|
|
// attachInterrupt() - attachPowerInterrupts() on the boards that define PMU_IRQ - installs the ISR
|
|
// service with a level condition already pending and nothing to clear it. gpio_wakeup_disable()
|
|
// alone leaves int_type behind, so set an edge type too, matching what the button path does.
|
|
if (pmu_found) {
|
|
gpio_wakeup_disable((gpio_num_t)PMU_IRQ);
|
|
gpio_set_intr_type((gpio_num_t)PMU_IRQ, GPIO_INTR_NEGEDGE);
|
|
}
|
|
#endif
|
|
#ifdef INPUTDRIVER_WAKE_BTN_PIN
|
|
gpio_wakeup_disable((gpio_num_t)INPUTDRIVER_WAKE_BTN_PIN);
|
|
#undef INPUTDRIVER_WAKE_BTN_PIN
|
|
#endif
|
|
#if defined(WAKE_ON_TOUCH)
|
|
gpio_wakeup_disable((gpio_num_t)SCREEN_TOUCH_INT);
|
|
#endif
|
|
#ifdef MOTION_WAKE_INT_PIN
|
|
// Unconditional: the config can have changed while we were asleep.
|
|
gpio_wakeup_disable((gpio_num_t)MOTION_WAKE_INT_PIN);
|
|
#endif
|
|
#if defined(LORA_DIO1) || defined(SX128X_DIO1)
|
|
const int wokeDio1 = loraWakeDio1Pin(); // must match what enableLoraInterrupt() armed
|
|
if (wokeDio1 >= 0 && radioType != RF95_RADIO) {
|
|
restoreLoraIrqPin((gpio_num_t)wokeDio1);
|
|
}
|
|
#endif
|
|
#if defined(RF95_IRQ) && (RF95_IRQ != RADIOLIB_NC)
|
|
if (radioType == RF95_RADIO) {
|
|
restoreLoraIrqPin((gpio_num_t)RF95_IRQ);
|
|
}
|
|
#endif
|
|
|
|
// When we skipped the sleep because a LoRa IRQ was already pending, esp_sleep_get_wakeup_cause()
|
|
// would report the previous real sleep's cause (e.g. TIMER); synthesize a GPIO cause so the
|
|
// handling below (and PowerFSM) takes the packet-service path instead of a stale timer path.
|
|
esp_sleep_wakeup_cause_t cause = loraIrqPending ? ESP_SLEEP_WAKEUP_GPIO : esp_sleep_get_wakeup_cause();
|
|
notifyLightSleepEnd.notifyObservers(cause); // Button interrupts are reattached here
|
|
|
|
if (cause == ESP_SLEEP_WAKEUP_GPIO) {
|
|
LOG_INFO("Exit light sleep gpio");
|
|
// If we woke because of a GPIO, it's possible power needs to run to handle.
|
|
power->setIntervalFromNow(0);
|
|
runASAP = true;
|
|
} else {
|
|
LOG_INFO("Exit light sleep cause: %d", cause);
|
|
}
|
|
|
|
return cause;
|
|
}
|
|
|
|
// not legal on the stock android ESP build
|
|
|
|
/**
|
|
* enable modem sleep mode as needed and available. Should lower our CPU current draw to an average of about 20mA.
|
|
*
|
|
* per https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/system/power_management.html
|
|
*
|
|
* supposedly according to https://github.com/espressif/arduino-esp32/issues/475 this is already done in arduino
|
|
*/
|
|
void enableModemSleep()
|
|
{
|
|
static esp_pm_config_t esp32_config; // filled with zeros because bss
|
|
#if CONFIG_IDF_TARGET_ESP32S3
|
|
esp32_config.max_freq_mhz = CONFIG_ESP32S3_DEFAULT_CPU_FREQ_MHZ;
|
|
#elif CONFIG_IDF_TARGET_ESP32S2
|
|
esp32_config.max_freq_mhz = CONFIG_ESP32S2_DEFAULT_CPU_FREQ_MHZ;
|
|
#elif CONFIG_IDF_TARGET_ESP32C6
|
|
esp32_config.max_freq_mhz = CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ;
|
|
#elif CONFIG_IDF_TARGET_ESP32C3
|
|
esp32_config.max_freq_mhz = CONFIG_ESP32C3_DEFAULT_CPU_FREQ_MHZ;
|
|
#elif CONFIG_IDF_TARGET_ESP32P4
|
|
#if CONFIG_ESP32P4_REV_MIN_FULL < 300
|
|
esp32_config.max_freq_mhz = 360;
|
|
#else
|
|
esp32_config.max_freq_mhz = 400;
|
|
#endif
|
|
#else
|
|
esp32_config.max_freq_mhz = CONFIG_ESP32_DEFAULT_CPU_FREQ_MHZ;
|
|
#endif
|
|
esp32_config.min_freq_mhz = 20; // 10Mhz is minimum recommended
|
|
esp32_config.light_sleep_enable = false;
|
|
int rv = esp_pm_configure(&esp32_config);
|
|
LOG_DEBUG("Sleep request result %x", rv);
|
|
}
|
|
|
|
bool shouldLoraWake(uint32_t msecToWake)
|
|
{
|
|
return msecToWake < portMAX_DELAY && (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER ||
|
|
config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE);
|
|
}
|
|
|
|
void enableLoraInterrupt()
|
|
{
|
|
#if defined(LORA_DIO1_EXTENDED_IO)
|
|
// DIO1 is a virtual pin on an I/O expander - it cannot be a GPIO wakeup source
|
|
#elif defined(LORA_DIO1) || defined(SX128X_DIO1)
|
|
// Not also gated on SOC_PM_SUPPORT_EXT_WAKEUP: that is an IDF alias for ESP32/S2/S3 only, and nothing
|
|
// here uses ext wakeup - GPIO light-sleep wake and the pin pulls exist on every ESP32 variant.
|
|
const int loraDio1 = loraWakeDio1Pin();
|
|
esp_err_t res;
|
|
if (loraDio1 >= 0 && radioType != RF95_RADIO) {
|
|
res = gpio_pulldown_en((gpio_num_t)loraDio1);
|
|
if (res != ESP_OK) {
|
|
LOG_ERROR("gpio_pulldown_en(lora dio1) result %d", res);
|
|
}
|
|
}
|
|
#if defined(LORA_RESET) && (LORA_RESET != RADIOLIB_NC)
|
|
res = gpio_pullup_en((gpio_num_t)LORA_RESET);
|
|
if (res != ESP_OK) {
|
|
LOG_ERROR("gpio_pullup_en(LORA_RESET) result %d", res);
|
|
}
|
|
#endif
|
|
#if defined(LORA_CS) && (LORA_CS != RADIOLIB_NC) && !defined(ELECROW_PANEL)
|
|
gpio_pullup_en((gpio_num_t)LORA_CS);
|
|
#endif
|
|
|
|
#if HAS_LORA_FEM
|
|
loraFEMInterface.setRxModeEnableWhenMCUSleep();
|
|
#endif
|
|
|
|
if (loraDio1 >= 0 && radioType != RF95_RADIO) {
|
|
LOG_INFO("Wake on LoRa DIO1 (GPIO%02d) gpio interrupt", loraDio1);
|
|
armLoraWakePin((gpio_num_t)loraDio1);
|
|
}
|
|
|
|
#endif
|
|
#if defined(RF95_IRQ) && (RF95_IRQ != RADIOLIB_NC)
|
|
if (radioType == RF95_RADIO) {
|
|
LOG_INFO("Wake on RF95_IRQ (GPIO%02d) gpio interrupt", RF95_IRQ);
|
|
armLoraWakePin((gpio_num_t)RF95_IRQ); // RF95 interrupt, active high
|
|
}
|
|
#endif
|
|
}
|
|
#endif
|