,,
(黑河學(xué)院 計(jì)算機(jī)與信息工程學(xué)院,黑河 164300)
隨著計(jì)算機(jī)技術(shù)、網(wǎng)絡(luò)通信技術(shù)及微電子技術(shù)的迅猛發(fā)展和廣泛應(yīng)用,嵌入式技術(shù)的應(yīng)用已經(jīng)深入到當(dāng)今世界的各個(gè)角落。無(wú)論是工業(yè)現(xiàn)場(chǎng)的數(shù)據(jù)采集與測(cè)控系統(tǒng),還是數(shù)字化家用電器都從單機(jī)、局部的檢測(cè)控制,發(fā)展成為以高性能的嵌入式系統(tǒng)為核心、依靠有線/無(wú)線網(wǎng)絡(luò)、能夠進(jìn)行遠(yuǎn)程監(jiān)測(cè)和控制的復(fù)雜系統(tǒng)。許多高級(jí)的面向?qū)ο蠛兔嫦蚓W(wǎng)絡(luò)的程序設(shè)計(jì)方法大行其道,如C#和Java等漸漸成為程序設(shè)計(jì)的主流,甚至于在嵌入式系統(tǒng)中也出現(xiàn)了J2ME等先進(jìn)的程序設(shè)計(jì)技術(shù)。目前廣泛使用的網(wǎng)絡(luò)編程語(yǔ)言Java是一種適合于分布式計(jì)算的新型面向?qū)ο蟮模m應(yīng)網(wǎng)絡(luò)發(fā)展需要的程序設(shè)計(jì)語(yǔ)言。但JVM(Java虛擬機(jī))對(duì)硬件環(huán)境的要求比較高,需要較大的內(nèi)存和足夠高的運(yùn)行速度,這樣的硬件要求使設(shè)備的成本過(guò)于昂貴,從而限制了Java語(yǔ)言在低檔嵌入式系統(tǒng)中的應(yīng)用。
所以到目前為止,未能檢索到應(yīng)用于MCS-51系列之類較低檔的8位單片機(jī)的Java編譯器。而作為目前國(guó)內(nèi)使用最多,應(yīng)用最廣的嵌入式內(nèi)核,為了實(shí)現(xiàn)與Internet的連接,依然沿用傳統(tǒng)的設(shè)計(jì)方法,十分費(fèi)時(shí)費(fèi)力,所以只有極少數(shù)實(shí)力雄厚的公司和技術(shù)精湛的個(gè)人才能對(duì)其進(jìn)行開(kāi)發(fā)[1]。因此將Java語(yǔ)言引入MCS-51等廉價(jià)8位單片機(jī)的應(yīng)用設(shè)計(jì),可讓國(guó)內(nèi)嵌入式系統(tǒng)技術(shù)和家庭信息化技術(shù)跟上國(guó)外技術(shù)發(fā)展的潮流,使廣大的中小型公司和初入門(mén)的嵌入式系統(tǒng)設(shè)計(jì)者都能很方便地實(shí)現(xiàn)與網(wǎng)絡(luò)的連接,是一件非常重要和非常緊迫的工作。
本文的目標(biāo)是針對(duì)國(guó)內(nèi)外嵌入式系統(tǒng)的應(yīng)用現(xiàn)狀,在PC機(jī)的Windows環(huán)境下使用VC 6.0開(kāi)發(fā)工具,提出用編譯的方法,設(shè)計(jì)一款能直接生成MCS-51系列單片機(jī)的目標(biāo)代碼,不依賴操作系統(tǒng)和JVM的支持多線程的嵌入式Java編譯器。使設(shè)計(jì)者能用Java語(yǔ)言編程建立設(shè)備的Web頁(yè)面,使用普通的瀏覽器就可訪問(wèn)各種電器設(shè)備,實(shí)現(xiàn)遠(yuǎn)程的實(shí)時(shí)監(jiān)測(cè)和控制功能。由于Java語(yǔ)言是面向?qū)ο蠛兔嫦蚓W(wǎng)絡(luò)的,比匯編和C語(yǔ)言都更接近自然語(yǔ)言,可以降低嵌入式設(shè)計(jì)技術(shù)的難度,而MCS-51系列單片機(jī)具有最大的應(yīng)用范圍,可以實(shí)現(xiàn)成本低廉的工業(yè)環(huán)境甚至家庭電器的監(jiān)測(cè)和控制。這種基于互聯(lián)網(wǎng)的嵌入式監(jiān)控系統(tǒng)技術(shù)的普及,能使信息技術(shù)在全社會(huì)的各個(gè)方面得到更廣泛的應(yīng)用。
Java程序設(shè)計(jì)語(yǔ)言是一種面向?qū)ο蟮?構(gòu)造精美的通用程序設(shè)計(jì)語(yǔ)言,其正在迅速地發(fā)揮它的潛能,改變著軟件開(kāi)發(fā)方式,主要由以下模塊組成:Java源語(yǔ)言、Java的class文件格式、Java應(yīng)用程序接口和JVM。其中,JVM和Java class文件是任何傳統(tǒng)Java運(yùn)行環(huán)境必不可少的。Java源語(yǔ)言通過(guò)編譯器(命令工具Javac)編譯為字節(jié)碼(Class文件),它為Java源程序提供了隔離運(yùn)行環(huán)境的二進(jìn)制形式的服務(wù),這正是Java虛擬機(jī)需要的;軟件程序調(diào)用Java應(yīng)用程序接口訪問(wèn)系統(tǒng)資源,這就注定Java應(yīng)用程序接口的class庫(kù)文件與主機(jī)平臺(tái)是密切相關(guān)的。在一個(gè)平臺(tái)支持Java程序之前,必須在這個(gè)特定平臺(tái)上明確地實(shí)現(xiàn)API(應(yīng)用程序接口)的功能,API可以輕松地通過(guò)本地方法實(shí)現(xiàn)本地資源的訪問(wèn)。最后,通過(guò)Java虛擬機(jī)解釋運(yùn)行Java字節(jié)碼。Java編譯系統(tǒng)的體系結(jié)構(gòu)如圖1所示。
圖1 Java體系結(jié)構(gòu)
與傳統(tǒng)開(kāi)發(fā)語(yǔ)言相比,Java比C更具條理性,比C++更容易學(xué),彌補(bǔ)了C++中的一些弊端。但是標(biāo)準(zhǔn)Java語(yǔ)言是一門(mén)解釋型語(yǔ)言,運(yùn)行在Java虛擬機(jī)上的Java應(yīng)用程序執(zhí)行速度最快,也只能滿足ms級(jí)的軟實(shí)時(shí)需求。
雖然Sun公司不斷地改進(jìn)優(yōu)化運(yùn)行時(shí)的環(huán)境(HotSpot Java虛擬機(jī)),但是運(yùn)行速度還是不能滿足實(shí)時(shí)嵌入式系統(tǒng)的時(shí)間要求。Java語(yǔ)言的垃圾回收機(jī)制隨時(shí)都會(huì)讓系統(tǒng)停止運(yùn)行,這將導(dǎo)致時(shí)間的不可預(yù)測(cè)性,會(huì)造成嚴(yán)重的后果,嚴(yán)重違背了嵌入式系統(tǒng)的宗旨[1]。
對(duì)于嵌入式編程而言,選擇Java語(yǔ)言作為開(kāi)發(fā)工具看起來(lái)有些難以想象。傳統(tǒng)的Java技術(shù)在嵌入式系統(tǒng)應(yīng)用中有很多的不足,如動(dòng)態(tài)類加載,以及垃圾收集隨時(shí)停頓系統(tǒng)的運(yùn)行等,使得 Java技術(shù)的時(shí)間特性具有不確定性和抖動(dòng);同時(shí),Java語(yǔ)言是一門(mén)解釋型的語(yǔ)言,其性能會(huì)比C/C++編寫(xiě)的同樣程序運(yùn)行時(shí)慢許多。由于這些因素,致使Java語(yǔ)言不能在嵌入式系統(tǒng)中得到廣泛的應(yīng)用[2]。
編譯器是嵌入式技術(shù)的重要組成部分,它是運(yùn)行在主機(jī)平臺(tái)上,為另一個(gè)不同的目標(biāo)平臺(tái)生成可執(zhí)行代碼的特殊的編譯器[3]。本文編譯器的任務(wù)是將Java源程序編譯成MCS-51系列單片機(jī)的可執(zhí)行文件,其設(shè)計(jì)采用單遍掃描的編譯程序結(jié)構(gòu)。
現(xiàn)代編譯器開(kāi)發(fā)技術(shù)已經(jīng)存在許多非常標(biāo)準(zhǔn)的形式化方法,這些方法將極大地改變編譯器開(kāi)發(fā)的難度。本文運(yùn)用Windows下VC 6.0開(kāi)發(fā)工具,參照實(shí)時(shí)版jRate及Sun公司的GJC編譯系統(tǒng)[4],使用C++作為宿主語(yǔ)言,完成實(shí)時(shí)Java交叉編譯器的詞法分析器、語(yǔ)法分析器和語(yǔ)義分析器設(shè)計(jì),最后實(shí)現(xiàn)Java語(yǔ)言源程序的編譯并生成MCS-51系列單片機(jī)的目標(biāo)代碼。
詞法分析程序完成的是編譯第一階段的工作。詞法分析器的任務(wù)是對(duì)輸入的符號(hào)串形式的源程序進(jìn)行最初的加工處理,其掃描讀入高級(jí)語(yǔ)言源程序中的每個(gè)字符,識(shí)別出原程序中有獨(dú)立意義的源語(yǔ)言單詞,用某種特定的數(shù)據(jù)結(jié)構(gòu)對(duì)它的屬性予以表示和標(biāo)注,即Token碼(Token集合由詞法文法或正規(guī)文法表示)。
這些由單詞符號(hào)組成的Token碼傳遞給語(yǔ)法分析器作進(jìn)一步分析[5]。目前絕大多數(shù)的編譯器是將詞法分析程序設(shè)計(jì)成語(yǔ)法分析程序的一個(gè)子程序或者協(xié)作程序進(jìn)行實(shí)現(xiàn),當(dāng)語(yǔ)法分析部分調(diào)用詞法分析器時(shí),執(zhí)行 “取下一個(gè)Token碼”指令,詞法分析器讀取輸入字符,直到識(shí)別出下一個(gè)字符,如圖2所示。
圖2 語(yǔ)法分析器與詞法分析器的調(diào)用關(guān)系
在實(shí)時(shí)Java交叉編譯器中,詞法分析器是通過(guò)Scanner類實(shí)現(xiàn)的。在頭文件scanner.h中聲明了整型Token,識(shí)別出當(dāng)前字符是什么、關(guān)鍵字、標(biāo)識(shí)符或者是Java語(yǔ)言的符號(hào)等等。假設(shè)詞法分析器識(shí)別出當(dāng)前處理的單詞類型是標(biāo)識(shí)符,則必須要提供該標(biāo)識(shí)符的具體名字,方便將其存儲(chǔ)到符號(hào)表中;同時(shí),程序中還聲明了當(dāng)前字符的起始位置(Pos)、結(jié)束位置(endPos)和臨時(shí)記錄位置(temPos),這些都是位置記錄類Position的實(shí)例。
Position類用于存儲(chǔ)掃描器掃描到的字符位置,其line和col屬性分別表示字符所在行與列,這些內(nèi)容在源程序出現(xiàn)詞法錯(cuò)誤時(shí),用來(lái)提供錯(cuò)誤位置的相關(guān)信息,并且提供Set方法用于為Position實(shí)例賦值,Reset方法將行、列置零。
實(shí)時(shí)Java詞法分析器的初始化在Scanner的構(gòu)造函數(shù)中完成的,其主要完成將輸入文件中的字符讀到內(nèi)存的緩沖區(qū)內(nèi),預(yù)置當(dāng)前字符的位置,并且調(diào)用NextToken()方法讀取下一個(gè)Token碼,NextToken()方法也是語(yǔ)法分析器調(diào)用詞法分析器的接口。由于整個(gè)輸入文件在詞法分析器初始化時(shí)已經(jīng)讀入內(nèi)存緩沖區(qū)中,讀取字符就變得方便許多,只需移動(dòng)字符指針即可,并將該字符賦值給詞法分析器中當(dāng)前正在處理字符的整型變量ch。
具體實(shí)現(xiàn)方式見(jiàn)scanchar()方法,同時(shí)也應(yīng)維護(hù)當(dāng)前字符的位置相關(guān)信息。詞法分析器還有一個(gè)重要任務(wù)——注釋和空格的過(guò)濾,本文通過(guò)Scanner類的ScanCommentChar()和SkipComment()兩個(gè)方法實(shí)現(xiàn),它們分別表示掃描注釋標(biāo)志和跳過(guò)注釋內(nèi)容。
根據(jù)上面所述,當(dāng)讀取到一個(gè)字符時(shí),可根據(jù)不同的情況(case語(yǔ)句)調(diào)用相應(yīng)的處理函數(shù)進(jìn)行處理。本文設(shè)計(jì)的Scanner類的主要屬性如下所示:
class Scanner{
Position pos;
//當(dāng)前的token的起始位置
Position endPos;//當(dāng)前的token的結(jié)束位置
Position tmpPos;//臨時(shí)記錄用位
void ScanCommentChar();//掃描注釋標(biāo)志
void SkipComment();//跳過(guò)注釋內(nèi)容
......
void NextToken();//掃描下一個(gè)token
void OutputToken();//輸出當(dāng)前的token
};
根據(jù)Java語(yǔ)言語(yǔ)法規(guī)范,為了使方法具有確定性,本文選擇按照LL(1)文法采用遞歸下降法進(jìn)行總體語(yǔ)法分析,并結(jié)合使用自底向上分析法的算符、優(yōu)先規(guī)則處理文法符號(hào)求出其屬性值[6]。本文編譯器的語(yǔ)法/語(yǔ)義分析器是采用語(yǔ)法制導(dǎo)的翻譯模式,通過(guò)遞歸下降分析法和算符分析優(yōu)先法實(shí)現(xiàn)的。編譯器的詞法分析器、語(yǔ)法分析器、語(yǔ)義分析器及抽象語(yǔ)法樹(shù)之間的關(guān)系如圖3所示。
圖3 嵌入式Java編譯器的前端流程圖
實(shí)時(shí)Java編譯器的語(yǔ)法/語(yǔ)義分析器主要是由Syntax類完成的,該語(yǔ)法分析器的主要任務(wù)是將輸入的Token字符串通過(guò)遞歸下降分析法構(gòu)造成抽象語(yǔ)法樹(shù),同時(shí)進(jìn)行語(yǔ)義分析,遍歷語(yǔ)法樹(shù)并在語(yǔ)法樹(shù)的各結(jié)點(diǎn)處按語(yǔ)義規(guī)則進(jìn)行計(jì)算。
Syntax類的MakeTree方法實(shí)現(xiàn)了各種語(yǔ)法樹(shù)的構(gòu)造。Word結(jié)構(gòu)用于記錄每個(gè)單詞的信息包括其詳細(xì)屬性,所在源程序的行和列,以及一個(gè)存儲(chǔ)值的聯(lián)合類型。在本文中,語(yǔ)法和語(yǔ)義的檢查方法是基于語(yǔ)義數(shù)據(jù)結(jié)構(gòu)的,作者在數(shù)據(jù)結(jié)構(gòu)中定義了一個(gè)符號(hào)表——HashTable,它將源程序的單詞名稱作為關(guān)鍵字。每個(gè)名字包括兩個(gè)屬性:該字符起始位置的偏移量和名字的長(zhǎng)度。
在語(yǔ)法分析過(guò)程中,有時(shí)候會(huì)遇到很多語(yǔ)法及語(yǔ)義錯(cuò)誤。但是當(dāng)遇到錯(cuò)誤時(shí),分析過(guò)程不會(huì)停止,它會(huì)繼續(xù)完成整個(gè)程序的分析。錯(cuò)誤處理是由Syntax類中的Skip()方法實(shí)現(xiàn)的,并且通過(guò)TellError()報(bào)告錯(cuò)誤的相關(guān)信息,并寫(xiě)入記錄文件。錯(cuò)誤記錄包括語(yǔ)法錯(cuò)誤的類型,所在源程序的行與列等。本文設(shè)計(jì)的Syntax類的部分屬性如下所示:
class Syntax{
viod Skip();
void TellError(const char * reason);//報(bào)告錯(cuò)誤
……
int Id_To_Index(const char * id);//由標(biāo)識(shí)符查找索引,
//不存在返回-1
void MakeTree(); //構(gòu)造語(yǔ)法樹(shù)
};
目標(biāo)代碼生成作為實(shí)時(shí)Java編譯器的最后階段,把經(jīng)過(guò)語(yǔ)法分析或優(yōu)化后的中間代碼作為輸入,將其轉(zhuǎn)換成特定機(jī)器的機(jī)器語(yǔ)言或匯編語(yǔ)言作為輸出,這樣的轉(zhuǎn)換程序稱為代碼生成器。對(duì)于嵌入式交叉編譯器而言,代碼生成是非常復(fù)雜的過(guò)程,因?yàn)槠洳坏蕾嚫呒?jí)語(yǔ)言的特征,而且還依賴于硬件平臺(tái),與運(yùn)行在硬件平臺(tái)上的操作系統(tǒng)也有密切的關(guān)系[7]。
總體來(lái)講,代碼生成器的輸入包括前端產(chǎn)生的源程序的中間表示與符號(hào)表信息,負(fù)責(zé)存儲(chǔ)的管理,與編譯器的前端協(xié)作將符號(hào)表中的數(shù)據(jù)對(duì)象映射為運(yùn)行時(shí)存儲(chǔ)器中數(shù)據(jù)對(duì)象的地址。目標(biāo)代碼的輸出可以是多種多樣的:可重定位的機(jī)器語(yǔ)言、絕對(duì)機(jī)器語(yǔ)言和匯編代碼。
前面兩種語(yǔ)言的設(shè)計(jì)難度比較高,而且?guī)缀趺總€(gè)系統(tǒng)都有其對(duì)應(yīng)的匯編程序,因此本文選用生成匯編代碼作為代碼生成的目標(biāo)文件程序,這樣就可以避免編寫(xiě)編譯程序的目標(biāo)機(jī)器代碼部分,同時(shí)可以利用匯編器的宏功能幫助代碼生成。
本文的代碼生成過(guò)程是由語(yǔ)法分析器Syntax類的WriteAsm()函數(shù)完成的,通過(guò)該函數(shù)完成抽象語(yǔ)法樹(shù)的遍歷。程序運(yùn)行時(shí),該函數(shù)會(huì)為進(jìn)入的函數(shù)或者過(guò)程在內(nèi)存中開(kāi)辟一個(gè)棧區(qū)并且按棧的特性進(jìn)行存儲(chǔ)分配,其需要的存儲(chǔ)空間就被分配于棧頂,函數(shù)返回時(shí)釋放所占有的空間。在成功存儲(chǔ)“標(biāo)識(shí)符”等符號(hào)后,同時(shí)也將生成的匯編程序的數(shù)據(jù)段寫(xiě)入符號(hào)表,符號(hào)表用以記錄單詞符號(hào)與其索引號(hào)的對(duì)應(yīng)關(guān)系,符號(hào)的索引號(hào)用于表明相應(yīng)符號(hào)在匯編語(yǔ)言文件數(shù)據(jù)段中的地址標(biāo)號(hào)。當(dāng)符號(hào)再次出現(xiàn)時(shí),通過(guò)索引號(hào)在匯編語(yǔ)言的代碼段中引用。代碼生成程序的主要內(nèi)容如下所示,主要給出了while循環(huán)語(yǔ)句及各種運(yùn)算符的函數(shù)定義:
void Asm_WhileBegin();//循環(huán)開(kāi)始
void Asm_WhileLoop(const char * jump);
//循環(huán)判斷(jump=跳出循環(huán)的跳轉(zhuǎn)語(yǔ)句)
void Asm_WhileEnd();//循環(huán)結(jié)束
……
void Asm_Expression(struct Word * theoptr_ptr); //處理表達(dá)式
void Asm_Comment(const char * comment); //加入注釋
void WriteAsm(); //生成匯編語(yǔ)言
嵌入式Java編譯器是基于C++語(yǔ)言開(kāi)發(fā)的,在實(shí)現(xiàn)上主要包括兩部分:用戶界面和編譯器。在用戶界面方面其表現(xiàn)為一個(gè)圖形化繼承開(kāi)發(fā)環(huán)境,所有的代碼編輯和編譯都可在該環(huán)境中完成,將給出開(kāi)發(fā)平臺(tái)的新建Java應(yīng)用工程的過(guò)程,如圖4所示。
圖4 開(kāi)發(fā)平臺(tái)的新建工程界面
對(duì)移植的結(jié)果進(jìn)行測(cè)試非常重要,但是目前缺少全面有效的測(cè)試方法,本文采用的是用模擬器和實(shí)際測(cè)試版相結(jié)合的測(cè)試方法。采用模擬器可以大大地降低測(cè)試的難度,提高效率,更重要的是可以進(jìn)行跟蹤調(diào)試以幫助找到錯(cuò)誤。同時(shí),為了測(cè)試實(shí)時(shí)Java編譯器的正確性,即測(cè)試實(shí)時(shí)Java編譯器輸出的MCS-51系列單片機(jī)匯編程序是否正確,在實(shí)際測(cè)試板上運(yùn)行輸出的程序是非常必要的,只有真正在硬件機(jī)器上運(yùn)行過(guò)的程序才能確保編譯結(jié)果的正確性。測(cè)試結(jié)果分析,本文嵌入式Java編譯器已經(jīng)基本實(shí)現(xiàn)了從Java語(yǔ)言到MCS-51系列單片機(jī)匯編指令的編譯。
[1] 滕海坤.基于RTSJ的智能家居系統(tǒng)網(wǎng)關(guān)設(shè)計(jì)[J].桂林理工大學(xué)學(xué)報(bào),2011(1).
[2] 高愛(ài)玲.Java與C/C++的編譯器優(yōu)劣探討[J].信息通信,2014(3).
[3] 沈健.基于嵌入式linux系統(tǒng)交叉編譯的實(shí)現(xiàn)[J].赤子,2014(12).
[4] 朱磊.一種污水處理仿真語(yǔ)言編譯器的實(shí)現(xiàn)與應(yīng)用[J].軟件,2014,35(2).
[5] John Levine. Flex與Bison (中文版)[M].陸軍,譯.南京: 東南大學(xué)出版社,2012.
[6] Jourdan J-H, Laporte V, Blazy S,et al. A formally-verified c static analyzer In: Proc. of the POPL2015[M]. Mumbai:ACM Press, 2015.
[7] 尚書(shū).可信編譯器L2C的核心編譯步驟及其設(shè)計(jì)與實(shí)現(xiàn)[J].軟件學(xué)報(bào),2017,28(5).
[8] 周文.復(fù)雜系統(tǒng)建模仿真語(yǔ)言編譯器的實(shí)現(xiàn)與應(yīng)用[J].系統(tǒng)仿真學(xué)報(bào),2016,28(7).
單片機(jī)與嵌入式系統(tǒng)應(yīng)用2018年4期