Définition de la taille du tas et de la pile pour un microcontrôleur ARM Cortex-M4?


11

J'ai travaillé sur et hors sur de petits projets de systèmes embarqués de temps en temps. Certains de ces projets utilisaient un processeur de base ARM Cortex-M4. Dans le dossier du projet, il y a un fichier startup.s . Dans ce fichier, j'ai noté les deux lignes de commande suivantes.

;******************************************************************************
;
; <o> Stack Size (in Bytes) <0x0-0xFFFFFFFF:8>
;
;******************************************************************************
Stack   EQU     0x00000400

;******************************************************************************
;
; <o> Heap Size (in Bytes) <0x0-0xFFFFFFFF:8>
;
;******************************************************************************
Heap    EQU     0x00000000

Comment définit-on la taille du tas et de la pile pour un microcontrôleur? Y a-t-il des informations spécifiques dans la fiche technique pour vous aider à obtenir la valeur correcte? Si oui, que faut-il rechercher dans la fiche technique?


Les références:

Réponses:


12

La pile et le tas sont des concepts logiciels, pas des concepts matériels. Ce que le matériel fournit, c'est de la mémoire. La définition des zones de mémoire, dont l'une est appelée «pile» et dont l'une est appelée «tas», est un choix de votre programme.

Le matériel aide aux piles. La plupart des architectures ont un registre dédié appelé pointeur de pile. Son utilisation prévue est que lorsque le programme effectue un appel de fonction, les paramètres de la fonction et l'adresse de retour sont poussés vers la pile, et ils sont extraits lorsque la fonction se termine et revient à son appelant. Pousser sur la pile signifie écrire à l'adresse donnée par le pointeur de pile et décrémenter le pointeur de pile en conséquence (ou incrémenter, selon la direction dans laquelle la pile se développe). Popping signifie incrémenter (ou décrémenter) le pointeur de pile; l'adresse de retour est lue à partir de l'adresse donnée par le pointeur de pile.

Certaines architectures (pas ARM cependant) ont une instruction d'appel de sous-programme qui combine un saut avec l'écriture à l'adresse donnée par le pointeur de pile, et une instruction de retour de sous-programme qui combine la lecture à partir de l'adresse donnée par le pointeur de pile et le saut à cette adresse. Sur ARM, l'enregistrement et la restauration d'adresse se font dans le registre LR, les instructions d'appel et de retour n'utilisent pas le pointeur de pile. Il existe cependant des instructions pour faciliter l'écriture ou la lecture de plusieurs registres à l'adresse donnée par le pointeur de pile, pour pousser et faire apparaître des arguments de fonction.

