☰
S7-200 SMART指针寻址库:让存储区像数组一样用
2026/10/18 8:25:06 网站建设 项目流程

前阵子做一条分拣线的PLC程序,里面有大段逻辑要反复读写V区里的配方数据。一开始全是直接寻址,VB、VW、VD写得到处都是,改一个偏移量就得把好几个网络翻出来重改。后来我想明白一件事:S7-200 SMART的V区本质上就是一块连续内存,直接寻址只是把地址写死了而已,如果我们用指针再加上一些封装,完全可以让这块存储区拥有类似高级语言里数组的用法——位数组、字节数组、字数组都能做。这篇文章就把我做的这套“寻址-库”的思路、代码和踩过的坑完整写出来,给同样被S7-200 SMART寻址问题折腾过的朋友参考。

1. 为什么我会折腾一个“寻址库”

1.1 批量数据处理时直接寻址的尴尬

S7-200 SMART没有数组类型,也没有结构体,这是很多从高级语言转过来的人第一个不适应的地方。你在HMI上配了50个配方参数,存到V区就是VB0到VB49;想处理第n个参数,只能手动算出地址,写VB0、VB1、VB2……这种展开式的代码。

举个例子,要把一片连续的V区清零,直接寻址就得写一串MOVB指令:

MOVB 0, VB100 MOVB 0, VB101 MOVB 0, VB102 MOVB 0, VB103 MOVB 0, VB104 ……

40个字节还好,200个字节怎么办?再加个“从第n个字节开始清m个字节”这种需求,直接寻址基本就写不动了。我当时那个项目里,配方区一共120个字节,每条配方12字节,要按“第几条配方”去访问,代码里全是手动算好的偏移量,比如第3条配方起始地址是VB100+3*12=VB136。改配方长度,所有地址全部重算,还容易算错,查错查到怀疑人生。

后来我意识到,这不是PLC不行,是我的写法有问题。V区就是一块连续内存,数组的本质无非就是“基址+下标×元素大小”,那我只要把“地址计算”这件事封装起来,让调用者传一个首地址和一个下标,剩下的运算全交给子程序,问题不就解了。

1.2 指针:把“写死的地址”变成“算出来的地址”

PLC有两种寻址方式。直接寻址,好比快递单上写死“XX小区XX栋XX室”,换个包裹就得重新写一单;间接寻址,相当于先告诉快递员“去小区门口,根据门牌号找”,地址是可以算的。

S7-200 SMART的间接寻址用的是32位指针。你可以用“&VB100”把VB100的地址抓出来,放到一个双字里,再用“*”去访问这个双字指向的单元。比如:

MOVD &VB100, VD200 MOVB *VD200, VB300

这段代码的意思就是:先把VB100的地址放到VD200,然后执行第二句时,CPU通过VD200里存的地址找到VB100,把它读到VB300。VD200像个路牌,指向哪里由路牌决定,程序运行时改路牌内容,访问的目标地址就变了。

数组访问的底层就是这条路牌机制:我要访问第i个字节,只要让路牌指向“首地址+i”,然后用“*”一取,就拿到数据了。有了这个思路,“类数组寻址”就有了实现的可能。

1.3 封装成库之后,调用长什么样

如果只是偶尔用一次指针,直接在程序里写也就算了。但项目里要反复读写各种数组,比如配方数组、报警记录数组、通信缓冲区数组,每次都把指针运算写一遍,代码一样乱。这时候就应该做“库”了。

S7-200 SMART的库功能,本质上就是把自己写的子程序打包成库指令文件,像官方指令一样拖到梯形图里调用。我做的这个“寻址-库”,对外只需要填三样东西:数组起始地址、数组下标、要写入或读出的数据。调用者完全不用碰指针,内部怎么算地址,全被库藏起来了。

我当时封装完之后,主程序里访问数组变成这个样子:

// 读配方区第三个字节放到VB500 Ary_RdB &VB100, 3, VB500 // 把第19位(bit数组)置1 Ary_SetBit &VB200, 19, TRUE

主程序干净得让自己都惊讶。标题里说的“寻址-库能让SMART存储区变得跟数组一样”,说白了就是把“基址+偏移”的指针运算做成库,把存储区当数组用,位数组、字节数组、字数组都能这么折腾。

