Forum: Compiler & IDEs GCC, Linkerscript & Startupcode für STM32F429-Discovery & SDRAM anpassen, offene Fragen


von Ralf (Gast)


Lesenswert?

Guten Abend,

ich möchte das SDRAM auf einem STM32F429-Discoveryboard mit ins 
Linkerscript aufnehmen, was bis dato unbekanntes Terrain ist. Als IDE 
verwende ich Atollic TrueStudio. Für's SDRAM auf dem Discoveryboard gibt 
es im Forum schon einige Beiträge, die verschiedene Anwendungen 
abdecken, bspw. als Speicher für den Heap. Ich habe noch 
Verständnisschwierigkeiten bzgl. der Sektionen bzw. Segmente und den 
notwendigen Modifikationen des Startupcodes. Und ich will rausfinden, 
was geht bzw. nicht geht..

Die TEXT und RODATA Segmente sind Programmcode bzw. Konstanten und 
landen daher im Flash. DATA beinhaltet initialisierte Variablen. Für das 
BSS Segment hab ich unterschiedliche Aussagen gefunden: im einen Fall 
sind dort die nicht initialisierten Variablen abgelegt, diese werden 
(üblicherweise) mit dem Wert Null initialisiert. Im anderen Fall sind 
dort die explizit mit dem Wert Null initialisierten Variablen abgelegt - 
ich gehe momentan davon aus, dass letzteres wahr ist, weil man damit 
Speicher bei der Initialisierung spart. Womit die nicht initialisierten 
Variablen einen mehr oder weniger zufälligen Wert haben.
DATA und BSS beinhalten aber nur globale oder statische Variablen 
innerhalb von Funktionen. Nicht statische lokale Variablen landen immer 
auf dem Stack. Den Heap i.V.m. malloc() & Co. lasse ich jetzt mal außen 
vor. Ist das soweit korrekt?

Solange das alles in einem(!) internen RAM-Block läuft, ist mir das 
denke ich klar - zumindest wenn meine obige Aussage korrekt ist :)

Wie ist das nun, wenn weiteres internes oder externes RAM dazu kommt? 
Das Discoveryboard hat 8MByte SDRAM, und der STM32F429 selbst hat 
mehrere RAM-Blöcke zu 192kByte, 64kByte und 4kByte, also insgesamt 
260kByte.

Das mit STMCubeMX generierte Linkerscript hat ohne Modifikationen 
folgende Sektionen:
1
  FLASH (rx)      : ORIGIN = 0x08000000, LENGTH = 2048K
2
  RAM (xrw)       : ORIGIN = 0x20000000, LENGTH = 192K
3
  MEMORY_B1 (rx)  : ORIGIN = 0x60000000, LENGTH = 0K
4
  CCMRAM (rw)     : ORIGIN = 0x10000000, LENGTH = 64K
D.h. das CoreCoupled-RAM ist ebenfalls angelegt. Weiter unten im 
Linkerscript gibt's den Hinweis, dass der Startupcode angepasst werden 
muss, wenn initialisierte Variablen verwendet werden. Dass ich dazu den 
Teil des Startupcodes, welcher das DATA Segment bedient, kopieren und 
mit den entsprechenden Anfangs- und Endadressen modifizieren muss ist 
mir klar.

Was mir nun fehlt, ist die Differenzierung zwischen DATA und BSS für die 
entsprechende Sektion. Ausgehend davon, dass BSS die mit Null 
initialisierten Variablen meint: Wird für Sektionen ungleich der 
"Standard"-RAM-Sektion keine Unterscheidung zwischen DATA und BSS 
getroffen? Ich habe ein bisschen experimentiert: ich habe ein 
entsprechendes Projekt erstellt (ohne irgendwelchen Modifikationen bzgl. 
Linkerscript) und zwei globale Variablen im CC-RAM angelegt:
1
volatile uint32_t TestCCbss __attribute__ ((section(".ccmram")));
2
volatile uint32_t TestCCi __attribute__ ((section(".ccmram"))) = 0x55555555;
Die eine ist initialisiert, die andere nicht. Ich hab mir mit "objdump 
-h" mal die ganzen Sektionen ausgeben lassen, ich sehe im CC-RAM 8 
Bytes. Der BuildAnalyzer in TrueStudio behauptet das gleiche :) Die 
Frage ist hier also, ob in anderen Sektionen tatsächlich nicht zwischen 
initialisiert mit Null, ungleich Null und nicht initialisiert 
unterschieden wird (oder werden kann) oder ob das durchaus geht und für 
mich nicht offensichtlich ist. Was ich nämlich auch sehe ist, dass die 
unitialisierte Variable eine LoadAddress (LMA) im ELF-File hat, d.h. 
deren Wert wird implizit mit 0 in den Output mit aufgenommen und belegt 
damit unnötig Platz.
Wo liegt hier der Fehler? Muss man hier zwangsweise ohne Initialisierung 
arbeiten und die Sektionen im Linkerscript mit mit NOLOAD bzw. NOINIT 
angeben?