Pour choisir la taille du tas et de la pile, les seules informations pertinentes du matériel sont la quantité totale de mémoire dont vous disposez. Vous faites ensuite votre choix en fonction de ce que vous souhaitez stocker en mémoire (en tenant compte du code, des données statiques et d'autres programmes).

Un programme utilise généralement ces constantes pour initialiser certaines données en mémoire qui seront utilisées par le reste du code, telles que l'adresse du haut de la pile, peut-être une valeur quelque part pour vérifier les débordements de pile, les limites de l'allocateur de tas , etc.

Dans le code que vous regardez , la Stack_Sizeconstante est utilisée pour réserver un bloc de mémoire dans la zone de code (via une SPACEdirective dans l'assemblage ARM). L'adresse supérieure de ce bloc reçoit l'étiquette __initial_sp, et elle est stockée dans la table vectorielle (le processeur utilise cette entrée pour définir le SP après une réinitialisation logicielle) ainsi qu'exportée pour être utilisée dans d'autres fichiers source. La Heap_Sizeconstante est également utilisée pour réserver un bloc de mémoire et les étiquettes à ses limites ( __heap_baseet __heap_limit) sont exportées pour être utilisées dans d'autres fichiers source.

; Amount of memory (in bytes) allocated for Stack
; Tailor this value to your application needs
; <h> Stack Configuration
;   <o> Stack Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>

Stack_Size      EQU     0x00000400

                AREA    STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem       SPACE   Stack_Size
__initial_sp


; <h> Heap Configuration
;   <o>  Heap Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>

Heap_Size       EQU     0x00000200

                AREA    HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem        SPACE   Heap_Size
__heap_limit

…
__Vectors       DCD     __initial_sp               ; Top of Stack
                DCD     Reset_Handler              ; Reset Handler
                DCD     NMI_Handler                ; NMI Handler

…

                 EXPORT  __initial_sp
                 EXPORT  __heap_base
                 EXPORT  __heap_limit

Savez-vous comment ces valeurs 0x00200 et 0x000400 sont déterminées
Mahendra Gunawardena

@MahendraGunawardena À vous de les déterminer en fonction des besoins de votre programme. La réponse de Niall donne quelques conseils.
Gilles 'SO- arrête d'être méchant'

7

Les tailles de la pile et du tas sont définies par votre application, pas n'importe où dans la fiche technique du microcontrôleur.

La pile

La pile est utilisée pour stocker les valeurs des variables locales à l'intérieur des fonctions, les valeurs précédentes des registres CPU utilisés pour les variables locales (afin qu'elles puissent être restaurées à la sortie de la fonction), l'adresse du programme à laquelle revenir lorsque vous quittez ces fonctions, plus certains frais généraux pour la gestion de la pile elle-même.

Lorsque vous développez un système intégré, vous estimez la profondeur d'appel maximale que vous prévoyez d'avoir, additionnez les tailles de toutes les variables locales dans les fonctions de cette hiérarchie, puis ajoutez un remplissage pour permettre la surcharge mentionnée ci-dessus, puis ajoutez-en plus pour toute interruption pouvant survenir lors de l'exécution de votre programme.

Une autre méthode d'estimation (où la RAM n'est pas contrainte) consiste à allouer beaucoup plus d'espace de pile que vous n'en aurez jamais besoin, à remplir la pile avec une valeur sentinelle, puis à surveiller la quantité que vous utilisez réellement pendant l'exécution. J'ai vu des versions de débogage des exécutions en langage C qui le feront automatiquement pour vous. Ensuite, lorsque vous avez terminé de développer, vous pouvez réduire la taille de la pile si vous le souhaitez.

Le tas

Le calcul de la taille du tas dont vous avez besoin peut être plus délicat. Le tas est utilisé pour les variables allouées dynamiquement, donc si vous utilisez malloc()et free()dans un programme en langage C, ou newet deleteen C ++, c'est là que ces variables vivent.

Cependant, en C ++ en particulier, il peut y avoir une allocation de mémoire dynamique cachée en cours. Par exemple, si vous avez des objets alloués statiquement, le langage nécessite que leurs destructeurs soient appelés à la fin du programme. Je connais au moins un runtime où les adresses des destructeurs sont stockées dans une liste liée allouée dynamiquement.

Donc, pour estimer la taille du tas dont vous avez besoin, regardez toute l'allocation de mémoire dynamique dans chaque chemin à travers votre arborescence d'appels, calculez le maximum et ajoutez un peu de remplissage. Le runtime de langue peut fournir des diagnostics que vous pouvez utiliser pour surveiller l'utilisation totale du tas, la fragmentation, etc.


Merci pour la réponse, j'aime comment déterminer le nombre spécifique tel que 0x00400 et ainsi de suite
Mahendra Gunawardena

5

En plus des autres réponses, j'aimerais ajouter que lors de la création de RAM entre la pile et l'espace de tas, vous devez également tenir compte de l'espace pour les données statiques non constantes (par exemple, les fichiers globaux, les fonctions statiques et à l'échelle du programme). globales du point de vue C, et probablement d'autres pour C ++).

Fonctionnement de l'allocation de pile / segment de mémoire

