三菱ST数组越界:FOR上限写16,为什么丢了17号

发布时间:2026/9/29 13:12:56
三菱ST数组越界:FOR上限写16,为什么丢了17号 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 15ARRAY[1..16] OF INT16116FOR i : 1 TO 16ARRAY[0..16] OF INT17016FOR i : 0 TO 16ARRAY[1..17] OF INT17117FOR i : 1 TO 17ARRAY[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_VAR4.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]元素个数变成了11SIZE_OF返回11我却还是写FOR i : 1 TO 10结果DataBuf[0]没被处理到。这个坑的教训是数组大小和最大下标是两个不同的数改数组声明的时候两个都要检查。