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() {
|
2026-04-04 09:25:43 -06:00
|
|
|
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);
|
|
|
|
|
}
|
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
|
|
|
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();
|
|
|
|
|
}
|
fix: use sleep routine from the original firmware (#1298)
## Summary
Fixes #1263
I spent half of my day(-off) reverse engineering the stock english
firmware V3.1.1, it's more or less like solving a sudoku with some known
pieces (like debug strings, known static addresses, known compiled
function, etc) and then the task is to guess the rest.
Long story short, this is the sleep routine that they use:
<img width="674" height="604" alt="image"
src="https://github.com/user-attachments/assets/6d53ce44-7bae-40c7-b4fb-24f898dbcc05"
/>
From the code above:
- They pull down GPIO13 (value = 0xd) before sleep
- They verify that power button is released by doing a delay loop of
50ms, similar to what we're doing
- `esp_sleep_config_gpio_isolate` is called but I'm not 100% sure why
- Pull up power button, note that it's likely redundant because power
button should already pulled up by `InputManager`
- `param1` and `param2` means enabling front/side buttons for wake up,
but it doesn't used in the code in reality. But I think it's physically
impossible, see the explanation below
- `param3` means "wake up from power button"
- `esp_sleep_start` is used; there is a logic to handle if it fails to
sleep, then retry recursively (no idea why!)
My observation is that they use GPIO13 so that it will be on HIGH state
when the chip is powered on, without any user space code to keep it on
that state. And once going to deep sleep, it goes into FLOATING by
default. That may explain why it need to be in LOW state before going to
sleep. (Nice trick btw)
Looking again at the circuit diagram provided
[here](https://github.com/sunwoods/Xteink-X4/blob/main/readme-img/sch.jpg)
(note: it's not official):
<img width="705" height="384" alt="image"
src="https://github.com/user-attachments/assets/b98d59fd-47ca-4d3d-a24a-94bf999e957b"
/>
It kinda make sense as the GPIO13 and VBUS (USB VCC) have the same role,
they are part of a simple "battery protection" cirtuit
Now, we may wonder, how the device wake up when there is no battery at
all?
<img width="440" height="323" alt="image"
src="https://github.com/user-attachments/assets/2981c411-239b-49a7-b9f7-9a75b6c1b6d3"
/>
It seems like power button is not just a simple switch between GPIO3 and
ground, but it also linked the POWER_CTRL, which leads to nowhere on the
diagram, but I suppose it connects the battery back for a short amount
of time, just enough for the MCU to wake up, and GPIO13 goes HIGH again.
It may also explain why power button becomes non-responsive for ~1
second after power on, as it's being pulled up by the current from
battery (remind: high = not pressed, low = pressed)
To test the theory above, I simply **comment out** the
`esp_deep_sleep_enable_gpio_wakeup`:
- On battery, power button works as nothing happen
- On USB, it doesn't wake up, I need to press RST
---
Important things about my analysis:
1. I had to name every function on the code above **manually**, but I'm
99% confident about it. The only function that I'm not sure is
`esp_wifi_bt_power_domain_off` ; Edit: it was indeed mislabeled, see
https://github.com/crosspoint-reader/crosspoint-reader/pull/1298#discussion_r2879670852
2. Some logic inside the stock firmware looks very strange, there is
almost no mention to "arduino" in the hardware, suggesting that they may
just call esp-idf functions directly, bypassing the arduino abstraction.
---
### 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**
---------
Co-authored-by: Zach Nelson <zach@zdnelson.com>
2026-03-21 19:35:38 +01:00
|
|
|
// 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);
|
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
|
|
|
// Arm the wakeup trigger *after* the button is released
|
fix: use sleep routine from the original firmware (#1298)
## Summary
Fixes #1263
I spent half of my day(-off) reverse engineering the stock english
firmware V3.1.1, it's more or less like solving a sudoku with some known
pieces (like debug strings, known static addresses, known compiled
function, etc) and then the task is to guess the rest.
Long story short, this is the sleep routine that they use:
<img width="674" height="604" alt="image"
src="https://github.com/user-attachments/assets/6d53ce44-7bae-40c7-b4fb-24f898dbcc05"
/>
From the code above:
- They pull down GPIO13 (value = 0xd) before sleep
- They verify that power button is released by doing a delay loop of
50ms, similar to what we're doing
- `esp_sleep_config_gpio_isolate` is called but I'm not 100% sure why
- Pull up power button, note that it's likely redundant because power
button should already pulled up by `InputManager`
- `param1` and `param2` means enabling front/side buttons for wake up,
but it doesn't used in the code in reality. But I think it's physically
impossible, see the explanation below
- `param3` means "wake up from power button"
- `esp_sleep_start` is used; there is a logic to handle if it fails to
sleep, then retry recursively (no idea why!)
My observation is that they use GPIO13 so that it will be on HIGH state
when the chip is powered on, without any user space code to keep it on
that state. And once going to deep sleep, it goes into FLOATING by
default. That may explain why it need to be in LOW state before going to
sleep. (Nice trick btw)
Looking again at the circuit diagram provided
[here](https://github.com/sunwoods/Xteink-X4/blob/main/readme-img/sch.jpg)
(note: it's not official):
<img width="705" height="384" alt="image"
src="https://github.com/user-attachments/assets/b98d59fd-47ca-4d3d-a24a-94bf999e957b"
/>
It kinda make sense as the GPIO13 and VBUS (USB VCC) have the same role,
they are part of a simple "battery protection" cirtuit
Now, we may wonder, how the device wake up when there is no battery at
all?
<img width="440" height="323" alt="image"
src="https://github.com/user-attachments/assets/2981c411-239b-49a7-b9f7-9a75b6c1b6d3"
/>
It seems like power button is not just a simple switch between GPIO3 and
ground, but it also linked the POWER_CTRL, which leads to nowhere on the
diagram, but I suppose it connects the battery back for a short amount
of time, just enough for the MCU to wake up, and GPIO13 goes HIGH again.
It may also explain why power button becomes non-responsive for ~1
second after power on, as it's being pulled up by the current from
battery (remind: high = not pressed, low = pressed)
To test the theory above, I simply **comment out** the
`esp_deep_sleep_enable_gpio_wakeup`:
- On battery, power button works as nothing happen
- On USB, it doesn't wake up, I need to press RST
---
Important things about my analysis:
1. I had to name every function on the code above **manually**, but I'm
99% confident about it. The only function that I'm not sure is
`esp_wifi_bt_power_domain_off` ; Edit: it was indeed mislabeled, see
https://github.com/crosspoint-reader/crosspoint-reader/pull/1298#discussion_r2879670852
2. Some logic inside the stock firmware looks very strange, there is
almost no mention to "arduino" in the hardware, suggesting that they may
just call esp-idf functions directly, bypassing the arduino abstraction.
---
### 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**
---------
Co-authored-by: Zach Nelson <zach@zdnelson.com>
2026-03-21 19:35:38 +01:00
|
|
|
// 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
|
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
|
|
|
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 {
|
2026-04-04 09:25:43 -06:00
|
|
|
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;
|
|
|
|
|
}
|
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);
|
2026-04-04 09:25:43 -06:00
|
|
|
_batteryCachedPercent = battery.readPercentage();
|
|
|
|
|
return _batteryCachedPercent;
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|