SRAM Management

Ü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:

  1. Flash, SRAM und EEPROM
  2. Der SRAM im Überblick
  3. Wie Variablen im SRAM gespeichert werden
  4. Stack Management
  5. String Handling im SRAM
    1. String-Analyse in Stack und Heap
    2. Heap Fragmentierung
    3. Verketten von Strings
    4. Darf ich Strings denn nun benutzen oder nicht?
  6. Character Arrays
  7. Variablen in den Heap zwingen
  8. 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. 

Speicher verschiedener Mikrocontroller
Speicher verschiedener Mikrocontroller

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:

Speicherinfos beim Programmupload
Speicherinfos beim Programmupload.

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:

Der SRAM des ATmega328P
Der SRAM des ATmega328P

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 Bücherstapel vor. Wenn ihr Buch aus einem Stapel nehmt, dann wird die Lücke, der Schwerkraft folgend, gefüllt. Neue Bücher landen oben auf dem Stapel. So 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:

example_1.ino - Ausgabe bei Verwendung eines Arduino Nano
example_1.ino – Ausgabe bei Verwendung eines 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:

example_1.ino – Ausgabe bei Verwendung eines ESP32

Wie ihr seht, ist der Speicher des ESP32 auf eine andere Art organisiert. Die Adressen für die globalen Variablen liegen oberhalb der Adressen der lokalen Variablen. 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):

Ausgabe von example_2.ino
Ausgabe von example_2.ino (Arduino Nano)

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 uns getLargestAvailableBlock().
    • 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:

Ausgabe example_3.ino (Arduino Nano)
Ausgabe example_3.ino (Arduino Nano)

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:

Ausgabe example_3_mod.ino (ESP32)
Ausgabe example_3_mod.ino (ESP32)

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:

Ausgabe example_4.ino (Arduino Nano)
Ausgabe example_4.ino (Arduino Nano)

5.2.2. Besprechung von example_4

Wir erzeugen drei Strings:

  1. s: „Arduino is great“ – 16 Zeichen, belegt 19 Byte im Heap.
  2. t:  „ESP32 is fast“ -13 Zeichen, belegt 16 Byte im Heap.
  3. u: „Wemos is fabulous“ – 17 Zeichen, belegt 20 Byte im Heap.
Heap Fragmentierung
Heap Fragmentierung

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:

Ausgabe example_5.ino (Arduino Nano)
Ausgabe example_5.ino (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:

Ausgabe example_6.ino (Arduino Nano)
Ausgabe example_6.ino (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:

  1. Deklaration mit dem Schlüsselwort new.
  2. 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:

Ausgabe example_7 (Arduino Nano)
Ausgabe example_7 (Arduino Nano)

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:

Saving SRAM with PROGMEM
Saving SRAM with PROGMEM

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.

13 thoughts on “SRAM Management

  1. 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

  2. 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 …

  3. 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.

  4. 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

    1. Danke! Kümmere mich gleich drum.

      Update: jetzt sollte alles gehen! Da hatte sich WordPress irgendwie verschluckt…

  5. 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?

    1. Vielen Dank! Das sollte natürlich am unteren Ende des SRAM sein für die globalen Variablen. Ändere ich gleich.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert