TwinCAT3 - 实现GEM协议
0 引子
某天和倍福的技术聊天,聊起有一些半导体设备厂产品推不进去,因为原生不支持GEM功能。
而欧姆龙等PLC开始支持GEM,德国倍福官方又没有支持GEM的计划,在产品推广时遇到一些阻力。
正好我对HSMS/SECS/GEM协议簇略知一二,就说要不我来试试,就用TwinCAT3原生的功能实现GEM协议,因此就有了这个函数库项目EEEE_SECSGEM。
经过1个多月的开发,目前已经实现了GEM200中的模型和常用消息。函数库可以免费短期试用,但是长期使用需要License。后续有需求的话函数库可能会继续更新。
下文都是EEEE_SECSGEM的使用说明书了,文末有下载链接。
1 前言
函数库使用纯ST语言,实现了SEMI E37/E5/E30(即HSMS/SECSII/GEM)。GEM只实现了协议由强制性要求的部分。
使用之前建议对SEMI协议簇有一定了解。库设计上是分层的,普通用户建议从GEM开始使用,资深用户可以选择从GEM或SECS开始使用。
下面以mock一台简单的真空炉为例,说明函数库的使用全流程。
2 准备
安装EEEE_SECSGEM和倍福官方的TCPIP服务。
准备一款SECS模拟器,我准备的是ITRI开发的SECSEmulator。
我用在开发PC上跑的TwinCAT Runtime做Equipment。SECSEmulator做Host。
3 基础编程
下面从从GEM开始使用。新建一个项目,就叫EEEE_SECSGEM_Test吧。引用EEEE_SECSGEM。如下图
3.1 完整代码
先把完整代码贴出来,然后一段一段地说明。
PROGRAM MAIN
VAR
Equip: FB_GEMEquipment; // 核心
//INPUT-----
bEnable: BOOL:= TRUE; // 通信状态
bOnline: BOOL:= TRUE; // 控制状态
LocalOrRemote: INT:= 1; // 控制子状态 0:Local 1:Remote
Mode: E_HSMSConnectionMode:= E_HSMSConnectionMode.Active; // 0:Passive(TCPServer) 1:active(TCPClient)
Host: STRING(15):= '127.0.0.1'; //active时为RemoteHost,passive时为LocalHost
Port: UINT:= 200; // active时为RemotePort,passive时为LocalPort
SessionId: UINT:= 0; // 数据消息中也称DeviceId
HSMSTimeoutConfig: ST_HSMSTimeoutConfig; // 超时参数
GEMEquipmentConfig: ST_GEMEquipmentConfig; // 设备信息
ADSPort: UINT:= 851; // TwinCAT运行时的Port
ProcessingCommand: E_GEMProcessingCommand; // 工艺状态命令
bChangeTime: BOOL; // 修改系统时间
//OUTPUT-----
GEMCommunicationState: E_GEMCommunicationState; //通信状态模型
GEMControlState: E_GEMControlState; //控制状态模型
GEMProcessingState: E_GEMProcessingState; //工艺状态模型
//-----
GEMHandler: FB_GEMHandler; // 用户自定义的事件处理器
//-----
CollectionEvent: ST_CollectionEvent; // 临时变量
Report: ST_Report; // 临时变量
StatusVariable: ST_Variable; // 临时变量
EquipConstant: ST_Variable; // 临时变量
Alarm: ST_Alarm; // 临时变量
//-----
bInitData: BOOL; // 初始化业务测试数据
WaferID: STRING; // 业务数据,晶圆ID
TubeID: INT; // 业务数据,炉管ID
Temp: ARRAY[0..2] OF REAL; // 业务数据,炉管温度
Pressure: ARRAY[0..2] OF LREAL; // 业务数据,炉管压力
Flow: ARRAY[0..1] OF LINT; // 业务数据,炉管流量
Ready: BOOL; // 业务数据,炉管就绪
//-----
WaferMaterial: STRING; // 业务数据,晶圆材料
WaferSize: REAL; // 业务数据,晶圆尺寸
//-----
bSendTimeRequest: BOOL; // 发送时间请求,即S2F17
bSendEvent: BOOL; // 发送事件,即S6F11
bSendAlarm: BOOL; // 发送报警,即S5F1
bSendTerminal: BOOL; // 发送终端消息,即S10F1
//-----
bInitList: BOOL; // 初始化CEID,RPTID,VID,ALID列表
END_VAR
Equip(bEnable:= bEnable, bOnline:= bOnline, LocalOrRemote:= LocalOrRemote, Mode:= Mode, Host:= Host, Port:= Port,
SessionId:= SessionId, HSMSTimeoutConfig:= HSMSTimeoutConfig, GEMEquipmentConfig:= GEMEquipmentConfig,
RecipeFolder:= 'C:\EEEE_SECS\', RecipeExtension:= 'recipe', ADSPort:= ADSPort,
GEMCommunicationState=> GEMCommunicationState, GEMControlState=> GEMControlState,
GEMProcessingState=> GEMProcessingState);
IF ProcessingCommand > E_GEMProcessingCommand.None THEN
Equip.ChangeProcessState(ProcessingCommand);
ProcessingCommand:= E_GEMProcessingCommand.None;
END_IF
IF bChangeTime THEN
Equip.ChangeTime('2026063012304500'); // 2026年06月30日12时30分45秒00厘秒
bChangeTime:= FALSE;
END_IF
Equip.SubscribeHandler(GEMHandler);
IF bSendTimeRequest THEN
Equip.SendTimeRequest(); // 发送S2F17
bSendTimeRequest:= FALSE;
END_IF
IF bSendEvent THEN
Equip.SendEvent(100); // 发送CEID=100的事件
bSendEvent:= FALSE;
END_IF
IF bSendAlarm THEN
Equip.SendAlarm(100, TRUE); // 发送ALID=100的报警
bSendAlarm:= FALSE;
END_IF
IF bSendTerminal THEN
Equip.SendTerminal(1, 'Terminal Message ABCDEFG');
bSendTerminal:= FALSE;
END_IF
IF bInitList THEN // 初始化CEID,RPTID,VID,ALID列表
Equip.InitList();
bInitList:= FALSE;
END_IF
IF bInitData THEN // 初始化测试数据
CollectionEvent.Name:= 'WaferIn'; // 进晶片事件
CollectionEvent.CEID:= 100;
CollectionEvent.RPTIDList[0]:= 100; // 晶片报告
CollectionEvent.CEED:= TRUE;
Equip.CollectionEventList.AddOrUpdateCollectionEvent(CollectionEvent);
CollectionEvent.Name:= 'WaferOut'; // 出晶片事件
CollectionEvent.CEID:= 101;
CollectionEvent.RPTIDList[0]:= 100; // 晶片报告
CollectionEvent.CEED:= TRUE;
Equip.CollectionEventList.AddOrUpdateCollectionEvent(CollectionEvent);
Report.Name:= 'WaferReport'; // 晶片报告
Report.RPTID:= 100;
Report.VIDList[0]:= 100; // 晶片号
Report.VIDList[1]:= 101; // 管号
Report.VIDList[2]:= 102; // 温度
Report.VIDList[3]:= 105; // 就绪
Report.VIDList[4]:= 1000; // 晶片材料
Report.VIDList[5]:= 1001; // 晶片尺寸
Equip.ReportList.AddOrUpdateReport(Report);
StatusVariable.Name:= 'WaferID';
StatusVariable.VariableType:= E_VariableType.SV;
StatusVariable.VID:= 100;
StatusVariable.Format:= E_SECSItemFormat.ASCII;
StatusVariable.VariablePLCName:= 'MAIN.WaferID';
StatusVariable.ValueCount:= 0;
Equip.VariableList.AddVariable(StatusVariable);
StatusVariable.Name:= 'TubeID';
StatusVariable.VariableType:= E_VariableType.SV;
StatusVariable.VID:= 101;
StatusVariable.Format:= E_SECSItemFormat.I2;
StatusVariable.VariablePLCName:= 'MAIN.TubeID';
StatusVariable.ValueCount:= 1;
Equip.VariableList.AddVariable(StatusVariable);
StatusVariable.Name:= 'Temp';
StatusVariable.VariableType:= E_VariableType.SV;
StatusVariable.VID:= 102;
StatusVariable.Unit:= 'degC'; //摄氏度
StatusVariable.Format:= E_SECSItemFormat.F4;
StatusVariable.VariablePLCName:='MAIN.Temp';
StatusVariable.ValueCount:= 3;
Equip.VariableList.AddVariable(StatusVariable);
StatusVariable.Name:= 'Pressure';
StatusVariable.VariableType:= E_VariableType.SV;
StatusVariable.VID:= 103;
StatusVariable.Unit:= 'Pa';
StatusVariable.Format:= E_SECSItemFormat.F8;
StatusVariable.VariablePLCName:= 'MAIN.Pressure';
StatusVariable.ValueCount:= 3;
Equip.VariableList.AddVariable(StatusVariable);
StatusVariable.Name:= 'Flow';
StatusVariable.VariableType:= E_VariableType.SV;
StatusVariable.VID:= 104;
StatusVariable.Unit:= 'sccm';
StatusVariable.Format:= E_SECSItemFormat.I8;
StatusVariable.VariablePLCName:= 'MAIN.Flow';
StatusVariable.ValueCount:= 2;
Equip.VariableList.AddVariable(StatusVariable);
StatusVariable.Name:= 'Ready';
StatusVariable.VariableType:= E_VariableType.SV;
StatusVariable.VID:= 105;
StatusVariable.Unit:= '';
StatusVariable.Format:= E_SECSItemFormat.Boolean;
StatusVariable.VariablePLCName:= 'MAIN.Ready';
StatusVariable.ValueCount:= 1;
Equip.VariableList.AddVariable(StatusVariable);
EquipConstant.Name:= 'WaferMaterial';
EquipConstant.VariableType:= E_VariableType.ECV;
EquipConstant.VID:= 1000;
EquipConstant.Format:= E_SECSItemFormat.ASCII;
EquipConstant.VariablePLCName:= 'MAIN.WaferMaterial';
EquipConstant.ValueCount:= 0;
Equip.VariableList.AddVariable(EquipConstant);
EquipConstant.Name:= 'WaferSize';
EquipConstant.VariableType:= E_VariableType.ECV;
EquipConstant.VID:= 1001;
EquipConstant.Format:= E_SECSItemFormat.F4;
EquipConstant.VariablePLCName:= 'MAIN.WaferSize';
EquipConstant.ValueCount:= 1;
EquipConstant.Unit:= 'inch';
Equip.VariableList.AddVariable(EquipConstant);
Alarm.Name:= 'OverTemp';
Alarm.ALID:= 100;
Alarm.AlarmCategory:= E_AlarmCategory.EquipmentSafety;
Alarm.AlarmState:= FALSE;
Alarm.ALTX:= 'chamber over temperature!';
Alarm.AlarmEnable:= TRUE;
Equip.AlarmList.AddAlarm(Alarm);
Alarm.Name:= 'OverPress';
Alarm.ALID:= 101;
Alarm.AlarmCategory:= E_AlarmCategory.EquipmentSafety;
Alarm.AlarmState:= FALSE;
Alarm.ALTX:= 'chamber over pressure!';
Alarm.AlarmEnable:= TRUE;
Equip.AlarmList.AddAlarm(Alarm);
WaferID:= 'SuperWafer';
TubeID:= 5;
Temp[0]:= 123.456;
Temp[1]:= 234.567;
Temp[2]:= 345.678;
Pressure[0]:= 0.00123;
Pressure[1]:= 0.00456;
Pressure[2]:= 0.00789;
Flow[0]:= 100;
Flow[1]:= 200;
Ready:= TRUE;
WaferMaterial:= 'SiC';
WaferSize:= 8;
bInitData:= FALSE;
END_IF
3.2 分段说明
3.2.1 片段1
Equip(bEnable:= bEnable, bOnline:= bOnline, LocalOrRemote:= LocalOrRemote, Mode:= Mode, Host:= Host, Port:= Port,
SessionId:= SessionId, HSMSTimeoutConfig:= HSMSTimeoutConfig, GEMEquipmentConfig:= GEMEquipmentConfig,
RecipeFolder:= 'C:\EEEE_SECS\', RecipeExtension:= 'recipe', ADSPort:= ADSPort,
GEMCommunicationState=> GEMCommunicationState, GEMControlState=> GEMControlState,
GEMProcessingState=> GEMProcessingState);
IF ProcessingCommand > E_GEMProcessingCommand.None THEN
Equip.ChangeProcessState(ProcessingCommand);
ProcessingCommand:= E_GEMProcessingCommand.None;
END_IF
IF bChangeTime THEN
Equip.ChangeTime('2026063012304500'); // 2026年06月30日12时30分45秒00厘秒
bChangeTime:= FALSE;
END_IF
说明一下FB_Equipment用户可以配置的部分,消息都在内部自动处理,用户只需要配置。FB_Equipment的所有能力在后文进行说明。
-
三个通信状态模型:通信状态模型,控制状态模型,工艺状态模型。
- 通信状态:由bEnable进行控制,由GEMCommunicationState进行状态反馈。
- 控制状态:由bOnline和LocalOrRemote进行控制,由GEMControlState进行状态反馈。
- 工艺状态:由FB_Equipment.ChangeProcessState方法进行控制,由GEMProcessingState进行反馈。注意状态切换要符合GEM工艺状态表,否则切换不动。
-
连接和设备信息
- 设备模式:由Mode指定,avtive就是做TCPClient,passive就是做TCPServer。
- 地址和端口:由Host和Port指定。
- SessionId:也叫DeviceId,用于标识设备
- HSMS超时时间:HSMSTimeoutConfig,目前只实现了T3和T5。
- 设备信息:GEMEquipmentConfig,设备型号,设备名称,设备软件版本等
-
工艺管理
- 工艺文件夹:由RecipeFolder指定。可填绝对路径,也可以填相对路径,相对路径的根是Boot文件夹
- 工艺文件扩展名:比如123.recipe,如果留空则默认是recipe
-
TwinCAT运行时
- 运行时的ADSPort:一般填851,需要自行在项目中确认。
-
系统时间
- 修改系统时间:用户可以主动使用FB_Equipment.ChangeTime修改系统时间。
3.2.2 片段2
Equip.SubscribeHandler(GEMHandler);
GEMHandler是用户自定义的GEM事件处理器,然后FB_Equipment订阅此GEMHandler。如果你熟悉C#,这就类似于Equip.GEMHander += GEMHandler。
GEMHandler可以处理收到消息,发送消息,内部消息,状态改变,收到时间(S2F18),收到远程命令(S2F41),收到终端消息(S10F3)等事件。
我写了一个简单的GEMHandler,完整代码如下。用户可以根据自己的需要编写。
FUNCTION_BLOCK FB_GEMHandler IMPLEMENTS I_GEMHandler
VAR_INPUT
END_VAR
VAR_OUTPUT
END_VAR
VAR
fbSECSMessage: FB_SECSMessage; // 处理SECS消息的功能块
END_VAR
FB_GEMHandler实现了I_GEMHandler接口。下面都是I_GEMHandler接口方法的具体实现。
METHOD GEMDataMessageReceived : BOOL
VAR_INPUT
RemoteAddr: STRING(15);
RemotePort: UDINT;
stMessageReceived: REFERENCE TO ST_SECSMessage;
END_VAR
VAR
Str: T_MaxString;
END_VAR
Str:= CONCAT('Receive SECSMessage from remote', RemoteAddr);
Str:= CONCAT(Str, ':');
Str:= CONCAT(Str, UDINT_TO_STRING(RemotePort));
Str:= CONCAT(Str, ' ');
Str:= CONCAT(Str, fbSECSMessage.ToString(stMessageReceived)); // 转换成SML
ADSLOGSTR(msgCtrlMask:= ADSLOG_MSGTYPE_HINT, msgFmtStr:= Str, strArg:= '');
把收到的消息打印出来,不想看就不需要实现此方法。其中FB_SECSMessage.ToString方法是把SECS消息转换成SML格式。
METHOD GEMDataMessageReceived : BOOL
VAR_INPUT
RemoteAddr: STRING(15);
RemotePort: UDINT;
stMessageReceived: REFERENCE TO ST_SECSMessage;
END_VAR
VAR
Str: T_MaxString;
END_VAR
Str:= CONCAT('Receive SECSMessage from remote', RemoteAddr);
Str:= CONCAT(Str, ':');
Str:= CONCAT(Str, UDINT_TO_STRING(RemotePort));
Str:= CONCAT(Str, ' ');
Str:= CONCAT(Str, fbSECSMessage.ToString(stMessageReceived)); // 转换成SML
ADSLOGSTR(msgCtrlMask:= ADSLOG_MSGTYPE_HINT, msgFmtStr:= Str, strArg:= '');
把发送的消息打印出来,不想看就不需要实现此方法。
METHOD GEMInternalMessage : BOOL
VAR_INPUT
sMsg: T_MaxString;
END_VAR
ADSLOGSTR(msgCtrlMask:= ADSLOG_MSGTYPE_HINT, msgFmtStr:= sMsg, strArg:= '');
把功能块内部消息打印出来,建议实时监控,包含很多重要信息。
METHOD GEMStateChanged : BOOL
VAR_INPUT
RemoteAddr: STRING(15);
RemotePort: UDINT;
eState: E_HSMSConnectionState;
END_VAR
VAR
Str: STRING;
END_VAR
Str:= CONCAT('Remote ', RemoteAddr);
Str:= CONCAT(Str, ' HSMS Connection State change to ');
Str:= CONCAT(Str, F_HSMSConnectionStateToSTRING(eState));
ADSLOGSTR(msgCtrlMask:= ADSLOG_MSGTYPE_HINT, msgFmtStr:= Str, strArg:= '');
HSMS连接状态改变,建议实时监控。
METHOD GEMDataAndTimeReceived : BOOL // 收到S2F18
VAR_INPUT
Str : STRING; // 16byte格式,YYYYMMDDhhmmsscc
END_VAR
Str:= CONCAT('Receive Data and Time from remote', Str);
ADSLOGSTR(msgCtrlMask:= ADSLOG_MSGTYPE_HINT, msgFmtStr:= Str, strArg:= '');
把收到的时间打印出来。当Equipment发送S2F17时,会收到Host回复的S2F18,常规操作就是调用FB_Equipment.ChangeTime修改系统时间。如果你确定不会发S2F17就不需要实现此方法。
METHOD GEMRemoteCommandReceived : E_RemoteCommandAcknowledge // 收到S2F41
VAR_INPUT
RCMD: STRING; // 远程命令
CPNAME: ARRAY [0..9] OF STRING; // 参数名称
CPVAL: ARRAY [0..9] OF STRING; // 参数值
CPCount: INT; // 参数个数
END_VAR
VAR
A: INT;
Str: T_MaxString;
END_VAR
IF FIND(RCMD, 'START') >= 1 THEN // Start指令:Ready->Executing
// TODO:用户执行命令
GEMRemoteCommandReceived:= E_RemoteCommandAcknowledge.AcknowledgePerformed;
ELSIF FIND(RCMD, 'STOP') >= 1 THEN // Stop指令:Executing or Pause->Idle
// TODO:用户执行命令
GEMRemoteCommandReceived:= E_RemoteCommandAcknowledge.AcknowledgePerformed;
ELSIF FIND(RCMD, 'PAUSE') >= 1 THEN // Pause指令:Executing or Ready->Pause
// TODO:用户执行命令
GEMRemoteCommandReceived:= E_RemoteCommandAcknowledge.AcknowledgePerformed;
ELSIF FIND(RCMD, 'RESUME') >= 1 THEN // Resume指令:Pause->Executing
// TODO:用户执行命令
GEMRemoteCommandReceived:= E_RemoteCommandAcknowledge.AcknowledgePerformed;
ELSIF FIND(RCMD, 'ABORT') >= 1 THEN // Abort指令:Any except Idle->Idle
// TODO:用户执行命令
GEMRemoteCommandReceived:= E_RemoteCommandAcknowledge.AcknowledgePerformed;
ELSIF FIND(RCMD, 'PP-SELECT') >= 1 THEN // 工艺选择指令
// TODO:用户执行命令
GEMRemoteCommandReceived:= E_RemoteCommandAcknowledge.AcknowledgePerformed;
ELSE
GEMRemoteCommandReceived:= E_RemoteCommandAcknowledge.CommandNotExist;
END_IF
ADSLOGSTR(msgCtrlMask:= ADSLOG_MSGTYPE_HINT, msgFmtStr:= CONCAT('Remote Command: ', RCMD), strArg:= '');
FOR A:= 0 TO MIN(CPCount - 1, 9) DO
Str:= CONCAT(CPNAME[A], ':');
Str:= CONCAT(Str, CPVAL[A]);
ADSLOGSTR(msgCtrlMask:= ADSLOG_MSGTYPE_HINT, msgFmtStr:= Str, strArg:= '');
END_FOR
处理收到的远程命令,GEM中提到的远程命令有START,STOP,PAUSE,RESUME,ABORT,PP-SELECT。用户收到这些命令后,应该在自己的业务程序中执行这些命令,然后返回合适的E_RemoteCommandAcknowledge。FB_Equipment会根据返回的E_RemoteCommandAcknowledge构造S2F42回复给Host。
METHOD GEMTerminalDisplay : BOOL // 收到S10F3
VAR_INPUT
TID : BYTE; // Terminal number
TEXT : Tc2_System.T_MaxString;
END_VAR
VAR
Str: T_MaxString;
END_VAR
Str:= CONCAT('Receive Terminal Display TID:', BYTE_TO_STRING(TID));
Str:= CONCAT(Str, ' TEXT:');
Str:= CONCAT(Str, TEXT);
ADSLOGSTR(msgCtrlMask:= ADSLOG_MSGTYPE_HINT, msgFmtStr:= Str, strArg:= '');
把终端消息打印出来,不想看就不需要实现此方法。
3.2.3 片段3
IF bSendTimeRequest THEN
Equip.SendTimeRequest(); // 发送S2F17
bSendTimeRequest:= FALSE;
END_IF
IF bSendEvent THEN
Equip.SendEvent(100); // 发送CEID=100的事件
bSendEvent:= FALSE;
END_IF
IF bSendAlarm THEN
Equip.SendAlarm(100, TRUE); // 发送ALID=100的报警
bSendAlarm:= FALSE;
END_IF
IF bSendTerminal THEN
Equip.SendTerminal(1, 'Terminal Message ABCDEFG'); // 给TID=1的终端发送文本消息
bSendTerminal:= FALSE;
END_IF
- FB_Equipment.SendTimeRequest:向Host发送时间请求,一般用于跟Host进行时间同步。
- FB_Equipment.SendEvent:向Host发送CollectionEvent,用于通知业务事件。
- FB_Equipment.SendAlarm:向Host发送报警,包括报警发生和报警清除,用于通知业务报警。
- FB_Equipment.SendTerminal:向Host发送终端消息,用于通知用户文本消息。
3.2.4 片段4
IF bInitList THEN // 初始化CEID,RPTID,VID,ALID列表
Equip.InitList();
bInitList:= FALSE;
END_IF
IF bInitData THEN // 初始化测试数据
CollectionEvent.Name:= 'WaferIn'; // 进晶片事件
CollectionEvent.CEID:= 100;
CollectionEvent.RPTIDList[0]:= 100; // 晶片报告
CollectionEvent.CEED:= TRUE; // 事件启用
Equip.CollectionEventList.AddOrUpdateCollectionEvent(CollectionEvent);
...省略...
StatusVariable.Name:= 'WaferID'; // 变量名称
StatusVariable.VariableType:= E_VariableType.SV; // 变量类型
StatusVariable.VID:= 100;
StatusVariable.Format:= E_SECSItemFormat.ASCII; // 变量Format
StatusVariable.VariablePLCName:= 'MAIN.WaferID'; // PLC变量名称
StatusVariable.ValueCount:= 0; // 变量数量
Equip.VariableList.AddVariable(StatusVariable);
StatusVariable.Name:= 'Temp'; // 变量名称
StatusVariable.VariableType:= E_VariableType.SV; // 变量类型
StatusVariable.VID:= 102;
StatusVariable.Unit:= 'degC'; // 摄氏度
StatusVariable.Format:= E_SECSItemFormat.F4; // 变量Format
StatusVariable.VariablePLCName:='MAIN.Temp'; // PLC变量名称
StatusVariable.ValueCount:= 3; // 变量数量
Equip.VariableList.AddVariable(StatusVariable);
...省略...
WaferMaterial:= 'SiC'; // 业务数据,晶圆材料
WaferSize:= 8; // 业务数据,晶圆尺寸
bInitData:= FALSE;
END_IF
这一大段是对CEID,RPTID,VID,ALID列表的操作,包括了列表的初始化和用户业务逻辑相关的列表操作。
-
FB_Equipment.InitList:初始化所有CEID,RPTID,VID,ALID列表,具体操作是先清空CEID,RPTID,VID,ALID列表,然后添加内置的CEID,RPTID,VID。内置列表只包含了GEM强制要求实现的事件。
内置的CEID:
- CEID:1=Equipment OFF-LINE,设备Offline,链接到RPTID1
- CEID:2=Control State LOCAL,设备Online-LOCAL,链接到RPTID1
- CEID:3=Control State REMOTE,设备Online-REMOTE,链接到RPTID1
- CEID:10=Processing Started,工艺开始,链接到RPTID10
- CEID:11=Processing Completed,工艺完成,链接到RPTID10
- CEID:12=Processing Stopped,工艺停止,链接到RPTID10
- CEID:13=Processing State Change,控制状态改变,链接到RPTID11
内置的RPTID:
- RPTID:1=控制状态,链接到SVID1 + SVID2
- RPTID:10=工艺状态,链接到SVID2 + SVID11
- RPTID:11=工艺状态,链接到SVID2 + SVID10 + SVID11
内置的VID:
- SVID:1=ControlState,当前控制状态
- SVID:2=CLOCK,系统时间,YYYYMMDDhhmmsscc
- SVID:10=ProcessState,当前工艺状态
- SVID:11=PreviousProcessState,上次工艺状态
第一次调用FB_Equipment时会自动调用InitList。如果运行过程中,用户想重新规划这些列表,就可以自行调用InitList方法。
可以看到上述ID基本上都小于100。用户添加自定义的ID时请尽量大于100,避免与内置ID冲突。 -
Equip.CollectionEventList:用户可以自行操作CEID列表,带有增删一系列改查功能,操作类似于C#的List<ST_CollectionEvent>。
-
Equip.ReportList:用户可以自行操作RPTID列表,带有增删一系列改查功能,操作类似于C#的List<ST_Report>。
-
Equip.VariableList:用户可以自行操作VID列表,带有增删一系列改查功能,操作类似于C#的List<ST_Variable>。
注意必须正确填写ST_Variable.VariablePLCName,填写ADS Symbol Info中的全称,函数库会自动找到对应的PLC变量并实时更新Variable的值。FB_Equipment.VariablePLCNameValid为TRUE则表示PLC变量名都写对了。如果填写PLC变量名错误,FB_Equipment.VariablePLCNameValid会置FALSE,导致FB_Equipment功能块停止执行。
此处我挑选了2种变量进行说明
StatusVariable.Name:= 'WaferID'; // 变量名称 StatusVariable.VariableType:= E_VariableType.SV; // 变量类型 StatusVariable.VID:= 100; StatusVariable.Format:= E_SECSItemFormat.ASCII; // 变量格式 StatusVariable.VariablePLCName:= 'MAIN.WaferID'; // PLC变量名称 StatusVariable.ValueCount:= 0; // 变量数量-
Name,变量名为
WaferID,此变量名用于S1F12中的SVNAME。 -
VariableType,变量类型可选SV,ECV,DVVAL
-
VID,就是Variable ID
-
Format,格式为ASCII,也就是字符串,变量格式可选Binary,Boolean,ASCII,I8,I1,I2,I4,F8,F4,U8,U1,U2,U4
-
VariablePLCName,用户定义的PLC变量名称,Variable的值来源,必须填写全称才能识别到
-
ValueCount,变量个数,对于ASCII填0就行,会自动获取字符串长度。对于其他格式,填数组的长度(填0会自动判断数组长度)。
StatusVariable.Name:= 'Temp'; // 变量名称 StatusVariable.VariableType:= E_VariableType.SV; // 变量类型 StatusVariable.VID:= 102; StatusVariable.Unit:= 'degC'; // 摄氏度 StatusVariable.Format:= E_SECSItemFormat.F4; // 变量Format StatusVariable.VariablePLCName:='MAIN.Temp'; // PLC变量名称 StatusVariable.ValueCount:= 3; // 变量数量- Format,格式为F4,也就是REAL
- ValueCount,变量个数为3,表示变量
MAIN.Temp是ARRAY[0..2] OF REAL,转换成SML为 F4[3] 值1, 值2, 值3
-
-
Equip.AlarmList:用户可以自行操作ALID列表,带有增删一系列改查功能,操作类似于C#的List<ST_Alarm>。
-
建议1:一般而言,列表在PLC开始运行后配置一次就行了,很少会在运行过程中去手动修改。
-
建议2:建议把添加业务相关的列表的代码放在FB_Equipment调用(即初始化)之后,否则可能被初始化过程清空掉。
至此,mock一台简单的真空炉代码就完成了,接下来我们把代码跑起来测试一下。
3.3 测试
选取了几个常用的通信测试,更多的功能可以自行尝试。
3.3.1 建立连接
我让Equipment做Active,Host做Passive,运行起来。

