1. 这不是教科书里的PLC课,而是产线老师傅手把手拆给你的IEC61131真功夫
你是不是也遇到过这样的情况:刚拿到PLC编程软件,界面一堆梯形图、功能块、结构化文本选项,点开一个空白编辑区却不知道从哪下手;或者在调试现场,看到设备突然停机,HMI上只显示“FB27_ERR”这种代号,翻遍手册也找不到对应逻辑在哪一层嵌套里;又或者被要求把一段老式继电器控制逻辑改写成符合IEC61131标准的程序,结果改完一上电,电机就狂转不停——不是逻辑写错了,是没搞懂“执行顺序”和“扫描周期”这两个词背后的真实物理含义。
IEC61131标准从来就不是一纸空文,它是全球自动化设备能互相“听懂对方说话”的通用语法。它不教你如何接线,但决定了你写的每一行代码,在CPU里被读取、计算、输出的毫秒级时序;它不规定用什么品牌PLC,却让西门子S7-1200、三菱Q系列、倍福CX系列甚至国产某型控制器,都能用同一套编程思维去理解“一个启动按钮按下后,延时3秒再触发气缸动作”这件事。而所谓“PLC编程基础知识”,也不是指背熟LD、FBD、ST这些缩写字母,而是要明白:为什么梯形图适合描述电气逻辑,而结构化文本更适合做数据运算;为什么同一个定时器,在不同任务级别下会表现出完全不同的响应延迟;为什么你把两个并行分支写进同一个POU(程序组织单元),结果其中一个分支卡死,整个扫描周期就被拖垮了。
这篇文章就是为那些已经站在控制柜前、手里拿着万用表和笔记本、但还没真正“看穿”PLC运行本质的人写的。它不讲抽象理论,只讲我带过的十几个产线改造项目里,反复验证过的底层逻辑、实操陷阱和调试口诀。比如:如何用三行ST代码精准捕捉一个20ms宽的光电开关脉冲,而不依赖硬件滤波;为什么在S7-1500里把“温度超限报警”逻辑放在循环任务里,比放在等时同步任务里更安全;还有那个几乎所有新手都踩过的坑——在FB(功能块)里误用了全局变量,导致多实例调用时数据相互覆盖,最后查了三天才发现问题出在变量声明那一行小小的“RETAIN”勾选框上。接下来的内容,全部来自真实产线、真实故障、真实调试记录,你可以直接抄作业,也可以带着疑问去验证。毕竟,PLC这东西,写得对不对,不是编译通过了就算数,而是设备连续跑72小时不掉站、不误动作、不丢数据,才算真正过关。
2. IEC61131标准不是五种语言的说明书,而是整套工业控制系统的“操作系统内核”
2.1 标准的五个部分到底在管什么?别再被“五种编程语言”带偏了
很多人第一次接触IEC61131,第一反应就是:“哦,有五种编程语言可以选。”然后就开始纠结:该学梯形图还是结构化文本?是不是ST更高级?其实这是个根本性误解。IEC61131-3标准本身只定义了一套可执行模型,而LD(梯形图)、FBD(功能块图)、ST(结构化文本)、IL(指令表)、SFC(顺序功能图)这五种,只是这个模型在不同应用场景下的表现层语法糖。它们不是并列关系,而是同一套底层机制的不同“皮肤”。
举个最典型的例子:一个简单的“启动-保持-停止”电路。用LD画出来,就是常开触点串联自锁回路;用FBD,就是两个AND块加一个OR块连起来;用ST,就是一行Q := (I_start OR Q) AND NOT I_stop;。表面上看写法天差地别,但当你打开PLC的在线监控,观察它们在内存中的执行过程,你会发现:所有写法最终都被编译成相同的字节码序列,占用完全相同的DB(数据块)地址空间,执行时间误差在±0.2μs以内。这说明什么?说明标准真正强制约束的,不是你怎么写,而是你写的逻辑如何被调度、如何被存储、如何与硬件IO交互。
IEC61131标准真正的骨架,其实是另外四个部分:
- IEC61131-1:术语与通用概念。比如什么叫“任务”(Task)、什么叫“POU”(Program Organization Unit)、什么叫“资源”(Resource)。这里定义了整个标准的语言基础。很多初学者看不懂手册,不是因为技术难,是因为连“资源”这个词在标准里特指“CPU上可独立配置执行周期的一组程序”,而不是泛指“可用的内存或CPU时间”,所以后面所有关于多任务调度的描述都成了天书。
- IEC61131-2:PLC硬件要求。它规定了输入滤波时间必须≤10ms、输出响应延迟必须≤100ms、抗干扰等级必须达到IEC61000-4-4 Level 3等硬指标。这意味着,哪怕你用ST写出再完美的算法,如果硬件滤波设置不当,一个电网瞬时跌落就会让你的“启动信号”被误判成三次抖动,从而触发三次启动流程——这不是程序bug,是标准对硬件的底线要求没被满足。
- IEC61131-3:编程语言与系统架构。这才是核心。它定义了POU的三种类型(Program程序、Function函数、Function Block功能块)、变量的作用域规则(LOCAL/IN/OUT/IN_OUT/TEMP/RETAIN)、执行控制流(如SFC中的步与转换条件)、以及最关键的——任务管理模型。一个PLC项目里可以有多个任务,每个任务有自己的优先级、执行周期和绑定的POU。这才是为什么你在S7-1200里能看到“MainTask”、“HighSpeedTask”、“SafetyTask”三个并存的任务——它们不是软件功能模块,而是标准强制要求的、物理隔离的执行容器。
- IEC61131-4:用户导则。这部分最容易被忽略,但它才是连接标准与产线的桥梁。比如它明确规定:当一个FB被多次实例化时,每个实例必须拥有独立的数据存储区(即不能共用DB);又比如它强调:所有与物理IO映射的变量,必须声明为“AT %IXx.x”或“AT %QXx.x”格式,否则在不同品牌PLC移植时,IO地址绑定会彻底错乱。这些不是建议,是标准条款,违反就意味着你的程序不具备可移植性。
所以,与其说IEC61131是“五种语言标准”,不如说它是一套工业控制操作系统的ABI(应用二进制接口)规范。就像Linux系统里,你可以用C写程序,也可以用Python写,但最终都要通过syscalls调用内核服务;IEC61131下的LD、ST、FBD,也都要通过标准定义的“执行控制服务”(Execution Control Service)来申请CPU时间、访问IO、触发中断。理解这一点,你就不会再纠结“该学哪种语言”,而是会问:“这个控制需求,用哪种表现形式最贴近它的物理本质?”
2.2 为什么必须分清Program、Function、Function Block?一个漏油的液压站教会我的事
在某次液压站改造项目中,我们接手了一个运行十年的老系统。原程序用LD写的,逻辑清晰但维护困难。新需求是要增加“油温超限自动降频+声光报警+历史数据记录”功能。团队里一位刚毕业的工程师,直接新建了一个Program,把所有新逻辑都塞进去,还顺手把温度采集、PID调节、报警输出全写在一个大块里。结果上线后,油温正常时一切OK,但一旦触发超限,PLC扫描周期就从8ms飙升到45ms,导致伺服轴位置环严重失步,整条产线抖动。
问题出在哪?就在他没吃透IEC61131对POU类型的强制语义约束。
- Program(程序):是标准里唯一能直接绑定到任务的POU类型。它像一个“主函数”,每次任务触发,就从头到尾执行一遍。它的变量全是静态的(STATIC),生命周期=任务周期。如果你把耗时的PID计算、大数据量的历史记录都塞进Program里,那每一次扫描,CPU都得硬扛着把这些全算一遍,哪怕当前根本不需要更新数据。
- Function(函数):是纯计算单元,没有内部状态,必须有RETURN值,且不能包含任何非确定性操作(如访问IO、调用FB)。它像数学里的f(x)=x²,输入确定,输出就确定。适合做单位换算、查表插值、CRC校验这类无副作用的运算。
- Function Block(功能块):这才是工业控制的“主力军”。它自带私有数据区(Instance DB),可以保存状态(比如定时器的当前值、计数器的累计值),支持多实例化(比如同时控制12台电机,就建12个FB实例,每个都有自己的速度设定值和运行标志)。最关键的是,它的执行是事件驱动的——只有当使能输入(EN)为TRUE时,它才执行内部逻辑;EN为FALSE时,它就“挂起”,不消耗CPU时间。
回到液压站案例。正确做法应该是:
- 把温度采集封装成一个Function,因为它只是把模拟量通道的原始值(如0~27648)线性换算成0~100℃;
- 把“超限判断+降频输出”封装成一个Function Block,里面包含一个TON定时器(用于防抖)、一个MOVE指令(用于输出降频值)、一个SET指令(用于置位报警标志);
- 在主Program里,只调用这个FB,并传入当前温度值和使能信号;
- 把历史数据记录单独做成另一个FB,用上升沿触发(仅在报警首次发生时记录一次),避免每周期都写Flash导致寿命衰减。
这样改完,扫描周期立刻回落到9ms以内。因为FB的执行是受控的,而历史记录FB更是只在特定事件下才激活。这背后,是IEC61131对资源隔离的硬性要求:Program负责调度,FB负责状态管理,Function负责纯计算——三者各司其职,CPU才不会被无谓的逻辑拖垮。
提示:在TIA Portal或Codesys里,新建FB时务必勾选“Multiple instances allowed”,否则即使你写了FB,也无法实现多实例调用。这是新手最容易忽略的配置项,也是导致“为什么我复制了FB实例,数据却串了”的根本原因。
2.3 任务(Task)不是“多线程”,而是“确定性时间片轮转”——产线节拍就是你的时钟源
很多从IT转行做自动化的人,习惯性把PLC的任务理解成“多线程”。这是个危险的类比。操作系统里的线程,切换时机由调度器动态决定,可能毫秒级,也可能微秒级,存在不可预测的延迟;而PLC的任务,是严格按预设周期、确定性执行的硬实时容器。它的存在,不是为了“并发”,而是为了匹配物理世界的节拍。
在汽车焊装车间的一个机器人工作站里,我们部署了三个任务:
- MainTask(10ms周期):负责所有常规逻辑,如夹具到位检测、安全门状态监控、主轴启停控制;
- HighSpeedTask(1ms周期):只绑定一个FB,专门处理激光测距传感器的实时位置反馈,用于动态修正焊接轨迹;
- SafetyTask(5ms周期):独立运行,只包含急停回路、光栅信号处理、安全PLC通信,且与MainTask完全物理隔离(使用不同CPU核心或专用安全协处理器)。
这三个任务绝不是“谁快谁先跑”。它们的执行顺序和时间点,是由硬件时钟源精确锁定的。比如,HighSpeedTask永远在每个1ms整点触发(t=0ms,1ms,2ms…),而MainTask则在每个10ms整点触发(t=0ms,10ms,20ms…)。这意味着,在t=1ms时刻,HighSpeedTask正在读取激光传感器的最新值;而在t=9ms时刻,MainTask可能还在处理上一个周期遗留的报警确认逻辑——两者互不干扰,因为它们的内存空间、寄存器上下文、甚至中断向量表都是独立的。
这种设计带来的直接好处是可预测性。你可以精确计算出:从光电开关检测到工件到达,到气缸开始动作,中间最多经过多少个扫描周期。假设光电开关接入MainTask(10ms),气缸输出也在MainTask里,那么最坏情况就是:开关信号在t=0.1ms被采样,但程序要等到t=10ms才执行到输出指令,所以最大延迟是10ms。这个数字,是机械设计必须预留的安全裕量。
反观如果错误地把高速采样逻辑塞进MainTask,那么一旦MainTask里某个复杂计算耗时超过10ms(比如一次数据库查询),整个周期就崩了,激光反馈延迟可能飙到20ms以上,焊接轨迹偏差直接超标。这就是为什么IEC61131-3标准里,对任务的“确定性执行”有明文要求:任务的最坏执行时间(WCET)必须小于其周期,且必须通过工具链进行静态分析验证。
实操中,任务配置的关键参数只有三个:
- 周期时间(Cycle Time):必须是硬件时钟基准的整数倍,常见值为1ms、2ms、5ms、10ms、100ms。选得太小,CPU空转浪费;选太大,实时性丧失。
- 优先级(Priority):仅在多个任务周期相同时起作用。比如两个10ms任务,高优先级的总在低优先级之前执行。
- 启动延迟(Startup Delay):用于错开同周期任务的首次执行,避免瞬时负载峰值。比如三个10ms任务,分别设延迟0ms、3ms、7ms,就能把CPU占用率拉平。
记住:PLC的任务,不是让你“多做事”,而是让你“在正确的时间,做正确的事”。产线的节拍,就是你的最高指令。
3. PLC编程的四大基石:变量、IO映射、执行顺序、调试方法论
3.1 变量声明不是填空题,而是内存布局的精密设计
在PLC编程里,声明一个变量,远不止是起个名字、选个类型那么简单。它是在为CPU的RAM空间做一次微型“土木工程”——你要规划它的位置、尺寸、访问权限、生命周期,稍有不慎,轻则内存溢出,重则数据错乱。
先看一个典型错误案例。某包装机项目,需要记录每包产品的重量、日期、批次号。工程师新建了一个STRUCT结构体:
TYPE T_PackRecord : STRUCT Weight : REAL; // 4字节 Date : DATE_AND_TIME; // 8字节 BatchNo : STRING[20]; // 21字节(含结束符) END_STRUCT END_TYPE然后声明了一个数组:PackHistory : ARRAY[0..999] OF T_PackRecord;
表面看,1000条记录 × (4+8+21)=33KB,而PLC有64MB RAM,绰绰有余。但上线后,第327条记录开始,Date字段总是显示1970年1月1日——典型的内存越界覆盖。
问题出在字节对齐(Alignment)上。IEC61131标准规定,REAL类型必须按4字节边界对齐,DATE_AND_TIME按8字节,STRING按1字节。但STRUCT内部,编译器会自动插入填充字节(Padding)以满足对齐要求。实际内存布局是:
| 字段 | 偏移 | 大小 | 说明 |
|---|---|---|---|
| Weight | 0 | 4 | REAL,对齐OK |
| Padding | 4 | 4 | 插入4字节,确保下一个8字节对齐 |
| Date | 8 | 8 | DATE_AND_TIME,对齐OK |
| BatchNo | 16 | 21 | STRING,无需对齐 |
所以单个T_PackRecord实际占29字节(0~28),而非33字节。1000条就是29KB。但问题在于,当数组索引i=326时,PackHistory[326].Date的地址是BaseAddr + 326×29 + 8 = BaseAddr + 9454;而PackHistory[327].Weight的地址是BaseAddr + 327×29 = BaseAddr + 9483。两者相差29字节,但Date字段需要8字节空间,而327号记录的起始地址(9483)不是8的倍数,导致Date字段跨了两个内存页——某些PLC固件对此处理不严谨,就返回了默认值。
解决方案不是改数组大小,而是显式控制对齐:
TYPE T_PackRecord : STRUCT Weight : REAL; // 4字节 _Pad1 : BYTE; // 强制占位1字节 _Pad2 : BYTE; // 强制占位1字节 _Pad3 : BYTE; // 强制占位1字节 Date : DATE_AND_TIME; // 从偏移8开始,完美对齐 BatchNo : STRING[20]; END_STRUCT END_TYPE或者更优雅地,用__ALIGNED(8)属性(如果平台支持)。
这引出了变量声明的四大黄金法则:
- 生命周期法则:LOCAL变量在POU执行完即释放;STATIC变量在POU整个生命周期内驻留;RETAIN变量断电后仍保持(需额外电池或超级电容支持)。比如一个计数器的当前值,必须声明为RETAIN,否则断电重启就归零了。
- 作用域法则:IN/OUT/IN_OUT参数只能在调用时传入,不能在FB内部直接赋值;TEMP变量只在本次执行有效,下次执行就清零;GLOBAL变量虽方便,但破坏封装性,应尽量避免。
- 内存效率法则:能用BYTE不用WORD,能用WORD不用DWORD。一个开关量状态,用BOOL(1位)足够,但若声明为INT(16位),就浪费了15位空间。在大型项目中,这种浪费会累积成MB级内存缺口。
- 可读性法则:变量名必须体现物理意义,而非编程逻辑。
Motor1_Running比Flag12好一万倍;Conveyor_Speed_Setpoint比SPD_VAL更易维护。
注意:在Codesys中,STRUCT的默认对齐是按最大成员类型对齐;而在TIA Portal中,可以通过“优化块访问”选项开关来控制是否启用紧凑布局。关闭优化,内存更规整,但访问稍慢;开启优化,内存更省,但调试时变量地址可能跳跃。这是个典型的“性能vs可维护性”权衡,需根据项目规模选择。
3.2 IO映射不是自动的,而是你和硬件之间的“宪法性协议”
PLC程序和物理世界对话的唯一通道,就是IO映射。它不是软件自动识别的魔法,而是一份你必须亲手签署、逐字审核的“宪法性协议”。协议里规定了:哪个内存地址对应哪个端子,数据格式是位还是字,更新时机是立即还是周期性。
最常见的错误,是以为“把程序下载到PLC,IO就自动通了”。真相是:IO映射必须在硬件组态阶段完成,且必须与现场接线100%一致。
以一个典型的数字量输入模块为例(如西门子SM1221):
- 模块有16个通道,编号I0.0 ~ I0.15;
- 每个通道对应一个物理端子(如端子1对应I0.0,端子2对应I0.1);
- 但在PLC程序里,你声明的变量
Start_Button : BOOL AT %I0.0;,这个%I0.0就是映射协议的“法律条文”。
如果现场接线时,把启动按钮接到了端子2(本该是I0.1),而你程序里还写着%I0.0,那按钮永远按不响——不是程序错,是协议违约。
更隐蔽的坑在字节序(Endianness)和数据格式上。比如一个模拟量输入模块(4-20mA),厂商手册写“输出值范围0~27648对应4-20mA”。但这个27648是16位有符号整数(INT)还是无符号整数(UINT)?如果是INT,那0x0000=0,0x6C00=27648,没问题;但如果是UINT,0xFFFF=65535,那27648就只是中间值。如果程序里用INT类型读取,而硬件实际输出UINT,就会出现负数溢出。
解决方法只有一个:用万用表+示波器+PLC在线监控三件套,做端到端验证。
- 给输入端加4mA电流,看PLC监控窗口里对应地址的值是不是0;
- 加20mA,看是不是27648;
- 加12mA(中间值),看是不是13824;
- 同时用示波器抓取模块的SPI或RS485通信波形,确认数据帧格式(如是否含CRC校验、起始位长度)。
这个过程,就是你在亲手签署那份IO宪法。签完,才能放心写逻辑。
对于输出,原则同样严格。比如控制一个电磁阀,程序里写Valve_Open : BOOL AT %Q0.0;,那必须确保:
- 端子Q0.0确实连到了这个阀的线圈;
- 模块供电电压(24VDC)与阀额定电压匹配;
- 模块输出类型(源型/漏型)与阀线圈接法一致(源型输出需接阀的负极,漏型需接正极)。
曾有个项目,电磁阀始终不动作。查了三天,最后发现是模块配置成了源型输出,而现场接线按漏型接的——电流方向反了,阀线圈根本得不到驱动电压。这种问题,100%源于IO映射协议没签清楚。
3.3 执行顺序不是“从上到下”,而是“任务→POU→扫描周期”的三维时空模型
新手写PLC程序,最大的幻觉就是“代码从上到下执行”。在IEC61131的世界里,执行顺序是一个严格的三维模型:时间维度(扫描周期)× 空间维度(任务层级)× 逻辑维度(POU调用栈)。
以一个简单启停控制为例:
// MainTask (10ms) 中的 MainProgram PROGRAM MainProgram VAR MotorCtrl : FB_MotorControl; // 实例化一个FB StartBtn : BOOL AT %I0.0; StopBtn : BOOL AT %I0.1; MotorOut : BOOL AT %Q0.0; END_VAR MotorCtrl( EN := TRUE, Start := StartBtn, Stop := StopBtn, Q => MotorOut );它的执行过程是这样的:
- 时间切片:在t=0ms时刻,CPU硬件定时器触发MainTask;
- 空间加载:CPU从MainTask的配置中,找到它绑定的MainProgram,并为其分配栈空间;
- 逻辑展开:执行MainProgram代码,遇到
MotorCtrl(...)调用,CPU跳转到FB_MotorControl的代码入口,并为其创建独立的Instance DB(含所有STATIC/RETAIN变量); - IO刷新:在任务执行前,CPU先批量读取所有%I地址的物理输入值,存入过程映像区(PII);在任务执行后,再批量把所有%Q地址的输出值,写入过程映像区(PIQ),最后统一刷新到物理输出端子。
关键点在于:整个MainProgram的执行,必须在10ms内完成。如果FB_MotorControl里有个死循环,或者做了耗时的浮点运算,MainTask就会超时,PLC会报“Watchdog Timeout”错误,强制停机。
更复杂的情况是多任务嵌套。比如:
- HighSpeedTask (1ms) 调用 FB_SensorRead,读取编码器值;
- MainTask (10ms) 调用 FB_MotorControl,而FB_MotorControl的内部逻辑又调用了 FB_SensorRead 的一个实例。
这时,执行顺序是:
- t=0ms:HighSpeedTask执行,FB_SensorRead实例A更新编码器值;
- t=1ms:HighSpeedTask再次执行,实例A再次更新;
- ...
- t=9ms:HighSpeedTask第10次执行,实例A最新值已写入共享DB;
- t=10ms:MainTask执行,FB_MotorControl调用时,从共享DB读取实例A的最新值。
你看,这不是简单的“上→下”,而是时间轴上的离散采样 + 空间轴上的模块化调用 + 数据轴上的显式传递。理解这个模型,你才能回答:为什么我在FB里加了个100ms的TON定时器,但实际延时却是110ms?因为定时器的使能信号(EN)是在t=10ms时刻才变TRUE的,而TON的计时是从下一个扫描周期(t=20ms)才开始的——它遵循的是任务周期,不是代码行号。
3.4 调试不是“看变量”,而是“追踪信号在时空中的足迹”
PLC调试的最高境界,不是盯着变量窗口看数值变不变,而是在时间与空间的交叉坐标系里,追踪一个信号从物理世界产生,到被程序捕获,再到驱动执行器的完整足迹。
我总结了一套“四维调试法”,已在十几个项目中验证有效:
第一维:物理层(Physical)
用万用表测端子电压/电流,确认信号真实存在。比如启动按钮按下,I0.0端子电压是否从0V跳到24V?这是所有调试的起点。跳过这一步,后面全是空中楼阁。
第二维:IO层(IO Mapping)
在PLC软件里打开“监控表”,添加地址%I0.0,看它是否随按钮动作实时变化。如果物理层OK,IO层不动,那就是映射配置错误(如模块未使能、地址输错、通道被禁用)。
第三维:逻辑层(Logic Flow)
找到StartBtn变量在程序中的所有引用点。用软件的“交叉引用”功能,列出它被用在哪些FB、哪些表达式里。然后逐个检查:在FB_MotorControl里,Start输入引脚是否真的连到了StartBtn?有没有拼写错误(如StartBnt)?有没有被其他逻辑强制覆盖(如安全回路的Safe_Enable为FALSE时,把Start置成了FALSE)?
第四维:时间层(Timing)
这是最易被忽视,却最关键的维度。用PLC的“趋势图”功能,同时监控三个信号:
StartBtn(原始输入)MotorCtrl.Start(FB的输入引脚)MotorOut(最终输出)
观察它们之间的时间差。正常情况下,三者应在同一扫描周期内完成(<10ms)。如果MotorOut比StartBtn晚了20ms,说明FB_MotorControl的执行被阻塞了——可能是它内部调用了另一个耗时FB,或者有未处理的异常导致程序跳转。
曾有一个案例,趋势图显示MotorOut总比StartBtn晚整整100ms。排查发现,FB_MotorControl里有个TON定时器,但使能条件写成了StartBtn AND NOT MotorOut。结果按钮一按,MotorOut还是FALSE,所以NOT MotorOut为TRUE,定时器开始计时;但定时器一到时,MotorOut变成TRUE,NOT MotorOut立刻变FALSE,定时器复位——于是它永远在“启动→计时→复位”的死循环里,每次循环耗时100ms。问题不在硬件,不在IO,而在时间维度上的逻辑悖论。
实操心得:在Codesys里,按Ctrl+Shift+F7可以快速打开“变量趋势”,选中多个变量,设置采样间隔(建议设为任务周期的1/10,如1ms任务设0.1ms),就能看到毫秒级的信号时序。这是诊断“为什么动作慢半拍”的终极武器。
4. 从入门到可靠:六个必须跨越的认知门槛与避坑清单
4.1 认知门槛一:PLC不是计算机,是“确定性状态机”
绝大多数编程思维的灾难,都源于把PLC当成一台小型计算机。计算机追求“功能丰富、响应灵活、界面友好”,PLC追求“确定性、鲁棒性、可预测性”。前者可以容忍几秒的卡顿,后者毫秒级的延迟都可能导致设备撞机。
所以,PLC编程的第一戒律是:永远不要做“不确定时间”的事。
- ❌ 不要调用系统时间函数(如TIME_OF_DAY)做核心逻辑判断,因为时钟精度受RTC晶振影响,可能漂移;
- ❌ 不要写递归函数,栈空间有限,深度不可控;
- ❌ 不要在循环里做未设上限的等待(如
WHILE NOT Flag DO END_WHILE),万一Flag永远不置位,程序就死锁; - ✅ 正确做法是:用TON定时器替代时间函数;用FOR循环替代WHILE(明确指定次数);用状态机(SFC)替代复杂的条件嵌套。
一个经典的状态机设计:自动灌装机的“灌装→封盖→贴标”流程。不用写几十行IF-ELSE,而是用SFC画三个步(Step),每个步里放对应的输出指令,步与步之间用转换条件(Transition)连接,比如“灌装完成且液位传感器为TRUE”。这样,逻辑清晰、易于验证、时间可控——每一步的执行时间,就是它所绑定任务的周期。
4.2 认知门槛二:错误不是Bug,是设计缺陷的必然暴露
在PLC世界里,“程序没报错”不等于“程序正确”。IEC61131标准允许大量语法合法但语义危险的写法。比如:
Q := I1 AND I2 OR I3;这行ST代码,编译完全通过,但它的执行顺序是(I1 AND I2) OR I3,还是I1 AND (I2 OR I3)?不同PLC厂商的运算符优先级可能不同!FB1(IN1:=X, IN2:=X);把同一个变量X同时传给两个输入引脚,如果FB1内部对IN1和IN2做了不同处理,结果就不可预测。
所以,真正的调试,不是找“哪里报错”,而是做防御性设计:
- 所有布尔表达式,强制加括号,明确优先级;
- 所有FB调用,输入引脚必须用独立变量,禁止复用;
- 所有关键输出,必须有“失效安全”逻辑,比如电机输出
MotorOut,必须配一个MotorSafe变量,当安全回路断开时,MotorSafe强制为FALSE,MotorOut := MotorCmd AND MotorSafe;。
这就是为什么IEC61131-3标准里,专门有一章讲“安全相关系统的设计指南”。它不是教你怎么写安全PLC,而是告诉你:普通PLC程序,也必须按安全逻辑来设计。
4.3 认知门槛三:文档不是附属品,是程序不可分割的DNA
很多工程师觉得“写完程序就完了”,文档是交差时才补的。在IEC61131项目里,这是自杀行为。标准明确要求:所有POU必须有接口文档(Interface Documentation),说明每个输入/输出引脚的物理意义、数据类型、有效范围、更新时机。
一份合格的FB文档,应该长这样:
FB_MotorControl - 三相异步电机控制功能块 【功能】实现电机启停、正反转、故障复位,支持本地/远程模式切换 【输入】 EN : BOOL // 使能信号,上升沿触发初始化 Start : BOOL // 启动命令,高电平有效,需持续至少1个扫描周期 Stop : BOOL // 停止命令,高电平有效,立即生效 Mode : INT // 运行模式:0=本地,1=远程,2=维护(仅限授权) 【输出】 Q : BOOL // 电机输出,仅在Mode=1且无故障时为TRUE Fault : BOOL // 故障标志,高电平表示过载/短路/通讯失败 Status : WORD // 状态字,bit0=运行中,bit1=正转,bit2=反转... 【注意事项】 - 必须在MainTask(10ms)中调用,否则定时器精度无法保证 - Mode切换时,需先Stop再切换,否则可能触发保护 - Fault为TRUE时,必须调用Reset()方法清除,否则Q保持FALSE这份文档,不是给人看的,是给未来的你、给接班的同事、给审计的第三方看的。它和代码一样,是程序的一部分。没有它,你的程序就是一座没有图纸的桥,看起来结实,但没人敢走。
4.4 避坑清单:六个血泪教训换来的实操守则
以下是我从十几个项目里,用真金白银和停产损失换来的六条铁律,每一条都对应一个真实翻车现场:
- “绝对不要在FB里用全局变量”
某输送线项目,为图省事,在FB里直接读写Global_DB.SpeedRef。结果当输送线A和B同时调用该FB时,A改了SpeedRef,B立刻跟着变——两条线速被迫同步。正确做法:所有FB必须通过IN