Die nächste Frage betrifft die Verwendung der verschiedenen RAM-Blöcke 
(intern wie extern). Sobald ich eine Variable mittels attribute-section 
in ein anderes Segment schubse, muss diese Variable global sein. 
Innerhalb einer Funktion kann ich die Variable (mit Ausnahme der 
dynamischen Allozierung mit malloc() & co) nicht in einem anderen 
Segment anlegen, ist das korrekt? So etwas wie ein
1
#pragma AB_HIER_ALLES_IN_EINE_ANDERE_SEKTION
2
...
3
#pragma AB_JETZT_WIEDER_ALLES_NORMAL
gibt es vermutlich nicht :)

Das heisst die anderen RAM-Blöcke außer dem 192kByte Hauptblock eignen 
sich "nur" für größere statische Buffer und/oder Verwendung als Heap, da 
man die Blöcke nicht zusammen fassen kann? Mir ist klar, dass man nicht 
jeden vorhandenen RAM-Block auch ins Linkerscript aufnehmen muss, da man 
je nach Controller den Speicher auch dediziert bspw. für DMA-Buffer o.ä. 
verwenden kann, aber diesen Ansatz lasse ich mal aussen vor. Mir geht's 
um den Fall dass alle Blöcke dem Linker bekannt sind).

Umgekehrt könnte man DATA/BSS/Stack/Heap ins externe SDRAM linken. Wobei 
das aber vermutlich zu Lasten der Performance geht, je nachdem was man 
alles ins SDRAM (oder einen anderen internen Block) nimmt. Hier wäre 
nebenläufig interessant, wie man denn die Performance aussagekräftig 
testen könnte. Den Heap jeweils intern und im SDRAM und dann viele 
kleine dynamisch allozierte Speicherblöcke mit komplexen Berechnungen 
füllen? Oder jeweils intern und extern ein uint8_t-Array mit einer Länge 
von 128k von 0 bis "voll" inkrementieren? Das könnte dauern :D Okay, 
ernsthaft, gibt es einen Quasi-Standard für den Performancetest von 
Speichern an einem Microcontroller? Ich weiss, dass es diverse 
Benchmarks für die Controller selbst gibt, da liefern sich ja die 
Hersteller immer ihre Schlachten um DMIPS & Co, aber das betrifft ja 
soweit ich weiß immer nur den Controllerkern, nicht den internen oder 
externen Speicher.

Für Hinweise über weitere "Anwendungsfälle" bzw. Do's & Don'ts bin ich 
dankbar :)

Gruß Ralf

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


Lesenswert?

Ralf schrieb:
> Womit die nicht initialisierten Variablen einen mehr oder weniger
> zufälligen Wert haben.

Variablen mit statischer Lebensdauer (also alle globalen plus solche, 
die explizit als "static" markiert sind) werden in der 
Programmiersprache C immer initialisiert. Wenn nichts angegeben ist, 
dann (de facto für alle hier und heute
praktikablen Fälle) mit 0.

Es ist dabei egal, ob du nun
1
int foo = 0;

oder (auf globalem Niveau)
1
int foo;

schreibst; in beiden Fällen landet die Variable im .bss. Bei sehr alten 
GCC-Versionen war es noch ein Unterschied: explizite Initialiserung 
brachte sie dort nach .data, bei dem ein explizitert Initialwert im 
Binary (bzw. hier im Flash) hinterlegt wird, d.h. dort war es 
ausgesprochen unvorteilhaft, eine explizite Initialisierung anzugeben.

Habe nicht alles gelesen, aber in Kurzform: Daten kannst du by default 
nur entweder in den einen oder in den anderen RAM packen lassen. Alles 
andere musst du über explizite .section-Attribute bei jeder 
non-default-memory-space-Variablen organisieren.

