搞三菱MELSEC ST语言编程的兄弟,八成纠结过一个问题:给变量设初值,到底用声明赋值bStart : BOOL := FALSE;好,还是在程序开头搞一个首次扫描触发块统一赋值好?表面上看,两种写法在PLC启动后的第一次扫描都能让变量变成目标值,可真移植到现场项目里,往往就差着一层。这篇文章就掰开揉碎讲清楚声明赋值和首扫描的底层时机、复位行为、保持逻辑以及各种坑,结合GX Works3里的对比工程示例,让刚上手ST的新人和写了两三年但没细究过的老手都能少走弯路。
1. 两种写法的直观对比与问题起源
1.1 声明赋值:写在变量声明区的“出厂设置”
ST语言里最常见也最直观的初值写法,就是在声明变量时直接赋值。三菱GX Works3中,你会在ST程序的变量声明区看到类似这样的代码:
VAR_GLOBAL iMode : INT := 1; iCount : INT := 0; bCyl : BOOL := FALSE; bAlarm : BOOL := FALSE; END_VAR这种写法是IEC 61131-3标准里明确支持的语言特性,声明区里的初值会在程序编译后作为变量的初始数据被固化下来。用通俗的话讲,这相当于给变量设置了一个“出厂设置”。PLC的CPU从STOP切换到RUN的时候,系统会在执行任何用户逻辑之前,先把这些初值加载到变量对应的内存区。也就是说,第一个扫描周期还没正式开跑,变量就已经是声明区里写的那个值了。
这个特性非常省事,尤其在处理单个变量的固定默认值时。比如某个模式字、某个使能位、某个计时器预设值,只要在声明区写一个常量就行,程序体里什么都不用做。也正因为它简单,很多初学ST的人会把所有初值都堆在声明区里,这本身没问题,但一旦变量多了、初始化逻辑复杂了,就会暴露出它的局限性。
1.2 首扫描赋值:利用特殊继电器的“一次性闹钟”
另一种做法,是把初始化写在程序体里,用一个“首次扫描标志”包住。在三菱MELSEC系列PLC里,不同系列的首次扫描特殊继电器名称不同,Q系列常用SM402,FX系列常用M8002,它们会在CPU由STOP切到RUN后的第一个扫描周期内保持ON,之后就变成OFF。ST代码会长这样:
IF SM402 THEN iMode := 1; iCount := 0; bCyl := FALSE; bAlarm := FALSE; END_IF;这段逻辑本质上不是一个语言级功能,而是一个应用级逻辑。它做的事是:在程序启动后的第一个扫描周期里,执行一段“初始化流程”。你可以把它理解成一个一次性闹钟——响过一次后面就不响了。因为它是普通ST语句,里面可以写任意复杂的表达式、条件判断、循环甚至调用功能块,所以它比声明赋值灵活得多。
1.3 既然结果看起来一样,为什么要纠结
如果只看启动后的第一个扫描周期结束时的变量值,两种写法得到的结果确实一样。但“第一次扫描结束时相同”不代表“整个生命周期里都相同”。它们最大的区别在于初始化的归属和生效时机:声明赋值是系统在扫描逻辑开始前完成的,首扫描赋值则是用户逻辑在扫描过程中“抽空”做的。
这种区别带来的连锁反应很多。比如,如果首扫描赋值的程序块在扫描顺序里排得靠后,那么同一扫描周期里排在前面的程序块读到的可能还是“老值”;又比如,如果变量声明成保持型RETAIN,首扫描赋值会无条件下次上电时强制覆盖,而声明赋值一般不会覆盖恢复后的保持值;再比如,在线修改程序时,两种写法都不会重新触发初始化,但很多人会误以为改完程序后变量会恢复初值。这些问题如果不搞清楚,现场迟早要背锅。
2. 声明赋值与首扫描的底层生效机制
2.1 声明赋值:发生在“逻辑执行之前”
要理解声明赋值和首扫描的差别,先得理清PLC一个扫描周期的基本流程。以三菱MELSEC的中大型PLC为例,CPU从STOP切到RUN时,并不是直接就跳到第一条用户程序去执行。它要经过内部诊断、系统初始化、程序加载等步骤,然后才进入周期性的扫描循环:输入刷新、程序执行、输出刷新。
ST变量声明区里的初值,正是在“系统初始化”这个阶段写入变量内存的。也就是说,在用户程序的第一条语句执行之前,变量值已经被写成了声明初值。因为这个动作发生在逻辑执行之前,所以不管这个ST程序块在任务里排第几个执行,也不管程序块内部有没有代码,所有程序块在第一个扫描周期里读到的变量,都是已经初始化好的值。
这里有一个实际工程里很多人忽略的细节:如果你在RUN状态下用GX Works3在线修改程序,CPU一般不会重新加载声明初值。声明初值的重载发生在CPU的STOP到RUN切换、断电重新上电后的运行启动,或者你手动执行了“全清除/初始化”这类操作时。在线修改程序只改了代码逻辑,没有重新初始化变量区,所以那些声明初值并不会因为这次修改而重新生效。这个坑在后续章节我会详细展开。
2.2 首扫描赋值:跟着扫描顺序和任务执行走
首扫描赋值就不一样了。虽然外面的“IF SM402”条件只在第一个扫描周期内为真,但它终究还是程序逻辑。这就意味着,它的执行时机完全取决于当前ST程序块在任务调度中的位置。
举个例子,一个工程里可能有10个程序块,初始化专用的块被排在最后一个。那么CPU执行第一个扫描周期时,会先把前面9个程序块都跑完,才跑到初始化块。前面9个程序块如果引用了iMode或bAlarm,它们看到的仍然是变量内存里原来的值。这个“原来的值”可能来自哪里?可能是上一次运行残留的值,可能是系统默认的0,也可能就是声明区里写了但还没被覆盖的初值。总之,在这个扫描周期的前半段,变量并不是最终的初始化状态。
这种“逻辑窗口”很讨厌,因为它和扫描顺序强相关。你在调试的时候可能单步看变量值是好的,但换成另一个任务配置、或者把初始化块挪了个位置,问题就冒出来了。首扫描标志本身并没有做错什么,错的是你把它当成了“在逻辑执行之前”的工具,可它本质上只是“在程序扫描的中途某个点”的工具。
2.3 冷启动、热启动与在线修改的不同命运
讨论初值不能只看正常上电那一下,还要看PLC运行的几种常见状态。
第一种是冷启动,也就是STOP到RUN、断电重新上电后启动。这是两种写法都能正常发挥作用的场景:声明赋值由系统加载初值,首扫描赋值在第一个扫描周期执行。表面上看起来殊途同归,但注意如果变量是RETAIN保持型,冷启动的行为就有变化了。保持型变量断电后如果由电池或超级电容维持,重新上电时系统会从保持区恢复数据,这时候声明初值不一定能覆盖回来;而首扫描赋值只要SM402能ON,就会老老实实把值再写一遍,保持型变量的效果直接被打回原形。
第二种是热启动,指的是PLC在运行中由于某些原因暂停扫描又恢复,或者某些CPU支持的部分重启模式。这个时候首次扫描标志可能触发,也可能不触发,具体要看CPU手册。声明赋值同样不一定重载。所以如果你依赖这两种方式去恢复初值,在热启动场景下很可能失灵。
第三种是RUN状态下的在线修改。GX Works3支持在线变更程序,这时候CPU不会重新初始化变量区,也不会重新触发首次扫描标志。大多数人改完程序发现变量值没有回到初值,就是没搞明白这个机制。在线修改本质上是“不打扰CPU运行状态地更新逻辑内容”,任何初始化动作都更像是额外福利,而不是默认行为。想要变量在在线修改后重新初始化,你得在程序里做一个手动“初始化请求”机制,或者干脆停机重启。
3. 五个关键差异维度的详细拆解
3.1 差异速查表
为了让大家一目了然,我把两个方案的关键差异整理成一张表。这张表每个格都可以在工程现场作为检查清单用。
| 对比维度 | 声明赋值 | 首扫描赋值 |
|---|---|---|
| 生效时机 | CPU开始扫描用户逻辑前 | 第一个扫描周期内、程序块被执行时 |
| 生效范围 | 所有程序块统一可见,无顺序差异 | 只对初始化块之后的逻辑可见 |
| 在线修改程序后 | 不重新加载初值 | 不会再次触发首次扫描标志 |
| RETAIN保持变量 | 上电恢复保持值时一般不会被覆盖 | 只要执行就强制覆盖 |
| 初始化逻辑复杂度 | 只能写固定常量,无法写复杂逻辑 | 可写条件、运算、循环、调用FB |
| 多变量联动 | 不能互相依赖 | 可以按顺序逐步计算赋值 |
| 可维护性 | 集中声明,简单直观 | 需要单独POU或块来承载,否则容易散落 |
| 典型应用场景 | 默认常量、单变量初值 | 初始化序列、条件化初值、批量复位 |
表格只是一个速览,真正要理解的是背后为什么会有这些差异。下面挑几个最容易出事的维度展开说。
3.2 维度一:生效时机带来的“逻辑窗口”
前面提到,首扫描赋值会受扫描顺序影响。这个“逻辑窗口”在实战里造成的故障往往非常隐蔽。我用一个真实感很强的例子来说明。
假设有一个全局变量g_bReady,声明区里写死了FALSE。首扫描初始化块在程序末尾把它置成TRUE。程序里还有两个功能块需要用到这个变量:功能块A在初始化块之前执行,它判断g_bReady = FALSE,于是进入“待机分支”;功能块B在初始化块之后执行,它判断g_bReady = TRUE,于是进入“运行准备分支”。同一个扫描周期,同一个变量,两个功能块却做出了不同的判断。如果g_bReady还影响了输出,那在第一个扫描周期里就很可能出现一次瞬间的错误输出。
不要觉得一个扫描周期很快无所谓。在设备启动瞬间,哪怕几十毫秒的错误输出都可能驱动气缸动一下、变频器抖一下、报警灯闪一下,现场影响很大。而用声明赋值就不存在这个问题,因为变量在扫描开始前就已经是最终值了。所以我一直强调:如果某个变量会被多个程序块在同一扫描周期内读取,并且读取结果会直接影响动作,那就别用首扫描来做它的初值,至少要把初始化块排在最前面。
3.3 维度二:保持变量冲突
保持变量是MELSEC工程里很常用的一个特性。把变量声明为RETAIN之后,断电再上电,CPU会从保持区把值恢复回来。这本来是为了满足“设备断电后重新上电,需要恢复上次运行状态”的需求。
但这时候如果初始化块里写了一句iRecipe := DEFAULT_RECIPE;,那每次上电首扫描标志一ON,这句话就会把保持值覆盖掉。你辛辛苦苦设计的配方号、累计产量、模式选择,瞬间被刷回默认值。这个故障和“保持丢失”的表现症状几乎一样,排查起来很容易走弯路。
反过来,声明赋值在保持型变量上的行为就温和得多。如果保持区已经恢复了断电前的值,系统一般不会再去写声明初值。也就是说,RETAIN变量用声明赋值可以做到“首次下载时给一个初值,运行后断电再上电则恢复保持值”。这通常是工程师想要的效果。但也要注意,如果保持区里的数据因为电池耗尽等原因失效,CPU会重新把声明初值加载进去,这时候相当于“出厂重置”。这本身是合理的,但必须心里有数。
有一种折中做法可以兼顾“真正的首次上电初始化”和“后续上电保持恢复”:用一个保持型标志变量bInitDone,初始值为FALSE,首扫描时判断它,如果没初始化过才执行初始化逻辑,并把标志置TRUE。这样只有第一次冷启动会初始化,后续掉电再上电都会正常恢复保持值。这个方法看起来很完美,但它有一个前提:下载程序的时候要保持区里的bInitDone不是一直为TRUE,否则初始化逻辑会被跳过。所以工程上还要专门做一个“恢复出厂设置”的按钮,以及一套清除保持区的操作流程。
3.4 维度三:初始化逻辑的复杂度上限
声明赋值能做的其实很有限,它只支持在变量声明时给定一个常量初值。你没法写iB := iA + 1;这种依赖关系的赋值,也没法根据某个外部输入开关来决定初值是1还是2,更没法在声明的过程中调用一个功能块去完成复杂运算。
首扫描赋值就没有这个限制。它本质上是一段完整的ST逻辑,可以写IF、CASE、FOR、WHILE,可以调用功能块,也可以按顺序给几百个变量赋值。因此,当你的设备有一个复杂的“上电初始化序列”时,首扫描是最自然的实现手段。
不过灵活也意味着更容易失控。我见过一些ST程序,初始化逻辑没放在独立POU里,而是顺手塞在各个功能块里,每人塞几行,最后启动时到底先执行哪段初始化根本没人说得清。要是再赶上多个功能块里都用SM402,那就更乱了。建议的做法是单独建一个“Init”POU或功能块,里面只干初始化这一件事,并且把它放到任务列表的最前面执行。
3.5 维度四:任务周期和中断程序的影响
很多三菱中大型PLC支持多种任务类型:扫描执行、固定周期执行、事件中断执行。ST程序不一定全都在扫描任务里跑。首扫描标志SM402是CPU扫描层面的一个特殊继电器,它只在RUN后的第一个扫描周期内为ON。如果你把初始化逻辑放在一个固定周期任务里,而这个固定周期任务的第一次执行点正好在CPU第一个扫描周期内,那没问题;但如果你放在一个中断任务里,中断的触发时机是不可预测的,可能第一个扫描周期根本没到,初始化代码就已经在某个中断里执行了;也可能第一个扫描周期结束时中断都没触发,SM402已经变回OFF,初始化代码永远没机会执行。
所以我的经验是:首扫描初始化逻辑千万不要放在中断任务里,也不要放在一个触发条件本身需要外部事件的程序块里。老老实实放在主扫描任务的最前面最可靠。声明赋值则完全没有这个顾虑,它和任务调度无关,系统加载初值之后所有任务里读到的都是初始化好的值。
4. GX Works3里的对比实验:从代码到现象
4.1 场景与变量规划
光讲理论不够,我把这个对比做成一个可以直接在GX Works3里复现的小实验。假设我们要为一个简单的工装设备写初始化逻辑,需要设置四个变量:
iMode:设备模式,默认1表示自动模式iCount:计数值,默认0bCylinder:气缸输出,默认FALSEbAlarm:报警标志,默认FALSE
实验目标非常明确:对比两种写法在断电上电、STOP到RUN、在线修改、RETAIN保持四种情境下,这四个变量的真实表现。
4.2 方案A:只用声明赋值
在GX Works3里新建一个ST程序,变量声明区写:
VAR_GLOBAL iMode : INT := 1; iCount : INT := 0; bCylinder : BOOL := FALSE; bAlarm : BOOL := FALSE; END_VAR程序体其实可以什么都不写,或者只留一行注释。声明初值会在CPU启动时由系统加载,程序体有没有代码并不影响。如果你愿意,也可以在程序体里加一行RETURN;,保证编译器不抱怨空代码。
注意到这里用的是VAR_GLOBAL,因为我们需要在监控窗口里直接观察全局变量。如果写在局部VAR里,只有对应的功能块实例能看到,监控起来要更麻烦一些。实际工程里,初始化对象往往是全局变量或直接被程序块引用的标签。
4.3 方案B:首扫描赋值
同样新建一个ST程序,变量声明区不写初值,程序体里写:
IF SM402 THEN iMode := 1; iCount := 0; bCylinder := FALSE; bAlarm := FALSE; END_IF;这里的SM402是三菱Q/R系列的首扫描特殊继电器。如果你用的是FX系列,需要把SM402换成M8002。下面所有实验结果对这两种系列来说,机制是一样的。
实验时,我会把初始化逻辑故意放在任务列表的最后一个程序块里,用来演示首扫描赋值的顺序问题。后续再做一个“放在最前面”的对比。
4.4 实测步骤与预期结果
你可以在自己的工程里按下面步骤一步一步做,每一步都记录变量值的变化。
第一步,把方案A的程序下载到CPU,然后执行STOP到RUN。在线监控iMode、iCount、bCylinder、bAlarm。理论上四个变量都应该等于声明区里写的初值。这一步方案A和方案B没有明显差别。
第二步,手动修改变量值。比如在线把iCount改成5,把bAlarm改成TRUE。然后把CPU切到STOP再切回RUN。此时方案A和方案B都恢复了初值,看起来还是一样。
第三步,在RUN状态下做一次在线修改,随便改一个不影响初值的逻辑,比如加一行注释。修改完成后不要重启CPU,观察变量值。你会发现不管是方案A还是方案B,之前手动改的iCount=5、bAlarm=TRUE都还保持着,不会自动回到0或FALSE。这就验证了前面说的:在线修改程序不会重新加载变量初值,也不会重新触发首扫描标志。
第四步,把iCount的声明改成RETAIN保持型。方案A里iCount : INT := 0;,方案B里iCount : INT;。下载程序后把iCount改成100,断电再上电。方案A中,如果保持区正常工作,iCount会恢复为100,因为声明初值0不会覆盖保持值;方案B中,只要首扫描代码执行了iCount := 0,它就会被强制写回0。这就是两种写法在保持变量上最核心的差异。
第五步,把初始化逻辑在任务里的顺序从第一个调到最后一个,然后再次启动CPU。方案B中,你需要在初始化块前面的程序块里加一个断点或监控,去看看在初始化块执行之前变量值是什么。你会发现在第一个扫描周期的前半段,变量可能还是上电后的默认值或者上上次的残留值,直到执行到初始化块才变过来。方案A没有这个问题。
4.5 声明初值与首扫描同时存在的后果
还有一类工程代码,声明区里也写了初值,首扫描块里也做了赋值。这种做法不能说绝对错,但很容易让人迷糊。比如:
VAR_GLOBAL iMode : INT := 2; END_VAR程序体里:
IF SM402 THEN iMode := 1; END_IF;启动后的最终值当然是1,因为首扫描块在扫描周期内把声明初值2覆盖掉了。但如果你在线修改了程序,SM402没有触发,这时声明初值也不会重新加载,那iMode到底是2还是1就取决于你之前是什么状态。如果你在某个中间时刻手动把iMode改成了5,在线修改后它可能一直是5。这种代码会让调试的人抓狂,因为你很难判断当前值到底属于哪种机制控制的。
我的建议是:同一组变量的初值尽量只用一个机制管理。要么全部走声明赋值,要么走首扫描初始化。如果实在要用两者,也要明确分工,比如声明区只负责“非保持变量的默认值”,首扫描块负责“具备复杂依赖关系的初始化序列”,并且把分工写进注释里。
5. 常见问题与排查思路实录
5.1 问题1:首扫描赋值没有执行
遇到最多的情况就是“程序跑起来了,但初始化代码根本不生效”。排查时先确认特殊继电器有没有用对。
第一,三菱不同系列的首扫描继电器名称不一样,Q/R系列是SM402,FX系列是M8002。有的人把SM402当成了常ON继电器,结果程序里被周期性地反复赋值,变量永远停留在最后一个值。也有的把SM400当成首扫描,SM400是常ON,当然不对。
第二,确认这个ST程序块确实被分配到了一个被执行的任务里。GX Works3里如果你新建了一个POU但没有放到任务里,那它压根不会执行。首扫描赋值再合理,代码不跑也白搭。
第三,确认这个程序块所在的任务确实在RUN后第一次扫描周期内执行了。如果放在了某个触发条件极其苛刻的中断任务里,SM402信号可能根本轮不到它。
第四,看有没有其他代码把SM402对应的标志或变量提前改了。比如有人在更前面的程序块里写了SM402 := FALSE这样的逻辑(虽然是特殊继电器,理论上不能随意写,但项目里如果通过中间变量做了转发,就可能被干扰)。
排查顺序不重要,重要的是脑子里先有一个概念:首扫描赋值是应用逻辑,它能不能执行取决于任务调度和程序块位置,而不像声明赋值那样由系统保证。
5.2 问题2:在线修改程序后,初值没恢复
这是一个典型的认知偏差。很多人觉得“我改了程序,变量就应该重新初始化”,但PLC在线修改的机制不是这样。在线修改只更新用户程序区的内容,不碰变量区,也不触发启动流程。
所以如果你需要在线修改后让某些变量回到初值,最直接的办法是修改完后执行一次STOP到RUN切换。如果不能停机,那就在程序里设计一个“初始化请求”变量,比如bManualInitReq,在程序里写:
IF bManualInitReq THEN iMode := 1; iCount := 0; bCylinder := FALSE; bAlarm := FALSE; bManualInitReq := FALSE; END_IF;手动在监控窗口里把bManualInitReq置TRUE,就能触发一次初始化。这种做法对声明赋值也有效,因为它本质上是应用逻辑,不受系统加载初值限制。这也是为什么我建议在复杂的设备控制程序里,除了启动初值之外,一定要留一个人工初始化入口。
5.3 问题3:RETAIN保持变量被首扫描刷掉
这个问题我在现场遇到过不止一次。最典型的场景是设备有一个“配方号”或“累计产量”变量,声明成RETAIN,起初是用声明赋值给了一个默认配方0。后来做设备改造时,工程师觉得上电初始化逻辑太散,于是把所有变量初值统一挪到首扫描块里,加了一句iRecipe := 0;。结果客户反映:每次设备断电再上电,配方号都会跳回0,工程师的第一反应是电池没电了或者保持区坏了,换了一块新电池还是没有解决。
排查到最后才发现,是首扫描赋值把保持值覆盖了。RETAIN变量恢复的是断电前的值,首扫描一旦执行iRecipe := 0,恢复等于没恢复。解决的办法很简单:把要保持在断电前状态的变量从首扫描初始化块里摘出来,让它们只依赖保持区的恢复机制;或者用前面讲的bInitDone标志,只在真正的首次启动时才初始化RETAIN变量。
这里要给一个额外提醒:如果你用了bInitDone标志这种方案,下载程序时如果不清除保持区,bInitDone在上电后可能已经是TRUE,初始化逻辑不会执行。所以工程上必须配套“保持区清除”或“出厂重置”操作,否则新的初值逻辑无法生效。这不是代码bug,而是保持变量的固有特性。
5.4 问题4:初始化块执行顺序靠后导致初值不稳定
如果首扫描初始化块排在任务列表的最后,而前面的程序块已经使用了那些变量,常见的故障表现为:设备每次上电,某个输出会短暂地闪一下,或者某个计数值会在第一个扫描周期被错误地加了一次,等到初始化块执行完才恢复正常。
这种问题和初值本身没有关系,而是初始化时机太晚。解决办法很直接:把初始化POU移到任务列表的第一个位置。在三菱GX Works3里,任务属性中可以调整程序块的执行顺序,把Init块拖到最顶上就行。如果工程里有多个任务,还要确认这些任务之间是否有优先级关系,初始化要放在所有可能读取该变量的任务之前。
如果你实在动不了任务顺序,还有一个备用方案:在变量声明区使用声明初值,然后在首扫描块里只处理“需要复杂计算才能得到”的那部分变量。这样至少保证基础变量从一开始就是确定的,复杂变量虽然晚一点,但不会把这个扫描周期搅得天翻地覆。
5.5 问题5:功能块内部使用首扫描标志导致初始化多次执行
在ST中使用功能块FB时,每个FB实例都有自己的局部变量区。如果你在FB内部写了IF SM402 THEN来初始化局部变量,那么有多少个实例,这段初始化逻辑就会执行多少次。如果这些实例分布在不同的程序块里,有些在第一扫描周期先执行,有些在中间执行,没有一个统一的初始化时刻,调试起来会非常混乱。
更麻烦的是,有些FB实例可能只在某个分支条件下被调用,首扫描标志ON时该分支压根没走到,FB的初始化就没执行;等下次这个FB再被创建或调用,SM402早就不是ON了。这个FB的局部变量就处于一个“没有初始化”的状态。
所以,功能块内部尽量不要依赖首扫描特殊继电器。如果确实需要给FB内部变量做初始化,应该让上位逻辑通过输入参数传入一个初始化标志,或者把FB的内部变量声明初值利用起来。声明初值在FB实例化时对每个实例都会生效,这一点的确定性比首扫描强得多。
6. 到底怎么选:我的决策建议与实战体会
6.1 一套能直接套用的选型原则
综合前面的机制和坑,我给自己定了一套选型原则,写在这里供参考。
如果变量初值是一个固定常量,和外部状态、其他变量没有任何依赖关系,而且变量不需要掉电保持,直接用声明赋值最省事。一个冒号一个等号就搞定,不用额外建初始化块,代码评审时一眼就能看明白。
如果初始化逻辑涉及多个变量,而且它们之间有先后依赖或条件判断,比如“只有急停没被按下时才允许把模式设为自动”,那就必须用首扫描块。为了保证不出现“逻辑窗口”,把初始化块放到主扫描任务的第一个位置,最好单独建一个Init POU。
如果变量声明为RETAIN保持型,并且希望断电后恢复保持值,绝对不要用首扫描块无条件强制覆盖。需要首次真正上电初始化时,用bInitDone保持标志加条件判断,并且配套出厂重置机制。
如果只是在线调试时希望变量快速回到初值,不要依赖声明赋值和首扫描,直接在程序里做一个手动初始化请求变量,或者停机再启动。
如果是安全相关的初始状态,比如气缸、夹具、危险动作输出,无论用哪种初值写法,都不能只靠软件初始化来保证安全,还要有硬件回路和设备侧的保护逻辑。这一点无论如何强调都不过分。
6.2 我在实际项目里踩过的那一脚
最后分享一个具体的改造经历。之前给一台设备做程序优化,把原来散落各处的一堆声明区初值统一改成了一个首扫描初始化块。看着确实整洁了,所有初始化逻辑集中在一个地方,还能跑条件判断,心里美滋滋。结果客户做验收的时候发现,设备断电重启后,操作员在触摸屏上保存的“快速循环模式”每次都会变成默认的“手动模式”,客户怀疑程序丢数据了。
我查了很久才发现,那个模式变量是RETAIN类型,旧程序里声明赋初值并不会在恢复保持值时覆盖它,问题不大。而我改成了首扫描统一初始化之后,每次上电都执行了一次强制赋值,相当于把保持值给“洗”掉了。后来我把bInitDone机制加上去,只在第一次真正上电时初始化,后续断电重启恢复保持值,这个故障才彻底消失。
也是从那次之后,我养成了一个习惯:每次听到“初值”两个字,都会多问一句“这个变量要不要保持”,然后才决定用声明赋值还是首扫描块。初值这种东西,看起来只是启动瞬间转眼即逝的一小步,但在设备上电那一刻,它决定了机器的“第一口动作”,尤其涉及到气缸、电机和危险输出时,一点都马虎不得。希望这篇文章能帮你把这一步走稳。