嵌入式程序完成后会发生什么?

电器工程 微控制器 嵌入式 微处理器
2022-01-30 11:21:12

当执行到最后一条return语句时,嵌入式处理器会发生什么?功耗等,天空中有一个永恒的NOP?还是连续执行 NOP,或者处理器会完全关闭?

我问的部分原因是我想知道处理器在完成执行之前是否需要关闭电源,如果它事先已经关闭,它如何完成执行?

4个回答

这是我父亲经常问我的问题。为什么不直接遍历所有指令并在最后停止?

我们来看一个病态的例子。以下代码是在 Microchip 的 PIC18 的 C18 编译器中编译的:

void main(void)
{

}

它产生以下汇编器输出:

addr    opco     instruction
----    ----     -----------
0000    EF63     GOTO 0xc6
0002    F000     NOP
0004    0012     RETURN 0
.
. some instructions removed for brevity
.
00C6    EE15     LFSR 0x1, 0x500
00C8    F000     NOP
00CA    EE25     LFSR 0x2, 0x500
00CC    F000     NOP
.
. some instructions removed for brevity
.
00D6    EC72     CALL 0xe4, 0            // Call the initialisation code
00D8    F000     NOP                     //  
00DA    EC71     CALL 0xe2, 0            // Here we call main()
00DC    F000     NOP                     // 
00DE    D7FB     BRA 0xd6                // Jump back to address 00D6
.
. some instructions removed for brevity
.

00E2    0012     RETURN 0                // This is main()

00E4    0012     RETURN 0                // This is the initialisation code

如您所见,main() 被调用,并且最后包含一个 return 语句,尽管我们自己并没有明确地把它放在那里。当 main 返回时,CPU 执行下一条指令,该指令只是一个 GOTO 以返回到代码的开头。main() 只是被一遍又一遍地调用。

现在,话虽如此,这不是人们通常做事的方式。我从来没有写过任何允许 main() 这样退出的嵌入式代码。大多数情况下,我的代码看起来像这样:

void main(void)
{
    while(1)
    {
        wait_timer();
        do_some_task();
    }    
}

所以我通常不会让 main() 退出。

“好吧好吧”你说。所有这一切都非常有趣,编译器确保永远不会有最后一个 return 语句。但是,如果我们强行解决这个问题会发生什么?如果我手动编写了我的汇编程序,并且没有跳回到开头怎么办?

好吧,显然 CPU 会继续执行下一条指令。那些看起来像这样:

addr    opco     instruction
----    ----     -----------
00E6    FFFF     NOP
00E8    FFFF     NOP
00EA    FFFF     NOP
00EB    FFFF     NOP
.
. some instructions removed for brevity
.
7EE8    FFFF     NOP
7FFA    FFFF     NOP
7FFC    FFFF     NOP
7FFE    FFFF     NOP

main() 中最后一条指令之后的下一个内存地址为空。在具有 FLASH 存储器的微控制器上,空指令包含值 0xFFFF。至少在 PIC 上,该操作码被解释为“nop”或“无操作”。它根本什么都不做。CPU 将继续执行这些 nop 一直到内存的最后。

那之后呢?

在最后一条指令处,CPU 的指令指针为 0x7FFe。当 CPU 给它的指令指针加 2 时,它得到 0x8000,这在只有 32k FLASH 的 PIC 上被认为是溢出,所以它回绕回 0x0000,CPU 愉快地继续执行代码开头的指令,就像它被重置一样。


您还询问了是否需要关闭电源。基本上你可以做任何你想做的事,这取决于你的应用程序。

如果你确实有一个应用程序只需要在开机后做一件事,然后什么都不做,你可以放一会儿(1);在 main() 的末尾,以便 CPU 停止做任何值得注意的事情。

如果应用程序需要 CPU 关闭电源,那么根据 CPU 的不同,可能会有各种可用的睡眠模式。但是,CPU 有再次唤醒的习惯,因此您必须确保睡眠没有时间限制,并且没有 Watch Dog Timer 处于活动状态等。

你甚至可以组织一些外部电路,让 CPU 在完成后完全切断自己的电源。请参阅此问题:使用瞬时按钮作为锁定开关切换开关

对于已编译的代码,它取决于编译器。我使用的 Rowley CrossWorks gcc ARM 编译器跳转到 crt0.s 文件中的代码,该文件具有无限循环。用于 16 位 dsPIC 和 PIC24 器件(也基于 gcc)的 Microchip C30 编译器复位处理器。

当然,大多数嵌入式软件永远不会像那样终止,而是在循环中连续执行代码。

这里有两点需要说明:

  • 严格来说,嵌入式程序无法“完成”。
  • 很少需要运行嵌入式程序一段时间然后“完成”。

程序关闭的概念通常不存在于嵌入式环境中。在低级别,CPU 将尽其所能执行指令;没有“最终退货声明”之类的东西。如果 CPU 遇到不可恢复的故障或显式停止(进入睡眠模式、低功耗模式等),CPU 可能会停止执行,但请注意,即使是睡眠模式或不可恢复的故障通常也不能保证不会有更多代码执行被执行。您可以从睡眠模式中唤醒(这是它们通常的使用方式),甚至锁定的 CPU 仍然可以执行 NMI 处理程序(Cortex-M 就是这种情况)。看门狗仍然会运行,一旦启用,您可能无法在某些微控制器上禁用它。架构之间的细节差异很大。

如果固件是用 C 或 C++ 等语言编写的,那么如果 main() 退出会发生什么由启动代码决定。例如,这里是来自 STM32 标准外设库的启动代码的相关部分(对于 GNU 工具链,注释是我的):

Reset_Handler:  
  /*  ...  */
  bl  main    ; call main(), lr points to next instruction
  bx  lr      ; infinite loop

当 main() 返回时,此代码将进入无限循环,尽管方式不明显(bl main加载lr下一条指令的地址,这实际上是跳转到自身)。不会尝试停止 CPU 或使其进入低功耗模式等。如果您在应用程序中有任何合法需求,则必须自己做。

请注意,按照 ARMv7-M ARM A2.3.1 中的规定,链接寄存器在复位时设置为 0xFFFFFFFF,并且跳转到该地址将触发故障。因此 Cortex-M 的设计者决定将复位处理程序的返回视为异常,并且很难与他们争论。

说到固件完成后停止 CPU 的合法需求,很难想象设备断电不会更好地满足任何需求。(如果您确实“永久”禁用了 CPU,那么唯一可以对设备执行的操作是重启电源或外部硬件重置。)您可以取消断言 DC/DC 转换器的 ENABLE 信号或关闭电源其他方式,就像 ATX PC 一样。

问的时候return,你想的太高了。C 代码被翻译成机器代码。因此,如果您考虑处理器盲目地将指令从内存中拉出并执行它们,那么它不知道哪一个是“最终的” return因此,处理器没有固有的结束,而是由程序员来处理最终情况。正如 Leon 在他的回答中指出的那样,编译器已经对默认行为进行了编程,但通常程序员可能想要自己的关机顺序(我已经做了各种事情,比如进入低功耗模式并停止,或者等待 USB 电缆插入然后重新启动)。

许多微处理器都有停止指令,可以在不影响外围设备的情况下停止处理器。其他处理器可能会通过简单地重复跳转到同一地址来依赖“停止”。有可能的选择,但这取决于程序员,因为处理器只会继续从内存中读取指令,即使该内存并非旨在成为指令。