Forum: Mikrocontroller und Digitale Elektronik [ARM M4] Stack-Size, SW-Architektur


von Thomas (Gast)


Lesenswert?

Hallo Zusammen,

zur Zeit beschäftige ich mich gerade mit ARM µC Programmierung 
(STM32L4*) und bin natürlich auch relativ schnell auf ein paar Fragen 
gestoßen.

Ich verfolge kein bestimmtes Projekt, sondern wollte auf meinem 
Discovery-Board einfach mal Uart, Temperatur-Sensor (IIC), ITM und ein 
Wifi-Module (SPI) in Betrieb nehmen. Das alles hatte ich mir in C++ 
vorgenommen.

Hier ein paar Fragen, die mir so untergekommen sind, vielleicht könnt 
Ihr mir hier weiter helfen?

Stack-Size / SRAM
Ich bin nach einiger Zeit immer in den HardFault_Handler gelaufen. Ich 
denke, es lag an der Stack-Size. Nach dem vergrößern im *.ld File, lief 
wieder alles Problemlos.


Was sind denn eigentlich übliche größen für den Stack? Mein µC hat 96kB 
SRAM1, und der Stack war auf 1kB konfiguriert (Heap 0.5kB). Ist es 
üblich, dass man diese Bereich manuell vergrößern muss, oder stimmt 
etwas mit meinem Software-Design nicht?

Wozu braucht man denn den SRAM - neben Heap und Stack - noch? Könnte man 
Heap und Stack einfach über den gesamten SRAM verteilen?

Springt man, sobald man mit dem Stack die vorgegebenen Grenzen von 
diesem überschreitet, eigentlich direkt in den Hard-Fault Handler? Auch, 
wenn man gerade eigentlich gar nichts kritisches überschreibt?


Globale vs. Lokale Variablen
In meiner main-Loop habe ich mir einfach alle meine Klassen/Treiber 
lokal erzeugt. Etwa so:
1
int main_loop()
2
{
3
4
    hal::Uart   terminal(USART1_BASE);
5
    hal::IIC    iic2(I2C2_BASE);
6
    hal::SPI    spi3(SPI3_BASE);
7
    
8
    dev::HTS223::ChipHTS221     hts221;
9
    dev::ISM43362::WifiModule   wifiModule;
10
    
11
    //Initialisation Peripherals
12
    if (!terminal.init()) {
13
        //error..
14
    }
15
    //...
16
17
    //Initialisation Driver
18
    if (!hts221.init(&spi3)) {
19
        //error..
20
    }
21
    //...
22
    
23
    while(1)
24
    {
25
        //Read out Sensor...
26
        //Do some Wifi Stuff...
27
    }
28
29
    return 0;
30
}

Dies führt dann auch vermutlich dazu, dass der Stack sehr schnell 
ausgeschöpft ist, weil alle Variablen/Klassen lokal in der Funktion 
instantiiert werden. Gerade wenn ich jetzt in meinen Klassen auch noch 
hier und dort einen größeren Puffer einbauen würde, bekomme ich ja 
schnell Probleme.

Ist es schlechte Praxis, solche Dinge lokal in Funktionen zu erzeugen? 
Würde ich diese global instantiieren, hätte ich das Problem mit dem 
Stack ja nicht mehr. Wenn ich es richtig verstehe, werden die globalen 
Variablen dann im Code-Bereich instantiiert.

Allerdings sagt ~man~ doch, man solle eher auf globale Variablen 
verzichten, oder?


Viele Grüße,
  Thomas

von Random .. (thorstendb) Benutzerseite


Lesenswert?

Stackanalyse:
http://www.keil.com/appnotes/files/apnt_316.pdf

Ansonsten empfehle ich, ein RTOS zu verwenden, da kann man die einzelnen 
Stacks/Sizes sehr schön debuggen. Ausserdem entzerrt es die Software 
gewaltig (z.B. durch die Verwendung von Events) und bringt ggf. 
zusätzliche Ressourcen, die ansonsten von der while(1) geschluckt 
werden.

von Thomas (Gast)


Lesenswert?

Vielen Dank für das Dokument bzgl. der Stack-Analyse!

Mir ist aufgefallen, dass der HardFault Handler gar nicht direkt 
anspringt, wenn die Grenzen des Stacks überschritten werden. Wenn ich 
das richtig verstehe, ist das Problem dann eher, dass eben 
"irgendwelche" Speicherbereiche überschrieben werden und man unter 
Umständen undefiniertes Verhalten hat. Dies kann dann auch den HardFault 
Handler auslösen.

