/****************************************************************************** * @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 #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 */