140 lines
4.4 KiB
C
140 lines
4.4 KiB
C
#include "battery_monitor.h"
|
|
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
|
|
#include "esp_adc/adc_oneshot.h"
|
|
#include "esp_err.h"
|
|
#include "esp_log.h"
|
|
|
|
static const char *TAG = "BATTERY";
|
|
|
|
// Battery monitor wiring:
|
|
// Battery + -> 360k -> GPIO35 ADC point -> 47k -> GND
|
|
// Divider factor = (360k + 47k) / 47k = 8.659574
|
|
static const adc_unit_t BATTERY_ADC_UNIT = ADC_UNIT_1;
|
|
static const adc_channel_t BATTERY_ADC_CHANNEL = ADC_CHANNEL_7; // GPIO35 on ESP32
|
|
static const adc_atten_t BATTERY_ADC_ATTEN = ADC_ATTEN_DB_12;
|
|
static const adc_bitwidth_t BATTERY_ADC_WIDTH = ADC_BITWIDTH_12;
|
|
static const float BATTERY_R_TOP_OHM = 360000.0f;
|
|
static const float BATTERY_R_BOTTOM_OHM = 47000.0f;
|
|
static const float BATTERY_DIVIDER_FACTOR = (BATTERY_R_TOP_OHM + BATTERY_R_BOTTOM_OHM) / BATTERY_R_BOTTOM_OHM;
|
|
// Calibration based on real measurement:
|
|
// Pack measured by multimeter = 21.43 V
|
|
// App/firmware before calibration = 20.20 V
|
|
// Calibration multiplier = 21.43 / 20.20 = 1.0619
|
|
static const float BATTERY_CALIBRATION_MULTIPLIER = 1.062f;
|
|
static const float ADC_REFERENCE_VOLTAGE = 3.30f;
|
|
static const float ADC_MAX_RAW = 4095.0f;
|
|
// Project decision for battery percentage display:
|
|
// 6S pack full = 25.2 V (4.2 V/cell).
|
|
// Display 0% = 20.4 V (3.4 V/cell), so 21.6 V is still shown
|
|
// as a low but usable battery level instead of immediately becoming 0%.
|
|
// Low-battery auto-stop/lockout is intentionally NOT enabled in this patch.
|
|
static const float BATTERY_EMPTY_VOLTAGE = 20.40f;
|
|
static const float BATTERY_FULL_VOLTAGE = 25.20f;
|
|
static const int BATTERY_SAMPLE_COUNT = 16;
|
|
|
|
static adc_oneshot_unit_handle_t s_adc_handle = NULL;
|
|
static bool s_initialized = false;
|
|
static float s_voltage = 0.0f;
|
|
static int s_percent = 0;
|
|
static bool s_percent_initialized = false;
|
|
|
|
static int clamp_percent(float value)
|
|
{
|
|
if (value <= 0.0f) {
|
|
return 0;
|
|
}
|
|
if (value >= 100.0f) {
|
|
return 100;
|
|
}
|
|
return (int)(value + 0.5f);
|
|
}
|
|
|
|
esp_err_t battery_monitor_init(void)
|
|
{
|
|
adc_oneshot_unit_init_cfg_t unit_cfg = {
|
|
.unit_id = BATTERY_ADC_UNIT,
|
|
};
|
|
|
|
esp_err_t err = adc_oneshot_new_unit(&unit_cfg, &s_adc_handle);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "adc_oneshot_new_unit failed: %s", esp_err_to_name(err));
|
|
return err;
|
|
}
|
|
|
|
adc_oneshot_chan_cfg_t channel_cfg = {
|
|
.atten = BATTERY_ADC_ATTEN,
|
|
.bitwidth = BATTERY_ADC_WIDTH,
|
|
};
|
|
|
|
err = adc_oneshot_config_channel(s_adc_handle, BATTERY_ADC_CHANNEL, &channel_cfg);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "adc_oneshot_config_channel failed: %s", esp_err_to_name(err));
|
|
return err;
|
|
}
|
|
|
|
s_initialized = true;
|
|
battery_monitor_update();
|
|
ESP_LOGI(TAG, "Battery monitor ready on GPIO35 with divider 360k/47k, calibration %.3f", BATTERY_CALIBRATION_MULTIPLIER);
|
|
return ESP_OK;
|
|
}
|
|
|
|
void battery_monitor_update(void)
|
|
{
|
|
if (!s_initialized || s_adc_handle == NULL) {
|
|
return;
|
|
}
|
|
|
|
int raw_total = 0;
|
|
int valid_samples = 0;
|
|
for (int i = 0; i < BATTERY_SAMPLE_COUNT; i++) {
|
|
int raw = 0;
|
|
esp_err_t err = adc_oneshot_read(s_adc_handle, BATTERY_ADC_CHANNEL, &raw);
|
|
if (err == ESP_OK) {
|
|
raw_total += raw;
|
|
valid_samples++;
|
|
}
|
|
}
|
|
|
|
if (valid_samples <= 0) {
|
|
return;
|
|
}
|
|
|
|
const float raw_avg = (float)raw_total / (float)valid_samples;
|
|
const float adc_voltage = (raw_avg / ADC_MAX_RAW) * ADC_REFERENCE_VOLTAGE;
|
|
const float battery_voltage = adc_voltage * BATTERY_DIVIDER_FACTOR * BATTERY_CALIBRATION_MULTIPLIER;
|
|
|
|
// Light smoothing to avoid flickering on the UI.
|
|
if (s_voltage <= 0.01f) {
|
|
s_voltage = battery_voltage;
|
|
} else {
|
|
s_voltage = (s_voltage * 0.80f) + (battery_voltage * 0.20f);
|
|
}
|
|
|
|
const float percent = ((s_voltage - BATTERY_EMPTY_VOLTAGE) /
|
|
(BATTERY_FULL_VOLTAGE - BATTERY_EMPTY_VOLTAGE)) * 100.0f;
|
|
const int computed_percent = clamp_percent(percent);
|
|
|
|
// During normal discharge, the displayed percentage should not jump upward
|
|
// due to ADC noise or voltage recovery after motor load is removed.
|
|
// Charging detection will be handled separately in the later low-battery patch.
|
|
if (!s_percent_initialized) {
|
|
s_percent = computed_percent;
|
|
s_percent_initialized = true;
|
|
} else if (computed_percent < s_percent) {
|
|
s_percent = computed_percent;
|
|
}
|
|
}
|
|
|
|
float battery_monitor_get_voltage(void)
|
|
{
|
|
return s_voltage;
|
|
}
|
|
|
|
int battery_monitor_get_percent(void)
|
|
{
|
|
return s_percent;
|
|
}
|