Programmspeicherplatz und dynamischer Speicher schnell voll

Hallo,

um meine Heimautomatisierung mit mehr Sensorwerten zu füttern möchte ich über mein Lan einen Arduino anbinden. Dazu habe ich einen Arduino Nano und einen Ethernet shield mit W5100 Chip verwendet. Als Protokoll benutze ich MQTT (library).
Momentan hab ich per 1-Wire drei DS18B20 Sensoren und mit dieser library zwei DHT22 Sensoren eingebunden. Ein bmp180 hat mit dieser library auch noch seinen Platz bekommen.

Diesen Code habe ich mir erstellt der auch soweit funktioniert.

#include <SPI.h>
#include <Ethernet.h>
#include <PubSubClient.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <dht.h>
#include <SFE_BMP180.h>
#include <Wire.h>

//------------1Wire---------------------

// Data wire is plugged into port 2 on the Arduino
#define ONE_WIRE_BUS 7

// Setup a oneWire instance to communicate with any OneWire devices (not just Maxim/Dallas temperature ICs)
OneWire oneWire(ONE_WIRE_BUS);

// Pass our oneWire reference to Dallas Temperature.
DallasTemperature sensors(&oneWire);

//-------------DHT-----------------------

dht DHT;

byte DHT22_PIN;

//------------- BMP180 --------------

SFE_BMP180 pressure;

#define ALTITUDE 221.0 // Altitude in meter

//----------------Netzwerk---------------

byte mac[]    = {  0xDE, 0xED, 0xBA, 0x1A, 0x1A, 0x1A };
IPAddress ip(192, 168, 0, 214);
IPAddress server(192, 168, 0, 20);

EthernetClient ethClient;

PubSubClient client(ethClient);
char message_buff[100];



long lastReconnectAttempt = 0;
unsigned long previousMillis = 0;
unsigned long currentMillis = 0;
unsigned long interval = 120000;

void setup()
{
  Serial.begin(115200);

  // Startup BMP180
  pressure.begin();
  //------------------------------

  // Start up the Dallas library
  sensors.begin();
  //-----------------------------

  client.setServer(server, 1883);
  client.setCallback(callback);

  Ethernet.begin(mac, ip);
  // Allow the hardware to sort itself out
  delay(1500);
}

void loop()
{
  if (!client.connected()) {
    reconnect();
  }
  client.loop();

  currentMillis = millis();

  if (currentMillis - previousMillis >= interval) {

    DHT_shout();

    ds18b20_reading();

    bmp180();

    previousMillis = currentMillis;
  }
}

void DHT_reading()
{
  // READ DATA
  Serial.println();
  int chk = DHT.read22(DHT22_PIN);


}

void DHT_shout()
{
  DHT22_PIN = 5;
  DHT_reading();

  // DISPLAY DATA
  Serial.println("DHT Sensors");

  Serial.print("Sensor 0: h:");
  Serial.print(DHT.humidity, 1);
  Serial.print(" t:");
  Serial.println(DHT.temperature, 1);

  stringconv (DHT.humidity);
  client.publish("dht_hum_0", message_buff);
  stringconv (DHT.temperature);
  client.publish("dht_temp_0", message_buff);

  delay (5);

  DHT22_PIN = 6;
  DHT_reading();

  // DISPLAY DATA
  Serial.print("Sensor 1: h:");
  Serial.print(DHT.humidity, 1);
  Serial.print(" t:");
  Serial.println(DHT.temperature, 1);

  stringconv (DHT.humidity);
  client.publish("dht_hum_1", message_buff);
  stringconv (DHT.temperature);
  client.publish("dht_temp_1", message_buff);
  Serial.println();


  DHT22_PIN = 5;


}

void ds18b20_reading()
{
  // call sensors.requestTemperatures() to issue a global temperature
  // request to all devices on the bus
  Serial.print("Requesting DS18B20 temperatures...");
  sensors.requestTemperatures(); // Send the command to get temperatures
  Serial.println("DONE");

  ds18b20_shout();
}

