¿Qué sucede en un procesador integrado cuando la ejecución llega a esa return
declaració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?
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í:
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 main
carga lr
con 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.
loop: b loop
. Me pregunto si en realidad tenían la intención de hacer una devolución pero se olvidaron de guardar 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 main
se 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:
bl lr
o b .
), que será similar a "fin de programa", pero las interrupciones y los periféricos habilitados anteriormente seguirán funcionando,main
).simplemente ignore "lo que será lo próximo" después de llamar a las main
devoluciones.
main
comportamiento, 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 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.
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.
Telaclavo
Lee Kowalkowski
Stefan Paul Noack
usuario924