做PLC项目这么多年,我一直觉得电梯控制是练状态机和SCL编程最合适的场景之一。这次要聊的项目,就是在博途V15开发环境下,用1500系列CPU配合SCL语言,实现单部六层电梯的完整控制逻辑。很多人一听“电梯控制”就以为只是简单的上行下行,实际拆开看,它涉及呼叫登记、方向决策、平层停靠、开关门时序、安全联锁一大堆逻辑,而且任何一个环节处理不好,现场跑起来就会出现“反方向跑”“该停不停”“门开了车还在动”这种低级但难查的问题。这篇内容既是给刚接触SCL的PLC工程师一份可参考的结构化编程思路,也适合正在做类似设备控制项目的人拿来对照自己的方案。
1. 项目背景与整体设计思路
1.1 六层电梯的功能需求拆解:先盘清楚输入输出再谈逻辑
单部六层电梯听起来规模不大,但真正动手做需求拆解时,你会发现它同时牵扯四个层面:信号采集、对象控制、调度逻辑、安全联锁。输入侧光是和楼层相关的就有三类信号:轿内选层按钮6个,各层外呼上行按钮(1层到5层布置,顶层一般不做上行外呼),各层外呼下行按钮(2层到6层布置,底层一般不做下行外呼)。除此之外还有每层的平层感应信号、门锁反馈、开门到位限位、关门到位限位、井道上下限位、检修和消防信号。
输出侧也不只是给变频器一个运行指令那么简单。方向输出要区分上行和下行,运行输出要控制抱闸和接触器,还要驱动开门门机、关门门机,以及楼层显示数码管、方向指示灯和到站钟。
调度逻辑是这次SCL探索的核心。它要处理三件事:呼叫登记与清除、方向保持与掉头、目标楼层选择。所谓顺向截梯,就是电梯上行途中如果前方楼层出现任何新的呼叫,只要还没到达当前最远目标,就应该把新呼叫纳入这趟行程。六层虽然只有六个楼层,但把这些逻辑全部写清楚,已经能把状态机的精华覆盖完。
安全联锁是电气设计上碰不得的部分。门未关好不允许启动,运行过程中开门命令必须被切断,上下限位要能强制切断运行输出。软件能做的是把安全条件作为程序的前置判断,真正硬线回路还要靠继电器和接触器实现。我在程序里会留出对应的输入点,把硬线信号读进来参与判断。
1.2 为什么选博途V15加SCL而不是梯形图
楼宇设备类的项目里,很多人习惯用梯形图,觉得直观、电气师傅看得懂。但如果目标是写出真正可扩展、可维护的调度逻辑,SCL的优势在六层这种规模上就已经很明显了。
拿“当前楼层上方是否存在任何未响应的呼叫”这个条件举例。梯形图的做法是把2层、3层、4层、5层、6层的呼叫触点一并联,逐层判断,如果电梯是二十层,那个网络得画到屏幕外面去。SCL只需要一个FOR循环遍历楼层数组,几行代码搞定,而且楼层层数变化时循环范围改一下就行,梯形图却要改整个网络结构。
还有一个更关键的点,电梯调度不是单纯的“有呼叫就运行”,它涉及状态迁移。状态机用SCL的CASE语句写出来非常清爽:待机、上行、下行、开门、关门、故障,每种状态下的动作和转移条件一目了然。梯形图实现状态机不是不行,只是状态多了以后,互锁条件交织在一起,调试时光“在线看状态”就得一层层翻网络。
博途V15配合S7-1500系列CPU,性能完全不是瓶颈。六层电梯的呼叫扫描、目标搜索、状态刷新,哪怕按10毫秒周期跑,1500系列的处理能力也绰绰有余。而且博途V15本身支持PLCSIM仿真,在写代码阶段就能先把调度逻辑跑一遍,这对电梯这种逻辑密集型项目特别有价值。
1.3 程序块规划:别把所有逻辑塞进一个OB1
我见过不少新手把整个电梯控制写在一个OB1循环里,几百行代码从上往下堆,后面想查方向跑到哪里出问题,光找变量就找半天。这次我采用的拆分方式是:OB1只做输入映射和主调度调用,真正逻辑分到FC和FB里。
具体来说,我规划了这样几个块:
- 一个共享数据块存放所有中间变量和登记信息,统一命名,方便HMI读写;
- 一个FC专门做呼叫登记,接收按钮信号并更新登记数组;
- 一个FC做方向决策和目标楼层搜索,输入是当前楼层、方向、登记数组,输出是目标楼层;
- 一个FB做运行控制和开关门时序,因为这里涉及定时器和电机状态的保持,用FB配合背景数据块会更合适。
这样一个块干一件事,出问题时排查范围小。而且SCL程序不像梯形图那样强依赖网络注释,FUNCTION块顶部的接口注释写清楚入参、出参,后面维护的人不至于一头雾水。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 电梯控制核心逻辑与状态机设计
2.1 状态机是程序的第一层防护网
电梯这种设备,最怕的不是逻辑复杂,而是状态混乱。如果程序里没有一个明确的状态概念,全靠散落各处的BOOL变量去推“现在是什么阶段”,早晚会出现两个互斥动作同时输出的情况。
我定义了一个INT变量state作为当前状态,取值含义固定,一方面供逻辑判断,另一方面直接送到HMI显示,调试时能看到电梯当前到底处于什么阶段。
| 状态值 | 含义 | 说明 |
|---|---|---|
| 0 | IDLE 待机 | 无呼叫,停在某层,门关 |
| 10 | RUN_UP 上行 | 正在向上运行 |
| 20 | RUN_DOWN 下行 | 正在向下运行 |
| 30 | OPEN_DOOR 开门 | 到达目标层,执行开门 |
| 40 | CLOSE_DOOR 关门 | 延时结束,执行关门 |
| 50 | FAULT 故障 | 超时、限位动作等 |
状态迁移的核心是“单向链路”:待机到运行,运行到开门,开门到关门,关门回到待机并重新判断下一目标。我不会允许从运行直接跳关门,也不允许从开门直接跳运行,所有直接跳转都会在互锁条件上设置障碍。
这里有个值得注意的细节:状态机的驱动源是“条件满足后的一次性跳转”,而不是每个周期都去评估所有状态。SCL里用CASE语句天然适合这种写法,每个状态分支内做该状态该做的事,再把转移条件写在分支尾部。
2.2 楼层位置追踪:平层感应器做基准
六层电梯的楼层位置判断,最简单也最可靠的方案是每层装一个平层感应器,轿厢侧面装隔磁板,当电梯到达某层平层区域时,对应感应器动作。程序中不需要去做复杂的编码器脉冲计算,直接把“哪个平层感应器有信号”映射成当前楼层就可以了。
这样做的原因是,脉冲累加存在累计误差,哪怕编码器精度很高,运行几千次后楼层位置也会漂移,最后出现显示楼层与实际楼层错位的情况。而平层感应器是物理位置基准,每一层都校正一次,不存在累积问题。
SCL实现非常简单,把六个平层信号做成数组,遍历赋值即可。实际使用时要注意信号抖动,特别是低速爬行阶段平层感应器可能反复通断。我的做法是加一个短延时确认:平层信号持续50毫秒以上才认定到达该层。这个延时放在FB里用TON实例做,不会阻塞整个扫描周期。
楼层位置确定后,电梯的目标楼层比较、方向搜索就都有了参照。程序里我让curFloor每次都在状态机迁移到IDLE前刷新,避免运行过程中频繁更新导致其他逻辑误判。
2.3 呼叫登记的数据结构:三个数组搞定一切
六层电梯的呼叫信息,我用了三个BOOL数组来保存:
- innerCall数组,下标1到6,存轿内选层登记;
- upCall数组,下标1到6,存各层上行外呼登记;
- downCall数组,下标1到6,存各层下行外呼登记。
数组下标直接对应楼层,这个设计比用一堆独立BOOL变量清爽得多。六层时还不明显,如果哪天要扩展到十层,只需要改数组长度和调用处循环上界,逻辑主体一行不用动。
顺向截梯的决策,本质上就是三个数组的扫描合并。电梯上行时,从当前楼层的下一层开始向上找,只要某个楼层在三个数组中有任何一个为TRUE,就认为该楼层存在待响应呼叫。下行同理。
呼叫清除的时机也很关键:电梯平层到位、开门之后,把当前楼层的innerCall、upCall、downCall全部复位。可能有同行会问,这样做会不会把反向呼叫也清掉了?实际不会造成丢失,因为外呼按钮是脉冲登记,乘客再次按下时会重新登记。多数电梯控制程序在开门后清空本楼层全部呼叫,逻辑上更省事,乘客上轿厢后自然会按内呼选层。
3. 博途V15下SCL编程实现要点
3.1 先建好数据结构:命名规范直接决定调试效率
我在博途V15里新建了一个共享数据块,取名Elevator_DB,把程序中要用到的输入映射、按钮登记、状态变量、定时器实例全部放进去。变量命名用驼峰式,首字母小写,类型尽量用BOOL、INT、ARRAY这些基础类型,不要为了炫技用太花哨的结构体。
scl复制// Elevator_DB数据结构示意
innerCall : ARRAY[1..6] OF BOOL; // 轿内选层登记
upCall : ARRAY[1..6] OF BOOL; // 上行外呼登记
downCall : ARRAY[1..6] OF BOOL; // 下行外呼登记
curFloor : INT; // 当前逻辑楼层
targetFloor : INT; // 目标楼层
direction : INT; // 1上行 -1下行 0待机
state : INT; // 状态机值
doorClosed : BOOL; // 门锁闭合反馈
doorOpenLimit : BOOL; // 开门到位
doorCloseLimit : BOOL; // 关门到位
upLimit : BOOL; // 井道上限位
downLimit : BOOL; // 井道下限位
数组的下标我统一从1开始,楼层号就是下标,不需要做偏移换算,不容易出错。upCall和downCall在顶层、底层会有不使用的元素,留空不参与判断,不会影响逻辑。
3.2 呼叫登记与自动清除的SCL写法
按钮信号的采集,最忌讳直接用触点电平去赋值登记位。因为按钮是电平保持型,人按一下可能持续几百毫秒,如果程序周期是10毫秒,同一个呼叫会被重复登记几十次,虽然BOOL重复赋值没有危害,但在后面做统计、做HMI状态时会出错。所以必须做上升沿处理。
scl复制// 轿内按钮登记:采用上升沿检测
FOR n := 1 TO 6 DO
IF innerBtn[n] AND NOT innerBtnLast[n] THEN
innerCall[n] := TRUE;
END_IF;
innerBtnLast[n] := innerBtn[n];
END_FOR;
同样的方式处理外呼按钮。外呼按钮分为上行和下行两组,登记逻辑完全一致,只是写入的数组不同。这里我把每个按钮的上周期状态也存在数据块里,程序周期开始时一次性映射,避免在多个块中重复访问物理输入。
呼叫清除的逻辑放在状态机进入开门状态之后。到达目标楼层,平层确认,开门命令发出,此时就把当前楼层的所有登记位复位:
scl复制// 开门状态下清除当前楼层全部呼叫登记
innerCall[curFloor] := FALSE;
upCall[curFloor] := FALSE;
downCall[curFloor] := FALSE;
清除动作我特意放在开门之后,而不是刚检测到平层时就执行,目的是防止电梯平层信号抖动导致还没真正停稳就误清呼叫,万一没停住又走了,呼叫就丢了。
3.3 方向判定与目标楼层搜索:顺向截梯的核心
这部分是整个SCL程序里最有含金量的地方。方向判定不应该是“只要有上方呼叫就上行、有下方呼叫就下行”这么粗鲁,因为现实场景经常是电梯在3层,4层有一个内呼,2层有一个外呼上行,这时候电梯先去哪?正确处理是看呼叫产生的先后顺序?实际多数控制采用就近原则加方向优先,我这次用了“保持当前方向优先、同向顺路响应”的策略。
先看目标搜索。以direction=1上行状态为例:
scl复制// 上行状态:寻找当前楼层上方最近的带呼叫楼层
IF direction = 1 THEN
targetFloor := 0;
FOR n := curFloor + 1 TO 6 DO
IF innerCall[n] OR upCall[n] OR downCall[n] THEN
targetFloor := n;
EXIT;
END_IF;
END_FOR;
// 上方无任何呼叫,准备掉头下行
IF targetFloor = 0 THEN
direction := -1;
targetFloor := curFloor - 1; // 先让状态机进入下行搜索
END_IF;
END_IF;
注意数组里的下行外呼也被纳入上行搜索条件,这是因为电梯上行途中如果某层有人按下行外呼,说明该层有乘客要下楼,电梯顺路停靠接人,乘客进轿厢后再按内呼选择目的楼层,这是完全合理的。只要该楼层有任何一种呼叫,电梯就把它当作可停靠目标。
下行分支的写法是对称的,从curFloor-1开始向下找,找到最近的带呼叫楼层作为targetFloor。如果下方没有呼叫,direction翻转为1,重新搜索。
待机状态生成初始方向时,需要做上下两个方向的扫描,比较最近呼叫距离:
scl复制// 待机状态:按就近原则决定初始方向
nearestUp := 99;
nearestDown := 99;
FOR n := curFloor + 1 TO 6 DO
IF innerCall[n] OR upCall[n] OR downCall[n] THEN
nearestUp := n - curFloor;
EXIT;
END_IF;
END_FOR;
FOR n := curFloor - 1 TO 1 BY -1 DO
IF innerCall[n] OR upCall[n] OR downCall[n] THEN
nearestDown := curFloor - n;
EXIT;
END_IF;
END_FOR;
IF nearestUp < nearestDown THEN
direction := 1;
ELSIF nearestDown < nearestUp THEN
direction := -1;
ELSE
direction := 1; // 两边距离一样时默认上行
END_IF;
这个策略对单部电梯足够好用。多个同行如果都在等电梯,最优先的是“让电梯尽快完成当前的响应序列”,而不是反复改方向去抢一个新呼叫。所以一旦电梯进入运行状态,我每周期扫描一次targetFloor,只有当前目标被服务完(平层开门)后,才会重新搜索下一目标。运行途中出现的新呼叫不会被单独响应,而是被顺路登记,等电梯到达当前目标后再次搜索时自然带上。这其实就是顺向截梯的软件表达。
3.4 运行、开关门与安全互锁:SCL里的CASE状态机
有了目标和方向,接下来的运行控制用状态机来写就非常顺。我在FB里用CASE语句处理状态迁移,输出指令在状态分支里直接赋值。
scl复制CASE state OF
0: // IDLE 待机
// 如果存在任何呼叫,先生成方向
IF hasAnyCall THEN
direction := 0; // 由方向判定逻辑刷新
END_IF;
10: // RUN_UP 上行
// 到达目标楼层且平层信号有效,进入开门
IF pingCeng[targetFloor] AND curFloor = targetFloor THEN
motorUp := FALSE;
brake := FALSE;
state := 30;
END_IF;
20: // RUN_DOWN 下行,逻辑对称,不再展开
30: // OPEN_DOOR 开门
doorOpenCmd := TRUE;
doorCloseCmd := FALSE;
IF doorOpenLimit THEN
state := 40;
doorOpenCmd := FALSE;
END_IF;
40: // CLOSE_DOOR 关门
doorCloseCmd := TRUE;
doorOpenCmd := FALSE;
IF doorCloseLimit THEN
doorCloseCmd := FALSE;
state := 0;
END_IF;
END_CASE;
运行输出是单独做互锁的,不能直接让状态机去控制两个方向的接触器。我单独写了一段互锁输出逻辑:
scl复制// 电机正反转硬性互锁
IF state = 10 AND doorClosed AND NOT upLimit THEN
motorUp := TRUE;
motorDown := FALSE;
ELSIF state = 20 AND doorClosed AND NOT downLimit THEN
motorDown := TRUE;
motorUp := FALSE;
ELSE
motorUp := FALSE;
motorDown := FALSE;
END_IF;
// 软件再次保证,防止上面分支遗漏
IF motorUp THEN
motorDown := FALSE;
END_IF;
IF motorDown THEN
motorUp := FALSE;
END_IF;
门锁信号的判断我放在了运行条件里:doorClosed必须为TRUE才允许输出运行指令,这从程序层面保证了“门不开车”的底线。
还有个容易被忽略的点,就是运行超时保护。电梯从启动到平层,正常十几秒内就能完成,一旦出现卡阻、目标楼层丢失或者传感器故障,电梯会一直运行到顶。所以我加了一个运行计时,超过20秒还没有平层信号,直接置故障状态并切除所有输出:
scl复制// 运行超时保护
IF (state = 10 OR state = 20) AND runningTime > T#20S THEN
faultCode := 1;
state := 50;
motorUp := FALSE;
motorDown := FALSE;
END_IF;
定时器我直接用FB里的TON实例,IN接“正在运行”的条件,PT设置为20秒,Q输出作为超时标志。这种处理让电梯在任何异常情况下都不会失控跑飞,现场调试时能少背很多锅。
4. 调试方法与常见问题排查实录
4.1 博途V15下用PLCSIM和监控表跑通逻辑
电梯这种设备,不可能一开始就上真梯调,太危险。我的习惯是把程序写到一定程度后,用博途V15自带的PLCSIM做纯逻辑仿真。S7-1500系列在V15版本下可以创建仿真PLC实例,把程序下载到仿真CPU里运行,然后在变量表里强制输入信号。
仿真阶段最实用的是“手动置位法”。把六个内呼按钮对应的BOOL变量改成TRUE,观察登记数组是否置位;再修改curFloor和pingCeng信号模拟电梯运行到某层,看状态机是否按预期从上行跳到开门。所有逻辑验证通过后,再上电空载测试。
监控表里我习惯同时看几个关键量:state、direction、curFloor、targetFloor、四个呼叫数组。只要这几个量变化规律符合预期,控制逻辑基本没问题。真正容易出错的往往是边界条件,比如电梯在1层待机时只有6层有呼叫,初始方向必须是上行,但如果程序里方向生成的逻辑写反了,电梯就会先往下跑撞下限位,这种问题只能在仿真里提前暴露。
4.2 方向跑飞、呼叫不清零、平层抖动这些高频Bug
这一节全是我自己踩过的坑,每一个都对应过一次现场尴尬。
方向反复横跳是最常见的。典型场景:电梯上行过程中,程序每周期都重新搜索目标,而搜索函数里把“本层带呼叫”也算进去了,结果电梯还没离开当前层就发现本层有呼叫,targetFloor一直是当前层,运行状态进不去。后来我改成搜索时严格从curFloor+1开始,并把状态机的进入RUN条件设为targetFloor大于curFloor,就再没出现过。
呼叫清除错位是另一个高频问题。最开始我把清除逻辑放在检测到平层信号的那一刻,结果电梯低速爬行时平层信号断断续续,逻辑反复清除又被重新登记。后来我把清除动作挪到开门状态,让平层信号先稳定确认,再进入开门,再清除,问题彻底解决。
平层信号抖动还直接导致过楼层显示乱跳。电梯经过六层时,五层、六层感应器动作时间差很小,如果程序没有延时确认,curFloor会在几个楼层之间闪。我用了TON延时50毫秒确认,相当于把感应器抖动过滤掉,楼层显示一下稳定了。
还有一个容易被忽略的:掉头逻辑与目标搜索的顺序。如果程序先改变direction再搜索目标,而搜索逻辑里同时又需要判断上下方向,就会出现掉头后第一次搜索拿到的是旧方向的残留目标。我的做法是:目标搜索永远以当前direction为准,掉头动作只发生在“当前方向搜索不到目标”之后,用一个布尔变量表示“需要掉头”,在下一个扫描周期再执行,不在同一周期连环修改方向和目标,这样逻辑上清爽很多。
4.3 现场调试的几点避坑经验
即使仿真全过,真梯调试还是会有幺蛾子。我这里说几个保命和保设备的心得。
第一,门锁信号强烈建议用中间继电器隔离后接入PLC,不要直接并接触点。门锁回路上带的触点太多,很容易有感应电和压降,直接进AI模块会读出飘忽不定的值。我处理过一次因为门锁反馈线太长导致高电平不稳的问题,加了继电器之后立刻干净。
第二,方向输出在程序上互锁还不够,硬线回路上也要用接触器互锁。如果软件异常导致上下行同时输出,至少接触器层面的机械互锁还能挡住。这种双保险在电梯这种运动设备上是必须的。
第三,检修模式一定要优先于自动模式。程序里我给检修信号做了最高优先级,检修时屏蔽所有调度,只允许点动上行、点动下行输出。这样维保人员在井道里作业时,至少不会被自动逻辑突然送电吓到。
第四,每次改完程序下载在线前,先确认电梯处于检修状态或者断电状态。PLC下载过程中输出可能被清掉,正在运行的电梯会停在半空。这个教训我是花钱买来的,从那以后下载程序再也没有贪图方便直接在线下载。
最后再分享一个小习惯:所有楼层按钮信号进PLC之后,我会在程序里对每个按钮做一个累计计数器,登记一次加一。这个计数器平时没人看,但一旦现场反馈“电梯不响应某层呼叫”,翻一下这个计数就能判断是按钮坏了、接线断了,还是程序逻辑把呼叫丢了。诊断效率比拿万用表捅半天高得多。
电梯控制这块,SCL带来的优势不是“代码写得快”,而是“逻辑理得清”。六层只是一个起点,这套状态机和数组调度的思路,往上扩展成十层、二十层只是改参数的问题。真要说有什么值得反复揣摩的,还是那些边界条件和异常情况的处理思路。设备和人的安全,永远排在调度算法的前面。