Wenn du in beiden RAM-Bereichen initialisierte Variablen unterbringen 
können möchtest, dann musst du dich sowohl im Linkerscript als auch in 
einem dazu passenden Startup-Code darum kümmern, dass deren Werte aus 
dem Flash kopiert werden. Das macht man, indem man im Linkerscript 
entsprechende globale Symbole platziert, die dann vom Startupcode 
referenziert werden. (Gleiches trifft auf nicht explizit initialisierte 
Variablen zu, die ausgenullt werden müssen.) Beim SDRAM wird sich der 
Startupcode ja vermutlich vorher auch noch drum kümmern müssen, dass die 
Hardware zur Ansteuerung des SDRAM passend initialisiert wird, bevor man 
dort was ausnullt oder initialisiert.

Alternative: den SDRAM halt nur für sowas wie einen Heap benutzen. 
malloc() gibt per definitionem einen Speicherbereich zurück, der 
„irgendwas“ enthalten darf.

: Bearbeitet durch Moderator
von Ralf (Gast)


Lesenswert?

Hallo Jörg,

> Variablen mit statischer Lebensdauer ... werden in der Programmiersprache
> C immer initialisiert. Wenn nichts angegeben ist, dann (de facto für
> alle hier und heute praktikablen Fälle) mit 0.
Ah, wieder was gelernt. Ich dachte die statischen, aber nicht 
initialisierten haben einen Zufallswert.

> Bei sehr alten GCC-Versionen war es noch ein Unterschied: explizite
> Initialiserung brachte sie dort nach .data, bei dem ein explizitert
> Initialwert im Binary (bzw. hier im Flash) hinterlegt wird, d.h. dort war
> es ausgesprochen unvorteilhaft, eine explizite Initialisierung anzugeben.
Du beziehst dich jetzt auf die Initialisierung mit Null, oder? D.h. die 
Null wurde, obwohl eigentlich nicht nötig, explizit ins Binary gesetzt?

> Habe nicht alles gelesen, aber in Kurzform: Daten kannst du by default
> nur entweder in den einen oder in den anderen RAM packen lassen. Alles
> andere musst du über explizite .section-Attribute bei jeder
> non-default-memory-space-Variablen organisieren.
D.h. es gibt neben dem attribute-section noch weitere Attribute, die ich 
den Variablen verpassen kann? Dann werd ich da gleich mal nachforschen 
:)

> Wenn du in beiden RAM-Bereichen initialisierte Variablen unterbringen
> können möchtest, dann musst du dich sowohl im Linkerscript als auch in
> einem dazu passenden Startup-Code darum kümmern, dass deren Werte aus
> dem Flash kopiert werden. Das macht man, indem man im Linkerscript
> entsprechende globale Symbole platziert, die dann vom Startupcode
> referenziert werden.
Hab ich gesehen, das wären dann bspw. die Symbole _sdata bzw. _edata für 
die initialisierten Variablen. Hier muss ich dann selbst passende 
Symbole vergeben und gucken, dass ich den LocationCounter richtig 
bediene :)

> (Gleiches trifft auf nicht explizit initialisierte Variablen zu, die
> ausgenullt werden müssen.) Beim SDRAM wird sich der Startupcode ja
> vermutlich vorher auch noch drum kümmern müssen, dass die Hardware zur
> Ansteuerung des SDRAM passend initialisiert wird, bevor man dort was
> ausnullt oder initialisiert.
Guter Hinweis, ich darf nicht vergessen, die Initialisierung für's SDRAM 
in den ResetHandler zu setzen.

> Alternative: den SDRAM halt nur für sowas wie einen Heap benutzen.
> malloc() gibt per definitionem einen Speicherbereich zurück, der
> „irgendwas“ enthalten darf.
Ja, das muss ich noch ausknobeln, ob ich das SDRAM nur für den 
Displaybuffer und den Heap verwende (Heap wäre beim aktuellen Projekt 
das Hauptziel). Deswegen hatte ich auch gefragt, was denn alles (nicht) 
geht. Ich muss noch rausfinden, ob es für die anderen Blöcke auch eine 
Trennung nach initialisiert gleich und ungleich Null gibt (geben kann).

Danke schonmal, ein Teil der Fragen hat somit schon eine Antwort 
bekommen.

Ralf

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


Lesenswert?

Ralf schrieb:

> Ah, wieder was gelernt. Ich dachte die statischen, aber nicht
> initialisierten haben einen Zufallswert.

Nein, nur "auto"-Variablen (wobei das "auto" normalerweise ja 
weggelassen wird, und in C++ auch gleich mal eine andere Bedeutung 
bekommen hat).

> Du beziehst dich jetzt auf die Initialisierung mit Null, oder? D.h. die
> Null wurde, obwohl eigentlich nicht nötig, explizit ins Binary gesetzt?

ja