Il convient de noter que le fichier d'assemblage de démarrage est un moyen de définir la région; la chaîne d'outils (à la fois votre environnement de génération et votre environnement d'exécution) se soucie principalement des symboles qui définissent le début de stackspace (utilisé pour stocker le pointeur de pile initial dans la table vectorielle) et le début et la fin de l'espace de tas (utilisé par la dynamique allocateur de mémoire, généralement fourni par votre libc)

Dans l'exemple d'OP, seuls 2 symboles sont définis, une taille de pile à 1 ko et une taille de tas à 0 b. Ces valeurs sont utilisées ailleurs pour produire réellement les espaces de pile et de tas

Dans l'exemple @Gilles, les tailles sont définies et utilisées dans le fichier d'assemblage pour définir un espace de pile commençant n'importe où et durant la taille, identifié par le symbole Stack_Mem et définissant une étiquette __initial_sp à la fin. De même pour le tas, où l'espace est le symbole Heap_Mem (0,5 ko en taille), mais avec des étiquettes au début et à la fin (__heap_base et __heap_limit).

Ceux-ci sont traités par l'éditeur de liens, qui n'allouera rien dans l'espace de pile et l'espace de tas car cette mémoire est occupée (par les symboles Stack_Mem et Heap_Mem), mais il peut placer ces mémoires et tous les globaux où il a besoin. Les étiquettes finissent par être des symboles sans longueur aux adresses données. Le __initial_sp est utilisé directement pour la table vectorielle au moment du lien, et le __heap_base et __heap_limit par votre code d'exécution. Les adresses réelles des symboles sont attribuées par l'éditeur de liens en fonction de leur emplacement.

Comme je l'ai mentionné ci-dessus, ces symboles ne doivent pas réellement provenir d'un fichier startup.s. Ils peuvent provenir de la configuration de votre éditeur de liens (fichier Scatter Load dans Keil, linkerscript dans GNU), et dans ceux-ci, vous pouvez avoir un contrôle plus fin sur le placement. Par exemple, vous pouvez forcer la pile au début ou à la fin de la RAM, ou garder vos globaux à l'écart du tas ou de tout ce que vous voulez. Vous pouvez même spécifier que le HEAP ou la STACK n'occupent que la RAM restante après le placement des globaux. NOTEZ cependant que vous devez être prudent en ajoutant plus de variables statiques que votre autre mémoire diminuera.

Cependant, chaque chaîne d'outils est différente et la façon d'écrire le fichier de configuration et les symboles que votre allocateur de mémoire dynamique utilisera devront provenir de la documentation de votre environnement particulier.

Dimensionnement de la pile

En ce qui concerne la façon de déterminer la taille de la pile, de nombreuses chaînes d'outils peuvent vous donner une profondeur de pile maximale en analysant les arborescences d'appels de fonction de votre programme, SI vous n'utilisez pas de récursivité ou des pointeurs de fonction. Si vous les utilisez, estimez une taille de pile et pré-remplissez-la avec des valeurs cardinales (peut-être via la fonction d'entrée avant main), puis vérifiez après que votre programme s'est exécuté pendant un certain temps où la profondeur maximale était (qui est où les valeurs cardinales fin). Si vous avez pleinement exercé votre programme à ses limites, vous saurez assez précisément si vous pouvez réduire la pile ou, si votre programme se bloque ou s'il ne reste aucune valeur cardinale, que vous devez augmenter la pile et réessayer.

Dimensionnement du tas

La détermination de la taille du segment de mémoire dépend un peu plus de l'application. Si vous ne faites que l'allocation dynamique au démarrage, vous pouvez simplement ajouter l'espace requis dans votre code de démarrage (plus une surcharge pour la gestion de la mémoire). Si vous avez accès à la source de votre gestionnaire de mémoire, vous pouvez savoir exactement ce qu'est la surcharge, et peut-être même écrire du code pour parcourir la mémoire et vous donner des informations d'utilisation. Pour les applications qui ont besoin de mémoire d'exécution dynamique (par exemple l'allocation de tampons pour les trames Ethernet entrantes), le mieux que je puisse suggérer est d'affiner soigneusement votre taille de pile et de donner au tas tout ce qui reste après la pile et la statique.

Note finale (RTOS)

La question d'OP a été étiquetée pour le métal nu, mais je veux ajouter une note pour les RTOS. Souvent (toujours?) Chaque tâche / processus / thread (je vais simplement écrire la tâche ici pour plus de simplicité) se verra attribuer une taille de pile lors de la création de la tâche, en plus des piles de tâches, il y aura probablement un petit système d'exploitation pile (utilisée pour les interruptions et autres)

Les structures de comptabilité des tâches et les piles doivent être allouées quelque part, et ce sera souvent à partir de l'espace de tas global de votre application. Dans ces cas, la taille initiale de votre pile n'a souvent pas d'importance, car le système d'exploitation ne l'utilisera que lors de l'initialisation. J'ai vu, par exemple, spécifier TOUS les espaces restants pendant la liaison alloués au HEAP et placer le pointeur de pile initial à la fin du tas pour grandir dans le tas, sachant que le système d'exploitation allouera à partir du début du tas et allouera la pile du système d'exploitation juste avant d'abandonner la pile initial_sp. Ensuite, tout l'espace est utilisé pour allouer des piles de tâches et d'autres mémoires allouées dynamiquement.

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.