☰
三菱PLC的ST语言数组偏移:从常量变量之争到跨机型复用
2026/9/30 1:17:52 网站建设 项目流程

三菱PLC的ST语言里,数组偏移到底写常量还是变量,这个问题我在技术群里被翻来覆去问过很多次,而且每次都会跟着下一句:那换个机型是不是全得重写?说实话,这两个问题确实连在一起——很多人在FX3U上写ST代码时,数组下标习惯用绝对软元件地址或者写死的常量偏移,当时跑得很顺,等项目换到FX5U或者Q系列,才发现整个程序到处都要改,改到最后跟重写也没什么区别。这篇文章不打算绕弯子,直接把我的结论放在前面:数组访问的表达式中,下标用变量通常是被允许的,也是我推荐的做法;真正要求常量的,是数组定义时的长度和一小类特殊指令的操作数。而所谓换机型全重写,十有八九不是ST语法造成的,而是寻址方式和工程格式的问题。下文会把这几个层面拆开讲,最后给出一套我实际在项目里复用的代码结构。

1. 常量还是变量:这个问题其实被问偏了

1.1 一个典型案例:真空搬运线的7轴配方数组

去年做一个真空搬运项目,CPU用的是FX3U-64MT/ES,7个气动轴加两个电动夹爪,每轴有8个工艺参数,包括行程速度、动作延时、原点偏置之类,全部存在D200开始的连续数据区里。当时为了贪快,ST代码里大量出现带偏移的数组访问,有的地方直接写死常量偏移,比如"第3轴的延时就是D200+2*8+1",有的地方用变址寄存器Z0做动态偏移。程序调试倒是很顺利,气缸动作快慢调起来也方便。问题出在项目验收之后,客户提出要升级到FX5U-80MT,说是后续还要加两套定位模块。

新工程一打开,事情就大了。FX5U的D区范围、默认断电保持区、特殊继电器编号和FX3U完全不是一回事,我原来用M8000做上电初始化,到了FX5U要对到SM400;原来定位相关的D8340那些地址,在新机型里根本没有对应的平移关系。最要命的是那些散落在ST块里的绝对软元件号和写死的偏移量,我整整改了三天,一边改一边对着手册逐条比对,有几个块改完测出来还是错的,最后干脆推倒重来。这就是典型的"换机型全重写"现场,根源根本不在于数组下标用常量还是变量,而在于硬件地址已经钻进了业务代码的每个角落。

1.2 先分清三个层面,再谈怎么写

"数组偏移写常量还是变量"这个问题,看着是在问一行代码,实际上牵涉到三个完全不同的层面,不分开谈永远理不清。

第一层是语法层,回答的是"编译器放不放行"。三菱ST编译器对数组定义长度、数组访问下标各有各的规矩,这一层决定了你代码里能不能写变量下标,以及哪些场景会被编译报错直接拦下来。

第二层是寻址层,回答的是"数组元素背后绑在哪个软元件上"。同样一个数组,在FX3U里可能映射到D区,在Q系列里映射到另一个数据区,在FX5U里可能完全走标签寻址,这一层决定了换机型时你的代码会不会地址全部失效。

第三层是架构层,回答的是"以后动不动就要重写"。如果你的业务程序里随处可见D100加某个偏移的手算表达式,那无论语法层怎么写,换设备时都是灾难。

三个层面的关系和关注点,我一般直接看下表:

层面核心问题典型表现不处理好会怎样
语法层能不能用变量下标编译报"数组下标必须为常量"程序写不出来
寻址层数组落在哪个软元件上换设备后地址对不上程序整个扫描一遍重改
架构层地址和业务逻辑是否解耦代码里直接写D200+n*8换机型=全重写

下面我会按这三个层面逐层深入。

2. 三菱ST里数组偏移的真实规则:编译器到底卡在哪

2.1 数组定义:长度必须常量,但这是"定义"不是"访问"

先看最容易踩坑的语法环节:数组定义。无论你在GX Works2还是GX Works3里写ST,定义一个数组时,它的长度/维度必须在定义阶段就是确定的常量。比如:

