¿Qué sucede cuando finaliza un programa incrustado?

29

Lo que sucede en un procesador incorporado cuando la ejecución alcanza esa returndeclaración final ¿Todo se congela tal como está? consumo de energía, etc., con un NOP eterno 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 porque me pregunto si un procesador necesita apagarse antes de finalizar la ejecución y, si lo hace, ¿cómo termina alguna vez la ejecución si se ha apagado antes?

Toby
fuente
22
Depende de tus creencias. Algunos dicen que reencarnará.
Telaclavo
9
¿Es por un misil?
Lee Kowalkowski
66
algunos sistemas admiten la instrucción HCF (Halt and Catch Fire) . :)
Stefan Paul Noack
1
pasará a la rutina de autodestrucción

Respuestas:

41

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

Echemos un vistazo a un ejemplo patológico. El siguiente código fue compilado en el compilador C18 de Microchip para el 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 devolución, aunque no lo pusimos explícitamente allí. Cuando main regresa, la CPU ejecuta la siguiente instrucción, que es simplemente un GOTO para volver al comienzo del código. main () simplemente se llama una y otra vez.

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

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

Por lo tanto, normalmente nunca dejaría que salga main ().

"OK ok" estás diciendo. Todo esto es muy interesante porque el compilador se asegura de que nunca haya una última declaración de devolución. Pero, ¿qué sucede si forzamos el problema? ¿Qué pasa si codifico mi ensamblador a mano y no vuelvo al principio?

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

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 operativo se interpreta como 'nop' o 'no operación'. Simplemente no hace nada. La CPU continuaría ejecutando esos nops hasta el final de la memoria.

¿Qué hay después de eso?

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


También preguntaste sobre la necesidad de apagar. Básicamente puedes hacer lo que quieras, y depende de tu aplicación.

Si tuvieras una aplicación que solo necesitara hacer una cosa después del encendido, y luego no hacer nada más, solo podrías poner un momento (1); al final de main () para que la CPU deje de hacer algo notable.

Si la aplicación requería que la CPU se apagara, entonces, dependiendo de la CPU, probablemente habrá varios modos de suspensión disponibles. Sin embargo, las CPU tienen la costumbre de volver a despertarse, por lo que debe asegurarse de que no haya un límite de tiempo para el sueño, y que no haya un temporizador Watch Dog activo, etc.

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

Rocketmagnet
fuente
20

Para el código compilado, depende del compilador. El compilador ARM gcc Rowley CrossWorks que utilizo salta para codificar 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 basado en gcc) reinicia el procesador.

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

Leon Heller
fuente
13

Hay dos puntos a destacar aquí:

  • Un programa incrustado, estrictamente hablando, no puede "terminar".
  • Rara vez es necesario ejecutar un programa incrustado durante un tiempo y luego "finalizar".

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 (ponerla en modo de suspensión, un modo de baja potencia, etc.), pero tenga en cuenta que incluso los modos de suspensión o fallas irrecuperables generalmente no garantizan que no vaya a haber más código ser ejecutado. Puede despertar desde los modos de suspensión (así es como se usan normalmente), e incluso una CPU bloqueada puede ejecutar un controlador NMI (este es el caso de Cortex-M). También se ejecutará un watchdog, y es posible que no pueda deshabilitarlo 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 Periférica 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 ingresará 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 en sí mismo). No se hacen intentos para 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, deberá hacerlo usted mismo.

Tenga en cuenta que, como se especifica en el 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 provocará un error. Entonces, los diseñadores de Cortex-M decidieron tratar el 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 el firmware haya finalizado, 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 puede hacer con su dispositivo es un ciclo de energía o un reinicio externo del hardware). Puede desactivar una señal ENABLE para su convertidor DC / DC o apagar su fuente de alimentación en de otra manera, como lo hace una PC ATX.

Espina
fuente
1
"Puede despertarse desde los modos de suspensión (así es como se usan normalmente), e incluso una CPU bloqueada puede ejecutar un controlador NMI (este es el caso de Cortex-M)". <- suena como la parte increíble de una trama de libro o película. :)
Mark Allen
El "bl main" cargará "lr" con la dirección de la siguiente instrucción (el "bx lr"), ¿no? ¿Hay alguna razón para esperar que "lr" contenga algo más cuando se ejecuta "bx lr"?
supercat
@ 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 realmente querían hacer una devolución, pero se olvidaron de ahorrar lr.
Thorn
Es curioso Esperaría que saliera una gran cantidad de código ARM 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 en otro registro y luego llamar a alguna otra rutina. Si lr se considera "desconocido" en la devolución, se 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.
supercat
En realidad, estoy bastante seguro de que se espera que las funciones que no sean hojas salven lr. Por lo general, empujan lr a la pila en el prólogo y regresan haciendo estallar el valor guardado en la PC. Esto es lo que haría, por ejemplo, un C o C ++ main (), pero los desarrolladores de la biblioteca en cuestión obviamente no hicieron nada como esto en Reset_Handler.
Thorn
9

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

Muchos microprocesadores tienen instrucciones de detención, que detienen el procesador sin afectar los periféricos. Otros procesadores pueden confiar en "detenerse" 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.

Klox
fuente
7

El problema no es incrustado (un sistema embebido puede ejecutar Linux o incluso Windows), sino autónomo o completamente desnudo: 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 termina el 'principal' y no hay ningún sistema operativo al que volver. Para C, depende de lo que esté 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 esté en el código de inicio que especifique.

En la práctica hay 3 enfoques:

  • No tome medidas especiales. qué sucede cuando el rendimiento principal no está definido.

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

  • ingrese un bucle cerrado (o deshabilite las interrupciones y ejecute una instrucción de detención), bloqueando el procesador para siempre.

Lo 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.

Wouter van Ooijen
fuente
5

Estaba mirando un código ATtiny45 desmontado (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 el ú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 pase a 0x0000.

En AVR, un byte de 00 (valor predeterminado cuando una celda está vacía) es un NOP = Sin operación. Por lo tanto, funciona muy rápido, sin hacer nada más que tomarse un tiempo.

jippie
fuente
1

Generalmente, el maincódigo compilado se vincula luego con el código de inicio (puede integrarse en la cadena de herramientas, proporcionado por el proveedor del chip, escrito por usted, etc.).

Luego, Linker coloca todas las aplicaciones y el código de inicio en segmentos de memoria, por lo que la respuesta a sus preguntas depende de: 1. el código de inicio, porque puede, por ejemplo:

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

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

  • Si la siguiente memoria comienza con una instrucción incorrecta, se podría generar una excepción y MCU eventualmente se reiniciará (si la excepción genera un reinicio).

Si watchdog está habilitado, eventualmente restablecerá MCU a pesar de todos los bucles interminables en los que se encuentra (por supuesto, si no se volverá a cargar).

kwesolowski
fuente
-1

La mejor manera de detener un dispositivo integrado es esperar para siempre con las instrucciones de NOP.

La segunda forma es cerrar el dispositivo utilizando el dispositivo mismo. Si puede controlar un relé con sus instrucciones, simplemente puede abrir el interruptor que está alimentando su dispositivo incorporado y no habrá consumo de energía.

Enes Unal
fuente
Eso realmente no responde la pregunta.
Matt Young
-4

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

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

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

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

Sandun estándar
fuente
3
-1: el TS no mencionó ninguna CPU específica, así que no asumas demasiado. Los diferentes sistemas manejan este caso de maneras muy diferentes.
Wouter van Ooijen