状态机引擎与故障管理系统:回调注册表、报警锁存与监控任务

Babel36acl 嵌入式实战 无~ 3 次阅读 预计阅读时间: 17 分钟 发布于 1 天前 最后更新于 1 小时前 3735 字


状态机引擎与故障管理系统:回调注册表、报警锁存与监控任务

本文将一个工业控制器状态机子系统拆解为两大模块协同运作:packer_state_handler(状态切换引擎 + 事件系统)和 packer_fault_latch(报警锁存 + 监控任务 + 运行标志 + 状态快照)。前者决定"下一步该做什么",后者决定"出问题了怎么处理"。两部分通过运行时标志和报警码紧密衔接。

stateDiagram-v2
    state packer_state_handler {
        [*] --> Idle
        Idle --> Running : 启动
        Running --> FaultDetected : 报警
        FaultDetected --> Locked : 锁存
        Locked --> Idle : 人工复位
    }
    state packer_fault_latch {
        [*] --> Monitoring
        Monitoring --> Latched : 故障条件成立
        Latched --> Monitoring : 条件消除
    }

一、整体架构

状态机子系统由以下模块组成:

  • packer_state_handler — 回调注册表引擎。维护 18 个状态的处理函数注册表,负责状态切换、超时管理、事件分发
  • packer_fault_latch — 报警仲裁器。三源竞争锁存,两级优先级仲裁
  • app_monitor — 监控任务。喂狗、限位冲突检测、栈水位统计
  • packer_runtime_flags — 16 位运行标志位图。跨任务协调
  • packer_status_snapshot — 状态快照聚合层。为协议响应提供统一数据

StateMachineTask 每个周期调用 psh_task_once(),该函数按固定顺序执行:输入更新 → 标志同步 → 边界命令消费 → 报警检查 → 停止命令 → 当前状态 on_run。


二、packer_state_handler — 回调注册表引擎

2.1 为什么不要 switch-case

传统状态机用 switch(state) 分发,状态多了以后单个函数膨胀到上千行,分支预测效率下降,新增状态需要修改已有代码。回调注册表把每个状态的处理函数独立成模块,运行时动态注册。

2.2 核心数据结构

typedef struct {
    packer_state_t state;              // 状态枚举
    psh_on_enter_t on_enter;           // 进入回调
    psh_on_run_t   on_run;             // 周期运行回调
    psh_on_exit_t  on_exit;            // 退出回调
    const char    *name;               // 日志用名称
} packer_state_handler_t;              // 注册表最大 24 项

static packer_state_handler_t s_handlers[PSH_MAX_HANDLERS];
static packer_state_t         s_state;           // 当前状态
static uint32_t               s_state_deadline;  // 超时 tick

2.3 注册机制

psh_register() 插入第一个空槽,psh_register_batch() 循环注册数组。各子状态机(sm_bag_flow、sm_seal_flow、sm_self_check)在初始化时批量注册自己的状态处理函数:

void sm_bag_flow_init(void) {
    static const packer_state_handler_t handlers[] = {
        {STATE_BAG_CALIB_CHECK,  on_bag_calib_check_enter,  on_bag_calib_check_run,  NULL},
        {STATE_BAG_CALIB_CLEAR,  on_bag_calib_clear_enter,  on_bag_calib_clear_run,  NULL},
        // ... 共 8 个状态
    };
    psh_register_batch(handlers, sizeof(handlers)/sizeof(handlers[0]));
}

2.4 psh_transition — 状态切换

void psh_transition(packer_state_t next_state, uint32_t delay_ms) {
    packer_state_handler_t *old = psh_find_handler(s_state);
    packer_state_handler_t *new = psh_find_handler(next_state);
    if (old->on_exit) old->on_exit(next_state);          // 退出旧状态
    s_state = next_state;                                   // 更新状态
    s_state_deadline = osKernelGetTickCount() + delay_ms;   // 设置超时
    if (new->on_enter) new->on_enter(prev_state);           // 进入新状态
}

delay_ms=0 表示无超时,状态将无限期停留直到外部事件驱动切换。

2.5 超时检测 — 回绕安全

int psh_is_timed_out(void) {
    return ((int32_t)(osKernelGetTickCount() - s_state_deadline) >= 0);
}

用 int32_t 有符号比较,正确处理 32 位 tick 计数器的回绕。当 now 回绕到 0 而 deadline 还在高位时,差值负转正,检测仍然正确。

2.6 事件系统 — 子流程解耦

预定义事件:

  • PSH_EVENT_BOOT_BAG_CALIB_COMPLETE — 开机校准完成,sm_self_check 接手后续步骤
  • PSH_EVENT_PRESS_OPEN_DELEGATE — 进入压杆打开,询问 sm_self_check 是否优先处理
typedef int (*psh_event_handler_t)(psh_event_t event);
int psh_fire_event(psh_event_t event) {
    return s_event_handlers[event]
        ? s_event_handlers[event](event)
        : 0;
}

2.7 psh_task_once — 主循环 7 步

