1. 从继电器到软逻辑:IEC 61131标准到底解决了什么问题
干了十几年工控,每次带新人,我都会先问一个问题:你觉得PLC编程和单片机编程最大的区别是什么?大部分人答不上来。其实答案就藏在IEC 61131这个标准里。它不是某个厂商的私有规范,而是整个工业控制领域的一次“书同文、车同轨”。在没有这套标准之前,你写三菱的梯形图是一套逻辑,换到西门子又是另一套语法,项目移植的成本高得离谱。IEC 61131的出现,本质上是给PLC编程定义了一套通用的语言框架和工程模型,让代码在不同品牌、不同硬件平台之间有了对话的基础。
这套标准全称是《可编程控制器系统标准》,由国际电工委员会发布,总共分为十个部分,但真正和一线编程打交道的主要是第三部分——编程语言。它定义了五种编程语言:梯形图(LD)、功能块图(FBD)、顺序功能图(SFC)、指令表(IL)和结构化文本(ST)。前三种是图形化语言,后两种是文本化语言。这个分类不是拍脑袋定的,而是对应了不同场景下工程师的思维习惯。比如电气出身的人看梯形图就像看继电器回路,一目了然;而计算机背景的人更习惯用ST写算法,逻辑紧凑、可读性强。
我见过太多项目因为选错了语言而埋下隐患。有个做包装线的朋友,用梯形图硬写了一套复杂的PID温控算法,结果调试了整整两周,代码臃肿到没人愿意接手维护。后来换成ST重写,核心逻辑不到五十行,参数整定也清晰了。这不是说梯形图不好,而是每种语言都有它的“舒适区”。IEC 61131的价值就在于,它给了你选择的自由,同时也要求你理解每种语言背后的适用边界。
对于刚入行的朋友,我的建议是先吃透梯形图和ST这两种。梯形图是基本功,它能帮你建立“扫描周期”“能流”“软元件”这些核心概念;ST则是进阶利器,处理数学运算、字符串操作、状态机时效率极高。至于FBD和SFC,在特定场景下很好用,比如FBD适合做信号流清晰的逻辑组合,SFC适合写工序步进控制,但不必一开始就全部掌握。关键是理解这套标准背后的统一数据模型——变量、数据类型、程序组织单元,这些才是跨平台移植的根基。
2. 五种编程语言的选择逻辑与实战避坑
2.1 梯形图:电气工程师的母语,但别把它当万能钥匙
梯形图(Ladder Diagram)是PLC编程里最古老也最直观的语言。它的符号体系直接脱胎于继电器控制电路:常开触点、常闭触点、线圈、定时器、计数器,这些元素对电气背景的人来说几乎零学习成本。你画一个启动-保持-停止回路,和实际接线的逻辑完全一致,这种“所见即所得”的特性让它在离散制造领域统治了几十年。
但梯形图的局限也很明显。它的执行模型是基于能流从左到右、从上到下,这意味着复杂的条件判断和循环操作写起来非常别扭。我见过有人用梯形图实现冒泡排序,那代码简直像用筷子吃牛排——不是不行,是没必要。梯形图最适合的是布尔逻辑、互锁、顺序控制这类场景。一旦涉及浮点运算、数组操作、复杂条件分支,就应该果断换ST。
还有一个坑是“双线圈”问题。在梯形图里,同一个输出线圈如果在不同位置被多次驱动,实际生效的是最后一个扫描周期的结果。新手经常在这里翻车,明明逻辑写对了,输出就是不对。解决办法是养成“一个线圈只出现一次”的习惯,需要多条件驱动时用中间变量合并。这个规则在IEC 61131的规范里虽然没有强制,但所有主流编程软件都会给出警告,听劝就对了。
2.2 结构化文本:算法密集型任务的正确打开方式
结构化文本(Structured Text)的语法类似Pascal,支持条件语句、循环语句、函数调用,是五种语言里最接近通用编程语言的。它的优势在于代码紧凑、可读性强,特别适合写PID控制、运动轨迹计算、通信协议解析这类逻辑复杂的模块。
我个人的习惯是:凡是能用数学公式描述的,一律用ST。比如一个简单的斜坡函数发生器,用ST写就是几行代码的事,用梯形图得搭一堆定时器和加法器。但ST也有它的“坑”。首先是执行顺序问题,ST代码是按行顺序执行的,如果你在程序开头读取了某个输入,在程序结尾又去读同一个输入,中间如果被其他任务修改了,结果可能和你预期的不一样。所以关键变量最好在程序开头统一采样,后面都用采样值。
另一个坑是数据类型的隐式转换。ST对类型检查比梯形图严格得多,但有些平台为了兼容性会做隐式转换,导致精度丢失或溢出。比如把一个REAL类型的值赋给INT变量,小数部分直接被截断,而且不报错。这种问题在调试时极难发现,因为逻辑看起来完全正确。我的做法是:所有变量声明时明确类型,赋值时如果类型不一致,手动加转换函数,宁可多写一行代码,也不留隐患。
2.3 功能块图与顺序功能图:特定场景的利器
功能块图(FBD)用方框和连线表示逻辑关系,看起来像电路图,适合做信号处理和逻辑组合。它的优点是数据流清晰,一个方框的输出连到另一个方框的输入,一眼就能看出信号走向。但FBD的布局很占空间,复杂逻辑会画得像蜘蛛网,维护起来头疼。我一般只在做模拟量处理时用FBD,比如滤波、标定、报警限值判断,这些逻辑用方框表示比梯形图更直观。
顺序功能图(SFC)则是专门为步进控制设计的。它把控制过程分解成一个个“步”,每个步有进入动作、退出动作和转移条件。这种模型和很多实际设备的运行逻辑天然吻合,比如注塑机的合模-注射-保压-冷却-开模流程,用SFC写出来结构清晰,调试时也能快速定位到卡在哪个步。但SFC的坑在于“步”的激活和去激活时机,如果转移条件写得不好,容易出现多个步同时激活或者步死锁。我的经验是:每个转移条件必须互斥,而且尽量用“与”逻辑,避免复杂的“或”嵌套。
2.4 指令表:正在消失的语言,但值得了解
指令表(IL)是一种汇编风格的文本语言,每条指令对应一个操作。它在早期PLC上很常见,因为执行效率高、占用内存小。但现在的主流编程软件基本都把它藏起来了,甚至有些新平台直接不支持。不过了解IL有助于理解PLC的底层执行机制,比如累加器的概念、跳转指令的实现。如果你维护的是老设备,可能还会遇到IL代码,看懂它不难,但没必要用它写新项目。
3. 程序组织单元与变量作用域:代码复用的基石
3.1 功能、功能块与程序的本质区别
IEC 61131定义了三种程序组织单元:功能(Function)、功能块(Function Block)和程序(Program)。这三者的区别是新手最容易混淆的地方,但理解之后,你的代码组织能力会有一个质的飞跃。
功能(FC)是没有内部状态的,同样的输入永远得到同样的输出。比如一个计算平均值的功能,你给它五个数,它返回平均值,下次再调用,它不会记得上次算过什么。功能适合做纯数学运算、类型转换、单位换算这类无状态操作。
功能块(FB)则是有状态的。它内部有静态变量,每次调用时这些变量的值会保留。最典型的例子是定时器:你调用一个TON功能块,给它使能信号和预设时间,它内部会记录已经计时了多久。下次扫描周期再调用同一个功能块实例,它会接着上次的状态继续。功能块适合做需要记忆的操作,比如PID控制器、计数器、状态机。
程序(PRG)是最高层的组织单元,它可以包含功能、功能块和其他程序的调用。程序通常对应一个具体的控制任务,比如“主控程序”“报警处理程序”“通信程序”。在资源受限的PLC上,程序的数量和大小都有限制,所以合理划分程序边界很重要。
我见过一个典型的错误:有人把PID控制写成了功能(FC),结果每次调用都重新初始化,积分项永远累积不起来,控制效果一塌糊涂。这就是没理解“状态”的概念。记住一句话:需要记住上次结果的,用功能块;纯计算不依赖历史的,用功能。
3.2 变量作用域:全局与局部的取舍
变量作用域决定了变量在多大范围内可见。IEC 61131把变量分为全局变量和局部变量。全局变量在整个项目里都能访问,局部变量只在所属的程序组织单元内有效。
新手往往喜欢把所有变量都定义成全局的,觉得这样方便,哪里都能用。但这是埋雷。全局变量多了之后,你根本不知道哪个程序修改了它,调试时像大海捞针。而且全局变量会增加程序间的耦合,一个地方改了,另一个地方可能就出问题。
我的原则是:能用局部变量就用局部变量,全局变量只保留三类——硬件输入输出映射、跨程序共享的关键状态、系统配置参数。局部变量命名可以简短,因为作用域小,不会混淆;全局变量命名必须带前缀,比如g_开头,一眼就能识别。
还有一个细节是变量的保持性。有些变量在断电后需要保持,比如累计产量、配方参数。IEC 61131没有强制规定保持变量的实现方式,但主流平台都支持在变量声明时加RETAIN或PERSISTENT修饰符。注意:保持变量会占用非易失存储空间,而且写入速度比普通变量慢,不要滥用。我一般只对真正需要断电保持的数据加这个修饰符,比如设备运行总时长、校准系数。
3.3 数据类型:别让隐式转换坑了你
IEC 61131定义了一套标准数据类型,包括布尔型(BOOL)、整型(INT、DINT)、浮点型(REAL、LREAL)、字符串(STRING)、时间(TIME)等。这些类型在不同平台上的位宽可能略有差异,比如INT在某些平台是16位,在另一些平台是32位。写跨平台代码时,最好用标准里明确位宽的类型,比如SINT(8位)、INT(16位)、DINT(32位)。
隐式转换是另一个重灾区。比如你把一个REAL值赋给INT变量,有些平台会四舍五入,有些会直接截断,有些甚至不报错但结果完全错误。我的做法是:所有类型转换都用显式函数,比如REAL_TO_INT()、INT_TO_REAL(),虽然多打几个字,但避免了平台差异带来的不确定性。
还有一个容易被忽略的是字符串处理。IEC 61131的STRING类型通常有长度限制,比如255个字符。如果你拼接的字符串超过这个长度,会被截断,而且不一定报错。做通信协议解析时,这个坑特别常见。解决办法是:拼接前先计算长度,超长就报警或分段处理。
4. 任务调度与扫描周期:理解PLC的“心跳”
4.1 扫描周期的本质与影响因素
PLC的工作方式是循环扫描:读取输入、执行程序、刷新输出,然后从头再来。这个循环一次的时间就是扫描周期。扫描周期不是固定的,它取决于程序大小、指令复杂度、通信负载等因素。一个简单的逻辑程序可能只要几毫秒,而一个复杂的运动控制程序可能要几十毫秒。
扫描周期直接影响控制精度。比如你做一个高速计数应用,如果扫描周期是10毫秒,那么两次扫描之间发生的脉冲就可能丢失。解决办法是用硬件中断或高速计数模块,让计数不依赖扫描周期。但中断程序要尽量短,否则会拖慢主程序。
我见过一个案例:某设备用普通输入点检测工件到位,然后触发气缸动作。工件移动速度很快,扫描周期又比较长,结果工件已经过去了,程序才读到输入信号,气缸动作滞后,产品直接报废。后来换成中断输入,问题立刻解决。这个教训是:高速信号一定要用中断或专用模块,别指望普通扫描能抓住。
4.2 任务优先级与周期任务配置
IEC 61131支持多任务调度,你可以定义不同优先级的任务,比如高速任务、中速任务、低速任务。高速任务用于处理紧急事件,比如急停、限位保护;低速任务用于处理人机界面刷新、数据记录这类对实时性要求不高的操作。
配置任务时要注意:高优先级任务会抢占低优先级任务的执行时间。如果高优先级任务执行太频繁或耗时太长,低优先级任务可能永远得不到执行,这叫“任务饥饿”。我的经验是:高优先级任务的周期至少是低优先级任务周期的十分之一,而且高优先级任务的代码要尽可能短。
还有一个坑是任务间的数据共享。如果两个任务同时读写同一个变量,可能出现数据不一致。解决办法是用信号量或双缓冲机制。不过大多数PLC平台对简单变量读写是原子的,不会出现“读一半被中断”的情况,但结构体或数组的读写就不一定了。保险起见,跨任务共享的数据要么加锁,要么用单变量传递。
4.3 程序执行顺序的隐形陷阱
在同一个任务里,程序的执行顺序是从上到下。这意味着如果你在程序A里修改了一个变量,程序B在后面读取这个变量,读到的是修改后的值;如果程序B在程序A前面,读到的就是旧值。这个顺序依赖在梯形图里尤其隐蔽,因为梯形图的网络顺序就是执行顺序。
我调试过一个故障:两个程序分别控制两台电机,要求电机A先启动,延时后电机B启动。结果实际运行时电机B偶尔会先动。查了半天发现是程序顺序反了,电机B的控制程序排在电机A前面,虽然逻辑上写了延时,但延时定时器还没开始计时,电机B的启动条件就已经满足了。把程序顺序调换后问题消失。这个教训是:有依赖关系的程序,一定要按依赖顺序排列,并且在注释里写清楚。
5. 从标准到落地:常见问题与排查技巧实录
5.1 变量未初始化导致的随机故障
IEC 61131标准规定,变量在声明时可以指定初始值。但如果你不指定,大多数平台会默认初始化为0或FALSE。问题在于,有些平台的局部变量在每次调用时不会自动重置,而是保留上次的值。如果你依赖变量初始值为0,但实际保留了上次的残值,逻辑就会出错。
我遇到过一个典型案例:一个功能块内部有个计数器变量,用于统计调用次数。第一次调用时计数器从0开始,正常。但设备运行一段时间后,功能块被重新实例化,计数器没有重置,结果统计值从上次的值继续累加,导致报警阈值提前触发。解决办法是在功能块内部显式初始化所有静态变量,或者在首次调用时用初始化标志位重置。
提示:不要依赖平台的默认初始化行为,所有变量声明时都显式赋初值,尤其是功能块内部的静态变量。
5.2 浮点数比较的精度陷阱
浮点数在PLC里是近似表示,两个理论上相等的浮点数,实际可能差一个极小值。如果你用IF a = b THEN判断,可能永远不成立。正确的做法是判断差值是否小于一个容差,比如IF ABS(a - b) < 0.001 THEN。
这个坑在PID控制里特别常见。比如你判断温度是否达到设定值,用等于判断,结果温度在设定值附近波动,永远不满足条件。改成容差判断后,逻辑立刻正常。容差取多大取决于你的控制精度,一般取传感器分辨率的1到2倍。
5.3 通信超时与数据一致性
PLC和上位机、触摸屏、其他PLC之间的通信,经常遇到超时或数据错位。常见原因有三个:一是通信负载太重,扫描周期被拉长;二是数据没有打包发送,多个变量分开读写,中间被其他任务插入;三是没有校验机制,数据错了也不知道。
我的做法是:关键数据打包成一个结构体,一次性发送,接收方也一次性读取。这样保证数据的一致性。另外加一个心跳计数器,每次发送递增,接收方发现心跳不变化就知道通信断了。对于模拟量数据,还可以加CRC校验,虽然增加一点开销,但能避免错误数据导致误动作。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| 输出不动作 | 双线圈、程序顺序、扫描周期 | 检查输出线圈是否多次出现,确认程序执行顺序 | 合并线圈驱动条件,调整程序顺序 |
| 定时器不准 | 扫描周期影响、定时器类型选错 | 测量实际扫描周期,确认定时器是TON还是TOF | 改用中断定时或高精度定时器 |
| 模拟量波动大 | 滤波不足、接地干扰、量程配置错误 | 检查滤波参数,测量信号地是否干净 | 增加滤波时间常数,检查屏蔽接地 |
| 通信断断续续 | 负载过重、线缆过长、波特率不匹配 | 监控通信负载率,检查线缆规格 | 降低通信频率,缩短线缆,统一波特率 |
| 变量值异常 | 隐式转换、作用域冲突、未初始化 | 检查变量声明和赋值语句 | 显式转换类型,缩小作用域,初始化变量 |
5.5 独家避坑心得
干了这么多年,我总结了几条“血泪经验”。第一条:任何超过三行的逻辑,都要写注释。不是给被人看,是给三个月后的自己看。第二条:调试时先强制变量,再跑实际信号。强制能快速验证逻辑对不对,实际信号能验证接线和配置对不对,分开排查效率高。第三条:版本管理很重要。每次修改前备份,改完后记录改了什么、为什么改。我见过太多人改来改去,最后发现还是第一版对,但第一版已经找不到了。
还有一条:不要迷信“标准写法”。IEC 61131给了框架,但具体怎么写,取决于你的设备、你的工艺、你的维护团队。适合的才是最好的。比如有些老设备只支持梯形图,你非要用ST,那就是给自己找麻烦。反过来,新项目如果团队都会ST,那就大胆用,效率确实高。
最后说一个容易被忽略的点:PLC程序的“可测试性”。写代码时就要想好怎么测试。比如把核心算法封装成功能块,输入输出明确,这样可以在不接实际硬件的情况下用仿真测试。我现在的习惯是:每个功能块都配一个测试用例,输入几组典型值,验证输出是否正确。这个习惯让我在项目现场少熬了很多夜。