Que se passe-t-il lorsqu'un programme intégré se termine?


29

Que se passe-t-il dans un processeur intégré lorsque l'exécution atteint cette returndéclaration finale? Tout se bloque tel quel; consommation d'énergie, etc., avec un long NOP éternel dans le ciel? ou les NOP sont-ils exécutés en continu, ou un processeur sera-t-il complètement arrêté?

Je demande en partie si je me demande si un processeur doit être mis hors tension avant de terminer l'exécution et s'il le fait, comment finit-il jamais l'exécution s'il a déjà été mis hors tension?


22
Cela dépend de vos croyances. Certains disent qu'il se réincarnera.
Telaclavo

9
Est-ce pour un missile?
Lee Kowalkowski

6
certains systèmes prennent en charge l' instruction HCF (Halt and Catch Fire) . :)
Stefan Paul Noack

1
cela va se ramifier à la routine d'auto-destruction

Réponses:


41

C'est une question que mon père me posait toujours. " Pourquoi ne passe-t-il pas par toutes les instructions et ne s'arrête-t-il pas à la fin? "

Jetons un coup d'œil à un exemple pathologique. Le code suivant a été compilé dans le compilateur C18 de Microchip pour le PIC18:

void main(void)
{

}

Il produit la sortie d'assembleur suivante:

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

Comme vous pouvez le voir, main () est appelée et contient à la fin une déclaration de retour, bien que nous ne l'ayons pas explicitement placée là-bas. Lorsque principal revient, le CPU exécute l'instruction suivante qui est simplement un GOTO pour revenir au début du code. main () est simplement appelée maintes et maintes fois.

Cela dit, ce n'est pas ainsi que les gens font les choses habituellement. Je n'ai jamais écrit de code embarqué qui permettrait à main () de sortir comme ça. Généralement, mon code ressemblerait à ceci:

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

Donc je ne laisserais jamais normalement main () sortir.

"OK ok" vous dites. Tout cela est très intéressant car le compilateur s'assure qu'il n'y a jamais de dernière déclaration de retour. Mais que se passe-t-il si nous forçons le problème? Et si je codais à la main mon assembleur et ne remettais pas au début?

Eh bien, évidemment, le CPU continuerait simplement d'exécuter les prochaines instructions. Cela ressemblerait à ceci:

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

L'adresse mémoire suivante après la dernière instruction dans main () est vide. Sur un microcontrôleur avec mémoire FLASH, une instruction vide contient la valeur 0xFFFF. Sur un PIC au moins, ce code op est interprété comme un «nop» ou «aucune opération». Cela ne fait tout simplement rien. Le CPU continuerait à exécuter ces nops jusqu'à la fin de la mémoire.

Qu'y a-t-il après ça?

À la dernière instruction, le pointeur d'instruction du CPU est 0x7FFe. Lorsque le CPU ajoute 2 à son pointeur d'instructions, il obtient 0x8000, ce qui est considéré comme un débordement sur un PIC avec seulement 32k FLASH, et il revient donc à 0x0000, et le CPU continue joyeusement à exécuter les instructions au début du code , comme s'il avait été réinitialisé.


Vous avez également demandé s'il fallait éteindre. Fondamentalement, vous pouvez faire ce que vous voulez et cela dépend de votre application.

Si vous aviez une application qui n'avait besoin que de faire une chose après la mise sous tension, puis de ne rien faire d'autre, vous pourriez simplement mettre un certain temps (1); à la fin de main () afin que le CPU cesse de faire quoi que ce soit de notable.

Si l'application nécessite la mise hors tension du CPU, alors, selon le CPU, il y aura probablement différents modes de veille disponibles. Cependant, les processeurs ont l'habitude de se réveiller à nouveau, vous devez donc vous assurer qu'il n'y a pas de limite de temps pour le sommeil, et qu'aucune horloge de surveillance n'est active, etc.

Vous pouvez même organiser des circuits externes qui permettraient au processeur de couper complètement sa propre alimentation une fois terminé. Voir cette question: Utilisation d'un bouton-poussoir momentané comme interrupteur à bascule on-off à verrouillage .


20

Pour le code compilé, cela dépend du compilateur. Le compilateur Rowley CrossWorks gcc ARM que j'utilise passe au code dans le fichier crt0.s qui a une boucle infinie. Le compilateur Microchip C30 pour les périphériques 16 bits dsPIC et PIC24 (également basé sur gcc) réinitialise le processeur.

Bien sûr, la plupart des logiciels embarqués ne se terminent jamais ainsi et exécutent du code en continu en boucle.


13

Il y a deux points à souligner ici:

  • Un programme embarqué, à proprement parler, ne peut pas "terminer".
  • Il est très rarement nécessaire d'exécuter un programme intégré pendant un certain temps, puis de «terminer».

