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然后下一次判断1716才退出。所以FOR i : 0 TO 16 DO实际上会执行i0,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(i0;i16;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-0117。很多人看到16脑子里想的是“16个元素”然后FOR写TO 15觉得15是最后一个结果就丢了。这个思维惯性非常致命。2.2 FOR循环的终止条件到底怎么判断三菱ST的FOR循环执行逻辑是这样的循环变量赋初值。判断循环变量是否大于终值。如果大于退出循环。执行循环体。循环变量增加步长默认是1。回到第2步。所以FOR i : 0 TO 16 DO的执行序列是i00不大于16执行i1执行...i1616不大于16执行i1717大于16退出。一共执行了17次i从0到16。如果你写FOR i : 0 TO 15 DO执行序列是i0到i15共16次i16的时候判断1615退出所以下标16没执行。这个逻辑和C语言、Java、Python都不一样。C语言里for(i0;i16;i)才是17次for(i0;i16;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]结果循环到i16的时候访问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结果i16的时候写入了Data[16]而这个地址恰好是另一个重要变量SetPoint的存储位置导致设定值被覆盖设备运行异常。他查了两天才发现是数组越界。所以数组越界在三菱ST里不是“可能没事”而是“一定有事”只是什么时候爆发的问题。3. 实操排查如何快速定位FOR循环丢数据的问题3.1 用GX Works3的监视功能确认循环次数当你怀疑FOR循环丢数据时第一步是在GX Works3里监视循环变量和数组元素。具体操作在ST程序里把FOR循环的循环变量i和数组MyArray添加到监视窗口。在线运行触发循环。观察i的最终值。如果循环结束后i的值是16说明循环执行到了16如果是15说明只到15。同时观察MyArray[16]的值看是否被更新。这里有一个细节三菱的FOR循环循环变量在循环结束后会保留最后一次的值。如果循环正常结束i的值应该是终值1。比如FOR i : 0 TO 16 DO结束后i17。如果你看到i16说明循环在i16的时候退出了那可能是终值写成了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; // 跳过i5 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品牌的对比不同品牌的PLCST语言的FOR循环设计也不一样。比如西门子的SCLFOR 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编程的问题欢迎交流。