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 触发条件优先级与协作

三种触发条件不是孤立的,它们需要协作。我画了一张流程图来说明它们的关系:

会话保存触发条件协作流程图 设备运行中 空闲超时检测 用户离开检测 低电量检测 超时? 离开? 低电? 是 否 是 否 是 否 合并触发条件(位掩码) 去抖通过? 否 是 执行会话保存

嗯,这张图把整个流程串起来了。核心思路就是:先检测,再合并,最后去抖。三个触发条件各自独立检测,但最终只执行一次保存操作。

总结一下触发条件设计的三个原则:

  1. 按优先级排序:低电量 > 用户离开 > 空闲超时
  2. 必须加去抖:最小保存间隔不能少于30秒(视Flash规格调整)
  3. 合并执行:多个触发条件同时满足时,只保存一次

做到这三点,会话恢复功能就不会成为功耗的负担。我见过太多项目因为触发条件没设计好,导致设备续航直接腰斩。说白了,触发条件设计就是「该出手时就出手,不该出手时绝不出手」。