专题介绍:基于KQD系统进行定制开发


基本信息

快驱动PLC系统虽然提供了工业领域的常用驱动,但是很多时候,并不能完全满足客户的需求。针对这种情况,我们提供了定制开发接口。对于有单片机开发能力的客户,可以基于此接口,对快驱动系统进行功能扩展:

  • 用户可以用C代码开发硬件驱动并内嵌到PLC系统中运行,在Designer中未配置的单片机硬件资源,其使用和释放完全由用户管理。
  • 定制开发的硬件驱动,可以通过I/O变量来传递数据,也可以通过定制的指令来控制硬件。
  • 用户可以用C代码开发各种算法,然后在Application Editor中通过子程序调用这些算法。子程序的参数则用来传递输入输出数据。
  • 如果配置了西门子全兼容系统,也可以在STEP 7-MicroWIN SMART中通过子程序调用这些算法。
  • PLC系统对定制开发的IDE和编译工具没有强制要求,用户可以使用Keil,也可以基于开源的gcc-arm来开发。
  • 如果在Designer的配置中,SWD调试管脚(例如STM32F407的PA13、PA14)未分配功能,则可以使用调试器(例如ST-LINK)对定制开发项目进行调试。其开发过程与正常的单片机开发并无根本的区别。

实现原理

FLASH的分区

在Designer中查看一个典型的FLASH分配:

在这个STM32F407VG单片机中,最开始的两个Sector预留给了Bootloader。紧随其后的两个Sector分别是User_BANK0和User_BANK1,用来存放Application Editor的梯形图。最后面的Sector则分配给PLC系统固件使用。我们知道,Application Editor自带的EC30/EC40都是编译型的PLC。Application Editor根据用户的梯形图逻辑,生成C代码;然后C代码编译成CPU的机器码,下载到User_BANK0或者User_BANK1。每一个User_BANK,都是一套独立的程序。系统支持多个BANK的梯形图一起运行,也是为了方便各个阶段的开发方单独下载自己的梯形图程序。例如控制器生产方可以将硬件相关的模拟量标定程序、滤波程序下载到BANK0,设备制造方则用BANK1来写控制工艺。

在Designer中,可以根据需要定义BANK的数量和大小(V1.2.0版本之后):

每一个BANK,就是一段独立的程序,而且不一定就是梯形图程序生成的,系统支持的程序有:

  • EC30/EC40 编译型梯形图生成的程序。
  • IEC61131 IEC61131代码生成的程序。
  • CUSTOM 定制开发程序。

PLC系统在上电后,会首先检查这几个BANK是否有有效的程序,如果有,则会获取BANK的类型。如果BANK类型是定制开发的程序,则系统会对这部分程序进行初始化并在合适的时候调用此BANK中的程序。如果是编译型梯形图,或者IEC61131代码生成的程序,编译过程完全由Application Editor自动完成,如果是定制开发的程序,则需要用户自行编译并下载到对应的BANK上去。为了让系统正确识别BANK类型并调用,定制开发的程序,需要构造一个表头,放在代码的最前面。

EC30/EC40/IEC61131 的表头:

地址偏移符号说明
0Fcc_TableIndex_SIZE基础服务函数地址的数量,固定为7
4Fcc_TableIndex_SYS系统块表格的地址
8Fcc_TableIndex_DAT数据块表格的地址
12Fcc_TableIndex_INT梯形图中断函数表的地址
16Fcc_TableIndex_SBR梯形图子程序函数表的地址
20Fcc_TableIndex_EXT扩展函数表的地址
24Fcc_TableIndex_RTEC运行时初始化服务函数地址
28Fcc_TableIndex_TargetValue读取设备信息函数地址
32Fcc_TableIndex_Marker扩展标记,固定为0x347D058E
36Fcc_TableIndex_CRC32此BANK有效数据的CRC32(在线监控前,上位机读取此值判断单片机BANK中的数据与上位机项目是否一致。)
40 Fcc_TableIndex_BankType BANK的类型标记:
  • 0xB1000000: EC30/EC40 Hex
  • 0xB2000000: IEC Hex
  • 0xC1000000: Custom Hex
44Fcc_TableIndex_IECInitIEC初始化函数的地址
48Fcc_TableIndex_IECRunIEC运行函数的地址
52Fcc_TableIndex_IECLookupVariablesIEC变量监控函数的地址

CUSTOM 定制开发程序的表头:

地址偏移符号说明
0CustomTableIndex_SIZE基础服务函数地址的数量,固定为7
4CustomTableIndex_R1保留
8CustomTableIndex_R2保留
12CustomTableIndex_R3保留
16CustomTableIndex_R4保留
20CustomTableIndex_R5保留
24CustomTableIndex_R6保留
28CustomTableIndex_R7保留
32CustomTableIndex_Marker扩展标记,固定为0x347D058E
36CustomTableIndex_CRC32此BANK有效数据的CRC32(在线监控前,上位机读取此值判断单片机BANK中的数据与上位机项目是否一致。)
40 CustomTableIndex_BankType BANK的类型标记:
  • 0xB1000000: EC30/EC40 Hex
  • 0xB2000000: IEC Hex
  • 0xC1000000: Custom Hex
44CustomTableIndex_RTEInitC运行时初始化服务函数地址
48CustomTableIndex_USRInit用户初始化服务函数地址

系统调用定制开发程序:直接调用

直接调用是最简单的方式,定制开发程序的表头中,提供了可以直接调用的函数地址。例如在CustomTableIndex_USRInit中,存放的就是定制开发程序初始化函数的地址,系统识别到有效的定制开发程序,则直接调用此地址上的函数。


Std_ReturnType Fcc_FlashExcuteCustomUSRInit(Fs_BankType bankId) {
    /* 判断BANK是否有效。*/
    if (Fs_StatusResult_Read != Fs_GetStatus(bankId)) {
        return E_NOT_OK;
    }
    /* 判断BANK的类型是否为定制开发 */
    if (!(Fs_GetContent(bankId) == Fs_Content_Custom)) {
        return E_NOT_OK;
    }
    /* 获取BANK首地址 */
    uint32 flashAddress = Fcc_FlashGetBankAddress(bankId, NULL_PTR, NULL_PTR);
    if (flashAddress == (uint32_t)-1) {
        return E_NOT_OK;
    }
    /* 获取USRInit服务函数地址并判断地址是否有效 */
    uint32 dataAddress = ((const uint32*)flashAddress)[Fcc_CustomTableIndex_USRInit];
    if (!Fcc_FlashIsValidAddress(bankId, dataAddress)) {
        return E_NOT_OK;
    }
    /* 直接调用此服务函数 */
    (*(void(*)(void))dataAddress)();
    return E_OK;
}
                    

