Über den Beitrag
In diesem Beitrag reisen wir in die Tiefen des SRAM (Static Random Access Memory). Dabei gehe ich auf den Unterschied zwischen Stack und Heap ein und zeige, wie Variablen in diesen Speicherbereichen angelegt und verwaltet werden. Damit verbunden ist die Frage, warum man bei der Nutzung von Strings im Arduinobereich Vorsicht walten lassen sollte. Und schließlich werde ich noch einige Hinweise geben, wie ihr sparsam mit der Ressource SRAM umgeht.
All das ist nicht neu und es gibt schon eine ganze Reihe guter Artikel dazu im Netz. Ich denke, was diesen Beitrag von vielen anderen unterscheidet, ist der Detaillierungsgrad und die Herangehensweise. Vor allem möchte ich ganz konkret auf Adressebene zeigen, was im SRAM beim Anlegen verschiedener Variablen vor sich geht. Dabei zeige ich auch, wie ihr an die Adressen von Variablen im Heap gelangt. „Forensische Arduinoistik“, sozusagen.
Der Beitrag setzt Basiswissen über Zeiger, Referenzen und den Adressoperator voraus. Solltet Ihr Schwierigkeiten damit haben, dann lest am besten meinen letzten Beitrag.
Das kommt auf euch zu:
- Flash, SRAM und EEPROM
- Der SRAM im Überblick
- Wie Variablen im SRAM gespeichert werden
- Stack Management
- String Handling im SRAM
- Character Arrays
- Variablen in den Heap zwingen
- SRAM sparen mit PROGMEM und F()
1. Flash, SRAM und EEPROM
1.1. Überblick
Der Aufbau des Speichers eines Mikrocontrollers ist modellabhängig. In meinen Ausführungen beziehe ich mich hauptsächlich auf den ATmega328P, der beispielsweise im Arduino UNO, Nano und Pro Mini zur Anwendung kommt.
Wie ihr in der Darstellung rechts seht, besitzt der ATmega328P drei verschiedene Speicher, nämlich den relativ großen Flash, den SRAM und den EEPROM.
Auch wenn sich die Speicher anderer Mikrocontroller recht stark unterscheiden (siehe Tabelle unten), fällt der SRAM gegenüber dem Flash bei allen vergleichsweise kleiner aus und ist damit eine schützenswerte Ressource.

Der Flash wird auch als Programmspeicher bezeichnet. Wenn ihr eure Sketche hochladet, werden sie zunächst kompiliert, dann in maschinenlesbaren Code übersetzt und schließlich im Flash gespeichert. Außerdem befindet sich dort der Bootloader. Der Flash ist nicht-volatil, d. h. der Speicherinhalt bleibt auch bei Trennung von der Versorgungsspannung erhalten. Zudem ändert er sich während der Programmausführung nicht, sodass es damit keine Laufzeitprobleme gibt.
Auch der EEPROM ist nicht-volatil. Ich habe ihn detailliert in diesem Beitrag beschrieben. Besonders gut geeignet ist er für Daten, die sich zwar während der Laufzeit ändern können, andererseits aber dauerhaft erhalten bleiben sollen. Das könnten beispielsweise Kalibrierfaktoren für Sensoren sein oder Daten aus dem letzten Programmdurchlauf.
Der SRAM ist euer Arbeitsspeicher. Er ist den Variablen eurer Programme vorbehalten. Zu den Herausforderungen beim Umgang mit dem SRAM gehört, dass sein Belegungsgrad nur in begrenztem Maße planbar ist.
1.2. Speicherinfo beim Sketchupload
Wenn ihr eure Sketche per Arduino IDE auf euren Mikrocontroller hochladet, bekommt ihr eine Meldung wie die folgende:

In diesem Beispiel belegt das Programm 9716 Byte des verfügbaren Flashs, also 31 % von 30720. Eigentlich stehen 32 KB Flash zur Verfügung, die Differenz ist dem Bootloader geschuldet. 2048 Byte SRAM stehen zur Verfügung. Davon belegt der Sketch 751 Byte (36 %) mit globalen Variablen. Dieser Wert ist zum Zeitpunkt der Kompilierung bekannt und ändert sich nicht während der Laufzeit. Lokale Variablen hingegen „kommen und gehen“. Wie viele davon zu einem bestimmten Zeitpunkt „am Leben sind“ und wie viel Platz sie benötigen, lässt sich nicht unbedingt voraussagen.
1.3. Statisch und dynamisch – verwirrende Begriffe
RAM ist der Oberbegriff für SRAM (Static RAM) und DRAM (Dynamic RAM). SRAM und DRAM unterscheiden sich in ihrem physikalischen Aufbau. SRAM ist erheblich teurer, aber dafür auch schneller und stromsparender. In Mikrocontrollern kommt meistens SRAM zum Einsatz. Mehr Informationen über die Unterschiede von SRAM und DRAM findet ihr beispielsweise hier.
In der ESP32 Dokumentation wird der Begriff DRAM für Data RAM verwendet, um ihn vom IRAM, dem Instruction RAM, abzugrenzen (siehe hier). Es geht dabei also nicht um einen physikalischen, sondern einen funktionalen Unterschied.
Manchmal wird der SRAM auch als dynamischer Speicher bezeichnet – z. B. in der Arduino IDE, wie wir gerade gesehen haben. Wieder andere meinen nur den Heap, wenn sie vom dynamischen Speicher reden.
Und um die Verwirrung zu komplettieren, bezeichnet man die globalen und die statischen lokalen Variablen als „Static Data“, die in einem bestimmten Bereich des SRAM gespeichert werden.
Ich werde deshalb versuchen, weitestgehend auf die Begriffe „statisch“ und „dynamisch“ zu verzichten.
2. Der SRAM im Überblick
In meinen bisherigen Ausführungen habe ich den Speicherbereich für die Register unterschlagen, da er nicht zur freien Verfügung steht. Er befindet sich unterhalb des SRAM und ist der Grund dafür, dass die Speicheradressen nicht bei null beginnen, sondern im Fall des ATmega328P bei 256:
Im unteren Bereich des SRAM werden die schon erwähnten statischen Daten bzw. Variablen gespeichert. Alle anderen Variablen befinden sich entweder im Stack („Stapel“) oder im Heap („Haufen“). Der Stack wird von oben nach unten gefüllt und der Heap von unten nach oben. Wenn nicht genügend Speicher vorhanden ist, dann treffen die Bereiche (ohne Fehlermeldung!) aufeinander. In diesem Fall wird das Programm nicht mehr wie gewünscht funktionieren. Wie sich das konkret äußert, ist allerdings nicht voraussehbar.
Vergesst für einen Moment die Tatsache, dass der Stack wie ein Stalaktit „von der Decke hängt“ und stellt ihn euch wie einen sorgfältig geplanten Bücherstapel vor. Aufgrund der Schwerkraft gibt es keine Lücken. Neue Bücher (z. B. bei Funktionsaufrufen) werden oben auf den Stapel gelegt und von dort wieder heruntergenommen (Return aus der Funktion). Durch die systematische Planung müssen keine Bücher aus der Mitte des Stapels gezogen werden. So in der Art verhält es sich auch mit dem Stack, nur dass die Welt auf dem Kopf steht.
Ein Haufen ist weniger geordnet als ein Stapel – insofern passt die Namensgebung für den Heap. Ich würde den Heap aber eher als eine Art Bücherschrank beschreiben, den ihr streng von unten nach oben befüllt. Nehmt ihr ein Buch heraus, dann bleibt eine Lücke. Packt ihr dann ein Neues hinein, wird es selten dieselbe Größe haben. Ist es kleiner, dann passt es in die Lücke, aber sie wird nicht vollständig gefüllt. Ist es hingegen größer, muss es ganz nach oben. Die Bücher werden also nicht zusammengeschoben, um Lücken zu beseitigen. Und wenn ihr nichts dagegen tut, dann werdet ihr mit der Zeit immer mehr Platz in eurem Bücherschrank ungenutzt lassen.
Dieses Problem heißt Heap-Fragmentierung. In der Praxis tritt es vor allem beim Gebrauch von String-Objekten auf. Aber das schauen wir uns noch schrittweise und ganz praktisch im Detail an.
3. Wie Variablen im SRAM gespeichert werden
3.1. Beispielsketch
Im ersten Beispielsketch legen wir ein bunte Mischung von Variablen an und lassen uns anzeigen, wo sie im SRAM zu finden sind.
int a = 42; int b = 43; int c = 44; void setup() { Serial.begin(9600); delay(2000); // needed for some boards int d = 45; int e = 46; int f = 47; char g[] = "I am a char array"; char h[] = "I am a char array, too"; char i[] = "No surprise what I am"; String j = "I am a string"; String k = "I am a string, too"; String l = "Guess what I am"; Serial.println(F("Addresses in SRAM:")); Serial.print(F("a (int, global): ")); printVariableDetails(a); Serial.print(F("b (int, global): ")); printVariableDetails(b); Serial.print(F("c (int, global): ")); printVariableDetails(c); Serial.print(F("d (int, local): ")); printVariableDetails(d); Serial.print(F("e (int, local): ")); printVariableDetails(e); Serial.print(F("f (int, local): ")); printVariableDetails(f); Serial.print(F("g (char array): ")); Serial.print((int)&g); Serial.print(F(" - ")); Serial.println((int)&g + sizeof(g) - 1); Serial.print(F("h (char array): ")); Serial.print((int)&h); Serial.print(F(" - ")); Serial.println((int)&h + sizeof(h) - 1); Serial.print(F("i (char array): ")); Serial.print((int)&i); Serial.print(F(" - ")); Serial.println((int)&i + sizeof(i) - 1); Serial.print(F("j (String): ")); printVariableDetails(j); Serial.print(F("k (String): ")); printVariableDetails(k); Serial.print(F("l (String): ")); printVariableDetails(l); } void loop() {} template<typename T> void printVariableDetails(T &i){ Serial.print((int)&i); Serial.print(F(" - ")); Serial.println((int)&i + sizeof(i) - 1); } /* For those who are not familiar with templates: * If you have functions to which you want to pass different data types or which shall * return different variable types, you can use templates to avoid defining several functions * for each data type like: * void printVariableDetails(int &i){.....} * and: * void printVariableDetails(String &i){.....} */
Hier die Ausgabe auf einem Arduino Nano:

3.2. Besprechung von example_1.ino
Was wir daraus schließen können:
- Am Beispiel der Integer-Variablen a, b und c erkennen wir, dass globale Variablen am unteren Ende des SRAM („Static Data“ Bereich) gespeichert werden. Auf einem ATmega328P basierten Board belegen Integer-Variablen je zwei Byte.
- Die Integer-Werte stehen in diesem Sketch stellvertretend für einfache Variablentypen. Genauso hätten wir Variablen vom Typ Long, Float, Double usw. nehmen können.
- Die Reihenfolge der Variablenadressen entspricht nicht unbedingt der Reihenfolge, in der die Variablen definiert werden.
- Im Setup haben wir noch einmal drei Integer-Variablen definiert, nämlich d, e und f. Sie befinden sich aber im Stack, also am oberen Ende des SRAM, weil es sich um lokale Variablen handelt.
- Auch die Character-Arrays g, h und i werden im Stack angelegt. Ihr Platzbedarf entspricht der Anzahl der ihrer Zeichen plus einem Extra-Byte für die abschließende Null (ASCII-Zeichen 0 = ‘\0’).
- Zu guter Letzt haben wir noch die Strings j, k und l definiert. Genauer ausgedrückt: Wir haben Objekte der Klasse String erzeugt. Sie belegen unabhängig von ihrer Länge je 6 Byte im Stack. Zumindest gilt das für die AVR basierten Arduinos.
Der letzte Punkt mag verwundern, zumal ich ja schon vorweggenommen hatte, dass Strings Speicherplatz im Heap belegen. Des Rätsels Lösung ist: Ein String-Objekt besteht aus zwei Teilen. Die eigentliche Zeichenkette befindet sich im Heap. Im Stack wird hingegen nur eine Art Inhaltsverzeichnis des String-Objektes angelegt, das aus drei Komponenten besteht (die String-Variable, sozusagen):
char *buffer; // the actual char array unsigned int capacity; // the array length minus one (for the '\0') unsigned int len; // the String length (not counting the '\0')
Die ersten zwei Bytes der String-Variablen repräsentieren den Zeiger auf die im Heap befindliche Zeichenkette („buffer“). Darauf folgt ein Integerwert, bei dem es sich um den für die Zeichenkette reservierten Speicherbedarf handelt („capacity“). Dann folgt noch ein Integerwert, der die tatsächliche Länge der Zeichenkette („len“) enthält. Nehmt es erst einmal so hin, ich komme gleich wieder darauf zurück.
Ihr findet die Definition der Klasse String übrigens in den Tiefen der Arduino Bibliotheksdateien in WString.h: …\AppData\Local\Arduino15\packages\arduino\hardware\avr\version\cores\arduino\WString.h (mit version = Versionsnummer).
3.3. example_1.ino auf anderen MCUs
Der Sketch example_1 ist auch auf anderen Boards unverändert lauffähig. Getestet habe ich ihn auf einem Nano Every, Mega2560, ESP32, Nano 33 IoT und einem Wemos D1 Mini (ESP8266).
Hier die Ausgabe auf einem ESP32:

Wie ihr seht, ist der Speicher des ESP32 auf eine andere Art organisiert. Heap und Stack gibt es auch, aber der Abstand zwischen der höchsten verwendeten Adresse und der niedrigsten ist: 1073500571 – 1073470312 = 30259, was nur etwa ein Zehntel des verfügbaren Arbeitsspeichers ausmacht. Außerdem benötigt ein String 16 Byte im Stack. Das liegt nicht zuletzt daran, dass Adressen auf dem ESP32 4 Byte in Anspruch nehmen. Die Definition der Klasse String für den ESP32 findet ihr hier: …\AppData\Local\Arduino15\packages\esp32\hardware\esp32\version\cores\esp32\WString.h.
Wer sich für die Details des ESP32 Speichers im Allgemeinen interessiert, dem empfehle ich diesen Artikel.
4. Stack Management
4.1. Beispielsketch
Mit dem nächsten Sketch möchte ich zeigen, dass der Speicher für nicht mehr benötigte Variablen im Stack automatisch wieder freigegeben wird. Mit anderen Worten: ihr müsst euch um das Stack Management nicht kümmern!
void setup() { Serial.begin(9600); delay(2000); // needed for some boards int a = 1111; Serial.print(F("a: ")); Serial.print((int)&a); Serial.println(F(" (setup)")); Serial.println(); function_1(); Serial.println(); int c = 3333; Serial.print(F("c: ")); Serial.print((int)&c); Serial.println(F(" (setup)")); Serial.println(); function_2(); Serial.println(); function_3(); } void loop() {} void function_1(){ int b = 2222; Serial.print(F("b: ")); Serial.print((int)&b); Serial.println(F(" (function_1)")); } void function_2(){ int d = 4444; Serial.print(F("d: ")); Serial.print((int)&d); Serial.println(F(" (function_2)")); function_2a(); } void function_2a(void){ int e = 5555; Serial.print(F("e: ")); Serial.print((int)&e); Serial.println(F(" (function_2a)")); function_2b(); } void function_2b(void){ int f = 6666; Serial.print(F("f: ")); Serial.print((int)&f); Serial.println(F(" (function_2b)")); } void function_3(void){ int g = 7777; Serial.print(F("g: ")); Serial.print((int)&g); Serial.println(F(" (function_3)")); function_3a(); } void function_3a(void){ int h = 8888; Serial.print(F("h: ")); Serial.print((int)&h); Serial.println(F(" (function_3a)")); }
Hier die Ausgabe für einen Arduino Nano (ATmega328P):

4.2. Besprechung von example_2.ino
Die Variablen a und c „überleben“, solange wir uns im Setup befinden. Alle anderen Variablen existieren nur innerhalb ihrer Funktionen und „sterben“ nach Rückkehr in das Setup. Der Speicherplatz wird wiederverwendet. Da die Funktion 2 die Funktion 2a und die wiederum die Funktion 2b aufruft, existieren die Variablen d, e und f nebeneinander. Gleiches gilt für g und h.
„Hausaufgabe“: Stellt bei einer der Integer Definitionen ein „static“ voran. Also beispielsweise: static int d = 4444;
und schaut, was passiert.
5. String Handling im SRAM
In den nächsten Beispielsketchen nehmen wir das Handling von Strings in Stack und Heap unter die Lupe. Dafür kommen ein paar Hilfsmittel zum Einsatz:
- Um den Wert einer Variablen an einer bestimmten Speicheradresse zu lesen, verwenden wir Zeiger:
value = *(datatype*)address = *(datatype*)&variable
. Siehe dazu meinen letzten Beitrag. - Den gesamten, noch verfügbaren SRAM ermitteln wir mit
getTotalAvailableMemory()
. Den größten, zusammenhängenden freien Speicherblock verrät unsgetLargestAvailableBlock()
.- Die beiden Funktionen sind Teil von MemoryInfo.Avr.cpp. Um auf sie zugreifen zu können, müsst ihr MemoryInfo.Avr.cpp als Extra-Tab in die Sketche einbinden.
- MemoryInfo.Avr.cpp stammt von Benoît Blanchon. Ich habe den Code hier auf GitHub gefunden.
5.1. String-Analyse in Stack und Heap
5.1.1. Beispielsketch
Zunächst definieren wir nur einen einzigen String:
void setup() { Serial.begin(9600); delay(2000); // needed for some boards printMemoryDetails(); String str = "Arduino is great"; Serial.print(str); Serial.println(F(" - details:")); Serial.print(F("Stack address:\t")); Serial.println((int)&str); Serial.print(F("Heap address: \t")); int *strPtr; // pointer to heap strPtr = (int*)(int)&str; Serial.println(*strPtr); Serial.print(F("Capacity:\t")); Serial.println(*(uint16_t*)((int)&str + 2)); Serial.print(F("Length: \t")); Serial.println(*(uint16_t*)((int)&str + 4)); Serial.println(F("Read from Heap:")); for(unsigned int i=*strPtr; i<(*strPtr + str.length()); i++){ Serial.print(*(char*)(i)); Serial.print(" "); } Serial.println("\n"); printMemoryDetails(); } void loop() { Serial.println(F("In loop: ")); printMemoryDetails(); while(1); // stop here } void printMemoryDetails(){ Serial.print(F("Free memory: ")); Serial.print(getTotalAvailableMemory()); Serial.print(F(" / Biggest free block: ")); Serial.println(getLargestAvailableBlock()); Serial.println(); }
// C++ for Arduino // What is heap fragmentation? // https://cpp4arduino.com/ // This source file captures the platform dependent code. // This version was tested with the AVR Core version 1.6.22 // This code is freely inspired from https://github.com/McNeight/MemoryFree // This heap allocator defines this structure to keep track of free blocks. struct block_t { size_t sz; struct block_t *nx; }; // NOTE. The following extern variables are defined in malloc.c in avr-stdlib // A pointer to the first block extern struct block_t *__flp; // A pointer to the end of the heap, initialized at first malloc() extern char *__brkval; // A pointer to the beginning of the heap extern char *__malloc_heap_start; static size_t getBlockSize(struct block_t *block) { return block->sz + 2; } static size_t getUnusedBytes() { char foo; if (__brkval) { return size_t(&foo - __brkval); } else { return size_t(&foo - __malloc_heap_start); } } size_t getTotalAvailableMemory() { size_t sum = getUnusedBytes(); for (struct block_t *block = __flp; block; block = block->nx) { sum += getBlockSize(block); } return sum; } size_t getLargestAvailableBlock() { size_t largest = getUnusedBytes(); for (struct block_t *block = __flp; block; block = block->nx) { size_t size = getBlockSize(block); if (size > largest) { largest = size; } } return largest; }
Das ist die Ausgabe bei Verwendung eines ATmega328P basierten Boards:

5.1.2. Besprechung von example_3.ino
Der zur Verfügung stehende Speicher beträgt zu Beginn des Programms 1815 Byte. Dann definieren wir den String „str“ (= „Arduino is great“). Er besteht aus 16 Zeichen und seine Adresse im Stack ist 2294.
„strPtr“ ist der Zeiger auf die Adresse der Zeichenkette im Heap. Mit strPtr = (int*)(int)&str;
lesen wir den Zeiger aus „str“ aus (Adresse 2294-2295). Mit derselben Technik ermitteln wir die Kapazität und die Länge. Beide Werte betragen 16.
Dann lesen wir die Zeichenkette des String-Objektes direkt aus dem Heap. Nicht, dass man das grundsätzlich tun sollte! Es dient lediglich dem Beweis, dass die Zeichenkette dort tatsächlich steht.
Der freie Speicher wird durch den String um 19 Byte reduziert. Aber warum belegt der String 19 Byte im Heap und nicht 16? Ein Byte wird für den Null-Terminator ‚\0‘ benötigt. Grundsätzlich werden Variablen im Heap noch durch zwei weitere Bytes voneinander getrennt. Deren Zweck ist mir allerdings nicht wirklich klar (für Hinweise wäre ich dankbar!).
Und wo sind die 6 Byte für die Variable „str“ aus dem Stack in dieser Rechnung? Sie gehen nicht in die Rechnung ein – der freie Speicher wird auf Grundlage der maximalen Stackausdehnung berechnet.
Nach Beendigung des Setup wird der durch den String belegte Speicherplatz im Heap wieder freigegeben. Somit stehen in der Hauptschleife (loop) wieder 1815 Byte zur Verfügung.
5.1.3. Anpassung für ESP32 und ESP8266
Der Sketch example_3 funktioniert nur auf AVR Boards. Hier eine angepasste Version für den ESP32 und ESP8266:
void setup() { Serial.begin(9600); delay(2000); // needed for some boards printMemoryDetails(); String str = "Arduino is great"; Serial.print(str); Serial.println(F(" - details:")); Serial.print(F("Stack address:\t")); Serial.println((int)&str); Serial.print(F("Heap address: \t")); int *strPtr; strPtr = (int*)(int)&str; Serial.println(*strPtr); Serial.print(F("Capacity:\t")); Serial.println(*(uint16_t*)((int)&str + 4)); Serial.print(F("Length: \t")); Serial.println(*(uint16_t*)((int)&str + 8)); Serial.println(F("Read from Heap:")); for(unsigned int i=*strPtr; i<(*strPtr + str.length()); i++){ Serial.print(*(char*)(i)); Serial.print(" "); } Serial.println("\n"); printMemoryDetails(); } void loop() { Serial.println(F("In loop: ")); printMemoryDetails(); while(1){ // stop here delay(1000); // prevents WDT reset on ESP8266 } } void printMemoryDetails(){ Serial.print(F("Free memory: ")); // for ESP32: Serial.println(esp_get_free_heap_size()); // for ESP8266: //Serial.println(ESP.getFreeHeap()); Serial.println(); }
Der Sketch erzeugt bei Verwendung eines ESP32 die folgende Ausgabe:

Strings werden auf dem ESP32 also anders verarbeitet. Wenn ihr ein wenig mit der Länge des Strings spielt, werdet ihr sehen:
- Hat der String eine Länge bis 15 Zeichen, wird ihm eine Kapazität von 15 Zeichen (= 15 Byte) reserviert. Wie bei den AVR Mikrocontrollern kommen noch drei Byte hinzu, sodass der Platzbedarf für die Zeichenkette im Heap 18 Byte beträgt. Hinzu kommen die zuvor genannten 12 Byte für die „String-Variable“ im Stack.
- Ist die Länge zwischen 15 und 31 Zeichen, dann werden 31 Zeichen reserviert, zwischen 32 und 47 sind es 47 – und so geht es in 16er-Schritten weiter.
- Strings einer Länge von kleiner 14 werden gar nicht auf den Heap ausgelagert. Probiert es aus, indem ihr den String auf „Arduino“ kürzt. Der Sketch gibt dann eine Kapazität von 28265 und eine Länge von 111 aus. Das ist natürlich Blödsinn. Was wir hier lesen ist 28265 = 0x6E69 ⇒ 110 (0x6E) / 105 (0x69) ⇒ ASCII-Zeichen ‘n’ / ‘i’ bzw. 111 ⇒ ‘o’, also die Zeichen aus „Arduino“.
- Diese Optimierung heißt Small String Optimization (SSO).
Beim ESP8266 werden Strings mit weniger als 11 Zeichen nicht ausgelagert, ansonsten verhält er sich ähnlich.
Bei den nächsten Beispielen werde ich nicht mehr auf Anpassungen für Nicht-AVR-Boards eingehen, da der Beitrag ohnehin schon viel zu lang ist. Auf Basis der bisherigen Erklärungen solltet ihr das bei Bedarf selbst können.
5.2. Heap Fragmentierung
5.2.1. Beispielsketch
Viele warnen vor der Verwendung von Strings im Bereich der Mikrocontroller. Eines der Hauptargumente ist dabei die drohende Fragmentierung des Heap (ihr erinnert euch an den zu Beginn erwähnten Bücherschrank?).
Mit dem folgenden Sketch erzeugen wir ein Loch im Heap:
void setup() { Serial.begin(9600); delay(2000); // needed for some boards Serial.print(F("Free memory: ")); Serial.print(getTotalAvailableMemory()); Serial.print(F(" / Biggest free block: ")); Serial.println(getLargestAvailableBlock()); Serial.println(); String s = "Arduino is great"; //s.reserve(26); printStringDetails(s); String t = "ESP32 is fast"; //t.reserve(23); printStringDetails(t); String u = "Wemos is fabulous"; //u.reserve(27); printStringDetails(u); Serial.println(); for(int i = 1; i<=10; i++){ s += "!"; } printStringDetails(s); for(int i = 1; i<=10; i++){ t += "!"; } printStringDetails(t); for(int i = 1; i<=10; i++){ u += "!"; } printStringDetails(u); } void loop() {} void printStringDetails(String &str){ Serial.print(str); Serial.println(F(" - details:")); Serial.print(F("Stack address: ")); Serial.print((int)&str); Serial.print(F(" / Heap address: ")); int *strPtr; //Pointer to heap address uint16_t capacity = *(uint16_t*)((int)&str + 2); // for some boards + 4 uint16_t len = *(uint16_t*)((int)&str + 4); // for some boards + 6 strPtr = (int*)(int)&str; Serial.print(*strPtr); Serial.print(F(" - ")); Serial.println(*strPtr + capacity + 2); Serial.print(F("Capacity: ")); Serial.print(capacity); Serial.print(F(" / Length: ")); Serial.println(len); // Read from Heap if you want // for(unsigned int i=*strPtr; i<(*strPtr + str.length()); i++){ // Serial.print(*(char*)(i)); // Serial.print(" "); // } // Serial.println(); // comment the following lines if you use a non-AVR based board Serial.print(F("Free memory: ")); Serial.print(getTotalAvailableMemory()); Serial.print(F(" / Biggest free block: ")); Serial.println(getLargestAvailableBlock()); Serial.println(""); }
// C++ for Arduino // What is heap fragmentation? // https://cpp4arduino.com/ // This source file captures the platform dependent code. // This version was tested with the AVR Core version 1.6.22 // This code is freely inspired from https://github.com/McNeight/MemoryFree // This heap allocator defines this structure to keep track of free blocks. struct block_t { size_t sz; struct block_t *nx; }; // NOTE. The following extern variables are defined in malloc.c in avr-stdlib // A pointer to the first block extern struct block_t *__flp; // A pointer to the end of the heap, initialized at first malloc() extern char *__brkval; // A pointer to the beginning of the heap extern char *__malloc_heap_start; static size_t getBlockSize(struct block_t *block) { return block->sz + 2; } static size_t getUnusedBytes() { char foo; if (__brkval) { return size_t(&foo - __brkval); } else { return size_t(&foo - __malloc_heap_start); } } size_t getTotalAvailableMemory() { size_t sum = getUnusedBytes(); for (struct block_t *block = __flp; block; block = block->nx) { sum += getBlockSize(block); } return sum; } size_t getLargestAvailableBlock() { size_t largest = getUnusedBytes(); for (struct block_t *block = __flp; block; block = block->nx) { size_t size = getBlockSize(block); if (size > largest) { largest = size; } } return largest; }
Auf einem Arduino Nano habe ich die folgende Ausgabe erhalten:

5.2.2. Besprechung von example_4
Wir erzeugen drei Strings:
- s: „Arduino is great“ – 16 Zeichen, belegt 19 Byte im Heap.
- t: „ESP32 is fast“ -13 Zeichen, belegt 16 Byte im Heap.
- u: „Wemos is fabulous“ – 17 Zeichen, belegt 20 Byte im Heap.

Alle drei Zeichenketten werden hintereinander im Heap gespeichert (Reihenfolge: s→t→u). Dann spendieren wir String „s“ 10 Ausrufezeichen. Dadurch nimmt er zusätzliche 10 Byte in Anspruch und passt nicht mehr an seinen ursprünglichen Speicherplatz. Deshalb wandert er an die nächste freie Stelle im Heap. Der Heap hat nun ein Loch von 19 Byte und die Reihenfolge ist t→u→s.
Jetzt verlängern wir String „t“ um 10 Ausrufezeichen. Seine Startadresse wandert an die ehemalige Startadresse von „s“. Trotz der Verlängerung verbleibt aber immer noch eine Lücke von 9 Byte (532 – 540).
Schließlich bekommt auch „u“ seine 10 Extra-Zeichen. Zwischen 532 und 560 sind 29 Byte Spielraum. „u“ braucht aber 30 Byte und wandert deshalb nach oben (Reihenfolge t→s→u) und die Lücke wird wieder größer.
In diesem Beispiel ist die Lücke nicht weiter schlimm, da der Heap mit dem Verlassen des Setup wieder „clean“ ist. Problematischer ist das Hantieren mit Strings in der Hauptschleife. Wenn ihr dort mit Strings unterschiedlicher Länge arbeitet, können sich, wenn es dumm läuft, die Löcher aufaddieren. Einen tollen Beitrag, in dem das auf die Spitze getrieben wird, findet ihr hier.
5.2.3. Heap Fragmentierung mit reserve() verhindern
Die eben gezeigte Fragmentierung lässt sich einfach verhindern. Ihr überlegt euch, wie lang euer String maximal werden kann und reserviert den Platz mit Stringname.reserve(maximum_length)
. Probiert es aus, indem ihr die Zeilen 11, 15 und 19 in example_4 entkommentiert.
Selbst wenn ihr etwas mehr Speicher reserviert, als notwendig wäre (z.B. weil ihr die tatsächliche Länge nicht kennt), verliert ihr unter Umständen weniger Speicher als durch die Fragmentierung. Außerdem habt ihr die Dinge unter Kontrolle. Und ihr spart Zeit, da das „Umziehen“ der Strings im Heap einen erheblichen Rechenaufwand erfordert.
5.3. Verketten von Strings
5.3.1 Beispielsketch
Es gibt aber noch mindestens eine weitere Stolperfalle bei Verwendung von Strings, und zwar betrifft das ihre Verkettung.
Im folgenden Sketch verketten (addieren) wir drei Strings:
void setup() { Serial.begin(9600); delay(2000); String s = "Arduino is great"; printStringDetails(s); String t = "ESP32 is fast"; printStringDetails(t); String u = "Wemos is fabulous"; printStringDetails(u); String v = ""; v = s + t + u; // v += s; // alternative: v.concat(s); // v += t; // alternative: v.concat(t); // v += u; // alternative: v.concat(u); printStringDetails(v); } void loop() {} void printStringDetails(String &str){ Serial.print(str); Serial.println(F(" - details:")); Serial.print(F("Stack address: ")); Serial.print((int)&str); Serial.print(F(" / Heap address: ")); int *strPtr; //Pointer to heap address uint16_t capacity = *(uint16_t*)((int)&str + 2); uint16_t len = *(uint16_t*)((int)&str + 4); strPtr = (int*)(int)&str; Serial.print(*strPtr); Serial.print(F(" - ")); Serial.println(*strPtr + capacity + 2); Serial.print(F("Capacity: ")); Serial.print(capacity); Serial.print(F(" / Length: ")); Serial.println(len); // for(uint16_t i=*strPtr; i<(*strPtr + str.length()); i++){ // Serial.print(*(char*)(i)); // Serial.print(" "); // } // Serial.println(); Serial.print(F("Free memory: ")); Serial.print(getTotalAvailableMemory()); Serial.print(F(" / Biggest free block: ")); Serial.println(getLargestAvailableBlock()); Serial.println(""); }
// C++ for Arduino // What is heap fragmentation? // https://cpp4arduino.com/ // This source file captures the platform dependent code. // This version was tested with the AVR Core version 1.6.22 // This code is freely inspired from https://github.com/McNeight/MemoryFree // This heap allocator defines this structure to keep track of free blocks. struct block_t { size_t sz; struct block_t *nx; }; // NOTE. The following extern variables are defined in malloc.c in avr-stdlib // A pointer to the first block extern struct block_t *__flp; // A pointer to the end of the heap, initialized at first malloc() extern char *__brkval; // A pointer to the beginning of the heap extern char *__malloc_heap_start; static size_t getBlockSize(struct block_t *block) { return block->sz + 2; } static size_t getUnusedBytes() { char foo; if (__brkval) { return size_t(&foo - __brkval); } else { return size_t(&foo - __malloc_heap_start); } } size_t getTotalAvailableMemory() { size_t sum = getUnusedBytes(); for (struct block_t *block = __flp; block; block = block->nx) { sum += getBlockSize(block); } return sum; } size_t getLargestAvailableBlock() { size_t largest = getUnusedBytes(); for (struct block_t *block = __flp; block; block = block->nx) { size_t size = getBlockSize(block); if (size > largest) { largest = size; } } return largest; }
Ausgabe auf einem Arduino Nano:

5.3.2. Besprechung von example_5
Der Sketch reißt ein großes Loch von 53 Byte in den Heap. Der Grund dafür ist das Zwischenergebnis, das im Heap abgelegt wird. s und t und u werden erst addiert und dann das Ergebnis v zugewiesen.
Die gute Nachricht: Ihr könnt das Loch durch eine kleine Veränderung im Code verhindern. Kommentiert die Zeile 13 aus und entkommentiert die Zeilen 14 bis 16. Das Ergebnis ist „lochfrei“. „Free memory“ und „Biggest free block“ sind 1659 Byte. Probiert es einfach mal aus.
5.4. Darf ich Strings denn nun benutzen oder nicht?
Für viele Programmierer sind Strings im Mikrocontrollerbereich Teufelszeug, das man auf gar keinen Fall verwenden darf. Keine Frage: Character Arrays sind ressourcenschonender und schneller. Allerdings kann man bei ihrer Verwendung auch viele Fehler machen. Hinzu kommt, dass der Code zumindest für Einsteiger schwerer lesbar ist. Zudem greifen viele Hobbyisten heute zu schnellen und mit reichlich SRAM gesegneten Mikrocontrollern wie dem ESP32 oder ESP8266.
Wer sich darin bestätigt sehen möchte, dass man keine Strings verwenden sollte, der kann sich diesen Artikel durchlesen: The evils of Arduino Strings. Wer ein etwas differenzierteres Urteil hören möchte, der schaue sich den wunderbaren Artikel Taming Arduino Strings (Zähmen von Arduino Strings) an.
Meine Meinung ist: wenn es nicht auf Geschwindigkeit ankommt und ihr nicht knapp an SRAM seid, dann benutzt ruhig Strings. Ich tue es jedenfalls! Aber seid euch der Gefahren bewusst und trefft einige Vorkehrungen:
- Vermeidet es, Strings in Loop() zu erzeugen.
- Nutzt reserve(), wenn sich die Länge euer Strings ändern kann.
- Verbindet Strings mit „+=“ oder concat().
- Um SRAM zu sparen, übergebt Strings als Referenzen (siehe letzter Beitrag).
Weitere Tipps findet ihr hier in dem schon erwähnten Artikel „Zähmen von Arduino Strings“ oder in diesem schönen Artikel.
6. Character Arrays
Der Vollständigkeit halber noch ein paar Worte zu den Character Arrays. Wenn ihr die Länge nicht festlegt, dann belegen sie im Stack – wie schon erwähnt – so viele Bytes wie sie Zeichen haben, plus eines für den Null-Terminator. Wenn ihr das Character Array nicht ändern wollt, dann solltet ihr es als Konstante deklarieren. Das schützt euch vor eigenen Fehlern und macht den Code klarer. Sollte die Länge des Character Array variieren, dann reserviert so viel Platz, wie ihr maximal erwartet.
Hier das Beispiel dazu:
void setup() { Serial.begin(9600); const char a[] = "I am a character array"; char b[30] = "Arduino is great"; char c[30] = "ESP32 is fast"; char d[30] = "Wemos is fabulous"; printCharArrayDetails(a, sizeof(a)); printCharArrayDetails(b, sizeof(b)); printCharArrayDetails(c, sizeof(c)); printCharArrayDetails(d, sizeof(d)); Serial.println(); strcat(b, "!!!!!!!!!!"); strcat(c, "!!!!!!!!!!"); strcat(d, "!!!!!!!!!!"); printCharArrayDetails(b, sizeof(b)); printCharArrayDetails(c, sizeof(c)); printCharArrayDetails(d, sizeof(d)); } void loop() {} void printCharArrayDetails(char* cArr, int len){ for(int i=0; i<len; i++){ Serial.print(cArr[i]); } Serial.println(F(" - details:")); Serial.print(F("Length: ")); Serial.print(len); Serial.print(F(" / Address: ")); Serial.print((int)cArr); Serial.println("\n\r"); }
Und hier die Ausgabe bei Verwendung eines Arduino Nano:

7. Variablen in den Heap zwingen
Wie ihr gesehen habt, werden die meisten lokalen Variablen automatisch im Stack gespeichert, es sei denn, es handelt sich um Strings. Ihr könnt Variablen aber auch in den Heap zwingen. Dafür gibt es zwei Methoden:
- Deklaration mit dem Schlüsselwort
new
. - Zuweisung von Speicherplatz per
malloc()
(memory allocation).
Der folgende Sketch veranschaulicht den Gebrauch:
void setup() { Serial.begin(9600); delay(2000); int *a = new int[5]; // reserve memory for 5 integers for(int i=0; i<5; i++){ a[i] = 2*i; } int heapAddrA = *(int*)(int)&a; // just to show you find a in heap char *b = new char[2]; // reserve memory for a char array b[0] = 'b'; int heapAddrB = *(int*)(int)&b; Serial.print(F("Stack address a: ")); Serial.println((int)&a); Serial.print(F("Stack address b: ")); Serial.println((int)&b); Serial.print(F("Heap address a: ")); Serial.println(heapAddrA); Serial.print(F("Heap address b: ")); Serial.println(heapAddrB); int *c = (int*)malloc(5 * sizeof(int)); // reserve memory for 5 integers for(int i=0; i<5; i++){ c[i] = i * 2000; } int heapAddrC = *(int*)(int)&c; Serial.print(F("Stack address c: ")); Serial.println((int)&c); Serial.print(F("Heap address c: ")); Serial.println(heapAddrC); Serial.println(); Serial.print(F("Available memory before deletion: \t")); Serial.println(getTotalAvailableMemory()); delete a; // new -> delete delete b; free(c); // malloc -> free Serial.print(F("New available memory after deletion: \t")); Serial.println(getTotalAvailableMemory()); } void loop() {}
// C++ for Arduino // What is heap fragmentation? // https://cpp4arduino.com/ // This source file captures the platform dependent code. // This version was tested with the AVR Core version 1.6.22 // This code is freely inspired from https://github.com/McNeight/MemoryFree // This heap allocator defines this structure to keep track of free blocks. struct block_t { size_t sz; struct block_t *nx; }; // NOTE. The following extern variables are defined in malloc.c in avr-stdlib // A pointer to the first block extern struct block_t *__flp; // A pointer to the end of the heap, initialized at first malloc() extern char *__brkval; // A pointer to the beginning of the heap extern char *__malloc_heap_start; static size_t getBlockSize(struct block_t *block) { return block->sz + 2; } static size_t getUnusedBytes() { char foo; if (__brkval) { return size_t(&foo - __brkval); } else { return size_t(&foo - __malloc_heap_start); } } size_t getTotalAvailableMemory() { size_t sum = getUnusedBytes(); for (struct block_t *block = __flp; block; block = block->nx) { sum += getBlockSize(block); } return sum; } size_t getLargestAvailableBlock() { size_t largest = getUnusedBytes(); for (struct block_t *block = __flp; block; block = block->nx) { size_t size = getBlockSize(block); if (size > largest) { largest = size; } } return largest; }
Hier die Ausgabe:

Ihr seht, dass die mit new
und malloc()
erzeugten Objekte je zwei Byte im Stack belegen. Dabei handelt es sich aber nur um den Zeiger auf die eigentlichen Daten im Heap.
Und was soll das? Es gibt Anwendungen, bei denen man im Programmverlauf Variablen oder Objekte erzeugen muss, aber man weiß erst zur Laufzeit, um wie viele es sich handelt und wie groß sie werden. Dann bieten sich new
und malloc()
an, um den benötigten Speicherplatz zu reservieren. Der Vorteil ist, dass ihr den Speicherplatz mit delete
bzw. free()
wieder freigeben könnt. Auf den ersten Blick tun new
und malloc()
dasselbe, es gibt aber ein paar wichtige Unterschiede. Interessierte mögen hier schauen.
Der Einsatz von new
und malloc()
ist nicht frei von Risiken. Vergesst ihr den Speicherplatz wieder freizugeben, könnte euch der Speicher ausgehen. Oder ihr vergesst, dass ihr den Speicherplatz schon freigegeben habt und versucht immer noch mit eurem Zeiger darauf zuzugreifen. Das Problem ist: Es funktioniert. Fügt beispielsweise nach free(c)
ein Serial.println(c[3])
ein. Ihr lest immer noch den Wert, der dort vorher stand. Allerdings nur solange, wie der Speicherplatz noch nicht überschrieben wurde. Danach lest ihr dort irgendetwas und wundert euch, warum sich euer Programm merkwürdig verhält. Solche Fehler sind schwer zu finden.
8. SRAM sparen mit PROGMEM und F()
8.1. PROGMEM
Konstanten, die euch zu viel SRAM wegnehmen, könnt ihr ganz bequem aus dem SRAM verbannen. Sie werden dann aus dem Flash gelesen. Das bietet sich natürlich besonders bei langen Zahlenarrays und Zeichenketten an. Bei der Definition der Konstanten müsst ihr lediglich das Schlüsselwort PROGMEM hinzufügen, also: const datatype arrayName[] PROGMEM = { data };
.
Das Auslesen der Daten erfordert nur wenig Umgewöhnung. Anstelle von element_i = arrayName[i];
schreibt ihr element_i = pgm_read_type_near (arrayName + i)
mit type = byte, word, oder dword
.
Hier ein Beispielsketch, der die Funktion veranschaulichen soll:
const byte byteArray[] PROGMEM = {11, 22, 33, 44, 55, 66}; const int intArray[] PROGMEM = {1111, 2222, 3333, 4444}; const unsigned long longArray[] PROGMEM = {1111111, 2222222, 3333333, 4444444, 5555555}; const char charArray[] PROGMEM = {"Hello, PROGMEM helps you saving SRAM!"}; void setup() { Serial.begin(9600); delay(2000); // needed for some boards for(unsigned int i=0; i<sizeof(byteArray)/sizeof(byte); i++){ byte element = pgm_read_byte_near(byteArray + i); Serial.print(element); Serial.print(" "); } Serial.println("\n"); for(unsigned int i=0; i<sizeof(intArray)/sizeof(int); i++){ int element = pgm_read_word_near(intArray + i); Serial.print(element); Serial.print(" "); } Serial.println("\n"); for(unsigned int i=0; i<sizeof(longArray)/sizeof(long); i++){ long element = pgm_read_dword_near(longArray + i); Serial.print(element); Serial.print(" "); } Serial.println("\n"); for(unsigned int i=0; i<strlen_P(charArray); i++){ // alternative: i<sizeof(charArray)/sizeof(char); char element = pgm_read_byte_near(charArray + i); Serial.print(element); } Serial.println("\n"); } void loop() {}
Ich habe noch eine zweite Version von example_8 ohne PROGMEM geschrieben (hier aber nicht abgebildet) und dann beide Versionen zum Vergleich hochgeladen. Der Unterschied bei der Belegung des SRAM durch globale Variablen beträgt 72 Byte:

Das sind genau die 72 Byte, die die Konstanten in example_8 auf einem ATmega328P basierten Board benötigen. Falls ihr nachzählt, vergesst nicht den Null-Terminator.
8.2. Das F()-Makro
Vielleicht ist euch aufgefallen, dass ich in diesem Beitrag konsequent das F-Makro verwende, also Serial.print(F("blabla"));
. Ohne F() würde „blabla“ bei Programmstart in den Static Data Bereich des SRAM geschrieben und dort bei Bedarf gelesen. Mit F() liest der Mikrocontroller „blabla“ direkt aus dem Flash. Das ist eine sehr einfache Methode, SRAM einzusparen. Ich habe hierzu keinen Beispielsketch. Nehmt einfach einmal bei den obigen Sketchen ein F() heraus und vergleicht die Belegung durch globale Variablen.
Danksagung
Den Hintergrund meines Beitragsbildes verdanke ich Daan Lenaerts auf Pixabay.
Vielen Dank für diese sehr aufschlussreiche Erklärung dieser doch ziemlich komplexen Thematik. Ich bin zwar noch nicht lange unter Deiner Leserschaft, aber ich konnte schon viel von Deinem Wissen profitieren. Deine Beiträge sind echt gut geschrieben!!
Ich habe aber dennoch 3 (möglicherweise blöde – entschuldige!) Fragen, die mich nicht loslassen:
1. Ganz oben schreibst Du: „[…], warum man bei der Nutzung von Strings im Arduinobereich Vorsicht walten lassen sollte.“: Was ist damit gemeint? Bezieht sich diese Aussage auf die Programmierung beliebiger Boards (bspw. auch ESP32 Dev Boards) mittels der Arduino-IDE? Spricht: Würde der ESP32 z.B. mittels Espressif IDF programmiert, gäbe es dieses Problem nicht? Weshalb? Weil es dort keine String-Klasse gibt und alles mit Char Arrays gemacht werden müsste?
– Kap. 2: Wieso wurde der Vorteil vom Stack (dass sich die Lücken „automatisch“ schliessen) beim Heap nicht „einfach“ auch so implementiert? Oder anders: Ist es vielleicht so, dass sich beim Stack gar keine Lücken bilden können, weil dieser jeweils nur am Ende (also unten) dynamisch vergrössert resp. verkleinert wird? Die Analogie mit dem Buchstapel stimmt so also eigentlich nicht, da es tatsächlich nie Lücken gibt? Denn auch das Beispiel im Kap. 4.1 zeigt keine Lücken auf, sondern vergrössert resp. verkleinert den Stack nur an dessen jeweiligen Ende…
– Kap. 3.3: „Die Adressen für die globalen Variablen liegen oberhalb der Adressen der lokalen Variablen“: Ist dies wirklich so? Denn in der Abbildung liegen die globalen Variablen ab dem Speicherbereich 10734xxxxx, und die lokalen ab 10735xxxxx. Ich sehe also keinen Unterschied zum ATmega328P. In wie fern soll denn der Speicher des ESP32 auf eine andere Art organisiert sein? Dies erschliesst sich mir nicht.
Vielen Dank, Aumy
Hallo,
1) Der Grund für die Fragmentierung ist nicht die Arduino IDE, sondern wie die String-Klasse programmiert ist. Das grundsätzliche Prinzip ist beim ESP32 das gleiche, d. h. man kann auch mit dem ESP32 bei Verwendung des Arduino Boardpaketes Löcher im Arbeitsspeicher produzieren. Bei genauem Hinschauen gibt es dann aber doch Unterschiede. Z.B. werden beim ESP32 Strings bis zu einer Länge von 11 Zeichen (einschließlich 0-Terminator) ausschließlich im Stack gespeichert (SSO = Small String Optimization). Ich habe mich nicht mit den String-Klassen aller verfügbaren Boards bzw. mit allen Boardpaketen beschäftigt. Aber soweit ich weiß, ist das Grundprinzip immer gleich.
2) a) Warum Heap und Stack so organisiert sind, wie sie es sind, kann ich dir nicht sagen. Das Bild vom Bücherstapel ist sicherlich vereinfachend, aber damit wollte ich ja genau zum Ausdruck bringen, dass sich keine Lücken bilden. Ich ziehe ein Buch heraus, der Rest rutscht nach. Im Prinzip ist es dasselbe, wie im Stack. Und der vergrößerte oder verkleinerte belegte Stack ist die Höhe des Bücherstapels. Insofern finde ich den Vergleich immer noch gut, und insbesondere dass sich der Heap hingegen eher wie ein Bücherregal verhält, in dem beim Herausnehmen Lücken entstehen. .
2) Du hast recht bzgl. global vs. lokal (Danke). Das muss ich korrigieren. Trotzdem ist der Speicher anders organisiert. In meiner Beispielausgabe für den ESP32 ist der Abstand von der höchsten Adresse zur niedrigsten: 1073500460 – 1073470320 = 30140 Bytes. Das ist nur ca. ein Zehntel des zur Verfügung stehenden Arbeitsspeichers. Also wird hier nicht ganz oben und ganz unten begonnen. Im Beitrag ist auch ein Link, der zu einem Artikel über den Speicher des ESP32 führt.
VG, Wolfgang
Danke Dir Wolfgang für die rasche Bentwortung meiner Fragen und sorry, dass ich bzgl. Stack- und Heap-Organisation nochmals nachhake: Ich finde Dein Beispiel mit dem Bücherstapel und -Regal definitiv sinnvoll und einleuchtend. Aber an einem Punkt passt die Analogie zwischen Stack und Bücherstapel eben meines Erachtens nicht – und das ist doch genau der zentrale Unterschied zwischen Stack und Heap:
Im Stack können per Defintion erst gar keine Löcher entstehen (im Unterschied zum Bücherstapel, wo sie durch die Schwerkraft automatisch geschlossen werden). Die Analogie also, dass im Bücherstapel ein Buch herausgezogen wird, gibts beim Stack gar nicht: Der Stack wird nur an seinem Ende kontinurierlich auf- und abgebaut (nach dem LIFO-Prinzip: Last In First Out). Das Löschen einer Variable (also das „Herausziehen eines Buches“) mitten im Stack gibts doch schlicht nicht, oder sehe ich das falsch?
Siehe dazu auch Dein Beispiel im Kapitel 3.1 (Ausgabe auf einem Nano): Nehmen wir an, dass die im Stack befindliche Variable ‚e‘ mittendrin gelöscht würde, müssten die Adressen der anschliessenden Variablen ‚f‘ bis ‚l‘ alle je um 2 Bytes nach oben rücken – nur so würde keine Lücke entstehen (und die Analogie mit dem Bücherstapel wäre korrekt). Das Löschen mittendrin im Stack passiert meines Erachtens aber eben in KEINEM Fall, oder?
Liege ich mit meiner Vermutung richtig, oder kannst Du mir ein Beispiel nennen, in dem *mittem im Stack* eine Variable gelöscht wird?
M.E. ist das der konzeptionell fundametale Unterschied zwischen Stack und Heap.
Hi, danke, schon einleuchtend. Ich muss aber noch einmal in Ruhe darüber nachdenken, bevor ich heute abend Blödsinn schreibe!
Hi, du hast Recht. Ich muss das Bild des Stapels dahingehend ändern, dass Bücher nicht herausgezogen werden, sondern lediglich auf den Stapel gelegt und von oben wieder heruntergenommen werden. Durch die systematische Stapelung besteht keine Notwenigkeit ein Buch herausziehen zu müssen. Werde ich abändern. Vielen lieben Dank!
VG, Wolfgang
Grossartig – danke! Um die vielen Details zu verstehen brauche ich noch ein paar Durchläufe. Aber die fundamentalen Tipps zur Speichersparung machen das Leben (meines und dasjenige des SRAMs) viiiiel einfacher.
Danke auch für dein Bemühen, die meisten Sketches sogar für ESP umzuschreiben.
Für Hobbyisten wie mich, die nur an der Oberfläche der Möglichkeiten kratzen sind deine Ausführungen abenteuerliche Expeditionen in die Unterwelt.
Andreas
Die Grafiken und Bilder die du verwendest haben echt Klasse und gefallen!
Ich danke vielmals – das tut sehr gut!
Andrew S. Tanenbaum … findest du auch in der Library Genesis, http://libgen.rs/
sowie andere Standardwerke zu Algorithmen, Data Structure, RTOS, … und ganz viel C++ sowie Python.
Damit sind wir gut beschäftigt bis ans Ende unserer Zeit 🙂 und brauchen keine Glotze.
Zum Thema:
const byte byteArray[] PROGMEM würde ich sinniger immmer so schreiben
PROGMEM const byte byteArray[]
Dein nächstes Thema könnte ja mal EEPROM sein, da sollte immer ein „grosses struct“ verwendet werden, damit nicht per Hand die Adressen abgezählt werden müssen, wie Hilflose sehr oft gerne machen und aufzeigen wie aus Type float ein uint32_t wird und umgekehrt. Union ist auch hilfreich und wichtig zu verstehen …
Ich habe auch was gelernt, weil über String Handling habe ich noch nicht viel nachgedacht.
Interessant ist auch FixPoint Aritmetik, code size printf/sprintf/… und eigene printf Funktion.
Für AVR wären auch die GPIO Register (328p hat 3 davon) wichtig zu kennen und was man damit anfangen kann.
Themen werden eher nie ausgehen …
Vielen Dank für deine sehr kompetenten Kommentare!
Vielen Dank für deine sehr kompetenten Kommentare!
Die 2 Bytes bei einem Heap-Block sind leicht erklärt. Um den Heap zu verwalten muss man auf irgend eine Weise speichern, wo die Speicherblöcke liegen und wie groß sie sind. Spiel mal mit malloc() und mach Hex-Dumps, dann sieht man, hier wird First-Fit als Algorithmus verwendet und eine einfach verkettete Liste als Verwaltungsstruktur genutzt. VOR jeden Speicherblock steht die Größe des Blocks. Allokiert man einen Block (hier der String str), wird an dessen Ende der neu entstandene Block, also der bisherige freie Bereich minus des gerade allokierten Bereichs, eingetragen. Sucht man einen freien Speicher, geht man die Liste der Blöcke von vorn bis hinten durch, bis man einen passenden findet. FirstFit hat Nachteile bei der Laufzeit und bei der Fragmentierung, aber hat wenig Verschnitt (hier halt 2 Bytes je Block). Bei größeren Systemen nimmt man deshalb andere Verfahren.
Habe z.B. mal den Code mit weiteren Strings erweitert:
String str = „Arduino is great“;
String str1 = „Space“;
String str2 = „more space“;
String str3 = „even more space“;
und dann einen Hex-Dump gemacht (vorher gesamten speicher mit 0 gefüllt):
00000270 – 41 72 64 75 69 6E 6F 20 69 73 20 67 72 65 61 74 : Arduino is great
00000280 – 00 06 00 53 70 61 63 65 00 0B 00 6D 6F 72 65 20 : …Space …more
00000290 – 73 70 61 63 65 00 10 00 65 76 65 6E 20 6D 6F 72 : space… even mor
000002A0 – 65 20 73 70 61 63 65 00 00 00 00 00 00 00 00 00 : e space. ……..
Nach der \0 des ersten Strings folgt eine 0x0006, bedeutet 6 bytes weiter endet der nächste Block.
An dessen Ende steht 0x000B etc…. bis am ende eine 0 eingetragen wird, weil kein weiterer Block folgt.
Womöglich wird das MSBit als marker für belegt/frei verwendet, wobei 0 „belegt“ bedeuten dürfte.
Beste Erklärung von Speicherstrukturen und Allokationsalgorithmen ist in dem uralten Schinken von Andrew S. Tanenbaum „Betriebssysteme“. Ist immer noch das Standardwerk für Embedded zeugs.
Vielen Dank, danach hatte ich gesucht, aber nirgendwo gefunden. Cool. VG, Wolfgang
Hallo Wolfgang,
vielen Dank für diesen Beitrag. Wieder einmal viele interessante Details, über die ich mir bislang keine Gedanken gemacht habe.
Aber bitte noch einmal nach den Verknüpfungen schauen; außer zu deinem letzten Beitrag funktionieren diese nicht.
VG Wolfram
Danke! Kümmere mich gleich drum.
Update: jetzt sollte alles gehen! Da hatte sich WordPress irgendwie verschluckt…
Danke für den Beitrag 😀
Etwas verwirrend liest sich im Punkt „3.2. Besprechung von example_1.ino“
„Am Beispiel der Integer-Variablen a, b und c erkennen wir, dass globale Variablen am unteren Ende des Stack („Static Data“ Bereich) gespeichert werden.“
Und etwas später
„Im Setup haben wir noch einmal drei Integer-Variablen definiert, nämlich d, e und f. Sie befinden sich aber im Stack, also am oberen Ende des SRAM, weil es sich um lokale Variablen handelt.„
Warum kommt ein „aber“ im Satz vor? Wo ist der Bezug auf das was nicht im Stack liegt?
Globale und lokale Variablen liegen beide im Stack? Nur Strings liegen im Heap?
Vielen Dank! Das sollte natürlich am unteren Ende des SRAM sein für die globalen Variablen. Ändere ich gleich.