ESP32/main/battery_monitor.c
2026-06-24 09:38:19 +08:00

237 lines
6.2 KiB
C

#include "battery_monitor.h"
#include <stdbool.h>
#include "esp_adc/adc_cali.h"
#include "esp_adc/adc_cali_scheme.h"
#include "esp_adc/adc_oneshot.h"
#include "esp_check.h"
#include "esp_err.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/portmacro.h"
#include "freertos/task.h"
static const char *TAG = "battery";
#define BATTERY_ADC_UNIT ADC_UNIT_1
#define BATTERY_ADC_CHANNEL ADC_CHANNEL_3
#define BATTERY_ADC_ATTEN ADC_ATTEN_DB_12
#define BATTERY_DIVIDER 3
static adc_oneshot_unit_handle_t s_adc_handle;
static adc_cali_handle_t s_cali_handle;
static bool s_cali_enabled;
static portMUX_TYPE s_battery_mux = portMUX_INITIALIZER_UNLOCKED;
static int s_latest_percent;
static int s_latest_millivolts;
static bool s_has_latest;
typedef struct {
int millivolts;
int raw_avg;
int pin_millivolts;
int samples;
} battery_reading_t;
typedef struct {
int millivolts;
int percent;
} battery_curve_point_t;
static const battery_curve_point_t s_battery_curve[] = {
{ 4200, 100 },
{ 4160, 95 },
{ 4110, 90 },
{ 4080, 85 },
{ 4020, 80 },
{ 3980, 75 },
{ 3950, 70 },
{ 3910, 65 },
{ 3870, 60 },
{ 3850, 55 },
{ 3830, 50 },
{ 3810, 45 },
{ 3790, 40 },
{ 3770, 35 },
{ 3750, 30 },
{ 3730, 25 },
{ 3710, 20 },
{ 3690, 15 },
{ 3650, 10 },
{ 3570, 5 },
{ 3300, 0 },
};
static int clamp_percent(int value)
{
if (value < 0) {
return 0;
}
if (value > 100) {
return 100;
}
return value;
}
static int battery_percent_from_mv(int mv)
{
if (mv <= 0) {
return 0;
}
const int count = sizeof(s_battery_curve) / sizeof(s_battery_curve[0]);
if (mv >= s_battery_curve[0].millivolts) {
return s_battery_curve[0].percent;
}
for (int i = 1; i < count; i++) {
const battery_curve_point_t high = s_battery_curve[i - 1];
const battery_curve_point_t low = s_battery_curve[i];
if (mv >= low.millivolts) {
int mv_range = high.millivolts - low.millivolts;
int percent_range = high.percent - low.percent;
int percent = low.percent;
if (mv_range > 0) {
percent += ((mv - low.millivolts) * percent_range + mv_range / 2) / mv_range;
}
return clamp_percent(percent);
}
}
return s_battery_curve[count - 1].percent;
}
static esp_err_t battery_adc_init(void)
{
adc_oneshot_unit_init_cfg_t unit_config = {
.unit_id = BATTERY_ADC_UNIT,
.clk_src = ADC_RTC_CLK_SRC_DEFAULT,
.ulp_mode = ADC_ULP_MODE_DISABLE,
};
ESP_RETURN_ON_ERROR(adc_oneshot_new_unit(&unit_config, &s_adc_handle), TAG, "adc unit init failed");
adc_oneshot_chan_cfg_t channel_config = {
.atten = BATTERY_ADC_ATTEN,
.bitwidth = ADC_BITWIDTH_DEFAULT,
};
ESP_RETURN_ON_ERROR(adc_oneshot_config_channel(s_adc_handle, BATTERY_ADC_CHANNEL, &channel_config),
TAG, "adc channel config failed");
#if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED
adc_cali_curve_fitting_config_t cali_config = {
.unit_id = BATTERY_ADC_UNIT,
.chan = BATTERY_ADC_CHANNEL,
.atten = BATTERY_ADC_ATTEN,
.bitwidth = ADC_BITWIDTH_DEFAULT,
};
esp_err_t cali_ret = adc_cali_create_scheme_curve_fitting(&cali_config, &s_cali_handle);
s_cali_enabled = (cali_ret == ESP_OK);
if (!s_cali_enabled) {
ESP_LOGW(TAG, "ADC calibration unavailable: %s", esp_err_to_name(cali_ret));
}
#endif
return ESP_OK;
}
static battery_reading_t read_battery(void)
{
int raw_sum = 0;
int mv_sum = 0;
int samples = 0;
battery_reading_t reading = { 0 };
for (int i = 0; i < 8; i++) {
int raw = 0;
if (adc_oneshot_read(s_adc_handle, BATTERY_ADC_CHANNEL, &raw) != ESP_OK) {
continue;
}
raw_sum += raw;
samples++;
if (s_cali_enabled) {
int pin_mv = 0;
if (adc_cali_raw_to_voltage(s_cali_handle, raw, &pin_mv) == ESP_OK) {
mv_sum += pin_mv;
}
}
}
if (samples == 0) {
return reading;
}
reading.samples = samples;
reading.raw_avg = raw_sum / samples;
if (s_cali_enabled && mv_sum > 0) {
reading.pin_millivolts = mv_sum / samples;
reading.millivolts = reading.pin_millivolts * BATTERY_DIVIDER;
return reading;
}
reading.pin_millivolts = (reading.raw_avg * 3100) / 4095;
reading.millivolts = reading.pin_millivolts * BATTERY_DIVIDER;
return reading;
}
static void battery_task(void *arg)
{
(void)arg;
int log_ticks = 0;
while (1) {
battery_reading_t reading = read_battery();
int percent = battery_percent_from_mv(reading.millivolts);
portENTER_CRITICAL(&s_battery_mux);
s_latest_percent = percent;
s_latest_millivolts = reading.millivolts;
s_has_latest = true;
portEXIT_CRITICAL(&s_battery_mux);
if (log_ticks < 6 || (log_ticks % 6) == 0) {
ESP_LOGI(TAG, "adc raw=%d pin_mv=%d battery_mv=%d percent=%d cali=%d samples=%d",
reading.raw_avg, reading.pin_millivolts, reading.millivolts,
percent, s_cali_enabled ? 1 : 0, reading.samples);
}
log_ticks++;
vTaskDelay(pdMS_TO_TICKS(10000));
}
}
void battery_monitor_start(void)
{
esp_err_t ret = battery_adc_init();
if (ret != ESP_OK) {
ESP_LOGW(TAG, "battery monitor disabled: %s", esp_err_to_name(ret));
portENTER_CRITICAL(&s_battery_mux);
s_latest_percent = 0;
s_latest_millivolts = 0;
s_has_latest = true;
portEXIT_CRITICAL(&s_battery_mux);
return;
}
xTaskCreate(battery_task, "battery", 4096, NULL, 2, NULL);
}
bool battery_monitor_take_latest(int *percent, int *millivolts)
{
if (percent == NULL || millivolts == NULL) {
return false;
}
bool has_latest = false;
portENTER_CRITICAL(&s_battery_mux);
if (s_has_latest) {
*percent = s_latest_percent;
*millivolts = s_latest_millivolts;
s_has_latest = false;
has_latest = true;
}
portEXIT_CRITICAL(&s_battery_mux);
return has_latest;
}