Files
JBao_Magic_Box_FW/apps/usr_app/app_power.c
T

447 lines
14 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/******************************************************************************
* @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 BAT_LOW_WIN 3 /* 连续约 2.4s 才进低电告警(仅闪灯,不关机) */
/** 充电检测:CHG/VPWR 消抖后的结果 */
typedef struct {
uint8_t usb_online;
uint8_t charge_led; /**< 0=无 1=充电红 2=充满绿 */
uint8_t charge_led_last; /**< 仅用于变化时打日志 */
uint16_t full_ms; /**< 拔电瞬间 CHG 先变高,需等满 1s 才当充满 */
} 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_PCT5%)时立刻触发 */
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 充电检测:CHG 低=充电中;VPWR 在且 CHG 高满 1s=充满;拔电报 USB_OUT
* @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 vpwr = bsp_vpwr_is_online();
uint8_t usb;
AppPowerEvt_t evt = APP_POWER_EVT_NONE;
if (chg_low) {
/* CHG 拉低:正在充电,红灯 */
c->full_ms = 0;
c->charge_led = 1;
gDevData.charge = CHARGE_1;
usb = 1;
} else if (vpwr) {
/* VPWR 在但 CHG 已高:可能是充满,也可能是拔电瞬间 CHG 先变高。
* 等满 BOARD_CHG_FULL_MS 才当真充满,避免拔电误报绿灯 */
c->full_ms = app_add_ms(c->full_ms, dt);
if (c->full_ms >= BOARD_CHG_FULL_MS) {
c->charge_led = 2;
}
gDevData.charge = (c->charge_led == 1) ? CHARGE_1 : CHARGE_2;
usb = 1;
} else {
/* USB 不在位:充电灯灭 */
c->full_ms = 0;
c->charge_led = 0;
gDevData.charge = CHARGE_2;
usb = 0;
}
if (usb != c->usb_online) {
c->usb_online = usb;
log_info("usb %s vpwr=%d chg=%d mv=%d pair=%d goto=%d\n",
usb ? "in" : "out", vpwr, chg_low, bsp_vpwr_mv(),
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=%d vpwr=%d mv=%d\n",
c->charge_led, chg_low, vpwr, bsp_vpwr_mv());
if (c->charge_led == 2) {
/* 硬件(CHG+VPWR 满 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_PCT5%)关机,跟 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);
}