Ist das so richtig?

Weiterhin ist mir nicht klar, ob man nun solche "Module" besser global 
anstatt lokal instantiieren sollte? Dadurch würde man den Platz auf dem 
Stack nicht verbrauchen.

Z.b. könnte man sich eine Klasse Plattform erstellen, in welcher man 
alle Module unterbringt:
1
class Plattform
2
{
3
public:
4
  bool init();
5
  bool deinit();
6
  //..
7
  
8
  hal::Uart *getUart3();
9
  hal::SPI  *getSpi3();
10
  hal::IIC  *getIic2();
11
  
12
private:
13
14
  //MC Peripherals...
15
  hal::Uart  m_uart3;
16
  hal::SPI  m_spi3;
17
  hal::IIC  m_iic2;
18
  
19
}

Hat man diese Klasse dann global, kann man natürlich auch von überall 
darauf zugreifen.

Wie Strukturiert Ihr eure Module in euren Projekten?

Oder ist es in der Regel besser - wie bereits von Thorstendb angedeutet 
-  ein RTOS zu verwenden, wodurch man die Verwendung von einzelnen 
Modulen besser auf verschiedene Threads verteilt? Z.b. ein Thread 
kümmert sich nur um das Wifi-Module und instantiiert dann lokal eben das 
Interface (SPI) und den entsprechenden Treiber?

Viele Grüße

von Stefan F. (Gast)


Lesenswert?

Thomas schrieb:
> Was sind denn eigentlich übliche größen für den Stack?

Hängt vom Programm ab.

> Wozu braucht man denn den SRAM - neben Heap und Stack - noch?

Für statische und globale Variablen.

> Könnte man Heap und Stack einfach über den gesamten SRAM verteilen?

Ja, kann man.

> Springt man, sobald man mit dem Stack die vorgegebenen Grenzen von
> diesem überschreitet, eigentlich direkt in den Hard-Fault Handler?

Jein. In den Hardfault Handler kommst du, wenn du auf Adressen ins 
Nirvana zugreifst. Wie groß der Stack ist, weiß der Cortex M4 nicht. Je 
nach Lage im RAM ergibt sich aber aus Anfangsadresse des Stack und Ende 
des RAM.

> Ist es schlechte Praxis, solche Dinge lokal in Funktionen zu erzeugen?

Meistens ja

> Würde ich diese global instantiieren, hätte ich das Problem mit dem
> Stack ja nicht mehr.

Richtig. Aber der Speicherbedarf wird dadurch nicht kleiner. Du 
vermeidest allerdings, dass Fehler erst später irgendwann zur Laufzeit 
auftreten.

> Wenn ich es richtig verstehe, werden die globalen
> Variablen dann im Code-Bereich instantiiert.

Der Code liegt normalerweise im Flash Speicher, das ist kein RAM.

> Allerdings sagt ~man~ doch, man solle eher auf
> globale Variablen verzichten, oder?

Wer ist "man"? Java EE Entwickler sagen das vielleicht, aber das ist 
eine ganz andere Baustelle mit anderen Bedürfnissen.

von Frank K. (fchk)


Lesenswert?

Insbesondere auf einer Mikrocontroller-Plattform ohne PMMU solltest Du 
kein new/delete oder malloc/free verwenden, auch nicht implizit 
(Achtung, große *Falle!* ). Wenn, dann nur einmal beim Programmstart 
und dann nie wieder.

Grund für diese Empfehlung: Speicherfragmentierung.

Einfaches Beispiel:
1. Du hast 1024 Byte Speicher.
2. p1=malloc(256); -> funktioniert
3. p2=malloc(512); -> funktioniert
4. free(p1); -> funktioniert
5. p3=malloc(512); -> schlägt fehl

Das letzte malloc schlägt fehl, obwohl noch genügend Speicher frei wäre. 
Der freie Speicher ist aber nicht zusammenhängend. Der Speicher ist 
fragmentiert.

Da hilft nur ein Neustart. Auf einem PC ist das normal, bei einem 
Embedded-Prozessor, der jahrzehntelang ununterbrochen ohne Neustart 
laufen muss, ist das ein grober Designfehler.

Das oben ist nur ein sehr einfaches Beispiel.

