ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MELSEC ST变量初值:声明赋值与SM402首扫描的区别与实战

MELSEC ST变量初值:声明赋值与SM402首扫描的区别与实战 写 MELSEC 的 ST 程序绕不开变量初值这件事。尤其当你同时看到“声明区里直接写: 初值”和“程序里用IF SM402 THEN做首扫描初始化”两种写法时十有八九会问一句这俩到底差在哪结果不是都让变量从某个值开始吗我之前也这么想直到在一台设备上被“初值被覆盖”“上电后不是初值”“第一个扫描周期数据不对”这几个问题来回折腾之后才把两套机制的边界彻底摸清。这篇就把我的理解和踩坑记录整理出来不扯教科书只说调试现场怎么判断Q 系列、iQ-R 系列的 ST 都适用。1. 先把两种写法摆出来声明赋值、首扫描各自长什么样1.1 声明赋值在变量声明区直接给出初始数据在三菱 GX Works3 的 ST 程序里可以在变量声明区直接写初值用的就是 IEC 61131-3 里最常见的:语法VAR iCnt : INT : 0; // 声明赋值初值 0 bReady : BOOL : FALSE; // 声明赋值初值 FALSE wMode : WORD : 16#FF; // 十六进制初值 END_VAR这里有几个 ST 的写法和梯形图思维不一样的地方。字符串常量必须用单引号不是双引号VAR sModel : STRING(32) : MELSEC-ST; END_VAR编译之后这组初值会作为变量存储区的“初始数据”在 CPU 从 STOP 转到 RUN 时由系统自动加载。这个过程不占用扫描周期也不依赖任何用户程序逻辑。换句话说不管你的程序第一个扫描周期里写了什么系统都已经在程序开跑之前把这些初值塞进了变量区。这是“系统层面”的初始化。1.2 首扫描初始化用特殊继电器在第一个扫描周期里干活首扫描的“首”指的是 CPU 进入 RUN 之后第一个扫描周期。三菱为此提供了专用特殊继电器MELSEC-Q 和 iQ-R 系列用 SM402FX 系列对应的是 M8002。它们的行为很明确CPU 停止→运行转换后的第一个扫描周期为 ON第二个扫描周期自动变成 OFF之后就再也不触发了除非再次执行 STOP→RUN 或 CPU 复位。ST 语言里没有梯形图的“触点”概念SM402 在 ST 程序里可以直接当作一个 BOOL 变量来读取IF SM402 THEN iCnt : 100; // 第一个扫描周期执行一次 bReady : TRUE; END_IF;这段代码写的是“程序行为”而不是“系统行为”。它必须等程序真正跑起来之后由 CPU 执行到这一行才生效。如果首扫描程序在扫描顺序里排得靠后它生效的时间点也会跟着靠后这一点后面会展开讲。1.3 从结果看“一样”但机制完全不同如果程序只有一个任务、一个扫描周期其他什么都不考虑那么上面两种写法跑完后iCnt都是 100bReady都是 TRUE看起来一模一样。但“看起来一样”恰恰是最容易埋坑的地方。声明赋值是系统在扫描开始前加载的静态初值首扫描是程序在运行中完成的动态赋值。这两个动作的生效时刻、生效范围、与断电保持属性的关系、与程序文件执行顺序的关系完全不同。遇到多任务、多程序块、FB 多实例、数据保持需求时差异立刻就出来了。2. 核心差异初值到底在什么时刻、由谁加载、能否被覆盖2.1 声明赋值的生效时机和影响变量声明赋值不做任何人的程序逻辑控制。CPU 上电、STOP→RUN 转换时系统根据 PLC 参数里的“初始值模式”设置把声明区的初值整体写入变量存储区写完之后第一个扫描周期才开始执行。程序一跑起来声明区就不会每个周期重新覆盖变量了后续值完全由程序逻辑决定。这里有一个非常容易被忽略的变数——三菱 CPU 参数里的初始值执行模式。GX Works3 中通常可以看到两种选择每次 RUN 时执行固定值初始化仅初次 RUN 时执行固定值初始化。如果选的是“每次 RUN”那么每次 STOP→RUN 都会把声明赋值的值打回原样不管你上次运行结束时变量是多少。如果选的是“仅初次 RUN”则声明赋值只在第一次启动时起作用之后即使你反复切换 STOP→RUN变量也不会被声明初值重置。很多人在工程里抱怨“明明写了初值第二次 RUN 却不生效”十有八九就是栽在这个参数上。声明赋值还会被锁存范围影响。如果变量被分配到 Q/iQ-R 的锁存软元件范围内或者标签属性里设置了断电保持那么断电重启后CPU 会优先恢复锁存值。声明初值能不能覆盖锁存值取决于初始值模式以及该软元件是否真的进入了锁存区。所以记住一句话声明赋值是“默认的初始数据”而不是“一定会生效的铁律”。2.2 首扫描的执行时机、可见性和扫描顺序首扫描则完全是另一套逻辑。SM402 触发时你的程序才真正开始执行初始化动作。这个动作发生在“第一个扫描周期内”而不是“第一个扫描周期前”。因为是程序行为它严格受程序文件执行顺序的影响。三菱 CPU 里的程序文件是有执行顺序的比如 P0 先执行、P1 后执行依次往下。如果你把首扫描初始化写在 P2 程序里而 P1 在扫描顺序上排在 P2 前面那么第一个扫描周期里P1 读到这个变量的时候P2 的首扫描代码还没跑P1 看到的仍然是“声明赋值”的初值。这就产生了很实际的乱子同一个变量在第一个扫描周期的不同程序块里看到的初值不一样。还有一个细节容易被忽略SM402 的“一个扫描周期”指的是 CPU 系统层的一个扫描周期。如果 CPU 一直处于 RUN 状态通过 GX Works3 做在线修改、程序写下载SM402 不会随意重新触发。只有真正执行了 STOP→RUN 转换或 CPU 复位它才会再次 ON。调试时别把“首扫描”当作“每次修改程序后都重新初始化”。2.3 一张表讲透两者的核心区别对比项声明赋值首扫描初始化生效时机扫描开始前系统加载第一个扫描周期内程序执行是否占用扫描周期不占用占用正常程序扫描时间依赖因素初始值模式、锁存范围程序文件执行顺序、SM402 状态可编程性固定值不支持复杂判断可在条件里判断支持后续状态机可被程序覆盖会被程序后续赋值覆盖执行顺序靠后时可能看不到断电保持处理受锁存和初始值模式制约可读取保数据再决定写什么常见用途默认值、冷启动基线模块初始化、运行状态恢复、启动握手核心一句话声明赋值管“变量出生时的值”首扫描管“程序开跑后第一次干活时的值”。前者是静态加载后者是动态逻辑。3. 实际场景选哪种、混着用怎么才不踩坑3.1 默认值归声明赋值条件初始化归首扫描我在项目里的习惯是分得很开的。如果只是为了给 HMI 一个默认显示值、给报警计数器一个默认 0直接在声明区赋值干净利落代码上一眼就能看懂。但如果初值要根据现场情况来定比如设备停机时有没有保持数据、过流次数要不要保留、当前操作模式要不要重新读取声明赋值就太“死”了必须在首扫描里写判断逻辑。举个例子。一台设备有报警累计次数要求“设备断电后累计次数保持不变但操作工按下清零按钮后重新计数”。如果直接在声明区写wAlarmCnt : 0配合“每次 RUN 初始化”参数就会把上次累计清的干干净净保持功能完全失效。正确做法是声明区给默认值首扫描里根据一个“是否允许清零”的保持标记来决定要不要重置// 声明区给默认值 VAR wAlarmCnt : WORD : 0; END_VAR // 首扫描里根据保持标记决定是否清零 IF SM402 AND NOT bKeepAlarm THEN wAlarmCnt : 0; // 不带保持的情况下清零 END_IF;这里bKeepAlarm本身是锁存保持变量断电前它是 TRUE断电重启后还是 TRUE所以首扫描不会再去动wAlarmCnt累计值自然就保住了。3.2 断电保持变量最怕“每次 RUN 都初始化”实际项目里最容易出问题的就是断电保持数据叠加了“每次 RUN 初始化”。比如计数器要求断电后继续从上次值跑你声明区又写了iCnt : 0CPU 参数还选的是“每次 RUN 时固定值初始化”结果就是每次从运行状态停到重新运行计数都会清零。这种问题光看代码看不出毛病因为代码逻辑都是对的问题出在系统配置层面。遇到这种情况一般有两个方向。要么把变量设成锁存属性CPU 参数改成“仅初次 RUN 时初始化”让声明赋值只在第一次上电时生效要么就不在声明区写初值完全交给首扫描去控制。我个人更推荐后者因为锁存范围和 CPU 参数通常由整个工程的软元件分配决定牵一发动全身而首扫描里用条件判断可以做到“只有第一次上电才清零其余上电全部保持”VAR bEverRun : BOOL : FALSE; // 这个变量必须锁存保持 END_VAR IF SM402 AND NOT bEverRun THEN iCnt : 0; // 第一次上电才清零 bEverRun : TRUE; // 标记已经跑过 END_IF;bEverRun断电后保持为 TRUE下次上电首扫描走到SM402 AND NOT bEverRun时条件不成立初始化代码不会执行。这是声明赋值做不到的“有记忆的初始化”。3.3 FB 多实例里初值的坑FB函数块的内部变量和三菱的局部标签一样需要区分清楚。以常见的声明区写法为例FUNCTION_BLOCK FB_InitTest VAR iLocal : INT : 0; END_VAR当你在设备里创建了这个 FB 的 3 个实例比如 FB1、FB2、FB3每个实例的iLocal初值都来自声明区的 0它们之间互不干扰这是实例静态存储的特性没有毛病。真正的坑在于FB 的VAR声明初值是“程序启动时一次性写入”的之后每次调用 FB并不会把iLocal重新设回 0。假设你写了一个计数 FB内部iLocal从 0 开始加每个扫描周期都调用一次。你以为它每次调用都是从 0 重新开始实际情况是它只从 0 开始一次之后一直在上次的基础上累加。要区分清楚“程序启动时初始化一次”和“每次调用时初始化一次”。如果需要每次调用都从零开始就别把状态放在 FB 内部声明区放在调用点的外部变量里。FB 内部做首扫描也要当心。SM402 是全局唯一的如果多个 FB 实例内部都写IF SM402 THEN那么这些实例会在同一个第一个扫描周期里全部执行初始化这在很多场景下没问题。但如果你希望同一个 FB 的不同实例在各自“首次使能”时才各自初始化一次SM402 就不够用了得在 FB 内部维护一个属于自己实例的标志位VAR bInited : BOOL : FALSE; // 实例自己的标志 END_VAR IF NOT bInited AND bEnable THEN // 实例真正的一次性初始化 bInited : TRUE; END_IF;因为bInited是实例静态变量每个实例都有一份所以能做到“每个实例各初始化一次”比全局 SM402 灵活得多。这个设计在多个轴控、多组模拟量通道的项目里非常实用。3.4 通信模块、轴控模块的上电时序还有一个实战里常遇到的问题MELSEC 上电后通信模块和智能功能模块的启动速度不一定和 CPU 同步。声明初值可以被程序立刻引用但这时候外部模块可能还没准备好。典型的例子是轴控模块比如 iQ-R 系列的 RD77MS上电后需要先建立模块通信第一个扫描周期就直接去写轴参数大概率写不进去。这种情况下首扫描的作用就不只是“给变量赋初值”了而是“给模块一个启动握手”。我的做法是首扫描里先布一个启动状态机IF SM402 THEN nBootStep : 0; // 上电启动步骤从 0 开始 bDeviceRun : FALSE; END_IF; // 模块就绪后再推进到下一步 IF nBootStep 0 AND bModuleReady THEN nBootStep : 1; // 在这里初始化轴参数或通信参数 END_IF;声明赋值只能保证 PLC 内部变量有一个初值它无法建立和外部设备之间的时序关系。首扫描虽然也只占一个周期但可以顺势拉起一套“启动状态机”让后续初始化步骤按模块状态逐个推进。这是声明赋值替代不了的。3.5 多程序文件执行顺序的建议如果你确认要用首扫描做初始化我有一条硬建议把初始化程序文件放在扫描执行顺序的最前面。在 GX Works3 的程序文件列表里调整执行顺序即可。让首扫描程序成为第一个被执行的程序块这样整个扫描周期里所有后续程序都能看到初始化后的值逻辑上最干净。如果你把首扫描放在中间或后面前面已经跑过的程序在第一个周期里用的还是声明初值整个工程就会时不时出现“第一个周期数据异常后面又正常了”这类烧脑问题。运动控制和通信处理的工程里这种时序问题往往比语法错误更难排查。4. 实操要点GX Works3 里的具体配置与常见排查4.1 声明区写法和标签编辑器初始值的关系GX Works3 里变量初值其实有两层一层是 ST 声明区直接写: 0另一层是全局标签和局部标签编辑器里的“初始值”列。两者最终都会生成初始化数据但呈现的位置不同。我个人的习惯是跟着程序走的变量初值写在 ST 声明区改程序块时不容易忘标签编辑器里的初始值适合作为整机级的默认参数比如 HMI 公共变量、配方变量方便集中管理。写声明区的时候有几个 ST 语法细节容易踩。十六进制必须写成16#FF而不是0xFF字符串必须单引号带长度的字符串三菱写法是STRING(32)。写错了编译会直接报错这类错误本身好查但新手往往被报错信息里的一堆文件路径唬住其实问题就在声明区那一两行。4.2 首扫描代码的组织方式首扫描代码最忌讳塞一大堆逻辑最后变成一坨“启动大杂烩”。我的做法是把首扫描里要做的事拆成三个顺序先复位所有运行标志和临时状态量比如bRunning、nStep再根据保持变量恢复上次运行的关键参数比如累加量、操作模式最后对通信模块、轴模块做启动握手或者把外部操作请求置位。每一类逻辑尽量单独做成一个 Function 或独立程序段别把所有东西都堆在IF SM402里。首扫描虽然只执行一次但它的可读性直接决定后续维护的人愿不愿意碰它。另一个小技巧首扫描不要只依赖 SM402。很多项目里我会再加一个自定义的“上电总复位”标签由系统首扫描置位在当前周期里自复位这样既保留了 SM402 的时序也方便日后做手动复位IF SM402 THEN bSysInit : TRUE; END_IF; IF bSysInit THEN // 初始化逻辑 bSysInit : FALSE; // 只执行一次 END_IF;哪天需要“不重启 CPU 就重新初始化全部变量”只要手动把bSysInit置 TRUE就能触发同一套初始化逻辑不需要去动首扫描代码。排查现场问题的时候这个自定义标志位特别好用。4.3 常见问题排查速查表这些年下来被问得最多的初值相关的问题基本都能套进下面这张表现象可能原因排查/解决声明初值没生效上电变量不是初值锁存范围包含该变量初始值模式是“仅初次 RUN”打开 PLC 参数确认初始值模式是“每次 RUN”检查标签锁存属性首扫描执行了但别的程序没看到首扫描程序文件执行顺序太靠后把首扫描程序块调到扫描顺序最前第二次 STOP→RUN 后变量不清零初始值模式为“仅初次 RUN”确认期望是保持数据还是清零改参数或改用首扫描无条件清零在线修改程序后 SM402 不再触发SM402 只在 STOP→RUN 或复位后触发需要手动初始化时用自定义bSysInit标志位触发FB 多个实例内部初始化被重复执行多个实例共享 SM402或实例变量被误用FB 内用实例自己的bInited标志位STOP→RUN 后保持变量被声明初值覆盖声明初值 “每次 RUN”初始值模式要保留保持值的话变量设锁存属性或初始值模式改“仅初次 RUN”字符串初值编译报错用了双引号而非单引号三菱 ST 字符串常量必须用单引号例如abc这张表是我每次给同事讲“初值问题”时都会贴出来的。多数问题不是语法问题是时序和属性设置问题。4.4 我个人在项目里的分流规则最后说下我自己写 MELSEC ST 程序时的一个不成文规则凡是“设备运行过程中要被程序修改、断电又要保持”的不依赖声明赋值放锁存用首扫描或启动状态机去判断是否要恢复凡是“纯显示、软元件默认值”这类声明赋值直接搞定简单直接凡是“和外部模块交互、有上电时序要求”的永远不要在声明区里想当然老老实实设计启动步骤。这样分完之后声明赋值和首扫描就不再是“二选一”的关系而是各管一头。声明赋值负责冷启动的基线首扫描负责运行条件和保持数据的碰撞。关于声明赋值和首扫描的这个问题我最后的体会是别把“初值”理解成一个值而应理解成“变量在时间轴上的一个起点”。声明赋值是系统给的起点首扫描是程序自己定义的起点两者的坐标不同自然会有偏差。调试时如果发现初值不对不要急着改代码先想清楚这个变量的起点到底应该是 CPU 启动那一刻还是程序逻辑第一次处理它的那一刻想通了问题基本就定位了。这个思路我在 Q 系列和 iQ-R 系列上都验证过FX5U 的 ST 也适用只是特殊继电器编号要换成 M8002其他机制相通。希望这篇能帮你少走点弯路。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进