1. 程式人生 > >51微控制器的模擬棧(模擬棧/可重入棧)

51微控制器的模擬棧(模擬棧/可重入棧)

51微控制器的模擬棧(模擬棧/可重入棧)

51微控制器的模擬棧(又叫模擬棧、或者可重入棧)。

首先來看,51的系統棧(又叫系統棧,或者硬體棧),就是SP所指向的棧,他是一個滿增棧(註釋1),位於片內RAM的128 bytes之中,上電之後系統堆疊指標SP的初值等於多少呢?這個要從51的啟動檔案來分析,啟動檔案中有這樣的彙編程式碼:

?STACK SEGMENT IDATA ;定義一個片內資料段,段名:?STACK

RSEG ?STACK ;選擇之前定義過的一個可重定位的段?STACK,下面的彙編語句將會被放置到該段,直到遇到下一個段定位指令,例如CSEG/RSEG。

DS 1 ;預留儲存區命令。宣告先佔用一個位元組的空間,在編譯時,這個預留的空間不會被其他變數所使用。在這裡的意義是,給硬體棧分配1個byte(實際這樣是有問題的,應該為硬體棧預留更多空間)

還有:

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檔案摘抄:

              • 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只能定址07FH共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只能使用片內0127共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。究其原因在於,共享地址的編譯方式生成的函式,只要分時呼叫就不會產生被破壞的情形,但是發生中斷導致了分時機制被破壞,以至於產生了同時呼叫。

結論:中斷中使用的函式,要麼是可重入的,要麼是該函式的區域性變數全部是獨享記憶體單元的。

(轉載的:http://www.21ic.com/jichuzhishi/mcu/questions/2018-05-02/759519.html)

51微控制器stack堆疊

一般編譯器的堆疊用於儲存區域性變數、函式的引數、函式的返回值、中斷上下文資訊等。但Keil對區域性變數、函式引數預先分配空間(放在靜態全域性變數區),Keil的堆疊只是用於儲存函式巢狀呼叫的PC、中斷上下文資訊。

從主程式進入中斷需要入棧位元組數:13+PC=15 Byte(ACC,PSW,B,DPH,DPL,R0~R7)

Keil支援2級中斷15×2=30 Byte。

主程式中每級呼叫需要消耗2位元組儲存PC,如呼叫深度6級,需要6×2=12位元組堆疊空間。

Keil51 編譯後會生成M51檔案,查詢?Stack地址,為堆疊起始地址,向上增長到0xff,溢位則導致微控制器復位。

C51必讀, Startup.A51作用

這裡講述一些初學者學習C51的一些誤區和注意事項。高手的特別應用不包括在內。

C忌諱絕對定位
常看見初學者要求使用_at_,這是一種謬誤,把C當作ASM看待了。在C中變數的定位是編譯器的事情,初學者只要定義變數和變數的作用域,編譯器就把一個固定地址給這個變數。怎麼取得這個變數的地址?要用指標。比如unsigned char data x;後,x的地址就是&x,你只要檢視這個引數,就可以在程式中知道具體的地址了。所以俺一看見要使用絕對定位的人,第一印象就是:這大概是個初學者。

設定SP的問題

原因和1差不對,編譯器在把所有變數和緩衝區賦予地址後,自動把最後一個位元組開始的地方,作為SP的開始位置,所以初學者是不必要去理會的。這體現C的優越性,很多事情C編譯時候做了。

這些檔案都屬於CRT,也就是c執行庫,如果你不加入到你工程裡,keil也會自動連結預設的。如果你加入到工程裡了,他就用工程裡面的。
常見的有startup.A51裡面修改喂狗,或者初始化時鐘等。

malloc printf putchar 等c的庫函式也是。
啟動檔案. 清理RAM.設定堆疊等.即執行完start.a51後跳轉到.c檔案的main函式.
和彙編一樣,在C中定義的那些變數和陣列的初始化就在startup.a51中進行,如果你在定義全域性變數時帶有數值,如unsigned char data xxx=“100”;,那startup.a51中就會有相關的賦值。如果沒有=100,startup.a51就會把他清0。(startup.a51==變數的初始化)。這些初始化完畢後,還會設定SP指標。對非變數區域,如堆疊區,將不會有賦值或清零動作。