51單片機(jī)的仿真棧(模擬棧/可重入棧)
51單片機(jī)的仿真棧(又叫模擬棧、或者可重入棧)。
首先來(lái)看,51的系統(tǒng)棧(又叫系統(tǒng)棧,或者硬件棧),就是SP所指向的棧,他是一個(gè)滿(mǎn)增棧(注釋1),位于片內(nèi)RAM的128 bytes之中,上電之后系統(tǒng)堆棧指針SP的初值等于多少呢?這個(gè)要從51的啟動(dòng)文件來(lái)分析,啟動(dòng)文件中有這樣的匯編代碼:
?STACK SEGMENT IDATA ;定義一個(gè)片內(nèi)數(shù)據(jù)段,段名:?STACK
RSEG ?STACK ;選擇之前定義過(guò)的一個(gè)可重定位的段?STACK,下面的匯編語(yǔ)句將會(huì)被放置到該段,直到遇到下一個(gè)段定位指令,例如CSEG/RSEG。
DS 1 ;預(yù)留存儲(chǔ)區(qū)命令。聲明先占用一個(gè)字節(jié)的空間,在編譯時(shí),這個(gè)預(yù)留的空間不會(huì)被其他變量所使用。在這里的意義是,給硬件棧分配1個(gè)byte(實(shí)際這樣是有問(wèn)題的,應(yīng)該為硬件棧預(yù)留更多空間)
還有:
MOV SP,#?STACK-1
由上可見(jiàn),SP被初始化為#?STACK-1,在#?STACK地址處,DS指令預(yù)留了N個(gè)字節(jié)的空間,這些空間就是硬件棧的空間
但啟動(dòng)文件的代碼中,DS 1相當(dāng)于只給硬件棧預(yù)留了1個(gè)字節(jié),這實(shí)際上會(huì)出問(wèn)題,原因如下:片內(nèi)RAM中會(huì)有多個(gè)數(shù)據(jù)段,只要使用XX SEGMENT IDATA指令即可在片內(nèi)RAM中聲明一個(gè)數(shù)據(jù)段XX,如果整個(gè)工程程序中,聲明了多個(gè)數(shù)據(jù)段,?STACK數(shù)據(jù)段就只是片內(nèi)RAM中眾多數(shù)據(jù)段中的一個(gè),如果只給?STACK段預(yù)留1個(gè)字節(jié),而?STACK數(shù)據(jù)段后面又有別的數(shù)據(jù)段,那么我們的硬件棧就只有1個(gè)字節(jié)了,一旦發(fā)生中斷,CPU寄存器自動(dòng)入棧立即導(dǎo)致棧溢出,溢出后踩了別的變量的內(nèi)存,程序基本崩潰;對(duì)于這個(gè)問(wèn)題,keil是這樣處理的:keil在鏈接階段總是把?STACK數(shù)據(jù)段鏈接為片內(nèi)RAM中的最后一個(gè)數(shù)據(jù)段,即使我們只給他預(yù)留了1個(gè)字節(jié),那也不要緊,反正該段后面沒(méi)有別的變量占用,只要SP別超出0X7F(片內(nèi)RAM地址的上限)就行了。通過(guò)觀(guān)察.m51(map文件)我們發(fā)現(xiàn),keil確實(shí)是把?STACK數(shù)據(jù)段放到了片內(nèi)RAM的最后,下面是某個(gè)51工程生成的map文件摘抄:
* * * * * * * D A T A M E M O R Y * * * * * * *
REG 0000H 0008H ABSOLUTE "REG BANK 0"
DATA 0008H 0002H UNIT ?C?LIB_DATA
IDATA 000AH 000DH UNIT ?ID?UCOS_II
0017H 0009H *** GAP ***
BIT 0020H.0 0000H.1 UNIT ?BI?SERIAL
0020H.1 0000H.7 *** GAP ***
IDATA 0021H 0041H UNIT ?STACK ; 作者注:就是這一行!
* * * * * * * X D A T A M E M O R Y * * * * * * *
XDATA 0000H 080EH UNIT ?XD?SERIAL
XDATA 080EH 0804H UNIT ?XD?MAIN
XDATA 1012H 0490H UNIT ?XD?UCOS_II
XDATA 14A2H 005CH UNIT _XDATA_GROUP_
為避免系統(tǒng)棧不夠用,一個(gè)比較穩(wěn)妥的辦法就是,用匯編指令DS給?STACK數(shù)據(jù)段預(yù)留更多的空間,上面這個(gè)51工程中在另一個(gè)匯編文件中又給?STACK數(shù)據(jù)留出了40H個(gè)字節(jié),這樣總共就有41H個(gè)字節(jié)了。這樣做的好處是可以在編譯鏈接階段即可排查堆棧錯(cuò)誤,舉個(gè)例子: 假設(shè)片內(nèi)RAM中的數(shù)據(jù)段有很多,以至于,除了?STACK數(shù)據(jù)段之外,片內(nèi)RAM只剩2個(gè)字節(jié)了,而?STACK數(shù)據(jù)段我們只默認(rèn)采用了啟動(dòng)文件中的配置預(yù)留一個(gè)字節(jié),這樣編譯沒(méi)有任何問(wèn)題,keil給編譯通過(guò)了,但是運(yùn)行過(guò)程中系統(tǒng)棧只有2個(gè)字節(jié),肯定是分分鐘就發(fā)生棧溢出,然后崩潰;假設(shè)片內(nèi)RAM中的數(shù)據(jù)段有很多,以至于,除了?STACK數(shù)據(jù)段之外,片內(nèi)RAM只剩2個(gè)字節(jié)了,而如果我們給?STACK數(shù)據(jù)段用DS指令分配40H個(gè)字節(jié),這樣keil在編譯時(shí)就會(huì)發(fā)現(xiàn)51的片內(nèi)RAM不足而報(bào)錯(cuò),無(wú)法編譯,從而在編譯鏈接階段幫助我們發(fā)現(xiàn)堆棧問(wèn)題。
繼續(xù)上面的問(wèn)題,SP復(fù)位后的初值是多少,SP復(fù)位后等于0X07,但是立即就被啟動(dòng)文件通過(guò)語(yǔ)句MOV SP,#?STACK-1給改掉了,所以在進(jìn)入main函數(shù)時(shí)SP的值是啟動(dòng)文件修改后的值,也即#?STACK-1(注,很好理解,這里-1是滿(mǎn)增棧的特性),那么#?STACK的值又是多少呢?看上面的匯編語(yǔ)句?STACK SEGMENT IDATA,這一句聲明?STACK段為一個(gè)可重定位的段,也就是說(shuō),?STACK段的首地址(#?STACK)在編譯器進(jìn)行程序鏈接時(shí)才能確定下來(lái),也就是說(shuō),#?STACK的值是在鏈接時(shí)由編譯器自動(dòng)分配的,編譯階段不分配。仍然以上面摘抄的這段map文件為例,我們發(fā)現(xiàn),?STACK段的起始地址是0021H,也就是說(shuō),#?STACK就等于21H。
仿真棧是keil為51生成可重入函數(shù)時(shí)用的(通過(guò)給函數(shù)使用關(guān)鍵詞 REENTRANT限定,可使該函數(shù)具備可重入特性),對(duì)于STM32來(lái)說(shuō),默認(rèn)生成的函數(shù)(不含全局變量和靜態(tài)局部變量的函數(shù))就是可重入的,而keil為51生成的函數(shù),即使這個(gè)函數(shù)不含全局變量和靜態(tài)局部變量,默認(rèn)情況下keil也不會(huì)把這個(gè)函數(shù)匯編成可重入的,我認(rèn)為keil主要是考慮到51的片內(nèi)RAM匱乏,在不外接RAM的情況下,函數(shù)如果被編譯為可重入的,可重入函數(shù)的執(zhí)行需要占用一定的?臻g(尤其是由可重入函數(shù)嵌套調(diào)用產(chǎn)生的長(zhǎng)的調(diào)用鏈,所需的棧更多)。
可重入函數(shù)在執(zhí)行過(guò)程中是需要使用棧的,那么51的可重入函數(shù)使用的棧在哪呢?是SP指向的那個(gè)系統(tǒng)棧嗎?答案是:不是。下面是解釋?zhuān)?/p>
當(dāng)我們給51外擴(kuò)了大的片外RAM時(shí),就不用擔(dān)心RAM不夠的問(wèn)題了,但是還有一個(gè)問(wèn)題,系統(tǒng)棧指針SP只能尋址0~7FH共128字節(jié)的空間,可重入函數(shù)肯定不允許被編譯成使用系統(tǒng)棧,否則,就算外擴(kuò)了RAM,這個(gè)外擴(kuò)RAM又無(wú)法供系統(tǒng)棧來(lái)使用,外擴(kuò)RAM就沒(méi)有意義了,所以keil為51打造了一個(gè)仿真棧的概念,keil在啟動(dòng)文件中聲明了一個(gè)1或2字節(jié)的變量作為棧指針,這個(gè)棧指針的名字和大小根據(jù)編譯模式的不同而不同,以大編譯模式(注釋2)為例,大編譯模式下,啟動(dòng)文件中的XBPSTACK常量需要程序員手動(dòng)設(shè)置為1,這樣啟動(dòng)文件中使用到的條件編譯,將會(huì)引用到一個(gè)2字節(jié)的仿真棧指針?C_XBP,由于keil把仿真棧作為滿(mǎn)減棧,所以這個(gè)仿真棧指針?C_XBP被初始化為片外RAM地址的最大值加1,若我們外接了一個(gè)64K的片外RAM,該RAM的最大地址是0XFFFF,那么棧指針?C_XBP被初始化為0XFFFF+1=溢出為0x0000。再舉一個(gè)小編譯模式的例子,小編譯模式是用來(lái)給沒(méi)有外擴(kuò)RAM的51用的,這樣51只能使用片內(nèi)0~127共128字節(jié)的RAM(這128RAN中還有一部分是Rn等,留給程序可用的RAM就更少了),在小編譯模式下,keil給51生成的仿真棧指針名叫?C_IBP,同時(shí)需要程序員手動(dòng)把IBPSTACK常量設(shè)置為1,指針?C_IBP的初值被初始化為可用RAM的最大地址(127)加1,也即0x7f+1。關(guān)于小編譯模式small、壓縮編譯模式compact、大編譯模式large在堆棧處理上方面的不同,可參考這篇文章點(diǎn)擊打開(kāi)鏈接,如果鏈接掛了,可自行搜索:《Keil模式設(shè)置和編程事項(xiàng)》。
注釋1:滿(mǎn)增棧,滿(mǎn)指的是SP總是指向最后一個(gè)入棧的字節(jié)的地址,增指的是每入棧一次,SP變大。相應(yīng)的,還有空增棧、空減棧、滿(mǎn)減棧,空指的是SP總是指向棧中下一個(gè)空閑位置的地址。
注釋2:如何選擇大編譯模式:以keil5為例,依次選擇->魔術(shù)棒->Target選項(xiàng)卡,Memory Model選擇Large:var...,Code Rom Size選擇Large....
附:舉一個(gè)不可重入函數(shù)使用中可能發(fā)生的陷阱,假設(shè)有分別有如下兩個(gè)函數(shù),第一個(gè)可重入,第二個(gè)不可重入
int add5_re(char a1,char a2,char a3,char a4,char a5) REENTRANT
{
int sum;
sum=a1+a2+a3+a4+a5;
return sum;
}
int add5(char a1,char a2,char a3,char a4,char a5)
{
int sum;
sum=a1+a2+a3+a4+a5;
return sum;
}
這兩個(gè)函數(shù)的形參以及局部變量分配等信息我們查閱.m51文件,分別如下(分號(hào)后面的注釋是博主自己加上的):
[plain] view plain copy------- PROC _?ADD5_RE
x:0002H SYMBOL a1 ;注意,地址標(biāo)號(hào)前為小x,指a1倍分配到了仿真棧中
x:0003H SYMBOL a2
x:0004H SYMBOL a3
x:0005H SYMBOL a4
x:0006H SYMBOL a5
------- DO
x:0000H SYMBOL sum
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
------- PROC _ADD5
D:0007H SYMBOL a1 ;R7
D:0005H SYMBOL a2 ;R5
D:0003H SYMBOL a3 ;R3
X:14ABH SYMBOL a4 ;注意地址標(biāo)號(hào)前為大X,指外部RAM
X:14ACH SYMBOL a5
------- DO
D:0006H SYMBOL sum ;R6
我們發(fā)現(xiàn),add5中的形參和局部變量a1/a2/a3/sum分到了Rn中,a4/A5分到了外部RAM xdata的絕對(duì)地址處,如果我們?cè)趍ain的調(diào)用鏈中和中斷函數(shù)中都調(diào)用了add5這個(gè)函數(shù),就會(huì)發(fā)生錯(cuò)誤,假設(shè)恰好在main的調(diào)用鏈中執(zhí)行add5時(shí)發(fā)生了中斷,切換到中斷函數(shù)中去執(zhí)行add5,那么main調(diào)用鏈中的a1/a2/a3/sum因?yàn)楸环值搅薘n中,進(jìn)入中斷會(huì)切換register BANK,使得main調(diào)用鏈中的a1/a2/a3/sum沒(méi)有被破壞,得以幸免,但是a4/a5因?yàn)楸环峙涞搅私^對(duì)地址中,在中斷執(zhí)行完add5以后,main鏈條中的add5的a4/a5肯定會(huì)被破壞!!
對(duì)于可重入的add5_re函數(shù),即使main調(diào)用鏈和中斷同時(shí)調(diào)用它也不會(huì)出現(xiàn)上述被破壞的情形,因?yàn)閍dd5_re的形參和局部變量全部都被定義到了仿真棧中(見(jiàn)上述代碼注釋),main調(diào)用鏈中使用add5_re函數(shù)會(huì)申請(qǐng)?臻g,中斷時(shí)add5_re又會(huì)申請(qǐng)新的?臻g。
還要注意的是,因?yàn)閗eil編譯51程序時(shí),使用了覆蓋技術(shù)(不同函數(shù)的形參和局部變量可分時(shí)共享同一個(gè)絕對(duì)內(nèi)存單元),這也有可能產(chǎn)生陷阱,假設(shè)這樣一種情況:有一個(gè)函數(shù)func2( )的局部變量b在編譯后被分配到了絕對(duì)xdata的地址14ABH處,和上文的add5的a4變量共享內(nèi)存,這種情況下,即使 { func2( )僅在中斷中被調(diào)用,main調(diào)用鏈中不調(diào)用func2( )}、且{ add5僅在main調(diào)用鏈中被調(diào)用,中斷中不調(diào)用add5 },也會(huì)出問(wèn)題,原因是顯而易見(jiàn)的,如果在add5執(zhí)行過(guò)程中發(fā)生中斷,中斷中使用過(guò)變量b之后,會(huì)破壞add5中的變量a4。究其原因在于,共享地址的編譯方式生成的函數(shù),只要分時(shí)調(diào)用就不會(huì)產(chǎn)生被破壞的情形,但是發(fā)生中斷導(dǎo)致了分時(shí)機(jī)制被破壞,以至于產(chǎn)生了同時(shí)調(diào)用。
結(jié)論:中斷中使用的函數(shù),要么是可重入的,要么是該函數(shù)的局部變量全部是獨(dú)享內(nèi)存單元的。
編輯:admin 最后修改時(shí)間:2018-05-18