void psh_task_once(void) {
    packer_input_update();                          // 1. 输入更新
    psh_sync_runtime_flags();                       // 2. 标志同步
    psh_try_consume_control_command();              // 3. 边界命令消费
    if (fault_active && !in_error)                  // 4. 报警检查
        { psh_enter_error(); return; }
    if (stop_pending)                               // 5. 停止命令
        { force_idle(); return; }
    psh_find_handler(s_state)->on_run();            // 6. 运行当前状态
    psh_fire_event(PSH_EVENT_PRESS_OPEN_DELEGATE);  // 7. 尝试事件
}

2.8 psh_enter_error — 错误处理

当报警被触发时,psh_enter_error() 执行:停止所有步进电机(保持使能)→ 停止所有直流电机 → 设置停止标志 → 清除流程标志 → 触发报警锁存 → 取消打印机 → 重置子状态机 → 切换到 ERROR 状态。ERROR 状态有超时倒计时,超时后通过 POWER_ON → SELF_CHECK 自动尝试恢复。

2.9 IDLE 状态与流程启动

IDLE 是唯一的"空闲"状态。psh_idle_run() 设置 task_status=0,检查 run_enabled 标志,然后调用 psh_try_start_pending_flow() 消费命令网关中的动作命令:

void psh_try_start_pending_flow(void) {
    packer_command_t cmd;
    if (!packer_command_gateway_pop_action(&cmd)) return;
    switch (cmd.type) {
        case COMMAND_TRIGGER_BAG:    start_bag_flow();    break;
        case COMMAND_TRIGGER_SEAL:   start_seal_flow();   break;
        case COMMAND_TRIGGER_DELIVER: start_deliver_flow(); break;
    }
}

2.10 18 个状态一览

状态 所属子 SM 说明
POWER_ON 0 sm_self_check 上电等待,超时后进入自检
SELF_CHECK 1 sm_self_check 含 7 个子阶段(打印检测→校准→压杆测试→DC 测试)
IDLE 2 内置 等待主机命令,唯一可接收动作指令的状态
BAG_CALIB_CHECK 3 sm_bag_flow 检测校准传感器状态
BAG_CALIB_CLEAR 4 sm_bag_flow 等待传感器复位
BAG_CALIB_SEEK 5 sm_bag_flow 步进找校准位置
BAG_CALIB_BACKOFF 6 sm_bag_flow 从校准位后退
BAG_OUT 7 sm_bag_flow 主出袋脉冲,定时触发打印
PREHEAT 8 sm_bag_flow 预热等待(密封前加热)
SEAL_FORWARD 9 sm_bag_flow 前送阶段
CONVEYOR_RUN 10 sm_bag_flow 输送机运行(投料)
SEAL_BACKWARD 11 sm_seal_flow 密封后退准备
PRESS_RETRACT 12 sm_seal_flow 压杆下压(3 轴联动)
PRESS_CLOSE 13 sm_seal_flow 合拢(即过)
SEAL_HOLD 14 sm_seal_flow 保持密封压力,超时后脱离
TEAR_OFF 15 sm_seal_flow 撕断(轴 1 脉冲)
PRESS_OPEN 16 sm_seal_flow+sm_self_check 压杆回位(2 阶段:双轴回 + 轴 2 回 IN1)
RESET 17 sm_seal_flow 复位关风机,回到 IDLE
ERROR 18 内置 报警状态,超时自动尝试恢复

三、packer_fault_latch — 报警锁存与优先级仲裁

3.1 为什么需要专用锁存模块

多源报警(协议层、状态机、监控任务)同时触发时,需要一套仲裁规则决定哪个报警优先显示和处理。此外,"是否发生过报警"这个状态需要在报警条件消失后继续保持(锁存),直到操作员确认清除。

3.2 三源竞争架构

三个报警源具有不同优先级:

  • 协议层:优先级 1(最低)— 来自主机命令
  • 状态机:优先级 2 — 来自流程超时/故障
  • 监控任务:优先级 3(最高)— 安全监控

3.3 两级优先级仲裁

packer_fault_latch_raise(alarm_code, source) 执行:

  1. 如果当前无报警 → 接受新报警
  2. 如果已有报警 → 先比报警严重度(severity 数值越高越严重)
  3. 严重度相同 → 比源优先级(source 数值越高越权威)
  4. 更低的报警或源被拒绝(记录日志 PACKER_ALARM_KEEP)

3.4 13 个报警码

名称 严重度组 来源
0x00 NORMAL 0 无报警
0x01 MATERIAL_EMPTY 3 状态机
0x02 STAGE_TIMEOUT 1 状态机
0x05 CLAMP_ERR 4 监控任务
0x06-0x0D STEP_TIMEOUT / PULSE_OVERFLOW 等 4 状态机
0x0A PRINTER_ERR 2 状态机
0x0E POWER_OVER_LIMIT 4(预留) 功率限制框架

严重度分组:0=NORMAL,1=平台超时,2=打印机错误,3=材料空,4=步进超时/夹具错误。

3.5 锁存语义