> D.h. es gibt neben dem attribute-section noch weitere Attribute, die ich
> den Variablen verpassen kann? Dann werd ich da gleich mal nachforschen
> :)

Es gibt andere, aber hier musst du das schon mit 
__attribute__((section(""))) lösen.

> Hab ich gesehen, das wären dann bspw. die Symbole _sdata bzw. _edata für
> die initialisierten Variablen. Hier muss ich dann selbst passende
> Symbole vergeben und gucken, dass ich den LocationCounter richtig
> bediene :)

Richtig, das ist der Zusammenhang. Du hast im Moment einen Block Code im 
Startup, der Daten aus dem Flash in den RAM initialisiert (für .data) 
und einen zweiten (üblicherweise direkt danach), der Daten des .bss 
löscht.

Diese beiden Codeblöcke müsstest du für einen weiteren RAM-Typ 
duplizieren, einschließlich der zugehörigen Symbole im Linkerscript.

von Ralf (Gast)


Lesenswert?

Hi Jörg,

> Richtig, das ist der Zusammenhang. Du hast im Moment einen Block Code im
> Startup, der Daten aus dem Flash in den RAM initialisiert (für .data)
> und einen zweiten (üblicherweise direkt danach), der Daten des .bss
> löscht.
Bin an der Stelle theoretisch auch ein bisschen weiter, was die 
Geschichte bzgl. der separaten DATA/BSS Sektionen angeht. Anscheinend 
kann man da tatsächlich nicht trennen, wenn man die Variablen dediziert 
in andere Sektionen schiebt. Alternativ könnte man wohl entweder im 
Outputfile die DATA/BSS Sektionen vor dem Linken umbenennen oder im 
Linkerscript explizit die jeweiligen Outputfiles entsprechend 
verarbeiten. Das werd ich mal noch weiter untersuchen.

Ralf

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


Lesenswert?

Naja, ist eigentlich auch keine Raketenwissenschaft. :-)

In einem "normalen" ELF-Binary (also einem, was auf dem Host läuft), 
werden die Sections .text und .data beim Programmstart geladen¹), .bss 
wird als genullter Speicher vom Betriebssystem bereit gestellt 
(möglicherweise auf einer Seitengrenze im Speicher ausgerichtet).

¹) "Geladen" heißt dabei nur, dass sie Seitentabellen vorbereitet 
werden, aber es wird nichts real "geladen". Stattdessen gibt es beim 
ersten Zugriff auf eine Seite einen page fault, den fängt das 
Betriebssystem ab, lädt die Seite (und vielleicht noch nachfolgende) 
wirklich in den RAM, aktualisiert die Seitentabellen, und die 
Applikation wird dort weiter fortgesetzt, wo der page fault sie zuvor 
unterbrochen hatte.

Bei einem Controller geht das alles so nicht. .text wird daher direkt 
auf den Flash abgebildet, der Controller kann das dann von da lesen. 
.data dagegen benötigt Initialwerte, die ja hier nicht mehr aus der 
ELF-Datei kommen können wie auf einem Hostsystem. Daher wird der Bereich 
von .data parallel sowohl im Flash (für die Initialwerte) also auch im 
RAM (für tatsächlichen Speicher) geführt. Das macht der Linkerscript. 
Der Startupcode wiederum muss die vom Linker hinterlegten Daten aus dem 
Flash beim Start in den RAM kopieren. Bei .bss ist es einfacher, da 
braucht man eigentlich nur noch Anfang und Ende (per Linker-Symbol), der 
Inhalt wird mit Nullen gefüllt.

Die Verbindung zwischen Linkerscript und Startupcode wird dabei durch 
globale Symbole erzeugt, die der Linkerscript definiert.

von Ralf (Gast)


Lesenswert?

Ja, das hab ich soweit auch verstanden. Funktioniert ja auch - solange 
es eben für jeden Segmenttyp nur einen möglichen Speicher gibt. Auf nem 
PC ist das der Fall, auf nem Controller sieht's anders aus. Da werd ich 
mal noch knobeln.

Ralf

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


Lesenswert?

Naja, du musst dann wirklich section overrides benutzen bei den 
Variablen.
1
int bss; // normal .bss
2
int data = 42; // normal .data
3
int largebss[33512] __attribute__((section(".sdram_bss"))); // .bss @SDRAM
4
int largedata[502334] __attribute__((section(".sdram_data"))) = {
5
    42, 42, 42, 42, 42
6
}; // .data @SDRAM

.sdram_bss und .sdram_data musst du dann in Linkerscript und Startup 
behandeln.

von Ralf (Gast)


Lesenswert?

