在 TIA 博图里点开任意一个 FB 或 FC接口区最上面那一排 Input、Output、Inout、Static、Temp、Constant、Return几乎是每个做西门子 PLC 的人每天都要摸的东西。但说实话能把这几类参数全都用对、用顺的人真不多。我这些年给别人收拾过的项目里功能块跑起来没报错、但后台一团糟的情况太常见了背景数据块被 Input 和 Output 撑得比程序还大Temp 变量被当成断电保持位用FC 的输出参数在调用处悬空不接在线一监控全是随机数还有人把 Static 当成断电保持的代名词结果设备一断电参数全丢。这篇文章就把这七类接口参数从头到尾捋一遍——它们各自存在哪儿、什么时候会被复制、什么时候是传引用、编译期发生了什么、哪些坑是新手几乎一定会踩的。不管你是刚学博图、还在照着视频抄程序的入门选手还是写了几年 FB、接口设计一直凭感觉的中级工程师看完至少能明确一件事接口区里每一个格子都不是随便填的。1. FB 与 FC 到底差在哪不讲清这个参数永远用不明白1.1 FB 有记忆FC 是一次性的讨论参数之前得先把 FB 和 FC 的底层存储模型说透因为七种接口参数的行为差异一大半都来源于这里。FBFunction Block功能块在调用时会自动生成或者绑定一个背景数据块业内也叫实例 DB、Instance DB。这个 DB 就是 FB 的记忆体块里所有非临时性质的东西——Input、Output、Inout、Static——最终都落在这一块 DB 存储区里。也就是说你第一次调用这个 FB 实例时给它赋的输入值只要下一次调用时在调用处不重新给它会一直保留着上次写进去的内容。FCFunction函数则完全相反。FC 没有背景数据块调用 FC 的时候系统从 CPU 的本地数据区也就是常说的 L 堆栈上临时划一块空间来放它的接口参数和 Temp 变量块执行完这块空间立刻被回收下一次调用可能被别的块占用了。这意味着 FC 是彻底的无状态块它进来的时候什么样取决于调用者给了什么它出去的时候留不下任何痕迹。同一段逻辑你用 FC 写十遍调用十次每次都是从零开始。这个差异带来的最直接的后果是FB 的背景 DB 是可以被外部读写的。你在 HMI 上想直接改某个设备的启动延时、想在监控表里看某个状态机的当前步序只要把这个 FB 实例的实例 DB 拖到 HMI 变量或者监控表里就能直接访问里面的 Input 和 Static 变量。而 FC 完全没有这个能力它的内部数据在块返回的瞬间就消失了你想看也看不到。1.2 选 FB 还是 FC判断标准其实只有一条网上关于什么时候用 FB、什么时候用 FC的说法五花八门有的按功能分、有的按调用次数分其实真正的判断标准只有一条这段逻辑需不需要跨扫描周期记住东西。需要记住的典型场景包括边沿检测必须记住上一次的输入状态、状态机必须记住当前步序、累计计数必须记住累加到哪了、延时和超时判断必须记住定时器实例、报警确认必须记住哪条报警已经确认过。这些一律用 FB把需要记住的量放进 Static 区。不需要记住的典型场景单位换算、模拟量标定、字符串拼接、CRC 校验、位组合逻辑、报警字按位打包。这些一律用 FC输入进来、算完、结果返回干净利落不留任何存储。我见过最常见的错误用法是所有块都建成 FB。新人觉得 FB 高级、能存东西于是连一个把摄氏度转华氏度的小函数都非要建成 FB。一个中等规模的项目这么干下来背景 DB 数量能翻两三倍程序编译慢、下载慢、在线监控卡而且每个 DB 都占着存储空间纯属浪费。反过来也有另一个极端把所有带状态的逻辑硬塞进 FC然后在外面的 DB 里手动传状态变量。这么做能跑但每次调用 FC 都要手动挂一堆状态引脚代码可读性极差想换个地方复用都难。提示新建块的时候TIA 默认是 FB。养成一个习惯先想清楚这个东西需不需要记住上一次的状态再决定点 FB 还是 FC。建成之后发现选错改块类型能改但接口区的内容要重新搬挺麻烦的。2. 七种接口参数逐个拆解2.1 Input 与 Output一对最容易被搞混的老搭档Input 是输入形参调用方传值进来块内部读。Output 是输出形参块内部写值调用方读走。听起来很简单但有两个细节几乎所有人都会踩。第一个细节是 Input 在 SCL 里可以被写。你在 SCL 代码里对 IN 参数赋值编译器通常不会直接拦你最多给个警告但运行时的行为会让很多人困惑值确实改了可是当块返回、你在调用方再去看那个实参的时候它一点变化都没有。因为这个赋值只作用在块内部的那份副本上。对 FB 来说这份副本存在背景 DB 里改了之后下次调用还能看到这个改变值——但它跟调用方传进来的那个变量已经没有任何关系了。我见过有人想用在 FB 内部改输入参数的方式实现反馈置位调用方的位调试半天看不到效果。想往调用方写数据只有一条路用 Output 或者 Inout。第二个细节是 Output 在 FC 和 FB 里的行为差异。FB 的 Output 存在背景 DB 中如果某次调用因为逻辑分支没走到、没有给这个 Output 赋值那么它保持的是上一次调用写进去的值也就是记忆型输出。这有时是好事有时是灾难。而 FC 的 Output 没有存储位置如果块内部某条路径没有赋值接收方拿到的就是它原本的值——如果接收方是个没初始化的临时变量或者 M 点那就是一个随机数。所以写 FC 的时候养成一个习惯在块的开头先把所有 Output 按最坏情况赋一遍初值把每条分支都覆盖到。在 LAD/FBD 里调用这两种块还有一个实用差别。FB 的输入引脚可以不连接不连接时会使用背景 DB 中当前保留的值这个特性特别适合做 HMI 可调的参数——工程师在 HMI 上把这个实例 DB 里的参数改了程序不用动下次调用直接生效。而 FC 的输入引脚在调用时一般不允许留空编译器会提示参数未连接。同理FB 的输出引脚不连接也不会报错值留在 DB 里FC 的输出引脚不接则会直接编译报错。2.2 Inout能读能写但不是万金油InoutIN_OUT是既能读又能写的参数行为上像是把 Input 和 Output 合体了。但它的价值远不止省一个引脚真正的核心在于传递方式。在 FC 中IN_OUT 参数是通过引用传递的用行话说就是把实参的地址交进来了块内部对它的读写直接作用在调用方那块原始存储区上。这一个特性决定了 IN_OUT 最重要的使用场景传递大块数据。比如你写一个 FC 用来处理一个有 100 个元素的 REAL 数组如果用 Input 参数接这个数组那么每次调用系统都要把这 100 个 REAL 复制一遍到 L 堆栈上800 字节的复制开销而用 IN_OUT 参数接传的只是一个地址块内部直接访问原始数组零复制。数组越大、调用越频繁这个差异越明显。也正是因为 IN_OUT 处理的是引用而不是副本它带来了一堆需要警惕的问题。首先传给 IN_OUT 的实参必须是一个可寻址的存储位置——DB 里的变量、M 区的变量、FB 实例里的 Static 或 Inout都行但你不能传一个常量或者一个字面量进去编译器会直接拒绝。其次实参不能是那种块执行完就失效的东西。最典型的坑是在 FC1 里调用 FC2把一个 Temp 数组传给 FC2 的 IN_OUT 参数。FC2 执行的时候这个数组还在没问题但只要 FC2 内部把这个指针存下来或者传递出去出了 FC1 之后就指向了一片无效区域后面再访问就是标准的内存乱读甚至停机。还有一个隐蔽的问题是两个 IN_OUT 参数指向了同一块内存。比如你写一个 FC接口是IN_OUT arr1、IN_OUT arr2本意是做一个数组对数组的搬运结果调用的时候不小心把同一个数组传了两遍。如果块内部是先读 arr1、再写 arr2的顺序看着没事但如果逻辑是逐元素边读边写就会出现一半数据被覆盖的情况而且这种 bug 极难定位因为在线监控的时候每一帧看着都像是正常的。在 FB 中IN_OUT 还有一个不太为人知的特性它也存在背景 DB 里。所以对一个 FB 来说把一个数组声明成 IN_OUT 而不是 Input并不省背景 DB 的空间省的是调用时的数据复制开销。这一点跟 FC 里的情况不一样要分清楚。注意IN_OUT 不是什么场景都该用。如果只是想传一个数值进来读一下、算个结果返回用 Input Output 语义更清晰也更容易被别人看懂。IN_OUT 留给那些进来之后要被改写、或者数据量大到不能复制的场景。2.3 StaticFB 专属的持久记忆体Static 只存在于 FB 里这是 FB 和 FC 在接口区最直观的差异。凡是需要跨扫描周期保留的东西——边沿检测的上一拍状态、状态机的当前步、TON 定时器实例、计数器实例、多重实例调用的子 FB——都放在这一区。很多人对 Static 有一个根深蒂固的误解以为放进 Static 就是断电保持。这是错的。Static 变量的持久只是相对于扫描周期而言它存在背景 DB 里CPU 运行期间一直有效程序跑到哪一轮它都在。但掉电之后能不能回来取决于 CPU 的保持性设置。在 S7-1200/1500 上默认情况下 DB 里的变量是不保持的掉电再来就是初始值。想要真正掉电保持需要在 CPU 属性里配置保持性存储区然后在 DB 的保持性列上逐个变量勾选设置。默认不保持这个设定救过很多人也坑过很多人。Static 区另一个容易被低估的用途是放多重实例。西门子的定时器、计数器本质上是 FB 或者系统功能块它们的实例也必须占存储。你可以为每个定时器单独建一个背景 DB文件树里就会多出一堆 DB更好的做法是在当前 FB 的 Static 区声明一个 TON_TIME 类型的 Static 变量然后直接调用它。这样一个设备的十几个定时器全都塞在它自己的背景 DB 里文件树干净DB 数量也少。这就是多重实例的写法工程实践中非常推荐。Static 里还可以放数组和结构体。比如做一台设备的运行统计用一个长度为 30 的 INT 数组记录最近 30 天的产量这个数组声明在 Static 区每次调用 FB 往里写就行。放到 Temp 里就完全不行因为 Temp 出了块就没了。2.4 Temp生命周期只有一个扫描周期的临时工Temp 变量分配在块的本地数据区随块调用而生随块返回而灭。它最大的特点也是最大的坑系统不会自动给它初始化。这一点必须反复强调。Temp 变量的初值是上一次占用这块内存的那个块留下来的垃圾数据可能是某个 REAL 的小数部分可能是某个状态机的步序也可能是看起来很像那么回事的一个整数。你在 FC 里声明一个 Temp 的 INT写完逻辑忘了初始化直接拿来做判断程序十有八九会在某个特定的时刻莫名其妙地跳闸一次然后你再复现又复现不出来。所以规则只有一条Temp 变量必须先赋值、后使用没有例外。Temp 的第二个特点是不适合装大东西。本地数据区的容量有限可以在 CPU 属性里看到上限值超了就会报本地数据栈溢出之类的错误。有些人不注意在一个 FC 里声明一个长度 1000 的 REAL 数组做临时缓冲一下就是 4000 字节的本地数据稍微复杂一点的调用链就爆了。这种大数据缓冲要么用 IN_OUT 参数把外部存储传进来要么干脆上 DB。Temp 也有它的正确用法。最典型的两个场景一是纯粹的过程计算中间量进来就用、算完就扔不跨语句保留二是配合循环使用的循环变量FOR 里的 i这类变量的生命周期本来就在块内。还有一类是临时拷贝比如你先从一个 Static 结构体里把某个字段复制到 Temp 做运算避免频繁访问背景 DB这也算合理优化但前提是复制完立刻就用。最后提醒一句关于边沿检测的经典错误有人在 FC 里用 Temp 变量实现上升沿检测思路是把上一拍的输入状态存起来跟这一拍比较。用 Temp 存这个上一拍状态是完全无效的因为上一拍的状态早就随着上次调用消失了你比较的永远是随机值和当前值。边沿检测的上一拍状态必须存在 FB 的 Static 里、或者存在 M 点、或者用系统提供的 R_TRIG/F_TRIG 功能块实例。2.5 Constant编译期就被替换掉的只读替身Constant 是在块的接口区定义的常量。定义的时候要写名称、数据类型和值比如名叫MAX_START_COUNT、类型 INT、值 100000。Constant 和普通变量的本质区别在于它不占存储。编译的时候代码里所有用到这个常量的地方会被直接替换成那个字面值运行时不存在去某个地址读常量这个动作。所以在 FB 里大量使用 Constant背景 DB 不会因为多了几个常量就变大运行速度也没有任何损失。Constant 的使用场景大多是那些写死了逻辑里到处要用的数字或字符串。比如一个状态机的状态码与其在代码里到处写 10、20、30不如定义成STATE_IDLE : 10、STATE_RUNNING : 20代码一眼就能读懂。又比如一个字符串提示信息定义成 Constant 之后在代码里引用名字以后想改文案只需要改接口区那一个地方。Constant 有一个限制必须知道它是只读的不能出现在赋值号左边。你写MAX_START_COUNT : 200;编译器会直接报错。想改它的值得回到接口区改然后重新编译下载。同一个项目里其实有两套常量体系容易混淆。一套是刚才说的块接口区的 Constant只在当前这个块内部有效出了这个块别的块看不到它也不能引用。另一套是PLC 变量表里的常量它是全局的任何块都能直接引用适合做那种全项目统一的参数比如设备的额定电压、通信超时时间。两套体系配合着用块内专用的放接口区跨块共享的放变量表。2.6 ReturnFC 的返回值别和 RETURN 指令搞混Return 这个参数只在 FC 里存在FB 是没有的。它的作用很直接给 FC 定义一个返回值调用方可以直接把 FC 当成一个表达式来用。这个设计的价值在于写复合算式的时候特别舒服。比如模拟量标定这个典型场景标定公式是工程值 (原始值 - 原始下限) × (工程上限 - 工程下限) / (原始上限 - 原始下限) 工程下限。如果做成一个带 Output 参数的 FC每次调用都要先定义一个接收变量再写调用语句两行而做成有 Return 值的 FC直接一行写进表达式就完事了甚至可以把好几个 FC 串联起来做流水线式的计算。在 SCL 里给返回值赋值的写法是给块名本身赋值。假设你要写一个 FC 叫Scale_Analog代码里最后一句就是Scale_Analog : 计算结果;。第一次写会觉得有点别扭习惯就好了。如果你在 SCL 编辑器里拿不准块名的正确写法可以把光标放到语句行输入块名几个字母之后按 Tab编辑器会自动补全。这里必须把两个容易混的东西分清楚。接口区里的 Return 是一个参数它定义了这个 FC 的返回类型和返回值。而 SCL 代码里的RETURN;是一条语句作用是提前结束当前块的执行跟返回值完全没有关系。我见过有人在 FC 里写完RETURN;以为把结果返回了结果调用方拿到的一直是 0。这两件事在中文里都叫返回但在 TIA 里是两码事。Return 的数据类型建议用基本数据类型REAL、INT、BOOL、DINT 这些都没问题。理论上也可以选 STRUCT 或者 UDT但我个人不建议——返回一个复杂结构会带来额外的数据复制而且调用方拿到之后还要拆包可读性反而变差。需要返回多个值的时候老老实实用 Output 参数更清楚。还有一个小细节值得一提在 LAD/FBD 里调用带返回值的 FC返回值会显示为块框上方的一个引脚。这个引脚不连接是允许的返回值直接被丢弃编译不会报错。这跟 Output 参数在 LAD 里必须连接的要求不一样很多人在两者之间切换的时候会犯迷糊。3. 参数到底怎么传值传递、引用传递与内存去向3.1 三种传递方式的对照理解了每种参数的定位之后还得知道它们底层到底是怎么搬数据的这直接影响程序的性能和稳定性。下面这张表是我自己的经验总结把七种参数的关键属性拉通对比了一遍。参数类型所在块存储位置传递方式跨周期保持是否占背景DBInputFB / FCFB 在实例 DBFC 在 L 堆栈基本类型值传递复杂类型可能复制FB 保持FC 不保持FB 占FC 不占OutputFB / FCFB 在实例 DBFC 在 L 堆栈值传递FB 保持FC 不保持FB 占FC 不占InoutFB / FCFB 在实例 DBFC 为引用引用传递为主FB 保持FC 不保持FB 占FC 不占Static仅 FB实例 DB内部直接访问保持掉电保持需单独配置占TempFB / FCL 堆栈不适用不保持不占ConstantFB / FC编译期替换无存储不适用不适用不占Return仅 FCL 堆栈值传递不保持不占从表里能看出几个关键结论。第一FB 的背景 DB 之所以容易变大是因为 Input、Output、Inout、Static 四类全都在里面只有 Temp 和 Constant 不占。第二FC 里唯一能省下本地数据区开销的手段就是用 IN_OUT 传大数组因为它是引用。第三掉电保持这件事跟参数类型没有直接关系跟 CPU 的保持性配置有关系。3.2 优化访问和标准访问带来的差异比想象的大TIA 里新建 DB 和 FB 的时候属性里有一个优化的块访问选项默认是勾上的。这个选项对参数的实际行为有影响值得单独说一下。在启用优化的块访问的情况下FB 实例 DB 里的变量不再按固定的字节偏移排列而是由编译器自己决定怎么放可能为了对齐插入空隙也可能把多个小变量紧凑地排在一起。这么做的好处是访问速度快、存储利用率高而且在后面给变量加新字段的时候不会打乱已有变量的地址修改接口不需要重新下载所有相关的块。缺点是你没法再对这个 DB 做绝对地址寻址比如DB1.DBD0这种写法就不行了必须用符号名。对绝大多数应用来说这个缺点根本不构成问题因为我们本来就应该用符号名编程。如果把优化访问取消DB 就变成了传统 S7-300/400 那种标准布局每个变量都有固定的绝对地址同时复杂类型的 IN_OUT 参数会以经典指针的形式传递。这种模式主要在两种场合需要一是和老程序或者第三方设备做地址级对接二是某些库需要绝对寻址。新项目我建议始终保持优化访问勾选状态除非有明确的理由。还有一个和优化访问有关的细节在优化的 FB 里IN_OUT 参数即使对基本数据类型编译器内部也是按引用的思路处理的所以你在块内对它赋值调用方立刻就能看到变化不存在值只改在本地的问题。这一点和 Input 的赋值行为形成鲜明对比也是很多人混淆的地方。3.3 各参数在内存里的实际归宿把视角拉高一点看一个 FB 实例在内存里大致长这样背景 DB 的最前面放着一组系统信息块头包含块号、时间戳一类的元数据往后依次是 Input、Output、Inout、Static 这几块区域。每一部分的大小取决于你在接口区声明了多少东西、分别是什么数据类型。一个 BOOL 在优化的块访问下可能只占一个位在标准访问下对齐后会占一个字节甚至更多。一个 REAL 固定占 4 字节。一个 STRING 占的字节数跟最大长度有关比如 STRING[20] 实际占 24 字节2 字节头部加 22 字节内容空间。一个 TON_TIME 定时器实例大概占 16 字节左右。这些数字在做背景 DB 容量估算的时候很有用。举个实际例子一个控制电机的 FB接口区里放了 12 个 BOOL 输入、6 个 BOOL 输出、4 个 REAL 参数、2 个 IN_OUT 的 REAL、3 个 TON_TIMER 实例、1 个整数状态机、1 个累计运行时间 REAL。粗算一下BOOL 加起来 18 个位、大约 3 字节REAL 部分 4 加 2 加 1 共 7 个、28 字节定时器 3 个 48 字节加上状态机 2 字节加上块头 40 字节左右一个实例大概 120 到 150 字节。一个项目里有 200 台电机就是 30KB 左右的背景 DB完全可以接受。但如果把每个 BOOL 都声明成 INT或者给每台设备都塞一个长度 100 的数组那数字就会变得很难看。提示想知道某个 FB 实例到底占了多少存储不用自己算。在项目树里选中这个 FB 右键分配资源或者看编译后的交叉引用也可以把实例 DB 打开在信息里能看到字节数。做设备量大的项目之前先估算一遍避免下载的时候才发现存储不够。4. 实操从零搭一个电机控制 FB 加一个标定 FC光说理论没意思下面用一个完整的小例子把七种参数都用一遍你可以直接照着敲。4.1 需求拆解与接口设计目标做一个电机控制 FB实现启动、停止、故障处理、状态反馈、运行时间累计再做一个模拟量标定 FC把 0 到 27648 的原始值换算成 0 到 100 的工程值。先设计 FB 的接口。我把每一类参数都刻意用上方便你对照。Input 区i_StartBOOL启动、i_StopBOOL停止、i_FaultBOOL故障信号、i_RunFeedbackBOOL运行反馈、i_RatedCurrentREAL额定电流。Output 区o_RunBOOL运行输出、o_FaultLatchBOOL故障锁存、o_StatusWordWORD状态字。Inout 区io_RuntimeAccREAL累计运行小时数。这个参数之所以用 IN_OUT 而不是 Static是因为我想让它的值存在 HMI 的 DB 里方便画面直接显示和历史归档FB 只负责改它。Static 区stat_RTrigStartR_TRIG启动沿、stat_RTrigStopR_TRIG停止沿、stat_TON_FeedbackTON_TIME反馈超时、stat_StepINT状态机步序、stat_StartCountDINT启动次数。Temp 区tmp_RuntimeDeltaREAL本次扫描的运行时间增量。Constant 区C_FEEDBACK_TIMEOUTTIME反馈超时时间值 T#3S、C_MAX_START_COUNTDINT最大启动次数值 100000。你会看到这里有一个刻意的设计状态量和边沿检测实例放 Static跨设备共享的累计值走 IN_OUT一次性的计算量放 Temp固定参数放 Constant。这个划分方式我在很多项目里用下来比较顺手你也可以按这个思路套自己的设备。4.2 SCL 代码实现下面是我在 TIA 里用 SCL 写的 FB 主体注释我写得比较细方便你逐行对照。// FB: Motor_Ctrl —— 电机控制功能块 // 边沿检测启动沿与停止沿必须用 Static 里的实例 #stat_RTrigStart(CLK : #i_Start); #stat_RTrigStop(CLK : #i_Stop); // 启动次数上限保护 IF #stat_StartCount #C_MAX_START_COUNT THEN #o_FaultLatch : TRUE; END_IF; // 状态机0停止, 10启动中, 20运行, 30故障 CASE #stat_Step OF 0: #o_Run : FALSE; IF #stat_RTrigStart.Q AND NOT #i_Fault THEN #stat_StartCount : #stat_StartCount 1; #stat_TON_Feedback(IN : TRUE, PT : #C_FEEDBACK_TIMEOUT); #stat_Step : 10; END_IF; 10: // 等待运行反馈超时报故障 IF #i_RunFeedback THEN #stat_TON_Feedback(IN : FALSE); #o_Run : TRUE; #stat_Step : 20; ELSIF #stat_TON_Feedback.Q THEN #o_Run : FALSE; #o_FaultLatch : TRUE; #stat_Step : 30; END_IF; 20: #o_Run : TRUE; IF #stat_RTrigStop.Q OR #i_Fault THEN #o_Run : FALSE; #stat_Step : 0; END_IF; 30: // 故障状态等外部复位 #o_Run : FALSE; IF NOT #i_Fault THEN #o_FaultLatch : FALSE; #stat_Step : 0; END_IF; END_CASE; // 运行时间累计假设本 FB 在 100ms 的循环 OB 中调用 IF #o_Run THEN #tmp_RuntimeDelta : 0.1 / 3600.0; #io_RuntimeAcc : #io_RuntimeAcc #tmp_RuntimeDelta; END_IF; // 状态字按位打包方便 HMI 一次读取 #o_StatusWord : 0; IF #o_Run THEN #o_StatusWord : #o_StatusWord OR 16#0001; END_IF; IF #o_FaultLatch THEN #o_StatusWord : #o_StatusWord OR 16#0002; END_IF; #o_StatusWord : #o_StatusWord OR SHL(INT_TO_WORD(#stat_Step), 8);这段代码里有几个地方值得单独说。stat_Step用 0、10、20、30 这种间隔编号是为了以后想在中间插状态的时候不用重排。tmp_RuntimeDelta只在IF #o_Run成立的那条分支里被赋值和使用一出块就作废这是 Temp 的典型用法。io_RuntimeAcc每扫描周期加一点点累加结果直接写在调用方传进来的存储位置上这就是 IN_OUT 引用的效果。再看标定 FC。这个 FC 用上了 Input、Temp、Return、Constant 四类参数。// FC: Scale_Analog —— 模拟量标定返回工程量 // 输入原始值、原始上下限、工程上下限 // 返回工程量 REAL // 用 Temp 做中间计算避免整数运算精度丢失 #tmpRawReal : INT_TO_REAL(#i_Raw); // 防止除零上限等于下限时直接返回工程下限 IF #i_RawMax #i_RawMin THEN Scale_Analog : #i_EngMin; RETURN; // 注意这是提前结束块的语句 END_IF; // 标定公式结果直接赋给块名返回值 Scale_Analog : (#tmpRawReal - #i_RawMin) * (#i_EngMax - #i_EngMin) / (#i_RawMax - #i_RawMin) #i_EngMin;注意这里的RETURN;是提前结束块的语句而Scale_Analog :才是给返回值赋值两个东西不要搞混。这是我在前面强调过的那一点写代码的时候看清楚。4.3 调用、监控与验证先看 FB 在 SCL 中的调用。注意输入用:输出用IN_OUT 用:因为它既要进去也要回来。// 在 OB1 或某个 FC 中调用电机 FB Motor_Ctrl_DB_01(i_Start : I_Start, i_Stop : I_Stop, i_Fault : I_Fault, i_RunFeedback : I_RunFb, i_RatedCurrent : 5.6, o_Run Q_Run, o_FaultLatch Q_FaultLatch, o_StatusWord MW100, io_RuntimeAcc : DB_HMI.Motor01_Runtime);这里io_RuntimeAcc接的是 HMI 数据块里的一个 REALHMI 画面可以直接显示它也可以在归档趋势里记录FB 每次调用都会往里累加两边看到的是同一块内存。再看 FC 的调用在 SCL 里可以直接写进表达式用起来很舒服// 把 IW64 的原始值标定成 0 到 100 的百分比 Valve_OpenPct : Scale_Analog(i_Raw : IW64, i_RawMin : 0, i_RawMax : 27648, i_EngMin : 0.0, i_EngMax : 100.0);上线验证的时候我一般按这个顺序走一遍。第一步把 FB 的实例 DB 打开在线监控stat_Step手动在监控表里给i_Start置 1看步序是不是从 0 跳到 10。第二步不给定反馈信号等 3 秒看是不是跳到了故障状态、o_FaultLatch是不是置位这一条验证的是 Static 里的定时器实例和 Constant 里的超时时间有没有生效。第三步把反馈信号置位看运行时间是不是在往上走这一条验证的是 IN_OUT 有没有真的写回 HMI 的 DB。第四步把 FC 的实参分别改成边界值比如原始值等于下限、上限等于下限看返回结果是不是符合预期这一条验证的是 Temp 初始化和除零保护。5. 常见问题与排查技巧实录5.1 常见问题速查表下面这张表是我这些年被问得最多的一些现象以及对应的排查方向。遇到问题先照着现象查比盲目翻程序快得多。现象可能原因排查方向FC 输出时有时无特定情况下是莫名其妙的数FC 的 Output 在某条分支没赋值打开 FC 逐条分支看是否有路径跳过了赋值FB 输出保持上次的值逻辑上应该清零FB 的 Output 存在实例 DB 里未赋值即保留在不需要保持的输出上开头先赋默认值程序运行一段时间后行为异常重启又正常Temp 变量未初始化读到了垃圾数据搜 Temp 区所有变量确认使用前都赋过值边沿检测偶尔漏掉一次触发用了 Temp 或 FC 存上一拍状态改用 FB 的 Static 或 R_TRIG/F_TRIG 实例背景 DB 特别大下载慢Input、Output、Inout、Static 全在 DB 里合并冗余参数考虑用 UDT 和数组掉电后参数全丢Static 不等于掉电保持在 CPU 和 DB 层面配置保持性编译报参数未赋值LAD 中 FC 的输出引脚未连接连接实参或改用 FBFB 内部改了 Input调用方看不到Input 是值传递改用 Output 或 Inout大数组传递后程序变卡复杂类型 Input 触发数据复制改用 Inout 传引用多个 FB 实例互相干扰共用了同一个背景 DB 或全局变量检查实例 DB 是否各自独立常量改了但程序行为没变Constant 在编译期替换改完必须重新编译下载修改后重新编译整个项目递归调用时参数错乱FB 的接口参数存在共享的实例 DB 中尽量避免递归或改用 FC 加参数传递5.2 几个只有踩过才知道的细节第一个细节跟在线监控有关。Temp 变量是可以监控的但你在监控表里看到的永远是这一瞬间的值因为下个扫描周期它就被别的块覆盖了。有人在监控表里盯着一个 Temp 看发现它一直在乱跳以为程序出问题了其实是正常的。想稳定观察一个中间量把它挪到 Static 或者临时挂到 M 区上。第二个细节跟未连接的输入引脚有关。在 LAD 里调用 FB如果某个输入引脚你留空不接它用的就是实例 DB 里的当前值。这个特性适合做在线可调参数但也埋了一个隐患有人本来想接一个信号画图的时候漏了编译不报错运行起来用的是上次调试时手改的值结果现场表现跟程序对不上。所以组态完之后我习惯把 FB 调用框挨个看一遍确认所有该接的引脚都接了。第三个细节是关于 IN_OUT 参数的实参类型。在 FC 里把一个 INT 数组传给 IN_OUT 参数实参的类型必须跟形参完全一致包括数组长度和元素类型。数组长度差一个元素编译直接报错不会给你兼容处理。这一点比某些高级语言要严格得多好处是不会出现传进去 10 个元素、取出来 20 个这种错位问题。第四个细节是 FC 里 Temp 变量的实际分配时机。有人以为只有调用时才分配 L 堆栈其实在块调用链上每一层嵌套的块都会占用自己的一份本地数据嵌套越深L 堆栈占用越多。在一个 FC 里再调用五层 FC每层都用 Temp 存了个大数组即使每个都不大加起来也可能超限。做复杂算法的朋友尤其要注意控制嵌套层数。第五个细节跟 Constant 有关。常量不能用于数组下标作为变量那种场景因为它不是一个存储位置。但常量可以用于数组声明时指定长度比如ARRAY[0..C_MAX_DEVICE] OF INT这个用法在做设备数量固定的项目时很好用改一个常量就能改整个数组的规模。注意上面这些坑里面Temp 未初始化是最致命、最难查的。我的习惯是每次写完一个块专门花两分钟把 Temp 区从头到尾看一遍逐个确认这个变量在哪一行被赋值、赋值之前有没有被读过。这两分钟能省下的现场调试时间可能是两天。6. 大型项目里的接口约定6.1 命名和分区规范比写代码本身更重要单机项目里接口参数怎么命名都无所谓反正只有你自己看。但几十上百个 FB 的项目里命名规范能直接决定后期维护的痛苦程度。我目前比较习惯的一套前缀规则是这样的输入用i_输出用o_输入输出用io_静态用stat_临时用tmp_常量用C_。这样一眼看过去就知道一个变量属于哪一类在 SCL 里写代码的时候也不容易搞混。除了前缀还有一个更重要的约定是接口区不要过于臃肿。我见过一个控制阀门的 FB输入区里塞了 40 多个 BOOL全是各种使能、屏蔽、模式选择。这种块调用的时候每次都要接一大堆引脚容易漏、容易接错。更好的做法是把相关的参数打包成结构体。比如把超时时间、最大开度、最小开度、动作次数上限这几个参数打包成一个 UDT 叫Type_ValveParam接口区声明一个这个类型的 Input 就够了。调用方传一个结构体变量进来参数改起来也集中。同样地输出区如果状态位很多可以打包成一个 WORD 或者一个 UDT 状态结构体。前面例子里的o_StatusWord就是这个思路底层 FB 把 16 个状态位按位塞进一个字上位 HMI 只需要读一个字然后用位选择功能显示不同指示灯通信负担小程序也清爽。6.2 什么时候该升级成 UDT、Array 和 Variant随着项目规模变大会遇到一些单靠基础数据类型解决不了的需求这时候要考虑升级接口的设计手法。第一种情况是同类设备成批出现。一台设备一个 FB 实例是可以的但 50 台一样的设备如果每台都用独立的一组输入输出变量交叉引用会乱成一团。更好的做法是用数组加循环调用定义一个ARRAY[1..50] OF Type_MotorParam的参数结构然后用 FOR 循环遍历调用同一个 FB。这样一是代码量少二是增删设备只需要改数组范围。多实例调用的时候每个设备对应一个实例实例也可以放在一个数组里。第二种情况是块需要处理不确定类型的数据。比如写一个通用的通信打包 FC收到的数据可能是 INT、REAL、STRING 中的任意一种长度也不一样。这时候就可以用 Variant 类型的参数它能接收任意类型的实参块内部用TypeOf、TypeOfElements这类系统函数判断类型再分支处理。Variant 很灵活但也很难调试因为它把类型检查从编译期挪到了运行期用错了要到运行时才报错。我的建议是能用具体类型解决的就别上 Variant只有在写通用库的时候才考虑。第三种情况是需要在多个 FB 之间共享一个复杂状态。比如一个工艺段的状态要同时被顺控 FB、报警 FB、HMI 通信 FB 访问这时候不要每个 FB 都通过接口传一份而是定义一个全局 DB把状态结构体放在里面每个 FB 直接访问这个 DB。这么做打破了块之间通过接口通信的纯粹性但在实际项目里非常常见也比较高效。关键是要给这个全局 DB 定好访问规则谁写、谁读写的地方要尽量少免得出现多个块同时改一个状态的问题。最后回到开头那个话题。七种接口参数看着多其实核心逻辑就三条需要跨周期记住的东西放 Static一次性的计算放 Temp需要影响调用方的量走 Output 或 Inout。把这三条记牢剩下的都是细节。我在带新人的时候一般会让他们先把一个电机控制 FB 反复写三遍——第一遍随便写第二遍按接口规范重写第三遍改成用 UDT 和数组管理多台设备。三遍写完接口区那一排格子基本上就刻在脑子里了后面再看到别人写的块一眼就能看出哪里的参数用错了类型。