以上是KQD系统调用定制开发程序初始化函数的过程。

定制开发调用系统程序:直接调用

和上面的过程相反,定制程序有时候需要调用系统的一些服务,KQD系统将一些最常用的服务函数地址打包成一个表格,放置在FLASH的特定位置,供定制程序使用:


typedef struct {
    void(*Fcc_IdleHook)(void);
    void* (*Fcc_GetExtensionApi)(Fcc_ExtensionType extensionId);
    Std_ReturnType(*Fcc_ExcuteInstruction)(Fcc_InstructionType instructionId);
    Std_ReturnType(*Fcc_WakeupEvent)(Fcc_EventType eventId);
    uint16(*Fcc_GetTimeElapsed)(uint8 timerIndex);
    void(*Fcc_SetNonFatalError)(uint32 position, uint16 errCode);
    void(*Fcc_SetFatalError)(uint32 position, uint16 errCode);
    void(*Os_SuspendAllInterrupts)(void);
    void(*Os_ResumeAllInterrupts)(void);
    uint32(*Os_GetSysTick)(void);
    uint32 VersionMarker;
    void(*Fcc_IECMonitorPOUEnter)(char* pouName);
    void(*Fcc_IECMonitorPOULeave)(void);
    void(*Fcc_IECMonitorValue)(uint16_t lastLine, uint16_t lastColumn, uint8 dataType, uint8* dataPtr, uint8 dataSize);
    void(*Fcc_IECVariableMatched)(uint16_t variableLine, uint8 dataType, uint8* dataPtr, uint8 dataSize);
    void*(*Fcc_GetCustomApi)(uint32 apiId);
} Fcc_ExportApiType;
                    

以下是这些系统服务函数的大致说明:

索引符号说明
0Fcc_IdleHook空闲回调。在执行长任务或查询等待时,需要周期调用此接口。系统在此接口中进行事件处理,若长时间不调用此函数,所有事件会被阻塞。如果是EC30/EC40梯形图,每执行一个NETWORK,会调用一次此函数。
1Fcc_GetExtensionApi获取系统扩展的API接口表。例如TinyLcd的用户程序,通过此接口获取TinyLcd的API接口表,通过表中的TinyLcd_SetExtensionConfig服务设置LCD组态数据。
2Fcc_ExcuteInstruction调用系统提供的梯形图指令。
3Fcc_WakeupEvent激活某个事件。
4Fcc_GetTimeElapsed按照定时器编号,获取定时器的时间增量。
5Fcc_SetNonFatalError设置非致命错误号。
6Fcc_SetFatalError设置致命错误号。
7Os_SuspendAllInterrupts开启中断保护。
8Os_ResumeAllInterrupts关闭中断保活。
9VersionMarker扩展标记,固定为0xDC4D1E00,有此标记说明后续函数服务存在。没有此标记说明是V1.2.0之前的固件,并不支持定制开发。
10Fcc_IECMonitorPOUEnterIEC过程量监控:通知系统,IEC程序开始执行某POU。
11Fcc_IECMonitorPOULeaveIEC过程量监控:通知系统,IEC程序退出执行某POU。
12Fcc_IECMonitorValueIEC过程量监控:通知系统,IEC程序在某位置读写了某值。
13Fcc_IECVariableMatchedIEC全局变量监控:通知系统,某变量命中设定的读写范围。
14Fcc_GetCustomApi获取定制开发系统服务API接口表。

对于定制开发来说,其中最重要的接口就是Fcc_GetCustomApi,所有和定制开发相关的系统API接口表,都通过此接口获得。

可查看定制开发程序使用此接口的代码:


/* Fcc_exportApi在系统固件的FLASH中,需要在LD脚本中指定Fcc_exportApi的地址 */
extern const Fcc_ExportApiType Fcc_exportApi;
/* 获取定制开发系统服务API接口表 */
void* Fcc_GetCustomApi(uint32 apiId) {
    /* 检查扩展标记 */
    if (Fcc_exportApi.VersionMarker == 0xDC4D1E00) {
        /* 直接调用表中的Fcc_GetCustomApi函数 */
        return Fcc_exportApi.Fcc_GetCustomApi(apiId);
    }
    return NULL_PTR;
}
                    

定制开发调用系统程序:通过API接口表

考虑到后续版本兼容,绝大部分系统服务并不能直接调用。KQD系统提供了一个可以直接调用的Fcc_GetCustomApi接口。其他定制开发相关的系统服务,都需要通过此接口间接获得。KQD系统将不同功能的服务函数进行分组,功能相关的服务函数地址放在一个表格中,做成一个API接口表。定制开发程序调用Fcc_GetCustomApi接口时,提供一个参数:apiId。Fcc_GetCustomApi根据此参数的值返回指定的表格。通过这种间接的方式,Fcc_GetCustomApi可以检查指定的API接口表是否存在,如果不存在,返回一个NULL_PTR可以明确告知定制开发程序,此API接口表并不被系统支持。

具体可参考KQD系统的Fcc_GetCustomApi实现:


#define CUSTOMIZED_API(v)   (0x1673B900 | (v))
#define FCC_API(v)          (0xA3CF9000 | (v))
#define MB_API(v)           (0x32BED700 | (v))
void* Customized_GetCustomApi(uint32 apiId) {
    switch (apiId) {
    case CUSTOMIZED_API(0):
        return (void*)&Customized_exportApi;
    case FCC_API(0):
        return (void*)&Customized_exportFccApi;
    case MB_API(0):
        return (void*)&Customized_exportMbApi;
    default:
        break;
    }
    return NULL_PTR;
}
                    

例如定制开发程序请求获取MB_API(0)这个API接口表,函数Customized_GetCustomApi便将Customized_exportMbApi这个API接口表返回给定制开发程序。Customized_exportMbApi的定义如下:


typedef struct {
    Std_ReturnType(*Mb_GetBlockInfos)(Mb_BlockIdType blockId, Mb_BlockInfosType* blockInfosPtr);
    Std_ReturnType(*Mb_CheckReadAccess)(Mb_BlockIdType blockId, uint8* bytes, uint32 byteCount, Mb_AccessType accessType);
    Std_ReturnType(*Mb_CheckWriteAccess)(Mb_BlockIdType blockId, uint8* bytes, uint32 byteCount, Mb_AccessType accessType);
    Mb_BlockIdType(*Mb_GetBlockByPduId)(uint8 pduId);
    Mb_BlockIdType(*Mb_GetBlockByAccess)(uint8* bytes, uint32 byteCount);
    uint8*(*Mb_GetData)(Mb_BlockIdType blockId, uint16 blockOffset);
} Customized_MbApiType;
const Customized_MbApiType Customized_exportMbApi = {
    Mb_GetBlockInfos,
    Mb_CheckReadAccess,
    Mb_CheckWriteAccess,
    Mb_GetBlockByPduId,
    Mb_GetBlockByAccess,
    Mb_GetData
};
                    