Wichtig sind nicht nur die malloc/free und new/delete, die Du selber 
hinschreibst, sondern auch die, die sich in irgendwelchen Konstruktoren 
oder Klassen (z.B. std::string) verstecken oder die der Compiler 
automatisch erzeugt. Das gibt irgendwann garantiert Bruch.

Die Strategie bei kleinen Mikrocontrollern besteht darin, möglichst 
alles bereits zur Compilezeit festzuzurren und rein gar nichts dynamisch 
zu erzeugen. Das kann dann auch niemals fehlschlagen.

fchk

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Frank K. schrieb:

> Grund für diese Empfehlung: Speicherfragmentierung.

Wird regelmäßig angeführt. Stimmt trotzdem mit dieser Absolutheit nicht.

Wenn ich ein dynamisches Problem habe (nur dann braucht man 
dynamischen Speicher, einmalige Aufrufe sind quasi-statisch und zählen 
da nicht rein), dann:

> Die Strategie bei kleinen Mikrocontrollern besteht darin, möglichst
> alles bereits zur Compilezeit festzuzurren und rein gar nichts dynamisch
> zu erzeugen.

… führt genau das dazu, dass letztlich die Ressourcen ineffizient 
genutzt werden.

Um deiner Milchmädchenrechnung eine andere entgegenzusetzen: ich habe 
die gleichen 1024 Byte freien Speicher und dynamische Anforderungen, die 
(bspw. in Form von Wireless-Daten) zwischen 20 und 512 Byte schwanken 
können.

Alloziere ich alles statisch vor, habe ich Platz für genau 2 
Datenpakete, denn für jedes muss ich 512 Byte einplanen (Maximalgröße). 
Bei dynamischer Allozierung kann ich problemlos eine Folge von 512+, 
20+, 128+, 20-, 256+, 512-, 48+, 128-, 256-, 48- verkraften (+ 
bezeichnet die Allokation, - die Deallokation). Die gleiche Folge würde 
bei statischer Vorbelegung 1536 Byte brauchen, während die dynamische 
Folge einen Spitzenverbrauch von 932 Byte hatte (mal mit 4 Byte Overhead 
pro Block gerechnet), also in 1024 Byte abhandelbar ist.

Für ein dynamisches Problem (nochmal: nur dann braucht man solche 
Überlegungen überhaupt) muss ich wiederum grundsätzlich den Fall 
einplanen, dass meine verfügbaren Ressourcen nicht ausreichen, um alle 
Anforderungen zu bedienen. Dabei ist es völlig unerheblich, ob die 
Ressourcen nun fest als 2 x 512 aufgeteilt waren (wo es bereits bei der 
dritten Anforderung nicht mehr gereicht hätte) oder komplett dynamisch. 
Ob dieses Handling nun darin besteht, dass man die nachfolgenden 
Anforderungen verwirft, irgendwelche alten Sachen vorzeitig "ausräumt" 
oder gar die Flucht nach vorn über einen Reboot antritt, kann nur von 
Fall zu Fall entschieden werden.

Das größere Problem hierbei ist, dass bei einem Schema, das einem 
maximale Freiheit bezüglich der Speicheraufteilung zwischen Stack und 
Heap gibt (Heap wächst von unten, Stack von oben) beide miteinander zur 
Laufzeit kollidieren können, und bei einem Controller ohne MMU 
insbesondere die Ausdehnung des Stacks in den Heap hinein nicht 
unmittelbar festgestellt werden kann. Dagegen hilft (und damit sind wir 
wieder beim ursprünglichen Problem), dass man den Stack an den Anfang 
des RAMs legt und seine Maximalgröße zur Linkzeit vorgibt, denn dann 
führt ein Stacküberlauf bei üblichen Cortex-M-Architekturen zu einem 
Hardfault, da dabei auf Adressen zugegriffen wird, an denen kein 
Speicher existiert.

> Da hilft nur ein Neustart. Auf einem PC ist das normal

Grmpf. Nein, ist auch dort nicht normal, ich möchte meinen PC nicht neu 
starten müssen, nur weil ein Programmierer nicht in der Lage war, seine 
Speicherlecks zu finden. Allerdings hat man auf dem PC insgesamt mehr 
RAM und außerdem noch Virtual Memory, wodurch der Spielraum für "gleicht 
sich über die Laufzeit selbst allmählich aus" viel größer ist. Aber es 
gibt auch dort Kandidaten (wie Firefox), die man tatsächlich von Zeit zu 
Zeit neu starten muss, weil sie sich sonst übermäßig aufblähen.

