天天看點

單片機main函數退出後發生什麼——以stm32為例

讨論啟動代碼的文章繁多,可是單片機程式結束流程鮮有讨論。本文基于STM32探究了CM4核心的處理機制。

STM32:main函數退出後發生什麼?

我們都在說單片機要運作在無限循環裡,不能退出,可退出之後會發生什麼?

讨論STM32啟動過程的文章數不勝數,可main函數結束之後會發生什麼卻少有讨論。

幾日前突然想到這個問題,便開始了探究。

如果不想看冗長的調查和實驗過程,可以直接到文章底部看結論,也有流程圖版哦。

目錄

    • 網上搜尋
    • 文檔查閱
    • 實驗測試
      • 非半主機
      • 半主機
    • 結論

可能因為大家不太關心這種情況,我沒有找到有關論述單片機main函數退出的文章。不過在ST Community、阿莫BBS、StackOverflow看到有人在問同樣的問題,下面摘錄了一些不同角度的回答:

  1. C語言環境角度,三種可能性
    • 編譯器在main函數後加入隐性的無限循環
    • 編譯器在main外面添加一層無限循環
    • CPU繼續向下取址運作(也就是跑飛了)
  2. 單片機設計角度,退出會引發異常、事件等
  3. 實際測試,網友們得到的結果卻不太一樣
    • 有的會自動循環,像是自動複位了
    • 有的會循環同一段彙編

可以看出,答案衆說紛纭,并沒有權威性,于是就轉向了最權威的資料:Keil手冊,arm官方工具鍊文檔。

為了尋找main外面的調用情況,我們要從熟悉的啟動代碼開始:

Reset_Handler    PROC
                 EXPORT  Reset_Handler             [WEAK]
        IMPORT  SystemInit
        IMPORT  __main

                 LDR     R0, =SystemInit
                 BLX     R0
                 LDR     R0, =__main
                 BX      R0
                 ENDP
           

我們知道,是

__main

調用了使用者main函數,在手冊

1.8.1 Initialization of the execution environment and execution of the application

這一小節,概述了

__main

的作用:

  1. 複制RO和RW段的内容,必要的話進行解壓縮
  2. 初始化ZI段(置零)
  3. 調用

    __rt_entry

那這個

__rt_entry

是十分的重要啊!

按圖索骥,

__rt_entry

的功能有以下幾點:

  1. 調用函數初始化堆棧
  2. 初始化C庫,runtime
  3. 調用使用者的main
  4. Calls exit() with the value returned by main()
情況不唯一,第四步的exit()可以換為另外兩個退出函數,他們三個退出函數的關系在後面會提到。

情況變得明朗起來,隻要找到這個

exit()

調用的實作即可。我在stdlib.h中找到了

exit

的聲明:

extern _ARMABI_NORETURN void exit(int /*status*/);
   /*注釋有删減,删掉了不少次要内容,有興趣可以去看一看。
    * First, all functions registered by the atexit function are called.
    * Next, all open output streams are flushed, all open streams are closed,
    * and all files created by the tmpfile function are removed.
    * Finally, control is returned to the host environment.
    */
           

總結下來有三個功能:1. 調用之前注冊過的atexit函數 2. 關閉C運作時 3. 向宿主環境上交控制權。

然而具體實作細節還是未知的,我們回到

__rt_entry

的文檔中看看:

單片機main函數退出後發生什麼——以stm32為例

最後一步,必須調用

exit

__rt_exit

_sys_exit

三個中的一個。然而仔細觀察他們三個的功能,是不是能察覺出一絲重複的意味。在功能上,

exit

包含

__rt_exit

_sys_exit

,顯然他們三個不會是毫無關聯的。

在閱讀完所有相關文檔後,我們能得出結論:

exit

__rt_exit

_sys_exit

,後面實驗中的彙編也印證了這一點。

然而,其中的

_sys_exit

是不是看起來很眼熟呢?相信用過STM32的朋友都了解序列槽列印調試與

printf

函數重定向(隻讨論不使用microlib的情況),其中會有這樣一段函數定義:

void _sys_exit(int x) //避免半主機模式
{ 
	x = x; 
} 
           

如果閱讀了

1.6.4 Using the libraries in a nonsemihosting environment

這一節,我們就會發現

_sys_exit

是典型的依賴半主機模式的調用。因為啟動代碼中的函數一路調用會調用到

_sys_exit

上去,是以在非半主機模式下我們需要自己提供它的定義。

Semihosting,半主機模式會把标準C庫中的一些應該提供的函數使用特定的指令交給調試主機來實作。由

1.8.5 Direct semihosting C library function dependencies

可知這些函數包括:

_sys_exit

_sys_close

_sys_open

_sys_write

等,在半主機模式下,對這些函數直接或者間接的調用将轉化為特定的指令。在非半主機模式下,就需要手動實作被調用的函數。

半主機作為一種調試手段,聽起來非常誘人,ARM自己的Keil MDK竟然不支援。既然半主機模式影響了必然會被調用的

_sys_exit

