1. 立体车库仿真项目概述
这个三行六列16车位的立体车库仿真项目,是我去年为一个自动化停车场系统开发的MCGS6.2人机界面程序。不同于常规矩阵式布局,这个设计在3×6的框架中巧妙隐藏了两个设备间,形成了16个可用车位的特殊结构。这种非常规布局给运动控制逻辑带来了独特挑战,特别是当需要同时处理多车位升降请求时。
项目核心在于用脚本精确控制每个车位的垂直运动,同时确保与PLC的实时数据同步。通过MCGS的动画组件和脚本系统,我们实现了车位状态的动态显示、冲突检测和优化调度。整个开发过程中最耗时的部分不是基础功能实现,而是处理那些"边界情况"——比如当两个相邻车位同时请求升降时,如何避免机械干涉;又或者当通信延迟发生时,怎样保证状态同步不出现逻辑混乱。
2. 界面布局与动画实现
2.1 车位元件构建技巧
在MCGS中构建立体车库界面时,我放弃了使用现成的3D组件,转而采用基础的矩形元件组合。这种方法虽然看起来原始,但实际运行效率更高,也更便于精确控制每个车位的独立运动。具体实现时,每个车位由三个矩形组成:底部托盘(纯色填充)、四周边框(较深色线条)和顶部标识(车位编号文本)。
关键设置在于元件的垂直移动属性。通过脚本动态设置移动范围和速度:
basic复制!SetMove("车位A1", 0, 120, 0) // 设置垂直移动范围0-120mm
!SetMoveSpeed("车位A1", 2.3) // 移动速度2.3mm/s
这里有个细节需要注意:移动范围的上限值(示例中的120mm)必须与PLC程序中设定的物理限位完全一致,否则会导致动画位置与实际设备位置不同步。我通常在项目文档中用红色标注这个参数,提醒后续维护人员。
2.2 动画流畅性优化
初期直接使用While循环控制动画刷新时,遇到了严重的界面卡顿问题。后来改为定时器驱动方案,核心代码如下:
basic复制// 在窗体初始化时设置定时器
!SetTimer(1, 50) // 50ms触发一次
// 定时器事件处理
Sub OnTimer1()
For i = 1 To 16
currentPos = !GetPosition("车位" + i)
targetPos = !GetDevice("PLC_车位" + i + "_位置")
If Abs(currentPos - targetPos) > 2 Then
!SetMoveStep("车位" + i, (targetPos - currentPos)/5)
End If
Next
End Sub
这种增量式移动算法不仅解决了卡顿问题,还使动画更加平滑。参数中的除数5是个经验值,太小会导致移动抖动,太大则显得迟缓。不同硬件环境下可能需要微调。
3. 核心控制逻辑设计
3.1 车位状态管理方案
最初采用二维数组存储车位状态时,遇到了索引混乱的问题。最终实现的混合方案结合了数组和字典的优点:
basic复制// 全局变量定义
Dim slotStatus[16] As Integer // 索引1-16对应实际车位
Dim slotMap As Object // 键值对映射
// 初始化映射关系
Sub InitSlotMap()
slotMap = {"A1":1, "A2":2, "A3":3, "A4":4,
"B1":5, "B2":6, "B3":7, "B4":8,
"C1":9, "C2":10, "C3":11, "C4":12,
"D1":13, "D2":14, "D3":15, "D4":16}
// 注意:B5,C5为设备间,不参与控制
End Sub
这种设计既保持了数组的访问效率,又提供了灵活的命名转换。在实际调用时,通过slotMap["A1"]即可获取到数组索引,再操作slotStatus数组。
3.2 升降控制状态机
每个车位的完整生命周期包含四种状态:
- 空闲(0):车位处于就绪状态
- 预约中(1):已接收指令但未开始移动
- 移动中(2):正在执行升降动作
- 故障(-1):传感器异常或超时
状态转换逻辑通过以下函数实现:
basic复制Function ChangeSlotState(slotName, newState)
index = slotMap[slotName]
If index = 0 Then Return False // 无效车位
oldState = slotStatus[index]
// 状态转换校验
Select Case oldState
Case 0: // 空闲
If newState <> 1 Then Return False
Case 1: // 预约中
If newState <> 2 Then Return False
Case 2: // 移动中
If newState <> 0 And newState <> -1 Then Return False
Case -1: // 故障
If newState <> 0 Then Return False
End Select
slotStatus[index] = newState
!SetDevice("PLC_车位状态", index, newState) // 同步到PLC
Return True
End Function
4. 多线程冲突解决方案
4.1 基于令牌桶的请求调度
当多个车位同时请求操作时,简单的互斥锁会导致效率低下。我设计了一个令牌桶算法来控制并发:
basic复制// 全局变量
Dim operationTokens As Integer = 2 // 同时允许2个车位运动
Dim tokenLock As Integer = 0 // 令牌锁
Function AcquireToken() As Boolean
// 自旋锁实现
For i = 1 To 100 // 最大尝试次数
If !CompareAndSet(tokenLock, 0, 1) Then
If operationTokens > 0 Then
operationTokens = operationTokens - 1
tokenLock = 0
Return True
End If
tokenLock = 0
End If
Delay(10)
Next
Return False
End Function
Sub ReleaseToken()
!AtomicIncrement(operationTokens)
End Sub
这个实现中CompareAndSet是模拟的原子操作,实际MCGS环境中需要用特殊技巧实现。令牌数设为2是因为物理设备允许两个升降电机同时工作,但不可更多。
4.2 死锁检测与恢复
在长时间运行测试中,我们发现某些异常情况下锁会无法释放。于是增加了看门狗机制:
basic复制// 在全局定时器中检查
Sub WatchdogCheck()
For Each slot In slotStatus
If slot = 2 Then // 移动中状态
If !GetSystemTime() - slot.startTime > 30000 Then // 30秒超时
!EmergencyStop(slot)
slotStatus[slot.index] = 0 // 强制恢复空闲
ReleaseToken()
End If
End If
Next
End Sub
5. PLC通信优化实践
5.1 数据批量传输方案
原始的逐个寄存器读取方式导致通信周期长达500ms,优化后采用批量读写策略:
basic复制// 通信配置
!SetMBTCPAddr("PLC1", "192.168.1.10", 502)
!SetMBTCPBatch("车位状态", 4000, 16, 2) // 从4000开始读16个字的保持寄存器
// 定时读取
Sub ReadPLCData()
data = !MBTCPRead("PLC1", "车位状态")
If data.Length = 16 Then
For i = 1 To 16
ProcessSlotData(i, data[i])
Next
End If
End Sub
批量读取将16个车位的状态一次性获取,通信周期降至200ms以内。注意保持寄存器的地址需要与PLC程序严格对应。
5.2 事件触发通信机制
为进一步降低通信负载,我们实现了状态变化触发机制:
basic复制// PLC端配置
当任何车位状态变化时:
设置标志位MW100=1
将变化的车位索引存入MW101
将新状态存入MW102
// MCGS端处理
Sub OnPLCEvent()
If !GetDevice("PLC_MW100") = 1 Then
index = !GetDevice("PLC_MW101")
newState = !GetDevice("PLC_MW102")
slotStatus[index] = newState
!SetDevice("PLC_MW100", 0) // 确认接收
End If
End Sub
这种推模式(push)相比轮询(polling)能减少约70%的通信量,特别适合状态变化不频繁的场景。
6. 调试与性能优化记录
6.1 运动参数调优
通过大量测试,我们确定了不同负载下的最优速度参数:
| 负载重量(kg) | 建议速度(mm/s) | 加速度(mm/s²) |
|---|---|---|
| 0-500 | 2.5 | 5 |
| 500-1000 | 2.0 | 4 |
| 1000-1500 | 1.5 | 3 |
这些参数需要写入PLC的配方数据块,MCGS通过以下代码动态设置:
basic复制Sub SetMotionParams(weight)
Select Case weight
Case 0 To 500
!SetDevice("PLC_速度设定", 25) // 单位0.1mm/s
!SetDevice("PLC_加速度设定", 50) // 单位0.1mm/s²
Case 500 To 1000
!SetDevice("PLC_速度设定", 20)
!SetDevice("PLC_加速度设定", 40)
Case Else
!SetDevice("PLC_速度设定", 15)
!SetDevice("PLC_加速度设定", 30)
End Select
End Sub
6.2 传感器防抖处理
现场测试发现光电传感器偶尔会误触发,增加了软件滤波:
basic复制Function GetStableSensor(sensorName, checkTimes) As Boolean
stableCount = 0
For i = 1 To checkTimes
If !GetDevice(sensorName) Then
stableCount = stableCount + 1
Else
stableCount = 0
End If
If stableCount >= 3 Then Return True
Delay(10)
Next
Return False
End Function
这个函数会在50ms内多次检测传感器状态,只有连续3次一致才认为有效,大幅降低了误报率。
7. 异常处理与安全机制
7.1 多级急停系统
安全设计包含三个层次的保护:
- 软件急停:通过MCGS界面按钮触发,执行正常停止流程
- 硬件急停:直接切断电机电源,响应时间<100ms
- 机械限位:物理挡块防止超程
对应的MCGS处理逻辑:
basic复制Sub EmergencyStop(reason)
// 记录急停事件
!LogEvent("急停触发:" + reason)
// 发送停止命令
!SetDevice("PLC_急停", 1)
// 更新界面状态
For i = 1 To 16
If slotStatus[i] = 2 Then // 正在移动的车位
slotStatus[i] = 0
!UpdateSlotUI(i, "急停")
End If
Next
// 复位令牌桶
operationTokens = 2
End Sub
7.2 断电恢复处理
针对突然断电的情况,设计了状态恢复流程:
basic复制Sub PowerOnRecovery()
// 从PLC读取实际位置
For i = 1 To 16
actualPos = !GetDevice("PLC_车位" + i + "_实际位置")
If actualPos > 0 Then
!SetPosition("车位" + i, actualPos)
slotStatus[i] = 0 // 强制设为空闲
End If
Next
// 检查是否有未完成操作
pending = !GetDevice("PLC_未完成操作")
If pending > 0 Then
!Alarm("检测到未完成操作,请手动处理车位" + pending)
End If
End Sub
这个函数在系统启动时自动调用,确保界面状态与实际设备一致。