488 lines
15 KiB
C
488 lines
15 KiB
C
/******************************************************************************
|
||
* @file app_power.c
|
||
* @brief USB / 充电检测、电量采样与低电策略、深睡请求生命周期实现
|
||
* @author cyWu <1917507415@qq.com>
|
||
* @date 2026.08.27
|
||
* @version V1.2.0
|
||
* @history
|
||
* - V1.0.0, 2026.08.25, cyWu, 首次发布,实现 USB/充电检测、电量采样与深睡请求
|
||
* - V1.1.0, 2026.08.26, cyWu, 取消低电闪烁 1 分钟后自动关机;自动关机门限
|
||
* 改为约 5%(BOARD_VBAT_SHUTDOWN_MV),与开机
|
||
* 低电拦截(app_boot)共用同一门限
|
||
* - V1.2.0, 2026.08.27, cyWu, 运行中空电关机改用显示百分比,到 5% 立即停电机
|
||
* 并请求空电灯效后再掉电,不再做连续次数窗口
|
||
******************************************************************************/
|
||
|
||
#include "app_power.h"
|
||
#include "app_time_util.h"
|
||
#include "app_battery.h"
|
||
#include "bsp_hw.h"
|
||
#include "board_pin.h"
|
||
#include "jb_product.h"
|
||
#include "app_main.h"
|
||
#include "usr_le_api.h"
|
||
#include "system/includes.h"
|
||
#include "ui_manage.h"
|
||
#include <string.h>
|
||
|
||
#define LOG_TAG_CONST APP
|
||
#define LOG_TAG "[APP_PWR]"
|
||
#define LOG_INFO_ENABLE
|
||
#include "debug.h"
|
||
|
||
#define BAT_SAMPLE_MS 100
|
||
#define BAT_AVG_N 8
|
||
#define BAT_UPDATE_MS (BAT_SAMPLE_MS * BAT_AVG_N) /**< 凑够一次新均值的真实耗时,喂给 app_battery */
|
||
#define BAT_BOOT_MS 3000 /* 上电 3s 内不因低电深睡 */
|
||
#define BAT_LOG_MS 10000
|
||
#define BAT_PERIODIC_LOG_ENABLE 0 /* Production standby must not wake UART periodically. */
|
||
#define CHG_ON_CONFIRM_MS 50u
|
||
#define CHG_OFF_CONFIRM_MS 150u
|
||
#define CHG_FULL_RECHECK_MS 1000u
|
||
#define BAT_LOW_WIN 3 /* 连续约 2.4s 才进低电告警(仅闪灯,不关机) */
|
||
|
||
/** TP4056 CHRG/STDBY debounced charging state. */
|
||
typedef struct {
|
||
uint8_t usb_online;
|
||
uint8_t charge_led;
|
||
uint8_t charge_led_last;
|
||
uint16_t charging_ms;
|
||
uint16_t unplugged_ms;
|
||
uint16_t full_ms;
|
||
uint16_t full_recheck_ms;
|
||
uint8_t full_latched;
|
||
} AppChargeState_t;
|
||
|
||
/** 电量采样:滑动平均 + 百分比 */
|
||
typedef struct {
|
||
uint16_t tick_ms;
|
||
uint16_t acc_mv;
|
||
uint8_t n;
|
||
uint16_t mv;
|
||
uint8_t pct;
|
||
uint16_t boot_ms;
|
||
uint16_t log_ms;
|
||
} AppBatterySample_t;
|
||
|
||
/** 低电告警:进入/退出都要连续窗口消抖,避免电机拉压降误报
|
||
* @note 低电(BOARD_VBAT_LOW_MV)只负责红灯闪烁提示,不再有超时自动关机;
|
||
* 真正的自动关机改由 power_battery_check_empty 在显示电量
|
||
* <= BOARD_BATTERY_SHUTDOWN_PCT(5%)时立刻触发 */
|
||
typedef struct {
|
||
uint8_t low_bat;
|
||
uint8_t enter_win;
|
||
uint8_t exit_win;
|
||
} AppBatteryAlarm_t;
|
||
|
||
/** 电源模块总状态,替代原来一堆散落的 static 变量 */
|
||
typedef struct {
|
||
AppChargeState_t charge;
|
||
AppBatterySample_t sample;
|
||
AppBatteryAlarm_t alarm;
|
||
uint8_t req_poweroff;
|
||
uint8_t empty_off; /**< 1=空电路径:闪完红灯再掉电 */
|
||
AppPowerHook_t on_request;
|
||
AppPowerHook_t on_finalize;
|
||
} AppPowerCtx_t;
|
||
|
||
static AppPowerCtx_t s_power;
|
||
|
||
static void power_do_poweroff(void *priv);
|
||
static AppPowerEvt_t power_charge_tick(uint16_t dt);
|
||
static void power_battery_tick(uint16_t dt);
|
||
static uint8_t power_battery_sample_ready(uint16_t dt, uint16_t *out_mv);
|
||
static void power_battery_update_percent(uint16_t mv);
|
||
static void power_battery_check_empty(void);
|
||
static void power_battery_check_low(uint16_t mv);
|
||
static void power_battery_periodic_log(uint16_t dt);
|
||
|
||
/**
|
||
* @brief 初始化电源模块状态
|
||
* @return 无
|
||
*/
|
||
void app_power_init(void)
|
||
{
|
||
memset(&s_power, 0, sizeof(s_power));
|
||
s_power.charge.charge_led_last = 0xff;
|
||
app_battery_init();
|
||
}
|
||
|
||
/**
|
||
* @brief 注册深睡请求的两段式回调
|
||
* @param on_request 刚请求深睡时调用(停电机;灯效由调用方按关机类型决定)
|
||
* @param on_finalize 延时到点后调用(真正进深睡,不会返回)
|
||
* @return 无
|
||
*/
|
||
void app_power_set_hooks(AppPowerHook_t on_request, AppPowerHook_t on_finalize)
|
||
{
|
||
s_power.on_request = on_request;
|
||
s_power.on_finalize = on_finalize;
|
||
}
|
||
|
||
/**
|
||
* @brief 充电检测 + 电量采样,每个业务 tick 调用一次
|
||
* @param dt 距上次调用的毫秒数
|
||
* @return USB 插拔边沿事件,多数时候是 APP_POWER_EVT_NONE
|
||
*/
|
||
AppPowerEvt_t app_power_tick_1ms(uint16_t dt)
|
||
{
|
||
AppPowerEvt_t evt = power_charge_tick(dt);
|
||
|
||
power_battery_tick(dt);
|
||
return evt;
|
||
}
|
||
|
||
/**
|
||
* @brief 查询 USB 是否在位(含正在充电)
|
||
* @return 1=在位,0=未接
|
||
*/
|
||
uint8_t app_power_usb_online(void)
|
||
{
|
||
return s_power.charge.usb_online;
|
||
}
|
||
|
||
/**
|
||
* @brief 查询充电灯状态
|
||
* @return 0=无 1=充电中红灯 2=已充满绿灯
|
||
*/
|
||
uint8_t app_power_charge_led(void)
|
||
{
|
||
return s_power.charge.charge_led;
|
||
}
|
||
|
||
/**
|
||
* @brief 查询是否处于低电告警
|
||
* @return 1=低电,0=正常
|
||
*/
|
||
uint8_t app_power_low_bat(void)
|
||
{
|
||
return s_power.alarm.low_bat;
|
||
}
|
||
|
||
/**
|
||
* @brief 请求深睡(按键 3s 等):立即触发 on_request 钩子,50ms 后触发 on_finalize
|
||
* @return 无
|
||
*/
|
||
void app_power_request_poweroff(void)
|
||
{
|
||
if (s_power.req_poweroff) {
|
||
return; /* 已经在关机流程里,不要重复挂定时器 */
|
||
}
|
||
s_power.req_poweroff = 1;
|
||
s_power.empty_off = 0;
|
||
log_info("request deep off\n");
|
||
if (s_power.on_request) {
|
||
s_power.on_request(); /* 立刻停电机、灯全灭 */
|
||
}
|
||
sys_timeout_add(NULL, power_do_poweroff, 50); /* 给 GPIO 落稳一点再真正掉电 */
|
||
}
|
||
|
||
/**
|
||
* @brief 空电关机:立刻停电机,等红灯闪完 5 下后再真正掉电
|
||
* @return 无
|
||
*/
|
||
void app_power_request_empty_poweroff(void)
|
||
{
|
||
if (s_power.req_poweroff) {
|
||
return;
|
||
}
|
||
s_power.req_poweroff = 1;
|
||
s_power.empty_off = 1;
|
||
log_info("request empty off, wait %ums\n", (unsigned)BOARD_LED_EMPTY_OFF_WAIT_MS);
|
||
if (s_power.on_request) {
|
||
s_power.on_request(); /* 立刻停电机,调用方挂空电红闪 */
|
||
}
|
||
sys_timeout_add(NULL, power_do_poweroff, BOARD_LED_EMPTY_OFF_WAIT_MS);
|
||
}
|
||
|
||
/**
|
||
* @brief 查询深睡请求是否已发起
|
||
* @return 1=已发起,0=未发起
|
||
*/
|
||
uint8_t app_power_poweroff_pending(void)
|
||
{
|
||
return s_power.req_poweroff;
|
||
}
|
||
|
||
/**
|
||
* @brief 当前这次关机是否为空电路径
|
||
* @return 1=空电关机,0=普通关机
|
||
*/
|
||
uint8_t app_power_empty_off_pending(void)
|
||
{
|
||
return s_power.empty_off;
|
||
}
|
||
|
||
/**
|
||
* @brief 50ms 延时到点:真正进入深睡
|
||
* @param priv 未使用
|
||
* @return 无
|
||
*/
|
||
static void power_do_poweroff(void *priv)
|
||
{
|
||
(void)priv;
|
||
log_info("enter deep off\n");
|
||
if (s_power.on_finalize) {
|
||
s_power.on_finalize();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief TP4056 CHRG/STDBY 组合检测,稳定状态变化后报告 USB 事件
|
||
* @param dt 距上次调用的毫秒数
|
||
* @return USB 插拔边沿事件
|
||
*/
|
||
static AppPowerEvt_t power_charge_tick(uint16_t dt)
|
||
{
|
||
AppChargeState_t *c = &s_power.charge;
|
||
uint8_t chg_low = bsp_chg_is_low();
|
||
uint8_t stby_low = bsp_stby_is_low();
|
||
uint8_t usb = c->usb_online;
|
||
AppPowerEvt_t evt = APP_POWER_EVT_NONE;
|
||
|
||
if (chg_low && stby_low) {
|
||
/* 00 is invalid: keep the previous stable state. */
|
||
c->charging_ms = 0;
|
||
c->unplugged_ms = 0;
|
||
c->full_ms = 0;
|
||
c->full_recheck_ms = 0;
|
||
} else if (!chg_low && !stby_low) {
|
||
/* 11: charger absent. */
|
||
c->charging_ms = 0;
|
||
c->full_ms = 0;
|
||
c->full_recheck_ms = 0;
|
||
c->unplugged_ms = app_add_ms(c->unplugged_ms, dt);
|
||
if (c->unplugged_ms >= CHG_OFF_CONFIRM_MS) {
|
||
c->charge_led = 0;
|
||
c->full_latched = 0;
|
||
usb = 0;
|
||
}
|
||
} else if (chg_low) {
|
||
/* 01: charging. */
|
||
c->unplugged_ms = 0;
|
||
c->full_ms = 0;
|
||
if (c->full_latched) {
|
||
c->charging_ms = 0;
|
||
c->full_recheck_ms = app_add_ms(c->full_recheck_ms, dt);
|
||
if (c->full_recheck_ms >= CHG_FULL_RECHECK_MS) {
|
||
c->full_latched = 0;
|
||
c->charge_led = 1;
|
||
}
|
||
usb = 1;
|
||
} else {
|
||
c->full_recheck_ms = 0;
|
||
c->charging_ms = app_add_ms(c->charging_ms, dt);
|
||
if (c->charging_ms >= CHG_ON_CONFIRM_MS) {
|
||
c->charge_led = 1;
|
||
usb = 1;
|
||
}
|
||
}
|
||
} else {
|
||
/* 10: charge complete. */
|
||
c->charging_ms = 0;
|
||
c->unplugged_ms = 0;
|
||
c->full_recheck_ms = 0;
|
||
if (c->full_latched) {
|
||
c->charge_led = 2;
|
||
usb = 1;
|
||
} else {
|
||
c->full_ms = app_add_ms(c->full_ms, dt);
|
||
if (c->full_ms >= BOARD_CHG_FULL_MS) {
|
||
c->charge_led = 2;
|
||
c->full_latched = 1;
|
||
usb = 1;
|
||
}
|
||
}
|
||
}
|
||
|
||
gDevData.charge = (c->charge_led == 1) ? CHARGE_1 : CHARGE_2;
|
||
|
||
if (usb != c->usb_online) {
|
||
c->usb_online = usb;
|
||
log_info("charge input %s chg_low=%d stby_low=%d pair=%d goto=%d\n",
|
||
usb ? "in" : "out", chg_low, stby_low,
|
||
usr_get_pair_flag(), usr_var.usr_goto_pair);
|
||
usr_var.usr_power_charge_flag = usb ? 1 : 2;
|
||
if (usb) {
|
||
evt = APP_POWER_EVT_USB_IN;
|
||
} else {
|
||
ui_update_status(STATUS_NORMAL_POWER);
|
||
evt = APP_POWER_EVT_USB_OUT;
|
||
}
|
||
}
|
||
|
||
if (c->charge_led != c->charge_led_last) {
|
||
c->charge_led_last = c->charge_led;
|
||
log_info("charge_led=%d chg_low=%d stby_low=%d\n",
|
||
c->charge_led, chg_low, stby_low);
|
||
if (c->charge_led == 2) {
|
||
/* STDBY 持续为低满 BOARD_CHG_FULL_MS 后判定充满,电量强制收尾到
|
||
* 100%,避免电压拟合曲线因基准/温漂等原因走不到 100% 就卡住 */
|
||
app_battery_set_percent(100);
|
||
s_power.sample.pct = 100;
|
||
gDevData.battery = 100;
|
||
}
|
||
}
|
||
|
||
return evt;
|
||
}
|
||
|
||
/**
|
||
* @brief 滑动平均采样:每 BAT_SAMPLE_MS 采一次电压,凑够 BAT_AVG_N 个才出新均值
|
||
* @note 靠采样点数(而非时间)判满,dt 波动也不会多算/少算样本
|
||
* @param dt 距上次调用的毫秒数
|
||
* @param out_mv 新均值输出,仅在返回 1 时有效
|
||
* @return 1=本次凑出新均值,0=还在累积
|
||
*/
|
||
static uint8_t power_battery_sample_ready(uint16_t dt, uint16_t *out_mv)
|
||
{
|
||
AppBatterySample_t *s = &s_power.sample;
|
||
|
||
s->tick_ms = app_add_ms(s->tick_ms, dt);
|
||
if (s->tick_ms < BAT_SAMPLE_MS) {
|
||
return 0; /* 还没到采样点 */
|
||
}
|
||
s->tick_ms = 0;
|
||
s->acc_mv += bsp_vbat_mv();
|
||
s->n++;
|
||
if (s->n < BAT_AVG_N) {
|
||
return 0; /* 样本还不够,继续累加 */
|
||
}
|
||
|
||
*out_mv = (uint16_t)(s->acc_mv / s->n);
|
||
s->acc_mv = 0;
|
||
s->n = 0;
|
||
s->mv = *out_mv;
|
||
return 1;
|
||
}
|
||
|
||
/**
|
||
* @brief 喂给电量算法模块(防抖/充放电基准/缓冲平滑见 app_battery.c),
|
||
* 仅在结果变化时才写协议层数据
|
||
* @param mv 本次均值电压,单位 mV
|
||
* @return 无
|
||
*/
|
||
static void power_battery_update_percent(uint16_t mv)
|
||
{
|
||
uint8_t pct;
|
||
|
||
app_battery_tick(BAT_UPDATE_MS, mv, s_power.charge.usb_online);
|
||
pct = app_battery_get_percent();
|
||
if (pct != s_power.sample.pct) {
|
||
s_power.sample.pct = pct;
|
||
gDevData.battery = pct;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief 空电关机判定:显示电量一到 <= 5% 立刻停电机并请求空电关机
|
||
* @note 充电中不判定。百分比已经在 app_battery 做过滤波/缓冲,这里不再做连续次数窗口。
|
||
* 开机拦截(app_boot)仍用 BOARD_VBAT_SHUTDOWN_MV,因为那时电量算法还没跑。
|
||
* @return 无
|
||
*/
|
||
static void power_battery_check_empty(void)
|
||
{
|
||
uint8_t pct = s_power.sample.pct;
|
||
|
||
if (s_power.charge.usb_online || pct > BOARD_BATTERY_SHUTDOWN_PCT) {
|
||
return;
|
||
}
|
||
s_power.alarm.low_bat = 1;
|
||
gDevData.alm_low_battery = 1;
|
||
if (!s_power.req_poweroff) {
|
||
log_info("battery %d%% <=%d%%, stop motor + blink red then off\n",
|
||
pct, BOARD_BATTERY_SHUTDOWN_PCT);
|
||
app_power_request_empty_poweroff();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief 低电迟滞判定:进入/退出各自要求连续 BAT_LOW_WIN 次采样,避免电压在门限附近抖动
|
||
* @param mv 本次均值电压,单位 mV
|
||
* @return 无
|
||
*/
|
||
static void power_battery_check_low(uint16_t mv)
|
||
{
|
||
AppBatteryAlarm_t *a = &s_power.alarm;
|
||
|
||
if (mv < BOARD_VBAT_LOW_MV) {
|
||
a->exit_win = 0;
|
||
if (a->low_bat || a->enter_win >= 0xFF) {
|
||
return; /* 已经在低电,或窗口已饱和,不用再加 */
|
||
}
|
||
a->enter_win++;
|
||
if (a->enter_win >= BAT_LOW_WIN) {
|
||
a->low_bat = 1; /* 连续够次数才进低电,只闪灯不关机 */
|
||
gDevData.alm_low_battery = 1;
|
||
log_info("low battery %dmV %d%%, blink only, no auto off\n", mv, s_power.sample.pct);
|
||
}
|
||
} else if (mv > (BOARD_VBAT_LOW_MV + BOARD_VBAT_LOW_HYST_MV)) {
|
||
a->enter_win = 0;
|
||
if (!a->low_bat) {
|
||
return;
|
||
}
|
||
if (a->exit_win < 0xFF) {
|
||
a->exit_win++;
|
||
}
|
||
if (a->exit_win >= BAT_LOW_WIN) {
|
||
a->low_bat = 0;
|
||
gDevData.alm_low_battery = 0;
|
||
log_info("battery recover %dmV %d%%\n", mv, s_power.sample.pct);
|
||
}
|
||
} else {
|
||
/* 低电门限与迟滞上限之间的死区,两个窗口都不动 */
|
||
a->enter_win = 0;
|
||
a->exit_win = 0;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief 每 BAT_LOG_MS 打印一次电量状态,方便离线看日志排查
|
||
* @param dt 距上次调用的毫秒数
|
||
* @return 无
|
||
*/
|
||
static void power_battery_periodic_log(uint16_t dt)
|
||
{
|
||
#if BAT_PERIODIC_LOG_ENABLE
|
||
AppBatterySample_t *s = &s_power.sample;
|
||
|
||
s->log_ms = app_add_ms(s->log_ms, dt);
|
||
if (s->log_ms >= BAT_LOG_MS) {
|
||
s->log_ms = 0;
|
||
log_info("bat %d%% %dmV alm=%d\n", s->pct, s->mv, s_power.alarm.low_bat);
|
||
}
|
||
#else
|
||
(void)dt;
|
||
#endif
|
||
}
|
||
|
||
/**
|
||
* @brief 电量采样与低电策略,每个业务 tick 调用一次
|
||
* @note 显示用的百分比(滤波/充放电基准/突变防抖/缓冲平滑)交给 app_battery
|
||
* 模块处理。check_low 仍按原始电压做 3.7V 红灯提示;check_empty
|
||
* 按显示电量 <= BOARD_BATTERY_SHUTDOWN_PCT(5%)关机,跟 APP 看到
|
||
* 的数字对齐,避免电压先掉、百分比还没到 5% 就关机。
|
||
* @param dt 距上次调用的毫秒数
|
||
* @return 无
|
||
*/
|
||
static void power_battery_tick(uint16_t dt)
|
||
{
|
||
AppBatterySample_t *s = &s_power.sample;
|
||
uint16_t mv;
|
||
|
||
if (s->boot_ms < BAT_BOOT_MS) {
|
||
s->boot_ms = app_add_ms(s->boot_ms, dt);
|
||
}
|
||
|
||
if (power_battery_sample_ready(dt, &mv)) {
|
||
power_battery_update_percent(mv);
|
||
/* 开机保护期内只更新百分比,不判定任何告警 */
|
||
if (s->boot_ms >= BAT_BOOT_MS) {
|
||
power_battery_check_empty();
|
||
power_battery_check_low(mv);
|
||
}
|
||
}
|
||
|
||
power_battery_periodic_log(dt);
|
||
}
|