状态机引擎与故障管理系统:回调注册表、报警锁存与监控任务
本文将一个工业控制器状态机子系统拆解为两大模块协同运作: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) 执行:
- 如果当前无报警 → 接受新报警
- 如果已有报警 → 先比报警严重度(severity 数值越高越严重)
- 严重度相同 → 比源优先级(source 数值越高越权威)
- 更低的报警或源被拒绝(记录日志 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
八、设计要点与踩坑记录
- 注册表 vs switch-case:注册表模式让每个状态独立成模块,新增状态不需要修改已有代码。但调试时状态跳转不能从单一 switch 看到,需要配合日志分析
- 超时回绕安全:(int32_t) 比较正确处理 tick 回绕,但 delay_ms 最大值不能超过 int32_t 正半(约 24 天)
- 事件系统 vs 直接调用:事件系统解耦了子状态机,但也增加了间接性。只在真正需要跨模块交互时使用,内部子状态使用直接函数调用
- 报警锁存 vs 瞬态清除:锁存标志在报警消失后仍然保持,操作员必须主动清除。这防止了瞬态故障被忽略
- 临界区粒度:BSP 层用 PRIMASK(关全局中断),APP 层用 taskENTER_CRITICAL(仅关调度器)。混用可能导致优先级反转
- task_status 计算:task_status = FLOW_BUSY || JOG_BUSY,这是一个合成值而不是存储值,避免了双源不一致
INDL_CONTROLLER · STM32F103 · FreeRTOS · Keil MDK-ARM
Comments NOTHING