feat(power): add controlled low-battery shutdown

This commit is contained in:
chenjintao
2026-08-24 15:06:02 +08:00
parent 96e975d84c
commit 23f4969404
13 changed files with 467 additions and 19 deletions
@@ -79,6 +79,14 @@ static esp_err_t app_composition_run_plan_a_boot_sequence(void)
return err;
}
esp_err_t battery_guard_err = control_plane_battery_apply_boot_guard();
if (battery_guard_err != ESP_OK &&
battery_guard_err != ESP_ERR_NOT_SUPPORTED) {
ESP_LOGW(TAG,
"Battery boot guard failed, continue boot: %s",
esp_err_to_name(battery_guard_err));
}
app_composition_log_stage(APP_COMPOSITION_BOOT_STAGE_AUDIO_INIT,
"audio_init",
"boot_sound_policy_begin");
@@ -130,6 +130,12 @@ esp_err_t control_plane_init(void);
*/
esp_err_t control_plane_battery_service_start(void);
/**
* Re-enter deep sleep before normal boot when a low-battery wake has not
* recovered and no charger is active.
*/
esp_err_t control_plane_battery_apply_boot_guard(void);
/**
* @brief Prepare platform-backed persistent storage before runtime workers start.
*
@@ -147,6 +147,7 @@ bool control_plane_voice_turn_is_idle(void);
bool control_plane_ota_voice_services_allowed(void);
bool control_plane_ota_business_interactions_allowed(void);
bool control_plane_runtime_is_stopping(void);
void control_plane_set_runtime_stop_requested(bool stop_requested);
esp_err_t control_plane_battery_service_stop(int timeout_ms);
void control_plane_battery_service_set_light_standby(bool standby);
@@ -25,9 +25,31 @@
#define BATTERY_FULL_CONFIRM_TIME_MS 5000
#define BATTERY_FULL_CONFIRM_SAMPLES 3U
#define BATTERY_FULL_MIN_VOLTAGE_MV 4050
#define BATTERY_SHUTDOWN_CONFIRM_SAMPLES 3U
#define BATTERY_EMERGENCY_CONFIRM_SAMPLES 2U
#define BATTERY_SHUTDOWN_RETRY_MS 1000
#define BATTERY_SHUTDOWN_TASK_STACK_BYTES 6144
#define BATTERY_SHUTDOWN_TASK_PRIORITY 5
#define BATTERY_SHUTDOWN_MOTOR_STOP_MS 2000
#define BATTERY_LOW_PROMPT_FILE "/littlefs/audio_prompts/battery_low_20.opus"
#define BATTERY_FULL_PROMPT_FILE "/littlefs/audio_prompts/charge_complete.opus"
#ifndef CONFIG_TQ_BATTERY_CONTROLLED_SHUTDOWN_ENABLE
#define CONFIG_TQ_BATTERY_CONTROLLED_SHUTDOWN_ENABLE 0
#endif
#ifndef CONFIG_TQ_BATTERY_SHUTDOWN_PROMPT_FILE
#define CONFIG_TQ_BATTERY_SHUTDOWN_PROMPT_FILE "/littlefs/audio_prompts/shutdown_power_empty.opus"
#endif
#if CONFIG_TQ_BATTERY_CONTROLLED_SHUTDOWN_ENABLE
#if CONFIG_TQ_BATTERY_EMERGENCY_VOLTAGE_MV >= CONFIG_TQ_BATTERY_SHUTDOWN_VOLTAGE_MV
#error "Emergency battery voltage must be below the controlled shutdown threshold"
#endif
#if CONFIG_TQ_BATTERY_WAKE_RECOVERY_VOLTAGE_MV <= CONFIG_TQ_BATTERY_SHUTDOWN_VOLTAGE_MV
#error "Battery wake recovery voltage must exceed the shutdown threshold"
#endif
#endif
#ifndef CONFIG_TQ_LIGHT_STANDBY_BATTERY_INTERVAL_SEC
#define CONFIG_TQ_LIGHT_STANDBY_BATTERY_INTERVAL_SEC 300
#endif
@@ -38,6 +60,17 @@ static bool s_stop_requested;
static bool s_starting;
static bool s_charge_gpio_isr_registered;
static bool s_light_standby;
static TaskHandle_t s_shutdown_task;
static uint8_t s_shutdown_confirmations;
static uint8_t s_emergency_confirmations;
typedef enum {
BATTERY_POWER_STATE_RUNNING = 0,
BATTERY_POWER_STATE_SHUTDOWN_REQUESTED,
BATTERY_POWER_STATE_SHUTTING_DOWN,
} battery_power_state_t;
static battery_power_state_t s_power_state = BATTERY_POWER_STATE_RUNNING;
static portMUX_TYPE s_battery_lock = portMUX_INITIALIZER_UNLOCKED;
static void IRAM_ATTR battery_charge_gpio_isr(void *arg)
@@ -145,14 +178,137 @@ static void battery_worker_finish(void)
portEXIT_CRITICAL(&s_battery_lock);
}
static void control_plane_battery_critical_hook(
static void battery_shutdown_task(void *arg)
{
(void)arg;
portENTER_CRITICAL(&s_battery_lock);
s_power_state = BATTERY_POWER_STATE_SHUTTING_DOWN;
portEXIT_CRITICAL(&s_battery_lock);
ESP_LOGW(TAG, "Controlled low-battery shutdown started");
control_plane_set_runtime_stop_requested(true);
(void)control_plane_local_wake_stop(BATTERY_SHUTDOWN_MOTOR_STOP_MS);
(void)control_plane_stop_ota_service(BATTERY_SHUTDOWN_MOTOR_STOP_MS);
(void)control_plane_stop_multimodal_client();
control_plane_stop_key_input_task();
control_plane_stop_system_event_task();
esp_err_t motor_err = domain_direction_release_motor_hold(
BATTERY_SHUTDOWN_MOTOR_STOP_MS);
if (motor_err != ESP_OK) {
ESP_LOGW(TAG, "Release stepper before shutdown failed: %s",
esp_err_to_name(motor_err));
}
(void)domain_runtime_multimodal_audio_stop_downstream_playback();
for (;;) {
esp_err_t audio_init_err = domain_runtime_multimodal_audio_init();
esp_err_t prompt_err = audio_init_err == ESP_OK
? domain_runtime_behavior_play_prompt_file(
CONFIG_TQ_BATTERY_SHUTDOWN_PROMPT_FILE)
: audio_init_err;
if (prompt_err == ESP_OK) {
break;
}
/* The product requires the complete prompt before entering deep sleep. */
ESP_LOGE(TAG,
"Shutdown prompt did not complete; retrying: %s",
esp_err_to_name(prompt_err));
vTaskDelay(pdMS_TO_TICKS(BATTERY_SHUTDOWN_RETRY_MS));
}
(void)domain_runtime_multimodal_audio_set_speaker_output_enabled(false);
domain_runtime_multimodal_audio_deinit();
(void)domain_runtime_set_lcd_backlight_brightness(0U);
(void)domain_runtime_wifi_portal_stop_sta();
ESP_LOGW(TAG, "Shutdown prompt completed; entering deep sleep");
esp_err_t sleep_err = domain_battery_prepare_shutdown();
ESP_LOGE(TAG, "Enter deep sleep failed: %s", esp_err_to_name(sleep_err));
portENTER_CRITICAL(&s_battery_lock);
s_power_state = BATTERY_POWER_STATE_RUNNING;
s_shutdown_task = NULL;
portEXIT_CRITICAL(&s_battery_lock);
vTaskDelete(NULL);
}
static void battery_consider_controlled_shutdown(
const domain_battery_status_t *status)
{
#if CONFIG_TQ_BATTERY_CONTROLLED_SHUTDOWN_ENABLE
if (status == NULL) {
return;
}
portENTER_CRITICAL(&s_battery_lock);
bool already_requested = s_power_state != BATTERY_POWER_STATE_RUNNING;
portEXIT_CRITICAL(&s_battery_lock);
if (already_requested) {
return;
}
if (status->charge_state == DOMAIN_BATTERY_CHARGE_CHARGING) {
s_shutdown_confirmations = 0U;
s_emergency_confirmations = 0U;
return;
}
bool normal_sample = status->valid && !status->measurement_suppressed &&
status->percentage <= CONFIG_TQ_BATTERY_CRITICAL_ENTER_PERCENT &&
status->voltage_mv <= CONFIG_TQ_BATTERY_SHUTDOWN_VOLTAGE_MV;
bool emergency_sample = status->latest_sample_valid &&
status->latest_sample_mv <= CONFIG_TQ_BATTERY_EMERGENCY_VOLTAGE_MV;
if (normal_sample) {
if (s_shutdown_confirmations < UINT8_MAX) {
++s_shutdown_confirmations;
}
} else {
s_shutdown_confirmations = 0U;
}
if (emergency_sample) {
if (s_emergency_confirmations < UINT8_MAX) {
++s_emergency_confirmations;
}
} else {
s_emergency_confirmations = 0U;
}
if (s_shutdown_confirmations < BATTERY_SHUTDOWN_CONFIRM_SAMPLES &&
s_emergency_confirmations < BATTERY_EMERGENCY_CONFIRM_SAMPLES) {
return;
}
portENTER_CRITICAL(&s_battery_lock);
if (s_power_state != BATTERY_POWER_STATE_RUNNING || s_shutdown_task != NULL) {
portEXIT_CRITICAL(&s_battery_lock);
return;
}
s_power_state = BATTERY_POWER_STATE_SHUTDOWN_REQUESTED;
portEXIT_CRITICAL(&s_battery_lock);
TaskHandle_t task = NULL;
BaseType_t created = xTaskCreate(battery_shutdown_task,
"cp_bat_shutdown",
BATTERY_SHUTDOWN_TASK_STACK_BYTES,
NULL,
BATTERY_SHUTDOWN_TASK_PRIORITY,
&task);
portENTER_CRITICAL(&s_battery_lock);
if (created == pdPASS && task != NULL) {
s_shutdown_task = task;
} else {
s_power_state = BATTERY_POWER_STATE_RUNNING;
}
portEXIT_CRITICAL(&s_battery_lock);
if (created != pdPASS || task == NULL) {
ESP_LOGE(TAG, "Create controlled-shutdown task failed");
}
#else
(void)status;
/*
* Reserved for a future product decision. Do not stop actuators, enter
* sleep, or cut board power in this release.
*/
#endif
}
static void control_plane_battery_low_hook(
@@ -268,9 +424,6 @@ static void battery_worker(void *arg)
previous_level == DOMAIN_BATTERY_LEVEL_UNKNOWN)) {
control_plane_battery_low_hook(&status);
}
if (status.level_state == DOMAIN_BATTERY_LEVEL_CRITICAL) {
control_plane_battery_critical_hook(&status);
}
previous_level = status.level_state;
}
if (status.charge_state != previous_charge) {
@@ -321,12 +474,15 @@ static void battery_worker(void *arg)
}
}
}
battery_consider_controlled_shutdown(&status);
}
uint32_t delay_ms;
if (status_err == ESP_OK && status.measurement_suppressed) {
delay_ms = BATTERY_SUPPRESSED_RETRY_MS;
} else if (full_hint_candidate) {
} else if (full_hint_candidate || s_shutdown_confirmations > 0U ||
s_emergency_confirmations > 0U) {
delay_ms = BATTERY_FULL_CONFIRM_RETRY_MS;
} else if (battery_light_standby()) {
delay_ms = (uint32_t)CONFIG_TQ_LIGHT_STANDBY_BATTERY_INTERVAL_SEC * 1000U;
@@ -347,6 +503,42 @@ static void battery_worker(void *arg)
vTaskDelete(NULL);
}
esp_err_t control_plane_battery_apply_boot_guard(void)
{
#if CONFIG_TQ_BATTERY_CONTROLLED_SHUTDOWN_ENABLE
if (!domain_battery_woke_from_deep_sleep()) {
return ESP_OK;
}
esp_err_t sample_err = domain_battery_sample();
if (sample_err != ESP_OK) {
return sample_err;
}
domain_battery_status_t status = {0};
esp_err_t status_err = domain_battery_get_status(&status);
if (status_err != ESP_OK) {
return status_err;
}
ESP_LOGW(TAG,
"Deep-sleep wake battery check voltage=%dmV charge=%s valid=%d",
status.voltage_mv,
charge_state_name(status.charge_state),
status.valid);
if (status.charge_state == DOMAIN_BATTERY_CHARGE_CHARGING ||
(status.valid &&
status.voltage_mv >= CONFIG_TQ_BATTERY_WAKE_RECOVERY_VOLTAGE_MV)) {
domain_battery_complete_wakeup();
return ESP_OK;
}
ESP_LOGW(TAG, "Battery has not recovered; returning to deep sleep");
return domain_battery_prepare_shutdown();
#else
return ESP_ERR_NOT_SUPPORTED;
#endif
}
esp_err_t control_plane_battery_service_start(void)
{
portENTER_CRITICAL(&s_battery_lock);
@@ -442,6 +634,11 @@ esp_err_t control_plane_battery_service_start(void)
return ESP_ERR_NOT_SUPPORTED;
}
esp_err_t control_plane_battery_apply_boot_guard(void)
{
return ESP_ERR_NOT_SUPPORTED;
}
esp_err_t control_plane_battery_service_stop(int timeout_ms)
{
return timeout_ms < 0 ? ESP_ERR_INVALID_ARG : ESP_OK;
@@ -490,7 +490,7 @@ static bool control_plane_runtime_stop_requested(void)
return control_plane_runtime_is_stopping();
}
static void control_plane_set_runtime_stop_requested(bool stop_requested)
void control_plane_set_runtime_stop_requested(bool stop_requested)
{
portENTER_CRITICAL(&s_runtime_lock);
s_runtime_stop_requested = stop_requested;
+7 -1
View File
@@ -67,9 +67,15 @@ esp_err_t domain_battery_sample(void);
/** Copy the latest battery SSOT snapshot into `out_status`. */
esp_err_t domain_battery_get_status(domain_battery_status_t *out_status);
/** Reserved policy hook; this release intentionally performs no shutdown. */
/** Enter the configured board deep-sleep state; a successful call does not return. */
esp_err_t domain_battery_prepare_shutdown(void);
/** Return true when the current boot resumed through a deep-sleep wake source. */
bool domain_battery_woke_from_deep_sleep(void);
/** Release low-power GPIO holds after accepting a deep-sleep wake. */
void domain_battery_complete_wakeup(void);
typedef enum {
DOMAIN_WIFI_PROMPT_WIFI_SUCCESS = 0,
DOMAIN_WIFI_PROMPT_AFTER_WIFI_SUCCESS,
@@ -40,6 +40,14 @@ typedef struct {
uint8_t percentage;
} battery_curve_point_t;
typedef struct {
uint8_t version;
uint8_t reason;
uint8_t percentage;
uint8_t reserved;
int32_t voltage_mv;
} battery_shutdown_record_t;
static const battery_curve_point_t s_discharge_curve[] = {
{3300, 0},
{3400, 2},
@@ -329,8 +337,37 @@ esp_err_t domain_battery_get_status(domain_battery_status_t *out_status)
esp_err_t domain_battery_prepare_shutdown(void)
{
/* Reserved for a future board-specific sleep or power-cut sequence. */
#if CONFIG_TQ_BATTERY_CONTROLLED_SHUTDOWN_ENABLE
battery_shutdown_record_t record = {
.version = 1U,
.reason = 1U,
};
if (lock_state()) {
record.percentage = s_status.percentage;
record.voltage_mv = s_status.voltage_mv;
unlock_state();
}
/* Best-effort bounded record; a storage failure must not block deep sleep. */
(void)platform_storage_set_blob("battery",
"last_shutdown",
&record,
sizeof(record));
return platform_power_enter_deep_sleep(
CONFIG_TQ_BATTERY_CHARGE_GPIO,
CONFIG_TQ_BATTERY_SHUTDOWN_WAKE_BUTTON_GPIO);
#else
return ESP_ERR_NOT_SUPPORTED;
#endif
}
bool domain_battery_woke_from_deep_sleep(void)
{
return platform_power_woke_from_deep_sleep();
}
void domain_battery_complete_wakeup(void)
{
platform_power_release_deep_sleep_holds();
}
#else
@@ -358,4 +395,13 @@ esp_err_t domain_battery_prepare_shutdown(void)
return ESP_ERR_NOT_SUPPORTED;
}
bool domain_battery_woke_from_deep_sleep(void)
{
return false;
}
void domain_battery_complete_wakeup(void)
{
}
#endif
+1
View File
@@ -44,6 +44,7 @@ set(platform_priv_requires
app_update
esp_http_client
esp_http_server
esp_hw_support
esp_lcd
esp_netif
esp_psram
+12
View File
@@ -310,6 +310,18 @@ esp_err_t platform_stepper_rotate_angle_with_accel_timeout(float angle,
esp_err_t platform_stepper_release_hold(int timeout_ms);
void platform_stepper_power_off(void);
/** Return true when this boot was caused by a configured deep-sleep wake source. */
bool platform_power_woke_from_deep_sleep(void);
/** Release board-output holds after a deep-sleep wake is accepted. */
void platform_power_release_deep_sleep_holds(void);
/**
* Enter deep sleep after configuring active-low charger and optional button
* wake GPIOs. A successful call does not return.
*/
esp_err_t platform_power_enter_deep_sleep(int charger_gpio, int button_gpio);
typedef enum {
PLATFORM_BATTERY_CHARGE_UNKNOWN = 0,
PLATFORM_BATTERY_CHARGE_NOT_CHARGING,
@@ -11,6 +11,7 @@
#include "driver/gpio.h"
#include "esp_app_desc.h"
#include "esp_log.h"
#include "esp_sleep.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "platform_internal.h"
@@ -20,6 +21,18 @@
#ifndef CONFIG_TQ_STEPPER_ENABLE
#define CONFIG_TQ_STEPPER_ENABLE 1
#endif
#ifndef CONFIG_MULTIMODAL_AUDIO_PA_GPIO
#define CONFIG_MULTIMODAL_AUDIO_PA_GPIO -1
#endif
#ifndef CONFIG_TQ_SCREEN_BACKLIGHT_GPIO
#define CONFIG_TQ_SCREEN_BACKLIGHT_GPIO -1
#endif
#ifndef CONFIG_TQ_STEPPER_IN1_GPIO
#define CONFIG_TQ_STEPPER_IN1_GPIO -1
#define CONFIG_TQ_STEPPER_IN2_GPIO -1
#define CONFIG_TQ_STEPPER_IN3_GPIO -1
#define CONFIG_TQ_STEPPER_IN4_GPIO -1
#endif
static const char *TAG = "platform_core";
static bool s_initialized;
@@ -35,6 +48,106 @@ static const char *const s_known_nvs_namespaces[] = {
"fortune",
};
bool platform_power_woke_from_deep_sleep(void)
{
return esp_sleep_get_wakeup_cause() != ESP_SLEEP_WAKEUP_UNDEFINED;
}
static void platform_power_set_output_low_and_hold(int gpio)
{
if (gpio < 0 || gpio >= GPIO_NUM_MAX) {
return;
}
(void)gpio_hold_dis((gpio_num_t)gpio);
(void)gpio_set_direction((gpio_num_t)gpio, GPIO_MODE_OUTPUT);
(void)gpio_set_level((gpio_num_t)gpio, 0);
(void)gpio_hold_en((gpio_num_t)gpio);
}
static void platform_power_set_board_outputs_held(bool held)
{
const int output_gpios[] = {
CONFIG_MULTIMODAL_AUDIO_PA_GPIO,
CONFIG_TQ_SCREEN_BACKLIGHT_GPIO,
CONFIG_TQ_STEPPER_IN1_GPIO,
CONFIG_TQ_STEPPER_IN2_GPIO,
CONFIG_TQ_STEPPER_IN3_GPIO,
CONFIG_TQ_STEPPER_IN4_GPIO,
};
for (size_t i = 0; i < sizeof(output_gpios) / sizeof(output_gpios[0]); ++i) {
int gpio = output_gpios[i];
if (gpio < 0 || gpio >= GPIO_NUM_MAX) {
continue;
}
if (held) {
platform_power_set_output_low_and_hold(gpio);
} else {
(void)gpio_hold_dis((gpio_num_t)gpio);
}
}
if (held) {
gpio_deep_sleep_hold_en();
} else {
gpio_deep_sleep_hold_dis();
}
}
void platform_power_release_deep_sleep_holds(void)
{
platform_power_set_board_outputs_held(false);
}
esp_err_t platform_power_enter_deep_sleep(int charger_gpio, int button_gpio)
{
uint64_t wake_mask = 0U;
const int wake_gpios[] = {charger_gpio, button_gpio};
for (size_t i = 0; i < sizeof(wake_gpios) / sizeof(wake_gpios[0]); ++i) {
int gpio = wake_gpios[i];
if (gpio < 0) {
continue;
}
if (gpio >= GPIO_NUM_MAX ||
!esp_sleep_is_valid_wakeup_gpio((gpio_num_t)gpio)) {
ESP_LOGE(TAG, "Invalid deep-sleep wake GPIO: %d", gpio);
return ESP_ERR_INVALID_ARG;
}
gpio_config_t input_config = {
.pin_bit_mask = 1ULL << (uint32_t)gpio,
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
esp_err_t gpio_err = gpio_config(&input_config);
if (gpio_err != ESP_OK) {
return gpio_err;
}
wake_mask |= 1ULL << (uint32_t)gpio;
}
if (wake_mask == 0U) {
ESP_LOGE(TAG, "Deep sleep requires at least one wake GPIO");
return ESP_ERR_INVALID_STATE;
}
esp_err_t wake_err = esp_sleep_enable_ext1_wakeup_io(
wake_mask,
ESP_EXT1_WAKEUP_ANY_LOW);
if (wake_err != ESP_OK) {
return wake_err;
}
platform_power_set_board_outputs_held(true);
ESP_LOGW(TAG,
"Entering deep sleep wake_mask=0x%llx",
(unsigned long long)wake_mask);
esp_deep_sleep_start();
return ESP_ERR_INVALID_STATE;
}
static esp_err_t platform_gpio_isr_service_init(void)
{
if (s_gpio_isr_service_installed) {