4. 空闲状态详解:上电初始化、自检流程、等待按键事件、进入低功耗模式
好,咱们接着聊。上一章我们把状态机的大框架搭起来了,这一章我带你深入看看第一个也是最重要的状态——空闲状态。
你想想看,血压计一开机,首先进入的就是空闲状态。它就像人的「待机模式」,但绝不是简单的傻等。我做了这么多年嵌入式,发现很多新手容易把空闲状态理解成「啥也不干」,其实恰恰相反——空闲状态要做的事情,比你想象的多得多。
4.1 上电初始化:别让硬件「裸奔」
上电那一刻,MCU 内部其实是一片混沌。寄存器值不确定,外设时钟没开,GPIO 电平乱跳。这时候直接跑应用代码?那肯定翻车。
我个人习惯把初始化分成三个阶段:
- 硬件级初始化——关总中断、配时钟、设堆栈
- 板级初始化——GPIO 方向、ADC 采样率、LCD 背光
- 应用级初始化——清零测量缓存、设默认用户 ID、清报警标志
这里有个坑。我曾经在一个项目里,把 LCD 初始化放在了很靠后的位置。结果上电瞬间,LCD 显示了一堆乱码,虽然只有几百毫秒,但客户就是不接受。后来我学乖了:先把 LCD 清屏,再初始化其他外设。用户看到的永远是干净的界面。
核心原则:初始化顺序 = 从内到外,从快到慢。先保证 CPU 能跑,再管外设,最后管人机交互。
4.2 自检流程:不是走过场,是真检查
自检,说白了就是「设备自己给自己做体检」。但很多产品把自检做成了「亮个灯、滴一声」就完事。我个人觉得,这不够。
一个靠谱的自检至少应该覆盖:
- 气路完整性——气泵能不能建立压力?电磁阀能不能正常泄气?
- 传感器偏置——压力传感器在零压时的输出值,是否在合理范围内?
- 电池电量——低于 3.3V 直接报警,别等测量到一半没电了
- 存储介质——Flash 或 EEPROM 的读写是否正常?
我记得有一次,客户反馈说「测了十次,有两次结果明显偏高」。查了半天,发现是压力传感器的零点漂移了。从那以后,我在自检里加了一行代码:
/* 检查压力传感器零点偏移 */
uint16_t adc_val = adc_read(PRESSURE_SENSOR_CH);
if (adc_val < ZERO_MIN || adc_val > ZERO_MAX) {
error_code |= ERR_SENSOR_DRIFT;
enter_error_state();
}
嗯,这行代码后来救了我好几次。
注意:自检不能阻塞太久。用户按了开机键,等 3 秒还没反应,他可能就以为死机了。我建议自检总耗时控制在 500ms 以内,超时就跳过非关键项。
4.3 等待按键事件:别用「死循环轮询」
很多初学者写按键检测,喜欢这样:
while(1) {
if (KEY_PRESSED) {
// 处理按键
}
}
这有什么问题?CPU 被占死了。你想想看,如果这时候还要刷新 LCD、检测电池、处理定时器……根本忙不过来。
我建议的做法是:用状态机 + 定时中断。主循环里只做状态切换,按键检测放在 10ms 的定时中断里。这样空闲状态下的 CPU 占用率可以降到 5% 以下。
具体来说:
- 定时中断每 10ms 扫描一次按键
- 检测到有效按键后,设置一个全局事件标志
- 主循环检查到事件标志,就触发状态迁移
这样做的好处是——空闲状态可以随时被中断唤醒,也可以随时进入低功耗。你想想看,如果主循环在死等按键,你怎么让它去睡觉?
4.4 进入低功耗模式:省电是门艺术
血压计这种便携设备,电池续航是硬指标。空闲状态下,如果用户 30 秒没操作,就应该进入低功耗模式。
低功耗模式的选择,取决于你的硬件设计:
| 模式 | 电流消耗 | 唤醒方式 | 适用场景 |
|---|---|---|---|
| Sleep(睡眠) | ~1mA | 外部中断 / RTC | 短时待机(<1分钟) |
| Stop(停止) | ~10μA | 外部中断 | 长时待机(>1分钟) |
| Standby(待机) | ~1μA | 唤醒引脚 | 深度休眠(>10分钟) |
我个人习惯用 Stop 模式。为什么?因为 Sleep 模式省电不够,Standby 模式唤醒后要重新初始化太多东西。Stop 模式刚好折中——唤醒快,功耗低。
但这里有个细节:进入低功耗前,一定要把不用的外设时钟关掉。我曾经犯过一个错:ADC 的时钟没关,进入 Stop 模式后电流还有 200μA。查了半天才发现,ADC 的时钟还在跑。
小技巧:在进入低功耗前,把所有 GPIO 都设成「模拟输入」或「上拉输入」。因为浮空的 GPIO 会产生漏电流,积少成多,很可观。
4.5 空闲状态的整体流程
好了,我们把上面这些串起来,看看空闲状态到底长什么样:
void idle_state_handler(void) {
static uint32_t idle_timer = 0;
// 1. 检查是否有按键事件
if (g_key_event != KEY_NONE) {
process_key_event(g_key_event);
g_key_event = KEY_NONE;
idle_timer = 0; // 重置空闲计时器
return;
}
// 2. 检查是否超时
if (++idle_timer > IDLE_TIMEOUT_MS / TICK_MS) {
// 进入低功耗
enter_low_power_mode();
// 醒来后继续
idle_timer = 0;
}
// 3. 刷新显示(比如时钟、日期)
refresh_idle_display();
}
你看,这个状态机里没有阻塞,没有死等。它一直在「检查→处理→返回」的循环里转,但每次只做一点点事。这就是状态机的精髓——把大任务拆成小步骤,每一步都不卡住。
嗯,说到这我想起一个项目。当时有个同事把整个测量流程写在一个 while 循环里,结果用户按了取消键,要等 5 秒才有反应。后来改成状态机,响应时间降到了 50ms。用户反馈说「这机器反应真快」。其实不是硬件快,是软件架构好。
下一章,我们会进入「测量状态」,看看血压计是怎么一步步完成一次测量的。到时候你会发现,状态机的威力才刚刚开始展现。