ANSIC程序到KeilC51的移植心得
李章林 張立民
(
南開大學(xué)信息技術(shù)科學(xué)學(xué)院, 天津 300071 )
摘要:文章講述了將ANSIC程序移植到KeilC51上應(yīng)該注意的事項(xiàng)。文章在總結(jié)作者使用KeilC51編寫程序和移植程序的基礎(chǔ)上,講述了存儲(chǔ)類型、指針類型、重入函數(shù)、根據(jù)目標(biāo)系統(tǒng)RAM的分布的段定位和仿真棧設(shè)置、函數(shù)指針、NULL指針問(wèn)題、字節(jié)順序、交叉匯編等移植時(shí)需要注意的事項(xiàng)。對(duì)存儲(chǔ)類型、指針類型、重入函數(shù)對(duì)程序的效率的影響進(jìn)行了分析,從而對(duì)如何進(jìn)行高效的移植給出了指導(dǎo)。最后文章以將ucosii移植到KeilC51的小模式下為實(shí)例,講述了移植的一般步驟,希望給讀者正確而高效的移植ANSIC程序到KeilC51提供參考。
關(guān)鍵字:ANSI C程序 移植 KeilC
1 引言
C語(yǔ)言是應(yīng)用很廣泛的計(jì)算機(jī)語(yǔ)言。因?yàn)樗哂泻軓?qiáng)的移植性等優(yōu)點(diǎn),在編寫單片機(jī)程序時(shí),有時(shí)系統(tǒng)的可讀性、易維護(hù)性往往比程序的效率更重要,這時(shí)候我們可以選擇C語(yǔ)言作為程序語(yǔ)言。使用C語(yǔ)言的另一個(gè)優(yōu)點(diǎn)是可以利用大量的程序資源,為X8086等CPU編寫的C程序只要稍加修改就可以拿過(guò)來(lái)用,避免了重復(fù)開發(fā)。KeilC51是51系列單片機(jī)上的優(yōu)秀的C編譯器,了解KeilC的特點(diǎn)將有利于編寫和移植高效的C51程序。
2 指定存儲(chǔ)類型,盡量使用小模式編譯
KeilC中的變量除了可以設(shè)置數(shù)據(jù)類型以外還可以設(shè)置存儲(chǔ)類型(Memory type)。對(duì)于變量常需要在data,idata,pdata和xdata這幾個(gè)存儲(chǔ)類型之間做一個(gè)選擇,它們分別將變量放在內(nèi)部RAM,間接尋址內(nèi)部RAM,用R0、R1尋址的外部RAM,用DPTR尋址的外部RAM。KeilC編譯器使用的存儲(chǔ)模式(memory
model)有小模式、緊湊模式和大模式。在各個(gè)模式下,如果變量沒(méi)有指定存儲(chǔ)類型,默認(rèn)分別對(duì)應(yīng)data、pdata、xdata存儲(chǔ)類型。四種存儲(chǔ)類型訪問(wèn)速度依次降低,但是可用空間依次增多。
稍大的C程序有較多的外部變量,如果從ANSI移植到KeilC不給變量指定存儲(chǔ)類型,那么一般只能使用大模式編譯,這樣程序速度較慢。為了能在小模式下編譯,我們可以將數(shù)據(jù)量大、訪問(wèn)量小的變量定義為xdata類型,我的做法是將所有的外部變量都定義為xdata或者pdata,局部變量不指定存儲(chǔ)類型,這樣一般能在小模式下編譯。
3 盡量使用指定存儲(chǔ)類型的指針(memory-specific
pointer)不使用一般指針(generic pointer)
如果程序移植的時(shí)候不做修改,所有的指針將都是“一般指針”,我們的建議是盡量修改為“指定存儲(chǔ)類型”的指針,因?yàn)樗男室吆芏?/span>(1)。
首先一般指針使用三個(gè)字節(jié),第一個(gè)字節(jié)指示是什么存儲(chǔ)類型,后兩個(gè)字節(jié)是指針指向的地址!爸付ù鎯(chǔ)類型”的指針則只用一個(gè)或者兩個(gè)字節(jié)(1)。可見(jiàn)“一般指針”占用內(nèi)存多。
另外,為了取得“一般指針”指向的數(shù)據(jù),程序必須調(diào)用?C?CLDPTR函數(shù),在?C?CLDPTR中根據(jù)指針第一字節(jié)指示的存儲(chǔ)類型采取不同的讀取RAM的方式。而使用“指定存儲(chǔ)類型”的指針時(shí),采取哪種讀取RAM的方式在編譯時(shí)已經(jīng)確定,不用在運(yùn)行時(shí)動(dòng)態(tài)判斷?梢(jiàn)“一般指針”運(yùn)行效率低。
“指定存儲(chǔ)類型”的指針指向的變量必須要有明確的存儲(chǔ)類型。一般情況下程序中使用指針是為了指向大塊內(nèi)存,而KeilC中大塊內(nèi)存一般定義為外部變量。依照第一點(diǎn)移植建議,所有的外部變量都定義為xdata或者pdata類型了,有明確的存儲(chǔ)類型,這說(shuō)明程序中的指針基本都可以改為“指定存儲(chǔ)類型”的指針。
4 需重入函數(shù)增加reentrant關(guān)鍵字
X8086CPU上運(yùn)行的Dos和Windows程序中的函數(shù)都是可重入函數(shù)。但是為提高效率,KeilC默認(rèn)情況下使用寄存器傳遞參數(shù),局部變量放在固定的內(nèi)存空間,這樣函數(shù)就不可重入了。如果不加修改的將ANSI程序移植到KeilC,發(fā)生不可重入函數(shù)被重入時(shí),程序運(yùn)行將出錯(cuò)。這時(shí)我們需要將可能被重入的函數(shù)后增加reentrant關(guān)鍵字(1)。
但是我們往往對(duì)需要移植的程序的流程不太了解,這樣也就不清楚哪個(gè)函數(shù)可能被重入。這里提供一個(gè)方法:首先不添加reentrant,在KeilC下編譯連接,將會(huì)有警告。如果提示“recursive
call to non-reentrant function”,說(shuō)明此函數(shù)被遞歸調(diào)用而重入;如果提示“multiple call to segment”,說(shuō)明此函數(shù)很可能是被中斷函數(shù)和非中斷函數(shù)都調(diào)用而重入。然后,在有以上警告的函數(shù)后增加reentrant關(guān)鍵字。但是以上的設(shè)置方法并不是萬(wàn)無(wú)一失,比如有函數(shù)指針存在的程序,函數(shù)調(diào)用樹(call
tree)不能反映真實(shí)調(diào)用情況;又如程序中改變壓入堆棧的程序指針,使得函數(shù)返回時(shí)不回到原來(lái)的調(diào)用點(diǎn),例如ucosii就是采用這種方式進(jìn)行任務(wù)切換,這時(shí)KeilC編譯器無(wú)法建立正確的函數(shù)調(diào)用樹,無(wú)法判斷是否被重入。
既然判斷函數(shù)是否會(huì)被重入較麻煩,為何不將所有的函數(shù)都設(shè)置為reentrant類型?為了明白這點(diǎn),我們首先要了解一下reentrant函數(shù)的執(zhí)行速度和代碼量。
為了使函數(shù)可重入,KeilC使用了仿真棧(simulated
stack),它區(qū)別于SP寄存器指向的硬件棧(hardware stack)。在大模式、緊湊模式和小模式下仿真棧分別被定義在XDATA、PDATA、IDATA空間中。仿真棧從上向下生長(zhǎng)。有一個(gè)全局變量(編譯器自動(dòng)定義的)指向棧頂,對(duì)于不同的存儲(chǔ)模式該變量分別是:?C_XBP、
?C_PBP、 ?C_IBP(1)。仿真棧的作用和Dos操作系統(tǒng)下的堆棧作用是類似的。重入函數(shù)和非重入函數(shù)運(yùn)行時(shí)的區(qū)別主要有:
|
情況 |
非重入函數(shù) |
重入函數(shù) |
|
函數(shù)參數(shù)無(wú)法全部通過(guò)寄存器傳遞時(shí) |
通過(guò)局部數(shù)據(jù)段傳遞 |
通過(guò)仿真棧傳遞 |
|
需要局部變量時(shí) |
局部變量放在局部數(shù)據(jù)段中 |
局部變量放在仿真棧中 |
|
函數(shù)返回時(shí) |
|
調(diào)整仿真棧頂 |
X0886CPU支持類似于mov eax, dword ptr [esp+20]的匯編語(yǔ)言來(lái)讀取堆棧的內(nèi)容,而51單片機(jī)沒(méi)有讀取仿真棧的配套指令,所以仿真棧的額外操作使得速度變慢、代碼量增大。如果你的移植系統(tǒng)對(duì)速度和代碼量有要求,要避免設(shè)置不必要的函數(shù)為reentrant類型。
5目標(biāo)系統(tǒng)的外部RAM起始地址影響段定位和仿真棧設(shè)置
例如你的系統(tǒng)的外部RAM為32K,而KeilC默認(rèn)情況下認(rèn)為外部RAM為64K,如果移植程序使用了超過(guò)32K的RAM,編譯器不會(huì)報(bào)錯(cuò),但是程序運(yùn)行將會(huì)出錯(cuò);又如,你的系統(tǒng)為了某種需要將RAM范圍設(shè)置為0x8000-0xFFFF,這時(shí)也需要告訴KeilC地址范圍。
設(shè)置xdata段定位的方法。例如外部RAM地址分布為0x0000-0x4000和0xC000-0xFFFF。命令行方式下使用BL51的選項(xiàng)XDATA(2):BL51 MyProgram.obj
XDATA(0x0000-0x4000,0xC000-0xFFFF)。在KeilC集成開發(fā)環(huán)境中,找到菜單project-》option for
target1-》BL51 location,在Xdata輸入框中輸入0x0000-0x4000,0xC000-0xFFFF。
設(shè)置pdata段定位的方法。如果讓pdata使用0x8000-0x80FF之間的外部RAM,在命令行方式下使用BL51的選項(xiàng)PDATA(2):BL51 MyProgram.obj PDATA(0x8000)。在集成開發(fā)環(huán)境下,找到菜單project-》option
for target1-》BL51 location,在Pdata輸入框中輸入0x8000。其中0x8000就是pdata的起始地址。還要修改Startup.a51,修改如下:
① 增加Startup.a51到工程:將KeilC\C51\LIB\Startup.a51拷貝一份到你的工作目錄下,然后添加到你的工程中。② 找到startup.a51中的
PPAGEENABLE EQU 0 ;
set to 1 if pdata object are used.
PPAGE EQU 0 ;
define PPAGE number.
修改為:
PPAGEENABLE EQU 1 ;
set to 1 if pdata object are used.
PPAGE EQU 80H ;
define PPAGE number.
初始化時(shí),PPAGE將被賦予單片機(jī)P2口寄存器,當(dāng)程序使用類似MOVX
A,@R0時(shí),高8位地址就是PPAGE的值。使用pdata類型數(shù)據(jù)時(shí),要特別注意不能隨意在程序中修改P2寄存器的值。
大模式下設(shè)置仿真棧頂。在大模式下仿真棧在xdata空間。如果外部RAM地址范圍是0x0000到0x8000。此時(shí)需要設(shè)置棧頂為0x8000,默認(rèn)情況下的(0xFFFF+1
)將會(huì)使程序出錯(cuò)。設(shè)置方法是:① 增加startup.a51。② 修改startup.a51中的部分代碼為如下代碼:
XBPSTACK EQU 1 ;
set to 1 if large reentrant is used.
XBPSTACKTOP EQU 7FFFH+1; set top of stack to highest
location+1..
緊湊模式下設(shè)置仿真棧頂。默認(rèn)的情況下為0xFF+1。但是某些時(shí)候采用默認(rèn)值會(huì)出錯(cuò)。比如pdata所有變量占用0x80字節(jié)的空間,并且你的程序中有0x80字節(jié)的xdata類型的數(shù)據(jù)。那么默認(rèn)情況下pdata數(shù)據(jù)放到0-0x007F,xdata放到0x0080-0x00FF。這時(shí)默認(rèn)的仿真棧頂在0x00FF,它和xdata數(shù)據(jù)區(qū)沖突。一個(gè)解決的辦法是將pdata段定位到xdata段的后面,例如這里將pdata段起始地址定位在0x100。
6 KeilC中的函數(shù)指針
如果被移植的程序中使用了函數(shù)指針,那么就要注意覆蓋分析的出錯(cuò)問(wèn)題(3)。問(wèn)題的產(chǎn)生在于“覆蓋分析”(overlay)技術(shù)。在小模式下編譯的C51程序局部變量都放在data空間中,為了重復(fù)利用data空間,KeilC采用了overlay技術(shù):一個(gè)程序中函數(shù)的層層調(diào)用會(huì)形成一個(gè)函數(shù)“調(diào)用樹”(call
tree),處于函數(shù)調(diào)用樹的不同樹枝上的函數(shù)可以共享一塊內(nèi)存空間(即覆蓋),這樣就節(jié)省了內(nèi)存空間的使用。KeilC能夠根據(jù)函數(shù)調(diào)用樹進(jìn)行正確的覆蓋分析。使用函數(shù)指針一般有兩種操作:①
將一個(gè)函數(shù)名賦給一個(gè)函數(shù)指針,這時(shí)KeilC誤認(rèn)為調(diào)用了這個(gè)函數(shù)名對(duì)應(yīng)的函數(shù)。② 使用函數(shù)指針調(diào)用函數(shù),這時(shí)KeilC不能發(fā)現(xiàn)調(diào)用了函數(shù)。這都使得函數(shù)調(diào)用樹出錯(cuò),由此調(diào)用樹進(jìn)行的覆蓋分析也將出錯(cuò),致使局部變量沖突,程序出錯(cuò)。對(duì)此有兩種措施:①
手動(dòng)修正調(diào)用樹:使用BL51的OVERLAY選項(xiàng)增刪調(diào)用樹的樹枝(3)。② 將通過(guò)函數(shù)指針調(diào)用的函數(shù)都設(shè)置為reentrant類型,由于reentrant類型局部變量在仿真棧中,不會(huì)引起局部變量沖突。
ANSIC中,通過(guò)函數(shù)指針調(diào)用的函數(shù)的參數(shù)的個(gè)數(shù)沒(méi)有限制,但是KeilC對(duì)此有限制,至多3個(gè)參數(shù)(3)。因?yàn)椋?span lang=EN-US>KeilC編譯時(shí),無(wú)法通過(guò)函數(shù)指針找到該函數(shù)的局部數(shù)據(jù)段,也就無(wú)法通過(guò)局部數(shù)據(jù)段傳遞參數(shù),只能通過(guò)寄存器傳遞參數(shù),所以參數(shù)個(gè)數(shù)是有限制的。碰到這個(gè)問(wèn)題時(shí)解決辦法是:①
將該函數(shù)改為reentarnt類型。② 修改源程序,將多個(gè)參數(shù)放在一個(gè)結(jié)構(gòu)體中傳遞。
7 NULL指針問(wèn)題
C程序一般規(guī)定任何變量都不能使用地址為0的內(nèi)存。但是單片機(jī)的xdata空間的0地址內(nèi)存在默認(rèn)的情況下是可以被使用的,F(xiàn)假如有內(nèi)存分配函數(shù)malloc(int
size),malloc函數(shù)成功分配了一塊0地址開始的內(nèi)存,返回首地址0,當(dāng)程序發(fā)現(xiàn)返回值等于NULL時(shí)誤認(rèn)為內(nèi)存分配失敗。為了防止以上錯(cuò)誤,我們移植時(shí)要增加以下一個(gè)全局變量:
Char xdata NULLAddr _at_ 0
這里使用了KeilC的_at_關(guān)鍵字將一個(gè)變量NULLAddr指定在0地址,從而避免了其它變量占用0地址。
8 字節(jié)順序(byte
order)
X8086等CPU在內(nèi)存中雙字節(jié)變量:高字節(jié)在高地址,低字節(jié)在低地址。KeilC51默認(rèn)雙字節(jié)變量則順序相反。字節(jié)順序引起修改的一個(gè)典型例子:TCP/IP程序中的htons()函數(shù)將主機(jī)字節(jié)順序轉(zhuǎn)化為網(wǎng)絡(luò)字節(jié)順序,對(duì)于X8086和KeilC51這個(gè)htons()函數(shù)是不同的。
9 交叉匯編
移植的時(shí)候可能還需要編寫少量的51匯編程序。匯編和C互相調(diào)用應(yīng)該遵守KeilC的參數(shù)傳遞和返回值傳遞規(guī)則(1)。為了使匯編程序也能夠進(jìn)行overlay分析,匯編的書寫要有一定的格式(1)。另外需要強(qiáng)調(diào)的一點(diǎn)是:被C程序調(diào)用的匯編函數(shù)可以使用所有的寄存器,而不用擔(dān)心會(huì)修改C程序中使用的寄存器(1)。
10 關(guān)鍵字
pdata、data等KeilC關(guān)鍵字可能被ANSIC程序中用作變量名,必須修改之。
11 實(shí)例:Ucosii到KeilC小模式下的移植
Ucosii已經(jīng)由楊屹移植到KeilC的大模式下(4),本文講述將其修改為小模式的方法。移植步驟如下:
(1)將所有的外部變量定義為xdata儲(chǔ)存類型。
(2)修改指針:查找’*’符號(hào),發(fā)現(xiàn)是指針定義的地方在’*’號(hào)前加xdata。
(3)在所有的函數(shù)申明后增加reentrant關(guān)鍵字。對(duì)Ucosii,無(wú)法用上文提到的方法判斷哪些函數(shù)可能被重入,只好全部設(shè)置為可重入函數(shù)。
(4)根據(jù)你的目標(biāo)系統(tǒng)的外部RAM起始地址定義xdata段的起始地址。下面具體講一下移植到小模式下仿真棧的使用。
在小模式下仿真棧頂默認(rèn)設(shè)置在內(nèi)部RAM空間的頂端0xFF。硬件棧頂初始值由KeilC自動(dòng)分配,實(shí)際上在決定棧頂以前KeilC先安排所有的data類型變量,然后設(shè)置SP指向空余data空間的開始。這時(shí)兩個(gè)堆棧上下相對(duì)增長(zhǎng)。對(duì)于堆棧是否會(huì)溢出,KeilC本身不提供編譯警告,只能在程序運(yùn)行時(shí)調(diào)試。
Ucosii任務(wù)棧中是否需要保存堆棧,因移植系統(tǒng)的不同而不同。① 移植到堆棧在外部RAM中的系統(tǒng)上(例如Dos)時(shí),只要保存當(dāng)前堆棧的指針就可以了。②
移植到KeilC大模式下時(shí),需要保存硬件棧的內(nèi)容和仿真棧的指針(5)。③ 移植到KeilC小模式下,需要保存硬件棧的內(nèi)容和仿真棧的內(nèi)容,它的任務(wù)棧的結(jié)構(gòu)如右圖所示。
通過(guò)?C_IBP可以知道仿真棧所在的內(nèi)部RAM區(qū)間。用以下的方法可以獲得初始硬件棧頂(4),在匯編程序中增加以下代碼:
?STACK SEGMENT IDATA
RSEG
?STACK
StkBottom:
標(biāo)號(hào)StkBottom即為硬件棧的初始棧頂。通過(guò)硬件棧大小和初始棧頂可以知道硬件棧所在內(nèi)部RAM的區(qū)間。圖中的寄存器的排列順序和KeilC在進(jìn)入中斷以后保存寄存器的順序是一致的,和中斷時(shí)寄存器壓棧順序一致是ucosii所要求的。
(5)函數(shù)指針問(wèn)題。Ucosii有任務(wù)切換,KeilC得到函數(shù)調(diào)用樹是錯(cuò)誤的。另外在main函數(shù)中一般將任務(wù)函數(shù)(例如Task1)作為參數(shù)傳遞給OSTaskCreate函數(shù),KeilC誤認(rèn)為main函數(shù)調(diào)用了Task1。由于已經(jīng)將所有的函數(shù)都申明為reentrant類型,所以沒(méi)有必要手動(dòng)修正調(diào)用樹,實(shí)際上也很難修正。
(6)NULL指針問(wèn)題。使用以上提到的方法,避免NULL指針問(wèn)題。
(7)交叉匯編。Ucosii移植的需要編譯一部分51匯編程序。
(8)關(guān)鍵字。Ucosii中使用pdata、data作為變量名,修改這些變量名(4)。
參考文獻(xiàn):
[1]德國(guó)KeilC公司 《Cx51 Compiler》http://www.keil.com 2001年5月 P103-P108,P126,P155-P158
[2]德國(guó)KeilC公司 《Macro Assembler and
Utilities for 8051 and Variants》http://www.keil.com 2000年7月 p325,p317
[3)德國(guó)KeilC公司 《Function Pointers in C51》http://www.keil.com/appnotes/files/apnt_129.pdf 1999年4月27
[4]楊屹 《uCOS51 移植心得》http://www.zlgmcu.com/philips/philips-embedsys.asp
2002年10月3 P3,P3
[5]楊屹 《uCOS51重入問(wèn)題的解決》http://www.zlgmcu.com/philips/philips-embedsys.asp
2002年10月9
了解單片機(jī)TCP/IP更多方案:http://m.wusss.cn/products_serial_server.htm
What I learned from Porting of ANSI C program to KeilC51
Li Zhanglin Zhang Limin
(.College of
Information Technology Science,
ABSTRACT:The thesis introduces what should be
noted when porting an ANSI C program to KeilC51. It explains memory type,
pointer type, reentrant function, segment locating and simulated stack setting
based on your target system, function pointer, NULL pointer issue, byte order,
cross assembly and so on about notation when porting, based on summary of the
author's programming and porting with KeilC51. The thesis gives a analysis in how memory type, pointer type,
reentrant function affect efficiency of program and give a direction on
efficient porting. Finally it illustrates porting of ucosii to KeilC small
model as a example.
Key word:ANSI C program porting KeilC