如代码所示,Customized_exportMbApi这个API接口表包含6个API接口,都用于PLC变量相关的操作。在定制开发程序中,如果需要调用系统的Mb_GetBlockInfos服务函数来获取PLC区域的信息,可使用已经做好的一个封装:


/* Fcc_exportApi在系统固件的FLASH中,需要在LD脚本中指定Fcc_exportApi的地址 */
extern const Fcc_ExportApiType Fcc_exportApi;
/* 获取定制开发系统服务API接口表 */
void* Fcc_GetCustomApi(uint32 apiId) {
    /* 检查扩展标记 */
    if (Fcc_exportApi.VersionMarker == 0xDC4D1E00) {
        /* 直接调用表中的Fcc_GetCustomApi函数 */
        return Fcc_exportApi.Fcc_GetCustomApi(apiId);
    }
    return NULL_PTR;
}
/* PLC变量相关操作的API接口表,默认为NULL_PTR */
const Customized_MbApiType* Mb_api = NULL_PTR;
Std_ReturnType Mb_GetBlockInfos(Mb_BlockIdType blockId, Mb_BlockInfosType* blockInfosPtr) {
    /* 若API接口表为空指针,调用Fcc_GetCustomApi直接接口获取API接口表 */
    if (Mb_api == NULL_PTR) {
        Mb_api = (const Customized_MbApiType*)Fcc_GetCustomApi(MB_API(0));
    }
    if (Mb_api == NULL_PTR) {
        /* 接口不被支持,返回失败 */
        return E_NOT_OK;
    }
    /* 接口存在,调用系统提供的Mb_GetBlockInfos服务函数 */
    return Mb_api->Mb_GetBlockInfos(blockId, blockInfosPtr);
}
                    

系统调用定制开发程序:注册回调函数表

在PLC系统状态发生变化时(例如客户重新下载了梯形图需要对硬件进行重新配置),PLC系统需要调用定制开发程序的接口,告知定制开发程序,系统状态发生了变化。在这种情况下,制开发程序需要准备一个回调函数表,通过调用告诉系统,我已经准备好了。你在必要的时候,调用我回调函数表中的函数通知我。

查看定制开发程序EcuM模块中的实现:


/* 默认回调函数为空,可在其他文件重定义 */
__WEAK void EcuMNotify_APP_OnUserInit(EcuM_InitPriorityType priority) {
}
__WEAK void EcuMNotify_APP_OnLoopUserDeInit(EcuM_InitPriorityType priority) {
}
__WEAK void EcuMNotify_APP_OnLoopUserInit(EcuM_InitPriorityType priority) {
}
__WEAK void EcuMNotify_APP_OnLoopGetInput(void) {
}
__WEAK void EcuMNotify_APP_OnLoopLink(void) {
}
__WEAK void EcuMNotify_APP_OnLoopMainScanEnter(EcuM_InitPriorityType priority) {
}
__WEAK void EcuMNotify_APP_OnLoopMainScan(void) {
}
__WEAK void EcuMNotify_APP_OnLoopMainScanLeave(EcuM_InitPriorityType priority) {
}
__WEAK void EcuMNotify_APP_OnLoopSetOutput(void) {
}
__WEAK void EcuMNotify_APP_OnUserDeInit(EcuM_InitPriorityType priority) {
}
/* 创建回调函数表 */
const Customized_EcuMNotifyType EcuM_notify = {
    EcuMNotify_APP_OnUserInit,
    EcuMNotify_APP_OnLoopUserDeInit,
    EcuMNotify_APP_OnLoopUserInit,
    EcuMNotify_APP_OnLoopGetInput,
    EcuMNotify_APP_OnLoopLink,
    EcuMNotify_APP_OnLoopMainScanEnter,
    EcuMNotify_APP_OnLoopMainScan,
    EcuMNotify_APP_OnLoopMainScanLeave,
    EcuMNotify_APP_OnLoopSetOutput,
    EcuMNotify_APP_OnUserDeInit
};
Std_ReturnType EcuM_NotifyEnable(Customized_PortType port) {
    /* 注册回调函数表 */
    return Customized_NotifyEnable(port, ECUM_NOTIFY(0), &EcuM_notify);
}
                    

这里用__WEAK关键之定义了一系列默认的回调函数,然后在下方将其组合到EcuM_notify形成一个回调函数表。最后调用Customized_NotifyEnable,在PLC系统中注册这个回调函数表。

在PLC系统中Customized_NotifyEnable用来注册回调函数:


Std_ReturnType Customized_NotifyEnable(Customized_PortType port, uint32 notifyId, const void* notifyTable) {
    /* 判断端口是否正确 */
    if (!(port < Customized_config.numOfPorts)) {
        return E_NOT_OK;
    }
    /* 判断端口是否使能 */
    Customized_PortDataType* portData = &Customized_portDatas[port];
    if (portData->enabled == FALSE) {
        return E_NOT_OK;
    }
    switch (notifyId) {
    /* ECUM的回调 */
    case ECUM_NOTIFY(0):
        if (portData->notifyTableEcuM) {
            /* 已经被注册,返回失败 */
            return E_NOT_OK;
        }
        portData->notifyTableEcuM = (const Customized_EcuMNotifyType*)notifyTable;
        /* 记录回调函数表,返回成功 */
        return E_OK;
    /* case ... */
    default:
        break;
    }
    return E_NOT_OK;
}
                    

例如在PLC主循环中需要获取输入时,就会调用前面注册好的回调:


/* 在PLC主循环中需要将输入状态写入映射区I、AI时  */
void Customized_APP_OnLoopGetInput(void) {
    /* 遍历定制开发端口 */
    const Customized_PortConfigType* portConfig = &Customized_portConfigs[0];
    Customized_PortDataType* portData = &Customized_portDatas[0];
    Customized_PortType port = 0;
    for (; port != Customized_config.numOfPorts; ++port, ++portData, ++portConfig) {
        /* 如果端口被使能且注册了ECUM回调 */
        if (portData->enabled != FALSE && portData->notifyTableEcuM != NULL_PTR) {
            /* 调用回调函数表中的对应函数 */
            (*portData->notifyTableEcuM->EcuMNotify_APP_OnLoopGetInput)();
        }
    }
}
                    

