──────────────────────────────────────────────────
一、引言:从"面条代码"到结构化编程
某工程师接手一条产线的PLC程序,打开后发现:
·3000+ 行代码全部写在OB1(主循环组织块)
·没有FC/FB,全部是network 1~network 50
·变量名全是I0.0/Q0.0/DB1.DBB0,没有符号表
·逻辑跳转(JMP)满天飞,完全看不懂执行顺序
这就是典型的"面条代码"——所有逻辑搅在一起,维护难度堪比"解乱麻"。
本文将从结构化编程和状态机设计两个维度,带你写出"可读、可维护、可复用"的PLC程序。
──────────────────────────────────────────────────
二、结构化编程基础:FC/FB/DB的合理使用
2.1 FC vs FB:什么时候用哪个?
FC(Function)和FB(Function Block)是PLC结构化编程的核心,但很多人分不清"什么时候用FC,什么时候用FB"。
【FC vs FB 选择决策树】
问题1:是否需要记忆状态?(是 → FB;否 → FC)
问题2:是否需要多次实例化?(是 → FB;否 → FC)
问题3:逻辑是否复杂?(是 → FB/FC + 多个Network;否 → 直接写在OB1)
通用规则:
- FC:用于"无状态"的逻辑(如计算、转换、简单控制)
- FB:用于"有状态"的逻辑(如电机控制、阀门控制、PID调节)
- OB1:只写"调用逻辑",不写"实现逻辑"
2.2 FC编程实战:电机启动逻辑
# FC1: 电机启动逻辑(无状态)
#输入:Start_Button (BOOL), Stop_Button (BOOL), Fault (BOOL)
#输出:Motor_Run (BOOL)
Network 1: 启动逻辑
START: Start_Button AND NOT Stop_Button AND NOT Fault
Motor_Run: = START OR (Motor_Run AND NOT Stop_Button AND NOT Fault)
#调用方式(在OB1中)
CALL FC1 (Start_Button := I0.0, Stop_Button := I0.1, Fault := I0.2, Motor_Run := Q0.0)
2.3 FB编程实战:电机控制块(有状态)
# FB1: 电机控制块(有状态,需要记忆运行时间)
#输入:Start (BOOL), Stop (BOOL), Fault (BOOL)
#输出:Run (BOOL), Run_Time (TIME)
#静态变量(存储在Instance DB中)
# Static: Run_Start_Time (TIME)
Network 1: 启动逻辑
Start AND NOT Stop AND NOT Fault -> Run
Network 2: 运行时间累计
IF Run THEN Run_Time := Run_Time + (NOW() - Run_Start_Time); Run_Start_Time := NOW(); END_IF
#调用方式(需要分配Instance DB)
CALL FB1, DB10 (Start := I0.0, Stop := I0.1, Fault := I0.2, Run := Q0.0, Run_Time := MD0)
2.4 DB的合理使用:全局DB vs 实例DB
·全局DB(Global DB):用于存储"跨FC/FB"共享的数据(如工艺参数、系统状态)
·实例DB(Instance DB):每个FB调用时自动生成,存储该FB的"私有数据"
·推荐规则:
·工艺参数 → 全局DB(方便HMI/SCADA访问)
·设备状态 → 实例DB(每个设备独立)
·临时变量 → FC/FB的TEMP区(不存储)
──────────────────────────────────────────────────
三、状态机设计:让程序从"面条"变成"流程图"
3.1 为什么需要状态机?
传统PLC编程的常见问题:
·逻辑分散:设备的"启动→运行→停止→故障"逻辑散落在OB1的各个network
·状态不清:不知道设备当前处于哪个状态,调试时"猜"
·扩展困难:新增状态(如"暂停""清洗")需要大改代码
状态机(State Machine)的核心思想:将设备的所有状态"枚举"出来,然后用"状态转移条件"控制流转。
3.2 状态机设计四步法
·步骤1:枚举所有状态(如:Idle/Running/Fault/Pause)
·步骤2:定义状态转移条件(如:Idle → Running,条件=Start_Button)
·步骤3:为每个状态编写"进入动作"和"退出动作"
·步骤4:在主循环中调用状态机(CASE语句)
3.3 实战案例:电机状态机(SCL语言)
# FB10: 电机状态机(SCL语言,推荐用于复杂逻辑)
#状态枚举
TYPE Motor_State : (Idle, Running, Fault, Pause);
#静态变量
Static Current_State : Motor_State := Idle;
Static Fault_Code : INT := 0;
#状态机主逻辑(在OB1中调用)
CASE Current_State OF
Idle:
IF Start_Button AND NOT Fault THEN Current_State := Running; END_IF;
IF Fault THEN Current_State := Fault; END_IF;
Running:
Motor_Run := TRUE;
IF Stop_Button THEN Current_State := Idle; Motor_Run := FALSE; END_IF;
IF Pause_Button THEN Current_State := Pause; END_IF;
IF Fault THEN Current_State := Fault; Motor_Run := FALSE; END_IF;
Pause:
Motor_Run := FALSE;
IF Start_Button THEN Current_State := Running; END_IF;
IF Fault THEN Current_State := Fault; END_IF;
Fault:
Motor_Run := FALSE;
IF Reset_Button AND NOT Fault THEN Current_State := Idle; END_IF;
END_CASE;
【状态机设计要点】
每个状态的责任要"单一":不要在一个状态里做多件事
状态转移条件要"互斥":避免同时触发多个转移
故障状态要"独立":Fault状态只负责"安全停机+报警",不负责"故障处理"
调试技巧:在HMI上显示Current_State,让操作员"看到"设备状态
──────────────────────────────────────────────────
四、代码复用:建立自己的PLC代码库
很多工程师每次新项目都"从零开始"写PLC程序——这是巨大的浪费。
建立代码库的核心价值:
·开发效率提升:新项目直接"拿来就用",开发周期缩短40%
·质量可控:经过多个项目验证的代码,bug率远低于"现场手写"
·知识沉淀:团队经验固化成代码,避免"老师傅离职,经验带走"
4.1 代码库的组织结构
# PLC代码库目录结构(推荐)
PLC_Library/
├── FC/ # 通用FC(无状态)
│ ├── FC1_Start_Stop.scl # 启停逻辑
│ ├── FC2_TON.scl # 延时接通
│ └── FC3_Counter.scl # 计数器
├── FB/ # 通用FB(有状态)
│ ├── FB1_Motor.scl # 电机控制
│ ├── FB2_Valve.scl # 阀门控制
│ └── FB3_PID.scl # PID调节
├── DB/ # 全局DB模板
│ ├── DB1_System.scl # 系统参数
│ └── DB2_Recipe.scl # 配方数据
└── Project_Template/ # 项目模板(含硬件配置+符号表)
└── Template.ap18 # TIA Portal模板文件
4.2 代码复用三原则
·原则1:接口标准化——所有FB/FC的输入输出命名要统一(如:Start/Stop/Fault/Run)
·原则2:参数可配置——不要写死参数(如延时时间),用Input参数传入
·原则3:注释完整——每个FC/FB都要有"功能说明+调用示例+修改记录"
4.3 实战案例:建立电机控制FB库
# FB1_Motor (V1.0) - 电机控制块
#功能:启停控制 + 运行时间累计 + 故障诊断
#输入:Start (BOOL), Stop (BOOL), Fault (BOOL), Reset (BOOL)
#输出:Run (BOOL), Run_Time (TIME), Fault_Code (INT)
#调用示例:
# CALL FB1_Motor, DB10 (Start := I0.0, Stop := I0.1, Fault := I0.2, Reset := I0.3, Run := Q0.0, Run_Time := MD0, Fault_Code := MW10)
#修改记录:
# V1.0 (2025-01-15): 初始版本
# V1.1 (2025-03-20): 增加"Pause"功能
# V1.2 (2025-06-10): 修复"复位后Run_Time清零"的bug
──────────────────────────────────────────────────
五、三个真实案例复盘
案例1:状态机让调试时间从3天降至3小时
某包装产线,原有程序是"面条代码",调试时需要"一步步跟踪"才能找到问题,平均调试时间3天。
·问题根因:逻辑分散,状态不清晰,调试时"盲人摸象"
·解决方案:用状态机重构程序(枚举所有状态+CASE语句)
·量化结果:调试时间从3天降至3小时,效率提升80%
·经验公式:调试效率 = 1 / 状态清晰度 × 代码可读性
案例2:结构化编程让程序可读性提升80%
某水处理项目,原有程序3000+行全部写在OB1,新员工需要2周才能"看懂"程序。
·问题根因:没有结构化,所有逻辑搅在一起
·解决方案:用FC/FB重构,OB1只写"调用逻辑",每个FC/FB不超过50行
·量化结果:新员工"看懂"程序的时间从2周降至3天,可读性提升80%
·经验公式:代码可读性 = 结构化程度 × 注释完整度 × 命名规范性
案例3:代码复用让新项目开发周期缩短40%
某设备制造商,每个新项目都"从零开始"写PLC程序,开发周期平均6周。
·问题根因:没有代码库,重复"造轮子"
·解决方案:建立PLC代码库(电机/阀门/PID/通信等通用模块)
·量化结果:新项目开发周期从6周降至3.5周,缩短40%
·经验公式:开发效率提升 = 代码复用率 × 代码质量 × 团队熟练度
──────────────────────────────────────────────────
六、PLC高级编程最佳实践总结
#公式1:PLC程序质量
Q_plc = 结构化程度 × 状态机清晰度 × 代码复用率 × 注释完整度
#公式2:调试效率
E_debug = 1 / (状态清晰度 × 代码可读性)
#公式3:开发效率
E_dev = 代码复用率 × 0.4 + 结构化程度 × 0.3 + 经验积累 × 0.3
#公式4:代码库ROI
ROI_library = (节省的工时 × 工时单价 - 代码库建设成本) / 代码库建设成本
#经验值:代码库建设投入50万元,第一年回报约150万元(按节省500工时计算)
【PLC编程 checklist】
结构化:OB1只写调用逻辑,实现逻辑放在FC/FB中
状态机:复杂设备用状态机设计,枚举所有状态+转移条件
命名规范:变量名用"动词+名词"(如Start_Button、Motor_Run)
注释完整:每个FC/FB都要有功能说明+调用示例+修改记录
代码复用:建立自己的PLC代码库,新项目直接"拿来就用"
版本管理:用Git管理PLC代码(TIA Portal支持导出源码)
──────────────────────────────────────────────────
七、一句话总结
PLC高级编程 = 结构化 + 状态机 + 代码复用
核心原则:写PLC程序不是为了"能跑",而是为了"能维护、能复用、能传承"。
──────────────────────────────────────────────────
八、全文公式总结
#公式1:PLC程序质量
Q_plc = 结构化程度 × 状态机清晰度 × 代码复用率 × 注释完整度
#公式2:调试效率
E_debug = 1 / (状态清晰度 × 代码可读性)
#公式3:开发效率
E_dev = 代码复用率 × 0.4 + 结构化程度 × 0.3 + 经验积累 × 0.3
#公式4:代码库ROI
ROI_library = (节省的工时 × 工时单价 - 代码库建设成本) / 代码库建设成本
#公式5:PLC编程终极目标
目标 = 让新成员在3天内"看懂"你的程序,让10年后的人还能"维护"你的程序
──────────────────────────────────────────────────
—— 瓦特与算法