☰
三菱ST编程:FOR循环上限写16为何丢17号数据?数组越界排查与避坑指南
2026/9/29 15:07:32 网站建设 项目流程

1. 问题现场还原:一个让老手也翻车的FOR循环

先还原一下这个问题的典型现场。你手里有一台三菱FX5U或者Q系列PLC,用GX Works3写ST语言,定义了一个数组,比如MyArray : ARRAY[0..16] OF INT;,这个数组一共17个元素,下标从0到16。然后你写了一个FOR循环,想把数组里的数据挨个读出来或者写进去,循环上限你写了16,心里想的是“0到16,正好17个,没问题”。结果跑起来发现,第17号元素,也就是下标16的那个,数据死活不对,要么是0,要么是上一次的旧值,要么干脆报错。

你盯着屏幕看了半天,心里想:我上限写16,循环体里访问MyArray[i],i从0到15,那不是只访问了16个元素吗?下标16根本没进去啊。对,问题就出在这里。FOR循环的上限值,在ST语言里是“小于等于”还是“小于”,这个细节决定了你丢不丢数据。

这个坑我踩过不止一次,而且我发现很多做PLC编程的朋友,尤其是从梯形图转ST的,特别容易在这里翻车。因为梯形图里你用变址寄存器或者指针,逻辑是线性的,但ST语言的FOR循环,它的终止条件是“循环变量大于终止值时才退出”,也就是说,如果终止值是16,循环变量会一直执行到16,然后下一次判断17>16才退出。所以FOR i := 0 TO 16 DO实际上会执行i=0,1,2,...,16,一共17次。但如果你写的是FOR i := 0 TO 15 DO,那就只执行0到15,共16次,下标16就丢了。

那为什么标题里说“FOR上限写16为何丢17号”?这里有两种可能。第一种,你数组定义的是ARRAY[0..16],但你FOR写的是FOR i := 0 TO 15 DO,那确实丢了16号。第二种,你数组定义的是ARRAY[1..17],下标从1开始,你FOR写FOR i := 1 TO 16 DO,那17号就丢了。不管是哪种,核心都是FOR循环的终止值和数组下标范围没有对齐。

注意:三菱ST的FOR语法是FOR <循环变量> := <初值> TO <终值> BY <步长> DO ... END_FOR;,其中终值是包含在循环内的。这一点和C语言里for(i=0;i<16;i++)的写法完全不同,C语言里16是不包含的,但ST里16是包含的。

我见过一个项目,工程师用ST写了一个配方管理,数组存了17个配方参数,FOR循环上限写了16,结果第17个配方永远读不到,产线跑了半个月才发现,每次换型都少一个参数,导致产品批次不一致。这种问题在实验室里很难发现,因为你不一定会去检查最后一个元素,但到了现场就是批量事故。

所以这篇文章,我就围绕这个“FOR上限写16丢17号”的问题,把三菱ST数组越界的来龙去脉、底层逻辑、排查方法、避坑技巧全部讲清楚。不管你是刚接触ST的新手,还是用了几年GX Works3的老手,只要你在用数组和循环,这篇文章里的坑你大概率都会遇到。

2. 三菱ST数组与FOR循环的底层逻辑拆解

2.1 数组下标到底从0还是从1开始

三菱的ST语言,数组下标默认是从0开始的。你写ARRAY[0..16] OF INT,那就是17个元素,下标0到16。但三菱也支持自定义起始下标,比如ARRAY[1..17] OF INT,这也是17个元素,下标1到17。这两种写法在GX Works3里都合法,但带来的问题完全不同。

如果你用ARRAY[0..16],然后FOR循环写FOR i := 0 TO 16 DO,那正好17次,没问题。但如果你写FOR i := 0 TO 15 DO,那就只循环了16次,下标16没进去。反过来,如果你用ARRAY[1..17],FOR写FOR i := 1 TO 17 DO,也是17次,没问题。但如果你写FOR i := 1 TO 16 DO,那就丢了17号。

我个人的习惯是,统一用0起始下标,因为这样和大多数高级语言一致,查资料、移植代码都方便。但有些老工程师习惯1起始,觉得“第1个就是1号”,这也没错,关键是你要在FOR循环里把终值写对。

这里有一个很容易混淆的点:数组定义里的[0..16],这个16是最大下标,不是元素个数。元素个数是16-0+1=17。很多人看到16,脑子里想的是“16个元素”,然后FOR写TO 15,觉得15是最后一个,结果就丢了。这个思维惯性非常致命。

2.2 FOR循环的终止条件到底怎么判断

