¿Qué sucede cuando finaliza un programa incrustado?

¿Qué sucede en un procesador integrado cuando la ejecución llega a esa returndeclaración final? ¿Todo simplemente se congela como está; consumo de energía, etc., con un largo y eterno NOP en el cielo? ¿O los NOP se ejecutan continuamente, o un procesador se apagará por completo?

Parte de la razón por la que pregunto es si un procesador necesita apagarse antes de que finalice la ejecución y, si es así, ¿cómo termina la ejecución si se ha apagado antes?

Depende de tus creencias. Algunos dicen que se reencarnará.
¿Es para un misil?
algunos sistemas admiten la instrucción HCF (Halt and Catch Fire) . :)
se ramificará a la rutina de autodestrucción

Respuestas (9)

Esta es una pregunta que mi papá siempre me hacía. " ¿Por qué no sigue todas las instrucciones y se detiene al final? "

Veamos un ejemplo patológico. El siguiente código fue compilado en el compilador C18 de Microchip para PIC18:

void main(void)
{

}

Produce la siguiente salida del ensamblador:

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

Como puede ver, se llama a main() y al final contiene una declaración de retorno, aunque no la pusimos explícitamente nosotros mismos. Cuando main regresa, la CPU ejecuta la siguiente instrucción, que es simplemente un GOTO para volver al principio del código. main() simplemente se llama una y otra vez.

Ahora, habiendo dicho esto, esta no es la forma en que la gente normalmente haría las cosas. Nunca he escrito ningún código incrustado que permita que main() salga así. En su mayoría, mi código se vería así:

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

Así que normalmente nunca dejaría salir main().

"Está bien, está bien", dices. Todo esto es muy interesante porque el compilador se asegura de que nunca haya una última declaración de retorno. Pero, ¿qué pasa si forzamos el asunto? ¿Qué pasa si codifiqué a mano mi ensamblador y no volví al principio?

Bueno, obviamente la CPU seguiría ejecutando las siguientes instrucciones. Esos se verían algo como esto:

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

La siguiente dirección de memoria después de la última instrucción en main() está vacía. En un microcontrolador con memoria FLASH, una instrucción vacía contiene el valor 0xFFFF. Al menos en un PIC, ese código de operación se interpreta como 'nop' o 'sin operación'. Simplemente no hace nada. La CPU continuaría ejecutando esos nops en toda la memoria hasta el final.

¿Qué hay después de eso?

En la última instrucción, el puntero de instrucción de la CPU es 0x7FFe. Cuando la CPU agrega 2 a su puntero de instrucción, obtiene 0x8000, que se considera un desbordamiento en un PIC con solo 32k FLASH, por lo que regresa a 0x0000, y la CPU felizmente continúa ejecutando instrucciones al comienzo del código. , como si se hubiera reiniciado.


También preguntaste sobre la necesidad de apagar. Básicamente, puede hacer lo que quiera, y depende de su aplicación.

Si tenía una aplicación que solo necesitaba hacer una cosa después de encenderla y luego no hacer nada más, podría poner un tiempo (1); al final de main() para que la CPU deje de hacer algo notable.

Si la aplicación requiere que la CPU se apague, entonces, dependiendo de la CPU, probablemente habrá varios modos de suspensión disponibles. Sin embargo, las CPU tienen la costumbre de volver a activarse, por lo que debe asegurarse de que no haya un límite de tiempo para la suspensión y que no haya Watch Dog Timer activo, etc.

Incluso podría organizar algunos circuitos externos que permitirían que la CPU cortara completamente su propia energía cuando hubiera terminado. Consulte esta pregunta: Uso de un botón pulsador momentáneo como interruptor de palanca de encendido y apagado .

Para el código compilado, depende del compilador. El compilador Rowley CrossWorks gcc ARM que utilizo salta al código en el archivo crt0.s que tiene un bucle infinito. El compilador Microchip C30 para los dispositivos dsPIC y PIC24 de 16 bits (también basados ​​en gcc) restablece el procesador.

Por supuesto, la mayoría del software integrado nunca termina así y ejecuta el código continuamente en un bucle.

Hay dos puntos que se deben hacer aquí:

  • Un programa embebido, estrictamente hablando, no puede "terminar".
  • Muy rara vez es necesario ejecutar un programa incrustado durante algún tiempo y luego "terminar".

