342 lines
14 KiB
C
342 lines
14 KiB
C
/*
|
|
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <math.h>
|
|
#include "esp_log.h"
|
|
#include "esp_check.h"
|
|
#include "esp_timer.h"
|
|
#include "adc_battery_estimation.h"
|
|
|
|
static const char* TAG = "adc_battery_estimation";
|
|
|
|
typedef struct {
|
|
adc_oneshot_unit_handle_t adc_handle;
|
|
adc_cali_handle_t adc_cali_handle;
|
|
adc_channel_t adc_channel;
|
|
adc_unit_t adc_unit;
|
|
bool is_adc_handle_owned;
|
|
const battery_point_t *battery_points;
|
|
size_t battery_points_count;
|
|
adc_battery_charging_detect_cb_t charging_detect_cb;
|
|
void *charging_detect_user_data;
|
|
float last_capacity; /*!< Last calculated battery capacity in percentage */
|
|
bool is_first_read; /*!< Flag for first capacity reading */
|
|
bool last_charging_state; /*!< Last charging state */
|
|
float voltage_divider_ratio; /*!< Voltage divider ratio */
|
|
float filter_alpha; /*!< Low-pass filter coefficient (0 < alpha < 1) */
|
|
#if CONFIG_BATTERY_STATE_SOFTWARE_ESTIMATION
|
|
uint64_t last_time_ms; /*!< Last time in milliseconds */
|
|
int battery_state_estimation_buffer[CONFIG_SOFTWARE_ESTIMATION_SAMPLE_COUNT]; /*!< Buffer to store ADC readings */
|
|
int battery_state_estimation_index; /*!< Current index in the buffer */
|
|
#endif
|
|
} adc_battery_estimation_ctx_t;
|
|
|
|
// Helper function to calculate battery capacity based on voltage
|
|
static float calculate_battery_capacity(float voltage, const battery_point_t *points, size_t points_count)
|
|
{
|
|
// Find the two points that bracket the current voltage
|
|
size_t i;
|
|
for (i = 0; i < points_count - 1; i++) {
|
|
if (voltage >= points[i + 1].voltage && voltage <= points[i].voltage) {
|
|
// Linear interpolation between the two points
|
|
float voltage_range = points[i].voltage - points[i + 1].voltage;
|
|
float capacity_range = points[i].capacity - points[i + 1].capacity;
|
|
float voltage_offset = voltage - points[i + 1].voltage;
|
|
|
|
return points[i + 1].capacity + (voltage_offset * capacity_range / voltage_range);
|
|
}
|
|
}
|
|
|
|
// If voltage is outside the range, clamp to the nearest point
|
|
if (voltage > points[0].voltage) {
|
|
return points[0].capacity;
|
|
} else {
|
|
return points[points_count - 1].capacity;
|
|
}
|
|
}
|
|
|
|
// Helper function to analyze battery trend
|
|
static bool analyze_battery_trend(const int *buffer, int buffer_size, bool last_charging_state)
|
|
{
|
|
int increasing_count = 0;
|
|
int decreasing_count = 0;
|
|
|
|
// Count increasing and decreasing points
|
|
for (int i = 1; i < buffer_size; i++) {
|
|
if (buffer[i] > buffer[i - 1]) {
|
|
increasing_count++;
|
|
} else if (buffer[i] < buffer[i - 1]) {
|
|
decreasing_count++;
|
|
}
|
|
}
|
|
|
|
// Log the analysis results
|
|
ESP_LOGD(TAG, "Trend analysis: increasing=%d, decreasing=%d", increasing_count, decreasing_count);
|
|
|
|
// If increasing and decreasing counts are equal, keep the last state
|
|
if (increasing_count == decreasing_count) {
|
|
return last_charging_state;
|
|
}
|
|
// Otherwise, determine by increasing/decreasing trend
|
|
return increasing_count > decreasing_count;
|
|
}
|
|
|
|
adc_battery_estimation_handle_t adc_battery_estimation_create(adc_battery_estimation_t *config)
|
|
{
|
|
ESP_RETURN_ON_FALSE(config, NULL, TAG, "Config is NULL");
|
|
|
|
adc_battery_estimation_ctx_t *ctx = (adc_battery_estimation_ctx_t *) calloc(1, sizeof(adc_battery_estimation_ctx_t));
|
|
ESP_RETURN_ON_FALSE(ctx, NULL, TAG, "Failed to allocate memory for context");
|
|
|
|
ctx->adc_channel = config->adc_channel;
|
|
ctx->charging_detect_cb = config->charging_detect_cb;
|
|
ctx->charging_detect_user_data = config->charging_detect_user_data;
|
|
ctx->is_first_read = true;
|
|
|
|
// Use default battery points if not provided
|
|
if (config->battery_points == NULL || config->battery_points_count == 0) {
|
|
ctx->battery_points = default_battery_points;
|
|
ctx->battery_points_count = DEFAULT_POINTS_COUNT;
|
|
} else {
|
|
ctx->battery_points = config->battery_points;
|
|
ctx->battery_points_count = config->battery_points_count;
|
|
}
|
|
|
|
// Use external ADC handle if provided
|
|
if (config->external.adc_handle != NULL && config->external.adc_cali_handle != NULL) {
|
|
ctx->adc_handle = config->external.adc_handle;
|
|
ctx->adc_cali_handle = config->external.adc_cali_handle;
|
|
ctx->is_adc_handle_owned = false;
|
|
} else {
|
|
// Create new ADC unit and channel
|
|
adc_oneshot_unit_init_cfg_t init_cfg = {
|
|
.unit_id = config->internal.adc_unit,
|
|
};
|
|
ESP_RETURN_ON_FALSE(adc_oneshot_new_unit(&init_cfg, &ctx->adc_handle) == ESP_OK, NULL, TAG, "Failed to create ADC unit");
|
|
|
|
adc_oneshot_chan_cfg_t chan_cfg = {
|
|
.atten = config->internal.adc_atten,
|
|
.bitwidth = config->internal.adc_bitwidth,
|
|
};
|
|
ESP_RETURN_ON_FALSE(adc_oneshot_config_channel(ctx->adc_handle, ctx->adc_channel, &chan_cfg) == ESP_OK, NULL, TAG, "Failed to configure ADC channel");
|
|
|
|
#if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED
|
|
adc_cali_curve_fitting_config_t cali_config = {
|
|
.unit_id = config->internal.adc_unit,
|
|
.chan = config->adc_channel,
|
|
.atten = config->internal.adc_atten,
|
|
.bitwidth = config->internal.adc_bitwidth,
|
|
};
|
|
ESP_RETURN_ON_FALSE(adc_cali_create_scheme_curve_fitting(&cali_config, &ctx->adc_cali_handle) == ESP_OK, NULL, TAG, "Failed to create ADC calibration scheme");
|
|
#elif ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED
|
|
adc_cali_line_fitting_config_t cali_config = {
|
|
.unit_id = config->internal.adc_unit,
|
|
.atten = config->internal.adc_atten,
|
|
.bitwidth = config->internal.adc_bitwidth,
|
|
};
|
|
ESP_RETURN_ON_FALSE(adc_cali_create_scheme_line_fitting(&cali_config, &ctx->adc_cali_handle) == ESP_OK, NULL, TAG, "Failed to create ADC calibration scheme");
|
|
#endif
|
|
ctx->is_adc_handle_owned = true;
|
|
}
|
|
|
|
// Validate voltage divider resistors
|
|
if (config->upper_resistor <= 0.0f || config->lower_resistor <= 0.0f) {
|
|
ESP_LOGE(TAG, "Invalid resistor values: upper_resistor=%.2f, lower_resistor=%.2f",
|
|
config->upper_resistor, config->lower_resistor);
|
|
return NULL;
|
|
}
|
|
|
|
float total_resistance = config->upper_resistor + config->lower_resistor;
|
|
if (total_resistance <= 0.0f) {
|
|
ESP_LOGE(TAG, "Total resistance is zero or negative: %.2f", total_resistance);
|
|
return NULL;
|
|
}
|
|
|
|
ctx->voltage_divider_ratio = config->lower_resistor / total_resistance;
|
|
ctx->filter_alpha = CONFIG_BATTERY_CAPACITY_LPF_COEFFICIENT / 10.0f;
|
|
|
|
#if CONFIG_BATTERY_STATE_SOFTWARE_ESTIMATION
|
|
ctx->battery_state_estimation_index = 0;
|
|
ctx->last_time_ms = 0;
|
|
#endif
|
|
|
|
return (adc_battery_estimation_handle_t) ctx;
|
|
}
|
|
|
|
esp_err_t adc_battery_estimation_destroy(adc_battery_estimation_handle_t handle)
|
|
{
|
|
esp_err_t ret = ESP_OK;
|
|
if (handle == NULL) {
|
|
return ESP_OK;
|
|
}
|
|
|
|
adc_battery_estimation_ctx_t *ctx = (adc_battery_estimation_ctx_t *) handle;
|
|
if (ctx->is_adc_handle_owned) {
|
|
printf("delete internal adc unit\n");
|
|
// Delete ADC unit and calibration scheme if owned
|
|
ret = adc_oneshot_del_unit(ctx->adc_handle);
|
|
if (ret != ESP_OK) {
|
|
ESP_LOGE(TAG, "Failed to delete ADC unit: %s", esp_err_to_name(ret));
|
|
return ret;
|
|
}
|
|
|
|
#if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED
|
|
ret = adc_cali_delete_scheme_curve_fitting(ctx->adc_cali_handle);
|
|
if (ret != ESP_OK) {
|
|
ESP_LOGE(TAG, "Failed to delete ADC calibration scheme: %s", esp_err_to_name(ret));
|
|
return ret;
|
|
}
|
|
#elif ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED
|
|
ret = adc_cali_delete_scheme_line_fitting(ctx->adc_cali_handle);
|
|
if (ret != ESP_OK) {
|
|
ESP_LOGE(TAG, "Failed to delete ADC calibration scheme: %s", esp_err_to_name(ret));
|
|
return ret;
|
|
}
|
|
#endif
|
|
}
|
|
free(ctx);
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t adc_battery_estimation_get_capacity(adc_battery_estimation_handle_t handle, float *capacity)
|
|
{
|
|
esp_err_t ret = ESP_OK;
|
|
ESP_RETURN_ON_FALSE(handle && capacity, ESP_ERR_INVALID_ARG, TAG, "Invalid arguments");
|
|
|
|
adc_battery_estimation_ctx_t *ctx = (adc_battery_estimation_ctx_t *) handle;
|
|
bool is_charging = false;
|
|
#if CONFIG_BATTERY_STATE_SOFTWARE_ESTIMATION
|
|
uint64_t current_time_ms = esp_timer_get_time() / 1000;
|
|
#endif
|
|
// Check charging state if callback is provided
|
|
if (ctx->charging_detect_cb) {
|
|
is_charging = ctx->charging_detect_cb(ctx->charging_detect_user_data);
|
|
}
|
|
#if CONFIG_BATTERY_STATE_SOFTWARE_ESTIMATION
|
|
else {
|
|
// Use last charging state if no callback is provided
|
|
is_charging = ctx->last_charging_state;
|
|
}
|
|
#endif
|
|
|
|
// Get ADC reading via filtering
|
|
int vol[CONFIG_ADC_FILTER_WINDOW_SIZE] = {0};
|
|
int avg = 0, std_vol = 0, filtered_vol = 0, filtered_result = 0, filtered_count = 0;
|
|
for (int i = 0; i < CONFIG_ADC_FILTER_WINDOW_SIZE; i++) {
|
|
int adc_raw = 0;
|
|
ret = adc_oneshot_read(ctx->adc_handle, ctx->adc_channel, &adc_raw);
|
|
if (ret != ESP_OK) {
|
|
ESP_LOGE(TAG, "Failed to read ADC: %s", esp_err_to_name(ret));
|
|
return ret;
|
|
}
|
|
|
|
ret = adc_cali_raw_to_voltage(ctx->adc_cali_handle, adc_raw, &vol[i]);
|
|
if (ret != ESP_OK) {
|
|
ESP_LOGE(TAG, "Failed to convert ADC raw to voltage: %s", esp_err_to_name(ret));
|
|
return ret;
|
|
}
|
|
avg += vol[i];
|
|
}
|
|
avg /= CONFIG_ADC_FILTER_WINDOW_SIZE;
|
|
filtered_result = avg;
|
|
|
|
for (int i = 0; i < CONFIG_ADC_FILTER_WINDOW_SIZE; i++) {
|
|
std_vol += (vol[i] - avg) * (vol[i] - avg);
|
|
}
|
|
std_vol = (int)sqrt(std_vol / (CONFIG_ADC_FILTER_WINDOW_SIZE));
|
|
|
|
for (int i = 0; i < CONFIG_ADC_FILTER_WINDOW_SIZE; i++) {
|
|
if (abs(vol[i] - avg) < std_vol) {
|
|
filtered_vol += vol[i];
|
|
filtered_count++;
|
|
}
|
|
}
|
|
|
|
if (filtered_count > 0) {
|
|
filtered_result = filtered_vol / filtered_count;
|
|
}
|
|
|
|
#if CONFIG_BATTERY_STATE_SOFTWARE_ESTIMATION
|
|
// Record filtered_result every CONFIG_SOFTWARE_ESTIMATION_SAMPLE_INTERVAL ms
|
|
if (current_time_ms - ctx->last_time_ms >= CONFIG_SOFTWARE_ESTIMATION_SAMPLE_INTERVAL) {
|
|
// Store the new value at current index
|
|
ctx->battery_state_estimation_buffer[ctx->battery_state_estimation_index] = filtered_result;
|
|
// Update index, wrap around when reaching the end
|
|
ctx->battery_state_estimation_index = (ctx->battery_state_estimation_index + 1) % CONFIG_SOFTWARE_ESTIMATION_SAMPLE_COUNT;
|
|
|
|
// If buffer is full (index is 0), analyze the trend
|
|
if (ctx->battery_state_estimation_index == 0) {
|
|
bool trend_is_charging = analyze_battery_trend(ctx->battery_state_estimation_buffer,
|
|
CONFIG_SOFTWARE_ESTIMATION_SAMPLE_COUNT,
|
|
ctx->last_charging_state);
|
|
ESP_LOGD(TAG, "Battery trend analysis: %s", trend_is_charging ? "Charging" : "Discharging");
|
|
|
|
// Update last charging state
|
|
ctx->last_charging_state = trend_is_charging;
|
|
// If no charging detection callback is provided, use trend analysis
|
|
if (!ctx->charging_detect_cb) {
|
|
is_charging = trend_is_charging;
|
|
}
|
|
}
|
|
ctx->last_time_ms = current_time_ms;
|
|
}
|
|
#endif
|
|
// Convert ADC voltage (mV) to battery voltage (V)
|
|
float battery_voltage = (float)filtered_result / 1000.0f / ctx->voltage_divider_ratio;
|
|
|
|
// Calculate battery capacity based on voltage
|
|
float current_capacity = calculate_battery_capacity(battery_voltage, ctx->battery_points, ctx->battery_points_count);
|
|
|
|
// Apply low-pass filter and handle capacity monotonicity
|
|
if (!ctx->is_first_read) {
|
|
// Apply low-pass filter
|
|
float filtered_capacity = ctx->filter_alpha * current_capacity + (1.0f - ctx->filter_alpha) * ctx->last_capacity;
|
|
|
|
if (is_charging) {
|
|
// In charging state, capacity should not decrease
|
|
if (filtered_capacity < ctx->last_capacity) {
|
|
ESP_LOGD(TAG, "Capacity decreased in charging state (%.1f%% -> %.1f%%), keeping previous value",
|
|
ctx->last_capacity, filtered_capacity);
|
|
filtered_capacity = ctx->last_capacity;
|
|
}
|
|
} else {
|
|
// In discharging state, capacity should not increase
|
|
if (filtered_capacity > ctx->last_capacity) {
|
|
ESP_LOGD(TAG, "Capacity increased in discharging state (%.1f%% -> %.1f%%), keeping previous value",
|
|
ctx->last_capacity, filtered_capacity);
|
|
filtered_capacity = ctx->last_capacity;
|
|
}
|
|
}
|
|
current_capacity = filtered_capacity;
|
|
} else {
|
|
// First reading, just store it
|
|
ctx->is_first_read = false;
|
|
}
|
|
|
|
// Update last capacity and charging state
|
|
ctx->last_capacity = current_capacity;
|
|
ctx->last_charging_state = is_charging;
|
|
*capacity = current_capacity;
|
|
|
|
ESP_LOGD(TAG, "Battery capacity: %.1f%%", *capacity);
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t adc_battery_estimation_get_charging_state(adc_battery_estimation_handle_t handle, bool *is_charging)
|
|
{
|
|
ESP_RETURN_ON_FALSE(handle && is_charging, ESP_ERR_INVALID_ARG, TAG, "Invalid arguments");
|
|
|
|
adc_battery_estimation_ctx_t *ctx = (adc_battery_estimation_ctx_t *) handle;
|
|
if (ctx->charging_detect_cb) {
|
|
*is_charging = ctx->charging_detect_cb(ctx->charging_detect_user_data);
|
|
} else {
|
|
*is_charging = ctx->last_charging_state;
|
|
}
|
|
|
|
return ESP_OK;
|
|
}
|