Ja, ich schätze fast, dass es auf sowas rauslaufen wird. Schade, dass 
das, was du beispielhaft angeführt hast, nicht automatisch geschieht: 
das Anhängen von _bss, _data an den Sektionsnamen. Dann bräuchte man 
nicht darauf achten, wo man die Variablen ablegt. Aber egal, es scheint 
der bisher praktikabelste Weg zu sein.

Besten Dank für die Unterstützung.

Ralf

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


Lesenswert?

Ehrlich: ich würde mir den Zirkus sparen, und würde den SDRAM als Heap 
benutzen. Falls du malloc() sonst nicht groß brauchst, einfach direkt, 
indem du _sbrk() passend implementierst. Falls du zwischen „schnellem“ 
(interner SRAM) und „langsamem“ (SDRAM) Heap unterscheiden musst, dann 
halt für einen von beiden einen händischen Allocator bauen.

Ich weiß, malloc und Heap sind für viele hier ein rotes Tuch, aber man 
muss der Realität auch mal ins Auge blicken, dass sich mehrere hundert 
Kilobyte RAM bei wirklich dynamischen Anforderungen damit sinnvoller 
verwalten lassen als durch irgendwelche statischen Zuweisungen, von 
denen sich dann nach einem halben Jahr produktivem Einsatz zeigt, dass 
sie eigentlich doch nur zu klein gewählt waren.

: Bearbeitet durch Moderator
von Ralf (Gast)


Lesenswert?

Guten Morgen Jörg,

> Ehrlich: ...
das Hauptziel für den aktuellen Fall ist tatsächlich der Heap. Ich 
portiere gerade eine fremde C++-Applikation, die jede Menge dynamischen 
Speicher verwendet. Der erste Lauf auf einem F401 mit 96kB RAM hat 
gezeigt, dass das gerade so passt - ungefähr 80kB gehen da für 
dynamische Allozierung drauf. Da bleibt nicht viel übrig, um die 
Applikation an sich dann noch mit Daten zu füttern.
Für die Verwendung des SDRAMs als reinen Heap sind hier im Forum schon 
Beispiele verfügbar, d.h. das kann ich vermutlich umsetzen. Nur guck ich 
bei was neuem auch gleich gern mal links und rechts drüber raus, um zu 
sehen, was noch gehen kann - oder eben nicht. Daher auch meine Fragen 
über die ganzen Sektionen und wie GCC das macht, etc.

> Ich weiß, malloc und Heap sind für viele hier ein rotes Tuch, ...
Für mich nicht, wenn ich's brauch, dann nehm ich's auch :) Ehrlich 
gesagt finde ich (bisher) die Aussagen, dass Heap & Co. auf nem 
Controller überflüssig, gefährlich, unsinnig oder sonstwas sind, 
übertrieben. Wenn man es richtig macht, dann ist das schon ne 
Erleichterung. Richtig bedeutet hier, dass man trotz allem nicht 
vergessen darf, dass man zwar auf einem PC mit zig GB RAM entwickelt, 
aber eben nur für einen Controller mit wenigen kB oder wenn überhaupt MB 
RAM. Eine bare-metal Implementation mit RAM im GB Bereich ist selten.
Einen eigenen Allokator würd ich auch gern mal selbst schreiben, aber 
das trau ich mir nicht zu. Habe kürzlich (und nebenläufig) im Rahmen der 
Portierung angefangen, den Heap wie ihn GCC bzw. newlib implementiert, 
zu untersuchen bzw. zu verstehen. Auch nicht so einfach, weil ich kaum 
was dazu gefunden habe, wie die Verwaltung intern abläuft.

Ich werde nun versuchen, das RAM und das SDRAM aufzuteilen. Die 192kB 
RAM als normalen Arbeitsspeicher, die 64kB CCRAM als Stack, und das 
SDRAM teile ich auf in Displaybuffer, Heap und einen speziellen Bereich 
für die Eingangsdaten der Applikation.

Ralf

von Nop (Gast)


Lesenswert?

Benutzt man den Heap wirklich dynamisch, dann handelt man sich 
Speicherfragmentierung ein, weil mangels MMU der logische Adreßraum 
gleich dem physikalischen ist, und der ist relativ klein. Damit sammelt 
man Zustand unter der Haube an, und das System ist nicht mehr testbar.

Solange man quasi-statisch alloziert, also einmal beim Startup, ist 
malloc auch auf einem Controller kein Problem.

