☰
三菱ST数组越界:FOR上限写16,为什么丢了17号
2026/9/29 13:12:48 网站建设 项目流程

1. 一个让老手都翻车的下标问题

三菱GX Works3里写ST程序,数组和FOR循环是再基础不过的组合。但就是这个基础组合,坑过的人不在少数。我见过一个现场调试的哥们,数组开了16个元素,FOR循环上限也老老实实写了16,结果第17号数据死活读不出来,查了半天硬件、查了半天通讯,最后发现问题出在自己写的循环上。

这个标题说的就是这件事:三菱ST数组越界,FOR上限写16,为什么丢了17号。听起来像绕口令,实际上是一个关于数组下标起点、循环边界条件、以及ST语言数组声明方式的综合问题。它牵扯到的知识点不复杂,但每一个都容易在实操中被忽略。

这篇文章适合所有用GX Works3写ST程序的同行看,不管你是刚接触ST的新手,还是写了好几年梯形图刚转ST的老手,这个问题都值得花时间彻底搞明白。我会从数组的声明方式讲起,把下标起点、元素个数、循环边界这三者的关系掰开揉碎,再给出可以直接抄的代码模板和排查清单。看完之后,你至少不会再在FOR循环的边界上栽跟头。

2. 数组声明方式决定了你的下标从几开始

2.1 三菱ST数组的两种声明写法

在三菱的ST语言里,数组声明有两种常见写法,而这两种写法直接决定了你的下标是从0开始还是从1开始。很多人出问题,就是没搞清楚自己用的到底是哪一种。

第一种写法是指定下标范围:

VAR DataBuf : ARRAY[0..15] OF INT; END_VAR

这种写法明确告诉编译器,这个数组的下标范围是0到15,一共16个元素。下标从0开始,最后一个元素是DataBuf[15]。

第二种写法是只给元素个数:

VAR DataBuf : ARRAY[1..16] OF INT; END_VAR

这种写法下标范围是1到16,也是16个元素,但下标从1开始,最后一个元素是DataBuf[16]。

还有一种是很多从其他PLC平台转过来的人习惯用的写法:

VAR DataBuf : ARRAY[16] OF INT; END_VAR

在三菱的ST里,这种写法默认下标从0开始,也就是说它的有效下标是0到15,一共16个元素。这一点和某些其他品牌的PLC不一样,那些平台可能默认从1开始。如果你带着别的平台的思维来写三菱ST,这里就是第一个坑。

2.2 下标起点不同带来的连锁反应

下标起点不同,影响的不只是你访问元素时写的数字,更关键的是循环的边界条件。

假设你声明了ARRAY[0..15] OF INT,一共16个元素。如果你想遍历所有元素,FOR循环应该这样写:

FOR i := 0 TO 15 DO Process(DataBuf[i]); END_FOR;

循环变量i从0开始,到15结束,正好覆盖16个元素。

但如果你声明的是ARRAY[1..16] OF INT,遍历所有元素的循环就应该是:

FOR i := 1 TO 16 DO Process(DataBuf[i]); END_FOR;

循环变量i从1开始,到16结束,也是16个元素。

问题来了:标题里说的“FOR上限写16,丢了17号”,说明写代码的人心里想的是“我要处理17个数据”,但数组只开了16个元素的空间。这多出来的第17号数据,要么是数组声明的时候少开了一个,要么是循环边界写错了导致访问了不存在的元素。

2.3 一个容易混淆的场景:数组元素个数与最大下标

这里有一个非常容易混淆的点,我单独拎出来说。

当你看到ARRAY[0..15]的时候,元素个数是16,最大下标是15。当你看到ARRAY[1..16]的时候,元素个数也是16,最大下标是16。

很多人脑子里记住的是“我开了16个元素的数组”,然后在写FOR循环的时候,下意识地写FOR i := 0 TO 16,觉得“16个元素嘛,循环到16就对了”。但实际上,如果下标从0开始,循环到16就访问了第17个元素,而那个元素根本不存在,直接触发数组越界。

反过来,如果下标从1开始,FOR i := 1 TO 16是正确的,但如果你写成FOR i := 0 TO 15,虽然循环次数也是16次,但第一次访问的是DataBuf[0],这个下标在ARRAY[1..16]里是不存在的,同样越界。