Le concept d'arrêt d'un programme n'existe normalement pas dans un environnement intégré. À un niveau bas, un CPU exécutera des instructions tant qu'il le pourra; il n'y a pas de "déclaration de retour final". Un CPU peut arrêter l'exécution s'il rencontre un défaut irrécupérable ou s'il est explicitement arrêté (mis en mode veille, mode basse consommation, etc.), mais notez que même les modes veille ou les défauts irrécupérables ne garantissent généralement pas que plus de code ne va être exécuté. Vous pouvez vous réveiller des modes veille (c'est ainsi qu'ils sont normalement utilisés), et même un processeur verrouillé peut toujours exécuter un gestionnaire NMI (c'est le cas pour Cortex-M). Un chien de garde fonctionnera également, et vous ne pourrez peut-être pas le désactiver sur certains microcontrôleurs une fois qu'il est activé. Les détails varient considérablement d'une architecture à l'autre.

Dans le cas d'un firmware écrit dans un langage tel que C ou C ++, ce qui se passe si main () se termine est déterminé par le code de démarrage. Par exemple, voici la partie pertinente du code de démarrage de la bibliothèque périphérique standard STM32 (pour une chaîne d'outils GNU, les commentaires sont les miens):

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

Ce code entrera dans une boucle infinie lors du retour de main (), bien que de manière non évidente ( bl maincharge lravec l'adresse de l'instruction suivante qui est en fait un saut vers lui-même). Aucune tentative n'est faite pour arrêter le processeur ou le faire passer en mode basse consommation, etc. Si vous avez un besoin légitime de tout cela dans votre application, vous devrez le faire vous-même.

Notez que comme spécifié dans ARMv7-M ARM A2.3.1, le registre de liaison est défini sur 0xFFFFFFFF lors de la réinitialisation, et une branche à cette adresse déclenchera une erreur. Les concepteurs de Cortex-M ont donc décidé de traiter un retour du gestionnaire de réinitialisation comme anormal, et il est difficile de discuter avec eux.

En parlant d'un besoin légitime d'arrêter le CPU après la fin du firmware, il est difficile d'imaginer celui qui ne serait pas mieux servi par une mise hors tension de votre appareil. (Si vous désactivez votre CPU "pour de bon", la seule chose qui peut être faite à votre appareil est un cycle d'alimentation ou une réinitialisation matérielle externe.) Vous pouvez désactiver le signal ENABLE pour votre convertisseur DC / DC ou couper l'alimentation en d'une autre manière, comme le fait un PC ATX.


1
"Vous pouvez vous réveiller des modes veille (c'est ainsi qu'ils sont normalement utilisés), et même un processeur verrouillé peut toujours exécuter un gestionnaire NMI (c'est le cas pour Cortex-M)." <- sonne comme la partie impressionnante de un livre ou un film. :)
Mark Allen

Le "bl main" chargera "lr" avec l'adresse de l'instruction suivante (le "bx lr"), n'est-ce pas? Y a-t-il une raison de s'attendre à ce que "lr" contienne autre chose lorsque le "bx lr" est exécuté?
supercat

@supercat: vous avez bien sûr raison. J'ai modifié ma réponse pour supprimer l'erreur et la développer un peu. En y réfléchissant, la façon dont ils implémentent cette boucle est assez étrange; ils auraient pu facilement le faire loop: b loop. Je me demande s'ils voulaient vraiment faire un retour mais ont oublié d'enregistrer lr.
Thorn le

C'est curieux. Je m'attendrais à ce que beaucoup de code ARM se termine avec LR conservant la même valeur qu'il détenait à l'entrée, mais je ne sais pas s'il est garanti. Une telle garantie ne serait pas souvent utile, mais pour la maintenir, il faudrait ajouter une instruction aux routines qui copient r14 dans un autre registre puis appellent une autre routine. Si lr est considéré comme "inconnu" au retour, on pourrait "bx" le registre contenant la copie sauvegardée. Cela provoquerait cependant un comportement très étrange avec le code indiqué.
supercat

En fait, je suis assez sûr que les fonctions non-feuilles devraient sauver lr. Ceux-ci poussent généralement lr sur la pile dans le prologue et reviennent en insérant la valeur enregistrée dans pc. C'est ce que ferait par exemple un C (ou C ++ main (), mais les développeurs de la bibliothèque en question n'ont évidemment rien fait de tel dans Reset_Handler.
Thorn

9

