【Codesys-Runtime】-组件的编译和核心函数分析
经过之前四篇文章的铺垫,终于可以进入纯粹的下位机组件C语言开发了。本篇算是下位机开发的入门篇,掌握了几个核心函数的功能之后,再去结合上位机IDE来讲解一些操作流程,就可以从原理上进行讨论了。所以,本篇文章主要包括如下几部分内容:1.组件编译过程分析;2.组件加载过程分析;3.组件核心函数分析;01-组件编译过程上一篇文章介绍了开发下位机组件的完整操作过程,最终得到的文件libCmpFirstTest.so 是一个动态链接库文件:组件 CmpFirstTest 目录中,参与编译过程的所有文件有:当然了,还有 SDK 目录也是非常重要的编译支撑:可以看出:编译过程使用 make 工具,因此分析编译过程的详细细节就是分析组件的 makefile 文件,以及 SDK 中的 makefile 文件。为了降低非相关代码的干扰,我们把上篇文章中的组件代码进行小小的删减。上篇文章中的组件实现了一个 IEC Library 中的函数(Function),这个是组件的一个“feature”,所以称之为外部库组件。本篇文章只讨论一个最简单组件的编译和函数,所以把这些无关的代码都删除,所以就称之为组件。我的测试代码具体操作如下:
[*]以 CmpFirstTest 组件目录为模板,复制出一个新的目录CmpTest02;
[*]把 CmpTest02 目录中的文件名改为 CmpTest02.c、CmpTest02Itf.m4,并且删除两个文件中与函数 my_add2_cext 相关的所有内容;
[*]把此目录中三个文件中所有 CmpFirstTest 字眼都修改为 CmpTest02;
[*]执行命令:make all,确保能顺利编译出 out/libCmpTest02.so 文件即可;
最终的组件目录如下:CmpTest02.c 内容几乎是空的,只包含了几个头文件;CmpTest02Itf.m4 中只包含了几个宏定义;首先分析一下 CmpTest02 目录中的 makefile 文件,删除掉注释之后的内容如下:MAJORVER=0
MINORVER=1
CMPVERSION=0x01000000
CMPID=0x2000
DOS2UNIX = dos2unix
M4 = m4
CC = gcc
#CFLAGS += -g
#INCLUDES += -I.
#LDFLAGS +=
#LDLIBS += -lc
SDKDIR=/root/codesys/ExtensionSDK
include ${SDKDIR}/makefile一共包括五部分内容:
[*]版本定义;
[*]组件ID定义;
[*]一些通用的工具命令声明;
[*]gcc编译器、编译参数等设置;
[*]声明 SDK 所在的目录,并且引用其中的 makefile文件;
可以看到,核心在于最后引入的 SDK 部分,这部分才是重点。SDK 中的 makefile 内容比较多,这里贴出其中的“骨架”部分内容:# get some info from generated files
ITFM4 = $(wildcard *Itf.m4)
CSRC = $(wildcard *.c)
CMPBASENAME=$(CSRC:.c=)
# set some variables
OUTDIR=./out
ITF = $(basename ${ITFM4})
TEMPLATEDEP = $(SDKDIR)/m4/TemplateDep.m4
DEP = ${CMPBASENAME}Dep
# set name of shared object
SONAME= lib${CMPBASENAME}.so
# set sources of component
SRC += ${CSRC} CmpFrame.c
# objects: all sources
OBJS = $(addprefix ${OUTDIR}/,$(SRC:.c=.o))
# set default includes
INCLUDES += -I${SDKDIR}/include/ -I${OUTDIR}/ -I.
CFLAGS += -fPIC -g -O0 -Wall -Wextra -Wno-unused
CFLAGS += -DDEP_H=\"${DEP}.h\"
# set a vendorID
CMPVENDORID=0xFFFF
vpath %.m4 ${SDKDIR}/cmpstub/
vpath %.c${SDKDIR}/src/
.PHONY:all
all: ${OUTDIR}/$(SONAME)
SDK_BASE=$(dir $(lastword $(MAKEFILE_LIST)))
${OUTDIR}/%.o: %.c
xxx
${OUTDIR}/${SONAME}: ${OUTDIR}/$(DEP).h ${OUTDIR}/$(ITF).h $(OBJS)
xxx
${OUTDIR}/$(DEP).m4:
xxx
${OUTDIR}/$(DEP).h: ${OUTDIR}/$(DEP).m4 $(ITF).m4
xxx
${OUTDIR}/$(ITF).h: $(ITF).m4
xxx
.PHONY:clean
clean:
xxx删减之后的内容看起来还是挺多的,完成的核心工作如下图所示:绿色:组件中参与编译的文件;蓝色:SDK中参与编译的文件;黄色:编译输出的组件(动态链接库);其它:编译过程中输出的中间文件;蓝色虚线箭头:include 头文件;黑色实体箭头:编译输入、输出文件;先抛开两个 .m4 文件,其它文件的编译过程很清晰:两个 .c 文件引用一些 .h 文件,编译得到组件。因为两个 .h 文件的内容忒过复杂,所以 Codesys 希望我们不要直接编写 .h 文件,而是编写 .m4 文件,然后通过 m4 指令来动态生成 .h 头文件,这样就降低了复杂性。对于这两个 .m4 文件,目前先不需要深究,理解它们的作用即可。CmpTest02Dep.m4:定义了 CmpTest02 这个组件中,需要依赖(调用) Runtime 中 其它哪些组件中的哪些函数;CmpTest02Itf.m4:定义了 CmpTest02 这个组件提供了哪些 API 接口函数,可以 被 Runtime 中的其它组件调用;以上是对组件编译过程的宏观理解,下面看一下在组件目录中执行 make all 指令时的输出内容:编译指令的输出过程,与上面的图示是一致的。因为组件代码 CmpTest02.c 中几乎都是空的,所以组件中的代码、数据内容,几乎都是由 SDK 中的 CmpFrame.c 文件的编译输出结果。Codesys 的 SDK 之所以把 .c 文件分开,是为了让编译过程变得更清晰;同时也间接强调了 CmpFrame.c 文件中的几个函数是与 组件架构相关的、非常重要的函数;
02-组件加载过程Codesys 的下位机组件是一种 插件机制,在 Linux 系统中的插件机制大部分都是 动态链接库文件,通过 dlopen 把插件代码加载到内存中,然后通过查找一些 固定名称的函数地址来进行调用。Codesys 的组件本质上也是如此,也有一个固定名称的入口函数。 不过 Codesys 的 组件管理器 与组件之间有更加严格的交互关系。组件中最重要的入口函数就是:DLL_DECL int CDECL ComponentEntry(INIT_STRUCT *pInitStruct)
{
pInitStruct->CmpId = COMPONENT_ID;
pInitStruct->pfExportFunctions = ExportFunctions;
pInitStruct->pfImportFunctions = ImportFunctions;
pInitStruct->pfGetVersion = CmpGetVersion;
pInitStruct->pfHookFunction = HookFunction;
pInitStruct->pfCreateInstance = NULL;
pInitStruct->pfDeleteInstance = NULL;
s_pfCMRegisterAPI = pInitStruct->pfCMRegisterAPI;
s_pfCMRegisterAPI2 = pInitStruct->pfCMRegisterAPI2;
s_pfCMGetAPI = pInitStruct->pfCMGetAPI;
s_pfCMGetAPI2 = pInitStruct->pfCMGetAPI2;
s_pfCMCallHook = pInitStruct->pfCMCallHook;
s_pfCMRegisterClass = pInitStruct->pfCMRegisterClass;
s_pfCMCreateInstance = pInitStruct->pfCMCreateInstance;
return ERR_OK;
}此函数的额代码只有区区十几行,但是已经暴露出 Codesys Runtime 中组件交互的核心机制: 通过入口函数来交换组件管理器与组件之间的一系列函数指针。所谓的组件管理器:是组件机制的核心,专门用来管理 Runtime 中各种不同类型的组件,比如:核心组件:组件管理器组件、内存池管理组件、参数设置组件、文件系统组件等;静态组件:各类通信组件、操作系统接口组件等;动态组件:在配置文件中登记的组件,例如:libCmpTest02.so;这里的静态与动态的概念, 并不是链接过程中的静态、动态的意思。而是指在 Codesys 架构中,不同组件之间的函数调用时,是 直接调用到目标函数地址(静态组件),还是通过组件管理器进行 动态的查找(动态组件)。这个概念暂时也不需要深究。简言之:组建管理器把组件 libCmpTest02.so 加载到内存之后,会调用其中的 ComponentEntry 函数,函数的入参是一个结构体,结构体成员可以分为两部分:第一部分:用来把 CmpTest02 中定义的函数地址返回给组件管理器(CM:Component Manager);第二部分:CmpTest02 中定义了几个静态函数指针变量,用来保存组件管理器中提供的几个特定函数;此函数执行之后,组件管理器与组件之间,就知道了彼此的一些函数地址,可以实现相互调用了。为了验证上述的内容,可以在 CmpFrame.c 中的每个函数开头,使用 printf 打印一些信息,来观察一下加载的过程。操作过程如下:
[*]在每个函数的开头打印:printf("----->xxx start. \n");
[*]最后一个函数 HookFunction 中,把第一个参数的值也打印一下:printf("-----> HookFunction start. ulHook = 0x%x\n", ulHook);
[*]编译:make all,把得到的动态库文件复制到 codesyscontrol 所在目录的lib 文件夹下面;
[*]在配置文件 config/CODESYSControl_User.cfg 中登记组件:Component.xxx=CmpTest02
上述步骤3和4,之前的文章已有详细介绍,此处不再赘述。关闭 codesyscontrol 进程,重新启动,观察输出结果。因为输出的内容非常多,所以可以手动来启动 codesyscontrol,并且通过 grep 指令来过滤我们打印的内容,例如:./start_codesyscontrol.sh | grep -n "start"可以看到输出结果如下:9:------> ComponentEntry start// 入口函数
10:-----> CmpGetVersion start// 读取版本
11:-----> ExportFunctions start// 导出函数
XXX 这里是非常多的重复打印信息
53:-----> HookFunction start. ulHook = 0x1// 周期调用函数
69:-----> HookFunction start. ulHook = 0x8
70:-----> ExportFunctions start
XXX 这里是非常多的重复打印信息
235:-----> ExportFunctions start
236:-----> HookFunction start. ulHook = 0x2
237:-----> HookFunction start. ulHook = 0xc8
238:-----> HookFunction start. ulHook = 0xfa
239:-----> HookFunction start. ulHook = 0x3
240:-----> HookFunction start. ulHook = 0x7
241:-----> HookFunction start. ulHook = 0x4
242:-----> HookFunction start. ulHook = 0x1f4
243:-----> ExportFunctions start
244:-----> HookFunction start. ulHook = 0x5
245:-----> HookFunction start. ulHook = 0x6
246:-----> HookFunction start. ulHook = 0x3e8
251:-----> HookFunction start. ulHook = 0x14
256:-----> HookFunction start. ulHook = 0x14
XXX 非常多的重复信息,如果不终止进程,会一直打印 HookFunction start。CmpFrame.c 中一共有 5 个函数,可以观察一下每个函数被调用的次数:
[*]ComponentEntry:1 次;
[*]ExportFunctions:N 次;
[*]ImportFunctions:1 次;
[*]CmpGetVersion:1 次;
[*]HookFunction:N 次;
ExportFunctions 函数被调用多次,是因为其它组件在查找很多不同的依赖函数时,也会到 ComTest02 组件中进行查找。例如:组件A调用了其它组件中提供的一个函数 DoSomething,组件A就会向组件管理器进行请求:我想知道 DoSomething 这个函数的地址,请你帮我找一下是哪个组件中实现了这个函数。于是组件管理器就逐个询问每一个被加载的组件。组件管理器想问询 CmpTest02 组件时,就会调用其中的 ExportFunctions 函数。因为我们的组件中没有提供这个 API 接口函数,所以组件管理器在得到否定的答复后,继续问询其它组件。最终组件B会告诉组件管理器说:我实现了这个函数,函数地址是 XXX,你把这个地址告诉组件A吧。另外一个函数 ImportFunctions 被调用 1 次,这是组件管理器给我们一个机会,问一下:你是否需要其它组件中提供的 API 接口函数?如果需要的话,我帮你查找定位一下。因为我们的组件中目前不需要调用其它组件中的函数,所以这里也就没有什么实质性工作。以上两个导出和导出函数的过程也是比较复杂的,目前了解其中的概念和流程即可。实际上,我们已经从概念和流程上介绍了组件管理器的一个核心功能,只是还没从代码层面进行讨论。言归正传,组件中的几个函数中,最重要的函数当属 HookFunction 函数了,这是 Runtime 用来控制所有的组件在加载过程中步调一致的核心函数。何为步调一致?就是让每一个组件步调一致:在不同的阶段,完成同一类事情。另外,在 ComponentEntry 函数中看到几个函数指针赋值为 NULL。只是因为当前这个组件是一个最最简单的组件,只需要这几个函数就足够了。如果以后需要写一个 IO 驱动程序组件,那么这里的 pfCreateInstance和 pfDeleteInstance 函数指针变量就不能设置为 NULL了。03-组件核心函数分析这部分内容主要就是分析 HookFunction 函数的执行逻辑。static RTS_RESULT CDECL HookFunction(RTS_UI32 ulHook, RTS_UINTPTR ulParam1, RTS_UINTPTR ulParam2)
{
printf("-----> HookFunction start. ulHook = 0x%x\n", ulHook);
return ERR_OK;
}下面这张图是从网络上截取下来的,体现了在启动和停止过程中,每一个组件在不同阶段需要完成的事情:上图中的 CH_XXX 表示不同的 Event 事件编号,其中的 INIT, EXIT, COMM_CYCLE 分别表示:启动阶段、停止阶段以及正常运行阶段。也就是说:组件管理器会调用每个组件的 HookFunction 函数,通过参数1来告知组件:目前是出于哪一个阶段。如果此事件需要参数,就通过参数2和参数3来传递。最后一个 Event 事件比较特殊 CH_COMM_CYCLE,它是被周期性调用的,目的是:每个组件可能需要定时做一些事情,那么就可以放在这个 Event 事件中来完成。但是需要注意处理过程不能耗时太长,否则就会耽搁其它组件的定时处理事情了,因为这个函数是组件管理器在一个线程中串行调用的。如果组件需要定时处理的工作的确耗时太久,那么就可以在启动阶段单独创建一个 Task 任务(线程)来实现。Codesys 中提供的 Event 事件非常多,远远不止上图中列出的那些;目的就是让每一个组件能够捕捉到(Hook)Runtime 在各种不同场景下的处理逻辑,让感兴趣的组件有机会进行一些干预或者处理。例如 Codesys 官方提供了一个示例:Runtime 准备加载用户下载的 Application 程序之前,会发出 Event 事件。此时某个组件就可以捕捉到这个事件,如果侦测到用户把 switch 开关拨到了 OFF 上,就可以反馈信息给 Runtime:不要启动 Application。
页:
[1]