ST结构化文本语言与示例(持续更新20221117)

基本数据类型

类型名称 类型标识符 数据下限 数据上限 存储空间
布尔型 BOOL 0 1 1bit
整型 字节型 BYTE 0 255 1byte
字型 WORD 0 65535 2byte
双字型 DWORD 0 4294967295 4byte
短整型 SINT -128 127 1byte
无符号短整型 USINT 0 255 1byte
整型 INT -32768 32767 2byte
无符号整型 UINT 0 65535 2byte
长整型 DINT -2147483648 2147483647 4byte
无符号长整型 UDINT 0 4294967295 4byte
实数型 REAL -3.402823E+38 3.402823E+38 4byte
字符串行 STRING 默认 80 个字符,最大255个字符,第一个字节存储字符串长度。 256byte
时间型 时间型 TIME TIME 表示一个时间值, 单位为毫秒, 初始值为 0。 4byte
时刻型 TOD TOD 表示当天的时刻, 单位为毫
秒, 初始值为凌晨 0 点 0 分。
4byte
日期型 DATE DATE 表示当前日期, 单位为秒,初始值是 1970 年 1 月 1 日。 4byte
日期时刻型 DT DT 表示当前日期和时刻, 单位为秒, 初始值是 1970 年 1 月 1 日凌晨 0 点 0 分。 4byte

自定义数据类型

数组定义:
<数组名>:ARRAY [<L1>..<M1>,<L2>..<M2>,<L3>..<M3>] OF <基本数据类型>一维数组
A1: ARRAY[0..9] OF BOOL;
二维数组
A2: ARRAY[0..9, 0..9] OF REAL;
三维数组
A3: ARRAY[0..9, 0..9, 0..9] OF INT;

枚举:
TYPE<标识符>:(<Enum_1>,<Enum_2>,…,<Enum_3>);
END_TYPE
定义举例
TYPE Color :
(
Red := 0,
Green,
Yellow
);
END_TYPE

结构:
TYPE <结构名>:
STRUCT
<变量声明 1>
<变量声明 2>
… <变
量声明 n>
END_STRUCT
END_TYPE
定义举例
TYPE Motor :
STRUCT
V:REAL;
A:REAL;
Hz:REAL;
END_STRUCT
END_TYPE

指针:
<指针名>: POINTER TO <数据类型或功能块类型>
定义举例
Pt:POINTER TO INT;
Int1:INT;
Int2:INT;
Pt:=ADR(Int1);(*取地址*)
Int2:=Pt^;(*取值*)

CAPTRON电容式触摸开关简单测试

收到CAPTRON提供的电容式触摸开关的试用样品。

电源是24VDC,输出是PNP形式,脉冲时间大约持续1秒钟左右,触控后指示灯变红,自带一个绿色指示灯。

包装在工控行业算是比较精美了。

当然价格也很美丽。

外观是下面的这个样子。

接上线是下面的这个样子。

接上电源后。

触控后显示的是红灯。

 

 

 

PLC程序之线圈指令

我们学习PLC程序设计的时候第一个程序可能就是启保停程序,这个程序的线圈输出是PLC指令中一个比较特殊的指令。

上述的这个程序可以转换为Y0=(X0 or Y0) and (not X1),这段程序只要执行了,就要把结果输出给Y0,我们常常认为上述的程序段要比下面的程序段安全,在断电后不会出现设备自启动情况。

如果Y0定义成了掉电保存,那么上述的第一段程序会在上电后一直执行,结果是Y0没有输出,而第二段代码则不会执行,Y0会保持掉电前的状态,所以会认为第一段代码更安全一些。

第一段代码如果用语句表来表达,会更容易理解,因为解释这段代码要牵扯到堆栈操作,这里就先不做解释了。

因为线圈指令的特殊性,所以很多教材都强调避免双线圈输出,就像下面这段代码,线圈的输出结果是最后一条指令执行的结果。

上述Y0的输出是取决于X1的断开或闭合,X0是不起作用的。部分编译器会提示警告,而不是错误。

上述双线圈程序是否可以改一下,也是可以使用的,比如下面这种方式,采用两个子程序调用,而两个子程序的调用时互斥的。

主程序

子程序1

子程序2

双线圈输出使用尽量小心,但是还是比较容易出错,还是尽量避免使用比较合适。

想起一个例子:这里用线圈指令的特点和中断程序实现X0单键控制Y0的启停,具体做法:按下X0,上升沿触发中断程序,使Y0闭合;再次按下X0,上升沿触发中断程序,使Y0断开;周而复始执行上述过程。

主程序里关联硬件和中断程序。

中断程序如下

每次按下X0的按钮时,执行一次中断程序,相当于Y0的输出取反。

徐大军

2022年9月3日星期六

控制器介绍

