资讯动态

三菱ST数组越界陷阱:FOR循环上限写16为何丢17号

发布时间:2026/9/29 11:03:26 来源:尧图企业网站定制
1. 从丢17号说起一个看似低级的数组越界问题搞三菱STStructured Text的朋友大概率都遇到过这种让人抓狂的场景明明数组声明了ARRAY[0..16] OF INTFOR循环也老老实实写了FOR i : 0 TO 16 DO结果跑下来第17号元素也就是索引16那个死活读不到或者写进去的值莫名其妙被覆盖。更诡异的是有时候编译不报错运行也不报错就是数据对不上。这个问题的核心其实就藏在标题里那句FOR上限写16为何丢17号——数组下标从0开始ARRAY[0..16]一共17个元素但很多人脑子里默认16就是第16个于是循环上限写成16恰好漏掉了最后一个。听起来像是新手才会犯的错但我在实际项目里见过太多干了五六年的工程师栽在这上面尤其是在移植代码、改需求、加元素的时候。这篇内容适合所有用三菱GX Works3写ST程序的人不管你是刚接触ST的电气工程师还是从梯形图转过来的老手。我会把数组边界、FOR循环的上下限、LOWER_BOUND/UPPER_BOUND这些函数的用法以及实际调试中怎么快速定位这类问题全部拆开讲清楚。看完你至少能少熬两个通宵。2. 数组下标与FOR循环的错位陷阱2.1 为什么ARRAY[0..16]有17个元素先把这个最基础但最容易被忽略的点说透。三菱ST里声明一个数组arrSample : ARRAY[0..16] OF INT;这个声明表示下标从0开始到16结束包含两端。所以元素个数是16 - 0 1 17个。下标依次是0、1、2、…、15、16。很多人脑子里的映射是这样的人的直觉实际下标是第几个元素10第1个21第2个………1615第16个没有第17个的概念16第17个问题就出在最后一行。当你写FOR i : 0 TO 16 DO的时候循环体执行了17次i从0到16这是对的。但如果你写的是FOR i : 0 TO 15 DO那就只执行了16次索引16那个元素永远碰不到。而标题里说的FOR上限写16如果数组是ARRAY[1..16]那FOR i : 1 TO 16 DO刚好16个元素没问题。但如果数组是ARRAY[0..16]你写FOR i : 0 TO 16 DO也是17次也没问题。真正丢17号的情况往往是数组声明和循环上限的基准不一致——比如数组是ARRAY[0..16]但你按ARRAY[1..16]的习惯写了FOR i : 1 TO 16 DO那索引0就被跳过了同时如果你以为一共16个元素实际上第17个索引16反而被循环到了但索引0丢了。反过来数组是ARRAY[1..16]你写FOR i : 0 TO 16 DO那索引0是越界的索引16也是越界的如果上限是16的话ARRAY[1..16]的上限就是16索引16合法索引0非法。我把常见的几种组合列出来你对照自己的代码看一眼数组声明正确循环常见错误写法后果ARRAY[0..16]FOR i : 0 TO 16FOR i : 1 TO 16丢索引0共16次ARRAY[0..16]FOR i : 0 TO 16FOR i : 0 TO 15丢索引16共16次ARRAY[1..16]FOR i : 1 TO 16FOR i : 0 TO 16索引0越界可能报错或读到脏数据ARRAY[1..16]FOR i : 1 TO 16FOR i : 1 TO 15丢索引16共15次注意三菱GX Works3对数组越界的检查在不同固件版本和编译选项下表现不一样。有的版本直接编译报错有的版本运行时不报错但读到的是相邻变量的值这种静默越界最危险。2.2 FOR循环上限到底该写多少这里给一个不会出错的判断方法先看数组声明的两个端点循环的起点等于左端点终点等于右端点一个都不要改。// 数组声明 arrData : ARRAY[0..16] OF INT; // 循环就写 FOR i : 0 TO 16 DO // 处理 arrData[i] END_FOR;如果你不确定数组的边界或者数组边界以后可能会改那就别硬编码数字用LOWER_BOUND和UPPER_BOUNDFOR i : LOWER_BOUND(arrData, 1) TO UPPER_BOUND(arrData, 1) DO // 处理 arrData[i] END_FOR;这两个函数是三菱ST内置的LOWER_BOUND(数组名, 维度)返回该维度的最小下标UPPER_BOUND(数组名, 维度)返回最大下标。维度对于一维数组就是1。这样写的好处是以后你把数组改成ARRAY[0..31]循环代码一行都不用动。我个人的习惯是只要数组不是临时用一下就扔的一律用LOWER_BOUND/UPPER_BOUND。多打几个字省下的是半夜被叫去现场改程序的时间。2.3 一个真实的丢17号排查过程去年帮一个做包装线的朋友看程序他的情况是一个ARRAY[0..16] OF REAL存16个工位的温度值实际有17个工位编号0到16HMI上第17个工位编号16的温度一直显示0。他检查了传感器、模拟量模块、接线折腾了一整天。我让他把循环那段发过来代码是这样的FOR i : 1 TO 16 DO arrTemp[i] : INT_TO_REAL(analogIn[i]); END_FOR;问题一目了然数组是[0..16]循环从1开始索引0没赋值循环到16结束索引16赋了值但HMI读的是arrTemp[16]按理说应该有值。再一看HMI那边他读的是arrTemp[17]——因为他以为工位编号是1到17数组下标也是1到17。实际上数组下标是0到16工位16对应的是arrTemp[16]他读arrTemp[17]已经越界了读到的自然是0或者随机值。这个案例里丢17号其实是两个错误叠加循环起点错了导致索引0丢数据HMI读取索引错了导致读不到第17个工位。所以遇到这类问题不要只盯着FOR循环要把数组的写入端和读取端一起看。3. LOWER_BOUND与UPPER_BOUND让边界自己说话3.1 这两个函数到底返回什么LOWER_BOUND和UPPER_BOUND在三菱ST里的行为和IEC 61131-3标准一致。对于arrA : ARRAY[0..16] OF INT; arrB : ARRAY[1..16] OF INT; arrC : ARRAY[-5..5] OF INT; arrD : ARRAY[0..3, 0..7] OF INT; // 二维数组返回值如下调用返回值说明LOWER_BOUND(arrA, 1)0一维最小下标0UPPER_BOUND(arrA, 1)16一维最大下标16LOWER_BOUND(arrB, 1)1一维最小下标1UPPER_BOUND(arrB, 1)16一维最大下标16LOWER_BOUND(arrC, 1)-5支持负下标UPPER_BOUND(arrC, 1)5支持负下标LOWER_BOUND(arrD, 1)0第一维最小下标UPPER_BOUND(arrD, 1)3第一维最大下标LOWER_BOUND(arrD, 2)0第二维最小下标UPPER_BOUND(arrD, 2)7第二维最大下标注意第二个参数是维度从1开始计数。一维数组写1二维数组第一维写1、第二维写2以此类推。3.2 用边界函数改写循环的实战写法一维数组遍历FOR i : LOWER_BOUND(arrData, 1) TO UPPER_BOUND(arrData, 1) DO arrData[i] : 0; END_FOR;二维数组遍历FOR i : LOWER_BOUND(arrMatrix, 1) TO UPPER_BOUND(arrMatrix, 1) DO FOR j : LOWER_BOUND(arrMatrix, 2) TO UPPER_BOUND(arrMatrix, 2) DO arrMatrix[i, j] : 0; END_FOR; END_FOR;这种写法有一个额外的好处当你把数组作为参数传给函数或功能块时边界函数依然能正确返回实际数组的边界。三菱ST支持把数组作为VAR_IN_OUT传递在功能块内部用LOWER_BOUND/UPPER_BOUND就能拿到调用者传入数组的真实边界不用额外传一个长度参数。FUNCTION_BLOCK FB_ArraySum VAR_IN_OUT arrIn : ARRAY[*] OF INT; // 注意这里用*表示边界待定 END_VAR VAR_OUTPUT sum : INT; END_VAR VAR i : INT; END_VAR sum : 0; FOR i : LOWER_BOUND(arrIn, 1) TO UPPER_BOUND(arrIn, 1) DO sum : sum arrIn[i]; END_FOR; END_FUNCTION_BLOCK提示ARRAY[*]这种声明方式在三菱GX Works3里是否支持取决于具体型号和固件版本。如果不支持就老老实实传一个长度参数或者把数组声明成足够大的固定边界。3.3 边界函数也不是万能的有两个坑要注意第一LOWER_BOUND和UPPER_BOUND的返回值是INT类型如果你的数组下标范围超过INT的范围-32768到32767理论上会有问题但实际项目中数组很少超过这个范围所以基本不用操心。第二这两个函数在循环条件里每次迭代都会调用。对于大多数PLC的扫描周期来说这点开销可以忽略但如果你在一个1ms周期的高速任务里跑一个几千次的循环可以考虑把边界值先存到局部变量里lower : LOWER_BOUND(arrData, 1); upper : UPPER_BOUND(arrData, 1); FOR i : lower TO upper DO // ... END_FOR;这样可读性也更好调试的时候一眼就能看到边界值是多少。4. 越界之后发生了什么静默错误比报错更可怕4.1 三菱ST的越界行为分几种情况数组越界在三菱ST里不是一个统一行为它取决于几个因素编译器的检查级别、运行时环境、变量在内存中的布局。我实测和收集到的情况大致分三类第一类编译期报错。如果你写的是常量下标越界比如arrData[20]而数组上限是16GX Works3在编译时就会报错提示数组下标超出范围。这种最好办改掉就行。第二类运行时报错。如果下标是变量编译器无法在编译期确定就会在运行时检查。越界时PLC会报一个运算错误CPU可能进入STOP状态。这种也还好至少你知道出问题了。第三类静默越界。这是最坑的。某些情况下越界读写不会触发任何错误而是直接访问了相邻变量的内存。你读到的可能是另一个变量的值写进去的可能会覆盖另一个变量的值。程序逻辑看起来完全正常但数据就是不对。我遇到过最离谱的一次一个ARRAY[0..15] OF INT的数组循环写成了FOR i : 0 TO 20 DO多出来的5次写操作覆盖了后面5个BOOL变量导致几个气缸的使能信号莫名其妙被置位。排查了半天才发现是数组越界。4.2 怎么判断是不是越界导致的当你遇到数据莫名其妙不对但程序不报错的情况按这个顺序排查检查所有数组的声明边界把ARRAY[a..b]的a和b记下来。检查所有访问该数组的下标包括FOR循环的起止值、直接写的常量下标、通过变量计算出来的下标。检查数组的写入端和读取端是否一致写入用的是哪个下标范围读取用的是哪个下标范围。在循环里加临时变量记录i的值或者用在线监控看循环执行了多少次。如果数组是在功能块之间传递的还要检查调用者和被调用者对数组边界的理解是否一致。我见过一个案例主程序里数组是ARRAY[0..16]传给功能块后功能块内部按ARRAY[1..16]处理结果索引0的数据在功能块里被当成越界跳过了。4.3 一个实用的防御性写法对于关键数组我习惯在循环开始前加一个边界断言IF LOWER_BOUND(arrData, 1) 0 OR UPPER_BOUND(arrData, 1) 16 THEN // 边界不符合预期置错误标志 bArrayBoundError : TRUE; END_IF;这样如果以后有人改了数组声明但忘了改循环程序会给出一个明确的错误标志而不是静默地算错。当然这个断言本身也要跟着数组声明一起改但至少它把隐式错误变成了显式错误。5. 从FOR循环到循环单链表边界思维的其他应用5.1 循环单链表的边界和数组异曲同工热词里出现了循环单链表这其实和数组越界是同一类问题的不同表现形式。循环单链表的特点是尾节点的next指针指回头节点遍历的时候如果没有正确的终止条件就会无限循环。在ST里实现循环单链表遍历终止条件不能是next为空而应该是回到起点pNode : pHead; IF pNode 0 THEN REPEAT // 处理 pNode 指向的节点 pNode : pNode^.next; UNTIL pNode pHead END_REPEAT; END_IF;这里的pNode pHead就是边界条件。如果写成UNTIL pNode 0而链表里没有空指针那就死循环了。这和数组循环上限写错导致丢数据本质都是对边界在哪里的判断错误。5.2 其他语言里的循环边界问题热词里还有shell脚本for循环、python循环、js事件循环、C语言分支与循环语句等说明循环边界问题是跨语言的通病。举几个例子Shellfor i in {0..16}在bash里是0到16共17次但for i in {1..16}是1到16共16次。如果你从C语言转过来习惯了下标从0开始很容易在这里搞混。Pythonfor i in range(17)是0到16共17次for i in range(1, 17)是1到16共16次。range的右端点是开区间这个和ST的FOR i : 0 TO 16闭区间不一样。C语言for (i 0; i 16; i)是17次for (i 0; i 16; i)是16次。和的区别就是差一次。这些差异在单独写一种语言的时候不会出问题但在多语言混合的项目里或者从一种语言移植到另一种语言的时候就是bug的高发区。我的经验是每次移植循环代码先把原语言的循环次数算清楚再在新语言里用等价的方式写出来不要凭感觉改。5.3 循环不变式证明你的循环是对的热词里出现了CLRS选择排序循环不变量证明这是一个很好的思路。循环不变式Loop Invariant是指在循环的每次迭代开始前和结束后都成立的一个条件。对于数组遍历循环不变式可以写成在处理第i个元素之前前i个元素已经被正确处理。用这个思路检查你的循环初始化循环开始前不变式成立吗i等于起始下标时前0个元素已处理是平凡成立的保持每次迭代后不变式还成立吗处理完arr[i]后前i1个元素已处理终止循环结束后不变式给出了什么结论i等于终止下标1时所有元素已处理如果终止时不变式不能覆盖所有元素那就说明循环边界写错了。这个方法听起来有点学院派但在排查复杂循环逻辑的时候非常好用。6. 实操建议与常见问题速查6.1 写循环时的几条硬规矩根据我这些年的踩坑经验总结几条写ST循环的硬规矩数组声明和循环边界必须来自同一个真相源。要么都用常量要么都用LOWER_BOUND/UPPER_BOUND不要一个用常量一个用函数。循环变量用INT或DINT不要用WORD或UINT。虽然后者在某些情况下也能用但一旦涉及到负下标或者边界比较无符号类型会出幺蛾子。不要在循环体内修改循环变量的值。ST标准不保证这种行为的结果三菱的实现可能和其他品牌不一样。嵌套循环的内层变量不要和外层重名。这个不用多解释重名了编译器可能不报错但逻辑一定错。循环次数超过1000次的考虑用WHILE替代FOR或者在循环里加看门狗复位避免扫描周期超时。6.2 常见问题速查表现象可能原因排查方法最后一个元素读不到循环上限比数组上限小1检查UPPER_BOUND和循环终值第一个元素读不到循环起点比数组下限大1检查LOWER_BOUND和循环初值程序不报错但数据错静默越界覆盖了相邻变量在线监控看越界下标对应的内存编译报错下标超出范围常量下标越界直接改下标运行时报运算错误变量下标越界加边界检查或改用边界函数循环次数不对闭区间/开区间理解错误算清楚上限-下限16.3 一个可以直接抄的模板最后给一个我常用的数组遍历模板适用于大多数场景// 数组声明 arrData : ARRAY[0..16] OF REAL; // 遍历模板 FOR i : LOWER_BOUND(arrData, 1) TO UPPER_BOUND(arrData, 1) DO // 在这里处理 arrData[i] // 例如arrData[i] : 0.0; END_FOR; // 如果需要提前退出 FOR i : LOWER_BOUND(arrData, 1) TO UPPER_BOUND(arrData, 1) DO IF bAbort THEN EXIT; // 三菱ST支持EXIT跳出循环 END_IF; // 处理 arrData[i] END_FOR;三菱ST支持EXIT和CONTINUEEXIT跳出整个循环CONTINUE跳过本次迭代进入下一次。这两个关键字在需要提前结束循环的时候很有用但不要滥用否则循环逻辑会变得很难读。我在实际项目里还遇到过一个情况数组是在HMI上通过配方功能写入的HMI那边按1到17编号PLC这边按0到16编号中间没有做转换。结果操作工在HMI上选第17号配方PLC实际写入的是索引17越界了。这种跨系统的下标基准不一致比单纯的FOR循环写错更隐蔽。解决办法是在HMI和PLC之间加一层转换或者统一约定都用0基编号。我倾向于后者因为0基编号在代码里更自然但要在HMI上给操作工显示工位0确实有点怪所以实际项目中往往是HMI显示1基、PLC内部用0基转换逻辑放在通信层统一处理。还有一点关于UPPER_BOUND的返回值类型它返回的是INT但如果你把它直接用在FOR的终值表达式里三菱编译器有时会给出一个类型不匹配的警告。稳妥的做法是先赋值给一个INT变量再用nUpper : UPPER_BOUND(arrData, 1); FOR i : 0 TO nUpper DO // ... END_FOR;这样既避免了类型警告调试的时候也能在监控表里直接看到nUpper的值比在循环条件里嵌函数调用要直观得多。

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

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

免费获取报价 →
↑