Memory Problem with Nano

Hi

I use this Sketch for my Weatherstation. It works so far but when I plug in the Ethernet and the associated Function runs I run out of Memory (the Display gets white and flickers).
93% for the Program and 73% for the Memory. I use a Nano.

Can someone please take a look at the Sketch ? I'm sure there are some Pieces to save some Memory but I'm not that advanced. Please help.

#include <Wire.h>
#include <DS3232.h>
DS3232 clock;
const unsigned int DISPLAY_TIMEOUT = 8000;
bool displayIsOn;
unsigned long displayTimeout;
#define DS3231_I2C_ADDRESS 0x68
float temp3231;

#include <SoftwareSerial.h>
SoftwareSerial BTserial(2, 3);
// BTconnected will = false when not connected and true when connected
boolean BTconnected = false;
// connect the STATE pin to Arduino pin D4
const byte BTpin = 4;

#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
// BLK an 3,3V VCC
#define TFT_CLK 13
#define TFT_MISO 12
#define TFT_MOSI 11
#define TFT_DC 5
#define TFT_CS -1  //erforderlich!
#define TFT_RST 8
#define TFT_BLK 9
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);

#include <TimeLib.h>
#include <EtherCard.h>
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };
// NTP Server
const char timeServer[] PROGMEM = "pool.ntp.org";
const int utcOffset = 1;  // Central European Time
// Packet buffer, must be big enough to packet and payload
#define BUFFER_SIZE 550
byte Ethernet::buffer[BUFFER_SIZE];
const unsigned int remotePort = 123;
boolean IPconnected = true;
time_t prevDisplay = 0;  // when the digital clock was displayed
boolean timeset = false;

byte trigger = 7;     //Trigger-Pin des Ultraschallsensors an Pin7 des Arduino-Boards
byte echo = 6;        // Echo-Pim des Ultraschallsensors an Pin6 des Arduino-Boards
long dauer = 0;       // Das Wort dauer ist jetzt eine Variable, unter der die Zeit gespeichert wird, die eine Schallwelle bis zur Reflektion und zurück benötigt. Startwert ist hier 0.
long entfernung = 0;  // Das Wort „entfernung“ ist jetzt die variable, unter der die berechnete Entfernung gespeichert wird. Info: Anstelle von „int“ steht hier vor den beiden Variablen „long“. Das hat den Vorteil, dass eine größere Zahl gespeichert werden kann. Nachteil: Die Variable benötigt mehr Platz im Speicher.
byte zl1 = 0;
byte zl2 = 31;
byte zl3 = 62;
byte zl4 = 93;
byte zl5 = 124;
byte zl6 = 155;
byte zl7 = 186;
byte zl8 = 217;
char buffer[9];

void setup() {
  Wire.begin();
  clock.begin();
  //Serial.begin(9600);
  tft.begin();
  tft.setRotation(1);
  tft.fillScreen(ILI9341_BLACK);
  tft.setTextSize(3);  //TextSize(3) is 18 pixels wide by 24 pixels high.
  tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
  tft.setCursor(0, zl3);
  tft.print(F("Balkon:"));
  tft.setCursor(0, zl5);
  tft.print(F("Box:"));
  tft.setCursor(0, zl6);
  tft.print(F("IP:"));
  analogWrite(TFT_BLK, 200);
  displayIsOn = true;

  tft.setTextColor(ILI9341_RED, ILI9341_BLACK);
  tft.setCursor(72, zl6);
  tft.print(F("Warte auf IP"));
  if (ether.begin(BUFFER_SIZE, mac) == 0) {
    // no point in carrying on, so do nothing forevermore:
    IPconnected = false;
  }
  if (!ether.dhcpSetup()) {
    // no point in carrying on, so do nothing forevermore:
    IPconnected = false;
  }
  if (IPconnected == true) {
    char resultip[16] = {};
    sprintf_P(resultip, PSTR("%d:%d:%d:%d"), ether.myip[0], ether.myip[1], ether.myip[2], ether.myip[3]);
    tft.setTextColor(ILI9341_DARKCYAN, ILI9341_BLACK);
    tft.setCursor(72, zl6);
    tft.print(resultip);
  } else {
    tft.setTextColor(ILI9341_RED, ILI9341_BLACK);
    tft.setCursor(72, zl6);
    tft.print("Keine IP      ");
  }

  BTserial.begin(38400);
  pinMode(trigger, OUTPUT);  // Trigger-Pin ist ein Ausgang
  pinMode(echo, INPUT);      // Echo-Pin ist ein Eingang
}

