"Mit der Erfahrung kommt ein wissenschaftlicher Standardansatz zur Berechnung der richtigen Stapelgröße: Nehmen Sie eine Zufallszahl und hoffen Sie auf das Beste."
- Jack Ganssle, „Die Kunst, eingebettete Systeme zu entwerfen“Hallo Habr!
So seltsam es auch scheinen mag, bei der überwiegenden Mehrheit der „STM32-Primer“, die ich im Besonderen gesehen habe, und bei Mikrocontrollern im Allgemeinen gibt es im Allgemeinen nichts über Speicherzuweisung, Stapelplatzierung und vor allem das Verhindern eines Speicherüberlaufs - als Folge davon Ein Bereich franst einen anderen aus und alles bricht zusammen, normalerweise mit bezaubernden Effekten.
Dies ist teilweise auf die Einfachheit von Schulungsprojekten zurückzuführen, die auf Debug-Boards mit relativ fettigen Mikrocontrollern durchgeführt werden, bei denen es schwierig ist, durch Blinken einer LED in einen Speichermangel zu geraten. In letzter Zeit werden jedoch selbst für Anfänger-Amateure immer häufiger Verweise auf Controller vom Typ STM32F030F4P6 verwendet. , einfach zu installieren, einen Cent wert, aber auch mit einer Speichereinheit von Kilobyte.
Mit solchen Controllern können Sie ganz ernsthafte Dinge für sich selbst tun (nun, hier wurde zum Beispiel eine so
vollständig geeignete Messung für uns am STM32F042K6T6 mit 6 KB RAM durchgeführt, von dem etwas mehr als 100 Bytes frei bleiben), aber wenn Sie mit Speicher arbeiten, benötigen Sie eine bestimmte Menge an Speicher Ordentlichkeit.
Ich möchte über diese Genauigkeit sprechen. Der Artikel wird kurz sein, Profis werden nichts Neues lernen - aber für Anfänger ist dieses Wissen sehr zu empfehlen.
In einem typischen Projekt auf einem Mikrocontroller, der auf einem Cortex-M-Kern basiert, ist RAM in vier Abschnitte unterteilt:
- Daten - Daten, die durch einen bestimmten Wert initialisiert wurden
- bss - Daten auf Null initialisiert
- heap - heap (dynamischer Bereich, aus dem der Speicher explizit mit malloc zugewiesen wird)
- Stack - der Stack (der dynamische Bereich, aus dem der Compiler implizit Speicher zuweist)
Der noinit-Bereich kann auch gelegentlich auftreten (nicht initialisierte Variablen - sie sind praktisch, da sie den Wert zwischen Neustarts beibehalten), noch seltener einige andere Bereiche, die für bestimmte Aufgaben zugewiesen sind.
Sie befinden sich auf eine bestimmte Art und Weise im physischen Speicher - Tatsache ist, dass der Stapel in Mikrocontrollern auf ARM-Kernen von oben nach unten wächst. Daher befindet es sich getrennt von den verbleibenden Speicherblöcken am Ende des RAM:

Standardmäßig entspricht seine Adresse normalerweise der neuesten RAM-Adresse, und von dort aus sinkt sie mit zunehmendem Wachstum - und ein äußerst unangenehmes Merkmal des Stapels wächst daraus heraus: Er kann bss erreichen und seine Oberseite neu schreiben, und Sie wissen nichts explizites darüber.
Statische und dynamische Speicherbereiche
Der gesamte Speicher ist in zwei Kategorien unterteilt - statisch zugeordnet, d. H. Speicher, dessen Gesamtmenge aus dem Programmtext ersichtlich ist und nicht von der Reihenfolge seiner Ausführung abhängt, und dynamisch zugewiesen wird, dessen erforderliches Volumen vom Fortschritt des Programms abhängt.
Letzteres beinhaltet einen Haufen (von dem wir mit malloc Brocken nehmen und mit free zurückgeben) und einen Stapel, der von selbst wächst und schrumpft.
Im Allgemeinen wird von der Verwendung von malloc auf Mikrocontrollern
dringend abgeraten, es sei denn, Sie wissen genau, was Sie tun. Das Hauptproblem, das sie mit sich bringen, ist die Speicherfragmentierung. Wenn Sie 10 Teile zu je 10 Bytes zuweisen und dann jede Sekunde frei werden, erhalten Sie keine 50 Bytes frei. Sie erhalten 5 kostenlose Stücke zu je 10 Bytes.
Darüber hinaus kann der Compiler in der Phase des Kompilierens des Programms nicht automatisch bestimmen, wie viel Speicher Ihr Malloc benötigt (insbesondere unter Berücksichtigung der Fragmentierung, die nicht nur von der Größe der angeforderten Teile, sondern auch von der Reihenfolge ihrer Zuordnung und Veröffentlichung abhängt), und kann Sie daher nicht warnen wenn am Ende nicht genug Speicher vorhanden ist.
Es gibt Methoden, um dieses Problem zu umgehen - spezielle Malloc-Implementierungen, die in einem statisch zugewiesenen Bereich und nicht im gesamten RAM funktionieren, sorgfältige Verwendung von Malloc unter Berücksichtigung einer möglichen Fragmentierung auf Programmlogikebene usw. - aber im Allgemeinen ist
Malloc besser nicht zu berühren .
Alle Speicherbereiche mit Grenzen und Adressen werden in einer Datei mit der Erweiterung .LD registriert, an der sich der Linker beim Erstellen des Projekts orientiert.
Statisch zugeordneter Speicher
Aus dem statisch zugewiesenen Speicher haben wir also zwei Bereiche - bss und data, die sich nur formal unterscheiden. Wenn das System initialisiert wird, wird der Datenblock aus dem Flash kopiert, wo die erforderlichen Initialisierungswerte dafür gespeichert werden. Der bss-Block wird einfach mit Nullen gefüllt (zumindest das Füllen mit Nullen wird als gute Form angesehen).
Beide Dinge - das Kopieren von einem Flash und das Füllen mit Nullen - werden im Programmcode
in einer expliziten Form ausgeführt , jedoch nicht in Ihrem main (), sondern in einer separaten Datei, die zuerst ausgeführt wird. Sie wird einmal geschrieben und einfach von Projekt zu Projekt gezogen.
Dies interessiert uns jedoch nicht jetzt - sondern wie wir verstehen werden, ob unsere Daten überhaupt in den RAM unseres Controllers passen.
Es wird sehr einfach - vom Dienstprogramm arm-none-eabi-size mit einem einzigen Parameter - die kompilierte ELF-Datei unseres Programms erkannt (häufig wird ihr Aufruf am Ende des Makefiles eingefügt, weil es praktisch ist):

