摘要
可编程逻辑控制器(PLC)是工业自动化领域的核心控制设备,广泛应用于制造业、能源、交通等行业。本文从PLC的基本原理出发,深入讲解IEC 61131-3标准下的结构化文本(ST)编程语言,通过一个完整的物料分拣控制系统案例,展示从需求分析、程序架构设计到代码实现的全流程。文章提供可直接运行的Codesys环境代码,并针对工程实践中的常见问题给出解决方案,帮助读者建立从理论到落地的完整知识体系。
应用场景
PLC控制系统在以下工业场景中具有不可替代的作用:
- 离散制造:汽车焊装线、电子装配线、包装机械的顺序控制与联锁保护
- 过程控制:化工反应釜温度PID调节、水处理厂流量比例控制
- 运动控制:伺服电机定位、传送带同步、机器人轨迹插补
- 能源管理:变电站自动切换、楼宇空调能效优化、光伏逆变器并网控制
本文以"三色物料分拣系统"为案例,模拟实际产线中根据物料颜色(红/绿/蓝)进行分拣的典型场景,涵盖传感器输入、逻辑判断、执行器输出、故障处理等完整控制链。
核心原理
1. PLC扫描周期
PLC采用循环扫描工作方式,每个扫描周期分为三个阶段:
- 输入采样:将物理输入信号(传感器、按钮)读入输入映像区
- 程序执行:CPU逐行执行用户程序,结果存入输出映像区
- 输出刷新:将输出映像区数据一次性写入物理输出点(电磁阀、电机)
理解扫描周期对程序实时性至关重要:同一周期内,输入状态在程序执行过程中保持不变,输出状态在周期结束时统一更新。
2. 结构化文本(ST)语言特性
ST语言基于Pascal语法,具有以下核心要素:
- 变量声明:VAR/END_VAR块定义局部变量,VAR_INPUT/END_VAR定义输入参数
- 数据类型:BOOL、INT、REAL、TIME、ARRAY、STRUCT
- 控制结构:IF-THEN-ELSIF-ELSE、CASE、FOR、WHILE、REPEAT
- 函数与功能块:可重用代码模块,支持输入输出参数
3. 状态机设计模式
工业控制中推荐使用有限状态机(FSM)实现逻辑清晰、易于维护的程序架构。本文采用三状态模型:
- IDLE:待机状态,等待启动信号
- RUNNING:运行状态,执行分拣逻辑
- ERROR:错误状态,触发报警并保持安全状态
详细步骤
步骤1:系统硬件配置
假设硬件配置如下:
- CPU:Codesys Control Win V3(仿真环境)
- 数字量输入:3个颜色传感器(DI0=红,DI1=绿,DI2=蓝),1个启动按钮(DI3),1个急停按钮(DI4)
- 数字量输出:3个推杆气缸(DO0=红料仓,DO1=绿料仓,DO2=蓝料仓),1个传送带电机(DO3),1个报警灯(DO4)
步骤2:程序架构设计
采用分层架构:
- 主程序(Main):循环调用各功能块
- 状态机管理(FSM_Manager):处理系统状态切换
- 传感器处理(Sensor_Handler):去抖滤波与信号确认
- 执行器控制(Actuator_Control):输出逻辑与互锁保护
- 故障诊断(Fault_Diagnosis):超时监控与错误处理
步骤3:变量定义
在全局变量列表中定义:
objectivec
VAR_GLOBAL
// 系统状态枚举
eSystemState : INT := 0; // 0=IDLE, 1=RUNNING, 2=ERROR
// 输入映射(实际项目中会通过IO映射)
Sensor_Red : BOOL := FALSE;
Sensor_Green : BOOL := FALSE;
Sensor_Blue : BOOL := FALSE;
Start_Button : BOOL := FALSE;
Emergency_Stop : BOOL := FALSE;
// 输出映射
Cylinder_Red : BOOL := FALSE;
Cylinder_Green : BOOL := FALSE;
Cylinder_Blue : BOOL := FALSE;
Conveyor_Motor : BOOL := FALSE;
Alarm_Lamp : BOOL := FALSE;
// 内部变量
Conveyor_Timer : TON; // 传送带运行计时
Sensor_Debounce : TON; // 传感器去抖
Fault_Code : INT := 0; // 故障代码
END_VAR
步骤4:主程序编写
主程序采用结构化文本,实现状态机循环:
ini
PROGRAM Main
VAR
// 功能块实例化
fsm : FSM_Manager;
sensor : Sensor_Handler;
actuator : Actuator_Control;
fault : Fault_Diagnosis;
// 临时变量
currentState : INT;
sensorValue : ARRAY[0..2] OF BOOL;
cmdCylinder : ARRAY[0..2] OF BOOL;
cmdConveyor : BOOL;
cmdAlarm : BOOL;
END_VAR
// 主扫描周期逻辑
// 第一步:读取物理输入(仿真环境直接读取全局变量)
sensorValue[0] := Sensor_Red;
sensorValue[1] := Sensor_Green;
sensorValue[2] := Sensor_Blue;
// 第二步:传感器去抖处理
sensor(
rawInputs := sensorValue,
debounceTime := T#50MS,
filteredOutputs => sensorValue
);
// 第三步:状态机管理
fsm(
startSignal := Start_Button,
emergencyStop := Emergency_Stop,
faultCode := Fault_Code,
currentState => currentState
);
// 第四步:根据当前状态执行控制逻辑
CASE currentState OF
0: // IDLE状态
cmdConveyor := FALSE;
cmdCylinder[0] := FALSE;
cmdCylinder[1] := FALSE;
cmdCylinder[2] := FALSE;
cmdAlarm := FALSE;
1: // RUNNING状态
// 传送带持续运行
cmdConveyor := TRUE;
// 颜色判断与推杆控制
IF sensorValue[0] THEN
cmdCylinder[0] := TRUE;
ELSIF sensorValue[1] THEN
cmdCylinder[1] := TRUE;
ELSIF sensorValue[2] THEN
cmdCylinder[2] := TRUE;
ELSE
// 无物料时所有推杆复位
cmdCylinder[0] := FALSE;
cmdCylinder[1] := FALSE;
cmdCylinder[2] := FALSE;
END_IF
// 故障检测:如果多个传感器同时触发,视为异常
IF (sensorValue[0] AND sensorValue[1]) OR
(sensorValue[0] AND sensorValue[2]) OR
(sensorValue[1] AND sensorValue[2]) THEN
Fault_Code := 1; // 传感器冲突故障
END_IF
2: // ERROR状态
cmdConveyor := FALSE;
cmdCylinder[0] := FALSE;
cmdCylinder[1] := FALSE;
cmdCylinder[2] := FALSE;
cmdAlarm := TRUE; // 报警灯常亮
END_CASE
// 第五步:执行器输出(含互锁保护)
actuator(
cmdConveyor := cmdConveyor,
cmdCylinders := cmdCylinder,
emergencyStop := Emergency_Stop,
conveyorOut => Conveyor_Motor,
cylinderOuts => [Cylinder_Red, Cylinder_Green, Cylinder_Blue],
alarmOut => Alarm_Lamp
);
// 第六步:故障诊断(超时监控等)
fault(
conveyorRunning := Conveyor_Motor,
timeout := T#10S,
faultCode => Fault_Code
);
步骤5:功能块实现
FSM_Manager功能块:
go
FUNCTION_BLOCK FSM_Manager
VAR_INPUT
startSignal : BOOL;
emergencyStop : BOOL;
faultCode : INT;
END_VAR
VAR_OUTPUT
currentState : INT;
END_VAR
VAR
prevStart : BOOL := FALSE;
stateTimer : TON;
END_VAR
// 状态切换逻辑
IF emergencyStop THEN
// 急停直接进入ERROR状态
currentState := 2;
ELSIF faultCode > 0 THEN
// 故障进入ERROR状态
currentState := 2;
ELSE
CASE currentState OF
0: // IDLE
// 检测到启动信号的上升沿
IF startSignal AND NOT prevStart THEN
currentState := 1;
stateTimer(IN := TRUE, PT := T#0S);
END_IF
1: // RUNNING
// 运行中检测到停止信号或故障
IF NOT startSignal THEN
currentState := 0;
END_IF
2: // ERROR
// 故障复位条件:急停释放且故障代码清零
IF NOT emergencyStop AND faultCode = 0 THEN
currentState := 0;
END_IF
END_CASE
END_IF
// 保存上次启动信号状态用于上升沿检测
prevStart := startSignal;
Sensor_Handler功能块:
ini
FUNCTION_BLOCK Sensor_Handler
VAR_INPUT
rawInputs : ARRAY[0..2] OF BOOL;
debounceTime : TIME;
END_VAR
VAR_OUTPUT
filteredOutputs : ARRAY[0..2] OF BOOL;
END_VAR
VAR
debounceTimers : ARRAY[0..2] OF TON;
stableStates : ARRAY[0..2] OF BOOL := [FALSE, FALSE, FALSE];
END_VAR
// 对每个传感器进行去抖处理
FOR idx := 0 TO 2 DO
// 当输入为TRUE时启动计时器
IF rawInputs[idx] THEN
debounceTimers[idx](IN := TRUE, PT := debounceTime);
// 计时到达后输出稳定状态
IF debounceTimers[idx].Q THEN
stableStates[idx] := TRUE;
END_IF
ELSE
// 输入为FALSE时立即复位
debounceTimers[idx](IN := FALSE);
stableStates[idx] := FALSE;
END_IF
filteredOutputs[idx] := stableStates[idx];
END_FOR
Actuator_Control功能块:
ini
FUNCTION_BLOCK Actuator_Control
VAR_INPUT
cmdConveyor : BOOL;
cmdCylinders : ARRAY[0..2] OF BOOL;
emergencyStop : BOOL;
END_VAR
VAR_OUTPUT
conveyorOut : BOOL;
cylinderOuts : ARRAY[0..2] OF BOOL;
alarmOut : BOOL;
END_VAR
// 急停状态下所有输出强制关闭
IF emergencyStop THEN
conveyorOut := FALSE;
cylinderOuts[0] := FALSE;
cylinderOuts[1] := FALSE;
cylinderOuts[2] := FALSE;
alarmOut := TRUE; // 急停时报警
ELSE
// 正常输出
conveyorOut := cmdConveyor;
cylinderOuts[0] := cmdCylinders[0];
cylinderOuts[1] := cmdCylinders[1];
cylinderOuts[2] := cmdCylinders[2];
alarmOut := FALSE;
END_IF
// 互锁保护:防止两个气缸同时动作(物理干涉)
IF cylinderOuts[0] AND cylinderOuts[1] THEN
cylinderOuts[0] := FALSE;
cylinderOuts[1] := FALSE;
END_IF
IF cylinderOuts[0] AND cylinderOuts[2] THEN
cylinderOuts[0] := FALSE;
cylinderOuts[2] := FALSE;
END_IF
IF cylinderOuts[1] AND cylinderOuts[2] THEN
cylinderOuts[1] := FALSE;
cylinderOuts[2] := FALSE;
END_IF
Fault_Diagnosis功能块:
ini
FUNCTION_BLOCK Fault_Diagnosis
VAR_INPUT
conveyorRunning : BOOL;
timeout : TIME;
END_VAR
VAR_OUTPUT
faultCode : INT;
END_VAR
VAR
runTimer : TON;
END_VAR
// 传送带运行超时监控
IF conveyorRunning THEN
runTimer(IN := TRUE, PT := timeout);
IF runTimer.Q THEN
// 传送带运行超过设定时间未停止,视为异常
faultCode := 2; // 运行超时故障
END_IF
ELSE
runTimer(IN := FALSE);
// 传送带停止后清除超时故障(如果只有此故障)
IF faultCode = 2 THEN
faultCode := 0;
END_IF
END_IF
完整可运行代码
以下为Codesys环境下的完整项目代码(含注释),可直接复制到Codesys中创建新项目运行。
全局变量列表(GVL):
objectivec
VAR_GLOBAL
// 系统状态枚举
eSystemState : INT := 0; // 0=IDLE, 1=RUNNING, 2=ERROR
// 输入映射(仿真时手动赋值)
Sensor_Red : BOOL := FALSE;
Sensor_Green : BOOL := FALSE;
Sensor_Blue : BOOL := FALSE;
Start_Button : BOOL := FALSE;
Emergency_Stop : BOOL := FALSE;
// 输出映射
Cylinder_Red : BOOL := FALSE;
Cylinder_Green : BOOL := FALSE;
Cylinder_Blue : BOOL := FALSE;
Conveyor_Motor : BOOL := FALSE;
Alarm_Lamp : BOOL := FALSE;
// 内部变量
Conveyor_Timer : TON; // 传送带运行计时
Sensor_Debounce : TON; // 传感器去抖
Fault_Code : INT := 0; // 故障代码
END_VAR
主程序(Main):
ini
PROGRAM Main
VAR
// 功能块实例化
fsm : FSM_Manager;
sensor : Sensor_Handler;
actuator : Actuator_Control;
fault : Fault_Diagnosis;
// 临时变量
currentState : INT;
sensorValue : ARRAY[0..2] OF BOOL;
cmdCylinder : ARRAY[0..2] OF BOOL;
cmdConveyor : BOOL;
cmdAlarm : BOOL;
END_VAR
// 主扫描周期逻辑
// 第一步:读取物理输入(仿真环境直接读取全局变量)
sensorValue[0] := Sensor_Red;
sensorValue[1] := Sensor_Green;
sensorValue[2] := Sensor_Blue;
// 第二步:传感器去抖处理
sensor(
rawInputs := sensorValue,
debounceTime := T#50MS,
filteredOutputs => sensorValue
);
// 第三步:状态机管理
fsm(
startSignal := Start_Button,
emergencyStop := Emergency_Stop,
faultCode := Fault_Code,
currentState => currentState
);
// 第四步:根据当前状态执行控制逻辑
CASE currentState OF
0: // IDLE状态
cmdConveyor := FALSE;
cmdCylinder[0] := FALSE;
cmdCylinder[1] := FALSE;
cmdCylinder[2] := FALSE;
cmdAlarm := FALSE;
1: // RUNNING状态
// 传送带持续运行
cmdConveyor := TRUE;
// 颜色判断与推杆控制
IF sensorValue[0] THEN
cmdCylinder[0] := TRUE;
ELSIF sensorValue[1] THEN
cmdCylinder[1] := TRUE;
ELSIF sensorValue[2] THEN
cmdCylinder[2] := TRUE;
ELSE
// 无物料时所有推杆复位
cmdCylinder[0] := FALSE;
cmdCylinder[1] := FALSE;
cmdCylinder[2] := FALSE;
END_IF
// 故障检测:如果多个传感器同时触发,视为异常
IF (sensorValue[0] AND sensorValue[1]) OR
(sensorValue[0] AND sensorValue[2]) OR
(sensorValue[1] AND sensorValue[2]) THEN
Fault_Code := 1; // 传感器冲突故障
END_IF
2: // ERROR状态
cmdConveyor := FALSE;
cmdCylinder[0] := FALSE;
cmdCylinder[1] := FALSE;
cmdCylinder[2] := FALSE;
cmdAlarm := TRUE; // 报警灯常亮
END_CASE
// 第五步:执行器输出(含互锁保护)
actuator(
cmdConveyor := cmdConveyor,
cmdCylinders := cmdCylinder,
emergencyStop := Emergency_Stop,
conveyorOut => Conveyor_Motor,
cylinderOuts => [Cylinder_Red, Cylinder_Green, Cylinder_Blue],
alarmOut => Alarm_Lamp
);
// 第六步:故障诊断(超时监控等)
fault(
conveyorRunning := Conveyor_Motor,
timeout := T#10S,
faultCode => Fault_Code
);
FSM_Manager功能块:
go
FUNCTION_BLOCK FSM_Manager
VAR_INPUT
startSignal : BOOL;
emergencyStop : BOOL;
faultCode : INT;
END_VAR
VAR_OUTPUT
currentState : INT;
END_VAR
VAR
prevStart : BOOL := FALSE;
stateTimer : TON;
END_VAR
// 状态切换逻辑
IF emergencyStop THEN
// 急停直接进入ERROR状态
currentState := 2;
ELSIF faultCode > 0 THEN
// 故障进入ERROR状态
currentState := 2;
ELSE
CASE currentState OF
0: // IDLE
// 检测到启动信号的上升沿
IF startSignal AND NOT prevStart THEN
currentState := 1;
stateTimer(IN := TRUE, PT := T#0S);
END_IF
1: // RUNNING
// 运行中检测到停止信号或故障
IF NOT startSignal THEN
currentState := 0;
END_IF
2: // ERROR
// 故障复位条件:急停释放且故障代码清零
IF NOT emergencyStop AND faultCode = 0 THEN
currentState := 0;
END_IF
END_CASE
END_IF
// 保存上次启动信号状态用于上升沿检测
prevStart := startSignal;
Sensor_Handler功能块:
ini
FUNCTION_BLOCK Sensor_Handler
VAR_INPUT
rawInputs : ARRAY[0..2] OF BOOL;
debounceTime : TIME;
END_VAR
VAR_OUTPUT
filteredOutputs : ARRAY[0..2] OF BOOL;
END_VAR
VAR
debounceTimers : ARRAY[0..2] OF TON;
stableStates : ARRAY[0..2] OF BOOL := [FALSE, FALSE, FALSE];
END_VAR
// 对每个传感器进行去抖处理
FOR idx := 0 TO 2 DO
// 当输入为TRUE时启动计时器
IF rawInputs[idx] THEN
debounceTimers[idx](IN := TRUE, PT := debounceTime);
// 计时到达后输出稳定状态
IF debounceTimers[idx].Q THEN
stableStates[idx] := TRUE;
END_IF
ELSE
// 输入为FALSE时立即复位
debounceTimers[idx](IN := FALSE);
stableStates[idx] := FALSE;
END_IF
filteredOutputs[idx] := stableStates[idx];
END_FOR
Actuator_Control功能块:
ini
FUNCTION_BLOCK Actuator_Control
VAR_INPUT
cmdConveyor : BOOL;
cmdCylinders : ARRAY[0..2] OF BOOL;
emergencyStop : BOOL;
END_VAR
VAR_OUTPUT
conveyorOut : BOOL;
cylinderOuts : ARRAY[0..2] OF BOOL;
alarmOut : BOOL;
END_VAR
// 急停状态下所有输出强制关闭
IF emergencyStop THEN
conveyorOut := FALSE;
cylinderOuts[0] := FALSE;
cylinderOuts[1] := FALSE;
cylinderOuts[2] := FALSE;
alarmOut := TRUE; // 急停时报警
ELSE
// 正常输出
conveyorOut := cmdConveyor;
cylinderOuts[0] := cmdCylinders[0];
cylinderOuts[1] := cmdCylinders[1];
cylinderOuts[2] := cmdCylinders[2];
alarmOut := FALSE;
END_IF
// 互锁保护:防止两个气缸同时动作(物理干涉)
IF cylinderOuts[0] AND cylinderOuts[1] THEN
cylinderOuts[0] := FALSE;
cylinderOuts[1] := FALSE;
END_IF
IF cylinderOuts[0] AND cylinderOuts[2] THEN
cylinderOuts[0] := FALSE;
cylinderOuts[2] := FALSE;
END_IF
IF cylinderOuts[1] AND cylinderOuts[2] THEN
cylinderOuts[1] := FALSE;
cylinderOuts[2] := FALSE;
END_IF
Fault_Diagnosis功能块:
ini
FUNCTION_BLOCK Fault_Diagnosis
VAR_INPUT
conveyorRunning : BOOL;
timeout : TIME;
END_VAR
VAR_OUTPUT
faultCode : INT;
END_VAR
VAR
runTimer : TON;
END_VAR
// 传送带运行超时监控
IF conveyorRunning THEN
runTimer(IN := TRUE, PT := timeout);
IF runTimer.Q THEN
// 传送带运行超过设定时间未停止,视为异常
faultCode := 2; // 运行超时故障
END_IF
ELSE
runTimer(IN := FALSE);
// 传送带停止后清除超时故障(如果只有此故障)
IF faultCode = 2 THEN
faultCode := 0;
END_IF
END_IF
运行结果说明
在Codesys仿真环境中运行上述程序,通过手动修改全局变量模拟输入,可观察到以下行为:
- 初始状态:系统处于IDLE状态(eSystemState=0),所有输出为FALSE
- 启动操作:将Start_Button设为TRUE,系统切换至RUNNING状态,Conveyor_Motor变为TRUE
- 物料分拣 :
- 将Sensor_Red设为TRUE,Cylinder_Red在50ms去抖后变为TRUE,其余气缸保持FALSE
- 将Sensor_Red复位,Sensor_Green设为TRUE,Cylinder_Red复位,Cylinder_Green激活
- 故障模拟:同时将Sensor_Red和Sensor_Green设为TRUE,Fault_Code变为1,系统进入ERROR状态,所有输出复位,Alarm_Lamp点亮
- 故障恢复:将Fault_Code手动清零,Emergency_Stop设为FALSE,系统返回IDLE状态
- 超时测试:在RUNNING状态下持续运行超过10秒,Fault_Code变为2,系统进入ERROR状态
常见问题与避坑
1. 扫描周期导致的逻辑错误
问题:在同一个扫描周期内同时读取和修改同一变量,可能导致预期外的行为。
解决方案:
- 采用"读-计算-写"三段式结构,避免在计算过程中修改输入
- 使用中间变量暂存计算结果,周期结束时统一输出
- 对于需要跨周期保持的状态,使用功能块内部变量
2. 传感器信号抖动
问题:机械触点或光电传感器在切换瞬间会产生多次跳变,导致误判。
解决方案:
- 本文已实现50ms去抖滤波器,可根据现场噪声特性调整时间常数
- 对于高速场景,使用硬件滤波(如RC电路)配合软件滤波
- 避免在去抖时间内进行关键逻辑判断
3. 输出互锁缺失
问题:两个物理上不能同时动作的执行器(如正反转接触器)同时得电,导致短路或机械损坏。
解决方案:
- 在Actuator_Control功能块中实现了三重互锁逻辑
- 互锁应在输出刷新前的最后环节执行,确保不被后续逻辑覆盖
- 对于关键设备(如电机正反转),建议在硬件电路层面增加互锁
4. 状态机死锁
问题:状态切换条件不满足,导致系统卡在某个状态无法退出。
解决方案:
- 每个状态必须定义明确的进入和退出条件
- 设置看门狗定时器,在超时后强制切换到安全状态
- 本文的Fault_Diagnosis功能块实现了运行超时监控,可防止传送带无限运行
5. 变量初始化遗漏
问题:PLC上电后,未初始化的变量可能包含随机值,导致程序异常启动。
解决方案:
- 所有全局变量和功能块内部变量必须显式初始化
- 在程序启动的第一个扫描周期执行初始化逻辑
- 使用RETAIN变量保存断电保持数据时,注意区分初始值和保持值
6. 数组边界越界
问题:在FOR循环中访问数组时,索引超出声明范围。
解决方案:
- 使用LOWER_BOUND和UPPER_BOUND函数获取数组边界
- 循环变量使用与数组索引一致的数据类型(如INT)
- 在Codesys中启用运行时边界检查功能
7. 功能块实例化数量
问题:每个功能块实例独立占用内存,过多实例可能导致内存溢出。
解决方案:
- 根据实际需求合理规划实例数量
- 对于重复逻辑,考虑使用函数(FUNCTION)而非功能块
- 使用ARRAY OF功能块管理同类设备