首页 / 资讯中心 / 文章详情

三菱ST编程:常量与枚举如何终结写死值噩梦

三菱ST编程:常量与枚举如何终结写死值噩梦 ★ FEATURED ARTICLE
前阵子帮一个客户收拾一台老设备程序是别人写的三菱FX5UGX Works3环境结构化文本ST大概1700行。设备本身是台十二工位转盘式灌装机运行两年多没大改过这次要新增一个配方规格我打开程序一看血压直接上来了——里面直接写死的数值一堆一堆的什么K100、16#64、D300后面跟的索引下标全是裸数字。更要命的是我把一个关键模式号从100改成120之后全文搜了一遍以为改干净了结果调试的时候才发现1700行程序里实际写着这个值的地方有十几处我漏改了8处其中3处是真真切切在设备上踩了雷。这篇文章就把这个过程完整复盘一下重点聊聊三菱ST里常量vs枚举到底怎么用、为什么写死的值这么难改干净、以及我后来摸索出来的一套防漏改机制。搞三菱PLC的同行尤其是用ST写逻辑的这篇应该能帮你少走不少弯路。1. 一个搜干净了却依然报错的魔幻现场1.1 这个程序当初是怎么变成写死值重灾区的先看看这1700行ST到底是个什么结构。这台灌装机分成上瓶、灌装、封盖、贴标、下料几个工位工位之间用凸轮主轴联动每个工位有一段独立的ST程序块再加上配方管理、报警汇总、统计计数几个功能块。整套程序大概是这么分布的主程序MAIN300行左右负责流程状态机和工位调用配方加载FB250行从触摸屏读配方号然后把对应参数写进工作寄存器报警汇总FB200行收集各工位报警映射到HMI报警编号各工位动作逻辑1200行左右全是气缸、电机、阀岛的动作序列这种结构其实很典型问题出在配方加载和报警映射这两块。配方里有个模式号参数全设备共用原工程师不知道从哪个老项目里拷过来的习惯直接用数值字面量比较IF RecipeNo 100 THEN // 启动模式A ELSIF RecipeNo 101 THEN // 启动模式B END_IF;然后报警映射那边也全是字面量IF AlarmCode 200 THEN AlarmDisp : 3001; // HMI报警号 END_IF;你把程序往下翻会发现这种写法到处都是。为什么大概率是当时赶工期先在线上把值试出来想着回头再整理结果一回头就是两年。1.2 同一个数值在ST程序里能有多少种长相这次踩雷的核心原因就是搜索替换的时候忽略了同一个值在不同上下文里的不同写法。三菱ST虽然不像C语言那样有#define但数值字面量的写法非常灵活尤其是常量在GX Works3里可以有多种存储形式。我这次项目里同一个100程序里至少出现了6种写法写法出现位置我当时搜到了没K100比较指令、MOV指令搜到了100ST算术表达式x : 100搜到了16#64报警代码比较漏了100.0浮点比较比如计时器设定漏了100作为数组下标Data[100]这种索引写法漏了HMI脚本里的100GOT画面宏脚本不在ST里漏了这就是写死值最恶心的地方。你以为你用的是同一个逻辑值但在程序里它可以是整数、是十六进制、是浮点、是数组索引甚至跑到触摸屏脚本里成了另一个独立副本。等你意识到要改的时候光靠眼睛找根本不现实。1.3 设备端的三个看上去正常的假象为什么漏改了8处当时没爆出来因为设备并不是立刻全崩而是以一种看起来正常但细节不对劲的方式在运行这种最坑。第一假象设备能跑。模式号漏改的那几处因为原来的100和新的120之间有部分重叠某些工位还是能动作只是动作组合和预期不一样。比如灌装工位应该用新配方的高速参数结果还在走老配方的低速参数产量上不去但你没察觉是模式号的问题。第二假象报警不报错。PLC没有报编译错误因为数值字面量之间做比较根本没有语法问题。三菱的ST编译器不会因为你用了K100就提醒你这里有个写死的值。第三假象触摸屏显示正常。部分报警编号映射漏改了但HMI上已有的报警记录还在你看着报警列表觉得还挺全实际上新加的那几个报警点根本没接上。2. CONSTANT常量与伪枚举三菱ST里替代写死值的两条路线2.1 常量声明GX Works3的ST语法与适用边界先说正路。三菱GX Works3的ST语言是支持常量声明的最常见的写在POU的声明区里格式长这样VAR CONSTANT cModeStart : INT : 100; cModeChange : INT : 101; cAlarmFill : INT : 200; cAlarmCap : INT : 201; cHmiAlarmBase : INT : 3000; END_VAR也可以直接在全局标签里建一个标签类型选INT勾上常量复选框这样所有POU都能直接引用不用到处声明。常量声明的好处不用多说你只要改cModeStart这个定义程序里所有用cModeStart的地方自动跟着变。但这里有个边界你得知道常量只解决值改了不用到处改的问题不解决这个值和那个值属于同一类的问题。换句话说常量能消灭魔法数字但没办法把模式号100和报警号200这两类东西在类型上区分开。实际用它的时候我还有个体会命名规范比声明本身重要得多。你光是声明了C100这种常量名跟写死K100没本质区别因为你根本不知道C100是干嘛的。我现在的习惯是常量名必须带类型前缀和业务语义前缀比如cMode,cAlarm,cRecipe这种。2.2 没有原生enum怎么办用常量组模拟枚举的两种手法聊到枚举就得说实话三菱ST语言本身没有像C语言或高级语言那样原生的ENUM类型。你翻遍GX Works3的帮助文档找不到TYPE ... ENUM这种定义。所以严格意义上标题里的枚举在ST里玩的是模拟。我常用的模拟方式有两种。第一种是常量分组法。思路很简单把同一业务域的一组相关常量放在一起用统一前缀区分靠命名把这是一个枚举集合这个信息传达出来VAR CONSTANT // 灌装模式枚举 eFillMode_Stop : INT : 0; eFillMode_Manual : INT : 1; eFillMode_AutoFast : INT : 2; eFillMode_AutoSlow : INT : 3; // 灌装头状态枚举 eHeadState_Idle : INT : 0; eHeadState_Filling : INT : 1; eHeadState_Sealing : INT : 2; END_VAR注意看前缀eFillMode_和eHeadState_等于手工实现了两个枚举集合。美中不足的是三菱不会真的检查你是不是把一个FillMode的值赋给了HeadState变量因为底层都是INT。也就是说类型安全性还是得靠人肉保证。第二种是结构体打包法组织性更好TYPE stFillMode : STRUCT Stop : INT : 0; Manual : INT : 1; AutoFast : INT : 2; AutoSlow : INT : 3; END_STRUCT END_TYPE VAR FillMode : stFillMode; END_VAR然后程序里就用FillMode.AutoFast这种形式访问。好处是有了命名空间看起来更像真枚举坏处是每次引用都写一长串代码会显得啰嗦而且结构体成员不是常量如果你不小心在逻辑里给它赋值还会被改掉。所以我个人还是更推荐第一种常量分组法简单直接维护成本低。2.3 常量与枚举到底怎么选一张判断表经常有人问我既然ST没有真枚举那到底是全用常量还是硬凑一套枚举出来我的判断标准看三条判断维度用常量用枚举模拟值本身有没有业务含义只有单一数值含义如最大量程、基础偏移一组同类的状态/模式如运行模式、气缸位置集合是否固定量程、时间、系数这类可能随工艺调整状态机状态、报警类别这类相对固定是否经常出现在比较逻辑里偶尔出现频繁在CASE、IF条件里互相比较具体到我这个项目灌装模式、工位状态、报警类型、配方编号都是典型的枚举场景用常量组模拟而主轴速度上限、灌装量补偿系数、延时时间这种就是纯常量直接VAR CONSTANT声明就行。这里还有个重要原则宁可全都用常量组模拟枚举也不要在逻辑里留一个裸数字。裸数字 危险品。哪怕你声明一个cDummy : 0然后在下面写IF x cDummy都比直接写IF x 0安全因为至少下次搜索cDummy能搜到所有用它的地方。3. 8处漏改、3处踩雷一次完整的排查复盘3.1 第一轮搜索我只搜了K100漏了16#64当时我接到的任务是把模式号100改成120。我自认为抓到了重点直接在GX Works3的软件自带的查找功能里搜100然后逐个手工确认改了5处。当时我还挺得意因为这5处确实都是模式号比较改完编译无错误下载到PLC设备也能正常运转。结果第二天客户打电话来说灌装工位速度不对。我远程一看才发现PLC里同时存在IF RecipeNo 100我以为改掉了和IF RecipeNo 16#64这个我一直没注意到。在ST里16#64就是十进制的100三菱的ST编译器完全认这个语法而且因为你是在ST的表达式里写的它连转换警告都不会给。两段代码逻辑一模一样一个是新值120一个是旧值100设备到底走哪条分支取决于你先碰哪个条件。这第一轮漏改的根源就是我用自己的想当然代替了穷举所有可能的数值表示形式。十六进制、十进制、二进制前缀写法在ST里都是合法的整数字面量你必须承认同一个逻辑值可能有多种物理写法。3.2 第二轮排查从报警记录反推漏网点找到第一处问题之后我不再信任肉眼搜索了改用从现象反推数值的办法。具体怎么做的我把HMI报警历史调出来找出最近三天所有触发过的报警编号然后回到ST程序里搜这些编号对应的字面量一个一个比。这一比不要紧又揪出来好几处报警映射FB里有IF AlarmCode 200200没搜因为它是报警号不是模式号配方加载FB里有ArrayIndex : 100把模式号当数组下标用还有一个报警号3001在HMI脚本里用16#7D1写了这个居然是同一个意义这就是第二轮的教训你排查漏网之鱼的时候不能只盯着你这次要改的那个值而得从这个值在系统里承担什么角色入手。模式号100它同时也是配方数组的索引也是若干报警条件里的一个判断源还是HMI显示配方编号的输入变量。同一个业务值在不同层级的程序里以不同形式存在。3.3 三处踩雷分别在什么环节模式选择、报警映射、数据下标8处漏改不是平均分布的最后真正在设备上出问题的就3处但每一处的坑法都不一样。第一处是模式选择分支。就是前面说的16#64那段典型的表现是设备会随机出现走了新配方但又混合了旧参数的情况因为新旧两个条件都能命中后命中的覆盖先命中的。这个最隐蔽因为它是间歇性的和生产顺序强相关。第二处是报警映射。报警编号200在报警FB里被写死导致新模式的报警一直映射不到HMI的报警显示区。设备异常停机时操作工根本看不到对应报警只能干瞪眼误以为设备没报错。第三处是数据下标。配方参数存在D寄存器数组里索引基准值是100。我把配方结构加了长度之后这个基准没跟着改导致新配方读取时索引偏移了8个寄存器。表面上看配方号能读进来但里面每个参数都错位了比如灌装时间读成了灌装温度。这三处分别代表了写死值最常出问题的三类场景流程分支、映射表、数组寻址。你排查的时候要对这三类格外敏感。3.4 为什么编译器没有拦住这三处错误这也是很多ST新手会问的问题三菱不是会做数据类型检查吗为什么16#64和K100混用不报错答案很简单因为这两个字面量在语义层面是同一个INT值编译器只看到左边是INT右边是INT比较合法它根本不管你俩是从哪个进制写出来的。你要是去搜索表达式必须含有常量值这种报错那是C语言或者部分嵌入式编译器会报的三菱ST的比较宽松数值字面量之间的比较只要类型匹配就放行。所以结论很扎心你不能指望编译器帮你抓魔法数字。写死值这个问题从诞生那一刻起就注定要用人肉纪律来兜底。这也是我后来下定决心重构这套逻辑的原因。4. 不靠细心靠机制从声明规范到交叉引用的防漏改手段4.1 命名规范常量名前缀怎么定定完就得执行经过这次事件我把这套程序的常量命名全部重排了。现在我在项目里推的规范是三段式c 业务域 语义 常量名具体格式VAR CONSTANT cMode_Start : INT : 100; // 模式启动 cMode_Change : INT : 101; // 模式切换 cAlarm_Fill : INT : 200; // 报警灌装异常 cAlarm_Cap : INT : 201; // 报警封盖异常 cIdx_RecipeBase : INT : 100; // 配方数组索引基准 END_VAR业务域我用三种Mode流程模式、Alarm报警、Idx索引/偏移。命名定下来之后全项目强制执行写ST的时候只要看到裸数字就停下来问一句这个值后面要不要改要改就声明成常量当场声明不要拖。这里我得承认规范这东西最难的不是定规则而是坚持。好在ST程序不像梯形图那样散代码块结构集中代码复查的时候一眼扫过去就能看到有没有裸数字。4.2 交叉引用不是摆设改值前必做的三件套GX Works3的软件里其实自带交叉引用功能很多人不知道或者不怎么用。我现在每次改一个数值固定走三件套流程定义处定位先找到这个常量/字面量最初的定义位置搞清楚它到底代表什么业务含义交叉引用全查右键交叉引用把所有引用这个值的位置列出来一个都不放过写入/读取双向确认不光看读取的地方还要看有没有哪个程序往这个变量里写值交叉引用界面能把所有引用点列成列表还能看到每个引用是在哪个POU、哪一行。这一步花不了两分钟但能避免90%的漏改。我这次如果当时老老实实点一下交叉引用而不是用软件的全局搜索可能早就看到那8处漏网之鱼了。4.3 文本编辑器的正则统计用计数代替目测对于ST源文件我还有一个很多人不知道的小技巧把ST程序导出成文本文件用VS Code或者Notepad打开用正则统计某个数值出现的次数。比如搜\b(?:100|16#64|1100100b)\b一条正则就能把一个数值的所有进制形态一把抓出来。搜索之前先用区分大小写全词匹配锁定边界避免把1000或者K1001这种误判进来。正则搜索的结果列表自带计数你会清楚看到这个值到底出现了多少次。我当时要是这么干就会发现我以为只有5处实际应该有十几处而不是等设备出问题才回头查。4.4 触摸屏与PLC共享变量写死值的另一个重灾区前面说到HMI脚本里也有写死值。这是最容易漏掉的一块因为它是另一个工程文件GX Works3里搜不到。GOT触摸屏的扩展脚本比如配方切换、报警提示那些宏里经常直接写寄存器编号和数值判断这些数字在PLC工程里没有任何影子。我的做法是给触摸屏脚本也建一张符号映射表放在脚本文件开头的注释里// 寄存器映射 // D100 配方号 // D110 模式号 // 报警号 3001 对应 PLC 报警汇总的 200脚本里凡是用到这些编号的地方全部改成注释引用每次改PLC侧的值同步更新这张表。虽然麻烦但好过改漏了之后到现场拿万用表对信号。5. 重构之后的维护体验与长期习惯5.1 改完之后代码检查时长从半天降到半小时把1700行ST里所有写死的业务值清点完、换成常量组之后第一感觉是代码变胖了——声明区多出来几十行常量定义。但第二感觉是查问题变快了。以前排查一个报警得满程序搜数字翻个半天才能确定报警号是从哪个分支出来的。现在直接在报警域常量组里找cAlarm_xxx交叉引用点一下两分钟就能顺着引用链路把源头找出来。实测下来一次常规的代码检查从原来的大半天缩短到半小时左右而且基本不会出现搜不到的情况。5.2 几个值得长期坚持的习惯这次踩坑之后我给自己列了五条铁律分享出来供参考ST程序里禁止出现裸数字业务值。要么是常量要么是带注释的立即数同一业务域的所有相关常量必须放一起用统一前缀禁止散落在各个POU里凡是设计到模式/状态/报警类型的值按枚举的方式组织用常量组模拟改任何值之前先交叉引用再正则统计确认数量对得上再动手触摸屏脚本里的数值单独建映射表和PLC侧常量同步维护5.3 如果你刚接手一个老项目先从这里下手最后再说一个实际经验。如果你刚拿到一个别人的三菱ST项目想快速判断这程序值不值得重构先搜几个关键词看程序里有没有16#开头的字面量、有没有大段的IF x 数字比较、有没有数组索引直接用数字的。这三样只要中两样基本可以断定是写死值重灾区。这时候别急着大改。先花半天时间把所有数值字面量列个清单按流程模式、报警、索引、参数四类分组再决定哪些转常量、哪些转枚举模拟。这个准备工作做完后续改起来才心里有数。这台灌装机从那次重构到现在跑了快半年再没出现过改一个值带出三个坑的情况。回过头来看所谓常量vs枚举的争论其实是表象核心问题是你愿不愿意在写每一行代码的时候多想一步这个值未来会不会变。如果会就该用符号替代它。这一步多想省下来的可不只是排查时间还有半夜远程连客户设备的那些糟心时刻。
阅读完成 · 觉得有帮助?
咨询建站