系统调用定制开发程序:注册中断回调

用户定制程序有时候需要用到硬件中断,硬件中断是由PLC系统统一管理的,如果转发给定制程序,需要定制程序单独注册。和注册回调函数表不同,注册中断回调一次只注册一个中断回调,而不是一组。


/* 中断服务函数,调用STM32CubeMX的实现 */
void TIM4_Irq(void) {
    HAL_TIM_IRQHandler(&htim4);
}
/* 当需要执行用户初始化时 */
void EcuMNotify_APP_OnUserInit(EcuM_InitPriorityType priority) {
    /* 定制程序被分配了端口 */
    if (portId != 0xFF) {
        /* 调用STM32CubeMX生成的定时器初始化 */
        MX_TIM4_Init();
        /* 在PLC系统中注册此中断 */
        Customized_InterruptEnable(portId, 0, TIM4_Irq);
    }
}
                    

在PLC系统中Customized_InterruptEnable用来注册中断回调:


Std_ReturnType Customized_InterruptEnable(Customized_PortType port, uint8 interruptIndex, Customized_IsaType isa) {
    /* 判断端口是否正确 */
    if (!(port < Customized_config.numOfPorts)) {
        return E_NOT_OK;
    }
    /* 判断端口是否使能 */
    Customized_PortDataType* portData = &Customized_portDatas[port];
    if (portData->enabled == FALSE) {
        return E_NOT_OK;
    }
    /* 判断中断索引是否正确 */
    const Customized_PortConfigType* portConfig = &Customized_portConfigs[port];
    if (!(interruptIndex < portConfig->numOfInterrupts)) {
        return E_NOT_OK;
    }
    Customized_InterruptDataType* interruptData = &portConfig->interruptDatas[interruptIndex];
    if (interruptData->isa != NULL_PTR) {
        /* 已经被注册,返回失败 */
        return E_NOT_OK;
    }
    /* 记录中断服务函数 */
    interruptData->isa = isa;
    /* 在PLC系统中开启中断 */
    CMSIS_InterruptEnable(portConfig->interruptConfigs[interruptIndex].irqNumber, Os_PendingLevel_Medium);
    /* 返回成功 */
    return E_OK;
}
                    

最后,当硬件中断触发时:


void Customized_Irq(Customized_PortType port, uint8 interruptIndex) {
    /* 判断端口是否正确 */
    if (!(port < Customized_config.numOfPorts)) {
        return;
    }
    /* 判断端口是否使能 */
    Customized_PortDataType* portData = &Customized_portDatas[port];
    if (portData->enabled == FALSE) {
        return;
    }
    /* 判断中断索引是否正确 */
    const Customized_PortConfigType* portConfig = &Customized_portConfigs[port];
    if (!(interruptIndex < portConfig->numOfInterrupts)) {
        return;
    }
    /* 已经被注册,则调用被注册的中断服务函数 */
    Customized_InterruptDataType* interruptData = &portConfig->interruptDatas[interruptIndex];
    if (interruptData->isa != NULL_PTR) {
        (*interruptData->isa)();
    }
}
                    

系统调用定制开发程序:注册指令回调

有时候定制开发者希望将算法或者是硬件操作包装成特殊指令。因为指令可以带输入输出参数,这样比单纯的靠I、AI、Q、AQ变量区域来交换信息更为灵活。与中断回调类似,定制开发程序需要针对定制的指令准备指令回调程序。当梯形图执行到此特殊指令时,PLC系统会调用定制开程序注册的回调程序,以实现特殊指令的功能。与中断回调不同的时,对于单一特殊指令,定制开发程序需要准备两份实现,一份用于EC30/EC40的梯形图,一份用于西门子全兼容系统。下面是在定制系统中,注册指令回调的例子:


/* 当需要执行用户初始化时 */
void EcuMNotify_APP_OnUserInit(EcuM_InitPriorityType priority) {
    /* 定制程序被分配了端口 */
    if (portId != 0xFF) {
        /* 在PLC系统中注册这两个指令回调 */
        Customized_InstructionEnable(portId, 0, Instruction_Avg, Instruction_Avg_ByEcc);
    }
}
                    

其中,Instruction_Avg是求平均指令的EC30/EC40实现,Instruction_Avg_ByEcc是求平均指令的西门子兼容系统实现。

在PLC系统中Customized_InstructionEnable用来注册指令回调:


Std_ReturnType Customized_InstructionEnable(Customized_PortType port, uint8 instructionIndex, Customized_IcType ic, Customized_IcByEccType icByEcc) {
    /* 判断端口是否正确 */
    if (!(port < Customized_config.numOfPorts)) {
        return E_NOT_OK;
    }
    /* 判断端口是否使能 */
    Customized_PortDataType* portData = &Customized_portDatas[port];
    if (portData->enabled == FALSE) {
        return E_NOT_OK;
    }
    /* 判断指令索引是否正确 */
    const Customized_PortConfigType* portConfig = &Customized_portConfigs[port];
    if (!(instructionIndex < portConfig->numOfInstructions)) {
        return E_NOT_OK;
    }
    /* 记录两个指令服务函数 */
    Customized_InstructionDataType* instructionData = &portConfig->instructionDatas[instructionIndex];
    instructionData->ic = ic;
    instructionData->icByEcc = icByEcc;
    /* 返回成功 */
    return E_OK;
}
                    

当PLC系统遇到特殊指令调用时,会根据ID调用这两个函数:


/* EC30/EC40定制指令调用 */
void Customized_InstructionCall(Customized_PortType port, uint8 instructionIndex) {
    /* 判断端口是否正确 */
    if (!(port < Customized_config.numOfPorts)) {
        return;
    }
    /* 判断端口是否使能 */
    Customized_PortDataType* portData = &Customized_portDatas[port];
    if (portData->enabled == FALSE) {
        return;
    }
    /* 判断指令索引是否正确 */
    const Customized_PortConfigType* portConfig = &Customized_portConfigs[port];
    if (!(instructionIndex < portConfig->numOfInstructions)) {
        return;
    }
    /* 已经被注册,则调用被注册的指令服务函数 */
    Customized_InstructionDataType* instructionData = &portConfig->instructionDatas[instructionIndex];
    if (instructionData->ic != NULL_PTR) {
        (*instructionData->ic)();
    }
}
/* 西门子全兼容定制指令调用 */
Std_ReturnType Customized_InstructionCall_ByEcc(Customized_PortType port, uint8 instructionIndex, uint8* tempMem, Fcc_PointerParserType pointerParser) {
    /* 判断端口是否正确 */
    if (!(port < Customized_config.numOfPorts)) {
        return E_NOT_OK;
    }
    /* 判断端口是否使能 */
    Customized_PortDataType* portData = &Customized_portDatas[port];
    if (portData->enabled == FALSE) {
        return E_NOT_OK;
    }
    /* 判断指令索引是否正确 */
    const Customized_PortConfigType* portConfig = &Customized_portConfigs[port];
    if (!(instructionIndex < portConfig->numOfInstructions)) {
        return E_NOT_OK;
    }
    /* 已经被注册,则调用被注册的指令服务函数 */
    Customized_InstructionDataType* instructionData = &portConfig->instructionDatas[instructionIndex];
    if (instructionData->icByEcc != NULL_PTR) {
        return (*instructionData->icByEcc)(tempMem, pointerParser);
    }
    return E_NOT_OK;
}
                    