三菱ST的FOR循环,执行逻辑是这样的:

  1. 循环变量赋初值。
  2. 判断循环变量是否大于终值。如果大于,退出循环。
  3. 执行循环体。
  4. 循环变量增加步长(默认是1)。
  5. 回到第2步。

所以FOR i := 0 TO 16 DO的执行序列是:i=0(0不大于16,执行),i=1(执行),...,i=16(16不大于16,执行),i=17(17大于16,退出)。一共执行了17次,i从0到16。

如果你写FOR i := 0 TO 15 DO,执行序列是:i=0到i=15,共16次,i=16的时候判断16>15,退出,所以下标16没执行。

这个逻辑和C语言、Java、Python都不一样。C语言里for(i=0;i<=16;i++)才是17次,for(i=0;i<16;i++)是16次。Python里for i in range(0,16)是0到15,共16次,range(0,17)才是17次。三菱ST的FOR,终值是闭区间,包含终值本身。

提示:如果你从C语言或者Python转过来,一定要在脑子里把“小于”改成“小于等于”。ST的FOR终值是包含的,不是排除的。

我见过有人为了“保险”,FOR写TO 16,但数组是[0..15],结果循环到i=16的时候访问MyArray[16],直接越界。三菱的ST在编译时不一定报错,但运行时可能读到非法内存,或者触发PLC的异常。这种越界比丢数据更危险,因为它可能改写其他变量的值,导致莫名其妙的故障。

2.3 数组越界在三菱PLC里到底会发生什么

三菱的PLC,尤其是FX5U和Q系列,对数组越界的处理方式不太一样。在GX Works3里编译ST代码时,如果你用的是常量下标越界,比如MyArray[20]而数组只到16,编译器会直接报错,不让你下载。但如果你用的是变量下标,比如MyArray[i],编译器无法在编译期判断i的范围,所以不会报错,但运行时可能出问题。

运行时越界的行为,取决于PLC的型号和固件版本。有些情况下,越界读会返回0或者随机值,越界写会改写相邻变量的内存,导致其他数据被污染。最麻烦的是,这种污染不一定马上表现出来,可能过几个小时或者几天才出故障,排查起来非常困难。

我遇到过一个案例:一个工程师用ST写了一个数据采集程序,数组Data[0..15],FOR循环写TO 16,结果i=16的时候写入了Data[16],而这个地址恰好是另一个重要变量SetPoint的存储位置,导致设定值被覆盖,设备运行异常。他查了两天才发现是数组越界。

所以,数组越界在三菱ST里不是“可能没事”,而是“一定有事”,只是什么时候爆发的问题。

3. 实操排查:如何快速定位FOR循环丢数据的问题

3.1 用GX Works3的监视功能确认循环次数

当你怀疑FOR循环丢数据时,第一步是在GX Works3里监视循环变量和数组元素。具体操作:

  1. 在ST程序里,把FOR循环的循环变量i和数组MyArray添加到监视窗口。
  2. 在线运行,触发循环。
  3. 观察i的最终值。如果循环结束后i的值是16,说明循环执行到了16;如果是15,说明只到15。
  4. 同时观察MyArray[16]的值,看是否被更新。

这里有一个细节:三菱的FOR循环,循环变量在循环结束后会保留最后一次的值。如果循环正常结束,i的值应该是终值+1。比如FOR i := 0 TO 16 DO,结束后i=17。如果你看到i=16,说明循环在i=16的时候退出了,那可能是终值写成了15。

注意:有些PLC在FOR循环结束后会把循环变量复位,但三菱的ST通常保留最后值。你可以用这个特性来判断循环是否执行到了预期的次数。

我通常会在循环体里加一个计数器,比如Count := Count + 1;,循环结束后看Count的值。如果数组有17个元素,Count应该是17。如果是16,那就丢了。这个方法比看i的值更直观,因为Count直接告诉你执行了多少次。

3.2 用边界测试法验证数组范围

另一个实用的方法是边界测试。你可以在程序里临时加一段代码,手动访问数组的第一个和最后一个元素,看是否正常。

比如:

MyArray[0] := 100; MyArray[16] := 200;

如果编译报错,说明数组定义的范围不对。如果编译通过但运行异常,说明数组可能没有分配到足够的空间。

然后你再写一个FOR循环,把每个元素赋值为它的下标:

FOR i := 0 TO 16 DO MyArray[i] := i; END_FOR;

运行后检查MyArray[16]是否等于16。如果不是,说明循环没执行到16。

