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,运行起来。

image

运行起来后发生的事情如下:

  • 因为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 定义报告

image

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 发送事件

image

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

3.3.4 工艺文件

image

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消息。

image

读取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。

点击下载EEEE_SECSGEM.compiled-library

posted @ 2026-07-14 11:11  tossorrow  阅读(34)  评论(0)    收藏  举报