ANSIC程序到KeilC51的移植心得
李章林 張立民
(
南開大學信息技術科學學院, 天津 300071 )
摘(zhai)要:文章講述了將ANSIC程(cheng)序(xu)移植(zhi)(zhi)到KeilC51上應該注意(yi)(yi)的(de)(de)事(shi)項。文章在(zai)總(zong)結作者使用KeilC51編寫程(cheng)序(xu)和移植(zhi)(zhi)程(cheng)序(xu)的(de)(de)基礎上,講述了(le)存儲(chu)類(lei)型(xing)、指(zhi)針(zhen)類(lei)型(xing)、重入函數、根據目標系統(tong)RAM的(de)(de)分(fen)(fen)布的(de)(de)段定位(wei)和仿真棧設置(zhi)、函數指(zhi)針(zhen)、NULL指(zhi)針(zhen)問題、字節順(shun)序(xu)、交叉匯編等(deng)移植(zhi)(zhi)時(shi)需要注意(yi)(yi)的(de)(de)事(shi)項。對(dui)存儲(chu)類(lei)型(xing)、指(zhi)針(zhen)類(lei)型(xing)、重入函數對(dui)程(cheng)序(xu)的(de)(de)效率的(de)(de)影響進(jin)行了(le)分(fen)(fen)析,從而對(dui)如何進(jin)行高(gao)效的(de)(de)移植(zhi)(zhi)給(gei)出了(le)指(zhi)導。最后文章以(yi)將(jiang)ucosii移植(zhi)(zhi)到KeilC51的(de)(de)小模式下為實例,講述了(le)移植(zhi)(zhi)的(de)(de)一般步驟,希望給(gei)讀者正確而高(gao)效的(de)(de)移植(zhi)(zhi)ANSIC程(cheng)序(xu)到KeilC51提供參(can)考。
關鍵字:ANSI C程序 移植 KeilC
1 引言
C語言是應用很廣泛的計算機語言。因為它具有很強的移植性等優點,在編寫單片機程序時,有時系統的可讀性、易維護性往往比程序的效率更重要,這時候我們可以選擇C語言作為程序語言。使用C語言的另一個優點是可以利用大量的程序資源,為X8086等CPU編寫的C程序只要稍加修改就可以拿過來用,避免了重復開發。KeilC51是51系列單片機上的優秀的C編譯器,了解KeilC的特點將有利于編寫和移植高效的C51程序。
2 指定存儲類型,盡量使用小模式編譯
KeilC中的變量除了可以設置數據類型以外還可以設置存儲類型(Memory type)。對于變量常需要在data,idata,pdata和xdata這幾個存儲類型之間做一個選擇,它們分別將變量放在內部RAM,間接尋址內部RAM,用R0、R1尋址的外部RAM,用DPTR尋址的外部RAM。KeilC編譯器使用的存儲模式(memory
model)有小模式、緊湊模式和大模式。在各個模式下,如果變量沒有指定存儲類型,默認分別對應data、pdata、xdata存儲類型。四種存儲類型訪問速度依次降低,但是可用空間依次增多。
稍大的C程序有較多的外部變量,如果從ANSI移植到KeilC不給變量指定存儲類型,那么一般只能使用大模式編譯,這樣程序速度較慢。為了能在小模式下編譯,我們可以將數據量大、訪問量小的變量定義為xdata類型,我的做法是將所有的外部變量都定義為xdata或者pdata,局部變量不指定存儲類型,這樣一般能在小模式下編譯。
3 盡量(liang)使用指定(ding)存儲類(lei)型(xing)的指針(zhen)(memory-specific
pointer)不使用一般指針(zhen)(generic pointer)
如果程序移植的時候不做修改,所有的指針將都是“一般指針”,我們的建議是盡量修改為“指定存儲類型”的指針,因為它的效率要高很多(1)。
首先一般指針使用三個字節,第一個字節指示是什么存儲類型,后兩個字節是指針指向的地址。“指定存儲類型”的指針則只用一個或者兩個字節(1)。可見“一般指針”占用內存多。
另外,為了取得“一般指針”指向的數據,程序必須調用?C?CLDPTR函數,在?C?CLDPTR中根據指針第一字節指示的存儲類型采取不同的讀取RAM的方式。而使用“指定存儲類型”的指針時,采取哪種讀取RAM的方式在編譯時已經確定,不用在運行時動態判斷。可見“一般指針”運行效率低。
“指定存儲類型”的指針指向的變量必須要有明確的存儲類型。一般情況下程序中使用指針是為了指向大塊內存,而KeilC中大塊內存一般定義為外部變量。依照第一點移植建議,所有的外部變量都定義為xdata或者pdata類型了,有明確的存儲類型,這說明程序中的指針基本都可以改為“指定存儲類型”的指針。
4 需重入函數增加reentrant關鍵字
X8086CPU上運行的Dos和Windows程序中的函數都是可重入函數。但是為提高效率,KeilC默認情況下使用寄存器傳遞參數,局部變量放在固定的內存空間,這樣函數就不可重入了。如果不加修改的將ANSI程序移植到KeilC,發生不可重入函數被重入時,程序運行將出錯。這時我們需要將可能被重入的函數后增加reentrant關鍵字(1)。
但是我們往往對需要移植的程序的流程不太了解,這樣也就不清楚哪個函數可能被重入。這里提供一個方法:首先不添加reentrant,在KeilC下編譯連接,將會有警告。如果提示“recursive
call to non-reentrant function”,說明此函數被遞歸調用而重入;如果提示“multiple call to segment”,說明此函數很可能是被中斷函數和非中斷函數都調用而重入。然后,在有以上警告的函數后增加reentrant關鍵字。但是以上的設置方法并不是萬無一失,比如有函數指針存在的程序,函數調用樹(call
tree)不能反映真實調用情況;又如程序中改變壓入堆棧的程序指針,使得函數返回時不回到原來的調用點,例如ucosii就是采用這種方式進行任務切換,這時KeilC編譯器無法建立正確的函數調用樹,無法判斷是否被重入。
既然判斷函數是否會被重入較麻煩,為何不將所有的函數都設置為reentrant類型?為了明白這點,我們首先要了解一下reentrant函數的執行速度和代碼量。
為了使函數可重入,KeilC使用了仿真棧(simulated
stack),它區別于SP寄存器指向的硬件棧(hardware stack)。在大模式、緊湊模式和小模式下仿真棧分別被定義在XDATA、PDATA、IDATA空間中。仿真棧從上向下生長。有一個全局變量(編譯器自動定義的)指向棧頂,對于不同的存儲模式該變量分別是:?C_XBP、
?C_PBP、 ?C_IBP(1)。仿真棧的作用和Dos操作系統下的堆棧作用是類似的。重入函數和非重入函數運行時的區別主要有:
情況 |
非重入函數 |
重入函數 |
函數參數無法全部通過寄存器傳遞時 |
通過局部數據段傳遞 |
通過仿真棧傳遞 |
需要局部變量時 |
局部變量放在局部數據段中 |
局部變量放在仿真棧中 |
函數返回時 |
|
調整仿真棧頂 |
X0886CPU支持類似于mov eax, dword ptr [esp+20]的匯編語言來讀取堆棧的內容,而51單片機沒有讀取仿真棧的配套指令,所以仿真棧的額外操作使得速度變慢、代碼量增大。如果你的移植系統對速度和代碼量有要求,要避免設置不必要的函數為reentrant類型。
5目標系統的外部RAM起始地址影響段定位和仿真棧設置
例如你的系統的外部RAM為32K,而KeilC默認情況下認為外部RAM為64K,如果移植程序使用了超過32K的RAM,編譯器不會報錯,但是程序運行將會出錯;又如,你的系統為了某種需要將RAM范圍設置為0x8000-0xFFFF,這時也需要告訴KeilC地址范圍。
設置xdata段定位的方法。例如外部RAM地址分布為0x0000-0x4000和0xC000-0xFFFF。命令行方式下使用BL51的選項XDATA(2):BL51 MyProgram.obj
XDATA(0x0000-0x4000,0xC000-0xFFFF)。在KeilC集成開發環境中,找到菜單project-》option for
target1-》BL51 location,在Xdata輸入框中輸入0x0000-0x4000,0xC000-0xFFFF。
設置pdata段定位的方法。如果讓pdata使用0x8000-0x80FF之間的外部RAM,在命令行方式下使用BL51的選項PDATA(2):BL51 MyProgram.obj PDATA(0x8000)。在集成開發環境下,找到菜單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.
初始化時,PPAGE將被賦予單片機P2口寄存器,當程序使用類似MOVX
A,@R0時,高8位地址就是PPAGE的值。使用pdata類型數據時,要特別注意不能隨意在程序中修改P2寄存器的值。
大模式下設置仿真棧頂。在大模式下仿真棧在xdata空間。如果外部RAM地址范圍是0x0000到0x8000。此時需要設置棧頂為0x8000,默認情況下的(0xFFFF+1
)將會使程序出錯。設置方法是:① 增加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..
緊湊模式下設置仿真棧頂。默認的情況下為0xFF+1。但是某些時候采用默認值會出錯。比如pdata所有變量占用0x80字節的空間,并且你的程序中有0x80字節的xdata類型的數據。那么默認情況下pdata數據放到0-0x007F,xdata放到0x0080-0x00FF。這時默認的仿真棧頂在0x00FF,它和xdata數據區沖突。一個解決的辦法是將pdata段定位到xdata段的后面,例如這里將pdata段起始地址定位在0x100。
6 KeilC中的函數指針
如果被移植的程序中使用了函數指針,那么就要注意覆蓋分析的出錯問題(3)。問題的產生在于“覆蓋分析”(overlay)技術。在(zai)(zai)小(xiao)模式下(xia)編譯的(de)(de)(de)C51程序局(ju)部變(bian)量(liang)都放在(zai)(zai)data空間中(zhong)(zhong),為了重復利用(yong)(yong)(yong)(yong)(yong)data空間,KeilC采用(yong)(yong)(yong)(yong)(yong)了overlay技術:一個程序中(zhong)(zhong)函(han)數(shu)的(de)(de)(de)層層調(diao)(diao)(diao)(diao)用(yong)(yong)(yong)(yong)(yong)會形成(cheng)一個函(han)數(shu)“調(diao)(diao)(diao)(diao)用(yong)(yong)(yong)(yong)(yong)樹(shu)”(call
tree),處于函(han)數(shu)調(diao)(diao)(diao)(diao)用(yong)(yong)(yong)(yong)(yong)樹(shu)的(de)(de)(de)不(bu)同樹(shu)枝上的(de)(de)(de)函(han)數(shu)可以共享(xiang)一塊(kuai)內(nei)存空間(即覆蓋),這(zhe)(zhe)(zhe)樣就(jiu)節省了內(nei)存空間的(de)(de)(de)使用(yong)(yong)(yong)(yong)(yong)。KeilC能夠根據函(han)數(shu)調(diao)(diao)(diao)(diao)用(yong)(yong)(yong)(yong)(yong)樹(shu)進行(xing)(xing)正(zheng)確(que)的(de)(de)(de)覆蓋分析(xi)。使用(yong)(yong)(yong)(yong)(yong)函(han)數(shu)指(zhi)針(zhen)(zhen)一般有(you)兩種(zhong)操作:①
將(jiang)一個函(han)數(shu)名賦給(gei)一個函(han)數(shu)指(zhi)針(zhen)(zhen),這(zhe)(zhe)(zhe)時(shi)KeilC誤認為調(diao)(diao)(diao)(diao)用(yong)(yong)(yong)(yong)(yong)了這(zhe)(zhe)(zhe)個函(han)數(shu)名對應的(de)(de)(de)函(han)數(shu)。② 使用(yong)(yong)(yong)(yong)(yong)函(han)數(shu)指(zhi)針(zhen)(zhen)調(diao)(diao)(diao)(diao)用(yong)(yong)(yong)(yong)(yong)函(han)數(shu),這(zhe)(zhe)(zhe)時(shi)KeilC不(bu)能發現調(diao)(diao)(diao)(diao)用(yong)(yong)(yong)(yong)(yong)了函(han)數(shu)。這(zhe)(zhe)(zhe)都使得(de)函(han)數(shu)調(diao)(diao)(diao)(diao)用(yong)(yong)(yong)(yong)(yong)樹(shu)出錯(cuo)(cuo),由此調(diao)(diao)(diao)(diao)用(yong)(yong)(yong)(yong)(yong)樹(shu)進行(xing)(xing)的(de)(de)(de)覆蓋分析(xi)也將(jiang)出錯(cuo)(cuo),致使局(ju)部變(bian)量(liang)沖突,程序出錯(cuo)(cuo)。對此有(you)兩種(zhong)措施:①
手動修正(zheng)調(diao)(diao)(diao)(diao)用(yong)(yong)(yong)(yong)(yong)樹(shu):使用(yong)(yong)(yong)(yong)(yong)BL51的(de)(de)(de)OVERLAY選項增刪(shan)調(diao)(diao)(diao)(diao)用(yong)(yong)(yong)(yong)(yong)樹(shu)的(de)(de)(de)樹(shu)枝(3)。② 將通過函數指針調用的函數都設置為reentrant類型,由于reentrant類型局部變量在仿真棧中,不會引起局部變量沖突。
ANSIC中,通過函數指針調用的函數的參數的個數沒有限制,但是KeilC對此有限制,至多3個參數(3)。因為,KeilC編譯時,無法通過函數指針找到該函數的局部數據段,也就無法通過局部數據段傳遞參數,只能通過寄存器傳遞參數,所以參數個數是有限制的。碰到這個問題時解決辦法是:①
將該函數改為reentarnt類型。② 修改源程序,將多個參數放在一個結構體中傳遞。
7 NULL指針問題
C程序一般規定任何變量都不能使用地址為0的內存。但是單片機的xdata空間的0地址內存在默認的情況下是可以被使用的。現假如有內存分配函數malloc(int
size),malloc函數成功分配了一塊0地址開始的內存,返回首地址0,當程序發現返回值等于NULL時誤認為內存分配失敗。為了防止以上錯誤,我們移植時要增加以下一個全局變量:
Char xdata NULLAddr _at_ 0
這里使用了KeilC的_at_關鍵字將一個變量NULLAddr指定在0地址,從而避免了其它變量占用0地址。
8 字節順序(byte
order)
X8086等CPU在內存中雙字節變量:高字節在高地址,低字節在低地址。KeilC51默認雙字節變量則順序相反。字節順序引起修改的一個典型例子:TCP/IP程序中的htons()函數將主機字節順序轉化為網絡字節順序,對于X8086和KeilC51這個htons()函數是不同的。
9 交叉匯編
移植的時候可能還需要編寫少量的51匯編程序。匯編和C互相調用應該遵守KeilC的參數傳遞和返回值傳遞規則(1)。為了使匯編程序也能夠進行overlay分(fen)析,匯編的書寫要有(you)一(yi)定的格(ge)式(1)。另外需要強調的一點是:被C程序調用的(de)匯(hui)編函數可(ke)以(yi)使(shi)用所有的(de)寄存(cun)器,而不用擔心會(hui)修改C程序中使(shi)用的(de)寄存(cun)器(1)。
10 關鍵字
pdata、data等KeilC關鍵字可能被ANSIC程序中用作變量名,必須修改之。
11 實例:Ucosii到KeilC小模式下的移植
Ucosii已經由楊屹移植到KeilC的大模式下(4),本文講述將其修改為小模式的方法。移植步驟如下:
(1)將所有的外部變量定義為xdata儲存類型。
(2)修改指針:查找’*’符號,發現是指針定義的地方在’*’號前加xdata。
(3)在所有的函數申明后增加reentrant關鍵字。對Ucosii,無法用上文提到的方法判斷哪些函數可能被重入,只好全部設置為可重入函數。
(4)根據你的目標系統的外部RAM起始地址定義xdata段的起始地址。下面具體講一下移植到小模式下仿真棧的使用。
在小模式下仿真棧頂默認設置在內部RAM空間的頂端0xFF。硬件棧頂初始值由KeilC自動分配,實際上在決定棧頂以前KeilC先安排所有的data類型變量,然后設置SP指向空余data空間的開始。這時兩個堆棧上下相對增長。對于堆棧是否會溢出,KeilC本身不提供編譯警告,只能在程序運行時調試。
Ucosii任務棧中(zhong)是否(fou)需要(yao)(yao)保(bao)(bao)存堆棧,因移(yi)植系統的(de)不同而不同。① 移(yi)植到堆棧在(zai)外部(bu)RAM中(zhong)的(de)系統上(例如Dos)時,只(zhi)要(yao)(yao)保(bao)(bao)存當前堆棧的(de)指針就可以了。②
移(yi)植到KeilC大(da)模式下時,需要(yao)(yao)保(bao)(bao)存硬件棧的(de)內(nei)容和仿真(zhen)棧的(de)指針(5)。③ 移植到KeilC小模式下,需要保存硬件棧的內容和仿真棧的內容,它的任務棧的結構如右圖所示。
通過?C_IBP可以知道仿真棧所在的內部RAM區間。用以下的方法可以獲得初始硬件棧頂(4),在匯編程序中增加以下代碼:
?STACK SEGMENT IDATA
RSEG
?STACK
StkBottom:
標號StkBottom即為硬件棧的初始棧頂。通過硬件棧大小和初始棧頂可以知道硬件棧所在內部RAM的區間。圖中的寄存器的排列順序和KeilC在進入中斷以后保存寄存器的順序是一致的,和中斷時寄存器壓棧順序一致是ucosii所要求的。
(5)函數指針問題。Ucosii有任務切換,KeilC得到函數調用樹是錯誤的。另外在main函數中一般將任務函數(例如Task1)作為參數傳遞給OSTaskCreate函數,KeilC誤認為main函數調用了Task1。由于已經將所有的函數都申明為reentrant類型,所以沒有必要手動修正調用樹,實際上也很難修正。
(6)NULL指針問題。使用以上提到的方法,避免NULL指針問題。
(7)交叉匯編。Ucosii移植的需要編譯一部分51匯編程序。
(8)關鍵字。Ucosii中使用pdata、data作(zuo)為變(bian)量(liang)名(ming),修改(gai)這些變(bian)量(liang)名(ming)(4)。
參考文獻:
[1]德國KeilC公司 《Cx51 Compiler》//www.keil.com 2001年5月 P103-P108,P126,P155-P158
[2]德國KeilC公司 《Macro Assembler and
Utilities for 8051 and Variants》//www.keil.com 2000年7月 p325,p317
[3)德國KeilC公司 《Function Pointers in C51》//www.keil.com/appnotes/files/apnt_129.pdf 1999年4月27
[4]楊屹 《uCOS51 移植心得》//www.zlgmcu.com/philips/philips-embedsys.asp
2002年10月3 P3,P3
[5]楊屹 《uCOS51重入問題的解決》//www.zlgmcu.com/philips/philips-embedsys.asp
2002年10月9
了解單片機TCP/IP更多方案://515x.com.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