Fix T-Deck battery estimate

This commit is contained in:
Trail Mate Dev
2026-05-30 16:46:24 +08:00
parent ff98125e91
commit fad0ca9bb2
13 changed files with 417 additions and 103 deletions
@@ -30,6 +30,8 @@ MemoryStats memory_stats();
const char* firmware_version();
void handle_low_battery(const BatteryInfo& info);
bool supports_screen_brightness();
bool supports_configurable_battery_gauge();
void reload_configurable_battery_gauge();
uint8_t screen_brightness();
void set_screen_brightness(uint8_t level);
void trigger_haptic();
@@ -0,0 +1,62 @@
#pragma once
#include <cstdint>
namespace power
{
struct BatteryEstimatorSample
{
uint32_t now_ms = 0;
int pmu_percent = -1;
int pmu_battery_mv = -1;
int adc_battery_mv = -1;
bool charging = false;
bool vbus_present = false;
};
enum class BatteryEstimateReason
{
NoSample,
PmuPercent,
VoltageFallback,
ChargerStableHold,
DetachStableHold,
DropGuard,
ZeroGuard,
RateLimited,
};
struct BatteryEstimate
{
BatteryEstimate() = default;
BatteryEstimate(int percent_value, BatteryEstimateReason reason_value)
: percent(percent_value), reason(reason_value)
{
}
int percent = -1;
BatteryEstimateReason reason = BatteryEstimateReason::NoSample;
};
class BatteryEstimator
{
public:
BatteryEstimate update(const BatteryEstimatorSample& sample);
int lastPercent() const { return last_percent_; }
uint32_t lastVbusDetachMs() const { return last_vbus_detach_ms_; }
void reset();
private:
int last_percent_ = -1;
uint32_t last_sample_ms_ = 0;
uint32_t last_vbus_detach_ms_ = 0;
bool has_last_power_state_ = false;
bool last_vbus_present_ = false;
bool last_charging_ = false;
uint8_t zero_streak_ = 0;
};
int batteryPercentFromLipoMillivolts(int mv);
} // namespace power
@@ -0,0 +1,188 @@
#include "power/battery_estimator.h"
#include <cstddef>
namespace power
{
namespace
{
constexpr uint32_t kMinReadIntervalMs = 1000;
constexpr uint32_t kDetachStabilizeMs = 120000;
constexpr int kDropGuardPct = 12;
constexpr int kChargeDropGuardPct = 3;
constexpr int kAllowedUnplugDropPct = 5;
constexpr int kVoltageSupportMarginPct = 10;
constexpr int kRiseGuardPct = 2;
constexpr int kZeroGuardMinPct = 15;
constexpr uint8_t kZeroGuardCount = 3;
int clamp_percent(int percent)
{
if (percent < 0)
{
return -1;
}
if (percent > 100)
{
return 100;
}
return percent;
}
int best_voltage_percent(const BatteryEstimatorSample& sample)
{
int percent = batteryPercentFromLipoMillivolts(sample.pmu_battery_mv);
if (percent < 0)
{
percent = batteryPercentFromLipoMillivolts(sample.adc_battery_mv);
}
return percent;
}
bool voltage_supports_last(int voltage_percent, int last_percent)
{
return voltage_percent >= 0 && last_percent >= 0 &&
voltage_percent + kVoltageSupportMarginPct >= last_percent;
}
} // namespace
int batteryPercentFromLipoMillivolts(int mv)
{
if (mv <= 0)
{
return -1;
}
static constexpr int kCurve[][2] = {
{4200, 100},
{4100, 90},
{4000, 80},
{3900, 60},
{3800, 40},
{3700, 20},
{3600, 10},
{3300, 0},
};
if (mv >= kCurve[0][0])
{
return 100;
}
if (mv <= kCurve[7][0])
{
return 0;
}
for (std::size_t i = 0; i + 1 < (sizeof(kCurve) / sizeof(kCurve[0])); ++i)
{
const int v1 = kCurve[i][0];
const int p1 = kCurve[i][1];
const int v2 = kCurve[i + 1][0];
const int p2 = kCurve[i + 1][1];
if (mv <= v1 && mv >= v2)
{
return p2 + (mv - v2) * (p1 - p2) / (v1 - v2);
}
}
return -1;
}
BatteryEstimate BatteryEstimator::update(const BatteryEstimatorSample& sample)
{
const bool was_powered = has_last_power_state_ && (last_vbus_present_ || last_charging_);
const bool is_powered = sample.vbus_present || sample.charging;
if (was_powered && !is_powered)
{
last_vbus_detach_ms_ = sample.now_ms;
}
has_last_power_state_ = true;
last_vbus_present_ = sample.vbus_present;
last_charging_ = sample.charging;
if (last_percent_ >= 0 && last_sample_ms_ != 0 &&
sample.now_ms - last_sample_ms_ < kMinReadIntervalMs)
{
return {last_percent_, BatteryEstimateReason::RateLimited};
}
const int voltage_percent = best_voltage_percent(sample);
int level = clamp_percent(sample.pmu_percent);
BatteryEstimateReason reason = BatteryEstimateReason::PmuPercent;
if (level < 0)
{
level = voltage_percent;
reason = BatteryEstimateReason::VoltageFallback;
}
if (level < 0)
{
last_sample_ms_ = sample.now_ms;
return {last_percent_, BatteryEstimateReason::NoSample};
}
if (last_percent_ >= 0)
{
if (!sample.charging && level > last_percent_ + kRiseGuardPct)
{
level = last_percent_;
reason = BatteryEstimateReason::DropGuard;
}
if (sample.charging && level + kChargeDropGuardPct < last_percent_)
{
level = last_percent_;
reason = BatteryEstimateReason::ChargerStableHold;
}
const bool in_detach_window =
last_vbus_detach_ms_ != 0 && sample.now_ms - last_vbus_detach_ms_ <= kDetachStabilizeMs;
if (!is_powered && in_detach_window && level + kAllowedUnplugDropPct < last_percent_ &&
voltage_supports_last(voltage_percent, last_percent_))
{
level = last_percent_;
reason = BatteryEstimateReason::DetachStableHold;
}
if (!sample.charging && level + kDropGuardPct < last_percent_ &&
voltage_supports_last(voltage_percent, last_percent_))
{
level = last_percent_;
reason = BatteryEstimateReason::DropGuard;
}
}
if (!sample.charging && level == 0 && last_percent_ >= kZeroGuardMinPct)
{
if (zero_streak_ < kZeroGuardCount)
{
++zero_streak_;
last_sample_ms_ = sample.now_ms;
return {last_percent_, BatteryEstimateReason::ZeroGuard};
}
}
else
{
zero_streak_ = 0;
}
last_percent_ = level;
last_sample_ms_ = sample.now_ms;
return {last_percent_, reason};
}
void BatteryEstimator::reset()
{
last_percent_ = -1;
last_sample_ms_ = 0;
last_vbus_detach_ms_ = 0;
has_last_power_state_ = false;
last_vbus_present_ = false;
last_charging_ = false;
zero_streak_ = 0;
}
} // namespace power
@@ -0,0 +1,91 @@
#include "power/battery_estimator.h"
#include <cassert>
namespace
{
power::BatteryEstimatorSample sample(uint32_t now_ms,
int pmu_percent,
int pmu_battery_mv,
int adc_battery_mv,
bool charging,
bool vbus_present)
{
power::BatteryEstimatorSample s{};
s.now_ms = now_ms;
s.pmu_percent = pmu_percent;
s.pmu_battery_mv = pmu_battery_mv;
s.adc_battery_mv = adc_battery_mv;
s.charging = charging;
s.vbus_present = vbus_present;
return s;
}
void holds_full_reading_during_unplug_stabilization()
{
power::BatteryEstimator estimator;
auto result = estimator.update(sample(1000, 100, 4200, 4190, true, true));
assert(result.percent == 100);
result = estimator.update(sample(3000, 82, 4120, 4110, false, false));
assert(result.percent == 100);
assert(result.reason == power::BatteryEstimateReason::DetachStableHold);
}
void accepts_sustained_low_voltage_after_detach_window()
{
power::BatteryEstimator estimator;
auto result = estimator.update(sample(1000, 100, 4200, 4190, true, true));
assert(result.percent == 100);
result = estimator.update(sample(130000, 72, 3950, 3940, false, false));
assert(result.percent == 72);
}
void uses_voltage_curve_when_pmu_percent_is_invalid()
{
power::BatteryEstimator estimator;
auto result = estimator.update(sample(1000, -1, 3900, -1, false, false));
assert(result.percent == 60);
assert(result.reason == power::BatteryEstimateReason::VoltageFallback);
}
void suppresses_small_unplugged_rises()
{
power::BatteryEstimator estimator;
auto result = estimator.update(sample(1000, 74, 3970, 3970, false, false));
assert(result.percent == 74);
result = estimator.update(sample(3000, 79, 3980, 3980, false, false));
assert(result.percent == 74);
assert(result.reason == power::BatteryEstimateReason::DropGuard);
}
void guards_repeated_fake_zero_samples()
{
power::BatteryEstimator estimator;
auto result = estimator.update(sample(1000, 45, 3820, 3820, false, false));
assert(result.percent == 45);
result = estimator.update(sample(3000, 0, -1, -1, false, false));
assert(result.percent == 45);
assert(result.reason == power::BatteryEstimateReason::ZeroGuard);
}
} // namespace
int main()
{
assert(power::batteryPercentFromLipoMillivolts(4200) == 100);
assert(power::batteryPercentFromLipoMillivolts(4000) == 80);
assert(power::batteryPercentFromLipoMillivolts(3900) == 60);
assert(power::batteryPercentFromLipoMillivolts(3300) == 0);
holds_full_reading_during_unplug_stabilization();
accepts_sustained_low_voltage_after_detach_window();
uses_voltage_curve_when_pmu_percent_is_invalid();
suppresses_small_unplugged_rises();
guards_repeated_fake_zero_samples();
return 0;
}