讨論啟動代碼的文章繁多,可是單片機程式結束流程鮮有讨論。本文基于STM32探究了CM4核心的處理機制。
STM32:main函數退出後發生什麼?
我們都在說單片機要運作在無限循環裡,不能退出,可退出之後會發生什麼?
讨論STM32啟動過程的文章數不勝數,可main函數結束之後會發生什麼卻少有讨論。
幾日前突然想到這個問題,便開始了探究。
如果不想看冗長的調查和實驗過程,可以直接到文章底部看結論,也有流程圖版哦。
目錄
-
- 網上搜尋
- 文檔查閱
- 實驗測試
- 非半主機
- 半主機
- 結論
可能因為大家不太關心這種情況,我沒有找到有關論述單片機main函數退出的文章。不過在ST Community、阿莫BBS、StackOverflow看到有人在問同樣的問題,下面摘錄了一些不同角度的回答:
- C語言環境角度,三種可能性
- 編譯器在main函數後加入隐性的無限循環
- 編譯器在main外面添加一層無限循環
- CPU繼續向下取址運作(也就是跑飛了)
- 單片機設計角度,退出會引發異常、事件等
- 實際測試,網友們得到的結果卻不太一樣
- 有的會自動循環,像是自動複位了
- 有的會循環同一段彙編
可以看出,答案衆說紛纭,并沒有權威性,于是就轉向了最權威的資料: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
的作用:
- 複制RO和RW段的内容,必要的話進行解壓縮
- 初始化ZI段(置零)
- 調用
__rt_entry
那這個
__rt_entry
是十分的重要啊!
按圖索骥,
__rt_entry
的功能有以下幾點:
- 調用函數初始化堆棧
- 初始化C庫,runtime
- 調用使用者的main
- 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
的文檔中看看:

最後一步,必須調用
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
的函數
__rt_entry
中:
這也印證了之前的推斷,在預設情況下,調用的是
exit
。實際運作與之前分析一緻,
exit
__rt_exit
_sys_exit
。
最後調用的,我們自己定義的
_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
。緊接着,就進入了跳轉到自己的死循環。
至此,單片機陷入空白死循環,形成了前文所說的執行一次現象。
通過查閱官方文檔,以及調試實測,我們能得出結論:
在關閉半主機模式下,STM32的使用者main函數退出了,單片機将會複位,形成循環的效果。開啟半主機模式下,如果退出主程式,會在空循環卡死,表現為隻會執行一遍主函數内容。補充一點,如果使用微庫(microlib),文檔中明文禁止退出main函數。
使用流程圖表示如下:
這篇文章所讨論的退出主函數,對于沒有OS的單片機來說,可以說是一種未定義行為,本身是不安全的、不被推薦的。以上的讨論與實驗。雖然實用性不高,但在學習過程中仍有不少的收獲。
這個主題原本是偶然想到的,花費了一些精力,算把這個問題弄清楚一些了。同時,在這個過程中産生了更多的疑問:将啟動代碼放置在
_sys_exit
之後是ARM還是ST的安排?是在哪一步實施的?文中的實驗具有普适性嗎?等等疑問還等待着解答。
技術新人,水準有限,希望各位前輩、高人不吝賜教,如有錯誤請一定指出。更多嵌入式原創文章可以來公衆号,來找我聊聊天吧:
歡迎轉載,請注明作者與原文連結。
作者:胡小安
原文連結:https://www.cnblogs.com/huxiaoan/p/15821662.html