资讯动态

S7-200 SMART指针寻址库:让存储区像数组一样用

发布时间:2026/10/18 8:25:24 来源:尧图企业网站定制
前阵子做一条分拣线的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条配方起始地址是VB1003*12VB136。改配方长度所有地址全部重算还容易算错查错查到怀疑人生。后来我意识到这不是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÷82也就是基址2的那个字节再算出它是该字节的第几位19 mod 83然后用字节目录的方式把这个字节读出来在内存里做位运算再写回去。这就是所谓“读-改-写”。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数组首地址调用时传VBxxxxLD4nIndexIN, INT数组下标从0开始LD6dataOUT, BYTE读到的字节数据S7-200 SMART的变量表里DWORD占4字节INT占2字节OUT类型也占用L区。这里LD0到LD5是输入参数LD6开始是输出参数。这些偏移不用手工算软件会自己排但是在STL里引用时建议直接用符号名可读性远远强过记LD几。还需要两个临时变量一个DWORD用来放指针一个DINT用来放32位索引。临时变量放在变量表的TEMP段比如LD10是#ptrLD14是#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里用了移位和与运算而不是除法指令因为移位更快而且不用处理商和余数两个输出。举个例子nIndex19byteIndex2bitIndex3也就是说第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位bitIndex0时掩码是0x01bitIndex3时掩码是0x08bitIndex7时掩码是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到底代表什么。给地址一个清晰的名字比什么都重要。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