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