PLC结构化文本设计模式和算法

前言

.
  目前PLC应用学科并没有设计模式方面的教程,导致学员解题都得从零开始设计。梯形图无法发挥出PLC的真正功能,所以我们需要以结构化文本来实现设计模式。本文以普及结构化文本为目的,将教大家结构化文本的入门和PLC编程设计模式。
  本文将会讲解高度抽象的设计模式,设计模式与案例无关,并不以案例讲题。反正学校按这种方式教了那么久也没人听得懂。我将主要以理论为主,避免与具体案例绑定,以防止读者先入为主,以为某一设计模式只能适用于某案例,或某一设计模式只能单独使用,不能结合其它设计模式,又或者某一案例只适合某一设计模式。
  为方便读者查阅,我将把所有的内容放在同一篇文章中,以免大家到处找章节。

结构化文本入门

.  要学习设计模式,我们需要先学习结构化文本。本章将带领新手学习结构化文本的语法。

Hello World

.
  学习一门编程语言,总是从Hello World开始的。所谓Hello World就是输出流测试程序,用来测试机器是否正常的。PLC自然不能输出字母,它只能输出开关量和模拟量。
  首先是创建工程。打开GX Work2,按顺序点击键盘上的这些键,你将看到屏幕上的动作。

  • Alt
  • P
  • N
  • ↓(工程类型选结构化工程)
  • Alt + S
  • 在选中的地方用鼠标选择FXCPU
  • Alt + T
  • 在选中的地方用鼠标选择FX3U/FX3UC(为了接地气,我将使用FX3U为大家介绍结构化文本)
  • Alt + G
  • 在鼠标选中的地方选择 ST(三菱的结构化文本名为ST,西门子为SCL)
  • 按Enter键
    .
      默认打开"POU_01[PRG]程序本体",也就是主程序。此外还有一个未激活的标签叫"局部标签设置POU_01",这是主程序的局部变量表。需要注意的是,三菱的所有局部变量都是静态变量,也就是里面存储的信息能够跨扫描周期使用,PLC不会在下一个扫描周期对它进行自动初始化。而西门子PLC有保持或不保持的选项。
      现在,我们在POU_01程序本体中写下第一个工程。
Y0 := TRUE;
K1Y1 := HF;

.
  现在按步骤按下:

  • Shift + Alt + F4
  • Enter(美式英语叫"Enter",英式英语叫"Entre")
      进行编译。
      现在按下:
  • Alt
  • O
  • W
    .
      现在你将看到一个叫“在线数据操作”的弹窗。现在按下:
  • Alt + P(参数+程序)
  • Alt + E(执行)
  • 一路点确认(Y),直到小弹窗消失,编译完成。
  • Esc(退出"在线数据操作")
    .
      你将看到PLC的Y0~Y4指示灯全亮了。
      这两行代码是什么意思呢?
      第一行Y0 := TRUE;代表设置Y0为导通,实际对应的单片机输出一般是低电平,也就是上拉电路在输入低电平时导通。结构化文本(不论三菱还是西门子)都是用冒号等号的赋值表达式,这是PASCAL时代的编程风格,PASCAL风格在新生代编程语言中已经被整体淘汰。
      第二行K1Y1 := HF;是指从Y1开始的4个连续端口设置为二进制1111,也就是四路导通。其中,K1是指4个连续端口。为什么是4个,1位16进制数代表4个端口。这个K1是指“1位16进制数”的意思,也就是4个开关量。如果改为K2,就是“2位16进制数”,代表8个开关量。Y1表示从Y1端口开始计数,实际上就是C语言里的起始指针,数组指针之类的含义。Y很好理解,为什么要用K开头呢?K在PLC符号中代表后面的文本将作为十进制数字使用,然而数字不能用于标识符的第一个字母,这是所有编程语言的通例,K1Y1显然是一个标识符而不是纯数字,所以这个K不能省略。后面我们在编程的时候将对十进制的数值省略这个K,写上也是合法的,但我一般不写。HF就是16进制数F,二进制1111。H和K一样,是一个暗号,告诉编译器后面将会有一个数值,而H代表后面是16进制数值。那么是高位的1代表Y1还是低位的1代表Y1呢。我们可以把这个HF改成H1,这样就知道16进制数的位与输出端口的关系了。编译下载后可以看到,这时只有Y1亮了,说明它是低位对应小端口号的。
      三菱ST编程相对西门子更简单,因为它可以直接使用X、Y、M、D这几个系统标识,而西门子变量和端口全部都要声明,然后定义到指定地址才能使用。
      现在我们来做一个记忆按钮,就是按下X0的时候,如果Y0是亮的,就让它灭,如果是灭的就让它亮。我将使用单片机的风格编写,这种程序并不建议在三菱PLC上实际应用,仅仅为了讲课临时编了一个低级程序,供大家学习,但请不要模仿。
