Files
JBao_Door_Clamp_FW/apps/usr_app/app_battery.c
T
wuchuyuan 215e1be6bb 1、运行中空电关机改看显示百分比,到 5% 立刻停电机、关绿灯,红灯闪 5 下后再深睡,避免电压先掉、APP 还没到 5% 就关机
2、充电/放电电量缓冲加快,放电突变拦截先关掉,电量曲线满电点收到 4.12V
3、上电串口打印软硬件版本和编译时间
2026-08-27 11:40:57 +08:00

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 40000u /**< 充电缓冲:40 秒走 1% */
#define BAT_DISCHARGE_BUFFER_MS 10000u /**< 放电缓冲:10 秒走 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 */