El concepto de apagado de un programa normalmente no existe en un entorno integrado. A un nivel bajo, una CPU ejecutará instrucciones mientras pueda; no existe tal cosa como una "declaración de devolución final". Una CPU puede detener la ejecución si encuentra una falla irrecuperable o si se detiene explícitamente (se pone en modo de suspensión, modo de bajo consumo, etc.), pero tenga en cuenta que incluso los modos de suspensión o las fallas irrecuperables generalmente no garantizan que no se ejecutará más código. ser ejecutado. Puede activarse desde los modos de suspensión (así es como se usan normalmente), e incluso una CPU bloqueada aún puede ejecutar un controlador NMI (este es el caso de Cortex-M). También se ejecutará un perro guardián, y es posible que no pueda desactivarlo en algunos microcontroladores una vez que esté habilitado. Los detalles varían mucho entre arquitecturas.

En el caso de firmware escrito en un lenguaje como C o C++, lo que sucede si main() sale está determinado por el código de inicio. Por ejemplo, aquí está la parte relevante del código de inicio de la Biblioteca de periféricos estándar STM32 (para una cadena de herramientas GNU, los comentarios son míos):

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

Este código entrará en un bucle infinito cuando main() regrese, aunque de una manera no obvia ( se bl maincarga lrcon la dirección de la siguiente instrucción que es efectivamente un salto a sí mismo). No se intenta detener la CPU o hacer que entre en un modo de bajo consumo, etc. Si tiene una necesidad legítima de algo de eso en su aplicación, tendrá que hacerlo usted mismo.

Tenga en cuenta que, como se especifica en ARMv7-M ARM A2.3.1, el registro de enlace se establece en 0xFFFFFFFF en el reinicio, y una bifurcación a esa dirección activará una falla. Entonces, los diseñadores de Cortex-M decidieron tratar un retorno del controlador de reinicio como anormal, y es difícil discutir con ellos.

Hablando de una necesidad legítima de detener la CPU después de que finalice el firmware, es difícil imaginar algo que no sea mejor atendido por un apagado de su dispositivo. (Si deshabilita su CPU "para siempre", lo único que se le puede hacer a su dispositivo es un ciclo de encendido o un reinicio de hardware externo). de otra manera, como lo hace una PC ATX.

"Puede despertarse de los modos de suspensión (así es como se usan normalmente), e incluso una CPU bloqueada aún puede ejecutar un controlador NMI (este es el caso de Cortex-M)". <-- suena como la parte increíble de trama de un libro o de una película. :)
El "bl main" cargará "lr" con la dirección de la siguiente instrucción (el "bx lr"), ¿no es así? ¿Hay alguna razón para esperar que "lr" contenga algo más cuando se ejecuta "bx lr"?
@supercat: tienes razón, por supuesto. Edité mi respuesta para eliminar el error y expandirlo un poco. Pensando en esto, la forma en que implementan este ciclo es bastante extraña; podrían haberlo hecho fácilmente loop: b loop. Me pregunto si en realidad tenían la intención de hacer una devolución pero se olvidaron de guardar lr.
es curioso Esperaría que una gran cantidad de código ARM saliera con LR con el mismo valor que tenía en la entrada, pero no sé si está garantizado. Tal garantía a menudo no sería útil, pero mantenerla requeriría agregar una instrucción a las rutinas que copian r14 a algún otro registro y luego llaman a alguna otra rutina. Si lr se considera "desconocido" al regresar, uno podría "bx" el registro que contiene la copia guardada. Sin embargo, eso causaría un comportamiento muy extraño con el código indicado.
En realidad, estoy bastante seguro de que se espera que las funciones que no son hojas guarden lr. Por lo general, estos empujan lr a la pila en el prólogo y regresan colocando el valor guardado en pc. Esto es lo que haría, por ejemplo, un main() de C o C++, pero los desarrolladores de la biblioteca en cuestión obviamente no hicieron nada como esto en Reset_Handler.
@Thorn: Me pregunto con qué frecuencia los métodos regresan con LR igual a la dirección de retorno, dado que si un método usa STM para guardar varios registros, incluido LR en la entrada, la forma más económica de restaurarlos es con un LDM que usa PC en lugar de LR.

Al preguntar sobre return, estás pensando en un nivel demasiado alto. El código C se traduce a código máquina. Entonces, si en cambio piensas en el procesador extrayendo ciegamente las instrucciones de la memoria y ejecutándolas, no tiene idea de cuál es la "final" return. Por lo tanto, los procesadores no tienen un final inherente, sino que depende del programador manejar el caso final. Como señala Leon en su respuesta, los compiladores han programado un comportamiento predeterminado, pero muchas veces el programador puede querer su propia secuencia de apagado (he hecho varias cosas, como ingresar a un modo de bajo consumo y detenerme, o esperar a que se conecte un cable USB y luego reiniciar).