这个方法我称之为“下标回填法”,非常直观。你甚至可以把数组里的值读出来显示在HMI上,一眼就能看出哪个下标没被写入。

3.3 常见错误写法对照表

下面这张表列出了几种常见的FOR循环写法,以及它们对应的数组访问范围。你可以对照自己的代码,看看属于哪一种。

数组定义FOR写法实际访问下标是否丢数据风险
ARRAY[0..16]FOR i:=0 TO 160,1,...,16不丢正确
ARRAY[0..16]FOR i:=0 TO 150,1,...,15丢16号数据不完整
ARRAY[0..16]FOR i:=0 TO 170,1,...,17越界可能改写其他变量
ARRAY[1..17]FOR i:=1 TO 171,2,...,17不丢正确
ARRAY[1..17]FOR i:=1 TO 161,2,...,16丢17号数据不完整
ARRAY[1..17]FOR i:=0 TO 170,1,...,17越界下标0非法,可能异常

这张表建议你保存下来,写代码的时候对照一下。尤其是从0起始和从1起始混用的时候,特别容易出错。

4. 避坑指南:写三菱ST数组循环的几条铁律

4.1 铁律一:数组定义和FOR终值必须联动

我的习惯是,在定义数组的时候,顺便定义一个常量来表示数组长度,然后在FOR循环里用这个常量。

比如:

VAR_GLOBAL ARRAY_SIZE : INT := 17; MyArray : ARRAY[0..ARRAY_SIZE-1] OF INT; END_VAR

然后FOR循环写:

FOR i := 0 TO ARRAY_SIZE - 1 DO // 访问 MyArray[i] END_FOR;

这样,如果你以后要改数组大小,只需要改ARRAY_SIZE一个地方,FOR循环自动跟着变。三菱的ST支持常量表达式,ARRAY_SIZE - 1在编译时就能算出16,不会影响性能。

提示:三菱GX Works3里,数组定义的下标必须是常量,不能用变量。但你可以用常量名,比如ARRAY[0..MAX_INDEX],只要MAX_INDEX是常量就行。

这个方法我用了很多年,几乎没再出现过丢数据的问题。因为终值和数组长度绑定了,你改一个地方,两边都改。

4.2 铁律二:循环体内不要修改循环变量

三菱ST的FOR循环,循环变量是由系统自动递增的。如果你在循环体里手动修改了循环变量,比如i := i + 1;,那循环次数就乱了。有些PLC允许你修改,但行为不确定,可能跳过某些下标,也可能提前退出。

我见过有人为了“跳过某个元素”,在循环体里写IF i = 5 THEN i := 6; END_IF;,结果循环逻辑完全乱套。正确的做法是用CONTINUE或者IF判断来跳过,而不是修改循环变量。

FOR i := 0 TO 16 DO IF i = 5 THEN CONTINUE; // 跳过i=5 END_IF; // 正常处理 END_FOR;

三菱ST支持CONTINUE和EXIT,用这两个来控制流程,比修改循环变量安全得多。

4.3 铁律三:越界写比越界读更危险

越界读通常只是读到错误的值,但越界写会破坏其他变量的数据。所以,如果你不确定循环范围,宁可少循环一次,也不要多循环一次。

比如,你数组是[0..16],你不确定FOR该写15还是16,那就先写15,跑一下看最后一个元素有没有被处理。如果没有,再改成16。这样至少不会越界写。

当然,更好的方法是按照4.1节的建议,用常量绑定,从根本上避免这个问题。

4.4 铁律四:用断言或者范围检查兜底

三菱的ST没有像C语言那样的assert宏,但你可以自己写一个范围检查:

FOR i := 0 TO ARRAY_SIZE - 1 DO IF i < 0 OR i > ARRAY_SIZE - 1 THEN // 触发报警或者记录日志 EXIT; END_IF; // 正常处理 END_FOR;

虽然这个检查在正常情况下永远不会触发,但它是一个安全网。万一以后有人改了数组定义但忘了改FOR,这个检查能帮你提前发现问题。

我通常会在循环体开头加一个简单的范围判断,成本很低,但能避免大事故。

5. 更深一层:为什么三菱ST的FOR设计成闭区间

5.1 历史原因:从梯形图到ST的延续

三菱的PLC编程,最早是梯形图,后来加了ST。梯形图里的循环,通常是用跳转指令或者变址寄存器实现的,逻辑上是“执行到条件满足为止”。ST的FOR循环,设计成闭区间,可能是为了和梯形图的思维保持一致。

