作为一名经常和PLC打交道的上位机工程师,经常听到PLC工程师说什么状态机,以及使用状态机的好处,于是乎,我也借助豆老师简单的检索了一下资料。开始之前,要牢记这句话,不要写一堆 if-else,而是把 “当前状态” 单独拎出来管理。

一、状态机定义

有限状态机 FSM

        设备任何时刻只处在一个固定工况,收到外部触发信号 / IO 信号 / 指令 / 定时器到,按预设规则从一个状态跳到另一个状态,且禁止非法跳转

三大核心要素(设备):

  1. 状态 State:设备当前工况 → 待机、复位中、自动运行、手动、半自动、故障、暂停、下料中、上料中
  2. 触发 Trigger:启动按钮、IO 感应、气缸到位、伺服到位、报警信号、上位机下发指令、定时器超时
  3. 流转 Transition:规定好什么状态下收到什么信号,能跳到哪个状态

总结:把设备所有工况拆成固定状态,用规则管住跳转,不用一堆 if-else 乱嵌套。

二、C# 非标设备开发中哪些场景会用到状态机

几乎所有带流程、带模式切换的功能,都能用状态机优化。

1. 整机运行状态(最常用)

设备全局状态枚举:

  • 待机 Idle → 复位中 Reset → 就绪 Ready → 自动运行 AutoRun → 手动模式 Manual → 暂停 Pause → 故障 Alarm → 急停 Emergency约束规则:
  • 故障状态下禁止启动
  • 急停后只能回到待机,不能直接进自动
  • 必须先复位完成才能进自动这就是标准状态机,不用状态机,你要写几十行 if 嵌套,后期改逻辑麻烦的要死

2. 单工位 / 单工艺流程状态

比如:上料工位、移栽工位、真空吸附、压合、烘烤、下料

拆分状态:待机 → 取料开始 → 上升到位 → 平移 → 下降到位 → 放料完成 → 回归待机

每一步必须上一步完成才能进下一步,一步错整个流程卡死,用状态机管控步骤流转。

3. 气缸 / 电磁阀 / 伺服单轴动作状态

单轴:空闲、动作中、到位、超时故障

管控:动作中不能重复发指令;超时自动切故障状态。

4. 报警 & 故障处理状态

正常运行 → 轻微报警 → 严重故障 → 确认复位 → 恢复待机

限制:故障未确认,禁止自动启动。

5. 人机界面 HMI 页面状态

页面:初始加载、连接 PLC 中、通信正常、通信断开、参数配置中、配方下载中

6. 串口 / TCP/SECS/GEM 通信状态

半导体设备肯定用到:未连接 → 连接中 → 已连接 → 通信交互中 → 断连 → 重连

SECS(半导体标准协议) 消息收发、会话建立、断线重连全靠状态机管住。

7. 配方 / 流程步骤执行

多工序依次执行:步骤 1→步骤 2→步骤 3→结束,每步等待外部信号再跳转,典型状态机。

三、状态机 和 异步编程、多线程 到底矛不矛盾?

        给出结论:完全不矛盾,状态机≠单线程,我最初有这个疑惑是因为PLC的编程方式是单线程编程,从头到尾循环扫描

1.三者定位

  • 状态机:管业务逻辑、工况流转、步骤顺序、合法跳转只管「现在是什么状态、下一步能去哪」
  • 多线程:管并行干活比如:UI 线程、PLC 通信线程、IO 轮询线程、日志线程、工艺后台线程
  • 异步编程 async/await:管不阻塞线程、等待 IO / 等待动作完成比如等待气缸到位、等待伺服定位完成、等待 PLC 回复、等待 SECS 消息

2. 三者关系

  1. 状态机:作为业务骨架,不管你是多线程还是异步,所有动作都在当前状态规则内执行
  2. 多线程:开独立线程循环跑状态机逻辑例:一个专用逻辑线程,while 死循环,每帧扫描当前状态、检测触发信号、做状态跳转。
  3. 异步编程:在某个状态里,用 async/await 等待动作完成,不卡 UI、不卡整个状态机循环

异步是用来等动作,多线程是用来并行,状态机是用来管流程,三层各司其职,不冲突。

3. 会不会冲突?什么时候要注意?

不矛盾,但要注意线程安全

  • 设备当前状态变量 多线程会读写,要加锁 / 原子变量
  • 禁止在 UI 线程、通信线程、逻辑线程同时乱改状态
  • 状态切换统一收口到一个方法,集中加锁控制

四、PLC 单线程状态机 和 C# 多线程 / 异步 对比

1、核心区别

维度 PLC C# 上位机 / 非标软件
运行模型 单线程循环扫描、串行 多线程、Task、async/await、并行
状态机写法 标志位 / SFC/ST 枚举 Switch Enum + 后台线程循环 + 异步等待
等待动作 靠下一个扫描周期轮询判断到位 await 异步等待、不阻塞线程
并发能力 弱,只能分时串行 强,可以同时跑通信、UI、日志、多工位逻辑

2.、通俗类比

  • PLC:一个工人,从头走到尾,一圈又一圈,重复执行,啥事都按顺序轮询查一遍(单线程状态机)
  • C# 上位机:多个工人分工,一个管 UI、一个管 PLC 通信、一个管工艺逻辑、一个记日志,同时干活,工艺逻辑里还用状态机管流程(多线程 + 状态机 + 异步)

要注意:C# 多线程下状态变量要加锁、防并发乱改状态

五、举例子-非标设备电芯搬运程序

        假设我们不用PLC,而使用运动控制卡来做一个锂电设备电芯搬运程序

设计思路

1、电芯本体有二维码,二维码中有追溯信息,反馈该电芯是OK还是NG

2、设备有NG储料盒,储料盒满,设备停止运行,需要手动把储料盒中的电芯拿走,复位并启动

3、搬运过程中安全光栅被触发,机台停止,需要复位并启动

4、搬运和吸取使用轴和气缸实现

先画出流程图,后续直接写代码就行,

通过枚举的状态来控制整个流程,但是要注意,加锁,要不然会线程混乱,代码写的很粗糙,大概懂意思就行

    public enum MachineState
    {
        Idle,       // 待机
        Resetting,  // 复位
        Ready,      // 就绪
        Stop,       // 停止
        Scanning,   // 扫码
        Picking,    // 吸取
        MovingOK,   // 搬OK
        MovingNG,   // 搬NG
        AlarmNgFull,// NG满
        AlarmGuard  // 光栅
    }

    public partial class MainWindow : Window
    {
        // 核心状态 + 线程锁
        private MachineState _currentState;
        private readonly object _stateLock = new object(); // 加锁!解决混乱
        private Thread _stateThread;
        private bool _isRunning;

        // 信号
        private bool _ngBoxFull;
        private bool _guardTriggered;

        public MainWindow()
        {
            InitializeComponent();
            _currentState = MachineState.Idle;

            // 常驻线程
            _stateThread = new Thread(StateMachineLoop)
            {
                IsBackground = true,
                Priority = ThreadPriority.Normal
            };
            _stateThread.Start();

            UpdateUI();
        }

        // ===================== 状态机循环 =====================
        private void StateMachineLoop()
        {
            while (true)
            {
                Thread.Sleep(200);
                lock (_stateLock) // 加锁,保证同一时间只执行一个状态
                {
                    try
                    {
                        // ============== 最高优先级:停止 ==============
                        if (_currentState == MachineState.Stop)
                        {
                            continue; // 彻底卡住,不往下执行
                        }

                        // ============== 第二优先级:安全光栅 ==============
                        if (_guardTriggered)
                        {
                            _currentState = MachineState.AlarmGuard;
                            Log("安全光栅触发 → 立即停止");
                            continue;
                        }

                        // ============== 未运行 ==============
                        if (!_isRunning)
                        {
                            _currentState = MachineState.Idle;
                            continue;
                        }

                        // ============== 主状态机 ==============
                        switch (_currentState)
                        {
                            case MachineState.Idle:
                                break;

                            case MachineState.Resetting:
                                Log("复位中...");
                                Thread.Sleep(1500);
                                _ngBoxFull = false;
                                _guardTriggered = false;
                                Log("复位完成 → 待机");
                                _currentState = MachineState.Idle; // 正确回到Idle
                                _isRunning = false;
                                break;

                            case MachineState.Ready:
                                if (_ngBoxFull)
                                {
                                    _currentState = MachineState.AlarmNgFull;
                                    break;
                                }

                                Log("等待电池...");
                                Thread.Sleep(1000);
                                _currentState = MachineState.Scanning;
                                break;

                            case MachineState.Scanning:
                                Log("扫码中...");
                                Thread.Sleep(800);
                                bool ok = new Random().Next(2) == 0;
                                Log(ok ? "OK" : "NG");
                                _currentState = MachineState.Picking;
                                break;

                            case MachineState.Picking:
                                Log("吸取电池");
                                Thread.Sleep(1000);
                                _currentState = ListLog.Items.Cast<string>().Last().Contains("OK")
                                    ? MachineState.MovingOK
                                    : MachineState.MovingNG;
                                break;

                            case MachineState.MovingOK:
                                Log("搬运至传送带");
                                Thread.Sleep(1200);
                                _currentState = MachineState.Ready;
                                break;

                            case MachineState.MovingNG:
                                Log("搬运至NG盒");
                                Thread.Sleep(1200);
                                _currentState = _ngBoxFull ? MachineState.AlarmNgFull : MachineState.Ready;
                                break;

                            case MachineState.AlarmNgFull:
                                Log("NG盒满 → 停止");
                                break;

                            case MachineState.AlarmGuard:
                                Log("光栅报警 → 停止");
                                break;
                        }
                    }
                    catch (Exception ex)
                    {
                        Log("异常:" + ex.Message);
                    }
                }

                UpdateUI();
            }
        }

        // ===================== 按钮 =====================
        private void BtnStart_Click(object sender, RoutedEventArgs e)
        {
            lock (_stateLock)
            {
                if (_currentState != MachineState.Idle)
                {
                    Log("必须在待机状态启动");
                    return;
                }

                Log("启动设备");
                _isRunning = true;
                _currentState = MachineState.Ready;
                UpdateUI();
            }
        }

        private void BtnReset_Click(object sender, RoutedEventArgs e)
        {
            lock (_stateLock)
            {
                Log("执行复位");
                _isRunning = false;
                _guardTriggered = false;
                _ngBoxFull = false;
                _currentState = MachineState.Idle;
                UpdateUI();
            }
        }

        private void BtnEnd_Click(object sender, RoutedEventArgs e)
        {
            lock (_stateLock)
            {
                Log("手动停止 → 设备停止");
                _isRunning = false;
                _currentState = MachineState.Stop; // 真正停止
                UpdateUI();
            }
        }

        private void BtnNgFull_Click(object sender, RoutedEventArgs e)
        {
            lock (_stateLock)
            {
                _ngBoxFull = true;
            }
        }

        private void BtnGuard_Click(object sender, RoutedEventArgs e)
        {
            lock (_stateLock)
            {
                _guardTriggered = !_guardTriggered;
            }
        }

        // ===================== UI =====================
        private void UpdateUI()
        {
            Dispatcher.Invoke(() =>
            {
                TxtState.Text = _currentState.ToString();

                TxtState.Foreground = _currentState.ToString().Contains("Alarm") || _currentState == MachineState.Stop
                    ? Brushes.Red
                    : _currentState == MachineState.Ready
                        ? Brushes.Green
                        : Brushes.Blue;

                BtnStart.IsEnabled = _currentState == MachineState.Idle;
                BtnStart.Background = _currentState == MachineState.Ready ? Brushes.LightGreen : Brushes.LightGray;
                BtnEnd.Background = _currentState == MachineState.Stop ? Brushes.Red : Brushes.LightGray;
                BtnGuard.Background = _guardTriggered ? Brushes.Red : Brushes.LightGray;
            });
        }

        private void Log(string msg)
        {
            Dispatcher.Invoke(() =>
            {
                ListLog.Items.Add($"{DateTime.Now:HH:mm:ss} | {msg}");
            });
        }
    }

更多推荐