,那就會影響到main函數退出之後的動向。在下一節的實測中,也确實展現出了巨大的差異。

晶片:STM32F407ZGT6

仿真器:DAP-Link

環境:ARMCC V5.06 update 6 ,Keil 5.25.2.0 , -O0

main函數内容如下:

int main(void){
	GPIO_InitTypeDef GPIO_Initure;
     
    HAL_Init();                    	 			//初始化HAL庫    
    Stm32_Clock_Init(336,8,2,7);   				//設定時鐘,168Mhz
    __HAL_RCC_GPIOF_CLK_ENABLE();           	//開啟GPIOF時鐘
    GPIO_Initure.Pin=GPIO_PIN_9|GPIO_PIN_10; 	//PF9,10
    GPIO_Initure.Mode=GPIO_MODE_OUTPUT_PP;  	//推挽輸出
    GPIO_Initure.Pull=GPIO_PULLUP;          	//上拉
    GPIO_Initure.Speed=GPIO_SPEED_HIGH;    	 	

	//開燈
    HAL_GPIO_WritePin(GPIOF,GPIO_PIN_9,GPIO_PIN_RESET);	
    HAL_GPIO_WritePin(GPIOF,GPIO_PIN_10,GPIO_PIN_RESET);
    HAL_Delay(1000);
	//關燈
    HAL_GPIO_WritePin(GPIOF,GPIO_PIN_9,GPIO_PIN_SET);		
    HAL_GPIO_WritePin(GPIOF,GPIO_PIN_10,GPIO_PIN_SET);		
    HAL_Delay(1000);
}
           

PF9和PF10是開發闆上兩顆LED燈,能提供直覺展示。

現象:兩顆LED不斷閃動,就像處于循環之中。

為了找出原因,自然是要開始打斷點+單步調試彙編。

單片機main函數退出後發生什麼——以stm32為例

此時的彙編是這樣的:

單片機main函數退出後發生什麼——以stm32為例

繼續向下取址的話,接下來會彈棧,也将傳回到調用

main

的函數

__rt_entry

中:

單片機main函數退出後發生什麼——以stm32為例

這也印證了之前的推斷,在預設情況下,調用的是

exit

。實際運作與之前分析一緻,

exit

__rt_exit

_sys_exit

單片機main函數退出後發生什麼——以stm32為例
單片機main函數退出後發生什麼——以stm32為例

最後調用的,我們自己定義的

_sys_exit

,可以看出

x=x

被編譯器優化成為一句空指令。

重點在于接下來,按照手冊上說,

_sys_exit

将會把控制權交回宿主環境,此時C運作庫已經被關閉。然而下一句彙編

BX lr

直接将函數傳回

0x08000227

,也就是

__rt_exit

函數調用

_sys_exit

的下文。在上上張圖中,可以發現代碼又回到了熟悉的啟動代碼,接下來,時鐘、堆棧、C庫依次初始化,main函數被調用,形成循環。

這就是退出主函數後表現為循環的原因。

如果想進入半主機模式,我們可以将

#pragma import(__use_no_semihosting)

這句宏删除,之後把自定義的

_sys_exit

等函數注釋掉,再進行編譯、下載下傳、調試。

現象:LED燈亮滅一次,無後序現象。

啟動以及退出流程與非半主機完全一樣,除了在調用

_sys_exit

時會變為相應的核心特定指令。ARM處理器在進入半主機模式時會調用

trap instruction

,對于所有的

Cortex-M

微處理器來說,這個指令是

BKPT 0xAB

。緊接着,就進入了跳轉到自己的死循環。

單片機main函數退出後發生什麼——以stm32為例

至此,單片機陷入空白死循環,形成了前文所說的執行一次現象。

通過查閱官方文檔,以及調試實測,我們能得出結論:

在關閉半主機模式下,STM32的使用者main函數退出了,單片機将會複位,形成循環的效果。開啟半主機模式下,如果退出主程式,會在空循環卡死,表現為隻會執行一遍主函數内容。補充一點,如果使用微庫(microlib),文檔中明文禁止退出main函數。

使用流程圖表示如下:

單片機main函數退出後發生什麼——以stm32為例

這篇文章所讨論的退出主函數,對于沒有OS的單片機來說,可以說是一種未定義行為,本身是不安全的、不被推薦的。以上的讨論與實驗。雖然實用性不高,但在學習過程中仍有不少的收獲。

這個主題原本是偶然想到的,花費了一些精力,算把這個問題弄清楚一些了。同時,在這個過程中産生了更多的疑問:将啟動代碼放置在

_sys_exit

之後是ARM還是ST的安排?是在哪一步實施的?文中的實驗具有普适性嗎?等等疑問還等待着解答。

技術新人,水準有限,希望各位前輩、高人不吝賜教,如有錯誤請一定指出。更多嵌入式原創文章可以來公衆号,來找我聊聊天吧:

單片機main函數退出後發生什麼——以stm32為例

歡迎轉載,請注明作者與原文連結。

作者:胡小安

原文連結:https://www.cnblogs.com/huxiaoan/p/15821662.html