三菱PLC ST编程:从常量到枚举,告别写死代码

发布时间:2026/10/30 14:51:12
三菱PLC ST编程:从常量到枚举,告别写死代码

干了十来年三菱PLC,最近看到群里有人吐槽一个很有意思的标题:“三菱ST常量vs枚举,1700写死8处漏改3处”。

我一看就乐了,这简直是PLC程序员最经典的翻车现场之一。所谓“写死”,就是在ST语言、梯形图或者结构化文本里,直接把数字、编号这种“魔法数字”硬编码在逻辑里。比如IF DI1 = 1 THEN、REF[1700] := 5这样的代码,看着简洁,但改起来就是一场灾难。标题里说的“1700写死8处漏改3处”,大概率是有人在1000多行代码里用了8个同一个含义的原始数字常量,后来设备类型变了需要整体更换编号,结果程序里只改了5处,剩下3处漏网,导致设备死活不对——这种故障在调试现场非常典型,看起来像硬件坏了,其实全是软件里的老古董数字在作祟。

今天这篇文章就不绕弯子,专门把三菱ST语言里常量和枚举这两个概念掰开揉碎,讲讲为什么“写死”是万恶之源,以及怎么用常量和枚举把代码从“能用”提升到“好改”。内容会比较偏实战,适合正在用GX Works3开发FX5U、L系列和Q系列的小伙伴参考。

1. 先从痛点聊起:你写的程序为什么越来越难改

先问个特别基础的问题:你在写ST程序的时候,有没有直接用过“裸数字”?

我见过太多种写法的了。比如在三菱FX5U的ST程序里,有人会直接写:

IF D100 >= 5 THEN (* 这个5代表什么?没人知道 *) END_IF;

再比如,报警代码比较,设备状态判断,甚至轴号的选择,全都直接塞数字。这在一开始是没有问题的,写着还很快,因为不需要动脑子去定义名字。但是,当程序到1000行、2000行,当这个逻辑被维护半年、一年之后,那个“5”到底是“等待5秒”还是“模式5”,就只有天知道了。

1.1 为什么说“写死”的代码会越改越乱

在标题的场景里,“1700写死8处漏改3处”这种事故,核心原因就是数字散落各地,彼此没有任何关联。

举个例子,假设你在一台设备上用了气缸伸出位置的到位传感器。不同工位可能对应不同传感器地址编号,整套设备换了型号后,可能所有传感器的接线编号都变了。如果你在一百个地方直接写了IF X60 THEN,那么换型时你要一个个去找X60,找得到就改,找不到就漏。但是如果你用的是常量:

VAR CONSTANT CYL_A_AT_POS : WORD := 1600; (* 地址1600 *) END_VAR

程序里只需要引用CYL_A_AT_POS,那么换型的时候你就只有一个地方要改,那就是常量定义区。

“写死”最可怕的地方在于:它不是改起来难,而是“漏改”了你根本发现不了。改错数字,程序编译可能会报错。漏改数字,程序编译不会报错,但是设备动作就是不对,而且极难排查。

1.2 你以为你是在写程序,其实你是在埋雷

我们做一个很简单的推理。假如一个项目中有5个设备编号需要用到同一个“设备类型”数字0x03。代码里直接写IF DEV_CODE = 3 THEN,用了8次。突然有一天,你发现实际设备类型编号标准更新了,3要改成7,你会怎么做?

你会按下Ctrl+F查找“DEV_CODE”?可惜,你程序里根本没这个名字。你只能查找“= 3”,但是结果会让你崩溃:程序里还有大量的D[3]、#3、TIMER_3、M3等无关结果。你在一片噪声里找了半天,改了其中8个里的5个,第6个在子程序里你没看见,第7个在第2个功能块里,第8个在FB的局部注释底下藏着。

最后的结果就是标题说的:1700行代码,8处写死,漏改3处,设备彻底乱套。

所以,“写死”这个问题的本质不是代码量,而是可搜索性和唯一性。常量解决的就是这两个问题:程序里出现的名字是唯一的,你搜索CYL_A_AT_POS一搜一个准,绝不会搜到无关的“1600”。

2. 三菱ST语言里的常量:最朴素的解药