在设计器中配置定制模块

如果希望PLC系统固件支持定制开发,首先需要在设计器(KQD Designer)中,添加定制模块Customized支持:

在设计器中,选择模块子页面。找到Customized模块,选中后点击“添加”按钮。如果需要定制特殊指令,需要在设计器中继续添加Customized_Fe支持:

在设计器中,选择蓝图子页面。点击蓝图中的空白位置,在蓝图中不选择任何模块,则可以在下方列出全部驱动配置。在NvM模块后面一般就能找到Customized配置。点击配置下面的Port端口,添加一个定制开发端口Port0支持:

添加Port0节点后,下面可以设置两个数值,一个是UserKey,一个是FeatureKey。这两个数字建议手动填写一个特殊值,用来匹配定制开发项目。

定制开发项目在初始化的时候,需要注册端口。注册端口的时候,要给出这两个特殊值。如果值不匹配,注册将会被拒绝。每个Port可以配置若干中断(Interrupt)和若干指令(Instruction)。这两个组的配置,会在开发演示中详细说明。

PLC系统的运行流程

状态说明
APP_OnUserInit在这之前系统会调用定制开发的RTEInit和USRInitm,到这里执行用户初始化:
  • 向定制开发项目发送EcuMNotify_APP_OnUserInit回调。
  • 向定制开发项目发送FccNotify_APP_NvReadAll回调,通知定制开发程序读取非易失数据。
  • 应用PLC程序中的数据块。
APP_OnLoopUserDeInit对于动态配置的硬件,尝试释放资源。
APP_OnLoopUserInit对于动态配置的硬件,尝试配置资源。同时还执行:
  • 向定制开发项目发送FccNotify_APP_NvReadAllInProgress回调。
  • 如果所有驱动模块和定制工程读取非易失数据完成,设置PLC的状态为“Fcc_Step_ReadyToRun”。
  • 如果BANK是EC30/EC40梯形图,执行RTEInit。
  • 如果BANK是IEC61131,除了执行RTEInit,还要执行IECInit。
  • 调用BANK上的ExcuteEXT,执行扩展功能配置。
APP_OnLoopGetInput获取PLC的输入,状态写输入映像I、AI。
APP_OnLoopLink处理通讯。
APP_OnLoopMainScanEnter处理强制输入。
APP_OnLoopMainScan执行PLC的主循环扫描。
APP_OnLoopMainScanLeave处理强制输出
APP_OnLoopSetOutput根据输出映像Q、AQ设置PLC的输出。同时还执行:
  • 如果有RESET请求,转跳APP_OnUserDeInit状态。
  • 如果没有RESET请求,转跳APP_OnLoopUserDeInit状态,继续PLC主循环。
APP_OnUserDeInit向定制开发项目发送EcuMNotify_APP_OnUserDeInit回调。

开发演示

准备开发硬件

准备开发板DK60-STM32F412RE

打开开发板的默认蓝图,添加Customized、Customized_Fe模块:

编译项目,修改Customized中的UserKey和FeatureKey:

蓝图上传到服务器,烧录固件到开发板并部署项目到Appliction Editor。

准备STM32CubeIDE项目

在STM32CubeIDE中,新建一个STM32项目:

芯片选择页面选择目标MCU:STM32F412RET6:

项目取名Customized_F4,其他保持默认:

点击完成,得到一个STM32默认项目:

替换链接脚本文件

STM32CubeIDE生成的项目,MCU程序默认是放在芯片FLASH最开始的位置。我们这里要求定制开发程序放在BANK0或者BANK1上。因此这里不能使用STM32CubeIDE自带的链接脚本文件STM32F412RETX_FLASH.ld,而是用我们自己的链接脚本文件。考虑到的Application Editor在编译EC30/EC40程序时,也要用到链接脚本文件,因此我们完全可以借用这个脚本文件。如何得到这个文件呢,选择开发板配置好的PLC类型,编译一个空程序,就能在编译目录中找到这个链接脚本文件。

打开软件Application Editor,确保通过网线或者串口连接到开发板,使用主菜单“PLC”下的“获取PLC类型”命令:

选择PLC类型,注意PLC类型的后缀,如果希望定制开发的程序放在BANK0,就选择BANK0结尾的PLC类型。如果希望定制开发的程序放在BANK1,就选择BANK1结尾的PLC类型。这里我们选择BANK1来放置定制开发程序,那么BANK0就可以用来放置普通梯形图。

选择好PLC类型后,编译一个空的梯形图。出现下面的信息,则说明编译成功。

打开路径:KQDSoftware\版本号\User\Functionals\SMX\Temp,找到“ARMv7M.ld”和“ExternSymbols.txt”这两个文件,将其拷贝到STM32CubeIDE生成的项目中去:

也可以打开ARMv7M.ld,比较设计器Designer中对BANK1的配置,检查是否一致:


ENTRY(Fcc_TABLE)
/* Highest address of the user mode stack */
_estack = 0x20040000;    /* end of RAM */
/* Generate a link error if heap and stack don't fit into RAM */
_Min_Heap_Size = 0;  /* required amount of heap  */
_Min_Stack_Size = 0; /* required amount of stack */
/* Specify the memory areas */
MEMORY
{
FLASH (rx)      : ORIGIN = 0x800C000, LENGTH = 0x4000
RAM (xrw)      : ORIGIN = 0x2003FC00, LENGTH = 0x400
}
                                

特别注意的是,在ARMv7M.ld,程序的入口被修改成了“Fcc_TABLE”。

在STM32CubeIDE中,右键点击项目Customized_F4,选择项目属性,在C/C++ Build的设置中,找到链接参数,将链接脚本文件“STM32F412RETX_FLASH.ld”修改成“ARMv7M.ld”,然后点击应用并关闭。

