4、唤醒逻辑状态机:空闲态、唤醒态、休眠态、状态转换条件、伪代码实现

讲完了按键的硬件消抖和中断配置,咱们终于要碰这个课程里最核心的一块了——唤醒逻辑状态机

说实话,我早年做第一款低功耗手表时,就栽在这个状态机上。当时觉得「不就是休眠、唤醒、空闲三个状态嘛,简单」。结果样机做出来,手表放在桌上不动,半小时后按按键没反应——死锁了。后来查了两天才发现,是状态转换条件里漏了一个超时复位。

嗯,咱们今天就把这个坑填上。

4.1 为什么需要状态机?

你想想看,智能手表的功耗大头在屏幕和主控。如果一直让屏幕亮着、CPU跑着,电池撑不过半天。但完全关掉也不行——用户一抬手,你得立刻亮屏。

所以我们需要一个有限状态机(FSM)来管理这几种模式。说白了,就是让系统知道:

  • 现在该不该干活?
  • 什么时候该睡?
  • 被谁叫醒?

我个人习惯把唤醒逻辑拆成三个状态,不多不少,刚刚好。

4.2 三个核心状态

状态 英文名 含义 典型功耗
空闲态 IDLE 系统正常运行,但屏幕熄灭,等待用户交互 ~5mW(仅MCU低功耗运行)
唤醒态 ACTIVE 屏幕点亮,CPU全速运行,处理用户操作 ~200mW(含屏幕背光)
休眠态 SLEEP 深度睡眠,仅RTC和唤醒引脚工作 ~10µW

这三个状态,我习惯用一句话记:空闲态是待命,唤醒态是干活,休眠态是关机省电

4.3 状态转换条件

状态不能乱跳,得有规矩。我在项目中总结了一套转换条件,你直接拿去用,基本不会出问题。

转换条件一览

  • 空闲态 → 唤醒态:任意按键按下 / 抬手亮屏传感器触发 / 通知到达
  • 唤醒态 → 空闲态:无操作超时(我一般设5秒) / 用户手动熄屏
  • 空闲态 → 休眠态:长时间无操作(比如30秒) / 低电量强制休眠
  • 休眠态 → 唤醒态:专用唤醒按键(比如电源键) / RTC闹钟
  • 休眠态 → 空闲态:一般不允许直接转,必须先唤醒再降级

这里有个细节要注意:从休眠态唤醒时,系统需要重新初始化外设。我曾经遇到过一个问题——休眠时把I2C总线断电了,唤醒后直接去读传感器,结果一直卡在总线等待上。嗯,后来我在唤醒态入口加了一个「外设恢复」步骤,才搞定。

4.4 伪代码实现

下面是我在实际项目中用过的伪代码框架。注意,这不是某个具体芯片的代码,而是逻辑骨架。你移植到STM32、NXP、或者国产MCU上,思路都一样。

// 状态枚举
typedef enum {
    STATE_IDLE,
    STATE_ACTIVE,
    STATE_SLEEP
} wakeup_state_t;

// 全局状态变量
static wakeup_state_t current_state = STATE_IDLE;
static uint32_t idle_timer = 0;    // 空闲计时器
static uint32_t active_timer = 0;  // 唤醒计时器

// 主状态机处理函数(1ms定时器中断中调用)
void wakeup_fsm_process(void) {
    switch (current_state) {
        case STATE_IDLE:
            // 空闲态:等待按键或传感器
            if (key_pressed() || gesture_detected()) {
                current_state = STATE_ACTIVE;
                active_timer = 0;
                screen_on();
                cpu_full_speed();
                log("进入唤醒态");
            }
            // 空闲超时 → 休眠
            idle_timer++;
            if (idle_timer >= 30000) {  // 30秒无操作
                current_state = STATE_SLEEP;
                enter_deep_sleep();
                log("进入休眠态");
            }
            break;

        case STATE_ACTIVE:
            // 唤醒态:处理用户操作
            if (user_operation()) {
                active_timer = 0;  // 有操作,重置超时
                process_user_input();
            }
            // 无操作超时 → 空闲
            active_timer++;
            if (active_timer >= 5000) {  // 5秒无操作
                current_state = STATE_IDLE;
                idle_timer = 0;
                screen_off();
                cpu_low_power();
                log("回到空闲态");
            }
            // 手动熄屏
            if (manual_screen_off()) {
                current_state = STATE_IDLE;
                idle_timer = 0;
                screen_off();
            }
            break;

        case STATE_SLEEP:
            // 休眠态:只有唤醒中断能跳出
            // 唤醒中断服务函数里会修改状态
            // 这里不做任何事,保持睡眠
            break;

        default:
            // 异常保护:万一状态跑飞,强制回到空闲态
            current_state = STATE_IDLE;
            idle_timer = 0;
            screen_off();
            break;
    }
}

// 唤醒中断服务函数(例如电源键按下)
void wakeup_isr_handler(void) {
    if (current_state == STATE_SLEEP) {
        current_state = STATE_ACTIVE;
        active_timer = 0;
        wakeup_from_deep_sleep();
        screen_on();
        cpu_full_speed();
        log("从休眠态唤醒");
    }
}

我的小技巧:在状态切换的地方加log输出。调试阶段特别有用。我曾经靠log发现「空闲态→休眠态」的计时器被意外清零了,原因是另一个中断里不小心改了idle_timer。这种bug,不看log根本找不到。

4.5 避坑指南

做唤醒状态机,有几个坑我踩过,你记一下:

  • 中断里不要做复杂操作:唤醒中断里只改状态标志,别在里面初始化外设、刷屏幕。否则中断嵌套能把系统搞崩。
  • 休眠前要保存上下文:尤其是RTC寄存器和GPIO状态。我有一版代码,休眠后醒来发现按键中断不工作了——因为GPIO的pull-up配置在休眠时丢了。
  • 超时时间别写死:用宏定义或者配置文件。不同产品、不同用户场景,超时时间差很多。运动模式下我设3秒,待机模式下设10秒。
  • 状态机要有默认处理:万一状态变量被篡改(比如静电干扰),default分支能救你一命。

警告:千万不要在休眠态里轮询按键!我见过有人这么干——在SLEEP状态里用while循环等按键。结果功耗下不去,电池一晚上掉30%。正确的做法是用外部中断唤醒,CPU全程睡觉。

4.6 小结

唤醒逻辑状态机,说白了就是三个状态加五条转换线。但越简单的东西,越容易在细节上翻车。

我个人建议:先把伪代码跑通,再移植到具体硬件。调试时用LED指示当前状态——红灯空闲、绿灯唤醒、蓝灯休眠。一眼就能看出状态机有没有按预期跑。

下一章咱们会讲「按键防抖与长按短按识别」,到时候你会看到,状态机和按键逻辑是怎么配合的。