Definieren der Heap- und Stackgröße für einen ARM Cortex-M4-Mikrocontroller?


11

Ich habe immer wieder an kleinen Embedded-Systemen gearbeitet. Einige dieser Projekte verwendeten einen ARM Cortex-M4-Basisprozessor. Im Projektordner befindet sich eine startup.s- Datei. In dieser Datei habe ich die folgenden zwei Befehlszeilen notiert.

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

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

Wie definiert man die Größe von Heap und Stack für einen Mikrocontroller? Enthält das Datenblatt spezifische Informationen, um den richtigen Wert zu ermitteln? Wenn ja, worauf sollte man im Datenblatt achten?


Verweise:

Antworten:


11

Stack und Heap sind Softwarekonzepte, keine Hardwarekonzepte. Was die Hardware bietet, ist Speicher. Das Definieren von Speicherbereichen, von denen einer als "Stapel" und einer als "Haufen" bezeichnet wird, ist eine Auswahl Ihres Programms.

Die Hardware hilft bei Stacks. Die meisten Architekturen haben ein dediziertes Register, das als Stapelzeiger bezeichnet wird. Die beabsichtigte Verwendung besteht darin, dass beim Aufrufen einer Funktion die Funktionsparameter und die Rücksprungadresse an den Stack gesendet werden und beim Beenden der Funktion und bei der Rückkehr zu ihrem Aufrufer abgerufen werden. Auf den Stapel schreiben heißt, an die vom Stapelzeiger angegebene Adresse schreiben und den Stapelzeiger entsprechend dekrementieren (oder inkrementieren, je nachdem, in welche Richtung der Stapel wächst). Poppen bedeutet Inkrementieren (oder Dekrementieren) des Stapelzeigers; Die Rücksprungadresse wird aus der vom Stapelzeiger angegebenen Adresse gelesen.

Einige Architekturen (jedoch nicht ARM) verfügen über einen Unterprogrammaufrufbefehl, der einen Sprung mit dem Schreiben an die vom Stapelzeiger angegebene Adresse kombiniert, und einen Unterprogrammrückgabebefehl, der das Lesen von der vom Stapelzeiger angegebenen Adresse und das Springen zu dieser Adresse kombiniert. In ARM erfolgt das Speichern und Wiederherstellen der Adresse im LR-Register. Die Aufruf- und Rückgabeanweisungen verwenden nicht den Stapelzeiger. Es gibt jedoch Anweisungen, um das Schreiben oder Lesen mehrerer Register an die vom Stapelzeiger angegebene Adresse zu erleichtern und Funktionsargumente zu verschieben und einzufügen.

Für die Auswahl der Heap- und Stack-Größe ist die einzige relevante Information von der Hardware, wie viel Gesamtspeicher Sie haben. Sie treffen dann Ihre Wahl in Abhängigkeit davon, was Sie im Speicher speichern möchten (unter Berücksichtigung von Code, statischen Daten und anderen Programmen).

Ein Programm verwendet diese Konstanten normalerweise, um einige Daten im Speicher zu initialisieren, die vom Rest des Codes verwendet werden, z. B. die Adresse oben im Stapel, möglicherweise ein Wert, der auf Stapelüberläufe überprüft werden soll, und Grenzen für den Heap-Allokator , usw.

In dem betrachteten Code wird die Stack_SizeKonstante verwendet, um einen Speicherblock im Codebereich zu reservieren (über eine SPACEDirektive in der ARM-Assembly). Die obere Adresse dieses Blocks erhält die Bezeichnung __initial_spund wird in der Vektortabelle gespeichert (der Prozessor verwendet diesen Eintrag, um SP nach einem Software-Reset einzustellen) und zur Verwendung in anderen Quelldateien exportiert. Die Heap_SizeKonstante wird auf ähnliche Weise verwendet, um einen Speicherblock zu reservieren, und Beschriftungen an den Grenzen ( __heap_baseund __heap_limit) werden zur Verwendung in anderen Quelldateien exportiert.

; 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

Wissen Sie, wie diese Werte 0x00200 und 0x000400 bestimmt werden
Mahendra Gunawardena

@ MahendraGunawardena Es liegt an Ihnen, sie zu bestimmen, basierend auf den Anforderungen Ihres Programms. Nialls Antwort gibt ein paar Tipps.
Gilles

7

Die Größe des Stapels und des Heaps wird von Ihrer Anwendung festgelegt, nicht im Datenblatt des Mikrocontrollers.

Der Stapel

Der Stack wird verwendet, um die Werte lokaler Variablen innerhalb von Funktionen zu speichern, die vorherigen Werte der CPU-Register, die für lokale Variablen verwendet wurden (damit sie beim Verlassen der Funktion wiederhergestellt werden können), die Programmadresse, zu der zurückgekehrt werden soll, wenn diese Funktionen verlassen werden etwas Aufwand für die Verwaltung des Stapels selbst.

