Features: RTC, WLAN, SD-Karte, Temperatur-, Luftdruck- und Luftfeuchtigkeitssensor und OLED-Display, Solarzelle
In diesem Beitrag wollen wir das bereits erstellte Datenlogger Modul auf den Betrieb mit einer Solarzelle umstellen.
In dieser Beitragsserie wollen wir einen Datenlogger bauen der Temperatur, Luftfeuchtigkeit und Luftdruck in eine .csv-Datei auf eine SD-Karte schreibt und nebenbei auch noch mobil von einer Solarzelle versorgt wird. In Teil 1 wird der Sensor und das LCD Display eingebunden. Anschließend folgt in Teil 2 die Erweiterung um eine Real-Time-Clock für präzise Zeitstempel und die SD-Karte für langfristiges Speichern der Messwerte. In Teil 3 machen wir die Anwendung mittels Solarzelle mobil und optimieren den Stromverbrauch. Dann folgt in Teil 4 noch das zusätzliche Senden der Daten an einen Datenserver und abschließend stellen wir diese Daten in Teil 5 noch grafisch dar.
Bauteile für D1 Mini Datenlogger
Folgende Bauteile wurden für diesen Beitrag verwendet / genauer betrachtet:
Waveshare Solar Power Manager
![](https://blog.db-es.com/wp-content/uploads/2023/06/20230603_093724.jpg)
Das Modul wird mit der Solarzelle versorgt und läd mit dieser den Li-Ion-Akku bzw. versorgt den 5V/1A Ausgang.
Der Eigenverbrauch wurde mit ca. 2 mA gemessen.
Die wichtigsten Parameter laut Waveshare Wiki sind
- Solar
- Eingangsbereich 6-24V
- Wirkungsgrad ca. 78%
- MPPT
- Akku
- 3,7V 850mAh 14500 (hier leuchtet mir nicht ein warum explizit diese mAh und Größe angegeben ist. Vielleicht bezieht sich diese Angabe des Herstellers auch nur auf den Halter auf der Platine. Es gibt noch die Möglichkeit den Akku über den PH2.0 Connector anzuschließen. Der Ladeelektronik sollte es schließlich egal sein ob 1. der Typ auch ein 18650er Akku ist oder 2. er 3100 mAh hat)
- Tiefentladeschutz
- Modul
- zusätzlicher Mikro-USB-Eingang
- Ausgang 5V/1A (für Betrieb mit Raspberry Pi nicht geeignet) USB oder Pin-Header
- Ruhestrom < 2 mA
- Überhitzungs- und Kurzschlussschutz (das würde ich aber vielleicht noch mal separat testen)
Es nutzt den CN3791 Chip für das Solar-Management. Auf der Unterseite befinden sich noch Dip-Schalter um die Spannung für die angeschlossene Solarzelle festzulegen. Damit soll die integrierte MPPT-Funktion optimiert werden, um so die Solarzelle effektiver auszunutzen.
Das MPPT (Maximum-Power-Point-Tracking) ist eine generelle Funktion in Zusammenhang mit Solarzellen. Man ist nämlich stets bestrebt den Punkt mit der maximalen Leistungsausbeute auf der lichtabhängingen I-U-Kennlinie der Solarzelle zu finden. Weitere Infos zum MPPT auf Wikipedia.
Li-Ion Akku NCR18650A
Bei dem Akku handelt es sich um eine sogenannte Rohzelle. Das heißt dieses Modul besitzt keine integrierte Schutzelektronik. Das ist in unserem Fall nicht weiter dramatisch, da wir ein Lademodul mit entsprechenden Schutzfunktionen einsetzen.
Das verwendete Li-Ion Akku hat folgende Eigenschaften:
Spannung | 3,6 V |
Kapazität | 3100 mAh |
Ladestrom | 1,55 A |
![Li-Ion Akku](https://blog.db-es.com/wp-content/uploads/2023/05/20230528_102954-143x300.jpg)
Solar Panel 6V 5W
![solarbetrieb](https://blog.db-es.com/wp-content/uploads/2023/06/20230603_102237.jpg)
Betriebsspannung | 6 V +/-5% |
Betriebsstrom | 833 mA +/-5% |
Leerlaufspannung | 7,2 V +/-5% |
Kurzschlussstrom | 916 mA +/-5% |
Leerlaufspannung wurde mit 7V gemessen, liegt also leicht unter dem angegebenen Wert aber innerhalb der Toleranz.
Weitere spannende Infos zu Solarzellen allgemein:
Stromverbrauch und Optimierung
Mit einem Tischmultimeter wurde zunächst die gesamt Stromaufnahme des Aufbaus gemessen. Also vom D1 Mini, dem RTC Modul, dem BME280 Sensor und dem OLED Display.
Zur Ermittlung wie viel Strom die jeweilige Komponente aufnimmt bzw. um insgesamt einen Rückschluss zu bestimmen wie lange unser Akku den theoretisch durchhält wurde der Gesamtstrom mit einem Tischmultimeter gemessen und nach und nach einzelne Bauteile und Module zu- bzw. abgeschaltet.
D1 Mini | ca 79 mA |
BME280 | < 1 mA |
OLED Display | ca 4 mA |
RTC/SD Shield | < 1 mA |
Summe | 85 mA |
Laut technischen Daten sollte das Display nur 0,04 W, also 1,2 mA an 3,3V aufnehmen. Die ermittelten 4 mA liegen also deutlich darüber (ca Faktor 4). Absolut gesehen ist das im Vergleich zum Verbrauch des D1 Mini jedoch nicht weiter dramatisch.
Es wurden insgesamt ca. 85 mA gemessen. Der Akku würde demnach theoretisch, wenn er ganz voll wäre und eine Entladetiefe von 100% verkraften würde in ca. 36 h leer sein.
Das ist kein zufriedenstellender Wert und soll nun optimiert werden 🙂
Optimierung 1: WLAN Modul abschalten
Das WLAN Modul mit seinen 2,4 GHz benötigt relativ viel Strom. Wir möchten dies wenn WLAN nicht benötigt wird deaktivieren. Dies erledigen wir mit folgendem Code-Schnipsel:
#include <ESP8266WiFi.h> // fuer WIFI ausschalten
setup() {
WiFi.mode( WIFI_OFF );
WiFi.forceSleepBegin();
}
Siehe da, mit dieser Maßnahme sinkt die Stromaufnahme vom D1 Mini auf ca. 31 mA. Oder anders ausgedrückt haben wir den Stromverbrauch um ca. 64% optimiert. Die theoretische Akku Haltedauer wäre damit von 36 h auf ca. 100 h erhöht.
Da geht aber noch mehr.
Optimierung 2: Sleep mode
Wenn keine Messung bzw. Anzeige aktiv ist dann kann sich der Controller eigentlich das „Rechnen“ sparen. Dazu gibt es den sogenannten Sleep-Mode. Der D0 pin muss mit dem RST verbunden werden. Falls es mit der Verbindung zu Problemen beim flashen kommt muss die Verbindung temporär wieder gelöst werden. Quelle: wemos Beispiel auf github.
loop() {
...
ESP.deepSleep(US_SCHLAFINTERVAL); // [us]
...
}
Damit kann man die „Schlafdauer“ in µs angeben oder mit Wert 0 für immer Schlafen legen.
Wir verbinden zusätzlich noch einen manuellen Taster mit Reset, da wir auch auf Knopfdruck die Sensor-Werte angezeigt bekommen möchten.
Im Schlafmodus werden nun noch ca. 12 mA gemessen.
Optimierung 3: Display ausschalten
Da wir nicht 24/7 auf dem Display Werte ablesen müssen, könnten wir in der verbleibenden Zeit das Display ausschalten. Dazu legen wir den VCC vom Display auf einen Pin (D4) des D1 mini. Dies ist möglich, da die Stromaufnahme vom Display geringer ist als das Schaltvermögen des Pin (4 mA < 20 mA).
So sparen wir nochmal ca. 4 mA.
Offensichtlich ist die effektivste Stromsparmethode das WLAN-Modul auszuschalten. Das allein spart schon mal ca. 60 mA bzw ca. 71%.
Schaltplan
![](https://blog.db-es.com/wp-content/uploads/2023/06/d1mini_logger_teil3_bb-1024x684.png)
Code
Das Programm wird einen Ticken komplizierter. Da ich zwischenzeitlich nach erfolgter Initialisierung des Displays dieses aus- und nochmal anschalten wollte erfolgt der Zugriff über Pointer. Der Hintergrund ist, dass ein zweiter Aufruf von display.init()
fehlschlagen würde, da der „interne“ Speicher vom Display bereits angelegt wurde und dies in der init()
Methode abgefragt wird (das sieht man nur wenn man in die Bibliotheksfunktionen von Adafruit hineinschaut). Damit wird ein Display über new erzeugt und mit delete gelöscht. Dies habe ich gemacht da die Adafruit-lib dynamisch Speicher mit malloc()
belegt und man diesen nicht freigeben kann ohne den Destruktor aufzurufen. Am Ende erschien allerdings eine andere Strategie sinnvoller:
Für ein festgelegtes Intervall wird der Controller in den deepSleep-Mode versetzt. Wird er nach dem Intervall oder nach dem manuellen Tastendruck aufgeweckt durchläuft er zwangsläufig wieder seine setup()
-Routine, so als ob er erstmalig mit Spannung versorgt wird. Dadurch löst sich das Problem mit dem doppelten Initialisieren quasi von selbst in Luft auf. Ich lasse es dennoch so, da es erstens funktioniert und zweitens ja vielleicht noch mal jemand in einem anderen Projekt gebrauchen kann.
ACHTUNG: Es muss vermieden werden Wire.begin()
mehrfach aufzurufen. Das hat mir (zumindest bei den Arduinos) schon viel Zeit bei der Fehlersuche gekostet. Falls Wire.begin()
öfter aufgerufen wird hat sich der Arduino wie „eingefroren“ verhalten.
#include <Wire.h> // fuer I2C
#include <ESP8266WiFi.h> // fuer WIFI ausschalten
#include <Adafruit_BME280.h>
// lcd display include
#include <Adafruit_SSD1306.h>
#include <Adafruit_GFX.h>
Adafruit_BME280 bme1;
float temp1(NAN), hum1(NAN), press1(NAN);
bool bme1SensorError = false; // bme sensor nicht vorhanden oder nicht ok?
bool displayError = false; // display initialisierung fehlgeschlagen?
// [ms] wie lange bleibt die anzeige an nach aktivierung ?
const unsigned long MS_ANZEIGEDAUER = 5000;
// [us] wie lange darf ich schlafen bevor ich wieder messe ?
const uint64_t US_SCHLAFINTERVAL = 10e6;
// lcd defines
#define OLED_RESET -1
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
//Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
Adafruit_SSD1306* displayPtr = NULL;
/* ACHTUNG: der display konstruktor allokiert dynamischen speicher,
* da das display aber zur laufzeit an und ausgeschaltet werden soll
* um strom zu sparen muss der dynamische speicher wieder freigegeben
* werden. da die implementierung in der bibliothek fix ist muessen
* wir es bei uns loesen und da sind die new / delete operatoren
* die einzige moeglichkeit die ich aktuell sehe. */
#define PIN_SCREEN_POWER D7
void initDisplay(bool periphBegin = false) {
digitalWrite(PIN_SCREEN_POWER, HIGH); // display einschalten
delay(100); // etwas zeit geben zum spannung stabilisieren (WICHTIG)
// etwas grosszuegig, aber wir habens ja nicht eilig :)
if (displayPtr) {
Serial.println("display objekt erst loeschen");
delete displayPtr;
displayPtr = NULL;
}
if (!displayPtr) {
Serial.println("display objekt erfolgreich geloescht");
}
displayPtr = new Adafruit_SSD1306(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
if (!displayPtr->begin(SSD1306_SWITCHCAPVCC, 0x3C, /*reset*/ true, /*periphBegin*/ periphBegin)){
Serial.println("display init error!");
displayError = true;
return;
}
displayPtr->setTextSize(2);
displayPtr->setTextColor(WHITE); // ohne Effekt -> das eingesetzte Display kann nur blau
displayPtr->clearDisplay();
displayPtr->setCursor(5, 0);
displayPtr->setTextSize(2);
displayPtr->println("Init");
displayPtr->display(); // Text zeigen
}
void setup()
{
Serial.begin(9600); // optional: aber praktisch fuer debug ausgaben ;)
// while (!Serial) ;
delay(200);
Serial.print("compiled: "); // kompilier zeitstempel ausgeben
Serial.print(__DATE__);
Serial.println(__TIME__);
pinMode(LED_BUILTIN, OUTPUT);
pinMode(PIN_SCREEN_POWER, OUTPUT); // spannungsversorgung vom display
digitalWrite(PIN_SCREEN_POWER, HIGH); // display einschalten
int i;
for (i = 0; i < 5; i++) { // max 5x versuchen mit bme zu kommunizieren / initialisieren
if (!bme1.begin()) { // KEIN erfolg?
Serial.println("BME280 sensor nicht gefunden. Versuche es in 1s noch einmal.");
delay(1000); // dann 1s warten
} else {
break; // nicht weiter probieren
}
}
if (i >= 5) { // 5x ohne erfolg versucht zu kommunizieren?
bme1SensorError = true;
Serial.println("Konnte BME280 Sensor nicht finden!");
} else {
Serial.println("BME280 Sensor gefunden! ERFOLG!!");
}
if (!displayPtr) {
Serial.println("init display");
displayPtr = new Adafruit_SSD1306(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
} else {
Serial.println("error display ptr not NULL");
}
initDisplay(true);
delay(200);
displayPtr->clearDisplay();
if (bme1SensorError) {
displayPtr->setCursor(5, 0);
displayPtr->println("Sensor-");
displayPtr->setCursor(5, 30);
displayPtr->print("fehler");
} else {
displayPtr->setCursor(5, 0);
displayPtr->println("Hallo");
displayPtr->setTextSize(2);
displayPtr->setCursor(5, 30);
displayPtr->print("Datenlogger");
}
displayPtr->display(); // Text zeigen
WiFi.mode( WIFI_OFF );
//digitalWrite(PIN_SCREEN_POWER, LOW); // display ausschalten -> strom sparen
WiFi.forceSleepBegin();
//ESP.deepSleep(10e6); // [us]
}
void loop()
{
static unsigned long millisOld = 0;
static unsigned long millisLastRead = 0; // letztes sensor auslesen
unsigned long curMillis = millis();
if (curMillis - millisOld > 1000) {
// led toggeln / invertieren => lebenszeichen
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
millisOld = curMillis;
}
// anzeigezeit abgelaufen ?
if (curMillis > MS_ANZEIGEDAUER) {
// dann display ausschalten
digitalWrite(PIN_SCREEN_POWER, LOW);
Serial.println("stoppe anzeige. lege mich schlafen...");
ESP.deepSleep(US_SCHLAFINTERVAL); // [us]
}
// anzeigezeit laeuft ?
else {
// dann alle 1s werte anzeigen
if (!bme1SensorError) {
if (curMillis - millisLastRead > 1000) {
readAndPrintBME280Data();
millisLastRead = curMillis;
}
}
}
}
void readAndPrintBME280Data()
{
temp1 = bme1.readTemperature();
hum1 = bme1.readHumidity();
press1 = bme1.readPressure() / 100;
String Temperatur = String(temp1);
Serial.print("Temp = " + Temperatur + " C | ");
String feuchte = String(hum1);
Serial.print("Feuchte = " + feuchte + " %RF | ");
// ohne nachkommastellen darstellen und dabei richtig runden
String druck = String(int(press1 + 0.5f));
Serial.println("Druck = " + druck + " hPa");
// lcd
displayPtr->clearDisplay();
displayPtr->setTextSize(2);
displayPtr->setTextColor(WHITE);
displayPtr->setCursor(1, 1);
displayPtr->println(Temperatur + " C");
displayPtr->setCursor(1, 25);
displayPtr->println(feuchte + " % RF");
displayPtr->setCursor(1, 50);
displayPtr->println(druck + " hPa");
displayPtr->display(); // anzeige auftrag ausfuehren
}
Praxis / Testergebnisse
Hier schreibe ich noch mal was die Praxis alles so ergeben hat…Inklusive Lessons learned und Stolperfallen :). Aktuell läuft nämlich ein kleiner Dauertest bei dem das Schlafintervall auf 120s erhöht wurde, was eigentlich für diesen Anwendungsfall ausreicht. Der Logger soll nämlich das Klima in einem Gewächshaus über einen längeren Zeitraum aufzeichnen.
Update und erste Erkenntnis nach dem Test: Im Großen und Ganzen klappt es ganz gut. Falls mal länger kein Licht zur Verfügung steht riegelt der Power Manager bei ca. 3,15 V ab und schützt den Li-Ion Akku vor Tiefentladung. Allerdings ist der Gesamtstrom etwas höher als erwartet. Nach einigen Minuten steigt die Gesamtaufnahme von 4 mA während des Schlafmodus auf 52 mA. Obwohl während der Anzeige nur 30 mA aufgenommen werden. Da scheint es ein Problem mit dem Aufwachen aus dem Schlafmodus zu geben.
Update 2: Nach einiger Recherche im Internet bin ich auf 3 mögliche Ursachen gestoßen:
- „Billiger“ Flash-Chip verhindert sauberes Wiederanlaufen. Das kann mit der Ausgabe überprüft werden (falls die ID auf 0B endet wäre es der „Billig-Chip“:
Serial.println(ESP.getFlashChipId(),HEX); // 16405E
- Falsche externe Beschaltung von Pins die für den Boot-Vorgang relevant sind. D3, D4, D8. Das Siehe auch diese Seite von espeasy und den Blog von Stefan Frings. Dies kann ebenfalls ausgeschlossen da zum Test mal nur die I2C Pins mit dem Display angeschlossen wurden.
- Einige mit ähnlichem Problem haben eine modifizierte sleep-Funktion aufgerufen und konnten damit ihr Problem lösen. Das hat mir leider nicht geholfen.
Am Ende habe ich das Problem durch Zufall gefunden und konnte im Internet niemand anders finden der das gleiche Problem beschrieben hat. Mir ist nämlich aufgefallen, dass ich das Problem immer dann habe wenn ich die Versorgung vom Display abziehe und in der Luft halte. Sobald ich VCC direkt auf 3,3V lege klappt der Sleep-Mode wunderbar. Ich habe keine vollständige Erklärung und eigentlich widerspricht das der Dokumentation. Laut dieser können D1 (SCL) und D2 (SDA) nämlich bedingungslos genutzt werden. Aber meine Vermutung ist, dass durch das Abziehen von VCC das Display diese in einen Zustand versetzt der offensichtlich doch einen Einfluss auf das Boot-Verhalten hat. Am Ende war ich froh die Ursache gefunden zu haben auch wenn es bedeutet, dass die Versorgung jetzt auf 3,3V gelegt werden muss und das Display somit dauerhaft eingeschaltet ist wodurch eine Stromsparmaßnahme leider wegfällt.
Im Sleepmode werden jetzt 8 mA gezogen und bei Betrieb alle 120s ca. 27 mA für 10s. Der Akku hält theoretisch also ca. 310 h was gute 12 Tage sind und zumindest im Sommer ganz gut machbar ist.
![](https://blog.db-es.com/wp-content/uploads/2023/06/datenlogger_test-1024x660.png)
Damit wären wir für diesen Teil fertig.
Schreibe einen Kommentar