2. 先把底层摸透:存储区布局与指针机制

2.1 常用存储区和V区的定位

S7-200 SMART的存储区大致有这几块:I区是输入映像区,Q区是输出映像区,M区是内部标志位,V区是用户数据区,SM区是特殊功能标志,S区是顺序控制继电器,还有定时器T、计数器C、累加器AC这些。做数组寻址,最常用的是V区,因为它就是留给用户随意放数据的连续空间,掉电保持也可以通过系统块设置。

V区从VB0开始,按字节、字、双字编址,例如VB100是单字节,VW100是以VB100为高字节的一个字,VD100是以VB100为高字节的一个双字。这些细节大家都知道,但真正要玩数组时,要意识到一件事:V区的地址是按字节连续排列的,所以“数组”在硬件层面并不神秘,它就是一组连续的V区字节。

顺便提一句,M区其实也可以当数组用,只是M区容量小,一般只放少量布尔标志和中间变量。真正常规做法是把配方、报警、通信缓存这类数据放V区。库设计时基址参数可以支持I、Q、M、V任意区的地址,因为S7-200 SMART取地址操作符“&”能对这些区域统一取址。

2.2 S7-200 SMART指针的规则

指针在S7-200 SMART里是32位双字,里面装的是地址。取地址用“&”,间接访问用“*”。有几个规则是写库前必须搞清楚的:

  • 指针加1,相当于向高地址方向移动1个字节;
  • 指针可以存放在VD区、累加器AC、局部变量L区;
  • 间接访问的最小单位是字节,你要访问字、双字,用的指令分别是MOVW、MOVD,指针本身不区分类型,访问时的指令决定访问宽度;
  • 不能用指针动态访问“某一位”,位没有独立地址,必须靠字节读、改、写。

第4条非常关键。举个例子,你能写“M0.5”这种位,但你不能把一个指针算到M0.5上然后直接通过指针来置位它。这一点决定了后面位数组的封装方式。

2.3 为什么位数组不能“直接指过去”

很多人一开始想把位数组做成“arr[19]就是某字节的第3位”,然后试图在梯形图里写一个动态位地址,结果发现S7-200 SMART根本不支持。平面PLC的位寻址必须写死,一个位变量在编译时地址就固定了,CPU没办法在运行时根据一个计算出来的数去访问“第几个位”。

正确做法是把位数组看成两层结构:外层是字节编号,内层是位编号。访问第19位,先算出它落在哪个字节(19÷8=2,也就是基址+2的那个字节),再算出它是该字节的第几位(19 mod 8=3),然后用字节目录的方式把这个字节读出来,在内存里做位运算,再写回去。

这就是所谓“读-改-写”。CPU读整个字节,改其中一位,把整个字节写回。因为位本身没有独立地址,我们必须以字节为最小操作单位,把位运算搬到逻辑运算里完成。理解了这一点,位数组库的设计就清晰了。

3. 寻址库的接口设计:让别人只填首地址和下标

3.1 对外暴露哪些函数

我做这套库时,函数命名用了“Ary_”前缀,方便跟普通子程序区分。第一批只做了5个最常用的:

函数名形参功能
Ary_RdBpBase(DWORD), nIndex(INT), data(BYTE_OUT)读字节数组
Ary_WrBpBase(DWORD), nIndex(INT), value(BYTE_IN)写字节数组
Ary_RdBitpBase(DWORD), nIndex(INT), data(BOOL_OUT)读位数组
Ary_SetBitpBase(DWORD), nIndex(INT)位置位
Ary_RstBitpBase(DWORD), nIndex(INT)位复位

pBase就是数组首地址,调用时写“&VB100”这类取地址表达式;nIndex是下标,从0开始。位数组的下标对应“第几位”,比如nIndex填19就是数组里的第19位,至于它落在哪个字节的哪一位,调用者完全不用管。

后来我还加了带长度参数的保护版本,比如Ary_RdB_Len,多一个nLen参数,越界时置错误标志。这个在后面会细说。

3.2 为什么参数越少越好

有人可能会想,把子程序写成“起始地址、元素个数、下标、读/写方向、数据”五个参数,看起来很全,但实际使用体验很差,而且S7-200 SMART子程序的局部变量区只有64字节,形参和临时变量全挤在一起,参数太多容易踩L区越界。