Lorsque vous posez des questions sur return, vous pensez trop haut niveau. Le code C est traduit en code machine. Donc, si vous pensez plutôt au processeur qui extrait aveuglément des instructions de la mémoire et les exécute, il n'a aucune idée de laquelle est la "finale" return. Ainsi, les processeurs n'ont pas de fin inhérente, mais à la place, c'est au programmeur de gérer le cas final. Comme Leon le souligne dans sa réponse, les compilateurs ont programmé un comportement par défaut, mais souvent le programmeur peut vouloir sa propre séquence d'arrêt (j'ai fait diverses choses comme entrer dans un mode basse consommation et s'arrêter, ou attendre qu'un câble USB soit branché dans puis redémarrer).

De nombreux microprocesseurs ont des instructions d'arrêt, ce qui arrête le processeur sans affecter les périphériques. D'autres processeurs peuvent compter sur "l'arrêt" en sautant simplement à la même adresse à plusieurs reprises. Il existe plusieurs options, mais cela dépend du programmeur car le processeur continuera simplement à lire les instructions de meory, même si cette mémoire n'était pas destinée à être des instructions.


7

Le problème n'est pas embarqué (un système embarqué peut exécuter Linux ou même Windows) mais autonome ou bare-metal: le programme d'application (compilé) est la seule chose qui s'exécute sur l'ordinateur (Peu importe s'il s'agit d'un microcontrôleur ou microprocesseur).

Pour la plupart des langues, la langue ne définit pas ce qui se passe lorsque le «principal» se termine et qu'il n'y a pas de système d'exploitation sur lequel revenir. Pour C, cela dépend de ce qui se trouve dans le fichier de démarrage (souvent crt0.s). Dans la plupart des cas, l'utilisateur peut (ou même doit) fournir le code de démarrage, donc la réponse finale est: tout ce que vous écrivez est le code de démarrage, ou ce qui se trouve dans le code de démarrage que vous spécifiez.

En pratique, il existe 3 approches:

  • ne prenez aucune mesure spéciale. ce qui se passe lorsque le retour principal n'est pas défini.

  • passer à 0 ou utiliser tout autre moyen pour redémarrer l'application.

  • entrez dans une boucle étroite (ou désactivez les interruptions et exécutez une instruction d'arrêt), verrouillant le processeur pour toujours.

Ce qui est approprié dépend de l'application. Une carte de voeux en fourrure et un système de contrôle des freins (pour ne citer que deux systèmes embarqués) devraient probablement redémarrer. L'inconvénient du redémarrage est que le problème peut passer inaperçu.


5

Je regardais un peu de code ATtiny45 démonté (C ++ compilé par avr-gcc) l'autre jour et ce qu'il fait à la fin du code est de passer à 0x0000. Fondamentalement, faire une réinitialisation / redémarrage.

Si ce dernier saut à 0x0000 est omis par le compilateur / assembleur, tous les octets dans la mémoire du programme sont interprétés comme du code machine `` valide '' et il s'exécute jusqu'à ce que le compteur du programme passe à 0x0000.

Sur AVR, un octet de 00 (valeur par défaut lorsqu'une cellule est vide) est un NOP = aucune opération. Cela fonctionne donc très rapidement, ne faisant rien mais prenant juste un peu de temps.


1

Le maincode généralement compilé est ensuite lié au code de démarrage (il peut être intégré dans la chaîne d'outils, fourni par le vendeur de puces, écrit par vous, etc.).

Linker place ensuite tous les codes d'application et de démarrage dans des segments de mémoire, donc la réponse à vos questions dépend: 1. du code du démarrage, car il peut par exemple:

  • se termine avec une boucle vide ( bl lrou b .), qui sera similaire à "fin de programme", mais les interruptions et les périphériques activés précédemment fonctionneront toujours,
  • se terminent par un saut au début du programme (soit relancer complètement le démarrage, soit passer à main).
  • ignorez simplement «ce qui sera le prochain» après l'appel aux mainretours.

    1. Dans la troisième puce, lorsque le compteur de programme incrémente simplement après le retour du maincomportement dépendra de votre éditeur de liens (et / ou script de l'éditeur de liens utilisé lors de la liaison).
  • Si d'autres fonctions / codes sont placés après votre, mainils seront exécutés avec des valeurs d'arguments invalides / non définies,

  • Si la mémoire suivante commence avec une mauvaise instruction, une exception peut être générée et le MCU finira par se réinitialiser (si une exception génère une réinitialisation).

Si le chien de garde est activé, il réinitialisera éventuellement le MCU malgré toutes les boucles sans fin dans lesquelles vous vous trouvez (bien sûr s'il ne sera pas rechargé).


-1

La meilleure façon d'arrêter un périphérique intégré est d'attendre indéfiniment avec les instructions NOP.

La deuxième façon consiste à fermer l'appareil en utilisant l'appareil lui-même. Si vous pouvez contrôler un relais avec vos instructions, vous pouvez simplement ouvrir le commutateur qui alimente votre appareil intégré et hein votre appareil intégré est parti sans consommation d'énergie.


Cela ne répond vraiment pas à la question.
Matt Young

-4

C'était clairement expliqué dans le manuel. En règle générale, une exception générale est levée par le processeur car il accède à un emplacement de mémoire qui se trouve en dehors du segment de pile. [exception de protection de la mémoire].

Qu'entendiez-vous par système embarqué? Microprocesseur ou microcontrôleur? Dans les deux cas, c'est défini dans le manuel.

Dans le processeur x86, nous éteignons l'ordinateur en envoyant la commande au contrôleur ACIP. Accès au mode de gestion du système. Ce contrôleur est donc une puce d'E / S et vous n'avez pas besoin de l'éteindre manuellement.

Lisez la spécification ACPI pour plus d'informations.


3
-1: le TS n'a pas mentionné de CPU spécifique, alors ne supposez pas trop. Différents systèmes traitent ce cas de manières très différentes.
Wouter van Ooijen
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.