feat(power): low-battery tip screen and auto power-off below 6.2V

Stop all sound, show UI0902 LOWBAT_TIP fullscreen, then soft-off after 10s. Skip while charging; speed up ADC sampling near empty. Pack new tip bitmap into ExtFlash UI0902 map.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
yuquanjun 2026-09-21 17:53:13 +08:00
parent f740b6fba0
commit 1b1b9b196d
13 changed files with 135 additions and 14 deletions

View File

@ -65,6 +65,18 @@ uint8_t percentage = 0;
//static uint32_t charge_step_tick = 0; //static uint32_t charge_step_tick = 0;
#define VBAT_MIN_RAW 2520 #define VBAT_MIN_RAW 2520
#define VBAT_MAX_RAW 3528 #define VBAT_MAX_RAW 3528
/* 包电压标定:MIN≈6.0V(0%) … MAX≈8.4V(100%);关机阈值 6.2V */
#define VBAT_PACK_V_MIN_MV 6000
#define VBAT_PACK_V_MAX_MV 8400
#define VBAT_SHUTDOWN_MV 6200
#define VBAT_SHUTDOWN_RAW \
(VBAT_MIN_RAW + (uint32_t)(VBAT_SHUTDOWN_MV - VBAT_PACK_V_MIN_MV) * \
(VBAT_MAX_RAW - VBAT_MIN_RAW) / (VBAT_PACK_V_MAX_MV - VBAT_PACK_V_MIN_MV))
/* 接近关机阈值时加快采样(约 <6.5V) */
#define VBAT_FAST_SAMPLE_RAW \
(VBAT_MIN_RAW + (uint32_t)(500) * (VBAT_MAX_RAW - VBAT_MIN_RAW) / \
(VBAT_PACK_V_MAX_MV - VBAT_PACK_V_MIN_MV))
#define BAT_SAMPLE_INTERVAL_LOW_MS 2000
@ -72,10 +84,14 @@ uint8_t percentage = 0;
static void app_adc_string_process(uint8_t channel) static void app_adc_string_process(uint8_t channel)
{ {
uint16_t adc_val;
uint16_t adc_val = ADC_ReadChannel(ADC_CHANNEL_0) >> 5;
uint8_t vel_out = 80; uint8_t vel_out = 80;
if (System_LowBat_IsActive())
return;
adc_val = ADC_ReadChannel(ADC_CHANNEL_0) >> 5;
if (adc_val >= 127) { if (adc_val >= 127) {
/* δ<><CEB4><EFBFBD><EFBFBD> <20><> Note Off */ /* δ<><CEB4><EFBFBD><EFBFBD> <20><> Note Off */
if (string_state[channel]) { if (string_state[channel]) {
@ -114,10 +130,16 @@ pick_state_t app_adc_get_pick_input(void)
static void app_adc_pick_process(void) static void app_adc_pick_process(void)
{ {
pick_state_t curstate = app_adc_get_pick_input(); pick_state_t curstate;
uint32_t now;
uint32_t now = rt_tick_get();
uint32_t hold_tick; uint32_t hold_tick;
if (System_LowBat_IsActive())
return;
curstate = app_adc_get_pick_input();
now = rt_tick_get();
switch(pick_fsm) switch(pick_fsm)
{ {
case PICK_FSM_STATE_IDLE: case PICK_FSM_STATE_IDLE:
@ -368,7 +390,7 @@ static void app_adc_battery_do_sample(void)
bat_index = (bat_index + 1) % BAT_SAMPLE_COUNT; bat_index = (bat_index + 1) % BAT_SAMPLE_COUNT;
} }
/* 上报:映射成百分比,变化才发消息 */ /* 上报:映射成百分比,变化才发消息;低于 6.2V 触发低电量关机流程 */
static void app_adc_battery_report(void) static void app_adc_battery_report(void)
{ {
uint16_t filtered = bat_sum / BAT_SAMPLE_COUNT; uint16_t filtered = bat_sum / BAT_SAMPLE_COUNT;
@ -382,6 +404,15 @@ static void app_adc_battery_report(void)
pct = 100 * (filtered - VBAT_MIN_RAW) / (VBAT_MAX_RAW - VBAT_MIN_RAW); pct = 100 * (filtered - VBAT_MIN_RAW) / (VBAT_MAX_RAW - VBAT_MIN_RAW);
pct = (pct / 10) * 10; pct = (pct / 10) * 10;
/* 开机且未充电:包电压低于 6.2V → 立即停声并弹关机提示(只触发一次) */
if (powon && !usb_charging_state && !System_LowBat_IsActive() &&
filtered < (uint16_t)VBAT_SHUTDOWN_RAW)
{
LOG_W("BAT", "lowbat raw=%u thr=%u → tip+shutdown",
(unsigned)filtered, (unsigned)VBAT_SHUTDOWN_RAW);
MainTask_Sendmsg(MSG_ID_LOW_BAT_TIP, pct, filtered, 0);
}
if (pct != percentage) { /* 变化才上报 */ if (pct != percentage) { /* 变化才上报 */
percentage = pct; percentage = pct;
if(usb_charging_state && !powon) if(usb_charging_state && !powon)
@ -404,14 +435,19 @@ static void app_adc_battery_process(void)
switch (bat_state) switch (bat_state)
{ {
case BAT_IDLE: case BAT_IDLE:
/* 触发:开机后首采,或距上次 ≥30s */ /* 触发:开机后首采,或距上次 ≥30s(接近低电时改为 2s) */
if (/*powon && */!bat_poweron_done) if (/*powon && */!bat_poweron_done)
{ {
bat_poweron_done = 1; bat_poweron_done = 1;
bat_state = BAT_START; bat_state = BAT_START;
} }
else if (now - bat_last_sample_tick >= BAT_SAMPLE_INTERVAL_MS) else
{ {
uint32_t interval = BAT_SAMPLE_INTERVAL_MS;
uint16_t last_avg = bat_sum / BAT_SAMPLE_COUNT;
if (last_avg != 0 && last_avg < (uint16_t)VBAT_FAST_SAMPLE_RAW)
interval = BAT_SAMPLE_INTERVAL_LOW_MS;
if (now - bat_last_sample_tick >= interval)
bat_state = BAT_START; bat_state = BAT_START;
} }
break; break;

View File

@ -184,6 +184,11 @@ static volatile uint8_t s_pwr_rails_cut = 0;
/* TMR7:关机长按已触发请求后的紧急倒计时(ms) */ /* TMR7:关机长按已触发请求后的紧急倒计时(ms) */
static volatile uint16_t s_pwr_off_grace_ms = 0; static volatile uint16_t s_pwr_off_grace_ms = 0;
/* 电量 <6.2V:提示界面停留后关机 */
#define LOWBAT_SHUTDOWN_DELAY_MS 10000u
static volatile uint8_t s_lowbat_active = 0;
static volatile uint32_t s_lowbat_deadline_ms = 0;
/* ==================== 开机/关机动作 ==================== */ /* ==================== 开机/关机动作 ==================== */
/* 仅 GPIO/背光:可在 ISR / HardFault 中调用,禁止 RTOS/Flash/日志 */ /* 仅 GPIO/背光:可在 ISR / HardFault 中调用,禁止 RTOS/Flash/日志 */
@ -214,6 +219,8 @@ void System_PowerOn(void)
app_tm1629_reset_key_state(); app_tm1629_reset_key_state();
s_pwr_rails_cut = 0u; s_pwr_rails_cut = 0u;
s_pwr_off_grace_ms = 0u; s_pwr_off_grace_ms = 0u;
s_lowbat_active = 0u;
s_lowbat_deadline_ms = 0u;
/* 本次长按已用于开机:先消耗,再置 powon,防止 TMR7 ISR 把同一按压力即判为关机 */ /* 本次长按已用于开机:先消耗,再置 powon,防止 TMR7 ISR 把同一按压力即判为关机 */
Power_Key_ConsumePress(); Power_Key_ConsumePress();
/* 控制电源硬件;蓝牙按 NVM/系统设置开关恢复,勿强制常开 */ /* 控制电源硬件;蓝牙按 NVM/系统设置开关恢复,勿强制常开 */
@ -318,6 +325,36 @@ void System_PowerOff_Poll(void)
} }
} }
/* 电量 <6.2V:提示界面停留后关机 */
void System_LowBat_Arm(void)
{
if (s_lowbat_active)
return;
s_lowbat_active = 1;
s_lowbat_deadline_ms = rt_tick_get() * 1000u / RT_TICK_PER_SECOND + LOWBAT_SHUTDOWN_DELAY_MS;
LOG_I("PWR", "lowbat tip armed, power_off in %ums", (unsigned)LOWBAT_SHUTDOWN_DELAY_MS);
}
uint8_t System_LowBat_IsActive(void)
{
return s_lowbat_active;
}
void System_LowBat_Poll(void)
{
uint32_t now;
if (!s_lowbat_active || !powon)
return;
now = rt_tick_get() * 1000u / RT_TICK_PER_SECOND;
if ((int32_t)(now - s_lowbat_deadline_ms) >= 0)
{
s_lowbat_active = 0;
LOG_I("PWR", "lowbat tip timeout → power_off");
System_RequestPowerOff();
}
}
/* /*
* TMR7 1ms(优先级高于伴奏 TMR6):与主循环解耦的关机键扫描。 * TMR7 1ms(优先级高于伴奏 TMR6):与主循环解耦的关机键扫描。
* 正常:请求优雅关机;超时未切轨 → 紧急断电并复位。 * 正常:请求优雅关机;超时未切轨 → 紧急断电并复位。

View File

@ -31,6 +31,7 @@
#define MSG_ID_CHARG 27 #define MSG_ID_CHARG 27
#define MSG_ID_BLE_UI_REFRESH 28 #define MSG_ID_BLE_UI_REFRESH 28
#define MSG_ID_BLE_STYLE_SET 29 #define MSG_ID_BLE_STYLE_SET 29
#define MSG_ID_LOW_BAT_TIP 30
#define GPIO_PIN_RESET false #define GPIO_PIN_RESET false
#define GPIO_PIN_SET true #define GPIO_PIN_SET true
@ -177,6 +178,10 @@ void AutoPowerOff_Scan(void);
void System_PowerOn(void); void System_PowerOn(void);
void System_RequestPowerOff(void); void System_RequestPowerOff(void);
void System_PowerOff_Poll(void); void System_PowerOff_Poll(void);
/* 低电量:停声+提示后 10s 关机(主循环 Poll) */
void System_LowBat_Arm(void);
void System_LowBat_Poll(void);
uint8_t System_LowBat_IsActive(void);
extern uint8_t Led; extern uint8_t Led;
extern bool PressFlag; extern bool PressFlag;

View File

@ -288,7 +288,11 @@
#define UI0902_FLASH_MODE_W 240 #define UI0902_FLASH_MODE_W 240
#define UI0902_FLASH_MODE_H 320 #define UI0902_FLASH_MODE_H 320
#define UI0902_BTN_CANCEL_BIG_ADDR (UI0902_RES_BASE + 0x0F8000UL) /* 11872 bytes */ #define UI0902_LOWBAT_TIP_ADDR (UI0902_RES_BASE + 0x0F8000UL) /* 153600 bytes */
#define UI0902_LOWBAT_TIP_W 240
#define UI0902_LOWBAT_TIP_H 320
#define UI0902_BTN_CANCEL_BIG_ADDR (UI0902_RES_BASE + 0x11E000UL) /* 11872 bytes */
#define UI0902_BTN_CANCEL_BIG_W 106 #define UI0902_BTN_CANCEL_BIG_W 106
#define UI0902_BTN_CANCEL_BIG_H 56 #define UI0902_BTN_CANCEL_BIG_H 56

View File

@ -108,6 +108,7 @@ int main(void)
Power_Key_Scan(); Power_Key_Scan();
AutoPowerOff_Scan(); AutoPowerOff_Scan();
System_PowerOff_Poll(); System_PowerOff_Poll();
System_LowBat_Poll();
LCD_Dump_Poll(); LCD_Dump_Poll();
#if !DEBUG_LCD_DUMP #if !DEBUG_LCD_DUMP
app_log_poll(); app_log_poll();

View File

@ -43,6 +43,28 @@ void TaskUIThread_entry(void* parameter)
StartFlag = 0; StartFlag = 0;
UI_ReloadTonePreset(); /* 内含 Stop_AutoBand */ UI_ReloadTonePreset(); /* 内含 Stop_AutoBand */
} }
/* 低电量提示:任意页面全屏覆盖;倒计时期间吞掉其它 UI 刷新,避免盖住提示 */
if (xEvent.MessageType == MSG_ID_LOW_BAT_TIP)
{
uint8_t peek[4];
LCD_WR_REG(0x29);
LCD_BLK_Set();
LCD_FillByColor(0, 0, 240, 320, BLACK);
W25Q128_Read(peek, UI0902_LOWBAT_TIP_ADDR, 4);
if (peek[0] != 0xFF || peek[1] != 0xFF)
{
LCD_WR_PIC_FROM_FLASH(0, 0, UI0902_LOWBAT_TIP_W, UI0902_LOWBAT_TIP_H,
UI0902_LOWBAT_TIP_ADDR);
}
else
{
LOG_W("UI", "lowbat flash empty @0x%08lX — flash ui0902",
(unsigned long)UI0902_LOWBAT_TIP_ADDR);
}
continue;
}
if (System_LowBat_IsActive())
continue;
CurrUIProcress(xEvent); CurrUIProcress(xEvent);
} }
} }
@ -84,15 +106,29 @@ void TaskMainThread_entry(void* parameter)
case MSG_ID_POWER_ON : UITask_Sendmsg(MSG_ID_POWER_ON, xEvent.ID, xEvent.HiByte, xEvent.LoByte); break; case MSG_ID_POWER_ON : UITask_Sendmsg(MSG_ID_POWER_ON, xEvent.ID, xEvent.HiByte, xEvent.LoByte); break;
case MSG_ID_POWER_OFF : break; case MSG_ID_POWER_OFF : break;
//case MSG_ID_ADCIN1KEY : ADC_IN1_KEY_Handle(xEvent.ID);break; //case MSG_ID_ADCIN1KEY : ADC_IN1_KEY_Handle(xEvent.ID);break;
case MSG_ID_BP_KEY : Pick_Handle(); break; case MSG_ID_BP_KEY:
case MSG_ID_LOOP_LIGHT : Loop_Light_Handle(xEvent.ID); break; if (!System_LowBat_IsActive())
Pick_Handle();
break;
case MSG_ID_LOOP_LIGHT:
if (!System_LowBat_IsActive())
Loop_Light_Handle(xEvent.ID);
break;
case MSG_ID_TM1629_KEY : break; case MSG_ID_TM1629_KEY : break;
case MSG_ID_TM1617_KEY : break; case MSG_ID_TM1617_KEY : break;
case MSG_ID_EC_VOL: case MSG_ID_EC_VOL:
/* 任意页面都先下发主音量,避免 UI 未处理导致屏显与实际脱节 */ /* 任意页面都先下发主音量,避免 UI 未处理导致屏显与实际脱节 */
if (!System_LowBat_IsActive())
Send_volume((uint8_t)xEvent.ID); Send_volume((uint8_t)xEvent.ID);
UITask_Sendmsg(xEvent.MessageType, xEvent.ID, xEvent.HiByte, xEvent.LoByte); UITask_Sendmsg(xEvent.MessageType, xEvent.ID, xEvent.HiByte, xEvent.LoByte);
break; break;
case MSG_ID_LOW_BAT_TIP:
/* 立即停声,再交给 UI 画提示;主循环 10s 后关机 */
Stop_AutoBand();
Send_volume(0);
System_LowBat_Arm();
UITask_Sendmsg(MSG_ID_LOW_BAT_TIP, xEvent.ID, xEvent.HiByte, xEvent.LoByte);
break;
case MSG_ID_TOUCH: case MSG_ID_TOUCH:
case MSG_ID_CHARG: case MSG_ID_CHARG:
case MSG_ID_TAP_SPEED: case MSG_ID_TAP_SPEED:

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -225,6 +225,8 @@ EXTERNAL = [
("BOOT_LOGO", "07_开机/开机界面-20.png"), ("BOOT_LOGO", "07_开机/开机界面-20.png"),
("CHARGE_SCREEN", "08_充电界面/充电界面-0902-09.png"), # 0904 最新全屏充电稿;百分比重叠由固件绘制 ("CHARGE_SCREEN", "08_充电界面/充电界面-0902-09.png"), # 0904 最新全屏充电稿;百分比重叠由固件绘制
("FLASH_MODE", "09_烧录模式/烧录模式-240x320.png"), # Boot IAP 烧录等待画面 ("FLASH_MODE", "09_烧录模式/烧录模式-240x320.png"), # Boot IAP 烧录等待画面
# 低电量关机提示(Doc/UI/K1标准界面图 0904/K1 低电量提醒状态界面.zip 合成全屏)
("LOWBAT_TIP", "10_低电量/低电量提醒-240x320.png"),
] ]
@ -274,7 +276,7 @@ def main():
if name in ("ROW_UNIVERSAL_NOT", "ROW_UNIVERSAL_SEL", "ROW_NORMAL_NOT", "ROW_NORMAL_SEL", "ROW_EXPERT_NOT", "ROW_EXPERT_SEL"): if name in ("ROW_UNIVERSAL_NOT", "ROW_UNIVERSAL_SEL", "ROW_NORMAL_NOT", "ROW_NORMAL_SEL", "ROW_EXPERT_NOT", "ROW_EXPERT_SEL"):
im = im.resize(ROW_RESIZE, Image.LANCZOS) im = im.resize(ROW_RESIZE, Image.LANCZOS)
data, w, h = rgba_to_rgb565(im) data, w, h = rgba_to_rgb565(im)
elif name in ("BOOT_LOGO", "CHARGE_SCREEN", "FLASH_MODE"): elif name in ("BOOT_LOGO", "CHARGE_SCREEN", "FLASH_MODE", "LOWBAT_TIP"):
if im.size[0] >= 241 and im.size[1] >= 321: if im.size[0] >= 241 and im.size[1] >= 321:
im = im.crop((0, 0, 240, 320)) im = im.crop((0, 0, 240, 320))
elif im.size != (240, 320): elif im.size != (240, 320):