M1 := X0 AND NOT M0;(* 上升沿检测,如果X0是通,M0是断,则M1是通 *)
Y0 := Y0 XOR M1;(* 上升沿的时候转换状态,如果Y0和M1中通的个数为单数,则Y0为通,否则为断 *)
M0 := X0;(* 记忆X0的上一个状态 *)

简单逻辑操作

.
  PLC的经典用法就是串并联逻辑电路,ST语言的串联用AND操作符,并联用OR操作符,动断触点(常闭)用NOT操作符,此外还支持奇校验XOR操作符。
  这里做一个自锁电路来演示串并联逻辑。这个电路的功能是X0为通时Y0设置为通,X1为通时Y0设置为断。自锁是继电控制的常用结构,但在PLC中属于外行的做法,请大家不要模仿。

Y0 := NOT X1 AND (X0 OR Y0);

.
  也就是将X0和Y0并联,再与X1的动断触点串联。对于梯形图,这行代码有一个很不好的顺序,NOT X1被放在左边了。应该把NOT X1放在AND右边才是优秀的梯形图。但是ST编译器会自动优化顺序,写ST程序的时候不需要在意顺序问题。另外,自锁是外行的程序,本身这行代码就不应该出现在正式的工程中。
  在梯形图中,有一种专业的自锁电路的替代方法,就是SET和RST指令。它们在ST中被封装成系统宏函数。下面演示一下这种方法。这种方法在ST中是比较常用的。

SET(X0, Y0);(* 按下X0时,Y0置位 *)
RST(X1, Y0);(* 按下X1时,Y0复位 *)

.
  这是推荐使用的方法,简单易懂。ST宏函数和系统功能块非常多,我们能够记住一些指令就很不容易了,何况它们还有不同的参数。GX Work2提供了一个快捷键,Ctrl + 1,把插入点定位在写好的函数名上,按Ctrl + 1,IDE会自动插入参数提示。如果连函数名也不记得了,可以在右边的窗口双击插入函数名或功能块名。西门子Portol则是从左边的自定义程序和右边的系统程序中拖拽到工作区。我们学程序,学结构化文本、C语言、C++、Java、VB、Matlab都不应该去背它的函数,因为根本背不完。应该善用IDE提供的提示功能,否则一个人根本无法掌握任何一种编程语言。
  系统函数的第一个形参都是使能标志,相当于每一个系统函数内置了一个条件表达式,只有当第一个形参传入TRUE(通)的时候,才执行指令的功能。
  SET的第二个形参就是要操作的位寄存器。可以填Y和M开头的寄存器,也可以写入用户自己声明的位变量。RST的第二个形参可以是任何可写的寄存器,支持Y、M、D、CN、TN,也可以用于用户自己声明的任何变量类型(Bit、SWord、UWord、SDWord、UDWord)。
  我们再做一个三联开关,X0,X1,X2的状态变化都会引起Y0翻转。

Y0 := X0 XOR X1 XOR X2;

.
  这个程序在X0,X1,X2之中任意一个改变状态时,都会使Y0状态变化,也就是任意一个开关都可以自由地操作灯的亮灭。这个表达式如果要用标准的串并联梯形图来做,会非常麻烦。连续XOR表达式也叫“奇校验”就是输入点中TRUE的个数为单数的时候,结果为TRUE,双数的时候结果为FALSE。而连续WOR同或运算中WOR出现的次数为奇数时是偶校验,出现的次数为偶数时是奇校验。因为偶校验没有简单的规律,容易编程失误,所以我们做逻辑运算多用的是奇校验公式,很少用到偶校验公式。当奇校验公式中有奇数个常量TRUE时,根据奇校验的算法,可以证明它等价于偶校验,但比偶校验公式更不容易出错。

上升沿与下降沿

.
  ST中的上升沿检测使用LDP,下降沿使用LDF。
  下面这个程序演示了一个升级版的记忆按钮:

Y0 := Y0 XOR LDP(TRUE, X0);

.
  LDP的第一个形参还是使能的意思,几乎所有的指令都有很大概率在第一个形参写TRUE,因为三菱PLC所有的指令都需要使能,而多数情况下,这些指令是需要永久使能的。
  LDP的第二个形参是上升沿检测的变量,这里检测的是X0的上升沿。实际上对应单片机内部电路,X0的引脚可能是收到了下降沿,这个LDP是指的从断到通的状态,与单片机电平方向无关。当收到上升沿的时候,LDP返回一个扫描周期的TRUE,TRUE与XOR组合,就等价于一个NOT,就将Y0翻转了,而LDP只返回一个扫描周期的TRUE,所以也只有这一个扫描周期的XOR等价于NOT,其余扫描周期则为XOR FALSE,也就是不起作用。这样就实现了X0在从断到通时Y0会发生状态翻转,也就是记忆按钮的效果。

定时器和计数器

.
  梯形图剩下的功能就只有定时器和计数器了。下面演示一下定时器和计数器的用法。这个程序功能为按下X0两次后,Y0输出1秒的通,然后变回断。

