Aus RN-Wissen.de
Wechseln zu: Navigation, Suche
Rasenmaehroboter Test

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.

Nutzung des SRAM durch avr-gcc

RAM-Layout für ein AVR mit 1kByte SRAM

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:

Tabelle: Zuordung des Platzberbrauchs zur Section-Größe
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.

Siehe auch


LiFePO4 Speicher Test