替换启动文件

下载我们提供的定制开发程序模板,在目录中找到“startup_armv7m.s”,将此文件拷贝到STM32CubeIDE项目的路径“Core\Startup”中,并删除原来的启动文件“startup_stm32f412retx.s”:

刷新下项目,确保启动文件已经被修改:

因为不需要管理中断,这个启动文件比默认的启动文件简单很多:


  .syntax unified
  .cpu cortex-m0
  .fpu softvfp
  .thumb
.global Fcc_TABLE
.word _sidata
.word _sdata
.word _edata
.word _sbss
.word _ebss
.word USR_Init
  .section .text.RTE_Init
  .type RTE_Init, %function
RTE_Init:
  movs r1, #0
  b LoopCopyDataInit
CopyDataInit:
  ldr r3, =_sidata
  ldr r3, [r3, r1]
  str r3, [r0, r1]
  adds r1, r1, #4
LoopCopyDataInit:
  ldr r0, =_sdata
  ldr r3, =_edata
  adds r2, r0, r1
  cmp r2, r3
  bcc CopyDataInit
  ldr r2, =_sbss
  b LoopFillZerobss
FillZerobss:
  movs r3, #0
  str  r3, [r2]
  adds r2, r2, #4
LoopFillZerobss:
  ldr r3, = _ebss
  cmp r2, r3
  bcc FillZerobss
  mov pc, lr
  .section .isr_vector,"a",%progbits
  .type Fcc_TABLE, %object
  .size Fcc_TABLE, .-Fcc_TABLE
Fcc_TABLE:
  .word 7
  .word 0x00000000
  .word 0x00000000
  .word 0x00000000
  .word 0x00000000
  .word 0x00000000
  .word 0x00000000
  .word 0x00000000
  .word 0x347D058E    // Mark
  .word 0xCCCCCCCC    // Check SUM of Project Binary
  .word 0xC1000000    // 0xB1000000: EC30/EC40 Hex
                      // 0xB2000000: IEC Hex
                      // 0xC1000000: Custom Hex
                      // ....
  .word RTE_Init
  .word USR_Init
                    

可以看到,这个启动文件包含了默认启动文件的内存初始化(清空段:bss,初始化段:data)。但是结束添加了函数返回命令“mov pc, lr”。这样内存初始化部分可以作为一个函数来调用。随后就是程序的入口部分Fcc_TABLE。这里按照定制程序表头的要求构建了Fcc_TABLE。在这个表中引用了一个内部函数:RTE_Init;引用了一个外部函数:USR_Init。注意到启动文件开始部分,定义的指令集是“.cpu cortex-m0”,因为STM32F4的Cortex-M4指令是向下兼容Cortex-M0的,这里可以不做修改。

添加接口文件

下载我们提供的定制开发程序模板,将System、User这两个文件夹拷贝到STM32CubeIDE项目中来:

右键点击项目,菜单中选择“刷新”命令:

分别右键点击System、User这两个目录,在目录属性对话框中,选择“C/C++ Build”选项,然后去掉“Exclude resource from build”的勾选。这样在编译项目时,会自动包含这两个目录中的文件。

为了正确的编译这两个目录中的文件,我们还需要添加C编译的预定义。在项目属性中,选择“C/C++ Build”选项的“Settings”子选项。在选项页中选择“MCU/MPU GCC Compiler”选项的“Preprocessor”子选项。在预定义符号中添加ARMv7M、ARMv7M4、STM32F4。

为了正确的编译这两个目录中的文件,我们还需要添加C头文件包含路径。在项目属性中,选择“C/C++ Build”选项的“Settings”子选项。在选项页中选择“MCU/MPU GCC Compiler”选项的“Include Paths”子选项。在路径中添加“../User”、“../System”、“../System/CommTypes”这三个路径。

另外,需要给出PLC系统固件中,系统服务函数表Fcc_exportApi的地址。在项目属性中,选择“C/C++ Build”选项的“Settings”子选项。在选项页中选择“MCU/MPU GCC Linker”选项的“Miscellaneous”子选项。在标志中添加“@../ExternSymbol.txt”。通过外部文件导入符号定义。

用Application Editor下载定制开发项目时,只接受HEX文件。因此,还需要在项目配置中添加HEX文件输出:

以上便完成了接口文件的添加并修改好了项目配置,可以编译下,看是否能够正确得到HEX文件。

如果可以看到此输出信息,说明文件导入成功。

内置算法

这里我们用定制开发程序做一个最简单的计算,每执行一次PLC主循环,将变量VW211自加1。STM32CubeIDE项目中打开User.c文件,做出如下修改:


#include "User.h"
#include "stm32f4xx_hal.h"
/* 记录当前定制开发程序被注册在那个定制端口
    如果是0xFF则说明注册不成功 */
Customized_PortType portId = 0xFF;
/* 定制程序初始化时由PLC系统固件调用,也是整个定制程序的入口 */
void USR_Init(void) {
    /* 用 (UserKey << 6) | FeatureKey 来注册定制端口 */
    portId = Customized_Enable((0x1234 << 16) | 0xABCD, (uint32)USR_Init);
    if (portId != 0xFF) {
        /* 注册成功,注册EcuM的回调 */
        EcuM_NotifyEnable(portId);
        /* 注册成功,注册BtL的回调 */
        BtL_NotifyEnable(portId);
    }
}
void EcuMNotify_APP_OnUserInit(EcuM_InitPriorityType priority) {
}
void EcuMNotify_APP_OnUserDeInit(EcuM_InitPriorityType priority) {
}
uint32_t User_loopCounter = 0;
/* 主循环的回调 */
void EcuMNotify_APP_OnLoopMainScan(void) {
    /* 读取VW211的值 */
    uint16 vl = GetU16(V, 211);
    ++vl; /* 值自加 */
    /* 自加后的值写入VW211 */
    SetU16(V, 211, vl);
    /* 运行计数 */
    ++User_loopCounter;
}
                    

编译STM32CubeIDE项目,得到HEX文件。然后打开软件Application Editor,通过任意方式(串口、以太网、CAN)连接到开发板后,选择BANK1后缀的PLC类型。在主菜单“文件”中,选择“下载(定制开发HEX)”:

选择STM32CubeIDE项目中的HEX文件,然后点击打开此文件,开始下载:

下载完成后,进入在线模式,状态表中输入变量VW211,可查看此变量的变化。如果此变量不断递增,则说明定制开发项目成功注册并运行。

