.NET Micro Framework和WinCE系统不同,从应用开发角度来说,仅支持C#开发(从V4.2版本开始,才支持VB.NET开发),而不像WinCE应用开发,既可以用C#/VB.Net,也可以用EVC等工具进行C/C++开发。针对.NET Micro Framework平台由于C#等.NET语言是托管代码,系统需要对中间语言进行解释执行,所以运行效率上和原生的C/C++相比,效率是打了一个折扣的,这样对一些实时性要求比较高的应用来说,是很难实现的。
在几年前,我就一直考虑能否采用Windows或WinCE平台的dll动态调用的思路,来实现.NET Micro Framework动态调用C/C++代码。所以后续也看了不少PE文件结构的文章,还有一些编译原理的书籍,但是由于自己知识储备不够,再加上该技术实现难度也比较高,一直不得其门。
public sealed class GeneralStream
{
publicGeneralStream();
public event GeneralStreamEventHandlerNotice;
public int Close();
public int IOControl(intcode);
public int IOControl(intcode, int parameter);
public int IOControl(intcode, byte[] inBuffer, intinCount, byte[] outBuffer, int outCount);
public int Open(stringname);
public int Open(string name,int config);
public int Open(string name,string config);
public int Read(byte[]buffer, int offset, intcount);
public int Write(byte[]buffer, int offset, intcount);
}
比较有特色的是,还提供了一个事件通知接口,这样就为各种硬件驱动开发提供了更灵活的支持。
有了这个流式接口,一般情况下,为上层C#语言提供专有的硬件底层功能,就不需要再编写接口相关的代码了,直接写相关的C/C++代码,然后编译链接即可。
由此,我突然想到,能否把基于流式接口开发的驱动,实现动态加载。
最初我开发的流式接口和各个流式驱动是在一个项目里面的,最终会编译成一个obj文件,后来考虑到便于调试和维护,把流式接口部分和各个流式驱动分开,每一个流式驱动都是一个单独的obj文件。
又受到ER_Config和ER_DAT的启发,在编译的时候,它们可以指定编译的起始位置,并且可以独立编译成一个bin(或hex)文件,所以我们的一个流式驱动也是可以独立的编译成一个bin文件的,这样部署的时候就可以单独部署了。
由于每个流式驱动的接口都是一致的,我们自然就可以想到,这个bin文件理论上是可以替换的,比如刚开始我们加载的是A功能的流式驱动,那么我们根据需要也可以替换为B功能的流式驱动。
那用户怎么开发这种相对独立的流式驱动模块呢?
如果还是基于.NETMicro Framework整个Porting kit开发环境,那对一般开发者来说,简直就是梦魇,因为光搭建环境,熟悉环境就得花很长时间。
所以最好的办法,就是用MDK开发环境,并且可以基于一个简单的流式驱动模块工程来开发驱动。
第一个问题,看似简单,但是实现起来我走了不少弯路。
首先,我们很容易想到,我们把流式驱动函数接口的指针存储到一个变量中去,如下面的代码所示:
const IGeneralStream g_GeneralStream_UserDriver =
{
&GeneralStream_Open1_UserDriver,
&GeneralStream_Open2_UserDriver,
&GeneralStream_Close_UserDriver,
&GeneralStream_IOControl1_UserDriver,
&GeneralStream_IOControl2_UserDriver,
&GeneralStream_Read_UserDriver,
&GeneralStream_Write_UserDriver,
};
我们只要知道g_GeneralStream_UserDriver的地址,就知道各个函数接口的地址了,换句话说,我们编译的时候,其实可以指定g_GeneralStream_UserDriver变量的地址的。但是问题来了,如果我编译的时候指定g_GeneralStream_UserDriver变量的地址,我们就无法固定流式驱动模块编译的起始地址,这样我们就不知道这个编译好的bin文件该部署到什么位置。另外g_GeneralStream_UserDriver变量也无法保持在和bin文件一个相对确定的位置上去(这和实际代码的多寡都有关系),所以解决这个问题我还是颇费周折的(如果大家有更好的方法确定g_GeneralStream_UserDriver地址的方法,可以交流一下)。
第一个问题,我们算解决了,我们实现了宿主加载和调用流式驱动接口。
第二个问题,我最初的做法是绝对定位,先根据系统函数的原型声明一个函数指针,然后根据编译后的map文件,查到这个函数的绝对地址,做一个转换。如下面的代码:
typedef void(*MF_lcd_printf)(char const * format,...);
#define lcd_printf ((MF_lcd_printf)0x0805ab73)
经过这一步后,我们就可以在流式驱动接口里直接调用这个系统函数了。但是这样做有一个明显的问题,就是一旦系统固件升级(需重新编译),那么这些绝对的地址,可能会发生变化。一旦有变化,这对流式驱动来说是致命的,不仅调用失败,并且非常可能导致系统挂起(如果是windows系统,此时就是蓝屏了)。
所以,我采用了另一种方式,和流式驱动提供流式驱动接口的方式一样,系统的API接口,也定义在一个变量中,如下面的代码所示:
const IGeneralStream_Function g_GeneralStream_Function =
{
&Notice_GenerateEvent,
&lcd_printf,
&debug_printf,
&HAL_Time_Sleep_MicroSeconds_InterruptEnabled,
&CPU_GPIO_DisablePin,
&CPU_GPIO_EnableInputPin,
&CPU_GPIO_EnableOutputPin,
&CPU_GPIO_GetPinState,
&CPU_GPIO_SetPinState,
&CPU_TIMER_Initialize,
&CPU_TIMER_Uninitialize,
&CPU_TIMER_Start,
&CPU_TIMER_Stop,
&CPU_TIMER_GetState,
&CPU_TIMER_SetState,
宿主调用流式驱动接口的时候,把这个g_GeneralStream_Function地址直接传个流式驱动即可。这种方法的优点是,不受系统固件的升级影响,但是缺点也很明显,就是系统给你提供了什么接口,你才能用什么接口(其实这个时候,第一种方法仍然有效,不过可以算作黑客的做法了)。
另外值得一提的是,由于这是驱动层面的开发,驱动程序理论上可以访问系统的任何资源,所以驱动程序一定尽可能在预先为自己规划好的代码区和RAM区工作,以免对系统的稳定性造成影响。
以上思路仅仅是一个初步,我们完全也可以像PE文件一样,为流式驱动程序加上一个类PE头,把导出的函数指针和需要引用的系统API指针等等资源,填写到类PE头上去。这样系统就可以根据PE头信息,自动加载各种流式驱动,以供上层应用程序调用。
下面一张截图是MDK开发流式驱动的场景,至于如何具体编写流式驱动和使用,后续我会专门写一篇.NET Micro Framework动态加载C/C++代码的应用篇。
<a href="http://weibo.com/1804832611?s=6uyXnP"></a>