add some code

This commit is contained in:
2025-09-05 13:25:11 +08:00
parent 9ff0a99e7a
commit 3cf1229a85
8911 changed files with 2535396 additions and 0 deletions

View File

@@ -0,0 +1 @@
a63de81f60adddd5204ed8f286754f1186f16a2fc0f2119152a9d36771f9526a

View File

@@ -0,0 +1,11 @@
# ChangeLog
## v0.2.0 - 2025-6-18
* Add software estimation for charging state
## v0.1.0 - 2025-5-16
### Enhancements:
* Initial version.

View File

@@ -0,0 +1 @@
{"version": "1.0", "algorithm": "sha256", "created_at": "2025-06-24T07:40:07.783598+00:00", "files": [{"path": "CHANGELOG.md", "size": 143, "hash": "c8b1038487312d410d1f9363329d5cbe18823b538c48728dde6c9352f7986486"}, {"path": "CMakeLists.txt", "size": 229, "hash": "90b188d14748bfeca1cc06a4e159e562a5ffc85717a80c1215c5c0063aa9bb60"}, {"path": "Kconfig", "size": 2974, "hash": "40c4ce2f50aec846ec7b504b8bd1bd734f1798e8994fcd35148d946650487b74"}, {"path": "README.md", "size": 3032, "hash": "c0eacbeeec963c3877860275921cbf8b328d5701873fe505e50af0013cc35812"}, {"path": "adc_battery_estimation.c", "size": 14241, "hash": "c09da57c7c724c380c0d39d119c388dad167f3e23b096807ed0bc99d9e62ec46"}, {"path": "idf_component.yml", "size": 507, "hash": "27afdbe4cd867007a393f58183f36d15716d98e65dc90acce4277843be3fcff8"}, {"path": "license.txt", "size": 11358, "hash": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"}, {"path": "include/adc_battery_estimation.h", "size": 4815, "hash": "e065748c2e9855fed413b3c6066f8cdc5e46a01f9637c910431bc15329448066"}, {"path": "test_apps/CMakeLists.txt", "size": 325, "hash": "65c63239ad8fff50ed27b99f880f2dc644bb32d69083e5323f71fc8f265ee262"}, {"path": "test_apps/sdkconfig.defaults", "size": 87, "hash": "4f240cc3912dcec12cf9d272853b0db2dcf9d5c0b24079440a0a264c68146210"}, {"path": "test_apps/main/CMakeLists.txt", "size": 139, "hash": "e9bb083d59b8c0bd8f33f707da6885cbd22c29d5ab72d4f2164d11f5b4a98fcc"}, {"path": "test_apps/main/adc_battery_estimation_test.c", "size": 6209, "hash": "195fa85998658f06b50aeaa45115a249388d4af64cd03d88081727e5a758209e"}, {"path": "test_apps/main/idf_component.yml", "size": 125, "hash": "3bb27c70bb2a5117938050d4ac83a740bfcb11359b8229b1acd775ccdce41def"}]}

View File

@@ -0,0 +1,6 @@
idf_component_register(SRCS "adc_battery_estimation.c"
INCLUDE_DIRS include
REQUIRES "esp_adc" "esp_timer")
include(package_manager)
cu_pkg_define_version(${CMAKE_CURRENT_LIST_DIR})

View File

@@ -0,0 +1,72 @@
menu "ADC Battery Estimation"
choice OCV_SOC_MODEL
bool "Select default OCV and battery capacity model"
default OCV_SOC_MODEL_1
help
Select Model
config OCV_SOC_MODEL_1
bool "OCV_SOC Model 1"
help
This model is based on TI's document: Battery Fuel Gauging Algorithm Comparison
You can find the document here: https://www.ti.com/lit/SLUAAR3
config OCV_SOC_MODEL_2
bool "OCV_OSC Model 2"
help
This model is based on Analog Devices' document:
"Characterizing a Lithium-Ion (Li+) Cell for Use with an Open-Circuit-Voltage (OCV) Based Fuel Gauge"
You can find the document at:
https://www.analog.com/en/resources/design-notes/characterizing-a-lithiumion-li-cell-for-use-with-an-opencircuitvoltage-ocv-based-fuel-gauge.html
endchoice
config ADC_FILTER_WINDOW_SIZE
int "ADC Filter Window size"
range 5 15
default 10
help
The number of ADC measurements to take before filtering the battery voltage, the
higher the value the more stable the battery voltage will be, but will take longer
to update
config BATTERY_CAPACITY_LPF_COEFFICIENT
int "Battery Capacity LPF Coefficient (/10)"
range 1 10
default 2
help
First-order low-pass filter coefficient for battery capacity calculation.
The value is divided by 10 to get the actual coefficient (e.g., 2 means 0.2).
Lower values provide more smoothing but slower response to changes.
Higher values provide less smoothing but faster response to changes.
config BATTERY_STATE_SOFTWARE_ESTIMATION
bool "Battery State Software Estimation"
default y
help
Enable software-based estimation of the battery state to determine
whether the battery is charging or discharging.
menu "Software Estimation Configuration"
depends on BATTERY_STATE_SOFTWARE_ESTIMATION
config SOFTWARE_ESTIMATION_SAMPLE_COUNT
int "Software Estimation Sample Count"
range 10 15
default 10
help
The number of samples to collect for software-based battery state estimation.
A higher number of samples can provide more accurate estimation but may increase
the time required for estimation.
config SOFTWARE_ESTIMATION_SAMPLE_INTERVAL
int "Software Estimation Sample Interval (ms)"
range 10000 100000
default 20000
help
The interval in milliseconds between each sample for software-based battery state estimation.
A shorter interval allows for quicker detection of changes in battery state but may consume
more power.
endmenu
endmenu

View File

@@ -0,0 +1,65 @@
## Battery capacity estimation based on ADC
`adc_battery_estimation` is a lithium battery capacity estimation component based on ADC, which converts battery voltage data collected by ADC into corresponding battery capacity according to the OCV-SOC model, and ensures the consistency of battery capacity data in both discharge and charge states. This component has the following features:
1. Provides basic battery level information while ensuring consistency in the estimated capacity
2. Supports both user-provided external ADC Handle or automatic creation by the component internally
3. Supports filtering of collected ADC data and estimated battery capacity
4. Provides a software-based charging state estimation method. If the user cannot provide a charging indicator pin and `BATTERY_STATE_SOFTWARE_ESTIMATION` is enabled in Kconfig, software charging state estimation will be activated
This component provides two OCV-SOC models, from [Ti](https://www.ti.com/lit/SLUAAR3) and [Analog Device](https://www.analog.com/en/resources/design-notes/characterizing-a-lithiumion-li-cell-for-use-with-an-opencircuitvoltage-ocv-based-fuel-gauge.html) respectively. Additionally, it supports user-defined custom OCV-SOC models.
![OCV-SOC](https://dl.espressif.com/AE/esp-iot-solution/OCV_SOC.png)
## Add component to your project
Please use the component manager command `add-dependency` to add the `adc_battery_estimation` to your project's dependency, during the `CMake` step the component will be downloaded automatically
```
idf.py add-dependency "espressif/adc_battery_estimation=*"
```
Alternatively, you can create `idf_component.yml`. More is in [Espressif's documentation](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/tools/idf-component-manager.html).
## Example use
```c
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "adc_battery_estimation.h"
#define TEST_ADC_UNIT (ADC_UNIT_1)
#define TEST_ADC_BITWIDTH (ADC_BITWIDTH_DEFAULT)
#define TEST_ADC_ATTEN (ADC_ATTEN_DB_12)
#define TEST_ADC_CHANNEL (ADC_CHANNEL_1)
#define TEST_RESISTOR_UPPER (460)
#define TEST_RESISTOR_LOWER (460)
#define TEST_ESTIMATION_TIME (50)
void app_main(void)
{
adc_battery_estimation_t config = {
.internal = {
.adc_unit = TEST_ADC_UNIT,
.adc_bitwidth = TEST_ADC_BITWIDTH,
.adc_atten = TEST_ADC_ATTEN,
},
.adc_channel = TEST_ADC_CHANNEL,
.lower_resistor = TEST_RESISTOR_LOWER,
.upper_resistor = TEST_RESISTOR_UPPER,
};
adc_battery_estimation_handle_t adc_battery_estimation_handle = adc_battery_estimation_create(&config);
for (int i = 0; i < TEST_ESTIMATION_TIME; i++) {
float capacity = 0;
adc_battery_estimation_get_capacity(adc_battery_estimation_handle, &capacity);
printf("Battery capacity: %.1f%%\n", capacity);
vTaskDelay(pdMS_TO_TICKS(500));
}
adc_battery_estimation_destroy(adc_battery_estimation_handle);
}
```

View File

@@ -0,0 +1,341 @@
/*
* 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;
}

View File

@@ -0,0 +1,11 @@
dependencies:
cmake_utilities: '*'
idf: '>=5.0'
description: Battery capacity estimation based on ADC
issues: https://github.com/espressif/esp-iot-solution/issues
repository: git://github.com/espressif/esp-iot-solution.git
repository_info:
commit_sha: e74bab6049d8506ce3194a7b440f9d72d83e4ad0
path: components/sensors/battery_fuel_gauge/adc_battery_estimation
url: https://github.com/espressif/esp-iot-solution/tree/master/components/sensors/battery_fuel_gauge/adc_battery_estimation
version: 0.2.0

View File

@@ -0,0 +1,132 @@
/*
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include "esp_adc/adc_oneshot.h"
#include "esp_adc/adc_cali.h"
#include "esp_adc/adc_cali_scheme.h"
typedef struct {
float voltage; /*!< Battery voltage in volts */
int capacity; /*!< Battery capacity in percentage (0-100) */
} battery_point_t;
// Charging state detection callback function type
typedef bool (*adc_battery_charging_detect_cb_t)(void *user_data);
typedef struct {
union {
struct {
adc_oneshot_unit_handle_t adc_handle; /*!< External ADC handle */
adc_cali_handle_t adc_cali_handle; /*!< External ADC calibration handle */
} external; /*!< Use external handles */
struct {
adc_unit_t adc_unit; /*!< ADC unit number */
adc_bitwidth_t adc_bitwidth; /*!< ADC bit width */
adc_atten_t adc_atten; /*!< ADC attenuation */
} internal; /*!< Create new ADC configuration */
}; /*!< Use external or internal handles */
adc_channel_t adc_channel; /*!< ADC channel number */
// Resistor configuration
float upper_resistor; /*!< Upper resistor value in ohms */
float lower_resistor; /*!< Lower resistor value in ohms */
// Battery voltage-capacity mapping configuration
const battery_point_t *battery_points; /*!< Array of voltage-capacity mapping points, NULL for default */
size_t battery_points_count; /*!< Number of points in the array, 0 for default */
// Charging state detection configuration
adc_battery_charging_detect_cb_t charging_detect_cb; /*!< Callback function to detect charging state */
void *charging_detect_user_data; /*!< User data passed to the callback function */
} adc_battery_estimation_t;
typedef void *adc_battery_estimation_handle_t;
// Default battery voltage-capacity mapping points
#if CONFIG_OCV_SOC_MODEL_1
#define DEFAULT_POINTS_COUNT 11
static const battery_point_t default_battery_points[DEFAULT_POINTS_COUNT] = {
{4.16, 100},
{4.07, 90},
{3.99, 80},
{3.90, 70},
{3.82, 60},
{3.72, 50},
{3.61, 40},
{3.53, 30},
{3.38, 20},
{3.20, 10},
{2.85, 0},
};
#elif CONFIG_OCV_SOC_MODEL_2
#define DEFAULT_POINTS_COUNT 21
static const battery_point_t default_battery_points[DEFAULT_POINTS_COUNT] = {
{4.177454, 100},
{4.129486, 95},
{4.085934, 90},
{4.045427, 85},
{4.008118, 80},
{3.974769, 75},
{3.945074, 70},
{3.917968, 65},
{3.884009, 60},
{3.841219, 55},
{3.820965, 50},
{3.805737, 45},
{3.79325, 40},
{3.783504, 35},
{3.775129, 30},
{3.762185, 25},
{3.741018, 20},
{3.7098, 15},
{3.686654, 10},
{3.674776, 5},
{3.305545, 0},
};
#endif
/**
* @brief Create a new ADC battery estimation handle
*
* @param config Pointer to the ADC battery estimation configuration
* @return adc_battery_estimation_handle_t Return the ADC battery estimation handle if created successfully, NULL if failed
*/
adc_battery_estimation_handle_t adc_battery_estimation_create(adc_battery_estimation_t *config);
/**
* @brief Destroy the ADC battery estimation handle
*
* @param handle Pointer to the ADC battery estimation handle
* @return esp_err_t Return ESP_OK if destroyed successfully, ESP_ERR_INVALID_ARG if invalid argument, ESP_FAIL if failed
*/
esp_err_t adc_battery_estimation_destroy(adc_battery_estimation_handle_t handle);
/**
* @brief Get the battery capacity in percentage
*
* @param handle Pointer to the ADC battery estimation handle
* @param capacity Pointer to the battery capacity in percentage
* @return esp_err_t Return ESP_OK if get capacity successfully, ESP_ERR_INVALID_ARG if invalid argument, ESP_FAIL if failed
*/
esp_err_t adc_battery_estimation_get_capacity(adc_battery_estimation_handle_t handle, float *capacity);
/**
* @brief Get the battery charging state
*
* @param handle Pointer to the ADC battery estimation handle
* @param is_charging Pointer to the battery charging state
* @return esp_err_t Return ESP_OK if get charging state successfully, ESP_ERR_INVALID_ARG if invalid argument, ESP_FAIL if failed
*/
esp_err_t adc_battery_estimation_get_charging_state(adc_battery_estimation_handle_t handle, bool *is_charging);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,7 @@
# The following lines of boilerplate have to be in your project's CMakeLists
# in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.5)
set(EXTRA_COMPONENT_DIRS "$ENV{IDF_PATH}/tools/unit-test-app/components")
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(adc_battery_estimation_test)

View File

@@ -0,0 +1,7 @@
idf_component_register(
SRC_DIRS "."
INCLUDE_DIRS "."
)
include(package_manager)
cu_pkg_define_version(${CMAKE_CURRENT_LIST_DIR})

View File

@@ -0,0 +1,176 @@
/*
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include <string.h>
#include "unity.h"
#include "unity_config.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "adc_battery_estimation.h"
#include "driver/gpio.h"
#define TEST_MEMORY_LEAK_THRESHOLD (-460)
#define TEST_ADC_UNIT (ADC_UNIT_1)
#define TEST_ADC_BITWIDTH (ADC_BITWIDTH_DEFAULT)
#define TEST_ADC_ATTEN (ADC_ATTEN_DB_12)
#define TEST_ADC_CHANNEL (ADC_CHANNEL_1)
#define TEST_CHARGE_GPIO_NUM (GPIO_NUM_0)
#define TEST_RESISTOR_UPPER (460)
#define TEST_RESISTOR_LOWER (460)
#define TEST_ESTIMATION_TIME (100)
static size_t before_free_8bit;
static size_t before_free_32bit;
bool battery_charging_detect(void *user_data)
{
if (gpio_get_level(TEST_CHARGE_GPIO_NUM) == 0) {
return true;
}
return false;
}
TEST_CASE("adc battery estimation test", "[internal adc]")
{
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << TEST_CHARGE_GPIO_NUM),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
gpio_config(&io_conf);
adc_battery_estimation_t config = {
.internal = {
.adc_unit = TEST_ADC_UNIT,
.adc_bitwidth = TEST_ADC_BITWIDTH,
.adc_atten = TEST_ADC_ATTEN,
},
.adc_channel = TEST_ADC_CHANNEL,
.lower_resistor = TEST_RESISTOR_LOWER,
.upper_resistor = TEST_RESISTOR_UPPER,
.charging_detect_cb = battery_charging_detect,
.charging_detect_user_data = NULL,
};
adc_battery_estimation_handle_t adc_battery_estimation_handle = adc_battery_estimation_create(&config);
TEST_ASSERT(adc_battery_estimation_handle != NULL);
for (int i = 0; i < TEST_ESTIMATION_TIME; i++) {
float capacity = 0;
adc_battery_estimation_get_capacity(adc_battery_estimation_handle, &capacity);
printf("Battery capacity: %.1f%%\n", capacity);
vTaskDelay(pdMS_TO_TICKS(500));
}
TEST_ESP_OK(adc_battery_estimation_destroy(adc_battery_estimation_handle));
}
TEST_CASE("adc battery estimation test", "[external adc]")
{
adc_oneshot_unit_handle_t adc_handle;
adc_cali_handle_t adc_cali_handle;
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << TEST_CHARGE_GPIO_NUM),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
gpio_config(&io_conf);
adc_oneshot_unit_init_cfg_t init_cfg = {
.unit_id = TEST_ADC_UNIT,
};
TEST_ESP_OK(adc_oneshot_new_unit(&init_cfg, &adc_handle));
adc_oneshot_chan_cfg_t chan_cfg = {
.atten = TEST_ADC_ATTEN,
.bitwidth = TEST_ADC_BITWIDTH,
};
TEST_ESP_OK(adc_oneshot_config_channel(adc_handle, TEST_ADC_CHANNEL, &chan_cfg));
#if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED
adc_cali_curve_fitting_config_t cali_config = {
.unit_id = TEST_ADC_UNIT,
.chan = TEST_ADC_CHANNEL,
.atten = TEST_ADC_ATTEN,
.bitwidth = TEST_ADC_BITWIDTH,
};
TEST_ESP_OK(adc_cali_create_scheme_curve_fitting(&cali_config, &adc_cali_handle));
#elif ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED
adc_cali_line_fitting_config_t cali_config = {
.unit_id = TEST_ADC_UNIT,
.atten = TEST_ADC_ATTEN,
.bitwidth = TEST_ADC_BITWIDTH,
};
TEST_ESP_OK(adc_cali_create_scheme_line_fitting(&cali_config, &adc_cali_handle));
#endif
adc_battery_estimation_t config = {
.external = {
.adc_handle = adc_handle,
.adc_cali_handle = adc_cali_handle,
},
.adc_channel = TEST_ADC_CHANNEL,
.lower_resistor = TEST_RESISTOR_LOWER,
.upper_resistor = TEST_RESISTOR_UPPER,
.charging_detect_cb = battery_charging_detect,
.charging_detect_user_data = NULL,
};
adc_battery_estimation_handle_t adc_battery_estimation_handle = adc_battery_estimation_create(&config);
TEST_ASSERT(adc_battery_estimation_handle != NULL);
for (int i = 0; i < TEST_ESTIMATION_TIME; i++) {
float capacity = 0;
adc_battery_estimation_get_capacity(adc_battery_estimation_handle, &capacity);
printf("Battery capacity: %.1f%%\n", capacity);
vTaskDelay(pdMS_TO_TICKS(500));
}
TEST_ESP_OK(adc_battery_estimation_destroy(adc_battery_estimation_handle));
TEST_ESP_OK(adc_oneshot_del_unit(adc_handle));
#if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED
TEST_ESP_OK(adc_cali_delete_scheme_curve_fitting(adc_cali_handle));
#elif ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED
TEST_ESP_OK(adc_cali_delete_scheme_line_fitting(adc_cali_handle));
#endif
}
static void check_leak(size_t before_free, size_t after_free, const char *type)
{
ssize_t delta = after_free - before_free;
printf("MALLOC_CAP_%s: Before %u bytes free, After %u bytes free (delta %d)\n", type, before_free, after_free, delta);
TEST_ASSERT_MESSAGE(delta >= TEST_MEMORY_LEAK_THRESHOLD, "memory leak");
}
void setUp(void)
{
before_free_8bit = heap_caps_get_free_size(MALLOC_CAP_8BIT);
before_free_32bit = heap_caps_get_free_size(MALLOC_CAP_32BIT);
}
void tearDown(void)
{
size_t after_free_8bit = heap_caps_get_free_size(MALLOC_CAP_8BIT);
size_t after_free_32bit = heap_caps_get_free_size(MALLOC_CAP_32BIT);
check_leak(before_free_8bit, after_free_8bit, "8BIT");
check_leak(before_free_32bit, after_free_32bit, "32BIT");
}
void app_main(void)
{
printf(" _ ____ ____ ____ _ _____ _ _ _ _ \n");
printf(" / \\ | _ \\ / ___| | __ ) __ _| |_ | ____|___| |_(_)_ __ ___ __ _| |_(_) ___ _ __ \n");
printf(" / _ \\ | | | | | | _ \\ / _` | __| | _| / __| __| | '_ ` _ \\ / _` | __| |/ _ \\| '_ \\ \n");
printf(" / ___ \\| |_| | |___ | |_) | (_| | |_ | |___\\__ \\ |_| | | | | | | (_| | |_| | (_) | | | |\n");
printf(" /_/ \\_\\____/ \\____| |____/ \\__,_|\\__| |_____|___/\\__|_|_| |_| |_|\\__,_|\\__|_|\\___/|_| |_|\n");
unity_run_menu();
}

View File

@@ -0,0 +1,5 @@
dependencies:
idf: ">=5.0"
adc_battery_estimation:
version: "*"
override_path: "../../../adc_battery_estimation"

View File

@@ -0,0 +1,3 @@
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y
CONFIG_FREERTOS_HZ=1000
CONFIG_ESP_TASK_WDT_EN=n