2.1 GX Works3里常量怎么定义

在三菱ST语言(GX Works3)中,常量(Constant)的定义非常明确,分为“全局常量”和“局部常量”。

在全局标签编辑器里,你可以直接建一个标签,把“数据类型”定义为“CONSTANT”或使用“VAR CONSTANT”语法。在ST程序里,最常用的写法是在变量声明区:

VAR CONSTANT C_MAX_SPEED : REAL := 60.5; C_NODE_ID : WORD := 5; C_ALARM_TIME : TIME := T#5S; END_VAR

这里的关键词是VAR CONSTANT ... END_VAR。定义了之后,程序正文里只能引用,不能再赋值。如果你不小心写了C_NODE_ID := 10;,编译系统会直接报错“表达式必须含有常量值”之类的提示。

2.2 定义常量的三个潜规则

实际工程中,我建议遵循三条规则:

第一,常量名要有明确前缀。我习惯用C_开头,因为后面还要配合全局标签和局部标签,用一个统一前缀能在搜索的时候减少噪声。否则你搜“SPEED”能搜出几十个变量。

第二,单位要写在注释里。三菱的ST变量注释虽然功能简单,但足够好用。在常量后面加上单位(* mm/s *),能避免以后维护的人猜来猜去。

第三,常量要集中放在一起。尽量把所有工程相关的常量放在同一个GTD全局标签的结构化分组里,而不是散在若干个FB里。这样设计变更时,打开标签表,一眼就能看到哪些是需要改的。

2.3 常量的致命短板:改了数值,但改不断逻辑关系

有一个非常容易踩的坑:常量虽然在“搜索”和“唯一性”上比裸数字好,但它本质上还是一个“共享变化值”。什么意思?

还是举例。你定义了:

VAR CONSTANT C_STATE_IDLE : WORD := 0; C_STATE_RUN : WORD := 1; C_STATE_STOP : WORD := 2; END_VAR

然后在ST里写IF CurrentState = C_STATE_RUN THEN,看起来很完美。但是,如果你某一天手滑把C_STATE_RUN的数值改成3,那么所有引用它的地方都会跟着变成3。可怕的是,程序编译不会报错,因为3也是合法的WORD值。

也就是说,常量能防“漏改”,但防不了“手工篡改”和“误赋值”。

再说一个更尴尬的场景:假设你有8个不同的状态量,它们都用WORD类型存着,你都用常量给它们起了名字。但有一天你要在设备上增加第9个状态,你发现所有常量都在一个池子里,没有约束。你的CurrentState变量既可以等于C_STATE_RUN,也可以等于C_STATE_STOP,编译器根本不关心它们各自是什么。

这就引入了枚举——它才是真正的类型安全工具。

3. 枚举:把“允许的取值”锁进盒子里

3.1 枚举是干什么的

“枚举”这个词其实很直白,就是把一组相关的常量“收集”成一个类型。你在ST里定义了一个枚举类型,后续凡是声明为这个类型的变量,就只能在这个列表里取值。超出列表的值,编译器直接拒绝。

在三菱GX Works3里,枚举类型是通过“全局标签”或者“共用标签”里的“数据类型”创建来实现的。操作路径大概是:工程项目 → 全局标签 → 数据类型 → 新建 → 选择“枚举类型(ENUM)”。

举例,我们可以创建一个名为MODE_TYPE的枚举:

TYPE MODE_TYPE : ( MODE_MANUAL, (* 手动 *) MODE_AUTO, (* 自动 *) MODE_STEP, (* 步进 *) MODE_RECOVER (* 复位恢复 *) ); END_TYPE

定义之后,你在FB或者程序里就可以声明一个变量:

VAR CurrentMode : MODE_TYPE; END_VAR

然后你可以赋值:

CurrentMode := MODE_AUTO;

但如果你试图写CurrentMode := 3;,编译就会报错,因为3不是MODE_TYPE类型。

3.2 枚举和常量,到底在内核上差在哪

这就要说到标题的核心了。“常量 vs 枚举”,都是一组名字对应一组数字,但它们的本质区别在于类型契约。

