Daten loggen auf SD - String und RAM-Probleme vermeiden, aber wie?

Hallo,

ich habe in meinem Sketsch mehre Variablen (8 Stk.) als Float definiert, da ich mit ihnen rechnen muss. Das Ergebnis soll in ein txt-File mit Zeitstempel vom DS3231 auf SD geschrieben und im Serialmonitor ausgegeben werden (idealerweise in Spalten formatiert).

Das hatte ich auch schon quasi fertig, indem ich alle Werte mit += in eine Stringvariable geschrieben habe, die ich dann mit print geschrieben habe.

Dummerweise gab es Probleme mit dem Öffnen des Textfiles von SD, was wohl auf RAM-Probleme infolge des Strings (gefüllt mit Float-Werten) zurückzuführen war.

Ich muss also eine andere, RAM schonende, Möglichkeit finden, die Werte mit wenige Quellcode zu schreiben.

Ich habe schon herausgefunden, dass ich die Float-Werte mit dtostrf() in Char konvertieren kann. Gelesen habe ich auch von einem Char Array als String mit definierter Größe und der Funktion sprintf(), mit der man die Anzahl der in den Zielstring zu schreibenden Zeichen auf n begrenzen kann.

Viel Input, aber der richtige Durchblick fehlt noch. Kann mir mal jmd. einen Tipp geben, wie man das richtig anpackt? Vllt. hat auch jmd. einen Beispielcode, oder weiß wo einer steht.

Cool wäre auch, wenn ich einen Tipp bekommen könnte, wie man diese Werte formatiert in Spalten nebeneinander ausgeben kann.

print() kann selbst Float Werte formatieren. Es ist ein Irrweg zu denken dass man Strings an einem Stück schreiben muss. Du kannst einfach sowas machen:

file.print(value1, 1);
file.print('\t');
file.print(value2, 1);
file.print('\t');
file.println(value3, 1);

Wenn du die Syntax etwas schöner haben willst, kannst du das nehmen:
http://arduiniana.org/libraries/streaming/

Damit hat man die Output Stream Syntax aus C++. Da wird aber nur der << Operator überladen und darin print() gemacht. Also der Code dahinter ist der gleiche. Es geht darum dass man weniger schreiben muss und der Code leserlicher ist

Dann noch ein Trick für die einfacherere Ausgabe auf SD und Serial:

void printValues(Stream& stream)
{
  stream.print(value1, 1);
  stream.print('\t');
  stream.print(value2, 1);
  stream.print('\t');
  stream.println(value3, 1);
}

Und so aufrufen:

printValues(Serial);
printValues(file);

Hier nutzt man aus, dass sowohl File als auch Serial Unterklassen der Stream Klasse sind, welche wiederum eine Unterklasse von Print ist, welche die ganzen print()/println() Methoden enthält. Deshalb verwendet man als Parameter eine Referenz auf Stream und kann dann Serial und File übergeben. So kannst du den exakt gleichen Code für die Ausgabe auf Serial und SD verwenden.

EDIT: geht doch mit Streaming wenn man es will. Ich hatte es erst ausprobiert und da hat es irgendwie nicht funktioniert :s

Noch eine Sache mit Strings:
Normal landen alle String Literale im RAM. Um das zu verhindern kannst du bei print()/println() das machen:

Serial.println(F("String im Flash"));

Dann belegt der kein RAM mehr.

Aber nur mit String Literalen (d.h. was in doppelten Anführungszeichen steht). Geht natürlich nicht mit Variablen oder char Literalen (einfache Anführungszeichen).

Hallo,

immer wieder sachlich schön erklärt. :)

Super Info, vielen Dank.
Weniger schreiben geht nicht mehr. Beim Zeitstempel lies sich die führende Null bei einstelligen Werten auch gleich mit integrieren. Für die “kurze” Schreibweise muss man allerdings noch die Library Streaming.h einbinden.

Nachfolgend mal mein Test-Sketch.

#include <Streaming.h>
#include <SD.h>
#include <Wire.h> 
#include <DS3232RTC.h>
#include <Time.h>
#include <SPI.h>

const int chipSelect = 4;
File dataFile;
float value1 = 1.22222222222222222222;
float value2 = 22.7777777777777777777;
float value3 =23.56;



