(→Nutzung des SRAM durch avr-gcc) |
Heha (Diskussion | Beiträge) K (→Nutzung des SRAM durch avr-gcc) |
||
(25 dazwischenliegende Versionen von 4 Benutzern werden nicht angezeigt) | |||
Zeile 15: | Zeile 15: | ||
* Dynamischer 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 | + | 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 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. | + | 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, | Die Art der Daten ergibt sich aus dem Programm und den zu lösenden Aufgaben, | ||
− | gleiches gilt für die | + | gleiches gilt für die Speicherklassen. |
− | 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 <tt>malloc</tt>, 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. | + | |
+ | 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 <tt>malloc</tt>, 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. | ||
=Nutzung des SRAM durch avr-gcc= | =Nutzung des SRAM durch avr-gcc= | ||
[[Bild:Avr-ram.png|right|thumb|RAM-Layout für ein AVR mit 1kByte SRAM]] | [[Bild:Avr-ram.png|right|thumb|RAM-Layout für ein AVR mit 1kByte SRAM]] | ||
− | [[avr-gcc]] legt die Daten in [[avr-gcc#Sections|Sections]] an. | + | [[avr-gcc]] legt die Daten in [[avr-gcc/Interna#Sections|Sections]] an. |
Nach aufsteigenden Speicheradressen sortiert sind diese: | Nach aufsteigenden Speicheradressen sortiert sind diese: | ||
− | ;<tt>.data</tt>: Statische und (modul-)globale, initialisierte Daten, denen man per Initializer einen Wert ungleich 0 zuweist. Beginnt an | + | ;<tt>.data</tt>: Statische und (modul-)globale, initialisierte Daten, denen man per Initializer einen Wert ungleich 0 zuweist. Beginnt an der unteren SRAM-Adresse <tt>0x60</tt> nach dem SFR-Bereich, je nach AVR-Derivat auch an anderer Adresse. |
;<tt>.bss</tt>: Statische und (modul-)globale, initialisierte Daten, die zu 0 initialisiert sind bzw. keinen Initializer haben (und also auch zu 0 initialisiert werden). | ;<tt>.bss</tt>: Statische und (modul-)globale, initialisierte Daten, die zu 0 initialisiert sind bzw. keinen Initializer haben (und also auch zu 0 initialisiert werden). | ||
− | ;<tt>.noinit</tt>: Statische und (modul-)globale Daten, die nicht vom Startup-Code initialisiert werden. | + | ;<tt>.noinit</tt>: Statische und (modul-)globale Daten, die nicht vom Startup-Code initialisiert werden und zum Beispiel einen [[Watchdog]]-Reset überdauern. Das ausgiebige Verwenden von <tt>.noinit</tt> hilft die Ausführungszeit des Initialisierungskodes vor <tt>main()</tt> zu verkürzen. Registervariablen werden ebenfalls nicht initialisiert, fallen jedoch in keine der RAM-Sections. |
− | Auf diese | + | Auf diese Sections folgen noch zwei Speicherbereiche für dynamische Daten: |
;<tt>Heap</tt>: Danach folgt der Heap. Das ist ein Speicherbereich, aus dem Speicherplatz via <tt>malloc</tt> etc. allokiert wird. | ;<tt>Heap</tt>: Danach folgt der Heap. Das ist ein Speicherbereich, aus dem Speicherplatz via <tt>malloc</tt> etc. allokiert wird. | ||
− | ;<tt>Stack</tt>: Auf dem Stapel werden lokale Variablen/Register während Funktionsaufrufen gesichert. Der Stack wird auch zur Parameterübergabe verwendet und die return-Adresse von Funktionen und [[ISR]]s wird dort abgelegt. Der Stack beginnt an der oberen SRAM-Adresse und wächst nicht wie die andern Bereiche nach oben, sondern nach unten. | + | ;<tt>Stack</tt>: 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 [[ISR]]s 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 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. | + | 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 | + | Heap und Stack müssen sich den Speicher, der nicht von <tt>.data</tt>, <tt>.bss</tt> und <tt>.noinit</tt> 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. | + | 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 Stapels hinauswächst. Genau genommen ist eine Designschwäche von avr-gcc, den angenommenen maximalen Stapelspeicherverbrauch nicht per Kommandozeile oder Linkerskript angeben zu müssen. |
+ | |||
+ | Die gängige Methode, den Stapel-Speicherverbrauch zu ''messen'' ist das Initialisieren des Freispeichers mit einem Kennbyte/-wort/-doppelwort und das gelegentliche Prüfen auf Veränderung in der Idle-Schleife des Hauptprogramms. | ||
+ | |||
+ | Einen Stapelüberlauf ''feststellen'' geht mit den gängigen ATtiny und ATmega nicht. Dazu braucht es einen geeigneten Interrupt oder Reset-Ursache. Der gängige Weg bei ''geeigneten'' Controllern (xmega?) ist die Umverteilung der Speicheranordnung im Linker-Skript (Stapel ''zuerst'' mit fester Größe) damit der Schreibzugriff außerhalb des RAM-Bereichs zur Ausnahme führt. | ||
+ | (Ein Stapel-Unterlauf, d.h. mehr <tt>pop</tt> als <tt>push</tt>, kommt eher selten vor, und führt schon vorher zu unerwartetem Programmverhalten.) | ||
+ | Eine Alternative zur Hardware-Exception wäre eine compilergenerierte Stapelüberprüfung zur Laufzeit. Leider bietet avr-gcc dies nicht an. Muss man ggf. selbst programmieren. | ||
=Flash- und statischer RAM-Verbrauch = | =Flash- und statischer RAM-Verbrauch = | ||
Zeile 52: | Zeile 62: | ||
! |belegter Speicher || Sections (Einzelgrößen addieren) ||Beschreibung | ! |belegter Speicher || Sections (Einzelgrößen addieren) ||Beschreibung | ||
|- | |- | ||
− | |Flash || <tt>.text</tt> + <tt>.data</tt> || Programmcode und Tabelle für initialisierte Daten | + | |Flash || <tt>.text</tt> + <tt>.bootloader</tt> + <tt>.data</tt> || Programmcode und Tabelle für initialisierte Daten |
|- | |- | ||
− | |SRAM || <tt>.data</tt> + <tt>.bss</tt> + <tt>.noinit</tt>|| Daten (initialisiert | + | |SRAM || <tt>.data</tt> + <tt>.bss</tt> + <tt>.noinit</tt>|| Daten (initialisiert + zu 0 initialisiert + nicht initialisiert) |
|- | |- | ||
|EEPROM ||<tt>.eeprom</tt> ||Daten, die man ins EEPROM gelegt hat | |EEPROM ||<tt>.eeprom</tt> ||Daten, die man ins EEPROM gelegt hat | ||
Zeile 69: | Zeile 79: | ||
Verbrauch des gesamten Programms auflisten: | Verbrauch des gesamten Programms auflisten: | ||
> avr-size -x -A foo.elf | > 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 <tt>--mcu</tt> können natürlich keine Prozentwerte berechnet werden, es werden dann nur die absoluten Verbrauche ausgedruckt. | ||
− | Platzverbrauch von Funktionen, Objekten, etc. nach Größe sortiert: | + | Platzverbrauch von Funktionen, Objekten, Variablen, etc. nach Größe sortiert: |
− | > avr-nm --size-sort - | + | > 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= | |
− | + | ||
− | + | ||
− | + | ||
− | extern unsigned | + | Um den momentan freien Speicher zu bestimmen, zieht man einfach den Anfang des Heaps vom Stackpointer ab: |
+ | #include <avr/io.h> | ||
+ | |||
+ | {{ccomment|__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 ungenutztem Speicher seit Programmstart 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 ungenutzt geblieben ist. Dazu wird im Startup-Code das Muster <tt>0xaa</tt> in den SRAM geschrieben. Durch Aufruf der Funktion <tt>get_mem_unused</tt> wird bestimmt, wieviel von diesem Muster zum Zeitpunkt des Aufrufs noch intakt ist. | |
− | < | + | |
− | + | ||
− | + | ||
− | / | + | Dieses Vorgehen ist deshalb notwendig, weil unter Umständen auch [[ISR]]-Routinen dynamisch Speicherplatz belegen, man jedoch <tt>get_mem_unused</tt> nicht ''in'' der ISR aufrufen will. |
− | + | ||
− | + | Mit optimierendem Compiler brauchen die beiden Routinen zusammen 42 Bytes an Flash. | |
− | + | ||
− | unsigned short | + | Damit der Code den richtigen Wert liefert, darf keine dynamische Speicherallokierung mit <tt>malloc()</tt> etc. geschehen sein; ein <tt>[[Avr-gcc/Interna#Dynamische Speicherallokierung|__builtin_alloca]]</tt> ist hingegen kein Problem, da letzteres den Platz vom Stapel nimmt. |
− | get_mem_unused (void) | + | |
− | { | + | Die Funktion <tt>init_mem</tt> wird in der Init-Phase [[Avr-gcc/Interna#Frühe Codeausführung vor main()|vor <tt>main</tt> aufgerufen]]. |
− | + | ||
− | + | ;<tt>mem-check.h</tt> | |
− | + | #ifndef MEM_CHECK_H | |
− | + | #define MEM_CHECK_H | |
− | + | ||
− | + | extern unsigned short get_mem_unused (void); | |
− | + | ||
− | + | #endif {{comment|MEM_CHECK_H}} | |
− | + | ||
− | + | ;<tt>mem-check.c</tt> | |
+ | #include <avr/io.h> {{ccomment|RAMEND}} | ||
+ | #include "mem-check.h" | ||
+ | |||
+ | {{ccomment|Mask to init SRAM and check against}} | ||
+ | #define MASK 0xaa | ||
+ | |||
+ | {{ccomment|From linker script}} | ||
+ | extern unsigned char __heap_start; | ||
+ | |||
+ | {{ccomment|!!! This doesn't work together with malloc et.al. (whose use is}} | ||
+ | {{ccomment|!!! discouraged on AVR, anyway). alloca, however, is no problem}} | ||
+ | {{ccomment|!!! 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; | ||
+ | } | ||
+ | |||
+ | {{ccomment|!!! Never call this function, it is part of .init-Code}} | ||
+ | static void [[Avr-gcc/Interna#Attribute|__attribute__]] ((naked, used, section(".init3"))) | ||
+ | init_mem (void) | ||
+ | { | ||
+ | {{ccomment|Use inline assembler so it works even with optimization turned off.}} | ||
+ | {{ccomment|Apart from that, according to GCC docs, the only code that's safe}} | ||
+ | {{ccomment|in a naked function is inline assembly.}} | ||
+ | __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" | ||
+ | "brne 0b" | ||
+ | : | ||
+ | : "i" (MASK), "i" (RAMEND) | ||
+ | ); | ||
+ | } | ||
− | + | Der Grund, hier auf [[Inline-Assembler in avr-gcc|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. | |
− | + | ||
− | + | =Siehe auch= | |
− | + | * [[AVR]] | |
− | + | * [[avr-gcc]] | |
− | + | * [[Inline-Assembler in avr-gcc|Inline-Assembler]] | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
[[Kategorie:Quellcode C]] | [[Kategorie:Quellcode C]] | ||
− | [[Kategorie:Quellcode Assembler]] | + | [[Kategorie:Quellcode Assembler AVR]] |
[[Kategorie:Software]] | [[Kategorie:Software]] |
Aktuelle Version vom 17. Oktober 2023, 09:55 Uhr
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. Das ausgiebige Verwenden von .noinit hilft die Ausführungszeit des Initialisierungskodes vor main() zu verkürzen. Registervariablen werden ebenfalls nicht initialisiert, fallen jedoch in keine der RAM-Sections.
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 Stapels hinauswächst. Genau genommen ist eine Designschwäche von avr-gcc, den angenommenen maximalen Stapelspeicherverbrauch nicht per Kommandozeile oder Linkerskript angeben zu müssen.
Die gängige Methode, den Stapel-Speicherverbrauch zu messen ist das Initialisieren des Freispeichers mit einem Kennbyte/-wort/-doppelwort und das gelegentliche Prüfen auf Veränderung in der Idle-Schleife des Hauptprogramms.
Einen Stapelüberlauf feststellen geht mit den gängigen ATtiny und ATmega nicht. Dazu braucht es einen geeigneten Interrupt oder Reset-Ursache. Der gängige Weg bei geeigneten Controllern (xmega?) ist die Umverteilung der Speicheranordnung im Linker-Skript (Stapel zuerst mit fester Größe) damit der Schreibzugriff außerhalb des RAM-Bereichs zur Ausnahme führt. (Ein Stapel-Unterlauf, d.h. mehr pop als push, kommt eher selten vor, und führt schon vorher zu unerwartetem Programmverhalten.) Eine Alternative zur Hardware-Exception wäre eine compilergenerierte Stapelüberprüfung zur Laufzeit. Leider bietet avr-gcc dies nicht an. Muss man ggf. selbst programmieren.
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 ungenutztem Speicher seit Programmstart 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 ungenutzt 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 zum Zeitpunkt des Aufrufs noch intakt ist.
Dieses Vorgehen ist deshalb notwendig, weil unter Umständen auch ISR-Routinen dynamisch Speicherplatz belegen, man jedoch get_mem_unused nicht in der ISR aufrufen will.
Mit optimierendem Compiler brauchen die beiden Routinen zusammen 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. // 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 static void __attribute__ ((naked, used, section(".init3"))) init_mem (void) { // Use inline assembler so it works even with optimization turned off. // Apart from that, according to GCC docs, the only code that's safe // in a naked function is inline assembly. __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" "brne 0b" : : "i" (MASK), "i" (RAMEND) ); }
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.