void loop() {
  digitalWrite(trigger, LOW);
  delay(5);
  digitalWrite(trigger, HIGH);         //Jetzt sendet man eine Ultraschallwelle los.
  delay(10);                           //Dieser „Ton“ erklingt für 10 Millisekunden.
  digitalWrite(trigger, LOW);          //Dann wird der „Ton“ abgeschaltet.
  dauer = pulseIn(echo, HIGH);         //Mit dem Befehl „pulseIn“ zählt der Mikrokontroller die Zeit in Mikrosekunden, bis der Schall zum Ultraschallsensor zurückkehrt.
  entfernung = (dauer / 2) * 0.03432;  //Nun berechnet man die Entfernung in Zentimetern. Man teilt zunächst die Zeit durch zwei (Weil man ja nur eine Strecke berechnen möchte und nicht die Strecke hin- und zurück). Den Wert multipliziert man mit der Schallgeschwindigkeit in der Einheit Zentimeter/Mikrosekunde und erhält dann den Wert in Zentimetern.

  displayBacklight();
  tft.setTextColor(ILI9341_PINK, ILI9341_BLACK);
  clock.read();
  sprintf(buffer, "%02u.%02u.%02u", clock.day(), clock.month(), clock.year());
  tft.setCursor(0, zl1);
  tft.print(buffer);
  tft.print(" ");
  sprintf(buffer, "%02u:%02u:%02u", clock.hours(), clock.minutes(), clock.seconds());
  tft.print(buffer);
  tft.setCursor(0, zl2);
  switch (clock.weekDay()) {
    case 1: tft.print(F("Montag     ")); break;
    case 2: tft.print(F("Dienstag   ")); break;
    case 3: tft.print(F("Mittwoch   ")); break;
    case 4: tft.print(F("Donnerstag ")); break;
    case 5: tft.print(F("Freitag    ")); break;
    case 6: tft.print(F("Samstag    ")); break;
    case 7: tft.print(F("Sonntag    ")); break;
    default: tft.print(F("Wochentag ?"));
  }
  tft.setTextColor(ILI9341_CYAN, ILI9341_BLACK);
  tft.setCursor(90, zl5);
  tft.print(getDS3231temp());
  tft.print("C  ");

  if ((IPconnected == true) && (clock.weekDay() == 1) && (timeset == false)) {
    setSyncProvider(getDstCorrectedTime);  // Use this for local, DST-corrected time
    clock.setSeconds(second());
    clock.setMinutes(minute());
    clock.setHours(hour());
    clock.setDay(day());
    clock.setMonth(month());
    clock.setYear(year() - 2000);
    //Serial.println(year() - 2000);
    clock.write();
    timeset = true;
    digitalClockDisplay();
  }
  if (clock.weekDay() != 1) { timeset = false; }

  if (digitalRead(BTpin) == HIGH) {
    if (BTserial.available() > 0) {
      delay(100);
      String readString = BTserial.readString();
      int firstClosingBracket = readString.indexOf('<');
      int secondClosingBracket = readString.indexOf('>', firstClosingBracket + 1);
      String readString1 = readString.substring(firstClosingBracket + 1, secondClosingBracket);
      if (readString1.length() > 17) { readString1.remove(17); }
      tft.setTextColor(ILI9341_GREEN, ILI9341_BLACK);
      tft.setCursor(0, zl4);
      tft.print(readString1);
      readString = "";
      readString1 = "";
    }
  } else {
    tft.setTextColor(ILI9341_RED, ILI9341_BLACK);
    tft.setCursor(0, zl4);
    tft.print("Kein Signal      ");
    tft.setCursor(120, zl7);
  }
}
void displayBacklight() {
  if (displayIsOn && (millis() - displayTimeout <= DISPLAY_TIMEOUT)) {
    tft.setTextColor(ILI9341_ORANGE, ILI9341_BLACK);
    tft.setCursor(120, zl7);
    unsigned long count1 = ((millis() - displayTimeout) / 1000) + 1;
    unsigned long count2 = (DISPLAY_TIMEOUT / 1000) + 1;
    tft.print(String(abs(int(count1) - int(count2))) + " Sek.");
  }
  if (entfernung > 0 && entfernung <= 30) {
    displayTimeout = millis();
    if (!displayIsOn) {
      displayIsOn = true;
      analogWrite(TFT_BLK, 200);
    }
    return;
  }
  if (displayIsOn && (millis() - displayTimeout > DISPLAY_TIMEOUT)) {
    displayIsOn = false;
    tft.setCursor(120, zl7);
    tft.print("       ");
    analogWrite(TFT_BLK, 0);
  }
}
void digitalClockDisplay() {
  tft.setTextColor(ILI9341_LIGHTGREY, ILI9341_BLACK);
  //tft.setCursor(0, zl8);
  sprintf(buffer, "%02u.%02u.%02u", day(), month(), year() - 2000);
  tft.setCursor(0, zl8);
  tft.print(buffer);
  tft.print(" ");
  sprintf(buffer, "%02u:%02u:%02u", hour(), minute(), second());
  tft.print(buffer);
}