VAR recipe : ARRAY[0..9] OF INT; // 10个元素,长度写死了 END_VAR

这里的10必须是一个常量值,不能是某个运行期才会算出来的变量。你不可能写:

VAR n : INT := 10; recipe : ARRAY[0..n] OF INT; // 编译不过,n不是编译期常量 END_VAR

原因是编译器要给这个数组分配固定的内存空间,就像你去租储物柜,柜子数量必须先定死,人家才能给你安排哪几号柜子,营业员不可能说"先给你1到N号,N等你进门再说"。这个规则在C语言、Java、ST语言里都是一样的,属于语言底层的存储模型问题,不是三菱为了刁难人。

很多人在这一步被报错吓住,就误以为整个ST里数组偏移都只能用常量了,这是一个很深的误解。定义长度用常量,和访问时下标用变量,完全是两回事。

2.2 数组访问:下标用变量通常没有任何问题

一旦数组定义好了,访问表达式里的下标(也就是偏移量)是可以使用变量的。这是IEC 61131-3标准ST语言的基本能力,三菱在GX Works2和GX Works3里都遵循这一规则。

举个例子,我在搬运设备里做配方切换,直接用FOR循环遍历所有轴参数:

VAR i : INT; tempSpeed : REAL; END_VAR // 读取第i个轴的配方速度(variable作为下标) FOR i := 0 TO 9 DO tempSpeed := g_RecipeParam[i].SpeedMax; END_FOR;

这段代码里,i就是一个运行期变量,编译器不会拦你。它的底层逻辑是:编译时将这种动态下标转换成变址寻址方式,真正执行到g_RecipeParam[i]这一行时,CPU才根据当前i的值计算出真实的内存地址。

所以结论很明确:数组访问的下标用变量,不是"开不开后门"的问题,而是标准操作。你在循环里遍历数组、用轴号做索引取参数、根据配方编号读取数据,这些都理所当然应该用变量。问题从来不是"能不能",而是"你知不知道哪些场景真的不行"。

2.3 必须用常量的高频场景与绕过方式

虽然访问下标可以用变量,但实际项目中仍然会遇到一些编译失败的情况,编译器会提示"数组下标必须是常量"。我踩过几次坑之后总结了一下,真正要求常量下标的场景其实很集中:

第一类是位操作。当数组元素作为位指令的处理对象时,比如需要对某个BOOL元素的某一位进行置位、复位,或者数组元素被传到位串指令里,很多三菱编译器推导不出运行期的位地址,就会要求你写常量下标。

第二类是字符串和ASCII转换指令。GX Works系列的字符串/字符转换类指令,很多对操作数的地址有编译期要求,数组元素一旦作为这类指令的操作数,编译器可能直接报错。

第三类是某些FB实例数组的定义绑定。FB实例本身是编译期静态分配的,实例数组的维度定义必须用常量,这跟普通数组的长度定义是同一类逻辑。

遇到这类报错,最直接的绕过方式就是一个中间变量中转。不要试图直接对数组元素做指令级操作,而是先把它取出来,处理完再写回去:

// 假设某个位操作指令要求常量下标 tempBool := g_FlagArray[i]; // 用变量下标访问,先把元素取出来 someBitOp(tempBool); // 对中间变量做位操作 g_FlagArray[i] := tempBool; // 再写回去

这一招几乎能解决所有"必须常量"的编译报错,代价只是多一个中间变量和一次赋值,运行时间差到可以忽略。我现在的习惯是:遇到编译器报错,先看指令类型,再判断要不要中转,而不是一开始就跟语法较劲。

2.4 索引寄存器Z:老一代的动态偏移方案

聊到"变量偏移",不能不提三菱PLC一个很有年代感的东西:索引寄存器Z。在FX3U以及更老的FX系列里,大量梯形图程序会看到D100Z0这种写法,含义是"以D100为基址,用Z0的值作为偏移量取数"。也就是说D100Z0等价于"距离D100有Z0个字距离的那个数据寄存器"。

这就是老一代的动态偏移,本质是把"数组下标"拆成了"基址+变址",变量放在Z寄存器里。它确实能用变量做偏移,但有几个明显痛点:

对比项传统软元件变址(D100Z0)标签数组变量下标
可读性差,满屏魔法数字好,一眼看出取的是哪个轴
可维护性差,偏移量手算容易错好,编译器帮你管理
跨机型很差,Z数量、字长都不一致好,只要软元件映射不变
越界保护无,超出范围直接取相邻地址可通过代码逻辑做边界判断

所以我现在的项目里基本不再用索引寄存器做数组偏移了。不是说它不能用,而是它的可移植性太差,换到FX5U、Q系列,索引寄存器的数量、字长都不一样,等于又多了一处"全重写"的坑。

3. 换机型"全重写"的根源,不在ST语法而在寻址与工程格式

3.1 软元件体系的差异:从FX3U到Q系列没有"平移"

很多人以为换机型就是把程序文件打开、改个CPU型号就算完事,实际上一台PLC的程序能不能迁移,很大程度取决于软元件体系的兼容程度。从FX3U换到FX5U,再换到Q系列,软元件编号的对应关系根本不是一张映射表能解决的。

FX3U里有M8000作为常通继电器、D8000系列作为特殊数据寄存器,这些到Q系列里对应的是SM400和SD寄存器。听起来是改个名字就能用,实际上SM、SD的编号语义和数量规格完全不同。还有定位控制相关的一组D8340等软元件,在FX3U里是CPU自带的脉冲输出状态区,到了Q系列如果接的是QD77MS定位模块,访问方式变成了智能功能模块的缓冲区读写,ST代码完全要重新写。

再有就是IO地址。FX3U是小型机,X/Y地址按主机和扩展模块的自然编号排列;Q系列是模块化PLC,输入输出模块插在哪个槽位,地址就跟着槽位走。同样是第一块DI模块,两台设备的X地址几乎不可能一样。这些差异叠加起来,所有写死的绝对地址全部失效,不重写才怪。

我见过很多朋友在迁移时最痛苦的一步,不是ST语法看不懂,而是对着手册把一个一个D寄存器、M继电器的旧地址翻译成新地址,翻译到后面对都没法对。这不是翻译任务,这是重新设计程序的地图。

3.2 标签寻址加AT分配:把硬件地址请出业务代码

解决这一层问题的思路其实很成熟,就是标签寻址。GX Works3里可以定义全局标签,标签本质上是给一段软元件区域起个业务含义的名字,然后在标签编辑器里用AT指令把标签绑定到一个真实的软元件地址上。

举个实际例子。我在GX Works3里定义一个全局标签:

// 标签编辑器中定义: // 标签名:g_RecipeParam // 数据类型:ARRAY[0..9] OF INT // 软元件分配:AT D200

这个标签一旦定义完毕,ST代码里就完全不需要再关心D200这个地址了。你要访问配方第i个参数,直接写g_RecipeParam[i]即可。等以后换机型,比如从FX3U换到FX5U,D区的起始地址变了,你只需要在标签编辑器里把AT绑定改为AT D0或者其他任何可用的数据区,ST代码本身一个字都不用改。

这一招是我现在做跨机型项目的基础,也是把"全重写"变成"改映射表"的核心操作。说夸张一点,标签寻址就是给硬件地址做了一层"翻译官",业务代码永远只跟翻译官说话,不跟硬件地址说话。

3.3 工程格式迁移:GX Works2和GX Works3之间没有魔法

除了软元件地址,还有一个容易被忽略的重写根源:工程格式。FX3U时代最常用的是GX Works2,FX5U和后续机型主推GX Works3,Q系列虽然在两个软件里都有对应入口,但工程文件的格式和结构化程度完全不一样。

从GX Works2的ST程序块搬到GX Works3,实际经历过的朋友都知道,基本没有"打开就迁移"这回事。三菱提供的工程转换工具能做简单工程的转换,但结构化工程的全局标签、FB实例、ST源程序往往会被打散。尤其是你用ST写的结构化程序块,转换之后经常出现标签丢失、数据类型错乱。我见过有人转换完一个工程,FB全部变成普通子程序,接口参数全没了,只能手动重建。