所以核心原则是:FOR循环的起始值和结束值,必须和数组声明的下标范围完全匹配。起始值等于数组的最小下标,结束值等于数组的最大下标。不要用元素个数去推导循环边界,要用下标范围去写。

3. FOR循环边界写错的三种典型情况

3.1 情况一:下标从0开始,循环却从1开始

这是最常见的一种错误。数组声明为ARRAY[0..15],但写循环的时候写成了:

FOR i := 1 TO 16 DO Process(DataBuf[i]); END_FOR;

这个循环有两个问题。第一,DataBuf[0]被跳过了,第一个元素没处理到。第二,DataBuf[16]不存在,最后一次循环直接越界。结果就是“丢了0号,多了16号”,程序要么报错,要么读到垃圾数据。

这种错误通常发生在从其他平台转过来的人身上,因为有些平台的数组默认从1开始,写习惯了FOR i := 1 TO N,到了三菱这边没改过来。

3.2 情况二:下标从1开始,循环却从0开始

反过来,数组声明为ARRAY[1..16],循环写成:

FOR i := 0 TO 15 DO Process(DataBuf[i]); END_FOR;

第一次循环访问DataBuf[0],这个下标不存在,直接越界。后面虽然访问了1到15,但DataBuf[16]没被处理到。结果是“多了0号,丢了16号”。

这种错误往往是因为写代码的人记得“数组有16个元素”,然后习惯性地从0数到15,忘了这个数组的下标是从1开始的。

3.3 情况三:元素个数与下标范围混用

这是标题里描述的那种情况。写代码的人心里想的是“我要处理17个数据”,于是数组声明为ARRAY[0..16],一共17个元素。但写FOR循环的时候,脑子里想的是“16个元素”,于是写了FOR i := 0 TO 15。结果第17号元素DataBuf[16]没被处理到,看起来就是“丢了17号”。

或者反过来,数组声明为ARRAY[0..15],一共16个元素,但写循环的时候想的是“我要处理到16号”,于是写了FOR i := 0 TO 16。结果最后一次循环访问DataBuf[16],越界。

这两种情况的根源都是一样的:元素个数和最大下标是两个不同的概念,混在一起用就出错。

3.4 用表格把关系理清楚

我把常见的数组声明和对应的正确循环写法整理成一张表,方便你对照检查:

数组声明元素个数最小下标最大下标正确的FOR循环
ARRAY[0..15] OF INT16015FOR i := 0 TO 15
ARRAY[1..16] OF INT16116FOR i := 1 TO 16
ARRAY[0..16] OF INT17016FOR i := 0 TO 16
ARRAY[1..17] OF INT17117FOR i := 1 TO 17
ARRAY[16] OF INT16015FOR i := 0 TO 15

这张表建议你截图存手机里,写ST的时候拿出来对一眼,能省下大量调试时间。

4. 实操:从零搭建一个不会越界的数组处理程序

4.1 需求分析与数组容量规划

假设我们要做一个配方数据缓冲区,需要存储17个工位的温度设定值。每个工位一个INT数据,总共17个数据。

第一步是确定数组声明。17个数据,如果下标从0开始,声明为ARRAY[0..16];如果下标从1开始,声明为ARRAY[1..17]。两种都可以,关键是后续的循环要匹配。

我个人的习惯是统一用0作为起始下标,因为这样和大多数编程语言的惯例一致,查资料、看例程的时候不容易混淆。所以这里声明为:

VAR RecipeTemp : ARRAY[0..16] OF INT; (* 17个工位温度设定值 *) i : INT; Sum : INT; Avg : INT; END_VAR

4.2 正确的FOR循环写法与参数计算

遍历这17个元素,求平均值:

Sum := 0; FOR i := 0 TO 16 DO Sum := Sum + RecipeTemp[i]; END_FOR; Avg := Sum / 17;

循环变量i从0到16,一共循环17次,正好覆盖17个元素。这里的关键是:结束值16来自数组声明的最大下标,而不是元素个数17。如果你写成FOR i := 0 TO 17,就会越界;如果写成FOR i := 0 TO 15,就会漏掉最后一个元素。

再强调一遍:FOR的结束值 = 数组声明的最大下标。这个等式永远成立,不管数组有多少个元素。

4.3 用常量定义数组大小,避免硬编码

在实际项目里,数组大小可能会调整。如果每次调整都要去改FOR循环的边界,很容易漏改。更好的做法是用常量来定义:

VAR_GLOBAL CONSTANT RECIPE_COUNT : INT := 17; RECIPE_MAX_IDX : INT := 16; END_VAR VAR RecipeTemp : ARRAY[0..RECIPE_MAX_IDX] OF INT; i : INT; END_VAR FOR i := 0 TO RECIPE_MAX_IDX DO Process(RecipeTemp[i]); END_FOR;

这样,如果以后工位数量变成20个,只需要改RECIPE_COUNT和RECIPE_MAX_IDX两个常量,循环边界自动跟着变,不会出现漏改的情况。

注意:三菱ST里数组声明使用常量作为边界时,常量必须是编译期可确定的。用VAR_GLOBAL CONSTANT定义的常量可以满足这个要求。如果你用的是VAR定义的变量,编译器会报错。

4.4 实操现场记录:一次真实的排查过程

我之前在一个包装机项目上遇到过类似的问题。设备有24个气缸,每个气缸有两个位置传感器,一共48个BOOL量。我声明了一个ARRAY[0..47] OF BOOL来存传感器状态,然后写了一个FOR循环来扫描:

FOR i := 0 TO 48 DO IF CylSensor[i] THEN CylCount := CylCount + 1; END_IF; END_FOR;

编译没报错,下载运行也没立刻出问题。但运行了大概十几分钟后,PLC报了“数组下标越界”的错误,设备停机。

排查的时候我一开始怀疑是传感器抖动导致的,查了半天硬件没发现问题。后来把程序调出来逐行看,才发现FOR循环写的是0 TO 48,而数组最大下标是47。每次扫描都会访问CylSensor[48],这个下标不存在,偶尔读到的是相邻变量的数据,偶尔直接触发越界保护。

改成FOR i := 0 TO 47之后,问题消失。这个坑的教训是:编译通过不代表运行没问题,数组越界有时候不会在编译阶段被发现,而是在运行时才暴露。

5. 数组越界的排查思路与常见问题速查

5.1 编译期能发现和不能发现的越界

三菱GX Works3对数组越界的检查分两个层面。

编译期能发现的:如果你直接用常量下标访问数组,比如DataBuf[20],而数组声明是ARRAY[0..15],编译器会直接报错,告诉你下标超出范围。这种错误最容易发现,也最容易改。

编译期不能发现的:如果你用变量作为下标,比如DataBuf[i],编译器无法在编译阶段知道i的值是多少,所以不会报错。只有在运行时i的实际值超出了数组范围,才会触发越界。这种错误最危险,因为它可能潜伏很久才暴露,而且暴露的时候往往是在客户现场。

还有一种情况是间接越界:你用了一个中间变量来计算下标,比如DataBuf[Base + Offset],编译器同样无法在编译期判断。这种越界最隐蔽,排查起来也最费时间。

5.2 运行时越界的表现与诊断方法

运行时数组越界在三菱PLC上的表现有几种:

第一种是PLC报错停机,错误代码里会提示数组下标越界。这是最好的情况,因为问题立刻暴露,你知道去查数组相关的代码就行。

第二种是读到错误数据但不报错。PLC访问了数组范围之外的内存区域,读到的可能是其他变量的值,也可能是随机数据。程序继续运行,但逻辑已经错了。这种最麻烦,因为现象可能千奇百怪,你很难第一时间想到是数组越界。

第三种是写入越界。如果越界的是写操作,可能会覆盖其他变量的值,导致完全不相关的功能出问题。比如你本来想写DataBuf[16],结果写到了相邻的Timer变量上,定时器就乱了。

诊断方法我一般用这几招:

  • 在FOR循环里加一个范围检查,如果i超出预期范围就置一个标志位,方便事后查看。
  • 用GX Works3的监视功能,在线监视循环变量i的值,看它实际循环到了多少。
  • 在循环前后各加一个计数器,对比循环次数是否和预期一致。

5.3 常见问题速查表

现象可能原因排查方法解决方法
PLC报数组越界错误FOR结束值大于最大下标检查FOR的TO后面的值改为最大下标
第一个元素没被处理FOR起始值大于最小下标检查FOR的:=后面的值改为最小下标
最后一个元素没被处理FOR结束值小于最大下标检查FOR的TO后面的值改为最大下标
编译报错“下标超出范围”常量下标超出声明范围检查数组声明和访问代码调整声明或访问下标
运行一段时间后出错变量下标在特定条件下越界监视下标变量的取值范围加范围限制或修正逻辑
数据莫名其妙被改写写操作越界覆盖了其他变量检查所有数组写操作修正下标范围