常量是“全世界都可以用”的裸值。它没有一个“类型”作为容器,IDE不知道你到底该把哪个常量给哪个变量。枚举则自带类型约束,你的变量一旦声明成某个枚举类型,编辑器就会在编译阶段帮你把关。

用一句大白话说:

  • 常量防的是“找不到”,但防不了“用错”。
  • 枚举防的是“用错”,把错误拦截在编译期。

比如你在ST里有1700行代码,其中8处直接写了“4”这个数字,它们可能有的含义是“模式4”,有的是“第4号轴”,有的是“4mm/s”。这种情况下用常量,你只是把“4”变成了MODE_4、AXIS_4、SPEED_4,从搜索角度来说方便了,但你还是有可能把AXIS_4误赋给一个Mode变量。而用枚举,AXIS_4的变量类型是AXIS_NO_ENUM,Mode变量是MODE_TYPE,它们之间根本没有赋值通道,你连错误的机会都没有。

3.3 三菱ST枚举的三点实战经验

第一,别把枚举值直接和整型做算术运算。GX Works3对枚举类型的算术支持比较弱。不能直接写CurrentMode := CurrentMode + 1;来切换下一个模式。如果你要循环切换,建议加一个转换函数,或者定义一个WORD辅助变量替代。

第二,枚举在HMI关联时要小心。人机界面上的下拉列表通常需要整数编号。你在触摸屏上选择的“模式1”最终可能会以WORD类型的数值发给PLC。这时可以在PLC内部做一个映射:

IF RawModeWord = 0 THEN CurrentMode := MODE_MANUAL; ELSIF RawModeWord = 1 THEN CurrentMode := MODE_AUTO; END_IF;

这样可以避免在HMI上直接“裸写”枚举编号。

第三,枚举定义的位置很关键。我强烈建议在全局标签里定义枚举数据类型,而不是在FB内部定义。因为如果你在功能块里定义的类型,外部程序是无法用来声明变量的,将来无法做统一管理和扩展。全局定义了,别的FB和主程序才能共同使用,多人协作时大家的基础类型才会统一。

4. 光会用还不够,你得会选:常量还是枚举

4.1 一张表说清怎么选型

每次做内部培训时,我都会画一张类似的表,这次直接分享出来。

维度裸数字(魔法数)常量枚举
查找修改极难,噪声多容易,集中改动容易,集中改动
防漏改完全没有基本可以,有命名提醒完全可以
防止错误赋值完全没有较弱(类型还是INT/WORD)极强(类型锁死)
编译检查能力无无约束力强约束力
HMI通信友好度很高较高中等(需手动转换)
适合场景临时脚本、快速调试全局配置、传感器编号、固定参数模式、状态、报警码、类型标识
代码阅读体验很差一般偏上极佳

从这张表能看出来,常量适合“这个数字是个物理属性”的场景,比如速度上限、轴脉冲数、设备地址编号(传感器接点地址)。枚举适合“这个数字是个分类/模式/状态”的场景,比如手动、自动、回原点、异常,报警类型A、B、C。

4.2 实际工程里我会怎么做

以三菱FX5U的项目为例。我一般会在全局标签区做三组定义:

第一组,物理性常量用VAR CONSTANT,直接定义系统参数和设备连接编号。比如:

VAR CONSTANT C_FEED_SPEED_1 : REAL := 500.0; (* mm/min *) C_SETTLE_TIME : TIME := T#500MS; C_CONV_ADDR_MAIN : WORD := 1; END_VAR

第二组,设备状态逻辑用枚举类型。我会在全局标签里新建枚举STATION_STATE,包含STS_INIT、STS_READY、STS_BUSY、STS_DONE、STS_ALARM。

第三组,报警码用另一个枚举ALARM_CODE,包含ALM_ESTOP、ALM_OVERRUN、ALM_MAINTENANCE等。

这样整套程序的“数字”全部都退居幕后,程序正文里只看到语义化的名字。后期如果要修改编号规则,只动对应枚举的赋值就好,几乎是改一处完成全部。

4.3 什么时候可以反向操作:用常量给枚举做底层数值

有一种高级玩法,推荐给有一定基础的工程师。

在GX Works3中,BCL(三菱MELSEC的ST)的枚举类型定义通常是不带显式数值的,只靠顺序。但在某些需要把枚举值直接映射到HMI或者通讯协议的项目里,就得用到“常量+枚举”的组合。