void setup() {
  Serial.begin(9600);
  
  //*************** DS3231 initialisieren **************
  setSyncProvider(RTC.get);
  if(timeStatus()!= timeSet) {
     Serial.println("Zeisynchronisation fehlgeschlagen"); 
  }
  else {
     Serial.println("Zeitsynchronisation erfolgreich"); // alles ok
  }
  
  // **************** SD Karte initialisieren ***********
  Serial.print("Initializing SD card...");
  if (!SD.begin(chipSelect)) {
    Serial.println("Card failed, or not present");
    return;
  }
  Serial.println("card initialized.");
  if (SD.exists("datalog.txt")) {
    Serial.println("datalog.txt exists.");
  }
  else {
    Serial.println("datalog.txt doesn't exist.");  
  }

  // **************** Schreibe Titelzeile *********************
 Serial << "Zeitstempel" << '\t' << '\t' << "Value1" << '\t' << "Value2" << '\t' << "Value3" << endl;   
 // freeRam 670 ohne zu 636 mit dieser Zeile :(
}

void loop() {

  printValues(Serial);
  Serial.println(FreeRam());
  
  SD.begin(chipSelect);
  dataFile = SD.open("datalog.txt", FILE_WRITE);
  if (dataFile) {
    printValues(dataFile);
    dataFile.close();
  }
  else {
    Serial.println("error opening datalog.txt");
  }
  delay(10000);

}


void printValues(Stream& stream){
  setSyncProvider(RTC.get);
  stream << day() << "." << month() << "." << year() << " " << ((hour()<10)?"0":"") << hour() << ":" << ((minute()<10)?"0":"") << minute() << ":" << ((second()<10)?"0":"") << second() << '\t' << value1 << '\t' << value2 << '\t' << value3 << endl;
}

Sorry, wenn er ein wenig unformatiert ist.

Was mich wundert:
Im Setup habe ich für den Serial Monitor mit Stream eine Spaltenüberschrift definiert. Eigentlich dürfte das keinerlei Ram Ressourcen verschlingen, wenn ich dass alles richtig verstanden habe. Kontrolliere ich das ganze aber über die Ausgabe des freien Speichers über FreeRam(), dann bekomme ich mit der Titelzeile einen Wert von 636 und ohne diese Zeile von 670.
Demnach würde das ganze doch Ram belegen und nach Ausführung nicht wieder frei geben???
Kann das sein??? Dann hätte ich etwas nicht reichtig verstanden, oder in meinem Code ist noch ein Fehler :confused:

Im Setup habe ich für den Serial Monitor mit Stream eine Spaltenüberschrift definiert. Eigentlich dürfte das keinerlei Ram Ressourcen verschlingen, wenn ich dass alles richtig verstanden habe.

Durch die Harvard Architektur des Prozessors bedingt (d.h. getrennte Adressräume für Flash und RAM) landen alle String Literale erst mal im RAM. Und das permanent, auch wenn sie nur lokal verwendet werden. Es wird nur kein RAM belegt wenn man das F() Makro verwendet.

Das geht auch mit Streaming:

Serial << F("String im Flash") << endl;

Mach das mal überall wo die String Literale mit print()/println() hast. Da kannst du einiges sparen. Mit SD.exists() oder SD.open() geht das aber nicht.

Wo du noch hier und da ein paar Byte RAM sparen kannst ist wenn du statt z.B. “:” nur ‘:’ schreibst. Also bei einzelnen Zeichen ein char Literal statt ein String Literal. Der String braucht nämlich 2 Bytes (1 Zeichen + Terminator), während der char aus dem Flash gelesen werden kann.

Eine Sache die vielleicht nicht offensichtlich ist: Falls du die Anzahl der Nachkommastellen angeben willst (Standard sind 2, wenn man nichts macht) geht das so:

Serial << _FLOAT(value, 1);

Und nochwas:

setSyncProvider(RTC.get);

Ich stelle hier immer wieder fest, dass Leute nicht verstehen was diese Funktion macht. Die setzt nur die Synchronisierungsfunktion. Das macht man einmal in setup(). Danach erfolgt die Synchronisation mit der RTC automatisch wenn die eingestellte Sync Zeit abgelaufen ist (per setSynchInterval() ) und man einen Wert ausliest.

Schadet zwar an der Stelle nichts, aber bringt auch nichts

Ich stelle hier immer wieder fest, dass Leute nicht verstehen was diese Funktion macht.

;)

Ich stelle immer wieder fest, dass die Leute fette Libraries verwenden mit Funktionen die sie (eigentlich) gar nicht brauchen. Dass dann Speicher-Probleme (evtl. sogar im Flash) auch wahrscheinlicher werden, ist eigentlich eher ein Nebeneffekt.

