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