三菱ST编程避开裸数字陷阱:用常量与枚举重构PLC代码 去年改一套三菱FX5U的ST程序差点被三个数字拖进加班的泥潭。项目本身不复杂一套设备模式切换逻辑ST程序段满打满算1700行但里面到处是写死的模式数字模式1、模式2、模式3散落在不同的FB和CASE分支里粗看下来至少有8处。客户要求把原来的四档模式扩展成六档模式编码整体往后顺延我一开始真觉得这是小改搜一下、改一下、下载完事。结果现场试机时还是冒出来3处没改到老逻辑拿着旧数字去判断新状态动作完全对不上。这篇文章就是把我这次“常量还是枚举”的翻车经过和补救方法摊开聊一聊希望你在写三菱ST时别重蹈覆辙。1. 事故复盘1700行里的8处写死数字1.1 那些散落的“裸数字”到底长什么样当时的程序里有好几个带编号的变量比如模式变量iMode、配方变量iRecipeNo、报警代码变量iAlarmCode它们都是INT类型判断和赋值直接上数字。随手能看到的写法大致是下面这些IF iMode 1 THEN // 自动模式动作 END_IF; CASE iMode OF 1: // 自动模式 2: // 手动模式 3: // 远程模式 END_CASE; iMode : 2;表面上每个数字都很好理解写的时候也快敲个1就完事。但问题是这套程序不是只有一个人在维护过两个月客户提需求要扩展模式时你就得从1700行里把所有和“模式”相关的1、2、3找出来。更麻烦的是程序里不只有模式用数字配方编号、报警代码、气缸工位号也都在用数字你搜索一个2出来的结果可能一半是配方、一半是报警、一半是时间参数真正属于模式的反而混在里面。1.2 漏改的3处问题为什么没在编译时暴露我当时第一轮改得还算顺利能看到的1、2、3基本都改成了新值。漏掉的是3个不容易被搜索命中的位置一个藏在CASE语句的某个分支里因为那个2:和另一个装配步骤判断的2:紧挨着翻页的时候压根没注意到一个写在某个FB的输出参数初始化里iMode : 2;没有上下文被眼睛自动忽略还有一个是最隐蔽的发生在报警逻辑里iAlarmCode : 3;本来含义是“报警代码3”和模式编码一点关系都没有但当时被新方案的3改动晃了眼顺手把这里也当成模式扩展去理解结果逻辑错位。编译能过因为所有变量都还是INT数字和数字之间不存在类型问题。PLC也不会报错直到设备在某种状态下把模式值传给报警判断旧数字对应的含义彻底乱了才被现场动作暴露出来。这正是“写死数字”最坑的地方它在编译期是完全合法的“垃圾”只有运行时才翻车。1.3 这次事故的直接代价和教训代价说出来不大但很磨人程序下载重启设备重新跑流程试机到一半发现模式切换不按预期然后停下来查逻辑、定位问题、改代码、再下载、再验证一个原本半天能收工的变更硬生生拖到第二天。复盘的时候我画了一张“数字-含义”对照表发现所有散落的数字其实早就该被命名成一个符号。问题的核心不是改动力度大而是“裸数字”没有任何语义也没有任何编译约束变更时全靠肉眼和意志力去盯。2. 三菱ST中常量与枚举的正确打开方式2.1 常量定义至少让数字有个名字在GX Works3的ST语言里常量定义有几个位置可以用。最简单的做法是在FB或全局ST程序里用VAR CONSTANT声明局部常量VAR CONSTANT c_ModeAuto : INT : 1; c_ModeManual : INT : 2; c_ModeRemote : INT : 3; END_VAR也可以放在全局标签中把常量当成一个固定值的软元件标签来管理。我个人的习惯是真正跨FB使用的常量放到全局标签里统一维护只有单个FB内部用到的常量才写在VAR CONSTANT段里避免全局命名空间被塞满。常量带来的第一个好处是改名方便。c_ModeAuto这个名字出现8次你只需要改定义处的值剩下7处自动跟着生效这是相对于裸数字的绝对优势。第二个好处是可读性别人看代码看到IF iMode c_ModeAuto至少知道这里在比较“自动模式”而不是面对一个冰冷的1。但常量有个隐藏局限它本质上还是一个INT。你把c_ModeAuto传给一个配方参数编译器一样放行因为它俩都是INT。模式常量和配方常量在类型上没有任何区分所以一旦接口设计不合理还是会混用。2.2 枚举定义把一组数字装进一个独立类型枚举和常量的区别在于它不仅给数字起名字还把这一组名字包装成一个新的数据类型。三菱GX Works3里定义枚举有两种方式一种是在公共数据类型中图形化创建一种是在ST文本中直接写TYPE。文字定义大概是这样的TYPE E_Mode : ( MODE_IDLE : 0, MODE_AUTO : 1, MODE_MANUAL : 2, MODE_REMOTE : 3 ) : MODE_IDLE; END_TYPE定义好之后变量声明就可以用这个类型VAR iMode : E_Mode : MODE_IDLE; END_VAR很多做上位机或算法的朋友听到“枚举”第一反应是暴力枚举算法把候选值一个个遍历扫过去。ST里的枚举完全不是这个意思它是在语法层面规定一个变量只能取这一组符号值让状态量天然拥有“集合”的概念编译器也会把好这一关。2.3 常量与枚举的核心差异在哪里普通常量解决的是“数字没有名字”的问题枚举解决的是“不同种类的数字互相混淆”的问题。我后来总结过一张对比表对比项普通整数常量枚举类型定义方式VAR CONSTANT本质还是INTTYPE定义独立数据类型可读性有名字但可以和其他INT混用成员名自带集合语境类型检查不阻止模式常量传给配方参数类型不一致会被编译器提示漏改发现时间运行时逻辑分析才能发现修改接口/变量类型时提前暴露使用成本低旧代码容易改造稍高需要定义类型和调整接口之前那次事故里如果iMode是E_Mode类型的枚举变量当我尝试把iMode : 2写进去时编译器至少会提示数据类型不一致就逼着我把每一处和旧数字有关的逻辑重新过一遍。这正是枚举相比常量最值钱的地方它不只是一个命名工具还是一个约束工具。3. 把1700行里的8处写死改成枚举/常量的实操步骤3.1 先做一份“数字清单”而不是急着改代码任何重构的第一步都不是打开编辑器而是把散落在程序里的数字全部捞出来做成表格。我当时自己动手列了一个速查表格式大概是这样位置代码原文实际含义改造后写法Main_ST 第162行IF iMode 1 THEN自动模式判断IF iMode MODE_AUTO THENStep_FB 第88行CASE iMode OF 2:手动模式分支CASE iMode OF MODE_MANUAL:Alarm_FB 第218行iAlarmCode : 3;报警代码3与模式无关iAlarmCode : ALM_003;Recipe_FB 第76行iRecipeNo : 2;配方编号2与模式无关iRecipeNo : RECIPE_TYPE_B;Output_FB 第314行IF iMode 2 THEN手动模式联动输出IF iMode MODE_MANUAL THEN表格做好之后你会很直观地发现有些数字根本不是模式不能一刀切地全局替换。当时我就是没做这一步直接从编辑器里全局搜“2”结果把报警代码和配方编号也卷进了模式变更里制造了新的隐患。先列清单还有个好处改完一项划掉一项漏掉的东西会非常显眼。3.2 定义枚举类型并修改接口把清单做完接下来才是动手。我建议按这个顺序推进在公共数据类型里新建E_Mode枚举把模式成员全部列出来。把全局变量里iMode的类型从INT改成E_Mode。把FB的接口定义也一起改掉比如VAR_INPUT里如果原来接收INT模式参数就改成E_Mode。改FB内部的局部变量和逻辑判断。这时编译大概率会报出一批错误位置就集中在所有旧数字和类型不匹配的地方。比如IF iMode 1当iMode变成E_Mode后这个等式两边类型对不上编译器会给出提示。错误列表在这里反而是好事它就是一份自动生成的“漏改清单”。需要注意GX Works3不同版本对类型匹配的严格程度不完全一样有些版本对INT与枚举的比较只是Warning不是Error。我的建议是无论是Error还是Warning一律逐条看过去千万别在编译信息里看到Warning就直接Skip。3.3 修改顺序和替换技巧替换时不要一上来就大段大段改。我的做法是分三层走第一层先把类型定义和全局变量改完编译一次。这一层通过的标志是所有FB接口层面的错误被暴露出来。第二层逐个FB改内部逻辑。每改完一个FB单独编译一次保证这个FB的错误清零再进下一个。ST语言的FB之间引用关系很密一次全改完再编译错误列表会非常长反而容易搞混归属。第三层处理跨FB的参数传递。比如某个FB输出一个INT给另一个FB但那个FB的输入已经改成了E_Mode这时就需要在中间做显式转换或者把源头变量的类型也统一改成枚举。宁可多花几分钟把类型理顺也别在接口处放一个强转的临时变量糊弄过去。3.4 下载前用交叉引用做最后一遍检查代码改完、编译清零后不代表就能直接下载。我还会做两件收尾工作。第一件打开GX Works3里的标签引用列表或交叉引用功能找到E_Mode的所有使用位置逐条和前面做的“数字清单”对照确认没有清单之外的地方被误改也没有漏改的地方还残留旧数字。第二件在仿真或离线状态下把设备模式全部走一遍覆盖每个模式分支确认枚举值对应的动作和旧版本完全一致。这一步花的时间其实不多但能挡掉绝大多数低级错误。尤其是枚举值重排过顺序、成员默认值发生变化的时候交叉引用列表就是最后一道安全网。4. 枚举也不是万能药这些坑要提前知道4.1 与整型指令混用时的麻烦前面夸了枚举很多但它不是拿来就能随便用。三菱ST里不少指令和功能块的参数类型是ANY或者只接受基本数据类型枚举变量不一定能直接往里塞。比如某些MOV指令从枚举变量取值或者往枚举变量写值不同固件版本表现不完全一致。遇到这种情况我的做法是准备一个显式的转换变量比如VAR iModeCode : INT; END_VAR在需要和整型指令交互的地方先把枚举变量赋给一个INT变量再传出去接收外部整型数据后再做反向的映射。虽然多写了一两行但程序意图清楚也不会和枚举的类型约束打架。另外三菱ST的CASE语句有一个典型报错“表达式必须含有常量值”。这是因为CASE分支的标签必须是编译期就能确定的常量表达式不能是运行时可变的变量。比如你写CASE iMode OF iTemp: END_CASE;编译器直接拒绝。但恰恰是这一点让CASE语句和常量/枚举天然配合得很好因为你本来就应该用常量符号或者枚举成员去写分支而不是用变量。4.2 在线修改枚举类型要谨慎GX Works3支持在线修改程序但修改枚举类型定义会比修改普通指令风险更高。如果你改了某个枚举的成员值顺序或者删掉了某个成员运行中的PLC里已经保存的变量数据可能对不上号出现当前值指向错误成员的情况。不同型号和固件对在线修改数据类型的支持程度也不一样稳妥的方案是改了枚举定义尽量安排一次程序下载和设备复位不要贪图在线修改的便利。另外FX3U这类老型号如果是在GX Works2的结构化工程里使用ST对枚举的支持远不如GX Works3里的FX5U和iQ-R系列那么完整。项目选型阶段就要确认平台支持到什么程度实在不支持的退而求其次用全局常量命名规范也好过硬编码裸数字。4.3 枚举命名的几个小建议枚举成员的命名建议统一加前缀比如MODE_、ALM_、RECIPE_避免多个枚举类型出现同名的AUTO或IDLE在使用时产生混淆。成员值不要随手重排新需求增加的成员放在末尾维持历史的数值映射关系。最后在类型定义处写一段注释说明这个枚举的取值范围和扩展规则比如“本枚举用于模式选择新增模式时从5开始追加禁止修改已有成员的值”。这些注释在三个月后回头看比任何代码都值钱。5. 日常养成的几条保命习惯5.1 新代码里禁止裸数字那次事故之后我给自己的代码规范加了一条硬性要求凡是代表模式、配方、报警、步骤等“状态语义”的数字一律不允许直接写在IF、CASE、赋值语句里。就算一时没时间定义枚举也至少先用VAR CONSTANT写一个符号。写代码的时候看似多花了几秒但省下的是以后成千上万次搜索核对的时间。在FB的头部注释里我会放一小段“数字规范”比如“本FB内所有模式判断使用E_Mode枚举报警代码使用ALM_常量禁止直接使用字面量”。这个规范在团队协作里尤其重要因为别人接手你的程序时如果没有这段约束很容易又回到写裸数字的老路上。5.2 变更先改字典再改逻辑我现在处理任何涉及数字变更的需求第一件事都是打开那张“数字-含义”速查表把新旧映射关系写好再动手改代码。这个速查表可以是Excel也可以是程序文件开头的注释块。没有字典就动手改等于蒙着眼睛拆弹漏改的概率和我们这次一样高。字典里不只有模式数字连配方编号、报警代码、气缸工位编号、时间参数都列在一起。每次改完一项就在旁边打个勾。这张表后来成了项目维护的核心资产尤其是客户过半年再来一版扩展需求的时候我基本不用重新在整个程序里考古。5.3 搜索和交叉引用要配合作战有人觉得GX Works3的搜索功能不如电脑上的高级IDE好用这是事实但搜索本身也有技巧。搜数字时别只搜一个2那样会把配方、报警、时间参数全捞出来。尽量搜 2、: 2、: 2这种带语法上下文的写法结果会干净很多。更重要的一步是使用标签引用功能把iMode、MODE_AUTO的引用位置一次性列出来再逐条核对比肉眼在全文里翻找可靠得多。在线调试时枚举类型还有一个额外优势监视窗口直接显示成员名。你看到iMode MODE_MANUAL比看到iMode 2舒服多了至少不用每次去翻速查表回想2到底代表什么。那次事故后我养成的最后一个习惯是重构时小步走。不要指望一次性把1700行里所有裸数字全部改成枚举那是大工程风险也高。挑最核心的模式变量先改编译、下载、验证确认没问题后再处理下一个变量。每一步都保留一个可以回退的版本这样的节奏虽然慢一点但稳得多。如果你也正打算把手头三菱ST程序里的魔法数字改成常量或枚举建议就从最常用的那个变量开始试一次你会感受到编译错误列表变成漏改清单的那种踏实感。