You've already forked crosspoint-reader
mirror of
https://github.com/crosspoint-reader/crosspoint-reader.git
synced 2026-04-29 10:26:52 -07:00
81ae9dd779
## Summary Battery percentage is calculated from the voltage, which is not totally stable. We smooth the battery percentage using a moving average. ## Additional Context issue discussion: https://github.com/crosspoint-reader/crosspoint-reader/issues/1444 --- ### 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? PARTIALLY
150 lines
4.8 KiB
C++
150 lines
4.8 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();
|
|
}
|
|
// 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);
|
|
}
|