Andererseits: Solange es passt, ist ja eigentlich gut (600 byte freier RAM bei SD-Verwendung ist doch was). Ob ein fertig programmierter Arduino auch mit weniger Speicher auskäme, ist eher eine Frage des sportlichen Ehrgeiz. Und je mehr unsichtbar im Hintergrund passiert ( siehe setSyncProvider ), desto eleganter ist es.

michael_x:
Ich stelle immer wieder fest, dass die Leute fette Libraries verwenden mit Funktionen die sie (eigentlich) gar nicht brauchen.

Tja, was soll man machen, wenn man nicht das nötige Know-How hat? Also nehmen was es gibt und ausprobieren. Und dann hier fragen :slight_smile:

Wenn man mit Unix-Zeit rechnen will ist die Time Library ganz nett, da sie bequeme Konvertierungsfunktionen enthält. Oder Zeit per NTP Server holen und zwischen den Sychronisierungen auf dem Arduino zählen. Braucht auch nicht wirklich viel Speicher.

Hier ist sie allerdings in der Tat total überflüssig. Die Zeit kann man auch direkt aus der RTC auslesen.

Also ich bin kein Profi, sondern Anfänger. Ich schau mir erst mal viel an, probiere aus und freu mich wenn´s funktioniert. Insofern hat Klaus_ww recht, wenn ich seine Aussage auch als ein wenig selbstherrlich erachte. Sie bringt hier in diesem Thread keinem was. Die Aussage von Michael_x ist in diesem Zusammenhang erstmal wenig hilfreich, da sie zwar andeutet, aber dann wenig konstruktiv ist.

@Serenifly: Danke für die ausführliche Hilfestellung. Deine Hinweise haben echt geholfen. Ich werde mir das mit der Time Library mal ansehen. Die des DS3232RTC werde ich aber doch weiterhin brauchen, oder?

Eine Library beinhaltet immer mehr Funktionen, als ggf. vom Sketch tatsächlich benötigt wird. Ich dachte, beim Kompilieren würde nur der benötigte Teil der Library umgesetzt. Wenn ich euch richtig verstehe, wird aber die ganze Bibliothek hochgeladen?

stonev:
Die des DS3232RTC werde ich aber doch weiterhin brauchen, oder?

Ja. Wobei es auch nicht schwer ist RTCs per Hand auszulesen. Aber dann schreibt man nur den Code selbst, den eine Library schon fertig hat.

Wenn ich euch richtig verstehe, wird aber die ganze Bibliothek hochgeladen?

Nein, du hattest schon recht. Es gibt hier aber ein paar Leute die nehmen es mit der Effizienz ganz, ganz genau und/oder reagieren etwas allergisch auf Libraries, selbst wenn sie gar nicht viel Speicher brauchen.

Die Time Library ist bei deiner Anwendung zwar unnütz, aber schaden tut sie auch nicht wirklich

Noch eine ganz andere Frage, will aber keinen Thread aufmachen:

In der Anwendung wird auch noch ein Temperatur/Luftfeuchtesensor ausgelesen (DHT22).
Derzeit noch an einem Digitalpin angeschlossen funktioniert er gut.

Allerdings soll er später ca. 3m weite weg vom Arduino an einer Außenwand hängen. Soweit ich weiß, kann ich am Digitalen Pin kein 3m Kabel anschließen.

Der Gedanke ist, über SCL/SDA zwei I2C Extender (P82B715) zu nutzen. Slave wäre dann der Außensensor mit dem DHT22. Nun kann ich aber den DHT22 nicht an SCL/SDA des P82B715 anschließen. Aber das müsste doch mit einem PortExpander machbar sein?
Ich hätte dann am Außensensor den P82B715 mit den PullUps, den Portexpander (z.B. PCF8574) und den Sensor.

Kann das so funktionieren, bin ich auf dem Holzweg oder geht es noch viel einfacher?

Die Aussage von Michael_x ist in diesem Zusammenhang erstmal wenig hilfreich, da sie zwar andeutet, aber dann wenig konstruktiv ist.

Muss ich dir, ehrlich gesagt, recht geben.
Sind allerdings eigentlich zwei Aussagen:

  • Musst du dich um die Libraries kümmern
  • Wenn’s geht ( 600 Byte RAM frei), dann lass es doch wie es ist.

Die [Library] DS3232RTC werde ich aber doch weiterhin brauchen, oder?

Auf die Wire.h würde ich nicht verzichten, wenn deine Uhr läuft und in deinem DatenLogger-Sketch nicht gestellt werden muss, ist das Auslesen der Uhrzeit in 6 Byte - Variable ein Klacks.

