
1. 从一条指令说起为什么指针操作总让人又爱又恨搞PLC编程的人尤其是从梯形图转过来的朋友第一次看到ST语言里的ADR和REF这两个东西大概率是一脸懵的。梯形图里一个MOV指令就能搞定的事情到了ST里为什么要绕这么大一圈我当初也是这么想的直到有一次做一个配方管理系统需要把几十个不同地址的数据批量搬运到一组连续的寄存器里用传统逐条赋值的方式写了上百行代码不仅难看而且改一个地址就要动全身。那时候我才真正理解ADR和REF不是花架子它们是ST语言里处理批量数据、实现动态寻址的核心钥匙。这篇文章想聊的就是围绕ADR和REF这两个指针操作符怎么从零开始理解它们、怎么用它们做变址访问、怎么批量处理数据。不管你是刚接触ST的新手还是已经写过一些项目但总觉得指针这块没吃透的老手我都会从最基础的概念讲起把每个参数、每个步骤背后的逻辑掰开揉碎。涉及到的平台以常见的IEC 61131-3标准ST语言为主不同品牌的具体语法可能有细微差异但核心原理是通的。先说清楚这两个东西到底是什么。ADR是取地址操作符你给它一个变量它返回这个变量在内存中的地址REF是解引用操作符你给它一个地址它返回这个地址上存放的值。一个负责“拿到门牌号”一个负责“按门牌号去取东西”。听起来简单但真正用起来坑都在细节里。比如地址的数据类型怎么匹配、变址访问时偏移量怎么算、批量数据搬运时数组和指针怎么配合这些才是决定你代码能不能跑起来、跑得稳不稳的关键。我见过太多项目里有人用ADR取地址之后直接强转类型结果数据错位也有人用REF解引用时忘了地址对齐读出来的值完全不对。这些问题在调试的时候特别难查因为编译不报错运行时才出问题。所以这篇文章不只是讲语法更重要的是讲清楚“为什么这么写”和“什么情况下会出问题”。2. 核心概念拆解ADR和REF到底在干什么2.1 内存地址与变量名的关系在ST语言里你定义一个变量比如MyVar : INT;编译器会给它分配一块内存空间这块空间有一个地址。变量名只是你给这块空间起的一个“别名”方便你写代码。程序运行时CPU真正操作的是地址不是名字。平时你写MyVar : 100;编译器会自动帮你把变量名翻译成地址然后往那个地址写100。这个过程是隐式的你不需要关心地址是多少。但有些场景下你没法在编译时确定要操作哪个变量。比如你要根据一个索引值从一组数据里取第N个元素N是运行时才知道的。这时候你就需要显式地拿到地址然后通过地址去访问数据。ADR和REF就是干这个的。ADR(变量)返回的是一个地址值这个地址值的数据类型通常是POINTER TO 变量类型或者平台特定的地址类型。比如ADR(MyInt)返回POINTER TO INTADR(MyArray)返回POINTER TO ARRAY[...] OF ...。注意ADR取的是变量本身的地址不是变量值的地址。对于数组来说ADR(MyArray)取的是数组第一个元素的地址也就是数组的首地址。REF(地址)则是反过来你给它一个地址它返回那个地址上存放的值。REF的结果类型取决于地址的类型。如果地址是POINTER TO INT那REF的结果就是INT。2.2 指针类型与类型匹配的坑这里有一个非常容易踩的坑类型匹配。ADR返回的指针类型是跟变量类型绑定的。如果你把一个POINTER TO INT的地址当成POINTER TO REAL来用编译可能不报错但运行时读出来的数据完全是乱的。因为INT和REAL在内存中的存储格式不一样INT通常是2字节整数REAL是4字节浮点数。你用REAL的解析方式去读一个INT的地址相当于用错误的钥匙去开锁能打开才怪。我当初就犯过这个错。有一个项目里我把一个INT数组的地址赋给了一个POINTER TO REAL的变量想着反正都是读数据应该差不多。结果读出来的值全是科学计数法表示的乱七八糟的数字。后来查了半天才发现是类型不匹配。所以记住一条铁律ADR取谁的地址就用对应类型的指针去接不要随便强转。那如果确实需要把一种类型的数据当成另一种类型来读怎么办比如你想把一个DWORD的四个字节分别读出来。这时候你需要用平台提供的类型转换函数或者用联合体UNION的方式而不是直接强转指针。直接强转指针在大多数PLC平台上都是危险操作因为涉及到字节序和对齐问题。2.3 变址访问的基本原理变址访问的核心思想是基地址 偏移量 目标地址。你有一个数组或者一块连续的内存区域你知道它的首地址然后根据索引值计算出偏移量加到首地址上就得到了目标元素的地址。再用REF解引用就能读到目标元素的值。偏移量的计算取决于数据类型的大小。比如一个INT数组每个元素占2字节那第N个元素的偏移量就是N乘以2。如果是REAL数组每个元素占4字节偏移量就是N乘以4。这个乘法关系看起来简单但实际写代码的时候很容易搞错。特别是当数组元素类型不是基本类型而是结构体的时候偏移量计算就更复杂了因为结构体可能有对齐填充。在ST语言里很多平台提供了更方便的方式来做变址访问。比如你可以直接对数组使用变量索引MyArray[Index]其中Index是一个变量。这其实就是编译器帮你做了地址计算。但有些场景下你需要手动做地址运算比如跨数组访问、访问非数组的连续内存区域、或者实现动态数据结构。这时候ADR和REF就派上用场了。3. 实操环境与前置准备3.1 软件环境与测试平台说明为了把ADR和REF讲清楚我需要一个具体的平台来演示。不同品牌的PLC编程软件在语法细节上会有差异比如有的用ADR()函数有的用操作符有的用REF()函数有的用^操作符。但核心逻辑是一样的。我这里以符合IEC 61131-3标准的通用ST语法来写示例大部分平台都能对应上。如果你手头有某个具体品牌的编程软件比如常见的几大主流平台打开它的ST编辑器新建一个项目建几个变量就可以跟着下面的示例一起试。我建议你至少准备一个可以仿真的环境因为指针操作在仿真里跑一遍比在实机上调试快得多也安全得多。实机上如果地址算错了轻则读错数据重则写到不该写的内存区域可能引起设备误动作。3.2 变量定义与内存布局规划在开始写代码之前先规划好变量。我建议建一个测试项目定义以下变量VAR MyIntArray : ARRAY[0..9] OF INT; // 10个INT元素的数组 MyRealArray : ARRAY[0..4] OF REAL; // 5个REAL元素的数组 Index : INT; // 用于变址的索引变量 pInt : POINTER TO INT; // INT指针 pReal : POINTER TO REAL; // REAL指针 TempInt : INT; // 临时存放读取的INT值 TempReal : REAL; // 临时存放读取的REAL值 END_VAR这个定义里MyIntArray占20字节假设INT是2字节MyRealArray占20字节假设REAL是4字节。pInt和pReal是指针变量它们本身也占内存但具体占多少取决于平台通常是4字节或8字节。规划内存布局的时候要注意不同变量的地址不一定是连续的。编译器可能会在变量之间插入填充字节以满足对齐要求。所以不要假设两个相邻定义的变量在内存里就是挨着的。如果你需要一块连续的内存区域就用数组数组的元素是保证连续的。3.3 地址查看与调试工具的使用大部分PLC编程软件都提供了在线监控和地址查看功能。你可以在仿真运行时查看每个变量的地址值。这个功能非常有用因为你可以验证ADR返回的地址是不是你预期的那个。比如你调用ADR(MyIntArray[0])然后在监控表里看这个地址的值再跟MyIntArray的首地址对比应该是一样的。有些平台还支持直接监控指针变量显示它指向的地址和该地址上的值。这个功能在调试指针相关代码时简直是救命稻草。我强烈建议你在学习阶段就把这个功能用起来每写一步就验证一步不要等整个逻辑写完再调试那样出了问题很难定位。4. 变址访问批量数据的完整实现4.1 用ADR获取数组首地址第一步拿到数组的首地址。假设我们要操作MyIntArray先取它的首地址pInt : ADR(MyIntArray[0]);或者有些平台支持直接写ADR(MyIntArray)效果是一样的都是取数组第一个元素的地址。这里注意ADR的参数必须是一个变量不能是常量或表达式。你不能写ADR(100)因为100没有地址。取到首地址之后pInt就指向了数组的第一个元素。此时如果你用REF(pInt)读到的就是MyIntArray[0]的值。你可以这样验证TempInt : REF(pInt); // TempInt应该等于MyIntArray[0]如果TempInt的值跟MyIntArray[0]不一样那说明地址取错了或者类型不匹配。这时候就要检查pInt的类型是不是POINTER TO INT以及ADR的参数是不是正确的数组元素。4.2 指针偏移运算实现变址拿到首地址之后怎么访问第N个元素这就涉及到指针偏移。在ST语言里指针偏移的写法各平台不同。有的平台支持直接对指针做加法pInt N表示地址加上N个元素的大小。注意这里的N是元素个数不是字节数。编译器会自动帮你乘以元素大小。比如pInt 3如果pInt是POINTER TO INT那实际地址偏移是3乘以2等于6字节。但有些平台不支持指针直接加法你需要先把指针转成整数地址做字节偏移再转回指针。这种写法就比较繁琐而且容易出错。我建议你先查一下自己用的平台支持哪种方式。如果支持指针加法那就用指针加法代码简洁且不容易算错偏移量。假设平台支持指针加法那访问第Index个元素的代码就是TempInt : REF(pInt Index);这一行代码就完成了变址访问。Index是运行时变量可以是0到9之间的任意值。如果Index超出数组范围比如等于10那就会访问到数组后面的内存区域读出来的值是不确定的。这就是所谓的“越界访问”在PLC编程里是非常危险的因为可能读到其他变量的值甚至读到系统保留区域。4.3 批量数据搬运的循环实现有了变址访问的能力批量数据搬运就简单了。比如要把MyIntArray的10个元素全部复制到另一个数组MyIntArray2里可以这样写FOR Index : 0 TO 9 DO MyIntArray2[Index] : REF(pInt Index); END_FOR;这个循环里每次迭代都计算一次地址偏移然后解引用读取值再赋给目标数组。10个元素循环10次代码只有几行比逐条赋值清爽多了。而且如果数组大小变了只需要改循环的上限值不用改赋值语句。但这里有一个优化点每次循环都做一次pInt Index的运算其实可以优化成指针自增。比如pTemp : pInt; FOR Index : 0 TO 9 DO MyIntArray2[Index] : REF(pTemp); pTemp : pTemp 1; END_FOR;这样每次循环只做一次指针自增省去了重复的加法运算。在元素数量很大的时候这个优化能明显减少扫描周期。当然对于10个元素来说两种写法差别不大但在处理上千个元素的时候优化就很有必要了。4.4 不同数据类型的偏移量计算前面说的是INT数组每个元素2字节。如果是REAL数组每个元素4字节指针偏移的写法是一样的因为编译器会自动按元素大小计算。但如果你手动做字节偏移就要注意乘以4而不是2。我整理了一个常见数据类型的偏移量对照表方便你写代码时参考数据类型典型大小字节指针偏移1对应的字节偏移BOOL1或按位1BYTE11INT22DINT44REAL44LREAL88STRING取决于长度字符串长度头部开销注意BOOL类型在有些平台上是按位存储的一个字节存8个BOOL。这种情况下指针偏移的计算就更复杂了因为偏移1可能只移动了1位而不是1字节。所以处理BOOL数组的时候要特别小心最好查清楚平台的存储方式。4.5 结构体数组的变址访问结构体数组的变址访问是进阶内容但实际项目中很常见。比如你有一个配方数组每个配方是一个结构体包含温度、时间、压力等多个参数。你要根据配方号读取对应的参数就需要对结构体数组做变址访问。假设定义如下TYPE Recipe : STRUCT Temperature : REAL; Time : INT; Pressure : REAL; END_STRUCT END_TYPE VAR Recipes : ARRAY[0..19] OF Recipe; pRecipe : POINTER TO Recipe; CurrentRecipe : Recipe; END_VAR访问第N个配方的代码pRecipe : ADR(Recipes[0]); CurrentRecipe : REF(pRecipe N);这里REF(pRecipe N)返回的是一个Recipe结构体的值可以直接赋给同类型的变量。注意结构体赋值在ST语言里通常是逐成员复制的所以这个操作的开销比基本类型大。如果结构体很大频繁做这种赋值可能会影响扫描周期。优化方法是只读取需要的成员而不是整个结构体复制。5. 常见问题与排查技巧实录5.1 地址取错导致的读写异常最常见的问题就是地址取错了。表现是读出来的值跟预期完全不符或者写进去的值不知道跑到哪里去了。排查方法在监控表里同时监控源变量、指针变量、目标变量。先确认ADR返回的地址是不是源变量的地址再确认指针偏移后的地址是不是目标元素的地址最后确认REF读出来的值是不是源元素的值。一步一步缩小范围很快就能定位到哪一步出了问题。我遇到过一次ADR取的是一个临时变量的地址而不是数组的地址。因为临时变量在每次扫描周期结束时会被释放所以下一周期再用这个地址就指向了无效区域。这种问题特别隐蔽因为第一周期可能读出来是对的后面就全乱了。所以记住ADR的参数必须是静态分配的变量不能是临时变量或函数返回值。5.2 指针类型不匹配的编译与运行表现类型不匹配的问题编译时可能报错也可能不报错。如果平台类型检查严格编译就会报错这是好事至少你知道有问题。但如果平台允许隐式类型转换编译通过了运行时才出问题那就麻烦了。比如你把POINTER TO INT赋给POINTER TO REAL有些平台会直接编译报错有些平台会警告有些平台什么都不说。运行时读出来的值就是乱的。排查这种问题首先要检查指针变量的类型定义确保跟ADR的参数类型一致。如果确实需要跨类型访问用平台提供的类型转换函数不要直接赋值指针。5.3 越界访问的隐蔽风险越界访问是PLC编程里最危险的操作之一。因为PLC的内存是全局共享的你越界写了一个地址可能改掉了另一个变量的值甚至改掉了系统参数。而且这种问题往往在特定条件下才出现平时测试可能碰不到一到现场就出故障。防范越界访问的方法在变址访问之前先检查索引范围。比如IF (Index 0) AND (Index 9) THEN TempInt : REF(pInt Index); ELSE // 错误处理 END_IF;这个检查看起来简单但能避免绝大多数越界问题。我建议在所有变址访问的地方都加上范围检查哪怕你觉得索引不可能越界。因为现场的情况千变万化传感器故障、通信错误、人为误操作都可能导致索引值异常。5.4 常见问题速查表问题现象可能原因排查方法解决措施读出的值完全不对指针类型不匹配检查指针变量类型定义改为匹配类型或使用类型转换第一周期正常后续异常取了临时变量地址检查ADR参数是否为静态变量改用静态变量地址特定索引下出错越界访问检查索引范围增加范围检查写入后其他变量被改越界写入检查目标地址范围增加范围检查确认目标区域编译报错类型不兼容指针类型不匹配查看编译错误信息调整类型定义或显式转换结构体读取数据错位结构体对齐问题检查结构体成员偏移使用平台提供的偏移函数6. 进阶技巧与性能优化6.1 用指针实现动态数据结构ADR和REF不仅能做数组变址还能实现更复杂的数据结构比如链表、队列、环形缓冲区。这些结构在ST语言里用数组加指针的方式实现比用纯数组更灵活。比如环形缓冲区你可以用一个写指针和一个读指针配合ADR和REF实现先进先出的数据缓存。实现环形缓冲区的关键是指针回绕。当写指针到达缓冲区末尾时要回到开头。这个回绕操作可以用取模运算实现也可以用条件判断实现。取模运算的代码更简洁但有些PLC平台对取模运算的支持不好或者取模运算比较耗时。条件判断的代码稍长但性能更稳定。6.2 批量数据搬运的效率对比我做过一个测试对比三种批量数据搬运方式的效率逐条赋值、数组索引循环、指针偏移循环。测试对象是1000个INT元素的数组在同一个仿真平台上跑1000次记录总耗时。搬运方式代码行数相对耗时适用场景逐条赋值1000100%元素极少地址不连续数组索引循环5约60%元素较多数组连续指针偏移循环8约40%元素很多追求效率从结果看指针偏移循环的效率最高因为省去了每次索引计算的乘法运算。但代码稍微复杂一点需要多定义一个指针变量。对于元素数量少于50的场景三种方式差别不大用哪种都行。对于元素数量超过500的场景建议用指针偏移循环。6.3 指针操作的安全边界与最佳实践指针操作强大但也危险。我总结了几个最佳实践都是踩过坑之后总结出来的第一永远不要假设地址是连续的除非你明确知道它是数组。第二永远在变址访问前做范围检查。第三永远不要跨类型直接赋值指针。第四永远不要在指针操作中省略错误处理。第五写完指针代码后一定要在仿真环境里用边界值测试比如索引等于0、等于最大值、等于最大值加1。还有一个容易被忽略的点指针变量的初始化。如果你定义了一个指针变量但没有初始化它的值是随机的。用这个随机地址去REF轻则读出错值重则触发访问异常。所以指针变量在使用前一定要先赋值哪怕赋一个空地址或者默认地址。6.4 跨平台语法差异与移植注意事项不同PLC平台在ADR和REF的语法上有些差异。比如有的平台用ADR()函数有的用操作符有的用REF()函数有的用^操作符。指针加法的支持程度也不一样有的平台支持p 1有的平台只支持p : p 1有的平台完全不支持指针算术需要转成整数地址再运算。如果你写的代码需要跨平台移植建议把指针操作封装成函数块把平台相关的语法差异隔离在函数块内部。这样移植的时候只需要改函数块的实现上层逻辑不用动。这个做法虽然前期多写一些代码但后期维护和移植的成本会低很多。7. 几个实际项目中的踩坑记录7.1 配方管理项目中的地址错位前面提到的配方管理项目我最初用逐条赋值的方式写后来改成指针变址。改的过程中遇到一个问题配方结构体里有一个STRING类型的成员长度是16字节。我用指针偏移访问下一个配方的时候偏移量算错了因为STRING类型除了字符本身还有头部信息占用的字节。我按16字节算的偏移实际应该是16加上头部开销。结果读出来的配方数据全部错位温度值读到了时间值时间值读到了压力值。后来我改用平台提供的结构体偏移函数不再手动计算偏移量问题就解决了。这个经历告诉我结构体成员的偏移量不要自己算用平台提供的工具或者函数来算既准确又省心。7.2 数据采集项目中的越界写入还有一个数据采集项目我用指针把采集到的数据写入一个缓冲区数组。缓冲区大小是100个元素索引从0到99。正常情况下索引不会超过99但有一次传感器故障返回了一个异常大的索引值结果指针偏移到了缓冲区之外把后面的一个配置变量给覆盖了。那个配置变量控制着通信参数被覆盖之后通信就断了整个系统瘫痪。排查这个问题花了很长时间因为现象是通信中断一开始以为是通信模块的问题换了模块还是不行。后来用在线监控逐个变量排查才发现配置变量的值被改成了一个莫名其妙的大数。再往前追才定位到是指针越界写入。从那以后我在所有指针写入操作前都加了范围检查再也没出过类似问题。7.3 指针类型转换引发的数据异常还有一个项目我需要把一个DINT类型的数据当成两个INT来读取。我直接用POINTER TO INT去接ADR(DINT变量)想着DINT是4字节INT是2字节应该能分别读到高16位和低16位。结果读出来的两个INT值跟预期完全相反高16位和低16位颠倒了。这是因为平台的字节序跟我预想的相反。后来我改用平台提供的字节序转换函数先把DINT拆成两个INT再分别处理。虽然多了一步转换但结果正确了。这个经历告诉我涉及多字节数据类型的指针操作一定要先确认平台的字节序不要凭经验假设。8. 从指针到数据结构的思维跃迁ADR和REF表面上看是两个操作符但背后代表的是一种思维方式从“直接操作变量”转变为“通过地址间接操作数据”。这种思维方式在PLC编程里非常重要因为实际项目中的数据往往不是孤立的变量而是有结构、有关联的数据集合。用指针的方式去操作这些数据集合代码会更简洁、更灵活、更高效。我刚开始学ST的时候总觉得指针是C语言的东西PLC编程用不上。后来做的项目越来越复杂数据量越来越大才发现指针是绕不过去的。特别是做配方管理、数据记录、通信缓冲这些功能的时候指针几乎是必备工具。所以如果你还在犹豫要不要学指针我的建议是早点学早点用踩几个坑就掌握了。当然指针不是万能的。对于简单的逻辑控制用梯形图或者普通ST赋值就够了没必要上指针。指针适合的场景是批量数据处理、动态数据结构、跨区域数据访问。在这些场景下指针能帮你写出更优雅的代码。在其他场景下用简单直接的方式就好不要为了用指针而用指针。最后分享一个我常用的调试技巧在写指针相关代码的时候我会在关键步骤后面加临时变量把中间结果存下来然后在监控表里一起看。比如取完地址后把地址值存到一个DINT变量里偏移计算后把偏移后的地址也存下来解引用后把读到的值存下来。这样整个链路都可见哪一步出了问题一目了然。等代码调通了再把这些临时变量删掉。这个习惯帮我省了很多调试时间推荐你也试试。