OUT_C(X0, CC0, 2);(* 对X0计数,它是边沿有效的,计数器C0,CC0是C0的线圈,第2个C代表"Coin" *)
SET(CS0, M0);(* 达到2的时候,CS0变为TRUE,对M0置位,将M0也变为TRUE,CS0是C0的触点,第2个S代表"Switch"开关的意思 *)
RST(M0, CN0);(* M0为TRUE时,复位计数器C0,M0为TRUE时,按X0不会计数,因为它一直被复位,CN0是C0的数值,第2个N是"Number"数字的意思 *)
OUT_T(M0, TC0, 10);(* M0为TRUE时,定时器T0开始计时,三菱FX3U定时器单位是100ms,第3个参数10表示1秒 *)
RST(TS0, M0);(* 时间到的时候TS0为TRUE,这一步就会复位M0,将系统状态变回起始值 *)
Y0 := M0;(* 将M0的值从Y0输出 *)

.
  只需要学到这种程度,读者就已经掌握了用ST完成梯形图的所有功能了。接下来,进入ST不同于梯形图的部分。

条件分支IF-THEN…ELSE_IF-THEN…ELSE…END_IF;

.
  我们再来试试自锁电路的一种标准化写法,它同样具有很好的可读性。

IF X0 THEN
	Y0 := TRUE;
ELSE_IF X1 THEN
	Y0 := FALSE;
END_IF;(* 注意:这个END_IF是要加分号结尾的,ST语言坑死人,多少新手被坑在这里好久才发现突然需要一个分号 *)

.
  比起SET和RST,使用了条件分支使程序看起来更像是说明文档。它的大意是:“如果X0通了,则让Y0也通,如果X0没通,但是X1通了,就让Y0断开,如果以上条件都不满足,就什么也不做,保持之前的状态。”
  再来一个自锁(X0,X1)+强制点动(X2)+翻转(X3)的综合型开关电路,动力输出设计为Y4,并且每当按下按钮时,会有对应的指示灯亮起,指示灯设计Yn对应Xn:

K1Y0 := K1X0;(* 4路指示灯直接赋值 *)
IF X0 THEN
	Y4 := TRUE;(* 启动 *)
ELSE_IF X1 THEN
	Y4 := FALSE;(* 停止 *)
ELSE_IF LDP(TRUE, X3) THEN
	Y4 := NOT Y4;(* 翻转 *)
ELSE_IF LDP(TRUE, X2) OR LDF(TRUE, X2) THEN
	Y4 := X2;(* X2动作时,取消记忆,强制变为手动 *)
END_IF;(* 再次强调:这个END_IF是要加分号结尾的,ST语言坑死人,多少新手被坑在这里好久才发现突然需要一个分号 *)

.
  读者可以自己试试用梯形图串并联方式实现这个电路,一个简单的功能做成梯形图也会非常复杂。
  条件分支的完整模板为

IF conditions THEN
	tasks;(* 语法上IF语句任何一段分支的tasks都可以是空的。如果是空的,分号也可以不要 *)
ELSE_IF conditions THEN (* 如果没有其它条件,ELSE_IF必须省略,如果有多个,ELSE_IF应当有很多,语法上没有上限,可以写到程序存储器不够了为止 *)
	tasks;
ELSE_IF conditions THEN
	tasks;
...
ELSE (* 如果没有默认操作,ELSE可以省略,也可以不省略 *)
	tasks;
END_IF;(* 这个地方破天荒地有一个分号 *)

状态分支CASE-OF 1: … 2: … 3: … n: … ELSE … END_CASE;

.
  这种分支结构是专门用于状态变量的,需要使用数字作为状态变量,但代码中不一定要按大小顺序排版,也不一定要连续。未显式定义的数字会转接到ELSE段中。
  做一个演示用的红绿灯,有红亮(Y0)、黄亮(Y1)、绿亮(Y2)三种状态,用一个按钮X0来控制。这个程序并不十分适合这一场景,只是做为介绍语法之用,具体工程中只需要5~6行代码就搞定了。

IF LDP(TRUE, X0) THEN
	D0 := D0 + 1;(* 状态变量如果无止境地加上去会怎么样?事实上不会,因为在CASE里有做限制 *)
END_IF;
CASE D0 OF
	0:(* 状态0的时候Y0通。不一定要换行,只是为了写注释方便我在冒号后面直接换行了*)
		Y0 := TRUE;
		Y1 := FALSE;
		Y2 := FALSE;
	1:(* 状态1时Y1通 *)
		Y0 := FALSE;
		Y1 := TRUE;
		Y2 := FALSE;
	2:(* 状态2时Y2通*)
		Y0 := FALSE;
		Y1 := FALSE;
		Y2 := TRUE;
	ELSE(* 其它状态时,跳到状态0 *)
		D0 := 0;
END_CASE;