在梯形图里,如果你用FOR指令(比如FX系列的FOR和NEXT指令),它的循环次数是写在指令里的,比如FOR K16表示循环16次。这个16是次数,不是下标。但ST的FOR,终值是下标,不是次数。这两个概念容易混淆。

注意:FX系列的梯形图FOR指令,FOR K16是循环16次,不是循环到16。但ST的FOR i := 0 TO 16是循环17次。这两个“16”含义完全不同。

所以,如果你从梯形图转ST,一定要把“次数”和“终值”区分开。梯形图的FOR是次数,ST的FOR是终值。

5.2 和其他PLC品牌的对比

不同品牌的PLC,ST语言的FOR循环设计也不一样。比如西门子的SCL,FOR i := 0 TO 16 DO也是闭区间,包含16。欧姆龙的ST,也是闭区间。但有些品牌的FOR是开区间,比如某些日系PLC的FOR,终值不包含。

所以,如果你从其他品牌转到三菱,一定要查一下手册,确认FOR的终值是否包含。不要凭经验写代码,经验有时候是错的。

我个人的做法是,不管哪个品牌,第一次用的时候都写一个简单的测试程序,循环几次,看循环变量的最终值,确认清楚再写正式代码。

5.3 数组越界的编译期和运行期差异

三菱GX Works3在编译ST代码时,对数组越界的检查是有限的。常量下标越界会报错,但变量下标越界不会。这意味着,如果你用MyArray[i],编译器不会帮你检查i的范围,你需要自己保证。

有些高级语言,比如C#,运行时会检查数组边界,越界会抛异常。但三菱的ST,运行时不一定检查,越界可能静默通过,然后改写其他内存。这就是为什么数组越界在三菱PLC里特别危险。

我的建议是,永远不要依赖编译器的检查,永远自己保证下标在合法范围内。用常量绑定、范围检查、边界测试,三重保险。

6. 实战案例:一个配方管理程序的完整修复过程

6.1 问题描述

去年我帮一个朋友排查一个配方管理的问题。设备是FX5U,用ST写的配方程序,数组Recipe[0..16],存了17个参数。操作员在HMI上选择配方号,PLC根据配方号读取对应的参数。但操作员反映,每次选第17个配方,参数都不对,要么是0,要么是上一个配方的值。

6.2 排查过程

我先让他把程序在线监视,看FOR循环的循环变量。结果发现,循环结束后i的值是16,而不是17。这说明循环只执行到了15,下标16没进去。

再看代码,FOR写的是FOR i := 0 TO 15 DO。他解释说,他以为数组是16个元素,因为定义写的是[0..16],他脑子里想的是“0到16是16个”,其实0到16是17个。

这就是典型的“最大下标”和“元素个数”混淆。

6.3 修复方案

修复很简单,把FOR改成FOR i := 0 TO 16 DO。但为了防止以后再出问题,我帮他改成了常量绑定:

VAR_GLOBAL RECIPE_COUNT : INT := 17; Recipe : ARRAY[0..RECIPE_COUNT-1] OF REAL; END_VAR FOR i := 0 TO RECIPE_COUNT - 1 DO // 读取配方参数 END_FOR;

这样,以后如果要增加配方数量,只需要改RECIPE_COUNT,数组和FOR自动调整。

6.4 后续验证

改完之后,我们做了边界测试:手动写入Recipe[16],然后读取,确认数据正确。又跑了几个批次,确认第17个配方能正常读取。问题解决。

这个案例告诉我,数组越界和FOR循环丢数据,往往不是技术难题,而是思维惯性。你脑子里想的是“16个”,代码写的是“0到16”,实际是17个。这种错误,只有通过严格的常量绑定和边界测试才能避免。

7. 常见问题速查与排查技巧

7.1 FOR循环丢数据问题速查表

现象可能原因排查方法解决方案
最后一个元素没更新FOR终值小于最大下标监视循环变量最终值改终值为最大下标
第一个元素没更新FOR初值大于最小下标检查初值改初值为最小下标
中间某个元素没更新循环体内有CONTINUE或EXIT检查循环体逻辑调整跳过条件
数组越界报警FOR终值大于最大下标检查数组定义和FOR终值改终值为最大下标
数据被莫名改写越界写破坏了其他变量检查所有数组访问加范围检查

7.2 独家避坑技巧

技巧一:用“下标回填法”快速验证。在程序初始化时,把数组每个元素赋值为它的下标,然后运行FOR循环,看最后一个元素是否等于最大下标。这个方法能在几秒钟内确认循环范围是否正确。

