feat(power): add battery monitoring and status overlays

This commit is contained in:
chenjintao
2026-08-18 11:53:14 +08:00
parent 609fe978f1
commit 8f8254b397
31 changed files with 1655 additions and 22 deletions
@@ -0,0 +1,361 @@
/*
* SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "domain.h"
#include <stdlib.h>
#include <string.h>
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "platform.h"
#include "sdkconfig.h"
#if CONFIG_TQ_BATTERY_ENABLE
#if CONFIG_TQ_BATTERY_LOW_EXIT_PERCENT <= CONFIG_TQ_BATTERY_LOW_ENTER_PERCENT
#error "Battery low recovery threshold must exceed the entry threshold"
#endif
#if CONFIG_TQ_BATTERY_CRITICAL_EXIT_PERCENT <= CONFIG_TQ_BATTERY_CRITICAL_ENTER_PERCENT
#error "Battery critical recovery threshold must exceed the entry threshold"
#endif
#if CONFIG_TQ_BATTERY_CRITICAL_ENTER_PERCENT >= CONFIG_TQ_BATTERY_LOW_ENTER_PERCENT
#error "Battery critical entry threshold must be below the low entry threshold"
#endif
#define BATTERY_STATE_CONFIRM_SAMPLES 3U
#define BATTERY_MAX_ACCEPTED_STEP_MV 300
#define BATTERY_JUMP_MATCH_WINDOW_MV 100
#define BATTERY_FILTER_OLD_WEIGHT 8
#define BATTERY_FILTER_NEW_WEIGHT 2
typedef struct {
int voltage_mv;
uint8_t percentage;
} battery_curve_point_t;
static const battery_curve_point_t s_discharge_curve[] = {
{3300, 0},
{3400, 2},
{3600, 8},
{3700, 18},
{3800, 35},
{3900, 55},
{4000, 75},
{4100, 90},
{4200, 100},
};
static StaticSemaphore_t s_lock_buffer;
static SemaphoreHandle_t s_lock;
static portMUX_TYPE s_lock_init_guard = portMUX_INITIALIZER_UNLOCKED;
static domain_battery_status_t s_status;
static bool s_initialized;
static bool s_filter_initialized;
static int s_filtered_mv;
static int s_jump_candidate_mv;
static uint8_t s_jump_candidate_count;
static uint8_t s_low_count;
static uint8_t s_critical_count;
static bool ensure_lock(void)
{
if (s_lock != NULL) {
return true;
}
portENTER_CRITICAL(&s_lock_init_guard);
if (s_lock == NULL) {
s_lock = xSemaphoreCreateMutexStatic(&s_lock_buffer);
}
portEXIT_CRITICAL(&s_lock_init_guard);
return s_lock != NULL;
}
static bool lock_state(void)
{
return ensure_lock() && xSemaphoreTake(s_lock, pdMS_TO_TICKS(100)) == pdTRUE;
}
static void unlock_state(void)
{
(void)xSemaphoreGive(s_lock);
}
static domain_battery_charge_state_t map_charge_state(
platform_battery_charge_state_t state)
{
switch (state) {
case PLATFORM_BATTERY_CHARGE_CHARGING:
return DOMAIN_BATTERY_CHARGE_CHARGING;
case PLATFORM_BATTERY_CHARGE_NOT_CHARGING:
return DOMAIN_BATTERY_CHARGE_NOT_CHARGING;
default:
return DOMAIN_BATTERY_CHARGE_UNKNOWN;
}
}
static uint8_t voltage_to_percentage(int voltage_mv)
{
if (voltage_mv <= s_discharge_curve[0].voltage_mv) {
return s_discharge_curve[0].percentage;
}
const size_t count = sizeof(s_discharge_curve) / sizeof(s_discharge_curve[0]);
for (size_t i = 1; i < count; ++i) {
if (voltage_mv <= s_discharge_curve[i].voltage_mv) {
int lower_mv = s_discharge_curve[i - 1U].voltage_mv;
int upper_mv = s_discharge_curve[i].voltage_mv;
int lower_pct = s_discharge_curve[i - 1U].percentage;
int upper_pct = s_discharge_curve[i].percentage;
return (uint8_t)(lower_pct +
(voltage_mv - lower_mv) * (upper_pct - lower_pct) /
(upper_mv - lower_mv));
}
}
return 100U;
}
static bool accept_voltage_step(int voltage_mv)
{
if (!s_filter_initialized || abs(voltage_mv - s_filtered_mv) <= BATTERY_MAX_ACCEPTED_STEP_MV) {
s_jump_candidate_count = 0U;
return true;
}
if (s_jump_candidate_count == 0U ||
abs(voltage_mv - s_jump_candidate_mv) > BATTERY_JUMP_MATCH_WINDOW_MV) {
s_jump_candidate_mv = voltage_mv;
s_jump_candidate_count = 1U;
return false;
}
if (s_jump_candidate_count < BATTERY_STATE_CONFIRM_SAMPLES) {
++s_jump_candidate_count;
}
if (s_jump_candidate_count < BATTERY_STATE_CONFIRM_SAMPLES) {
return false;
}
s_jump_candidate_count = 0U;
return true;
}
static void update_level_state(uint8_t percentage,
domain_battery_charge_state_t charge_state)
{
if (charge_state == DOMAIN_BATTERY_CHARGE_CHARGING &&
s_status.level_state == DOMAIN_BATTERY_LEVEL_CRITICAL) {
s_status.level_state = DOMAIN_BATTERY_LEVEL_LOW;
s_critical_count = 0U;
}
switch (s_status.level_state) {
case DOMAIN_BATTERY_LEVEL_UNKNOWN:
case DOMAIN_BATTERY_LEVEL_NORMAL:
s_status.level_state = DOMAIN_BATTERY_LEVEL_NORMAL;
if (percentage <= CONFIG_TQ_BATTERY_LOW_ENTER_PERCENT) {
if (++s_low_count >= BATTERY_STATE_CONFIRM_SAMPLES) {
s_status.level_state = DOMAIN_BATTERY_LEVEL_LOW;
s_low_count = 0U;
}
} else {
s_low_count = 0U;
}
break;
case DOMAIN_BATTERY_LEVEL_LOW:
if (percentage >= CONFIG_TQ_BATTERY_LOW_EXIT_PERCENT) {
s_status.level_state = DOMAIN_BATTERY_LEVEL_NORMAL;
s_critical_count = 0U;
} else if (charge_state == DOMAIN_BATTERY_CHARGE_CHARGING) {
s_critical_count = 0U;
} else if (percentage <= CONFIG_TQ_BATTERY_CRITICAL_ENTER_PERCENT) {
if (++s_critical_count >= BATTERY_STATE_CONFIRM_SAMPLES) {
s_status.level_state = DOMAIN_BATTERY_LEVEL_CRITICAL;
s_critical_count = 0U;
}
} else {
s_critical_count = 0U;
}
break;
case DOMAIN_BATTERY_LEVEL_CRITICAL:
if (charge_state == DOMAIN_BATTERY_CHARGE_CHARGING ||
percentage >= CONFIG_TQ_BATTERY_CRITICAL_EXIT_PERCENT) {
s_status.level_state = percentage >= CONFIG_TQ_BATTERY_LOW_EXIT_PERCENT
? DOMAIN_BATTERY_LEVEL_NORMAL
: DOMAIN_BATTERY_LEVEL_LOW;
}
break;
}
}
esp_err_t domain_battery_init(void)
{
if (!lock_state()) {
return ESP_ERR_TIMEOUT;
}
if (s_initialized) {
unlock_state();
return ESP_OK;
}
esp_err_t err = platform_battery_init();
if (err != ESP_OK) {
unlock_state();
return err;
}
memset(&s_status, 0, sizeof(s_status));
s_status.charge_state = DOMAIN_BATTERY_CHARGE_UNKNOWN;
s_status.level_state = DOMAIN_BATTERY_LEVEL_UNKNOWN;
s_initialized = true;
unlock_state();
return ESP_OK;
}
esp_err_t domain_battery_sample(void)
{
esp_err_t err = domain_battery_init();
if (err != ESP_OK) {
return err;
}
platform_battery_sample_t sample = {0};
err = platform_battery_read_sample(&sample);
if (!lock_state()) {
return ESP_ERR_TIMEOUT;
}
if (err != ESP_OK) {
s_status.latest_sample_valid = false;
if (s_status.consecutive_failures < UINT32_MAX) {
++s_status.consecutive_failures;
}
if (s_status.consecutive_failures >= BATTERY_STATE_CONFIRM_SAMPLES) {
s_status.valid = false;
}
unlock_state();
return err;
}
s_status.latest_adc_mv = sample.adc_voltage_mv;
s_status.latest_sample_mv = sample.voltage_mv;
s_status.latest_sample_valid = sample.voltage_valid;
s_status.charge_state = map_charge_state(sample.charge_state);
s_status.high_load_active = sample.high_load_active;
s_status.cooldown_active = sample.cooldown_active;
s_status.measurement_suppressed = sample.high_load_active || sample.cooldown_active;
if (s_status.measurement_suppressed) {
unlock_state();
return ESP_OK;
}
if (!sample.voltage_valid) {
if (s_status.consecutive_failures < UINT32_MAX) {
++s_status.consecutive_failures;
}
if (s_status.consecutive_failures >= BATTERY_STATE_CONFIRM_SAMPLES) {
s_status.valid = false;
}
unlock_state();
return ESP_ERR_INVALID_RESPONSE;
}
if (!accept_voltage_step(sample.voltage_mv)) {
unlock_state();
return ESP_OK;
}
if (!s_filter_initialized) {
s_filtered_mv = sample.voltage_mv;
s_filter_initialized = true;
} else {
s_filtered_mv = (s_filtered_mv * BATTERY_FILTER_OLD_WEIGHT +
sample.voltage_mv * BATTERY_FILTER_NEW_WEIGHT) /
(BATTERY_FILTER_OLD_WEIGHT + BATTERY_FILTER_NEW_WEIGHT);
}
uint8_t percentage = voltage_to_percentage(s_filtered_mv);
if (s_status.valid) {
if (s_status.charge_state == DOMAIN_BATTERY_CHARGE_CHARGING &&
percentage < s_status.percentage) {
percentage = s_status.percentage;
} else if (s_status.charge_state != DOMAIN_BATTERY_CHARGE_CHARGING &&
percentage > s_status.percentage) {
percentage = s_status.percentage;
}
if (percentage > s_status.percentage + 2U) {
percentage = s_status.percentage + 2U;
} else if (s_status.percentage > percentage + 2U) {
percentage = s_status.percentage - 2U;
}
}
s_status.voltage_mv = s_filtered_mv;
s_status.percentage = percentage;
s_status.valid = true;
s_status.measurement_suppressed = false;
s_status.consecutive_failures = 0U;
s_status.last_valid_sample_time_ms = esp_timer_get_time() / 1000;
update_level_state(percentage, s_status.charge_state);
unlock_state();
return ESP_OK;
}
esp_err_t domain_battery_get_status(domain_battery_status_t *out_status)
{
if (out_status == NULL) {
return ESP_ERR_INVALID_ARG;
}
if (!lock_state()) {
return ESP_ERR_TIMEOUT;
}
if (!s_initialized) {
unlock_state();
return ESP_ERR_INVALID_STATE;
}
*out_status = s_status;
unlock_state();
return ESP_OK;
}
esp_err_t domain_battery_prepare_shutdown(void)
{
/* Reserved for a future board-specific sleep or power-cut sequence. */
return ESP_ERR_NOT_SUPPORTED;
}
#else
esp_err_t domain_battery_init(void)
{
return ESP_ERR_NOT_SUPPORTED;
}
esp_err_t domain_battery_sample(void)
{
return ESP_ERR_NOT_SUPPORTED;
}
esp_err_t domain_battery_get_status(domain_battery_status_t *out_status)
{
if (out_status != NULL) {
memset(out_status, 0, sizeof(*out_status));
}
return ESP_ERR_NOT_SUPPORTED;
}
esp_err_t domain_battery_prepare_shutdown(void)
{
return ESP_ERR_NOT_SUPPORTED;
}
#endif
@@ -1318,6 +1318,22 @@ esp_err_t domain_runtime_stop_lcd_render_loop(int timeout_ms)
return platform_stop_lcd_render_loop(timeout_ms);
}
esp_err_t domain_runtime_update_battery_overlay(const domain_battery_status_t *status)
{
if (status == NULL) {
return ESP_ERR_INVALID_ARG;
}
const platform_lcd_status_overlay_t overlay = {
.low_battery_visible = status->valid &&
(status->level_state == DOMAIN_BATTERY_LEVEL_LOW ||
status->level_state == DOMAIN_BATTERY_LEVEL_CRITICAL),
.charging_visible =
status->charge_state == DOMAIN_BATTERY_CHARGE_CHARGING,
};
return platform_lcd_set_status_overlay(&overlay);
}
static const domain_expression_catalog_entry_t *domain_find_expression_entry(domain_expression_id_t id)
{
for (size_t i = 0; i < sizeof(s_expression_catalog) / sizeof(s_expression_catalog[0]); ++i) {