Aus RN-Wissen.de
Version vom 27. Oktober 2006, 10:51 Uhr von SprinterSB_alt (Diskussion | Beiträge) (Dynamischer RAM-Verbrauch)

Wechseln zu: Navigation, Suche
Laderegler Test Tueftler Seite

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 er unteren SRAM-Adresse 0x60 nach dem SFR-Bereich.
.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.

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 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 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 teilen, der nicht von .data, .bss und .noinit belegt ist.

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:

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, etc. nach Größe sortiert:

> avr-nm --size-sort -S foo.elf

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 zu kennen, bzw das Minimum an freiem Speicher, also den worst case zu kennen.

Mit der folgenden kleinen Routine kann der noch freie SRAM-Bereich bestimmt werden. Es wird allerdings 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.

Mit optimierendem Compiler brauchen die beiden Routinen 42 Bytes an Flash.

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;

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 !!! */
void __attribute__ ((naked, section (".init8")))
__init8_mem (void)
{
   __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)
   );
}

Siehe auch


LiFePO4 Speicher Test