做法是定义一个常量表:

VAR CONSTANT C_MODE_MANUAL : WORD := 16#0100; C_MODE_AUTO : WORD := 16#0200; END_VAR

然后在程序中把通讯原始值(WORD)映射到枚举:

IF RawMode = C_MODE_AUTO THEN CurrentMode := MODE_AUTO; END_IF;

这种做法的好处是:协议上的编号和程序逻辑解耦。如果通讯协议升级了,编号变了,只改C_MODE_XXXX这一处,程序逻辑全部不变。而CurrentMode继续享受枚举的类型安全检查。

5. 实操复盘:从1700行代码里挖出“8处写死”并改造

5.1 第一步:先全面盘查,找出所有魔法数字

拿到旧的ST代码,不要急着写新代码。先老老实实用查找工具,把程序里所有的数字量筛一遍。

重点查两类:

  • IF xx = 数字 THEN这种比较逻辑
  • xx := 数字;这种赋值逻辑

F3快捷键在GX Works3的ST编辑器里是查找。查找时先查一些常见的小数字:0、1、2、3、4、5、8、16。这些数字是重灾区。

另外要查16#前缀的十六进制数,比如16#03这种往往是通讯协议、报警码、硬件地址。

我一般会建一个Excel表格,把行号、代码、所在程序块、疑似含义列出来。防止后面改乱。

5.2 第二步:按“物理参数”和“状态类别”归类命名

查出来的数字,不要急着全部替换。先分两个篮子:

  • 物理参数类(速度、时间、距离、地址编号)→ 定义为常量。
  • 状态、模式、报警类型、设备动作类型 → 定义为枚举。

这一步非常关键。如果分错,后面会出现“拿枚举做参数运算”的尴尬。

比如程序里有MODE := 3,这明显是“模式”含义,命名成MODE_AUTO,走枚举。如果是D100 := 30000,30000是脉冲频率,定义成常量C_PULSE_HIGH。

5.3 第三步:分批替换,一次只改一个FB

替换数字是有风险的。如果是1600行代码一次性全部替换,编译报错会把你淹没。我习惯把涉及的数字按功能块分批处理。

操作顺序:

  1. 先把全局常量和枚举类型定义好。
  2. 编译一次,确保类型定义无误。
  3. 然后逐个打开功能块,把IF X60改成IF C_SENSOR_AT_POS。
  4. 每改完一个FB,编译一次。
  5. 所有FB都改完后,用Ctrl+F再搜一次原来的数字,确认没有遗漏。

替换过程中最需要注意的是“数字是不是被用到了数组下标”。比如REF[5]、ARRAY_DATA[2]这种,如果5代表第5号配方,我建议把它定义成常量C_RECIPE_INDEX_5,而不是枚举,因为数组下标本质上还是整数运算。

5.4 第四步:把“黑话”注释补上,防住未来的自己

改造完不是万事大吉。在整个项目的关键函数入口、状态机切换处,务必补上一段注释。三菱ST支持(* ... *)注释,写清楚这段逻辑用的“状态枚举”到底名字是什么。

我见过有人定义了枚举,然而注释还是写的“(* 模式 3 *)”,这就等于白弄。注释要和代码里语义化名字完全一致,不能脱节。

这段改造过程,我自己实测大概1900行代码,差不多一个下午可以全部改完。花时间最多的是查数字而不是改数字。只要前期规划命名合理,中期逐个FB编译,后期基本不会出幺蛾子。

6. 常见问题与排查技巧实录

6.1 编译报错“表达式必须含有常量值”

这个报错我在GX Works3中见得太多了。通常出现在两种情况:

一是你把常量赋值给其他变量,然后试图在数组下标里用那个变量。数组下标必须是编译期可确定的常量值。比如:

C_INDEX : WORD := 5; ARRAY_DATA[C_INDEX] := 100; (* 如果C_INDEX是常量,可能可以通过 *)

但如果C_INDEX是普通变量,这行就报“表达式必须含有常量值”。

解决方法是:要么用VAR CONSTANT定义真正的常量,要么先把变量值复制到一个固定变量,再用固定变量赋值。