Weiterere Auswege sind, daß man immer nur Blöcke einer einzigen, festen 
Größe vergibt (und dann auch nur solche einzelnen Blöcke als 
kontinuierlich alloziert erwartet), oder immer nur in Potenzen von zwei 
(womit man nie mehr als 50% Speicher verschwendet).

Daß die Laufzeiten eines Allokators sehr variabel sein können, damit muß 
man halt leben und den nicht gerade z.B. in einem Interrupt aufrufen, 
oder für irgendwas mit schnellen Reaktionsanforderungen.

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


Lesenswert?

Nop schrieb:
> Benutzt man den Heap wirklich dynamisch, dann handelt man sich
> Speicherfragmentierung ein, weil mangels MMU der logische Adreßraum
> gleich dem physikalischen ist, und der ist relativ klein.

Das ist eine zwar immer wiederholte, aber in der Absolutität keineswegs 
korrekte Aussage. Man kann sich sowas einhandeln, ja. Aber man muss 
nicht zwangsläufig: wenn die Anforderungen tatsächlich einigermaßen 
stochastisch daher kommen (in der Größe und Lebensdauer der Requests), 
und man eine ausreichende Reserve an Speicher hat, dann sortiert sich 
das durch die Aggregierung des freigegebenen Speichers durchaus so weit, 
dass dieser Effekt normalerweise nicht eintritt. Genau dieser Fall 
(Allokation ist stark variabel und reichlich zufällig) würde jedeoch bei 
der immer wieder gepriesenen statischen Vorallozierung einen deutlich 
größeren „Verschnitt“ hervorrufen und daher viel eher in die Gefahr 
einer memory congestion laufen als vollkommen dynamische Allokation. 
Dass man in einem solchen Szenario grundsätzlich eine „Exit-Strategie“ 
braucht (Was tu ich, wenn der Speicher nicht ausreicht?), sollte 
sonnenklar sein.

Ralf schrieb:
> Habe kürzlich (und nebenläufig) im Rahmen der Portierung angefangen, den
> Heap wie ihn GCC bzw. newlib implementiert, zu untersuchen bzw. zu
> verstehen.

Du kannst dir den aus der avr-libc ansehen, der ist noch vergleichsweise 
einfach gehalten.

Auch der K&R (also das Papier-Buch :) enthält eine einfache 
Implementierung.

Für größere Devices lohnen sich halt aufwändigere Allokatoren, bei denen 
man mehrere Bereiche hat, aus denen unterschiedlich große Anforderungen 
erfüllt werden, um so den Verschnitt insgesamt geringer zu halten.

: Bearbeitet durch Moderator
von Ralf (Gast)


Lesenswert?

Hallo,

@Nop:
> Benutzt man den Heap wirklich dynamisch, dann handelt man sich
> Speicherfragmentierung ein, weil mangels MMU der logische Adreßraum
> gleich dem physikalischen ist, und der ist relativ klein. Damit sammelt
> man Zustand unter der Haube an, und das System ist nicht mehr testbar.
Hmm... ist das nicht zu arg einschränkend? Ohne jetzt genau zu wissen, 
wie der GCC-Allokator (oder generell ein Allokator) arbeitet, behaupte 
ich trotzdem, dass es drauf ankommt, wie man seine Applikation 
programmiert.
Da ich noch nie eine MMU/MPU gebraucht habe (auch wenn sie vorhanden 
waren), sind MMU/MPU für mich eher nette Beigaben als zwingende 
Voraussetzungen. Beachte: ich hatte bisher auch keine 
OS-Implementierungen am laufen, alles bare-metal und nix mit 
Multitasking. Nebenläufigkeit hab ich per StateMachine, etc. realisiert. 
Wenn ich mal wirklich mit einem OS spiele (z.B. steht Kennenlernen von 
FreeRTOS über die Weihnachtstage auf dem Plan :D ), dann mag es durchaus 
sein, dass ich die Vorzüge von MMU/MPU kennenlerne.

> Solange man quasi-statisch alloziert, also einmal beim Startup, ist
> malloc auch auf einem Controller kein Problem.
Das hab ich nun leider bei diesem konkreten Projekt eben nicht in der 
Hand. Ich kann nur sagen, dass definitiv nicht nur beim Start alloziert 
wird, sondern munter rauf und runter und überall und sowieso.

> Weiterere Auswege sind, daß man immer nur Blöcke einer einzigen, festen
> Größe vergibt (und dann auch nur solche einzelnen Blöcke als
> kontinuierlich alloziert erwartet), oder immer nur in Potenzen von zwei
> (womit man nie mehr als 50% Speicher verschwendet).
Dann brauch ich aber die Allozierung gleich gar nicht. Aus meiner Sicht 
ist die dynamische Speicherverwaltung ja genau für den Fall, dass man 
den Speicherbedarf vorab nicht kennt, da.

