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指示当前状态——红灯空闲、绿灯唤醒、蓝灯休眠。一眼就能看出状态机有没有按预期跑。
下一章咱们会讲「按键防抖与长按短按识别」,到时候你会看到,状态机和按键逻辑是怎么配合的。