18) Collision entre la PILE et le TAS. (Chapitre qui ne concerne que les programmeurs.)

 

Bien que non précisé dans la liste des conseils pour programmer avec méthode, car la page est bien assez remplie, ce serait dommage dans notre cheminement expérimental de ne pas aborder ce sujet, car forcément un jour ou l’autre vous risquerez d’en être la victime. Explications :
Phénomène particulièrement sournois, la collision de PILE survient brusquement sans qu’aucun signe avant-coureur ne nous prévienne. Pour comprendre de quoi il s’agit, il faut entrer dans la vie intime du fonctionnement des microcontrôleurs. Il serait hors propos dans ces lignes d’étudier à la loupe l’agencement matériel de l’ATmega328 et d’en détailler finement le fonctionnement interne. Nous allons dans ce chapitre nous en tenir au strict minimum vital.
Fonctionnement de la mémoire vive SRAM.
La mémoire vive (256 + 2Ko) est généralement divisée en quatre zones :
• Les 256 premiers octets pour les registres généraux du microcontrôleur (Représentée en jaune sur la Fig.85) occupent « le bas » de la SRAM. (En « assembleur » c’était la Page zéro ».)
• La zone nommée BSS qui contient toutes les variables globales, allouées statiquement au moment de l’édition de lien lors de la compilation. La BSS est utilisée par de nombreux compilateurs pour désigner une zone de données contenant les variables statiques définies dans les initialisations, et les déclarations avant void loop().
• Le TAS sur lequel on entasse du bas vers le haut est destiné aux allocations dynamiques dans lequel on peut attribuer et libérer des blocs de mémoire. (Nommé HEAP) Le TAS se fragmente généralement au cours de l’évolution du programme, (Car une variable locale libérant de la place laisse « un trou » libre.) avec un risque notable de le rendre inutilisable.
Défragmenter HEAP par une séquence de code de type « Ramasse miettes » est faisable mais relativement dangereux, car si l’on déplace une variable en cours d’utilisation, les conséquences peuvent s’avérer ingérables.
• La PILE nommée STACK mémorise temporairement :
* Les paramètres associés à l’appel des fonctions et procédures,
        * Les adresses de retour des fonctions et procédures,
* Les variables locales aux fonctions et procédures.
La PILE est une zone de mémoire commençant en haut de la SRAM qui se charge vers le bas de façon linéaire et continue lors des appels des fonctions ou des procédures. Elle se réduit vers le haut lors des retours. Chaque appel à une procédure empile l’adresse, chaque retour la dépile et libère la place. Au cours du programme, si un grand nombre de données sont sur le TAS qui est « très haut », et que l’on enchaîne un grand nombre d’appels à procédures sans retour, (Cas des subroutines récursives par exemple) il peut arriver que l’espace entre PILE et TAS devienne nul. C’est la collision et l’écrasement mutuel des OCTETs engendre un fonctionnement totalement imprévisible du microcontrôleur. Aussi, avant de considérer que notre programme est fiable, il faut impérativement vérifier que le risque de collision de PILE est dérisoire. L’expérience montre que lorsque toutes les initialisations de void setup() sont terminées, il est recommandé de ne pas avoir moins de 100 octets, car le risque de collision par fragmentation de la zone devient exagéré.

(@) : Sur la Fig.125 les noms des divers pointeurs sont imposés par le compilateur de l’IDE.

Prendre une assurance contre les collisions de PILE.

Invoquer du préventif dans ce domaine n’est pas une arme absolue. Observez le programme d’exploitation provisoire P16_Avec_Menu_RESET.ino dans lequel, dans la procédure nommée void Menu_Version() se trouve la séquence suivante qui affiche l’espace entre la PILE et le TAS à ce stade de l’exécution où l’ensemble du contexte est en place :


Juste au dessus on trouve la fonction qui calcule la valeur de l’espace disponible :


