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**
2026-02-18 15:12:29 +01:00
|
|
|
#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();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 15:05:35 -08:00
|
|
|
uint16_t HalPowerManager::getBatteryPercentage() const {
|
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**
2026-02-18 15:12:29 +01:00
|
|
|
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);
|
|
|
|
|
}
|