专题介绍:基于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 的表头:
| 地址偏移 | 符号 | 说明 |
| 0 | Fcc_TableIndex_SIZE | 基础服务函数地址的数量,固定为7 |
| 4 | Fcc_TableIndex_SYS | 系统块表格的地址 |
| 8 | Fcc_TableIndex_DAT | 数据块表格的地址 |
| 12 | Fcc_TableIndex_INT | 梯形图中断函数表的地址 |
| 16 | Fcc_TableIndex_SBR | 梯形图子程序函数表的地址 |
| 20 | Fcc_TableIndex_EXT | 扩展函数表的地址 |
| 24 | Fcc_TableIndex_RTE | C运行时初始化服务函数地址 |
| 28 | Fcc_TableIndex_TargetValue | 读取设备信息函数地址 |
| 32 | Fcc_TableIndex_Marker | 扩展标记,固定为0x347D058E |
| 36 | Fcc_TableIndex_CRC32 | 此BANK有效数据的CRC32(在线监控前,上位机读取此值判断单片机BANK中的数据与上位机项目是否一致。) |
| 40 |
Fcc_TableIndex_BankType |
BANK的类型标记:
- 0xB1000000: EC30/EC40 Hex
- 0xB2000000: IEC Hex
- 0xC1000000: Custom Hex
|
| 44 | Fcc_TableIndex_IECInit | IEC初始化函数的地址 |
| 48 | Fcc_TableIndex_IECRun | IEC运行函数的地址 |
| 52 | Fcc_TableIndex_IECLookupVariables | IEC变量监控函数的地址 |
CUSTOM 定制开发程序的表头:
| 地址偏移 | 符号 | 说明 |
| 0 | CustomTableIndex_SIZE | 基础服务函数地址的数量,固定为7 |
| 4 | CustomTableIndex_R1 | 保留 |
| 8 | CustomTableIndex_R2 | 保留 |
| 12 | CustomTableIndex_R3 | 保留 |
| 16 | CustomTableIndex_R4 | 保留 |
| 20 | CustomTableIndex_R5 | 保留 |
| 24 | CustomTableIndex_R6 | 保留 |
| 28 | CustomTableIndex_R7 | 保留 |
| 32 | CustomTableIndex_Marker | 扩展标记,固定为0x347D058E |
| 36 | CustomTableIndex_CRC32 | 此BANK有效数据的CRC32(在线监控前,上位机读取此值判断单片机BANK中的数据与上位机项目是否一致。) |
| 40 |
CustomTableIndex_BankType |
BANK的类型标记:
- 0xB1000000: EC30/EC40 Hex
- 0xB2000000: IEC Hex
- 0xC1000000: Custom Hex
|
| 44 | CustomTableIndex_RTEInit | C运行时初始化服务函数地址 |
| 48 | CustomTableIndex_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;
以下是这些系统服务函数的大致说明:
| 索引 | 符号 | 说明 |
| 0 | Fcc_IdleHook | 空闲回调。在执行长任务或查询等待时,需要周期调用此接口。系统在此接口中进行事件处理,若长时间不调用此函数,所有事件会被阻塞。如果是EC30/EC40梯形图,每执行一个NETWORK,会调用一次此函数。 |
| 1 | Fcc_GetExtensionApi | 获取系统扩展的API接口表。例如TinyLcd的用户程序,通过此接口获取TinyLcd的API接口表,通过表中的TinyLcd_SetExtensionConfig服务设置LCD组态数据。 |
| 2 | Fcc_ExcuteInstruction | 调用系统提供的梯形图指令。 |
| 3 | Fcc_WakeupEvent | 激活某个事件。 |
| 4 | Fcc_GetTimeElapsed | 按照定时器编号,获取定时器的时间增量。 |
| 5 | Fcc_SetNonFatalError | 设置非致命错误号。 |
| 6 | Fcc_SetFatalError | 设置致命错误号。 |
| 7 | Os_SuspendAllInterrupts | 开启中断保护。 |
| 8 | Os_ResumeAllInterrupts | 关闭中断保活。 |
| 9 | VersionMarker | 扩展标记,固定为0xDC4D1E00,有此标记说明后续函数服务存在。没有此标记说明是V1.2.0之前的固件,并不支持定制开发。 |
| 10 | Fcc_IECMonitorPOUEnter | IEC过程量监控:通知系统,IEC程序开始执行某POU。 |
| 11 | Fcc_IECMonitorPOULeave | IEC过程量监控:通知系统,IEC程序退出执行某POU。 |
| 12 | Fcc_IECMonitorValue | IEC过程量监控:通知系统,IEC程序在某位置读写了某值。 |
| 13 | Fcc_IECVariableMatched | IEC全局变量监控:通知系统,某变量命中设定的读写范围。 |
| 14 | Fcc_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:

接口参考
相关文件