5.4 几个容易忽略的边界场景

除了基本的FOR循环边界,还有几个场景容易出问题。

场景一:倒序循环。如果你写FOR i := 16 TO 0 DO,三菱ST默认是递减循环,从16到0,这是合法的。但如果你写FOR i := 0 TO 16 BY -1 DO,就会出问题,因为起始值小于结束值,步长为负,循环条件永远不满足,循环体一次都不执行。

场景二:嵌套循环共用循环变量。外层循环用i,内层循环也用i,内层循环结束后i的值被改掉了,外层循环的计数就乱了。这种错误不会报越界,但逻辑完全错误。解决办法是内外层用不同的循环变量。

场景三:循环体内修改循环变量。在FOR循环体里给i赋值,比如i := i + 1,会打乱循环的正常计数。三菱ST里FOR循环的计数是编译器管理的,你在循环体里改i的值,下一次循环的时候i会被重新赋值为下一个值,你的修改被覆盖。但这种行为在不同编译器上可能不一样,最好的做法是永远不要在FOR循环体里修改循环变量。

6. 写ST数组循环的几条铁律

6.1 声明与循环必须成对检查

每次写完一个数组和对应的FOR循环,花十秒钟做一次对照检查:数组的最小下标是不是等于FOR的起始值?数组的最大下标是不是等于FOR的结束值?这两个问题都回答“是”,才能往下走。

我自己的习惯是在数组声明后面加一行注释,写明最小下标和最大下标:

RecipeTemp : ARRAY[0..16] OF INT; (* idx: 0-16, cnt: 17 *)

这样写FOR循环的时候,直接看注释就知道该写0 TO 16,不用再去数元素个数。

6.2 用FOREACH思维替代手工下标

如果你的PLC型号和GX Works3版本支持,可以考虑用更高级的遍历方式。不过三菱ST对FOREACH的支持有限,大多数情况下还是得用FOR循环加下标。那就老老实实把下标范围写对。

6.3 边界测试不能省

程序写完之后,至少做一次边界测试:把数组的第一个元素和最后一个元素分别赋一个特殊值,然后运行循环,看这两个值有没有被正确处理。如果第一个元素没被处理到,说明起始值写大了;如果最后一个元素没被处理到,说明结束值写小了。

这个测试花不了两分钟,但能帮你提前发现大部分边界问题。

6.4 版本差异要注意

不同版本的GX Works3对数组越界的检查严格程度不一样。有些版本编译期检查很松,运行期也不一定报错;有些版本则很严格。如果你从旧版本升级到新版本,原来能跑的代码可能突然报越界错误。这不是新版本有问题,而是旧版本帮你掩盖了问题。遇到这种情况,老老实实把下标范围改对就行。

7. 从数组越界延伸出去的几个思考

数组越界这个问题,表面上看是FOR循环边界写错了,往深了想,其实是对“范围”这个概念的理解不够精确。在编程里,范围无处不在:数组有下标范围,循环有迭代范围,数据类型有取值范围,通讯有地址范围。每一个范围都有起点和终点,混淆起点终点、混淆范围大小和边界值,就会出问题。

我自己的经验是,写任何涉及范围的代码,都先把范围的起点和终点明确写出来,再写循环或访问逻辑。不要凭记忆,不要凭感觉,不要“我觉得应该是”。把范围写下来,对着写,出错概率能降低一大半。

另外,数组越界只是ST编程里众多边界问题的一个。类似的还有字符串处理时的长度边界、结构体数组的成员访问边界、二维数组的行列边界等等。掌握了“范围起点终点必须明确”这个原则,这些问题都能用同样的思路去排查和解决。

最后说一个我踩过的坑:有一次我用ARRAY[1..10]声明数组,然后在另一个地方用SIZE_OF函数去取数组大小,得到的结果是10。我下意识地认为最大下标就是10,循环写了FOR i := 1 TO 10,这是对的。但后来我把数组改成ARRAY[0..10],元素个数变成了11,SIZE_OF返回11,我却还是写FOR i := 1 TO 10,结果DataBuf[0]没被处理到。这个坑的教训是:数组大小和最大下标是两个不同的数,改数组声明的时候,两个都要检查。

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

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

立即咨询