Hier ist Text die Menge der Programmdaten, die im Flash liegen, und bss und Daten sind unsere statisch zugewiesenen Bereiche im RAM. Die letzten beiden Spalten stören uns nicht - dies ist die Summe der ersten drei, es hat keine praktische Bedeutung.
Insgesamt benötigen wir statisch im RAM bss + Datenbytes, in diesem Fall 5324 Bytes. Der Controller hat 6144 Bytes RAM, wir verwenden kein Malloc, 820 Bytes bleiben übrig.
Welches sollte für uns auf dem Stapel genug sein.
Aber genug? Wenn nicht, wächst unser Stack zu unseren eigenen Daten, und zuerst werden die Daten überschrieben, dann werden die Daten überschrieben, und dann stürzt alles ab. Darüber hinaus kann das Programm zwischen dem ersten und dem zweiten Punkt weiterarbeiten, ohne zu bemerken, dass die von ihm verarbeiteten Daten Müll enthalten. Im schlimmsten Fall sind es die Daten, die Sie notiert haben, als alles in Ordnung mit dem Stapel war, und jetzt lesen Sie einfach - zum Beispiel die Kalibrierungsparameter eines Sensors - und dann haben Sie keine offensichtliche Möglichkeit zu verstehen, dass mit ihnen alles schlecht ist. Dieses Programm läuft weiter, als wäre nichts passiert, und Sie erhalten Müll an der Ausgabe.
Dynamisch zugeordneter Speicher
Und hier beginnt der interessanteste Teil: Wenn Sie die Geschichte auf einen Satz reduzieren, ist es
fast unmöglich, die Größe des Stapels im Voraus zu bestimmen .
Theoretisch können Sie den Compiler auffordern, die von jeder einzelnen Funktion verwendete Stapelgröße anzugeben, ihn dann auffordern, den Ausführungsbaum Ihres Programms zurückzugeben, und für jeden Zweig darin die Summe der Stapel aller in diesem Baum vorhandenen Funktionen berechnen. Dies allein für ein mehr oder weniger komplexes Programm nimmt Ihnen viel Zeit in Anspruch.
Dann erinnern Sie sich, dass jederzeit eine Unterbrechung auftreten kann, deren Prozessor ebenfalls Speicher benötigt.
Dann - dass zwei oder drei verschachtelte Interrupts auftreten können, deren Handler ...
Im Allgemeinen verstehen Sie. Der Versuch, den Stapel für ein bestimmtes Programm zu zählen, ist eine aufregende und allgemein nützliche Aktivität, die Sie jedoch häufig nicht ausführen.
In der Praxis wird daher eine Technik verwendet, mit der Sie zumindest irgendwie verstehen können, ob sich alles in unserem Leben gut entwickelt - das sogenannte „Memory Painting“ (Memory Painting).
Bei dieser Methode ist es praktisch, dass sie nicht von den von Ihnen verwendeten Debugging-Tools abhängt. Wenn das System über mindestens einige Mittel zur Ausgabe von Informationen verfügt, können Sie überhaupt auf Debugging-Tools verzichten.
Das Wesentliche ist, dass wir das gesamte Array vom Ende von bss bis zum Anfang des Stapels irgendwo in der sehr frühen Phase der Programmausführung füllen, wenn der Stapel noch genau klein ist, mit demselben Wert.
Wenn wir überprüfen, an welcher Adresse dieser Wert bereits verschwunden ist, verstehen wir, wo der Stapel gefallen ist. Da die gelöschte Farbe selbst nicht wiederhergestellt werden kann, kann die Überprüfung sporadisch durchgeführt werden - es wird die maximal erreichte Stapelgröße angezeigt.
Definieren Sie die Farbe der Farbe - der spezifische Wert spielt keine Rolle, unten habe ich nur mit zwei Fingern meiner linken Hand getippt. Die Hauptsache ist, nicht 0 und FF zu wählen:
#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. , — .