Files
JBao_Magic_Box_FW/apps/usr_app/app_power.c
T
2026-08-25 14:59:39 +08:00

454 lines
12 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.25
* @version V1.0.0
* @history
* - V1.0.0, 2026.08.25, cyWu, 从 usr_jb_main 拆出
******************************************************************************/
#include "app_power.h"
#include "app_time_util.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_BOOT_MS 3000 /* 上电 3s 内不因低电深睡 */
#define BAT_LOG_MS 10000
#define BAT_LOW_WIN 3 /* 连续约 2.4s 才进低电告警 */
#define BAT_EMPTY_WIN 5 /* 连续约 4s 才 3.2V 深睡 */
/** 充电检测: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;
/** 低电告警:进入/退出都要连续窗口消抖,避免电机拉压降误报 */
typedef struct {
uint8_t low_bat;
uint32_t low_ms; /**< 低电已持续时长,满 1 分钟深睡 */
uint8_t empty_win;
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;
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_bat_percent(uint16_t mv);
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(uint16_t mv);
static void power_battery_check_low(uint16_t mv);
static void power_battery_check_low_timeout(uint16_t dt);
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;
}
/**
* @brief 注册深睡请求的两段式回调
* @param on_request 刚请求深睡时调用(停电机、灯全灭)
* @param on_finalize 50ms 后调用(真正进深睡,不会返回)
* @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 请求深睡:立即触发 on_request 钩子,50ms 后触发 on_finalize
* @return 无
*/
void app_power_request_poweroff(void)
{
if (s_power.req_poweroff) {
return;
}
s_power.req_poweroff = 1;
log_info("request deep off\n");
if (s_power.on_request) {
s_power.on_request();
}
sys_timeout_add(NULL, power_do_poweroff, 50);
}
/**
* @brief 查询深睡请求是否已发起
* @return 1=已发起,0=未发起
*/
uint8_t app_power_poweroff_pending(void)
{
return s_power.req_poweroff;
}
/**
* @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) {
c->full_ms = 0;
c->charge_led = 1;
gDevData.charge = CHARGE_1;
usb = 1;
} else if (vpwr) {
/* 拔电时 CHG 先变高、VPWR 还没掉,不能立刻当充满 */
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 {
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());
}
return evt;
}
/**
* @brief 电压换算电量:4.2V=100%3.2V=0%
* @param mv 电池电压,单位 mV
* @return 电量百分比,范围 0~100
*/
static uint8_t power_bat_percent(uint16_t mv)
{
int32_t span = (int32_t)BOARD_VBAT_FULL_MV - (int32_t)BOARD_VBAT_EMPTY_MV;
int32_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 / span;
if (pct < 0) {
pct = 0;
}
if (pct > 100) {
pct = 100;
}
return (uint8_t)pct;
}
/**
* @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 按新均值刷新电量百分比,仅在变化时才写协议层数据
* @param mv 本次均值电压,单位 mV
* @return 无
*/
static void power_battery_update_percent(uint16_t mv)
{
uint8_t pct = power_bat_percent(mv);
if (pct != s_power.sample.pct) {
s_power.sample.pct = pct;
gDevData.battery = pct;
}
}
/**
* @brief 电量耗尽判定:连续 BAT_EMPTY_WIN 次采样都 <= EMPTY_MV 才认定,直接请求深睡
* @note 充电中不判定;用连续窗口而非单次采样,避免电机拉压降瞬间跌破误报
* @param mv 本次均值电压,单位 mV
* @return 无
*/
static void power_battery_check_empty(uint16_t mv)
{
AppBatteryAlarm_t *a = &s_power.alarm;
if (s_power.charge.usb_online || mv > BOARD_VBAT_EMPTY_MV) {
a->empty_win = 0;
return;
}
if (a->empty_win < 0xFF) {
a->empty_win++;
}
if (a->empty_win < BAT_EMPTY_WIN) {
return;
}
a->low_bat = 1;
gDevData.alm_low_battery = 1;
if (!s_power.req_poweroff) {
log_info("vbat %dmV empty, deep off\n", mv);
app_power_request_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;
a->low_ms = 0;
gDevData.alm_low_battery = 1;
log_info("low battery %dmV %d%%\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;
a->low_ms = 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 低电持续满 BOARD_LOWBAT_SLEEP_MS1 分钟)才请求深睡
* @note 充电中 / 已请求深睡 / 仍在开机保护期内,计时清零不累加
* @param dt 距上次调用的毫秒数
* @return 无
*/
static void power_battery_check_low_timeout(uint16_t dt)
{
AppBatteryAlarm_t *a = &s_power.alarm;
if (s_power.charge.usb_online || s_power.req_poweroff
|| (s_power.sample.boot_ms < BAT_BOOT_MS)) {
a->low_ms = 0;
return;
}
if (!a->low_bat || a->low_ms >= BOARD_LOWBAT_SLEEP_MS) {
return;
}
a->low_ms += dt;
if (a->low_ms >= BOARD_LOWBAT_SLEEP_MS) {
a->low_ms = BOARD_LOWBAT_SLEEP_MS;
log_info("low battery 1min, deep off\n");
app_power_request_poweroff();
}
}
/**
* @brief 每 BAT_LOG_MS 打印一次电量状态,方便离线看日志排查
* @param dt 距上次调用的毫秒数
* @return 无
*/
static void power_battery_periodic_log(uint16_t dt)
{
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);
}
}
/**
* @brief 电量采样与低电策略,每个业务 tick 调用一次
* @note 电机运转时只记电压、不改百分比/告警,避免电流拉垮 VBAT 误报。
* 电机是否忙由调用方(app_function)决定,本模块不认识电机,
* 这里用「外部传入」而非直接查询,保持单向依赖。
* @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(mv);
power_battery_check_low(mv);
}
}
power_battery_check_low_timeout(dt);
power_battery_periodic_log(dt);
}