mirror of
https://github.com/google/pebble.git
synced 2025-04-30 07:21:39 -04:00
381 lines
14 KiB
C
381 lines
14 KiB
C
/*
|
|
* Copyright 2024 Google LLC
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#include "services/common/battery/battery_state.h"
|
|
|
|
#include "board/board.h"
|
|
#include "debug/power_tracking.h"
|
|
#include "drivers/battery.h"
|
|
#include "kernel/events.h"
|
|
#include "kernel/util/stop.h"
|
|
#include "services/common/analytics/analytics.h"
|
|
#include "services/common/battery/battery_curve.h"
|
|
#include "services/common/battery/battery_monitor.h"
|
|
#include "services/common/new_timer/new_timer.h"
|
|
#include "services/common/system_task.h"
|
|
#include "syscall/syscall_internal.h"
|
|
#include "system/logging.h"
|
|
#include "system/passert.h"
|
|
#include "util/math.h"
|
|
#include "util/ratio.h"
|
|
|
|
#ifdef DEBUG_BATTERY_STATE
|
|
#define BATTERY_SAMPLE_RATE_MS 1000
|
|
#else
|
|
#define BATTERY_SAMPLE_RATE_MS (60 * 1000)
|
|
#endif
|
|
|
|
typedef void (*EntryFunc)(void);
|
|
|
|
typedef enum {
|
|
ConnectionStateInvalid,
|
|
ConnectionStateChargingPlugged,
|
|
ConnectionStateDischargingPlugged,
|
|
ConnectionStateDischargingUnplugged
|
|
} ConnectionStateID;
|
|
|
|
typedef struct ConnectionState {
|
|
EntryFunc enter;
|
|
} ConnectionState;
|
|
|
|
static void prv_update_plugged_change(void);
|
|
static void prv_update_done_charging(void);
|
|
|
|
static const ConnectionState s_transitions[] = {
|
|
[ConnectionStateChargingPlugged] = { .enter = prv_update_plugged_change },
|
|
[ConnectionStateDischargingPlugged] = { .enter = prv_update_done_charging },
|
|
[ConnectionStateDischargingUnplugged] = { .enter = prv_update_plugged_change }
|
|
};
|
|
|
|
typedef struct BatteryState {
|
|
uint64_t init_time;
|
|
uint32_t percent;
|
|
uint16_t voltage;
|
|
uint8_t skip_count;
|
|
ConnectionStateID connection;
|
|
} BatteryState;
|
|
|
|
static BatteryState s_last_battery_state;
|
|
static TimerID s_periodic_timer_id = TIMER_INVALID_ID;
|
|
static int s_analytics_previous_mv = 0;
|
|
|
|
static void prv_schedule_update(uint32_t delay, bool force_update);
|
|
PreciseBatteryChargeState prv_get_precise_charge_state(const BatteryState *state);
|
|
|
|
static void prv_transition(BatteryState *state, ConnectionStateID next_state) {
|
|
state->connection = next_state;
|
|
s_transitions[state->connection].enter();
|
|
}
|
|
|
|
static void prv_update_plugged_change(void) {
|
|
// If the connection state changed or we finished charging, reset the filter since we're
|
|
// probably switching to a new curve.
|
|
battery_state_reset_filter();
|
|
|
|
|
|
bool is_charging = battery_charge_controller_thinks_we_are_charging();
|
|
if (is_charging) {
|
|
analytics_stopwatch_start(ANALYTICS_DEVICE_METRIC_BATTERY_CHARGE_TIME, AnalyticsClient_System);
|
|
} else {
|
|
analytics_stopwatch_stop(ANALYTICS_DEVICE_METRIC_BATTERY_CHARGE_TIME);
|
|
}
|
|
|
|
bool is_plugged = battery_is_usb_connected();
|
|
if (is_plugged) {
|
|
analytics_stopwatch_start(ANALYTICS_DEVICE_METRIC_BATTERY_PLUGGED_TIME, AnalyticsClient_System);
|
|
} else {
|
|
analytics_stopwatch_stop(ANALYTICS_DEVICE_METRIC_BATTERY_PLUGGED_TIME);
|
|
}
|
|
}
|
|
|
|
static void prv_update_done_charging(void) {
|
|
prv_update_plugged_change();
|
|
|
|
// Amount in mV to drop the "Full" voltage by to briefly stay at 100% once unplugged
|
|
const uint16_t BATTERY_FULL_FUDGE_AMOUNT = 10;
|
|
PBL_LOG(LOG_LEVEL_DEBUG, "Done charging - Updating curve");
|
|
battery_curve_set_full_voltage(s_last_battery_state.voltage - BATTERY_FULL_FUDGE_AMOUNT);
|
|
}
|
|
|
|
static void battery_state_put_change_event(PreciseBatteryChargeState state) {
|
|
PebbleEvent e = {
|
|
.type = PEBBLE_BATTERY_STATE_CHANGE_EVENT,
|
|
.battery_state = {
|
|
.new_state = state,
|
|
},
|
|
};
|
|
event_put(&e);
|
|
}
|
|
|
|
void battery_state_reset_filter(void) {
|
|
s_last_battery_state.voltage = battery_get_millivolts();
|
|
// Reset the stabilization timer in case we encountered a current spike during the reset
|
|
s_last_battery_state.init_time = rtc_get_ticks();
|
|
}
|
|
|
|
static uint32_t prv_filter_voltage(uint32_t avg_mv, uint32_t battery_mv) {
|
|
// Basic low-pass filter - See PBL-23637
|
|
const uint8_t VOLTAGE_FILTER_BETA = 2;
|
|
uint32_t avg = (avg_mv << VOLTAGE_FILTER_BETA);
|
|
avg -= avg_mv;
|
|
avg += battery_mv;
|
|
return avg >> VOLTAGE_FILTER_BETA;
|
|
}
|
|
|
|
static bool prv_is_stable(const BatteryState *state) {
|
|
// After a reboot, we typically source a lot of current which can drastically impact
|
|
// our mV readings due to the internal resistance of the battery. We use the
|
|
// system_likely_stabilized flag as an indicator of how trustworthy our readings are
|
|
const uint64_t STABLE_TICKS = 3 * 60 * RTC_TICKS_HZ;
|
|
uint64_t elapsed_ticks = rtc_get_ticks() - state->init_time;
|
|
return elapsed_ticks > STABLE_TICKS;
|
|
}
|
|
|
|
static ConnectionStateID prv_get_connection_state(void) {
|
|
const bool charging = battery_charge_controller_thinks_we_are_charging();
|
|
const bool plugged_in = battery_is_usb_connected();
|
|
|
|
if (plugged_in) {
|
|
if (charging) {
|
|
return ConnectionStateChargingPlugged;
|
|
} else {
|
|
return ConnectionStateDischargingPlugged;
|
|
}
|
|
} else {
|
|
if (charging) {
|
|
// Since we can't be charging and disconnected,
|
|
// just log a warning and pretend we aren't charging.
|
|
PBL_LOG(LOG_LEVEL_WARNING, "PMIC reported charging while unplugged - ignoring");
|
|
}
|
|
return ConnectionStateDischargingUnplugged;
|
|
}
|
|
}
|
|
|
|
static void prv_update_state(void *force_update) {
|
|
const uint8_t MAX_SAMPLE_SKIPS = 5;
|
|
bool forced = (bool)force_update;
|
|
// Large current draws will cause the voltage supplied by the battery to
|
|
// droop. We try to only sample the battery when there is minimal
|
|
// activity. We look to see if stop mode is allowed because this is a good
|
|
// indicator that no peripherals are in use (i.e vibe, backlight, etc)
|
|
if ((s_last_battery_state.skip_count < MAX_SAMPLE_SKIPS) &&
|
|
!forced && !stop_mode_is_allowed()) {
|
|
s_last_battery_state.skip_count++;
|
|
return;
|
|
}
|
|
|
|
if (s_last_battery_state.skip_count == MAX_SAMPLE_SKIPS) {
|
|
analytics_inc(ANALYTICS_DEVICE_METRIC_BATTERY_SAMPLE_SKIP_COUNT_EXCEEDED,
|
|
AnalyticsClient_System);
|
|
}
|
|
s_last_battery_state.skip_count = 0;
|
|
|
|
// Driver communication
|
|
|
|
bool state_changed = false;
|
|
ConnectionStateID next_state = prv_get_connection_state();
|
|
if (s_last_battery_state.connection != next_state) {
|
|
// Do not allow DischargingPlugged -> ChargingPlugged state
|
|
if ((s_last_battery_state.connection != ConnectionStateDischargingPlugged) ||
|
|
(next_state != ConnectionStateChargingPlugged)) {
|
|
prv_transition(&s_last_battery_state, next_state);
|
|
state_changed = true;
|
|
}
|
|
}
|
|
|
|
s_last_battery_state.voltage = prv_filter_voltage(s_last_battery_state.voltage,
|
|
battery_get_millivolts());
|
|
bool charging = (s_last_battery_state.connection == ConnectionStateChargingPlugged);
|
|
|
|
// Update Percent & Filtering
|
|
|
|
const uint32_t ALWAYS_UPDATE_THRESHOLD = ratio32_from_percent(10);
|
|
bool likely_stable = prv_is_stable(&s_last_battery_state);
|
|
|
|
uint32_t new_charge_percent =
|
|
battery_curve_sample_ratio32_charge_percent(s_last_battery_state.voltage, charging);
|
|
#ifndef TARGET_QEMU
|
|
// If QEMU, allow updates to always occur for ease of testing otherwise
|
|
// Allow updates iff:
|
|
// - We are charging
|
|
// - We are discharging and:
|
|
// - The readings have stabilized and the battery percent did not go up
|
|
// - The readings have not yet stablized
|
|
// TL;DR: Allow updates unless we're stable and discharging but the % went up.
|
|
if (!charging && likely_stable &&
|
|
new_charge_percent > s_last_battery_state.percent) {
|
|
// It's okay to return early since any connection/plugged changes will reset the filter,
|
|
// so we won't catch those.
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
s_last_battery_state.percent = new_charge_percent;
|
|
|
|
PBL_LOG(LOG_LEVEL_DEBUG, "mV Raw: %"PRIu16" Ratio: %"PRIu32" Percent: %"PRIu32,
|
|
s_last_battery_state.voltage, s_last_battery_state.percent,
|
|
ratio32_to_percent(s_last_battery_state.percent));
|
|
|
|
PWR_TRACK_BATT(charging ? "CHARGING" : "DISCHARGING", s_last_battery_state.voltage);
|
|
|
|
if (forced || likely_stable || s_last_battery_state.percent <= ALWAYS_UPDATE_THRESHOLD ||
|
|
charging || state_changed) {
|
|
battery_state_put_change_event(prv_get_precise_charge_state(&s_last_battery_state));
|
|
}
|
|
}
|
|
|
|
static void prv_update_callback(void *data) {
|
|
// Running the battery monitor on the timer task is not a good idea because
|
|
// we could be sampling right in the middle of a flash erase, etc. Therefore,
|
|
// dispatch to a lower priority task
|
|
system_task_add_callback(prv_update_state, data);
|
|
|
|
// Reschedule ourselves again so we create a loop
|
|
prv_schedule_update(BATTERY_SAMPLE_RATE_MS, false);
|
|
}
|
|
|
|
static void prv_schedule_update(uint32_t delay, bool force_update) {
|
|
bool success = new_timer_start(s_periodic_timer_id, delay, prv_update_callback,
|
|
(void *)force_update, 0 /*flags*/);
|
|
PBL_ASSERTN(success);
|
|
}
|
|
|
|
void battery_state_force_update(void) {
|
|
// Fire off our periodic timer. Note that we rely on the callback to reschedule the timer
|
|
// for 1 minute intervals rather than create it as a repeating timer. This is because
|
|
// we occasionally want the callback to get triggered immediately
|
|
// (in response to the charging cable being plugged in). In these instances, we reschedule it
|
|
// from the main task.
|
|
prv_schedule_update(0, true);
|
|
}
|
|
|
|
void battery_state_init(void) {
|
|
s_periodic_timer_id = new_timer_create();
|
|
|
|
s_last_battery_state = (BatteryState) { .connection = ConnectionStateDischargingUnplugged };
|
|
battery_state_reset_filter();
|
|
battery_state_force_update();
|
|
|
|
s_analytics_previous_mv = s_last_battery_state.voltage;
|
|
}
|
|
|
|
void battery_state_handle_connection_event(bool is_connected) {
|
|
static const uint32_t RECONNECTION_DELAY_MS = 1000;
|
|
|
|
PBL_LOG_VERBOSE("USB Connected:%d", is_connected);
|
|
|
|
// Trigger a reset update to the state machine. Delay the update to allow the battery voltage to
|
|
// settle and to debounce reconnection events
|
|
prv_schedule_update(RECONNECTION_DELAY_MS, true);
|
|
}
|
|
|
|
PreciseBatteryChargeState prv_get_precise_charge_state(const BatteryState *state) {
|
|
PreciseBatteryChargeState event_state = {
|
|
.charge_percent = state->percent,
|
|
.is_charging = (s_last_battery_state.connection == ConnectionStateChargingPlugged),
|
|
.is_plugged = (s_last_battery_state.connection != ConnectionStateDischargingUnplugged)
|
|
};
|
|
return event_state;
|
|
}
|
|
|
|
DEFINE_SYSCALL(BatteryChargeState, sys_battery_get_charge_state, void) {
|
|
return battery_get_charge_state();
|
|
}
|
|
|
|
BatteryChargeState battery_get_charge_state(void) {
|
|
bool is_plugged = (s_last_battery_state.connection != ConnectionStateDischargingUnplugged);
|
|
|
|
int32_t percent = ratio32_to_percent(s_last_battery_state.percent);
|
|
|
|
// subtract low power reserve, so developer will see 0% when we're approaching low power mode
|
|
int32_t percent_normalized = MAX((percent - BOARD_CONFIG_POWER.low_power_threshold
|
|
+ percent / (100 / BOARD_CONFIG_POWER.low_power_threshold)), 0);
|
|
|
|
// massage rounding factor so that between 100% to 50% charge the SOC reported is biased to a
|
|
// higher charge percent bin.
|
|
int32_t rounding_factor = 5 + MAX(((percent - 50) / 10), 0);
|
|
BatteryChargeState state = {
|
|
.charge_percent = MIN(10 * ((percent_normalized + rounding_factor) / 10), 100),
|
|
.is_charging = is_plugged && percent_normalized < 100,
|
|
.is_plugged = is_plugged,
|
|
};
|
|
return state;
|
|
}
|
|
|
|
// For unit tests
|
|
TimerID battery_state_get_periodic_timer_id(void) {
|
|
return s_periodic_timer_id;
|
|
}
|
|
|
|
uint16_t battery_state_get_voltage(void) {
|
|
return s_last_battery_state.voltage;
|
|
}
|
|
|
|
|
|
#include "console/prompt.h"
|
|
void command_print_battery_status(void) {
|
|
char buffer[32];
|
|
PreciseBatteryChargeState state = prv_get_precise_charge_state(&s_last_battery_state);
|
|
prompt_send_response_fmt(buffer, 32, "%"PRIu16" mV", s_last_battery_state.voltage);
|
|
prompt_send_response_fmt(buffer, 32,
|
|
"batt_percent: %"PRIu32"%%", ratio32_to_percent(state.charge_percent));
|
|
prompt_send_response_fmt(buffer, 32, "plugged: %s", state.is_plugged ? "YES" : "NO");
|
|
prompt_send_response_fmt(buffer, 32, "charging: %s", state.is_charging ? "YES" : "NO");
|
|
}
|
|
|
|
/////////////////
|
|
// Analytics
|
|
|
|
// Note that this is run on a different thread than battery_state!
|
|
void analytics_external_collect_battery(void) {
|
|
// This should not be called for an hour after bootup
|
|
|
|
int battery_mv = s_last_battery_state.voltage;
|
|
|
|
int d_mv = battery_mv - s_analytics_previous_mv;
|
|
analytics_set(ANALYTICS_DEVICE_METRIC_BATTERY_VOLTAGE, battery_mv, AnalyticsClient_System);
|
|
analytics_set(ANALYTICS_DEVICE_METRIC_BATTERY_VOLTAGE_DELTA, d_mv, AnalyticsClient_System);
|
|
|
|
int scaling_factor = INT32_MAX / 100; // we want to cover -100 to 100 percent
|
|
// Note: we assume that the watch was not charging during the hour.
|
|
int32_t start_percent = battery_curve_lookup_percent_with_scaling_factor(s_analytics_previous_mv,
|
|
false, scaling_factor);
|
|
int32_t curr_percent = battery_curve_lookup_percent_with_scaling_factor(battery_mv, false,
|
|
scaling_factor);
|
|
int32_t d_percent = curr_percent - start_percent;
|
|
|
|
s_analytics_previous_mv = battery_mv;
|
|
analytics_set(ANALYTICS_DEVICE_METRIC_BATTERY_PERCENT_DELTA, d_percent, AnalyticsClient_System);
|
|
|
|
analytics_set(ANALYTICS_DEVICE_METRIC_BATTERY_PERCENT,
|
|
ratio32_to_percent(s_last_battery_state.percent),
|
|
AnalyticsClient_System);
|
|
}
|
|
|
|
static void prv_set_forced_charge_state(bool is_charging) {
|
|
battery_force_charge_enable(is_charging);
|
|
|
|
// Trigger an immediate update to the state machine: may trigger an event
|
|
battery_state_force_update();
|
|
}
|
|
|
|
void command_battery_charge_option(const char* option) {
|
|
if (!strcmp("disable", option)) {
|
|
prv_set_forced_charge_state(false);
|
|
} else if (!strcmp("enable", option)) {
|
|
prv_set_forced_charge_state(true);
|
|
}
|
|
}
|