"Avec l'expérience, une approche scientifique standard pour calculer la bonne taille de pile vient: prendre un nombre aléatoire et espérer le meilleur."
- Jack Ganssle, «L'art de concevoir des systèmes embarqués»Bonjour, Habr!
Aussi étrange que cela puisse paraître, dans la grande majorité des «amorces STM32» que j'ai vues en particulier et des microcontrôleurs en général, il n'y a généralement rien de tel que l'allocation de mémoire, le placement de la pile et, plus important encore, la prévention du débordement de mémoire - à la suite de quoi une zone en frise une autre et tout s'écroule, généralement avec des effets enchanteurs.
Cela est dû en partie à la simplicité des projets de formation effectués sur des cartes de débogage avec des microcontrôleurs relativement gras, où il est difficile de survivre à une pénurie de mémoire en faisant clignoter une LED - cependant, récemment, même pour les amateurs débutants, les références, par exemple, aux contrôleurs de type STM32F030F4P6 sont de plus en plus courantes. , facile à installer, vaut un sou, mais aussi avec une unité de mémoire de kilo-octets.
De tels contrôleurs vous permettent de faire des choses assez graves pour vous-même (eh bien, ici, par exemple, une
mesure aussi parfaitement adaptée a été faite pour nous sur STM32F042K6T6 avec 6 Ko de RAM, dont un peu plus de 100 octets restent libres), mais lorsque vous traitez avec de la mémoire, vous avez besoin d'une certaine quantité de mémoire netteté.
Je veux parler de cette précision. L'article sera court, les professionnels n'apprendront rien de nouveau - mais pour les débutants cette connaissance est fortement recommandée.
Dans un projet typique sur un microcontrôleur basé sur un noyau Cortex-M, la RAM a une division conditionnelle en quatre sections:
- data - données initialisées par une valeur spécifique
- bss - données initialisées à zéro
- tas - tas (zone dynamique à partir de laquelle la mémoire est allouée explicitement à l'aide de malloc)
- stack - la pile (la région dynamique à partir de laquelle la mémoire est allouée par le compilateur implicitement)
La zone noinit peut également apparaître occasionnellement (variables non initialisées - elles sont pratiques en ce qu'elles conservent la valeur entre les redémarrages), encore moins souvent, certaines autres zones allouées pour des tâches spécifiques.
Ils sont situés dans la mémoire physique d'une manière assez spécifique - le fait est que la pile des microcontrôleurs sur les cœurs ARM croît de haut en bas. Par conséquent, il est situé séparément des blocs de mémoire restants, à la fin de la RAM:

Par défaut, son adresse est généralement égale à la dernière adresse RAM, et à partir de là, elle descend à mesure qu'elle grandit - et une caractéristique extrêmement désagréable de la pile en sort: elle peut atteindre bss et réécrire son sommet, et vous ne le saurez pas de manière explicite.
Zones de mémoire statiques et dynamiques
Toute la mémoire est divisée en deux catégories - allouées statiquement, c'est-à-dire mémoire dont le montant total est évident d'après le texte du programme et ne dépend pas de l'ordre de son exécution, et alloué dynamiquement, dont le volume requis dépend de l'avancement du programme.
Ce dernier comprend un tas (à partir duquel nous prenons des morceaux en utilisant malloc et retournons en utilisant free) et une pile qui grandit et se rétrécit d'elle-même.
De manière générale, l'utilisation de malloc sur les microcontrôleurs est
fortement déconseillée, sauf si vous savez exactement ce que vous faites. Le principal problème qu'ils apportent est la fragmentation de la mémoire - si vous allouez 10 morceaux de 10 octets, puis les libérez toutes les secondes, vous n'obtiendrez pas 50 octets gratuits. Vous obtiendrez 5 pièces gratuites de 10 octets chacune.
De plus, au stade de la compilation du programme, le compilateur ne sera pas en mesure de déterminer automatiquement la quantité de mémoire dont votre malloc aura besoin (en particulier en tenant compte de la fragmentation, qui dépend non seulement de la taille des pièces demandées, mais de la séquence de leur allocation et de leur libération), et ne pourra donc pas vous avertir si à la fin il n'y a pas assez de mémoire.
Il existe des méthodes pour contourner ce problème - implémentations spéciales de malloc qui fonctionnent dans une zone allouée statiquement, et non toute la RAM, utilisation prudente de malloc en tenant compte de la fragmentation possible au niveau de la logique du programme, etc. - mais en général
malloc vaut mieux ne pas toucher .
Toutes les zones de mémoire avec des limites et des adresses sont enregistrées dans un fichier avec l'extension .LD, sur lequel l'éditeur de liens est orienté lors de la construction du projet.
Mémoire allouée statiquement
Ainsi, à partir de la mémoire allouée statiquement, nous avons deux zones - bss et data, qui ne diffèrent que formellement. Lorsque le système est initialisé, le bloc de données est copié à partir du flash, où les valeurs d'initialisation nécessaires sont stockées pour lui, le bloc bss est simplement rempli de zéros (au moins le remplir de zéros est considéré comme une bonne forme).
Les deux choses - copier à partir d'un flash et remplir de zéros - sont effectuées dans le code du programme
sous une forme explicite , mais pas dans votre main (), mais dans un fichier séparé qui est exécuté en premier, il est écrit une fois et simplement glissé de projet en projet.
Cependant, ce n'est pas ce qui nous intéresse maintenant - mais comment nous comprendrons si nos données s'insèrent même dans la RAM de notre contrôleur.
Il est reconnu très simplement - par l'utilitaire arm-none-eabi-size avec un seul paramètre - le fichier ELF compilé de notre programme (souvent son appel est inséré à la fin du Makefile, car c'est pratique):

Ici, le texte est la quantité de données de programme se trouvant dans le flash, et bss et les données sont nos zones allouées statiquement dans la RAM. Les deux dernières colonnes ne nous dérangent pas - c'est la somme des trois premières, elle n'a pas de sens pratique.
Au total, statiquement dans la RAM, nous avons besoin de bss + octets de données, dans ce cas - 5324 octets. Le contrôleur dispose de 6144 octets de RAM, nous n'utilisons pas de malloc, il reste 820 octets.
Ce qui devrait nous suffire sur la pile.
Mais assez? Parce que sinon, notre pile se développera en nos propres données, puis elle écrasera d'abord les données, puis les données les écraseront, et ensuite tout plantera. De plus, entre le premier et le deuxième point, le programme peut continuer à fonctionner sans se rendre compte qu'il y a des ordures dans les données qu'il traite. Dans le pire des cas, ce seront les données que vous avez notées lorsque tout était en ordre avec la pile, et maintenant vous venez de lire - par exemple, les paramètres d'étalonnage d'un capteur - et alors vous n'avez aucun moyen évident de comprendre que tout va mal avec eux, Ce programme continuera de fonctionner, comme si rien ne s'était passé, vous donnant des ordures à la sortie.
Mémoire allouée dynamiquement
Et ici commence la partie la plus intéressante - si vous réduisez le récit à une phrase, il
est presque impossible de déterminer à l'avance la taille de la pile .
Théoriquement , vous pouvez demander au compilateur de vous donner la taille de pile utilisée par chaque fonction individuelle, puis lui demander de renvoyer l'arbre d'exécution de votre programme, et pour chaque branche de celui-ci, calculer la somme des piles de toutes les fonctions présentes dans cet arbre. Cela seul pour tout programme plus ou moins complexe vous prendra beaucoup de temps.
Ensuite, vous vous souvenez qu'à tout moment une interruption peut se produire, dont le processeur a également besoin de mémoire.
Ensuite - que deux ou trois interruptions imbriquées peuvent se produire, dont les gestionnaires ...
En général, vous comprenez. Essayer de compter la pile pour un programme spécifique est une activité passionnante et généralement utile, mais souvent vous ne le ferez pas.
Par conséquent, dans la pratique, une technique est utilisée qui vous permet de comprendre au moins en quelque sorte si tout dans notre vie se développe bien - ce qu'on appelle la «peinture de mémoire» (peinture de mémoire).
Ce qui est pratique dans cette méthode, c'est qu'elle ne dépend pas des outils de débogage que vous utilisez, et si le système a au moins un moyen de sortie d'informations, vous pouvez vous passer du tout des outils de débogage.
Son essence est que nous remplissions le tableau entier de la fin de bss au début de la pile quelque part au tout début de l'exécution du programme, lorsque la pile est encore exactement petite, avec la même valeur.
De plus, en vérifiant à quelle adresse cette valeur a déjà disparu, nous comprenons où la pile est tombée. Puisqu'une fois que la couleur effacée elle-même ne sera pas restaurée, la vérification peut être effectuée sporadiquement - elle montrera la taille maximale de pile atteinte.
Définissez la couleur de la peinture - la valeur spécifique n'a pas d'importance, en dessous je tape juste avec deux doigts de ma main gauche. L'essentiel n'est pas de choisir 0 et FF:
#define STACK_CANARY_WORD (0xCACACACAUL)
- , -, :
volatile unsigned *top, *start;
__asm__ volatile ("mov %[top], sp" : [top] "=r" (top) : : );
start = &_ebss;
while (start < top) {
*(start++) = STACK_CANARY_WORD;
}
? top , — ; start — bss (, ,
*.ld — libopencm3). bss .
:
unsigned check_stack_size(void) {
/* top of data section */
unsigned *addr = &_ebss;
/* look for the canary word till the end of RAM */
while ((addr < &_stack) && (*addr == STACK_CANARY_WORD)) {
addr++;
}
return ((unsigned)&_stack - (unsigned)addr);
}
_ebss , _stack —
, , , , .
.
— - check_stack_size() , , , .
.
712 — 6 108 .
Word of caution
— , , 100-% . ,
, , , , . , , -, 10-20 %, 108 .
, , , .
P.S. RTOS — MSP, , PSP. , — .