4、主状态机设计:系统主状态定义与状态转移条件分析

好,咱们进入正题。主状态机,说白了就是万用表的“大脑”。它决定了设备什么时候该干什么事。我做了这么多年固件,见过太多因为状态机设计混乱导致的bug——比如测量到一半突然跳进休眠,或者校准参数还没保存就被覆盖了。

今天我就把主状态机的设计思路掰开揉碎了讲给你听。你想想看,一个万用表,无非就是开机、测量、设置参数、校准精度、然后关机或休眠。但这里面的门道,可不少。

4.1 系统主状态定义

我个人习惯把万用表的主状态划分为五个核心状态。这五个状态基本覆盖了设备的所有工作模式:

状态编号 状态名称 状态描述 典型停留时间
S0 初始化 (INIT) 上电自检、硬件初始化、加载默认配置 500ms ~ 2s
S1 测量 (MEASURE) ADC采样、数据处理、显示更新 持续运行
S2 设置 (SETTING) 用户参数配置、量程选择、模式切换 用户交互期间
S3 校准 (CALIBRATION) 标准源接入、误差补偿、系数存储 30s ~ 5min
S4 休眠 (SLEEP) 低功耗模式、唤醒定时器、保持上下文 按需设定

核心原则:每个状态必须是“原子性”的。什么意思?就是在一个状态下,系统只做该状态该做的事,不要跨界。我在项目中遇到过有人把校准逻辑塞进测量状态里,结果每次切换量程都要重新校准一遍,用户都疯了。

4.2 初始化状态 (INIT) 详解

初始化是设备上电后的第一件事。嗯,这里要注意,初始化不是简单的“点个灯、亮个屏”就完事了。

我一般把初始化拆成三个阶段:

  1. 硬件自检:检查ADC是否就绪、LCD驱动是否正常、按键是否有卡死。
  2. 配置加载:从EEPROM或Flash读取用户设置、校准系数。
  3. 状态就绪:设置默认量程、清空显示缓冲区、进入测量状态。

这里有个坑——我曾经遇到过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只写了一半,校准系数就损坏了。从那以后,我强制要求校准状态必须使用“双备份存储”策略——先写备份区,校验通过后再更新主区。

校准状态的关键点:

  1. 标准源接入检测:确保外部标准信号稳定。
  2. 多点采样:至少采样10次取平均,减少噪声影响。
  3. 系数计算:线性拟合或查表补偿。
  4. 安全写入:先写备份,再写主区,最后校验。
// 校准状态写入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驱动和数据处理——那才是真正考验功力的地方。