运行起来后发生的事情如下:
-
因为bEnable为TRUE。Equipment通过TCP连上了Host,然后发送S1F13,收到S1F14后,进入通信状态模型的Communicating状态。
-
因为bOnline为TRUE,且LocalOrRemote为Remote。Equipment发送S1F1,收到S1S2后,进入控制状态模型OnlineRemote状态。
-
控制状态的切换触发了CEID=3的Control State REMOTE事件,因此发送S6F11报告事件。我们不妨解析一下这条S6F11消息。
L[3] // 长度为3的List
U4[1]3 // DATAID=3,自增
U4[1]3 // CEID=3,表示Control State REMOTE事件
L[1] // 长度为1的List
L[2]
U4[1]1 // RPTID=1,表示控制状态报告
L[2]
U1[1]5 // SV=5,表示控制状态ONLINE_Remote ,由报告链接的SVID=1获取
A[16]2026070110422767 // SV=2026070110422767,表示系统时间CLOCK,由报告链接的SVID=2获取
3.3.2 定义报告

Host发送了S2F33,Equipment收到之后,先把Report添加到ReportList,然后根据添加的结果回复S2F34。
可以通过如下代码获取所有的Report进行观察
ReportListArray: ARRAY[0..9] OF ST_Report; // 保存所有的Report,请保证长度足够
F_ClearData(ADR(ReportListArray), SIZEOF(ReportListArray));
Equip.ReportList.GetReportList(ADR(ReportListArray), 10);
3.3.3 发送事件

