This commit is contained in:
2026-05-26 14:23:19 +02:00
parent 52bab32309
commit 87cba0b419
10 changed files with 421 additions and 15 deletions

View File

@@ -2,12 +2,27 @@ menuconfig BATT_MGMT
bool "Battery Management"
default y
select ADC
select EVENT_MGMT
select GPIO
select USB_MGMT
select NRFX_SAADC
help
Library for initializing and managing the battery subsystem.
if BATT_MGMT
config BATT_MGMT_EMPTY_THRESHOLD
int "Battery Empty Voltage (mV)"
default 3400
help
Voltage threshold treated as empty battery level (0/5).
config BATT_MGMT_SHUTDOWN_THRESHOLD
int "Critical shutdown voltage (mV)"
default 3300
help
If measured battery voltage is less than or equal to this threshold,
the device will power off to protect the battery.
config BATT_MGMT_FULL_THRESHOLD
int "Battery Full Voltage (mV)"
default 3980
@@ -39,6 +54,24 @@ if BATT_MGMT
Time window to detect charger pin blinking (state changes). If the pin state
changes within this window, it's considered blinking (error state).
config BATT_MGMT_STANDBY_MEASURE_INTERVAL_MIN
int "Standby measurement interval (minutes)"
default 360
help
Measurement interval while idle in standby (no charging, no BLE, no audio).
config BATT_MGMT_ACTIVE_MEASURE_INTERVAL_MIN
int "Active measurement interval (minutes)"
default 1
help
Measurement interval while charging or BLE connected.
config BATT_MGMT_AUDIO_COOLDOWN_SEC
int "Audio cooldown before next measure (seconds)"
default 60
help
Delay before restarting periodic battery measurement after audio playback ends.
config BATT_MGMT_MONITOR_THREAD_STACK_SIZE
int "Battery monitor thread stack size"
default 1024

View File

@@ -15,6 +15,13 @@ typedef enum {
BATT_STATE_UNKNOWN,
} batt_mgmt_state_t;
typedef struct {
batt_mgmt_state_t state;
uint8_t level;
uint8_t percent;
int32_t voltage_mv;
} batt_mgmt_info_t;
/**
* @brief Measure battery VDDH voltage.
*
@@ -24,13 +31,53 @@ typedef enum {
*/
int batt_mgmt_measure_vddh_mv(uint8_t oversampling, int32_t *vddh_mv);
/**
* @brief Get display voltage in millivolts.
*
* Returns the voltage value intended for UI/protocol display. Currently this is
* the latest measured VDDH value cached by batt_mgmt.
*
* @param vddh_mv Pointer receiving display voltage in mV.
* @return 0 on success, negative errno on failure.
*/
int batt_mgmt_get_display_voltage_mv(int32_t *vddh_mv);
/**
* @brief Get battery level bucket (0..4) from configured thresholds.
*
* Level mapping:
* - 0: below 20% threshold
* - 1: 20%..49%
* - 2: 50%..79%
* - 3: 80%..99%
* - 4: full and above
*
* @param level Pointer receiving level in range 0..4.
* @return 0 on success, negative errno on failure.
*/
int batt_mgmt_get_battery_level(uint8_t *level);
/**
* @brief Get battery percentage (0-100) from configured voltage thresholds.
*
* Uses piecewise linear interpolation over:
* - EMPTY..20%
* - 20%..50%
* - 50%..80%
* - 80%..FULL
*
* @param percent Pointer receiving percentage in range 0..100.
* @return 0 on success, negative errno on failure.
*/
int batt_mgmt_get_battery_percent(uint8_t *percent);
/**
* @brief Get raw charger status (GPIO level).
*
* Returns the current logic level of the charger status pin (typically from ETA6003).
* This is the **raw GPIO status**, not a processed state machine result.
*
* @return true if charger pin is high (e.g., battery is "full"), false if low.
* @return true if charger pin is high (e.g., battery is "charging"), false if low.
*/
bool batt_mgmt_get_charger_status(void);
@@ -48,5 +95,13 @@ bool batt_mgmt_get_charger_status(void);
*/
batt_mgmt_state_t batt_mgmt_get_battery_state(void);
/**
* @brief Get all battery information in one call.
*
* @param info Pointer receiving state, level (0..4), percent (0..100), and voltage in mV.
* @return 0 on success, negative errno on failure.
*/
int batt_mgmt_get_info(batt_mgmt_info_t *info);
#endif

View File