> Daß die Laufzeiten eines Allokators sehr variabel sein können, damit muß
> man halt leben und den nicht gerade z.B. in einem Interrupt aufrufen,
> oder für irgendwas mit schnellen Reaktionsanforderungen.
Guter Hinweis. Ich nehme an, die Laufzeit des Allokators ergibt sich 
(abgesehen natürlich von der Implementierung selbst) dadurch, dass nach 
einem passenden Block gesucht werden muss? Was wiederum bei starker 
Fragmentierung entsprechend viel Zeit benötigt.

@Jörg:
> Du kannst dir den aus der avr-libc ansehen, der ist noch vergleichsweise
> einfach gehalten.
> Auch der K&R (also das Papier-Buch :) enthält eine einfache
> Implementierung.
Danke :)

> Für größere Devices lohnen sich halt aufwändigere Allokatoren, bei denen
> man mehrere Bereiche hat, aus denen unterschiedlich große Anforderungen
> erfüllt werden, um so den Verschnitt insgesamt geringer zu halten.
Sind das die "Bins"? Während ich auf der Suche nach weitergehenden Infos 
zum Heap, Malloc & Co war, ist mir der Begriff öfter untergekommen.
Gegenfrage: was erzeugt den von dir genannten Verschnitt? Ich kann da 
nicht ganz folgen :o

Grüße

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


Lesenswert?

Ralf schrieb:

>> Für größere Devices lohnen sich halt aufwändigere Allokatoren, bei denen
>> man mehrere Bereiche hat, aus denen unterschiedlich große Anforderungen
>> erfüllt werden, um so den Verschnitt insgesamt geringer zu halten.

> Sind das die "Bins"?

Ja. Ein "Bin" ist ja ein "Behälter" (denk an den Trashbin, Mülleimer).

> Gegenfrage: was erzeugt den von dir genannten Verschnitt? Ich kann da
> nicht ganz folgen :o

Wenn man ein bisschen mehr Reserven an Speicher hat, lohnt es sich, 
mehrere Größenklassen zu haben und diese zusammenzufassen, also bspw. 
alle Allokationen < 32 Byte in einen „Eimer“, dann alle >= 32 < 1024 in 
einen zweiten, alles größer wird einzeln genommen. Dann passiert es 
nicht, dass man zwischen zwei 1000er Allokationen einzelne Kleckerchen 
von wenigen Bytes hat.

von Ralf (Gast)


Lesenswert?

Ah, verstanden. Danke für die Erläuterung.

Grüße

von Nop (Gast)


Lesenswert?

Jörg W. schrieb:

> dann sortiert sich
> das durch die Aggregierung des freigegebenen Speichers durchaus so weit,
> dass dieser Effekt normalerweise nicht eintritt.

Wenn man halt auf nicht testbare Systeme steht und es nicht so drauf 
ankommt, ob man halt ab und zu mal einen Reset macht, kann man das schon 
machen.

> Genau dieser Fall
> (Allokation ist stark variabel und reichlich zufällig) würde jedeoch bei
> der immer wieder gepriesenen statischen Vorallozierung einen deutlich
> größeren „Verschnitt“ hervorrufen

Bei der statischen Vorallozierung spielt das keine Rolle, weil nur 
einmal alloziert wird. Das ist somit auch leicht testbar: fährt das 
Gerät hoch oder nicht?

> Dass man in einem solchen Szenario grundsätzlich eine „Exit-Strategie“
> braucht (Was tu ich, wenn der Speicher nicht ausreicht?), sollte
> sonnenklar sein.

Das ist eine Binsenweisheit, die auch ohne dynamischen Speicher gilt, 
z.B. beim Empfang serieller Daten ohne Hardware-Flußkontrolle.

von Nop (Gast)


Lesenswert?

Ralf schrieb:

> Ich kann nur sagen, dass definitiv nicht nur beim Start alloziert
> wird, sondern munter rauf und runter und überall und sowieso.

Keine guten Voraussetzungen.

> Dann brauch ich aber die Allozierung gleich gar nicht. Aus meiner Sicht
> ist die dynamische Speicherverwaltung ja genau für den Fall, dass man
> den Speicherbedarf vorab nicht kennt, da.

Ja und? Immer in Zweierpotenzen allozieren leistet das doch. Blöcke nur 
einer festen Größe leisten das auch, weil Du ja mehrere allozieren 
kannst. Du darfst Dich nur nicht drauf verlassen, daß die Blöcke "am 
Stück" im Speicher liegen.

