4、触发条件设计:何时触发会话保存
触发时机选不对,会话恢复功能反而会成为功耗杀手。我见过不少项目,为了保险把保存间隔设得很短,结果设备一直在写Flash,功耗比不保存还高。说白了,触发条件设计就是找那个「恰到好处」的平衡点。
4.1 三种核心触发场景
我个人习惯把触发场景分成三类:空闲超时、用户离开、低电量。每种场景的优先级和实现方式都不一样。
| 触发类型 | 触发条件 | 优先级 | 典型场景 |
|---|---|---|---|
| 空闲超时 | 无用户操作超过T_idle | 中 | 阅读器、仪表盘 |
| 用户离开 | 传感器检测到用户不在 | 高 | 智能家居面板、车载 |
| 低电量 | 电池电压低于阈值 | 最高 | 所有电池供电设备 |
4.2 空闲超时:最常用的触发方式
空闲超时是最基础的手段。设备检测到一段时间内没有用户交互,就自动保存会话。这里有个关键参数——超时时间T_idle怎么设?
我在项目中踩过一个坑:把T_idle设成了固定的30秒。结果用户看文档时翻页间隔刚好35秒,每次翻页前设备都在保存,白白浪费电量。后来我改成动态超时,根据用户操作频率自动调整。
动态超时算法思路:
- 记录最近N次操作的时间间隔
- 取中位数作为参考值
- T_idle = 参考值 × 2(留出余量)
- 设置上下限:最小5秒,最大120秒
// 动态超时计算示例
#define MIN_IDLE_MS 5000
#define MAX_IDLE_MS 120000
#define HISTORY_SIZE 5
static uint32_t idle_history[HISTORY_SIZE];
static uint8_t history_idx = 0;
uint32_t calc_dynamic_timeout(uint32_t last_interval_ms) {
// 更新历史记录
idle_history[history_idx % HISTORY_SIZE] = last_interval_ms;
history_idx++;
// 取中位数
uint32_t sorted[HISTORY_SIZE];
memcpy(sorted, idle_history, sizeof(sorted));
// 简单排序(实际项目用插入排序更高效)
for (int i = 0; i < HISTORY_SIZE - 1; i++) {
for (int j = i + 1; j < HISTORY_SIZE; j++) {
if (sorted[i] > sorted[j]) {
uint32_t tmp = sorted[i];
sorted[i] = sorted[j];
sorted[j] = tmp;
}
}
}
uint32_t median = sorted[HISTORY_SIZE / 2];
uint32_t timeout = median * 2;
// 限幅
if (timeout < MIN_IDLE_MS) timeout = MIN_IDLE_MS;
if (timeout > MAX_IDLE_MS) timeout = MAX_IDLE_MS;
return timeout;
}
小技巧:空闲超时触发后,不要立即进入深度睡眠。先保存会话,然后进入浅睡眠(比如只关屏幕,保留RAM),再等一个短超时(3-5秒)确认没有唤醒事件,再真正休眠。这样可以避免频繁的Flash写入。
4.3 用户离开:传感器辅助触发
用户离开场景需要硬件配合。比如红外传感器、超声波传感器、或者简单的电容触摸检测。我做过一个智能家居面板,用红外传感器检测用户距离,当距离超过1.5米且持续5秒,就触发会话保存。
这里有个容易忽略的问题:传感器本身也耗电。你想想看,如果为了检测用户离开,每隔100ms就读取一次传感器,那功耗可能比不检测还高。
避坑指南:我曾经在一个项目里用了超声波传感器做用户离开检测,采样频率设成了50Hz。结果传感器功耗占了整机功耗的30%。后来改成10Hz轮询+中断唤醒,功耗降到了3%以下。记住:检测频率要和场景匹配,不是越高越好。
用户离开触发的优先级应该高于空闲超时。为什么?因为用户离开意味着设备可能长时间无人操作,尽早保存会话可以避免突然断电导致数据丢失。
4.4 低电量:最高优先级触发
低电量触发是保底机制。当电池电压低于某个阈值时,不管当前在做什么,必须立即保存会话。这个阈值怎么定?我一般留出两次完整保存操作的电量余量。
// 低电量检测与触发
#define BATTERY_CRITICAL_MV 3200 // 3.2V 临界值
#define BATTERY_SAVE_MARGIN 150 // 保存一次消耗 150mV 电量
static uint16_t get_battery_mv(void) {
// ADC读取电池电压,返回毫伏值
return adc_read_battery();
}
void battery_check_and_save(void) {
uint16_t battery_mv = get_battery_mv();
if (battery_mv <= BATTERY_CRITICAL_MV) {
// 立即保存,不等待任何超时
save_session_immediate();
// 进入最低功耗模式
enter_deep_sleep();
} else if (battery_mv <= BATTERY_CRITICAL_MV + BATTERY_SAVE_MARGIN * 2) {
// 接近临界,提高保存频率
set_save_interval(SAVE_INTERVAL_FAST);
}
}
关键点:低电量触发后,保存操作本身也要优化。不要保存完整会话,只保存关键状态(当前页面、输入内容、滚动位置)。非关键数据(缓存、日志)直接丢弃。这样一次保存的耗电量可以降低50%以上。
4.5 如何避免频繁触发
频繁触发是会话恢复功能最大的坑。我见过一个设备,因为触发条件设计不合理,每10秒就保存一次,Flash芯片一年就写报废了。避免频繁触发,我有几个经验:
- 去抖机制:任何触发条件都要加去抖。比如空闲超时触发后,设置一个最短间隔(比如30秒),在这个间隔内不再触发第二次保存。
- 状态变化检测:只有会话状态真正变化时才保存。如果用户只是移动了鼠标但没有改变任何内容,不触发保存。
- 合并保存:多个触发条件同时满足时,只执行一次保存。比如用户离开时正好也低电量了,合并成一次保存操作。
- 自适应频率:根据Flash剩余寿命动态调整保存频率。Flash快写满时,降低保存频率,优先保证关键数据。
// 去抖与合并保存实现
typedef struct {
uint32_t last_save_time;
uint32_t min_interval_ms;
uint8_t pending_save;
uint8_t save_reason; // 位掩码:bit0=空闲, bit1=离开, bit2=低电
} save_controller_t;
static save_controller_t save_ctrl = {
.last_save_time = 0,
.min_interval_ms = 30000, // 30秒最小间隔
.pending_save = 0,
.save_reason = 0
};
void request_save(uint8_t reason) {
save_ctrl.save_reason |= reason;
if (!save_ctrl.pending_save) {
save_ctrl.pending_save = 1;
// 提交到任务队列,避免在中断中直接保存
task_post(SAVE_TASK, NULL);
}
}
void save_task_handler(void) {
uint32_t now = get_tick_ms();
// 检查最小间隔
if (now - save_ctrl.last_save_time < save_ctrl.min_interval_ms) {
// 还没到时间,延迟执行
task_post_delayed(SAVE_TASK, save_ctrl.min_interval_ms);
return;
}
// 执行保存
do_actual_save(save_ctrl.save_reason);
// 重置状态
save_ctrl.last_save_time = now;
save_ctrl.pending_save = 0;
save_ctrl.save_reason = 0;
}
经验之谈:我习惯在设备启动时记录Flash的剩余擦写次数。当剩余次数低于10%时,自动把最小保存间隔从30秒延长到120秒。虽然极端情况下可能丢失一些数据,但总比Flash彻底报废好。
4.6 触发条件优先级与协作
三种触发条件不是孤立的,它们需要协作。我画了一张流程图来说明它们的关系:
嗯,这张图把整个流程串起来了。核心思路就是:先检测,再合并,最后去抖。三个触发条件各自独立检测,但最终只执行一次保存操作。
总结一下触发条件设计的三个原则:
- 按优先级排序:低电量 > 用户离开 > 空闲超时
- 必须加去抖:最小保存间隔不能少于30秒(视Flash规格调整)
- 合并执行:多个触发条件同时满足时,只保存一次
做到这三点,会话恢复功能就不会成为功耗的负担。我见过太多项目因为触发条件没设计好,导致设备续航直接腰斩。说白了,触发条件设计就是「该出手时就出手,不该出手时绝不出手」。