void ds18b20_shout()
{
  Serial.print("Temperature for the device 1 (index 0) is: ");
  Serial.println(sensors.getTempCByIndex(0));
  stringconv (sensors.getTempCByIndex(0));
  client.publish("temp_index_0", message_buff);
  Serial.print("Temperature for the device 2 (index 1) is: ");
  Serial.println(sensors.getTempCByIndex(1));
  stringconv (sensors.getTempCByIndex(1));
  client.publish("temp_index_1", message_buff);


  Serial.print("Temperature for the device 3 (index 2) is: ");
  Serial.println(sensors.getTempCByIndex(2));
  stringconv (sensors.getTempCByIndex(2));
  client.publish("temp_index_2", message_buff);
  /*
  Serial.print("Temperature for the device 4 (index 3) is: ");
  Serial.println(sensors.getTempCByIndex(3));
  stringconv (sensors.getTempCByIndex(3));
  client.publish("temp_index_3", message_buff);
  */
}


void callback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Message arrived [");
  Serial.print(topic);
  Serial.print("] ");
  for (int i = 0; i < length; i++) {
    Serial.print((char)payload[i]);
  }
}

void reconnect()
{
  // Loop until we're reconnected
  while (!client.connected()) {

    long now = millis();
    if (now - lastReconnectAttempt > 5000) {
      lastReconnectAttempt = now;
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      // Wait 5 seconds before retrying
      // Attempt to connect
      if (client.connect("arduinoClient")) {
        Serial.println("connected");
        lastReconnectAttempt = 0;
        // ... and resubscribe
        client.subscribe("inTopic");
      }
    }
  }
}

void stringconv (float stc)
{
  String pubString = String (stc);
  pubString.toCharArray(message_buff, pubString.length() + 1);
  Serial.print("stringumwandlung: ");
  Serial.println(pubString);
}

weiter im nächsten post..

Mach mal überall wo du String Literale mit print()/println() verwendest ein F() um den String:

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

Dann belegt der String kein RAM mehr

Die stringconv() Funktion ist ein Irrweg. Die String Klasse verschwendet nur Speicher. print()/println() kann direkt Floats ausgeben. Und um Floats in C Strings zu wandeln gibt es dtostrf():
https://www.mikrocontroller.net/topic/86391
http://www.nongnu.org/avr-libc/user-manual/group__avr__stdlib.html#ga060c998e77fb5fc0d3168b3ce8771d42

Bei client.publish() kannst du für den ersten Parameter das machen:

char StringBuffer[20];
#define P(str) strcpy_P(stringBuffer, PSTR(str))

P() kann man dann wie F() verwenden, aber nicht nur für print(). Das belegt einmal RAM für den Puffer, aber all Literale verwenden den gleichen Puffer. Das Makro kopiert den String aus dem Flash in den Puffer und übergibt einen Zeiger auf den Puffer an die Funktion.

das wollte ich noch hinzufügen...

