feat: lower CPU freq on idle, add HalPowerManager (#852)
## Summary Continue my experiment from https://github.com/crosspoint-reader/crosspoint-reader/pull/801 This PR add the ability to lower the CPU frequency on extended idle period (currently set to 3 seconds). By default, the esp32c3 CPU is set to 160MHz, and now on idle, we can reduce it to just 10MHz. Note that while this functionality is already provided by [esp power management](https://docs.espressif.com/projects/esp-idf/en/v4.3/esp32c3/api-reference/system/power_management.html), the current Arduino build lacks of this, and enabling it is just too complicated (not worth the effort compared to this PR) Update: more info in https://github.com/crosspoint-reader/crosspoint-reader/pull/852#issuecomment-3904562827 ## Testing Pre-condition for each test case: the battery is charged to 100%, and is left plugged in after fully charged for an extra 1 hour. The table below shows how much battery is **used** for a given duration: | case / duration | 6 hrs | 12 hrs | | --- | --- | --- | | `delay(10)` | 26% | 48% | | `delay(50)`, PR https://github.com/crosspoint-reader/crosspoint-reader/pull/801 | 20% | Not tested | | `delay(50)` + low CPU freq (This PR) | Not tested | 25% | | `delay(10)` + low CPU freq (1) | Not tested | Not tested | (1) I decided not to test this case because it may not make sense. The problem is that CPU frequency vs power consumption do not follow a linear relationship, see [this](https://www.arrow.com/en/research-and-events/articles/esp32-power-consumption-can-be-reduced-with-sleep-modes) as an example. So, tight loop (10ms) + lower CPU freq significantly impact battery life, because the active CPU time is now much higher compared to the wall time. **So in conclusion, this PR improves ~150% to ~200% battery use time per charge.** The projected battery life is now: ~36-48 hrs of reading time (normal reading, no wifi) --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? **NO**
This commit is contained in:
@@ -1,11 +1,9 @@
|
|||||||
#include <HalGPIO.h>
|
#include <HalGPIO.h>
|
||||||
#include <SPI.h>
|
#include <SPI.h>
|
||||||
#include <esp_sleep.h>
|
|
||||||
|
|
||||||
void HalGPIO::begin() {
|
void HalGPIO::begin() {
|
||||||
inputMgr.begin();
|
inputMgr.begin();
|
||||||
SPI.begin(EPD_SCLK, SPI_MISO, EPD_MOSI, EPD_CS);
|
SPI.begin(EPD_SCLK, SPI_MISO, EPD_MOSI, EPD_CS);
|
||||||
pinMode(BAT_GPIO0, INPUT);
|
|
||||||
pinMode(UART0_RXD, INPUT);
|
pinMode(UART0_RXD, INPUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,23 +21,6 @@ bool HalGPIO::wasAnyReleased() const { return inputMgr.wasAnyReleased(); }
|
|||||||
|
|
||||||
unsigned long HalGPIO::getHeldTime() const { return inputMgr.getHeldTime(); }
|
unsigned long HalGPIO::getHeldTime() const { return inputMgr.getHeldTime(); }
|
||||||
|
|
||||||
void HalGPIO::startDeepSleep() {
|
|
||||||
// Ensure that the power button has been released to avoid immediately turning back on if you're holding it
|
|
||||||
while (inputMgr.isPressed(BTN_POWER)) {
|
|
||||||
delay(50);
|
|
||||||
inputMgr.update();
|
|
||||||
}
|
|
||||||
// Arm the wakeup trigger *after* the button is released
|
|
||||||
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
|
|
||||||
// Enter Deep Sleep
|
|
||||||
esp_deep_sleep_start();
|
|
||||||
}
|
|
||||||
|
|
||||||
int HalGPIO::getBatteryPercentage() const {
|
|
||||||
static const BatteryMonitor battery = BatteryMonitor(BAT_GPIO0);
|
|
||||||
return battery.readPercentage();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool HalGPIO::isUsbConnected() const {
|
bool HalGPIO::isUsbConnected() const {
|
||||||
// U0RXD/GPIO20 reads HIGH when USB is connected
|
// U0RXD/GPIO20 reads HIGH when USB is connected
|
||||||
return digitalRead(UART0_RXD) == HIGH;
|
return digitalRead(UART0_RXD) == HIGH;
|
||||||
|
|||||||
@@ -38,12 +38,6 @@ class HalGPIO {
|
|||||||
bool wasAnyReleased() const;
|
bool wasAnyReleased() const;
|
||||||
unsigned long getHeldTime() const;
|
unsigned long getHeldTime() const;
|
||||||
|
|
||||||
// Setup wake up GPIO and enter deep sleep
|
|
||||||
void startDeepSleep();
|
|
||||||
|
|
||||||
// Get battery percentage (range 0-100)
|
|
||||||
int getBatteryPercentage() const;
|
|
||||||
|
|
||||||
// Check if USB is connected
|
// Check if USB is connected
|
||||||
bool isUsbConnected() const;
|
bool isUsbConnected() const;
|
||||||
|
|
||||||
|
|||||||
95
lib/hal/HalPowerManager.cpp
Normal file
95
lib/hal/HalPowerManager.cpp
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
#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) 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();
|
||||||
|
}
|
||||||
|
// Arm the wakeup trigger *after* the button is released
|
||||||
|
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
|
||||||
|
// Enter Deep Sleep
|
||||||
|
esp_deep_sleep_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
int 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);
|
||||||
|
}
|
||||||
56
lib/hal/HalPowerManager.h
Normal file
56
lib/hal/HalPowerManager.h
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <InputManager.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
#include <freertos/semphr.h>
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
|
||||||
|
#include "HalGPIO.h"
|
||||||
|
|
||||||
|
class HalPowerManager;
|
||||||
|
extern HalPowerManager powerManager; // Singleton
|
||||||
|
|
||||||
|
class HalPowerManager {
|
||||||
|
int normalFreq = 0; // MHz
|
||||||
|
bool isLowPower = false;
|
||||||
|
|
||||||
|
enum LockMode { None, NormalSpeed };
|
||||||
|
LockMode currentLockMode = None;
|
||||||
|
SemaphoreHandle_t modeMutex = nullptr; // Protect access to currentLockMode
|
||||||
|
|
||||||
|
public:
|
||||||
|
static constexpr int LOW_POWER_FREQ = 10; // MHz
|
||||||
|
static constexpr unsigned long IDLE_POWER_SAVING_MS = 3000; // ms
|
||||||
|
|
||||||
|
void begin();
|
||||||
|
|
||||||
|
// Control CPU frequency for power saving
|
||||||
|
void setPowerSaving(bool enabled);
|
||||||
|
|
||||||
|
// Setup wake up GPIO and enter deep sleep
|
||||||
|
// Should be called inside main loop() to handle the currentLockMode
|
||||||
|
void startDeepSleep(HalGPIO& gpio) const;
|
||||||
|
|
||||||
|
// Get battery percentage (range 0-100)
|
||||||
|
int getBatteryPercentage() const;
|
||||||
|
|
||||||
|
// RAII helper class to manage power saving locks
|
||||||
|
// Usage: create an instance of Lock in a scope to disable power saving, for example when running a task that needs
|
||||||
|
// full performance. When the Lock instance is destroyed (goes out of scope), power saving will be re-enabled.
|
||||||
|
class Lock {
|
||||||
|
friend class HalPowerManager;
|
||||||
|
bool valid = false;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit Lock();
|
||||||
|
~Lock();
|
||||||
|
|
||||||
|
// Non-copyable and non-movable
|
||||||
|
Lock(const Lock&) = delete;
|
||||||
|
Lock& operator=(const Lock&) = delete;
|
||||||
|
Lock(Lock&&) = delete;
|
||||||
|
Lock& operator=(Lock&&) = delete;
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "Activity.h"
|
#include "Activity.h"
|
||||||
|
|
||||||
|
#include <HalPowerManager.h>
|
||||||
|
|
||||||
void Activity::renderTaskTrampoline(void* param) {
|
void Activity::renderTaskTrampoline(void* param) {
|
||||||
auto* self = static_cast<Activity*>(param);
|
auto* self = static_cast<Activity*>(param);
|
||||||
self->renderTaskLoop();
|
self->renderTaskLoop();
|
||||||
@@ -9,6 +11,7 @@ void Activity::renderTaskLoop() {
|
|||||||
while (true) {
|
while (true) {
|
||||||
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||||
{
|
{
|
||||||
|
HalPowerManager::Lock powerLock; // Ensure we don't go into low-power mode while rendering
|
||||||
RenderLock lock(*this);
|
RenderLock lock(*this);
|
||||||
render(std::move(lock));
|
render(std::move(lock));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
#include "ActivityWithSubactivity.h"
|
#include "ActivityWithSubactivity.h"
|
||||||
|
|
||||||
|
#include <HalPowerManager.h>
|
||||||
|
|
||||||
void ActivityWithSubactivity::renderTaskLoop() {
|
void ActivityWithSubactivity::renderTaskLoop() {
|
||||||
while (true) {
|
while (true) {
|
||||||
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||||
{
|
{
|
||||||
|
HalPowerManager::Lock powerLock; // Ensure we don't go into low-power mode while rendering
|
||||||
RenderLock lock(*this);
|
RenderLock lock(*this);
|
||||||
if (!subActivity) {
|
if (!subActivity) {
|
||||||
render(std::move(lock));
|
render(std::move(lock));
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class ClearCacheActivity final : public ActivityWithSubactivity {
|
|||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
void onExit() override;
|
void onExit() override;
|
||||||
void loop() override;
|
void loop() override;
|
||||||
|
bool skipLoopDelay() override { return true; } // Prevent power-saving mode
|
||||||
void render(Activity::RenderLock&&) override;
|
void render(Activity::RenderLock&&) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -34,4 +34,5 @@ class OtaUpdateActivity : public ActivityWithSubactivity {
|
|||||||
void loop() override;
|
void loop() override;
|
||||||
void render(Activity::RenderLock&&) override;
|
void render(Activity::RenderLock&&) override;
|
||||||
bool preventAutoSleep() override { return state == CHECKING_FOR_UPDATE || state == UPDATE_IN_PROGRESS; }
|
bool preventAutoSleep() override { return state == CHECKING_FOR_UPDATE || state == UPDATE_IN_PROGRESS; }
|
||||||
|
bool skipLoopDelay() override { return true; } // Prevent power-saving mode
|
||||||
};
|
};
|
||||||
|
|||||||
18
src/main.cpp
18
src/main.cpp
@@ -3,6 +3,7 @@
|
|||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <HalDisplay.h>
|
#include <HalDisplay.h>
|
||||||
#include <HalGPIO.h>
|
#include <HalGPIO.h>
|
||||||
|
#include <HalPowerManager.h>
|
||||||
#include <HalStorage.h>
|
#include <HalStorage.h>
|
||||||
#include <I18n.h>
|
#include <I18n.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
@@ -183,7 +184,7 @@ void verifyPowerButtonDuration() {
|
|||||||
if (abort) {
|
if (abort) {
|
||||||
// Button released too early. Returning to sleep.
|
// Button released too early. Returning to sleep.
|
||||||
// IMPORTANT: Re-arm the wakeup trigger before sleeping again
|
// IMPORTANT: Re-arm the wakeup trigger before sleeping again
|
||||||
gpio.startDeepSleep();
|
powerManager.startDeepSleep(gpio);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,7 +207,7 @@ void enterDeepSleep() {
|
|||||||
LOG_DBG("MAIN", "Power button press calibration value: %lu ms", t2 - t1);
|
LOG_DBG("MAIN", "Power button press calibration value: %lu ms", t2 - t1);
|
||||||
LOG_DBG("MAIN", "Entering deep sleep");
|
LOG_DBG("MAIN", "Entering deep sleep");
|
||||||
|
|
||||||
gpio.startDeepSleep();
|
powerManager.startDeepSleep(gpio);
|
||||||
}
|
}
|
||||||
|
|
||||||
void onGoHome();
|
void onGoHome();
|
||||||
@@ -283,6 +284,7 @@ void setup() {
|
|||||||
t1 = millis();
|
t1 = millis();
|
||||||
|
|
||||||
gpio.begin();
|
gpio.begin();
|
||||||
|
powerManager.begin();
|
||||||
|
|
||||||
// Only start serial if USB connected
|
// Only start serial if USB connected
|
||||||
if (gpio.isUsbConnected()) {
|
if (gpio.isUsbConnected()) {
|
||||||
@@ -319,7 +321,7 @@ void setup() {
|
|||||||
case HalGPIO::WakeupReason::AfterUSBPower:
|
case HalGPIO::WakeupReason::AfterUSBPower:
|
||||||
// If USB power caused a cold boot, go back to sleep
|
// If USB power caused a cold boot, go back to sleep
|
||||||
LOG_DBG("MAIN", "Wakeup reason: After USB Power");
|
LOG_DBG("MAIN", "Wakeup reason: After USB Power");
|
||||||
gpio.startDeepSleep();
|
powerManager.startDeepSleep(gpio);
|
||||||
break;
|
break;
|
||||||
case HalGPIO::WakeupReason::AfterFlash:
|
case HalGPIO::WakeupReason::AfterFlash:
|
||||||
// After flashing, just proceed to boot
|
// After flashing, just proceed to boot
|
||||||
@@ -391,7 +393,8 @@ void loop() {
|
|||||||
// Check for any user activity (button press or release) or active background work
|
// Check for any user activity (button press or release) or active background work
|
||||||
static unsigned long lastActivityTime = millis();
|
static unsigned long lastActivityTime = millis();
|
||||||
if (gpio.wasAnyPressed() || gpio.wasAnyReleased() || (currentActivity && currentActivity->preventAutoSleep())) {
|
if (gpio.wasAnyPressed() || gpio.wasAnyReleased() || (currentActivity && currentActivity->preventAutoSleep())) {
|
||||||
lastActivityTime = millis(); // Reset inactivity timer
|
lastActivityTime = millis(); // Reset inactivity timer
|
||||||
|
powerManager.setPowerSaving(false); // Restore normal CPU frequency on user activity
|
||||||
}
|
}
|
||||||
|
|
||||||
const unsigned long sleepTimeoutMs = SETTINGS.getSleepTimeoutMs();
|
const unsigned long sleepTimeoutMs = SETTINGS.getSleepTimeoutMs();
|
||||||
@@ -426,11 +429,12 @@ void loop() {
|
|||||||
// When an activity requests skip loop delay (e.g., webserver running), use yield() for faster response
|
// When an activity requests skip loop delay (e.g., webserver running), use yield() for faster response
|
||||||
// Otherwise, use longer delay to save power
|
// Otherwise, use longer delay to save power
|
||||||
if (currentActivity && currentActivity->skipLoopDelay()) {
|
if (currentActivity && currentActivity->skipLoopDelay()) {
|
||||||
yield(); // Give FreeRTOS a chance to run tasks, but return immediately
|
powerManager.setPowerSaving(false); // Make sure we're at full performance when skipLoopDelay is requested
|
||||||
|
yield(); // Give FreeRTOS a chance to run tasks, but return immediately
|
||||||
} else {
|
} else {
|
||||||
static constexpr unsigned long IDLE_POWER_SAVING_MS = 3000; // 3 seconds
|
if (millis() - lastActivityTime >= HalPowerManager::IDLE_POWER_SAVING_MS) {
|
||||||
if (millis() - lastActivityTime >= IDLE_POWER_SAVING_MS) {
|
|
||||||
// If we've been inactive for a while, increase the delay to save power
|
// If we've been inactive for a while, increase the delay to save power
|
||||||
|
powerManager.setPowerSaving(true); // Lower CPU frequency after extended inactivity
|
||||||
delay(50);
|
delay(50);
|
||||||
} else {
|
} else {
|
||||||
// Short delay to prevent tight loop while still being responsive
|
// Short delay to prevent tight loop while still being responsive
|
||||||
|
|||||||
Reference in New Issue
Block a user