二是使用枚举类型时,尝试直接用数字给枚举变量赋值。同样的:“表达式必须含有常量值”。

正确写法是:

CurrentMode := MODE_AUTO; (* 不能用 CurrentMode := 1 *)

6.2 枚举变量莫名其妙变成“未知值”

有时候你从HMI或者通信收到一个整数,经过映射写入枚举变量,但程序运行中发现枚举变量的值不在枚举列表里。

调试的时候,不要只盯着枚举变量本身。要看原始WORD值。GX Works3里可以在“标签监视”窗口把枚举类型临时修改为WORD类型(如果允许的话),或者另外添加一个WORD辅助变量来监视原始值。

这个问题的根源多半是HMI下拉列表的索引和你枚举顺序没对上。你枚举是0开始,HMI那个控件可能是1开始,直接错位。

对策很简单:在HMI和PLC之间定义一个人机接口专用WORD变量,做枚举和WORD的显式转换。不要依赖HMI直接写枚举。

6.3 删除魔法数字后,触摸屏无法联动

很多人把PLC内部数字全部语义化之后,发现触摸屏本来能直接读写D区数字,现在没法直接读写“枚举”了。

因为GX Works3的标记监控虽然能看到枚举类型,但实际外部通讯(如MODBUS/TCP、三菱MC协议)看到的是一个16位整数地址。你在触摸屏上的“数值显示”控件需要指定数据地址,比如D100,然后它读回来一个整数。

这时PLC内要做反向映射:

RawValue := CurrentMode; (* 可以隐式转换吗?要看具体型号 *)

如果编译器不让你直接把枚举传给WORD,可以用辅助变量:

RawValue := WORD_ENUM_TO_WORD(CurrentMode);

或者自己写一个简单选择分支。

注意,如果你在GX Works3里建的枚举类型占用的字大小需要配置正确,否则可能会出现地址错位。FX5U里枚举类型在标签表上默认占用一个字(16位),一般够用。

6.4 组态软件报警里看不到枚举名

最后一个小坑:你用枚举定义了几十个报警码,但在三菱GX Works3的报警监控或者GT Designer的报警浏览中,可能只能显示整数编号,看不到对应的文本。

因为报警文本是需要单独在GOT画面的系统报警里配置的,它不会自动读取你在PLC里建的枚举类型名称。

如果要在HMI上显示“手动模式启动中”这种文本,你需要单独在GT Designer的“报警”选项卡里,把数值和文本一一对应。这个映射关系最好和PLC里的枚举顺序完全一致,否则将来维护时对不上号,很麻烦。

我个人的建议是,快速维护型的报警可以直接用枚举,但HMI的报警配置仍然要走一遍标准流程;两个文件的一致性靠注释和版本管理来保证。

7. 最后的工程经验分享

说了这么多,回归到标题那台1700行的设备上。8处写死漏改3处,这种问题说大不大,说小不小,但它暴露的是整个工作流中缺少“抽象”的环节。我们写ST代码,本质上是在给设备造一套数字规则,但数字本身没有意义,只有它们背后的“模式”“状态”“参数”才有意义。

用常量和枚举,不是为了让代码变得花哨,而是为了让错误尽早爆发。写死在设备上的数字,往往要等设备动起来才能发现错了;写死在编译器的类型不对,当场就报错,你根本不需要上电。把一部分调试时间省下来,去做设备工艺优化,不是更好吗?

我的习惯是,从项目第一天就用枚举建状态,用常量建参数。一天两天看不出差别,项目超过三个月、代码超过一千行的时候,维护成本差别会越来越明显。实测下来,这种习惯能帮我把调试时间压缩30%以上,而且接手项目的人基本不会再打电话问我“这个5代表什么意思”。

最后再分享一个小技巧:每次你给PLC程序做常量/枚举改造时,顺手把Excel版本的变量映射表导出来,归档在项目文件夹里。半年后你要改设备编号,根本不用打开电脑屏伸手去翻代码,直接在Excel里查好语义名,再去GX Works3里搜那个名字,一种全项目都能改干净。这套组合拳打下来,“1700写死8处漏改3处”这种事情,会离你越来越远。