前阵子帮朋友调试一套小型电梯模型,楼层只有六层,但调度逻辑一开始用梯形图写,改来改去跟鬼打墙一样:明明按了三层,电梯跑到五层才停下,二层外呼又跟四层内选撞在一起。后来我索性把整个楼层扫描逻辑全部换成SCL的For循环,配合触摸屏做联动演示,半小时就把问题理顺了。西门子SCL这种文本化编程语言,在处理“逐个楼层扫描、按规则挑一个目标”这种需求时,天生就比梯形图顺手得多。
这篇文章不是讲电梯电气设计,也不是搞完整变频器拖动,核心就一件事:怎么用SCL的For循环,实现电梯楼层优先级调度,并且把当前楼层、目标楼层、运行状态同步到触摸屏上。如果你手头有西门子S7-1200或S7-1500,打算写楼层调度或者类似的工位流转逻辑,又或者刚接触SCL想找个能落地的例子,这篇文章应该能给你省不少弯路。
1. 先从调度需求说起
1.1 电梯楼层优先级到底在解决什么问题
一句话概括:电梯收到一堆楼层请求,不能乱跑,必须按规则决定“下一步去哪一层”。这里的优先级不是简单按数字大小排序,而是要考虑运行方向、当前楼层、内选外呼的区别。比如电梯在3楼往上走,这时候4楼有人外呼上楼,8楼也有人外呼下楼,正常策略应该是先停4楼,而不是直接冲8楼。因为4楼呼叫方向跟当前运行方向一致,属于顺路,8楼虽然数字大,但方向相反,得留到后面换向时再处理。
这种“顺路优先、远端换向”的规则,放在真实电梯里还要叠加满载、直驶、消防联动这些条件,但核心骨架永远是“扫描楼层→判断请求→比较优先级→选出目标”。一旦楼层数量多了,梯形图里那一排排比较指令和自锁互锁会让人崩溃。我见过有人用梯形图写十层调度,程序页能翻十几屏,改一个逻辑要顺藤摸瓜找半天,很痛苦。
SCL的好处是,你可以把楼层当成一个数组的下标,用For循环从头到尾扫一遍,再把每一层的请求状态放到IF条件里判断。代码量可能不比梯形图少太多,但逻辑是线性的,读起来就像在念需求文档,排查问题容易得多。这也是我这次选SCL的根本原因:不是梯形图不行,而是“扫描式判断”这种需求,文本语言更贴合人脑的思维习惯。
1.2 为什么用SCL而不是梯形图
很多刚从梯形图转SCL的朋友会问:SCL到底好在哪?梯形图直观,点一下线圈,看一眼触点,逻辑清清楚楚,为什么非要写成文本?我的看法是,分场景。单台设备启停、电机正反转、报警灯闪烁,梯形图确实顺手,电气老师傅闭着眼都能画。但碰到“遍历数组、按规则排序、循环比较”这类需求,梯形图要写一堆中间变量和跳转,SCL只需要一个FOR和几个IF。
拿电梯调度来说,最核心的动作是“从当前楼层往上找最近的一个有请求的楼层”。用SCL写就是:
FOR i := currentFloor + 1 TO maxFloor DO IF floorRequest[i] THEN targetFloor := i; EXIT; END_IF; END_FOR;这一段用梯形图怎么写?你得用加法指令生成当前楼层+1,再用比较器跟MAX比较,加一个循环跳转指针,还得处理数组下标……费半天劲写出来,别人还看不懂。SCL一行EXIT直接跳出循环,干净利落。
当然SCL也有缺点。它对结构化编程习惯要求更高,变量命名、块接口设计不好,一样会写成一坨。另外SCL的编译不如梯形图那么“所见即所得”,有些语法错误不编译根本发现不了。但总体利大于弊,尤其是做复杂逻辑时,SCL的维护性明显更好。
2. SCL的For循环:电梯调度里的正确打开方式
2.1 For循环的语法底子和SCL的特点
SCL的For循环跟Pascal、VBA的写法很像,基本格式是:
FOR 循环变量 := 初值 TO 终值 BY 步长 DO // 循环体 END_FOR;循环变量一般用整数,比如INT或DINT。步长默认是1,可以省略;如果要往下扫,初值大于终值,后面就要加BY -1。这个细节很多人第一次写会忽略,直接写FOR i := 10 TO 1 DO,编译不报错,但循环体一次都不执行,因为默认步长是+1,初值已经大于终值。这是SCL和某些高级语言不一样的地方,必须留意。
另外SCL的循环变量建议用局部变量,不要用全局DB里的变量。因为循环变量在循环过程中会被频繁修改,如果用全局变量,一旦程序里其他地方误读,容易出莫名其妙的问题。我在项目里通常直接在FB的VAR_TEMP里声明一个i,只在当前扫描周期内有效,干净安全。
除了FOR,SCL里还支持WHILE和REPEAT,但在电梯调度这种固定楼层扫描场景里,FOR更合适。因为楼层数量从一开始就知道,比如六层就是1到6,十层就是1到10,FOR循环天然匹配“从头到尾遍历”这个动作。而WHILE更适合“不知道要循环多少次,直到条件满足为止”的场景,比如等待某个标志位变化。
2.2 用数组+For循环实现“扫描式”楼层判断
做楼层调度,第一步是把所有楼层请求存到一个数组里。比如六层电梯,我可以定义:
VAR floorRequest : ARRAY[1..6] OF BOOL; // 楼层请求集合 floorCurrent : INT; // 当前楼层 floorTarget : INT; // 目标楼层 END_VAR每个数组元素对应一层楼,TRUE表示这一层有人呼叫。然后扫描逻辑就特别直观:电梯往上走时,从当前楼层+1开始,一直扫到最高层,遇到第一个TRUE就停,把下标赋值给目标楼层。
// 上升方向:从当前楼层的上一层往上找最近的请求 floorTarget := 0; // 0表示暂无目标 FOR i := floorCurrent + 1 TO 6 DO IF floorRequest[i] THEN floorTarget := i; EXIT; END_IF; END_FOR;这里有个很关键的技巧:EXIT。电梯要的是“最近的一个”,而不是“最远的一个”,所以扫到第一个TRUE就要跳出循环,否则继续扫下去会覆盖成更高的楼层。我见过有人忘了EXIT,结果电梯每次都直接奔顶楼去,查了半天才发现是循环没跳出。
同理,向下方向就倒着扫:
FOR i := floorCurrent - 1 TO 1 BY -1 DO IF floorRequest[i] THEN floorTarget := i; EXIT; END_IF; END_FOR;如果两段都扫不到目标,说明当前方向没有顺路请求,那就需要换向,再重新扫另一方向。比如上行扫不到,就下行从当前楼层-1扫到1;如果下行也扫不到,说明没有任何请求,电梯停在当前层待机。这些分支用IF套一下就行,整体逻辑很清晰。
2.3 循环里的优先级策略:最近优先还是方向优先
写到这里,你可能会问:那外呼和内选不用区分吗?真实电梯确实要区分,但基础逻辑可以先统一成一个“请求数组”,内选外呼都置位到这个数组里,只是外呼在电梯到达该楼层后会消除,而内选由对应楼层按钮消除。优先级策略上,最常见的是“同方向最近优先”。也就是说,顺着当前运行方向,离当前楼层最近的请求楼层,就是最高优先级。
用SCL实现时,这个“最近”就等于循环里第一次满足条件的下标。方向优先体现在:先扫上行,扫不到再扫下行,而不是把上下行全部混在一起排序。如果混在一起,就变成“绝对距离优先”,电梯会在上行过程中突然掉头去下一个很远的楼层,这在真实电梯里是不允许的,既危险又难受。
我在项目里还加了一个“最多顺路次数”的概念,用于防抖。什么意思呢?当电梯运行到目标楼层后,先停稳开门,而不是立刻去扫描下一个新请求;等门关好,再重新扫描一遍当前方向的请求,可能又顺路多停一两层。这个“重新扫描”的动作,SCL里就是一个循环函数块或一段FOR代码,重复调用就行,非常方便。
3. 实战:从功能块到触摸屏联动
3.1 先搭好PLC侧的FB接口与数据结构
我习惯用西门子TIA Portal V17编程,PLC选了S7-1200或S7-1500都行,代码基本通用。首先建一个功能块FB,命名叫“ElevatorDispatch”。接口里要有输入、输出,还有静态变量和临时变量。
输入接口包括启动信号、当前楼层反馈、各楼层内选按钮、各楼层外呼按钮(上行/下行,简单模型里可以合并成一个请求)。输出接口包括目标楼层、运行方向、请求指示数组等。这里我不建议把所有按钮都单独做输入点,而是直接定义两个数组:
// 输入 startSignal : BOOL; currentFloor : INT; callUp : ARRAY[1..6] OF BOOL; // 上行外呼 callDown : ARRAY[1..6] OF BOOL; // 下行外呼 cabCall : ARRAY[1..6] OF BOOL; // 内选 // 输出 targetFloor : INT; direction : INT; // 1=上行,-1=下行,0=停机 requestDisplay : ARRAY[1..6] OF BOOL;为什么外呼要分上下行?因为真实电梯方向优先逻辑必须区分,如果你用一个统一的请求位,从4楼去8楼,经过5楼时就不应该响应5楼的下行外呼。但为了简化演示,我后来在触摸屏上把外呼按钮做成自复位按钮,按一次就置位对应请求位,等电梯到达后自动复位,也能跑通。核心的优先级扫描逻辑,还是靠For循环。
临时变量里声明循环变量i、临时目标tempTarget,还有辅助判断标志位。这些不要放进DB,用VAR_TEMP就行。但注意,FB的背景DB里会为静态变量分配存储空间,如果你需要把请求状态保持住,必须用VAR(静态变量)而不是VAR_TEMP,因为临时变量在FB调用结束后会被覆盖。我第一次写时把cabCall声明成临时变量,结果按钮一松手,请求全丢了,电梯永远不会响应,排查了很久才意识到。
3.2 SCL代码逐段拆解(重点)
下面我把核心代码完整贴一遍,这段代码我在模拟器上跑过,也在模型上实测过,逻辑是通的。为了阅读方便,我只写六层版本,扩展更多楼层只需改数组上下限。
第一步,先把外呼、内选的所有请求合并到一个统一的请求数组里:
// 合并请求,内选或任意方向外呼都视为请求 FOR i := 1 TO 6 DO IF cabCall[i] OR callUp[i] OR callDown[i] THEN request[i] := TRUE; ELSE request[i] := FALSE; END_IF; END_FOR;第二步,根据当前方向扫描目标楼层。direction为1表示电梯当前往上走,-1表示往下走,0表示待机。待机时默认优先响应当前楼层上方的最近请求。
IF startSignal THEN // 先清空目标 targetFloor := 0; IF direction = 1 THEN // 顺着向上方向,找最近请求 FOR i := currentFloor + 1 TO 6 DO IF request[i] THEN targetFloor := i; EXIT; END_IF; END_FOR; // 上方没有,则搜索下方(准备换向) IF targetFloor = 0 THEN FOR i := currentFloor - 1 TO 1 BY -1 DO IF request[i] THEN targetFloor := i; direction := -1; EXIT; END_IF; END_FOR; END_IF; ELSIF direction = -1 THEN // 顺着向下方向,找最近请求 FOR i := currentFloor - 1 TO 1 BY -1 DO IF request[i] THEN targetFloor := i; EXIT; END_IF; END_FOR; // 下方没有,则搜索上方(准备换向) IF targetFloor = 0 THEN FOR i := currentFloor + 1 TO 6 DO IF request[i] THEN targetFloor := i; direction := 1; EXIT; END_IF; END_FOR; END_IF; ELSE // 待机方向:先上后下 FOR i := currentFloor + 1 TO 6 DO IF request[i] THEN targetFloor := i; direction := 1; EXIT; END_IF; END_FOR; IF targetFloor = 0 THEN FOR i := currentFloor - 1 TO 1 BY -1 DO IF request[i] THEN targetFloor := i; direction := -1; EXIT; END_IF; END_FOR; END_IF; END_IF; // 输出给触摸屏的请求显示数组 FOR i := 1 TO 6 DO requestDisplay[i] := request[i]; END_IF; END_IF;这段代码的核心逻辑说穿了就是“双扫描”:先顺着当前方向扫,扫不到就换方向扫。方向变量初始为0,第一次运行会向上找最近的请求;找到请求后置为1或-1,之后就一直保持方向,直到一个方向的请求都处理完了,才会切换。
第三步,到达楼层后的处理。真实电梯到了目标楼层要开门、复位请求、重新计算新目标。我这里简单演示一下:
// 到达目标层,清除该层所有请求 IF currentFloor = targetFloor THEN cabCall[currentFloor] := FALSE; callUp[currentFloor] := FALSE; callDown[currentFloor] := FALSE; request[currentFloor] := FALSE; // 开门延时后重新扫描 direction := 0; targetFloor := 0; END_IF;你可能会问,为什么到达后要把direction清0?因为“同方向最近优先”要求电梯每处理完一个目标楼层,就应该重新评估方向,而不是盲目继续往原来的方向冲。我在模型上试过不清零的版本:电梯从1楼上到6楼,途中接了3楼、4楼请求,都能停;但到6楼后如果同时有2楼和5楼请求,不清零的话它会优先处理5楼(因为还是上行方向),这其实也没错,但跟实际电梯“先完成当前方向全部请求再换向”的规则略有差别。看你需要哪种策略,代码调整就改一下方向清零的位置。
3.3 触摸屏联动怎么做
触摸屏联动这步,我用了威纶通MT8071iP,用Modbus TCP跟S7-1200通讯。如果用的是西门子精智面板,直接走TIA Portal组态更省事。为了让演示效果好,我把触摸屏当成一个“楼层呼叫面板+状态监视器”,画面分三块:
- 左上角是当前楼层数字显示,关联PLC里的currentFloor变量,整数格式,直接映射DB1.DBD0这种地址。
- 中间是六个楼层按钮,分别对应cabCall[1]到cabCall[6],按钮属性设为“切换状态”(Toggle),按一下置位,再按一下复位。有些朋友习惯设成“置位”型,按一下永远保持,那不行,电梯到站后PLC会强制复位,但触摸屏按钮如果没做状态绑定,画面上的灯就会跟PLC不一致。
- 下面是运行方向和目标楼层显示,关联direction和targetFloor。
这里最容易被坑的是变量地址映射。S7-1200的DB块默认没有开启“优化块访问”,如果没开,你可以在TIA里直接用绝对地址访问,比如DB1.DBX0.0对应数组第一个BOOL。但很多时候新建DB默认是优化访问,触摸屏通过Modbus TCP读不到DB里的符号变量,只能读地址。解决办法有两个:要么在DB属性里把“优化的块访问”取消勾选,要么在触摸屏组态软件里根据符号名映射,但威纶通不支持直接读TIA符号,所以我还是老实用绝对地址方式。
为了方便触摸屏读取,我建议把需要交互的数据单独放到一个“通讯DB”里,结构如下:
// 通讯数据块 DB_COMM // 偏移 0.0 bool startSignal // 偏移 0.1 bool resetSignal // 偏移 2.0 int currentFloor // 偏移 4.0 int targetFloor // 偏移 6.0 int direction // 偏移 10.0 bool cabCall[1..6] // 偏移 12.0 bool requestDisplay[1..6]触摸屏上所有按钮和指示元件的地址都指向这个DB,PLC逻辑里用全局DB访问,或者直接用“DB_COMM.cabCall[i]”访问。这样有个额外好处:程序逻辑块和通讯块解耦,将来换触摸屏型号,只需要改通讯组态,不用动逻辑代码。
4. 调试中踩过的坑和排查思路
4.1 变量更新与边沿触发的坑
第一个坑,也是最容易踩的:循环里修改了数组,但触摸屏上的请求灯不亮,或者亮了不灭。我排查后发现,问题出在请求数组的更新时机。比如cabCall按钮信号用普通触点读取,没有做上升沿检测,触摸屏按钮一直按住时,PLC每个扫描周期都执行“合并请求”的FOR循环,把TRUE再一次写入request,这本身没问题;但电梯到达楼层后,程序把cabCall和request都复位了,如果触摸屏按钮还按着不动,下一扫描周期它又会把TRUE写进去,电梯就会反复被同一层召唤。
解决办法是在触摸屏按钮上设成“瞬间ON”(Momentary),或者PLC侧用边沿指令锁存请求。我后来选择在PLC里用请求锁存逻辑:按钮信号上升沿触发置位,电梯到达后复位。这样即使触摸屏按钮卡住,也不影响请求清除逻辑。
写SCL时,边沿检测可以用自带指令R_TRIG,也可以自己用静态变量保存旧状态。我的习惯是在FB里为每个按钮信号存一个oldCabCall数组,每个扫描周期做一次:
FOR i := 1 TO 6 DO IF cabCall[i] AND NOT oldCabCall[i] THEN request[i] := TRUE; END_IF; oldCabCall[i] := cabCall[i]; END_FOR;注意oldCabCall必须定义成FB的VAR,而不是VAR_TEMP,否则下一扫描周期就被覆盖了。这个错误我在调试时栽过一次,当时电梯偶尔会漏掉某些楼层的请求,看上去就像随机失灵,找了一下午,最后才发现是临时变量没有保持功能造成的。
4.2 通讯与数据类型不一致
第二个坑在触摸屏通讯。威纶通跟S7-1200通过Modbus TCP通讯时,数据类型要看清楚。S7-1200的INT是16位有符号整数,范围-32768到32767,触摸屏这边也要设成16位有符号十进制。很多人组态时默认选了32位,或者选了16位BCD,读出来的楼层数就是乱码,10楼直接显示成16,非常诡异。
数组映射也有讲究。比如我把cabCall定义成BOOL数组,它在DB里每个元素占1个字节(BOOL在DB里默认一个位,但为了地址对齐,优化块访问关闭时会按字节偏置),触摸屏如果用线圈地址(0x)去读,可能只读到数组开头的位置。我建议在通讯DB里不要把BOOL数组紧跟INT变量,最好在它们之间留几个字节的地址偏移,避免地址重叠。
还有一个经验:把direction用INT类型输出到触摸屏,不要用BOOL,因为方向有三个状态(上行、下行、待机),用两个BOOL或者一个INT更直观。触摸屏上用“多状态显示”元件,对应INT值0、1、-1,分别显示待机、上行、下行,很方便。
4.3 性能与扫描周期
第三个坑,是For循环的扫描周期问题。六层电梯程序扫描时间几乎没影响,但如果以后扩展到二十层、三十层,或者同一个循环里还嵌套了内选、外呼、满载判断,就要考虑OB1里的执行时间。S7-1200的扫描周期一般在几毫秒到十几毫秒,For循环几十次根本不算事。但如果你在循环体里调用了通讯指令、PID运算,那就要小心了,可能会拉长扫描周期,导致电梯响应变慢。
我在代码里做了个粗超时保护:如果同一个目标楼层持续超过10秒都没变化,就强制复位所有请求。这主要是为了演示时防止模型卡死,但在真实项目里,门锁、平层信号这些外部反馈才是关键。SCL写循环时,尽量让循环体瘦身,不要把大量计算都塞进每个楼层判断里,该拆子程序就拆子程序,比如到达处理单独写一个方法块,扫描逻辑里只放判断和赋值。
还有个小技巧:如果楼层数很多,可以把“扫描最近请求”做成一个FC函数,输入当前楼层、方向、请求数组,输出目标楼层。这样主程序只调用几次函数,代码看着清爽,也方便批量测试。SCL的局部数组参数尽量用“变长数组”或者ARRAY[*]定义,博途里很方便,但是老版本不一定支持,要看清楚固件版本。
5. 演示效果与实际体会
最后说说这次联动演示的实际效果。触摸屏上按5楼,PLC立刻把request[5]置位,方向变成上行,当前楼层从1慢慢往上走,走到5楼停住,清除5楼请求,再重新扫描。整个过程中触摸屏上的当前楼层数字实时变化,目标楼层显示一直是5,运行方向箭头也跟着切。说实话,看到画面跟实际模型同步动起来的那一刻,还是挺有成就感的。
我个人感受最深的一点是:SCL的For循环并不神秘,它不过是把“一个个比较楼层”这个动作封装得更紧凑了。你真正要花心思的不是语法,而是调度策略本身:方向怎么保持,请求怎么合并,到达后怎么复位。这些想清楚了,循环只是实现工具。
如果你打算接着扩展,可以考虑加语音播报、到站指示灯,甚至用V90伺服模拟电梯门机。还有一种做法是用SCL写一个“楼层队列”,把请求楼层按顺序排队,而不是每次扫描都现算目标,那样可以降低扫描频率,但逻辑会复杂一些。从学习路径来说,先把For循环的扫描式调度吃透,再往队列、状态机方向走,会顺很多。