简介:这份资源是西门子杯中国智能制造挑战赛「六部十层电梯群控」赛项的一等奖参考程序,面向自动化、电气工程等专业的高校参赛选手与指导教师,也适合希望深入理解多梯协同调度逻辑的工程学习者。压缩包共70个文件,约8.52MB,以xml工程配置、del与cfs等西门子工程数据文件、tvx/tvd/tis等归档与索引文件为主,另含plf、idx、db等数据库与日志类文件,完整保留了原版工程目录结构。目前已有10250人学习下载,热度较高。程序围绕六台电梯服务十个楼层的群控场景,涵盖电梯基本控制逻辑、预测性群控调度算法、传感器与通信机制、基于西门子PLC与TIA Portal的编程实现以及故障检测与安全保护等核心知识点,读者可据此对照复现赛题方案、分析调度策略并排查调试问题,是一份兼具竞赛参考与工程学习价值的实战例程。
1. 六部十层电梯群控:一份一等奖例程到底解决了什么问题
六部十层电梯群控参考程序,这个标题里藏着三个关键信息:六部电梯、十层楼、群控。单部电梯的PLC程序很多工程师都写过,起停、开关门、楼层判断,逻辑清晰就能跑。但一旦电梯数量变成六部,楼层变成十层,问题就从“单机控制”跳到了“多智能体调度”——六部电梯共享十个楼层的外呼按钮,任何一层有人按了上行或下行,到底派哪部电梯去接?派错了,乘客等半天,系统效率崩盘。
这份一等奖例程的价值不在于代码写得多花哨,而在于它给出了一套经过竞赛验证的调度策略和状态机框架。适合谁看?正在准备西门子杯这类工程竞赛的学生、需要做电梯群控算法验证的自动化工程师、以及想理解多设备协同调度逻辑的PLC开发者。它解决的核心问题是:在有限楼层和电梯数量下,如何用可编程逻辑实现一套公平、高效、不打架的派梯规则。
我见过太多队伍在单梯程序上跑得飞起,一加群控就翻车——要么六部电梯抢同一个外呼,要么全部往一个方向跑导致反向楼层没人管。这份例程的思路值得拆开看。
2. 群控调度的核心逻辑:从单梯状态机到多梯派梯算法
2.1 单部电梯的状态机该怎么建
在讨论群控之前,必须先确保单部电梯的状态机是干净的。常见做法是把电梯运行拆成几个明确的状态:空闲、上行、下行、开门中、关门中、停靠。每个状态有明确的进入条件和退出条件,避免状态混乱导致电梯“抖动”——比如在两层之间反复起停。
我一般会用整数变量表示状态,而不是用多个布尔量互相嵌套。布尔量一多,逻辑交叉后极难排查。下面是一个状态定义的结构示例:
# 单部电梯状态机核心变量定义(伪代码,映射到SCL/ST) STATE_IDLE = 0 # 空闲,无任务 STATE_MOVING_UP = 1 # 上行中 STATE_MOVING_DOWN = 2 # 下行中 STATE_DOOR_OPEN = 3 # 开门中 STATE_DOOR_CLOSE = 4 # 关门中 # 每部电梯维护自己的状态 current_state = STATE_IDLE current_floor = 1 # 当前楼层 target_floor = 1 # 目标楼层 direction = 0 # 0停止 1上行 -1下行 car_calls = [False]*10 # 内呼按钮(轿厢内)这段代码的关键在于:每部电梯独立维护自己的状态和任务队列。car_calls是轿厢内按钮,和外呼按钮分开处理。很多新手会把内外呼混在一起,导致电梯响应了外呼却忘了内呼,或者反过来。
参数说明:current_floor和target_floor用整数表示楼层,方便比较大小判断方向。direction用三值而不是布尔,因为停止状态需要单独表达。car_calls用布尔数组,索引对应楼层减一。
2.2 外呼分配:为什么“最近优先”经常翻车
群控的核心是外呼分配。十层楼,每层有上行和下行两个外呼按钮(顶层只有下行,底层只有上行),总共18个外呼点。六部电梯共享这些外呼,当某个外呼被按下时,调度器需要决定派哪部电梯去响应。
最直觉的策略是“最近优先”——哪部电梯离呼叫楼层最近就派哪部。这个策略在低峰期没问题,但在高峰期会翻车。原因很简单:最近的那部电梯可能正在执行相反方向的任务,你把它派过去,它得先完成当前任务再掉头,实际到达时间反而更长。更糟的是,如果连续多个外呼都派给同一部电梯,其他电梯空闲,整体效率暴跌。
这份例程采用的是一种“顺向优先+负载均衡”的混合策略。具体逻辑是:先筛选出所有处于空闲状态或运行方向与呼叫方向一致的电梯,在这些候选电梯中,选择“预计到达时间最短”的那部。预计到达时间的估算要考虑电梯当前方向、当前楼层、已有任务队列的长度。
def assign_external_call(call_floor, call_direction, elevators): """ 外呼分配核心逻辑 call_floor: 呼叫楼层 1-10 call_direction: 1上行 -1下行 elevators: 六部电梯的状态列表 """ candidates = [] for i, elev in enumerate(elevators): # 条件1:空闲电梯,直接作为候选 if elev.state == STATE_IDLE: cost = abs(elev.current_floor - call_floor) candidates.append((i, cost)) # 条件2:同向电梯且尚未经过呼叫楼层 elif elev.direction == call_direction: if call_direction == 1 and elev.current_floor <= call_floor: cost = call_floor - elev.current_floor + len(elev.task_queue) * 2 candidates.append((i, cost)) elif call_direction == -1 and elev.current_floor >= call_floor: cost = elev.current_floor - call_floor + len(elev.task_queue) * 2 candidates.append((i, cost)) if not candidates: # 没有同向或空闲电梯,退化为最近优先 for i, elev in enumerate(elevators): cost = abs(elev.current_floor - call_floor) + len(elev.task_queue) * 3 candidates.append((i, cost)) # 选择代价最小的电梯 best = min(candidates, key=lambda x: x[1]) return best[0]逻辑说明:第一轮筛选只考虑空闲电梯和同向且未经过呼叫楼层的电梯。task_queue的长度乘以2作为惩罚项,意味着已有任务的电梯优先级降低,避免任务堆积。如果第一轮没有候选,说明所有电梯都在反向运行或已过楼层,此时退化为全局最近优先,但惩罚系数加大到3,更倾向于任务少的电梯。
参数调整建议:惩罚系数2和3不是固定的,可以根据实际测试调整。如果发现电梯任务分配不均,加大惩罚系数;如果发现响应太慢,减小惩罚系数。这个需要在实际仿真中反复调试。
2.3 内呼与外呼的优先级仲裁
内呼是轿厢内乘客按的,外呼是楼层等电梯的人按的。两者冲突时谁优先?常见做法是内呼绝对优先,因为轿厢内的乘客已经在电梯里了,先送他们到目标楼层是合理的。但这样会导致外呼等待时间过长,尤其在高峰期。
例程采用的是一种“内呼优先但设上限”的策略:内呼任务正常排队执行,但如果某个外呼的等待时间超过阈值(比如30秒),调度器会强制将该外呼插入到某部电梯的任务队列前端。这个阈值需要根据楼层数和电梯速度来定。
# 外呼等待超时强制插入 WAIT_THRESHOLD = 30 # 单位:秒 def check_overtime_calls(external_calls, elevators, current_time): for call in external_calls: if call.is_active and (current_time - call.press_time) > WAIT_THRESHOLD: # 找到任务队列最短的电梯,强制插入 target_elev = min(elevators, key=lambda e: len(e.task_queue)) target_elev.task_queue.insert(0, call) call.is_active = False # 标记已分配这段逻辑的关键是insert(0, call),把超时外呼插到队列最前面,确保下一个被执行。WAIT_THRESHOLD设30秒是经验值,楼层多、电梯慢的场景可以适当放宽。
3. 在博途里把六部电梯跑起来:从变量表到仿真验证
3.1 变量表设计与DB块规划
六部电梯意味着六套相同的状态数据。常见做法是建一个UDT(用户自定义数据类型),然后建一个包含六个该类型元素的DB块。这样代码可以写成循环,避免六份重复逻辑。
-- UDT定义:Elevator_Type STRUCT current_floor : Int; // 当前楼层 target_floor : Int; // 目标楼层 direction : Int; // 0停 1上 -1下 state : Int; // 状态机状态 door_open : Bool; // 门状态 car_calls : Array[1..10] of Bool; // 内呼 task_queue : Array[1..20] of Int; // 任务队列(楼层号) queue_length : Int; // 队列长度 END_STRUCT; -- DB块:Elevator_Group_DB STRUCT elevators : Array[1..6] of Elevator_Type; external_calls_up : Array[1..10] of Bool; // 上行外呼 external_calls_down : Array[1..10] of Bool; // 下行外呼 call_timestamp_up : Array[1..10] of DInt; // 外呼按下时间 call_timestamp_down : Array[1..10] of DInt; END_STRUCT;这种结构的优势是:所有电梯共用一套逻辑代码,通过数组索引访问不同电梯的数据。写程序时只需要写一份逻辑,用FOR循环遍历六部电梯。维护时改一处,六部电梯同时生效。
注意:task_queue的长度设为20是冗余设计,实际十层楼最多10个内呼加若干外呼,20够用。queue_length单独维护,避免每次遍历数组计算长度。
3.2 调度主循环的SCL实现框架
调度器需要周期性执行,通常放在OB1的主循环里,或者用一个定时中断OB(比如OB35,100ms周期)。周期太短浪费CPU,太长响应迟钝。100ms是常见选择。
// SCL代码框架:调度主循环 FOR #i := 1 TO 6 DO // 第一步:更新每部电梯的状态机 #elev := #Elevator_Group_DB.elevators[#i]; CASE #elev.state OF 0: // 空闲状态,检查任务队列 IF #elev.queue_length > 0 THEN #elev.target_floor := #elev.task_queue[1]; IF #elev.target_floor > #elev.current_floor THEN #elev.direction := 1; #elev.state := 1; ELSIF #elev.target_floor < #elev.current_floor THEN #elev.direction := -1; #elev.state := 2; ELSE #elev.state := 3; // 同层,直接开门 END_IF; END_IF; 1: // 上行中,检查是否到达目标楼层 IF #elev.current_floor = #elev.target_floor THEN #elev.state := 3; // 到达,开门 #elev.direction := 0; END_IF; // ... 其他状态处理 END_CASE; // 第二步:检查外呼分配 // 遍历所有外呼点,对未分配的外呼调用分配函数 END_FOR;这段框架的核心是:每部电梯独立跑状态机,调度器在外层统一处理外呼分配。CASE语句处理状态转移,每个状态只关心自己的进入和退出条件。
参数说明:#elev是临时变量,用于简化代码书写。实际写的时候要注意SCL的数组索引是从1开始还是从0开始,博途默认从0开始,但UDT里定义的是1..10,需要统一。
3.3 用PLCSIM做六部电梯的联合仿真
写完程序不仿真就下载到真实PLC,是典型的翻车操作。博途自带的PLCSIM可以仿真S7-1500系列PLC,支持多实例仿真。但六部电梯的仿真需要一些技巧。
常见做法是:建一个监控表,把六部电梯的关键变量都放进去,包括当前楼层、方向、状态、任务队列长度。然后手动触发外呼按钮,观察六部电梯的响应。更高效的方式是写一个简单的仿真脚本,自动生成外呼请求序列,模拟高峰期客流。
# 外部仿真脚本示例(通过OPC UA或S7通信写入PLC变量) import time import random def simulate_peak_traffic(): """模拟高峰期:随机楼层随机方向的外呼请求""" for _ in range(50): floor = random.randint(1, 10) direction = random.choice([1, -1]) # 底层只有上行,顶层只有下行 if floor == 1: direction = 1 elif floor == 10: direction = -1 # 写入PLC外呼变量 write_external_call(floor, direction) time.sleep(random.uniform(0.5, 2.0))这个脚本的作用是自动产生外呼请求,避免手动点击的繁琐。通过观察六部电梯的响应,可以快速发现调度逻辑的问题,比如某部电梯任务堆积、某层外呼长时间无人响应等。
注意:仿真时要把电梯的运行速度参数调快,否则等一次完整运行要几十秒,效率太低。在OB35里把楼层变化的时间间隔改小即可。
4. 避坑指南:群控程序调试中最容易翻车的五个点
4.1 六部电梯同时响应同一个外呼
现象:某一层按下外呼后,六部电梯同时改变方向朝该层运行,到达后互相等待,系统死锁。
原因:外呼分配逻辑没有做“已分配”标记。每部电梯都在独立扫描外呼按钮,发现有人按了就响应,没有全局协调。
解决:外呼按钮的状态需要三态:未按下、已按下未分配、已分配。调度器分配后立即将外呼标记为已分配,并记录分配的电梯编号。其他电梯扫描时跳过已分配的外呼。释放条件是该电梯到达该楼层并开门后,清除分配标记。
4.2 电梯在两层之间反复起停(抖动)
现象:电梯在3楼和4楼之间来回运行,不停开关门,但没有人进出。
原因:目标楼层判断逻辑有误。常见的是内呼和外呼同时存在时,电梯到达一个楼层后,队列更新不及时,导致目标楼层反复切换。
解决:任务队列的更新必须是原子的。到达楼层后,先清除该楼层的所有内呼和外呼,再从队列中移除已完成的任务,最后才重新计算下一个目标楼层。顺序不能乱。另外,在状态机里加一个“停靠延时”,到达楼层后至少保持开门状态2秒,避免瞬间切换。
4.3 外呼等待时间过长导致乘客流失
现象:仿真跑下来,某些外呼的平均等待时间超过60秒,远超合理范围。
原因:调度策略过于偏向内呼,或者惩罚系数设置不当,导致外呼总是被排到队列末尾。
解决:引入等待时间超时机制(前面提到的WAIT_THRESHOLD),同时调整任务队列的排序规则。我一般会把外呼的优先级设为内呼的1.5倍,即外呼在队列中的排序权重更高。具体实现是在插入队列时,根据任务类型决定插入位置。
4.4 PLCSIM仿真时变量不更新
现象:程序下载到PLCSIM后,监控表里的变量值不变,电梯不动。
原因:PLCSIM的扫描周期和真实PLC不同,OB35的定时中断可能没有正确触发。另外,某些通信写入的变量在PLCSIM里需要手动使能“允许PUT/GET通信”。
解决:检查OB35是否已经下载并在PLCSIM中运行。在PLCSIM的设置里开启“允许来自远程对象的PUT/GET通信访问”。如果用的是仿真脚本写入变量,确认通信连接正常,可以用PLCSIM自带的监控表先手动写入测试。
4.5 楼层索引越界导致PLC停机
现象:程序运行一段时间后PLC报错停机,诊断缓冲区显示“数组索引越界”。
原因:任务队列的索引没有做边界检查。当队列为空时,访问task_queue[1]会越界。或者楼层计算时出现了0或11这样的非法值。
解决:所有数组访问前必须检查索引范围。队列操作封装成函数,入队和出队都做边界判断。楼层变量在赋值前用LIMIT函数限制在1到10之间。这类错误在仿真时可能不报,下载到真机就停机,血泪经验。
5. 进阶技巧:用评分函数替代硬规则,让派梯更聪明
前面讲的调度策略本质上是基于规则的:同向优先、空闲优先、超时强制。规则的好处是逻辑清晰、容易调试,但缺点是参数固定,难以适应不同的客流模式。高峰期和低谷期的最优策略是不同的,硬规则很难兼顾。
进阶做法是引入一个评分函数,把每部电梯对某个外呼的“适合度”量化成一个分数,然后选分数最高的。评分函数可以综合考虑多个因素:预计到达时间、当前任务队列长度、电梯方向、历史响应速度等。每个因素乘以一个权重,权重可以根据时段动态调整。
def score_elevator(elev, call_floor, call_direction, weights): """ 评分函数:分数越高越适合响应外呼 weights: 权重字典,可根据时段调整 """ score = 0 # 因素1:距离分数(越近越高) distance = abs(elev.current_floor - call_floor) score += weights['distance'] * (10 - distance) # 10层楼,距离最大9 # 因素2:方向一致性分数 if elev.state == STATE_IDLE: score += weights['idle'] * 10 elif elev.direction == call_direction: score += weights['same_direction'] * 8 else: score += weights['opposite'] * 2 # 因素3:负载分数(任务越少越高) load_penalty = len(elev.task_queue) * weights['load'] score -= load_penalty # 因素4:历史响应速度(可选,需要记录数据) if elev.avg_response_time > 0: score += weights['history'] * (30 / elev.avg_response_time) return score这个评分函数的优势在于:权重可以动态调整。高峰期加大load权重,让任务少的电梯优先;低谷期加大distance权重,让最近的电梯优先。甚至可以按时段预设几组权重,比如早高峰、午间、晚高峰各一套。
参数调整建议:先用默认权重跑仿真,记录每部电梯的利用率和外呼平均等待时间。然后逐步调整权重,观察指标变化。distance和load是影响最大的两个权重,先调这两个。history权重需要积累一定数据后才有效,初期可以设为0。
验证方法:设计三组测试场景——均匀客流(每层随机外呼)、上行高峰(底层大量上行)、下行高峰(顶层大量下行)。分别记录平均等待时间、最长等待时间、电梯利用率。好的调度策略应该在三种场景下都不出现极端值。
我自己的习惯是:每次改完调度逻辑,先跑100次随机外呼的仿真,看统计指标。如果平均等待时间比上一版差,直接回退,不纠结。群控调度的优化是无止境的,但竞赛或实际项目都有时间限制,找到够用的策略就收手。希望帮到你。
本文还有配套的精品资源,点击获取