
TIA博图里新建一个FB接口区自上而下排着Input、Output、InOut、Static、Temp、ConstantFC里还会多出最上面一行Return。这七行东西说简单也简单说容易翻车也是真的容易翻车。我见过太多项目图省事把中间变量一股脑全声明成Static单次调试跑得挺顺结果同一个FB被第二个地方调用的时候就开始出怪事也见过把InOut当成Input使在块里顺手改了参数调用方那边的变量莫名其妙跟着变了查了两天。我不想再给你一张什么变量干什么用的对照表——那种表随手一搜就有几十份看完该不会用还是不会用。真正的问题从来不是这个变量叫什么而是三个事它存在哪块内存里、值在什么时候被拷贝、编译器允许你干什么不允许你干什么。把这三件事吃透了七行接口变量的用法是自然推导出来的不需要背。这篇文章面向的是刚上手博图的人也面向从S7-200/300转过来、写过不少SCL但没系统梳理过块接口的人。全文用一个统一的思路贯穿先讲每种变量的物理位置再讲它的读写规则最后落到我到底该选哪个。中间会穿插几个我自己踩过的坑包括一个让我在客户现场熬到凌晨两点的输出保持问题。看完之后你至少能做到看到一段FB代码能判断它这么声明对不对自己写的时候能一次选对。1. 七种接口变量的本质差异存储位置决定了行为很多人学接口变量是从功能入手的——Input是输入Output是输出Static是静态变量。这种理解不能说错但它解释不了任何异常现象。为什么Temp不初始化就是随机值为什么FC里没有Static为什么OUT在某个分支没赋值就保持上一次的值答案全在这块数据放在哪个物理位置。1.1 从一次输出保持旧值的现场故障说起前几年做一个包装线的改造项目有个计算长度的FC七个输入、三个输出逻辑不复杂。调试的时候一切正常唯独有个输出——我们就叫它ActLength吧——偶尔会粘住明明这次算出来应该是0画面上的数字还是上一轮的值。单独测这个FC怎么都复现不出来。后来把调用它的那段逻辑翻出来看问题就在一个IF上ActLength只在Mode 1的分支里被赋值Mode 2的时候整个分支没碰它。当时的想法是没赋值就自然保持原样呗——结果确实保持原样了保持的是上一轮的值而不是这一轮的0。这个坑的本质是FC里的输出参数如果你在某个执行路径上没有给它赋值那么执行完这个FC之后调用方那个变量完全不变。它不是被置0也不是被置成上一次计算的结果而是彻彻底底没被碰过。FC没有自己的存储区它只是拿到了一张往哪儿写的地址条你不写那个地址上的数据当然一动不动。理解这一点之后正确的写法就出来了所有OUT都只在块的末尾、唯一的一个出口处赋值中间过程全部用Temp变量来算。这样不管走哪条分支出口都只有一条输出的确定性就有了。// 反面写法分支里直接写OUT IF #Mode 1 THEN #ActLength : #RawLength - #Offset; END_IF; // Mode2 时 ActLength 保持上一次的值这就是故障来源 // 正面写法过程用Temp出口统一赋值 #tmpLength : 0.0; IF #Mode 1 THEN #tmpLength : #RawLength - #Offset; ELSIF #Mode 2 THEN #tmpLength : #RawLength; END_IF; #ActLength : #tmpLength; // 唯一出口1.2 每块数据住在哪一张表把物理位置说清楚接口变量最反直觉的地方在于FC的Input和FB的Input其实不是一回事。同样的Input声明在FC里和FB里的落地位置不同行为也就有细微差异。接口行SCL声明区关键字数据落点块内可读块内可写多次调用是否保持InputVAR_INPUTFC局部数据/调用方地址FB背景DB是否FB的IN在背景DB里能看到当前值OutputVAR_OUTPUTFC调用方变量的地址FB背景DB无意义是未赋值则保持调用方旧值InOutVAR_IN_OUT基本类型同INOUT结构类型直接是指针是是否StaticVARFB的背景DB是是是掉电可配保持TempVAR_TEMP局部数据栈L栈是是否调用结束即失效ConstantVAR CONSTANT不占运行时内存编译期替换是否编译期固定ReturnFUNCTION xx : 类型FC的返回区—是否这张表里最值得盯的是第三列。Static和Temp的分界线本质上是背景DB和局部数据栈的分界线。背景DB是CPU装载内存工作内存里实实在在的一块数据区每一个FB实例独占一份掉电保持还能单独配置局部数据栈是CPU运行时的栈空间所有块共用一级调用压一层出栈就没了。1.3 为什么FC天生装不下Static经常有人问我想在FC里记住一个标志位声明成Static行不行答案是不行FC的接口区根本不给你Static这一行。原因很直白Static变量需要一个稳定的、每次调用都在同一个位置的存储空间。FB有背景DB每个实例一块位置固定所以能放。FC没有背景DB——FC的设计初衷就是纯计算输入进去、算完输出不带走一片云彩。没有落脚的存储区Static自然就无从谈起。这也解释了另一个常见困惑为什么在FC里做边沿检测不靠谱因为边沿检测需要一个上一次的信号状态而FC里唯一能放中间结果的地方是TempTemp每次调用都是新的或者说是栈上残留的边沿检测的记忆根本存不住。要让FC做边沿检测唯一的办法是通过InOut参数从外面传一块记忆位进来——把状态存储的责任交还给调用方。2. Input和Output值传递的时机以及条件赋值的陷阱Input和Output是最常用的两行也最容易想当然。大部分人对它们的理解停留在输入不能改、输出只能写这条规则没错但它只是表层。真正会咬人的是值什么时候被拷贝这件事以及结构类型和基本类型在传递方式上的天壤之别。2.1 Input在块内为什么是只读的先说规则Input参数在块内部是只读的你写#MyInput : 5;编译器会直接报错。这不是博途在刁难人而是这个赋值本身没有意义——你改的只是块内部看到的一个副本或者副本的引用调用方那边的变量不会因此变化。与其让程序出现我明明改了但外面没变的幻觉不如在编译期就拦下来。那既然Input改了也没用为什么编译器不允许因为歧义太大。如果允许写而外部不变那这个变量在块内部就有了两种语义——前面读到的是外部传进来的值后面读到的是自己刚写的值。逻辑一旦出现这种分裂维护就是灾难。有个细节值得注意Input参数没接实参的时候块内部拿到的是接口声明里填的起始值Start value不是0也不是上一次的值。所以在写一个通用FB的时候给每个IN参数填一个有意义的起始值能省掉很多为什么这个参数是0的困惑。博途在调用块时如果发现某个参数没连会给一个编译提示别忽略它。2.2 基本类型按值拷贝结构类型按引用传递这是七种接口变量里最容易被忽略、但影响最大的一条规则。对于BOOL、INT、DINT、REAL、TIME这类基本数据类型IN/OUT/InOut参数在传递的时候是做值拷贝的调用开始时调用方的实际参数值被复制到被调块自己的数据区块执行完再把结果拷回去。这意味着块内部操作的是一份快照和外部变量在调用期间是解耦的。对于STRING、ARRAY、STRUCT、UDT、DATE_AND_TIME这类结构类型博途不会做拷贝而是直接传一个指针过去块内部访问的就是调用方那块内存本身。这两者的差异会带来几个很实际的后果性能一个1000个REAL的数组如果按值拷贝每次调用都要搬4KB数据。博途选择传指针正是为了避免这种开销。所以在高频调用的块里把大结构声明成Input是安全的不必担心性能。数据一致性基本类型参数在块执行期间不会因为外部变化而变结构类型参数是活的如果在块执行期间别的代码改了那块内存你读到的是新值。单线程的PLC扫描周期里这种情况不多但在中断OB里调用同一个块时就要小心了。InOut的必要性正因为结构类型是指针传递你才能在InOut参数上对一个大数组做原地修改。如果是值拷贝改完了还得拷回来成本高还容易出错。还有一个后果比较隐蔽大结构类型的IN参数虽然声明成只读但它并不占用局部数据栈空间。这在排查局部数据不够的问题时是个有用的线索——真正的L栈消耗大头往往是Temp变量和嵌套调用深度而不是IN参数本身。2.3 Output的写规则可以分多次写但出口必须确定Output在块内是可写的而且可以写很多次。中间过程写几次无所谓最后一次写进去的值就是最终结果。但这里有个陷阱前面提过了如果执行路径上没有走到任何一次赋值调用方那个变量就保持原值不变。对于FB来说情况稍微复杂一点。FB的Output存在背景DB里调用开始时背景DB里的OUT保存着上一次调用的结果。如果这次调用没给它赋值那么调用结束时背景DB里的旧值还是会被写回实际参数。所以现象上依然是输出带着上一次的值出来了。现实里的处理办法有两个我一般两个都用第一个是在块的入口把所有OUT清零。这行代码看着傻但它的作用是建立一个确定的基线后面无论走哪条分支输出都不会是上次的残留。// FB入口先清零建立确定基线 #ErrorCode : 0; #OutValue : 0.0; #Done : FALSE;第二个是用Temp算完末尾统一赋值。这种方式在逻辑分支多的时候特别清晰调试的时候打开块在线监视能一眼看出OUT是在哪一步被写进去的。注意给OUT清零这件事在块入口做一次就够了不需要每个分支都写一遍。分支里重复写清零代码会让块的执行路径变得难以追踪。2.4 一个可以直接抄的FC骨架把上面的规则落成一个模板写FC的时候按这个骨架填能避开九成的接口问题FUNCTION CalcLength : Void VAR_INPUT RawLength : Real; // 原始值 Offset : Real : 0.0; // 带默认值不连也能跑 Mode : Int; END_VAR VAR_OUTPUT ActLength : Real; // 结果 Valid : Bool; // 结果是否有效 END_VAR VAR_TEMP tmpLength : Real; // 中间结果不出块 tmpValid : Bool; END_VAR BEGIN // 1. 出口变量先建立确定基线 #tmpLength : 0.0; #tmpValid : FALSE; // 2. 全部计算过程都在Temp上做 CASE #Mode OF 1: #tmpLength : #RawLength - #Offset; #tmpValid : TRUE; 2: #tmpLength : #RawLength; #tmpValid : TRUE; ELSE #tmpValid : FALSE; END_CASE; // 3. 唯一出口统一赋值 #ActLength : #tmpLength; #Valid : #tmpValid; END_FUNCTION这个骨架的价值不在于格式好看而在于它把确定性问题从逻辑里剥离出来了。逻辑怎么改都行出口永远是确定的。3. InOut什么时候非它不可什么时候是过度设计InOut这个名字起得很实在——既能读又能写。但正因为太自由它也是最容易被滥用的一个。我见过有人把所有的Input都改成InOut理由是万一以后要在块里改呢。这种写法短期省事长期是给维护埋雷。3.1 InOut和Input加Output到底差在哪从功能上看InOut似乎就等于一个Input加一个Output。但两者的语义差别很大。用InputOutput表达的是我读一个值进来算出一个新值给你。这是单向的信息流调用方一眼就能看懂数据从哪来到哪去。用InOut表达的是我把你给我的这块数据改了。这是双向的调用方必须意识到传进去的这个变量调用之后可能变了。所以在选型上我的原则很明确能用InputOutput表达的就不要用InOut。只有当读-改-写必须在同一块内存上原地完成时InOut才是唯一正确的选择。典型场景有三类第一类是大结构的原地修改。比如一个FB负责往一个1000个元素的数组里追加数据。这个数组如果走Output每次调用都要拷贝一部分出去代价太大走InOut直接对原数组操作效率最高。第二类是跨调用的状态维护。前面提到的FC边沿检测就是典型例子——记忆位必须由调用方提供FC通过InOut读写它。第三类是需要读写同一个变量的算法。比如一个滑动平均滤波每次进来一个新值要把历史缓冲区的数据整体前移一位再把新值放到末尾。这个缓冲区必须是InOut因为它是读出来、改一改、再放回去。3.2 结构类型走InOut时实际发生的是原地操作这一点在调试时特别重要。当一个ARRAY或者UDT通过InOut传进块里块内部操作的和你在线监视看到的是同一块内存。你在块里改一个元素调用方那个数组立刻跟着变不需要等块执行结束。这有时候会造成我还没调用完数据怎么就变了的错觉。但换个角度看这正是InOut的价值所在大数据的处理不需要来回复制效率最高。反过来说如果你希望块内部对数据的修改是局部的、可控的那就不要用InOut用Input接进来在Temp里做一份副本处理完了通过Output输出。代价是拷贝开销收益是数据隔离。提示判断一个参数该用Input还是InOut问自己一句——调用方期望这个变量在调用之后发生变化吗如果答案是不期望那就用Input。3.3 别名问题同一个变量接了两个InOut参数这是一个真实存在但很少有人提前想到的坑。假设你写了一个函数签名是这样的FUNCTION Swap : Void VAR_IN_OUT A : Real; B : Real; END_VAR BEGIN #A : #A #B; #B : #A - #B; #A : #A - #B; END_FUNCTION正常情况下这个函数能正确交换A和B。但如果你调用的时候写成Swap(MyVar, MyVar)把同一个变量接到了两个InOut参数上结果就成了MyVar 0——因为A和B指向的其实是同一块内存。基本类型参数因为做的是值拷贝两个形参在块内是两份独立的副本所以在块执行期间不会互相干扰虽然出口的时候会互相覆盖结果同样是错的。结构类型的InOut参数因为是指针问题会更早暴露——你在块里改A的时候B当场就变了。这类问题编译器不会给你任何提示只能靠写代码的时候留个心眼。如果一个块有两个以上同类型的InOut参数在块的注释里明确写出来这两个参数不能传同一个变量这是最省事的预防办法。3.4 InOut参数的实参要求InOut有一个硬性要求跟Input不一样InOut参数不能接常量也不能接表达式必须接一个可写的变量。因为它需要一块能被写入的内存地址常量放在只读区表达式根本没有地址。另外InOut参数不允许留空。博途在调用块的时候如果发现InOut没连实参编译会直接报错而不是警告。这个设计和它的本质是一致的——InOut本质上是一个引用没有指向目标的引用没有意义。4. Static和Temp一个住在背景DB一个住在栈上这两行的区别是新手最容易搞混的。表面上看都是块内部的变量但它们的生命周期、存储位置、可用范围完全不同。我经常用一个类比来解释Static像是你自己家里的储物柜钥匙只有你有东西放进去下次还能找到Temp像是酒店房间的抽屉你住的时候能用退房之后下一个住客可能看到你留下的东西也可能啥都没有。4.1 StaticFB的状态记忆体Static变量只存在于FB里落地在背景DB。它有四个特点**第一跨调用保持。**这是Static存在的根本意义。同一个FB实例被调用一百次Static变量的值是一脉相承的。这就是为什么FB能实现状态机、累计计数、报警锁定这类需要记忆的功能。**第二每个实例一份。**这是FB区别于FC的核心优势。你写一个电机控制的FB用五个不同的背景DB调用五次每个实例的Static变量是完全独立的五份数据互不干扰。用FC实现同样的功能你就得给每个电机单独准备一套记忆变量那场面很难看。**第三掉电保持可以单独配置。**在S7-1200/1500的优化块上FB接口的Static区有一列保持性Retain勾上之后背景DB里对应的静态变量掉电后重新上电值不会丢。如果你的CPU还是经典的S7-300/400这个设置是在背景DB里做的不在FB接口上。这个差异在移植老程序的时候要特别注意。第四改动接口会导致背景DB重建。这条是我最想强调的实战经验。在调试阶段如果你回头去给FB加了一个Static变量或者删了一个博途会重新生成背景DB原来积累下来的所有静态变量值会被复位成起始值。我在现场吃过这个亏——设备运行了三天积累的一批统计数据因为改了一个接口变量全没了。所以设备运行期间不要动FB的接口调试用的中间变量尽量用Temp不要图方便往Static里塞。4.2 Temp必须先写后读否则就是随机值Temp的规则只有一条但违反这条规则的代价很大读之前必须先写。Temp落在局部数据栈上博途不会自动给你清零。为什么因为清零是要花时间的——每次调用都把这块内存清一遍在高频调用的块里是纯粹的开销。博途选择了不管把责任交给你。不初始化的后果是你读到的值是上一次使用这块栈空间的那个块留下的值或者是更早的残留。这个值看起来可能很合理——比如恰好是0或者是某个附近的值所以你的程序在测试的时候可能跑得好好的换个调用顺序就出问题。这类bug最难查因为它不可复现。// 危险写法 VAR_TEMP tmpIndex : Int; END_VAR BEGIN // 忘了初始化下面的FOR可能从任意值开始 FOR #tmpIndex : 1 TO 10 DO ... END_FOR; END_FUNCTION说实话FOR循环忘了给循环变量初始化是我见过最常见的Temp相关bug。因为FOR语句的起始值表达式里如果写的是Temp变量它就不会自动赋初值。养成习惯块的开头把所有Temp变量先赋一遍初值这几行代码的值远比它看起来大。4.3 Temp的一个反直觉特性同一层调用里它会记住值这条是我在某次排查边沿检测失效的时候才真正想明白的。局部数据栈是复用的。一个FC从OB1里被调用第一遍执行完出栈第二遍调用再压栈的时候栈指针回到了同一个位置。所以这两次调用Temp变量用的其实是同一块内存。后果是如果你在FC里用Temp做边沿检测写了个Rising : Signal AND NOT LastSignal; LastSignal : Signal;然后把这个FC在同一次扫描周期里连续调用两次第二次调用读到的LastSignal很可能就是第一次留下的值——因为栈位置相同。这就造成了一个假象这个FC好像能用。但这个假象极其脆弱。只要调用深度变了比如原来从OB1直接调后来改成从另一个FB里调或者两个调用的顺序调换了栈的位置就变了读到的东西就完全不一样边沿检测立刻失效。**所以Temp绝对不可以承担任何需要跨调用保持的职责。**这不是不推荐是一定不行。4.4 局部数据栈不是无限的Temp用得爽但它消耗的是CPU的局部数据栈。这个栈有个硬上限而且每层调用都要压一层。一个典型的失控场景是主程序里调用FB1FB1里调用FB2FB2里调用FB3……每层都声明了十几个Temp变量再加上参数传递的开销很容易就把栈吃满。症状是编译或者下载的时候报局部数据相关的错误或者程序运行时直接进故障。几个实际的控制手段Temp变量按需声明别把整个项目里可能用到的中间变量都在一个块里声明一遍。用完的Temp删除掉不影响逻辑。控制嵌套深度。如果发现调用链深到四五层了回头看看是不是可以把某些逻辑合并。在块的属性里看局部数据需求量。博途会显示每个块的局部数据占用几个大块叠起来是个什么量级心里要有数。调用块的属性里也能看到整条调用链的局部数据需求。注意Temp变量不在任何DB里所以打开背景DB是看不到它的。要观察Temp的当前值只能在这个块自己的在线监视界面里看。这一点在排查问题时很容易忘记白白在那儿翻背景DB。5. Constant和Return一个在编译期就消失一个是函数的身份标识这两行平时用得少但用对了能省很多事用错了则会产生我改了参数怎么没生效这类诡异问题。5.1 Constant块内的编译期常量Constant声明在接口里只有这个块自己能看见外部访问不到。它的特点是不占用任何运行时内存编译器在生成代码的时候直接把它替换成字面量。那它和直接在代码里写死了有什么区别区别在于可维护性。假设你的块里有个上限值100在五个地方用到了。写成VAR_TEMP或者直接写100将来要改成120你得改五处写成Constant改一处就行。Constant还有几个直接写死做不到的用法用作数组下标上限。声明一个数组的时候长度可以用Constant来指定这样长度调整的时候只需要改一处。用作CASE分支的标签。CASE语句的分支值必须是编译期常量Constant满足这个要求普通变量不行。用作有语义的运算符。比如给状态机的状态码起个名字ST_IDLE : 0、ST_RUN : 1代码可读性立刻上一个台阶。一个需要留意的地方Constant的值是在编译期固化的改了Constant必须重新编译所有使用它的块。如果你改了Constant但只下载了FB本身而没有重新编译调用它的地方可能会出现新旧值混用的情况。养成习惯改Constant之后执行一次全部重新编译。顺带说一个和它互补的东西全局常量。博途的PLC变量表里有一列常量勾上之后这个PLC变量就变成了全局的编译期常量所有块都能用且不占内存。项目里那些跨块使用的固定值比如工位数量、气缸数量、通信超时时间放全局常量比放在某个FB的接口里合适得多。5.2 Return只有FC才有的一行FB的接口区里没有Return这一行。原因和Static一样——FB有自己的背景DB需要往外传东西声明一个Output就行不需要返回值这个东西来承担。FC有返回类型这一行就叫Return。在这个单元格里填一个数据类型比如Int、Real、Bool甚至是自定义的结构类型。填Void就表示这个函数没有返回值。在SCL里返回值的赋值方式是直接把函数名当变量名用FUNCTION GetMax : Int VAR_INPUT A : Int; B : Int; END_VAR BEGIN IF #A #B THEN GetMax : #A; ELSE GetMax : #B; END_IF; END_FUNCTION在LAD/FBD里调用带返回值的函数块上会多出一个和返回类型对应的输出引脚把这个引脚接到变量上就能拿到返回值。这里有个初学者特别容易搞混的地方值得单独拎出来说。5.3 SCL里的RETURN语句和返回值是两码事SCL里有一个关键字叫RETURN写出来是这样的IF #Error THEN MyFC : -1; // 设置返回值 RETURN; // 立即结束这个块 END_IF;RETURN;是一条控制流指令作用是立即跳出当前块类似C语言里的return;。它和接口上那一行Return没有直接关系只是名字撞了。这个撞名造成了大量的困惑。有人以为执行了RETURN;就会把某个值返回出去实际上不会——你必须在RETURN;之前用函数名把返回值赋好。这两个操作是分开的漏掉任何一个都会出问题。顺便说一句SCL里的RETURN;是从较新版本才开始支持的。如果你在维护一个老项目发现编译报错先确认一下用的版本。5.4 返回值没赋值、没使用会怎样还有两个和Return有关的常见情况返回值在某个分支上没赋值。和Output一样如果某条执行路径上没有给函数名赋值那么返回的就是一个不确定的值。FC没有存储区来记住上一次的返回值所以这个不确定值可能来自累加器残留或者其他运行时数据完全不可控。解决办法还是那个老套路在块开头先给函数名赋一个确定的默认值。GetMax : 0; // 开头先定基线返回值没被使用。如果你调用了一个带返回值的函数但没有把返回值接到任何地方博途通常会给出一个提示。这个提示不是强制性的编译也能过但如果这个返回值是错误码忽略它就等于丢失了错误信息。项目里做一个统一的约定凡是返回错误码的函数调用处必须处理返回值比事后一个个排查要省事得多。6. 选型实战从接口变量反推FB和FC该怎么选讲到这里七种接口变量的机制基本都过了。最后我想把它们拉回到一个更实际的问题上面对一个具体需求我到底该怎么拆块、怎么声明接口。这个问题没有标准答案但有一些可以复用的判断路径。6.1 什么情况下必须用FB判断标准其实只有一个这个功能需不需要记住上一次的状态而且这个状态要按调用实例分别保存。需要就用FB。典型的场景包括状态机。设备的运行/暂停/报警/复位每一个状态都要记住。计数器、计时器。累计产量、累计运行时长、报警持续时间。边沿检测。需要一个静态位来存上一次的信号状态。需要多实例复用的设备控制。同一种设备在产线上有多台逻辑完全一样用FB加多个背景DB是最干净的做法。写FB的时候接口变量的分配我有几个固定习惯所有需要跨调用保持的东西放Static每次调用都要重新算的中间结果放Temp和外部交换的数据放Input和Output不要图省事直接操作Static。6.2 什么情况下FC反而更合适如果一段逻辑就是纯粹的计算——给几个输入、算出一个结果、不记住任何东西——那用FC比用FB好。理由很实在不占背景DB。背景DB是要吃CPU内存的一个复杂FB的背景DB可能好几百字节。项目里几百个背景DB叠起来对内存紧张的CPU来说不是小数目。没有实例管理的负担。FB要选背景DB、要建多实例、要处理实例之间的隔离FC不需要。跨项目复用更简单。一个纯计算的FC拷贝到别的项目里只要接口对上就能用不用担心背景DB的版本问题。典型适合FC的场景单位换算、量程转换、字符串拼接、CRC校验、数组求最大最小值。这些逻辑的共同点是输入确定输出就确定。6.3 用背景DB和在线监视快速定位接口问题说一个很实用的调试技巧当你不确定某个FB被调用时的输入输出到底是什么直接打开它的背景DB在线监视。FB的背景DB里Input、Output、Static这三类变量的当前值都能看到。输入对不对、输出算得对不对、状态变量走到哪一步了一目了然。这比在监控表里一个个加变量快得多尤其当这个FB被调用在很多地方的时候。不过有几个东西在背景DB里是看不到的想观察的东西能不能在背景DB里看到正确的观察方式Input参数能打开背景DB在线监视Output参数能打开背景DB在线监视Static变量能打开背景DB在线监视InOut参数基本类型部分能建议直接监视实际参数所在的变量Temp变量不能只能打开块本身用块的在线监视Constant不能不占内存看接口声明InOut参数有点特殊如果它指向的是一个全局DB里的数组那在背景DB里看到的是个引用真正的数据要看那个全局DB。这也是为什么用InOut的时候块的注释里写清楚这个参数指向哪个存储区很重要。6.4 常见的接口误用和对应症状最后把前面散落的一些坑收拢成一张表。调试的时候如果遇到类似症状可以对照着查。症状最可能的原因处理办法输出偶尔保持上一次的值OUT在某个分支没被赋值入口清零或改用Temp计算后在末尾统一赋值边沿检测偶尔失效FC里用Temp存上一次的信号状态改用InOut传记忆位或改成FB用Static数值偶尔是离谱的值Temp没初始化读到了栈上的残留块开头批量给Temp赋初值改了FB接口后数据全丢背景DB重建Static被复位成起始值运行期间不要动接口需要保持的数据放全局DB调用同一个FB两个实例互相干扰误把状态存到了FC的Temp或者全局变量上确认所有状态都在Static里编译提示局部数据不足Temp声明过多或者调用嵌套太深精简Temp合并嵌套层数两个InOut参数传了同一个变量结果不对别名问题传不同的变量或者在块注释里写明限制改了Constant但行为没变没有重新编译使用它的块改常量后做一次全部重新编译我自己在实际项目里的体会是这七行接口变量的选择短期看是能跑就行长期看决定了一个程序后面好不好维护。前期多花十分钟想清楚哪个变量该放哪儿后面能省掉的是成倍的调试时间。尤其是在设备已经在客户现场跑起来之后再回头改接口那成本和风险是另一个量级的事情。所以我的建议是从写第一个FB开始就把存储位置决定行为这条思路用起来别等到出问题了再回头补课。