When the device went into deep sleep while plugged into USB, a power-button press would occasionally not wake it. The display held its last frame, the chip stayed in deep sleep, and recovery required an unplug + reset + hold-power cycle. On battery the symptom never surfaced because the power button physically re-energises the chip. Two peripherals were holding power domains alive across the deep sleep boundary and interfering with the configured GPIO wake on the power button: 1. HWCDC. Once Serial is initialized, the USB Serial/JTAG peripheral keeps its power domain configured even with TX timeout at zero and no host draining. Tear it down with Serial.end() in HalPowerManager::startDeepSleep, gated by ENABLE_SERIAL_LOG to match the Serial.begin site. This hit me if I was charging off my computer. 2. WiFi. enterDeepSleep had no WiFi teardown, so sleeping from any network-using activity left the modem domain alive. Call WiFi.disconnect(true) + WiFi.mode(WIFI_OFF) when WiFi is active. Wake from deep sleep is effectively a chip reset, so no WiFi state needs to survive. While this doesn't cause higher power drain, it apparently was causing issues where I'd occasionally have the chip hang on sleep transition from a wifi activity. Confirmed on device. Did you use AI tools to help write this code? partial
159 lines
5.1 KiB
C++
159 lines
5.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() {
|
|
if (gpio.deviceIsX3()) {
|
|
// X3 uses an I2C fuel gauge for battery monitoring.
|
|
// I2C init must come AFTER gpio.begin() so early hardware detection/probes are finished.
|
|
Wire.begin(X3_I2C_SDA, X3_I2C_SCL, X3_I2C_FREQ);
|
|
Wire.setTimeOut(4);
|
|
_batteryUseI2C = true;
|
|
} else {
|
|
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) 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();
|
|
}
|
|
|
|
#ifdef ENABLE_SERIAL_LOG
|
|
// Tear down HWCDC so the host sees a clean disconnect and the peripheral
|
|
// doesn't hold power domains that interfere with USB-powered GPIO wake.
|
|
// logSerial is the raw HWCDC reference; Serial is the MySerialImpl proxy
|
|
// (which doesn't expose end()).
|
|
logSerial.end();
|
|
#endif
|
|
|
|
// Pre-sleep routines from the original firmware
|
|
// GPIO13 is connected to battery latch MOSFET, we need to make sure it's low during sleep
|
|
// Note that this means the MCU will be completely powered off during sleep, including RTC
|
|
constexpr gpio_num_t GPIO_SPIWP = GPIO_NUM_13;
|
|
gpio_set_direction(GPIO_SPIWP, GPIO_MODE_OUTPUT);
|
|
gpio_set_level(GPIO_SPIWP, 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: 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
|
|
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 {
|
|
if (_batteryUseI2C) {
|
|
const unsigned long now = millis();
|
|
if (_batteryLastPollMs != 0 && (now - _batteryLastPollMs) < BATTERY_POLL_MS) {
|
|
return _batteryCachedPercent;
|
|
}
|
|
|
|
// Read SOC directly from I2C fuel gauge (16-bit LE register).
|
|
// On I2C error, keep last known value to avoid UI jitter/slowdowns.
|
|
Wire.beginTransmission(I2C_ADDR_BQ27220);
|
|
Wire.write(BQ27220_SOC_REG);
|
|
if (Wire.endTransmission(false) != 0) {
|
|
_batteryLastPollMs = now;
|
|
return _batteryCachedPercent;
|
|
}
|
|
Wire.requestFrom(I2C_ADDR_BQ27220, (uint8_t)2);
|
|
if (Wire.available() < 2) {
|
|
_batteryLastPollMs = now;
|
|
return _batteryCachedPercent;
|
|
}
|
|
const uint8_t lo = Wire.read();
|
|
const uint8_t hi = Wire.read();
|
|
const uint16_t soc = (hi << 8) | lo;
|
|
_batteryCachedPercent = soc > 100 ? 100 : soc;
|
|
_batteryLastPollMs = now;
|
|
return _batteryCachedPercent;
|
|
}
|
|
static const BatteryMonitor battery = BatteryMonitor(BAT_GPIO0);
|
|
|
|
// smooth the battery %.
|
|
if (_batteryCachedPercent == 0) {
|
|
_batteryCachedPercent = 10 * battery.readPercentage();
|
|
} else {
|
|
_batteryCachedPercent = (_batteryCachedPercent * 9 + battery.readPercentage() * 10) / 10;
|
|
}
|
|
return _batteryCachedPercent / 10;
|
|
}
|
|
|
|
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);
|
|
}
|