.
  与C语言家族不同,结构化文本不需要break,它不会从一个状态直接就执行到下一个状态。case语句的结构很简单,开头CASE和OF中间只能写一个数值变量,后面是常量标签加个冒号,一个ELSE代表未定义的数值,最后以END_CASE;结尾,注意那个分号。
  下面为强迫症患者公开一下真正有工程意义的写法,它的功能是一样的。

IF LDP(TRUE, X0) THEN
	D0 := (D0 + 1) MOD 3;
END_IF;
Y0 := D0 = 0;
Y1 := D0 = 1;
Y2 := D0 = 2;

.
  还有这种。

IF LDP(TRUE, X0) THEN
	Y2 := Y1;
	Y1 := Y0;
END_IF;
Y0 := NOT (Y1 OR Y2);

.
  个人比较推荐6行写法,比较正规,5行写法太作了,可读性不好。
  目前看来CASE语句好像没什么用。事实上CASE语句是IF语句的定制化版本,适合范围更窄,但能少写一些代码。接下来的设计模式中如果条件允许,用CASE会有更好的可读性,因为不重要的部分(标签数字)不会太显眼。

循环

.
  我的习惯是C语言家族不用while,其它编程语言不用for。对C语言家族,for和while功能差不多,但对其它编程语言,for的用途比较小。但我觉得还是讲一下FOR的语法吧。

FOR temp := 起始值 TO 完成值 BY 增量 DO
	业务代码
END_FOR;

.
  等价于Java11或xtend代码(Java11比之前的版本多一个var关键字):

for(var temp = 起始值; temp != 完成值 + 增量; temp += 增量){
     
	业务代码
}

.
  等价于Matlab代码:

for temp = 起始值: 增量: 完成值
	业务代码
end

.
  如果是C++,大概可以这么写,但是没有一个专业的C++程序员会随便就用一个auto声明:

for(auto temp = 起始值; temp != 完成值 + 增量; temp += 增量){
     
	业务代码
}

.
  不得不说for语句的要素太多了,而且结构化文本的FOR语句真麻烦,要记FOR,TO,BY,DO四个关键字,中间还有一个赋值。我在C语言家族首选for是因为经常需要使用死循环,for的死循环很好做,又没有系统关键字true,这个在IDE里会变色的碍眼东西。但显然非C语言家族的for语句并不具备死循环的功能。
  因为太麻烦,就不细说了。下面是WHILE-DO … END_WHILE;
  这个比较中规中矩。用法如下:

while x0 then
	y0 := 1;
end_while;

.
  这是一个用来死机的程序。按下X0后PLC就不动了。理论上松开X0也没用,而且y0也不会有输出。因为X0和Y0将没有更新的机会,X0永远为TRUE,Y0永远无法输出。
  我这次全部都用的小写字母,也就是说结构化文本是不分大小写的。另外,可以用数字1代替true(通),用数字0代替false(断)。
  另外还有一个REPEAT … UNTIL … END_REPEAD;循环,相当于do-while一样的功能。它是这么用的。

REPEAT
	D0 := D0 + 1;
UNTIL D0 == 0 END_REPEAT;

.
  这是一个延时程序,如果三菱FX3U的指令时间是50μs的话,它大约可以产生3秒的延时。如果它是按STM32的标准主频72MHz来做的话,这个延时太短,将无法观测。
  对于FX3U,循环的用途较少。我不确定大型机对循环的需求,但如果真的需要复杂控制,还是建议用IPC加IO板卡,那样简单一些。

跳转指令

.
  这部分在小型机中应用较少,简略介绍一下。

  • EXIT
      提前终止当前循环,进入尾部的代码,相当于C的 break。
  • CONTINUE
      提前跳过本次循环,进入首部的代码,与C的continue类似。但三菱GX Works2中没有这个关键字。不确定是不是我的资料有问题。
  • RETURN
      返回,相当于X86汇编的RET指令,RETURN是无操作数指令。ST的函数返回值直接函数名接受值。三菱FX3U的函数是一次性的,建议不要将自定义函数用于FX3U。
  • JMP
      跳转到指定标签位置,和X86指令同名,可以用于代码混淆,不建议手写。但三菱GX Works2中没有这个关键字。不确定是不是我的资料的问题。三菱Q系列有JMP指令(比FX系列可以节省一个LD M8000,其实没什么很大的意义),但不能作为ST的函数或关键字。

设计模式

奇校验模式

.
  奇校验模式就是对一系列变量进行奇校验计算,当其中有一个输入值变化的时候,输出值也会变化。奇校验模式的代码如下:

M0 := M1 XOR M2 XOR ... XOR Mn;

.
  它有什么用呢?它主要用于多联开关的设计,没错,是多联开关,包括了从1~无穷大个走道灯开关的电路模式。如果你确实有需要的话,它也可以转换成梯形图。
  除了作为多联开关电路外,它还可以简化边沿检测的代码。例:

M0 := X0 XOR X1 XOR X2 XOR ... XOR Xn;
M0 := LDP(TRUE, M0) OR LDF(TRUE, M0);

