天天看點

.NET Micro Framework動态調用C/C++底層代碼(原理篇)

.NET Micro Framework和WinCE系統不同,從應用開發角度來說,僅支援C#開發(從V4.2版本開始,才支援VB.NET開發),而不像WinCE應用開發,既可以用C#/VB.Net,也可以用EVC等工具進行C/C++開發。針對.NET Micro Framework平台由于C#等.NET語言是托管代碼,系統需要對中間語言進行解釋執行,是以運作效率上和原生的C/C++相比,效率是打了一個折扣的,這樣對一些實時性要求比較高的應用來說,是很難實作的。

如果非要用.NETMicro Framework開發一些實時性高的應用,通常的做法就是從底層移植(Porting kit)入手,專門用C/C++寫一個驅動,然後再封裝一個可供C#調用的接口,以供應用開發者調用(參見《MicroFramework Interop功能實作》)。但是這種方法,必須要熟悉.NET Micro Framework系統移植,另外手頭還必須有一套系統源碼,不僅需要熟悉C/C++,還需要熟悉C#,以需上下結合,完成相關功能。從這個角度來說,對普通開發者來說太苛刻了,不僅對技術能力要求高,開發周期長,并且還需要重新編譯固件,對原有系統進行更新。

在幾年前,我就一直考慮能否采用Windows或WinCE平台的dll動态調用的思路,來實作.NET Micro Framework動态調用C/C++代碼。是以後續也看了不少PE檔案結構的文章,還有一些編譯原理的書籍,但是由于自己知識儲備不夠,再加上該技術實作難度也比較高,一直不得其門。

在.NET MicroFramework系統移植和開發過程中,深深感受到,封裝一個專有的硬體驅動接口是一件比較麻煩的事,是以受WinCE平台上流式驅動開發(可以參見我以前寫的《我的第一個WINCE驅動》)的啟迪,我封裝了一套基于.NET Micro Framework的流式接口(目前基于這個接口,我已經開發了DHT11溫濕度子產品、超音波子產品和看門狗的驅動程式,後續将發博文一一介紹),其C#語言的接口如下:

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開發環境,并且可以基于一個簡單的流式驅動子產品工程來開發驅動。

先等一下,我們暫且先不要考慮如何搭建MDK開發環境,讓我們理一下思路,即使我們解決了開發和編譯問題,但是最重要的是——需要解決流式驅動子產品如何和宿主(也就是TinyCLR)進行互動的問題。

這裡面有兩個問題需要解決,一是宿主如何擷取流式驅動子產品的接口位址?二是流式驅動子產品如何通路宿主的資源(我們在windows或wince平台就是通過所謂的API接口,通路系統資源的)。

第一個問題,看似簡單,但是實作起來我走了不少彎路。

首先,我們很容易想到,我們把流式驅動函數接口的指針存儲到一個變量中去,如下面的代碼所示:

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++代碼的應用篇。

.NET Micro Framework動态調用C/C++底層代碼(原理篇)

MF簡介:

http://blog.csdn.net/yefanqiu/article/details/5711770

MF資料:

http://www.sky-walker.com.cn/News.asp?Id=25