/*-------- NTP code ----------*/

// SyncProvider that returns UTC time
time_t getNtpTime() {
  // Send request
  tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
  tft.setCursor(0, zl8);
  tft.print("NTP Request");
  if (!ether.dnsLookup(timeServer)) {
    tft.print(" failed");
    return 0;  // return 0 if unable to get the time
  } else {
    ether.ntpRequest(ether.hisip, remotePort);

    // Wait for reply
    uint32_t beginWait = millis();
    while (millis() - beginWait < 1500) {
      word len = ether.packetReceive();
      ether.packetLoop(len);

      unsigned long secsSince1900 = 0L;
      if (len > 0 && ether.ntpProcessAnswer(&secsSince1900, remotePort)) {
        tft.setCursor(0, zl8);
        tft.print("Receive NTP");
        return secsSince1900 - 2208988800UL;
      }
    }
    tft.setCursor(0, zl8);
    tft.print("No NTP Response");
    return 0;
  }
}

/* Alternative SyncProvider that automatically handles Daylight Saving Time (DST) periods,
 * at least in Europe, see below.
 */
time_t getDstCorrectedTime(void) {
  time_t t = getNtpTime();
  if (t > 0) {
    TimeElements tm;
    breakTime(t, tm);
    t += (utcOffset + dstOffset(tm.Day, tm.Month, tm.Year + 1970, tm.Hour)) * SECS_PER_HOUR;
  }
  return t;
}

/* This function returns the DST offset for the current UTC time.
 * This is valid for the EU, for other places see
 * http://www.webexhibits.org/daylightsaving/i.html
 * 
 * Results have been checked for 2012-2030 (but should work since
 * 1996 to 2099) against the following references:
 * - http://www.uniquevisitor.it/magazine/ora-legale-italia.php
 * - http://www.calendario-365.it/ora-legale-orario-invernale.html
 */