> Guter Hinweis. Ich nehme an, die Laufzeit des Allokators ergibt sich
> (abgesehen natürlich von der Implementierung selbst) dadurch, dass nach
> einem passenden Block gesucht werden muss?

Nein, sondern viel schlimmer daraus, daß nach einiger Allozierung und 
Deallozierung oftmals auch Speicher wieder kontinuierlich frei ist, der 
aber aus mehreren kleinen freigegebenen Einzelblöcken besteht. Das muß 
dann auch wieder zusammengefaßt werden, und das kann länger dauern als 
die normale Allokation.

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


Lesenswert?

Nop schrieb:
> Das ist somit auch leicht testbar: fährt das Gerät hoch oder nicht?

Das genügt dir als Test für die im laufenden Betrieb zu verarbeitenden 
(zufällig eintreffenden) Daten?

Das kannst du mit malloc() auch haben: fährt ja sowieso hoch dann. :-)

Das Problem beim Vorallozieren ist doch, dass du in so einem Falle nicht 
weißt, ob deine Vorallozierung auch wirklich reichen wird für alle im 
Betrieb auftretenden Fälle.

Ich vermute, wir werden uns da nicht einig. Macht nichts, müssen wir 
auch nicht.

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


Lesenswert?

Nop schrieb:
> Nein, sondern viel schlimmer daraus, daß nach einiger Allozierung und
> Deallozierung oftmals auch Speicher wieder kontinuierlich frei ist, der
> aber aus mehreren kleinen freigegebenen Einzelblöcken besteht. Das muß
> dann auch wieder zusammengefaßt werden, und das kann länger dauern als
> die normale Allokation.

Das Zusammenfassen macht man natürlich beim free(), nicht erst beim 
nächsten malloc().

von Nop (Gast)


Lesenswert?

Jörg W. schrieb:

> Das genügt dir als Test für die im laufenden Betrieb zu verarbeitenden
> (zufällig eintreffenden) Daten?

Davon war nicht die Rede, und daß mehr Daten eintreffen als Speicher da 
ist, kannst Du mit wilder Allokation ebenfalls nicht verhindern. Die 
entsprechende Robustheit dagegen muß man unabhängig davon sowieso 
implementieren, so daß sie in diesem Zusammenhang irrelevant ist.

Jörg W. schrieb:

> Das Zusammenfassen macht man natürlich beim free(), nicht erst beim
> nächsten malloc().

Auch gut, aber es ändert nichts an der erheblichen Laufzeitvarianz.

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


Lesenswert?

Nop schrieb:
> Davon war nicht die Rede, und daß mehr Daten eintreffen als Speicher da
> ist, kannst Du mit wilder Allokation ebenfalls nicht verhindern.

Du reduzierst allerdings gegenüber starrer Vorab-Allokation (bei der du 
N Slots für Maximalgröße M allozieren musst) den „Verschnitt“, falls die 
tatsächlich anfallenden Größen sehr unterschiedlich sind. Damit passt 
insgesamt mit hoher Wahrscheinlichkeit weniger in den verfügbaren 
Speicher als mit einem komplett dynamischen Modell.

Sind sie wiederum alle gleich, dann entsteht auch die immer wieder 
herauf beschworene Fragmentierung nicht (aber dann hat malloc() 
natürlich im Vergleich zur Vorab-Allozierung mehr Overhead).

Dass es für vollständig dynamischen Datenanfall keine absolute 
Sicherheit geben kann, ist sonnenklar – egal, mit welcher Methode man 
den Speicher alloziert.

von Nop (Gast)


Lesenswert?

Jörg W. schrieb:

> Du reduzierst allerdings gegenüber starrer Vorab-Allokation (bei der du
> N Slots für Maximalgröße M allozieren musst) den „Verschnitt“, falls die
> tatsächlich anfallenden Größen sehr unterschiedlich sind.

Das stimmt. Allerdings hatte ich auch noch zwei weitere Optionen 
genannt. Guckst Du hier: 
Beitrag "Re: GCC, Linkerscript & Startupcode für STM32F429-Discovery & SDRAM anpassen, offene Fragen"

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


Lesenswert?

Nop schrieb:
> Allerdings hatte ich auch noch zwei weitere Optionen genannt.

Die zweite Option ist letztlich, wofür die "Bins" gut sind.

(Jetzt dämmert's mir auch wieder: man rundet dort halt auf die nächste 
2^N-Größe auf.)

: Bearbeitet durch Moderator
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.