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
+1
View File
@@ -46,6 +46,7 @@ The following matrix is authoritative for all new and refactored cross-layer con
| Connectivity semantic prompt IDs/events (connecting/success/fail/config) | `domain` | `components/domain/include/domain.h` | `platform` must not expose duplicated semantic enums in public facade; map semantics in adapters. |
| Mini-program Wi-Fi provisioning semantics (TalkingQ BLE discovery/GATT schema, credential events, status/device-info payloads) | `domain` | `components/domain/include/domain.h` | `platform` exposes generic BLE GATT server primitives only; `control_plane` owns Wi-Fi connection policy and worker lifecycle. |
| Hardware/runtime primitive descriptors (GPIO digital input config, low-level actuator primitives, storage identity/blob KV APIs, wall-clock sync API) | `platform` | `components/platform/include/platform.h` | Upper layers call through owner facade or owner-backed adapters; no semantic redefinition in non-owner public headers. |
| Battery sensing and battery-state policy (`platform_battery_*`, `domain_battery_*`) | `domain` for SOC/level semantics; `platform` for ADC, charger GPIO, and load-quality primitives | `components/domain/include/domain.h` for semantic state; `components/platform/include/platform.h` for hardware samples | `control_plane` consumes domain status only; platform load tracking stays internal and upper layers must not infer SOC from raw ADC values. |
| LAN HTTP transport contract (bounded request/response structs, handler delegate, server lifecycle/state) | `platform` | `components/platform/include/platform.h` | `domain` owns route semantics and maps them in implementation; upper layers must not expose platform HTTP types. |
| Multimodal session transport contract exposed to orchestration (connected/disconnected/error/text/binary event family, audio uplink callback family) | `domain` | `components/domain/include/domain.h` | `platform` internal transport types stay internal; cross-layer public contract is declared once at `domain` facade. |
| Device-auth persistence port (opaque auth key/state record slots and legacy migration targets) | `domain` | `components/domain/include/domain.h` | `control_plane` owns serialized auth record schemas and consumes typed auth slots through `domain`; `platform` remains an opaque blob KV owner only. |
@@ -68,7 +68,7 @@ static esp_err_t app_composition_run_plan_a_boot_sequence(void)
{
app_composition_log_stage(APP_COMPOSITION_BOOT_STAGE_POWER_HOLD,
"power_hold",
"already_asserted_by_platform_core");
"configured_or_disabled_by_platform_core");
app_composition_log_stage(APP_COMPOSITION_BOOT_STAGE_STORAGE_PREPARE,
"storage_prepare",
@@ -139,6 +139,13 @@ static esp_err_t app_composition_run_plan_a_boot_sequence(void)
return err;
}
esp_err_t battery_err = control_plane_battery_service_start();
if (battery_err != ESP_OK && battery_err != ESP_ERR_NOT_SUPPORTED) {
ESP_LOGW(TAG,
"Early battery service start failed, continue boot: %s",
esp_err_to_name(battery_err));
}
#if CONFIG_TQ_FACTORY_CLI_ENABLE
err = control_plane_factory_cli_start();
if (err != ESP_OK) {
+1
View File
@@ -18,6 +18,7 @@ idf_component_register(
"src/runtime/control_plane_runtime_workers.c"
"src/runtime/control_plane_ota.c"
"src/runtime/control_plane_factory_cli.c"
"src/runtime/control_plane_battery_service.c"
INCLUDE_DIRS
"include"
PRIV_INCLUDE_DIRS
@@ -88,6 +88,15 @@ typedef struct {
esp_err_t control_plane_init(void);
/**
* @brief Start battery sensing and charge-status presentation.
*
* Safe to call repeatedly. This service is intentionally started before
* connectivity bootstrap so battery and charger indications do not depend on
* Wi-Fi, provisioning, or server readiness.
*/
esp_err_t control_plane_battery_service_start(void);
/**
* @brief Prepare platform-backed persistent storage before runtime workers start.
*
@@ -147,6 +147,8 @@ bool control_plane_voice_turn_is_idle(void);
bool control_plane_ota_voice_services_allowed(void);
bool control_plane_runtime_is_stopping(void);
esp_err_t control_plane_battery_service_stop(int timeout_ms);
esp_err_t control_plane_start_system_event_task(void);
void control_plane_stop_system_event_task(void);
esp_err_t control_plane_post_system_event(const control_plane_system_event_t *event, uint32_t timeout_ms);
@@ -0,0 +1,426 @@
/*
* SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "control_plane_internal.h"
#include "domain.h"
#include "driver/gpio.h"
#include "esp_attr.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "sdkconfig.h"
#if CONFIG_TQ_BATTERY_ENABLE
#define BATTERY_WORKER_STACK_BYTES 4096
#define BATTERY_WORKER_PRIORITY 2
#define BATTERY_SUPPRESSED_RETRY_MS 1000
#define BATTERY_STOP_POLL_MS 10
#define BATTERY_CHARGE_DEBOUNCE_MS 30
#define BATTERY_FULL_CONFIRM_RETRY_MS 1000
#define BATTERY_FULL_CONFIRM_TIME_MS 5000
#define BATTERY_FULL_CONFIRM_SAMPLES 3U
#define BATTERY_FULL_MIN_VOLTAGE_MV 4050
#define BATTERY_LOW_PROMPT_FILE "/littlefs/audio_prompts/battery_low_20.opus"
#define BATTERY_FULL_PROMPT_FILE "/littlefs/audio_prompts/charge_complete.opus"
static const char *TAG = "cp_battery";
static TaskHandle_t s_battery_task;
static bool s_stop_requested;
static bool s_starting;
static bool s_charge_gpio_isr_registered;
static portMUX_TYPE s_battery_lock = portMUX_INITIALIZER_UNLOCKED;
static void IRAM_ATTR battery_charge_gpio_isr(void *arg)
{
(void)arg;
TaskHandle_t task = s_battery_task;
if (task == NULL) {
return;
}
BaseType_t higher_priority_task_woken = pdFALSE;
vTaskNotifyGiveFromISR(task, &higher_priority_task_woken);
if (higher_priority_task_woken == pdTRUE) {
portYIELD_FROM_ISR();
}
}
static esp_err_t battery_charge_interrupt_start(void)
{
if (CONFIG_TQ_BATTERY_CHARGE_GPIO < 0) {
return ESP_ERR_NOT_SUPPORTED;
}
esp_err_t err = gpio_install_isr_service(0);
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
return err;
}
err = gpio_set_intr_type((gpio_num_t)CONFIG_TQ_BATTERY_CHARGE_GPIO,
GPIO_INTR_ANYEDGE);
if (err != ESP_OK) {
return err;
}
err = gpio_isr_handler_add((gpio_num_t)CONFIG_TQ_BATTERY_CHARGE_GPIO,
battery_charge_gpio_isr,
NULL);
if (err != ESP_OK) {
(void)gpio_set_intr_type((gpio_num_t)CONFIG_TQ_BATTERY_CHARGE_GPIO,
GPIO_INTR_DISABLE);
return err;
}
s_charge_gpio_isr_registered = true;
return ESP_OK;
}
static void battery_charge_interrupt_stop(void)
{
if (!s_charge_gpio_isr_registered || CONFIG_TQ_BATTERY_CHARGE_GPIO < 0) {
return;
}
(void)gpio_intr_disable((gpio_num_t)CONFIG_TQ_BATTERY_CHARGE_GPIO);
(void)gpio_isr_handler_remove((gpio_num_t)CONFIG_TQ_BATTERY_CHARGE_GPIO);
(void)gpio_set_intr_type((gpio_num_t)CONFIG_TQ_BATTERY_CHARGE_GPIO,
GPIO_INTR_DISABLE);
s_charge_gpio_isr_registered = false;
}
static const char *charge_state_name(domain_battery_charge_state_t state)
{
switch (state) {
case DOMAIN_BATTERY_CHARGE_CHARGING:
return "charging";
case DOMAIN_BATTERY_CHARGE_NOT_CHARGING:
return "not_charging";
default:
return "unknown";
}
}
static const char *level_state_name(domain_battery_level_state_t state)
{
switch (state) {
case DOMAIN_BATTERY_LEVEL_NORMAL:
return "normal";
case DOMAIN_BATTERY_LEVEL_LOW:
return "low";
case DOMAIN_BATTERY_LEVEL_CRITICAL:
return "critical";
default:
return "unknown";
}
}
static bool battery_stop_requested(void)
{
bool requested;
portENTER_CRITICAL(&s_battery_lock);
requested = s_stop_requested;
portEXIT_CRITICAL(&s_battery_lock);
return requested;
}
static void battery_worker_finish(void)
{
portENTER_CRITICAL(&s_battery_lock);
s_battery_task = NULL;
s_stop_requested = false;
s_starting = false;
portEXIT_CRITICAL(&s_battery_lock);
}
static void control_plane_battery_critical_hook(
const domain_battery_status_t *status)
{
(void)status;
/*
* Reserved for a future product decision. Do not stop actuators, enter
* sleep, or cut board power in this release.
*/
}
static void control_plane_battery_low_hook(
const domain_battery_status_t *status)
{
if (status == NULL) {
return;
}
ESP_LOGI(TAG,
"Low-battery prompt start level=%u%% file=%s",
(unsigned)status->percentage,
BATTERY_LOW_PROMPT_FILE);
esp_err_t prompt_err = domain_runtime_behavior_play_prompt_file(
BATTERY_LOW_PROMPT_FILE);
if (prompt_err != ESP_OK) {
ESP_LOGW(TAG,
"Low-battery prompt failed: %s",
esp_err_to_name(prompt_err));
}
}
static void control_plane_battery_full_hint_hook(
const domain_battery_status_t *status)
{
if (status == NULL) {
return;
}
ESP_LOGI(TAG,
"Inferred charge-complete prompt start voltage=%dmV file=%s",
status->voltage_mv,
BATTERY_FULL_PROMPT_FILE);
esp_err_t prompt_err = domain_runtime_behavior_play_prompt_file(
BATTERY_FULL_PROMPT_FILE);
if (prompt_err != ESP_OK) {
ESP_LOGW(TAG,
"Charge-complete prompt failed: %s",
esp_err_to_name(prompt_err));
}
}
static void battery_worker(void *arg)
{
(void)arg;
(void)ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
domain_battery_level_state_t previous_level = DOMAIN_BATTERY_LEVEL_UNKNOWN;
domain_battery_charge_state_t previous_charge = DOMAIN_BATTERY_CHARGE_UNKNOWN;
bool full_hint_candidate = false;
TickType_t full_hint_started_at = 0;
uint8_t full_hint_confirmations = 0U;
esp_err_t init_err = domain_battery_init();
if (init_err != ESP_OK) {
ESP_LOGE(TAG, "Battery service initialization failed: %s", esp_err_to_name(init_err));
battery_worker_finish();
vTaskDelete(NULL);
return;
}
esp_err_t interrupt_err = battery_charge_interrupt_start();
if (interrupt_err == ESP_OK) {
ESP_LOGI(TAG,
"TP4057 charge-state interrupt ready gpio=%d debounce=%dms",
CONFIG_TQ_BATTERY_CHARGE_GPIO,
BATTERY_CHARGE_DEBOUNCE_MS);
} else if (interrupt_err != ESP_ERR_NOT_SUPPORTED) {
ESP_LOGW(TAG,
"TP4057 charge-state interrupt unavailable; using polling: %s",
esp_err_to_name(interrupt_err));
}
ESP_LOGI(TAG, "Battery monitoring worker started");
while (!battery_stop_requested()) {
esp_err_t sample_err = domain_battery_sample();
domain_battery_status_t status = {0};
esp_err_t status_err = domain_battery_get_status(&status);
if (sample_err != ESP_OK) {
ESP_LOGW(TAG, "Battery sample rejected: %s", esp_err_to_name(sample_err));
}
if (status_err == ESP_OK) {
esp_err_t overlay_err = domain_runtime_update_battery_overlay(&status);
if (overlay_err != ESP_OK && overlay_err != ESP_ERR_NOT_SUPPORTED) {
ESP_LOGD(TAG, "Battery LCD overlay update skipped: %s", esp_err_to_name(overlay_err));
}
if (status.valid && !status.measurement_suppressed) {
ESP_LOGI(TAG,
"Battery adc=%dmV voltage=%dmV level=%u%% charge=%s state=%s",
status.latest_adc_mv,
status.voltage_mv,
(unsigned)status.percentage,
charge_state_name(status.charge_state),
level_state_name(status.level_state));
} else if (status.measurement_suppressed) {
ESP_LOGD(TAG,
"Battery SOC update suppressed adc=%dmV raw=%dmV valid=%d load=%d cooldown=%d",
status.latest_adc_mv,
status.latest_sample_mv,
status.latest_sample_valid,
status.high_load_active,
status.cooldown_active);
}
if (status.level_state != previous_level) {
ESP_LOGW(TAG,
"Battery level state changed: %s -> %s",
level_state_name(previous_level),
level_state_name(status.level_state));
if (status.level_state == DOMAIN_BATTERY_LEVEL_LOW &&
(previous_level == DOMAIN_BATTERY_LEVEL_NORMAL ||
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) {
ESP_LOGI(TAG,
"Battery charge state changed: %s -> %s",
charge_state_name(previous_charge),
charge_state_name(status.charge_state));
if (previous_charge == DOMAIN_BATTERY_CHARGE_CHARGING &&
status.charge_state == DOMAIN_BATTERY_CHARGE_NOT_CHARGING) {
/*
* TP4057 exposes the same high level for charge termination
* and charger removal. Voltage can only make this a useful
* UI/audio hint, not a rigorous full-charge measurement.
* Never use this inference for safety, shutdown, or power
* switching decisions.
*/
full_hint_candidate = true;
full_hint_started_at = xTaskGetTickCount();
full_hint_confirmations = 0U;
} else if (status.charge_state != DOMAIN_BATTERY_CHARGE_NOT_CHARGING) {
full_hint_candidate = false;
full_hint_confirmations = 0U;
}
previous_charge = status.charge_state;
}
if (full_hint_candidate && status.valid &&
!status.measurement_suppressed) {
if (status.charge_state != DOMAIN_BATTERY_CHARGE_NOT_CHARGING ||
status.voltage_mv < BATTERY_FULL_MIN_VOLTAGE_MV) {
ESP_LOGI(TAG,
"Charge-complete inference cancelled charge=%s voltage=%dmV",
charge_state_name(status.charge_state),
status.voltage_mv);
full_hint_candidate = false;
full_hint_confirmations = 0U;
} else {
if (full_hint_confirmations < UINT8_MAX) {
++full_hint_confirmations;
}
TickType_t elapsed_ticks =
xTaskGetTickCount() - full_hint_started_at;
if (full_hint_confirmations >= BATTERY_FULL_CONFIRM_SAMPLES &&
elapsed_ticks >= pdMS_TO_TICKS(BATTERY_FULL_CONFIRM_TIME_MS)) {
control_plane_battery_full_hint_hook(&status);
full_hint_candidate = false;
full_hint_confirmations = 0U;
}
}
}
}
uint32_t delay_ms;
if (status_err == ESP_OK && status.measurement_suppressed) {
delay_ms = BATTERY_SUPPRESSED_RETRY_MS;
} else if (full_hint_candidate) {
delay_ms = BATTERY_FULL_CONFIRM_RETRY_MS;
} else {
delay_ms = (uint32_t)CONFIG_TQ_BATTERY_SAMPLE_INTERVAL_SEC * 1000U;
}
uint32_t notifications = ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(delay_ms));
if (notifications > 0U && !battery_stop_requested()) {
vTaskDelay(pdMS_TO_TICKS(BATTERY_CHARGE_DEBOUNCE_MS));
/* Collapse contact bounce into one fresh read of the final level. */
(void)ulTaskNotifyTake(pdTRUE, 0U);
}
}
battery_charge_interrupt_stop();
ESP_LOGI(TAG, "Battery monitoring worker stopped");
battery_worker_finish();
vTaskDelete(NULL);
}
esp_err_t control_plane_battery_service_start(void)
{
portENTER_CRITICAL(&s_battery_lock);
if (s_battery_task != NULL || s_starting) {
portEXIT_CRITICAL(&s_battery_lock);
return ESP_OK;
}
s_stop_requested = false;
s_starting = true;
portEXIT_CRITICAL(&s_battery_lock);
TaskHandle_t task = NULL;
BaseType_t created = xTaskCreate(battery_worker,
"cp_battery",
BATTERY_WORKER_STACK_BYTES,
NULL,
BATTERY_WORKER_PRIORITY,
&task);
if (created != pdPASS || task == NULL) {
portENTER_CRITICAL(&s_battery_lock);
s_starting = false;
portEXIT_CRITICAL(&s_battery_lock);
return ESP_ERR_NO_MEM;
}
portENTER_CRITICAL(&s_battery_lock);
s_battery_task = task;
s_starting = false;
portEXIT_CRITICAL(&s_battery_lock);
xTaskNotifyGive(task);
return ESP_OK;
}
esp_err_t control_plane_battery_service_stop(int timeout_ms)
{
if (timeout_ms < 0) {
return ESP_ERR_INVALID_ARG;
}
TaskHandle_t task;
portENTER_CRITICAL(&s_battery_lock);
task = s_battery_task;
s_stop_requested = task != NULL;
portEXIT_CRITICAL(&s_battery_lock);
if (task == NULL) {
return ESP_OK;
}
xTaskNotifyGive(task);
int waited_ms = 0;
while (waited_ms < timeout_ms) {
portENTER_CRITICAL(&s_battery_lock);
task = s_battery_task;
portEXIT_CRITICAL(&s_battery_lock);
if (task == NULL) {
return ESP_OK;
}
vTaskDelay(pdMS_TO_TICKS(BATTERY_STOP_POLL_MS));
waited_ms += BATTERY_STOP_POLL_MS;
}
portENTER_CRITICAL(&s_battery_lock);
task = s_battery_task;
s_battery_task = NULL;
s_stop_requested = false;
portEXIT_CRITICAL(&s_battery_lock);
if (task != NULL) {
ESP_LOGW(TAG, "Battery worker stop timeout; forcing teardown");
vTaskDelete(task);
battery_charge_interrupt_stop();
return ESP_ERR_TIMEOUT;
}
return ESP_OK;
}
#else
esp_err_t control_plane_battery_service_start(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;
}
#endif
@@ -2264,6 +2264,11 @@ esp_err_t control_plane_start_runtime_workers(void)
return queue_err;
}
esp_err_t battery_err = control_plane_battery_service_start();
if (battery_err != ESP_OK && battery_err != ESP_ERR_NOT_SUPPORTED) {
ESP_LOGW(TAG, "Battery service start failed: %s", esp_err_to_name(battery_err));
}
if (control_plane_get_task_handle(&s_model_cmd_task) == NULL) {
esp_err_t task_err = control_plane_create_task_psram_first(control_plane_model_command_task,
"cp_model_cmd",
@@ -2412,6 +2417,7 @@ void control_plane_stop_event_observer_runtime(void)
void control_plane_stop_runtime_workers(void)
{
control_plane_set_runtime_stop_requested(true);
(void)control_plane_battery_service_stop(CONTROL_PLANE_TASK_STOP_WAIT_MS);
(void)control_plane_stop_ota_service(CONFIG_TQ_LOCAL_WAKE_STOP_WAIT_MS);
(void)control_plane_stop_mini_program_provisioning(
CONTROL_PLANE_TASK_STOP_WAIT_MS);
+3
View File
@@ -7,6 +7,7 @@ idf_component_register(
"src/calibration/domain_calibration_service.c"
"src/fortune/domain_fortune_store.c"
"src/fortune/domain_user_config_server.c"
"src/battery/domain_battery_service.c"
"src/runtime/domain_runtime_adapters.c"
"src/runtime/domain_ota.c"
"src/runtime/domain_local_command.c"
@@ -17,6 +18,8 @@ idf_component_register(
"internal"
PRIV_REQUIRES
platform
esp_timer
freertos
esp-sr
log
json
+45
View File
@@ -29,6 +29,47 @@ typedef struct {
int settle_delay_ms;
} domain_direction_calibration_t;
typedef enum {
DOMAIN_BATTERY_CHARGE_UNKNOWN = 0,
DOMAIN_BATTERY_CHARGE_NOT_CHARGING,
DOMAIN_BATTERY_CHARGE_CHARGING,
} domain_battery_charge_state_t;
typedef enum {
DOMAIN_BATTERY_LEVEL_UNKNOWN = 0,
DOMAIN_BATTERY_LEVEL_NORMAL,
DOMAIN_BATTERY_LEVEL_LOW,
DOMAIN_BATTERY_LEVEL_CRITICAL,
} domain_battery_level_state_t;
typedef struct {
int voltage_mv;
int latest_adc_mv;
int latest_sample_mv;
uint8_t percentage;
domain_battery_charge_state_t charge_state;
domain_battery_level_state_t level_state;
bool valid;
bool latest_sample_valid;
bool measurement_suppressed;
bool high_load_active;
bool cooldown_active;
uint32_t consecutive_failures;
int64_t last_valid_sample_time_ms;
} domain_battery_status_t;
/** Initialize the battery measurement domain. Idempotent and thread-safe. */
esp_err_t domain_battery_init(void);
/** Acquire and process one battery sample without applying product policy. */
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. */
esp_err_t domain_battery_prepare_shutdown(void);
typedef enum {
DOMAIN_WIFI_PROMPT_WIFI_SUCCESS = 0,
DOMAIN_WIFI_PROMPT_AFTER_WIFI_SUCCESS,
@@ -560,6 +601,10 @@ void domain_runtime_start_lcd_render_loop(void);
* values wait up to the bound before lower layers apply deterministic fallback.
*/
esp_err_t domain_runtime_stop_lcd_render_loop(int timeout_ms);
/** Map the domain battery snapshot to persistent LCD status overlays. */
esp_err_t domain_runtime_update_battery_overlay(const domain_battery_status_t *status);
esp_err_t domain_runtime_request_expression(const domain_expression_request_t *request);
/**
@@ -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) {
+4
View File
@@ -11,6 +11,7 @@ set(platform_srcs
"src/runtime/platform_multimodal_runtime.c"
"src/runtime/platform_lcd_runtime.c"
"src/platform_stepper_driver.c"
"src/platform_battery_driver.c"
"src/platform_digital_input_driver.c"
"src/platform_io_effects.c"
"src/lcd_direct.c"
@@ -30,6 +31,7 @@ set(platform_priv_requires
cxx
driver
esp_audio_codec
esp_adc
esp_codec_dev
esp_driver_gpio
esp_driver_i2c
@@ -140,6 +142,8 @@ set(platform_lcd_png_assets
icon_huo0
icon_tu0
icon_jin0
charging
lowbat
qrcode
)
set(platform_lcd_png_asset_files)
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

+40
View File
@@ -302,6 +302,33 @@ esp_err_t platform_stepper_rotate_angle_with_accel_timeout(float angle,
int target_delay_us,
int timeout_ms);
void platform_stepper_power_off(void);
typedef enum {
PLATFORM_BATTERY_CHARGE_UNKNOWN = 0,
PLATFORM_BATTERY_CHARGE_NOT_CHARGING,
PLATFORM_BATTERY_CHARGE_CHARGING,
} platform_battery_charge_state_t;
typedef struct {
int adc_voltage_mv;
int voltage_mv;
platform_battery_charge_state_t charge_state;
bool voltage_valid;
bool high_load_active;
bool cooldown_active;
} platform_battery_sample_t;
/** Initialize the board battery ADC and TP4057 status input. Idempotent. */
esp_err_t platform_battery_init(void);
/**
* Read one trimmed-mean battery voltage sample and a load-quality snapshot.
* A successful call can still return voltage_valid=false for an implausible
* electrical reading; callers must not interpret such a sample as an empty
* battery.
*/
esp_err_t platform_battery_read_sample(platform_battery_sample_t *out_sample);
esp_err_t platform_configure_digital_input(const platform_digital_input_config_t *config);
bool platform_is_digital_input_active(const platform_digital_input_config_t *config);
@@ -480,6 +507,19 @@ typedef struct {
size_t step_count;
} platform_lcd_presentation_request_t;
typedef struct {
bool low_battery_visible;
bool charging_visible;
} platform_lcd_status_overlay_t;
/**
* @brief Set persistent LCD status overlays composited over presentation frames.
*
* The state is copied synchronously and remains active until replaced. The
* render task owns all PNG decoding and panel access.
*/
esp_err_t platform_lcd_set_status_overlay(const platform_lcd_status_overlay_t *overlay);
/**
* @brief Request a LCD presentation playback.
*
@@ -12,6 +12,7 @@ extern "C" {
esp_err_t lcd_direct_init_device(void);
void lcd_direct_start_render_loop(void);
esp_err_t lcd_direct_stop_render_loop(int timeout_ms);
esp_err_t lcd_direct_set_status_overlay(const platform_lcd_status_overlay_t *overlay);
esp_err_t lcd_direct_request_presentation(const platform_lcd_presentation_request_t *request);
esp_err_t lcd_direct_preheat_presentation(const platform_lcd_presentation_request_t *request);
uint32_t lcd_direct_presentation_completion_count(void);
@@ -23,6 +23,8 @@ typedef struct {
#define LCD_DIRECT_NO_ICON 0xFFFFU
#define LCD_DIRECT_DEFAULT_ICON_COUNT 5U
#define LCD_DIRECT_ICON_CHARGING 5U
#define LCD_DIRECT_ICON_LOW_BATTERY 6U
const lcd_direct_image_t *lcd_direct_default_image(void);
const lcd_direct_image_t *lcd_direct_asset_frame(const char *asset_dir, uint8_t frame_index);
@@ -0,0 +1,12 @@
#pragma once
#include <stdint.h>
typedef enum {
PLATFORM_BATTERY_LOAD_AUDIO = 0,
PLATFORM_BATTERY_LOAD_STEPPER,
PLATFORM_BATTERY_LOAD_COUNT,
} platform_battery_load_source_t;
void platform_battery_load_begin(platform_battery_load_source_t source);
void platform_battery_load_end(platform_battery_load_source_t source);
+84 -3
View File
@@ -46,8 +46,15 @@
#define LCD_DIRECT_PRESENTATION_WAIT_POLL_MS 20U
#define LCD_DIRECT_TRANSFER_TIMEOUT_MS 100U
#define LCD_DIRECT_PREHEAT_WAIT_TIMEOUT_MS 5000U
#define LCD_DIRECT_FRAME_CACHE_MAX 4U
#define LCD_DIRECT_FRAME_CACHE_MAX 6U
#define LCD_DIRECT_PATH_MAX 96U
#define LCD_DIRECT_CHARGING_ICON_WIDTH 50U
#define LCD_DIRECT_CHARGING_ICON_X (LCD_DIRECT_WIDTH - LCD_DIRECT_CHARGING_ICON_WIDTH - 8U)
#define LCD_DIRECT_CHARGING_ICON_Y 8U
#define LCD_DIRECT_LOW_BATTERY_ICON_WIDTH 232U
#define LCD_DIRECT_LOW_BATTERY_ICON_HEIGHT 36U
#define LCD_DIRECT_LOW_BATTERY_ICON_X ((LCD_DIRECT_WIDTH - LCD_DIRECT_LOW_BATTERY_ICON_WIDTH) / 2U)
#define LCD_DIRECT_LOW_BATTERY_ICON_Y (LCD_DIRECT_HEIGHT - LCD_DIRECT_LOW_BATTERY_ICON_HEIGHT - 8U)
#define LCD_DIRECT_LAST_FRAME_EXTRA_HOLD_MS 200U
#define LCD_DIRECT_BASIC_FACE_FRAME_HOLD_MS 100U
#define LCD_DIRECT_BASIC_FACE_LAST_FRAME_HOLD_MS 100U
@@ -135,6 +142,7 @@ static bool s_render_loop_running;
static bool s_stop_requested;
static bool s_pending_valid;
static lcd_direct_request_t s_pending;
static platform_lcd_status_overlay_t s_status_overlay;
static bool s_preheat_pending_valid;
static bool s_preheat_in_progress;
static bool s_preheat_waiter_active;
@@ -683,6 +691,38 @@ static void lcd_direct_release_prepared_icons(lcd_direct_prepared_icon_t *prepar
}
}
static size_t lcd_direct_append_status_icons(const lcd_direct_image_t *image,
lcd_direct_icon_draw_t *icons,
size_t icon_count,
size_t icon_capacity)
{
if (image == NULL || image->path == NULL || icons == NULL ||
icon_count >= icon_capacity || strstr(image->path, "/qrcode.png") != NULL) {
return icon_count;
}
platform_lcd_status_overlay_t overlay = {0};
portENTER_CRITICAL(&s_state_lock);
overlay = s_status_overlay;
portEXIT_CRITICAL(&s_state_lock);
if (overlay.low_battery_visible && icon_count < icon_capacity) {
icons[icon_count++] = (lcd_direct_icon_draw_t) {
.icon_id = LCD_DIRECT_ICON_LOW_BATTERY,
.x = LCD_DIRECT_LOW_BATTERY_ICON_X,
.y = LCD_DIRECT_LOW_BATTERY_ICON_Y,
};
}
if (overlay.charging_visible && icon_count < icon_capacity) {
icons[icon_count++] = (lcd_direct_icon_draw_t) {
.icon_id = LCD_DIRECT_ICON_CHARGING,
.x = LCD_DIRECT_CHARGING_ICON_X,
.y = LCD_DIRECT_CHARGING_ICON_Y,
};
}
return icon_count;
}
static void lcd_direct_overlay_icons(uint16_t *pixel_buffer,
uint16_t row_start,
uint16_t row_count,
@@ -766,9 +806,23 @@ static esp_err_t lcd_direct_draw_png_image(const lcd_direct_image_t *image,
decoded->pinned = true;
}
lcd_direct_icon_draw_t combined_icons[LCD_DIRECT_DEFAULT_ICON_COUNT] = {0};
size_t combined_icon_count = icon_count;
if (combined_icon_count > LCD_DIRECT_DEFAULT_ICON_COUNT) {
combined_icon_count = LCD_DIRECT_DEFAULT_ICON_COUNT;
}
if (icons != NULL && combined_icon_count > 0U) {
memcpy(combined_icons, icons, combined_icon_count * sizeof(combined_icons[0]));
}
combined_icon_count = lcd_direct_append_status_icons(
image,
combined_icons,
combined_icon_count,
LCD_DIRECT_DEFAULT_ICON_COUNT);
lcd_direct_prepared_icon_t prepared_icons[LCD_DIRECT_DEFAULT_ICON_COUNT] = {0};
size_t prepared_icon_count = lcd_direct_prepare_icons(icons,
icon_count,
size_t prepared_icon_count = lcd_direct_prepare_icons(combined_icons,
combined_icon_count,
prepared_icons,
sizeof(prepared_icons) / sizeof(prepared_icons[0]));
esp_err_t err = ESP_OK;
@@ -1424,6 +1478,33 @@ esp_err_t lcd_direct_stop_render_loop(int timeout_ms)
return ESP_ERR_TIMEOUT;
}
esp_err_t lcd_direct_set_status_overlay(const platform_lcd_status_overlay_t *overlay)
{
if (overlay == NULL) {
return ESP_ERR_INVALID_ARG;
}
bool changed = false;
TaskHandle_t render_task = NULL;
portENTER_CRITICAL(&s_state_lock);
changed = s_status_overlay.low_battery_visible != overlay->low_battery_visible ||
s_status_overlay.charging_visible != overlay->charging_visible;
s_status_overlay = *overlay;
render_task = s_render_task;
portEXIT_CRITICAL(&s_state_lock);
if (changed && render_task != NULL) {
xTaskNotifyGive(render_task);
}
if (changed) {
ESP_LOGI(TAG,
"LCD battery overlays low=%d charging=%d",
overlay->low_battery_visible,
overlay->charging_visible);
}
return ESP_OK;
}
esp_err_t lcd_direct_request_presentation(const platform_lcd_presentation_request_t *request)
{
if (!s_initialized || request == NULL) {
@@ -217,6 +217,12 @@ static const lcd_direct_icon_t s_wuxing_icons[] = {
{
.path = LCD_ASSET_BASE_PATH "icon_jin0.png",
},
{
.path = LCD_ASSET_BASE_PATH "charging.png",
},
{
.path = LCD_ASSET_BASE_PATH "lowbat.png",
},
};
static const lcd_direct_image_t *lcd_direct_frame_from_array(const lcd_direct_image_t *frames,
@@ -0,0 +1,353 @@
/*
* SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "platform.h"
#include <limits.h>
#include <stdint.h>
#include <string.h>
#include "driver/gpio.h"
#include "esp_adc/adc_cali.h"
#include "esp_adc/adc_cali_scheme.h"
#include "esp_adc/adc_oneshot.h"
#include "esp_log.h"
#include "esp_rom_sys.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/portmacro.h"
#include "freertos/semphr.h"
#include "platform_battery_internal.h"
#include "sdkconfig.h"
#if CONFIG_TQ_BATTERY_ENABLE
#define BATTERY_ADC_SAMPLE_COUNT 16
#define BATTERY_ADC_TRIM_COUNT 4
#define BATTERY_VALID_MIN_MV 2500
#define BATTERY_VALID_MAX_MV 4500
#define BATTERY_ADC_SAMPLE_DELAY_US 200
static const char *TAG = "platform_battery";
static adc_oneshot_unit_handle_t s_adc_handle;
static adc_cali_handle_t s_cali_handle;
static adc_channel_t s_adc_channel;
static bool s_initialized;
static StaticSemaphore_t s_adc_lock_buffer;
static SemaphoreHandle_t s_adc_lock;
static portMUX_TYPE s_adc_lock_init_guard = portMUX_INITIALIZER_UNLOCKED;
static portMUX_TYPE s_state_lock = portMUX_INITIALIZER_UNLOCKED;
static uint16_t s_load_ref_count[PLATFORM_BATTERY_LOAD_COUNT];
static int64_t s_cooldown_until_us;
static bool lock_adc(void)
{
if (s_adc_lock == NULL) {
portENTER_CRITICAL(&s_adc_lock_init_guard);
if (s_adc_lock == NULL) {
s_adc_lock = xSemaphoreCreateMutexStatic(&s_adc_lock_buffer);
}
portEXIT_CRITICAL(&s_adc_lock_init_guard);
}
return s_adc_lock != NULL &&
xSemaphoreTake(s_adc_lock, pdMS_TO_TICKS(100)) == pdTRUE;
}
static void unlock_adc(void)
{
(void)xSemaphoreGive(s_adc_lock);
}
static uint64_t gpio_mask(int gpio_num)
{
return 1ULL << (unsigned)gpio_num;
}
static void sort_ints(int *values, size_t count)
{
for (size_t i = 1; i < count; ++i) {
int value = values[i];
size_t j = i;
while (j > 0U && values[j - 1U] > value) {
values[j] = values[j - 1U];
--j;
}
values[j] = value;
}
}
static esp_err_t create_calibration(adc_unit_t unit, adc_channel_t channel)
{
#if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED
{
adc_cali_curve_fitting_config_t config = {
.unit_id = unit,
.chan = channel,
.atten = ADC_ATTEN_DB_12,
.bitwidth = ADC_BITWIDTH_DEFAULT,
};
esp_err_t err = adc_cali_create_scheme_curve_fitting(&config, &s_cali_handle);
if (err == ESP_OK) {
return ESP_OK;
}
ESP_LOGW(TAG, "Curve-fitting ADC calibration unavailable: %s", esp_err_to_name(err));
}
#endif
#if ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED
{
adc_cali_line_fitting_config_t config = {
.unit_id = unit,
.atten = ADC_ATTEN_DB_12,
.bitwidth = ADC_BITWIDTH_DEFAULT,
.default_vref = 0,
};
esp_err_t err = adc_cali_create_scheme_line_fitting(&config, &s_cali_handle);
if (err == ESP_OK) {
return ESP_OK;
}
ESP_LOGW(TAG, "Line-fitting ADC calibration unavailable: %s", esp_err_to_name(err));
}
#endif
return ESP_ERR_NOT_SUPPORTED;
}
esp_err_t platform_battery_init(void)
{
if (!lock_adc()) {
return ESP_ERR_TIMEOUT;
}
if (s_initialized) {
unlock_adc();
return ESP_OK;
}
if (CONFIG_TQ_BATTERY_CHARGE_GPIO >= GPIO_NUM_MAX) {
unlock_adc();
return ESP_ERR_INVALID_ARG;
}
if (CONFIG_TQ_BATTERY_CHARGE_GPIO >= 0) {
gpio_config_t charge_config = {
.pin_bit_mask = gpio_mask(CONFIG_TQ_BATTERY_CHARGE_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(&charge_config);
if (gpio_err != ESP_OK) {
ESP_LOGE(TAG, "Configure TP4057 status GPIO failed: %s", esp_err_to_name(gpio_err));
unlock_adc();
return gpio_err;
}
}
adc_unit_t unit = ADC_UNIT_1;
adc_channel_t channel = ADC_CHANNEL_0;
esp_err_t err = adc_oneshot_io_to_channel(CONFIG_TQ_BATTERY_ADC_GPIO,
&unit,
&channel);
if (err != ESP_OK || unit != ADC_UNIT_1) {
ESP_LOGE(TAG,
"Battery ADC GPIO%d is not a supported ADC1 input: %s",
CONFIG_TQ_BATTERY_ADC_GPIO,
esp_err_to_name(err));
unlock_adc();
return err != ESP_OK ? err : ESP_ERR_NOT_SUPPORTED;
}
adc_oneshot_unit_init_cfg_t unit_config = {
.unit_id = unit,
.ulp_mode = ADC_ULP_MODE_DISABLE,
};
err = adc_oneshot_new_unit(&unit_config, &s_adc_handle);
if (err != ESP_OK) {
unlock_adc();
return err;
}
adc_oneshot_chan_cfg_t channel_config = {
.atten = ADC_ATTEN_DB_12,
.bitwidth = ADC_BITWIDTH_DEFAULT,
};
err = adc_oneshot_config_channel(s_adc_handle, channel, &channel_config);
if (err != ESP_OK) {
(void)adc_oneshot_del_unit(s_adc_handle);
s_adc_handle = NULL;
unlock_adc();
return err;
}
err = create_calibration(unit, channel);
if (err != ESP_OK) {
(void)adc_oneshot_del_unit(s_adc_handle);
s_adc_handle = NULL;
unlock_adc();
return err;
}
s_adc_channel = channel;
s_initialized = true;
ESP_LOGI(TAG,
"Battery monitor ready ADC_GPIO=%d divider=%d/%d charge_GPIO=%d",
CONFIG_TQ_BATTERY_ADC_GPIO,
CONFIG_TQ_BATTERY_DIVIDER_HIGH_OHM,
CONFIG_TQ_BATTERY_DIVIDER_LOW_OHM,
CONFIG_TQ_BATTERY_CHARGE_GPIO);
unlock_adc();
return ESP_OK;
}
static void snapshot_load_state(bool *out_active, bool *out_cooldown)
{
bool active = false;
int64_t cooldown_until_us = 0;
portENTER_CRITICAL(&s_state_lock);
for (size_t i = 0; i < PLATFORM_BATTERY_LOAD_COUNT; ++i) {
if (s_load_ref_count[i] > 0U) {
active = true;
break;
}
}
cooldown_until_us = s_cooldown_until_us;
portEXIT_CRITICAL(&s_state_lock);
*out_active = active;
*out_cooldown = !active && esp_timer_get_time() < cooldown_until_us;
}
esp_err_t platform_battery_read_sample(platform_battery_sample_t *out_sample)
{
if (out_sample == NULL) {
return ESP_ERR_INVALID_ARG;
}
memset(out_sample, 0, sizeof(*out_sample));
esp_err_t err = platform_battery_init();
if (err != ESP_OK) {
return err;
}
if (!lock_adc()) {
return ESP_ERR_TIMEOUT;
}
int raw = 0;
for (size_t i = 0; i < 4U; ++i) {
err = adc_oneshot_read(s_adc_handle, s_adc_channel, &raw);
if (err != ESP_OK) {
unlock_adc();
return err;
}
esp_rom_delay_us(BATTERY_ADC_SAMPLE_DELAY_US);
}
int samples_mv[BATTERY_ADC_SAMPLE_COUNT];
for (size_t i = 0; i < BATTERY_ADC_SAMPLE_COUNT; ++i) {
err = adc_oneshot_read(s_adc_handle, s_adc_channel, &raw);
if (err != ESP_OK) {
unlock_adc();
return err;
}
err = adc_cali_raw_to_voltage(s_cali_handle, raw, &samples_mv[i]);
if (err != ESP_OK) {
unlock_adc();
return err;
}
esp_rom_delay_us(BATTERY_ADC_SAMPLE_DELAY_US);
}
sort_ints(samples_mv, BATTERY_ADC_SAMPLE_COUNT);
int64_t sum_mv = 0;
for (size_t i = BATTERY_ADC_TRIM_COUNT;
i < BATTERY_ADC_SAMPLE_COUNT - BATTERY_ADC_TRIM_COUNT;
++i) {
sum_mv += samples_mv[i];
}
int adc_mv = (int)(sum_mv /
(BATTERY_ADC_SAMPLE_COUNT - 2 * BATTERY_ADC_TRIM_COUNT));
int64_t battery_mv = (int64_t)adc_mv *
((int64_t)CONFIG_TQ_BATTERY_DIVIDER_HIGH_OHM +
CONFIG_TQ_BATTERY_DIVIDER_LOW_OHM) /
CONFIG_TQ_BATTERY_DIVIDER_LOW_OHM;
battery_mv = battery_mv * CONFIG_TQ_BATTERY_CALIBRATION_GAIN_PPM / 1000000LL +
CONFIG_TQ_BATTERY_CALIBRATION_OFFSET_MV;
out_sample->adc_voltage_mv = adc_mv;
out_sample->voltage_mv = battery_mv > INT_MAX ? INT_MAX : (int)battery_mv;
out_sample->voltage_valid = out_sample->voltage_mv >= BATTERY_VALID_MIN_MV &&
out_sample->voltage_mv <= BATTERY_VALID_MAX_MV;
snapshot_load_state(&out_sample->high_load_active, &out_sample->cooldown_active);
if (CONFIG_TQ_BATTERY_CHARGE_GPIO < 0) {
out_sample->charge_state = PLATFORM_BATTERY_CHARGE_UNKNOWN;
} else {
int level = gpio_get_level((gpio_num_t)CONFIG_TQ_BATTERY_CHARGE_GPIO);
out_sample->charge_state = level == CONFIG_TQ_BATTERY_CHARGE_ACTIVE_LEVEL
? PLATFORM_BATTERY_CHARGE_CHARGING
: PLATFORM_BATTERY_CHARGE_NOT_CHARGING;
}
unlock_adc();
return ESP_OK;
}
void platform_battery_load_begin(platform_battery_load_source_t source)
{
if (source < 0 || source >= PLATFORM_BATTERY_LOAD_COUNT) {
return;
}
portENTER_CRITICAL(&s_state_lock);
if (s_load_ref_count[source] < UINT16_MAX) {
++s_load_ref_count[source];
}
portEXIT_CRITICAL(&s_state_lock);
}
void platform_battery_load_end(platform_battery_load_source_t source)
{
if (source < 0 || source >= PLATFORM_BATTERY_LOAD_COUNT) {
return;
}
int64_t cooldown_until_us = esp_timer_get_time() +
(int64_t)CONFIG_TQ_BATTERY_LOAD_COOLDOWN_SEC * 1000000LL;
portENTER_CRITICAL(&s_state_lock);
if (s_load_ref_count[source] > 0U) {
--s_load_ref_count[source];
if (cooldown_until_us > s_cooldown_until_us) {
s_cooldown_until_us = cooldown_until_us;
}
}
portEXIT_CRITICAL(&s_state_lock);
}
#else
esp_err_t platform_battery_init(void)
{
return ESP_ERR_NOT_SUPPORTED;
}
esp_err_t platform_battery_read_sample(platform_battery_sample_t *out_sample)
{
if (out_sample != NULL) {
memset(out_sample, 0, sizeof(*out_sample));
}
return ESP_ERR_NOT_SUPPORTED;
}
void platform_battery_load_begin(platform_battery_load_source_t source)
{
(void)source;
}
void platform_battery_load_end(platform_battery_load_source_t source)
{
(void)source;
}
#endif
@@ -13,6 +13,7 @@
#include "nvs.h"
#include "nvs_flash.h"
#include "platform_internal.h"
#include "platform_battery_internal.h"
#include "sdkconfig.h"
#ifndef CONFIG_TQ_STEPPER_ENABLE
@@ -256,9 +257,16 @@ esp_err_t platform_stepper_rotate_angle_with_accel_timeout(float angle,
}
}
return platform_stepper_driver_rotate_angle_with_accel(angle,
if (angle == 0.0f) {
return ESP_OK;
}
platform_battery_load_begin(PLATFORM_BATTERY_LOAD_STEPPER);
esp_err_t err = platform_stepper_driver_rotate_angle_with_accel(angle,
target_delay_us,
timeout_ms);
platform_battery_load_end(PLATFORM_BATTERY_LOAD_STEPPER);
return err;
}
void platform_stepper_power_off(void)
@@ -22,6 +22,11 @@ esp_err_t platform_stop_lcd_render_loop(int timeout_ms)
return lcd_direct_stop_render_loop(timeout_ms);
}
esp_err_t platform_lcd_set_status_overlay(const platform_lcd_status_overlay_t *overlay)
{
return lcd_direct_set_status_overlay(overlay);
}
esp_err_t platform_lcd_request_presentation(const platform_lcd_presentation_request_t *request)
{
return lcd_direct_request_presentation(request);
@@ -10,6 +10,7 @@
#include "multimodal/audio_pipeline.h"
#include "multimodal/transport_ws.h"
#include "platform_battery_internal.h"
#include "freertos/FreeRTOS.h"
#include "freertos/portmacro.h"
@@ -17,6 +18,38 @@ static platform_multimodal_transport_callbacks_t s_multimodal_transport_callback
static portMUX_TYPE s_multimodal_transport_cb_lock = portMUX_INITIALIZER_UNLOCKED;
static platform_multimodal_downstream_playback_callbacks_t s_multimodal_downstream_playback_callbacks;
static portMUX_TYPE s_multimodal_downstream_playback_cb_lock = portMUX_INITIALIZER_UNLOCKED;
static uint32_t s_battery_playback_generation;
static bool s_battery_playback_active;
static void platform_battery_note_playback_event(
multimodal_audio_downstream_playback_event_type_t type,
uint32_t generation)
{
bool begin_load = false;
bool end_load = false;
portENTER_CRITICAL(&s_multimodal_downstream_playback_cb_lock);
if (type == MULTIMODAL_AUDIO_DOWNSTREAM_PLAYBACK_STARTED) {
if (!s_battery_playback_active) {
s_battery_playback_active = true;
begin_load = true;
}
s_battery_playback_generation = generation;
} else if (s_battery_playback_active &&
s_battery_playback_generation == generation) {
s_battery_playback_active = false;
s_battery_playback_generation = 0U;
end_load = true;
}
portEXIT_CRITICAL(&s_multimodal_downstream_playback_cb_lock);
if (begin_load) {
platform_battery_load_begin(PLATFORM_BATTERY_LOAD_AUDIO);
}
if (end_load) {
platform_battery_load_end(PLATFORM_BATTERY_LOAD_AUDIO);
}
}
static void snapshot_transport_callbacks(
platform_multimodal_transport_callbacks_t *out_callbacks)
@@ -228,6 +261,8 @@ static void platform_multimodal_downstream_playback_bridge(
return;
}
platform_battery_note_playback_event(event->type, event->playback_generation);
switch (event->type) {
case MULTIMODAL_AUDIO_DOWNSTREAM_PLAYBACK_STARTED:
if (callbacks.on_started != NULL) {
@@ -0,0 +1,43 @@
# ADR 0005: Battery Management Ownership
## Status
Accepted
## Context
The current board uses GPIO9 to measure a protected 600 mAh single-cell Li-ion
battery through a 100 kOhm / 100 kOhm divider. GPIO10 receives the active-low
TP4057 charge indication. Battery state is affected by ADC calibration, audio
and stepper load transients, SOC estimation, threshold hysteresis, and runtime
policy, so it cannot have duplicate state in several layers.
## Decision
- `platform` owns ADC1 setup, calibration, trimmed-mean sampling, divider
conversion, TP4057 GPIO sampling, and internal audio/stepper load-quality
tracking.
- `domain` owns the single battery status snapshot, voltage filtering, SOC
curve interpolation, implausible-jump confirmation, charge/discharge
direction constraints, and low/critical hysteresis.
- `control_plane` owns the periodic monitoring worker and reactions to semantic
state transitions.
- `domain` maps its battery snapshot to semantic LCD visibility, while
`platform` owns transparent PNG composition and panel serialization.
- The first release logs low and critical transitions but intentionally does
not stop actuators, enter sleep, or cut power. Typed shutdown preparation
remains a reserved, unsupported hook until hardware wake and shutdown policy
are decided.
- `app_composition` keeps using the aggregate control-plane runtime lifecycle;
it does not acquire battery samples directly.
## Consequences
- GPIO9 must never be configured as the legacy power-hold output on this board.
- High-load samples remain available for diagnostics but cannot update SOC or
low-battery state. Normal processing resumes after the configured cooldown.
- GPIO10 cannot distinguish a full battery from an unplugged charger because
both produce a high level; the public state therefore reports only charging
versus not charging.
- The voltage-to-SOC curve is an initial approximation and requires bench
calibration against measured battery voltage and a controlled discharge run.
+67
View File
@@ -0,0 +1,67 @@
# Battery Management
## Hardware Profile
- Cell: protected single-cell Li-ion, 600 mAh, 3.7 V nominal, 4.2 V full.
- Supported measurement range: 3.3 V to 4.2 V for SOC estimation.
- GPIO9: ADC1 battery sense through R17=100 kOhm (VBAT to ADC) and R18=100
kOhm (ADC to ground). `VBAT = VADC * 2`.
- GPIO10: TP4057 active-low charge output with an input pull-up. A high level
means either charge complete or charger absent.
- Legacy power hold: disabled (`CONFIG_TQ_POWER_HOLD_GPIO=-1`).
## Processing Pipeline
Each hardware sample consists of 16 calibrated ADC readings. The platform
driver sorts them, discards the lowest four and highest four, averages the
remaining eight, and applies the divider ratio. Domain processing then applies:
1. A 2.5 V to 4.5 V plausibility check.
2. Three-sample confirmation for voltage jumps greater than 300 mV.
3. An exponential filter with 80% previous and 20% new voltage.
4. Piecewise-linear interpolation over the Li-ion discharge curve.
5. A two-percent maximum SOC change per accepted sample.
6. Charge/discharge direction constraints.
7. Three-sample confirmation and hysteresis for low and critical states.
The initial state thresholds are 20%/25% for low entry/recovery and 5%/10% for
critical entry/recovery.
Board calibration can be adjusted without source changes using
`CONFIG_TQ_BATTERY_CALIBRATION_GAIN_PPM` and
`CONFIG_TQ_BATTERY_CALIBRATION_OFFSET_MV` after comparing the log against a
multimeter.
## High-Load Handling
Audio playback events and synchronous stepper rotations update one
platform-owned load tracker. While a load is active and for five seconds after
its terminal event, samples are marked suppressed. The monitor continues to
poll once per second for diagnostics and charge state, but suppressed voltage
does not update filtered voltage, SOC, or low-battery state.
Playback generations prevent a stale terminal event from ending suppression
for a newer playback. Stepper cleanup executes after every synchronous return,
including errors and timeouts.
## LCD Status Overlays
The direct LCD renderer composites the transparent `charging.png` asset in the
top-right corner whenever GPIO10 reports active-low charging. It composites
`lowbat.png` at the bottom center after the domain level enters low or critical
state. Both overlays may be visible together, and QR-code presentations omit
them to preserve scan reliability. Overlay state changes do not trigger any
shutdown, sleep, prompt, or actuator policy.
## Deferred Policy
Critical status currently produces a state-transition log only. The reserved
shutdown hook returns `ESP_ERR_NOT_SUPPORTED`; it does not stop audio or the
stepper, enter deep sleep, or release board power. Implement wake sources and
the product shutdown sequence before enabling any automatic critical action.
## Bench Calibration
After flashing, compare the `cp_battery` voltage log with a multimeter at no
load near 4.2 V, 3.9 V, 3.6 V, and 3.3 V. Record audio and stepper voltage sag,
then run a controlled discharge to tune the SOC curve and threshold timing.
+3 -3
View File
@@ -23,7 +23,7 @@ dark. This is accepted for this plan.
## Startup Order
```text
power hold
power-hold configuration (disabled on the current battery-sensing board)
-> audio init
-> play boot sound with timeout
-> audio deinit or idle release
@@ -42,7 +42,7 @@ power hold
| Order | Stage | Wait policy | Failure policy | Notes |
| --- | --- | --- | --- | --- |
| 1 | Power hold GPIO | Wait | Continue only if board power remains stable | Configure the hold GPIO and assert the active level before any heavier work. |
| 1 | Power-hold configuration | Wait | Continue only if board power remains stable | The current board sets the hold GPIO to `-1`; legacy boards may configure and assert a dedicated hold output. GPIO9 is reserved for battery ADC. |
| 2 | Audio init | Wait with bounded timeout | Log and skip boot sound | Initializes the codec path needed for playback: I2C, I2S, codec, PA control, codec buffers, and decoder resources. |
| 3 | Boot sound playback | Wait for completion or timeout | Stop playback and continue boot | The sound must not block startup indefinitely. Use a short product-defined timeout. |
| 4 | Audio release | Wait for synchronous release | Continue if release reports a recoverable error | If Wi-Fi/TLS starts soon after, deinit or release idle audio resources to reduce DMA/internal heap pressure. |
@@ -53,7 +53,7 @@ power hold
| 9 | Turntable calibration | Product decision; default wait with timeout | Continue with degraded direction accuracy on timeout/failure | This is the mechanical SW1 homing/calibration process, not the stepper electrical init. |
| 10 | Storage prepare | Wait | Stop features that need persistent state | Initializes NVS before Wi-Fi credentials, identity, auth, and fortune state are read or written. |
| 11 | Wi-Fi bootstrap | Wait until STA connected or AP config mode starts | Enter config AP or keep retry policy active | Must not overlap with boot sound playback or heavy audio initialization. |
| 12 | Runtime workers | Start and continue | Stop only failed dependent feature | Starts PTT, slot switch, Wi-Fi state monitor, model command worker, LCD runtime worker, and WebSocket wait worker as configured. |
| 12 | Runtime workers | Start and continue | Stop only failed dependent feature | Starts battery monitoring, PTT, slot switch, Wi-Fi state monitor, model command worker, LCD runtime worker, and WebSocket wait worker as configured. |
| 13 | Network services | Async after network readiness | Retry using existing policy | Includes multimodal WebSocket, LAN HTTP config server, SNTP, HTTPS fortune refresh, and auth flows. |
## Clarifications
+4 -2
View File
@@ -15,7 +15,8 @@ the ESP32-S3 MCU perspective.
| 6 | `CONFIG_TQ_SLOT_SWITCH_GPIO` | Input | Slot / key input | Rocker microswitch / original SW2 function | Uses the unconnected `TP_SCL` contact; active-low with internal pull-up |
| 7 | `CONFIG_TQ_SCREEN_BACKLIGHT_GPIO` | Output | LCD | LCD adapter backlight control | Active-high by default |
| 8 | `CONFIG_TQ_SCREEN_SPI_CLK_GPIO` | Output | LCD | ST7789P3 SPI clock / SCK | SPI2 host, 20 MHz default |
| 9 | `CONFIG_TQ_POWER_HOLD_GPIO` | Output | Power | Board power hold | Active-high by default |
| 9 | `CONFIG_TQ_BATTERY_ADC_GPIO` | Analog input | Battery | Battery voltage through 100 kOhm / 100 kOhm divider | ADC1 input; do not configure as a power-hold output |
| 10 | `CONFIG_TQ_BATTERY_CHARGE_GPIO` | Input | Battery charger | TP4057 charge status | Active-low, internal pull-up enabled |
| 11 | `CONFIG_MULTIMODAL_AUDIO_PA_GPIO` | Output | Audio amplifier | PA enable | Managed by codec GPIO interface |
| 12 | `CONFIG_TQ_CALIBRATION_SWITCH_GPIO` | Input | Turntable calibration | Button 1 / SW1 direction calibration | Active-low, external and internal pull-up |
| 13 | `CONFIG_MULTIMODAL_PTT_BUTTON_GPIO` | Input | Keys / multimodal | Button 2 / K2 push-to-talk | Active-low, external and internal pull-up |
@@ -46,6 +47,7 @@ is installed.
| `CONFIG_TQ_SLOT_SWITCH_PULLDOWN` | unset | SW2 uses pull-up only. |
| `CONFIG_TQ_CALIBRATION_SWITCH_PULLDOWN` | unset | Calibration switch uses pull-up only. |
| `CONFIG_MULTIMODAL_PTT_BUTTON_PULLDOWN` | unset | Button 2 uses pull-up only. |
| `CONFIG_TQ_POWER_HOLD_GPIO` | `-1` | Legacy board power-hold output is disabled because GPIO9 is the battery ADC input. |
## Boot Hardware Test Profile
@@ -90,7 +92,6 @@ them to an active project feature.
| --- | --- | --- |
| GPIO0 / K1 | Switch to GND, commonly boot/download related | Not consumed by application code |
| GPIO3 / `touch` | TTP223 touch output | Present on the board but not consumed by the current input policy |
| GPIO10 / `Charge` | Charger status | No current firmware consumer found |
| GPIO14 | Unassigned | Not used |
| GPIO35 | Unassigned | Not used |
| GPIO36 | Unassigned | Not used |
@@ -107,6 +108,7 @@ them to an active project feature.
- `docs/gpio-map.md`
- `components/platform/src/lcd_direct.c`
- `components/platform/src/platform_stepper_driver.c`
- `components/platform/src/platform_battery_driver.c`
- `components/platform/src/multimodal/audio_pipeline/audio_pipeline_codec_impl.c`
- `components/control_plane/src/runtime/control_plane_key_input.c`
- `components/platform/src/platform_console_driver.c`
+6 -5
View File
@@ -1,8 +1,9 @@
# ESP32-S3 GPIO Map
This document defines the reference GPIO assignments for the ESP32-S3 board
from `SCH_招财猫0.3&商界0.2_2026-07-24.pdf`. Signal direction is described from
the MCU's perspective.
This document defines the active firmware GPIO assignments for the current
battery-sensing ESP32-S3 board revision. Signal direction is described from the
MCU's perspective. Older 2026-07-24 drawings assigned GPIO9 to a legacy power
hold net; that assignment is not valid for the current board.
## GPIO Table
@@ -24,8 +25,8 @@ the MCU's perspective.
| 14 | GPIO20 | ESP_D_P | Bidirectional | USB D+ | Type-C |
| 15 | GPIO3 | touch | Input | TTP223 touch-button output | Present on the board but not consumed by the current input policy |
| 16 | GPIO46 | UNASSIGNED | N/A | No connection assigned | Reserved; no signal assigned |
| 17 | GPIO9 | Power | Output | Board power hold | Active high |
| 18 | GPIO10 | Charge | Input | Charger status | Not consumed by current firmware |
| 17 | GPIO9 | Power_t | Analog input | Battery voltage sense | R17=100 kOhm from VBAT and R18=100 kOhm to GND; ADC voltage is one half of VBAT |
| 18 | GPIO10 | Charge | Input | TP4057 charger status | Active-low while charging; high means full or external power absent |
| 19 | GPIO11 | PA_CTRL | Output | Audio power-amplifier enable | Amplifier control |
| 20 | GPIO12 | SW1 | Input | Button 1 / turntable calibration switch | External 10 kOhm pull-up; active low |
| 21 | GPIO13 | SW2 | Input | Button 2 / K2 push-to-talk input | External 10 kOhm pull-up; active low |
+86 -4
View File
@@ -718,12 +718,12 @@ menu "Example Configuration"
config TQ_POWER_HOLD_GPIO
int "Power hold GPIO (-1 to disable)"
default 9
default -1
range -1 48
help
GPIO used to keep board power enabled after boot. The
2026-07-24 board maps the Power net to IO9. Set to -1 only
when no external power-hold circuit is connected.
GPIO used to keep board power enabled after boot. The current
battery-sensing board uses IO9 as an ADC input and therefore
disables the legacy power-hold output by default.
config TQ_POWER_HOLD_ACTIVE_LEVEL
int "Power hold active level"
@@ -732,6 +732,88 @@ menu "Example Configuration"
help
Logic level that keeps the external power-hold circuit active.
config TQ_BATTERY_ENABLE
bool "Enable battery monitoring"
default y
config TQ_BATTERY_ADC_GPIO
int "Battery voltage ADC GPIO"
default 9
range 0 48
depends on TQ_BATTERY_ENABLE
config TQ_BATTERY_CHARGE_GPIO
int "TP4057 charge status GPIO (-1 to disable)"
default 10
range -1 48
depends on TQ_BATTERY_ENABLE
config TQ_BATTERY_CHARGE_ACTIVE_LEVEL
int "TP4057 charging active level"
default 0
range 0 1
depends on TQ_BATTERY_ENABLE
config TQ_BATTERY_DIVIDER_HIGH_OHM
int "Battery divider resistance from VBAT to ADC (ohm)"
default 100000
range 1 10000000
depends on TQ_BATTERY_ENABLE
config TQ_BATTERY_DIVIDER_LOW_OHM
int "Battery divider resistance from ADC to ground (ohm)"
default 100000
range 1 10000000
depends on TQ_BATTERY_ENABLE
config TQ_BATTERY_CALIBRATION_GAIN_PPM
int "Board battery voltage calibration gain (ppm)"
default 1000000
range 800000 1200000
depends on TQ_BATTERY_ENABLE
config TQ_BATTERY_CALIBRATION_OFFSET_MV
int "Board battery voltage calibration offset (mV)"
default 0
range -500 500
depends on TQ_BATTERY_ENABLE
config TQ_BATTERY_SAMPLE_INTERVAL_SEC
int "Battery monitoring interval (seconds)"
default 10
range 1 3600
depends on TQ_BATTERY_ENABLE
config TQ_BATTERY_LOAD_COOLDOWN_SEC
int "Battery sampling cooldown after high load (seconds)"
default 5
range 0 60
depends on TQ_BATTERY_ENABLE
config TQ_BATTERY_LOW_ENTER_PERCENT
int "Low battery entry threshold (percent)"
default 20
range 1 99
depends on TQ_BATTERY_ENABLE
config TQ_BATTERY_LOW_EXIT_PERCENT
int "Low battery recovery threshold (percent)"
default 25
range 1 100
depends on TQ_BATTERY_ENABLE
config TQ_BATTERY_CRITICAL_ENTER_PERCENT
int "Critical battery entry threshold (percent)"
default 5
range 0 99
depends on TQ_BATTERY_ENABLE
config TQ_BATTERY_CRITICAL_EXIT_PERCENT
int "Critical battery recovery threshold (percent)"
default 10
range 1 100
depends on TQ_BATTERY_ENABLE
endmenu
menu "Wi-Fi Provisioning"
+15 -1
View File
@@ -301,6 +301,20 @@ CONFIG_TQ_CALIBRATION_SWITCH_PULLUP=y
# CONFIG_TQ_CALIBRATION_SWITCH_PULLDOWN is not set
# CONFIG_TQ_SCREEN_COLOR_AUTOTEST is not set
CONFIG_TQ_SCREEN_COLOR_AUTOTEST_INTERVAL_MS=3000
CONFIG_TQ_POWER_HOLD_GPIO=9
CONFIG_TQ_POWER_HOLD_GPIO=-1
CONFIG_TQ_POWER_HOLD_ACTIVE_LEVEL=1
CONFIG_TQ_BATTERY_ENABLE=y
CONFIG_TQ_BATTERY_ADC_GPIO=9
CONFIG_TQ_BATTERY_CHARGE_GPIO=10
CONFIG_TQ_BATTERY_CHARGE_ACTIVE_LEVEL=0
CONFIG_TQ_BATTERY_DIVIDER_HIGH_OHM=100000
CONFIG_TQ_BATTERY_DIVIDER_LOW_OHM=100000
CONFIG_TQ_BATTERY_CALIBRATION_GAIN_PPM=1000000
CONFIG_TQ_BATTERY_CALIBRATION_OFFSET_MV=0
CONFIG_TQ_BATTERY_SAMPLE_INTERVAL_SEC=10
CONFIG_TQ_BATTERY_LOAD_COOLDOWN_SEC=5
CONFIG_TQ_BATTERY_LOW_ENTER_PERCENT=20
CONFIG_TQ_BATTERY_LOW_EXIT_PERCENT=25
CONFIG_TQ_BATTERY_CRITICAL_ENTER_PERCENT=5
CONFIG_TQ_BATTERY_CRITICAL_EXIT_PERCENT=10
CONFIG_MULTIMODAL_AUDIO_PA_GPIO=11