Wenn man den minimalistischen Ansatz betonen will:

byte bcdsekunde = 0x59; // Beispiel: 59 sek.
char time[] = "12:59:xx";
time[6] = '0' + bcdsekunde >> 4;    // Zehner
time[7] = '0' + bcdsekunde && 0x0F; // Einer

Serial.println(time); //  ;)

Beispiel

Das spart sogar die bcd2dec Funktion.
Aus der DS3232RTC brauchst du dann nur die vereinfachten Zeilen 100 - 120 aus
DS3232RTC.cpp

/*----------------------------------------------------------------------
 * Reads the current time from the RTC and returns it in an array 
 * byte time[6]; ( [0] = sek... )
 * Returns the I2C status (zero if successful).        
 *----------------------------------------------------------------------*/
byte DS3232RTC::read(byte* bcdTime)
{
    i2cBeginTransmission(RTC_ADDR);
    i2cWrite((uint8_t)RTC_SECONDS);
    if ( byte e = i2cEndTransmission() ) return e;
    //request 7 bytes (secs, min, hr, dow, date, mth, yr)
    i2cRequestFrom(RTC_ADDR, 7);
    *bcdTime++ = i2cRead() & ~_BV(DS1307_CH);   
    *bcdTime++ = i2cRead();
    *bcdTime++ = i2cRead() & ~_BV(HR1224);    //assumes 24hr clock
    i2cRead();  // DOW not used
    *bcdTime++ = i2cRead();
    *bcdTime++ = i2cRead() & ~_BV(CENTURY));  //don't use the Century bit
    *bcdTime = i2cRead();
    return 0;
}

Wie gesagt, wenn du Speicherplatz ausknautschen willst/musst, und zum RTC stellen temporär einen anderen Sketch lädst. Sonst kannst du natürlich gern die Library verwenden…

( Ob du die Konstanten (RTC_ADDR) umkopierst, die .h Datei includierst, oder direkt die Werte verwendest, macht an der Sketch-Größe keinen Unterschied )

Andeutungen ohne konkret zu werden, sind eigentlich spannender. :wink:
Ich hoffe, das war jetzt nicht zu ausführlich.

P.S. @Serenifly: Allergisch würde ich das nicht nennen :wink:
Aber wenn es es drum geht, was man ausser dem F() Makro noch machen kann, ist Libraries lesen und verstehen ( und evtl. anfassen ) recht lehrreich, finde ich.

@ michael_x:
Ich würde mal sagen: Voll rehabilitiert :wink:
Danke für eure Hilfe, ich werde mich am WE mal damit ausgiebig auseinandersetzen.

Der originale Sketch ist etwas größer und da tauchen die Speicherprobleme auf, da die Datei auf der SD Karte mangels Ram nicht beschrieben wird. Von daher ist das alles sehr hilf- und lehrreich.

Um noch mehr Bastelstoff für das WE zu haben, möchte ich noch einmal meinen Post #10 vorholen:

stonev:
Noch eine ganz andere Frage, will aber keinen Thread aufmachen:

In der Anwendung wird auch noch ein Temperatur/Luftfeuchtesensor ausgelesen (DHT22).
Derzeit noch an einem Digitalpin angeschlossen funktioniert er gut.

Allerdings soll er später ca. 3m weite weg vom Arduino an einer Außenwand hängen. Soweit ich weiß, kann ich am Digitalen Pin kein 3m Kabel anschließen.

Der Gedanke ist, über SCL/SDA zwei I2C Extender (P82B715) zu nutzen. Slave wäre dann der Außensensor mit dem DHT22. Nun kann ich aber den DHT22 nicht an SCL/SDA des P82B715 anschließen. Aber das müsste doch mit einem PortExpander machbar sein?
Ich hätte dann am Außensensor den P82B715 mit den PullUps, den Portexpander (z.B. PCF8574) und den Sensor.

Kann das so funktionieren, bin ich auf dem Holzweg oder geht es noch viel einfacher?

Ich habe gestern erst einmal den DHT22 nur über den Expander PCF8574 an den I2C Bus angeschlossen.
Nun rätsel ich krampfhaft, wie ich ihn über den Expander auslesen kann.
Das ich den Expander irgendwie adressieren muss ist klar. Er hat im Bus die Adresse 0x21 und der Sensor hängt am INT0.

Normalerweise wird der DHT (je nach Library) in etwa so abgefragt:

#include <DHT22.h>
int sensorPin = 8;
DHT22 mySensor(sensorPin);

void loop() {
mySensor.readData();
serial.println(mySensor.getTemperatureC());
serial.println(mySensor.getHumidity());
}

Wie bringe ich dem ganzen jetzt bei, dass der Sensor nicht am Pin 8, sondern INT0 des Expanders 0x21 hängt? Reicht es den PIN einfach anders zu definieren?

Oder muss mann etwas in folgende Richtung unternehmen:

#include <Wire.h>
#define PCF8574 0x20
DHT22 mySensor(PCF8574);


//Funktion zum Lesen des PCF8574-Bausteins hier aus dem Forum
byte PCF8574_Read(int adresse) {
byte datenByte=0xff;
Wire.requestFrom(adresse,1);
if(Wire.available()){
datenByte=Wire.read();
datenkomm=true;
}
else {
datenkomm=false;
}
return datenByte;
}

void setup(){

Wire.begin();
Serial.begin(9600);
}

void loop() {
byte daten;
daten=PCF8574_Read(PCF8574);
serial.println(daten.getTemperatureC());
serial.println(daten.getHumidity());
}

Das Beispiel steckt garantiert voller Fehler - ich weiß. Aber ich weiß einfach nicht, in welche Richtung ich paddeln muss. Ich finde auch wenig über Google. Wenn ich Beispiele finde, mit denen ich PINs des Expanders ansprechen kann, fehlt mit immer noch die Info, wie ich den DHT einmal auslese und dann alle Einzelwerte (Temperatur und Luftfeuchte) einzeln ausgeben kann. :confused:

OneWire-artige Dinger wie DHTs kannst du nicht über I2C Expander anschließen. Das geht vom elektrischen Protokoll her einfach nicht.

Mist, was habe ich denn für möglichkeiten? Klappt das den Sensor mit ca. 3m Kabel am Arduino anzuschließen?

Was könnte ich alternatv machen, ohne dem Sensor nicht gleich einen eigenen Arduino pro min 3.3v spendieren zu müssen?

Kannst du nicht den oneWire Bus mit dem I2C Treibern verlängern? beides ist bidirektional, ich könnte mir vorstellen, dass das sogar geht

ElEspanol: beides ist bidirektional, ich könnte mir vorstellen, dass das sogar geht

Nur wenn man die Library umschreibt und es dabei schafft das Timing einzuhalten. Nach man das Start-Signal sendet muss man ja jedes Bit einzeln einlesen. Dabei ist ein Low ca. 26µs lang und ein High 70µs.

Und ob man jetzt OneWire an einem 3m Kabel hat oder einen I2C Expander an einem 3m Kabel kommt doch auf das gleiche heraus, oder?

Na ja, ich bin halt kein Elektroniker, aber mein Gedanke war, wenn es mit I2C Speed bis 100m geht, geht es bei OneWire auch, da das ja langsamer ist (oder nicht?).

Oder ist es da kritischer, weil keineTaktleitung dabei ist?

Ist es sicher, dass es nicht geht oder hat es nur noch niemand ausprobiert?

Ist DHT Ansteuerung überhaupt OneWire?

Ich habe übrigens einen DS18B20 an einem 2m Kabel auf dem Fensterbrett, geht seit Monaten.

Ist es sicher, dass es nicht geht oder hat es nur noch niemand ausprobiert?

Weiß nicht, aber ich kann auf die schnelle keinen Code dazu finden.

ElEspanol: Ist DHT Ansteuerung überhaupt OneWire?

Ist glaube ich nicht identisch mit dem DS18B20 OneWire, aber das grobe Prinzip ist sehr ähnlich. Deshalb hatte ich es erst "OneWire-artig" genannt

OK, ich kann ja mal einen Test unternehmen und den PIN vom Arduino mit den Treibern verlängern. Versuch macht klug 8) Würde dann den "One-wire-ähnlichen" an SDA der Treiber hängen und SCL unbelegt lassen. SDA mit den üblichen Pullups oder lieber nicht?

Ansonsten würde ich einfach mal das lange Kabel verwenden. Wenn das nicht geht, habe ich endlich einen Grund die besseren Sensoren von Sensirion zu kaufen ;) Die gibt es in der Bucht derzeit recht günstig zwichen 2,50 (bei 20 Stk.) bis 4 Euro das Stück. Der SHT21 kommuniziert I2C. Muss dann nur noch jmd. finden, der mir das kleine Teil auf ne Platine lötet, aber da kenn ich einen...