byte dstOffset(byte d, byte m, unsigned int y, byte h) {
  // Day in March that DST starts on, at 1 am
  byte dstOn = (31 - (5 * y / 4 + 4) % 7);

  // Day in October that DST ends  on, at 2 am
  byte dstOff = (31 - (5 * y / 4 + 1) % 7);

  if ((m > 3 && m < 10) || (m == 3 && (d > dstOn || (d == dstOn && h >= 1))) || (m == 10 && (d < dstOff || (d == dstOff && h <= 1))))
    return 1;
  else
    return 0;
}
float getDS3231temp() {
  float temp3231 = 0;
  byte tMSB, tLSB;
  //temp registers (11h-12h) get updated automatically every 64s
  Wire.beginTransmission(DS3231_I2C_ADDRESS);
  Wire.write(0x11);
  Wire.endTransmission();
  Wire.requestFrom(DS3231_I2C_ADDRESS, 2);
  if (Wire.available()) {
    tMSB = Wire.read();  //2's complement int portion
    tLSB = Wire.read();  //fraction portion

    temp3231 = (tMSB & B01111111);     //do 2's math on Tmsb
    temp3231 += ((tLSB >> 6) * 0.25);  //only care about bits 7 & 8
  } else {
    //oh noes, no data!
  }
  return temp3231;
}

that's more than one forth of the memory...

At some point it's better to get a more capable arduino...

are you using an ENC28J60?
going for a Wiznet W5500 could probably help

PS: try to reduce it to 300 and see if things keep working.... in theory it should go above 1500..

1 Like

Hi

Yes, I use the ENC28J60. I looked at a Comparison but I'm not sure about the Advantages of the W5500 (it's newer).
And yes, a better Arduino would help but the Project is already soldered.

I will try 300.

the W5500 reduces the burden on the Arduino's CPU and memory thanks to its integrated TCP/IP stack

Ok. Thank you for this Info.
I reduced the Buffer Size to 340 and Memory is from 73% down to 65%. I tried lower Values but that didn't work.

Does it work with 340?

1 Like

Yes, it does. But it is only running for 30 Minutes or so with the 340. We will see.

Use of String objects leads to memory problems and program crashes on small MCUs. If you want your program to run reliably, replace all these String operations with C-strings and operations e.g. strtok(), strcat(), strcpy() etc. or better, the corresponding n versions strncat(), ....

2 Likes

Also, the sketch does not use Strings very much. Just for that BTSerial manipulation. So getting rid of Strings will also get rid of about 1 kBytes of malloc() and associated code.

A bitmapped display and an Ethernet is a lot to ask of a tiny 32k/2k processor...

Flash not RAM (String does not malloc 1K)

Yes. OP was at 93% flash usage as well.

yes - I was just unclear about the malloc() remark.

I tried to get rid of the String and use char instead. But I'm confused how to do this whole part with char:

      String readString = BTserial.readString();
      int firstClosingBracket = readString.indexOf('<');
      int secondClosingBracket = readString.indexOf('>', firstClosingBracket + 1);
      String readString1 = readString.substring(firstClosingBracket + 1, secondClosingBracket);
      if (readString1.length() > 17) { readString1.remove(17); }

instead of

you should study Serial Input Basics to receive the message (seems you have < and > as start and end markers)

what does the message look like?

It looks like this:

<  5.2C  56%  4.97>

I need to get rid of the < and >. As a String it's easy but I can't find a good way for the char. I'm heavily overthinking this ...

+1 for Serial Input Basics.

you missed the F-Makro several times. This are just some lines

tft.print("Kein Signal ");
tft.print(" ");

this doesn't need a 2 byte int. A uint8_t should be large enough

const int utcOffset = 1;

1 Like

try something like this

char buffer[80] = "<  5.2C  56%  4.97>"; // simulate what you received as a char buffer

