Der Speicherverbrauch eines C-Programmes lässt sich unter verschiedenen Gesichtspunkten betrachten.
- Ort der Datenablage
- SRAM
- Flash
- EEPROM
- Art der Daten
- Programmcode
- Daten
- veränderlich/unveränderlich
- persistent oder flüchtig
- Speicherklasse der Daten
- Statischer Speicherverbrauch
- Dynamischer Speicherverbrauch
Der Ort der Datenablage wird bestimmt von der Art der Daten. Daten, die nicht verändert werden dürfen oder nicht verändert werden müssen, wie der Programm-Code oder konstante Strings, wie sie oft zur Ausgabe verwendet werden, können im Flash gespeichert werden. Konstanten/Tabellen kann man auch im EEPROM speichern, wenn der Platz im Flash knapp ist. Der Programmcode selbst muss bei AVR im Flash liegen. Man kann den Code zwar auch ins RAM oder ins EEPROM legen, aber von dort nicht ausführen.
Daten, die zur Laufzeit verändert werden müssen, wird man im SRAM speichern, wenn sie einen Reset bzw. ein Ausschalten des Controllers nicht überleben müssen. Sollen die Daten auch ohne Strom erhalten bleiben, muss man den EEPROM oder den Flash als Ablageort wählen. Dabei ist das Speichern von Daten im Flash zur Laufzeit sehr aufwändig, weil man einen Bootloader für ihre Änderung anstrengen muss. Andererseits kann wesentlich schneller auf das Flash zugegriffen werden als auf den EEPROM-Speicher.
Die Art der Daten ergibt sich aus dem Programm und den zu lösenden Aufgaben, gleiches gilt für die Speicherklassen.
Die Ablageorte der statische Daten sind bereits zur Compilezeit bekannt. Hierzu gehören globale und statische Variablen. Dementsprechend ist auch schon zur Compile- bzw. Linkzeit bekannt, wieviel Speicher diese Daten belegen.
Speicherorte und Platzverbrauch dynamischer Variablen sind dem Compiler nicht bekannt. Sie ergeben sich erst zur Laufzeit durch das Allokieren von Speicher mit malloc, oder durch den Aufbau eines Stapels, auf dem lokale Variablen gesichert werden, während eine Funktion aufgerufen wird. Je nach Verschachtelungstiefe der Funktionsaufrufe — dazu gehören auch Interrupt Service Routinen, die prinzipiell jederzeit aufgerufen werden können — wird dafür auch unterschiedlich viel Speicher benötigt.
Inhaltsverzeichnis
Nutzung des SRAM durch avr-gcc
avr-gcc legt die Daten in Sections an. Nach aufsteigenden Speicheradressen sortiert sind diese:
- .data
- Statische und (modul-)globale, initialisierte Daten, denen man per Initializer einen Wert ungleich 0 zuweist. Beginnt an der unteren SRAM-Adresse 0x60 nach dem SFR-Bereich, je nach AVR-Derivat auch an anderer Adresse.
- .bss
- Statische und (modul-)globale, initialisierte Daten, die zu 0 initialisiert sind bzw. keinen Initializer haben (und also auch zu 0 initialisiert werden).
- .noinit
- Statische und (modul-)globale Daten, die nicht vom Startup-Code initialisiert werden und zum Beispiel einen Watchdog-Reset überdauern.
Auf diese Sections folgen noch zwei Speicherbereiche für dynamische Daten:
- Heap
- Danach folgt der Heap. Das ist ein Speicherbereich, aus dem Speicherplatz via malloc etc. allokiert wird.
- Stack
- Auf dem Stapel werden lokale Variablen/Register während Funktionsaufrufen gesichert. Der Stack wird teilweise auch zur Parameterübergabe verwendet und die return-Adresse von Funktionen und ISRs wird dort abgelegt. Der Stack beginnt an der oberen SRAM-Adresse und wächst nicht wie die andern Bereiche nach oben, sondern nach unten.
Die Größe der ersten drei Speicherbereiche kann man für jedes Modul bereits zur Compile-Zeit bestimmen, da sie unabhängig sind von der Programmausführung. Wie viel Platz die letzten beiden Bereiche brauchen, ergibt sich erst zur Laufzeit des Programms. Diese Größen ändern sich in aller Regel mit der Zeit.
Heap und Stack müssen sich den Speicher, der nicht von .data, .bss und .noinit belegt ist, teilen
Wird der Stackbereich zu groß, weil dort zu viele Daten abgelegt werden (zu viele lokale Variablen (lokale Arrays!), zu tief verschaltelte (rekursive) Funktionen, kaskadierende Interrupt Service Routinen, ...), dann überschreibt man damit womöglich andere Daten und es kommt zur Fehlfunktion des Programmes. Gleiches gilt, wenn die Obergrenze des Heap über der Untergrenze des Stabels hinauswächst.
Flash- und statischer RAM-Verbrauch
Zur Bestimmung des Speicherplatzes, den statische Daten belegen, verwendet man avr-size, das zu den Binutils gehört und z.B. bei WinAVR dabei ist.
Abhängig von der Section schlägt ihr Platzverbrauch in Flash/SRAM/EEPROM zu Buche:
belegter Speicher | Sections (Einzelgrößen addieren) | Beschreibung |
---|---|---|
Flash | .text + .bootloader + .data | Programmcode und Tabelle für initialisierte Daten |
SRAM | .data + .bss + .noinit | Daten (initialisiert + zu 0 initialisiert + nicht initialisiert) |
EEPROM | .eeprom | Daten, die man ins EEPROM gelegt hat |
Beispiele:
Das '>' nicht mittippen, es ist der Kommandozeilen-Prompt.
Verbrauch der einzelnen Module auflisten:
> avr-size -x foo1.o foo2.o ...
Verbrauch des gesamten Programms auflisten:
> avr-size -x -A foo.elf
In neueren Versionen von avr-size geht auch
> avr-size --mcu=atmega8 -C foo.elf
was den Verbrauch als Absolutwerte und in Prozentangabe auflistet. Ohne die Angabe der Controllers mit --mcu können natürlich keine Prozentwerte berechnet werden, es werden dann nur die absoluten Verbrauche ausgedruckt.
Platzverbrauch von Funktionen, Objekten, Variablen, etc. nach Größe sortiert:
> avr-nm --size-sort --print-size foo.elf
Hilfe zu avr-nm siehe: /WinAVR/doc/binutils/binutils.html/nm.html
Zusammenfassung aus www.mikrocontroller.net:
Großbuchstaben => globale Symbole / kleine Buchstaben => local symbols
T/t : The symbol is in the text (code) section.
D/d : The symbol is in the initialized data section.
B/b : The symbol is in the uninitialized data section (known as BSS).
Alle Symbole mit einem "T" (globale Funktionen), "t" (statische Funktionen) und letztlich auch mit einem "D" oder "d" (globale bzw. statische Daten, die haben ihre Initialisierungswerte im ROM) betreffen das FLASH-ROM. "B" und "b" brauchen ausschließlich RAM (werden beim Start mit 0 initialisiert). Die erste Spalte ist die Adresse des Symbols, die zweite ist die Größe (beides hexadezimal)
Dynamischer RAM-Verbrauch
Um den momentan freien Speicher zu bestimmen, zieht man einfach den Anfang des Heaps vom Stackpointer ab:
#include <avr/io.h> // __heap_start is declared in the linker script extern unsigned char __heap_start; ... uint16_t momentan_frei = SP - (uint16_t) &__heap_start;
Interessanter ist es jedoch, den Maximalverbrauch an Speicher bzw. das Minimum an freiem Speicher seit Applikationsstart zu bestimmen.
Mit der folgenden kleinen Routine kann der noch freie SRAM-Bereich bestimmt werden. Es wird nicht der momentan freie Speicher bestimmt, sondern das Minimum an Speicher, das bis dato frei geblieben ist. Dazu wird im Startup-Code das Muster 0xaa in den SRAM geschrieben. Durch Aufruf der Funktion get_mem_unused wird bestimmt, wieviel von diesem Muster noch intakt ist. Dieses Vorgehen ist deshalb notwendig, weil es auch ISR-Routinen gibt, die dynamisch Splatz (auf dem Stack) brauchen, und man eine Routine zur Bestimmung des Speicherverbrauchs nicht in den ISRs aufrufen will.
Mit optimierendem Compiler brauchen die beiden Routinen 42 Bytes an Flash.
Damit der Code den richtigen Wert liefert, darf keine dynamische Speicherallokierung mit malloc() etc. geschehen sein; ein __builtin_alloca ist hingegen kein Problem, da letzteres den Platz vom Stapel nimmt.
Die Funktion init_mem wird in der Init-Phase vor main aufgerufen.
- mem-check.h
#ifndef MEM_CHECK_H #define MEM_CHECK_H extern unsigned short get_mem_unused (void); #endif /* MEM_CHECK_H */
- mem-check.c
#include <avr/io.h> // RAMEND #include "mem-check.h" // Mask to init SRAM and check against #define MASK 0xaa // From linker script extern unsigned char __heap_start; // !!! This doesn't work together with malloc et.al. (whose use is // !!! discouraged on AVR, anyway). alloca, however, is no problem // !!! because it allocates on stack.
{{ccomment| Get minimum of free memory (in bytes) up to now. unsigned short get_mem_unused (void) { unsigned short unused = 0; unsigned char *p = &__heap_start; do { if (*p++ != MASK) break; unused++; } while (p <= (unsigned char*) RAMEND); return unused; } // !!! Never call this function, it is part of .init-Code void __attribute__ ((naked, section(".init3"))) init_mem (void); void init_mem (void) { // Use inline assembler so it works even with optimization turned off __asm volatile ( "ldi r30, lo8 (__heap_start)" "\n\t" "ldi r31, hi8 (__heap_start)" "\n\t" "ldi r24, %0" "\n\t" "ldi r25, hi8 (%1)" "\n" "0:" "\n\t" "st Z+, r24" "\n\t" "cpi r30, lo8 (%1)" "\n\t" "cpc r31, r25" "\n\t" "brlo 0b" : : "i" (MASK), "i" (RAMEND+1) ); }
Der Grund, hier auf Inline Assembler zurückzugreifen, ist folgender: Durch normalem C/C++-Code kann man nicht garantieren, daß alle Variablen in Registern leben. In diesem Falle würden Variablen, die auf dem Stack angelegt werden, durch die init-Routine überschrieben. Davon ab wird der Frame-Pointer erst zu Anfang von main gesetzt und um ihn zu lesen, bräuchte man ohnehin Inline-Assembler.