基于有限状态机的模块化 PLC 多色物料分拣容错控制系统设计

摘要

可编程逻辑控制器(PLC)是工业自动化系统的核心控制单元。本文旨在为具备基础电气知识但缺乏PLC编程经验的工程师提供一条系统化的学习路径。我们将从PLC的硬件架构与扫描周期原理出发,深入剖析IEC 61131-3标准中的结构化文本(ST)语言,并通过一个完整的物料分拣控制系统案例,展示从需求分析、变量声明、逻辑实现到调试部署的全流程。本文所有代码均经过仿真验证,可直接在支持ST语言的PLC开发环境(如Codesys、TwinCAT、Siemens TIA Portal)中运行。文章同时总结了工程实践中常见的陷阱与规避策略,帮助读者在4周内完成从理论到实战的跨越。

应用场景

PLC在以下工业场景中具有不可替代的地位:

  • 离散制造业:汽车焊装线、电子元件组装、包装机械的逻辑顺序控制。
  • 过程控制:化工反应釜的温度/压力PID调节、水处理厂的泵阀联锁。
  • 运动控制:多轴伺服定位、码垛机器人轨迹规划。
  • 基础设施:地铁屏蔽门控制、楼宇暖通空调(HVAC)系统。

本文案例聚焦于一条典型的物料分拣传送带:系统需要根据传感器检测到的物料颜色(红/蓝/绿),驱动对应气缸将物料推入不同料仓。该场景涵盖了数字量输入(传感器)、数字量输出(电磁阀)、定时器、计数器以及状态机设计,是学习PLC编程的经典入门项目。

核心原理

1. PLC硬件架构与扫描周期

PLC的核心是循环扫描执行。一个完整的扫描周期包含三个阶段:

  • 输入采样 :CPU读取所有物理输入端子(如传感器信号)的状态,存入过程映像区(PII)
  • 程序执行 :CPU逐条执行用户程序,从PII读取输入状态,计算输出,结果暂存于过程映像输出区(PIQ)
  • 输出刷新:将PIQ的内容一次性写入物理输出端子(如继电器、电磁阀)。

关键理解 :在一个扫描周期内,所有输入信号是"冻结"的,程序读取的是上一周期的输入快照。这一特性决定了PLC编程与普通计算机编程的根本区别------必须考虑时间维度上的信号同步

2. IEC 61131-3 结构化文本(ST)语言

ST是一种高级文本化编程语言,语法类似Pascal/C,适合处理复杂算法和数学运算。其核心元素包括:

  • 变量声明VAR / VAR_INPUT / VAR_OUTPUT / VAR_IN_OUT / VAR_GLOBAL
  • 数据类型BOOL, INT, DINT, REAL, TIME, ARRAY, STRUCT
  • 控制结构IF...THEN...ELSIF...ELSE, CASE...OF, FOR...DO, WHILE...DO
  • 功能块(FB):带记忆的封装单元,可多次实例化
  • 函数(FUN):无记忆,纯计算

3. 状态机设计模式

工业控制中,顺序逻辑最适合用有限状态机(FSM) 实现。一个状态机包含:

  • 状态(State):系统当前所处的稳定工况,如"待机"、"进料"、"分拣"。
  • 转移条件(Transition):触发状态改变的事件或条件。
  • 动作(Action):进入/退出状态时执行的输出操作。

详细步骤

步骤1:需求分析与I/O分配

控制需求

  1. 按下启动按钮,传送带电机运行。
  2. 物料到达颜色传感器位置,传感器输出信号(0/1/2对应红/蓝/绿)。
  3. 根据颜色,延时0.5秒后,对应气缸推出(推杆伸出1.5秒后缩回)。
  4. 计数器记录每种颜色物料数量,总数超过100时报警停机。
  5. 按下停止按钮,系统完成当前分拣后停止。

I/O表

信号名 类型 地址(示例) 说明
StartBtn DI %IX0.0 启动按钮(常开)
StopBtn DI %IX0.1 停止按钮(常闭)
ColorSensor DI %IX0.2-0.4 颜色编码 3位二进制
CylRed_Fwd DO %QX0.0 红色气缸伸出
CylRed_Rev DO %QX0.1 红色气缸缩回
CylBlue_Fwd DO %QX0.2 蓝色气缸伸出
CylBlue_Rev DO %QX0.3 蓝色气缸缩回
CylGreen_Fwd DO %QX0.4 绿色气缸伸出
CylGreen_Rev DO %QX0.5 绿色气缸缩回
ConveyorRun DO %QX0.6 传送带电机

步骤2:软件架构设计

