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
intmain_loop()
2
{
3
4
hal::Uartterminal(USART1_BASE);
5
hal::IICiic2(I2C2_BASE);
6
hal::SPIspi3(SPI3_BASE);
7
8
dev::HTS223::ChipHTS221hts221;
9
dev::ISM43362::WifiModulewifiModule;
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
return0;
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
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.
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
classPlattform
2
{
3
public:
4
boolinit();
5
booldeinit();
6
//..
7
8
hal::Uart*getUart3();
9
hal::SPI*getSpi3();
10
hal::IIC*getIic2();
11
12
private:
13
14
//MC Peripherals...
15
hal::Uartm_uart3;
16
hal::SPIm_spi3;
17
hal::IICm_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
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.
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
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.
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
>classPlattform
2
>{
3
>public:
4
>boolinit();
5
>booldeinit();
6
>//..
7
>
8
>hal::Uart*getUart3();
9
>hal::SPI*getSpi3();
10
>hal::IIC*getIic2();
11
>
12
>private:
13
>
14
>//MC Peripherals...
15
>hal::Uartm_uart3;
16
>hal::SPIm_spi3;
17
>hal::IICm_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.