.
  这时M0就代表了X0到Xn的所有输入的边沿变化情况。
  它也可以用来做翻转器:

M0 := X0 OR X1 OR X2 OR ... OR Xn;
M1 := LDP(TRUE, M0);
Y0 := Y0 XOR M1;

.
  这时,X0~Xn都能够对Y0进行翻转操作。但是通常建议翻转器不要有多个接口,防止偶然出现的多路信号同时为TRUE。
  下面是一个上升沿翻转器,X0接通的时候Y0翻转:

RST(NOT X0, M0);
Y0 := Y0 XOR X0 XOR M0;
SET(X0, M0);

.
  下面是一个下降沿翻转器,X0断开的时候Y0翻转:

SET(X0, M0);
Y0 := Y0 XOR X0 XOR M0;
RST(NOT X0, M0);

.
  下面是一个双边沿翻转器,X0接通或断开的时候Y0都会翻转:

Y0 := Y0 XOR X0 XOR M0;
M0 := X0;

时间轴模式

.
  时间轴模式用于仅仅按时间变化的流程控制,如用于喷泉、广告牌、老式冲压机控制系统。
  我们需要设计一个时间引擎。使用定时器产生时间脉冲。三菱FX3U的单位时间是100ms,西门子S7-1200则是以1ms为单位。例程为了简单,使用直接寻址,实际工程尽量使用声明变量的方法。

OUT_T(NOT TS0, TC0, 1);
M0 := TS0;

.
  M0将会以100ms为周期输出脉冲。可以说,在三菱PLC上,只需要两路时间脉冲,一路就是这个100ms脉冲,另一路是由系统时间比较得来的高精度1秒脉冲,实现方法较复杂,属于三菱PLC的细节应用,本文对细节不深究,有兴趣的读者可自行参阅三菱PLC系统时间的读写操作。
  有了时间引擎,我们需要把时间轴连接到引擎上。我用D0当作一条时间轴,设系统周期是10秒,则时间轴的代码如下。

IF LDP(TRUE, M0) THEN
	D0 := (D0 + 1) MOD 100;
END_IF;

.
  时间轴上应该要有历史事件,我们还需要给时间轴一个历史事件。假设我们现在需要不断重演的2面滚动广告牌事件,首先是A面广告上演了5秒,然后B面广告把A面挤下去,从B面开始动作的时候起,过5秒A面又重新滚动到前面。设计A面位置开关为X0,B面位置开关为X1,电机接触器信号为Y0那么我们可以这么写。

IF D0 < 50 THEN
	Y0 := NOT X0;(* 这5秒上帝希望A面显示 *)
ELSE_IF D0 < 100 THEN(* 因为D0不可能大于或等于100,这一行也可以改成ELSE。我这种写法只是为了代码看起来有一致性 *)
	Y0 := NOT X1;(* 这5秒上帝希望B面显示 *)
END_IF;

.
  如果位置不是由位置开关测量,而是由编码器测量的,那么我们不仅需要不断上演的历史事件,还需要永远走不完的空间。空间轴与时间轴都是循环的,设A面编码器位置为0(我们还需要设计一个清零的功能,清零位置开关用X2,这是一个内置的AB相计数器清零接口)、B面编码器位置为360×100(这个需要现场测量,我假设测量到这个长度)、总行程为360×200。
  下面是空间轴首尾相接的代码和清零代码。

OUT_T(TRUE, CC252, 0);(* 我们不需要用到CS252,所以第3个参数可以随便填。关于C252 AB相编码器计数器的功能,请参阅三菱FX3U的说明书。 *)
IF D0 < 50 THEN
	Y0 := NOT (CN252 < 360 OR CN252 > 360*199);(* 这5秒上帝希望A面显示 *)
ELSE_IF D0 < 100 THEN
	Y0 := NOT (CN252 < 360*101 AND CN 252 > 360*99);(* 这5秒上帝希望B面显示 *)
END_IF;

.
  我想这样就可以了,应该没有人会在广告牌上用伺服电机或变频器吧。那个是驱动设计的内容,不属于开发模式,本文不讲驱动设计。

操作票模式

.
  所谓操作票,就是写有工作过程的每一个细节和权限信息的一份文件。操作票模式用于有条件阻塞的循环控制流程,比如温度控制、液体定量混合之类的。
  操作票模式本身不依赖时间,但是它的业务代码中可能会有局部时间轴。本节只介绍纯正的操作票模式,也就是与时间无关,只跟阻塞条件有关的流程。
  我们先不管有什么项目,先建个操作票来看看它长什么样子。

CASE D0 OF
0:
1:
2:
ELSE
D0 := 0;
END_CASE;

