做非标自动化这些年,我越来越习惯在博图(TIA Portal)里用SCL和LAD混编的方式写程序。早年间我也迷信“一门语言打天下”,要么整个项目全LAD,要么全SCL,后来在某个配料站项目上被调试节奏逼得没办法,才真正把这两种语言的搭配逻辑想明白。这篇就分享一下我在博图SCL+LAD混编下的一套原创程序框架,包括状态机主控、外围互锁、模拟量处理,以及现场调试时踩过的几个真坑。内容偏实战,适合正在用TIA Portal做设备程序、又对两种语言的分工边界感到模糊的朋友,从需求拆解到代码片段,尽量写得可以直接参考。
1. 为什么要SCL和LAD混编:一个配料站程序的需求拆解
1.1 这套程序要解决的实际问题
当时那个项目任务是给一条小型配料产线写控制程序,硬件上有一台1500系列CPU、若干个称重传感器、进料阀、出料阀、搅拌电机、输送变频器,还有一堆按钮、指示灯和安全门开关。控制逻辑不算特别复杂,但有一个很典型的矛盾:自动流程中涉及到料仓选择、配方切换、累计量统计、多段顺序控制,这些逻辑如果用梯形图写,会变成一张巨大的、互相跳转的纸;而手动操作和安全互锁这块,如果全塞进SCL,现场电工师傅拿着图纸查线时又根本看不懂。
我当时定下的分工很简单:
- 主控流程、状态机、配方管理、数据处理用SCL,因为SCL支持CASE、循环、数组、结构化访问,处理这些“流程性、计算性”逻辑非常顺手;
- 手动操作、指示灯逻辑、设备级联锁、急停回路用LAD,因为这些属于“布尔逻辑 + 硬件回路”,LAD网络图拉出来跟电气图几乎一一对应,现场排查故障时效率极高。
整套程序在结构上拆成了三层。最底层是设备输出层,由LAD写好每个电机/阀门的非安全联动关系;中间层是自动控制层,由SCL状态机统一调度,但不直接碰物理输出;最上层是操作模式层,负责手自动切换、复位、报警确认。这样改任何一层都不会牵连另外两层,调试时特别省心。
1.2 SCL和LAD各自擅长干什么
很多朋友纠结“用哪个语言写程序更高级”,我觉得没有任何一种语言是万能的,只有合不合适。
SCL的核心优势在于“结构化”和“计算”。拿CASE指令举例,它能把多状态流程写成像查字典一样清晰,这是梯形图里的跳转指令很难做到的。另外SCL在数组处理、配方数据遍历、模拟量计算上几乎就是C语言风格,写起来非常舒服。但SCL写多了也有问题,尤其是程序块的输入输出接口如果设计得不好,整个流程很容易变成一个“黑盒子”,别人不知道你在里面做了什么。
LAD的优势恰恰是“透明”。一段串联常闭触点做急停,输出线圈带电机接触器,设备维护人员一眼就能顺着回路找问题。但LAD一旦写复杂的顺序控制,就必须用置位复位加跳转,程序越写越长不说,逻辑改动时的连带风险也很大。
所以说到底,没有谁替代谁,只有谁在主控层、谁在物理层。这句话是我做这个项目后最大的体会。
提示:不要为了“显得专业”强行混编。如果项目本身逻辑规模很小,全部用LAD其实也无可厚非;混编的目的是兼顾可读性和可维护性,而不是制造两种语言之间的通信成本。
2. 主控状态机的SCL实现:把流程写成一页纸
2.1 状态划分与数据块设计
自动流程本质上就是一个状态机。我把这套配料站设备的自动运行状态划分成六个阶段:
| 状态编号 | 状态名称 | 含义说明 |
|---|---|---|
| 0 | 初始化态 | 上电后变量复位,等待系统自检 |
| 1 | 待机态 | 无报警且允许启动,等待启动指令 |
| 2 | 进料态 | 按配方顺序开启对应料仓进料阀 |
| 3 | 搅拌态 | 达到目标重量后关闭进料阀,启动搅拌电机 |
| 4 | 排料态 | 搅拌完成后打开出料阀,排料至低点 |
| 5 | 故障暂停态 | 运行过程中出现报警,自动输出保持或停机 |
这个状态划分不一定是最优解,但胜在直观。我在实际项目里发现,状态越多,单个状态内的设备动作越简单,故障定位也越容易。而且每个状态必须有明确的“进入条件”和“离开条件”,不能模棱两可,否则SCL写到最后会变成一坨乱七八糟的IF嵌套。
对应的数据块我建议把接口分成三组:一是和HMI交互的变量,比如启动、停止、复位、配方编号、当前状态显示;二是和外围LAD块交互的设备命令变量,比如进料阀开、搅拌电机启动、出料阀开;三是设备反馈变量,比如称重值、电机运行反馈、阀门到位反馈。所有接口变量都集中在DB里统一命名,方便上下调用。
2.2 状态机核心代码段
下面这段是我在FB里写的状态机核心逻辑,已经去掉具体设备号和一些与项目绑定的细节,只保留通用框架。
// 主控状态机:FB_Automat_Control // 输入:启动命令、停止命令、复位命令、故障汇总、配方目标值 // 输出:各设备命令、状态字 CASE #State OF 0: // 初始化态 #InitDone := FALSE; #ValveIn := FALSE; #ValveOut := FALSE; #MotorStir := FALSE; IF #InitFlag THEN #InitDone := TRUE; #State := 1; END_IF; 1: // 待机态 // 自检完成后,只要按下启动,且没有报警,就进入进料态 IF #CmdStart AND NOT #AlarmSum THEN #State := 2; #RecipeNo := #HmiRecipeNo; // 锁定当前配方号 END_IF; 2: // 进料态 #ValveIn := TRUE; // 开启进料阀 IF #WeightRaw >= #TargetWeight THEN #ValveIn := FALSE; #State := 3; #StirTimer(IN := FALSE); // 复位定时器 END_IF; 3: // 搅拌态 #MotorStir := TRUE; // 使用IEC定时器,建议声明为FB的静态变量 #StirTimer(IN := TRUE, PT := #StirTime); IF #StirTimer.Q THEN #MotorStir := FALSE; #StirTimer(IN := FALSE); #State := 4; END_IF; 4: // 排料态 #ValveOut := TRUE; IF #WeightRaw <= #DischargeLimit THEN #ValveOut := FALSE; #State := 1; // 排空后回到待机态 END_IF; 5: // 故障暂停态 #ValveIn := FALSE; #MotorStir := FALSE; // 故障复位后回到待机态,而不是继续原有流程 IF #CmdReset THEN #State := 1; END_IF; END_CASE;有朋友会问,为什么故障状态要回到待机态,而不是回到故障前的状态继续运行?我的经验是:在大多数非标设备里,故障发生后的设备状态不可预测,盲目接着跑往往会发生二次风险。回到待机让操作员查看并手动干预,虽然会损失一点效率,但安全性高了不止一个等级。
2.3 为什么这个状态机不适合用LAD写
如果把上面这段逻辑用LAD实现,你大概率会写出几十个置位复位指令,再加四五个跳转。程序评审的时候,别人要顺着梯形图链条反推每个状态的前置条件,工作量很大。
SCL的CASE语句最大价值是“状态驱动态”的直观性:当前在哪个状态、执行哪些动作、满足什么条件跳转到哪个状态,三行代码就能讲清楚。而且如果后期要增加新状态,只需要在CASE结构中插入一个分支,不用动其他分支。这在LAD里几乎是一场灾难,因为每个新的跳转分支都意味着要重新梳理其他网络的结构。
还有一点是查错方便。程序运行异常时,我直接把HMI上的状态字和代码对比,一眼就能看出卡在哪个状态。用LAD的话,你需要在线监控十几个线圈的得电失电情况,然后再反推状态,效率差太多。
3. 外围互锁与手动逻辑的LAD设计:控制物理世界的最后一公里
3.1 安全链路的LAD网络结构
SCL负责“大脑”,LAD负责“手脚的筋腱”。最典型的就是急停回路。设备急停按钮、安全门开关、热继电器辅助触点这些物理信号,绝对不能只写在SCL里做判断,因为SCL程序一旦跑飞、CPU异常或者逻辑扫描没到位,输出极有可能保持在上一个状态。正确的做法是在LAD中把急停常闭触点直接串联到最终输出回路的输出使能位前面。
我在一个典型的电机输出网络里是这样做的:
- 网络1:急停回路监视。急停按钮常闭、安全门常闭、电机热继常闭串联,输出一个“安全允许”中间变量;
- 网络2:手自动选择。手动模式下外部按钮直接控制电机;自动模式下由SCL状态机的输出字控制;
- 网络3:最终输出。将“安全允许”与“手动或自动命令”串联后驱动接触器线圈,同时并联一个“运行反馈”输入端子;
- 网络4:故障锁存。一旦电机过载或变频器故障,不管SCL状态机那边给什么命令,输出都会被封锁,直到按下复位。
这样的好处是:哪怕SCL自动控制逻辑出现了不可预见的异常,物理安全链路依然生效。这也是为什么很多老工程师坚持让关键安全回路由LAD独立实现,而不是做进状态机里。
3.2 SCL块和LAD块之间的调用关系
在TIA Portal项目的OB1中,我通常把调用顺序设计成三段:
- 先调用模拟量处理FC,把各通道的原始值转换为工程值,写入全局DB;
- 再调用SCL写的主控状态机FB,它读取处理完的工程值并输出设备命令中间变量;
- 最后调用LAD写的设备输出FB,将中间变量结合手动按钮和安全回路后,真正驱动物理输出。
这里有个容易忽略的点:SCL的FB如果需要和LAD的FB共享大量中间变量,我建议把它们统一放在一个全局DB里,而不是各自维护背景DB再加接口转发。否则变量在两层之间传来传去,在线监控时很容易看花眼。
我在项目里专门建了一个叫“设备命令映像”的结构体,里面是每个阀门、每个电机的启动、停止、复位、状态反馈位。SCL只管写这个结构体,LAD只管读这个结构体,两层之间不直接引用对方的背景DB。这样做的好处非常直接——换一个人来维护的时候,系统结构一目了然。
3.3 混编时接口设计的一个教训
第一次混编时我犯过一个低级错误。SCL的FB里输出的是BOOL变量,LAD块直接引用这个BOOL作为网络输入。逻辑上没问题,但我在SCL里无意中把输出变量名写进了LAD块的“赋值”范围里,导致编译直接报“无法访问”错误。找了很多才知道是作用域问题。后来我给自己定了一条规矩:跨语言调用的接口变量,权限统一由主调方声明,被调方只做只读访问;需要双向读写的变量全部通过全局DB中转。
这个教训其实挺值钱的。混编程序最大的隐患不是语言本身,而是语言之间的接口约定不清晰。只要接口定了,用什么语言写内部逻辑,影响并不大。
4. 模拟量称重信号的处理细节:从原始值到工程值再到闭环判断
4.1 输入信号归一化与标定方法
配料站最关键的反馈就是称重值。如果称重信号处理不干净,状态机里所有关于重量的判断都会失真。我用SCL写了一个模拟量处理FC,先做一个归一化转换,再做滤波和超差判定。
在博图里,读取模拟量模块的原始值后,通常先用NORM_X把INT类型的0到27648归一化成0.0到1.0,再用SCALE_X映射到实际的量程范围。这个操作虽然HMI组态里也能做,但放在PLC程序里更方便后续判断和报警处理。
// 模拟量处理示例:称重通道0 #RawInt := #AI_Raw[0]; // 从AI模块读取的原始值 #Norm := NORM_X(MIN := 0, MAX := 27648, VALUE := #RawInt); #Weight := SCALE_X(MIN := 0.0, MAX := 500.0, VALUE := #Norm);这里有一个非常关键的细节:模拟量信号在工业现场往往有噪声波动,你不能拿这个瞬时值直接跟目标重量做比较。我在程序里加了一个滑动平均滤波逻辑,原理很简单:连续采样五次,去掉最大值和最小值,剩下三次取平均。这样既不会像简单均值那样反应迟钝,也不会因为单次毛刺导致误判。
当然,滤波会带来响应延迟。如果你的设备进料速度很快,五周期平均可能造成几千克的过冲。所以后来我改成了“粗进料”和“精进料”两档控制:粗进料时阀全开,快接近目标重量时关到位阀,只留小口径阀慢补,这样既保证速度又保证精度。
4.2 浮点数判断的经典坑
这一段得单独说,因为实在太容易踩了。SCL里直接用“等于”判断浮点数,在理论上是危险的。称重传感器反馈的值永远是波动的,你不可能让某次采样恰好等于500.00千克。正确的判断方式是设定一个回差区间。
// 错误做法:直接判断等于目标值,基本永远不成立 IF #Weight = #TargetWeight THEN #State := 3; END_IF; // 正确做法:进入区间即认为到位 IF #Weight >= (#TargetWeight - #Tolerance) THEN #State := 3; END_IF;这个#Tolerance值需要根据设备的实际精度来定,配料行业一般取满量程的0.1%到0.5%。太大会超量不准,太小又会频繁触发抖动。我这边取的是0.3%,运行下来比较稳定。
4.3 累计量与批次记录
除了实时称重,这个项目还有一个要求:每个配方的累计投料量要能被查询。我在SCL里用一个静态数组记录最近50批的数据,包括配方号、目标重量、实际重量、完成时间、结束状态。每个批次结束时,把关键数据压入数组、数组头指针加一,形成滚动记录。HMI通过访问这个数组就能生成报表。
这个功能用SCL写非常顺手,因为数组操作和指针偏移在SCL里就是几行代码的问题。如果换成LAD,你要去摆弄一堆变址寄存器,调试起来让人头大。这也是我极力主张“数据类逻辑全部进SCL”的原因之一。
5. 现场调试记录:混编程序的三个翻车现场与排查思路
5.1 背景数据块被清零导致配方全部丢失
有一次我整体下载程序后,发现所有配方参数和批次记录全部归零。第一反应是配方数据块没设成保持性。后来检查发现,我确实勾选了保持性,但问题出在优化访问与标准访问的切换上。
旧程序里数据块用的是标准访问,改成优化访问后,很多变量在组态软件里的地址映射发生了变化,导致下载时把保持性数据也一并初始化了。这种事在设备交付后出现,最头疼,因为现场可能已经跑了好几天。
排查思路是:先分开做实验,把程序下载和配方下载拆开,只下载块但不复位保持性区。如果还是清零,再检查DB属性里的“保持性”是否真的覆盖到所有变量。最后我干脆把配方数据单独放在一个DB里,并且每次修改程序前先把DB内容上传备份,修改后下载时选择“仅下载改动块”,避免整机复位。
5.2 SCL里定时器声明方式与LAD完全不一样
做这个项目之前我在LAD里用IEC定时器用习惯了,转到SCL里写定时器时犯了一个典型错误——我试图直接用定时器指令的字面量调用,结果编译直接报错。
SCL里的定时器要么作为FB的静态变量声明,要么通过实例化分配背景数据块。我用的方式是作为静态变量声明:
// FB Static变量声明区 #StirTimer : TON; // 声明一个IEC TON定时器 // FB代码区调用 #StirTimer(IN := #MotorStir, PT := #StirTime, Q => #TimerDone, ET => #TimerValue);这里有个细节,SCL调用定时器时不需要再为定时器单独建DB,因为它是FB的静态变量。一旦写成局部临时变量就会报错。另外,定时器的PT参数不要直接用硬编码数字,单独放一个变量,方便HMI在线修改。
5.3 下载后已保持的输出状态与状态机的错位
还有一次调试经历:程序下载后,SCL状态机处于待机态,但现场某个电机因为LAD输出回路之前保持得电状态,直接在没有命令的情况下转了。这个问题本质上是下装时输出模块的“保持上一状态”配置和程序初始值不一致造成的。
排查时我盯了很久才发现,LAD网络里复位逻辑只放在了“自动模式”分支下,手动模式的分支我图省事没做首次扫描复位。后来所有设备输出FB在第一扫描周期无条件执行一次“全输出复位”动作,并且把输出模块的“替代值”强制设为零,从此再没出现过类似问题。
这个小问题也让我养成了一个习惯:任何程序的开头一定要有一段“初始化不动输出”的逻辑,不管当前状态机是什么状态,只要程序启动,输出必须从零开始。
最后再分享一个实用技巧
如果你也打算在自己的项目里尝试SCL和LAD混编,我建议你从最小的设备开始,比如一个简单的阀门控制或电机控制,先建立一套自己的接口命名规则,再逐步扩展到整条产线。
我个人在实际项目里的体会是:不要迷信“语言决定水平”,决定项目好坏的是层次是否清晰、接口是否稳定、维护是否方便。SCR和LAD混编没那么玄乎,把它当成一把多功能的工具箱,该用什么用什么,才是真正有用的思路。
最后一个建议:把所有报警、参数、配方这类数据归拢到少数几个DB,并且在每次程序变更前养成“先上传备份”的习惯。混编程序的可读性再高,也扛不住一次意外的数据清零。