4、主状态机设计:系统主状态定义与状态转移条件分析
好,咱们进入正题。主状态机,说白了就是万用表的“大脑”。它决定了设备什么时候该干什么事。我做了这么多年固件,见过太多因为状态机设计混乱导致的bug——比如测量到一半突然跳进休眠,或者校准参数还没保存就被覆盖了。
今天我就把主状态机的设计思路掰开揉碎了讲给你听。你想想看,一个万用表,无非就是开机、测量、设置参数、校准精度、然后关机或休眠。但这里面的门道,可不少。
4.1 系统主状态定义
我个人习惯把万用表的主状态划分为五个核心状态。这五个状态基本覆盖了设备的所有工作模式:
| 状态编号 | 状态名称 | 状态描述 | 典型停留时间 |
|---|---|---|---|
| S0 | 初始化 (INIT) | 上电自检、硬件初始化、加载默认配置 | 500ms ~ 2s |
| S1 | 测量 (MEASURE) | ADC采样、数据处理、显示更新 | 持续运行 |
| S2 | 设置 (SETTING) | 用户参数配置、量程选择、模式切换 | 用户交互期间 |
| S3 | 校准 (CALIBRATION) | 标准源接入、误差补偿、系数存储 | 30s ~ 5min |
| S4 | 休眠 (SLEEP) | 低功耗模式、唤醒定时器、保持上下文 | 按需设定 |
核心原则:每个状态必须是“原子性”的。什么意思?就是在一个状态下,系统只做该状态该做的事,不要跨界。我在项目中遇到过有人把校准逻辑塞进测量状态里,结果每次切换量程都要重新校准一遍,用户都疯了。
4.2 初始化状态 (INIT) 详解
初始化是设备上电后的第一件事。嗯,这里要注意,初始化不是简单的“点个灯、亮个屏”就完事了。
我一般把初始化拆成三个阶段:
- 硬件自检:检查ADC是否就绪、LCD驱动是否正常、按键是否有卡死。
- 配置加载:从EEPROM或Flash读取用户设置、校准系数。
- 状态就绪:设置默认量程、清空显示缓冲区、进入测量状态。
这里有个坑——我曾经遇到过EEPROM读取失败的情况。如果校准系数全是0xFF,那测量结果就全废了。所以初始化阶段一定要做数据校验,比如CRC校验。
// 初始化状态处理函数示例
void state_init_handler(void) {
// 阶段1:硬件自检
if (adc_self_test() != PASS) {
error_handler("ADC故障");
return;
}
// 阶段2:加载配置
if (load_calibration_from_eeprom() != SUCCESS) {
// 加载失败,使用默认系数
use_default_calibration();
log_warning("校准系数加载失败,使用默认值");
}
// 阶段3:状态转移
set_next_state(STATE_MEASURE);
}
4.3 测量状态 (MEASURE) 核心逻辑
测量状态是万用表最核心的状态。说白了,设备99%的时间都待在这个状态里。
测量状态内部其实还有子状态,比如:
- 等待稳定:信号建立时间,尤其是测电容或电阻时。
- ADC采样:启动转换、等待完成、读取数据。
- 数据处理:滤波、量程换算、单位转换。
- 显示刷新:更新LCD内容。
你想想看,如果用户按了“设置”键,测量状态必须优雅地退出。不能采样到一半就扔了,那样数据会乱掉。
我的经验:在测量状态里,我习惯用一个“完成标志位”来控制状态退出。比如ADC采样完成后,先处理完当前数据,再检查是否有状态转移请求。这样能保证数据完整性。
4.4 设置状态 (SETTING) 交互设计
设置状态是用户和设备的交互窗口。这里的设计要点是“可中断”和“可保存”。
我记得有一次做项目,用户设置完量程后直接断电,结果下次开机又回到了默认量程。后来我加了一个“设置变更标志”,只有在用户确认保存后才写入EEPROM。
设置状态通常包含:
- 量程选择(自动/手动)
- 测量模式切换(电压/电流/电阻)
- 显示设置(背光、单位、小数位数)
- 通信参数(如果支持蓝牙或USB)
设置状态的退出条件很简单:用户按“确认”或“返回”键,或者超时自动退出。
4.5 校准状态 (CALIBRATION) 避坑指南
校准状态,说实话,是万用表固件里最容易出bug的地方。为什么?因为校准涉及外部标准源、用户操作时序、以及非易失性存储的写入。
我曾经犯过一个低级错误:在校准过程中,如果用户突然断电,EEPROM只写了一半,校准系数就损坏了。从那以后,我强制要求校准状态必须使用“双备份存储”策略——先写备份区,校验通过后再更新主区。
校准状态的关键点:
- 标准源接入检测:确保外部标准信号稳定。
- 多点采样:至少采样10次取平均,减少噪声影响。
- 系数计算:线性拟合或查表补偿。
- 安全写入:先写备份,再写主区,最后校验。
// 校准状态写入EEPROM的安全策略
void save_calibration_safe(uint8_t *data, uint16_t len) {
// 步骤1:写入备份区
eeprom_write(BACKUP_ADDR, data, len);
// 步骤2:校验备份区
if (eeprom_verify(BACKUP_ADDR, data, len) != PASS) {
error_handler("备份写入失败");
return;
}
// 步骤3:写入主区
eeprom_write(MAIN_ADDR, data, len);
// 步骤4:校验主区
if (eeprom_verify(MAIN_ADDR, data, len) != PASS) {
// 从备份区恢复
eeprom_copy(BACKUP_ADDR, MAIN_ADDR, len);
log_error("主区写入失败,已从备份恢复");
}
}
4.6 休眠状态 (SLEEP) 低功耗设计
休眠状态,说白了就是为了省电。但省电不能省功能——设备必须能在特定条件下被唤醒。
我设计的休眠状态通常包含:
- 外设断电:关闭ADC、LCD背光、通信模块。
- 保留上下文:当前量程、测量模式、校准状态存入低功耗RAM。
- 唤醒源配置:按键中断、定时器唤醒、外部触发。
这里有个细节:休眠前一定要确保ADC转换已经完成,否则下次唤醒后数据可能异常。嗯,这个坑我踩过。
4.7 状态转移条件分析
状态转移,说白了就是“什么情况下从A状态跳到B状态”。我习惯用一张表把转移条件列清楚:
| 当前状态 | 目标状态 | 转移条件 | 优先级 |
|---|---|---|---|
| INIT | MEASURE | 初始化完成,无致命错误 | 最高 |
| MEASURE | SETTING | 用户按下“设置”键 | 中 |
| MEASURE | SLEEP | 无操作超时(如30秒) | 低 |
| SETTING | MEASURE | 用户确认或超时退出 | 中 |
| MEASURE | CALIBRATION | 长按“校准”键3秒 | 低(需防误触) |
| CALIBRATION | MEASURE | 校准完成或用户取消 | 中 |
| SLEEP | MEASURE | 按键唤醒或定时器到期 | 最高 |
重要提醒:状态转移必须考虑“优先级”。比如用户正在测量,突然长按校准键,系统应该先完成当前测量周期,再进入校准状态。绝对不能中断正在进行的ADC采样。
4.8 状态机实现框架
最后,我给出一个典型的状态机实现框架。你直接拿去用,改改就能跑:
typedef enum {
STATE_INIT,
STATE_MEASURE,
STATE_SETTING,
STATE_CALIBRATION,
STATE_SLEEP,
STATE_MAX
} system_state_t;
// 状态处理函数指针数组
void (*state_handlers[STATE_MAX])(void) = {
state_init_handler,
state_measure_handler,
state_setting_handler,
state_calibration_handler,
state_sleep_handler
};
// 状态转移表
typedef struct {
system_state_t current;
system_state_t next;
bool (*condition)(void);
} state_transition_t;
state_transition_t transition_table[] = {
{STATE_INIT, STATE_MEASURE, is_init_complete},
{STATE_MEASURE, STATE_SETTING, is_setting_key_pressed},
{STATE_MEASURE, STATE_SLEEP, is_idle_timeout},
{STATE_SETTING, STATE_MEASURE, is_setting_done},
{STATE_MEASURE, STATE_CALIBRATION, is_calib_key_long_press},
{STATE_CALIBRATION, STATE_MEASURE, is_calib_complete},
{STATE_SLEEP, STATE_MEASURE, is_wakeup_event}
};
// 主循环
void main_loop(void) {
system_state_t current_state = STATE_INIT;
while(1) {
// 执行当前状态处理
state_handlers[current_state]();
// 检查状态转移
for(int i = 0; i < sizeof(transition_table)/sizeof(transition_table[0]); i++) {
if(transition_table[i].current == current_state) {
if(transition_table[i].condition()) {
current_state = transition_table[i].next;
break;
}
}
}
}
}
这个框架的好处是:新增状态或转移条件时,只需要修改表格,不需要动主循环逻辑。我在多个项目里都用这个结构,稳定可靠。
好了,主状态机设计就讲到这里。下一章咱们深入聊聊测量状态里的ADC驱动和数据处理——那才是真正考验功力的地方。