HC(S)08單片機(jī)的高效C語言編程
C語言最初是為UNIX操作系統(tǒng)的開發(fā)與應(yīng)用而開發(fā)設(shè)計(jì)的,目前已經(jīng)成為一種非常流行的編程語言。 因?yàn)镃語言既有高級(jí)語言可讀性強(qiáng)和易于維護(hù)升級(jí)的特點(diǎn),又能很好地支持位運(yùn)算操作,所以C常常被稱為中級(jí)語言。另外,C語言數(shù)據(jù)類型的定義比較自由,所以用它比較容易寫出結(jié)構(gòu)化的程序。和匯編語言相比,大多數(shù)電子工程師對(duì)C語言的代碼效率更關(guān)注。他們關(guān)心的問題主要集中在RAM、ROM和堆棧空間的使用效率以及編譯器編譯優(yōu)化效率等方面。要寫出一個(gè)高效的C語言程序,工程師們必須清楚的了解嵌入式系統(tǒng)中C語言編程的特點(diǎn),掌握MCU的硬件架構(gòu)和領(lǐng)會(huì)C語句是如何轉(zhuǎn)換成匯編語句的。從臺(tái)式機(jī)轉(zhuǎn)向嵌入式系統(tǒng)編程必須先了解嵌入式系統(tǒng)的特點(diǎn)。
* 存儲(chǔ)空間有限:盡管有些MCU有外部總線可以外擴(kuò)存儲(chǔ)器,但程序越小系統(tǒng)成本就越低,所以要盡可能優(yōu)化系統(tǒng)縮減代碼,經(jīng)濟(jì)地使用RAM(包括堆棧)和ROM存儲(chǔ)空間。
* 硬件導(dǎo)向:在臺(tái)式機(jī)上常常需要一個(gè)美觀的人機(jī)交互界面,但是在嵌入式系統(tǒng)中更關(guān)注的是對(duì)器件的控制。這就需要我們不僅要掌握這些器件的特性,還要了解與MCU時(shí)鐘有關(guān)的操作(比如中斷響應(yīng)),在精準(zhǔn)的時(shí)間點(diǎn)上對(duì)通用I/O口(GPIO)操作等。某些情況下,還需要根據(jù)生成的匯編語句去計(jì)算精確的運(yùn)行時(shí)間,甚至直接用匯編語句編寫代碼。
* 特殊的處理:與臺(tái)式機(jī)系統(tǒng)不同,MCU系統(tǒng)的編程常會(huì)用到一些非標(biāo)準(zhǔn)的語法來幫助編譯器根據(jù)不同的MCU內(nèi)核編譯生成不同的代碼。例如,在HC(S)08單片機(jī)中,有一種直接頁(或者叫零頁,地址從0x00到0xFF的頁面)的尋址模式。這種尋址模式比其他尋址模式的效率要高,所以我們常常會(huì)用一些編譯器指令來告訴編譯器把常用的變量放置在零頁地址內(nèi)。另外,不同的MCU內(nèi)核有不同的中斷處理方式、不同的存儲(chǔ)模式和不同的硬件語法結(jié)構(gòu)。要充分利用MCU內(nèi)核的優(yōu)點(diǎn),我們就必須靈活的使用一些關(guān)鍵字和特定的語法。
通常來說,在嵌入式系統(tǒng)中,一個(gè)優(yōu)秀的程序員用匯編寫出的代碼的效率要比C語言寫出的代碼高。但是,用C語言更容易寫出一個(gè)集效率、可讀性和可移植性于一身的好代碼。要寫出高效的C代碼,除了程序員有豐富的經(jīng)驗(yàn)外,MCU內(nèi)核對(duì)于C語言支持的好壞也起了很重要的作用。飛思卡爾公司的HC(S)08系列單片機(jī)的內(nèi)核在這方面是比較優(yōu)秀的,它可以很高效的支持C語言的編程。
HC(S)08系列單片機(jī)的嵌入式C語言
HC08和HCS08系列單片機(jī)都是采用CPU08內(nèi)核,該內(nèi)核能很好地支持C語言編程(更準(zhǔn)確的說,HCS08用的是增強(qiáng)型內(nèi)核,對(duì)C的支持更好)。CPU08內(nèi)核中有幾種尋址模式對(duì)C的支持非常好,第一種是變址后自加一尋址模式,這種尋址模式對(duì)于查表的操作十分有效。舉例來說,采用這種尋址模式的4字節(jié)指令加上CBEQ和BRA指令可以快速的從H:X寄存器所指向的表格中找到和累加寄存器A中相同值的字節(jié)。第二種是存儲(chǔ)器到存儲(chǔ)器的尋址,這種尋址方式能有效的支持變量的賦值。在零頁內(nèi)(地址從0x00 到 0xFF)數(shù)據(jù)拷貝,只需用一句MOV指令就可以了。最后一種但也很有用的尋址模式就是堆棧指針尋址。堆棧指針尋址使得函數(shù)參數(shù)的傳遞以及函數(shù)內(nèi)局部變量的訪問變得十分容易。另外,當(dāng)中斷屏蔽不用時(shí),堆棧指針可以用作第二個(gè)變址寄存器,這對(duì)多重表格的訪問很有用。堆棧在C中的作用主要有三點(diǎn):子程序參數(shù)的傳遞、局部變量的存放和遞歸函數(shù)的調(diào)用。CPU寄存器中如果沒法存放子程序的參數(shù)(包括地址),可以把它們存放在堆棧中。CPU08內(nèi)核在硬件上不僅提供了堆棧指針,還提供了堆棧指針尋址模式,這樣可以在不通過出棧入棧操作的情況下直接提取參數(shù)值。有了這種尋址模式,也就不需要給局部變量專門開辟一段存儲(chǔ)空間了。
高效C代碼的編寫
在討論代碼優(yōu)化之前,我們先要了解以下內(nèi)容。
* 編程經(jīng)驗(yàn)—隨著程序員編程經(jīng)驗(yàn)的增長(zhǎng),優(yōu)化代碼的技術(shù)也會(huì)相應(yīng)提高。
* 對(duì)指令集映射的理解—單片機(jī)的內(nèi)核不同其架構(gòu)和特性也不相同。必須清楚C語言和匯編語句之間的映射關(guān)系,即這句C語句生成了哪幾句匯編語句。
* 對(duì)編譯器/連接器特性的了解—單片機(jī)不同其編譯器也不同,即使是同一內(nèi)核的單片機(jī),不同編譯器的代碼效率和優(yōu)化方法也是不同的。
* 清楚地認(rèn)識(shí)系統(tǒng)—除了要了解與系統(tǒng)成本相關(guān)的內(nèi)存,也要了解系統(tǒng)中其他重要的部分,比如對(duì)系統(tǒng)運(yùn)行時(shí)間和運(yùn)行速度的控制、哪些存儲(chǔ)資源有限(RAM、ROM/Flash 和堆棧等) 以及系統(tǒng)的可讀性等等。
從減少ROM、RAM和堆??臻g的消耗以及提高系統(tǒng)執(zhí)行速度的角度來說,優(yōu)化代碼的方法有許多種。這里不可能給出所有的方法,只是將一些能顯著提高代碼效率的方法羅列出來。
變量的定義
要寫出好的程序,變量起了很重要的作用,因?yàn)榇蟛糠值拇a都是和數(shù)據(jù)有關(guān)的操作。即使是在以硬件控制為主的系統(tǒng)中,變量也起了很大的作用,MCU的大部分工作是在把外部硬件(如傳感器,按鈕等)的數(shù)值讀進(jìn)來,進(jìn)行運(yùn)算處理(和存儲(chǔ))之后輸出相應(yīng)的結(jié)果,用以驅(qū)動(dòng)外圍硬件。在使用變量的時(shí)候,以下幾點(diǎn)需要注意:
(1)變量的大小
不同架構(gòu)的MCU中,數(shù)據(jù)類型的長(zhǎng)度是不同的,這對(duì)于代碼效率有很大的影響。在8位機(jī)中,例如HC(S)08系列單片機(jī),8bit數(shù)據(jù)的執(zhí)行效率是最高的,因?yàn)榇蟛糠值闹噶疃家宰止?jié)為運(yùn)算單位。在臺(tái)式機(jī)環(huán)境下,我們通常用int(整型)作為數(shù)據(jù)類型,但是int數(shù)據(jù)的長(zhǎng)度在不同的機(jī)器和編譯器中是不同的。所以,要得到高效的C語言程序,我們應(yīng)該使用類型定義(typedef)的方式規(guī)定各種數(shù)據(jù)類型的長(zhǎng)度,盡可能的采用8位數(shù)據(jù)長(zhǎng)度。例如,用uint8_t表示一個(gè)無符號(hào)8位整型數(shù)據(jù)(一個(gè)字節(jié)),用uint16_t表示一個(gè)無符號(hào)16位整型數(shù)據(jù)。在運(yùn)算表達(dá)式中,采用類型轉(zhuǎn)換方式把表達(dá)式結(jié)果值的數(shù)據(jù)長(zhǎng)度縮減到最低所需。表1給出了零頁地址內(nèi)不同數(shù)據(jù)長(zhǎng)度的兩個(gè)變量相加得到不同數(shù)據(jù)長(zhǎng)度結(jié)果所需代碼的多少。從中我們可以看出,數(shù)據(jù)類型長(zhǎng)度的選擇對(duì)于代碼效率的影響是很大的。
(2)無符號(hào)數(shù)和定點(diǎn)數(shù)
除了數(shù)據(jù)長(zhǎng)度,數(shù)據(jù)是否是有符號(hào)數(shù)也會(huì)影響代碼效率。比如兩個(gè)8位長(zhǎng)度的有符號(hào)數(shù)相加,得到一個(gè)16位長(zhǎng)度的有符號(hào)數(shù),這需要31個(gè)字節(jié)的代碼,有符號(hào)數(shù)與無符號(hào)數(shù)進(jìn)行比較運(yùn)算所需的代碼也比兩個(gè)都是無符號(hào)數(shù)運(yùn)算所需的代碼要多。對(duì)于運(yùn)算復(fù)雜、精度要求較高的場(chǎng)合,常常需要用到浮點(diǎn)運(yùn)算。如果控制器硬件上帶有浮點(diǎn)運(yùn)算單元的話,執(zhí)行起來效率會(huì)比較高。但是,大多數(shù)8位MCU只支持整數(shù)運(yùn)算。對(duì)于浮點(diǎn)運(yùn)算,既要得到精確的計(jì)算結(jié)果又不降低代碼效率的話,我們可以先把數(shù)據(jù)按比例放大,運(yùn)算結(jié)束后再按相同比例縮小。因?yàn)镠C(S)08系列單片機(jī)的乘除運(yùn)算效率很高,把浮點(diǎn)數(shù)轉(zhuǎn)成定點(diǎn)數(shù)運(yùn)算,能提高代碼效率。此外,還可以用移位的方法來替代乘除運(yùn)算,Codewarrior支持用移位來替代2的倍數(shù)的乘除運(yùn)算。當(dāng)然,是否采用移位方式由程序員自己決定。當(dāng)然,在這個(gè)過程中需要考慮是否有溢出、取整是否合理等問題,否則不但可能得到錯(cuò)誤的結(jié)果,還有可能需要大的數(shù)據(jù)長(zhǎng)度(比如32位的數(shù)據(jù))來存儲(chǔ)中間值,反而降低了代碼效率。
(3)全局變量、靜態(tài)變量和局部變量
在嵌入式系統(tǒng)中,全局變量的使用可以有效地提高代碼效率。全局變量一般會(huì)有一個(gè)固定的存儲(chǔ)位置,如果把它放在零頁地址中,代碼效率將大大提高。給零頁地址中的全局變量賦值可以采用MOV指令,只有3個(gè)字節(jié)的代碼。而給非零頁地址中的全局變量賦值就需要用LDA和STA指令,這需要5個(gè)字節(jié)的代碼。如果用局部變量,因?yàn)樗谴娣旁诙褩V械?,所以在某些情況下需要用到H:X寄存器,而把堆棧指針放到H:X寄存器中去需要4到6個(gè)字節(jié)的代碼(如果堆棧是在零頁地址內(nèi))。在全局資源有限的情況下,使用局部變量反而代碼效率更高。這里的建議是把那些要頻繁使用的或者有位操作的變量定義為全局變量放置在零頁地址內(nèi),這樣能極大的提高代碼效率。使用靜態(tài)變量也是一種非常有用的方法,可以在把變量存儲(chǔ)在全局地址范圍的同時(shí)保持代碼的可移植性和再使用性。但是,用來存放靜態(tài)變量的RAM空間不能釋放出來給其他子程序使用。
靜態(tài)函數(shù)
把函數(shù)定義成靜態(tài)函數(shù)對(duì)于提高代碼效率是很有必要的。因?yàn)槟K內(nèi)的靜態(tài)函數(shù)只能被模塊中的函數(shù)所調(diào)用,不能被模塊以外的函數(shù)調(diào)用。因此,編譯器會(huì)有意識(shí)的把靜態(tài)函數(shù)放置在靠近其調(diào)用者的地方,這樣就可以用代碼少且執(zhí)行速度快的指令去訪問靜態(tài)函數(shù)。比如用BSR(短調(diào)用指令)而不是JSR(長(zhǎng)調(diào)用指令)。BSR是雙字節(jié)指令,花費(fèi)4個(gè)總線周期;JSR指令一般占用1~3個(gè)字節(jié)(跳轉(zhuǎn)到H:X寄存器所指的地址占用一字節(jié),但把地址移入H:X寄存器需要幾個(gè)字節(jié)的代碼)和4~6個(gè)總線周期。
數(shù)組和指針
當(dāng)需要訪問一系列數(shù)據(jù)的時(shí)候,在C語言中通常使用數(shù)組或者指針的方式。用固定序號(hào)的訪問方式(如Array[0])生成的代碼最少,執(zhí)行速度也比遞增索引方式(如Array[i++])快。在有些應(yīng)用場(chǎng)合,數(shù)組指針(*(Array++))比數(shù)組具有更好的靈活性,因?yàn)樗梢蚤g接的存取數(shù)據(jù)。但是,采用數(shù)組指針的話會(huì)占用較多的ROM(額外的代碼用于指針的初始化和使用過程中)和RAM(可能需要其他指針指向數(shù)組)。數(shù)組和指針除了用于數(shù)據(jù)的存取也可用于對(duì)函數(shù)的訪問。在嵌入式系統(tǒng)中,不同情況下經(jīng)常需要調(diào)用不同的函數(shù)。例如,在通訊中要根據(jù)不同的輸入數(shù)據(jù)給出相對(duì)應(yīng)的處理和應(yīng)答。
表2給出了不同方法對(duì)ROM和RAM空間的占用情況。從中可看出“switch”方式的可讀性最強(qiáng),但在反復(fù)次數(shù)少(函數(shù)個(gè)數(shù)少)的情況下,占用的空間最大。“if”方式的可讀性較好,占用的空間也比較小。而“pointer”方式占用ROM的空間相對(duì)變化不大,但占用許多RAM空間。
存儲(chǔ)模式和零頁的使用
不同的MCU有不同的存儲(chǔ)模式。在CodeWarrior for HC(S)08 (V3.1)中,建立工程的時(shí)候有small和tiny兩種模式可供選擇:SMALL模式,如果沒有特殊的說明,所有的指針和函數(shù)地址都被假定為16位的地址,此模式中代碼和數(shù)據(jù)都被存儲(chǔ)在64k的地址空間內(nèi);TINY 模式,所有的數(shù)據(jù)包括堆棧都分配在零頁地址空間內(nèi),如果沒用關(guān)鍵字_far作特殊說明,所有數(shù)據(jù)指針都被假定為8位地址,但是代碼的地址空間仍然是64k,函數(shù)指針也仍是16位的長(zhǎng)度。
前面討論中說過,變量放在零頁地址內(nèi)生成的代碼較少,而且能有效的支持位運(yùn)算。在HC(S)08系列單片機(jī)中,外圍寄存器一般占用$00-$3F的地址空間,所以留給RAM的零頁地址空間是有限的。為了縮減生成的代碼,就要把頻繁使用的變量放在零頁內(nèi)。要根據(jù)子程序、函數(shù)參數(shù)和局部變量使用的情況,確定堆棧的使用頻率,如果頻率高就把堆棧放置在零頁地址內(nèi)。減少生成的代碼,我們也要減少子程序中的參數(shù)(因?yàn)橐玫紸和HX寄存器),把經(jīng)常使用的臨時(shí)變量定義成全局變量放在零頁地址中。當(dāng)然,全局變量是共享的,所以用的時(shí)候我們要格外小心。下面的例程中,在Calc()函數(shù)中,可以改變?nèi)肿兞縢Temp2和gTemp3的值,但不能改變變量gTemp1的值,因?yàn)橐婚_始就對(duì)子程序進(jìn)行了這個(gè)設(shè)定。
初始化的優(yōu)化
在CodeWarrior中,每個(gè)工程都有一個(gè)模板,Start-up啟動(dòng)函數(shù)已經(jīng)預(yù)先寫好,我們可以在建工程的時(shí)候選擇是否采用ANSI標(biāo)準(zhǔn)初始化程序。通常,標(biāo)準(zhǔn)初始化程序的代碼效率并不高(可以參看start08.c文件中的源程序)。為了減少生成的代碼,我們應(yīng)該采用非ANSI標(biāo)準(zhǔn)的初始化程序,由用戶自行編寫。
除了這些通用的起始程序,還需要對(duì)硬件和變量進(jìn)行初始化。盡管寄存器都有默認(rèn)值,但仍要培養(yǎng)用軟件對(duì)硬件初始化的好習(xí)慣。對(duì)于變量,最好初始值為零,因?yàn)榍蹇誖AM代碼已經(jīng)完成了這個(gè)工作。為了防止代碼臃腫,建議把相同初始值的變量歸為一組,這樣可以用循環(huán)的方式對(duì)它們進(jìn)行初始化。在優(yōu)化代碼的時(shí)候,要特別注意那些可變型volatile變量(比如寄存器),因?yàn)榫幾g器是不會(huì)對(duì)這些變量進(jìn)行優(yōu)化的。
結(jié)語
本文簡(jiǎn)述了一些優(yōu)化代碼的方法,包括變量的選擇、使用靜態(tài)類型、數(shù)組和指針的挑選、如何使用存儲(chǔ)模式和如何進(jìn)行初始化等。但是,這僅是所有方法的一部分。一個(gè)高效的C語言程序,不僅要代碼少、執(zhí)行速度快,而且要清楚、簡(jiǎn)潔、準(zhǔn)確和易注釋。此外,程序要有一個(gè)好的架構(gòu),便于移植和維護(hù)。代碼的再使用性(reuse)也是一個(gè)關(guān)鍵因素,這不在于代碼本身,而在于它能減少開發(fā)調(diào)試時(shí)間。所以說,高效的C語言程序是各種因素的綜合體,需要我們?nèi)婵剂俊?/p>
評(píng)論