Le résultat sur la Fig.126 (Qui n’est qu’un clone de la Fig.120 pour nous éviter d’avoir à tourner les pages.) pour notre programme d’exploitation montre qu’avec une telle marge de sécurité, si notre logiciel se met à faire des choses étranges quand on l’a modifié, c’est que l’on a commis une erreur de logique, car ici la collision de PILE n’est pas vraisemblable. Notez au passage que le compilateur indique une place disponible pour les variables locales de 1043 octets ce qui n’est pas directement lié avec la RAM réservée aux données dynamiques. Enfin, je vous recommande très très fortement de toujours effectuer ce test quand vous venez de terminer un programme. Pour un skech qui consomme la presque totalité des 30720 OCTETS disponibles, on peut manquer de place pour loger ce test. Il importe alors de supprimer un ou deux appels à procédures (Ce qui ne modifie pas l’évaluation) pour faire provisoirement de la place. Une fois la marge de sécurité vérifiée, vous rétablissez les lignes provisoirement passées en remarques et l’affaire est définitivement classée !
Avant de clore définitivement ce chapitre il me semble utile de passer en revue les éléments de programmation qui influencent le plus le risque de collision de PILE de façon à ce que vous puissiez aborder sereinement le problème si d’infortune votre programme en était victime :

Configuration qui produit la collision de PILE avec le TAS.

Concrètement on oublie royalement qu’un nombre important d’interruptions se produisent en tâche de fond. Quelquefois un codeur rotatif génère des interruptions, sans compter les procédures delay(), les fonctions telles que millis(), la PWM … une foule de ressources internes déclenche des interruptions. C’est transparent pour l’utilisateur car c’est le compilateur C++ qui sur ces instructions fait sa cuisine interne. Arrive un moment, ou trop de données sont empilée sur le TAS et viennent écraser les adresses empilées. Puis le délimiteur ‘}‘ de fin d’une procédure ou d’une fonction demande au processeur de dépiler une adresse de retour. Comme cette dernière contient les résidus de la variable qui a « écrasé » les octets, le programme se « branche » strictement n’importe où. Étant alors sur du code objet incohérent, le comportement du logiciel devient totalement aléatoire.
Par exemple vous avez changé un texte « Salut » en « BONJOUR les amis« . Suite à cette broutille le programme diverge complètement ou « se fige ». Ce n’est manifestement pas un problème de logique. Il ne reste plus dans un tel cas qu’à diagnostiquer l’éventualité d’une collision de PILE.
PRÉVENTIF : Aucun programmeur n’est à l’abri d’une telle « catastrophe ». Aussi, pour minimiser les risques il faut placer le minimum de chaînes de caractères dans le programme car en réalité elles sont placées sur le TAS. Il faut également (Et surtout.) minimiser les tableaux.
CURATIF : Quand se produit le Scratchhh prouitchhh bom bring protchhhh ! c’est qu’il est trop tard. Nous avons placé plein plein plein de bavardages, alors que nous savons que ces « bla bla bla » sont entassés dans la mémoire dynamique. Notre démonstrateur comporte une foule de procédures et de fonctions qui passent des paramètres, sans compter les for (byte I=1, …) qui ne sont pas gratuits. En effet, les variables locales des boucles for doivent aussi être logées en RAM.
REMÈDE : Dégager impérativement de la place sur le TAS.

Errare humanum est, perseverare diabolicum.

Obnubilé par la rédaction du didacticiel et surtout par la mise en page, j’ai totalement oublié dans le MENU du RESET de décrire l’effet obtenu lorsque l’on active avec un clic court la touche de DROITE. En apparence il ne se passe rien. Dans la pratique, le programme remplit l’espace mémoire dédié aux enregistrements par une trace Longue simulant une sinusoïde amortie. On ne se rendra compte de sa présence que si dans le MENU de BASE on fait afficher la TRACE avant de déclencher une quelconque numérisation ou la récupération d’un enregistrement EEPROM. Ce correctif au tutoriel étant effectué, nous pouvons poursuivre notre chemin.

La suite est ici.