fix(battery): detect TP4056 charging and full states on new board
This commit is contained in:
@@ -7,8 +7,6 @@
|
|||||||
#include "control_plane_internal.h"
|
#include "control_plane_internal.h"
|
||||||
|
|
||||||
#include "domain.h"
|
#include "domain.h"
|
||||||
#include "driver/gpio.h"
|
|
||||||
#include "esp_attr.h"
|
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
@@ -18,7 +16,7 @@
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* 电池后台策略层:domain 负责把原始电压稳定为状态,本任务负责提示音、充满确认和受控关机。
|
* 电池后台策略层:domain 负责把原始电压稳定为状态,本任务负责提示音、充满确认和受控关机。
|
||||||
* GPIO ISR 只唤醒/通知,不读取 ADC 或播放音频;所有慢操作都在任务上下文完成。
|
* 两路充电输入固定100ms轮询;ADC按独立截止时间采样,信号抖动不加速电量和关机确认。
|
||||||
* 关机一旦进入独立任务即视为不可逆流程,以免普通采样任务重复触发并争用电机/音频资源。
|
* 关机一旦进入独立任务即视为不可逆流程,以免普通采样任务重复触发并争用电机/音频资源。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -26,11 +24,8 @@
|
|||||||
#define BATTERY_WORKER_PRIORITY 2
|
#define BATTERY_WORKER_PRIORITY 2
|
||||||
#define BATTERY_SUPPRESSED_RETRY_MS 1000
|
#define BATTERY_SUPPRESSED_RETRY_MS 1000
|
||||||
#define BATTERY_STOP_POLL_MS 10
|
#define BATTERY_STOP_POLL_MS 10
|
||||||
#define BATTERY_CHARGE_DEBOUNCE_MS 30
|
#define BATTERY_CHARGE_POLL_MS 100
|
||||||
#define BATTERY_FULL_CONFIRM_RETRY_MS 1000
|
#define BATTERY_CONFIRM_RETRY_MS 1000
|
||||||
#define BATTERY_FULL_CONFIRM_TIME_MS \
|
|
||||||
((uint32_t)CONFIG_TQ_BATTERY_FULL_CONFIRM_SEC * 1000U)
|
|
||||||
#define BATTERY_FULL_CONFIRM_SAMPLES 3U
|
|
||||||
#define BATTERY_SHUTDOWN_CONFIRM_SAMPLES 3U
|
#define BATTERY_SHUTDOWN_CONFIRM_SAMPLES 3U
|
||||||
#define BATTERY_EMERGENCY_CONFIRM_SAMPLES 2U
|
#define BATTERY_EMERGENCY_CONFIRM_SAMPLES 2U
|
||||||
#define BATTERY_SHUTDOWN_RETRY_MS 1000
|
#define BATTERY_SHUTDOWN_RETRY_MS 1000
|
||||||
@@ -40,7 +35,7 @@
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* 多次确认用于排除负载突变导致的瞬时低压;紧急阈值只需两次以缩短保护延迟。
|
* 多次确认用于排除负载突变导致的瞬时低压;紧急阈值只需两次以缩短保护延迟。
|
||||||
* 轻待机下采样间隔显著拉长,充电 GPIO 中断仍可及时唤醒状态更新。
|
* 轻待机下采样间隔显著拉长,充电输入仍保持100ms轮询。
|
||||||
*/
|
*/
|
||||||
#define BATTERY_LOW_PROMPT_FILE "audio_prompts/battery_low_20.opus"
|
#define BATTERY_LOW_PROMPT_FILE "audio_prompts/battery_low_20.opus"
|
||||||
#define BATTERY_FULL_PROMPT_FILE "audio_prompts/charge_complete.opus"
|
#define BATTERY_FULL_PROMPT_FILE "audio_prompts/charge_complete.opus"
|
||||||
@@ -69,7 +64,6 @@ static const char *TAG = "cp_battery";
|
|||||||
static TaskHandle_t s_battery_task;
|
static TaskHandle_t s_battery_task;
|
||||||
static bool s_stop_requested;
|
static bool s_stop_requested;
|
||||||
static bool s_starting;
|
static bool s_starting;
|
||||||
static bool s_charge_gpio_isr_registered;
|
|
||||||
static bool s_light_standby;
|
static bool s_light_standby;
|
||||||
static TaskHandle_t s_shutdown_task;
|
static TaskHandle_t s_shutdown_task;
|
||||||
static uint8_t s_shutdown_confirmations;
|
static uint8_t s_shutdown_confirmations;
|
||||||
@@ -84,58 +78,6 @@ typedef enum {
|
|||||||
static battery_power_state_t s_power_state = BATTERY_POWER_STATE_RUNNING;
|
static battery_power_state_t s_power_state = BATTERY_POWER_STATE_RUNNING;
|
||||||
static portMUX_TYPE s_battery_lock = portMUX_INITIALIZER_UNLOCKED;
|
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_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)
|
static const char *charge_state_name(domain_battery_charge_state_t state)
|
||||||
{
|
{
|
||||||
switch (state) {
|
switch (state) {
|
||||||
@@ -143,6 +85,10 @@ static const char *charge_state_name(domain_battery_charge_state_t state)
|
|||||||
return "charging";
|
return "charging";
|
||||||
case DOMAIN_BATTERY_CHARGE_NOT_CHARGING:
|
case DOMAIN_BATTERY_CHARGE_NOT_CHARGING:
|
||||||
return "not_charging";
|
return "not_charging";
|
||||||
|
case DOMAIN_BATTERY_CHARGE_FULL:
|
||||||
|
return "full";
|
||||||
|
case DOMAIN_BATTERY_CHARGE_FAULT:
|
||||||
|
return "fault";
|
||||||
default:
|
default:
|
||||||
return "unknown";
|
return "unknown";
|
||||||
}
|
}
|
||||||
@@ -342,7 +288,7 @@ static void control_plane_battery_low_hook(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void control_plane_battery_full_hint_hook(
|
static void control_plane_battery_full_hook(
|
||||||
const domain_battery_status_t *status)
|
const domain_battery_status_t *status)
|
||||||
{
|
{
|
||||||
if (status == NULL) {
|
if (status == NULL) {
|
||||||
@@ -350,7 +296,7 @@ static void control_plane_battery_full_hint_hook(
|
|||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGI(TAG,
|
ESP_LOGI(TAG,
|
||||||
"Inferred charge-complete prompt start voltage=%dmV level=%u%% file=%s",
|
"TP4056 charge-complete prompt start voltage=%dmV level=%u%% file=%s",
|
||||||
status->voltage_mv,
|
status->voltage_mv,
|
||||||
(unsigned)status->percentage,
|
(unsigned)status->percentage,
|
||||||
BATTERY_FULL_PROMPT_FILE);
|
BATTERY_FULL_PROMPT_FILE);
|
||||||
@@ -369,9 +315,10 @@ static void battery_worker(void *arg)
|
|||||||
(void)ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
(void)ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||||
domain_battery_level_state_t previous_level = DOMAIN_BATTERY_LEVEL_UNKNOWN;
|
domain_battery_level_state_t previous_level = DOMAIN_BATTERY_LEVEL_UNKNOWN;
|
||||||
domain_battery_charge_state_t previous_charge = DOMAIN_BATTERY_CHARGE_UNKNOWN;
|
domain_battery_charge_state_t previous_charge = DOMAIN_BATTERY_CHARGE_UNKNOWN;
|
||||||
bool full_hint_candidate = false;
|
/* 本次运行先观察到充电才允许报满;充满后拔插或输入毛刺不能反复播放。 */
|
||||||
TickType_t full_hint_started_at = 0;
|
bool full_prompt_armed = false;
|
||||||
uint8_t full_hint_confirmations = 0U;
|
TickType_t next_voltage_sample = xTaskGetTickCount();
|
||||||
|
bool previous_standby = battery_light_standby();
|
||||||
|
|
||||||
esp_err_t init_err = domain_battery_init();
|
esp_err_t init_err = domain_battery_init();
|
||||||
if (init_err != ESP_OK) {
|
if (init_err != ESP_OK) {
|
||||||
@@ -381,21 +328,20 @@ static void battery_worker(void *arg)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t interrupt_err = battery_charge_interrupt_start();
|
ESP_LOGI(TAG, "TP4056 status polling CHRG=%d FULL=%d interval=100ms stable=1000ms",
|
||||||
if (interrupt_err == ESP_OK) {
|
CONFIG_TQ_BATTERY_CHARGE_GPIO, CONFIG_TQ_BATTERY_FULL_GPIO);
|
||||||
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");
|
ESP_LOGI(TAG, "Battery monitoring worker started");
|
||||||
while (!battery_stop_requested()) {
|
while (!battery_stop_requested()) {
|
||||||
esp_err_t sample_err = domain_battery_sample();
|
esp_err_t charge_err = domain_battery_poll_charge();
|
||||||
|
TickType_t now = xTaskGetTickCount();
|
||||||
|
bool standby = battery_light_standby();
|
||||||
|
if (standby != previous_standby) {
|
||||||
|
next_voltage_sample = now;
|
||||||
|
previous_standby = standby;
|
||||||
|
}
|
||||||
|
bool voltage_due = (int32_t)(now - next_voltage_sample) >= 0;
|
||||||
|
esp_err_t sample_err = voltage_due ? domain_battery_sample() : ESP_OK;
|
||||||
domain_battery_status_t status = {0};
|
domain_battery_status_t status = {0};
|
||||||
esp_err_t status_err = domain_battery_get_status(&status);
|
esp_err_t status_err = domain_battery_get_status(&status);
|
||||||
|
|
||||||
@@ -403,12 +349,14 @@ static void battery_worker(void *arg)
|
|||||||
ESP_LOGW(TAG, "Battery sample rejected: %s", esp_err_to_name(sample_err));
|
ESP_LOGW(TAG, "Battery sample rejected: %s", esp_err_to_name(sample_err));
|
||||||
}
|
}
|
||||||
if (status_err == ESP_OK) {
|
if (status_err == ESP_OK) {
|
||||||
esp_err_t overlay_err = domain_runtime_update_battery_overlay(&status);
|
bool charge_changed = status.charge_state != previous_charge;
|
||||||
|
esp_err_t overlay_err = (voltage_due || charge_changed)
|
||||||
|
? domain_runtime_update_battery_overlay(&status) : ESP_OK;
|
||||||
if (overlay_err != ESP_OK && overlay_err != ESP_ERR_NOT_SUPPORTED) {
|
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));
|
ESP_LOGD(TAG, "Battery LCD overlay update skipped: %s", esp_err_to_name(overlay_err));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.valid && !status.measurement_suppressed) {
|
if (voltage_due && status.valid && !status.measurement_suppressed) {
|
||||||
ESP_LOGI(TAG,
|
ESP_LOGI(TAG,
|
||||||
"Battery adc=%dmV voltage=%dmV level=%u%% charge=%s state=%s",
|
"Battery adc=%dmV voltage=%dmV level=%u%% charge=%s state=%s",
|
||||||
status.latest_adc_mv,
|
status.latest_adc_mv,
|
||||||
@@ -416,7 +364,7 @@ static void battery_worker(void *arg)
|
|||||||
(unsigned)status.percentage,
|
(unsigned)status.percentage,
|
||||||
charge_state_name(status.charge_state),
|
charge_state_name(status.charge_state),
|
||||||
level_state_name(status.level_state));
|
level_state_name(status.level_state));
|
||||||
} else if (status.measurement_suppressed) {
|
} else if (voltage_due && status.measurement_suppressed) {
|
||||||
ESP_LOGD(TAG,
|
ESP_LOGD(TAG,
|
||||||
"Battery SOC update suppressed adc=%dmV raw=%dmV valid=%d load=%d cooldown=%d",
|
"Battery SOC update suppressed adc=%dmV raw=%dmV valid=%d load=%d cooldown=%d",
|
||||||
status.latest_adc_mv,
|
status.latest_adc_mv,
|
||||||
@@ -443,76 +391,55 @@ static void battery_worker(void *arg)
|
|||||||
"Battery charge state changed: %s -> %s",
|
"Battery charge state changed: %s -> %s",
|
||||||
charge_state_name(previous_charge),
|
charge_state_name(previous_charge),
|
||||||
charge_state_name(status.charge_state));
|
charge_state_name(status.charge_state));
|
||||||
if (previous_charge == DOMAIN_BATTERY_CHARGE_CHARGING &&
|
ESP_LOGI(TAG, "TP4056 inputs valid=%d charge_active=%d full_active=%d error=%s",
|
||||||
status.charge_state == DOMAIN_BATTERY_CHARGE_NOT_CHARGING) {
|
status.charge_inputs_valid, status.charge_signal_active,
|
||||||
/*
|
status.full_signal_active, esp_err_to_name(charge_err));
|
||||||
* TP4057 exposes the same high level for charge termination
|
if (status.charge_state == DOMAIN_BATTERY_CHARGE_CHARGING) {
|
||||||
* and charger removal. Voltage can only make this a useful
|
full_prompt_armed = true;
|
||||||
* UI/audio hint, not a rigorous full-charge measurement.
|
} else if (status.charge_state == DOMAIN_BATTERY_CHARGE_FULL) {
|
||||||
* Never use this inference for safety, shutdown, or power
|
bool play_full_prompt = full_prompt_armed;
|
||||||
* switching decisions.
|
full_prompt_armed = false;
|
||||||
*/
|
if (play_full_prompt) {
|
||||||
full_hint_candidate = true;
|
control_plane_battery_full_hook(&status);
|
||||||
full_hint_started_at = xTaskGetTickCount();
|
}
|
||||||
full_hint_confirmations = 0U;
|
} else {
|
||||||
} else if (status.charge_state != DOMAIN_BATTERY_CHARGE_NOT_CHARGING) {
|
/* 拔线、异常或读取失败都打断本轮,不能把旧充电历史当作完成事件。 */
|
||||||
full_hint_candidate = false;
|
full_prompt_armed = false;
|
||||||
full_hint_confirmations = 0U;
|
|
||||||
}
|
}
|
||||||
previous_charge = status.charge_state;
|
previous_charge = status.charge_state;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (full_hint_candidate && status.valid &&
|
/* 检测到充电立即打断低压确认,不能把插拔前后的样本拼成连续低压。 */
|
||||||
!status.measurement_suppressed) {
|
if (status.charge_state == DOMAIN_BATTERY_CHARGE_CHARGING) {
|
||||||
if (status.charge_state != DOMAIN_BATTERY_CHARGE_NOT_CHARGING ||
|
s_shutdown_confirmations = 0U;
|
||||||
status.voltage_mv < CONFIG_TQ_BATTERY_FULL_MIN_VOLTAGE_MV ||
|
s_emergency_confirmations = 0U;
|
||||||
status.percentage < CONFIG_TQ_BATTERY_FULL_MIN_PERCENT) {
|
}
|
||||||
ESP_LOGI(TAG,
|
/* 安全计数只消费新的ADC样本;100ms输入轮询不得重复累计旧电压。 */
|
||||||
"Charge-complete inference cancelled charge=%s voltage=%dmV level=%u%%",
|
if (voltage_due && sample_err == ESP_OK) {
|
||||||
charge_state_name(status.charge_state),
|
battery_consider_controlled_shutdown(&status);
|
||||||
status.voltage_mv,
|
} else if (voltage_due) {
|
||||||
(unsigned)status.percentage);
|
s_shutdown_confirmations = 0U;
|
||||||
full_hint_candidate = false;
|
s_emergency_confirmations = 0U;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
battery_consider_controlled_shutdown(&status);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t delay_ms;
|
if (voltage_due) {
|
||||||
if (status_err == ESP_OK && status.measurement_suppressed) {
|
uint32_t interval_ms;
|
||||||
delay_ms = BATTERY_SUPPRESSED_RETRY_MS;
|
if (status_err != ESP_OK || sample_err != ESP_OK || status.measurement_suppressed) {
|
||||||
} else if (full_hint_candidate || s_shutdown_confirmations > 0U ||
|
interval_ms = BATTERY_SUPPRESSED_RETRY_MS;
|
||||||
s_emergency_confirmations > 0U) {
|
} else if (s_shutdown_confirmations > 0U || s_emergency_confirmations > 0U) {
|
||||||
delay_ms = BATTERY_FULL_CONFIRM_RETRY_MS;
|
interval_ms = BATTERY_CONFIRM_RETRY_MS;
|
||||||
} else if (battery_light_standby()) {
|
} else if (battery_light_standby()) {
|
||||||
delay_ms = (uint32_t)CONFIG_TQ_LIGHT_STANDBY_BATTERY_INTERVAL_SEC * 1000U;
|
interval_ms = (uint32_t)CONFIG_TQ_LIGHT_STANDBY_BATTERY_INTERVAL_SEC * 1000U;
|
||||||
} else {
|
} else {
|
||||||
delay_ms = (uint32_t)CONFIG_TQ_BATTERY_SAMPLE_INTERVAL_SEC * 1000U;
|
interval_ms = (uint32_t)CONFIG_TQ_BATTERY_SAMPLE_INTERVAL_SEC * 1000U;
|
||||||
}
|
}
|
||||||
uint32_t notifications = ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(delay_ms));
|
next_voltage_sample = xTaskGetTickCount() + pdMS_TO_TICKS(interval_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);
|
|
||||||
}
|
}
|
||||||
|
/* 通知只用于停止/待机切换;不注册GPIO ISR,持续边沿也不会造成中断风暴。 */
|
||||||
|
(void)ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(BATTERY_CHARGE_POLL_MS));
|
||||||
}
|
}
|
||||||
|
|
||||||
battery_charge_interrupt_stop();
|
|
||||||
ESP_LOGI(TAG, "Battery monitoring worker stopped");
|
|
||||||
battery_worker_finish();
|
battery_worker_finish();
|
||||||
vTaskDelete(NULL);
|
vTaskDelete(NULL);
|
||||||
}
|
}
|
||||||
@@ -524,6 +451,11 @@ esp_err_t control_plane_battery_apply_boot_guard(void)
|
|||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 深睡唤醒时先给两路状态一个有界确认窗口;不能因启动时尚未去抖就再次睡眠。 */
|
||||||
|
for (unsigned i = 0; i < 12U; ++i) {
|
||||||
|
(void)domain_battery_poll_charge();
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(BATTERY_CHARGE_POLL_MS));
|
||||||
|
}
|
||||||
esp_err_t sample_err = domain_battery_sample();
|
esp_err_t sample_err = domain_battery_sample();
|
||||||
if (sample_err != ESP_OK) {
|
if (sample_err != ESP_OK) {
|
||||||
return sample_err;
|
return sample_err;
|
||||||
@@ -540,6 +472,7 @@ esp_err_t control_plane_battery_apply_boot_guard(void)
|
|||||||
charge_state_name(status.charge_state),
|
charge_state_name(status.charge_state),
|
||||||
status.valid);
|
status.valid);
|
||||||
if (status.charge_state == DOMAIN_BATTERY_CHARGE_CHARGING ||
|
if (status.charge_state == DOMAIN_BATTERY_CHARGE_CHARGING ||
|
||||||
|
status.charge_state == DOMAIN_BATTERY_CHARGE_FULL ||
|
||||||
(status.valid &&
|
(status.valid &&
|
||||||
status.voltage_mv >= CONFIG_TQ_BATTERY_WAKE_RECOVERY_VOLTAGE_MV)) {
|
status.voltage_mv >= CONFIG_TQ_BATTERY_WAKE_RECOVERY_VOLTAGE_MV)) {
|
||||||
domain_battery_complete_wakeup();
|
domain_battery_complete_wakeup();
|
||||||
@@ -623,7 +556,6 @@ esp_err_t control_plane_battery_service_stop(int timeout_ms)
|
|||||||
if (task != NULL) {
|
if (task != NULL) {
|
||||||
ESP_LOGW(TAG, "Battery worker stop timeout; forcing teardown");
|
ESP_LOGW(TAG, "Battery worker stop timeout; forcing teardown");
|
||||||
vTaskDelete(task);
|
vTaskDelete(task);
|
||||||
battery_charge_interrupt_stop();
|
|
||||||
return ESP_ERR_TIMEOUT;
|
return ESP_ERR_TIMEOUT;
|
||||||
}
|
}
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ typedef enum {
|
|||||||
DOMAIN_BATTERY_CHARGE_UNKNOWN = 0,
|
DOMAIN_BATTERY_CHARGE_UNKNOWN = 0,
|
||||||
DOMAIN_BATTERY_CHARGE_NOT_CHARGING,
|
DOMAIN_BATTERY_CHARGE_NOT_CHARGING,
|
||||||
DOMAIN_BATTERY_CHARGE_CHARGING,
|
DOMAIN_BATTERY_CHARGE_CHARGING,
|
||||||
|
DOMAIN_BATTERY_CHARGE_FULL,
|
||||||
|
DOMAIN_BATTERY_CHARGE_FAULT,
|
||||||
} domain_battery_charge_state_t;
|
} domain_battery_charge_state_t;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
@@ -67,6 +69,9 @@ typedef struct {
|
|||||||
int latest_sample_mv;
|
int latest_sample_mv;
|
||||||
uint8_t percentage;
|
uint8_t percentage;
|
||||||
domain_battery_charge_state_t charge_state;
|
domain_battery_charge_state_t charge_state;
|
||||||
|
bool charge_inputs_valid;
|
||||||
|
bool charge_signal_active;
|
||||||
|
bool full_signal_active;
|
||||||
domain_battery_level_state_t level_state;
|
domain_battery_level_state_t level_state;
|
||||||
bool valid;
|
bool valid;
|
||||||
bool latest_sample_valid;
|
bool latest_sample_valid;
|
||||||
@@ -83,6 +88,11 @@ esp_err_t domain_battery_init(void);
|
|||||||
/** 获取并处理一次电池采样,不执行产品策略。 / Process one battery sample without product policy. */
|
/** 获取并处理一次电池采样,不执行产品策略。 / Process one battery sample without product policy. */
|
||||||
esp_err_t domain_battery_sample(void);
|
esp_err_t domain_battery_sample(void);
|
||||||
|
|
||||||
|
/** 每100ms调用;两路输入连续采样稳定1秒后更新充电状态,间隔>250ms重新确认。
|
||||||
|
* 不访问ADC;两把锁各最多等待100ms,失败不会后台继续。并发调用安全。
|
||||||
|
*/
|
||||||
|
esp_err_t domain_battery_poll_charge(void);
|
||||||
|
|
||||||
/** 复制最新电池 SSOT 快照。 / Copy the latest battery SSOT snapshot into `out_status`. */
|
/** 复制最新电池 SSOT 快照。 / Copy the latest battery SSOT snapshot into `out_status`. */
|
||||||
esp_err_t domain_battery_get_status(domain_battery_status_t *out_status);
|
esp_err_t domain_battery_get_status(domain_battery_status_t *out_status);
|
||||||
|
|
||||||
|
|||||||
@@ -100,17 +100,66 @@ static void unlock_state(void)
|
|||||||
(void)xSemaphoreGive(s_lock);
|
(void)xSemaphoreGive(s_lock);
|
||||||
}
|
}
|
||||||
|
|
||||||
static domain_battery_charge_state_t map_charge_state(
|
/* 只有领域层解释 TP4056 CHRG#/STDBY#。两个低电平同时出现视为异常,
|
||||||
platform_battery_charge_state_t state)
|
* 两个高电平只表示未充电,不能证明 USB 是否接入或电池是否存在。 */
|
||||||
|
static domain_battery_charge_state_t s_charge_candidate;
|
||||||
|
static int64_t s_charge_candidate_since_ms;
|
||||||
|
static int64_t s_charge_last_poll_ms;
|
||||||
|
static bool s_charge_candidate_valid;
|
||||||
|
static int64_t s_charge_last_confirmed_ms;
|
||||||
|
|
||||||
|
static domain_battery_charge_state_t decode_charge_inputs(bool charging, bool full)
|
||||||
{
|
{
|
||||||
switch (state) {
|
if (charging && full) {
|
||||||
case PLATFORM_BATTERY_CHARGE_CHARGING:
|
return DOMAIN_BATTERY_CHARGE_FAULT;
|
||||||
return DOMAIN_BATTERY_CHARGE_CHARGING;
|
|
||||||
case PLATFORM_BATTERY_CHARGE_NOT_CHARGING:
|
|
||||||
return DOMAIN_BATTERY_CHARGE_NOT_CHARGING;
|
|
||||||
default:
|
|
||||||
return DOMAIN_BATTERY_CHARGE_UNKNOWN;
|
|
||||||
}
|
}
|
||||||
|
if (charging) {
|
||||||
|
return DOMAIN_BATTERY_CHARGE_CHARGING;
|
||||||
|
}
|
||||||
|
return full ? DOMAIN_BATTERY_CHARGE_FULL : DOMAIN_BATTERY_CHARGE_NOT_CHARGING;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t domain_battery_poll_charge(void)
|
||||||
|
{
|
||||||
|
/* 在领域锁内读取原始输入,保证并发采样不会倒序发布状态。
|
||||||
|
* 数字输入独立于ADC:ADC失败或电机负载抑制不影响充电状态更新。 */
|
||||||
|
if (!lock_state()) {
|
||||||
|
return ESP_ERR_TIMEOUT;
|
||||||
|
}
|
||||||
|
platform_battery_charge_inputs_t inputs = {0};
|
||||||
|
esp_err_t err = platform_battery_read_charge_inputs(&inputs);
|
||||||
|
int64_t now_ms = esp_timer_get_time() / 1000;
|
||||||
|
if (err != ESP_OK || !inputs.valid) {
|
||||||
|
s_charge_candidate_valid = false;
|
||||||
|
s_status.charge_inputs_valid = false;
|
||||||
|
s_status.charge_state = DOMAIN_BATTERY_CHARGE_UNKNOWN;
|
||||||
|
unlock_state();
|
||||||
|
return err != ESP_OK ? err : ESP_ERR_INVALID_RESPONSE;
|
||||||
|
}
|
||||||
|
s_status.charge_inputs_valid = true;
|
||||||
|
s_status.charge_signal_active = inputs.charge_level == CONFIG_TQ_BATTERY_CHARGE_ACTIVE_LEVEL;
|
||||||
|
s_status.full_signal_active = inputs.full_level == CONFIG_TQ_BATTERY_FULL_ACTIVE_LEVEL;
|
||||||
|
domain_battery_charge_state_t candidate = decode_charge_inputs(
|
||||||
|
s_status.charge_signal_active, s_status.full_signal_active);
|
||||||
|
if (!s_charge_candidate_valid || candidate != s_charge_candidate ||
|
||||||
|
now_ms - s_charge_last_poll_ms > 250 || now_ms < s_charge_last_poll_ms) {
|
||||||
|
if (s_charge_candidate_valid && now_ms - s_charge_last_poll_ms > 250) {
|
||||||
|
s_status.charge_state = DOMAIN_BATTERY_CHARGE_UNKNOWN;
|
||||||
|
}
|
||||||
|
s_charge_candidate = candidate;
|
||||||
|
s_charge_candidate_since_ms = now_ms;
|
||||||
|
s_charge_candidate_valid = true;
|
||||||
|
}
|
||||||
|
s_charge_last_poll_ms = now_ms;
|
||||||
|
if (now_ms - s_charge_candidate_since_ms >= 1000) {
|
||||||
|
s_status.charge_state = candidate;
|
||||||
|
s_charge_last_confirmed_ms = now_ms;
|
||||||
|
} else if (now_ms - s_charge_last_confirmed_ms >= 2000) {
|
||||||
|
/* 持续抖动不能无限沿用旧的“充电中”,否则可能长期屏蔽低压保护。 */
|
||||||
|
s_status.charge_state = DOMAIN_BATTERY_CHARGE_UNKNOWN;
|
||||||
|
}
|
||||||
|
unlock_state();
|
||||||
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
static uint8_t voltage_to_percentage(int voltage_mv)
|
static uint8_t voltage_to_percentage(int voltage_mv)
|
||||||
@@ -221,14 +270,8 @@ esp_err_t domain_battery_init(void)
|
|||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t err = platform_battery_init();
|
/* ADC由电压采样路径幂等初始化;失败时充电输入仍能独立工作。 */
|
||||||
if (err != ESP_OK) {
|
/* 允许启动保护在 ADC 初始化前独立轮询充电输入,不能清掉已确认状态。 */
|
||||||
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_status.level_state = DOMAIN_BATTERY_LEVEL_UNKNOWN;
|
||||||
s_initialized = true;
|
s_initialized = true;
|
||||||
unlock_state();
|
unlock_state();
|
||||||
@@ -264,7 +307,6 @@ esp_err_t domain_battery_sample(void)
|
|||||||
s_status.latest_adc_mv = sample.adc_voltage_mv;
|
s_status.latest_adc_mv = sample.adc_voltage_mv;
|
||||||
s_status.latest_sample_mv = sample.voltage_mv;
|
s_status.latest_sample_mv = sample.voltage_mv;
|
||||||
s_status.latest_sample_valid = sample.voltage_valid;
|
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.high_load_active = sample.high_load_active;
|
||||||
s_status.cooldown_active = sample.cooldown_active;
|
s_status.cooldown_active = sample.cooldown_active;
|
||||||
s_status.measurement_suppressed = sample.high_load_active || sample.cooldown_active;
|
s_status.measurement_suppressed = sample.high_load_active || sample.cooldown_active;
|
||||||
@@ -385,6 +427,11 @@ esp_err_t domain_battery_init(void)
|
|||||||
return ESP_ERR_NOT_SUPPORTED;
|
return ESP_ERR_NOT_SUPPORTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
esp_err_t domain_battery_poll_charge(void)
|
||||||
|
{
|
||||||
|
return ESP_ERR_NOT_SUPPORTED;
|
||||||
|
}
|
||||||
|
|
||||||
esp_err_t domain_battery_sample(void)
|
esp_err_t domain_battery_sample(void)
|
||||||
{
|
{
|
||||||
return ESP_ERR_NOT_SUPPORTED;
|
return ESP_ERR_NOT_SUPPORTED;
|
||||||
|
|||||||
@@ -403,22 +403,25 @@ void platform_power_release_deep_sleep_holds(void);
|
|||||||
esp_err_t platform_power_enter_deep_sleep(int charger_gpio, int button_gpio);
|
esp_err_t platform_power_enter_deep_sleep(int charger_gpio, int button_gpio);
|
||||||
|
|
||||||
/* platform 只报告原始充电和电压质量,电量等级/SOC 语义由 domain 层计算。 */
|
/* platform 只报告原始充电和电压质量,电量等级/SOC 语义由 domain 层计算。 */
|
||||||
typedef enum {
|
/* 板级数字输入原始电平;电平组合含义及去抖由 domain 唯一解释。 */
|
||||||
PLATFORM_BATTERY_CHARGE_UNKNOWN = 0,
|
typedef struct {
|
||||||
PLATFORM_BATTERY_CHARGE_NOT_CHARGING,
|
int charge_level;
|
||||||
PLATFORM_BATTERY_CHARGE_CHARGING,
|
int full_level;
|
||||||
} platform_battery_charge_state_t;
|
bool valid;
|
||||||
|
} platform_battery_charge_inputs_t;
|
||||||
|
|
||||||
|
/** 读取两路原始电平;无 ADC 访问,互斥等待最多100ms。 */
|
||||||
|
esp_err_t platform_battery_read_charge_inputs(platform_battery_charge_inputs_t *out_inputs);
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
int adc_voltage_mv;
|
int adc_voltage_mv;
|
||||||
int voltage_mv;
|
int voltage_mv;
|
||||||
platform_battery_charge_state_t charge_state;
|
|
||||||
bool voltage_valid;
|
bool voltage_valid;
|
||||||
bool high_load_active;
|
bool high_load_active;
|
||||||
bool cooldown_active;
|
bool cooldown_active;
|
||||||
} platform_battery_sample_t;
|
} platform_battery_sample_t;
|
||||||
|
|
||||||
/** 幂等初始化电池 ADC 和 TP4057 状态输入。 / Idempotently initialize battery sensing. */
|
/** 幂等初始化电池 ADC 和 TP4056 两路状态输入。 / Idempotently initialize battery sensing. */
|
||||||
esp_err_t platform_battery_init(void);
|
esp_err_t platform_battery_init(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ static adc_oneshot_unit_handle_t s_adc_handle;
|
|||||||
static adc_cali_handle_t s_cali_handle;
|
static adc_cali_handle_t s_cali_handle;
|
||||||
static adc_channel_t s_adc_channel;
|
static adc_channel_t s_adc_channel;
|
||||||
static bool s_initialized;
|
static bool s_initialized;
|
||||||
|
static bool s_inputs_initialized;
|
||||||
static StaticSemaphore_t s_adc_lock_buffer;
|
static StaticSemaphore_t s_adc_lock_buffer;
|
||||||
static SemaphoreHandle_t s_adc_lock;
|
static SemaphoreHandle_t s_adc_lock;
|
||||||
static portMUX_TYPE s_adc_lock_init_guard = portMUX_INITIALIZER_UNLOCKED;
|
static portMUX_TYPE s_adc_lock_init_guard = portMUX_INITIALIZER_UNLOCKED;
|
||||||
@@ -123,6 +124,53 @@ static esp_err_t create_calibration(adc_unit_t unit, adc_channel_t channel)
|
|||||||
return ESP_ERR_NOT_SUPPORTED;
|
return ESP_ERR_NOT_SUPPORTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 调用者持有 ADC/输入互斥锁;数字输入可独立初始化,不依赖 ADC 校准成功。 */
|
||||||
|
static esp_err_t configure_charge_inputs_locked(void)
|
||||||
|
{
|
||||||
|
if (s_inputs_initialized) {
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
if (!GPIO_IS_VALID_GPIO(CONFIG_TQ_BATTERY_CHARGE_GPIO) ||
|
||||||
|
!GPIO_IS_VALID_GPIO(CONFIG_TQ_BATTERY_FULL_GPIO) ||
|
||||||
|
CONFIG_TQ_BATTERY_CHARGE_GPIO == CONFIG_TQ_BATTERY_FULL_GPIO ||
|
||||||
|
CONFIG_TQ_BATTERY_ADC_GPIO == CONFIG_TQ_BATTERY_CHARGE_GPIO ||
|
||||||
|
CONFIG_TQ_BATTERY_ADC_GPIO == CONFIG_TQ_BATTERY_FULL_GPIO) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
gpio_config_t config = {
|
||||||
|
.pin_bit_mask = gpio_mask(CONFIG_TQ_BATTERY_CHARGE_GPIO) |
|
||||||
|
gpio_mask(CONFIG_TQ_BATTERY_FULL_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 err = gpio_config(&config);
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
s_inputs_initialized = true;
|
||||||
|
}
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t platform_battery_read_charge_inputs(platform_battery_charge_inputs_t *out_inputs)
|
||||||
|
{
|
||||||
|
if (out_inputs == NULL) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
memset(out_inputs, 0, sizeof(*out_inputs));
|
||||||
|
if (!lock_adc()) {
|
||||||
|
return ESP_ERR_TIMEOUT;
|
||||||
|
}
|
||||||
|
esp_err_t err = configure_charge_inputs_locked();
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
out_inputs->charge_level = gpio_get_level(CONFIG_TQ_BATTERY_CHARGE_GPIO);
|
||||||
|
out_inputs->full_level = gpio_get_level(CONFIG_TQ_BATTERY_FULL_GPIO);
|
||||||
|
out_inputs->valid = true;
|
||||||
|
}
|
||||||
|
unlock_adc();
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
esp_err_t platform_battery_init(void)
|
esp_err_t platform_battery_init(void)
|
||||||
{
|
{
|
||||||
if (!lock_adc()) {
|
if (!lock_adc()) {
|
||||||
@@ -133,24 +181,10 @@ esp_err_t platform_battery_init(void)
|
|||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CONFIG_TQ_BATTERY_CHARGE_GPIO >= GPIO_NUM_MAX) {
|
esp_err_t input_err = configure_charge_inputs_locked();
|
||||||
|
if (input_err != ESP_OK) {
|
||||||
unlock_adc();
|
unlock_adc();
|
||||||
return ESP_ERR_INVALID_ARG;
|
return input_err;
|
||||||
}
|
|
||||||
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 charger status GPIO failed: %s", esp_err_to_name(gpio_err));
|
|
||||||
unlock_adc();
|
|
||||||
return gpio_err;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
adc_unit_t unit = ADC_UNIT_1;
|
adc_unit_t unit = ADC_UNIT_1;
|
||||||
@@ -295,14 +329,6 @@ esp_err_t platform_battery_read_sample(platform_battery_sample_t *out_sample)
|
|||||||
out_sample->voltage_mv <= BATTERY_VALID_MAX_MV;
|
out_sample->voltage_mv <= BATTERY_VALID_MAX_MV;
|
||||||
snapshot_load_state(&out_sample->high_load_active, &out_sample->cooldown_active);
|
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();
|
unlock_adc();
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
@@ -341,6 +367,14 @@ void platform_battery_load_end(platform_battery_load_source_t source)
|
|||||||
|
|
||||||
#else
|
#else
|
||||||
|
|
||||||
|
esp_err_t platform_battery_read_charge_inputs(platform_battery_charge_inputs_t *out_inputs)
|
||||||
|
{
|
||||||
|
if (out_inputs != NULL) {
|
||||||
|
memset(out_inputs, 0, sizeof(*out_inputs));
|
||||||
|
}
|
||||||
|
return ESP_ERR_NOT_SUPPORTED;
|
||||||
|
}
|
||||||
|
|
||||||
esp_err_t platform_battery_init(void)
|
esp_err_t platform_battery_init(void)
|
||||||
{
|
{
|
||||||
return ESP_ERR_NOT_SUPPORTED;
|
return ESP_ERR_NOT_SUPPORTED;
|
||||||
|
|||||||
@@ -51,14 +51,15 @@ not been enabled as a replacement.
|
|||||||
| `CONFIG_TQ_POWER_HOLD_GPIO` | -1 | No MCU power-hold net on the new board |
|
| `CONFIG_TQ_POWER_HOLD_GPIO` | -1 | No MCU power-hold net on the new board |
|
||||||
| `CONFIG_TQ_RESET_BUTTON_ENABLE` | Disabled | The old optional reset default GPIO14 now belongs to stepper IN1; configure separate physical wiring before enabling |
|
| `CONFIG_TQ_RESET_BUTTON_ENABLE` | Disabled | The old optional reset default GPIO14 now belongs to stepper IN1; configure separate physical wiring before enabling |
|
||||||
| `CONFIG_TQ_BATTERY_SHUTDOWN_WAKE_BUTTON_GPIO` | -1 | No additional deep-sleep wake button |
|
| `CONFIG_TQ_BATTERY_SHUTDOWN_WAKE_BUTTON_GPIO` | -1 | No additional deep-sleep wake button |
|
||||||
| GPIO8 / 充满检测 | Not consumed | Full-charge input exists on hardware; no option/driver added |
|
| GPIO8 / 充满检测 | Input | `CONFIG_TQ_BATTERY_FULL_GPIO=8`, low-active TP4056 STDBY# via Q8 |
|
||||||
| GPIO10 / 马达控制 | Not consumed | Rocker-motor gate held off by external R30 pull-down; no new option/driver added |
|
| GPIO10 / 马达控制 | Not consumed | Rocker-motor gate held off by external R30 pull-down; no new option/driver added |
|
||||||
| GPIO3 / 35 / 36 / 37 / 45 / 46 | NC on schematic | Do not infer arbitrary availability from NC |
|
| GPIO3 / 35 / 36 / 37 / 45 / 46 | NC on schematic | Do not infer arbitrary availability from NC |
|
||||||
|
|
||||||
Mechanical switches use pull-up only. PTT and charger active levels are retained
|
Mechanical switches use pull-up only. PTT and charger active levels are retained
|
||||||
from the previous configuration and require actual idle/active measurements.
|
from the previous configuration and require actual idle/active measurements.
|
||||||
Battery full-charge prompts still use voltage/percentage/time inference rather
|
Battery charge/full state uses GPIO18/GPIO8 with 100ms polling and 1s sampled
|
||||||
than GPIO8. ADC divider gain/offset remain unchanged pending VBAT measurements.
|
stability confirmation. Both active means fault; both inactive means not charging.
|
||||||
|
Full prompts require a confirmed charging-to-full transition; no voltage inference. ADC divider gain/offset remain unchanged pending VBAT measurements.
|
||||||
|
|
||||||
## Boot Calibration And Sleep
|
## Boot Calibration And Sleep
|
||||||
|
|
||||||
@@ -66,8 +67,8 @@ than GPIO8. ADC divider gain/offset remain unchanged pending VBAT measurements.
|
|||||||
switch 1 on GPIO11 with IN1/IN2/IN3/IN4 = 14/21/47/48. Verify motor direction,
|
switch 1 on GPIO11 with IN1/IN2/IN3/IN4 = 14/21/47/48. Verify motor direction,
|
||||||
mechanical limits and bounded stop on hardware.
|
mechanical limits and bounded stop on hardware.
|
||||||
|
|
||||||
Charge-state edge interrupts and low-active deep-sleep wake both follow
|
Charge inputs are polled without GPIO interrupts. Low-active deep-sleep wake follows
|
||||||
`CONFIG_TQ_BATTERY_CHARGE_GPIO=18`. Verify charger insertion/removal and wake
|
`CONFIG_TQ_BATTERY_CHARGE_GPIO=18`; the full input is not a wake source. Verify charger insertion/removal and wake
|
||||||
behavior after changing the board.
|
behavior after changing the board.
|
||||||
|
|
||||||
## System Interfaces
|
## System Interfaces
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 新旧板 GPIO 对比与固件适配位置
|
# 新旧板 GPIO 对比与固件适配位置
|
||||||
|
|
||||||
核对日期:2026-10-23。初始对照保留在下文;当前已应用现有功能的新板引脚映射,新增功能仍未接入。验证结果见文末,未烧录或进行实机验收。
|
核对日期:2026-10-23。初始对照保留在下文;当前已应用新板引脚映射及GPIO8满电检测,GPIO10马达仍未接入。验证结果见文末,未烧录或进行实机验收。
|
||||||
|
|
||||||
## 对照基线
|
## 对照基线
|
||||||
|
|
||||||
@@ -17,10 +17,10 @@
|
|||||||
|
|
||||||
大部分引脚在 `sdkconfig.defaults` 和 `main/Kconfig.projbuild` 中修改即可;已有本地 `sdkconfig` 还须通过 `menuconfig` 同步,因为修改 defaults 不会覆盖已经保存的配置。不要手改 `build/config/sdkconfig.h`,也不要提交本地 `sdkconfig`。
|
大部分引脚在 `sdkconfig.defaults` 和 `main/Kconfig.projbuild` 中修改即可;已有本地 `sdkconfig` 还须通过 `menuconfig` 同步,因为修改 defaults 不会覆盖已经保存的配置。不要手改 `build/config/sdkconfig.h`,也不要提交本地 `sdkconfig`。
|
||||||
|
|
||||||
后续菜单整理:现有 42 个 GPIO/电平/上下拉配置已集中到
|
菜单整理:GPIO/电平/上下拉配置已集中到
|
||||||
`TQ Cat Configuration -> Board Hardware Configuration (GPIO)` 单页,按 LCD、音频、
|
`TQ Cat Configuration -> Board Hardware Configuration (GPIO)` 单页,按 LCD、音频、
|
||||||
输入、步进、电源/电池分区。配置名及依赖保持原样;默认值及本地有效配置已更新为新板映射,
|
输入、步进、电源/电池分区。配置名及依赖保持原样;默认值及本地有效配置已更新为新板映射,
|
||||||
未新增 GPIO8 充满检测或 GPIO10 马达配置。旧硬编码 GPIO4/5 功能保留源码,
|
本次充电逻辑重写已新增 GPIO8 充满输入及有效电平配置,GPIO10 马达仍未接入。旧硬编码 GPIO4/5 功能保留源码,
|
||||||
平台初始化已移除其启动调用,不再创建这些任务。
|
平台初始化已移除其启动调用,不再创建这些任务。
|
||||||
下文 Kconfig 旧行号属于对照时快照,搬迁后应按配置名定位。
|
下文 Kconfig 旧行号属于对照时快照,搬迁后应按配置名定位。
|
||||||
|
|
||||||
@@ -38,8 +38,8 @@
|
|||||||
| LCD 时钟 CLK / 新板 SCL | 8 | 8 | **16** | `TQ_SCREEN_SPI_CLK_GPIO`;`lcd_direct.c` SPI bus |
|
| LCD 时钟 CLK / 新板 SCL | 8 | 8 | **16** | `TQ_SCREEN_SPI_CLK_GPIO`;`lcd_direct.c` SPI bus |
|
||||||
| LCD MISO | 未接 | -1 | 未接 | `TQ_SCREEN_SPI_MISO_GPIO=-1` 保持 |
|
| LCD MISO | 未接 | -1 | 未接 | `TQ_SCREEN_SPI_MISO_GPIO=-1` 保持 |
|
||||||
| 电池电压检测 Power_t / 电量检测 | 9 | 9 | **17** | `TQ_BATTERY_ADC_GPIO`;`platform_battery_driver.c` |
|
| 电池电压检测 Power_t / 电量检测 | 9 | 9 | **17** | `TQ_BATTERY_ADC_GPIO`;`platform_battery_driver.c` |
|
||||||
| 充电状态 Charge / 充电检测 | 10 | 10 | **18** | `TQ_BATTERY_CHARGE_GPIO`;电池驱动、充电边沿中断及深睡唤醒链路 |
|
| 充电状态 Charge / 充电检测 | 10 | 10 | **18** | `TQ_BATTERY_CHARGE_GPIO`;电池驱动、双路轮询及深睡唤醒链路 |
|
||||||
| 独立充满检测 | 未接入 MCU | 无 | **8** | 当前无配置项;需新增采样配置和状态映射,见后文 |
|
| 独立充满检测 | 未接入 MCU | 无 | **8** | `TQ_BATTERY_FULL_GPIO` / `TQ_BATTERY_FULL_ACTIVE_LEVEL`;见后文 |
|
||||||
| 功放 PA_CTRL | 11 | 11 | **9** | `MULTIMODAL_AUDIO_PA_GPIO`;音频 codec 初始化及运行时使能 |
|
| 功放 PA_CTRL | 11 | 11 | **9** | `MULTIMODAL_AUDIO_PA_GPIO`;音频 codec 初始化及运行时使能 |
|
||||||
| 开关1 / 原 SW1 | 12 | 校准输入 12 | **11** | 若保持开关1作校准:`TQ_CALIBRATION_SWITCH_GPIO` |
|
| 开关1 / 原 SW1 | 12 | 校准输入 12 | **11** | 若保持开关1作校准:`TQ_CALIBRATION_SWITCH_GPIO` |
|
||||||
| 开关2 / 原 SW2 | 13 | 抽签输入 13 | **12** | 若保持开关2作抽签:`TQ_SLOT_SWITCH_GPIO` |
|
| 开关2 / 原 SW2 | 13 | 抽签输入 13 | **12** | 若保持开关2作抽签:`TQ_SLOT_SWITCH_GPIO` |
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
| [platform_battery_driver.c](../components/platform/src/platform_battery_driver.c) | 约 136 行状态输入;158 行 ADC 映射;296 行采样状态 | 迁移 ADC/充电输入;新增独立充满输入及原始样本处理 |
|
| [platform_battery_driver.c](../components/platform/src/platform_battery_driver.c) | 约 136 行状态输入;158 行 ADC 映射;296 行采样状态 | 迁移 ADC/充电输入;新增独立充满输入及原始样本处理 |
|
||||||
| [platform.h](../components/platform/include/platform.h) | 约 406 行电池硬件样本契约 | 如接入充满检测,扩展平台硬件样本,保留未知/无效状态 |
|
| [platform.h](../components/platform/include/platform.h) | 约 406 行电池硬件样本契约 | 如接入充满检测,扩展平台硬件样本,保留未知/无效状态 |
|
||||||
| [domain.h](../components/domain/include/domain.h) / [domain_battery_service.c](../components/domain/src/battery/domain_battery_service.c) | 领域电池语义;样本到领域状态映射;约 363 行深睡调用 | 领域层拥有充电/满电语义,不能由显示层直接读取 GPIO8 判断 |
|
| [domain.h](../components/domain/include/domain.h) / [domain_battery_service.c](../components/domain/src/battery/domain_battery_service.c) | 领域电池语义;样本到领域状态映射;约 363 行深睡调用 | 领域层拥有充电/满电语义,不能由显示层直接读取 GPIO8 判断 |
|
||||||
| [control_plane_battery_service.c](../components/control_plane/src/runtime/control_plane_battery_service.c) | 104 行起充电中断;约 400 行后满电推断/提示 | 迁移充电引脚配置自动覆盖现有中断;若新增满电检测,调整确认/提示策略和相应事件来源 |
|
| [control_plane_battery_service.c](../components/control_plane/src/runtime/control_plane_battery_service.c) | 充电轮询、独立ADC定时及提示 | 不注册充电GPIO中断;只有领域确认充电→满电才提示 |
|
||||||
| [domain_runtime_adapters.c](../components/domain/src/runtime/domain_runtime_adapters.c) | 807 / 836 / 865 行三路数字输入映射 | 已读取配置,单纯迁脚通常无需改;核对 PTT 有效电平 |
|
| [domain_runtime_adapters.c](../components/domain/src/runtime/domain_runtime_adapters.c) | 807 / 836 / 865 行三路数字输入映射 | 已读取配置,单纯迁脚通常无需改;核对 PTT 有效电平 |
|
||||||
| [control_plane_key_input.c](../components/control_plane/src/runtime/control_plane_key_input.c) | 158 行起 `s_key_configs` | 同步核对三路按键及可选 reset,不把两个功能配置到同一 GPIO |
|
| [control_plane_key_input.c](../components/control_plane/src/runtime/control_plane_key_input.c) | 158 行起 `s_key_configs` | 同步核对三路按键及可选 reset,不把两个功能配置到同一 GPIO |
|
||||||
| [audio_pipeline_codec_impl.c](../components/platform/src/multimodal/audio_pipeline/audio_pipeline_codec_impl.c) | 148 行 I2C;223 行 I2S;318 行 PA | I2C/I2S 不变,PA 配置迁移到 9 |
|
| [audio_pipeline_codec_impl.c](../components/platform/src/multimodal/audio_pipeline/audio_pipeline_codec_impl.c) | 148 行 I2C;223 行 I2S;318 行 PA | I2C/I2S 不变,PA 配置迁移到 9 |
|
||||||
@@ -122,11 +122,19 @@ GPIO17 的“电量检测”来自下板 Q5/Q6 控制后的电池分压:R6、R
|
|||||||
|
|
||||||
新下板正式电路使用 TP4056,CHRG#/STDBY# 经 Q7/Q8、3.3V 上拉后送至 GPIO18/GPIO8。按电路作用可预期状态拉低时对应输入低有效,但这属于图纸推断:首次上板应记录“未插电 / 正在充电 / 充满 / 无电池”的两路实际电平,再确定配置和异常组合处理。不能仅凭换了充电芯片就反转电平。
|
新下板正式电路使用 TP4056,CHRG#/STDBY# 经 Q7/Q8、3.3V 上拉后送至 GPIO18/GPIO8。按电路作用可预期状态拉低时对应输入低有效,但这属于图纸推断:首次上板应记录“未插电 / 正在充电 / 充满 / 无电池”的两路实际电平,再确定配置和异常组合处理。不能仅凭换了充电芯片就反转电平。
|
||||||
|
|
||||||
现有固件只有“未知 / 未充电 / 充电中”硬件充电状态,满电提示依赖电压、百分比、时间确认。`TQ_BATTERY_FULL_MIN_*` 是满电推断阈值,**不是 GPIO8 配置项**。
|
当前固件以两路输入生成“未知 / 未充电 / 充电中 / 满电 / 异常”状态。
|
||||||
|
每100ms轮询,采样电平组合稳定1秒才更新;采样间断超过250ms重新确认,
|
||||||
|
持续2秒未能确认稳定组合时退出旧状态并标记未知,避免旧“充电中”长期屏蔽低压保护。
|
||||||
|
两路同时有效为异常,两路均无效只代表未充电,不证明 USB 或电池是否存在。
|
||||||
|
ADC失败、负载抑制不阻塞数字输入更新。
|
||||||
|
|
||||||
最小迁移可以先只使用 GPIO18,保留旧满电推断,并明确 GPIO8 尚未使用。要利用独立充满检测,需增加配置、平台原始采样、领域状态解释及控制层提示策略;不在上层 facade 复制平台类型,也不在 LCD 内读取原始 GPIO。
|
满电提示要求本次运行先确认充电,再直接确认满电;启动已满不播、拔线/异常/读取失败
|
||||||
|
打断本轮、不用电压和百分比推断满电。旧 `TQ_BATTERY_FULL_MIN_*` 和
|
||||||
|
`TQ_BATTERY_FULL_CONFIRM_SEC` 已移除;新增两项仍放在 GPIO 单页。
|
||||||
|
详细触发规则、测试和实机检查见 [新板充电检测](新板充电检测.zh-CN.md)。
|
||||||
|
|
||||||
充电 GPIO 同时参与中断和深睡唤醒:`domain_battery_service.c` 将 `CONFIG_TQ_BATTERY_CHARGE_GPIO` 传给 `platform_power_enter_deep_sleep()`,实施时一并检查唤醒驱动对新引脚及有效电平的处理。
|
GPIO18仍为低有效深睡唤醒源,GPIO8不参与唤醒。唤醒保护先轮询两路约1.2秒再读取ADC,
|
||||||
|
避免尚未去抖就再次入睡;深睡接口仍要求板级低有效连接。
|
||||||
|
|
||||||
### 4. 两个机械开关与触摸 PTT
|
### 4. 两个机械开关与触摸 PTT
|
||||||
|
|
||||||
@@ -144,7 +152,7 @@ GPIO17 的“电量检测”来自下板 Q5/Q6 控制后的电池分压:R6、R
|
|||||||
|
|
||||||
## 已应用的新板配置
|
## 已应用的新板配置
|
||||||
|
|
||||||
以下已有配置项均已写入 Kconfig 默认值、sdkconfig.defaults 和本地 sdkconfig。PTT 有效电平仍待实测,新增 GPIO8/10 功能未接入。
|
以下已有配置项均已写入 Kconfig 默认值、sdkconfig.defaults 和本地 sdkconfig。PTT 有效电平仍待实测;GPIO8现已接入,GPIO10仍未接入。
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
CONFIG_TQ_SCREEN_BACKLIGHT_GPIO=4
|
CONFIG_TQ_SCREEN_BACKLIGHT_GPIO=4
|
||||||
@@ -171,7 +179,7 @@ CONFIG_TQ_STEPPER_IN3_GPIO=47
|
|||||||
CONFIG_TQ_STEPPER_IN4_GPIO=48
|
CONFIG_TQ_STEPPER_IN4_GPIO=48
|
||||||
```
|
```
|
||||||
|
|
||||||
平台初始化已停止调用旧 GPIO4/5 输出模块;GPIO8 满电和 GPIO10 马达没有新增配置项。音频 SDA=1、SCL=2、MCLK=38、BCLK=39、DOUT=40、WS=41、DIN=42 保持。
|
平台初始化已停止调用旧 GPIO4/5 输出模块;GPIO8满电输入已接入,GPIO10马达仍未接入。音频 SDA=1、SCL=2、MCLK=38、BCLK=39、DOUT=40、WS=41、DIN=42 保持。
|
||||||
|
|
||||||
## 实施顺序与验证边界
|
## 实施顺序与验证边界
|
||||||
|
|
||||||
@@ -184,7 +192,7 @@ CONFIG_TQ_STEPPER_IN4_GPIO=48
|
|||||||
|
|
||||||
本次证据层级:两份 PDF 文本提取及关键连线视觉核对、工作区配置和源码静态核对。新板配置的构建与回归记录见下方;没有设备验证结果。
|
本次证据层级:两份 PDF 文本提取及关键连线视觉核对、工作区配置和源码静态核对。新板配置的构建与回归记录见下方;没有设备验证结果。
|
||||||
|
|
||||||
### 2026-10-23 重新切换新板后的验证记录
|
### 2026-10-23 重新切换新板后的验证记录(充电逻辑重写前快照)
|
||||||
|
|
||||||
本次在保留 VAD 重复 PTT 修复和 kikivok.com 域名迁移的基础上重新应用;以下主机测试与构建均已重新执行。
|
本次在保留 VAD 重复 PTT 修复和 kikivok.com 域名迁移的基础上重新应用;以下主机测试与构建均已重新执行。
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -20,8 +20,8 @@ Configuration (GPIO)`; see [firmware usage](firmware-pin-usage.md).
|
|||||||
| 8 | GPIO15 | SDA | Output | ST7789P3 SPI MOSI | SPI data, not I2C SDA |
|
| 8 | GPIO15 | SDA | Output | ST7789P3 SPI MOSI | SPI data, not I2C SDA |
|
||||||
| 9 | GPIO16 | SCL | Output | ST7789P3 SPI clock | SPI clock, not I2C SCL |
|
| 9 | GPIO16 | SCL | Output | ST7789P3 SPI clock | SPI clock, not I2C SCL |
|
||||||
| 10 | GPIO17 | 电量检测 | Analog input | Battery voltage sense | Lower-board R6/R11=100 kOhm each, Q5/Q6 switching; upper-board C31 filter |
|
| 10 | GPIO17 | 电量检测 | Analog input | Battery voltage sense | Lower-board R6/R11=100 kOhm each, Q5/Q6 switching; upper-board C31 filter |
|
||||||
| 11 | GPIO18 | 充电检测 | Input | TP4056 CHRG# via Q7 | Low-active assumption requires hardware verification |
|
| 11 | GPIO18 | 充电检测 | Input | TP4056 CHRG# via Q7 | Low-active; polled with GPIO8; hardware verification pending |
|
||||||
| 12 | GPIO8 | 充满检测 | Input | TP4056 STDBY# via Q8 | Not consumed by current firmware; no new option added |
|
| 12 | GPIO8 | 充满检测 | Input | TP4056 STDBY# via Q8 | Low-active; CONFIG_TQ_BATTERY_FULL_GPIO; stable confirmation in domain |
|
||||||
| 13 | GPIO19 | ESP_D_N | Bidirectional | USB D- | Type-C |
|
| 13 | GPIO19 | ESP_D_N | Bidirectional | USB D- | Type-C |
|
||||||
| 14 | GPIO20 | ESP_D_P | Bidirectional | USB D+ | Type-C |
|
| 14 | GPIO20 | ESP_D_P | Bidirectional | USB D+ | Type-C |
|
||||||
| 15 | GPIO3 | NC | N/A | No assigned net | Old touch input moved to GPIO13 |
|
| 15 | GPIO3 | NC | N/A | No assigned net | Old touch input moved to GPIO13 |
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# 新板 TP4056 充电检测
|
||||||
|
|
||||||
|
2026-10-23:本实现替换旧单路充电检测与满电电压推断。
|
||||||
|
|
||||||
|
## 接线与状态
|
||||||
|
|
||||||
|
新板正式 TP4056 电路的 CHRG# 经 Q7 接 GPIO18,STDBY# 经 Q8 接 GPIO8。
|
||||||
|
两路默认低有效,3.3V上拉。配置位于 `TQ Cat Configuration -> Board Hardware
|
||||||
|
Configuration (GPIO)`;GPIO10马达没有接入。
|
||||||
|
|
||||||
|
| GPIO18 | GPIO8 | 确认后的领域状态 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 低 | 高 | charging,充电中 |
|
||||||
|
| 高 | 低 | full,充满信号有效 |
|
||||||
|
| 高 | 高 | not_charging,未充电 |
|
||||||
|
| 低 | 低 | fault,异常组合 |
|
||||||
|
|
||||||
|
原理图只能支持接线推断,仍须实测未插电、充电、充满、无电池的实际电平。
|
||||||
|
两个状态脚不能可靠确定USB存在或电池存在;`full`表示芯片输出的稳定充满信号,
|
||||||
|
不等于检测到了电池。百分比仍按电压估算,不因满电信号强制改为100%。
|
||||||
|
|
||||||
|
## 检测与提示
|
||||||
|
|
||||||
|
- 平台只读取原始数字电平和ADC,不解释充电语义。数字输入初始化不依赖ADC校准。
|
||||||
|
- 领域每100ms检查一次两路组合;连续采样稳定1秒才发布状态。超过250ms未轮询,
|
||||||
|
丢弃旧稳定假设并重新确认;持续抖动2秒则置未知。读取失败立即置未知。
|
||||||
|
这是采样去抖,不能证明两个采样点之间没有短脉冲。
|
||||||
|
- 控制层不注册GPIO边沿中断,复用原有电池任务,无新增任务或定时器。
|
||||||
|
领域快照同时保留最近读取的有效性和两路有效信号,供状态转换日志诊断;
|
||||||
|
最近原始输入可能与尚未改变的去抖状态不同。
|
||||||
|
- 本次运行先确认充电,再确认满电才播放一次满电提示。启动已满、充电后拔线、
|
||||||
|
异常或读取失败均不播放;后两类还会取消本轮提示资格。重新确认充电可开启下一轮。
|
||||||
|
- 移除旧的最低满电电压、百分比及30秒推断时间配置;满电不再靠电压推断。
|
||||||
|
GPIO页新增 `TQ_BATTERY_FULL_GPIO=8`、`TQ_BATTERY_FULL_ACTIVE_LEVEL=0`。
|
||||||
|
|
||||||
|
## ADC、低电量和深睡
|
||||||
|
|
||||||
|
ADC正常10秒一次,轻待机300秒一次,失败或负载抑制1秒后重试;进入或退出轻待机
|
||||||
|
立即安排新样本。低压关机确认期间1秒一次。数字信号抖动不加速ADC采样,也不重复
|
||||||
|
消费旧样本推进关机计数。ADC失败会清空控制层低压确认计数。
|
||||||
|
|
||||||
|
原有电量曲线、滤波、负载抑制及阈值保持:低电量≤20%确认3次,≥25%退出;
|
||||||
|
未判定充电时,电量≤5%且滤波电压≤3.45V确认3次,或最新有效电压≤3.35V确认2次,
|
||||||
|
进入受控关机。未知/异常不冒充充电,不用来长期屏蔽原有低压保护。
|
||||||
|
|
||||||
|
GPIO18仍是低有效深睡唤醒源;GPIO8不作为唤醒源。唤醒保护提供约1.2秒有界轮询窗口,
|
||||||
|
确认充电或满电,或有效电压≥3.55V后允许继续启动。窗口内调度中断或输入抖动可能
|
||||||
|
导致状态仍未知,此时按电压判定,不无限等待。
|
||||||
|
|
||||||
|
## 验证与实机验收
|
||||||
|
|
||||||
|
主机回归入口:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 tools/test_battery_adc_init.py
|
||||||
|
python3 tools/test_battery_charge_detection.py
|
||||||
|
```
|
||||||
|
|
||||||
|
第一项提取实际平台C函数,验证GPIO18/8联合输入初始化、独立数字读取、ADC1/ADC2、
|
||||||
|
锁失败、校准失败释放和重试。第二项编译实际领域代码,用可控时钟和GPIO/ADC验证
|
||||||
|
四种输入组合、启动未知、连续抖动、稳定确认、长采样间隔、读取失败、ADC失败及
|
||||||
|
负载抑制独立性;并提取实际控制层电池任务,验证ADC节奏、待机切换、启动已满、
|
||||||
|
充电→满电提示、拔线取消及ADC失败不累计关机确认。
|
||||||
|
使用AddressSanitizer和UndefinedBehaviorSanitizer。
|
||||||
|
|
||||||
|
本次上述两个电池测试与五个显示回归脚本已通过;ESP-IDF 5.5.4新板配置构建通过。
|
||||||
|
未烧录,尚无本次逻辑的实机电平、提示或深睡验收结果。
|
||||||
|
|
||||||
|
实机仍需验证:不插电静置、插拔充电、充满、无电池、扬声器/电机/Wi-Fi并行、低电量
|
||||||
|
保护、深睡插电唤醒。对照GPIO18/8电平和万用表VBAT;主机测试或构建不能代替这些测量。
|
||||||
+14
-24
@@ -278,9 +278,9 @@ menu "TQ Cat Configuration"
|
|||||||
depends on TQ_BATTERY_ENABLE
|
depends on TQ_BATTERY_ENABLE
|
||||||
|
|
||||||
config TQ_BATTERY_CHARGE_GPIO
|
config TQ_BATTERY_CHARGE_GPIO
|
||||||
int "TP4056 charge status GPIO (-1 to disable)"
|
int "TP4056 charge status GPIO"
|
||||||
default 18
|
default 18
|
||||||
range -1 48
|
range 0 48
|
||||||
depends on TQ_BATTERY_ENABLE
|
depends on TQ_BATTERY_ENABLE
|
||||||
|
|
||||||
config TQ_BATTERY_CHARGE_ACTIVE_LEVEL
|
config TQ_BATTERY_CHARGE_ACTIVE_LEVEL
|
||||||
@@ -289,6 +289,18 @@ menu "TQ Cat Configuration"
|
|||||||
range 0 1
|
range 0 1
|
||||||
depends on TQ_BATTERY_ENABLE
|
depends on TQ_BATTERY_ENABLE
|
||||||
|
|
||||||
|
config TQ_BATTERY_FULL_GPIO
|
||||||
|
int "TP4056 full status GPIO"
|
||||||
|
default 8
|
||||||
|
range 0 48
|
||||||
|
depends on TQ_BATTERY_ENABLE
|
||||||
|
|
||||||
|
config TQ_BATTERY_FULL_ACTIVE_LEVEL
|
||||||
|
int "TP4056 full active level"
|
||||||
|
default 0
|
||||||
|
range 0 1
|
||||||
|
depends on TQ_BATTERY_ENABLE
|
||||||
|
|
||||||
config TQ_BATTERY_SHUTDOWN_WAKE_BUTTON_GPIO
|
config TQ_BATTERY_SHUTDOWN_WAKE_BUTTON_GPIO
|
||||||
int "Optional active-low deep-sleep wake button GPIO (-1 to disable)"
|
int "Optional active-low deep-sleep wake button GPIO (-1 to disable)"
|
||||||
default -1
|
default -1
|
||||||
@@ -1053,28 +1065,6 @@ menu "TQ Cat Configuration"
|
|||||||
bool "Enable battery monitoring"
|
bool "Enable battery monitoring"
|
||||||
default y
|
default y
|
||||||
|
|
||||||
config TQ_BATTERY_FULL_MIN_VOLTAGE_MV
|
|
||||||
int "Minimum voltage for charge-complete prompt (mV)"
|
|
||||||
default 4150
|
|
||||||
range 4000 4200
|
|
||||||
depends on TQ_BATTERY_ENABLE
|
|
||||||
help
|
|
||||||
The charging-status input alone cannot distinguish charge termination
|
|
||||||
from charger removal. Require at least this filtered battery
|
|
||||||
voltage before inferring that charging has completed.
|
|
||||||
|
|
||||||
config TQ_BATTERY_FULL_MIN_PERCENT
|
|
||||||
int "Minimum level for charge-complete prompt (percent)"
|
|
||||||
default 95
|
|
||||||
range 90 100
|
|
||||||
depends on TQ_BATTERY_ENABLE
|
|
||||||
|
|
||||||
config TQ_BATTERY_FULL_CONFIRM_SEC
|
|
||||||
int "Charge-complete stable confirmation time (seconds)"
|
|
||||||
default 30
|
|
||||||
range 5 300
|
|
||||||
depends on TQ_BATTERY_ENABLE
|
|
||||||
|
|
||||||
config TQ_BATTERY_DIVIDER_HIGH_OHM
|
config TQ_BATTERY_DIVIDER_HIGH_OHM
|
||||||
int "Battery divider resistance from VBAT to ADC (ohm)"
|
int "Battery divider resistance from VBAT to ADC (ohm)"
|
||||||
default 100000
|
default 100000
|
||||||
|
|||||||
+2
-3
@@ -323,10 +323,9 @@ CONFIG_TQ_POWER_HOLD_ACTIVE_LEVEL=1
|
|||||||
CONFIG_TQ_BATTERY_ENABLE=y
|
CONFIG_TQ_BATTERY_ENABLE=y
|
||||||
CONFIG_TQ_BATTERY_ADC_GPIO=17
|
CONFIG_TQ_BATTERY_ADC_GPIO=17
|
||||||
CONFIG_TQ_BATTERY_CHARGE_GPIO=18
|
CONFIG_TQ_BATTERY_CHARGE_GPIO=18
|
||||||
|
CONFIG_TQ_BATTERY_FULL_GPIO=8
|
||||||
|
CONFIG_TQ_BATTERY_FULL_ACTIVE_LEVEL=0
|
||||||
CONFIG_TQ_BATTERY_CHARGE_ACTIVE_LEVEL=0
|
CONFIG_TQ_BATTERY_CHARGE_ACTIVE_LEVEL=0
|
||||||
CONFIG_TQ_BATTERY_FULL_MIN_VOLTAGE_MV=4150
|
|
||||||
CONFIG_TQ_BATTERY_FULL_MIN_PERCENT=95
|
|
||||||
CONFIG_TQ_BATTERY_FULL_CONFIRM_SEC=30
|
|
||||||
CONFIG_TQ_BATTERY_DIVIDER_HIGH_OHM=100000
|
CONFIG_TQ_BATTERY_DIVIDER_HIGH_OHM=100000
|
||||||
CONFIG_TQ_BATTERY_DIVIDER_LOW_OHM=100000
|
CONFIG_TQ_BATTERY_DIVIDER_LOW_OHM=100000
|
||||||
CONFIG_TQ_BATTERY_CALIBRATION_GAIN_PPM=1000000
|
CONFIG_TQ_BATTERY_CALIBRATION_GAIN_PPM=1000000
|
||||||
|
|||||||
@@ -12,13 +12,18 @@ SOURCE = ROOT / "components/platform/src/platform_battery_driver.c"
|
|||||||
|
|
||||||
def initializer():
|
def initializer():
|
||||||
source = SOURCE.read_text()
|
source = SOURCE.read_text()
|
||||||
start = source.index("esp_err_t platform_battery_init(void)")
|
parts = []
|
||||||
body = source.index("{", start)
|
for signature in ["static esp_err_t configure_charge_inputs_locked(void)",
|
||||||
end, depth = body + 1, 1
|
"esp_err_t platform_battery_read_charge_inputs(",
|
||||||
while depth:
|
"esp_err_t platform_battery_init(void)"]:
|
||||||
depth += (source[end] == "{") - (source[end] == "}")
|
start = source.index(signature)
|
||||||
end += 1
|
body = source.index("{", start)
|
||||||
return source[start:end]
|
end, depth = body + 1, 1
|
||||||
|
while depth:
|
||||||
|
depth += (source[end] == "{") - (source[end] == "}")
|
||||||
|
end += 1
|
||||||
|
parts.append(source[start:end])
|
||||||
|
return "\n".join(parts)
|
||||||
|
|
||||||
|
|
||||||
STUBS = r'''
|
STUBS = r'''
|
||||||
@@ -26,6 +31,7 @@ STUBS = r'''
|
|||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
typedef int esp_err_t;
|
typedef int esp_err_t;
|
||||||
typedef int adc_unit_t;
|
typedef int adc_unit_t;
|
||||||
typedef int adc_channel_t;
|
typedef int adc_channel_t;
|
||||||
@@ -58,7 +64,10 @@ typedef struct { int unit_id, ulp_mode; } adc_oneshot_unit_init_cfg_t;
|
|||||||
typedef struct { int atten, bitwidth; } adc_oneshot_chan_cfg_t;
|
typedef struct { int atten, bitwidth; } adc_oneshot_chan_cfg_t;
|
||||||
static adc_oneshot_unit_handle_t s_adc_handle;
|
static adc_oneshot_unit_handle_t s_adc_handle;
|
||||||
static adc_channel_t s_adc_channel;
|
static adc_channel_t s_adc_channel;
|
||||||
static bool s_initialized, locked;
|
static bool s_initialized, s_inputs_initialized, locked;
|
||||||
|
#define GPIO_IS_VALID_GPIO(n) ((n) >= 0 && (n) < GPIO_NUM_MAX)
|
||||||
|
typedef struct { int charge_level, full_level; bool valid; } platform_battery_charge_inputs_t;
|
||||||
|
static int gpio_get_level(int gpio) { return gpio == CONFIG_TQ_BATTERY_FULL_GPIO; }
|
||||||
static int fail_lock, gpio_error, mapping_error, unit_error, channel_error, calibration_error;
|
static int fail_lock, gpio_error, mapping_error, unit_error, channel_error, calibration_error;
|
||||||
static int gpio_calls, unit_calls, delete_calls, calibration_calls;
|
static int gpio_calls, unit_calls, delete_calls, calibration_calls;
|
||||||
static int actual_unit, actual_channel;
|
static int actual_unit, actual_channel;
|
||||||
@@ -103,7 +112,7 @@ static esp_err_t adc_oneshot_del_unit(adc_oneshot_unit_handle_t handle) {
|
|||||||
TESTS = r'''
|
TESTS = r'''
|
||||||
static void reset(void) {
|
static void reset(void) {
|
||||||
assert(!locked);
|
assert(!locked);
|
||||||
s_initialized = false; s_adc_handle = NULL;
|
s_initialized = s_inputs_initialized = false; s_adc_handle = NULL;
|
||||||
fail_lock = gpio_error = mapping_error = unit_error = channel_error = calibration_error = 0;
|
fail_lock = gpio_error = mapping_error = unit_error = channel_error = calibration_error = 0;
|
||||||
gpio_calls = unit_calls = delete_calls = calibration_calls = 0;
|
gpio_calls = unit_calls = delete_calls = calibration_calls = 0;
|
||||||
}
|
}
|
||||||
@@ -113,7 +122,7 @@ int main(void) {
|
|||||||
assert(s_initialized && !locked);
|
assert(s_initialized && !locked);
|
||||||
assert(actual_unit == EXPECTED_UNIT && actual_channel == EXPECTED_CHANNEL);
|
assert(actual_unit == EXPECTED_UNIT && actual_channel == EXPECTED_CHANNEL);
|
||||||
assert(s_adc_channel == EXPECTED_CHANNEL && calibration_calls == 1);
|
assert(s_adc_channel == EXPECTED_CHANNEL && calibration_calls == 1);
|
||||||
assert(charge_mask == (UINT64_C(1) << CONFIG_TQ_BATTERY_CHARGE_GPIO));
|
assert(charge_mask == ((UINT64_C(1) << CONFIG_TQ_BATTERY_CHARGE_GPIO) | (UINT64_C(1) << CONFIG_TQ_BATTERY_FULL_GPIO)));
|
||||||
assert(platform_battery_init() == ESP_OK && unit_calls == 1 && gpio_calls == 1);
|
assert(platform_battery_init() == ESP_OK && unit_calls == 1 && gpio_calls == 1);
|
||||||
reset(); fail_lock = 1;
|
reset(); fail_lock = 1;
|
||||||
assert(platform_battery_init() == ESP_ERR_TIMEOUT && !s_initialized && !unit_calls);
|
assert(platform_battery_init() == ESP_ERR_TIMEOUT && !s_initialized && !unit_calls);
|
||||||
@@ -131,8 +140,15 @@ int main(void) {
|
|||||||
reset(); calibration_error = ESP_ERR_NOT_SUPPORTED;
|
reset(); calibration_error = ESP_ERR_NOT_SUPPORTED;
|
||||||
assert(platform_battery_init() == calibration_error && !locked && !s_initialized);
|
assert(platform_battery_init() == calibration_error && !locked && !s_initialized);
|
||||||
assert(delete_calls == 1 && s_adc_handle == NULL);
|
assert(delete_calls == 1 && s_adc_handle == NULL);
|
||||||
|
platform_battery_charge_inputs_t inputs = {0};
|
||||||
|
assert(platform_battery_read_charge_inputs(&inputs) == ESP_OK);
|
||||||
|
assert(inputs.valid && inputs.charge_level == 0 && inputs.full_level == 1);
|
||||||
|
assert(!s_initialized); /* 数字输入不依赖ADC校准 */
|
||||||
calibration_error = ESP_OK;
|
calibration_error = ESP_OK;
|
||||||
assert(platform_battery_init() == ESP_OK && s_initialized);
|
assert(platform_battery_init() == ESP_OK && s_initialized);
|
||||||
|
assert(platform_battery_read_charge_inputs(NULL) == ESP_ERR_INVALID_ARG);
|
||||||
|
fail_lock = 1; inputs.valid = true;
|
||||||
|
assert(platform_battery_read_charge_inputs(&inputs) == ESP_ERR_TIMEOUT && !inputs.valid);
|
||||||
puts("PASS: battery ADC unit/channel propagation, idempotence, bounded lock failure, cleanup and retry");
|
puts("PASS: battery ADC unit/channel propagation, idempotence, bounded lock failure, cleanup and retry");
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
@@ -154,6 +170,7 @@ def main():
|
|||||||
"-fsanitize=address,undefined", "-fno-omit-frame-pointer",
|
"-fsanitize=address,undefined", "-fno-omit-frame-pointer",
|
||||||
f"-DCONFIG_TQ_BATTERY_ADC_GPIO={gpio}",
|
f"-DCONFIG_TQ_BATTERY_ADC_GPIO={gpio}",
|
||||||
f"-DCONFIG_TQ_BATTERY_CHARGE_GPIO={charge_gpio}",
|
f"-DCONFIG_TQ_BATTERY_CHARGE_GPIO={charge_gpio}",
|
||||||
|
"-DCONFIG_TQ_BATTERY_FULL_GPIO=8",
|
||||||
f"-DEXPECTED_UNIT={unit}", f"-DEXPECTED_CHANNEL={channel}",
|
f"-DEXPECTED_UNIT={unit}", f"-DEXPECTED_CHANNEL={channel}",
|
||||||
str(source), "-o", str(binary),
|
str(source), "-o", str(binary),
|
||||||
], check=True)
|
], check=True)
|
||||||
|
|||||||
@@ -0,0 +1,257 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Compile production battery domain with fake clock/GPIO/ADC; no copied state machine."""
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
def facade_type(path, name):
|
||||||
|
text = (ROOT / path).read_text()
|
||||||
|
return next(m.group() for m in re.finditer(r'typedef\s+(?:enum|struct)\s*\{[^}]*\}\s*\w+;', text)
|
||||||
|
if re.search(r'\}\s*' + name + ';', m.group()))
|
||||||
|
|
||||||
|
|
||||||
|
STUBS = r'''
|
||||||
|
#include <assert.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#define CONFIG_TQ_BATTERY_ENABLE 1
|
||||||
|
#define CONFIG_TQ_BATTERY_CONTROLLED_SHUTDOWN_ENABLE 0
|
||||||
|
#define CONFIG_TQ_BATTERY_LOW_ENTER_PERCENT 20
|
||||||
|
#define CONFIG_TQ_BATTERY_LOW_EXIT_PERCENT 25
|
||||||
|
#define CONFIG_TQ_BATTERY_CRITICAL_ENTER_PERCENT 5
|
||||||
|
#define CONFIG_TQ_BATTERY_CRITICAL_EXIT_PERCENT 10
|
||||||
|
#define CONFIG_TQ_BATTERY_CHARGE_ACTIVE_LEVEL 0
|
||||||
|
#define CONFIG_TQ_BATTERY_FULL_ACTIVE_LEVEL 0
|
||||||
|
#define ESP_OK 0
|
||||||
|
#define ESP_ERR_TIMEOUT 1
|
||||||
|
#define ESP_ERR_INVALID_ARG 2
|
||||||
|
#define ESP_ERR_INVALID_STATE 3
|
||||||
|
#define ESP_ERR_NOT_SUPPORTED 4
|
||||||
|
#define ESP_ERR_INVALID_RESPONSE 5
|
||||||
|
#define pdTRUE 1
|
||||||
|
#define pdMS_TO_TICKS(x) (x)
|
||||||
|
#define portMUX_INITIALIZER_UNLOCKED 0
|
||||||
|
#define portENTER_CRITICAL(x) ((void)(x))
|
||||||
|
#define portEXIT_CRITICAL(x) ((void)(x))
|
||||||
|
typedef int esp_err_t;
|
||||||
|
typedef int StaticSemaphore_t;
|
||||||
|
typedef int *SemaphoreHandle_t;
|
||||||
|
typedef int portMUX_TYPE;
|
||||||
|
static bool lock_failure;
|
||||||
|
static int64_t fake_ms;
|
||||||
|
static int gpio_error, adc_error;
|
||||||
|
static unsigned adc_calls;
|
||||||
|
static platform_battery_charge_inputs_t fake_inputs = {1, 1, true};
|
||||||
|
static platform_battery_sample_t fake_sample = {.voltage_mv=4180, .voltage_valid=true};
|
||||||
|
static int64_t esp_timer_get_time(void) { return fake_ms * 1000; }
|
||||||
|
static SemaphoreHandle_t xSemaphoreCreateMutexStatic(StaticSemaphore_t *p) { return p; }
|
||||||
|
static int xSemaphoreTake(SemaphoreHandle_t p, int ms) { assert(p && ms==100); return !lock_failure; }
|
||||||
|
static int xSemaphoreGive(SemaphoreHandle_t p) { assert(p); return 1; }
|
||||||
|
static esp_err_t platform_battery_read_charge_inputs(platform_battery_charge_inputs_t *out) {
|
||||||
|
*out = fake_inputs; return gpio_error;
|
||||||
|
}
|
||||||
|
static esp_err_t platform_battery_read_sample(platform_battery_sample_t *out) {
|
||||||
|
++adc_calls; *out = fake_sample; return adc_error;
|
||||||
|
}
|
||||||
|
static bool platform_power_woke_from_deep_sleep(void) { return false; }
|
||||||
|
static void platform_power_release_deep_sleep_holds(void) {}
|
||||||
|
'''
|
||||||
|
|
||||||
|
def production_function(text, signature):
|
||||||
|
start = text.index(signature)
|
||||||
|
body = text.index('{', start)
|
||||||
|
end, depth = body + 1, 1
|
||||||
|
while depth:
|
||||||
|
depth += (text[end] == '{') - (text[end] == '}')
|
||||||
|
end += 1
|
||||||
|
return text[start:end]
|
||||||
|
|
||||||
|
|
||||||
|
WORKER_STUBS = r'''
|
||||||
|
#define CONFIG_TQ_BATTERY_CHARGE_GPIO 18
|
||||||
|
#define CONFIG_TQ_BATTERY_FULL_GPIO 8
|
||||||
|
#define CONFIG_TQ_BATTERY_SAMPLE_INTERVAL_SEC 10
|
||||||
|
#define CONFIG_TQ_LIGHT_STANDBY_BATTERY_INTERVAL_SEC 300
|
||||||
|
#define BATTERY_CHARGE_POLL_MS 100
|
||||||
|
#define BATTERY_SUPPRESSED_RETRY_MS 1000
|
||||||
|
#define BATTERY_CONFIRM_RETRY_MS 1000
|
||||||
|
#define portMAX_DELAY UINT32_MAX
|
||||||
|
#define ESP_LOGI(tag, ...) do { if(0) printf(__VA_ARGS__); (void)(tag); } while(0)
|
||||||
|
#define ESP_LOGW ESP_LOGI
|
||||||
|
#define ESP_LOGE ESP_LOGI
|
||||||
|
#define ESP_LOGD ESP_LOGI
|
||||||
|
typedef uint32_t TickType_t;
|
||||||
|
static const char *TAG = "test";
|
||||||
|
static uint8_t s_shutdown_confirmations, s_emergency_confirmations;
|
||||||
|
static unsigned full_prompts, shutdown_checks, last_shutdown_adc_calls, worker_finishes;
|
||||||
|
static int scenario;
|
||||||
|
static int64_t worker_end_ms;
|
||||||
|
static const char *esp_err_to_name(esp_err_t err) { (void)err; return "test"; }
|
||||||
|
static TickType_t xTaskGetTickCount(void) { return (TickType_t)fake_ms; }
|
||||||
|
static bool battery_light_standby(void) {
|
||||||
|
return scenario == 4 && fake_ms >= 1000 && fake_ms < 3000;
|
||||||
|
}
|
||||||
|
static bool battery_stop_requested(void) { return fake_ms >= worker_end_ms; }
|
||||||
|
static void battery_worker_finish(void) { ++worker_finishes; }
|
||||||
|
static void vTaskDelete(void *p) { (void)p; }
|
||||||
|
static uint32_t ulTaskNotifyTake(int clear, TickType_t ticks) {
|
||||||
|
assert(clear);
|
||||||
|
if(ticks == portMAX_DELAY) return 1;
|
||||||
|
assert(ticks == 100);
|
||||||
|
fake_ms += ticks;
|
||||||
|
if(scenario == 1) {
|
||||||
|
fake_inputs.charge_level = 1; fake_inputs.full_level = 0;
|
||||||
|
} else if(scenario == 2) {
|
||||||
|
fake_inputs.charge_level = fake_ms < 2000 ? 0 : 1;
|
||||||
|
fake_inputs.full_level = fake_ms < 2000 ? 1 : 0;
|
||||||
|
} else if(scenario == 3) {
|
||||||
|
fake_inputs.charge_level = fake_ms < 2000 ? 0 : 1;
|
||||||
|
fake_inputs.full_level = fake_ms < 4000 ? 1 : 0;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
static esp_err_t domain_runtime_update_battery_overlay(const domain_battery_status_t *s) {
|
||||||
|
assert(s); return ESP_OK;
|
||||||
|
}
|
||||||
|
static void control_plane_battery_low_hook(const domain_battery_status_t *s) { assert(s); }
|
||||||
|
static void control_plane_battery_full_hook(const domain_battery_status_t *s) {
|
||||||
|
assert(s->charge_state == DOMAIN_BATTERY_CHARGE_FULL); ++full_prompts;
|
||||||
|
}
|
||||||
|
static void battery_consider_controlled_shutdown(const domain_battery_status_t *s) {
|
||||||
|
assert(s && adc_calls > last_shutdown_adc_calls);
|
||||||
|
last_shutdown_adc_calls = adc_calls; ++shutdown_checks;
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
TESTS = r'''
|
||||||
|
static void poll(int charging_level, int full_level, unsigned ms) {
|
||||||
|
fake_inputs.charge_level = charging_level;
|
||||||
|
fake_inputs.full_level = full_level;
|
||||||
|
for (unsigned i=0; i<ms/100; ++i) {
|
||||||
|
fake_ms += 100;
|
||||||
|
assert(domain_battery_poll_charge() == ESP_OK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int main(void) {
|
||||||
|
assert(domain_battery_init() == ESP_OK);
|
||||||
|
assert(domain_battery_init() == ESP_OK);
|
||||||
|
assert(adc_calls == 0);
|
||||||
|
assert(s_status.charge_state == DOMAIN_BATTERY_CHARGE_UNKNOWN);
|
||||||
|
poll(1, 1, 1000);
|
||||||
|
assert(s_status.charge_state == DOMAIN_BATTERY_CHARGE_UNKNOWN);
|
||||||
|
poll(1, 1, 100);
|
||||||
|
assert(s_status.charge_state == DOMAIN_BATTERY_CHARGE_NOT_CHARGING);
|
||||||
|
// Continuous chatter never counts as stable charging/full.
|
||||||
|
for (int i=0; i<500; ++i) poll(i%2, 1, 100);
|
||||||
|
assert(s_status.charge_state == DOMAIN_BATTERY_CHARGE_UNKNOWN && adc_calls == 0);
|
||||||
|
poll(0, 1, 1100);
|
||||||
|
assert(s_status.charge_state == DOMAIN_BATTERY_CHARGE_CHARGING);
|
||||||
|
poll(1, 0, 1000);
|
||||||
|
assert(s_status.charge_state == DOMAIN_BATTERY_CHARGE_CHARGING);
|
||||||
|
poll(1, 0, 100);
|
||||||
|
assert(s_status.charge_state == DOMAIN_BATTERY_CHARGE_FULL);
|
||||||
|
assert(s_status.charge_inputs_valid && !s_status.charge_signal_active && s_status.full_signal_active);
|
||||||
|
poll(1, 1, 1100);
|
||||||
|
assert(s_status.charge_state == DOMAIN_BATTERY_CHARGE_NOT_CHARGING);
|
||||||
|
poll(0, 0, 1100);
|
||||||
|
assert(s_status.charge_state == DOMAIN_BATTERY_CHARGE_FAULT);
|
||||||
|
// Reading failures invalidate stale charging; stable confirmation restarts.
|
||||||
|
gpio_error = ESP_ERR_TIMEOUT;
|
||||||
|
assert(domain_battery_poll_charge() == ESP_ERR_TIMEOUT);
|
||||||
|
assert(!s_status.charge_inputs_valid && s_status.charge_state == DOMAIN_BATTERY_CHARGE_UNKNOWN);
|
||||||
|
gpio_error = 0;
|
||||||
|
fake_inputs.valid = false;
|
||||||
|
assert(domain_battery_poll_charge() == ESP_ERR_INVALID_RESPONSE);
|
||||||
|
fake_inputs.valid = true;
|
||||||
|
poll(0, 1, 1100);
|
||||||
|
assert(s_status.charge_state == DOMAIN_BATTERY_CHARGE_CHARGING);
|
||||||
|
fake_ms += 1000; // Scheduling gap cannot be counted as stability.
|
||||||
|
poll(0, 1, 100);
|
||||||
|
assert(s_status.charge_state == DOMAIN_BATTERY_CHARGE_UNKNOWN);
|
||||||
|
poll(0, 1, 1000);
|
||||||
|
assert(s_status.charge_state == DOMAIN_BATTERY_CHARGE_CHARGING);
|
||||||
|
// Persistent bounce must not keep a stale charging state indefinitely.
|
||||||
|
for (int i=0; i<25; ++i) poll(i%2, 1, 100);
|
||||||
|
assert(s_status.charge_state == DOMAIN_BATTERY_CHARGE_UNKNOWN);
|
||||||
|
// ADC failure/load suppression does not block digital state changes.
|
||||||
|
adc_error = ESP_ERR_INVALID_RESPONSE;
|
||||||
|
assert(domain_battery_sample() == adc_error);
|
||||||
|
poll(1, 0, 1100);
|
||||||
|
assert(s_status.charge_state == DOMAIN_BATTERY_CHARGE_FULL);
|
||||||
|
adc_error = 0;
|
||||||
|
fake_sample.high_load_active = true;
|
||||||
|
assert(domain_battery_sample() == ESP_OK && s_status.measurement_suppressed);
|
||||||
|
poll(0, 1, 1100);
|
||||||
|
assert(s_status.charge_state == DOMAIN_BATTERY_CHARGE_CHARGING);
|
||||||
|
fake_sample.high_load_active = false;
|
||||||
|
assert(domain_battery_sample() == ESP_OK && s_status.valid);
|
||||||
|
domain_battery_status_t status;
|
||||||
|
assert(domain_battery_get_status(&status) == ESP_OK);
|
||||||
|
assert(status.voltage_mv == 4180 && status.percentage == 98);
|
||||||
|
assert(domain_battery_get_status(NULL) == ESP_ERR_INVALID_ARG);
|
||||||
|
lock_failure = true;
|
||||||
|
assert(domain_battery_poll_charge() == ESP_ERR_TIMEOUT);
|
||||||
|
assert(domain_battery_get_status(&status) == ESP_ERR_TIMEOUT);
|
||||||
|
lock_failure = false;
|
||||||
|
for(scenario=1; scenario<=5; ++scenario) {
|
||||||
|
fake_ms = 0; worker_end_ms = 35000;
|
||||||
|
full_prompts = shutdown_checks = last_shutdown_adc_calls = worker_finishes = adc_calls = 0;
|
||||||
|
s_initialized = s_filter_initialized = s_charge_candidate_valid = false;
|
||||||
|
s_charge_last_confirmed_ms = s_charge_candidate_since_ms = s_charge_last_poll_ms = 0;
|
||||||
|
memset(&s_status, 0, sizeof(s_status));
|
||||||
|
s_shutdown_confirmations = s_emergency_confirmations = 0;
|
||||||
|
fake_inputs = (platform_battery_charge_inputs_t){1, 1, true};
|
||||||
|
if(scenario == 1) fake_inputs.full_level = 0;
|
||||||
|
if(scenario == 2 || scenario == 3) fake_inputs.charge_level = 0;
|
||||||
|
adc_error = scenario == 5 ? ESP_ERR_INVALID_RESPONSE : ESP_OK;
|
||||||
|
battery_worker(NULL);
|
||||||
|
assert(worker_finishes == 1);
|
||||||
|
assert(full_prompts == (scenario == 2 ? 1U : 0U));
|
||||||
|
if(scenario == 4) assert(adc_calls == 6); // 0, 1, 3, 13, 23, 33 seconds
|
||||||
|
else if(scenario != 5) assert(adc_calls == 4);
|
||||||
|
if(scenario == 5) assert(shutdown_checks == 0 && adc_calls == 35);
|
||||||
|
else assert(shutdown_checks == adc_calls); // no stale-sample safety confirmations
|
||||||
|
}
|
||||||
|
puts("PASS: production worker ADC cadence, standby changes, startup full, charging/full prompt and unplug cancellation");
|
||||||
|
puts("PASS: TP4056 truth table, sampled stability, chatter, gaps, fault, GPIO/ADC failure and load independence");
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
types = '\n'.join(facade_type(path, name) for path, name in [
|
||||||
|
('components/platform/include/platform.h', 'platform_battery_charge_inputs_t'),
|
||||||
|
('components/platform/include/platform.h', 'platform_battery_sample_t'),
|
||||||
|
('components/domain/include/domain.h', 'domain_battery_charge_state_t'),
|
||||||
|
('components/domain/include/domain.h', 'domain_battery_level_state_t'),
|
||||||
|
('components/domain/include/domain.h', 'domain_battery_status_t'),
|
||||||
|
])
|
||||||
|
source = (ROOT / 'components/domain/src/battery/domain_battery_service.c').read_text()
|
||||||
|
source = source[source.index('#if CONFIG_TQ_BATTERY_ENABLE'):]
|
||||||
|
control = (ROOT / 'components/control_plane/src/runtime/control_plane_battery_service.c').read_text()
|
||||||
|
worker_source = '\n'.join(production_function(control, signature) for signature in [
|
||||||
|
'static const char *charge_state_name(', 'static const char *level_state_name(',
|
||||||
|
'static void battery_worker(void *arg)',
|
||||||
|
])
|
||||||
|
with tempfile.TemporaryDirectory(prefix='battery-charge-') as tmp:
|
||||||
|
path = Path(tmp)
|
||||||
|
cfile = path / 'test.c'
|
||||||
|
# Types need bool before declarations; stubs use the actual facade types.
|
||||||
|
cfile.write_text('#include <stdbool.h>\n#include <stdint.h>\n' + types + '\n' + STUBS + source + WORKER_STUBS + worker_source + TESTS)
|
||||||
|
subprocess.run(['clang', '-std=c11', '-Wall', '-Wextra', '-Werror',
|
||||||
|
'-fsanitize=address,undefined', '-fno-omit-frame-pointer',
|
||||||
|
str(cfile), '-o', str(path / 'test')], check=True)
|
||||||
|
subprocess.run([str(path / 'test')], check=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user