第4章:体温计状态机顶层设计:系统状态图

好,咱们今天来聊聊体温计状态机的顶层设计。说白了,就是画一张图,把体温计从开机到关机的整个生命周期给捋清楚。我做了这么多年嵌入式,发现很多新手工程师一上来就写代码,结果状态跳来跳去,bug 一堆。其实,先把状态图画好,后面写代码就是照着填空。

4.1 五大核心状态

一个典型的体温计,我习惯把它拆成五个状态:初始化待机测量报警关机。你想想看,这五个状态基本覆盖了所有使用场景。

状态名称 状态编号 简要说明
初始化 S_INIT 上电自检、传感器校准、系统配置
待机 S_IDLE 等待用户操作,低功耗模式
测量 S_MEASURE 采集温度数据,进行滤波和转换
报警 S_ALARM 温度超限,触发声光报警
关机 S_POWEROFF 保存数据,关闭外设,断电

嗯,这里要注意,状态编号最好用枚举或者宏定义,别用魔数。我以前见过有人直接在代码里写 if(state == 3),三个月后自己都看不懂那个 3 是啥意思。

4.2 状态转换图

状态之间的跳转,不是你想跳就能跳的。每个跳转都需要一个明确的触发事件。我给大家画一个典型的转换路径:

核心转换路径:
上电 → 初始化 → 待机 → 测量 → 待机(正常)
测量 → 报警 → 待机(超限)
待机 → 关机(长按电源键)

为什么会这样设计?你想想看,测量完成后必须回到待机,而不是直接关机。因为用户可能想再测一次。我曾经遇到一个产品,测量完直接关机,被用户吐槽说「每次测完都要重新开机,烦死了」。

4.3 每个状态的详细行为

4.3.1 初始化状态

上电后第一件事就是初始化。我个人习惯在这里做三件事:

  • 硬件自检:检查传感器是否连接正常、电池电量是否充足。如果电池低于 2.5V,直接进入低电量报警。
  • 传感器校准:读取 EEPROM 中的校准参数,写入传感器寄存器。我记得有一次,校准参数读出来全是 0xFF,后来发现是 I2C 通信时序不对,折腾了两天。
  • 系统配置:设置 ADC 采样率、定时器周期、中断优先级等。

初始化完成后,自动跳转到待机状态。如果初始化失败,比如传感器没响应,我建议进入错误处理循环,而不是强行运行。强行运行的后果就是测出来的温度全是乱码。

4.3.2 待机状态

待机是体温计最常待的状态。说白了,就是在等用户按按钮。这个状态下,功耗要压到最低:

  • 关闭 ADC、关闭显示背光、关闭蜂鸣器
  • 只保留按键中断和 RTC 定时唤醒
  • 如果 30 秒无操作,自动进入深度睡眠
避坑指南: 我曾经在待机状态下忘了关闭 ADC,结果待机电流高达 2mA,电池两天就耗光了。后来加上 ADC_DeInit(),待机电流降到 5μA,问题解决。

4.3.3 测量状态

进入测量状态后,系统开始干活。这里有几个关键点:

  1. 启动 ADC 采样:连续采样 16 次,做中值滤波,去掉最大值和最小值,取平均。这样能有效抑制噪声。
  2. 温度转换:根据传感器数据手册的公式,把 ADC 值换算成摄氏度。公式一般是 T = (ADC_value * Vref) / (4096 * 10mV/°C)
  3. 显示更新:每 0.5 秒刷新一次 LCD 显示,让用户看到温度在变化。

测量完成后,判断温度是否超过 37.5°C。如果超了,跳转到报警状态;否则回到待机。

4.3.4 报警状态

报警状态其实很简单,就是触发蜂鸣器和 LED 闪烁。但要注意:

  • 蜂鸣器不要一直响,响 3 秒停 1 秒,否则用户会烦
  • LED 以 1Hz 频率闪烁,红色表示高温,黄色表示低温
  • 用户按任意键可以退出报警,回到待机
警告: 报警状态不能无限循环。如果用户不按键,系统应该在 10 秒后自动回到待机,并保存报警记录。否则电池会被蜂鸣器耗光。

4.3.5 关机状态

关机不是简单的断电。我建议做以下收尾工作:

  • 将最后一次测量温度写入 EEPROM,下次开机可以显示
  • 关闭所有外设:LCD、蜂鸣器、LED、ADC
  • 进入 STOP 模式,等待电源管理 IC 切断供电

嗯,这里有个细节:关机前要确保测量状态已经退出。如果用户在测量过程中长按关机键,应该先中断测量,保存当前数据,再关机。我曾经没做这个处理,结果用户测到一半关机,数据丢失,被投诉了。

4.4 状态机代码骨架

最后,给大家一个简单的状态机代码骨架。实际项目中会复杂得多,但核心逻辑就是这样:

typedef enum {
    S_INIT,
    S_IDLE,
    S_MEASURE,
    S_ALARM,
    S_POWEROFF
} SystemState_t;

SystemState_t currentState = S_INIT;

void StateMachine_Run(void) {
    switch(currentState) {
        case S_INIT:
            // 执行初始化
            if(Init_Success()) {
                currentState = S_IDLE;
            }
            break;
        case S_IDLE:
            if(Key_Pressed()) {
                currentState = S_MEASURE;
            }
            if(Key_LongPress()) {
                currentState = S_POWEROFF;
            }
            break;
        case S_MEASURE:
            Measure_Temperature();
            if(Measure_Done()) {
                if(Temperature_ExceedLimit()) {
                    currentState = S_ALARM;
                } else {
                    currentState = S_IDLE;
                }
            }
            break;
        case S_ALARM:
            Alarm_Trigger();
            if(Key_Pressed() || Alarm_Timeout()) {
                currentState = S_IDLE;
            }
            break;
        case S_POWEROFF:
            Save_Data();
            PowerOff_System();
            break;
        default:
            // 错误处理
            break;
    }
}

你看,代码结构非常清晰。每个 case 只做一件事,状态跳转一目了然。我建议你在实际项目中,把状态机放在一个 1ms 的定时器中断里跑,或者放在主循环中轮询。千万别在中断里做复杂运算,否则系统会卡死。

好了,这一章就到这里。下一章咱们聊聊「测量状态」的详细设计,包括 ADC 采样、滤波算法和温度转换公式。到时候我会分享一个我踩过的坑——采样率设太高,结果 CPU 被占满,连按键都响应不了。