2、自动/手动互相切换都闪 2 下灯;APP 发停止后再发自动模式可正常启动,手动残留停止不再挡住自动 3、自动模式 1~6 单次最长跑 10 分钟后回待机;关机时长按 12 秒进 OTA,OTA 期间红绿交替闪
506 lines
16 KiB
C
506 lines
16 KiB
C
/******************************************************************************
|
|
* @file app_battery.c
|
|
* @brief 电量算法实现:电压滤波、充放电基准记录、突变防抖、缓冲平滑显示
|
|
* @author cyWu <1917507415@qq.com>
|
|
* @date 2026.08.27
|
|
* @version V1.0.1
|
|
* @history
|
|
* - V1.0.0, 2026.08.26, cyWu, 首次发布,实现电压滤波、充放电基准、突变防抖与缓冲平滑
|
|
* - V1.0.1, 2026.08.27, cyWu, 补充函数内部注释
|
|
******************************************************************************/
|
|
|
|
#include "app_battery.h"
|
|
#include "board_pin.h"
|
|
#include <string.h>
|
|
|
|
#define LOG_TAG_CONST APP
|
|
#define LOG_TAG "[APP_BAT]"
|
|
#define LOG_INFO_ENABLE
|
|
#include "debug.h"
|
|
|
|
/*========================= 时间/阈值配置 =========================*/
|
|
#define BAT_SWITCH_WAIT_CHARGE_MS 60000u /**< 切到充电后等 60s 才记基准 */
|
|
#define BAT_SWITCH_WAIT_DISCHARGE_MS 60000u /**< 切到放电后等 60s 才记基准 */
|
|
#define BAT_CHARGE_BUFFER_MS 60000u /**< 充电缓冲:1 分钟走 1% */
|
|
#define BAT_DISCHARGE_BUFFER_MS 20000u /**< 放电缓冲:20 秒走 1% */
|
|
#define BAT_DISCHARGE_FILTER_W 90 /**< 放电指数滤波权重(/100),压掉电机拉载噪声 */
|
|
#define BAT_SUDDEN_DROP_PCT 1 /**< 突变阈值:单次超过 1% 判定为突变 */
|
|
#define BAT_DROP_RECOVERY_MS 60000u /**< 突变超过此时长强制接受新电量 */
|
|
#define BAT_RECOVERY_WAIT_MS 30000u /**< 突变后电量回升,需稳定这么久才解除 */
|
|
|
|
#if APP_BATTERY_AUTO_INCREMENT_EN
|
|
#define BAT_AUTO_INC_INTERVAL_MS (3u * 60u * 1000u) /**< 自增间隔:3 分钟 */
|
|
#define BAT_AUTO_INC_TRIGGER_MV 2 /**< 距历史满电压 2mV 内触发自增模式 */
|
|
#endif
|
|
|
|
/** 充/放电各自的基准电压——记录"状态切换稳定后"的真实电压与当时电量,
|
|
* 之后按这个基准重新拟合电压-电量曲线,而不是死用出厂固定曲线 */
|
|
typedef struct {
|
|
uint16_t base_mv;
|
|
uint8_t base_percent;
|
|
uint8_t recorded;
|
|
} AppBatteryBase_t;
|
|
|
|
/** 模块状态,所有跟时间相关的字段都用 dt 累加,不依赖系统绝对时钟 */
|
|
typedef struct {
|
|
uint8_t initialized;
|
|
uint16_t voltage_mv; /**< 滤波后电压 */
|
|
uint8_t percent; /**< 当前对外显示的电量(已缓冲平滑) */
|
|
uint8_t target_percent; /**< 电压当下换算出的目标电量 */
|
|
uint8_t charging;
|
|
|
|
uint8_t is_switching; /**< 充放电状态刚切换,等稳定后才记基准 */
|
|
uint32_t switch_ms;
|
|
|
|
uint8_t is_buffering; /**< percent 正在按缓冲节奏逼近 target_percent */
|
|
uint32_t buffer_ms;
|
|
|
|
uint8_t drop_detected; /**< 放电时检测到电量单次跳变过大,先当噪声处理 */
|
|
uint8_t recovery_waiting;
|
|
uint8_t pre_drop_percent;
|
|
uint32_t drop_ms;
|
|
uint32_t recovery_ms;
|
|
|
|
#if APP_BATTERY_AUTO_INCREMENT_EN
|
|
uint8_t auto_inc_active;
|
|
uint8_t was_full;
|
|
uint32_t auto_inc_ms;
|
|
uint16_t max_charge_mv; /**< 本次充电周期内记录到的最高电压 */
|
|
uint16_t full_charge_mv; /**< 历史上电量到过 100% 时记录的电压,用于下次提前触发自增 */
|
|
#endif
|
|
} AppBatteryCtx_t;
|
|
|
|
static AppBatteryCtx_t s_batt;
|
|
static AppBatteryBase_t s_charge_base;
|
|
static AppBatteryBase_t s_discharge_base;
|
|
|
|
static uint8_t battery_mv_to_percent_discharge(uint16_t mv);
|
|
static uint8_t battery_mv_to_percent_charge(uint16_t mv);
|
|
static void battery_update_voltage_filter(uint16_t mv);
|
|
static void battery_start_buffer(uint8_t target_percent);
|
|
static void battery_update_buffer(uint16_t dt);
|
|
static uint8_t battery_check_sudden_drop(uint16_t dt, uint8_t target_percent);
|
|
|
|
#if APP_BATTERY_AUTO_INCREMENT_EN
|
|
static void battery_update_charge_max_mv(uint16_t mv);
|
|
static void battery_check_auto_increment(uint16_t mv);
|
|
static void battery_handle_auto_increment(uint16_t dt);
|
|
static void battery_learn_full_charge_mv(void);
|
|
#endif
|
|
|
|
/**
|
|
* @brief 初始化电量模块状态
|
|
* @return 无
|
|
*/
|
|
void app_battery_init(void)
|
|
{
|
|
memset(&s_batt, 0, sizeof(s_batt));
|
|
memset(&s_charge_base, 0, sizeof(s_charge_base));
|
|
memset(&s_discharge_base, 0, sizeof(s_discharge_base));
|
|
}
|
|
|
|
/**
|
|
* @brief 查询当前显示用电量百分比
|
|
* @return 0~100
|
|
*/
|
|
uint8_t app_battery_get_percent(void)
|
|
{
|
|
return s_batt.percent;
|
|
}
|
|
|
|
/**
|
|
* @brief 外部强制刷新电量,立即生效,清掉正在进行的缓冲,避免被缓冲拉回去
|
|
* @param percent 目标百分比,超过 100 会被钳到 100
|
|
* @return 无
|
|
*/
|
|
void app_battery_set_percent(uint8_t percent)
|
|
{
|
|
AppBatteryCtx_t *b = &s_batt;
|
|
|
|
if (percent > 100) {
|
|
percent = 100;
|
|
}
|
|
b->initialized = 1;
|
|
b->percent = percent;
|
|
b->target_percent = percent;
|
|
b->is_buffering = 0; /* 立刻生效,清掉正在走的缓冲,避免被拉回去 */
|
|
b->buffer_ms = 0;
|
|
log_info("battery percent forced to %d%%\n", percent);
|
|
}
|
|
|
|
/**
|
|
* @brief 按最新采样电压推进电量算法
|
|
* @param dt 距上次调用的毫秒数
|
|
* @param mv 本次采样均值电压,单位 mV
|
|
* @param charging 1=充电中,0=放电
|
|
* @return 无
|
|
*/
|
|
void app_battery_tick(uint16_t dt, uint16_t mv, uint8_t charging)
|
|
{
|
|
AppBatteryCtx_t *b = &s_batt;
|
|
int32_t new_percent;
|
|
uint8_t target;
|
|
|
|
/* 第一次进来:用当前电压直接算出显示电量,后面才走滤波/缓冲 */
|
|
if (!b->initialized) {
|
|
b->voltage_mv = mv;
|
|
b->charging = charging;
|
|
b->percent = charging ? battery_mv_to_percent_charge(mv)
|
|
: battery_mv_to_percent_discharge(mv);
|
|
b->target_percent = b->percent;
|
|
b->initialized = 1;
|
|
return;
|
|
}
|
|
|
|
/* 充放电状态刚切:先等电压稳定再记基准,这段时间显示电量维持原值 */
|
|
if (charging != b->charging) {
|
|
b->charging = charging;
|
|
b->is_switching = 1;
|
|
b->switch_ms = 0;
|
|
b->is_buffering = 0;
|
|
b->drop_detected = 0;
|
|
b->recovery_waiting = 0;
|
|
#if APP_BATTERY_AUTO_INCREMENT_EN
|
|
if (charging) {
|
|
b->max_charge_mv = 0;
|
|
b->auto_inc_active = 0;
|
|
}
|
|
#endif
|
|
log_info("battery switching to %s\n", charging ? "charge" : "discharge");
|
|
}
|
|
|
|
if (b->is_switching) {
|
|
uint32_t wait_ms = b->charging ? BAT_SWITCH_WAIT_CHARGE_MS : BAT_SWITCH_WAIT_DISCHARGE_MS;
|
|
|
|
b->switch_ms += dt;
|
|
if (b->switch_ms < wait_ms) {
|
|
return; /* 基准没记完前,显示电量维持原值,不参与后面的计算 */
|
|
}
|
|
{
|
|
AppBatteryBase_t *base = b->charging ? &s_charge_base : &s_discharge_base;
|
|
base->base_mv = mv;
|
|
base->base_percent = b->percent;
|
|
base->recorded = 1;
|
|
log_info("battery %s base %dmV %d%%\n",
|
|
b->charging ? "charge" : "discharge", mv, b->percent);
|
|
}
|
|
b->is_switching = 0;
|
|
b->voltage_mv = mv;
|
|
}
|
|
|
|
battery_update_voltage_filter(mv);
|
|
|
|
/* 电压换算成目标电量:放电只减不增,充电只增不减,避免噪声来回跳 */
|
|
if (!b->charging) {
|
|
new_percent = battery_mv_to_percent_discharge(b->voltage_mv);
|
|
if (new_percent > b->target_percent) {
|
|
new_percent = b->target_percent;
|
|
}
|
|
} else {
|
|
#if APP_BATTERY_AUTO_INCREMENT_EN
|
|
battery_update_charge_max_mv(mv);
|
|
battery_check_auto_increment(mv);
|
|
#endif
|
|
new_percent = battery_mv_to_percent_charge(b->voltage_mv);
|
|
if (new_percent < b->target_percent) {
|
|
new_percent = b->target_percent;
|
|
}
|
|
}
|
|
if (new_percent > 100) {
|
|
new_percent = 100;
|
|
} else if (new_percent < 0) {
|
|
new_percent = 0;
|
|
}
|
|
target = (uint8_t)new_percent;
|
|
|
|
/* 放电时单次跳变过大先当噪声,稳住后再更新 */
|
|
if (!b->charging && battery_check_sudden_drop(dt, target)) {
|
|
return;
|
|
}
|
|
|
|
if (target != b->target_percent) {
|
|
b->target_percent = target;
|
|
battery_start_buffer(target);
|
|
}
|
|
battery_update_buffer(dt);
|
|
|
|
#if APP_BATTERY_AUTO_INCREMENT_EN
|
|
if (b->charging) {
|
|
if (b->auto_inc_active) {
|
|
battery_handle_auto_increment(dt);
|
|
}
|
|
battery_learn_full_charge_mv();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/*========================= 私有函数实现 =========================*/
|
|
|
|
/**
|
|
* @brief 放电时电压转百分比:有基准用基准拟合,没有就用出厂固定曲线
|
|
*/
|
|
static uint8_t battery_mv_to_percent_discharge(uint16_t mv)
|
|
{
|
|
int32_t pct;
|
|
int32_t span;
|
|
|
|
/* 有放电基准:从当时记下的电压/电量,线性拟合到空电电压 */
|
|
if (s_discharge_base.recorded) {
|
|
uint16_t v0 = s_discharge_base.base_mv;
|
|
|
|
if (mv > v0) {
|
|
mv = v0; /* 放电电压不应比基准还高,高出的当噪声钳掉 */
|
|
}
|
|
span = (int32_t)v0 - BOARD_VBAT_EMPTY_MV;
|
|
if (span <= 0) {
|
|
return s_discharge_base.base_percent;
|
|
}
|
|
pct = (int32_t)(mv - BOARD_VBAT_EMPTY_MV) * s_discharge_base.base_percent / span;
|
|
if (pct < 0) {
|
|
pct = 0;
|
|
} else if (pct > s_discharge_base.base_percent) {
|
|
pct = s_discharge_base.base_percent;
|
|
}
|
|
return (uint8_t)pct;
|
|
}
|
|
|
|
/* 没有基准:用出厂固定曲线 空电~满电 线性换算 */
|
|
if (mv <= BOARD_VBAT_EMPTY_MV) {
|
|
return 0;
|
|
}
|
|
if (mv >= BOARD_VBAT_FULL_MV) {
|
|
return 100;
|
|
}
|
|
pct = (int32_t)(mv - BOARD_VBAT_EMPTY_MV) * 100 / (BOARD_VBAT_FULL_MV - BOARD_VBAT_EMPTY_MV);
|
|
return (uint8_t)pct;
|
|
}
|
|
|
|
/**
|
|
* @brief 充电时电压转百分比:有基准用基准拟合,没有就用出厂固定曲线
|
|
*/
|
|
static uint8_t battery_mv_to_percent_charge(uint16_t mv)
|
|
{
|
|
int32_t pct;
|
|
int32_t span;
|
|
int32_t remain;
|
|
|
|
/* 有充电基准:从当时记下的电压/电量,线性拟合到充满电压 */
|
|
if (s_charge_base.recorded) {
|
|
uint16_t v0 = s_charge_base.base_mv;
|
|
|
|
if (mv < v0) {
|
|
mv = v0; /* 充电电压不应比基准还低,低出的当噪声钳掉 */
|
|
}
|
|
span = (int32_t)BOARD_VBAT_CHARGE_FULL_MV - v0;
|
|
remain = 100 - s_charge_base.base_percent;
|
|
if (span <= 0) {
|
|
return 100;
|
|
}
|
|
pct = (int32_t)(mv - v0) * remain / span + s_charge_base.base_percent;
|
|
if (pct < s_charge_base.base_percent) {
|
|
pct = s_charge_base.base_percent;
|
|
} else if (pct > 100) {
|
|
pct = 100;
|
|
}
|
|
return (uint8_t)pct;
|
|
}
|
|
|
|
/* 没有基准:用出厂充电曲线线性换算 */
|
|
if (mv <= BOARD_VBAT_CHARGE_EMPTY_MV) {
|
|
return 0;
|
|
}
|
|
if (mv >= BOARD_VBAT_CHARGE_FULL_MV) {
|
|
return 100;
|
|
}
|
|
pct = (int32_t)(mv - BOARD_VBAT_CHARGE_EMPTY_MV) * 100
|
|
/ (BOARD_VBAT_CHARGE_FULL_MV - BOARD_VBAT_CHARGE_EMPTY_MV);
|
|
return (uint8_t)pct;
|
|
}
|
|
|
|
/**
|
|
* @brief 更新电压滤波值:放电用指数滤波压噪声,充电直接用采样值(充电曲线本身平缓)
|
|
*/
|
|
static void battery_update_voltage_filter(uint16_t mv)
|
|
{
|
|
if (!s_batt.charging) {
|
|
/* 放电:指数滤波压掉电机拉载噪声,权重越大越平滑、越滞后 */
|
|
s_batt.voltage_mv = (uint16_t)(((uint32_t)s_batt.voltage_mv * BAT_DISCHARGE_FILTER_W
|
|
+ (uint32_t)mv * (100 - BAT_DISCHARGE_FILTER_W)) / 100);
|
|
} else {
|
|
s_batt.voltage_mv = mv; /* 充电曲线本身平缓,直接用采样值 */
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief 启动电量缓冲:目标变了就重新计时,percent 后面按固定节奏逼近 target
|
|
*/
|
|
static void battery_start_buffer(uint8_t target_percent)
|
|
{
|
|
if (target_percent == s_batt.percent) {
|
|
s_batt.is_buffering = 0;
|
|
return;
|
|
}
|
|
s_batt.is_buffering = 1;
|
|
s_batt.buffer_ms = 0;
|
|
}
|
|
|
|
/**
|
|
* @brief 推进电量缓冲:满一个步进周期就 ±1%,保留余数不清零,避免长期漂移
|
|
*/
|
|
static void battery_update_buffer(uint16_t dt)
|
|
{
|
|
AppBatteryCtx_t *b = &s_batt;
|
|
uint32_t step_ms;
|
|
|
|
if (!b->is_buffering) {
|
|
return;
|
|
}
|
|
step_ms = b->charging ? BAT_CHARGE_BUFFER_MS : BAT_DISCHARGE_BUFFER_MS;
|
|
b->buffer_ms += dt;
|
|
if (b->buffer_ms < step_ms) {
|
|
return;
|
|
}
|
|
b->buffer_ms -= step_ms; /* 减一个周期而不是清零,余数留给下次,避免长期漂移 */
|
|
|
|
if (b->charging) {
|
|
if (b->percent < b->target_percent) {
|
|
b->percent++;
|
|
}
|
|
} else {
|
|
if (b->percent > b->target_percent) {
|
|
b->percent--;
|
|
}
|
|
}
|
|
if (b->percent == b->target_percent) {
|
|
b->is_buffering = 0;
|
|
b->buffer_ms = 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief 放电突变检测:单次跳变超过阈值先当噪声,回升稳定一段时间才解除;
|
|
* 若一直没回升,超时后强制接受,避免误判卡死电量不更新
|
|
* @param dt 距上次调用的毫秒数
|
|
* @param target_percent 本次电压算出来的目标电量
|
|
* @return 1=仍在突变检测中(不更新电量),0=正常
|
|
*/
|
|
static uint8_t battery_check_sudden_drop(uint16_t dt, uint8_t target_percent)
|
|
{
|
|
AppBatteryCtx_t *b = &s_batt;
|
|
|
|
if (b->drop_detected) {
|
|
int16_t recovery_diff;
|
|
|
|
b->drop_ms += dt;
|
|
recovery_diff = (int16_t)target_percent - (int16_t)b->pre_drop_percent;
|
|
|
|
/* 电量已经回升到突变阈值以内:再稳一段时间才真正解除 */
|
|
if (recovery_diff >= -(int16_t)BAT_SUDDEN_DROP_PCT) {
|
|
if (!b->recovery_waiting) {
|
|
b->recovery_waiting = 1;
|
|
b->recovery_ms = 0;
|
|
} else {
|
|
b->recovery_ms += dt;
|
|
}
|
|
if (b->recovery_ms >= BAT_RECOVERY_WAIT_MS) {
|
|
b->drop_detected = 0;
|
|
b->recovery_waiting = 0;
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
b->recovery_waiting = 0;
|
|
|
|
/* 一直没回升:超时后强制接受新电量,避免卡死不更新 */
|
|
if (b->drop_ms >= BAT_DROP_RECOVERY_MS) {
|
|
b->drop_detected = 0;
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
/* 单次跳变超过阈值,先当噪声,锁住显示电量 */
|
|
if ((int16_t)b->percent - (int16_t)target_percent > BAT_SUDDEN_DROP_PCT) {
|
|
b->drop_detected = 1;
|
|
b->pre_drop_percent = b->percent;
|
|
b->drop_ms = 0;
|
|
b->recovery_waiting = 0;
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
#if APP_BATTERY_AUTO_INCREMENT_EN
|
|
/**
|
|
* @brief 记录本次充电周期内的最高电压,用作下次提前触发自增的参考
|
|
*/
|
|
static void battery_update_charge_max_mv(uint16_t mv)
|
|
{
|
|
if (mv > s_batt.max_charge_mv) {
|
|
s_batt.max_charge_mv = mv;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief 接近历史满电压时提前启动自增模式,避免曲线末期平台期卡住不动
|
|
*/
|
|
static void battery_check_auto_increment(uint16_t mv)
|
|
{
|
|
AppBatteryCtx_t *b = &s_batt;
|
|
uint16_t trigger_mv;
|
|
|
|
if (b->auto_inc_active || b->full_charge_mv == 0 || b->percent >= 99) {
|
|
return;
|
|
}
|
|
trigger_mv = b->full_charge_mv - BAT_AUTO_INC_TRIGGER_MV;
|
|
if (mv >= trigger_mv) {
|
|
b->auto_inc_active = 1;
|
|
b->auto_inc_ms = 0;
|
|
log_info("battery auto-inc start %dmV trigger=%dmV full=%dmV\n",
|
|
mv, trigger_mv, b->full_charge_mv);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief 自增模式下每隔固定间隔 +1%,最多到 99%(100% 交给真实电压判定)
|
|
*/
|
|
static void battery_handle_auto_increment(uint16_t dt)
|
|
{
|
|
AppBatteryCtx_t *b = &s_batt;
|
|
|
|
if (b->percent >= 99) {
|
|
b->auto_inc_active = 0;
|
|
return;
|
|
}
|
|
b->auto_inc_ms += dt;
|
|
if (b->auto_inc_ms >= BAT_AUTO_INC_INTERVAL_MS) {
|
|
b->auto_inc_ms = 0;
|
|
b->percent++;
|
|
b->target_percent = b->percent;
|
|
log_info("battery auto-inc +1%% -> %d%%\n", b->percent);
|
|
if (b->percent >= 99) {
|
|
b->auto_inc_active = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief 电量首次到达 100% 时记下当时电压,供下次充电提前触发自增
|
|
*/
|
|
static void battery_learn_full_charge_mv(void)
|
|
{
|
|
AppBatteryCtx_t *b = &s_batt;
|
|
|
|
if (b->percent == 100) {
|
|
if (!b->was_full && b->max_charge_mv > 0) {
|
|
b->full_charge_mv = b->max_charge_mv;
|
|
log_info("battery full-charge voltage learned: %dmV\n", b->full_charge_mv);
|
|
b->auto_inc_active = 0;
|
|
}
|
|
b->was_full = 1;
|
|
} else {
|
|
b->was_full = 0;
|
|
}
|
|
}
|
|
#endif /* APP_BATTERY_AUTO_INCREMENT_EN */
|