STL编程中FC功能块的常见错误与排查指南

STL编程中FC功能块的常见错误与排查指南 干工控这些年STL写程序的人越来越少了但真正把设备跑得飞起的老师傅手里都有一本STL的账。这系列已经写到第6篇前几篇聊了寻址、定时器、位逻辑那些坑这篇专门说说FC——也就是Function功能块——在使用当中最容易被绊倒的地方。FC这东西看着简单一个子程序嘛但恰恰是这种看着简单的东西最容易在接口、临时变量、调用方式上埋雷而且雷多半是现场运行几天才炸。这篇东西不绕弯子直接把我自己踩过的坑、帮别人擦过的屁股、还有从老外程序里翻出来的毛病一并倒出来搞STL和FC的朋友耐心看能少熬几个夜。1. FC到底是个什么角色为什么它在STL里这么容易出错1.1 先搞明白FC和FB的本质区别很多刚接触STEP7的朋友会问FC和FB都有输入输出都能封装逻辑到底差在哪一句话FB有自己专门的背景数据块Instance DBFC没有。这句话是后面所有问题的总根源。FB调用时你给它配一个DB它内部的静态变量STAT有独立地盘这次调用和下次调用之间状态可以保持多个地方调用同一个FB时各用各的DB互不干扰。FC就不一样了它就像一个临时工干完活就拍屁股走人自己不保存任何私有状态。你要在FC里保持某个信号只能通过OUT参数传出去或者借用全局的M区、DB区而一旦借用就容易出大问题。STL里FC的这种无状态特性和梯形图中FB的使用习惯形成鲜明对比。很多从梯形图转到STL的人把FB那套思维方式带过来在FC里用M区当私有变量结果多处调用时相互覆盖设备动作就乱了套。1.2 STL用FC的典型场景为什么不用LAD/FBD讲错误之前得先讲场景。STL的优势在于灵活、紧凑、执行效率高特别适合逻辑嵌套复杂、位运算多、数学计算密集的程序段。FC在STL里最常见的使用场景有三类电机、阀门、气缸等设备的控制封装把启动条件、停止条件、保护联锁、反馈监视统一封装在一个FC里调用时只需传设备号和控制字。批量数据处理和换算比如将4-20mA信号转换为工程量、流量累积计算、PID前的事前处理等数据加工逻辑写成一个FC反复调用。工艺流程节拍控制大程序拆分成若干个FC每个FC管一段工艺便于模块化调试和故障定位。在这些场景下STLFC的组合比LAD更紧凑。同样的逻辑LAD可能画好几屏STL几行就完了。但代价是LAD画错了容易看出来STL写错了不容易发现尤其是FC这种跨模块调用的结构一旦接口上出问题排查看半天都找不到北。2. 参数接口上最容易埋雷的几个点2.1 形参和实参类型不匹配编译过了但一调用就报错STL调用FC时实参和形参的类型必须匹配但STEP7的编译器在FC内部声明时检查并不那么聪明。我最常遇到的情况是FC的形参声明为INT调用时实参给了一个MW地址但MW里实际存的是REAL格式的浮点数编译不报错一运行就出#8006之类的区域长度错误或者数据格式错误。排查这类问题先看两个东西一是FC块的接口声明二是调用时的实参类型。很多人习惯用MW给形参传值但MW只是个容器你得确认容器里装的东西和FC形参要求的一致。比如FC里要做浮点运算形参声明了REAL你在外面用一个MW去传MW只有16位REAL要32位STEP7一般会报参数长度不符但有些老版本软件只给警告照样能下载运行运行起来数据就乱了。我的习惯是为每个FC写一个参数表注释在FC的声明区用注释说明每个形参的类型、单位、取值范围。这个注释成本很低但等设备出问题回头查程序时它能帮你快速排除大量低级错误。2.2 临时变量不初始化第一次扫描就出幺蛾子FC的TEMP变量是最隐蔽的坑。TEMP变量存放在本地数据堆栈L Stack中FC每次被调用时系统分配一段L区但不会自动清零。这段L区里的内容可能是上几次调用的残留值也可能是其他FC用过之后留下的脏数据。很多初学者在FC里写这样的逻辑L #temp_counter // 读取临时变量 L 1 I T #temp_counter // 累加如果这个FC在一个扫描周期内被调用十次第一次调用时temp_counter里面的值可能是随机的整个计数逻辑直接就歪了。更麻烦的是这类问题不是每次都复现有时残留值恰好是0设备跑得好好的重启一次或者别的FC修改了一下L区布局问题就冒出来了。处理方式就两条要么在使用前先赋予初值要么把需要跨调用保持的变量放到FC之外的M区或DB区。我个人的习惯是FC里所有TEMP变量在使用前先做一次清零或赋值操作除非我能确凿地判断它一定会在赋值之后才被使用。这个习惯在STL里尤其重要因为STL不像LAD那样拖动连线时能看见数据流。2.3 IN、OUT、IN_OUT用错方向选反调试三天STEP7的FC接口有三种主要的数据方向IN调用时把外部值传给FC内部使用FC内部修改IN参数不影响外部变量。OUTFC内部赋值后传递给外部变量是FC把计算结果吐出来的通道。IN_OUT既是输入也是输出FC内部可以读它也可以写它而且读写的是外部变量的原地址相当于C语言里的指针传参。最容易出问题的就是IN和IN_OUT搞混。比如你要写一个电机控制FC需要读当前电机的运行状态还需要把本次扫描的启动命令锁存下来。如果你把状态变量声明为IN那么在FC内部对这个参数做赋值操作时程序不会报错有些情况下会警告但外部的M点不会被修改你锁存的命令根本出不去。反过来你把一个应该作为IN只读的变量声明成了IN_OUTFC内部不小心写了它外部变量就被莫名修改了——这类故障最难查因为你看调用FC的那段程序压根不会想到FC里动了外部变量。还有一个细节FC的IN_OUT参数在STL中传递的是地址而非值所以外部实参必须是可寻址的变量如M区、DB区、I/Q区不能是常数或立即数。有些人在FC里声明了一个IN_OUT调用时习惯性地写了一个常数值给它编译直接报错这就是没理解IN_OUT的本质。3. 调用点的暗坑不是FC写错了而是调用方式错了3.1 有条件调用导致输出状态丢失FC没有背景DB它本身不记忆东西。如果一个FC的输出在内部被计算出来但FC的调用是有条件的——比如只在某个步序激活时才调用——那么当FC不执行时它的输出参数就维持上一次的值还是自动复位答案是具体要看FC内部逻辑怎么写的但大多数情况下输出参数在上一次调用后保存在外部变量中FC不调用时外部变量保持不变。听起来没问题问题恰恰出在这里。假设你的FC输出一个设备启动信号Q0.0FC内部逻辑是满足条件就置位Q0.0。当某个联锁条件满足时你不想让设备启动于是你在调用条件上加了限制不让FC执行。看似Q0.0不会置位了但如果上一步FC已经执行过并且置位了Q0.0停用调用并不会把Q0.0复位设备就这么一直运行着直到你手动停。正确做法有几种一是把FC是否允许运行作为IN参数传入FC内部在FC内部统一处理联锁逻辑二是FC调用完之后对不会被调用的分支中的输出变量做统一的复位处理三是尽量让FC的调用是无条件的把条件判断放到FC内部的逻辑里。第三种是我现在比较推崇的方式因为FC内部逻辑越完整外部调用越简单排查问题时需要看的代码就越少。3.2 嵌套调用深度和优先级问题FC可以嵌套调用就是一个FC里再调用另一个FC。STEP7对嵌套层数有限制S7-300一般是8层S7-400可以更深超过限度会出现Stack overflow或L stack overflow类错误。这个错误在现场很讨厌因为程序编译下载都正常设备跑着跑着突然CPU进入STOP而且不是每次都停是偶发。我亲身遇到过一次一套设备正常跑了一个多月某天突然停机诊断缓冲区显示Local data stack overflow。查了半天发现是配方切换时一个FC通过间接寻址调用了另一个FC而那个FC又调用了第三个FC嵌套层数偶尔超限。这种问题和程序运行的实时路径相关极难复现排查思路只能靠缩小范围、检查嵌套树。经验是FC嵌套深度控制在4层以内不要超过5层。如果你写了一个复杂的工艺程序拆FC时不要搞出套娃结构宁可多写几个平级的FC在主OB里按顺序调用也不要做成深层次的链式调用。原因除了堆栈风险还有可读性——现场调试时一层层翻FC的调用关系真的会翻到崩溃。3.3 多个同类FC共用一个M区状态相互踩踏这个问题是FC使用中最高频的错误之一。很多人写FC时为了方便直接在FC里用固定M地址做内部静态存储。比如A #start S M10.0 // 用M10.0做锁存位如果这FC只在一个地方调用那么没问题。但你要是写了两个泵、两个阀的控制逻辑共用了同一个FC那M10.0就被两个设备共用了。泵1启动时把M10.0置位泵2停止时把M10.0复位两个泵的控制逻辑互相踩踏设备动作完全乱套。这是FC无状态特性的直接后果。解决办法有三种给FC增加IN_OUT参数把锁存位的地址通过参数传进来每个设备用不同的M位或DB位。把FC改成FB用背景DB来做状态保持。把FC内部存状态的位置放到一个DB里DB的偏移通过参数计算。第一种方法最轻量也最常用。比如你写一个泵控制FC给它加一个状态字IN_OUT参数调用时传入每个泵自己的状态字的地址FC内部用这个状态字里的位来做锁存。这样同一个FC就能安全地被多个设备复用。4. STL指令在FC中的误用一段代码引发的血案4.1 RLO状态被意外破坏位置不对结果天差地别STL编程的核心是RLO逻辑运算结果的传递。很多STL初学者在FC里写着写着RLO就被弄丢了。举个例子你想实现如果A和B都满足则启动同时把状态字里的某位置1。有人这样写A #A A #B #start // 把结果存起来 S #status_bit // 结果状态为1时置位表面看没问题指令不改变RLO所以S仍然使用当前的RLO。但如果中间插入了一个L或T指令情况就变了A #A A #B L #value // 装载操作改变了RLO吗 #startL指令会把RLO置为0在S7-300/400中L指令不直接改变RLO但会影响某些状态位尤其是累加器操作会重置RLO为0实际需要对照指令手册。确切地说累加器操作和数学运算指令会改变RLO或至少影响后续指令对RLO的使用如果你在A指令之后、指令之前插入了L/T类指令那么得到的结果就不是你想要的了。我在给一个老设备改程序时就踩过这个坑原来程序里有一段STL逻辑顺序是先做位运算然后做数值比较最后根据比较结果输出。原作者在比较指令之后写了 Q0.0但比较指令的执行结果不是简单的RLO状态比较指令会将结果存入RLO如果中间穿插了数学运算或者装载指令状态位就乱了。现场表现为Q0.0有时候该亮不亮有时候不该亮乱亮。最后我把逻辑拆开把比较结果先存到一个临时位里再用这个临时位做输出问题解决。经验法则在FC中不要跨计算类指令直接使用RLO。如果你需要保存前面一段逻辑的结果先用或S/R把它存到一个位变量里后面要用再A取出来。这虽然多花一两行代码但完全避免了一大类莫名其妙的问题。4.2 跳转指令的标号冲突和作用域问题STL里跳转指令JU、JC、JCN经常在FC中使用用来实现类似C语言if-else的分支结构。跳转的目标是标号LABELFC内部定义的标号通常以M001、M002这种形式出现S7-300/400中标签名可以自定义但一般保留Mxxx来表示跳转点。跳转指令常见的错误有两个第一个是标号作用域问题。FC内部定义的标号只能在该FC内部使用不能跨FC跳转。有些从汇编转过来的朋友习惯用全局标号做跳转在FC里编译直接报错。注意STEP7不允许你从FC外部跳进FC内部反过来也不行。第二个是跳转绕过初始化代码。比如你在FC开头写了一些初始化逻辑中间用JCN跳到了另一段代码结果跳过了某个TEMP变量的赋值。等程序走到后面要使用这个TEMP变量时数据是脏的。排查起来又回到临时变量不初始化的老问题。我的习惯是FC里尽量少用跳转指令。STL的跳转结构在简单场景下可读性还不错但一旦嵌套超过三层可读性急剧下降。我更倾向于把复杂的跳转逻辑改写成用临时位标记条件再用逻辑组合实现分支的方式。虽然程序变长了但每行逻辑都比较直观现场排查时至少能看懂。4.3 累加器使用不当数据还在栈里就被下一段逻辑覆盖了STL中数据运算依靠两个累加器ACCU1和ACCU2。很多运算指令会先把你当前ACUU1里的数据压入ACCU2然后把新数据放入ACCU1然后针对ACCU1和ACCU2做操作。比如I指令整数加法执行时会把ACCU1中的原值送往ACCU2再读入新值做加法。如果你在FC里写了一段连续计算的代码中间有分支跳转很容易出现数据还在ACCU2里跳转后又被别的指令覆盖的情况。这时算出来的结果完全不对而且很难从程序上看出问题因为单看每一行都没错。解决方法是复杂计算尽量使用显式的临时变量把中间结果保存到TEMP变量或局部变量中不要依赖累加器的隐式保持。例如L #input_a L #input_b *I // ACCU1 input_a * input_b L #input_c I // ACCU1 input_a * input_b input_c T #result这段代码逻辑上没问题但如果你在*I和I之间加了跳转或别的逻辑就要格外小心累加器状态。更好的写法是L #input_a L #input_b *I T #temp_mul // 积先存起来 L #temp_mul L #input_c I T #result多两条中间存取的指令但每一步的数据流向都清晰排查时不用在脑子里模拟累加器状态。5. 我踩过的最贵的坑一个FC错误的完整排查实录5.1 设备随机动作半天最后发现是OUT参数悬空有一年给某汽车零部件产线做改造现场一台清洗机的喷淋泵经常随机启停。操作工说它想开就开想停就停我蹲了三天看程序看了无数遍就是找不到规律。最后我打开那个泵的控制FC发现FC有个OUT参数叫#pump_cmd在FC内部被赋值。但在主程序的调用点这个OUT参数没有连接到任何实际变量——也就是说调用时这个参数是空的。STEP7对这类悬空参数默认不报错FC内部计算的结果没有输出到任何地方泵的启停信号实际上是靠FC内部直接操作的Q点去控制的而那个Q点和OUT参数对应的是同一个泵但逻辑路径不一致。结果就是OUT参数虽然没有连接但FC内部有一段逻辑写的当故障时复位Q点被正常执行了另一段当指令为1时置位Q点因为沿检测的问题偶尔被跳过。于是泵的动作就成了随机的。这次排查让我养成一个习惯FC的OUT参数必须实打实地连接到外部变量哪怕暂时用不到也接一个备用MB。空着的OUT参数就像电路里的悬空引脚不出事则以一出事就是莫名其妙的事故。5.2 工序偶尔跳步原来是把TEMP当静态变量用了另一个项目是做食品行业的批次控制PLC是S7-300程序是从国外原厂拿过来的老外写的STL。设备运行基本稳定但偶发工序跳步——有时候一个反应釜的温度还没到设定值程序就跳到下一步了。这问题很严重因为涉及工艺配方出一次错整批物料报废。老外的程序里每一步的完成标志用的是一个FC里的TEMP变量。逻辑大概是L #step_no L 5 I JCN M001 // 执行第5步的逻辑 A #temp_ok S #step_done M001: NOP 0这里#temp_ok是一个TEMP变量它的值在FC开头没有被明确赋值只是通过某段逻辑隐式地得到结果。在某些扫描周期里由于前一次调用的残留值恰好是1而当前周期条件刚好允许它通过判断步骤就提前完成了。老外程序的原作者显然没理解TEMP变量的生命周期把它当成了FB的静态变量。这个程序在别处复制了很多次每次复制到不同设备都出过类似问题但都没有深究。后来我在这个FC的声明里把#temp_ok改成从DB块里读取一个保持变量跳步问题彻底消失。这件事让我对别人程序里看似能用的FC始终保持警惕看着能跑不代表设计是对的TEMP变量当静态变量用迟早要出事。6. FC使用常见错误速查表与自查清单6.1 典型错误现象、原因与排查方向对照常见现象可能原因排查方向FC调用后输出不变OUT参数未连接到实际变量或IN_OUT地址传错检查调用点的实参连接确认OUT参数是实际地址设备动作随机、偶发M区冲突多个FC共用固定M地址搜索FC内所有S/R操作的M地址看是否多处引用CPU偶发STOP诊断显示L stack overflowFC嵌套调用过深或单个FC局部数据量过大检查FC调用树减少嵌套精简FC内部TEMP变量数量数据格式错误运算结果离谱形参和实参类型不一致如INT和REAL混用核对FC接口声明与调用点的数据类型使用TEMP变量做累加计数结果不稳定TEMP变量不初始化残留在L堆栈中修改为M/DB变量或者确保TEMP使用前先赋值同一个FC控制多个设备相互干扰FC内部用了固定M位做锁存增加IN_OUT状态字参数或者改为FB有条件调用FC时设备无法停止FC输出在非调用期间保持未进行复位将条件判断移入FC内部统一处理启动/停止逻辑程序跳转乱套步骤顺序错乱跳转标号重名或跳转绕过了初始化逻辑检查FC内所有JMP、JC、JCN的标号定义避免重名6.2 我写FC时的三个固定习惯第一每个FC开头放一段统一注释写明功能、调用条件、每个参数的解释、修改历史。这不是形式主义设备出问题翻代码时这段注释能帮你快速回忆当初的设计意图。第二FC内部不要裸用M地址。所有需要保持状态的变量要么从接口传入要么通过DB访问。固定M地址只在极其简单的临时逻辑里用而且我会用搜索工具全局查一遍确保该M地址没有被其他地方引用。第三下载前在变量表里给所有M区和DB的关键位做上注释和初值。STL本身的可读性就一般如果把M位的含义都记在脑子里过几个月自己都忘了。PLC变量表里的注释虽然不能直接改变逻辑但能让自己和同事少走很多弯路。7. 补一个很多人忽略的细节FC和定时器/计数器打交道时的坑7.1 定时器号不能写死尤其FC被多次调用时FC在STL里使用定时器时需要在FC内部调用定时器指令比如A T1、L T1、R T1等。很多人在FC内部直接写固定定时器号T1、T2。如果这个FC只在一个地方调用倒也还行但一旦FC被多个设备共用每个设备都需要独立的定时器固定T1就全乱套了。比如你写了一个电机启动后3秒内未反馈则报警的FCFC内部用了T1做计时。泵1、泵2、泵3都调用这个FC三个泵启动时都会操作T1定时的逻辑就全部交叉了。泵1没反馈也许泵2的定时器会超时报警信息完全错乱。解决办法是FC增加一个IN参数类型为TIMER调用时把定时器号作为实参传入。STEP7里可以直接把T1、T2这些作为参数传递。FC内部用#timer_no来引用就能实现一个FC对应多个独立定时器。// FC接口 // IN: timer_no TIMER // IN: mon_time S5TIME // IN: feedback BOOL // IN: alarm_out OUT BOOL A #feedback FN M001 // 没有反馈时启动定时 L #mon_time SD #timer_no ...7.2 定时器的时间基准和S5TIME格式STL的定时器预设值一般用S5TIME格式比如S5T#3S表示3秒。S5TIME在内部是一个16位字包含时间基准时基和数值。常见时基有10ms、100ms、1s、10s四种STEP7会按数值大小自动选择时基。但你在FC里做定时器预设值的计算时要注意如果你从DB里读取一个整数然后把它传给SD指令这个整数必须是S5TIME编码格式不是普通秒数或毫秒数。这个问题我在做触摸屏远程设定定时时间的项目时踩过HMI上设了一个延时秒数直接写进DBD然后FC里L #delay_time; SD #timer_no。结果定时器完全不准因为PLC把那个数值按S5TIME编码格式解析了和HMI上的秒数不是一回事。后来我在FC里加了数值转换逻辑把秒数转成S5TIME格式再传给定时器问题解决。如果不想做底层转换可以直接在HMI上选TIME类型但PLC侧要用IEC定时器TON、TOF两者数据格式不同。FC中使用什么定时器接口的数据类型就要配套这是容易忽略的细节。8. 从STL到STL之外的思考这个错误系列给我们的启发这个系列写到第6篇从寻址、位逻辑、定时器再到如今的FC其实核心就一句话STL不是不能写但每一条指令都要清楚它会在哪里留下隐形的脚印。FC作为模块化封装的好工具用好了可以让程序结构清晰、复用性极高用不好就是一台定时炸弹你不知道它什么时候在哪个现场炸。我见过太多经验丰富的工程师写梯形图时头头是道一碰STL就露怯。也有人反过来觉得STL是高级语言什么逻辑都往里塞最后程序变成天书。其实两者都没有抓住重点PLC程序最重要的是可预测、可调试、可维护。FC在这三者面前是一面放大镜——设计得好程序的可维护性被放大设计得糙故障排查时的痛苦也被放大。按我个人经验现场调试时遇到FC相关的疑难杂症优先从状态保持这个维度去思考这个FC有没有自己的状态状态存在哪里被谁修改过调用方式有没有可能让状态在不该被修改的时候被修改了沿着这条线十有八九能揪出元凶。剩下的就是多踩坑、多积累了。