Bei der Entwicklung eines eingebetteten Systems schätzen Sie die zu erwartende maximale Aufruftiefe, addieren die Größen aller lokalen Variablen in den Funktionen in dieser Hierarchie, fügen eine Auffüllung hinzu, um den oben genannten Overhead zu berücksichtigen, und fügen dann eine weitere hinzu Alle Interrupts, die während der Ausführung Ihres Programms auftreten können.

Eine alternative Schätzmethode (bei der der Arbeitsspeicher nicht eingeschränkt ist) besteht darin, viel mehr Stapelspeicher zuzuweisen, als Sie jemals benötigen, den Stapel mit einem Sentinel-Wert zu füllen und dann zu überwachen, wie viel Sie tatsächlich während der Ausführung verwenden. Ich habe Debug-Versionen von C-Laufzeitprogrammen gesehen, die dies automatisch für Sie erledigen. Wenn Sie mit dem Entwickeln fertig sind, können Sie die Stapelgröße reduzieren, wenn Sie möchten.

Der Haufen

Das Berechnen der Größe des benötigten Heapspeichers kann schwieriger sein. Der Heap wird für dynamisch zugewiesene Variablen verwendet. Wenn Sie also malloc()und free()in einem C-Programm oder newund deletein C ++ verwenden, befinden sich diese Variablen dort.

Insbesondere in C ++ kann jedoch eine versteckte dynamische Speicherzuweisung stattfinden. Wenn Sie beispielsweise Objekte statisch zugewiesen haben, müssen deren Destruktoren beim Beenden des Programms aufgerufen werden. Mir ist mindestens eine Laufzeit bekannt, in der die Adressen der Destruktoren in einer dynamisch zugewiesenen verknüpften Liste gespeichert sind.

Um die Größe des Heapspeichers zu schätzen, den Sie benötigen, überprüfen Sie die dynamische Speicherzuordnung in jedem Pfad durch Ihren Aufrufbaum, berechnen Sie das Maximum und fügen Sie eine Auffüllung hinzu. Die Sprachlaufzeit bietet möglicherweise Diagnosefunktionen, mit denen Sie die gesamte Heap-Nutzung, Fragmentierung usw. überwachen können.


Vielen Dank für die Antwort, ich möchte, wie Sie die spezifische Zahl wie 0x00400 und so weiter bestimmen
Mahendra Gunawardena

5

Zusätzlich zu den anderen Antworten möchte ich hinzufügen, dass Sie beim Aufteilen des Arbeitsspeichers zwischen Stapel- und Heapspeicher auch den Speicherplatz für statische nicht konstante Daten berücksichtigen müssen (z. B. Dateiglobale, Funktionsstatik und programmweit) Globale aus einer C-Perspektive und wahrscheinlich andere für C ++).

So funktioniert die Stapel- / Heap-Zuordnung

Es ist erwähnenswert, dass die Startassembly-Datei eine Möglichkeit zum Definieren der Region darstellt. Die Toolchain (sowohl Ihre Build-Umgebung als auch die Laufzeitumgebung) kümmert sich hauptsächlich um die Symbole, die den Anfang des Stapelbereichs (der zum Speichern des anfänglichen Stapelzeigers in der Vektortabelle verwendet wird) sowie den Anfang und das Ende des Heap-Bereichs (der von der Dynamik verwendet wird) definieren Speicherzuordnung, normalerweise bereitgestellt von Ihrer libc)

In OPs Beispiel sind nur 2 Symbole definiert, eine Stapelgröße bei 1 kB und eine Heapgröße bei 0B. Diese Werte werden an anderer Stelle verwendet, um die Stapel- und Heap-Räume tatsächlich zu erzeugen

Im Beispiel @Gilles werden die Größen definiert und in der Baugruppendatei verwendet, um einen Stapelbereich festzulegen, der an jeder beliebigen Stelle beginnt und die Größe beibehält. Dieser wird durch das Symbol Stack_Mem gekennzeichnet und am Ende durch eine Bezeichnung __initial_sp gekennzeichnet. Ähnliches gilt für den Heap, bei dem das Leerzeichen das Symbol Heap_Mem (0,5 KB groß) ist, jedoch mit Beschriftungen am Anfang und Ende (__heap_base und __heap_limit).

Diese werden vom Linker verarbeitet, der nichts im Stapelspeicher und im Heapspeicher zuweist, da dieser Speicher belegt ist (durch die Symbole Stack_Mem und Heap_Mem), aber er kann diese Speicher und alle globalen Speicher an beliebiger Stelle platzieren. Die Bezeichnungen sind an den angegebenen Adressen Symbole ohne Länge. Der __initial_sp wird zur Verbindungszeit direkt für die Vektortabelle verwendet, und die __heap_base und __heap_limit werden von Ihrem Laufzeitcode verwendet. Die tatsächlichen Adressen der Symbole werden vom Linker anhand der Position zugewiesen, an der sie platziert wurden.

