354 lines
10 KiB
C
354 lines
10 KiB
C
/*
|
|
* 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
|