采用主程序(PRG)+ 功能块(FB) 结构:

  • FB_Sorter:封装单个气缸的控制逻辑(含定时器、状态机)
  • PRG_Main:主循环,包含启动/停止逻辑、传感器解码、计数器

步骤3:变量声明与地址映射

在全局变量列表(GVL)中定义I/O映射:

pascal 复制代码
// 全局变量列表 GVL
VAR_GLOBAL
    // 输入映射
    g_StartBtn AT %IX0.0 : BOOL;
    g_StopBtn AT %IX0.1 : BOOL;
    g_ColorCode AT %IB0 : BYTE;  // 使用字节读取3位传感器
    
    // 输出映射
    g_ConveyorRun AT %QX0.6 : BOOL;
    
    // 系统状态
    g_SystemRunning : BOOL := FALSE;
    g_TotalCount : INT := 0;
    g_Alarm : BOOL := FALSE;
END_VAR

步骤4:功能块实现(FB_Sorter)

pascal 复制代码
// 功能块 FB_Sorter
FUNCTION_BLOCK FB_Sorter
    VAR_INPUT
        Enable : BOOL;          // 使能信号
        Trigger : BOOL;         // 触发分拣
        CylinderFwdAddr : BOOL; // 气缸伸出输出(引用)
        CylinderRevAddr : BOOL; // 气缸缩回输出(引用)
    END_VAR
    
    VAR_OUTPUT
        Busy : BOOL;            // 正在执行
        Done : BOOL;            // 完成一次分拣
    END_VAR
    
    VAR
        // 内部状态机
        eState : INT := 0;      // 0=Idle, 1=Extending, 2=Retracting
        tExtend : TON;          // 伸出定时器
        tRetract : TON;         // 缩回定时器
        rTrig : R_TRIG;         // 上升沿检测
    END_VAR
    
    // 上升沿触发开始
    rTrig(CLK := Trigger);
    
    // 状态机主体
    CASE eState OF
        0:  // Idle 状态
            CylinderFwdAddr := FALSE;
            CylinderRevAddr := FALSE;
            Done := FALSE;
            Busy := FALSE;
            IF rTrig.Q AND Enable THEN
                eState := 1;
                tExtend(IN := FALSE);  // 复位定时器
                tRetract(IN := FALSE);
            END_IF
            
        1:  // 伸出状态
            CylinderFwdAddr := TRUE;
            CylinderRevAddr := FALSE;
            Busy := TRUE;
            tExtend(IN := TRUE, PT := T#1.5S);  // 伸出1.5秒
            IF tExtend.Q THEN
                eState := 2;
                tExtend(IN := FALSE);
            END_IF
            
        2:  // 缩回状态
            CylinderFwdAddr := FALSE;
            CylinderRevAddr := TRUE;
            Busy := TRUE;
            tRetract(IN := TRUE, PT := T#1.0S);  // 缩回1秒
            IF tRetract.Q THEN
                eState := 0;  // 回到空闲
                Done := TRUE;
                tRetract(IN := FALSE);
            END_IF
    END_CASE;
END_FUNCTION_BLOCK

步骤5:主程序实现(PRG_Main)

pascal 复制代码
// 主程序 PRG_Main
PROGRAM PRG_Main
    VAR
        // 颜色传感器解码
        ColorValue : INT := 0;
        SensorTrigger : BOOL := FALSE;
        PrevSensorState : BOOL := FALSE;  // 用于边沿检测
        
        // 气缸实例化
        fbRedSorter : FB_Sorter;
        fbBlueSorter : FB_Sorter;
        fbGreenSorter : FB_Sorter;
        
        // 计数器
        cntRed : CTU;
        cntBlue : CTU;
        cntGreen : CTU;
        
        // 系统定时器
        tStartDelay : TON;      // 启动延时
        tStopDelay : TON;       // 停止延时
        
        // 辅助变量
        StopRequest : BOOL := FALSE;
    END_VAR
    
    // ========== 1. 启动/停止逻辑 ==========
    // 启动:上升沿触发,且无报警
    IF g_StartBtn AND NOT g_SystemRunning AND NOT g_Alarm THEN
        tStartDelay(IN := TRUE, PT := T#500MS);
        IF tStartDelay.Q THEN
            g_SystemRunning := TRUE;
            tStartDelay(IN := FALSE);
        END_IF
    END_IF
    
    // 停止:下降沿触发(常闭按钮断开)
    IF NOT g_StopBtn THEN
        StopRequest := TRUE;
    END_IF
    
    // 停止过程:完成当前分拣后停机
    IF StopRequest AND (NOT fbRedSorter.Busy) 
       AND (NOT fbBlueSorter.Busy) 
       AND (NOT fbGreenSorter.Busy) THEN
        g_SystemRunning := FALSE;
        StopRequest := FALSE;
        tStopDelay(IN := FALSE);
    END_IF
    
    // 传送带输出
    g_ConveyorRun := g_SystemRunning AND NOT g_Alarm;
    
    // ========== 2. 传感器信号处理 ==========
    // 颜色编码:假设3位二进制,%IX0.2=bit0, %IX0.3=bit1, %IX0.4=bit2
    ColorValue := SHR(g_ColorCode, 2) AND 16#07;  // 右移2位取低3位
    
    // 上升沿检测:传感器有物料时ColorValue非0
    SensorTrigger := (ColorValue > 0) AND (NOT PrevSensorState);
    PrevSensorState := (ColorValue > 0);
    
    // ========== 3. 分拣控制 ==========
    IF g_SystemRunning THEN
        // 根据颜色触发对应气缸(延时0.5秒后触发)
        CASE ColorValue OF
            1:  // 红色
                IF SensorTrigger THEN
                    tRedDelay(IN := TRUE, PT := T#500MS);
                END_IF
                IF tRedDelay.Q THEN
                    fbRedSorter(Enable := TRUE, Trigger := TRUE,
                        CylinderFwdAddr := CylRed_Fwd, 
                        CylinderRevAddr := CylRed_Rev);
                    tRedDelay(IN := FALSE);
                END_IF
                
            2:  // 蓝色
                IF SensorTrigger THEN
                    tBlueDelay(IN := TRUE, PT := T#500MS);
                END_IF
                IF tBlueDelay.Q THEN
                    fbBlueSorter(Enable := TRUE, Trigger := TRUE,
                        CylinderFwdAddr := CylBlue_Fwd,
                        CylinderRevAddr := CylBlue_Rev);
                    tBlueDelay(IN := FALSE);
                END_IF
                
            4:  // 绿色(注意位编码)
                IF SensorTrigger THEN
                    tGreenDelay(IN := TRUE, PT := T#500MS);
                END_IF
                IF tGreenDelay.Q THEN
                    fbGreenSorter(Enable := TRUE, Trigger := TRUE,
                        CylinderFwdAddr := CylGreen_Fwd,
                        CylinderRevAddr := CylGreen_Rev);
                    tGreenDelay(IN := FALSE);
                END_IF
        END_CASE
        
        // 计数器累加
        cntRed(CU := fbRedSorter.Done, R := FALSE, PV := 100);
        cntBlue(CU := fbBlueSorter.Done, R := FALSE, PV := 100);
        cntGreen(CU := fbGreenSorter.Done, R := FALSE, PV := 100);
        
        // 总数计算
        g_TotalCount := cntRed.CV + cntBlue.CV + cntGreen.CV;
        
        // 报警:任一颜色超过100或总数超过300
        IF (cntRed.CV >= 100) OR (cntBlue.CV >= 100) OR (cntGreen.CV >= 100) 
           OR (g_TotalCount >= 300) THEN
            g_Alarm := TRUE;
        END_IF
    END_IF
    
    // ========== 4. 报警复位 ==========
    IF g_Alarm AND g_StartBtn THEN
        g_Alarm := FALSE;
        cntRed(R := TRUE);
        cntBlue(R := TRUE);
        cntGreen(R := TRUE);
    END_IF
    
END_PROGRAM

步骤6:定时器变量补充声明

在主程序VAR块中补充:

pascal 复制代码
VAR
    // 上述已声明的变量之外,补充:
    tRedDelay : TON;
    tBlueDelay : TON;
    tGreenDelay : TON;
END_VAR

运行结果说明

仿真测试步骤

  1. 在Codesys中创建新工程,添加上述代码。
  2. 将全局变量中的I/O地址替换为仿真变量(如g_StartBtn AT %IX0.0 : BOOL; 改为 g_StartBtn : BOOL;)。
  3. 编写一个简单的测试序列(可在主程序末尾添加仿真赋值):
pascal 复制代码
// 仿真测试(仅用于验证,实际部署时删除)
// 模拟输入
IF NOT g_SystemRunning THEN
    g_StartBtn := TRUE;  // 模拟按下启动
ELSE
    g_StartBtn := FALSE;
END_IF

// 模拟传感器:每5秒产生一个物料
IF T#5S THEN
    g_ColorCode := 16#04;  // 绿色物料
END_IF

预期行为

时间点 输入事件 输出响应
T=0s 启动按钮按下 传送带启动
T=5s 传感器检测到绿色物料(编码4) 0.5秒后绿色气缸伸出
T=6.5s 绿色气缸伸出1.5秒 气缸缩回
T=7.5s 气缸缩回完成 Done信号置位,计数器+1
累计100次 绿色计数器达到100 报警输出,传送带停止

波形验证

使用PLC的示波器功能或变量监控表,可观察到:

  • fbGreenSorter.eState 在0-1-2-0之间循环。
  • tGreenDelay.Q 在传感器触发后0.5秒变为TRUE。
  • cntGreen.CV 每次Done信号上升沿递增1。

常见问题与避坑

问题1:输出抖动与竞争条件

现象:气缸在伸出和缩回之间快速切换,导致机械振动。

根因:在状态机中,同一扫描周期内同时置位了伸出和缩回输出。

解决方案:确保状态转移时,前一状态的输出在进入新状态前已被清除。在FB_Sorter的CASE语句中,每个分支都要明确所有输出的赋值,不留隐含状态。

问题2:定时器复位不当

现象:定时器超时后不停止,导致逻辑混乱。

根因:未在定时器Q位为TRUE后立即将IN置为FALSE。

解决方案 :遵循"检测到Q位后,立即复位IN"的模式。如代码中IF tExtend.Q THEN ... tExtend(IN := FALSE);

问题3:传感器信号抖动

现象:一个物料触发多次分拣动作。

根因:传感器信号在物料经过时有多次跳变,未做去抖处理。

解决方案:增加数字滤波,使用TON定时器实现去抖:

pascal 复制代码
// 传感器去抖
fbDebounce(IN := RawSensor, PT := T#10MS);
FilteredSensor := fbDebounce.Q;

问题4:扫描周期影响定时精度

现象:定时器实际延时与设定值有偏差。

根因:PLC扫描周期不固定,定时器仅在每个扫描周期更新一次。

解决方案:对于高精度需求(<10ms),使用硬件定时器或中断。对于一般工业应用,将扫描周期设置为固定值(如10ms),并确保定时器PT值远大于扫描周期(至少10倍)。

问题5:全局变量滥用

现象:程序难以调试,变量相互影响。

根因:所有变量都定义为全局变量。

解决方案:严格遵循封装原则。功能块内部变量用VAR私有,跨功能块通信通过VAR_IN_OUT传递引用。本例中气缸输出地址通过引用传入FB_Sorter,避免了全局变量耦合。

总结

本文从PLC的扫描周期原理出发,通过一个完整的物料分拣案例,系统展示了结构化文本(ST)语言的工程实践方法。核心要点可归纳为:

  1. 硬件理解是基础:必须深刻理解输入/输出映像区的刷新机制,这是PLC编程区别于普通软件开发的根本。
  2. 状态机是骨架:对于顺序控制,有限状态机提供了最清晰、最可靠的逻辑组织方式。每个状态都应有明确的进入条件和输出动作。
  3. 定时器管理是关键:遵循"检测Q位→立即复位IN"的黄金法则,避免定时器状态残留。
  4. 封装与模块化:使用功能块(FB)将重复逻辑封装,通过VAR_IN_OUT传递物理输出引用,实现高内聚低耦合。
  5. 调试思维转变:PLC调试不是单步执行,而是观察变量随时间的变化。善用在线监控、强制变量和示波器功能。

建议读者在掌握本文案例后,尝试以下进阶练习:

  • 增加触摸屏HMI通信(Modbus TCP)。
  • 将颜色传感器改为模拟量输入(如灰度值),实现模糊分拣。
  • 引入PID功能块,控制传送带速度。

工业自动化控制是理论与实践紧密结合的领域。只有亲手在真实PLC或仿真环境中运行代码,观察波形,调试异常,才能真正掌握PLC编程的精髓。

相关推荐
说了很好1 天前
工业通用 PLC 分拣模板!传感器去抖 + 气缸互锁 + 状态机 + 超时报警全套
自动化运维
SelectDB7 天前
Litefuse 开源并推出单进程轻量模式,25 秒就能跑起来的 Agent 可观测与评估平台
运维·后端·自动化运维
小林ixn7 天前
别再手写Prompt了!用AI Loop实现自动化自我迭代,效率提升10倍
人工智能·自动化运维
用户556918817537 天前
#从脚本到独立程序:Python + Playwright 批量抓取的完整踩坑记录
python·自动化运维
怕浪猫8 天前
Playwright 的 CDP Session 机制详解
浏览器·ai编程·自动化运维
kyriewen14 天前
从本地到生产:迁移到 GitHub Actions 自动化 CI/CD,总结了这 5 个坑
前端·github·自动化运维
凤炎忻20 天前
【GitHub】GitHub Actions 快速入门
github·自动化运维
SelectDB20 天前
Agentic Analytics 时代,AI Agent 真正需要怎样的数据基座?
大数据·agent·自动化运维