1、低电 3.7V 只闪红灯不再 1 分钟后关机;电量低于约 5% 自动关机,未充电开机先闪 5 下红灯再睡回,只能充电后才能开机

2、自动/手动互相切换都闪 2 下灯;APP 发停止后再发自动模式可正常启动,手动残留停止不再挡住自动
3、自动模式 1~6 单次最长跑 10 分钟后回待机;关机时长按 12 秒进 OTA,OTA 期间红绿交替闪
This commit is contained in:
2026-08-27 10:20:48 +08:00
parent 3d445ae5c5
commit fdb43011b7
28 changed files with 945 additions and 165 deletions
+52 -85
View File
@@ -2,14 +2,18 @@
* @file app_power.c
* @brief USB / 充电检测、电量采样与低电策略、深睡请求生命周期实现
* @author cyWu <1917507415@qq.com>
* @date 2026.08.25
* @version V1.0.0
* @date 2026.08.26
* @version V1.1.0
* @history
* - V1.0.0, 2026.08.25, cyWu, 从 usr_jb_main 拆出
* - V1.0.0, 2026.08.25, cyWu, 首次发布,实现 USB/充电检测、电量采样与深睡请求
* - V1.1.0, 2026.08.26, cyWu, 取消低电闪烁 1 分钟后自动关机;自动关机门限
* 改为约 5%BOARD_VBAT_SHUTDOWN_MV),与开机
* 低电拦截(app_boot)共用同一门限
******************************************************************************/
#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"
@@ -26,10 +30,11 @@
#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_LOW_WIN 3 /* 连续约 2.4s 才进低电告警 */
#define BAT_EMPTY_WIN 5 /* 连续约 4s 才 3.2V 深睡 */
#define BAT_LOW_WIN 3 /* 连续约 2.4s 才进低电告警(仅闪灯,不关机) */
#define BAT_EMPTY_WIN 5 /* 连续约 4s 才判定 <=5%,自动关机 */
/** 充电检测:CHG/VPWR 消抖后的结果 */
typedef struct {
@@ -50,10 +55,12 @@ typedef struct {
uint16_t log_ms;
} AppBatterySample_t;
/** 低电告警:进入/退出都要连续窗口消抖,避免电机拉压降误报 */
/** 低电告警:进入/退出都要连续窗口消抖,避免电机拉压降误报
* @note 低电(BOARD_VBAT_LOW_MV)只负责红灯闪烁提示,不再有超时自动关机;
* 真正的自动关机改由 power_battery_check_empty 在电量约 5%
* BOARD_VBAT_SHUTDOWN_MV)时触发 */
typedef struct {
uint8_t low_bat;
uint32_t low_ms; /**< 低电已持续时长,满 1 分钟深睡 */
uint8_t empty_win;
uint8_t enter_win;
uint8_t exit_win;
@@ -74,12 +81,10 @@ 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);
/**
@@ -90,6 +95,7 @@ void app_power_init(void)
{
memset(&s_power, 0, sizeof(s_power));
s_power.charge.charge_led_last = 0xff;
app_battery_init();
}
/**
@@ -151,14 +157,14 @@ uint8_t app_power_low_bat(void)
void app_power_request_poweroff(void)
{
if (s_power.req_poweroff) {
return;
return; /* 已经在关机流程里,不要重复挂 50ms 定时器 */
}
s_power.req_poweroff = 1;
log_info("request deep off\n");
if (s_power.on_request) {
s_power.on_request();
s_power.on_request(); /* 立刻停电机、灯全灭 */
}
sys_timeout_add(NULL, power_do_poweroff, 50);
sys_timeout_add(NULL, power_do_poweroff, 50); /* 给 GPIO 落稳一点再真正掉电 */
}
/**
@@ -198,12 +204,14 @@ static AppPowerEvt_t power_charge_tick(uint16_t dt)
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) {
/* 拔电时 CHG 先变高、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;
@@ -211,6 +219,7 @@ static AppPowerEvt_t power_charge_tick(uint16_t dt)
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;
@@ -235,37 +244,18 @@ static AppPowerEvt_t power_charge_tick(uint16_t dt)
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 电压换算电量: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 波动也不会多算/少算样本
@@ -279,13 +269,13 @@ static uint8_t power_battery_sample_ready(uint16_t dt, uint16_t *out_mv)
s->tick_ms = app_add_ms(s->tick_ms, dt);
if (s->tick_ms < BAT_SAMPLE_MS) {
return 0;
return 0; /* 还没到采样点 */
}
s->tick_ms = 0;
s->acc_mv += bsp_vbat_mv();
s->n++;
if (s->n < BAT_AVG_N) {
return 0;
return 0; /* 样本还不够,继续累加 */
}
*out_mv = (uint16_t)(s->acc_mv / s->n);
@@ -296,14 +286,17 @@ static uint8_t power_battery_sample_ready(uint16_t dt, uint16_t *out_mv)
}
/**
* @brief 按新均值刷新电量百分比,仅在变化时才写协议层数据
* @brief 喂给电量算法模块(防抖/充放电基准/缓冲平滑见 app_battery.c),
* 仅在结果变化时才写协议层数据
* @param mv 本次均值电压,单位 mV
* @return 无
*/
static void power_battery_update_percent(uint16_t mv)
{
uint8_t pct = power_bat_percent(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;
@@ -311,8 +304,9 @@ static void power_battery_update_percent(uint16_t mv)
}
/**
* @brief 电量耗尽判定:连续 BAT_EMPTY_WIN 次采样都 <= EMPTY_MV 才认定,直接请求深睡
* @note 充电中不判定;用连续窗口而非单次采样,避免电机拉压降瞬间跌破误报
* @brief 低电自动关机判定:连续 BAT_EMPTY_WIN 次采样都 <= SHUTDOWN_MV(约 5%才认定,直接请求深睡
* @note 充电中不判定;用连续窗口而非单次采样,避免电机拉压降瞬间跌破误报
* 对应“电量低于 5% 自动关机”,与开机时的低电拦截(app_boot)用同一门限。
* @param mv 本次均值电压,单位 mV
* @return 无
*/
@@ -320,20 +314,20 @@ 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;
if (s_power.charge.usb_online || mv > BOARD_VBAT_SHUTDOWN_MV) {
a->empty_win = 0; /* 充电中或电压还够,窗口清零 */
return;
}
if (a->empty_win < 0xFF) {
a->empty_win++;
}
if (a->empty_win < BAT_EMPTY_WIN) {
return;
return; /* 还没连续够次数,可能是电机拉压降的瞬间 */
}
a->low_bat = 1;
gDevData.alm_low_battery = 1;
if (!s_power.req_poweroff) {
log_info("vbat %dmV empty, deep off\n", mv);
log_info("vbat %dmV <=5%%, deep off\n", mv);
app_power_request_poweroff();
}
}
@@ -350,14 +344,13 @@ static void power_battery_check_low(uint16_t mv)
if (mv < BOARD_VBAT_LOW_MV) {
a->exit_win = 0;
if (a->low_bat || a->enter_win >= 0xFF) {
return;
return; /* 已经在低电,或窗口已饱和,不用再加 */
}
a->enter_win++;
if (a->enter_win >= BAT_LOW_WIN) {
a->low_bat = 1;
a->low_ms = 0;
a->low_bat = 1; /* 连续够次数才进低电,只闪灯不关机 */
gDevData.alm_low_battery = 1;
log_info("low battery %dmV %d%%\n", mv, s_power.sample.pct);
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;
@@ -369,7 +362,6 @@ static void power_battery_check_low(uint16_t mv)
}
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);
}
@@ -380,32 +372,6 @@ static void power_battery_check_low(uint16_t mv)
}
}
/**
* @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 距上次调用的毫秒数
@@ -424,9 +390,11 @@ static void power_battery_periodic_log(uint16_t dt)
/**
* @brief 电量采样与低电策略,每个业务 tick 调用一次
* @note 电机运转时只记电压、不改百分比/告警,避免电流拉垮 VBAT 误报。
* 电机是否忙由调用方(app_function)决定,本模块不认识电机,
* 这里用「外部传入」而非直接查询,保持单向依赖
* @note 显示用的百分比(滤波/充放电基准/突变防抖/缓冲平滑)交给 app_battery
* 模块处理;这里的 check_empty/check_low 是另一套基于原始电压的
* 低电告警迟滞,直接判 mv 阈值,跟 app_battery 各管一段、互不干扰
* check_low(3.7V)只负责红灯闪烁提示,不再有超时自动关机;
* check_empty(约 5%/3.25V)才是真正触发自动关机的判定。
* @param dt 距上次调用的毫秒数
* @return 无
*/
@@ -447,7 +415,6 @@ static void power_battery_tick(uint16_t dt)
power_battery_check_low(mv);
}
}
power_battery_check_low_timeout(dt);
power_battery_periodic_log(dt);
}