把接口拆小,每个函数只干一件事,参数少,一来L区压力小,二来调用处直观,三来库生成后别人用起来也容易上手。读字节就传三个参数,置位就传两个参数,简单粗暴。

另外,参数类型的选择也要注意。地址参数用DWORD,不要用INT,因为S7-200 SMART的地址是32位;下标我用INT,因为数组长度一般在几千以内,16位有符号整数足够了。如果你要支持大于32767的元素数,再把下标换成DINT。

3.3 从字节数组到字/双字/浮点数组

字节数组只是地基。同一个“基址+偏移”的思路,访问宽度改一下,就能得到字数组、双字数组、浮点数组:

  • 字数组:地址偏移 = 下标 × 2,访问用MOVW;
  • 双字数组:地址偏移 = 下标 × 4,访问用MOVD;
  • 浮点数组:地址偏移 = 下标 × 4,访问用MOVR。

所以字节数组库只需要再多做两套接口,比如Ary_RdW、Ary_RdD、Ary_RdR,每一种都是“首地址+下标”往里扔,内部把下标乘以元素宽度再加到指针上。设计库时,我建议把“元素宽度”作为库内部的一个固定常量,而不是让调用者传,因为类型不同宽度不同,让调用者传容易传错,自找麻烦。

二维数组也可以扩展,无非就是“基址 + 行索引×行宽 + 列索引”。比如配方区按行存,每行12字节,我想读第3行第5列,就封装一个Ary_RdB2D,形参里加一个rowWidth。这个用起来跟C语言里的二维数组就非常接近了。

4. 字节数组库的完整实现

4.1 子程序变量表怎么定义

在Micro/WIN SMART里新建一个子程序,名字改成Ary_RdB,然后打开变量表手工定义形参。我的定义方式:

地址符号名类型注释
LD0pBaseIN, DWORD数组首地址,调用时传&VBxxxx
LD4nIndexIN, INT数组下标,从0开始
LD6dataOUT, BYTE读到的字节数据

S7-200 SMART的变量表里,DWORD占4字节,INT占2字节,OUT类型也占用L区。这里LD0到LD5是输入参数,LD6开始是输出参数。这些偏移不用手工算,软件会自己排,但是在STL里引用时,建议直接用符号名,可读性远远强过记LD几。

还需要两个临时变量:一个DWORD用来放指针,一个DINT用来放32位索引。临时变量放在变量表的TEMP段,比如LD10是#ptr,LD14是#idx32。

4.2 STL代码:读一个字节

Ary_RdB的核心就4条STL指令:

NET1: MOVD #pBase, #ptr // 把基址送入局部指针 ITD #nIndex, #idx32 // 16位下标转成32位整数 +D #idx32, #ptr // 指针偏移:ptr = 基址 + 下标 MOVB *#ptr, #data // 间接访问,取出该字节

第一句是把基址赋给指针,第二句是把INT扩成DINT,因为做32位加法时类型要一致。第三句完成核心的“基址+下标”运算,S7-200 SMART的指针是按字节移动的,所以字节数组直接加下标就行。第四句用星号间接寻址,把指针指向的那个字节送给输出形参。

写字节Ary_WrB几乎一样:

NET1: MOVD #pBase, #ptr ITD #nIndex, #idx32 +D #idx32, #ptr MOVB #value, *#ptr

只是最后一句方向反过来。就这么点东西,库把一个看似玄乎的“类数组寻址”变成了四行代码。

4.3 创建库文件与库存储区分配

子程序写完,在项目树里右键“库”,选择“创建库”,把Ary_RdB、Ary_WrB这些子程序勾选进去,填上库名字和版本号,然后最关键的一步是设置库存储区。

库指令被调用时,需要一块V区作为内部暂存和状态记忆区,软件会弹出一个“库存储区”对话框,让你指定起始地址,并提示总共需要多少字节。我的建议是:

  • 把这片区域放在主程序不太会使用的V区末尾附近,比如CPU地址范围后段;
  • 库存储区一旦分配,主程序里不要再占用,否则数据会被悄悄覆盖;
  • 每次添加或修改库指令实例后,重新编译时留意软件的存储区分配变化。