Equipment发送了S6F11,Host回复S6F12。此处的CEID=100的事件就是业务相关自定义事件。
3.3.4 工艺文件

Host发送S7F3传输工艺文件,发送S7F5读取工艺文件。
3.5 GEM功能
以上只测试了部分功能。下面列举FB_GEMEquipment所有实现的消息(只列出了奇数Function)。
-
Stream1:S1F1,S1F3,S1F11,S1F13,S1F15,S1F17
-
Stream2:S2F13,S2F15,S2F17,S2F18,S2F23,S2F29,S2F31,S2F33,S2F35,S2F37,S2F41
-
Stream5:S5F1,S5F3,S5F5,S5F7
-
Stream6:S6F1,S6F11,S6F19
-
Stream7:S7F1,S7F3,S7F5,S7F17,S7F19
-
Stream9:S9F1,S9F7,S9F9(考虑到用户自定义消息处理,就没有实现S9F3和S9F5)
-
Stream10:S10F1,S10F3
3.6 小结
对于要求不是特别高的场合,上述GEM功能差不多够用了。
4 进阶编程
如果FB_GEMEquipment的能力满足不了需求。用户可以在其基础上增加自定义消息。主要靠的就是I_GEMHandler接口的GEMDataMessageReceived方法。
正好FB_GEMEquipment没有实现S6F21,下面就以S6F21为例,增加对此消息的处理。
4.1 消息说明
先讲解一下S6F21和S6F22消息
S6F21的内容就一条,要读取的ReportID。
S6F22的内容是一个列表,列表的元素个数是Report链接的VID数量。然后对每一个VID创建一个元素个数为2的列表,第1个元素的是VID本身,第2个元素是VID的值。如果RPTID不存在则发送一个长度为0的列表。
4.2 代码说明
先改造FB_GEMHandler,下面是修改的部分,只改了FB_GEMHandler和GEMDataMessageReceived方法
FUNCTION_BLOCK FB_GEMHandler IMPLEMENTS I_GEMHandler
VAR_INPUT
Equip: POINTER TO FB_GEMEquipment; // 用来处理和发送SECS消息
END_VAR
VAR_OUTPUT
END_VAR
VAR
// 用于处理SECS消息
stSECSMessage: ST_SECSMessage;
fbSECSMessage: FB_SECSMessage;
stSECSItem: ST_SECSItem;
fbSECSItem: FB_SECSItem;
TempUDINT: UDINT; // 临时变量
END_VAR
FB_GEMHandler新增了几个变量用来处理和收发SECS消息
METHOD GEMDataMessageReceived : BOOL
VAR_INPUT
RemoteAddr: STRING(15);
RemotePort: UDINT;
stMessageReceived: REFERENCE TO ST_SECSMessage;
END_VAR
VAR
Str: T_MaxString;
//-----
A: INT;
RPTIDIndex: INT;
VIDCount: INT;
END_VAR
// 原本的代码
Str:= CONCAT('Receive SECSMessage from remote', RemoteAddr);
Str:= CONCAT(Str, ':');
Str:= CONCAT(Str, UDINT_TO_STRING(RemotePort));
Str:= CONCAT(Str, ' ');
Str:= CONCAT(Str, fbSECSMessage.ToString(stMessageReceived));
ADSLOGSTR(msgCtrlMask:= ADSLOG_MSGTYPE_HINT, msgFmtStr:= Str, strArg:= '');
// 新增的代码,处理S6F21
IF stMessageReceived.Stream = 6 THEN
IF stMessageReceived.Function_ = 21 THEN // S6F21 Annotated Individual Report Request (AIRR)
IF stMessageReceived.stSECSItem.ItemCount = 1 THEN
fbSECSItem.ParseItemValue(stMessageReceived.stSECSItem, 0, ADR(TempUDINT), SIZEOF(TempUDINT)); // 获取RPTID
// 构造S6F22 Annotated Individual Report Data (AIRD)
fbSECSMessage.Clear(stSECSMessage);
stSECSMessage.SessionId:= Equip^.SessionId;
stSECSMessage.Stream:= 6;
stSECSMessage.Function_:= 22;
stSECSMessage.replyExpected:= FALSE;
stSECSMessage.SystemBytes:= stMessageReceived.SystemBytes;
// 检查RPTIDIndex是否存在
RPTIDIndex:= Equip^.ReportList.FindReportByRPTID(TempUDINT);
IF RPTIDIndex >= 0 THEN
// 存在则正常构造
FOR A:= 0 TO Var_Constant.ReportVIDCount - 1 DO //0..19
IF Equip^.ReportList.GetReport(RPTIDIndex)^.VIDList[A] > 0 THEN
VIDCount:= VIDCount + 1;
END_IF
END_FOR
fbSECSItem.CreateSECSListItem(VIDCount, stSECSItem); //L,n
fbSECSItem.AddItem(stSECSMessage.stSECSItem, stSECSItem);
FOR A:= 0 TO Var_Constant.ReportVIDCount - 1 DO //0..19
TempUDINT:= Equip^.ReportList.GetReport(RPTIDIndex)^.VIDList[A]; // 获取VID
IF TempUDINT > 0 THEN
fbSECSItem.CreateSECSListItem(2, stSECSItem); //L,2
fbSECSItem.AddItem(stSECSMessage.stSECSItem, stSECSItem);
fbSECSItem.CreateSECSU4Item(ADR(TempUDINT), 1, stSECSItem); //VID
fbSECSItem.AddItem(stSECSMessage.stSECSItem, stSECSItem);
IF NOT Equip^.VariableList.CreateSECSItemByVID(Equip^.ReportList.GetReport(RPTIDIndex)^.VIDList[A], stSECSItem) THEN // V,用VID构造SECSItem
fbSECSItem.CreateSECSBinaryItem(0, 0, stSECSItem); // Binary空数据
END_IF
fbSECSItem.AddItem(stSECSMessage.stSECSItem, stSECSItem);
END_IF
END_FOR
ELSE
// 不存在则回复L,0
fbSECSItem.CreateSECSListItem(0, stSECSItem); //L,0
fbSECSItem.AddItem(stSECSMessage.stSECSItem, stSECSItem);
END_IF
Equip^.SendSECSMessage(stSECSMessage); // 收到S6F21时回复S6F22
ELSE
Equip^.SendS9Fx(7, stMessageReceived); // 发送S9F7 Illegal Data (IDN)
END_IF
END_IF
END_IF
Equipment收到S6F21后,先获取RPTID,然后根据RPTID获取对应的VID以及VID的值,构造S6F22回复给Host。
重点是如何理解ST_SECSItem,ST_SECSItem可以表示为一条SECS消息的内容部分,即Message Text部分。
在ST_SECSItem中,原始数据保存在Data,消息内容的每一行都用一个ST_SECSItemInfo表示,消息内容的行数就是ItemCount。
解析和构造SECSItem需要用到FB_SECSItem和ST_SECSItem。
下面是解析SECSItem的代码
fbSECSItem.ParseItemValue(stMessageReceived.stSECSItem, 0, ADR(TempUDINT), SIZEOF(TempUDINT)); // 获取RPTID
ParseItemValue获取RPTID保存在TempUDINT。
- 第1个参数是收到的SECS消息的内容。
- 第2个参数是第几个Item,序号从0开始,S6F21只有1行,因此解析的就是S6F21的第1行,即RPTID。
- 第3个参数是解析出来的数据的保存地址。
- 第4个参数是保存地址的大小。
下面是构造SECSItem的代码
fbSECSItem.CreateSECSListItem(VIDCount, stSECSItem); //L,n
fbSECSItem.AddItem(stSECSMessage.stSECSItem, stSECSItem);
FOR A:= 0 TO Var_Constant.ReportVIDCount - 1 DO //0..19
TempUDINT:= Equip^.ReportList.GetReport(RPTIDIndex)^.VIDList[A]; // 获取VID
IF TempUDINT > 0 THEN
fbSECSItem.CreateSECSListItem(2, stSECSItem); //L,2
fbSECSItem.AddItem(stSECSMessage.stSECSItem, stSECSItem);
fbSECSItem.CreateSECSU4Item(ADR(TempUDINT), 1, stSECSItem); //VID
fbSECSItem.AddItem(stSECSMessage.stSECSItem, stSECSItem);
IF NOT Equip^.VariableList.CreateSECSItemByVID(Equip^.ReportList.GetReport(RPTIDIndex)^.VIDList[A], stSECSItem) THEN
fbSECSItem.CreateSECSBinaryItem(0, 0, stSECSItem); // Binary空数据
END_IF
fbSECSItem.AddItem(stSECSMessage.stSECSItem, stSECSItem);
END_IF
END_FOR
- 第1行:CreateSECSListItem是构造List,构造一个长度为VIDCount的List,VIDCount是RPTID对应的VID数量,然后把构造好的项保存到变量stSECSItem。
- 第2行:AddItem是把临时变量stSECSItem添加到stSECSMessage.stSECSItem中,相当于给整个消息添加了一行 L,n。
- 第8行:CreateSECSU4Item是构造U4,构造长度为1的U4数组,数组元素值是TempUDINT,然后把构造好的项保存到变量stSECSItem。如果长度大于1,就要用ARRAY OF UDINT了。
- 第10行:CreateSECSItemByVID,一个帮助方法,通过VID来创建stSECSItem。返回TRUE成功,返回FALSE失败,失败时创建一个 B[0]。
下面是发送SECS消息的代码
IF stMessageReceived.stSECSItem.ItemCount = 1 THEN // 简单判断格式
Equip^.SendSECSMessage(stSECSMessage);
ELSE
Equip^.SendS9Fx(7, stMessageReceived); // 发送S9F7 Illegal Data (IDN)
END_IF
如果收到的消息格式正确,则正常构造S6F22并回复。如果格式错误,则回复S9F7,表示非法数据。
4.3 测试
下面实际测试一下S6F21消息。

