单片机
返回首页

单片机main函数退出后发生什么——以stm32为例

2025-08-25 来源:cnblogs

网上搜索

可能因为大家不太关心这种情况,我没有找到有关论述单片机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的文档中看看:

image-20220117153954541

最后一步,必须调用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不断闪动,就像处于循环之中。


为了找出原因,自然是要开始打断点+单步调试汇编。

Image

此时的汇编是这样的:

image-20220118233158298

继续向下取址的话,接下来会弹栈,也将返回到调用main的函数__rt_entry中:

image-20220118233421880

这也印证了之前的推断,在默认情况下,调用的是exit。实际运行与之前分析一致,exit调用__rt_exit调用_sys_exit。


__rt_exit调用_sys_exit

自定义的_sys_exit

__rt_exit调用_sys_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。紧接着,就进入了跳转到自己的死循环。


Image

至此,单片机陷入空白死循环,形成了前文所说的执行一次现象。


结论

通过查阅官方文档,以及调试实测,我们能得出结论:


在关闭半主机模式下,STM32的用户main函数退出了,单片机将会复位,形成循环的效果。开启半主机模式下,如果退出主程序,会在空循环卡死,表现为只会执行一遍主函数内容。补充一点,如果使用微库(microlib),文档中明文禁止退出main函数。


使用流程图表示如下:

STM32启动退出流程

这篇文章所讨论的退出主函数,对于没有OS的单片机来说,可以说是一种未定义行为,本身是不安全的、不被推荐的。以上的讨论与实验。虽然实用性不高,但在学习过程中仍有不少的收获。

进入单片机查看更多内容>>
相关视频
  • 【TI MSPM0 应用实战】智能小车+工业角度编码器+血氧仪+烟雾探测器!硬核参考设计详解!

  • 2022 Digi-Key KOL 系列: 你见过1GHz主频的单片机吗?Teensy 4.1开发板介绍

  • TI 新一代 C2000™ 微控制器:全方位助力伺服及马达驱动应用

  • MSP430电容触摸技术 - 防水Demo演示

  • 直播回放: Microchip Timberwolf™ 音频处理器在线研讨会

  • 基于灵动MM32W0系列MCU的指夹血氧仪控制及OTA升级应用方案分享

精选电路图
  • 锂离子/锂聚合物USB电池充电器

  • 6晶体管H桥

  • AVR LCD温度计—LM35

  • AVR PC步进电机驱动器

  • AVR温度计TCN75

  • JDM2 PIC 18F 编程器

    相关电子头条文章