大约2019年的时候给同事讲解PLC的知识,保留了部分。

1、控制器简介

1.1、专用控制器

面向有限的几种控制方式,组态式编程。

西门子RUL220


1.2、DDC控制器

用途基本上局限在楼宇,电厂等几个行业。

霍尼韦尔的DDC控制器


西门DDC子POL638


1.3、嵌入式控制器

优点:大批量使用成本低廉,体积可以做的比较小。

单片机电路板


树莓派


1.4、PLC

专为控制领域设计的控制系统,可靠性高稳定性都比较高,防护能力强,无故障运行时间可达百万小时以上。可选择范围很广,宽温范围的可在-40-80℃运行,防护等级可做到IP65,双机冗余系统,安全PLC系统等。

专用PLC控制器,主要提供行业内功能指令


可编程继电器(德国金钟穆勒)


微型PLC


小型PLC



中型PLC


大型PLC(罗克韦尔的controllogix)


基于PC的软PLC



西门子TDC

扫描周期100us,一个机架可以配置20个CPU,最多可以44个机架同步工作。


1.5、边缘控制器(PLC)

Windos等操作系统+codesys(被称为工业领域的安卓)

Windos,linux等操作系统+LOGICLAB


1.6、触控一体机

本质上就是触摸屏和PLC集成到了一起。

集成一体,不可扩展


模块本体可扩展,也可远程IO模式


1、亿维一体机



 

2、虹科一体机



3、顾美一体机


个人感觉的西门子PLC的特点

个人喜好,个人见识有限,难免存在偏颇。
先说缺点:
1、编程软件庞大,我用博图V15的时候,它的安装包大约是16G左右,很考验电脑的性能;
2、如果没使用欧系的PLC,上手比较困难,很多概念不容易理解;
3、不支持枚举数据类型;
再说优点:
1、PLC,运动控制,HMI,SCADA等在一个软件内,写程序方便,支持拖拽,例如可以从PLC变量表中直接拖动变量到HMI中,自动生成关联,变量更新所有的地方都会更新,很方便;
2、软件支持PLC和HMI仿真,它们之间的通讯也能仿真;
3、通讯支持的协议比较多,扩展模块很方便;
4、模拟量模块可以设置电压或者电流的类型;
5、各种模块扩展方便,包括远程IO模块;
6、支持标签访问的模式,存储区的变动不影响通讯;
7、功能块类似于面向对象编程中的类,便于设备模块化;
9、支持结构化文本语言,使用比较顺手,尤其是复杂的控制逻辑或者计算;
10、支持数组和结构体类型,方便数据的管理;
11、调试方便,有调试面板和变量曲线显示的功能;
12、用的人多,资料也多,写程序容易找到例程;

程序集IL指令的反汇编工具ildasm.exe

这个工具的用途是查看生产的exe程序集中包含的IL指令代码。

工具的放置位置一般在:C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\

打开工具,界面如下:

我们写一段基本的代码。

using System;

namespace ConsoleApplication2

{


class
Program

{


static
void Main(string[] args)

{


Console.WriteLine(“Hello World!”);

}

}

}

我们用ildasm.exe程序打开生成的程序集,并双击”Main:void(string[])”。

2022年4月4日

23时57分

S71200 高速计数器中断编程

实现功能:高速计数器每计数100次,计数器清零并使Q0.7输出取反。

这里使用PWM产生周期10毫秒的脉冲,做如下设置。

启用脉冲发生器(没有视图的参数采用默认值)

设置脉冲参数

设置脉冲输出点位

设置高速计数器(没有视图的参数采用默认值)

启用高速计数器

设置计数器的参数

设置初始参考值100

设置事件中断

设置硬件输出

中断程序编写如下

主程序编写

接线Q0.7到Q0.2,别漏掉电源部分。

执行结果,Q0.7以2秒周期闪烁。

C# 异步编程示例

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

using System.Threading;

 

namespace ConsoleApplication3

{


class
Program

{


static
void Main(string[] args)

{


Console.WriteLine($”主程序头,当前线程ID{Thread.CurrentThread.ManagedThreadId});

CallSayHiAysnc(“Jack”);


Console.WriteLine($”主程序尾,当前线程ID{Thread.CurrentThread.ManagedThreadId});


Console.ReadKey();

}

 


static
async
void CallSayHiAysnc(string name)

{


Console.WriteLine($”异步调用头,当前线程ID{Thread.CurrentThread.ManagedThreadId});


string result = await SayHiAsync(name);


Console.WriteLine($”异步调用尾,当前线程ID{Thread.CurrentThread.ManagedThreadId});


Console.WriteLine(result);

}


static
Task<string> SayHiAsync(string name)

{


return
Task.Run<string>(()=> { return SayHi(name); });

}


static
string SayHi(string name)

{


Thread.Sleep(10000);


Console.WriteLine($”SayHi已执行,当前线程ID{Thread.CurrentThread.ManagedThreadId});


return
$”Hi,{name};

}

 

}

}

