4. 空闲状态详解:上电初始化、自检流程、等待按键事件、进入低功耗模式

好,咱们接着聊。上一章我们把状态机的大框架搭起来了,这一章我带你深入看看第一个也是最重要的状态——空闲状态

你想想看,血压计一开机,首先进入的就是空闲状态。它就像人的「待机模式」,但绝不是简单的傻等。我做了这么多年嵌入式,发现很多新手容易把空闲状态理解成「啥也不干」,其实恰恰相反——空闲状态要做的事情,比你想象的多得多。

4.1 上电初始化:别让硬件「裸奔」

上电那一刻,MCU 内部其实是一片混沌。寄存器值不确定,外设时钟没开,GPIO 电平乱跳。这时候直接跑应用代码?那肯定翻车。

我个人习惯把初始化分成三个阶段:

  1. 硬件级初始化——关总中断、配时钟、设堆栈
  2. 板级初始化——GPIO 方向、ADC 采样率、LCD 背光
  3. 应用级初始化——清零测量缓存、设默认用户 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。用户反馈说「这机器反应真快」。其实不是硬件快,是软件架构好。

下一章,我们会进入「测量状态」,看看血压计是怎么一步步完成一次测量的。到时候你会发现,状态机的威力才刚刚开始展现。