前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >[专家]西门子变址寻址的改变

[专家]西门子变址寻址的改变

作者头像
科控物联
发布2022-03-29 16:07:46
5.9K0
发布2022-03-29 16:07:46
举报
文章被收录于专栏:科控自动化

【1】看变址寻址的改变

S7-300/400使用的是绝对地址寻址,即使使用符号名称也是为了便于程序的阅读;而S7-1500正好相反,底层只支持符号寻址(早期做了一个测试发现的),即使使用绝对地址寻址也必须带有符号名称,感觉就是为了S7-300/400程序的移植。

S7-300/400推荐的编程语言是STL,因为STL指令执行效率高并且支持变址寻址,而其它语言(除SCL,SCL是可选软件,必须购买)例如LAD、FBD是不支持的,所以我们经常可以看到进口设备的程序中,大部分的专有技术程序块FB、FC是STL编写的,原因是程序块的开发者使用地址寄存器和变址寻址功能灵活方便,程序块的通用性比较强,例如特殊的地址偏移功能,可以在不同的编程环境中调用(直接调用或者使用多重背景),而一些连锁程序是使用LAD编写的,这部分程序通常是由程序块的使用者编写,没有特殊的要求。所以一套程序中既有STL又有LAD,这样混合的编程方式比较常见。S7-1500中使用的是符号寻址,推荐使用SCL和LAD方式编程(慢慢忘掉STL吧,STL使用的状态字、DB和地址寄存器在S7-1500中都是虚拟的,我认为都是为了S7-300/400程序移植到S7-1500中而存在的)。

变址寻址主要的应用就是地址循环迭代,从而完成批量处理,如果S7-300/400支持数组的“index”是变量的寻址方式(SCL支持,但是后台程序中也是使用大量的指针进行转换的,所以S7-300/400系统底层不支持这种方式),那么编程就容易了,所以S7-300/400只能使用变化地址指针的方式迭代地址。现在S7-1500系统支持可变“index”的数组寻址方式,数组长度也可以是变化的,所以相应的一些使用方式就会发生变化,另外推荐的“UDT”和“Variant”数据类型也更适合面向对象的程序开发。

好了,为了清楚地搞清变址寻址方式,我们从头开始介绍和探讨。首先看一下哪些存储区(没有使用地址区的原因就是还包括程序块的寻址)可以进行间接寻址,通常的情况下可以绝对地址寻址的都可以进行进间接寻址,可以绝对地址寻址的区域参考表1。

表1 绝对地址寻址区域

绝对地址访问DB区,可以直接引用数据块的号码,例如DB1.DBX120.0,但是间接寻址的方式只能访问数据块中的数据区,必须使用OPN指令打开。这也是不想参考别人程序的原因(不好看明白),在程序中都是DBW[XX]这样的引用,都不知道打开的是哪一个数据块。为了两个数据块间的运算,使用DI作为第二个打开的数据块,DI可以是背景数据块,但是最常用的还是作为第二个打开的数据块了。

这些区域的变址寻址都是使用指针完成的,S7-300/400的指针类型参考图1,指针的类型比较多,对应者不同的应用,指针大概按照存储指针的方式和参数类型进行划分。指针类型多而且还必须使用STL调用,这也是大家常常抱怨的地方,这也没有办法,主要是应用的种类太多了,等介绍完了以后在回头看这样的划分,这也可能是最好的了。

图1 S7-300/400指针类型

【2】16位地址指针

1. 存储器间接寻址

顾名思义,存储器间接寻址就是将指针存储于存储器中。存储于存储器中的地址指针分为16位地址指针和32位地址指针,允许存储地址指针的存储器为M(标志位)、L(区域数据)及数据块(DB或DI),过程影像区是不能存储地址指针的,即使没有连接实际的I/O模块。

1.1 存储器16位地址指针

16位地址指针用于定时器、计数器、程序块(DB、FC、FB)的寻址,16位指针被看作一个无符号整数(取值范围0~65535),它指向定时器(T)、计数器(C)、数据块(DB、DI)或程序块(FB、FC)的号,16位指针的格式如图2。

图2

所有的定时器、计数器及程序块都可以使用间接寻址访问,访问时需要使用T、C、DB、DI、FB、FC的等区域标识符,寻址的指针存储于一个16位的字中,地址寻址表示格式为:

区域标识符[16位地址指针]

例如打开一个DB块表示为:

使用16位地址指针访问一个定时器和计数器的示例程序如下:

L 11 //将11传送到累加器1中。

T MW 20 //将累加器1中的数值传送到MW20中。

A I 2.1 //如果I2.1为1,将预置值10秒装载到T11中。

L S5T#10S

SE T [MW 20]

L MW 20

L 1

+I

T MW 22 //MW20再加1。

A I 2.2 //如果I2.2为1,C12向上计数一次。

CU C [MW 22]

数据块可以使用DB打开也可以使用DI打开,如果地址指针为0,CPU不会报错,使用16位地址指针访问数据块的示例程序如下:

L 20

T MW 40

OPN DB [MW 40] //打开DB20。

L 1

+I

T MW 42

OPN DI [MW 42] //打开DI 21。

L DBW20 //装载DB20.DBW20

L DIW20 //装载DB21.DBW20

+I //相加

T DBW40 //结果存储于DB20.DBW40中

使用16位地址指针调用程序块FB、FC时只能使用UC(无条件调用)或CC(有条件调用),而不能使用CALL指令,函数块中不能带有任何接口参数或静态变量,使用指针调用函数及函数块的示例程序如下:

L 12

T LW 20

UC FC [LW 20] //无条件调用FC12

L 13

T MW 20

A I 2.3

CC FB [MW 20] //如果I2.3为1,调用FB13。

FC12和FB13不能带有形参,这是由CC和UC调用指令决定的。

上面就是16位地址指针应用的简单介绍。假设一个控制对象需要使用两个定时器,有20个相同的控制对象,那么我们想一个如何使用16位地址指针进行循环调用?

【3】16位地址指针使用示例

说实在的,这个程序总感觉编起来特别别扭,首先,不能使用 UC FC [XX]等程序块调用的指令,因为给出的应用是相同程序块的多次调用;其次是在程序块的接口参数中不能定以TIMER类型,例如控制对象使用FC编写,FC的接口参数如图3所示。