.
  这个就是一张操作票了。一个小型过程控制的PLC项目通常只有一张操作票。现在来看看操作票能干什么?
  我们现在设计一台水泥混合机,它将砂、石、混凝土、水按比例混合。由于这节不希望再看到时间轴,所以我们把搅拌的部分省略,单独看与时间无关的代码。
  机器的结构是一个大筒,用重力传感器称重,物理信号是电流环,然后在PLC中进行比较后得到物料足量的信号,此处重力传感器的处理代码省略。M0表示混凝土达到份量、M1是加入砂子达到份量、M2是加入石头达到份量、M3是水量达标。Y0是混凝土传送带电机接触器、Y1是砂子传送带、Y2是石头传送带、Y3是水管电磁阀。搅拌机代码省略。
  来看看我们的操作票长什么样子。

CASE D0 OF
0:
Y0 := TRUE;
IF M0 THEN
	Y0 := FALSE;
	D0 := 1;
END_IF;
1:
Y1 := TRUE;
IF M1 THEN
	Y1 := FALSE;
	D0 := 2;
END_IF;
2:
Y2 := TRUE;
IF M2 THEN
	Y2 := FALSE;
	D0 := 3;
END_IF;
3:
Y3 := TRUE;
IF M3 THEN
	Y3 := FALSE;
	(* 设置第4步延时 *)
	D0 := 4;
END_IF;
4:
(* 搅拌机开 *)
IF (* 时间到 *) THEN
	(* 搅拌机关 *)
	(* 设置第5步延时 *)
	D0 := 5;
END_IF;
5:
(* 倒出水泥 *)
IF (* 时间到 *) THEN
	(* 复位 *)
	D0 := 6;(* 是最后一步,也可以写D0 := 0,效果一样 *)
END_IF;
ELSE
	D0 := 0;(* 收尾 *)
END_CASE;

.
  程序以一种非常机械的方式不断重复着相似的模板,操作票的每一步都看起来非常像。就是这一步做什么,如果达到条件,结束这一步,进入下一步。
  现在我们看看把时间轴加进去是什么样子。

OUT_T(NOT TS0, TC0, 1);
(* D1是一个倒退的时间轴,习惯上叫倒计时器 *)
IF TS0 AND D1 > 0 THEN
	D1 := D1 - 1;
END_IF;
CASE D0 OF
0:
Y0 := TRUE;
IF M0 THEN
	Y0 := FALSE;
	D0 := 1;
END_IF;
1:
Y1 := TRUE;
IF M1 THEN
	Y1 := FALSE;
	D0 := 2;
END_IF;
2:
Y2 := TRUE;
IF M2 THEN
	Y2 := FALSE;
	D0 := 3;
END_IF;
3:
Y3 := TRUE;
IF M3 THEN
	Y3 := FALSE;
	D1 := 100(* 设置第4步延时,比如说我要10秒 *)
	D0 := 4;
END_IF;
4:
(* 搅拌机开 *)
IF D1 = 0(* 时间到 *) THEN
	(* 搅拌机关 *)
	D1 := 600(* 设置第5步延时,再次使用D1,设置为60秒 *)
	D0 := 5;
END_IF;
5:
(* 倒出水泥 *)
IF D1 = 0(* 时间到 *) THEN
	(* 复位 *)
	D0 := 6;(* 是最后一步,也可以写D0 := 0,效果一样 *)
END_IF;
ELSE
	D0 := 0;(* 收尾 *)
END_CASE;

.
  在同一张操作票里,逆向时间轴(倒计时器)是可以复用的,不需要搞一大堆的定时器。逆向时间轴不仅可以做倒计时器,也可以做多项历史事件。把D1 = 0改成其他数字,然后多扩展几步就变成有多个历史事件的时间轴了。只不过在操作票里面,可以用步骤来代替历史,所以不需要再像单纯的时间轴模式那样把一段时间轴分成几个历史事件了。

解释器模式

.
  解释器模式源于上位机开发模式,是状态法的模式化应用。解释器模式与操作票模式很像,但不再是按顺序执行,也不再是单纯由内部逻辑改变状态数。
  设计一个洗衣机驱动层(只是打个比方,要知道事实上没人会用PLC去控制洗衣机),由状态变量D0来决定工况。
  工况0:停机;
  工况1:正转,Y0;
  工况2:反转,Y1;
  工况3:加水,Y2;
  工况4:排水,Y3;

K1Y0 := 0 (* 统一初始化端口 *)
CASE D0 OF
	0:(* 停机 *)
	1:	Y0 := 1;(* 正转 *)
	2:	Y1 := 1;(* 反转 *)
	3:	Y2 := 1;(* 加水 *)
	4:	Y3 := 1;(* 排水 *)
	ELSE
		D0 := 0;(* 防止状态异常 *)
END_IF;

.
  然后现在我们有能力将驱动层与逻辑层分离了。现在给它配套一个逻辑层吧。

(* 启动时间轴 *)
OUT_T(NOT TS0, TC0, 1);
IF TS0 AND D2 >0 THEN
	D2 := D2 - 1;
END_IF;
(* 正反转交换的次数 *)
IF X0 THEN
	D3 := 30;
