摘要 在沒有硬件看門狗的係統以及一些對單片機I/O口線狀態高度敏感的係統中,軟複位功能相當重要。標準MCS-51以及很多常見的51內核單片機沒有提供“軟複位”方法。本文分別以5lasm子程序和C51函數的形式,為MCS-51單片機係統提供完善的“軟複位”方法。
關鍵詞 MCS-51單片機軟複位51asm C51
引 言
複位是單片機的初始化操作,其作用是使單片機和係統中其他部件處於一個確定的初始狀態,並從這個狀態開始工作。
標準MCS-51的複位邏輯比較簡單,隻有通過複位引腳RST進行外部擴展。對於具有外部看門狗芯片的係統,當單片機由於某種原因程序“跑飛”而沒有按時“喂狗”,或由軟件陷阱捕捉到程序運行的異常,而故意不“喂狗”時,看門狗芯片會給單片機的RST引腳提供一個複位信號,讓單片機進行一次“硬”複位,以恢複程序的正常運行;有些5l內核的單片機具有片內的看門狗,或者提供可訪問的寄存器實現“軟件複位”。一般實現的也都是與在RST引腳提供複位信號等價的“硬”複位。
在有些應用中,由於單片機所接外設嚴格依附幹單片機口線的時序,甚至不允許硬件複位時對口線的複位操作;或由於係統沒有外部看門狗,隻能用軟件監測程序運行異常並重新對單片機進行初始化操作,這時就需要所謂的“軟複位”了。
在互聯網上可以找到一些軟複位的方法,但都不夠完善或不方便使用,基於C5l的軟複位更是一個難點。本文提出一種功能完善、占用資源少的實現方法,在51asm和C51下都可方便使用。
1 “軟複位”要實現的功能
對於MCS-51係統,所謂“軟複位”是一種由用戶軟件控製的複位活動,利用一係列指令來模擬硬件複位所實現的各種操作內容,並且重新從頭開始執行用戶程序。其內容包括:
①程序計數器PC的複位,從0000H開始執行程序;
②中斷優先級狀態觸發器的複位;
③特殊功能寄存器的複位;
④程序跑飛前狀態的盡量恢複。
其中,特殊功能寄存器的複位可根據具體係統的需要,在軟複位以前對相關寄存器逐個賦值再軟複位的方法完成,或在複位以後的初始化程序中實現;程序跑飛前狀態的恢複也可根據RAM中存留的數據來進行判斷處理。本文重點討論關於程序計數器的複位和中斷優先級狀態觸發器的複位,在此基礎上不難再增加特殊功能寄存器的複位和程序跑飛前狀態的恢複,下文不再涉及相關代碼。
程序計數器的複位容易實現,一條“LJMP 0000H”即可。中斷優先級狀態觸發器的複位則比較困難,由於它們對於用戶程序是不可見的,無法直接讀寫其內容,隻有用RETI指令才能清除。又由於51單片機兩級中斷機製,低優先級中斷有可能被高優先級中斷再次中斷而形成中斷再入,而一次RETl隻能退出當前優先級中斷並清除相應的中斷優先級狀態觸發器,因此最壞情況下需要兩次RETI才能確保中斷優先級狀態觸發器的複位。綜合考慮,可先用壓棧的方法準備跳轉後的入口地址,再用RETI來完成跳轉和清除中斷優先級狀態觸發器的雙重任務,把以上過程執行兩次,前一次的目標地址是後一次的入口,後一次則跳轉到0000H,完成複位過程。
2 軟複位的實現方法
前已述及軟複位要完成的功能,包含程序計數器PC的複位、中斷優先級狀態觸發器的複位、特殊功能寄存器的複位(略)和程序跑飛前狀態的恢複(略)。下麵分別用匯編程序和C51程序來實現,重點介紹C51程序的實現方法。
2.1 51asm匯編程序實現

使用時,在軟件陷阱處理程序段或軟件看門狗中用“LJMP #RST_O”指令跳轉到此段程序入口處即可。
2.2 C51程序實現
用C語言實現MCS-51係統的軟複位,互聯網上出現過多種版本,但都不夠完善。以下基於業界應用最廣泛的編譯器Keil C51來討論。如:

這段程序,用強製類型轉換把地址0000H轉換成re-set_not_best_O()函數的入口。實際上調用函數reset_not_best_O()等價於“LJMP 0000H”,沒有處理可能的中斷狀態問題。
又如:

這段代碼中,一維字符數組中為複位代碼的機器碼。“(*((Void(*)())(rst)))()”是把rst這個字符數組的地址強製轉換成函數指針,並調用。實際上調用函數reset_not_best_1()是執行了一段如下代碼:

可見,調用一次reset_not_best_l()函數相當於執行了1次清除中斷優先級狀態觸發器的動作,然後跳轉到0000H重新執行。但此段代碼仍然可能被再次中斷失效,或者當原來堆棧已經溢出時導致對0000H地址的壓棧錯誤,不能正確實現“軟複位”功能。除此之外,由於KeilC5l在把控製權交給函數main()之前對內部RAM進行初始化的代碼是:

此處R0作為循環變量使用,對內部RAM從7FH單元開始,按照每次遞減的方法對內部RAM逐一進行清零。當R0指向00H時,以上程序可以很好地完成清零工作;然而R0依據RS0、RSl取值的不同,可以指向00H、08H、10H或18H單元。當:R0指向08H、10H或18H時,上麵所列清零程序將陷入死循環。分析如下:
以RS0=1、RSl=0,即RO指向08H為例。自標號LOOP處起始的循環進入時的狀態是:R0=7FH,PC=#LOOP。設程序已執行到R0=08H,PC=#LOOP,此時執行PC指向處的指令“MOV @RO,A”,將把(RO)清零,也即08H單元清零。由於R0指向08H,實際上R0也被清零了,此時RO=00H,PC=#LOOPl;再執行一條指令“DJNZ R0,LOOP”,R0將由00H自減為OFFH,回到R0=OFFH,PC=#LOOP的狀態;繼續執行,將再次出現R0==08H,PC=#LOOP的狀態,陷入死循環。當R0指向10H或18H單元時也類似會陷入死循環。為了避免上述問題,可以把字符數組中機器碼改為與以下程序段對應:

調用改造後的reset_not_best_l()函數將清除中斷優先級狀態觸發器,並跳轉到0000H單元繼續執行,從而實現了軟複位。但由於隻執行了1次RETT指令,在複位前出現了中斷再入的極端情況下,仍會出現低優先級中斷放鎖死的現象。由於無法在字符數組中實現對最終代碼地址的取得,無法如2.1節匯編程序中的“MOV DPTR,#RSTl”一樣讀取絕對地址,因此也無法實現如同2.1節中的兩次RETl來保證清除全部的中斷優先級狀態觸發器。解決的辦法是,在內存中設置標誌flag,每次複位後都檢查特定標誌,接如下偽代碼來判定(假定複位標誌設為0x55,若複位後發現標誌字為0x55則轉正常初始化程序,否則置複位標誌為0x55,再次複位):

但這種辦法有兩個弊端:其一,萬一在程序跑飛時剛好處在中斷再入中,且flag所在的RAM地址中由於某種原因恰好是0x55,低級中斷仍是被鎖死的;其二,Keil C5l的缺省編譯模式是加上了初始化程序段startup.A51的,在這段程序中對全部的內部RAM都進行了初始化,複位標誌也會被清0,flag將失效。因此要正常使用標誌flag必須手工修改startup.A5l,不要清除flag單元,或者禁止stanup.A5l的使用,自己對內部RAM進行初始化,都比較繁瑣。
若用以下reset_best()函數,則可順利解決上述問題:

在此函數中,首先定義了結構體類型ResetStruct,它包含字符數組rstcode和16位整型數addr兩個成員。在結構體變量的RST實例中,RST.rstcode是複位代碼對應的機器碼,RST.addr的值是RST.rstcode這個數組的首地址+偏移量0x15。其實是以下匯編代碼中標號rstl處的地址,也即是“#rstl”,由Keil C51在編譯時自動計算得到。


