今天继续推进《将“运动控制功能块”移植到OOP中》的学习,反复学习了6,7,8三个示例。三个示例本身很简单,通过状态机实现轴多轴的时序配合,通过itfAxis和itfCommand接口实现功能调用。但是在状态机中,对于运动控制相关的Method并没有循环调用,只在状态转移时调用一次,这是如何实现真正的运动控制过程控制的还很迷糊。为了一探究竟,我突然萌生了一共奇特的想法:我计划在接下来的几天把经典的Codesys的PLCopen库重新在西门子的1500上实现,且采用AX的面向对象的设计思想。让自动化行业最具有代表性的2个平台在AX的作用下融合在一起,想想都刺激!
下面是今天学习的一些笔记,供日后参阅。
6、仓库示例
6.1 应用说明
该应用的目的是自动从货架的储藏柜中取出货物。货物存放在托盘中,可以用叉子系统取出。
图4:仓储实例概述
仓库的任务是用三个轴移动叉子来放置或取走托盘:
- X 轴沿地面移动;
- Y 轴移动到所需高度;
- Z 轴将叉子移动到货架上,取走托盘。
顺序是将X轴和Y轴移动到所需位置。两个轴到达该位置后,Z轴立即移动到托盘下方的货架上,在本例中是移动1000毫米。然后,Y轴将托盘再提升100毫米,将托盘从货架上抬起,这样托盘就可以从货架上移出,并移动到所需位置进行交付。
这个示例可以用不同的方法实现。一种直接的方法是使用PLCopen第1部分功能块。另外,也可以在支持PLCopen第4部分(协调运动)的控制器中定义XYZ组,这样可以简化和优化运动
6.2 第一个编程示例(使用第1部分中的FB)
只需使用第1部分中的功能块,就可以通过以下方式实现。
图5: 仓储示例的第一个程序
6.3 时序图
下图显示了从仓储系统提取托盘的顺序。
图6:仓储示例的时间图
6.4 OOP实现
本示例用于展示ST语言的OO编程。对于PLC程序员来说,编程非常简单,后台使用的是标准PLCopen运动控制库第1部分(v2)。通常,这些FB是以循环方式调用的,这与OOP理念不太相符。如果不需要反馈(完成、错误…),那么不循环调用FB,也是可以的。通过GetCommandStatus方法,可以调用底层FB(如 MC_MoveAbsolute),并通过GetCommandStatus(通过内部调用MC_MoveAbsolute和其他实例)返回运动的状态信息(完成、错误等)。在此基础上,可以开始下一条指令。当然,这些方法的实现取决于具体的供应商。
程序使用了状态机,不同的状态反映了整个轨迹的不同阶段。通过使用变量lastCommandX、lastCommandY和lastCommandZ,程序变得非常透明(见图7:上部为变量声明部分,下部为以CASE指令形式实现的状态机)。由此产生的时序图如图8所示。
PROGRAM WarehosingExample
VAR
AxisX:itfAxis;
AxisY:itfAxis;
AxisZ:itfAxis;
stateOOP:INT;
lastCommandX:itfCommand;
lastCommandY:itfCommand;
lastCommandZ:itfCommnad;
stepOn:BOOL:= FALSE;
targetPosX:REAL:= 400;
targetPosY:REAL:= 600;
END_VAR
CASE stateOOP OF
0:;
10: //init
AxisX.Power(Enable := TRUE,EnablePositive := TRUE,EnableNegative := TRUE);
AxisY.Power(Enable := TRUE,EnablePositive := TRUE,EnableNegative := TRUE);
AxisZ.Power(Enable := TRUE,EnablePositive := TRUE,EnableNegative := TRUE);
IF stepOn THEN
stateOOP := stateOOP + 10;
END_IF
20: // start movement in XY
lastCommnadX := AxisX.MoveAbsolute(Position := targetX,Velocity := 40,Acceleration := 0,Deceleration:= 0,Jerk := 0,Direction := 0,BufferMode := MC_BUFFER_MODE.mcAborting);
lastCommnadY := AxisY.MoveAbsolute(Position := targetY,Velocity := 40,Acceleration := 0,Deceleration:= 0,Jerk := 0,Direction := 0,BufferMode := MC_BUFFER_MODE.mcAborting);
stateOOP := stateOOP + 10;
30: //wait till movement is finished to 'forkIn'
IF lastCommnadX.Done AND lastCommnadY.Done THEN
lastCommnadZ := AxisZ.MoveRelative(Distance := 100,Velocity := 20,Acceleration := 0,Deceleration:= 0,Jerk := 0,BufferMode := MC_BUFFER_MODE.mcAborting);
stateOOP := stateOOP + 10;
END_IF
40:
//lift pallet if fork in
IF lastCommnadZ.Done THEN
lastCommnadY := AxisY.MoveRelative(Distance := 100,Velocity := 20,Acceleration := 0,Deceleration:= 0,Jerk := 0,BufferMode := MC_BUFFER_MODE.mcAborting);
stateOOP := stateOOP + 10;
END_IF
50:
//fork out with pallet if forked in is done
IF lastCommnadY.Done THEN
lastCommnadZ := AxisZ.MoveAbsolute(Position := 0,Velocity := 40,Acceleration := 0,Deceleration:= 0,Jerk := 0,Direction := 0,BufferMode := MC_BUFFER_MODE.mcAborting);
stateOOP := stateOOP + 10;
END_IF
60:
//move to delivery if fork out is done
IF lastCommnadZ.Done THEN
lastCommnadX := AxisX.MoveAbsolute(Position := 0,Velocity := 40,Acceleration := 0,Deceleration:= 0,Jerk := 0,Direction := 0,BufferMode := MC_BUFFER_MODE.mcAborting);
lastCommnadY := AxisY.MoveAbsolute(Position := 0,Velocity := 40,Acceleration := 0,Deceleration:= 0,Jerk := 0,Direction := 0,BufferMode := MC_BUFFER_MODE.mcAborting);
stateOOP := stateOOP + 10;
END_IF
70:
//wait till finished
IF lastCommnadX.Done AND lastCommnadY.Done THEN
stateOOP := stateOOP + 10;
END_IF
80:
//ready
;
END_CASE
图7:OOP程序示例
图8:用OOP实现的仓库示例时序图
7、标签示例
7.1 应用描述
任务是将标签贴在产品的特定位置上。该应用有两个驱动器,一个用于通过传送带送入产品,另一个用于送入标签并将标签贴在产品上。贴标过程由位置检测传感器触发(参见图9:顶部)。从检测到产品到开始贴标,会有一个延迟时间,取决于传送带的速度、传感器的位置和标签在产品上的位置。
图9:贴标机
7.2 编程示例
本示例展示了用编程语言FBD解决这一任务的方法,如图10;
图10:贴标机程序示例
两个轴均以相同的速度设定值移动。TON的延迟时间根据传感器距离和速度计算得出。一个贴标步骤结束后,LabelDrive再次停止并等待下一次触发,同时传送带继续移动。
OOP实现
使用编程语言ST和引入的运动控制OOP元素,将标签示例转换为OOP。与仓库示例类似,后台使用标准PLCopen运动控制库第1部分(v2),程序使用状态机反映过程的不同状态,程序如下:
PROGRAM LabelingExample
VAR
//Hardware defineition
LabelDrive:Axis;
Coveyor:Axis;
LabelLength:REAL:=100;
SensorDistance:REAL:=10;
Velocity:REAL:= 5;
ProductDetection:BOOL:= FALSE;
DelayTimer:TON;
//States
CoveyorState:INT := 0;
LabelDriveState:INT := 0;
//Control
Start:BOOL := FALSE;
//Commnads
ConveyorMove : itfCommand;
LabelDriveMove : itfCommand;
END_VAR
//Run the timer. As it is a classic FB it should be called every cycle
DelayTimer(IN:= ProductDetection,PT:= INT_TO_TIME(REAL_TO_INT(SensorDistance * 1000 / Velocity)));
CASE CoveyorState OF
0: //Disabled
IF Start THEN
CoveyorState := CoveyorState + 1;
END_IF
1: // Power On
Coveyor.Power(Enable := TRUE ,EnablePositive := TRUE, EnableNegative := TRUE);
IF Coveyor.Status = AXIS_STATUS.Standstill THEN
CoveyorState := CoveyorState + 1;
END_IF
2: //Wait for LabelDrive
IF LabelDrive.Status = AXIS_STATUS.Standstill THEN
ConveyorMove := Coveyor.MoveVelocity(Velocity := Velocity,Acceleration:= 0,Deceleration:= 0,Jerk := 0, Direction := MC_Direction.mcPositiveDirection,BufferMode := MC_BUFFER_MODE.mcAborting );
CoveyorState := CoveyorState + 1;
END_IF
3: // Moving
IF NOT Start THEN
ConveyorMove := Coveyor.Halt(Deceleration := 0,Jerk := 0, BufferMode := MC_BUFFER_MODE.mcAborting);
CoveyorState := CoveyorState + 1;
END_IF
4: // Stoping
IF ConveyorMove.Done THEN
CoveyorState := CoveyorState + 1;
END_IF
5: // Power off
Coveyor.Power(Enable := FALSE ,EnablePositive := FALSE, EnableNegative := FALSE);
IF Coveyor.Status = AXIS_STATUS.Disabled THEN
CoveyorState := 0;
END_IF
END_CASE
CASE LabelDriveState OF
0: //Disabled
IF Start THEN
LabelDriveState := LabelDriveState + 1;
END_IF
1: // Power On
LabelDrive.Power(Enable := TRUE ,EnablePositive := TRUE, EnableNegative := TRUE);
IF LabelDrive.Status = AXIS_STATUS.Standstill THEN
LabelDriveState := LabelDriveState + 1;
END_IF
2: //Wait for Coveyor
IF Coveyor.Status = AXIS_STATUS.Standstill THEN
LabelDriveState := LabelDriveState + 1;
END_IF
3: // Waitf for product
IF NOT Start THEN
LabelDriveState := 5;
END_IF
IF DelayTimer.Q THEN
LabelDriveMove := LabelDrive.MoveRelative(Distance := LableLength,Velocity := Velocity,Acceleration:= 0,Deceleration:= 0,Jerk := 0,BufferMode := MC_BUFFER_MODE.mcAborting );
LabelDriveState := LabelDriveState + 1;
4: // Wait for lable to be transfered
IF LabelDriveMove.Done AND NOT DelayTimer.Q THEN
LabelDriveState := 3;
END_IF
5: // Power off
LabelDrive.Power(Enable := FALSE ,EnablePositive := FALSE, EnableNegative := FALSE);
IF LabelDriver.Status = AXIS_STATUS.Disabled THEN
LabelDriveState := 0;
END_IF
END_CASE
图13:标签示例时序图
8、带凸轮和齿轮的示例
8.1 应用说明
这是一个简单的应用程序演示,其中一个主轴以固定速度运动,第二个轴作为CAM从动轴,第三个齿轮从动轴通过OOP实现轴同步。
8.2 经典编程示例
共有三个驱动器,即MasterDrive、CamDrive和GearDrive。第一步是接通电源。通过CamTableSelect和CamIn将凸轮轴与主驱动器连接。齿轮传动装置通过GearIn连接。一旦两个从动轴和主轴都准备就绪(与主轴同步和齿轮同步),主轴开始以速度运动,两个从动轴也随之运动。FBD中该示例的经典实现如图 14 所示。
图14:三驱动同步示例的经典FBD实现
8.3 OOP实现
使用OOP的同步示例使用本文介绍的接口、ENUMS和STRUCT。主程序的声明部分如下图所示:它包含硬件定义、驱动器状态以及使两个从动轴与主轴同步的命令。为实现同步,定义的接口itfSynchronizedAxisCommand用于控制凸轮驱动器(CamIn)和齿轮驱动器(GearIn)。
该示例说明,使用新定义接口提供的命令来实现同步驱动相对简单。
PROGRAM CamGearExample
VAR
//Hardware definitions
MasterDrive:Axis;
CamDrive:Axis;
GearDrive:Axis;
Table:CamTable;
TableData:MC_CAM_REF;
Velocity:REAL:= 5;
//State
MasterState :INT := 0;
CamState:INT := 0;
GearState:INT := 0;
//Control
Start :BOOL := FALSE;
//Commands
MasterMove :itfCommand;
TableSet:itfCommand;
CmaMove:itfSynchronizeAxisCommand;
GearMove:itfSynchronizeAxisCommand;
END_VAR
CASE MasterState OF
0: //Disable
IF Start THEN
MasterState := MasterState + 1;
END_IF
1: //Power On
MasterDrive.Power(Enable := TRUE,EnablePositive := TRUE,EnableNegative := TRUE);
IF MasterDrive.Status = AXIS_STATUS.Standstill THNE
MasterState := MasterState + 1;
END_IF
2: //Wait for other drives
IF CamMove.InSync AND GearMove.InSync THEN
MasterMove := MasterDrive.MoveVelocity(Velocity := Velocity,Acceleration := 0,Deceleration :=0,Jerk := 0,Direction := 0,BufferMode := MC_BUFFER_MODE.mcAborting);
MasterState := MasterState + 1;
END_IF
3: //Moving
IF NOT Start THEN
MasterMove := MasterDrive.Halt(Deceleration := 0,Jerk := 0,BufferMode := MC_BUFFER_MODE.mcAborting);
MasterState := MasterState + 1;
END_IF
4: //Stoping
IF NOT MasterMove.Done THEN
MasterState := MasterState + 1;
END_IF
5: //Power off
MasterDrive.Power(Enable := FALSE,EnablePositive := FALSE,EnableNegative := FALSE);
IF MasterDrive.Status = AXIS_STATUS.Disable THNE
MasterState := 0;
END_IF
END_CASE
CASE CamState OF
0: //Disable
IF Start THEN
CamState := CamState + 1;
END_IF
1: //Power On
CamDrive.Power(Enable := TRUE,EnablePositive := TRUE,EnableNegative := TRUE);
IF CamDrive.Status = AXIS_STATUS.Standstill AND MasterDrive.Status = AXIS_STATUS.Standstill THNE
TableSet := Table.Select(CamTable := TableData,Periodic:= TRUE,MasterAbsolute := TRUE,SlaveAbsolute := TRUE);
CamState := CamState + 1;
END_IF
2: //Set CAM Table
IF TableSet.Done THEN
CamMove := CamDrive.CamIn(Master:= MasterDrive,MasterOffset := 0,SlaveOffset := 0,MasterScaling := 1 ,MasterStartDistance := 0,MasterSyncPosition := 0,StartMode := MC_StartMode.mcRelative,MasterValueSource := MC_Source.mcSetValue,CameTable:= Table,BUfferMode := MC_BUFFER_MODE.mcAborting);
CamState := CamState + 1;
END_IF
3: //Synchronized
IF NOT Start THEN
CamDrive.Release();
CamState := CamState + 1;
END_IF
4: //Power Off
CamDrive.Power(Enable := FALSE,EnablePositive := FALSE,EnableNegative := FALSE);
IF CamDrive.Status = AXIS_STATUS.Disable THNE
CamState := 0;
END_IF
END_CASE
CASE GearState OF
0: //Disable
IF Start THEN
GearState := GearState + 1;
END_IF
1: //Power On
GearDrive.Power(Enable := TRUE,EnablePositive := TRUE,EnableNegative := TRUE);
IF GearDrive.Status = AXIS_STATUS.Standstill AND MasterDrive.Status = AXIS_STATUS.Standstill THNE
GearMove := GearDrive.GearIn(Master := MasterDrive,Ratio:= 2,MasterValueSource := MC_Source.mcSetValue,Acceleration := 0,Deceleration := 0,Jerk:=0,BufferMode := MC_BUFFER_MODE.mcAborting);
GearState := GearState + 1;
END_IF
2: //Synchronized
IF NOT Start THEN
GearDrive.Release();
GearState := GearState + 1;
END_IF
3: //Power Off
GearDrive.Power(Enable := FALSE,EnablePositive := FALSE,EnableNegative := FALSE);
IF GearDrive.Status = AXIS_STATUS.Disable THNE
GearState := 0;
END_IF
END_CASE