Files
Crosspoint/lib/hal/HalPowerManager.cpp
T
jpirnayandClaude Sonnet 4.6 6c9f497d2b fix: release GPIO13 hold before re-entering deep sleep
gpio_hold_en(GPIO13) called in startDeepSleep() persists after wake when
keepClockAlive=true. A subsequent startDeepSleep() call (e.g. failed button
verification) would silently fail to change GPIO13's level, leaving the MOSFET
latched and trapping the device in a sleep/wake loop requiring a hardware reset.

Release the individual and global holds at the top of startDeepSleep() so each
sleep entry configures GPIO13 from a clean state. Also call
gpio_deep_sleep_hold_dis() immediately after wake in setup() as a defensive
measure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 11:00:20 +02:00

118 lines
4.1 KiB
C++

#include "HalPowerManager.h"
#include <Logging.h>
#include <WiFi.h>
#include <esp_sleep.h>
#include <cassert>
#include "HalGPIO.h"
HalPowerManager powerManager; // Singleton instance
void HalPowerManager::begin() {
pinMode(BAT_GPIO0, INPUT);
normalFreq = getCpuFrequencyMhz();
modeMutex = xSemaphoreCreateMutex();
assert(modeMutex != nullptr);
}
void HalPowerManager::setPowerSaving(bool enabled) {
if (normalFreq <= 0) {
return; // invalid state
}
auto wifiMode = WiFi.getMode();
if (wifiMode != WIFI_MODE_NULL) {
// Wifi is active, force disabling power saving
enabled = false;
}
// Note: We don't use mutex here to avoid too much overhead,
// it's not very important if we read a slightly stale value for currentLockMode
const LockMode mode = currentLockMode;
if (mode == None && enabled && !isLowPower) {
LOG_DBG("PWR", "Going to low-power mode");
if (!setCpuFrequencyMhz(LOW_POWER_FREQ)) {
LOG_DBG("PWR", "Failed to set CPU frequency = %d MHz", LOW_POWER_FREQ);
return;
}
isLowPower = true;
} else if ((!enabled || mode != None) && isLowPower) {
LOG_DBG("PWR", "Restoring normal CPU frequency");
if (!setCpuFrequencyMhz(normalFreq)) {
LOG_DBG("PWR", "Failed to set CPU frequency = %d MHz", normalFreq);
return;
}
isLowPower = false;
}
// Otherwise, no change needed
}
void HalPowerManager::startDeepSleep(HalGPIO& gpio, bool keepClockAlive) const {
// Ensure that the power button has been released to avoid immediately turning back on if you're holding it
while (gpio.isPressed(HalGPIO::BTN_POWER)) {
delay(50);
gpio.update();
}
// GPIO13 is connected to the battery latch MOSFET.
// When keepClockAlive is false (default): GPIO13 goes LOW, the MCU is
// completely powered off during sleep (including the LP timer / RTC memory).
// When keepClockAlive is true: GPIO13 stays HIGH, the MCU remains powered
// at ~3-4 mA so the LP timer keeps running and RTC memory is preserved.
// This allows HalClock to accurately compute elapsed sleep time on wake.
constexpr gpio_num_t GPIO_SPIWP = GPIO_NUM_13;
// Release any GPIO hold from a previous sleep cycle (keepClockAlive=true leaves GPIO13 held after wake).
// Without this, gpio_set_level() below silently fails and GPIO13 is stuck in its prior state,
// causing the device to enter a sleep/wake loop that requires a hardware reset to escape.
gpio_hold_dis(GPIO_SPIWP);
gpio_deep_sleep_hold_dis();
gpio_set_direction(GPIO_SPIWP, GPIO_MODE_OUTPUT);
gpio_set_level(GPIO_SPIWP, keepClockAlive ? 1 : 0);
esp_sleep_config_gpio_isolate();
gpio_deep_sleep_hold_en();
gpio_hold_en(GPIO_SPIWP);
pinMode(InputManager::POWER_BUTTON_PIN, INPUT_PULLUP);
// Arm the wakeup trigger *after* the button is released
// Note: when keepClockAlive is false, this is only useful for waking up on USB power. On battery, the MCU will be
// completely powered off, so the power button is hard-wired to briefly provide power to the MCU, waking it up
// regardless of the wakeup source configuration.
// When keepClockAlive is true, this is the actual wakeup mechanism since the MCU stays powered.
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
// Enter Deep Sleep
esp_deep_sleep_start();
}
uint16_t HalPowerManager::getBatteryPercentage() const {
static const BatteryMonitor battery = BatteryMonitor(BAT_GPIO0);
return battery.readPercentage();
}
HalPowerManager::Lock::Lock() {
xSemaphoreTake(powerManager.modeMutex, portMAX_DELAY);
// Current limitation: only one lock at a time
if (powerManager.currentLockMode != None) {
LOG_ERR("PWR", "Lock already held, ignore");
valid = false;
} else {
powerManager.currentLockMode = NormalSpeed;
valid = true;
}
xSemaphoreGive(powerManager.modeMutex);
if (valid) {
// Immediately restore normal CPU frequency if currently in low-power mode
powerManager.setPowerSaving(false);
}
}
HalPowerManager::Lock::~Lock() {
xSemaphoreTake(powerManager.modeMutex, portMAX_DELAY);
if (valid) {
powerManager.currentLockMode = None;
}
xSemaphoreGive(powerManager.modeMutex);
}