在进行基本的C51编程实验时,编写了一个简单的程序如下:
#include <REGX51.H>
void test(num) {
switch(num) {
case 1:
P2_0=0;
P2_1=0;
break;
}
}
void main(void) {
test(1);
}
程序执行完之后,可以看到实验板上有两个LED被点亮,但另外六个LED居然微微发亮。

如果在主程序中,增加一个无限循环:while(1);,则电路板上的就不再会出现“微微点亮”的现象了。

如上图,实验板上后面六个LED就不再点亮了。上面两种情况的区别,在于第二个程序中主函数main()始终没有退出,而第一个程序,main()函数退出了。这个现象提示我们,LED微微点亮应该与主函数退出之后,单片机的状态有关。
那么核心问题就是:对于普通的嵌入式系统,C语言编程中main()函数退出之后,程序去哪儿了?
程序去哪儿了?
上面实验使用的是C51的编译器,在一块C51开发板上进行。程序没有按照嵌入式开发的惯例——在主函数void main(void)中利用循环将程序控制住,因此出现了令人迷惑的结果。
1. 开天辟地:程序的启动
对于C语言编程来说,所有的用户程序世界都是从主函数 main() 开始的。给用户程序“开天辟地”的准备工作,是由一小段启动代码STARTUP.A51完成的。
下面截取了 STARTUP.A51 代码的一段,可以看到在单片机 RESET 之后,它做了一系列初始化工作(如初始化全局变量、堆栈指针等),之后便直接跳转至:?C_START。
...
LJMP ?C_START
...
实际上,?C_START 就对应着跳转到用户的 main() 函数。这个过程在相关博文中通过调试得到了验证:程序通过LJMP指令进入main()函数。
由于进入main()函数采用的是长跳转指令,因此通常情况下,main()函数是不会返回到启动程序STARTUP.A51的。那么,当main()函数执行完毕,程序到底去哪了呢?
2. 世界尽头:main()的终结
在博文单片机C语言while(1)的问题中,作者分别对Keil编译器和PIC的MAPLAB编译器在main函数结束后的行为进行了反汇编分析。
3. Keil编译器的处理
在Keil编译器生成的代码中,main函数的最后,被增加了以下几行汇编代码:
MOV R0, #0x7F
CLR A
MOV @R0, A
DJNZ R0, (3)
MOV SP, #0x0C
LJMP main
这几条语句的作用是:
- 前4条:将单片机内部RAM的前128个地址单元清零。
- 第5条:重新初始化堆栈指针。
- 第6条:使用长跳转指令,重新跳转到
main函数的第一条指令开始执行。
这意味着,对于Keil编译器,当main()函数退出后,它会先清理内存,然后让程序从头开始再次执行,相当于一个软复位循环。
4. MAPLAB编译器的处理
在对PIC单片机的程序进行跟踪时,发现main()函数的最后一条语句是reset,也就是直接让单片机复位。这是MAPLAB编译器针对PIC单片机特点所添加的语句。
总结
如果单片机程序从main函数中退出,具体执行什么操作是由所使用的C语言编译器决定的。对于没有运行RTOS的嵌入式系统,程序开发中的主函数(main())通常需要通过while(1)等机制使其持续运行,它不应该有终点。了解编译器在背后的行为,有助于我们理解嵌入式系统更深层的运行机制,并规避一些意想不到的硬件状态问题。
参考资料
[1] 51单片机程序执行流程(STARTUP.A51管理Main函数的执行)
[2] 51单片机程序执行流程(STARTUP.A51)