ich habe relativ neu mit dem Arduino angefangen und bin da auf ein Verständnisproblem bezgl. des Speicherorts gestoßen. Folgendes Codebeispiel als Diskussionsgrundlage:
const int N = 100;
void setup() {
Serial.begin(9600);
// auf serielle Verbindung warten
while (!Serial) {;}
}
void loop() {
float arr[N] = {0.0};
for(int i = 0; i < N; i++)
{
arr[i] = (float)analogRead(A0);
delay(10);
}
for(int i = 0; i <N; i++)
{
Serial.println(arr[i]);
delay(10);
}
}
Hier bekomme ich nach Compilierung:
Der Sketch verwendet 3326 Bytes (10%) des Programmspeicherplatzes. Das Maximum sind 32256 Bytes.
Globale Variablen verwenden 200 Bytes (9%) des dynamischen Speichers, 1848 Bytes für lokale Variablen verbleiben. Das Maximum sind 2048 Bytes.
Das float-Array arr wird offensichtlich im Programmspeicher abgelegt. Ich hatte gedacht, dass solche Variablen im dynamischen Speicher abgelegt werden und der Programmspeicher read-only ist. Definiere ich die Variable arr außerhalb Setup/Loop (also global), wird sie im dynamischen Speicher angelegt.
Was hat es denn jetzt genau mit den verschiedenen Speichern auf sich? Der Programmspeicher ist offensichtlich auch während der Laufzeit beschreibbar. Wo genau ist denn dann der Unterschied zum dynamischen Speicher?
Also stimmt das schon, dass der Programmspeicher nur ROM ist?
Ich werde trotzdem aus meinem Programm nicht schlau (hab hier ja nur ein Minimalbeispiel gebracht). Weil mein Programm nutzt mehrere float Arrays mit Länge von N=100 und das Programm läuft ohne Probleme. Wenn ich die Größen der Arrays zusammen addiere komme ich aber auf deutlich mehr Speicher, als der Uno dynamischen Speicher hat.
Bei einem RAM-Überlauf passiert undefiniertes Verhalten. Das heißt, dass nicht garantiert ist, dass und was du an Fehlverhalten mitkriegst. Außerdem optimiert der Compiler ziemlich viel weg, wenn es nie gebraucht wird. Manchmal mit, manchmal ohne Warnung.
Experimentell rauskriegen, was bei undefiniertem Verhalten in einem konkreten Fall tatsächlich passiert, ist für viele nicht so spannend, für manche gar Sünde (nicht dass man es irgendwann als gegeben voraussetzt)
Sünde nicht, aber unsinnig!
Alleine schon: Da eine Reproduzierbarkeit nicht garantiert werden kann.
Halt undefiniertes Verhalten, ungefähr das Gegenteil von dem, was man mit der Programmierung eigentlich erreichen möchte.
Wenn man auf ein "überraschendes" Verhalten trifft, macht es natürlich Sinn nachzuforschen, woran es liegt. Wenn dann aber die Ursache gefunden ist, z.B. weil das so in der Sprache definiert ist, dann ist fertig.
Dann lernt man noch, warum die Sprache das als "undefiniertes Verhalten" bezeichnet, und damit ist gut. In der Zukunft wird man diese Schnappfallen dann vermeiden können.
Wenn es in verschiedenen Funktionen bzw. Blöcken ist, kein Problem. Lokale Variablen existieren nur solang die Funktion bzw. der Block aktiv ist. Danach wird der Speicher freigegeben und kann von neuen lokalen Variablen belegt werden.
Wenn Du das sagst…. wird es wohl stimmen.
Gut, es ist eine erweiternde Konvertierung. Die wird wohl durch den Compiler implizit erfolgen, nehme ich mal an. Oder?
Ja, die automatische Konvertierung, von int zu float, geht ohne Meldung durch.
Sie ist das zu erwartende, hinreichend dokumentierte, Verhalten eines C oder C++ Compilers. So ist es in der Sprache definiert.
Soweit mir bekannt, kann man den Gcc zu einer Warnung/Error veranlassen, wenn man das ernstlich will.
Der UNO hat - aus HW Sicht - 3 verschiedene Speicherarten: Flash(Programmspeicher, für 'normale' Programme ReadOnly ), EEPROM ( nichtflüchtiger Datenspeicher) und RAM( flüchtiger Datenspeicher/dynamischer Speicher).
Das RAM wird aber vom Compiler nochmal in 3 Abschnitte unterteilt. Vereinfacht gesagt: ein Bereich für global definierte Variable, ein Bereich für dynamisch angelegte Variable und den Stack. Der Bereich für die globale Variablen startet am Anfang des RAM. Die belegte Länge ist nach der Compilierung fest und wird angezeigt ( Globale Variable verwenden .... )
Der Stack startet am Ende des verfügbaren RAM und wächst Richtung Anfang. Jeder Funktionsaufruf belegt dort Platz für die Rückkehradresse und lokale Variablen, der wieder freigegeben wird, wenn die Funktion beendet wird. Dieser Bereich hat also keine feste Größe, sondern verändert sich dynamisch - mal größer, mal kleiner - während des Programmablaufs.
Der Platz dazwischen ( nennt sich Heap) wird von dynamisch erzeugten Variablen belegt. Der Heap startet am Ende des globalen Bereiches und wächst Richtung Stack. Den Heap nutzt z.B. die 'String' Klasse. Das Problem ist , dass der Platz für den Heap nicht fix ist. Je größer der Stack wird ( z.B. viele geschachtelte Funktionsaufrufe mit evtl. vielen lokalen Variablen), umso enger wird's im Heap. Das kann z.B. zu schwer zu findenden Problemen führen, wenn man intensiv 'String' benutzt (oder auch selbst viel dynamisch erzeugte Variablen nutzt) und sich Heap und Stack in die Quere kommen.
Nur die Größe des unteren Bereiches wird nach der Compilierung angegeben. Dein Programm braucht auf jeden Fall mehr RAM als dort angegeben wird. U.U sogar viel mehr ....