我把库存储区设在了VB2000起始,然后主程序从头到尾都没有碰过VB2000开始的这几十个字节,运行几个月没出过存储冲突。

4.4 主程序调用时最容易踩的坑

库生成以后,拖到梯形图里,最常犯的错是pBase形参不写“&”。这个我见得最多,很多第一次用的人直接在pBase里填VB100,编译直接报类型不匹配,因为DWORD形参要的是地址常量,不是字节数据。

填法应该是“&VB100”,不是“VB100”。哪怕VB100前面已经定义了符号名,梯形图里也得写地址取址表达式。

另外,nIndex用的是INT类型,如果你在程序里用VW200存下标,注意VW200是有符号数,千万不要传大于32767的数值。还有一个小细节:库调用时EN位必须接通,很多人把EN悬空就以为能执行,结果整个网络不执行还半天找不到原因。

5. 位数组库的完整实现

5.1 位下标换算:右移和与运算

位数组库的下标是对“位”编号的。比如第19位,意思是从数组起始字节的第0位开始数,数到第19个bit。换算公式很简单:

  • 字节偏移 byteIndex = nIndex / 8;
  • 位号 bitIndex = nIndex mod 8。

用C语言思路写就是:

byteIndex = nIndex >> 3; bitIndex = nIndex & 7;

除以8就是右移3位,取余8就是与7按位与。我在STL里用了移位和与运算,而不是除法指令,因为移位更快,而且不用处理商和余数两个输出。

举个例子:nIndex=19,byteIndex=2,bitIndex=3,也就是说第19位存放在“基址+2”这个字节的第3位,也就是二进制0000_1000对应的那一位。

5.2 读位的实现

读位子程序Ary_RdBit的伪代码:

ptr = pBase + byteIndex; b = *ptr; b = b AND (1 << bitIndex); if b <> 0 then data = TRUE else data = FALSE;

先算字节地址,把整个字节读回来,然后用掩码把目标位单独拎出来。掩码的计算也很简单,1左移bitIndex位:bitIndex=0时掩码是0x01,bitIndex=3时掩码是0x08,bitIndex=7时掩码是0x80。

在STL里,可以先把掩码放一个局部字节,再与读回的字节做ANDB,最后用一个比较指令判断结果是否为0,从而给布尔输出赋TRUE或FALSE。注意,这里不能直接通过指针去读“位”,位的判断必须落在字节层面,这是S7-200 SMART的硬性限制。

5.3 置位/复位的读改写逻辑

置位Ary_SetBit的伪代码:

ptr = pBase + byteIndex; b = *ptr; b = b OR (1 << bitIndex); *ptr = b;

复位Ary_RstBit的伪代码:

ptr = pBase + byteIndex; b = *ptr; mask = 1 << bitIndex; b = b AND (NOT mask); *ptr = b;

这两个函数是读者写位数组库最喜欢的部分。它们看起来简单,但“读一遍、改一位、写回去”这个动作有一个隐藏风险:如果主程序和中断程序同时访问同一个字节数组,就可能互相覆盖。

举个例子,主程序正执行到“读出字节、准备把第3位置1”,按下没写完时中断来了,中断程序把第5位清零并写回,等中断返回主程序继续把整个字节写回,第5位就被主程序给覆盖回去了,中断写的白写了。好在这个问题可以用开/关中断指令规避:在进行读改写之前禁止中断,完成写回之后恢复中断。代价只是那几条指令的时间,整个字节数组操作本来也就是微秒级,牺牲很小。

5.4 越界参数和中断共享问题

S7-200 SMART没有任何数组越界保护,指针算到哪就是哪。库如果不做长度检查,数组下标超了,它会去访问相邻地址的数据,轻则读错数据,重则把一个无关的字节写坏,而且这种故障非常隐蔽,经常是运行好久才偶发。

所以正式项目里,我给每个数组库函数都加了长度保护版本。接口里加一个nLen参数,调用者把数组长度传进来,函数内部先判断nIndex是否在[0, nLen)之间,越界就跳过执行并把一个错误标志置位。虽然多传一个参数,但换来的安全性非常值。

中断共享的问题再补一句:如果你确定某个字节数组只在主程序里用,不进中断,那不需要处理;一旦中断程序也访问同一个数组,就必须在访问前禁止中断,访问后恢复,或者干脆用两个独立的数组空间,别让它们共享同一片内存。

