
做MELSEC PLC调试的人十有八九都写过类似这种代码VAR counter : INT : 0; END_IF;或者另一种IF SM402 THEN counter : 0; END_IF;乍一看都是“程序启动时把 counter 置 0”好像没什么区别。但真到了现场一旦碰到断电重启、保持型变量、RUN中在线修改程序这两种写法就会开始打架。我之前就在一个 iQ-R 系列的项目里翻过车设备断电重启后某个计数器的数值怎么都不归零查了半天发现不是“声明初值没写”而是保持型标签和首扫描逻辑在底层是两套完全不同的机制。这篇就把“声明赋值”和“首扫描”的本质区别拆开讲清楚包括各自什么时候生效、什么时候不生效以及工程上到底该怎么选。用GX Works3写ST或者刚接触MELSEC ST语言的朋友这篇应该能帮你少走不少弯路。1. 两种“赋初值”写法的真面目别把声明赋值当成首扫描1.1 声明赋值写在变量声明里的初值ST语言里最常见的初始化方式就是在变量声明段直接给初值。GX Works3里长这样VAR counter : INT : 0; enabled : BOOL : TRUE; speed : REAL : 12.5; END_VAR这叫“声明赋值”。它的本质是ST语言编译时的一部分编译器把counter的初始值 0、enabled的初始值 TRUE、speed的初始值 12.5作为程序本体的附属信息编译进目标代码。当程序装载到CPU时系统会在扫描周期正式开始之前先把这批变量初始化好然后再进入用户程序的循环扫描。这里要特别注意声明赋值不是用户程序里的一条“指令”而是程序装载阶段的一个系统动作。它不参与扫描周期也没有“执行顺序”的概念它就是变量在内存里的起点。1.2 首扫描用特殊继电器SM402搭出来的运行逻辑另一种常见写法是首扫描。MELSEC系列里有个特殊继电器 SM402它在CPU从STOP切换到RUN后的第一个扫描周期内保持ON之后变为OFF直到下一次STOP→RUN切换包括断电重启后再次RUN才会再次ON一次。于是很多人会写IF SM402 THEN counter : 0; enabled : TRUE; END_IF;这段逻辑从“结果”上看确实也是“程序启动时置初值”但它的本质和声明赋值完全不同。它是用户程序里一条真实可执行的分支指令在第一个扫描周期里被CPU取指、执行然后写入变量。也就是说首扫描赋值是“运行期行为”而不是“装载期行为”。它和普通逻辑的唯一区别就是触发的条件特殊——只在扫描启动后的第一个周期满足。1.3 为什么这么多人会搞混搞混很正常因为90%的情况下两者看起来效果一样。程序下载运行后counter 都是从0开始加第一次扫描时变量也正好是初始值。只有当你开始关心“变量到底是被谁、在什么时机写成了0”之后区别才会浮现出来。另外还有个背景知识容易干扰网上搜“ST表”通常指的是算法里的Sparse Table用来做区间最值查询的。但这里说的ST是Structured Text是PLC编程用的结构化文本跟算法表完全是两码事。搜索引擎会把“MELSEC ST”和“ST表”混在一起进一步加深了误解。下面进入正题。2. 声明赋值的执行时机它属于程序装载不属于扫描周期2.1 程序装载时发生了什么要理解声明赋值先理解PLC的启动流程。以GX Works3 MELSEC iQ-R为例程序下载到CPU后CPU执行一次“工程初始化”本质上做三件事装载程序本体梯形图、ST、运动控制等编译后的指令。按程序本体内记录的信息对全局标签和局部标签进行空间分配与初值装载。进入RUN状态开始周期扫描。步骤2里声明赋值的初值就被写入变量对应地址。所以我在前面说“声明赋值不是一条扫描指令”更准确的说法是它是程序装载阶段由系统自动完成的预处理动作。当你看到 counter 在程序启动后是0其实这个0早在我们用户代码开始执行之前就已经写好了。2.2 冷启动、热启动与保持型标签声明初值不保证每次都生效如果你以为“只要声明了初值每次运行都会从初值开始”那就踩坑了。至少有两个场景会让声明初值“失效”第一个是保持型标签RETAIN。GX Works3里标签可以设置保持属性。保持型标签的语义是断电不丢失、重启不重置。既然是“不重置”系统在装载阶段就不会用声明初值去覆盖它。假设counter声明为VAR RETAIN counter : INT : 0; END_VARPLC第一次下载时counter确实会被赋成0。但设备断电重启后如果保持内存有效counter会保持断电前的值比如37声明初值0并不会再次生效。这个不是bug是保持型标签的“保持”语义在起作用。只有在工程设置里明确要求“初始化保持型变量”或者下载程序时勾选了初始化选项声明初值才会覆盖保持值。第二个是RUN中的在线修改。在线修改RUN中写入时程序本体被部分更新但CPU并没有重新执行“装载初始化”动作。已经在运行的变量不会被重新赋成声明初值。这个问题后面第4章会展开这里先记住在线修改不触发声明初值的重新装载。2.3 最常见的翻车现场声明了初值运行却没有按初值走我见过最典型的困惑来自一个做包装设备的老工程师。他在ST里写VAR batchCount : INT : 0; END_VAR结果每次断电重启后batchCount 显示的却是上次生产剩余的数值。他以为是初值没生效反复重新编译下载了好几次问题依旧。原因很简单GX Works3的标签编辑器里这个变量默认被勾选了保持属性或者通过全局标签映射到了保持型软元件区。声明初值0只在下装那一刻生效后续断电重启自然不再重置。这种现象特别有迷惑性因为“声明初值”从字面上看很容易理解成“只要程序运行这个变量就该从0开始”。但实际上声明初值的生命周期是“装载→运行→修改→再装载→再运行”而不是“每次运行都重置”。而保持型标签恰恰绕过了“再装载”这个环节。2.4 快速验证声明初值是否生效如果你不确定当前工程的声明初值行为到底怎样最快的办法是做个实验。我在样机上经常这么验证新建一个测试程序声明一个简单变量VAR initTest : INT : 100; END_VAR。程序中每扫描周期让 initTest 自增1趋势图或监视窗口观察。执行断电重启观察 initTest 是回到100还是保持断电前的值。在RUN中写入把声明初值改成200观察运行时值是否变化。这一套跑下来当前CPU在当前工程设置下的行为基本就摸清了。不同系列、不同工程参数下结果确实可能有细微差异但测试思路是通用的。3. 首扫描赋值的执行时机它是用户程序第一条扫描指令3.1 SM402在什么条件下导通SM402是MELSEC特殊的系统继电器它在“RUN启动后的第一个扫描周期”内为ON。你可以在ST里直接引用它IF SM402 THEN (* 第一个周期才执行的初始化逻辑 *) END_IF;也可以搭配SM403这类“仅在第二个扫描周期ON”的继电器做更精细的分步初始化。关键是SM402在每一次从STOP到RUN的启动过程里都会重新ON一次。包括断电重启后再次进入RUN状态。这意味着首扫描逻辑的执行次数取决于“启动”发生的次数而不是“装载”发生的次数。3.2 首扫描不只会“赋初值”它更像一次软启动回调因为首扫描本质上是用户程序的一部分它能做的远不止给变量赋个初值。你可以在首扫描里做很多“软启动”时需要处理的事根据当前设备状态决定初始模式手动/自动。读取断电前保存的工艺参数并根据参数组合恢复运行状态。调用一个专门做“系统初始化”的功能块。对多个变量同时做有序的初始化而不是逐个声明。这些用声明赋值是做不到的因为声明赋值无法包含逻辑判断。它只能给固定变量赋固定值。这也是首扫描在实际工程里比声明赋值“高级”的地方它是“有条件的初始化”而声明赋值是“无条件的装载”。3.3 首扫描逻辑的副作用无条件覆盖RETAIN变量但首扫描也有它最危险的一面。如果你给保持型标签写了无条件首扫描赋值VAR RETAIN workMode : INT; END_VAR IF SM402 THEN workMode : 0; (* 每次启动都把保持值覆盖 *) END_IF;那么恭喜你这个变量虽然声明了保持型但每次断电重启后都会被强制改成0“掉电保持”功能就等于被你的首扫描逻辑废掉了。这在现场经常表现为设备断电前是自动模式重启后却变成手动模式或者某个累计数被清零生产数据对不上。这类问题特别难排查因为它不是“程序逻辑错误”那种一眼能看出来的问题而是“两套机制语义冲突”带来的隐性矛盾。你单独看每一行代码都没毛病组合在一起就是错误行为。3.4 多任务/多程序文件下首扫描执行的顺序影响MELSEC工程里可以有多个程序文件它们可以挂在不同优先级的中断任务或执行周期里。如果两个程序文件里都有SM402初始化逻辑比如A程序把 counter 设为100B程序把 counter 设为0那最终结果取决于任务执行顺序。声明赋值在装载阶段就完成了不涉及排序问题首扫描逻辑则完全受任务调度影响。所以我的建议是首扫描初始化逻辑尽量集中在一个专用的初始化程序里并把它放在最先执行的任务组。分散到多个程序里不仅难以维护还会出现“谁后执行谁说了算”的隐性依赖。4. 四个典型场景下的最终值差异一张表说清楚4.1 场景一正常下载程序后首次运行这是最常规的场景也是两种写法“看上去一样”的根源。下载程序时CPU完成装载初始化声明初值写入变量。紧接着进入RUNSM402在你的ST程序第一个扫描周期里导通首扫描逻辑执行。如果同一个变量在声明里赋了初值又在首扫描里赋了另一个值那么最终看到的是一轮扫描结束后的值——通常是首扫描逻辑里赋的值因为它在时间上晚于装载阶段的声明赋值。举个例子VAR value : INT : 1; END_VAR IF SM402 THEN value : 2; END_IF;程序首次运行后value显示为2而不是1。这在调试时会带来困惑声明里明明写了1为什么监控出来是2原因就在执行顺序上。所以如果两个地方同时对同一个变量做了初始化你需要明确知道谁落在最后。4.2 场景二断电重启后上电运行断电重启对于非保持型变量来说声明初值会重新生效因为没有保持内存去保留它对于保持型变量来说声明初值不会覆盖保持值前提是保持数据有效。但SM402不管这套它照常触发照常执行你的首扫描逻辑。这时候差异就非常明显了变量类型声明赋值行为SM402首扫描行为非保持型重新装载声明初值照常执行强制赋值保持型通常不覆盖保持值照常执行强制赋值结果就是如果你在首扫描里无条件给一个保持型变量赋初值你的“保持型”其实已经名存实亡了。反之如果保持型变量在首扫描里没被写到它的掉电保持才能发挥作用。4.3 场景三RUN中在线修改RUN中写入这是最容易踩坑、也最被忽视的场景。RUN中写入时CPU没有经历“装载→启动”的完整过程而是直接在运行状态下更新目标程序段。因此SM402不会再次导通原程序里的SM402初始化逻辑不会重新执行同时原变量的声明初值也不会重新装载。你已经改好一个新初值下载到PLC里满怀期待地以为变量会变成新初值结果监控窗口里它纹丝不动继续用旧值跑就是这个原因。新增变量则另说新变量加入时系统可能按声明初值初始化它但这取决于GX Works3版本和在线修改的具体方式不建议作为可靠依赖。总之任何时候不要把“在线修改之后变量自动重新初始化”当作预期行为。想要重置变量正规做法是STOP→RUN一次或者触发你专门写的初始化逻辑。4.4 场景四STOP→RUN热启动STOP→RUN切换时程序被重新启动但CPU并没有断电也没有重新装载程序本体。SM402会正常导通一次首扫描逻辑会执行。声明赋值是否重新生效这里要看当前工程的具体设置和变量属性。非保持型局部标签在部分设置下会重新初始化保持型不会。遇到这种情况时别凭经验硬猜建议用2.4节的方法实际测一遍因为你手头这台设备和上一台项目的设置很可能不一样。4.5 四场景汇总表与判定实验下面这张表可以印在脑子里场景声明初值SM402首扫描备注下载程序后首次RUN生效执行若同变量两处都写首扫描值在后断电重启后RUN非保持生效保持型通常不生效执行注意首扫描会覆盖保持值RUN中在线修改通常不重新装载不执行想重置变量别指望在线修改STOP→RUN热启动视工程设置执行建议实测确认如果你想彻底搞清楚自己手头这台CPU的行为我给一个标准操作在ST里设两个变量一个非保持一个保持声明初值都设为100。程序里让它们每扫描周期自增1。分别做断电重启、STOP→RUN、RUN中在线修改。把每次操作后的初值行为记下来。这个过程花费不超过20分钟但能帮你建立“这台CPU在这套工程设置下到底听谁的”的准确认知。5. 工程上该用哪种场景选择与组合写法5.1 用声明赋值的地方声明赋值的优势是“简单、直接、装载时确定”。它适合用在这些地方不会掉电保持的普通计数器、标志位的默认初始值。与应用逻辑无关、纯粹作为“程序本体默认配置”的参数。需要在程序下载那一刻固定下来的基准值比如默认速度、默认阈值。但有一条红线默认情况下不要在保持型标签上写声明初值。除非你确实想通过“下载程序并初始化”这个动作来重置保持数据否则它会造成“断电后初值不生效”的严重误解。如果非写不可一定要在注释里写清楚“这里初值只在下装时生效不断电重置。”5.2 用首扫描的地方首扫描适合用在这些地方需要根据当前状态做出判断后再决定初始值的场景。需要初始化一组相关的变量而不是单独一两个。需要调用功能块、子程序语言做初始化。需要在每一次RUN启动时都重新建立“运行起点”的场景。但首扫描有个前提你不能无条件覆盖保持型变量。如果某个变量既要保存掉电前的值又要在首扫描里做条件恢复就必须加上判断条件。5.3 真正“只上电首次生效”的组合写法很多人想要一个更精确的初始化时机“只在真正首次上电时执行一次断电重启不要执行”。这个需求在有些项目里很常见比如设备首次安装时需要初始化数据库、第一次上电时需要写入出厂设置。直接用SM402不行因为断电重启它也会ON。直接用声明初值也不行因为普通变量每次重启都会重置或者不重置取决于设置。这时候就需要用“保持型标志 SM402”组合VAR RETAIN initDone : BOOL : FALSE; END_VAR IF SM402 AND NOT initDone THEN (* 这里放只执行一次的初始化逻辑 *) initDone : TRUE; END_IF;逻辑很简单第一次下载运行后initDone 是FALSESM402导通初始化逻辑执行然后 initDone 被置为TRUE并保持。断电重启后initDone 保持TRUESM402即使再次导通条件NOT initDone也为假初始化代码不会再执行。这样既绕开了“声明初值每轮都重置”的问题也绕开了“保持型标签被无条件覆盖”的坑。如果你确实需要“首次安装才初始化”这个组合是最可靠的做法。要注意的地方是调试阶段你想让它重新初始化一次要么在PLC参数里做一次保持型变量初始化要么用工程里的“内存初始化”功能把保持区清掉再重新下载程序。5.4 一段可直接套用的ST初始化骨架根据我自己项目的使用习惯整理了一个比较通用的初始化骨架你直接参考VAR initStep : INT : 0; (* 初始化步骤计数非保持 *) defaultSpeed : INT : 100; (* 默认速度 *) END_VAR VAR RETAIN firstPowerOn : BOOL : FALSE; lastRunMode : INT; END_VAR (* 首次上电初始化只在下载后第一次运行时执行 *) IF SM402 AND NOT firstPowerOn THEN initStep : 0; defaultSpeed : 100; firstPowerOn : TRUE; END_IF; (* 每次RUN启动时的软初始化注意不要覆盖保持值 *) IF SM402 THEN initStep : 0; (* 如果需要恢复断电前的运行模式这里不要写 lastRunMode : 0 *) END_IF;这套骨架的优点是把“只做一次”的初始化不同时保持值和“每次启动都做”的软复位只在启动时重置运行期变量分开逻辑非常清楚。注释里还专门提醒了不要覆盖保持值避免过两个月你自己都忘了当时为什么这么写。5.5 踩坑清单最后把容易出问题的点集中列一下建议贴在你工位旁边别给保持型标签写无条件的SM402赋值除非你故意想清理保持值。别指望RUN中在线修改会让变量回到声明初值更别指望SM402会再次导通。别把初始化逻辑分散在多个程序文件里任务执行顺序会导致初始化顺序不确定。别在保持型标签上写声明初值它会让人误以为每次重启都会重置。别用经验代替测试同一套代码在不同CPU系列、不同工程设置下的表现可能有差异换平台后老老实实做一次变量观测实验。我在实际项目里吃过不少亏尤其是保持型标签和首扫描组合的那次排查花了大半天最后发现是自己代码里的“语义冲突”。从那以后我在ST里写任何初始化逻辑都会先在脑子里过一遍“这个变量是装载期初始化还是运行期初始化断电后它该保持还是该重置”想清楚这两个问题声明赋值和首扫描的取舍其实特别快。希望这篇能帮你少掉几根头发。