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