bool extractValues(char * message, double& temperature, long& humidity, double& somethingElse) {
  char* endPtr;

  char * startMarkPtr = strchr(message, '<'); // search for the first occurence of <
  char * endMarkPtr = strrchr(message, '>'); // search for the last occurence of >
  // does the message seem correct?
  if (startMarkPtr == nullptr || endMarkPtr == nullptr || endMarkPtr <= startMarkPtr ) return false;
  Serial.println(F("message looks correct"));

  // try reading the temperature as a floating point number after the start mark
  char * startOfParsingPtr = startMarkPtr + 1; // we start parsing after the >
  temperature = strtod(startOfParsingPtr, &endPtr); // https://cplusplus.com/reference/cstdlib/strtod/
  if (endPtr == startOfParsingPtr) return false; // the parse failed
  Serial.println(F("temperature extracted"));

  // try reading the humidity
  startOfParsingPtr = endPtr+1; // we are after the temperature, there is a C to skip (space is not an issue)
  humidity = strtol(startOfParsingPtr, &endPtr, 10); // https://cplusplus.com/reference/cstdlib/strtol/
  if (endPtr == startOfParsingPtr) return false; // the parse failed
  Serial.println(F("humidity extracted"));

  // try reading the last field as a floating point number after the start mark
  startOfParsingPtr = endPtr+1; // we are after the humidity, there is a % to skip (space is not an issue)
  somethingElse = strtod(startOfParsingPtr, &endPtr);
  if (endPtr == startOfParsingPtr) return false; // the parse failed
  Serial.println(F("somethingElse extracted"));

  return true;
}

void setup() {
  Serial.begin(115200);
  double temp, sthg;
  long hum;

  if (extractValues(buffer, temp, hum, sthg)) {
    Serial.print(F("Temperature = "));  Serial.println(temp, 3);
    Serial.print(F("Humidity = "));     Serial.print(hum); Serial.println(F("%"));
    Serial.print(F("Something Else = "));  Serial.println(sthg, 5);
  } else {
    Serial.println(F("Error parsing message"));
  }
}

void loop() {}

Hopefully the code is verbose enough for you to see what's going on.

Extracting a floating point value is done using strtod() and extracting a integral value (long) is done with strtol()

Those functions take a pointer to pointer argument that let you know where the parsing stopped extracting the value. So if the pointer did not move from the start place, it means nothing relevant could be read and that whatever is in the buffer at that place does not work.

side note:

Ideally you would check for a trailing null char before doing

startOfParsingPtr = startMarkPtr + 1;

I don't check if the endPtr would have reached the end of the string (would be needed before skipping the C and %) because I know there is a '>' somewhere further so at worst I hit that character when parsing and adding one would place me on the trailing null char.

If you also wanted to be very sure about the message structure, you would test to see if 'C' and '%' were present in the parsing.


This might feel a bit involved but understanding how strod() and strol() work (and related functions) is not rocket science and that will save you ~1KB of flash memory that is used by the String class's underlying functions (if you get rid of all the Strings).

if you were on a 32 bit MCU like an ESP32, you'd have more flash memory and you could use sscacnf() to extract the fields (on your Nano sscanf can't extract the floating point value that's why we use strod()

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

  char buffer[80] = "<  5.2C  56%  4.97>"; // simulate what you received as a char buffer
  double temp, sthg;
  long hum;
  if (sscanf(buffer, "<%lfC %d%\% %lf>", &temp, &hum, & sthg) == 3) { // if we extracted 3 values according to the format
    Serial.print(F("Temperature = "));  Serial.println(temp, 3);
    Serial.print(F("Humidity = "));     Serial.print(hum); Serial.println(F("%"));
    Serial.print(F("Something Else = "));  Serial.println(sthg, 5);
  } else {
    Serial.println(F("Error parsing message"));
  }
}

void loop() {}

of course sscanf() is using up more flash (similar to what you'd get with the String class)

1 Like

Hi Guys
Thank you very, very much for your help. I'm now down to 61% Memory. So far no more Issues (even with the one remaining String). :+1:

J-M-L
To be honest: I don't fully understand your Code but it works standalone. I've implemented it into my Sketch but it's too big for the Nano (101%). But I definitely keep it to study it closer and maybe could use it for future projects. Thank you for that !

it should be smaller than using String

this of course is not needed

that should be the incoming buffer you built