黎 建
(廣州工商學(xué)院,廣東 廣州 528138)
隨著科學(xué)技術(shù)的進(jìn)步,嵌入式系統(tǒng)已大量應(yīng)用于制造工業(yè)、過(guò)程控制、機(jī)器人、通訊、儀器儀表、汽車、船舶、航空航天、軍事裝備、消費(fèi)類產(chǎn)品等國(guó)民經(jīng)濟(jì)的主要行業(yè)[1-3]。在嵌入式產(chǎn)品日漸普及和迅速發(fā)展的今天,對(duì)嵌入式人才的需求越來(lái)越大,對(duì)嵌入式系統(tǒng)開(kāi)發(fā)人才的素質(zhì)和能力要求也越來(lái)越高。嵌入式技術(shù)人才緊缺現(xiàn)象日漸突出,眾多公司和科研院所不惜重金招聘嵌入式系統(tǒng)開(kāi)發(fā)方面的高層次人才。高校是培養(yǎng)科研人才的重要基地,是創(chuàng)造高科技產(chǎn)品的重要基地,也是推動(dòng)社會(huì)科技發(fā)展的重要力量,培養(yǎng)具備嵌入式系統(tǒng)設(shè)計(jì)能力的復(fù)合型專業(yè)人才已是迫在眉睫。然而,嵌入式技術(shù)涉及的知識(shí)面廣、實(shí)踐性強(qiáng)、技術(shù)發(fā)展快,跨越了電子、計(jì)算機(jī)、控制和通信等多個(gè)專業(yè),學(xué)習(xí)難度大[4],對(duì)嵌入式系統(tǒng)教學(xué)提出了更高要求。嵌入式C語(yǔ)言是對(duì)嵌入式系統(tǒng)開(kāi)發(fā)使用最多的語(yǔ)言,主要是由于C語(yǔ)言兼具高低級(jí)語(yǔ)言的特性,支持對(duì)硬件的直接操作[5],開(kāi)發(fā)速度快、可讀性好、工作效率高等優(yōu)勢(shì),加上高級(jí)功能的開(kāi)發(fā)必須在操作系統(tǒng)下進(jìn)行,故逐漸取代了匯編語(yǔ)言成為嵌入式系統(tǒng)的主流開(kāi)發(fā)工具。因此,在嵌入式教學(xué)中,都會(huì)使用嵌入式C語(yǔ)言來(lái)完成實(shí)驗(yàn)和實(shí)訓(xùn)項(xiàng)目。一般情況下,C語(yǔ)言課程都在大學(xué)一年級(jí)開(kāi)設(shè),是學(xué)生學(xué)習(xí)編程語(yǔ)言的入門課程。由于課時(shí)限制,學(xué)習(xí)的課程內(nèi)容有限,導(dǎo)致學(xué)生對(duì)C語(yǔ)言一知半解。在嵌入式課程實(shí)驗(yàn)中,C語(yǔ)言編寫的程序存在較多問(wèn)題,調(diào)試進(jìn)展緩慢,嚴(yán)重影響學(xué)習(xí)效率。讓學(xué)生了解嵌入式C語(yǔ)言的特點(diǎn),掌握嵌入式開(kāi)發(fā)技巧,設(shè)計(jì)出良好的軟件代碼,是教師的重要職責(zé)。
嵌入式系統(tǒng)是以應(yīng)用為中心,以計(jì)算機(jī)技術(shù)為基礎(chǔ),并且軟硬件可裁剪,適用于應(yīng)用系統(tǒng)對(duì)功能、可靠性、成本、體積、功耗有嚴(yán)格要求的專用計(jì)算機(jī)系統(tǒng)。嵌入式系統(tǒng)要求高效率、低成本,在軟件設(shè)計(jì)上,代碼的優(yōu)化尤其重要,還有硬件資源的利用。嵌入式的概念非常廣,嵌入式計(jì)算機(jī)可以從8位的51系列單片機(jī)到64位的ARM系處理器。嵌入式計(jì)算機(jī)有運(yùn)行內(nèi)存容量比通用計(jì)算機(jī)(如PC機(jī))少的特點(diǎn)(手機(jī)應(yīng)用除外),一般在100多字節(jié)到100 M字節(jié)間,因成本要求,不可能豐富。嵌入式系統(tǒng)課程都在大學(xué)三年級(jí)和四年級(jí)開(kāi)設(shè),而大學(xué)一、二年級(jí)的上機(jī)實(shí)驗(yàn)課程基本上在PC機(jī)上進(jìn)行,對(duì)硬件資源幾乎沒(méi)有約束,學(xué)生也就“大手大腳”:在程序代碼里喜歡定義和申請(qǐng)大數(shù)組,其實(shí)數(shù)據(jù)量并不大;可以使用一維數(shù)組的,卻使用二維數(shù)組;可以用char類型數(shù)組來(lái)存放的數(shù)據(jù),為圖方便,經(jīng)常使用int數(shù)組類型;本可以用一個(gè)for循環(huán)解決的問(wèn)題,卻使用多循環(huán)結(jié)構(gòu),只要結(jié)果正確就行,不管CPU耗時(shí)多少。如果嵌入式設(shè)計(jì)還是采取這樣的形式,可能導(dǎo)致項(xiàng)目失敗或成本劇增。為了糾正學(xué)生這些不良編程習(xí)慣,教師先要講解嵌入式系統(tǒng)資源的組成,不同芯片有不同資源;不同功能需求,使用不同的芯片;不同資源,有不同成本價(jià)格。設(shè)定項(xiàng)目需求、CPU類型和存儲(chǔ)容量,讓學(xué)生采用C語(yǔ)言編程實(shí)現(xiàn),通過(guò)檢查學(xué)生的代碼,了解他們對(duì)資源的使用情況、程序優(yōu)化情況以及運(yùn)行情況。通過(guò)這樣的訓(xùn)練,可以培養(yǎng)學(xué)生良好的編程習(xí)慣和嵌入式軟件編程風(fēng)格。
在嵌入式應(yīng)用領(lǐng)域,計(jì)算機(jī)運(yùn)行速度是一個(gè)非常重要的指標(biāo),在滿足功能的前提下,希望速度越快越好。在硬件一定的情況下,系統(tǒng)的執(zhí)行時(shí)間取決于編程效率和優(yōu)化處理等,而學(xué)生不一定能理解和掌握。
在嵌入式C語(yǔ)言中,內(nèi)存分配方式主要是堆和棧。棧是一種數(shù)據(jù)結(jié)構(gòu),計(jì)算機(jī)會(huì)在底層對(duì)棧提供支持,分配專門的寄存器存放棧的地址,壓棧出棧都有專門的執(zhí)行指令(如ARM系列的STMFD,LDMFD指令),這決定了棧的效率比較高,但棧空間一般都不大,很容易溢出。用malloc()分配的空間就是堆空間,編程者可以自由分配和釋放。另外,還有全局與靜態(tài)數(shù)據(jù)、常量等存放空間。顯然,由于??臻g有硬件的支持,棧內(nèi)存取數(shù)據(jù)是最快的,故存放函數(shù)的局部變量、返回值等熱數(shù)據(jù)。
下面的程序,破壞了數(shù)據(jù)訪問(wèn)局部性,運(yùn)行效率不高,執(zhí)行速度下降:
在求和過(guò)程中,變量sum要調(diào)用1 000次,是熱數(shù)據(jù)[6],應(yīng)該定義在函數(shù)內(nèi),放到??臻g里,上面程序沒(méi)有,而是作為全局變量定義。如果把sum作為靜態(tài)變量放在函數(shù)內(nèi),其效果類似于全局變量,執(zhí)行效率都受影響,因?yàn)槎叨紱](méi)有把變量定義在棧中,破壞了數(shù)據(jù)訪問(wèn)的時(shí)間局部性。
在實(shí)驗(yàn)項(xiàng)目編程中,很多學(xué)生喜歡使用C語(yǔ)言數(shù)組,覺(jué)得簡(jiǎn)單、方便,但對(duì)數(shù)組的讀寫機(jī)制并不清楚。看下面這2段程序。
程序1
程序1和2功能相同,時(shí)間空間復(fù)雜度一樣,執(zhí)行時(shí)間一樣嗎?多數(shù)同學(xué)會(huì)回答是一樣的。其實(shí),區(qū)別還是很大的,主要涉及2個(gè)方面的問(wèn)題:首先,數(shù)組分配內(nèi)存,一般是按照行優(yōu)先連續(xù)存放的,即存放順序?yàn)閍[0][0],a[0][1]…a[0][N-1],然后才是a[1][0],a[1][1]…;對(duì)于程序1,數(shù)組的讀寫,符合行優(yōu)先的規(guī)則,下個(gè)單元的讀寫,只需要地址按字增1即可,但對(duì)程序2,下個(gè)單元的讀寫,每次地址需要做+N運(yùn)算,降低了程序運(yùn)行速度。其次,高檔一點(diǎn)的嵌入式CPU,如ARM系列,為提高性能,一般都有cache,但容量不大(個(gè)別除外),如Cortex-A8處理器一級(jí)cache配置了16 kB;假定不考慮其他條件(如二級(jí)cache),對(duì)Cortex-A8處理器,使用程序1,每執(zhí)行2N次循環(huán),只需要從主內(nèi)存讀數(shù)據(jù)到cache一次(4*N*2=16 kB,int型4字節(jié)),如果使用程序2,每2次循環(huán)后,chache數(shù)據(jù)失缺[6],命中失敗,要從主內(nèi)存再讀2行數(shù)據(jù)到cache,如此類推,每執(zhí)行2N次循環(huán),需要從主內(nèi)存讀數(shù)據(jù)N次,很不合算,運(yùn)行速度大為降低。所以,一定要讓學(xué)生理解數(shù)組存放的行優(yōu)先原則(一般不會(huì)設(shè)置列優(yōu)先),還有cache的作用。當(dāng)然,如果使用指針來(lái)代替數(shù)組索引,還能加快速度。
很多嵌入式CPU(一些8位單片機(jī))并沒(méi)有乘法器硬件,做乘法運(yùn)算只能間接實(shí)現(xiàn),速度慢、效率低。其實(shí),可以通過(guò)移位操作和加法運(yùn)算來(lái)完成乘法運(yùn)算。一個(gè)二進(jìn)制數(shù)左移一位,相當(dāng)于乘2,右移一位,相當(dāng)于除2。利用這一性質(zhì),可以完成一些乘法運(yùn)算功能,而移位操作是最快的指令之一。
(1)變量X乘以2n,則直接左移n位即可。如8*X=23*X,編程可以寫成X=X<<3。
(2)變量X乘以其他數(shù),則需要進(jìn)行分解處理。如100*X,可以分解為(4+32+64)*X=(22+25+26)*X,編程同a類似,可以寫成X=(X<<2)+(X<<5)+(X<<6)。
總而言之,通過(guò)移位和加法運(yùn)算,可以完成一些主要的乘法和一些特殊的除法(右移一位相當(dāng)于除2),這對(duì)于沒(méi)有硬件乘法器的CPU,能加快運(yùn)行速度。很多學(xué)生不知道對(duì)沒(méi)有乘法器的CPU,應(yīng)該怎樣加快乘法運(yùn)算。
嵌入式C語(yǔ)言同其他語(yǔ)言一樣,有很多關(guān)鍵字,有幾個(gè)關(guān)鍵字在嵌入式軟件開(kāi)發(fā)中是很重要的,但在C語(yǔ)言課程中一般不會(huì)學(xué)到,這幾個(gè)關(guān)鍵字跟硬件有關(guān)。
interrupt不是標(biāo)準(zhǔn)的關(guān)鍵字,但在使用嵌入式C語(yǔ)言編程時(shí)普遍應(yīng)用,特別是在單片機(jī)項(xiàng)目中。用interrupt修飾的函數(shù),應(yīng)看成一個(gè)中斷處理函數(shù)(ISR)。中斷處理函數(shù)要求特殊的寄存器保存規(guī)則,以及一些特殊的返回序列。當(dāng)C代碼被中斷時(shí),ISR必須預(yù)先保存所有會(huì)被ISR用到的寄存器內(nèi)容,中斷返回時(shí),按逆順序彈出。中斷處理程序需要滿足下列要求:中斷處理程序不能有返回值,不能給中斷處理程序傳遞參數(shù),中斷處理程序應(yīng)盡量簡(jiǎn)單精煉[7]。由于學(xué)生以前沒(méi)有學(xué)過(guò)這些,往往很容易當(dāng)作普通函數(shù)對(duì)待,導(dǎo)致在實(shí)驗(yàn)中調(diào)試失敗。
volatile關(guān)鍵字在嵌入式C中頻繁使用,作用是告訴編譯器該變量是易變的,要編譯器去注意該變量的狀態(tài),變量是易變的,每次讀取該變量的值都重新從內(nèi)存中讀取。也就是說(shuō),優(yōu)化器在用到這個(gè)變量時(shí)必須每次都重新讀取這個(gè)變量的值,而不是使用保存在寄存器里的備份[8]。有幾種情況需要volatile關(guān)鍵字來(lái)修飾變量。
(1)變量值是一個(gè)特殊地址,如寄存器地址。
(2)子線程與主線程共享的全局變量。
(3)中斷處理函數(shù)(ISR)訪問(wèn)到的變量。
如果程序編譯時(shí)不做優(yōu)化處理,volatile可能看不出作用,不優(yōu)化的程序效率低下;如果做優(yōu)化處理,上面幾種情況的變量不使用volatile關(guān)鍵字,可能導(dǎo)致值不一致,如一個(gè)定時(shí)器內(nèi)的變量沒(méi)有用volatile修飾,其定時(shí)時(shí)間與計(jì)算的時(shí)間相差很大。
被regester修飾的變量,使用寄存器來(lái)存儲(chǔ)數(shù)據(jù)。由于寄存讀寫速度比內(nèi)存快一個(gè)數(shù)量級(jí)以上,對(duì)于經(jīng)常反復(fù)使用的變量,若在寄存器中讀寫,程序的執(zhí)行時(shí)間要快不少:
程序中的s、i變量每個(gè)循環(huán)都要使用,放入寄存器中能大大地縮短運(yùn)行時(shí)間。不過(guò),使用register修飾符有幾點(diǎn)限制。
(1)register變量長(zhǎng)度應(yīng)該小于或者等于整型的長(zhǎng)度(即機(jī)器的字長(zhǎng))。
(2)不能用“&”來(lái)獲取register變量的地址,寄存器不是內(nèi)存。
(3)只有局部變量和形參可以作為寄存器變量。
由于CPU中的寄存器數(shù)量有限(51系列單片機(jī)少的只有幾個(gè),而ARM系列則有幾十個(gè)),當(dāng)寄存器不夠用時(shí),編譯器會(huì)自動(dòng)忽略register修飾符。隨著編譯技術(shù)的發(fā)展,嵌入式C語(yǔ)言在優(yōu)化方面可能比程序員做得更好,在決定哪些變量應(yīng)該被存到寄存器中時(shí),可能編譯器會(huì)考慮。
在嵌入式軟件開(kāi)發(fā)過(guò)程中,經(jīng)常用到位操作,如I/O口控制、端口寄存器的設(shè)置等。大部分CISC類CPU,有位操作指令(如51系列單片機(jī)的SETB bit指令)。但是,對(duì)于RISC這類CPU,由于指令要精簡(jiǎn),沒(méi)有設(shè)計(jì)位指令。要完成對(duì)端口寄存器的設(shè)置,只能采用組合操作,不但麻煩,而且可讀性極差,學(xué)生不能理解,換一下設(shè)置要求就不會(huì)編程了,很難舉一反三。ARM系列S5PV210芯片是應(yīng)用很廣的32位CPU,有多個(gè)I/O口,既可以作為輸入口,也可以作輸出口使用,通過(guò)設(shè)置端口控制寄存器來(lái)復(fù)用,如設(shè)置GPH3的控制寄存器GPH3CON可以決定對(duì)應(yīng)的8個(gè)I/O口是作為輸入還是輸出使用。由于沒(méi)有位操作指令,要對(duì)變量(或寄存器)的某些位設(shè)置,只能通過(guò)嵌入式C語(yǔ)言的一些算術(shù)運(yùn)算和移位操作來(lái)達(dá)到。如果只是使用某個(gè)寄存器其中的某一位,其他位的定義不變,則:
這對(duì)剛進(jìn)入嵌入式領(lǐng)域的學(xué)生,上面的語(yǔ)句難以理解。
GPH3CON寄存器以4位為一組確定復(fù)用功能,0000作為輸入,0001作為輸出??梢?jiàn),GPH3需要用4×8=32位來(lái)控制8個(gè)I/O口。如果借助于C語(yǔ)言的聯(lián)合位域結(jié)構(gòu)來(lái)設(shè)置寄存器,學(xué)生就很容易理解了。這里假定,要求GPH3的0,2,4,6位作輸入,1,3,5,7位作為輸出,如設(shè)置GPH3CON寄存器的嵌入式C聯(lián)合體如下。}H3;
位域bit與gpcon共32位存儲(chǔ)單元,bit的每個(gè)成員占4位,正好作為設(shè)置GPH3CON寄存器復(fù)用功能的一組數(shù)據(jù)。
H3.gpcon=GPH3.GPH3CON;//如果H3的每個(gè)口都要設(shè)置,這一句就多余
顯然,這種設(shè)置控制寄存器的方法,學(xué)生容易理解,很快就會(huì)掌握設(shè)置方法。經(jīng)過(guò)多次練習(xí),上面寄存器設(shè)置,不再需要聯(lián)合位域,而是直接設(shè)置:GPH3.GPH3CON=0×10101010。
也可以舉一反三:如果定義輸入的4位值不是0000,是0010,則GPH3.GPH3CON=0×12121212。
隨著嵌入式技術(shù)的迅速發(fā)展,嵌入式C語(yǔ)言應(yīng)用越來(lái)越廣泛,對(duì)嵌入式人才的需求越來(lái)越大,開(kāi)設(shè)嵌入式系統(tǒng)設(shè)計(jì)課程的高校也越來(lái)越多。有資料統(tǒng)計(jì),90%以上的嵌入式系統(tǒng)應(yīng)用代碼都是采用C(或者C++)編寫的[9],掌握好C語(yǔ)言是學(xué)習(xí)嵌入式系統(tǒng)對(duì)前導(dǎo)課程的要求。由于課時(shí)限制,課程中很多內(nèi)容沒(méi)有講到,導(dǎo)致學(xué)生的C語(yǔ)言基礎(chǔ)不扎實(shí),在嵌入式課程實(shí)驗(yàn)中,往往C語(yǔ)言編寫的程序問(wèn)題較多,特別是不會(huì)優(yōu)化處理。筆者根據(jù)多年的教學(xué)經(jīng)驗(yàn),在文章中所提出的問(wèn)題,都是學(xué)生在學(xué)習(xí)嵌入式系統(tǒng)課程時(shí)經(jīng)常發(fā)生的,希望能夠起到拋磚引玉的作用,達(dá)到希望的教學(xué)目標(biāo),盡量通過(guò)程序優(yōu)化和編程技巧等方式降低前述問(wèn)題發(fā)生的概率,提升嵌入式軟件開(kāi)發(fā)的整體效率與質(zhì)量[10]。