锁存标志一旦置 1 就保持,直到 packer_fault_latch_clear_latch() 被显式调用。这允许系统回答"刚才发生过报警吗?"即使瞬态条件已消失。清除锁存和清除当前报警码是两个独立操作——RESET_FAULT 命令先清锁存,再清码。

void packer_fault_latch_raise(uint8_t alarm_code, uint8_t source) {
    if (s_alarm_code == APP_ALARM_NONE) {
        accept_new(alarm_code, source);
    } else {
        if (severity(alarm_code) > severity(s_alarm_code) ||
           (severity(alarm_code) == severity(s_alarm_code) &&
            source > s_alarm_source)) {
            replace_alarm(alarm_code, source);
        }
    }
    s_alarm_latched = 1;  // 锁存
}

四、app_monitor — 监控任务

4.1 职责

MonitorTask 是 FreeRTOS 任务(优先级 osPriorityNormal1,20ms 周期,2KB 栈),负责与业务逻辑解耦的"系统生存"监控:

  • 喂狗 (IWDG) — 通过 packer_watchdog_kick() 刷新独立看门狗,周期钳位到 IWDG_TIMEOUT_MS-500ms 保证安全余量
  • 限位冲突检测 — 检测压杆限位开关冲突(同轴两个限位同时激活)→ APP_ALARM_CLAMP_ERR
  • 栈水位统计 — 编译可选,查询所有 7 个任务的 uxTaskGetStackHighWaterMark
  • CPU 占用统计 — 编译可选,调用 vTaskGetRunTimeStats() 输出

4.2 跨重启诊断(.noinit)

MonitorTask 在 .noinit 段维护诊断结构(魔数 0x57444744),记录 last_kick_tick、kick_count、max_kick_gap_ms。boot_report() 在启动时打印上次运行的诊断数据,帮助分析看门狗复位原因。


五、packer_runtime_flags — 16 位运行标志位图

9 个标志位的轻量级位图,用于跨任务协调。所有 RMW 操作在 taskENTER_CRITICAL()/EXIT_CRITICAL() 下完成:

标志 含义
0x0001 RUN_ENABLED 主机已发送 CONTROL start
0x0002 FLOW_BUSY 流程进行中
0x0004 READY_FOR_SEAL 密封就绪(历史保留)
0x0008 ALARM_ACTIVE 有活跃报警
0x0010-0x0040 BAG/SEAL/CONVEYOR_DONE 各流程完成标志(锁存)
0x0080 JOG_BUSY 点动操作进行中
0x0100 BOOT_SEQUENCE_ACTIVE 开机自检未完成

关键设计:单个 volatile 16 位读取在 ARM Cortex-M3 上天然原子,因此 is_set() 不需要临界区。只有 set()/clear() 这种 RMW 操作需要临界区保护。


六、packer_status_snapshot — 状态快照聚合

为 STATUS(0x41)协议响应提供聚合数据。每个写操作同时更新缓存变量和 runtime_flags 相应位:

  • run_state(uint8_t 0/1)→ RUN_ENABLED
  • task_status(uint8_t 0/1)= FLOW_BUSY || JOG_BUSY(计算值)
  • bag/seal/conveyor_done 各自映射到对应标志位
  • busy_flags 位掩码:bit0=bag_done, bit1=seal_done, bit3=alarm_active, bit4=jog_busy

clear_process_flags() 批量清除 bag/seal/conveyor_done + ready_for_seal,在 CONTROL stop 和 RESET_FAULT scope=2 时调用。


七、模块协作全景

Protocol (0x41 STATUS query)
        ↓
packer_status_snapshot  ← reads ← packer_runtime_flags
        ↓                           (16-bit bitmap, CRIT on writes)
app_monitor → packer_fault_latch (priority arbitration)
        ↓           ↓
   watchdog kick    packer_runtime_flags (ALARM_ACTIVE bit)
        ↓           ↓
   stack stats    StateMachineTask checks each cycle
                     → psh_enter_error() on active alarm

八、设计要点与踩坑记录

  1. 注册表 vs switch-case:注册表模式让每个状态独立成模块,新增状态不需要修改已有代码。但调试时状态跳转不能从单一 switch 看到,需要配合日志分析
  2. 超时回绕安全:(int32_t) 比较正确处理 tick 回绕,但 delay_ms 最大值不能超过 int32_t 正半(约 24 天)
  3. 事件系统 vs 直接调用:事件系统解耦了子状态机,但也增加了间接性。只在真正需要跨模块交互时使用,内部子状态使用直接函数调用
  4. 报警锁存 vs 瞬态清除:锁存标志在报警消失后仍然保持,操作员必须主动清除。这防止了瞬态故障被忽略
  5. 临界区粒度:BSP 层用 PRIMASK(关全局中断),APP 层用 taskENTER_CRITICAL(仅关调度器)。混用可能导致优先级反转
  6. task_status 计算:task_status = FLOW_BUSY || JOG_BUSY,这是一个合成值而不是存储值,避免了双源不一致

INDL_CONTROLLER · STM32F103 · FreeRTOS · Keil MDK-ARM

此作者没有提供个人介绍。
最后更新于 2026-05-30