END_IF;
(* 操作票 *)
CASE D1 OF
	0:(* 待机 *)
		D0 := 0;
		IF D3 > 0 THEN
			D3 := D3 - 1;
			D1 := D1 + 1;
		END_IF;
	1:(* 注水 *)
		D0 := 3;
		IF X2 THEN
			D2 := 100;
			D1 := D1 + 1;
		END_IF;
	2:(* 正转10秒 *)
		D0 := 1;
		IF D2 <> 0 THEN
			D2 := 100;
			D1 := D1 + 1;
		END_IF;
	3:(* 反转10秒 *)
		D0 := 2;
		IF D2 <> 0 THEN
			(* 如果次数未完成,则倒退回上一步 *)
			IF D3 > 0 THEN
				D2 := 100;
				D3 := D3 - 1;
				D1 := D1 - 1;
			ELSE
				(* 如果次数完成,进入下一步 *)
				D1 := D1 + 1;
			END_IF;
		END_IF;
	4:(* 排水 *)
		D0 := 4;
		IF NOT X1  THEN
			D1 := D1 + 1;
		END_IF;
	ELSE
		D1 := 0;
END_CASE;

K1Y0 := 0 (* 统一初始化端口 *)
CASE D0 OF
	0:(* 停机 *)
	1:	Y0 := 1;(* 正转 *)
	2:	Y1 := 1;(* 反转 *)
	3:	Y2 := 1;(* 加水 *)
	4:	Y3 := 1;(* 排水 *)
	ELSE
		D0 := 0;(* 防止状态异常 *)
END_IF;

算法

.
  感觉到设计模式没什么东西可写了,下面也写点算法吧。

大批量逻辑电路

.
  这是一个32输入的异或门,它将M0~M31进行异或,赋值给M100。或门、与门也是相同的格式。它把级联电路转换成了移位后的逻辑运算,每增加一次运算可处理2倍的输入。但ST只支持到一次操作32位中间继电器。我们将惊喜地发现,所用的步数明显少于用梯形图串并联的程序。梯形图无论如何也做不到仅用20步就完成32个输入的与|或|异或门电路。

K8M1000 := K8M0;
K8M1000 := K8M1000 XOR K8M1001;
K8M1000 := K8M1000 XOR K8M1002;
K8M1000 := K8M1000 XOR K8M1004;
K8M1000 := K8M1000 XOR K8M1008;
K8M1000 := K8M1000 XOR K8M1016;
M100 := M1000;
0	LD SM400
1	DMOV K8M0 K8M1000
4	DXOR K8M1001 K8M1000
7	DXOR K8M1002 K8M1000
10	DXOR K8M1004 K8M1000
13	DXOR K8M1008 K8M1000
16	DXOR K8M1016 K8M1000
19	LD M1000
20	OUT M100

.
  需要注意的是,这种方法会使M1001~M1031的部分变脏(密码学术语,意为信息被加入干扰)。虽然操作的位置可以达到M1047,但超过M1031的部分只是读数据,并没有改写。不管怎样,建议复制一份副本进行处理。
  另外,5个XOR语句不一定要按这个顺序,改变顺序也是可以的。事实上,相反的顺序令人更有安全感,因为数据是按一个递进的规律变脏的。而这个代码的数据是以渗透的方式变脏的,看起来很不安心,但其实是一样的。共有5!种顺序,也就是120种顺序全部是有效的。

冒泡排序

.
  这个是上位机程序设计经典算法,功能是把一系列的数值按从小到大或从大到小的顺序进行整理。

arrayForSort[0] := D0;
arrayForSort[1] := D1;
arrayForSort[2] := D2;
arrayForSort[3] := D3;
arrayForSort[4] := D4;
arrayForSort[5] := D5;
arrayForSort[6] := D6;
arrayForSort[7] := D7;

FOR D1000 := 6 TO 0 BY -1 DO
	FOR D1001 := 0 TO D1000 BY 1 DO
		(* 如果低索引处的值高于高索引处的值,则交换数值,
		这样一级一级地把最大的那个数值推到终点。
		然后再重新一级一级把第二大的那个数值推到终点前的一个位置。
		如果要从大到小地排序,则把IF语句后面的">"改成"<"就可以了。
		显然,由于冒泡排序总是把相邻的两个数值交换,
		就使得冒泡排序的交换次数等于行列式学科中的所谓反序数。 *)
		IF arrayForSort[D1001] > arrayForSort[D1001+1] THEN
			D1002 := arrayForSort[D1001];
			arrayForSort[D1001] := arrayForSort[D1001+1];
			arrayForSort[D1001+1] := D1002;
		END_IF;
	END_FOR;
END_FOR;

D100 := arrayForSort[0];
D101 := arrayForSort[1];
D102 := arrayForSort[2];
D103 := arrayForSort[3];
D104 := arrayForSort[4];
D105 := arrayForSort[5];
D106 := arrayForSort[6];
D107 := arrayForSort[7];