void bmp180()
{
  char status;
  double T, P, p0, a;

  // Loop here getting pressure readings every 10 seconds.

  // If you want sea-level-compensated pressure, as used in weather reports,
  // you will need to know the altitude at which your measurements are taken.
  // We're using a constant called ALTITUDE in this sketch:

  Serial.println();
  Serial.print("provided altitude: ");
  Serial.print(ALTITUDE, 0);
  Serial.println(" meters, ");

  // If you want to measure altitude, and not pressure, you will instead need
  // to provide a known baseline pressure. This is shown at the end of the sketch.

  // You must first get a temperature measurement to perform a pressure reading.

  // Start a temperature measurement:
  // If request is successful, the number of ms to wait is returned.
  // If request is unsuccessful, 0 is returned.

  status = pressure.startTemperature();
  if (status != 0)
  {
    // Wait for the measurement to complete:
    delay(status);

    // Retrieve the completed temperature measurement:
    // Note that the measurement is stored in the variable T.
    // Function returns 1 if successful, 0 if failure.

    status = pressure.getTemperature(T);
    if (status != 0)
    {
      // Print out the measurement:
      Serial.print("temperature: ");
      Serial.print(T, 2);
      Serial.println(" deg C, ");
      stringconv (T);
      client.publish("bmp180_temperature", message_buff);

      // Start a pressure measurement:
      // The parameter is the oversampling setting, from 0 to 3 (highest res, longest wait).
      // If request is successful, the number of ms to wait is returned.
      // If request is unsuccessful, 0 is returned.

      status = pressure.startPressure(3);
      if (status != 0)
      {
        // Wait for the measurement to complete:
        delay(status);

        // Retrieve the completed pressure measurement:
        // Note that the measurement is stored in the variable P.
        // Note also that the function requires the previous temperature measurement (T).
        // (If temperature is stable, you can do one temperature measurement for a number of pressure measurements.)
        // Function returns 1 if successful, 0 if failure.

        status = pressure.getPressure(P, T);
        if (status != 0)
        {
          // Print out the measurement:
          Serial.print("absolute pressure: ");
          Serial.print(P, 2);
          Serial.println(" mb, ");

          // The pressure sensor returns abolute pressure, which varies with altitude.
          // To remove the effects of altitude, use the sealevel function and your current altitude.
          // This number is commonly used in weather reports.
          // Parameters: P = absolute pressure in mb, ALTITUDE = current altitude in m.
          // Result: p0 = sea-level compensated pressure in mb

          p0 = pressure.sealevel(P, ALTITUDE); // we're at 221 meters (Boulder, CO)
          Serial.print("relative (sea-level) pressure: ");
          Serial.print(p0, 2);
          Serial.println(" mb, ");
          stringconv (p0);
          client.publish("bmp180_pressure", message_buff);

          // On the other hand, if you want to determine your altitude from the pressure reading,
          // use the altitude function along with a baseline pressure (sea-level or other).
          // Parameters: P = absolute pressure in mb, p0 = baseline pressure in mb.
          // Result: a = altitude in m.

          a = pressure.altitude(P, p0);
          Serial.print("computed altitude: ");
          Serial.print(a, 0);
          Serial.println(" meters, ");

        }
        else Serial.println("error retrieving pressure measurement\n");
      }
      else Serial.println("error starting pressure measurement\n");
    }
    else Serial.println("error retrieving temperature measurement\n");
  }
  else Serial.println("error starting temperature measurement\n");
}

im Grunde hab ich mich nur an den Beispiel Codes für die einzelnen Sensoren orientiert und diese geringfügig angepasst. Während ich die Sensorwerte auslese übergebe ich den Wert an die Funktion stringconv, die mir einen String daraus bastelt und ich ihn so mit client.publish("namedessensors", message_buff); in mein Lan schicken kann.
Das funktioniert erstmal auch soweit, je öfter ich jetzt aber die Funktion stringconv zusammen mit dem Aufruf client.publish einbaue bläßt es mir den Speicher unheimlich auf. So kann ich z.B keine weitere Sensoren am 1-Wire einbauen.
Da ich Anfänger bin und ich mir den Code weitgehend aus dem Netz zusammengeklickt habe denke ich das ich da was grundsätzlich falsch mache. Hat jemand einen Tipp für mich oder kann mir weiterhelfen?

Die String Klasse ist Schrott. Braucht dynamischen Speicher und viel Flash.

Ich habe mein Post nochmal editiert. Am besten machst du die Konvertierung mit dtostrf(). Das arbeitet direkt mit C Strings nur statischem Speicher.

Bei den ganzen Serial Ausgaben musst du eben wissen dass String Literale auf dem AVR durch die Harvard Architektur alle im RAM landen. Deshalb am einfachsten bei print() das F() Makro verwenden. Bei andern Funktion muss man etwas ausholen wenn man RAM sparen will.

Vielen Dank für deine schnelle Anwort, das mit dem F() MAkro hab ich ja schnell verstanden, das ist gut zu wissen. Macht mir den RAM frei, funktioniert auch aber es mangelt ja auch schon an Programmspeicherplatz.

Das mit dem P() Makro habe ich versucht einzubauen,

hier

PubSubClient client(ethClient);
char message_buff[100];

char StringBuffer[20];
#define P(str) strcpy_P(stringBuffer, PSTR(str))[/b]

und z.B. in der Funktion DHT_shout hier:

void DHT_shout()
{
  ...  

  client.publish(P("dht_hum_0", message_buff));
 
 ...

was aber leider nicht funktioniert.
Ich glaube ich habe das nicht richtig verstanden soll ich jetzt auf dtostrf() umsteigen oder nicht?

Natürlich geht das nicht. Wie auch? P() ist für ein einziges String Literal.

So (für eine Nachkommastelle):

client.publish(P("dht_hum_0"), dtostrf(val, 1, 1, message_buff));

Das P() ist auch nur damit der String nicht im RAM landet. Es geht auch ohne

Du kannst auch dtostrf() nochmal in eine Funktion packen die einen char* zurück gibt:

char* convert(float val)
{
   return dtostrf(val, 1, 1, message_buff)
}

Dann muss man das nicht jedesmal ausschreiben. Ist aber Geschmackssache

Oder du machst es am besten gleich so:

void publish(const char* str, float val)
{
  char buffer[15];   //Puffer für Float Konvertierung
  strncpy_P(message_buff, str, sizeof(message_buff));  //String aus Flash ins RAM kopieren
  client.publish(message_buff, dtostrf(val, 1, 1, buffer));
}

Oder ganz ausführlich:

void publish(const char* str, float val)
{
  char buffer[15]; 
  strncpy_P(message_buff, str, sizeof(message_buff));
  dtostrf(val, 1, 1, buffer)
  client.publish(message_buff, buffer);
}

Und rufst das so auf:

publish(PSTR("Title"), 1.0);

PSTR() = Progmem String. Definiert einen C String im Flash. Das kannst du aber nur hier verwenden! Wenn du das bei einer Funktion verwendest du einen String im RAM erwartet (was bei den aller meisten der Fall ist), kompiliert das zwar, aber läuft nicht richtig!

buffer und message_buffer habe ich mal anders herum gemacht. Dann wird für den festen String der große Puffer verwendet und für die Zahl nur ein kleiner Puffer. Das ist sinnvoller.

Also ich habe im Code nun, wie von Dir netterweise Vorgeschlagen, meine Funktion stringconv durch deine:

void publish(const char* str, float val)
{
  char buffer[15];
  char message_buff[100];
  strncpy_P(message_buff, str, sizeof(message_buff));
  dtostrf(val, 1, 1, buffer);
  client.publish(message_buff, buffer);
}

ersetzt. Zusätlich habe ich char message_buff[100]; eingefügt, weil es vorher global deklariert war.

Die Aufrufe mache ich z.B. mit:

publish(PSTR("dht_hum_0"), DHT.humidity);

für das "mqtt broker".

So müsste es doch richtig sein? Es scheint bisher gut zu laufen.
Speicher habe ich auch viel gespart vorher 94% Programmspeicher und 85% dynamischer Speicher, jetzt 88% Programmspeicher und 73% dynamischer Speicher. Es kommt jetzt auch keine Speicherwarnung mehr.
Sollte ich die F Makros noch einbauen?

Kannst du bei so vielen Literalen wie möglich versuchen.

Sei dir aber bewusst, dass die Verwendung von F() etwas zusätzlichen Code erzeugt,
also den Programmspeicher doppelt belastet.

Ja, wobei 100 Byte da etwas viel sind. Nimm in etwas so viel wie der Titel-String maximal lang ist. 20-30 Bytes sollten eigentlich reichen, oder?

Und mit dem F() Makro bei print()/println() sparst du natürlich noch mehr RAM. Wenn man nichts macht landen wirklich alle Strings im RAM. Und das sind 1 Byte pro Zeichen! Wenn man da Romane auf Serial schreibt sind das schnell ein paar hundert Bytes. Das geht aber eben nur bei print()/println() (aber bei mehreren Klassen wie Serial, SD oder Ethernet). Deshalb muss man es bei publish() per Hand machen.

Dass du insgesamt nicht mehr so viel Flash (d.h. Programmspeicher) übrig hast ist aber normal bei so vielen Libraries.

ja da hast du recht ich habe message_buff entsprechend angepasst.
Jetzt habe ich eine saubere Lösung mit der ich noch etwas erweitern kann und einiges gelernt noch dazu :slight_smile:
Vielen Dank

edit: durch einsetzten des F() Makros konnte ich den Speicherbedarf auf
89% Programmspeicher und 46% dynamischer Speicher
nochmals erheblich verbessern.