von Ruediger A. (Firma: keine) (rac)


Lesenswert?

Thomas schrieb:
> Vielen Dank für das Dokument bzgl. der Stack-Analyse!
>
> Mir ist aufgefallen, dass der HardFault Handler gar nicht direkt
> anspringt, wenn die Grenzen des Stacks überschritten werden. Wenn ich
> das richtig verstehe, ist das Problem dann eher, dass eben
> "irgendwelche" Speicherbereiche überschrieben werden und man unter
> Umständen undefiniertes Verhalten hat. Dies kann dann auch den HardFault
> Handler auslösen.
>
> Ist das so richtig?
>

Ja. Wie Stefanus schon schrieb, taucht der Hard Fault erst beim Zugriff 
auf einen zerschossenen Speicher auf. Wenn ein stack platzt, ist das 
erstmal ein legaler schreibender Zugriff. Ein Problem dabei kann erst 
wesentlich später entstehen, manchmal gar nicht und in der Regel erst in 
missionskritischen Kundeninstallationen bei Vollmond.

> Weiterhin ist mir nicht klar, ob man nun solche "Module" besser global
> anstatt lokal instantiieren sollte? Dadurch würde man den Platz auf dem
> Stack nicht verbrauchen.
>
> Z.b. könnte man sich eine Klasse Plattform erstellen, in welcher man
> alle Module unterbringt:
>
>
1
> class Plattform
2
> {
3
> public:
4
>   bool init();
5
>   bool deinit();
6
>   //..
7
> 
8
>   hal::Uart *getUart3();
9
>   hal::SPI  *getSpi3();
10
>   hal::IIC  *getIic2();
11
> 
12
> private:
13
> 
14
>   //MC Peripherals...
15
>   hal::Uart  m_uart3;
16
>   hal::SPI  m_spi3;
17
>   hal::IIC  m_iic2;
18
> 
19
> }
20
>
>
> Hat man diese Klasse dann global, kann man natürlich auch von überall
> darauf zugreifen.
>
> Wie Strukturiert Ihr eure Module in euren Projekten?
>

Über Softwaredesign gibt es ganze Studiengänge und Doktorarbeiten, da 
gibt es kein Patentrezept. Es hängt immer von der Problemstellung und 
dem Umfeld ab, was die beste Lösung ist. Pauschalaussagen sind komplett 
unmöglich.

> Oder ist es in der Regel besser - wie bereits von Thorstendb angedeutet
> -  ein RTOS zu verwenden, wodurch man die Verwendung von einzelnen
> Modulen besser auf verschiedene Threads verteilt? Z.b. ein Thread
> kümmert sich nur um das Wifi-Module und instantiiert dann lokal eben das
> Interface (SPI) und den entsprechenden Treiber?
>

Auch das ist eine Frage des Systemdesigns. I.W. ist die Frage, ob sich 
die Aufgabenstellung gut nebenläufig beschreiben/modellieren lässt oder 
nicht. Wenn dein System nichts Anderes macht als in einer Endlosschleife 
Sensoren abzufragen und die Werte sofort an eine Hostschnittstelle 
weiterleitet, ist das ein linearer Prozess, in dem keine 
Nebenläufigkeiten auftreten, und da gewinnst Du nichts mit einem RTOS. 
Sobald aber wirklich nebenläufige Prozesse ins Spiel kommen, wird es 
ohne RTOS schnell sehr unübersichtlich und schwer zu warten.

Vorsicht Eigenwerbung: In Kapitel 4 meines Buces diskutiere ich die 
Frage "warum und wann RTOS?" ausführlicher.

> Viele Grüße

Zurück! ;-)

Edit: Meine Antwort hat sich mit der von Jörg überschnitten. Ich stimme 
völlig mit ihm überein. Dynamische Speicherverwaltung ist in vielen 
Szenarien absolut ok und darf nicht pauschal verurteilt werden. Es 
stimmt zwar, dass damit mehr potentielle Fehlerquellen auftreten, aber 
es ist eine Binsenweisheit, dass jede Entscheidung, die zur Compilezeit 
gefällt wird, zur Laufzeit keine dynamischen Fehler verursachen kann, 
aber damit auch Nachteile haben kann. Bei rein statischer 
Speicheralloziierung muss dein RAM so gross sein, dass jeder potentiell 
jemals benutzte Speicher in Maximalgrösse voralloziiert ist. Das geht in 
manchen Applikationen, in den meisten aber nicht.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.