使用ST-LINK调试定制开发项目

上面虽然在Application Editor中观察到了VW211的变化,但是并不能完整跟踪执行过程。对于复杂的定制开发项目,使用调试器对项目进行单步调试是非常必要的。如果需要调试STM32CubeIDE项目,首先需要完成调试配置。在主菜单“Run”的下拉命令中找到“Debug Configurations”:

找到本项目的调试配置选项,点击“调试器”选项卡,调试探头选择“ST-LINK (ST-LINK GBD Server)”,调试接口选择“SWD”:

点击“Startup”选项卡,选中唯一的文件“Customized_F4.elf”后,点击“Edit”按钮:

这里必须关闭ST-LINK的数据下载。HEX文件只能通过Application Editor下载,只有通过Application Editor的下载流程,PLC系统才会正确的记录BANK的信息,并将信息追加在BANK的最后位置。如果使用ST-LINK的数据下载,一般会破坏PLC系统需要的BANK信息,导致存放定制开发项目的BANK无法被PLC系统识别。

最后,因为原项目中main函数不会被调用,可以将自动断点由main修改成USR_Init:

到此,我们完成了调试配置。点击工具栏的“调试”命令,即可进入调试模式,一切正常的话,会停在USR_Init函数中:

USR_Init只会调用一次,我们也可以在EcuMNotify_APP_OnLoopMainScan函数中设置断点,观察此函数周期调用的情况:

也可以在表达式中输入全局变量“User_loopCounter”,观察每次断点这个值的变化。

硬件驱动

在设计器Designer中未使用的MCU硬件资源,定制开发程序可以获得完全的控制权。这里我们设计一个最简单的任务,在用户初始化时配置一个定时器,周期的触发中断。在中断中,我们对PLC变量VW221进行自加。用Designer打开开发板的配置,编译一次建立引用关系。选择蓝图页:“Core(最小系统模块)”,在蓝图页中选择模块:“McuR”。展开定时TIM,可看到,PLC系统只使用了TIM5,意味着除了TIM5,其余定时器我们的定制开发程序都可以使用。

点击蓝图页的空白处,配置中找到“Customized”,展开“Port[]”后,在“Port0”的“Interrupt[]”下面添加节点“TIM4_IRQ”。点击此节点,选择IRQ为“TIM4_IRQ”,并点击“应用修改”按钮将值写入配置。

将项目重新上传到KQD服务器,用这个新配置烧录固件到开发板,便完成了PLC系统关于中断的配置。

在STM32CubeIDE项目中,双击“Customized_F4.ioc”,打开硬件配置:

在硬件配置中,选择“TIM4”,勾选时钟为“Internal Clock”。“Parameter Settings”采用默认配置即可。

因为中断由PLC系统统一管理,中断优先级的设置和中断的开启关闭应该使用PLC系统提供的接口。因此这里需要选择“NVIC Settings”,确认下TIM4的全局中断没有被勾选。

最后保存配置文件,选择更新代码到项目。

在STM32CubeIDE项目中,打开“Core/Src/main.c”,查看自动生成的TIM4配置代码:


/**
  * @brief TIM4 Initialization Function
  * @param None
  * @retval None
  */
static void MX_TIM4_Init(void)
{
  /* USER CODE BEGIN TIM4_Init 0 */
  /* USER CODE END TIM4_Init 0 */
  TIM_ClockConfigTypeDef sClockSourceConfig = {0};
  TIM_MasterConfigTypeDef sMasterConfig = {0};
  /* USER CODE BEGIN TIM4_Init 1 */
  /* USER CODE END TIM4_Init 1 */
  htim4.Instance = TIM4;
  htim4.Init.Prescaler = 0;
  htim4.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim4.Init.Period = 65535;
  htim4.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim4.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
  if (HAL_TIM_Base_Init(&htim4) != HAL_OK)
  {
    Error_Handler();
  }
  sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
  if (HAL_TIM_ConfigClockSource(&htim4, &sClockSourceConfig) != HAL_OK)
  {
    Error_Handler();
  }
  sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
  sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
  if (HAL_TIMEx_MasterConfigSynchronization(&htim4, &sMasterConfig) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN TIM4_Init 2 */
  /* USER CODE END TIM4_Init 2 */
}
                    

这个函数“MX_TIM4_Init”是放在“main”中调用的,因为定制开发项目的主函数“main”并不执行,因此“MX_TIM4_Init”是不会被调用的。注意到“MX_TIM4_Init”被定义成“static”局部函数,如果我们需要在User.c中调用这个函数,可以用一个外部函数间接调用一下:


static void MX_TIM4_Init(void);
/* USER CODE BEGIN PFP */
void My_MX_TIM4_Init(void) {
    MX_TIM4_Init();
}
                    

在STM32CubeIDE项目中,打开“User/User.c”,对文件做如下修改:


#include "User.h"
#include "stm32f4xx_hal.h"
Customized_PortType portId = 0xFF;
void USR_Init(void) {
    /* (UserKey << 6) | FeatureKey */
    portId = Customized_Enable((0x1234 << 16) | 0xABCD, (uint32)USR_Init);
    if (portId != 0xFF) {
        EcuM_NotifyEnable(portId);
        BtL_NotifyEnable(portId);
    }
}
/* 引用Core/Src/main.c定义的htim4 */
extern TIM_HandleTypeDef htim4;
/* 此函数在HAL_TIM_IRQHandler中调用,指示TIM的CNT被更新 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
    if (htim->Instance == htim4.Instance) {
        /* 获取VW221 */
        uint16 vl = GetU16(V, 221);
        /* 值自加 */
        ++vl;
        /* 自加后的值写回VW221 */
        SetU16(V, 221, vl);
    }
}
/* 在PLC系统中注册的TIM4_IRQ的回调函数 */
void TIM4_Irq(void) {
    /* 调用HAL库的默认中断处理 */
    HAL_TIM_IRQHandler(&htim4);
}
/* 引用Core/Src/main.c定义的My_MX_TIM4_Init,转调MX_TIM4_Init,对TIM4进行配置 */
void My_MX_TIM4_Init(void);
/* 用户初始化回调,配置硬件 */
void EcuMNotify_APP_OnUserInit(EcuM_InitPriorityType priority) {
    /* 判断配置优先级,一个模块只使用一个优先级 */
    if (priority != EcuM_InitPriority_Medium) {
        return;
    }
    if (portId != 0xFF) {
        /* 调用HAL库生成的TIM4配置函数 */
        My_MX_TIM4_Init();
        /* 启动定时器并开启TIM4中断 */
        HAL_TIM_Base_Start_IT(&htim4);
        /* 调用PLC系统服务,在NIVC中打开TIM4_IRQ中断 */
        Customized_InterruptEnable(portId, 0, TIM4_Irq);
    }
}
/* 用户反初始化回调,释放硬件 */
void EcuMNotify_APP_OnUserDeInit(EcuM_InitPriorityType priority) {
    /* 判断配置优先级,一个模块只使用一个优先级 */
    if (priority != EcuM_InitPriority_Medium) {
        return;
    }
    if (portId != 0xFF) {
        /* 调用PLC系统服务,在NIVC中关闭TIM4_IRQ中断 */
        Customized_InterruptDisable(portId, 0);
        /* 调用HAL库释放TIM4 */
        HAL_TIM_Base_DeInit(&htim4);
    }
}
uint32_t User_loopCounter = 0;
void EcuMNotify_APP_OnLoopMainScan(void) {
    uint16 vl = GetU16(V, 211);
    ++vl;
    SetU16(V, 211, vl);
    ++User_loopCounter;
}
                    

在STM32CubeIDE中编译此项目,生成Customized_F4.hex后,用Application Editor将此文件下载到开发板。连接到开发板,在状态表中输入VW211和VW221,可以看到两个变量均在自增。两个变量的变化速度并不一致,因为一个在主循环中自增,一个在定时器TIM4的中断中自增:

梯形图接口

有的时候,用户希望通过梯形图指令来调用定制开发算法,或者是通过指令来控制特殊硬件。快驱动PLC系统预留了定制开发指令的接口。在下面的例子中,我们在定制条简单的算法指令。指令根据给定的PLC变量偏移和变量数量,计算这些变量的平均值并将结果返回梯形图。在设计器Designer中,点击蓝图页的空白处,配置中找到“Customized”,展开“Port[]”后,在“Port0”的“Instruction[]”下面添加节点“Avg”。点击此节点,UID输入一个随机的32位整数,并点击“应用修改”按钮将值写入配置。

将项目重新上传到KQD服务器,用这个新配置烧录固件到开发板,便完成了PLC系统关于定制指令配置。

在STM32CubeIDE项目中,打开“User/User.c”,对文件做如下修改:


/* EC30/EC40的指令回调 */
void Instruction_Avg(void) {
    /* 获取变量偏移 */
    uint16 offset = GetU16(L, 0);
    /* 获取变量数量 */
    uint16 cnt = GetU16(L, 2);
    /* 循环求和 */
    uint32 sum = 0;
    while (cnt--) {
        sum += GetU16(V, offset);
        offset += 2;
    }
    /* 平均值写结果 */
    cnt = GetU16(L, 2);
    SetU16(L, 4, sum / cnt);
}

/* 西门子全兼容的指令回调 */
Std_ReturnType Instruction_Avg_ByEcc(uint8* tempMem, Fcc_PointerParserType pointerParser) {
    /* 获取变量偏移 */
    uint16 offset = Mb_ReadHalfWord(&tempMem[0]);
    /* 获取变量数量 */
    uint16 cnt = Mb_ReadHalfWord(&tempMem[2]);
    /* 循环求和 */
    uint32 sum = 0;
    while (cnt--) {
        sum += GetU16(V, offset);
        offset += 2;
    }
    /* 平均值写结果 */
    cnt = Mb_ReadHalfWord(&tempMem[2]);
    Mb_WriteHalfWord(&tempMem[4], sum / cnt);
    return E_OK;
}

/* 用户初始化回调,配置硬件 */
void EcuMNotify_APP_OnUserInit(EcuM_InitPriorityType priority) {
    /* 判断配置优先级,一个模块只使用一个优先级 */
	if (priority != EcuM_InitPriority_Medium) {
		return;
	}
    if (portId != 0xFF) {
    	My_MX_TIM4_Init();
    	HAL_TIM_Base_Start_IT(&htim4);
        Customized_InterruptEnable(portId, 0, TIM4_Irq);
        /* 调用PLC系统服务,注册两个指令回调函数 */
        Customized_InstructionEnable(portId, 0, Instruction_Avg, Instruction_Avg_ByEcc);
    }
}
                    

在STM32CubeIDE中编译此项目,生成Customized_F4.hex后,用Application Editor将此文件下载到开发板。因为BANK1用来下载Customized_F4.hex,用来测试的梯形图只能使用BANK0。将PLC类型切换到BANK0后,在程序中创建一个子程序:

编写如下梯形图,指令的UID参数必须与蓝图中设置的UID保持一致:

修改子程序的关联变量:

在主程序中做如下调用:

在主程序中调用CalAvg子程序时,将地址偏移100写入LW0、变量数量4写入LW2。子程序调用Customized_Call指令。这个指令会查询定制开发项目是否有匹配的指令实现,最后会调用定制开发的指令实现。在定制开发项目的Instruction_Avg回调函数中,会在LW0中取地址偏移100,在LW2中取变量数量4,计算好平均值后,结果写回LW4。CalAvg子程序在返回时,将LW4作为计算结果写入VW20。将此程序下载到开发板,在状态监控表中修改VW100为100、VW102为100、VW104为100、VW106为0;便可以在VW20中得到这4个数的平均值75:

我们也可以在西门子软件STEP 7-MicroWIN SMART中进行同样的操作。为了不和EC20/EC30的程序冲突,首先在BANK0上,用Application Editor下载一个空的程序。打开西门子软件STEP 7-MicroWIN SMART后,在程序中创建一个子程序:

编写如下梯形图,首先让LD56等于16#0F40,这个值是固定的;然后让LD48等于UID;最后执行SET_ADDR。这里保持ADDR和PORT都是16#FF,这样SET_ADDR指令根据LD56的值知道是需要进行Customized_Call的内部调用。然后根据LD48的值,调用定制开发项目匹配的指令实现:

修改子程序的关联变量:

在主程序中做如下调用:

在主程序中调用CalAvg子程序时,将地址偏移100写入LW0、变量数量4写入LW2。子程序调用SET_ADDR指令进行Customized_Call的内部指令调用。这个指令会查询定制开发项目是否有匹配的指令实现,最后会调用定制开发的指令实现。在定制开发项目的Instruction_Avg_ByEcc回调函数中,会在LW0中取地址偏移100,在LW2中取变量数量4,计算好平均值后,结果写回LW4。CalAvg子程序在返回时,将LW4作为计算结果写入VW20。将此程序下载到开发板,在状态监控表中修改VW100为100、VW102为100、VW104为100、VW106为0;便可以在VW20中得到这4个数的平均值75:

接口参考

相关文件