@@ -4,6 +4,7 @@
#include <zephyr/init.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <nrfx_saadc.h>
#include <errno.h>
#include <stdbool.h>
@@ -23,9 +24,64 @@ static const struct gpio_dt_spec batt_chg_status = GPIO_DT_SPEC_GET(BATT_CHG_STA
static bool batt_mgmt_ready;
static int64_t batt_mgmt_last_chg_change_ms;
static int64_t batt_mgmt_last_offset_calib_ms;
static int32_t batt_mgmt_last_vddh_mv;
static bool batt_mgmt_chg_interrupt_enabled;
static K_THREAD_STACK_DEFINE(batt_monitor_stack, CONFIG_BATT_MGMT_MONITOR_THREAD_STACK_SIZE);
static struct k_thread batt_monitor_thread_data;
static struct k_sem batt_mgmt_saadc_calib_sem;
static int batt_mgmt_saadc_calib_rc;
static void batt_mgmt_saadc_calib_handler(nrfx_saadc_evt_t const *p_event)
{
if ((p_event == NULL) || (p_event->type != NRFX_SAADC_EVT_CALIBRATEDONE))
{
batt_mgmt_saadc_calib_rc = -EIO;
}
else
{
batt_mgmt_saadc_calib_rc = 0;
}
k_sem_give(&batt_mgmt_saadc_calib_sem);
}
static int batt_mgmt_calibrate_offset_if_needed(bool force)
{
int rc;
int64_t now_ms = k_uptime_get();
int64_t calib_interval_ms = (int64_t)CONFIG_BATT_MGMT_STANDBY_MEASURE_INTERVAL_MIN * 60 * 1000;
if (!force && (now_ms - batt_mgmt_last_offset_calib_ms < calib_interval_ms))
{
return 0;
}
while (k_sem_take(&batt_mgmt_saadc_calib_sem, K_NO_WAIT) == 0)
{
}
batt_mgmt_saadc_calib_rc = -EIO;
rc = nrfx_saadc_offset_calibrate(batt_mgmt_saadc_calib_handler);
if (rc < 0)
{
return rc;
}
rc = k_sem_take(&batt_mgmt_saadc_calib_sem, K_MSEC(100));
if (rc < 0)
{
return rc;
}
if (batt_mgmt_saadc_calib_rc < 0)
{
return batt_mgmt_saadc_calib_rc;
}
batt_mgmt_last_offset_calib_ms = k_uptime_get();
return 0;
}
static int batt_mgmt_to_adc_oversampling(uint8_t oversampling, uint8_t *adc_oversampling)
{
@@ -68,6 +124,12 @@ int batt_mgmt_measure_vddh_mv(uint8_t oversampling, int32_t *vddh_mv)
return -EAGAIN;
}
rc = batt_mgmt_calibrate_offset_if_needed(false);
if (rc < 0)
{
LOG_WRN("ADC offset calibration failed before measurement: %d", rc);
}
rc = batt_mgmt_to_adc_oversampling(oversampling, &adc_oversampling);
if (rc < 0)
{
@@ -96,19 +158,194 @@ int batt_mgmt_measure_vddh_mv(uint8_t oversampling, int32_t *vddh_mv)
}
*vddh_mv = input_mv * BATT_VDDH_DIVIDER_FACTOR;
batt_mgmt_last_vddh_mv = *vddh_mv;
LOG_DBG("Battery VDDH: %d mV (raw=%d, os=%ux)", *vddh_mv, raw_sample, oversampling);
return 0;
}
static int batt_mgmt_linear_percent(int32_t mv, int32_t lo_mv, int32_t hi_mv,
uint8_t lo_pct, uint8_t hi_pct, uint8_t *out_pct)
{
int32_t num;
int32_t den;
int32_t pct;
if (out_pct == NULL)
{
return -EINVAL;
}
if (hi_mv <= lo_mv)
{
return -EINVAL;
}
if (mv <= lo_mv)
{
*out_pct = lo_pct;
return 0;
}
if (mv >= hi_mv)
{
*out_pct = hi_pct;
return 0;
}
num = (mv - lo_mv) * (int32_t)(hi_pct - lo_pct);
den = hi_mv - lo_mv;
pct = (int32_t)lo_pct + (num / den);
if (pct < 0)
{
pct = 0;
}
else if (pct > 100)
{
pct = 100;
}
*out_pct = (uint8_t)pct;
return 0;
}
int batt_mgmt_get_display_voltage_mv(int32_t *vddh_mv)
{
int rc;
if (vddh_mv == NULL)
{
return -EINVAL;
}
if (!batt_mgmt_ready)
{
return -EAGAIN;
}
if (batt_mgmt_last_vddh_mv <= 0)
{
rc = batt_mgmt_measure_vddh_mv(BATT_MGMT_OVERSAMPLING_16X, &batt_mgmt_last_vddh_mv);
if (rc < 0)
{
return rc;
}
}
*vddh_mv = batt_mgmt_last_vddh_mv;
return 0;
}
int batt_mgmt_get_battery_level(uint8_t *level)
{
int32_t mv;
int rc;
if (level == NULL)
{
return -EINVAL;
}
rc = batt_mgmt_get_display_voltage_mv(&mv);
if (rc < 0)
{
return rc;
}
if (mv >= CONFIG_BATT_MGMT_FULL_THRESHOLD)
{
*level = 4U;
}
else if (mv >= CONFIG_BATT_MGMT_80P_THRESHOLD)
{
*level = 3U;
}
else if (mv >= CONFIG_BATT_MGMT_50P_THRESHOLD)
{
*level = 2U;
}
else if (mv >= CONFIG_BATT_MGMT_20P_THRESHOLD)
{
*level = 1U;
}
else
{
*level = 0U;
}
return 0;
}
int batt_mgmt_get_battery_percent(uint8_t *percent)
{
int32_t mv;
int rc;
if (percent == NULL)
{
return -EINVAL;
}
rc = batt_mgmt_get_display_voltage_mv(&mv);
if (rc < 0)
{
return rc;
}
if (mv <= CONFIG_BATT_MGMT_EMPTY_THRESHOLD)
{
*percent = 0;
return 0;
}
if (mv >= CONFIG_BATT_MGMT_FULL_THRESHOLD)
{
*percent = 100;
return 0;
}
if (mv < CONFIG_BATT_MGMT_20P_THRESHOLD)
{
return batt_mgmt_linear_percent(mv,
CONFIG_BATT_MGMT_EMPTY_THRESHOLD,
CONFIG_BATT_MGMT_20P_THRESHOLD,
0, 20, percent);
}
if (mv < CONFIG_BATT_MGMT_50P_THRESHOLD)
{
return batt_mgmt_linear_percent(mv,
CONFIG_BATT_MGMT_20P_THRESHOLD,
CONFIG_BATT_MGMT_50P_THRESHOLD,
20, 50, percent);
}
if (mv < CONFIG_BATT_MGMT_80P_THRESHOLD)
{
return batt_mgmt_linear_percent(mv,
CONFIG_BATT_MGMT_50P_THRESHOLD,
CONFIG_BATT_MGMT_80P_THRESHOLD,
50, 80, percent);
}
return batt_mgmt_linear_percent(mv,
CONFIG_BATT_MGMT_80P_THRESHOLD,
CONFIG_BATT_MGMT_FULL_THRESHOLD,
80, 100, percent);
}
static void batt_mgmt_chg_interrupt_handler(const struct device *dev,
struct gpio_callback *cb,
uint32_t pins)
{
int64_t now_ms = k_uptime_get();
int64_t delta_ms = now_ms - batt_mgmt_last_chg_change_ms;
ARG_UNUSED(dev);
ARG_UNUSED(cb);
ARG_UNUSED(pins);
#ifdef CONFIG_BATT_MGMT_BLINK_INTERVAL_LOGGING
int64_t delta_ms = now_ms - batt_mgmt_last_chg_change_ms;
LOG_DBG("CHG interrupt: delta=%lld ms", delta_ms);
#endif
@@ -227,6 +464,37 @@ batt_mgmt_state_t batt_mgmt_get_battery_state(void)
return BATT_STATE_DISCHARGING;
}
int batt_mgmt_get_info(batt_mgmt_info_t *info)
{
int rc;
if (info == NULL)
{
return -EINVAL;
}
rc = batt_mgmt_get_display_voltage_mv(&info->voltage_mv);
if (rc < 0)
{
return rc;
}
rc = batt_mgmt_get_battery_level(&info->level);
if (rc < 0)
{
return rc;
}
rc = batt_mgmt_get_battery_percent(&info->percent);
if (rc < 0)
{
return rc;
}
info->state = batt_mgmt_get_battery_state();
return 0;
}
static void batt_mgmt_monitor_thread(void *unused1, void *unused2, void *unused3)
{
(void)unused1;
@@ -288,8 +556,17 @@ static int batt_mgmt_init(void)
return rc;
}
k_sem_init(&batt_mgmt_saadc_calib_sem, 0, 1);
batt_mgmt_last_offset_calib_ms = 0;
batt_mgmt_ready = true;
rc = batt_mgmt_calibrate_offset_if_needed(true);
if (rc < 0)
{
LOG_WRN("Initial ADC offset calibration failed: %d", rc);
}
rc = batt_mgmt_measure_vddh_mv(BATT_MGMT_OVERSAMPLING_16X, &boot_vddh_mv);
if (rc < 0)
{
@@ -297,6 +574,7 @@ static int batt_mgmt_init(void)
return 0;
}
batt_mgmt_last_vddh_mv = boot_vddh_mv;
LOG_DBG("Initial battery VDDH: %d mV", boot_vddh_mv);
k_thread_create(&batt_monitor_thread_data,