6. 实测中的坑库与性能取舍

6.1 库存储区和主程序V区重叠的教训

这个坑我印象特别深。第一次做库时,我没仔细看库存储区分配,软件默认给了VB1000,我主程序配方区正好也从VB1000开始。编完运行,发现配方数据时不时被改掉几个字节,查了好久,最后在“库存储区”对话框里看到那个地址才反应过来——我的配方区和库的背景存储区撞车了。

从那以后我养成一个习惯:任何项目用库之前,先打开“库存储区”配置,把起始地址手动改到主程序绝对不用的空余区段,并且在程序注释里写明“VB2000~VB2040为库存储区,勿用”。这个习惯让我后来做其他项目少走了很多弯路。

另外还要注意,库里如果包含多个子程序,库存储区也会相应增大;如果你在多个地方调用同一个库指令,存储区按实例占用,不是按库文件数占用。所以库存储区宁可多留一点,也不要卡着边界分配。

6.2 封装库时全局符号的麻烦

子程序要打包成库,最好保持“自包含”,也就是所有输入输出都通过形参传递,内部不要引用全局数值,更不要用主程序里定义的符号名。我之前写过一个子程序,内部直接用了VB100这个全局地址,结果换一个项目调用这个库时,VB100在那边恰好是工艺参数,两者冲突,又是一轮排查。

如果子程序里确实需要一些固定的表或常量,建议把表也通过形参传入,或者做成独立的库存储区配置,不要隐性依赖某个全局V区。因为库一旦发布出去,使用者根本不知道库内部悄悄用了VB100,这种“看不见的依赖”最伤人。

封装时还要注意,库名称和版本号要写清楚。我习惯在库名里带版本,比如“ArrayLib V1.0”,一旦修改了库逻辑,升一版,调用处重新关联,也方便追溯。

6.3 批量循环调用对扫描周期的影响

有人一旦用了库,图省事就在主程序里放一个FOR循环,循环200次调用Ary_WrB,结果发现这个网络执行时间比以前直接写地址长了不少。这是正常的,库调用本身有参数压栈、跳转、返回这些开销,循环次数多时,这个开销会被放大。

我实测下来,单次Ary_RdB大概就多出几微秒到十几微秒,普通工艺程序几百次调用根本无感。但如果你是在高速计数中断或者通信中断里做几千次数组操作,那就要小心了,最好把批量处理封装成一个子程序,比如Ary_ZeroRange,直接把“基址+长度”传进去,库内部循环完成,而不是在主程序里反复调库指令。

批量字节清零这类操作,我通常会另外写一个专门的子程序:

ptr = pBase; for i := 0 to nCount-1 do begin *ptr := 0; ptr := ptr + 1; end;

一次调用完成整个区域的操作,扫描周期明显比反复调用单字节库稳定。

6.4 版本差异:指令操作数顺序要认真核对

最后提醒一下指令版本的坑。S7-200 SMART的STL助记符和S7-300的很多指令格式不一样,尤其移位、循环这类指令,操作数顺序在不同系列里可能相反。我刚开始写位数组库时,把S7-300的移位习惯带过来了,编译直接报错,后来查了Micro/WIN SMART自带的帮助才改对。

所以你在参考任何文章代码时,不要指望复制粘贴就能编译过,一定要用软件里的指令帮助对照一遍,确认操作数顺序和数据类型。这也是所有“抄库代码”的人最需要注意的地方。

文中的STL片段,我尽量用了通用风格,但具体到你手的软件版本,还是要以编辑器里的实际提示为准。

写在后面的话

我现在做S7-200 SMART项目,凡是涉及连续存储区批量读写的需求,第一反应都是“要不要做个寻址库”。字节数组、位数组、字数组这些说到底都是同一套指针思路,只要把基址和偏移搞清楚,V区就和高级语言里的数组没什么本质区别。

最后再分享一个工作习惯:我做完这套库以后,要求调用的地方把数组基址都写成带注释的符号,类似“配方区起始地址”。因为库参数框里只显示VB100这种地址,不写注释的话,过两周回看程序,根本看不清VB100到底代表什么。给地址一个清晰的名字,比什么都重要。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询