.
  这里,我们其实用到了数组。实际上数组是由变址寄存器实现的,我们不需要关心变址寄存器,那是编译器的事情。
  三菱PLC不支持直接用一个D开头的标签表达数组,只能在局部变量表中声明。arrayForSort是一个长度为8的数组,中括号内的数字代表下标(索引、偏移、变址寄存器值),实际上使用的仍然是一串D系列的寄存器。
  不仅是数值,开关量也可以冒泡排序。

arrayforbittest[0] := X0;
arrayforbittest[1] := X1;
arrayforbittest[2] := X2;
arrayforbittest[3] := X3;
arrayforbittest[4] := X4;
arrayforbittest[5] := X5;
arrayforbittest[6] := X6;
arrayforbittest[7] := X7;

FOR D1001 := 6 TO 0 BY -1 DO
	FOR D1000 := D1001 TO 6 BY 1 DO
		IF NOT arrayforbittest[D1000] AND arrayforbittest[D1000+1] THEN
			arrayForBitTest[D1000] := TRUE;
			arrayForBitTest[D1000+1] := FALSE;
		END_IF;
	END_FOR;
END_FOR;

Y0 := arrayforbittest[0];
Y1 := arrayforbittest[1];
Y2 := arrayforbittest[2];
Y3 := arrayforbittest[3];
Y4 := arrayforbittest[4];
Y5 := arrayforbittest[5];
Y6 := arrayforbittest[6];
Y7 := arrayforbittest[7];

.
  这个程序的效果就是X0到X7中的按钮按下了几个,Y0开始就连续亮几个灯。是一个用于新手了解数组的趣味小程序。

12键键盘

.
  这是一个类似电话键盘的功能模块,实现了可存储8个键码的12键数字键盘:

VAR_INPUT	key_1	Bit
VAR_INPUT	key_2	Bit
VAR_INPUT	key_3	Bit
VAR_INPUT	key_4	Bit
VAR_INPUT	key_5	Bit
VAR_INPUT	key_6	Bit
VAR_INPUT	key_7	Bit
VAR_INPUT	key_8	Bit
VAR_INPUT	key_9	Bit
VAR_INPUT	key_0	Bit
VAR_INPUT	key_star	Bit
VAR_INPUT	key_sharp	Bit
VAR_INPUT	key_backspace	Bit
VAR_INPUT	key_clear	Bit
VAR_OUTPUT	key_array	Word[Signed](0..7)
VAR	m_index	Word[Signed]
VAR	m_buffer	Word[Signed]
IF LDP(TRUE, KEY_1) THEN
	M_BUFFER := 1;
ELSIF LDP(TRUE, KEY_2) THEN
	M_BUFFER := 2;
ELSIF LDP(TRUE, KEY_3) THEN
	M_BUFFER := 3;
ELSIF LDP(TRUE, KEY_4) THEN
	M_BUFFER := 4;
ELSIF LDP(TRUE, KEY_5) THEN
	M_BUFFER := 5;
ELSIF LDP(TRUE, KEY_6) THEN
	M_BUFFER := 6;
ELSIF LDP(TRUE, KEY_7) THEN
	M_BUFFER := 7;
ELSIF LDP(TRUE, KEY_8) THEN
	M_BUFFER := 8;
ELSIF LDP(TRUE, KEY_9) THEN
	M_BUFFER := 9;
ELSIF LDP(TRUE, KEY_0) THEN
	M_BUFFER := 10;
ELSIF LDP(TRUE, KEY_SHARP) THEN
	M_BUFFER := 11;
ELSIF LDP(TRUE, KEY_STAR) THEN
	M_BUFFER := 12;
ELSIF LDP(TRUE, KEY_BACKSPACE) THEN
	M_BUFFER := -1;
ELSIF LDP(TRUE, KEY_CLEAR) THEN
	M_BUFFER := -2;
END_IF;

IF M_BUFFER <> 0 THEN
	IF M_BUFFER > 0 THEN
		FOR M_INDEX := 6 TO 0 BY -1 DO
			KEY_ARRAY[M_INDEX + 1] := KEY_ARRAY[M_INDEX];
		END_FOR;
		KEY_ARRAY[0] := M_BUFFER;
	ELSE
		CASE M_BUFFER OF
			-1:
			FOR M_INDEX := 0 TO 6 BY 1 DO
				KEY_ARRAY[M_INDEX] := KEY_ARRAY[M_INDEX + 1];
			END_FOR;
			KEY_ARRAY[7] := 0;
			-2:
			FOR M_INDEX := 0 TO 7 BY 1 DO
				KEY_ARRAY[M_INDEX] := 0;
			END_FOR;
			ELSE
			;
		END_CASE;
	END_IF;
	RST(TRUE, M_BUFFER);
END_IF;

.
  可用于输入配置参数、口令、验证码、激活码以及实现组合键功能。

你可能感兴趣的:(三菱PLC,算法,SCL,ST,结构化文本)