图3

使用OB1调用赋值时,只能赋值常数而不能是变量,如图4所示。

图4

所以FC的接口参数只能是INT类型变量,由于接口参数不能存储16指针,还必须通过temp临时变量进行中转,在FC中的编写的程序如图5所示。

图5

FC中的程序非常简单,如果输入TRIG1为1,使能定时器T[#T1_TEMP1] 10秒定时,定时器T[#T1_TEMP1] 为1,然后将结果传送到输出OUT1,10秒后计时停止,OUT1复位,等于TRIG1为1后,OUT1产生一个10秒的脉冲,相同程序,TRIG2为1后,OUT2产生一个5秒的脉冲。

然后调用该FC,参考图6。使用LOOP指令生成20次循环调用,每次使用的定时器号加1,这样FC的第一个定时器使用范围T1~T20,第二个定时器T100~T120。触发位和输出位使用了32位指针,为了显示方便,在程序中被屏蔽。

图6

程序编写的总体感觉:对所用指令都非常熟悉的情况下,花费时间大概1个多小时,而且程序结果不清晰。如果不是后续还有类似的应用,直接调用FC 二十遍反而更容易。这也是大多数工程师没有体验到的,要么没有遇到,要么次数少就直接调用了,我见到最多调用过一百多编的,并且接口参数还比较多,真是累眼睛呀。

基于对象的编程方式,一个对象使用FB进行控制比较方便。如果将上面的FC换为FB会怎么样呢?大家都知道调用FB块需要生成实例数据块,循环调用就不行了,不能只用一个实例数据块,所以只能使用UC或者CC调用,结果就是既没有接口参数也不生成实例数据块,只能使用全局变量进行数据交换,这样还使用什么FB块呀!总之就是繁琐。

1.2 存储器16位地址指针-S7-1500的处理方式

如果使用上面的编程方式,在S7-1500中也可以实现,主要是移植的考虑。如果重新编程肯定有更简单的方式。S7-1500中批量的处理都使用数组的方式,S7-300/400中使用的定时器(T)和计数器(C)由于在S7-1500不能作为数组中的数据元素,所以就不推荐使用了,取而代之的是IEC定时器和IEC计数器,它们可以在数据块中直接定义,参考图7。

图7

FC作为辅助功能,例如数据转换、控制对象调用前的预处理或者调用后的后期处理,也不支持批量调用,通常批量处理的是控制对象,控制对象使用FB编写,所以FB支持批量调用,可以作为数组的数据元素,参考图8。

图8

还是上面的应用,使用S7-1500,编写就变得非常简单,首先创建一个FB块,编写相关的控制程序,参考图9。

图9

然后在一个FB块中方循环调用该FB块,示例程序参考图10。

图10

可以看到使用S7-1500处理程序块的批量调用非常简单,并且数组的使用与编程语言无关。

【4】存储器32位地址指针

1.3存储器32位地址指针

32位地址指针用于I、Q、M、L、数据块等存储器中位、字节、字及双字的寻址,32位的地址指针使用一个双字表示,第0位~第2位作为寻址操作的位地址,第3位~第18位作为寻址操作的字节地址,第19位~第31位没有定义,32位地址指针的格式如下:

访问时需要使用地址存储器标识符及32位地址指针,地址寻址表示格式为:

地址存储器标识符 [32位地址指针],例如指针存储于LD20中,装载M存储器一个字节表示为:

前面介绍的存储器16位指针,特定用于T、C、程序块的寻址,使用一个INT格式的值表示,每次值加1,指向的对象地址加1,例如,T1、T2,其中的“1”、“2”就是指针的直接变化(好像没有说明白!)。32地址指针使用一个DINT格式的值表示,指向的是I、Q、M、L、数据块等存储器中位,每次值加1,指向的地址区位的号加1,区别就是在这里。一个字节是8个位,如果指向的是字节每次就要加8的倍数,例如地址MB[LD20],LD20值为0,则表示MB0,LD20值为8,则表示MB1,如果指向的是字每次就要加16的倍数,以此类推。

如果将32位指针按照16位指针的方式使用就会出问题,例如MB[LD20],LD值为10,指向地址为MB1.2,程序编译没有问题(语法正确),下载下去造成CPU停机,原因就是地址长度错误。在第三部分介绍定时器循环调用的示例程序中,TRIG1和TRIG2就是使用32指针迭代的,例如 A M [LD20],每次LD值加1,由于显示的问题别屏蔽了,所以程序量还有有一点的。前面也提到了优化的问题,16位和32指针由于使用方法不同,不能简单地合并。

32位指针也可以使用另外一种形式表示,就是使用P#X.Y,P表示指针,#表示特定符号,X表示字节地址,Y表示位地址,P#X.Y可以与DINT格式的值相互转换,DINT值=X*8+Y,

例如P#2.0转换DINT格式为16,P#3.1转换DINT格式为25。在指针寻址时可以使用指针的格式也可以使用DINT格式进行运算。使用32位地址指针寻址参考下面的示例程序:

OPN DB 1 //打开DB1。

OPN DI 3 //打开DB3,最多可以同时打开两个DB块。

L 4 //装载4到累加器1中。

SLD 3 //累加器1中数值左移3位,在程序中经常见到,

左移3位就是将原值乘以8

T MD 20 //将逻辑操作结果传送到MD20中,MD20包含地址指针为P#4.0。

L P#20.0 //将地址指针P#20.0装载到MD24中。

T MD 24

L 320 //320就是P#40.0装载到MD28中。

T MD 28

L DBW [MD 20] //装载DB1.DBW4。

L DBW [MD 24] //装载DB1.DBW20。

+I //相加

L DIW [MD 28] //装载DB3.DBW40。

-I //相减。

T DIW 2 //将运算结果传送到DB3.DBW2中。

使用32位地址指针寻址数据块地址时,数据块必须先打开,然后才能寻址,数据块寻址方法参考下面的示例程序,如果直接使用指令对完整数据格式地址(例如地址DB1.DBB[MD100])进行间接寻址被视为非法。

使用LOOP 指令与32位地址指针可以进行循环操作(这是固定的操作模式,在程序中一看到LOOP指令就要想到地址的迭代),假设一个编程应用:一个INT变量(MW2)与一个数组(假设存储于DB1中,包含100个元素为INT的数组)存储的值相比较,如果数值相同,指出第一个相同数值存储在DB块中的位置(数组中的位置)。使用通常的编程方法,需要逐个进行比较,程序量比较大,如果实际需要与1000个数值比较,将占用大量的存储空间,使用LOOP指令与地址指针相结合可以轻松解决上述问题,参考下面的示例程序:

L 0 //初始化MW100和MD4。

T MW 100

T MD 4

OPN DB 1 //打开DB1。

L 100 //循环操作的次数,100次。

next: T MW 100 //将循环100次装载到MW100中,固定格式。

L MW 2 // 进行比较的数值存储于MW2。

L DBW [MD 4] //与DB块中存储的值进行比较,开始地址为DBW0。

==I //如果数值相等跳到m1。

JC m1

L MD 4 //将地址指针加2(每个相邻的INT地址相差2)。

L P#2.0

+D

T MD 4

L MW 100 //次数减1,跳回next,如果MW100等于0,跳出

LOOP next //循环操作LOOP指令,LOOP指令固定格式。

m1: FP M 10.0 //如果数值相等,记录MD4指针的位置,转换

为数组的位置((地址值/P#2.0)+1)值并存储

于MD8中。

JCN m2 //为0跳转到m2。

L MD 4

L P#2.0

/D

+ 1

T MD 8

m2: NOP 0

地址的循环操作只是减少了程序量,CPU扫描时间不会减少。

1.4存储器32位地址指针-S7-1500的处理方式

与16位指针的处理方式是一样的,还是使用数组进行循环迭代。上面的应用如果在S7-1500中编写则非常方便,示例程序参考图11。

使用变量“START_COMP”作为开始信号,如果比较值“COMP_Value”与数组的元素“A.B["INDEX"]”不相等(INDEX缺省为0),则变量“INDEX”加1,如果大于等于100,则将“INDEX”清0,然后复位开始信号“START_COMP”;如果比较值“COMP_Value”与数组中的第一个元素“A.B["INDEX"]”相等,则将变量“INDEX”中的值传送到结果“RESULT”中,然后将“INDEX”清0并复位开始信号“START_COMP”。

程序比较简单,使用LAD即可编程程序,对编程人员的要求不高,如果使用SCL编写程序可能更加简单。

【5】寄存器间接寻址

2 寄存器间接寻址

与存储器间接寻址不同,寄存器间接寻址使用CPU内部集成的两个32位地址寄存器AR1、AR2存储地址指针。寄存器间接寻址分为32位内部区域指针和32位交叉区域指针。

2.1 访问地址寄存器AR1、AR2的指令

寄存器的访问需要使用特殊的指令。指令如下所示:

LAR1 :将ACCU1存储的地址指针写入AR1。

LAR1 <D> :将指明的地址指针写入AR1,例如LAR1 P#20.0或LAR1 MD20。

LAR1 AR2 :将AR2的内容写入AR1。

LAR2 :将ACCU1存储的地址指针写入AR2。

LAR2 <D> :将指明的地址指针写入AR2,与LAR1 <D>方式相同。

TAR1 : 将AR1存储的地址指针传输给ACCU1。

TAR1<D> : 将AR1存储的地址指针传输给指明的变量中。

TAR1 AR2 : 将AR1存储的地址指针传输给ACCU2。

TAR2 : 将AR2存储的地址指针传输给ACCU1。

TAR2 <D> : 将AR1存储的地址指针传输给指明的变量中。

CAR : 交换AR1和AR2的内容。

2.2 使用地址寄存器AR1、AR2的限制

都是32位指针,为什么不使用存储器替代CPU内部的地址寄存器呢?使用方法上完全可以,除此之外,CPU还利用AR1、AR2做了一些系统内部的工作,如果没有AR1、AR2将造成程序的混乱(这节内容是引深介绍,不感兴趣可以跳过)。同样系统和用户都可以使用AR1、AR2,用户使用时需要考虑是否系统也在使用,否则也将造成程序的混乱,同时也不能改写。所以用户使用时会有限制,例如接口参数的传递中,STEP7使用地址寄存器AR1访问函数FC接口及函数块FB“INOUT”接口中定义的复合类型参数,如ARRAY、STRUCT、DATE_AND_TIME等,AR1和DB块寄存器中的内容将被覆盖,例如在FC1中“IN”接口中定义一个数组变量,在OB1中调用,使用OB1的L区域数据进行赋值,调用关系如图12所示。

图12

如果在FC1中访问数组变量的元素如ARR_TEST[1],地址寄存器AR1及DB块寄存器会发生变化,示例程序如图13所示。

图13

图13示例程序中前两条语句中打开DB1并将P#20.0装载到AR1中,在第三条语句访问数组的一个元素后AR1存储的地址指针变为P# V20.0(指向OB1中实参ARR_TEST的地址,参考图13),程序下传到CPU后将出现故障报警。将程序的执行次序进行修改,CPU即可运行,修改后的程序如下:

L #ARR_TEST[1] //装载形参变量ARR_TEST[1]到累加器1中。

OPN DB 1 //打开DB1

LAR1 P#20.0 //将P#20.0装载到地址寄存器AR1中。

T DBW [AR1,P#0.0] //将累加器1中的值传送到DB1.DBW20中。

AR2和DI寄存器分别包含FB背景数据块的块号及在背景数据在背景数据块中偏移地址(多重背景数据块),在FB中使用AR2和DI寄存器将会覆盖系统存储的内容。如果必须在FB中使用AR2和DI寄存器,建议使用下面的方法处理AR2和DI寄存器,首先保存AR2和DI寄存器中的数据,程序如下:

TAR2 MD 100 //将AR2的数据存储于MD100中。

L DINO //将背景DB块块号存储于MW104中。

T MW 104

///////用户程序///////////////

然后编写用户程序,可以对AR2和DI寄存器进行操作,但是在程序中不能访问FB参数或静态变量。使用完成后恢复AR2和DI寄存器的系统值,程序如下:

LAR2 MD 100 //将MD100中存储的地址指针装载到AR2中。

OPN DI [MW 104] //打开DI数据块。

2.3 寄存器32位内部区域指针

寄存器32位内部区域指针用于I、Q、M、L、数据块等存储器中位、字节、字及双字的寻址,与32位存储器指针使用相同,不同之处只是指针存储的位置不同(几乎没有区别,就是表示格式不同)。32位内部区域地址指针的格式如图14所示。

图14

第0位~第2位作为寻址操作的位地址,第3位~第18位作为寻址操作的字节地址,第19位~第30位没有定义,第31为内部区域与交叉区域指针标识,0表示内部区域指针,1表示交叉区域指针。

32位内部区域指针地址寻址表示格式为:地址存储器标识符[地址寄存器,地址偏移常量],例如装载M存储器一个字节表示为:

AR1/2 :AR1或者AR2。注意使用限制。

指针指向地址= 地址寄存器存储地址 + 地址偏移常量,如果AR1装载的地址为P#8.0,实际装载的地址为MB18。32位内部区域指针的使用方法参考下面的示例程序:

OPN DB 1 //打开DB1。

LAR1 P#10.0 //将指针P#10.0 装载到地址寄存器1中。

L DBW [AR1,P#12.0] //将DBW22装载到累加器1中。

LAR1 MD 20 //将存储于MD20中的指针装载到地址寄存器1 中。

L DBW [AR1,P#0.0] //将DBW装载到累加器1中,地址存储于MD20中。

+I

LAR2 P#40.0 //将指针P#40.0 装载到地址寄存器2中。

T DBW [AR2,P#0.0] //运算结果传送到DBW40中。

2.4 寄存器32位交叉区域指针

32位交叉区域指针与32位内部指针相比,地址指针中带有存储区域如I、Q、M等, 32位交叉区域地址指针的格式如图15所示。

图 15

第0位~第2位作为寻址操作的位地址,第3位~第18位作为寻址操作的字节地址,第24位~第26位为地址标识符,表示的地址区域如下:

000表示没有地址区,例如P#12.0;

001表示输入地址区I,例如P#I12.0;

010表示输出地址区Q,例如P#Q12.0;

011表示标志位地址区M,例如P#M12.0;

100表示数据块(DB)中的数据,例如P#DBX12.0

101表示数据块(DI)中的数据,例如P#DIX12.0

110表示区域地址区L,例如P#L12.0;

111表示调用程序块的区域地址区V,例如P#V12.0;

第31为内部区域与交叉区域指针标识,0表示内部区域指针,1表示交叉区域指针。使用交叉区域指针的表示方法(例如装载M存储器一个字节)为:

LAR1/2 P#M 20.0 //装载地址指针P#M20.0到AR1或AR2。

指针指向地址 = 地址寄存器存储地址 + 地址偏移常量,上例中实际装载的地址为MB30。如果访问一个位信号则没有访问宽度。32位交叉区域指针的使用方法参考下面的示例程序:

LAR1 P#M 20.0 //将指针P#M20.0 装载到地址寄存器1中。

A [AR1,P#1.1] //M21.1“与”操作。

= Q 1.2 //如果M21.1为1,输出1.2为1。

L P#I 40.0 //将指针P#I40.0 装载到累加器1中。

LAR2 //将累加器1中存储的地址指针装载到地址寄存器2中。

L W [AR2,P#0.0] //装载IW40.0到累加器1中。

T MW 60 //将累加器1中存储的数值传送到MW60中。

2.5 使用寄存器间接寻址的思考

估计大家读完了寄存器间接寻址后,肯定有一些问题:

1:什么时候用地址寄存器间接寻址?

在我来看,地址寄存器是寄存器,利用寄存器间接寻址是使用寄存器存储指针,这两个要搞明白。地址寄存器是系统的,主要是系统处理内部的地址偏移和引用的问题,例如在FB块的开始写入指令T AR2,是指出在背景数据块数据开始的偏移地址(例如FB1调用多个FB块时,在OB1中调用FB1后生成一个多重背景DB块,其中包含各个调用FB块的数据区,如果没有编写上述的指令,那么调用FB块的数据区在背景数据块中将重叠),所有地址寄存器是不可或缺的!利用寄存器间接寻址使用最多的是拆分POINTER和ANY指针使用的(后续再介绍),其它使用的地方很少,还看个人的编程习惯吧。S7-1500没有绝对地址,所以也没有什么地址偏移的问题(即使有也不需要用户考虑),地址寄存器在S7-1500中是虚拟的,主要就是为了S7-300/400程序的移植,所以移植完成后还要测试一下程序,看看是否达到了原来的控制功能。

2:谁会使用地址寄存器?

很显然,地址寄存器还有数据块寄存器通常是程序块的开发人员使用,否则编程的程序块在有的调用方法上会出问题,主要是程序不完善造成的,程序块的使用者掌握存储器间接寻址就可以了,除非是特殊应用,例如使用指针变量(ANY 或者POINTER)赋值ANY 或者POINTER接口参数。

3:32位的存储器指针与32位的寄存器指针(交叉和内部)有什么区别?

我想应该是一样的,参考下面的一条指令:

LAR1 MD 20 //将存储于MD20中的指针装载到地址寄存器1 中。

指针可以相互传达,说明是一样的,所以指令也可以这样写:

LAR1 P#M10.0 //将指针P#M10.0装载到地址寄存器1 中。

L P#M10.0 //将指针P#M10.0装载到累加器1 中。

T MD 20 //传递到MD20中

指针类型虽然比较多,前面说了不好再归纳,因为再归纳反而不容易入门学习(划分),如果理解深刻了在你的头脑中就会形成自己的知识体系,便于记忆和使用,感觉就跟网络小说里面的“炼化”一样。

【6】pointer 和ANY

3 参数类型指针-POINTER与ANY

所谓参数类型指针就是专用于函数FC及函数块FB接口参数的传递,例如可以在IN、OUT、IN/OUT中声明这些变量,也可以在TEMP区(OB也可以)中声明作为一个变量赋值给调用FC/FB的接口参数。

3.1 POINTER指针的数据格式

POINTER指针占用48位地址空间,数据格式如图16所示。

POINTER指针前16位的数值表示数据块DB或DI的块号,如果指针没有指向一个DB块,则数值为0,POINTER指针可以指向的数据区参考下表:

与区域交叉指针相比,POINTER类型指针可以直接指向一个数据块中的变量,例如P#DB1.DBX0.0,所以 POINTER(6个字节)=DB块号(2个字节)+32位指针(4个字节,带有数据区的寄存器和存储器指针)。如果不是指向DB块,就是带有数据区的寄存器和存储器指针,例如P#M2.0。

调用FB、FC时,对POINTER指针数据类型的形参进行赋值时可以选择指针格式直接赋值,例如:

P# DB2.DBX12.0 //指向DB2.DBX12.0。

P#M12.1 //指向M12.1。

也可以选择使用地址声明或符号名(不使用符号P#)的方式进行赋值,例如:

DB2.DBX12.0 //指向DB2.DBX12.0。

M12.1 //指向M12.1。

3.2 ANY指针的数据格式

ANY数据类型指针中包括数据类型、重复系数、DB块号、存储器数据开始地址,占用80位地址空间,数据格式如图17所示。

ANY指针使用的数据类型(存储区)参考下表

ANY指针使用的数据类型(数据区)

ANY指针使用的数据类型(参数类型)参考下表

ANY指针使用的数据类型(参数类型)

ANY指针中的数据长度表示指向一个数据区域,例如指向整个数组、结构体等;如果ANY指针没有指向一个DB块,DB块号将为0;ANY指针的数据区与POINTER指针数据区定义相同。

与POINTER指针相比,ANY类型指针可以表示一段数据区域,例如P#DB1.DBX0.0 BYTE 10,表示指向DB1.DBB0~DB1.DBB9。调用FB、FC时,对ANY指针数据类型的形参进行赋值时可以选择直接使用指针格式赋值,例如:

以P#开头用于存储区数据类型

P# DB2.DBX12.0 WORD 22 //指向从DB2.DBW12开始22个字。

P#M12.1 BOOL 10 //指向从M12.1开始10个位信号。

以L#开头用于参数数据类型

L#2 BLOCK_FC 4 //指向从FC2开始4个FC。

L#4 TIMER 5 //指向从T4~T8。

也可以选择使用地址声明或符号名(不使用符号P#)的方式进行赋值,例如:

DB2.DBW12 //指向DB2.DBW12一个字,数据长度为1。

M12.1 //指向M12.1一个位信号,数据长度为1。

FC2 //指向FC2。

3.3 POINTER与ANY指针的拆分

32位地址指针可以直接装载到存储器或地址寄存器中,从而可以直接在程序块中使用,进行间接寻址,大家知道,S7-300/400可以直接引用的变量最大为4个字节,POINTER和ANY参数类型指针作为一个整体并大于4个字节而不能装载到存储器或寄存器中,所以不能在程序块中直接使用,必须进行拆分使用。拆分的指令只能使用STL完成,指令格式为:

P## <POINTER与ANY的符号名称>

表示指向POINTER与ANY指针的首地址,就是装载POINTER与ANY指针变量的开始地址。为了便于理解,假设FC接口中的一个ANY变量V_ANY开始地址为0(如果是临时变量区则为L0),那么指令:

L P##V_ANY //指向指针V_ANY的开始地址即L0。

如果将开始地址存储于存储器中,例如

T MD20 //将开始地址存储于MD20中,存储P#L0.0

如果要读出赋值ANY变量的数据类型就必须地址偏移一个字节即:

L MD 20

L P#1.0

+D

T MD 24 //数据类型开始地址存储于MD24中即P#L1.0

但是后面的问题来了,怎么样读出这个地址中的值?下面这样编写语法错误:

L B[MD24] //语法错误

只能使用交叉区域指针的方式,我在上一篇已经介绍了,

LAR1 MD 24 //将MD24中地址存储于地址寄存器AR1中

L B [AR1,P#0.0] //装载ANY指针的数据类型

T MB200 //存储于MB200中。

所以所有的拆分程序都必须使用寄存器交叉区域指针编写(不能使用存储器指针),这样就变成了固定格式,以ANY指针类型为例介绍拆分指令:

L P##V_ANY //指向指针V_ANY的开始地址即L0。并装载到地址寄存器AR1中。

LAR1

L B[AR1,P#1.0] //存储ANY指针的数据类型即LB1

L W[AR1,P#2.0] //存储ANY指针的数据长度即LW2

L W[AR1,P#4.0] //存储ANY指针的数据块号即LW4

L D[AR1,P#6.0] //存储ANY指针的开始地址区即LD6

IN、OUT、IN/OUT接口区不能显示绝对地址,所以必须使用拆分指令。

为了让大家更好地理解POINTER和ANY指针的应用,介绍一个编程应用。

3.4 ANY指针编程应用

编写一个计算功能的函数FC13,输入参数“In_Data”为一个数组变量,如果数组元素为浮点数,输出所有元素的平均值“OUT_VAL”,如果数组元素为其它数据类型,不执行计算功能。OB1中调用函数FC13的程序如下:

CALL FC 13 // 调用函数13。

In_Data:=P#DB1.DBX0.0 REAL 8 //输入数据区从DB1.DBD0开始8个浮点值。

OUT_VAL:=MD20 //计算结果。

完成的计算功能相当于MD20:=(DB1.DBD0+..+..+DB1.DBD28)/8。在函数FC13的接口参数中定义输入、输出变量及临时变量参考下表:

FC13接口参数

FC13中的示例程序如下:

3.5 使用ANY指针变量赋值

上面的示例程序在调用FC13时赋值的ANY数据类型为常数,如果使用变量则数据区、长度都可以变化,下面的应用示例使用ANY变量赋值SFC20(块复制)的接口参数,即将P#DB1.DBX0.0 INT 10复制到P#DB1.DBX20.0 INT 10中,函数接口声明参考图18。

在IN、OUT、IN_OUT区声明ANY变量没有绝对地址,所以必须使用拆分指令,在临时变量中声明ANY类型指针带有绝对地址,所以可以不用拆分指令直接赋值,示例程序如下:

数据块、地址区、开始地址和长度都可以作为变量,从而增加了程序的灵活性。

3.6 S7-1500处理参数类型指针的方式

1:如果习惯了S7-300/400的编程方式,可以直接使用AT指令在接口参数中进行拆分,这样使用LAD也可以编程,参考图19。

2:除此之外,还可以使用可变数组替代ANY类型指针,在S7-1500中可以非常方便地实现3.4章节的应用示例。在FC的接口参数中可以声明一个可变数组类型变量IN_DATA,数组元素为real(这个不可以变,在Variant中可以实现,后续章节介绍),输出所有元素的平均值“OUT_VAL”函数接口声明参考图20。

示例程序参考图21,比起Any的拆分,有股脱离苦海的感觉,并且没有编程语言的限制。

3:使用Variant,在后续章节中介绍。

【7】Variant数据类型指针

4 Variant数据类型指针

总结一下前面介绍过的指针,ANY可以包含POINTER,POINTER可以包含32位寄存器或者存储器指针,还有一个独立的16位指针,这些指针都是一个对象,占用存储器的空间。在S7-1500中,推荐使用的是符号寻址方式,没有绝对地址,所以上述指针都不适合符号寻址方式。从面向对象的编程方式上看,一个对象具有属性,可以是REAL类型、BOOL、字符串类型的组合,就算可以使用绝对地址(非优化的程序块),使用ANY、POINTER指针进行地址的迭代也感觉非常奇怪、不方便,不是以一个对象整处理。

在S7-1500中推出一个新的变量类型Variant,不占用存储器的空间(所以必须使用相关指令),感觉比ANY指针还要ANY,看一看Variant变量类型的特点吧:

1. 可以指向不同数据类型变量的指针。VARIANT 指针可以是基本数据类型(例如,INT 或 REAL)的对象,还可以是STRING、DTL、STRUCT 、PLC数据类型等元素构成的ARRAY。

2. VARIANT 指针可以识别PLC数据类型,并指向各个结构元素。

3. VARIANT 数据类型的操作数不占用背景数据块或工作存储器中的空间。VARIANT 类型的变量不是一个对象,而是对另一个对象的引用(ANY其实也是引用),因此不能在数据块或函数块的块接口静态部分中声明,只能在输入参数、输入输出参数、或临时变量区中声明。

4. 调用含有VARIANT类型参数的块时,可以将这些参数连接到任何数据类型的变量。块调用时,除了传递变量的指针外,还会传递变量的类型信息。块中的代码随后可以根据运行期间传递的变量类型来执行。

重点部分已经加粗标记,为了更加清晰化Variant变量类型的特点,我们使用示例的方式进行介绍。

4.1 VARIANT与PLC数据类型

在S7-1500中还推荐使用PLC数据类型,就是UDT,在UDT中可以定义控制对象的属性,当然,一个控制对象可以包含多个UDT。UDT就是一个优化的结构体变量,区别就是,1:在多个程序块中使用了相同的结构体变量,如果需要修改结构体变量,则必须逐一修改每一个块中的结构体变量,使用UDT可以中央修改,然后更新所有块中调用的UDT;2:UDT可以作为库中的模板,具有版本控制功能。

下面介绍一个VARIANT与PLC数据类型的使用示例(示例仅供参考),怎么识别PLC数据类型(参考VARIANT的特点2、4)。

假设变频器可以控制两种报文格式的通信,分别是PZD8/8(8个字输入/输出)和PZD10/10(10个字输入/输出),函数和函数块的开发者使用VARIANT作为输入形参,然后进行判断分析。好处是可以不用考虑程序块使用者需要赋值的是哪一个格式的报文(如果是数组可以使用可变数组,如果是结构体则可能变得麻烦)。

首先创建4个PLC数据类型(UDT),PZD8_IN、PZD8_OUT、PZD10_IN和PZD10_OUT,分别对应PZD8/8和PZD10/10的报文格式。然后创建一个FB块,定义接口参数和temp变量如图21所示。

图21

FB块中的程序参考图22。

图22

程序中对输入参数“mess format_in”的格式进行判断,如果数据类型是“PZD8_IN”,

则使用指令“VariantGet”将“mess format_in”的数据读出并复制到临时变量“temp_ PZD8_IN”,然后用户程序对变量“temp_ PZD8_IN”(变频器PZD 8输入的格式)的数据进行处理;如果数据类型是“PZD10_IN”,则复制到临时变量“temp_ PZD10_IN进行数据处理;如果数据类型是“PZD8_OUT”,则对临时变量“temp_ PZD8_OUT进行数据处理,然后使用指令“VariantPut”写回到参数“mess format_out”作为输出。

上面示例介绍的是Variant变量的识别功能,读写Variant变量的内容必须调用程序块,“VariantGet”和“VariantPut”,不能直接赋值,例如:

#TEMP_PZD8_IN:=mess format_in;

这是因为Variant变量不占用存储器空间,是对另外一个对象的引用,参考Variant变量特点3。

【8】Variant的使用

4.2 VARIANT与DB_ANY

4.1章节的示例程序在调用时只能赋值一个固定的类型,例如PZD8_IN或者PZD10_IN,一些应用需要赋值的参数也要根据要求变换,例如MES系统向PLC发送一组物料信息,PLC系统接收到数据后进行判断然后处理,MES系统可以向PLC发送不同的物料信息,每种物料信息的属性可以是不一样的,如果按照上面示例的方式,必须调用程序块多次,然后赋值给参数不同的物料信息,使用DB_ANY变量可以解决这样的问题,可以将PLC数据类型存储在数据块中,然后转换为VARIANT变量,最后对VARIANT变量进行分析判断从而得到需要处理的物料类型,参考下面的应用示例。

例如一个应用要处理3个不同的物料,物料信息由MES发送到PLC,在PLC中需要判断物料的类型,然后分别处理。

建立3个PLC数据类型Material_A 、Material_B 和Material_C代表3个物料类型 ,然后以这3个数据类型建议3个数据块 DB_ Material_A 、DB_ Material_B 和 DB_ Material_C。

创建一个FC块,接口声明和程序代码如图23所示。

图 23

指令DB_ANY_TO_VARIANT可以将输入的参数DB_ANY转换为VARIANT,DB_ANY的使用是有要求的,必须是格式固定的,参考下表(TIA博途的在线帮助)中的加粗标记。

程序中将DB_ANY类型转换为VARIANT类型,然后判断输入的PLC数据类型的格式,分别执行处理物料A、B和C的程序。

程序块在主程序中调用,如图24所示。

图 24

调用程序块时可以使用INT类型赋值DB ANY,例如“Tag_2”,“Tag_2”在变量表中定义的数据类型为DB ANY,“Tag_2”值与数据块的号相对应,例如值为3,表示输入的数据块为DB3。

有一个小问题,好像PLC系统并不能解析存储于数据块中的数据类型,调用DB_ANY_TO_VARIANT可能会得到输出错误代码 #8155,下面是TIA博途在线帮助中列出的原因和解决办法:

声明了一个 PLC 数据类型 (UDT1) 并创建了一个数据类型为“UDT1”的数据块 (例如DB2)。变量表中创建一个数据类型为 DB_ANY 的变量 (例如Tag_2)。随后,在主程序中调用了指令“DB_ANY_TO_VARIANT”并在 IN 参数中赋值变量 Tag_2。执行时,指令“DB_ANY_TO_VARIANT”返回错误代码 16#8155。

通过以下步骤消除该错误代码:

1)创建函数 (FC5) 并在 InOut 接口中声明数据类型为 VARIANT 的变量。

2)创建另一函数 (FC6),在 FC6 中的 Temp 接口中创建数据类型为“UDT1”的变量(例如Tag_1),然后调用 FC5,为 FC5 的 InOut 接口赋值变量Tag_1。

3)编译函数块(FC5 和 FC6)并下载到 CPU 中。在用户程序中无需调用这些块(FC5 和 FC6)。其实就是让系统先识别一下PLC数据类型,然后再调用就没有问题了,无语了。

4.3 VARIANT与数组DB

数组DB也是一个固定格式,就是在创建数据块时选择数组DB,参考图25。

图 25

数组DB与在全局DB中建立一个数组有什么区别呢?通常在全局数据块中包含一个或者多个数组,可能还有其它的变量,也就是说数组DB格式固定,可以与VARIANT进行转换。下面看一看数组DB的应用场合。

例如有一个应用,将接收到的物料信息(包括物料号、名称、数量、单位)存放到一个数据块中(堆栈操作)。如果存放满了,将覆盖最先进入的信息。函数、函数块的开发者面临的难题是不知道程序块使用者(开发与使用分开)赋值物料信息的格式和存储的个数(数据块的大小),这时使用VARIANT和数组DB将使开发过程变得非常容易,下面介绍实现的过程。

首先创建一个PLC数据类型“Mess_queue”,格式如图26所示。

图 26

然后创建一个函数,例如FC_Enqueue(堆栈操作),函数的接口声明如图27所示。

图 27

编写的代码如图28所示。

图 28

使用者需要创建物料的PLC数据类型“Material”和数组DB “materialBuffer”,并定义数组的长度,数组DB如图29所示。

图 29

然后创建一个PLC数据类型(Mess_queue)的DB,名称为“Material_DB”,并在启

动OB中初始化,如图30所示。

图 30

注意这里非常重要,将数组DB“materialBuffer”赋值给一个DB_ANY变量。在主程序中调用FC_Enqueue并赋值,如图31所示。

图 31

输入变量“material_input”为PLC数据类型“Material”,在变量表中定义,开始地址为I100.0。在这个示例中,程序的开发者在编写堆栈程序块FC_Enqueue时,使用VARIANT指针替代了未知的物料信息,使用DB_ANY替代了物料堆栈的存储空间。使用者创建物料堆栈的存储空间,必须是数组DB,然后通过DB_ANY传递给指令“WriteToArrayDB”,数组DB的使用在这里起到了至关重要的作用。

4.4 VARIANT与数组

VARIANT还可以指向一个数组,得到数组的类型、元素的类型和长度信息,主要用于数

据格式的比较。下面以示例方式介绍VARIANT与数组的使用。与介绍Any指针的应用相同,给定一个数组,如果数组元素的数据类型为浮点,求这个数组所有元素的平均值(接《从S7-300_400到S7-1500看变址寻址的改变-6》,3.6 节 的第三种方法)。首先创建一个FC,例如名称为AVERAGE,接口声明如图32所示。

图 32

函数AVERAGE的代码如图33所示。

图 33

代码说明:

通过形参“vaule”判断赋值的实参数据类型是否为数组、数组元素的数据类型是否为浮点,如果满足上述要求,使用指令“countofElements”得到数组的个数。数组的个数用于循环累加的次数。使用指令“MOVE_BLK_VARIANT”每次将数组一个元素(索引从0开始,与赋值数组的索引无关)的值存储到临时变量“temp_value”,然后进行累加运算,将累加的结果存储到临时变量“temp_valueadd”中,最后输出平均值到输出参数“value_out”中。

【9】引用

5 引用(References)

先说一下我自己的感觉,在TIA博途V13 SP1(具体是哪一个版本忘了)以前是没有引用(References)的,指正介绍都是围绕着Variant进行的。后续的版本才推出了引用(References),原来为什么没有推出?要不就是计划有,但是没有开发出来;要不就是市场有新的需求,新加入的功能。既然推出引用(References),就有它的应用地方,例如程序块的开发者声明一个INOUT参数,数据类型为Variant,如果程序块的开发者想修改赋值变量中的一个元素怎么办? 参考上面Variant部分,是不是先使用typeof判断类型,然后使用指令VariantGet读出并复制到一个类型相同的副本中,再修改变量中一个元素(例如PLC数据类型),最后使用指令VariantPut写出,问题出现了,写的时候只写其中一个元素,其它元素怎么办?一定是使用读出的值替代,如果外面的值发生变化,就会将过程值覆盖。怎样才能修改其中一个元素又不覆盖变量中其它元素的值呢?只有在INOUT参数中声明一个复制类型(不能是引用类型)的变量才可以,例如DINT、real类型变量,这样将使程序的灵活性降低(不能使用Variant了),为了解决这样的问题才推出了引用(References)类型(只代表我的理解呀!)。好了下面开始介绍引用(References)了。

引用(References)是一种变量,不含任何值却指向其它变量的存储位置,参考图34。通过引用,可在块外进行变量传递。因此,可直接修改变量的值,而无需创建变量副本。

图 34

使用加粗标记的就是引用(References)的特点。

5.1 引用声明

使用引用限制条件还是比较多的,看看下面的限制条件:

1:必须事先声明,也就是创建一个引用变量。引用可在优化访问的函数或函数块的接口中声明。并且地址区必须是临时变量区,例如:

● FC:Input, Output, Temp, Return

● FB:Temp

● OB:Temp

要进行引用声明,可使用关键字“REF_TO”指定被引用变量所需的数据类型。参考图35。

图 35

2:引用与被引用的数据类型必须相同,应用的数据类型如下:

● 位字符串(BYTE 、WORD、 DWORD、LWORD)

● 不支持 BOOL 引用。

● 整数

● 浮点数

● 字符串

● 不支持针对字符串的长度声明。

● IEC 定时器

● 支持 IEC_TIMER 和 IEC_LTIMER 引用。不支持派生数据类型引用,例如 TON。

● IEC 计数器

● 支持 IEC_COUNTER/IEC_UCOUNTER、IEC_SCOUNTER/IEC_USCOUNTER、IEC_DCOUNTER/IEC_UDCOUNTER 引用。不支持派生数据类型引用,例如 CTU。

● PLC 数据类型 (UDT)

● 系统数据类型 (SDT)

● 已声明的数据类型的 ARRAY

● 不支持 ARRAY[*] 引用。不支持没有声明的数据类型的ARRAY,例如ARRAY of

● REF_TO<数据类型>。

5.2 变量的引用

引用声明只是创建了一个变量和引用的数据类型,没有指向有效的存储器,系统使用值NULL对其进行初始化,如果在编程中使用将导致编程错误,所以必须为引用分配实际地址。

引用只能指向优化的全局 DB 或FB块静态变量中的数据。

使用关键字“REF()”,可将声明的引用指向变量,如图36所示。LAD和SCL两种编程语言都支持变量的引用。

图 36

5.3 解引用

引用指向变量只是指向变量的实际地址或者物理地址,要读取或写入一个被引用变量的值,则可使用插入符号“^”,这种访问方式又称为“解引用”。解引用的使用参考图37所示,程序块运行一次,“c”的值为15,此时“a”的值为10,在第二条语句中,将“My refint”指向变量“a”(引用),在第三条语句中解引用变量“My refint”并赋值20,这样20赋值给引用变量“My refint”, 引用变量“My refint”指向变量“a”的地址,实际上是将20传递到变量“a”,这时“c”就变为25了,第四、五条语句作用相同。如果程序再次执行一次,第一条语句中“c”的值将被25替代。

图 37

引用指向的是变量的地址,而解引用得到变量的值,在应用中容易混乱出错,例如语句:#ref_int1 := #ref_int2; 这条语句是将#ref_int2引用的变量地址赋值给#ref_int1,如果#ref_int2没有分配地址,相

当于将NULL赋值给NULL,程序将报错。引用也可以应用到函数、函数块间的数据传递。例如FC1(Block_1)中使用接口声明计算A+B=C,在OB中调用FC1并赋值,示例参考图38,

图 38

可以看到调用FC1时为接口声明A和B指向了实际地址,但是C有问题,赋值的方向反了,可以在FC1直接为引用C分配变量,或者不使用引用,示例如图39。

图 39

5.4 引用与VARIANT

引用声明时,使用关键字“REF_TO”指定被引用变量所需的数据类型必须是确定的,如果是数据类型是VARIANT,则不能使用上面介绍的引用方式,可以使用赋值尝试“?=”,将 VARIANT 数据类型的变量分配给一个引用。由于引用是后期开发的指令,与VARIANT数据类型的结合使用使开发更加灵活和简单。

VARIANT可以读出输入变量的数据类型,然后通过VariantGet指令复制到一个类型相同的副本中进行分析判断,使用赋值尝试“?=”,将 VARIANT 数据类型的变量分配给一个引用将使这样的应用变得更加简单,参考图40。

图 40

引用声明一个变量“Ref_motorA”,指定的数据类型为“REF_TO "Motor_A"”("Motor_A"为PLC数据类型),通过赋值尝试“?=”而不是关键字“REF()”,将VARIANT变量分配给“Ref_motorA”,如果分配成功,则变量“Ref_motorA”不等于 “NULL”,否则等于“NULL”。然后将“电机A”赋值给变量“#Ref_motorA^.Info”。

赋值尝试“?=”的方式也支持LAD编程语言,如图41所示,使用LAD编程语言调用赋值尝试指令“?=”,如果分配成功,ENO等于1,否则等于0。

图 41

上述的应用如果使用先前的方法:首先需要使用指令type of判断数据类型,然后使用VariantGet指令将数据复制到一个副本中,修改以后再使用VariantPut指令写回(开头应用介绍的使用场景),使用赋值尝试“?=”和引用的方式,使函数和函数块的开发变得更加方便。

好了,所有变址寻址的方式已经介绍完了,希望大家可以受益。上面的描述大部分是自己多年的总结和目前学习的心得,不足之处还请大家指正和补充,也希望大家提供一些应用的案例(自己的案例都是猜想,有可能不符合实际),一起探讨,分享给其他工程师学习使用!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-10-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 科控物联 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
数据保险箱
数据保险箱(Cloud Data Coffer Service,CDCS)为您提供更高安全系数的企业核心数据存储服务。您可以通过自定义过期天数的方法删除数据,避免误删带来的损害,还可以将数据跨地域存储,防止一些不可抗因素导致的数据丢失。数据保险箱支持通过控制台、API 等多样化方式快速简单接入,实现海量数据的存储管理。您可以使用数据保险箱对文件数据进行上传、下载,最终实现数据的安全存储和提取。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档