此reset_best()函數巧妙地利用C語言中數組名即是數組首地址(其實質就是這一段“軟複位”代碼的入口地址!),把數組名+偏移量0x15賦值給結構體的int型的成員addr,則正好把軟複位代碼中標號rstl的入口地址兩個字節取了出來(由KeilC51在編譯連接時完成),存放在RST.addr中,由於結構體連續存儲,RST.addr會緊接著軟複位代碼RST.rstcode存放。見上段程序中的“DB #LOW(rstcode+OFH)”和“DB #HIGH(rstcode+09H)”。
當本程序被調用並執行到“MOVC A,@A+PC”時,分兩次取到的分別是“#rstl”的低位和高位字節,把它們壓棧以後再調用RETI指令,除了清除可能有的中斷優先級狀態記錄外,還會跳轉到rstl執行後續的連續兩次壓棧操作,把第2次RETI的返回地址設為0000H,再通過緊接著的RETI指令,清除可能還有的中斷優先級狀態記錄,並跳轉到0000H完成完整的複位操作。
本程序使用一個C51函數完成了完整的包含兩次RETI在內的複位操作,消除了所有已知隱患,隻需在應用程序中包含此reset_best()函數,在需要軟複位時調用即可。2.1節中所列匯編語言的子程序也可使用本節分析時所用匯編代碼代替。
結 語
本文對MCS-51單片機的“軟複位”進行了深入討論,給出了分別基於51asm的匯編子程序和基於C5l的函數。使用本文所述的“軟複位”方法,無論是5lasm程序還是C51程序,所需的資源消耗都很小,隻占用二三十個字節的程序存儲器,使用也非常簡單,不會增加編程負擔。當具體應用係統還需進行特殊功能寄存器的複位以度程序跑飛前狀態的恢複時,在此基礎上也很容易實現。特別是當單片機所接外設嚴格依附於單片機口線的時序,須盡量避免硬件複位對口線的複位操作或係統不具備硬件看門狗時,對於提高單片機係統的抗幹擾能力有較大的實用價值。實際應用表明,這種軟複位方法是非常有效的。
關鍵詞 MCS-51單片機軟複位51asm C51
引 言
複位是單片機的初始化操作,其作用是使單片機和係統中其他部件處於一個確定的初始狀態,並從這個狀態開始工作。
標準MCS-51的複位邏輯比較簡單,隻有通過複位引腳RST進行外部擴展。對於具有外部看門狗芯片的係統,當單片機由於某種原因程序“跑飛”而沒有按時“喂狗”,或由軟件陷阱捕捉到程序運行的異常,而故意不“喂狗”時,看門狗芯片會給單片機的RST引腳提供一個複位信號,讓單片機進行一次“硬”複位,以恢複程序的正常運行;有些5l內核的單片機具有片內的看門狗,或者提供可訪問的寄存器實現“軟件複位”。一般實現的也都是與在RST引腳提供複位信號等價的“硬”複位。
在有些應用中,由於單片機所接外設嚴格依附幹單片機口線的時序,甚至不允許硬件複位時對口線的複位操作;或由於係統沒有外部看門狗,隻能用軟件監測程序運行異常並重新對單片機進行初始化操作,這時就需要所謂的“軟複位”了。
在互聯網上可以找到一些軟複位的方法,但都不夠完善或不方便使用,基於C5l的軟複位更是一個難點。本文提出一種功能完善、占用資源少的實現方法,在51asm和C51下都可方便使用。
1 “軟複位”要實現的功能
對於MCS-51係統,所謂“軟複位”是一種由用戶軟件控製的複位活動,利用一係列指令來模擬硬件複位所實現的各種操作內容,並且重新從頭開始執行用戶程序。其內容包括:
①程序計數器PC的複位,從0000H開始執行程序;
②中斷優先級狀態觸發器的複位;
③特殊功能寄存器的複位;
④程序跑飛前狀態的盡量恢複。
其中,特殊功能寄存器的複位可根據具體係統的需要,在軟複位以前對相關寄存器逐個賦值再軟複位的方法完成,或在複位以後的初始化程序中實現;程序跑飛前狀態的恢複也可根據RAM中存留的數據來進行判斷處理。本文重點討論關於程序計數器的複位和中斷優先級狀態觸發器的複位,在此基礎上不難再增加特殊功能寄存器的複位和程序跑飛前狀態的恢複,下文不再涉及相關代碼。
程序計數器的複位容易實現,一條“LJMP 0000H”即可。中斷優先級狀態觸發器的複位則比較困難,由於它們對於用戶程序是不可見的,無法直接讀寫其內容,隻有用RETI指令才能清除。又由於51單片機兩級中斷機製,低優先級中斷有可能被高優先級中斷再次中斷而形成中斷再入,而一次RETl隻能退出當前優先級中斷並清除相應的中斷優先級狀態觸發器,因此最壞情況下需要兩次RETI才能確保中斷優先級狀態觸發器的複位。綜合考慮,可先用壓棧的方法準備跳轉後的入口地址,再用RETI來完成跳轉和清除中斷優先級狀態觸發器的雙重任務,把以上過程執行兩次,前一次的目標地址是後一次的入口,後一次則跳轉到0000H,完成複位過程。
2 軟複位的實現方法
前已述及軟複位要完成的功能,包含程序計數器PC的複位、中斷優先級狀態觸發器的複位、特殊功能寄存器的複位(略)和程序跑飛前狀態的恢複(略)。下麵分別用匯編程序和C51程序來實現,重點介紹C51程序的實現方法。
2.1 51asm匯編程序實現