所以我现在的态度很明确:不要指望三菱提供一个"一键迁移"的魔法按钮。跨机型的工作,永远是先把标签体系重新搭好,再把ST代码块复制过来,最后修映射层。好在只要前面架构做得足够干净,复制过来的ST代码块基本可以直接用,重写只发生在标签和映射层面。

4. 一套能跨机型复用的ST代码结构,我是这么搭的

4.1 IO映射层单独隔离,换设备只改一张"接线表"

我按项目经验总结了一套三层结构,第一个原则就是IO映射层必须和业务逻辑彻底分开。具体做法是:在程序里开辟一个专门的程序块,只做一件事——把物理IO点赋值给内部标签,以及把内部标签输出到物理IO点。

// 映射程序块:全工程唯一允许出现X/Y绝对地址的地方 bHomeX := X0; bHomeY := X1; bWorkpiecePresent := X3; Y0 := outCylinder1Up; Y1 := outCylinder1Down;

业务逻辑里永远只出现bHomeX、outCylinder1Up这种看一眼就明白含义的标签。换设备时,X/Y地址表会变,但那又怎样?我只需要重新画一遍"接线表"——就是上面这一段映射代码——其余几十个程序块一行都不用动。这个经验在从FX3U换到Q系列的几次项目里被反复验证过,IO映射层占整个程序不到5%的代码量,却是唯一需要大面积改动的5%。

4.2 参数封成结构体数组,用轴号做索引而不是手算偏移

第二个原则是参数区必须结构化。早期我写7轴参数时,程序里到处是"D200+轴号*8+偏移量"的表达式,看着能用,实际上每一次增删参数字段都要重新数一遍偏移,一数就错。后来改成结构体数组,世界清净了。

// 在GX Works3的数据类型里定义结构体 TYPE AxisParam : STRUCT SpeedMax : REAL; AccTime : REAL; DecTime : REAL; HomeOffset : REAL; END_STRUCT END_TYPE // 全局标签 // g_AxisParam : ARRAY[0..6] OF AxisParam AT D200

这样ST代码里取第3轴的加速度时间,写g_AxisParam[3].AccTime就行了,偏移量由编译器管理,不需要我手算。将来增加一个轴的字段,比如加个SettleTime,只要在结构体里插入一项,把D区容量留够,所有代码访问自动适配,不会出现"第4轴的偏移从8变成9导致全组参数错位"这种阴间问题。

很多从梯形图转过来的工程师觉得结构体太抽象,其实就是把一组相关的数据打包进一个盒子里,盒子有编号,盒子里有抽屉,抽屉有名字。你按名字拿东西,比按"第三个抽屉往左数两格"要可靠得多。

4.3 机型差异集中到常量表和适配FB,业务逻辑保持不动

第三个原则是机型的差异化信息全部收敛到常量表和适配功能块里。所谓机型差异,包括轴数、脉冲当量、伺服模块的类型、定位模块缓冲区起始地址等。这些值不要在ST代码里零散出现,而是统一放到一个机型参数常量表里。

比如我在全局标签里定义一组常量,轴数cAxisCount := 7,脉冲当量cPulsePerMm := 1000.0。业务代码里所有循环上限、速度换算都用这些常量,而不是裸写7和1000。换到8轴设备时,只改常量定义,循环和计算自动跟随。

对于确实无法用常量表抹平的硬件接口差异,比如FX3U的脉冲输出方式和QD77MS定位模块的缓冲区读写方式不同,我会把它们封装在适配FB里。这个FB对外接口保持一致,输入轴号、目标位置、速度,输出状态;内部用CASE按机型分支处理。换机型时只动适配FB内部,调用它的所有业务代码全部不动。

这套三层结构——映射层、参数结构体、适配FB——是我目前能拿出来的最抗造的组合。凡是按这个结构写的工程,换机型时工作量集中在标签定义和映射程序块,其余ST逻辑代码能被完整复用,远达不到"全重写"的程度。

5. 实测里最容易翻车的三个细节,附真排查顺序

5.1 "数组下标必须为常量"该怎么一步步查

编译报"数组下标必须为常量"的时候,先别急着把代码改成常量了事。按我平时的排查顺序来:

第一步,看报错位置用的是什么指令。如果是位操作、字符串转换这类特殊指令,八九不离十是编译器对动态位的推导不了,老老实实用中间变量中转。

第二步,检查数组元素的数据类型和目标指令的操作数类型是否匹配。类型不匹配时,编译器有时不会直接报类型错误,而是报一个莫名其妙的"数组下标必须为常量",迷惑性很强。

第三步,看是不是数组越界。如果你的下标变量在某个路径下可能超过声明范围,编译器在某些情况下解析会出错,虽然不常见,但遇到过。

第四步,实在定位不了,就把那行访问语句拆成两行:先temp := arr[i];再用temp参与后续计算。这个做法能让编译器不再需要推导动态地址,绝大多数报错都能通过这个土办法掩盖过去——注意我说的是"掩盖",排查路径还是要走前三步,否则治标不治本。

这不仅是语法问题,还是一个调试思路问题。ST语言报错信息来源有限,不像C语言那种能给出很精细的提示,你要学会通过调整写法去反推编译器哪里不满意。

5.2 下标越界的隐藏行为:很多机型不报错,直接读脏数据

数组下标越界在任何语言里都是大忌,但PLC上的表现比PC上隐蔽得多。在C语言里越界大概率会段错误、崩进程,在ST里运行期越界时,三菱很多机型并不会第一时间给你CPU异常报警,而是直接读取相邻标签所在的内存区域。程序照样扫描,逻辑照样执行,只是数据来源是你从未预料到的"隔壁邻居"。

我遇到过一次很诡异的故障:一个轴的速度参数在循环里偶尔变成几千,排查了两天才发现是配方索引在某些边界条件下变成了 -1,负下标读到了结构体数组前一个实例的数据区。程序没报错,数据全错。

现在我的做法是在关键访问点加显式边界判断:

IF iAxis >= 0 AND iAxis <= cAxisCount - 1 THEN rSpeed := g_AxisParam[iAxis].SpeedMax; END_IF;

同时在上位机触摸屏端,所有配方选择框的索引范围做硬限幅。两层保险,才敢说把越界风险压到最低。PLC的ST环境不比高级语言,别指望运行时给你兜底。

5.3 断电保持区错位:换机后最阴间的数据事故

最后一个坑,是我个人认为换机型时最折磨人的:断电保持区错位。FX3U时代,D区有一部分默认是断电保持的,很多老程序靠这个特性让零点偏置、累计产量、配方参数在断电后自动存活。换到FX5U或者Q系列,保持区的默认范围变了,或者需要通过参数设置/标签保持属性显式指定,原来代码里"断电后数据还在"的隐性依赖一夜之间全部失效。

实际故障表现就是:客户断电重启后,设备的原点偏置变成0,累计产量清零,配方参数回到出厂默认,调试现场直接血压拉满。

对策其实不复杂,但要在换机前就做掉。把所有需要掉电保存的数据集中到一个专门的"保持型结构体"里,给它定义独立的保持型标签,显式设置保持属性,不依赖默认范围。上电初始化时用一个初始化标志加一个版本号字段来判断:如果标志不对,说明数据区是空的或者错位的,就主动装载默认值;如果标志正确,才直接用保持区数据。

这套做法在FX3U上也许显得多余,因为它的默认保持区很宽,但迁到FX5U后就是救命稻草。很多事故不是发生在换机当天,而是发生在换机后第一次断电重启,那才是真正检验迁移质量的时刻。


最后说一点个人体会。三菱的ST语言写起来并不难,难点从来都是回头想清楚"当初为什么把这一行写在这里"。我现在接手的每个新项目,都会先立一条规矩:业务代码里只允许出现标签和结构体,绝对软元件号只允许出现在IO映射和AT分配表里。这样做换机型虽然不是零改动,但至少能从"全重写"变成"改映射表和几处参数配置"。如果你正在跟这个标题同样的问题较劲,建议先别急着跟编译器掰扯,回头看看你的数组偏移到底散落在哪一层——多半会发现,真正的坑根本不在那对"常量"和"变量"里。

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

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

立即咨询