第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_DeInit(),待机电流降到 5μA,问题解决。
4.3.3 测量状态
进入测量状态后,系统开始干活。这里有几个关键点:
- 启动 ADC 采样:连续采样 16 次,做中值滤波,去掉最大值和最小值,取平均。这样能有效抑制噪声。
- 温度转换:根据传感器数据手册的公式,把 ADC 值换算成摄氏度。公式一般是
T = (ADC_value * Vref) / (4096 * 10mV/°C)。 - 显示更新:每 0.5 秒刷新一次 LCD 显示,让用户看到温度在变化。
测量完成后,判断温度是否超过 37.5°C。如果超了,跳转到报警状态;否则回到待机。
4.3.4 报警状态
报警状态其实很简单,就是触发蜂鸣器和 LED 闪烁。但要注意:
- 蜂鸣器不要一直响,响 3 秒停 1 秒,否则用户会烦
- LED 以 1Hz 频率闪烁,红色表示高温,黄色表示低温
- 用户按任意键可以退出报警,回到待机
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 被占满,连按键都响应不了。