使用時,在軟件陷阱處理程序段或軟件看門狗中用“LJMP #RST_O”指令跳轉到此段程序入口處即可。
2.2 C51程序實現
用C語言實現MCS-51係統的軟複位,互聯網上出現過多種版本,但都不夠完善。以下基於業界應用最廣泛的編譯器Keil C51來討論。如:

這段程序,用強製類型轉換把地址0000H轉換成re-set_not_best_O()函數的入口。實際上調用函數reset_not_best_O()等價於“LJMP 0000H”,沒有處理可能的中斷狀態問題。
又如:

這段代碼中,一維字符數組中為複位代碼的機器碼。“(*((Void(*)())(rst)))()”是把rst這個字符數組的地址強製轉換成函數指針,並調用。實際上調用函數reset_not_best_1()是執行了一段如下代碼:

可見,調用一次reset_not_best_l()函數相當於執行了1次清除中斷優先級狀態觸發器的動作,然後跳轉到0000H重新執行。但此段代碼仍然可能被再次中斷失效,或者當原來堆棧已經溢出時導致對0000H地址的壓棧錯誤,不能正確實現“軟複位”功能。除此之外,由於KeilC5l在把控製權交給函數main()之前對內部RAM進行初始化的代碼是:

此處R0作為循環變量使用,對內部RAM從7FH單元開始,按照每次遞減的方法對內部RAM逐一進行清零。當R0指向00H時,以上程序可以很好地完成清零工作;然而R0依據RS0、RSl取值的不同,可以指向00H、08H、10H或18H單元。當:R0指向08H、10H或18H時,上麵所列清零程序將陷入死循環。分析如下:
以RS0=1、RSl=0,即RO指向08H為例。自標號LOOP處起始的循環進入時的狀態是:R0=7FH,PC=#LOOP。設程序已執行到R0=08H,PC=#LOOP,此時執行PC指向處的指令“MOV @RO,A”,將把(RO)清零,也即08H單元清零。由於R0指向08H,實際上R0也被清零了,此時RO=00H,PC=#LOOPl;再執行一條指令“DJNZ R0,LOOP”,R0將由00H自減為OFFH,回到R0=OFFH,PC=#LOOP的狀態;繼續執行,將再次出現R0==08H,PC=#LOOP的狀態,陷入死循環。當R0指向10H或18H單元時也類似會陷入死循環。為了避免上述問題,可以把字符數組中機器碼改為與以下程序段對應:

調用改造後的reset_not_best_l()函數將清除中斷優先級狀態觸發器,並跳轉到0000H單元繼續執行,從而實現了軟複位。但由於隻執行了1次RETT指令,在複位前出現了中斷再入的極端情況下,仍會出現低優先級中斷放鎖死的現象。由於無法在字符數組中實現對最終代碼地址的取得,無法如2.1節匯編程序中的“MOV DPTR,#RSTl”一樣讀取絕對地址,因此也無法實現如同2.1節中的兩次RETl來保證清除全部的中斷優先級狀態觸發器。解決的辦法是,在內存中設置標誌flag,每次複位後都檢查特定標誌,接如下偽代碼來判定(假定複位標誌設為0x55,若複位後發現標誌字為0x55則轉正常初始化程序,否則置複位標誌為0x55,再次複位):

但這種辦法有兩個弊端:其一,萬一在程序跑飛時剛好處在中斷再入中,且flag所在的RAM地址中由於某種原因恰好是0x55,低級中斷仍是被鎖死的;其二,Keil C5l的缺省編譯模式是加上了初始化程序段startup.A51的,在這段程序中對全部的內部RAM都進行了初始化,複位標誌也會被清0,flag將失效。因此要正常使用標誌flag必須手工修改startup.A5l,不要清除flag單元,或者禁止stanup.A5l的使用,自己對內部RAM進行初始化,都比較繁瑣。
若用以下reset_best()函數,則可順利解決上述問題:

在此函數中,首先定義了結構體類型ResetStruct,它包含字符數組rstcode和16位整型數addr兩個成員。在結構體變量的RST實例中,RST.rstcode是複位代碼對應的機器碼,RST.addr的值是RST.rstcode這個數組的首地址+偏移量0x15。其實是以下匯編代碼中標號rstl處的地址,也即是“#rstl”,由Keil C51在編譯時自動計算得到。


此reset_best()函數巧妙地利用C語言中數組名即是數組首地址(其實質就是這一段“軟複位”代碼的入口地址!),把數組名+偏移量0x15賦值給結構體的int型的成員addr,則正好把軟複位代碼中標號rstl的入口地址兩個字節取了出來(由KeilC51在編譯連接時完成),存放在RST.addr中,由於結構體連續存儲,RST.addr會緊接著軟複位代碼RST.rstcode存放。見上段程序中的“DB #LOW(rstcode+OFH)”和“DB #HIGH(rstcode+09H)”。
當本程序被調用並執行到“MOVC A,@A+PC”時,分兩次取到的分別是“#rstl”的低位和高位字節,把它們壓棧以後再調用RETI指令,除了清除可能有的中斷優先級狀態記錄外,還會跳轉到rstl執行後續的連續兩次壓棧操作,把第2次RETI的返回地址設為0000H,再通過緊接著的RETI指令,清除可能還有的中斷優先級狀態記錄,並跳轉到0000H完成完整的複位操作。
本程序使用一個C51函數完成了完整的包含兩次RETI在內的複位操作,消除了所有已知隱患,隻需在應用程序中包含此reset_best()函數,在需要軟複位時調用即可。2.1節中所列匯編語言的子程序也可使用本節分析時所用匯編代碼代替。
結 語
本文對MCS-51單片機的“軟複位”進行了深入討論,給出了分別基於51asm的匯編子程序和基於C5l的函數。使用本文所述的“軟複位”方法,無論是5lasm程序還是C51程序,所需的資源消耗都很小,隻占用二三十個字節的程序存儲器,使用也非常簡單,不會增加編程負擔。當具體應用係統還需進行特殊功能寄存器的複位以度程序跑飛前狀態的恢複時,在此基礎上也很容易實現。特別是當單片機所接外設嚴格依附於單片機口線的時序,須盡量避免硬件複位對口線的複位操作或係統不具備硬件看門狗時,對於提高單片機係統的抗幹擾能力有較大的實用價值。實際應用表明,這種軟複位方法是非常有效的。