Wie oben bereits erwähnt, müssen diese Symbole nicht unbedingt aus einer startup.s-Datei stammen. Sie können aus Ihrer Linkerkonfiguration stammen (Scatter Load-Datei in Keil, Linkerscript in GNU) und in diesen können Sie die Platzierung genauer steuern. Sie können beispielsweise festlegen, dass sich der Stapel am Anfang oder Ende des Arbeitsspeichers befindet, oder Ihre globalen Daten vom Heap fernhalten oder was auch immer Sie möchten. Sie können sogar festlegen, dass HEAP oder STACK nur den verbleibenden Arbeitsspeicher belegen, nachdem Globals platziert wurden. Beachten Sie jedoch, dass Sie darauf achten müssen, dass mehr statische Variablen hinzugefügt werden, die Ihren anderen Speicher verringern.

Jede Toolchain ist jedoch unterschiedlich, und wie die Konfigurationsdatei geschrieben wird und welche Symbole Ihr dynamischer Speicherzuordner verwendet, muss aus der Dokumentation Ihrer speziellen Umgebung stammen.

Stapelgröße

In Bezug auf die Ermittlung der Stapelgröße können viele Toolchains eine maximale Stapeltiefe liefern, indem sie die Funktionsaufrufbäume Ihres Programms analysieren, WENN Sie keine Rekursions- oder Funktionszeiger verwenden. Wenn Sie diese verwenden, schätzen Sie eine Stapelgröße und füllen sie vorab mit Kardinalwerten (möglicherweise über die Eingabefunktion vor main). Überprüfen Sie dann, nachdem Ihr Programm eine Weile ausgeführt wurde, wo die maximale Tiefe war (wo sich die Kardinalwerte befanden Ende). Wenn Sie Ihr Programm vollständig ausgelastet haben, wissen Sie ziemlich genau, ob Sie den Stapel verkleinern können, oder ob Sie den Stapel vergrößern und es erneut versuchen müssen, wenn Ihr Programm abstürzt oder keine Kardinalwerte mehr vorhanden sind.

Haufengröße

Das Ermitteln der Heap-Größe ist etwas anwendungsabhängiger. Wenn Sie die dynamische Zuordnung nur während des Startvorgangs vornehmen, können Sie nur den in Ihrem Startcode erforderlichen Speicherplatz (zuzüglich eines gewissen Overheads für die Speicherverwaltung) addieren. Wenn Sie Zugriff auf die Quelle Ihres Speichermanagers haben, können Sie den Overhead genau kennen und möglicherweise sogar Code schreiben, um den Speicher zu durchsuchen und Ihnen Nutzungsinformationen zu geben. Für Anwendungen, die dynamischen Laufzeitspeicher benötigen (z. B. das Zuweisen von Puffern für eingehende Ethernet-Frames), kann ich nur empfehlen, die Stapelgröße sorgfältig zu verfeinern und dem Heap alles zu geben, was nach Stapel und Statik übrig bleibt.

Schlussnote (RTOS)

OPs Frage war für Bare-Metal markiert, aber ich möchte einen Hinweis für RTOSes hinzufügen. Oft (immer?) Wird jeder Aufgabe / Prozess / Thread (der Einfachheit halber schreibe ich hier nur die Aufgabe auf) eine Stapelgröße zugewiesen, wenn die Aufgabe erstellt wird. Zusätzlich zu den Aufgabenstapeln wird es wahrscheinlich ein kleines Betriebssystem geben Stack (wird für Interrupts und ähnliches verwendet)

Die Aufgabenabrechnungsstrukturen und die Stapel müssen von einem beliebigen Ort aus zugewiesen werden, und dies wird häufig vom gesamten Heap-Speicherplatz Ihrer Anwendung abhängen. In diesen Fällen spielt Ihre anfängliche Stapelgröße oft keine Rolle, da das Betriebssystem sie nur während der Initialisierung verwendet. Ich habe zum Beispiel gesehen, wie beim Verknüpfen ALLER verbleibender Speicherplatz dem HEAP zugewiesen wurde und der anfängliche Stapelzeiger am Ende des Heapspeichers platziert wurde, um in den Heapspeicher hineinzuwachsen reserviert den OS-Stack kurz vor dem Verlassen des initial_sp-Stacks. Der gesamte Speicherplatz wird dann zum Zuweisen von Taskstapeln und anderem dynamisch zugewiesenen Speicher verwendet.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.