技巧二:在HMI上显示数组长度和循环次数。把ARRAY_SIZE和循环计数器显示在HMI的调试画面上,操作员或者维护人员一眼就能看出是否匹配。

技巧三:用SIZEOF或者类似函数获取数组长度。三菱的ST没有直接的SIZEOF函数,但你可以用常量绑定来模拟。如果你用的是其他品牌PLC,查一下有没有获取数组长度的函数,有的话直接用,比手写常量更可靠。

技巧四:代码审查时重点看FOR和数组定义。我每次代码审查,都会把所有的FOR循环和数组定义列出来,一一对照。这个习惯帮我发现了不少潜在问题。

8. 从FOR循环延伸到ST编程的思维转变

8.1 从“次数思维”到“下标思维”

很多PLC工程师,尤其是从梯形图转过来的,习惯用“次数”来思考循环。比如“循环16次”,脑子里想的是执行16遍。但ST的FOR循环,你写的是“下标范围”,不是“次数”。FOR i := 0 TO 16是下标0到16,共17次。FOR i := 0 TO 15是下标0到15,共16次。

这个思维转变很重要。你写FOR的时候,不要想“我要循环几次”,而要想“我要访问哪些下标”。下标范围确定了,次数自然就确定了。

8.2 从“大概对”到“精确对”

梯形图编程,有时候“大概对”就能跑,因为逻辑是线性的,错一点可能不影响大局。但ST编程,尤其是数组和循环,必须“精确对”。差一个下标,可能就是丢数据或者越界。

我见过很多ST程序,功能都能跑,但边界条件处理得很粗糙。平时没事,一到边界就出问题。所以,写ST代码,一定要有“边界意识”,每个数组访问、每个循环范围,都要精确确认。

8.3 从“手动管理”到“常量绑定”

手动管理数组大小和循环范围,短期看没问题,长期看一定会出问题。因为人会忘,会改错。用常量绑定,把数组大小和循环范围关联起来,是减少人为错误的最有效方法。

三菱的ST支持常量,你可以定义ARRAY_SIZE,然后在数组定义和FOR循环里都用它。这样,你只需要维护一个常量,其他地方自动同步。

9. 工具与资源:GX Works3里的实用功能

9.1 交叉引用检查数组访问

GX Works3有交叉引用功能,你可以查看某个数组在哪些地方被访问。如果发现有的地方用常量下标,有的地方用变量下标,就要特别注意变量下标的范围。

具体操作:在GX Works3里,右键点击数组变量,选择“交叉引用”,然后查看所有引用位置。对于变量下标的引用,逐一确认循环范围。

9.2 用仿真功能测试边界

GX Works3支持仿真,你可以在仿真环境下测试数组边界。比如,写一个测试程序,故意让FOR循环越界,看仿真会不会报错。虽然仿真和实际PLC的行为可能不完全一样,但至少能发现明显的越界问题。

9.3 用监视表批量监视数组

GX Works3的监视表,可以批量添加数组元素。你可以把整个数组添加到监视表,然后运行程序,看每个元素的值。如果某个元素一直是0或者旧值,说明它没被循环访问到。

这个方法比逐个监视效率高得多,尤其是数组比较大的时候。

10. 最后分享几个实操中的小经验

第一个经验:写FOR循环之前,先写下数组的下标范围。比如你在纸上写“数组0到16”,然后FOR写TO 16,这样就不会错。不要凭记忆写,记忆有时候会骗你。

第二个经验:如果数组是从1开始的,FOR也从1开始。不要混用。我见过有人数组是[1..17],FOR写TO 17,但循环体里访问MyArray[i-1],结果下标0被访问,越界。这种混用非常危险。

第三个经验:在循环体开头加一句范围检查。虽然正常情况下不会触发,但万一以后有人改了数组定义,这个检查能帮你提前发现。

FOR i := 0 TO ARRAY_SIZE - 1 DO IF i < 0 OR i >= ARRAY_SIZE THEN EXIT; END_IF; // 正常处理 END_FOR;

第四个经验:用版本控制管理代码。每次修改数组定义或者FOR循环,都提交一次,写清楚改了什么。这样如果出了问题,可以回溯到之前的版本,看看是不是某次修改引入的。

第五个经验:定期做边界测试。不要等到出了问题才去查,平时就定期测试数组的边界元素,确保它们能被正确访问。这个习惯能帮你提前发现潜在问题。

这些经验都是我在实际项目中踩坑总结出来的,有些是花了很长时间才排查出来的。希望对你有所帮助。如果你也有类似的经历,或者有其他ST编程的问题,欢迎交流。

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

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

立即咨询