mirror of
https://github.com/crosspoint-reader/crosspoint-reader.git
synced 2026-08-13 14:21:57 -07:00
312 lines
13 KiB
C++
312 lines
13 KiB
C++
#include "HalPowerManager.h"
|
|
|
|
#include <BoardConfig.h>
|
|
#include <Logging.h>
|
|
#include <PowerManager.h>
|
|
#include <WiFi.h>
|
|
#include <driver/gpio.h>
|
|
#include <esp_sleep.h>
|
|
#include <soc/soc_caps.h>
|
|
|
|
#include <cassert>
|
|
|
|
#include "HalGPIO.h"
|
|
|
|
HalPowerManager powerManager; // Singleton instance
|
|
|
|
// GPIO13 must stay high during light sleep on the C3 Xteink boards: it controls
|
|
// the X4 battery latch and the X3 SD power rail. Other boards use it for
|
|
// unrelated signals, including the X4 Pro display chip select.
|
|
static constexpr gpio_num_t XTEINK_C3_GPIO13 = GPIO_NUM_13;
|
|
|
|
void HalPowerManager::begin() {
|
|
if (BoardConfig::ACTIVE.batteryAdc >= 0) {
|
|
pinMode(BoardConfig::ACTIVE.batteryAdc, INPUT);
|
|
}
|
|
mainLoopTask = xTaskGetCurrentTaskHandle(); // Arduino runs setup() on the loop task
|
|
normalFreq = getCpuFrequencyMhz();
|
|
modeMutex = xSemaphoreCreateMutex();
|
|
assert(modeMutex != nullptr);
|
|
sleepMutex = xSemaphoreCreateMutex();
|
|
assert(sleepMutex != 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 {
|
|
#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
|
|
|
|
#if !SOC_PM_SUPPORT_EXT1_WAKEUP
|
|
if (gpio.isXteinkDevice()) {
|
|
// GPIO13 gates the battery MOSFET on both Xteink C3 boards; driving it low
|
|
// is the battery power-off (the SDK wake source still handles USB power).
|
|
// Unlike upstream, X3 is included: lightSleep() pad-holds the latch HIGH
|
|
// persistently while running (the IDF flash-leakage workaround would drop
|
|
// it mid-slice, hard-powering the device off), so the implicit low that
|
|
// upstream's X3 relies on never happens here — release the hold and drive
|
|
// the latch low explicitly. The hold_en survives deep sleep via the SDK's
|
|
// deepSleep() (esp_sleep_config_gpio_isolate + gpio_deep_sleep_hold_en).
|
|
gpio_hold_dis(XTEINK_C3_GPIO13);
|
|
gpio_set_direction(XTEINK_C3_GPIO13, GPIO_MODE_OUTPUT);
|
|
gpio_set_level(XTEINK_C3_GPIO13, 0);
|
|
gpio_hold_en(XTEINK_C3_GPIO13);
|
|
}
|
|
#endif
|
|
|
|
// Cut the gated peripheral rails (touch/SD/EPD on boards like the Sticky) and
|
|
// hold the enables off through deep sleep — otherwise the GT911 and SD card
|
|
// stay powered all through "off" and drain the battery. No-op on boards with
|
|
// no switched rails (X4/X3). Trade-off: no touch-to-wake; wake is the power
|
|
// button. Must run after display.deepSleep() so the panel controller gets its
|
|
// deep-sleep command while its rail is still up (enterDeepSleep() in main.cpp
|
|
// guarantees that ordering).
|
|
freeink::PowerManager::powerDownRailsForSleep();
|
|
|
|
// Waits for the power button to be physically released (so holding it doesn't
|
|
// immediately wake the device again), then arms the wake source and sleeps.
|
|
freeink::PowerManager::deepSleepUntilPowerButton();
|
|
}
|
|
|
|
bool HalPowerManager::lightSleep(const HalGPIO& gpio) const {
|
|
// A performance Lock means a render (or similar) task is mid-flight; light sleep
|
|
// freezes the whole chip, so it would stall that task.
|
|
// Note: like setPowerSaving(), read without the mutex — stale in either
|
|
// direction is acceptable: a stale lock delays sleeping by one 50 ms slice;
|
|
// a Lock acquired after this check freezes that task for at most one slice
|
|
// (timer wake; RAM/peripheral state retained), which is cheaper than
|
|
// serializing Lock ctor/dtor against the entire sleep window.
|
|
if (currentLockMode != None) {
|
|
return false;
|
|
}
|
|
// Light sleep drops a WiFi association and kills an enumerated USB-CDC link.
|
|
if (WiFi.getMode() != WIFI_MODE_NULL || gpio.isUsbConnectedCached()) {
|
|
return false;
|
|
}
|
|
|
|
// Serialize wake-source arming with the render task's BUSY-wait slice (see
|
|
// sleepMutex declaration for the failure mode this prevents).
|
|
xSemaphoreTake(sleepMutex, portMAX_DELAY);
|
|
|
|
// Timer wake keeps the button/tilt poll cadence identical to the delay() it
|
|
// replaces: the front/side buttons are ADC resistor-ladder inputs whose
|
|
// pressed levels sit mid-rail, invisible to a digital GPIO wake, so they
|
|
// must be polled. The power button (a true GPIO) is ALSO armed, as a level
|
|
// wake at its pressed level: committing a press needs two update() samples
|
|
// >=5 ms apart, so at the timer-only 50 ms cadence a tap shorter than a
|
|
// slice could land in one sample — or none — and be dropped entirely
|
|
// (field-reported as unreliable short-press sleep). The wake is only an
|
|
// early poll: update()'s debounce still decides whether it was a real
|
|
// press, so a misread around the sleep transition costs one extra wake
|
|
// blip, never a phantom press. Idle cost is zero — the pin only holds its
|
|
// pressed level while a finger is on it.
|
|
esp_sleep_enable_timer_wakeup(static_cast<uint64_t>(LIGHT_SLEEP_SLICE_MS) * 1000ULL);
|
|
const int8_t powerPin = BoardConfig::ACTIVE.input.power;
|
|
if (powerPin >= 0) {
|
|
gpio_wakeup_enable(static_cast<gpio_num_t>(powerPin),
|
|
BoardConfig::ACTIVE.input.powerActiveHigh ? GPIO_INTR_HIGH_LEVEL : GPIO_INTR_LOW_LEVEL);
|
|
esp_sleep_enable_gpio_wakeup();
|
|
}
|
|
|
|
// The C3 flash-leakage workaround pulls its DIO-unused SPIWP pad low during
|
|
// light sleep. Hold GPIO13 high only on the X3/X4 boards that use that pad as
|
|
// a power control; on other boards GPIO13 may be a bus signal.
|
|
if (gpio.isXteinkDevice()) {
|
|
gpio_set_level(XTEINK_C3_GPIO13, 1);
|
|
gpio_set_direction(XTEINK_C3_GPIO13, GPIO_MODE_OUTPUT);
|
|
gpio_hold_en(XTEINK_C3_GPIO13);
|
|
}
|
|
|
|
const esp_err_t err = esp_light_sleep_start();
|
|
|
|
// Disarm immediately: an armed timer wake persists across sleep calls and would
|
|
// carry over into startDeepSleep(), waking the device on USB power after 50 ms.
|
|
// gpio_wakeup_disable() clears only the wake-enable bit — the pin's LEVEL
|
|
// interrupt type survives it. A leftover level type is live ammunition for
|
|
// any later-installed GPIO ISR service: an asserted level feeding the
|
|
// shared service with no per-pin handler registered re-enters the ISR
|
|
// forever and livelocks the CPU. Clear the type explicitly.
|
|
esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL);
|
|
if (powerPin >= 0) {
|
|
gpio_wakeup_disable(static_cast<gpio_num_t>(powerPin));
|
|
gpio_set_intr_type(static_cast<gpio_num_t>(powerPin), GPIO_INTR_DISABLE);
|
|
}
|
|
|
|
xSemaphoreGive(sleepMutex);
|
|
|
|
if (err != ESP_OK) {
|
|
// e.g. ESP_ERR_SLEEP_REJECT — we did not actually sleep
|
|
LOG_DBG("PWR", "Light sleep rejected: %d", static_cast<int>(err));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool HalPowerManager::onEinkBusyWaitSlice(const int8_t busyPin, const uint8_t busyLevel) {
|
|
// Same exclusions as lightSleep(): light sleep drops a WiFi association and
|
|
// kills an enumerated USB-CDC link. No LOG here — this runs ~50x/s mid-refresh.
|
|
if (WiFi.getMode() != WIFI_MODE_NULL || gpio.isUsbConnectedCached()) {
|
|
return false;
|
|
}
|
|
|
|
// Mid-debounce: committing needs a second sample, so let the SDK's short poll
|
|
// delay run instead of halting the chip. Same guard lightSleep() gets in loop().
|
|
// Unsynchronized like the checks above; stale costs at most one slice.
|
|
if (gpio.isDebouncePending()) {
|
|
return false;
|
|
}
|
|
|
|
xSemaphoreTake(sleepMutex, portMAX_DELAY);
|
|
|
|
// Wake the instant BUSY leaves its active level (refresh complete). Level
|
|
// wake (not edge) means an already-completed refresh returns immediately
|
|
// instead of sleeping a full slice. The timer bound only exists so the main
|
|
// loop gets scheduling windows to keep sampling the ADC-ladder buttons.
|
|
const auto pin = static_cast<gpio_num_t>(busyPin);
|
|
gpio_wakeup_enable(pin, busyLevel == HIGH ? GPIO_INTR_LOW_LEVEL : GPIO_INTR_HIGH_LEVEL);
|
|
// Also wake the instant the power button is pressed, so the main loop's poll
|
|
// sees even sub-slice taps mid-refresh (same rationale and safety argument
|
|
// as lightSleep(): the wake is an early poll, never a synthesized press).
|
|
const int8_t powerPin = BoardConfig::ACTIVE.input.power;
|
|
if (powerPin >= 0) {
|
|
gpio_wakeup_enable(static_cast<gpio_num_t>(powerPin),
|
|
BoardConfig::ACTIVE.input.powerActiveHigh ? GPIO_INTR_HIGH_LEVEL : GPIO_INTR_LOW_LEVEL);
|
|
}
|
|
esp_sleep_enable_gpio_wakeup();
|
|
esp_sleep_enable_timer_wakeup(static_cast<uint64_t>(BUSY_SLEEP_SLICE_MS) * 1000ULL);
|
|
|
|
// Preserve the C3 Xteink GPIO13 power control during this sleep slice without
|
|
// reconfiguring GPIO13 on boards where it is a bus signal.
|
|
if (gpio.isXteinkDevice()) {
|
|
gpio_set_level(XTEINK_C3_GPIO13, 1);
|
|
gpio_set_direction(XTEINK_C3_GPIO13, GPIO_MODE_OUTPUT);
|
|
gpio_hold_en(XTEINK_C3_GPIO13);
|
|
}
|
|
|
|
const esp_err_t err = esp_light_sleep_start();
|
|
|
|
// Disarm everything armed above; an armed source persisting into
|
|
// startDeepSleep() would wake the device on USB power (see lightSleep()).
|
|
// As in lightSleep(): also clear the LEVEL interrupt types, which
|
|
// gpio_wakeup_disable() leaves behind (see the livelock note there).
|
|
esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL);
|
|
gpio_wakeup_disable(pin);
|
|
gpio_set_intr_type(pin, GPIO_INTR_DISABLE);
|
|
if (powerPin >= 0) {
|
|
gpio_wakeup_disable(static_cast<gpio_num_t>(powerPin));
|
|
gpio_set_intr_type(static_cast<gpio_num_t>(powerPin), GPIO_INTR_DISABLE);
|
|
}
|
|
|
|
xSemaphoreGive(sleepMutex);
|
|
|
|
if (err != ESP_OK) {
|
|
return false; // e.g. ESP_ERR_SLEEP_REJECT — fall back to the SDK's poll delay
|
|
}
|
|
|
|
// Yield until the equal-priority main loop finishes an iteration, not just one
|
|
// tick: one tick is less awake CPU than an iteration needs, so we would re-halt
|
|
// the chip mid-poll. Capped so a loop parked on SD I/O can't hold the panel awake.
|
|
const uint32_t seenIterations = mainLoopIterations;
|
|
// Skipped entirely when the loop cannot make progress and the yield would just
|
|
// burn the cap awake: parked in requestUpdateAndWait() on this very render;
|
|
// never started (setup-time paints, iterations still 0); or this task IS the
|
|
// loop task (direct displayBuffer calls from setup()/loop() busy-wait here).
|
|
const bool loopCanPoll = !mainLoopBlocked && seenIterations != 0 && xTaskGetCurrentTaskHandle() != mainLoopTask;
|
|
unsigned yieldTicks = 0;
|
|
if (loopCanPoll) {
|
|
for (; yieldTicks < SLICE_YIELD_MAX_TICKS && mainLoopIterations == seenIterations; ++yieldTicks) {
|
|
vTaskDelay(1);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
uint16_t HalPowerManager::getBatteryPercentage() const {
|
|
static const BatteryMonitor battery;
|
|
if (BoardConfig::ACTIVE.batteryGauge.gaugeAddr != 0) {
|
|
const unsigned long now = millis();
|
|
if (_batteryLastPollMs != 0 && (now - _batteryLastPollMs) < BATTERY_POLL_MS) {
|
|
return _batteryCachedPercent;
|
|
}
|
|
|
|
_batteryLastPollMs = now;
|
|
uint16_t percent = 0;
|
|
if (!battery.readPercentageChecked(percent)) {
|
|
return _batteryCachedPercent;
|
|
}
|
|
_batteryCachedPercent = percent;
|
|
return _batteryCachedPercent;
|
|
}
|
|
|
|
// 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);
|
|
}
|