1. 项目概述与核心挑战
去年带队参加西门子杯离散行业自动化赛项,拿了个东北赛区一等奖,项目是经典的3部10层电梯群控系统。这个题目可以说是工控领域的“Hello World”,但想做好、做稳、做出彩,里头的门道可不少。很多新手一上来就埋头写梯形图,结果往往是逻辑混乱、响应迟钝,甚至出现“打架”现象——比如两部电梯抢一个召唤信号。我们这个一等奖方案,核心思路就一条:用状态机思维替代过程思维,用消息队列管理任务优先级。听起来有点软件工程的味道?没错,工控编程发展到今天,早已不是简单的继电器逻辑堆叠,尤其是面对多设备协同的离散制造场景,没有清晰的数据结构和任务调度策略,程序很快就会变成一团乱麻。
这个项目适合所有正在学习或使用西门子S7-1200/1500系列PLC,并希望深入理解离散自动化系统设计的朋友。无论你是备战比赛的学生,还是从事设备开发的工程师,通过拆解这个3部10层电梯的程序,你不仅能掌握PLC编程技巧,更能学到一套应对复杂逻辑系统的设计方法论。接下来,我会抛开比赛文档里那些华而不实的描述,直接切入我们实际编程中遇到的坑、解决的方案,以及那些让程序稳定运行的关键细节。
2. 整体架构设计与核心思路拆解
2.1 为什么选择“集中调度+分布式执行”架构?
面对3部电梯,最常见的错误架构是“平等分布式”,即每部电梯的程序完全独立,只根据自身的传感器和按钮信号行动。这种架构的致命缺陷在于“视野局限”。电梯A不知道电梯B即将到达5楼,可能就会发生两部电梯同时响应5楼的上行召唤,造成资源浪费。我们的方案采用了“集中调度+分布式执行”的混合架构。
核心组件与数据流:
- 集中调度器(中央PLC):我们选用一台S7-1200作为主站,它不直接控制电机和门机,只做一件事——大脑决策。它实时收集所有外呼按钮、内选按钮、各电梯的当前位置、运行方向、载重状态,并运行调度算法,为每一个召唤信号分配最优的电梯。
- 分布式执行器(电梯从站PLC):三部电梯各由一台S7-1200控制,作为PROFINET从站。它们接收主站发来的目标指令(如“去8楼”),但保留独立的安全逻辑和运动控制。例如,关门遇阻、超载保护、平层调整这些需要毫秒级响应的动作,完全由从站本地处理,不依赖网络,确保了安全性和实时性。
- 人机界面(WinCC上位机):用于状态监控、参数设置和故障记录。这里有个关键点:WinCC只做监视和记录,不参与实时控制逻辑。所有控制命令依然通过PLC逻辑发出,避免因上位机卡顿导致系统失控。
注意:很多团队为了省事,用WinCC的脚本直接发命令给PLC,这是比赛和工程中的大忌。一旦上位机死机,整个系统就可能瘫痪。我们的原则是:控制权牢牢掌握在PLC手中。
2.2 调度算法的核心:综合代价函数计算
调度算法的优劣直接决定了电梯群的运行效率。我们摒弃了简单的“最近原则”,采用了动态综合代价函数。每当一个新的召唤信号产生,主站会为三部电梯分别计算一个“代价”,选择代价最小的电梯前往响应。
代价函数包含以下几个核心变量:
- 基础行程代价:召唤楼层与电梯当前楼层的绝对差值。这是主要因素。
- 方向一致性代价:如果召唤方向(上/下)与电梯当前运行方向一致,且召唤楼层在电梯前进路径上,则代价大幅降低(甚至为负奖励);如果方向相反,则代价增加一个很大的惩罚值。这避免了电梯“调头”响应,保证了运行顺滑。
- 负载因子代价:电梯当前载重率(通过称重传感器获得)。如果接近满载,则增加其响应新召唤的代价,优先引导乘客使用较空的电梯。
- 预测等待时间代价:根据电梯当前任务队列,估算其到达召唤楼层的时间。这里我们为每层楼的运行时间赋予了不同值(如上行uu秒,下行dd秒),模拟真实速度。
计算过程用结构化文本(SCL)语言在一个功能块(FB)中实现,清晰且高效。下面是一个简化的伪代码逻辑展示:
FUNCTION_BLOCK FB_ElevatorScheduler VAR_INPUT CallFloor : INT; // 召唤楼层 CallDirection : INT; // 召唤方向:1=上,-1=下 ElevatorData : ARRAY[1..3] OF ST_Elevator; // 三部电梯的状态结构体 END_VAR VAR_OUTPUT AssignedElevatorID : INT; // 被分配的电梯编号 END_VAR VAR i : INT; MinCost : REAL := 1.0E38; TempCost : REAL; END_VAR FOR i := 1 TO 3 DO // 1. 计算基础行程代价 TempCost := ABS(CallFloor - ElevatorData[i].CurrentFloor) * COST_PER_FLOOR; // 2. 计算方向一致性代价 IF ElevatorData[i].Direction <> 0 THEN // 电梯正在运行 IF (CallDirection = ElevatorData[i].Direction) AND ((CallDirection > 0 AND CallFloor > ElevatorData[i].CurrentFloor) OR (CallDirection < 0 AND CallFloor < ElevatorData[i].CurrentFloor)) THEN TempCost := TempCost - DIRECTION_BONUS; // 方向一致奖励 ELSE TempCost := TempCost + DIRECTION_PENALTY; // 方向相反惩罚 END_IF; END_IF; // 3. 计算负载因子代价 TempCost := TempCost + (ElevatorData[i].LoadRatio * LOAD_FACTOR); // 4. 计算预测时间代价(略,需遍历任务队列) // TempCost := TempCost + EstimatedTime; // 找出代价最小的电梯 IF TempCost < MinCost THEN MinCost := TempCost; AssignedElevatorID := i; END_IF; END_FOR;这个算法在OB1(主循环)中每个扫描周期都执行,但通过一个“信号锁存”机制,只有在新召唤产生时才真正触发一次分配计算,避免不必要的运算。
3. 核心程序模块详解与实操要点
3.1 电梯状态机的设计与实现
这是整个程序的心脏。每部电梯都被建模为一个有限状态机(FSM),我们定义了7个核心状态:IDLE(空闲待命)、DOOR_OPENING(开门中)、DOOR_OPEN(门已开)、DOOR_CLOSING(关门中)、ACCELERATING(加速上行/下行)、RUNNING(匀速运行)、DECELERATING(减速平层)。状态迁移由严格的条件触发,例如从RUNNING到DECELERATING,必须同时满足“到达目标楼层前N米”和“当前速度高于阈值”两个条件。
在博途(TIA Portal)中,我们用GRAPH(顺序功能图)语言来实现这个状态机,直观且易于维护。但GRAPH在复杂并行分支上有时不够灵活,因此核心的状态迁移判断逻辑我们放在了SCL编写的FB中。一个关键技巧是:将状态机的“当前状态”和“目标状态”分开存储。CurrentState反映电梯实际物理状态,TargetState是逻辑希望达到的状态。这样做的好处是,当发生紧急情况(如安全钳动作)时,可以立即将CurrentState强制跳转到FAULT,而无需考虑复杂的TargetState逻辑,安全优先级最高。
3.2 网络通信与数据同步的稳定性保障
主站与三个从站之间通过PROFINET进行周期性数据交换。我们创建了一个精心设计的数据交换区(DB块)。
主站发送给每个从站的数据包包括:
AssignedTargetFloor:分配的目标楼层。Command:命令字(如:1=开门,2=关门,3=紧急停止)。SystemTime:同步时间戳。
每个从站返回给主站的数据包包括:
CurrentFloor:当前楼层(结合编码器与楼层传感器校正)。CurrentState:电梯状态机状态。Direction:运行方向。LoadWeight:实时载重。FaultCode:故障代码。
这里最大的坑是数据不同步。比如主站认为电梯已经在5楼停稳(状态为DOOR_OPEN),于是分配了一个6楼的任务给它。但实际上从站因为网络瞬间抖动,状态还没更新过来,可能还在RUNNING,这就导致逻辑错误。我们的解决方案是:
- 引入心跳包与超时机制:每个数据包都带序列号和时间戳。主站检测每个从站的“心跳”,超过500ms无更新,即认为该从站通信故障,将其置为“离线”状态,不再分配新任务,并触发报警。
- 采用“请求-确认”机制:对于关键命令(如“去X楼”),主站发送后,必须收到从站返回的“命令已接收”确认,并且检测到电梯状态已进入相应流程(如开始加速),才认为该命令执行成功。否则,主站会在超时后重发命令或重新调度。
- 在DB块中使用“影子寄存器”:发送区和接收区都使用双字(DWord)或结构体。写入新数据前,先写入一个“准备就绪”标志位;接收方读取数据后,检查标志位和校验和(如CRC16),确认数据完整有效后才更新内部变量。
3.3 安全回路与故障处理程序设计
安全是电梯的第一生命线。我们的安全回路完全采用硬件优先的原则。急停按钮、安全钳开关、限速器开关等信号,不经过PLC逻辑处理,直接串联进安全继电器(或PLC的故障安全型输入模块)回路,切断主接触器电源。PLC程序里的安全逻辑,是第二道防线。
在软件层面,我们设计了一个分层故障处理系统:
- Level 1 (最高级) - 立即停止:如超速、蹲底、冲顶。触发后,立即切断运行接触器,抱闸制动,状态机跳转至
FAULT,需人工复位。 - Level 2 - 完成当前服务后停止:如电机过热、门锁异常。PLC会记录故障,但允许电梯完成当前运送任务(到达最近楼层并开门)后,再进入
FAULT状态。 - Level 3 - 仅报警:如通讯超时、称重传感器漂移。系统继续运行,但在WinCC上弹出报警信息,提示维护。
所有故障信息都记录在一个FIFO(先入先出)队列中,存储在保持性DB块里,即使PLC断电也不会丢失。这对于赛后故障复盘和工程现场调试至关重要。
4. WinCC上位机监控界面设计与实用技巧
WinCC的作用是让无形的数据流变得可见、可控。我们的设计原则是:信息密度高、操作便捷、报警醒目。
4.1 动态画面与实时数据绑定
主监控画面是一个10层楼、3部电梯的剖面图。每一层的外呼按钮(上/下)、电梯轿厢的当前位置(用一个小方块表示)、运行方向箭头、门状态(开/关动画)、载重百分比,全部与PLC的变量直接绑定。这里的关键是使用“指针化”技术。我们为三部电梯定义了相同的DB结构体,在WinCC中,通过一个内部变量CurrentDisplayElevator(1,2,3)来切换指向哪一部电梯的数据源。这样,只需要做一套图形模板,就能通过脚本动态显示三部电梯的数据,大大减少了画面开发工作量。
例如,轿厢位置方块的垂直坐标,绑定到变量\PLC\DB_ElevatorData[CurrentDisplayElevator].CurrentFloor_Pixel。CurrentFloor_Pixel是在PLC中已经根据楼层计算好的像素坐标值,WinCC直接读取显示,无需在画面脚本中进行复杂的计算。
4.2 数据记录与趋势分析功能
为了分析电梯的运行效率(如平均等待时间、能耗),我们启用了WinCC的变量记录功能。将关键变量(如每部电梯的功率、运行楼层、门开关次数)以1秒为周期记录到数据库中。比赛答辩时,我们直接调出历史趋势图,展示在早高峰模拟时段,我们的调度算法如何将平均等待时间从45秒降低到28秒,视觉效果和说服力都非常强。
一个实用技巧是:合理设置归档周期和分段。对于快速变化的信号(如电流),可以设置更短的归档周期(如100ms),但只归档最近1小时的数据。对于状态信息(如楼层),用1秒周期归档,保存更长时间。这样可以平衡数据细节和存储空间。
4.3 报警管理与操作日志
所有Level 2和Level 3的故障,都在WinCC中配置了报警消息。我们为不同级别的报警设置了不同的颜色和确认方式(Level 2需要操作员确认,Level 3仅显示)。同时,我们增加了一个简单的操作日志功能:任何通过WinCC界面进行的操作(如强制某部电梯到某层、修改参数),都会记录操作员、时间、动作和旧值/新值到一个CSV文件。这在多人调试或比赛演示时,能有效追溯问题来源。
5. 调试过程与核心问题排查实录
5.1 电梯“冲过”目标楼层问题
这是调试初期最头疼的问题。电梯在减速后,没有精确停在平层位置,而是滑行了一段距离,或者直接冲过。
排查过程:
- 检查机械:首先排除机械问题,确认制动器(抱闸)动作迅速,闸瓦间隙正常,钢丝绳无打滑。
- 检查传感器:平层传感器(通常是光电或磁感应开关)安装位置是否准确?信号是否稳定?我们用示波器抓取了传感器信号,发现信号干净无毛刺。
- 分析程序:问题指向了控制逻辑。我们的减速点是根据“剩余距离”计算的。最初算法是:当
目标楼层 - 当前楼层 = 1时,开始减速。但这里忽略了电梯的实际速度和减速度。如果电梯高速运行,从触发减速点到完全停止所需的距离会更长。
解决方案:引入“速度-距离”曲线控制。我们为电梯建立了简单的运动学模型:
- 已知电梯最大运行速度
Vmax,最大加速度Aacc,最大减速度Adec。 - 在运行过程中,实时计算从当前速度
Vcurrent减速到0所需的距离S_decel = (Vcurrent^2) / (2 * Adec)。 - 当
剩余距离 <= S_decel + 安全余量时,立即触发减速流程。 我们在PLC中用SCL实现了这个计算,减速点从固定的“楼层差”变成了动态的“距离差”,问题迎刃而解。
5.2 多部电梯响应同一召唤的“抢单”现象
在调度算法未优化前,偶尔会出现两部电梯几乎同时响应同一个外呼按钮,然后一部电梯中途又取消,造成乘客困惑和资源浪费。
原因分析:根本原因是主站调度计算周期与从站状态更新不同步。假设在扫描周期开始时,主站读取到电梯A和B的状态都显示“空闲”,且距离召唤楼层相同。主站计算后,将任务分配给了电梯A,并更新了分配状态DB。但在同一个扫描周期内,这个“已分配”状态还没来得及通过网络传给从站A,也从站B。在下一个扫描周期,主站可能因为某种原因(如通信延迟导致它认为电梯A未响应)再次计算,又把任务分配给了电梯B。
解决方案:
- 状态快照与原子操作:在主站调度FB的开始,立即将所有电梯的实时状态一次性读入一个局部数组(状态快照)。整个代价计算和分配决策都基于这个快照进行,避免在计算过程中状态发生变化导致逻辑混乱。
- 引入“任务锁”:一旦一个召唤信号被分配给某部电梯,立即用一个唯一的任务ID“锁定”该信号和该电梯。在任务完成或超时取消前,调度器会忽略这个已被锁定的信号。任务ID可以是一个自增的整数,和召唤信号、目标电梯编号一起存储。
- 增加分配确认延时:主站发出分配指令后,会等待一个短暂的时间(如200ms),确认目标电梯的状态已从“空闲”变为“已接受任务”(如
ACCELERATING)。如果超时未确认,则释放“任务锁”,将该召唤信号重新放入调度队列。这避免了因单次通信失败导致的死锁。
5.3 WinCC画面切换卡顿与变量更新延迟
在早期版本中,点击WinCC画面上的按钮切换不同电梯的监控视图时,会有明显的卡顿,数据更新也不及时。
排查与解决:
- 减少画面对象数量:检查发现,为了美观,我们在背景上放置了大量静态的、复杂的矢量图形。这些图形虽然不绑定变量,但会消耗大量的渲染资源。我们将静态背景转换为一张优化后的JPG图片,卡顿立即减轻。
- 优化变量连接:WinCC默认的变量更新周期可能较长。我们进入“变量管理”,将关键实时变量(如楼层、状态)的“采集周期”和“更新周期”设置为更短的时间(如250ms)。对于非关键的变量(如总运行时间),则保持较长的周期(如2s)。
- 使用C脚本替代VBS:在一些需要复杂逻辑的动态显示中(如根据负载率改变轿厢颜色),我们最初用了VBS脚本,发现执行效率较低。后来改用WinCC C脚本(ANSI-C),执行速度提升了一个数量级。虽然C脚本写起来麻烦些,但对于性能关键点非常有效。
- 分页加载:将历史数据记录、参数设置等不常用的功能做到独立的弹出窗口或子画面中,而不是全部堆砌在主画面上,主画面只保留最核心的监控元素。
6. 工程化思维与比赛心得总结
做完这个项目,最大的体会是:比赛程序和工业程序的差距,往往在于对“异常”和“边界”的处理。比赛场景是理想的,但我们的程序必须考虑所有不理想的情况:网络断了怎么办?传感器突然失灵怎么办?按钮被长时间卡住怎么办?这些“怎么办”的解决方案,才是程序稳健性的关键。
给后来者的几点建议:
- 从数据字典开始:动手写第一行梯形图之前,先用Excel或思维导图把所有的IO点、中间变量、DB块结构定义清楚。统一的命名规范(如
M_开头表示中间变量,DB1_开头表示全局数据)会让你在后期调试时感谢自己。 - 模块化、标准化:把电梯控制分解成“运动控制”、“门控制”、“故障处理”、“通讯处理”等标准功能块(FB)。每部电梯都实例化这些FB。这样,修改逻辑只需改一个FB,所有电梯同步更新。
- 重视注释和文档:在每一个网络、每一个功能块的标题栏写清楚它的功能。复杂的算法,用SCL旁边的注释行解释每一步的目的。比赛答辩和日后维护时,清晰的注释就是最好的说明书。
- 模拟测试要做足:在连接真实电梯模型前,充分利用TIA Portal的PLCSIM Advanced和WinCC的Runtime进行联合仿真。模拟各种极端情况:同时按下所有楼层按钮、模拟通信中断、模拟超载。仿真中暴露的问题,比在硬件上调试容易解决十倍。
- 安全永远是第一位:无论逻辑多么精巧,硬件安全回路必须独立、可靠。软件里要有冗余判断和故障安全路径。在程序初始化时,增加一个全面的自检流程,检查所有关键传感器和执行器是否正常。
这个3部10层电梯的程序,代码量不大,但麻雀虽小五脏俱全。它涵盖了离散自动化中数据采集、集中决策、分布式控制、网络通信、人机交互、故障诊断几乎所有核心环节。吃透这个项目,再去面对更复杂的生产线、仓储物流系统,你会发现底层的思想是相通的:用状态描述设备,用消息驱动任务,用算法优化调度,用分层保障安全。希望这份结合了实战和教训的讲解,能给你带来实实在在的帮助。