读取RPTID=1的报告,上文提到过RPTID:1=控制状态,链接到SVID1 + SVID2。
而SVID:1=ControlState,当前控制状态。SVID:2=CLOCK,系统时间,YYYYMMDDhhmmsscc
因此收发都符合预期。
4.4 小结
对于要求更高的场合,用户通过自定义GEM功能也基本上能满足。
5 高阶编程
如果对我实现的FB_GEMEquipment不满意,或者自定义GEM功能也达不到要求。可以选择从SECS开始,甚至编写你自己的FB_GEMEquipment。
下面简单说一下怎么从SECS开始收发消息,新建一个项目,就叫EEEE_SECSGEM_Test1吧。引用EEEE_SECSGEM。
5.1 完整代码
先把完整代码贴出来,然后一段一段地说明。
PROGRAM MAIN
VAR
CurrentMode: E_HSMSConnectionMode:= E_HSMSConnectionMode.Active; // 连接模式
active: FB_SECSClient; // SECS端点
activeSECSHandler: FB_SECSHandler; // 用户自定义的事件处理器
passive: FB_SECSServer;
passiveSECSHandler: FB_SECSHandler;
SECSEndpoint: I_SECSEndpoint;
bSendS1F1: BOOL; // 发送S1F1消息
bSendS1F2: BOOL; // 发送S1F2消息
stSECSMessage: ST_SECSMessage;
fbSECSMessage: FB_SECSMessage;
END_VAR
IF CurrentMode = E_HSMSConnectionMode.Active THEN
active.SubscribeHandler(activeSECSHandler);
active(bEnable:= TRUE, RemoteHost:= '127.0.0.1', RemotePort:= 200, SessionId:= 1); //Mode:= E_HSMSConnectionMode.Active
SECSEndpoint:= active;
ELSIF CurrentMode = E_HSMSConnectionMode.Passive THEN
passive.SubscribeHandler(passiveSECSHandler);
passive(bEnable:= TRUE, LocalHost:= '127.0.0.1', LocalPort:= 300, SessionId:= 1); //Mode:= E_HSMSConnectionMode.Passive
SECSEndpoint:= passive;
END_IF
IF bSendS1F1 THEN
fbSECSMessage.Clear(stSECSMessage);
stSECSMessage.SessionId:= 1;
stSECSMessage.Stream:= 1;
stSECSMessage.Function_:= 1;
SECSEndpoint.SendMessage(stSECSMessage);
bSendS1F1:= FALSE;
END_IF
IF bSendS1F2 THEN
fbSECSMessage.Clear(stSECSMessage);
stSECSMessage.SessionId:= 1;
stSECSMessage.Stream:= 1;
stSECSMessage.Function_:= 2;
SECSEndpoint.SendMessage(stSECSMessage);
bSendS1F2:= FALSE;
END_IF
5.2 分段说明
5.2.1 片段1
active.SubscribeHandler(activeSECSHandler);
active(bEnable:= TRUE, RemoteHost:= '127.0.0.1', RemotePort:= 200, SessionId:= 1); //Mode:= E_HSMSConnectionMode.Active
SECSEndpoint:= active;
SECS端点分为active和passive,其实就是TCPClient和TCPServer,以active为例。
activeSECSHandler是用户自定义的SECS事件处理器,然后FB_SECSClient订阅此GEMHandler。如果你熟悉C#,这就类似于active.SECSHandler += activeSECSHandler。
SECSHandler可以处理收到消息,发送消息,内部消息,状态改变,T3超时等事件。
我写了一个简单的SECSHandler,完整代码如下。用户可以根据自己的需要编写。
FUNCTION_BLOCK FB_SECSHandler IMPLEMENTS I_SECSHandler
VAR_INPUT
END_VAR
VAR_OUTPUT
END_VAR
VAR
fbSECSMessage: FB_SECSMessage; // 处理SECS消息的功能块
END_VAR
FB_SECSHandler实现了I_SECSHandler接口。下面都是I_SECSHandler接口方法的具体实现。
METHOD SECSDataMessageReceived : BOOL
VAR_INPUT
RemoteAddr: STRING(15);
RemotePort: UDINT;
stMessageReceived: REFERENCE TO ST_SECSMessage;
END_VAR
VAR
Str: T_MaxString;
END_VAR
Str:= CONCAT('Receive SECSMessage from remote', RemoteAddr);
Str:= CONCAT(Str, ':');
Str:= CONCAT(Str, UDINT_TO_STRING(RemotePort));
Str:= CONCAT(Str, ' ');
Str:= CONCAT(Str, fbSECSMessage.ToString(stMessageReceived)); // 转换成SML
ADSLOGSTR(msgCtrlMask:= ADSLOG_MSGTYPE_HINT, msgFmtStr:= Str, strArg:= '');
这个方法最关键,消息处理的逻辑就要在这个方法实现。比如收到S1F1就回复S1S2并切换通信状态至Online。我仅仅是把收到的消息打印了出来。
METHOD SECSDataMessageSend : BOOL
VAR_INPUT
RemoteAddr: STRING(15);
RemotePort: UDINT;
stMessageSend: REFERENCE TO ST_SECSMessage;
END_VAR
VAR
Str: T_MaxString;
END_VAR
Str:= CONCAT('Send SECSMessage to remote', RemoteAddr);
Str:= CONCAT(Str, ':');
Str:= CONCAT(Str, UDINT_TO_STRING(RemotePort));
Str:= CONCAT(Str, ' ');
Str:= CONCAT(Str, fbSECSMessage.ToString(stMessageSend));
ADSLOGSTR(msgCtrlMask:= ADSLOG_MSGTYPE_HINT, msgFmtStr:= Str, strArg:= '');
把发送的消息打印出来,不想看就不需要实现此方法。
METHOD SECSInternalMessage : BOOL
VAR_INPUT
sMsg: T_MaxString;
END_VAR
ADSLOGSTR(msgCtrlMask:= ADSLOG_MSGTYPE_HINT, msgFmtStr:= sMsg, strArg:= '');
把功能块内部消息打印出来,建议实时监控,包含很多重要信息。
METHOD SECSStateChanged : BOOL
VAR_INPUT
RemoteAddr: STRING(15);
RemotePort: UDINT;
eState: E_HSMSConnectionState;
END_VAR
VAR
Str: STRING;
END_VAR
Str:= CONCAT('Remote ', RemoteAddr);
Str:= CONCAT(Str, ' HSMS Connection State change to ');
Str:= CONCAT(Str, F_HSMSConnectionStateToSTRING(eState));
ADSLOGSTR(msgCtrlMask:= ADSLOG_MSGTYPE_HINT, msgFmtStr:= Str, strArg:= '');
HSMS连接状态改变,建议实时监控。
METHOD SECST3Timeout : BOOL
VAR_INPUT
MessageHeader: ARRAY [0..9] OF BYTE;
END_VAR
ADSLOGSTR(msgCtrlMask:= ADSLOG_MSGTYPE_HINT, msgFmtStr:= 'T3 Timeout', strArg:= '');
T3超时报警,一般需要回复S9F9。
5.2.2 片段2
IF bSendS1F1 THEN
fbSECSMessage.Clear(stSECSMessage);
stSECSMessage.SessionId:= 1;
stSECSMessage.Stream:= 1;
stSECSMessage.Function_:= 1;
SECSEndpoint.SendMessage(stSECSMessage);
bSendS1F1:= FALSE;
END_IF
IF bSendS1F2 THEN
fbSECSMessage.Clear(stSECSMessage);
stSECSMessage.SessionId:= 1;
stSECSMessage.Stream:= 1;
stSECSMessage.Function_:= 2;
SECSEndpoint.SendMessage(stSECSMessage);
bSendS1F2:= FALSE;
END_IF
手动发送S1F1,S1F2,一般把Function为偶数的消息放在SECSDataMessageReceived方法里构造。此处只是测试消息的收发。
5.3 测试
从SECS开始,所有的SECS消息都需要用户自己解析,自己编写。
6 License
试用版可使用 2 小时。 长时间使用需要填写 license。

浙公网安备 33010602011771号