fix(battery): detect TP4056 charging and full states on new board
This commit is contained in:
@@ -12,13 +12,18 @@ SOURCE = ROOT / "components/platform/src/platform_battery_driver.c"
|
||||
|
||||
def initializer():
|
||||
source = SOURCE.read_text()
|
||||
start = source.index("esp_err_t platform_battery_init(void)")
|
||||
body = source.index("{", start)
|
||||
end, depth = body + 1, 1
|
||||
while depth:
|
||||
depth += (source[end] == "{") - (source[end] == "}")
|
||||
end += 1
|
||||
return source[start:end]
|
||||
parts = []
|
||||
for signature in ["static esp_err_t configure_charge_inputs_locked(void)",
|
||||
"esp_err_t platform_battery_read_charge_inputs(",
|
||||
"esp_err_t platform_battery_init(void)"]:
|
||||
start = source.index(signature)
|
||||
body = source.index("{", start)
|
||||
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'''
|
||||
@@ -26,6 +31,7 @@ STUBS = r'''
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
typedef int esp_err_t;
|
||||
typedef int adc_unit_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;
|
||||
static adc_oneshot_unit_handle_t s_adc_handle;
|
||||
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 gpio_calls, unit_calls, delete_calls, calibration_calls;
|
||||
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'''
|
||||
static void reset(void) {
|
||||
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;
|
||||
gpio_calls = unit_calls = delete_calls = calibration_calls = 0;
|
||||
}
|
||||
@@ -113,7 +122,7 @@ int main(void) {
|
||||
assert(s_initialized && !locked);
|
||||
assert(actual_unit == EXPECTED_UNIT && actual_channel == EXPECTED_CHANNEL);
|
||||
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);
|
||||
reset(); fail_lock = 1;
|
||||
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;
|
||||
assert(platform_battery_init() == calibration_error && !locked && !s_initialized);
|
||||
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;
|
||||
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");
|
||||
}
|
||||
'''
|
||||
@@ -154,6 +170,7 @@ def main():
|
||||
"-fsanitize=address,undefined", "-fno-omit-frame-pointer",
|
||||
f"-DCONFIG_TQ_BATTERY_ADC_GPIO={gpio}",
|
||||
f"-DCONFIG_TQ_BATTERY_CHARGE_GPIO={charge_gpio}",
|
||||
"-DCONFIG_TQ_BATTERY_FULL_GPIO=8",
|
||||
f"-DEXPECTED_UNIT={unit}", f"-DEXPECTED_CHANNEL={channel}",
|
||||
str(source), "-o", str(binary),
|
||||
], 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