结构化文本实现上升沿和下降沿指令

上升沿和下降沿指令是需要两个扫描周期检测的,另外需要一个点位存储上一次的扫描结果用于和本次扫描做对比。

西门子S7200的子程序中如果使用上升沿和下降沿程序,就只能调用一次,多次调用的话,上升沿和下降沿指令就会出现混乱。S71200使用带多重背景块的上升沿和下降沿指令的子程序就不存在这样的问题,因为背景数据块之间没有影响。

下面是梯形图的实现方式

然后我们写成结构化文本的模式;

横河PLC实现modbusrtu通讯

这里只介绍实现的思路

  1. 重点阅读名称H22-02E的帮助文档。
  2. 程序中会使用到间接寻址或偏移量寻址方式。
  3. CRC16校验采用小查表法易于实现。
  4. 接受数据失败,建议初始化数据接收缓冲区。

由于横河PLC没有更上层的指令,只能通过自由口通讯的模式实现。

串口设置部分:

设置参数包含906,907,908,909,912,913,918,其中912和913要设置为0,否则通信的会有问题,参见描述中的部分。

帮助中给的通讯参数设置程序:

帮助中给出的发送数据程序:

帮助中给出的接受数据程序:

 

CRC16 小查表法

#include
“stdafx.h”

 

static
unsigned
short crcTlb[] = { 0x0000, 0xCC01, 0xD801, 0x1400, 0xF001, 0x3C00, 0x2800, 0xE401,

0xA001, 0x6C00, 0x7800, 0xB401, 0x5000, 0x9C01, 0x8801, 0x4400 };

 

static
unsigned
short CalcCRC16(unsigned
short * pBuf, unsigned
int
length)

{

    unsigned
int i = 0, ch = 0;

    unsigned
int crc = 0xFFFF;

    for (i = 0; i < length; i++)

    {

        ch = pBuf[i];

        crc = crcTlb[(ch ^ crc) & 0x000F] ^ (crc >> 4);

        crc = crcTlb[((ch >> 4) ^ crc) & 0x000F] ^ (crc >> 4);

    }

    crc = (crc & 0x00FF) << 8 | (crc >> 8);

    return crc;

}

 

 

int main()

{

    unsigned
short ar[] = {0x01,0x03,0x00,0x56,0x00,0x09};

    unsigned
short crc16 = CalcCRC16(ar,6);

    printf(“%X\n”, crc16);


return 0;

}

 

运算符

意义

示例

对于每个位位置的结果(1=设定,0=清除)

    &    

位 AND

 x&y 

如果 x 和 y 都为 1,则得到 1;如果 x 或 y 任何一个为 0,或都为0,则得到 0

    |    

位 OR

 x|y 

如果 x 或 y 为 1,或都为 1,则得到 1;如果 x 和 y 都为 0,则得到 0

    ^    

位 XOR

 x^y 

如果 x 或 y 的值不同,则得到 1;如果两个值相同,则得到 0

    ~    

位 NOT(I的补码)

 ~x 

如果 x 为 0,则得到 1,如果 x 是 1,则得到 0

 

运算符

意义

示例

结果

<<

向左移位

x<<y

x 的每个位向左移动 y 个位

>>

向右移位

x>>y

x 的每个位向右移动 y 个位

Codesys或TwinCAT采用指针获取输入输出M存储区或其它连续存储区变量给数组赋值

VAR

a0 AT%M*:REAL:=0.0;

a1 AT%M*:REAL:=0.1;

a2 AT%M*:REAL:=0.2;

a3 AT%M*:REAL:=0.3;

a4 AT%M*:REAL:=0.4;

i:int:=0;

ar:array[0..4] of real;

b:bool;

pr:pointer to real;

r_trig1:R_TRIG;

END_VAR

r_trig1(clk:=b);

if r_trig1.q  then

b:=0;

for i:=0 to 4 do

pr:=adr(a0)+sizeof(a0)*i;

ar[i]:=pr^;

end_for

end_if;

 

Invoke实现跨线程访问控件

不带参数的跨线程


private
void button1_Click(object sender, EventArgs e)

{


Thread t = new
Thread(() =>

{


Random R = new
Random();


this.Invoke(new
Action(()=> { this.Text = R.NextDouble().ToString(); }));

});

t.Start();

}

注意:Invoke应用在其他线程内部;

带参数的跨线程


private
void button1_Click(object sender, EventArgs e)

{


Action<object> a = new
Action<object>(printf);


this.Invoke(a,“Caption”);

}


public
void printf(object s)

{


this.Text = s.ToString();

}