Muchos microprocesadores tienen instrucciones de detención, que detienen el procesador sin afectar los periféricos. Otros procesadores pueden depender de la "detención" simplemente saltando a la misma dirección repetidamente. Hay muchas opciones, pero depende del programador porque el procesador simplemente seguirá leyendo las instrucciones de la memoria, incluso si esa memoria no tenía la intención de ser instrucciones.

El problema no está incrustado (un sistema incrustado puede ejecutar Linux o incluso Windows), sino independiente o básico: el programa de aplicación (compilado) es lo único que se ejecuta en la computadora (no importa si es un microcontrolador o microprocesador).

Para la mayoría de los idiomas, el idioma no define lo que sucede cuando el 'principal' termina y no hay un sistema operativo al que volver. Para C, depende de lo que haya en el archivo de inicio (a menudo crt0.s). En la mayoría de los casos, el usuario puede (o incluso debe) proporcionar el código de inicio, por lo que la respuesta final es: lo que escriba es el código de inicio, o lo que se encuentre en el código de inicio que especifique.

En la práctica hay 3 enfoques:

  • no tome medidas especiales. qué sucede cuando los retornos principales no están definidos.

  • salte a 0, o use cualquier otro medio para reiniciar la aplicación.

  • entrar en un bucle cerrado (o deshabilitar las interrupciones y ejecutar una instrucción de detención), bloqueando el procesador para siempre.

Lo que es apropiado depende de la aplicación. Una tarjeta de felicitación fur-elise y un sistema de control de frenos (solo por mencionar dos sistemas integrados) probablemente deberían reiniciarse. La desventaja de reiniciar es que el problema puede pasar desapercibido.

Estaba mirando un código ATtiny45 desensamblado (C++ compilado por avr-gcc) el otro día y lo que hace al final del código es saltar a 0x0000. Básicamente haciendo un reinicio/reinicio.

Si el compilador/ensamblador omite ese último salto a 0x0000, todos los bytes en la memoria del programa se interpretan como código de máquina 'válido' y se ejecuta hasta que el contador del programa llega a 0x0000.

En AVR, un byte 00 (valor predeterminado cuando una celda está vacía) es NOP = Sin operación. Así que se ejecuta muy rápido, sin hacer nada más que tomar algo de tiempo.

Por lo general, el código compilado mainse vincula luego con el código de inicio (puede estar integrado en la cadena de herramientas, proporcionado por el proveedor del chip, escrito por usted, etc.).

Linker luego coloca toda la aplicación y el código de inicio en segmentos de memoria, por lo que la respuesta a sus preguntas depende de: 1. código de inicio, porque puede, por ejemplo:

  • terminar con bucle vacío ( bl lro b .), que será similar a "fin de programa", pero las interrupciones y los periféricos habilitados anteriormente seguirán funcionando,
  • finalice con un salto al comienzo del programa (ya sea completamente reiniciando el inicio o solo hasta main).
  • simplemente ignore "lo que será lo próximo" después de llamar a las maindevoluciones.

    1. En la tercera viñeta, cuando el contador del programa simplemente se incremente después de regresar del maincomportamiento, dependerá de su enlazador (y/o la secuencia de comandos del enlazador utilizada durante la vinculación).
  • Si se coloca otra función/código después de usted main, se ejecutará con valores de argumento no válidos/indefinidos,

  • Si la siguiente memoria comienza con una instrucción incorrecta, es posible que se genere una excepción y la MCU finalmente se reiniciará (si la excepción genera un reinicio).

Si el perro guardián está habilitado, eventualmente restablecerá la MCU a pesar de todos los bucles interminables en los que se encuentre (por supuesto, si no se recargará).

La mejor manera de detener un dispositivo integrado es esperar una eternidad con las instrucciones NOP.

La segunda forma es cerrar el dispositivo usando el propio dispositivo. Si puede controlar un relé con sus instrucciones, puede simplemente abrir el interruptor que está alimentando su dispositivo integrado y huh su dispositivo integrado se ha ido sin consumo de energía.

Eso realmente no responde la pregunta.

Estaba claramente explicado en el manual. Por lo general, la CPU lanzará una excepción general porque accederá a una ubicación de memoria que está fuera del segmento de pila. [ excepción de protección de memoria ].

¿Qué quiso decir con el sistema integrado? ¿Microprocesador o microcontrolador? De cualquier manera, está definido en el manual.

En CPU x86 apagamos la computadora enviando el comando al controlador ACIP. Entrar en el modo de gestión del sistema. Entonces ese controlador es un chip de E/S y no necesita apagarlo manualmente.

Lea la especificación ACPI para obtener más información.

-1: el TS no mencionó ninguna CPU específica, así que no asuma demasiado. Diferentes sistemas manejan este caso de maneras muy diferentes.