Mega stalling when attempting to write to LCD

MEGA 2560 R3 clone. I'm a retired longtime programmer and new Arduino and C++ user. I'm reading 3 UART serial data streams at 9600 baud and displaying related data on a 40x2 character LCD screen. Using I2C as a slave, getting data via I2C from a Nano with 2 IC2 devices - a clock and an Altimeter. Local LCD display is also I2C: making 4 devices on that bus.

Problem is that everything works fine for some period of time - not repeatable, but between 5 and 20 minutes. While attempting to write to the LCD (at "HERE2" or "HERE3" in the code), the whole thing stops running. Screen and serial moniter stop. Sometimes, I also get spurious characters on the display.

I had an earlier version of this code that used Strings, and I've eliminated those. Eliminated "Delay" verb and do millis-based timeouts. UART data is not always there, one or more of the devices may not be sending. I've got millis based timeout loops for those. I am flushing all of the the Serial buffers throughout the logic.

Appreciate your help. Steve

I did have code here, but it is too big and I am attaching it instead.

Arduino_Mega_Ass_I2C06_empty_buff02.ino (16.8 KB)

Is the altimeter also a 5V device ?

I wrote this a week ago: How to make a reliable I2C bus · Koepel/How-to-use-the-Arduino-Wire-library Wiki · GitHub.
Can you tell us about the GND, the wiring, a cable maybe, pullup resistors ?

There are low level timeouts for the I2C bus, to be able to recover from a bad I2C bus that halts the sketch.
It is still undocumented and not turned on by default: ArduinoCore-avr/Wire.cpp at master · arduino/ArduinoCore-avr · GitHub.
You should not fully rely on the timeout, you should also fix the I2C bus (if that is the problem).

The Arduino Nano is the Master and the Arduino Mega is the Slave.
However, when the Slave turns into a Master for the I2C display, then you have two Masters and there might be a collision.
Can you move the display to the (real) Master ? Or make a software I2C bus for the display ?

I prefer that you use the I2C with fixed length of data packages. At this moment it is too flexible, you don't know how much valid data there is.

I'm sorry, but I don't see any software-millis-timer.
What I see is not the same as the Blink Without Delay: https://www.arduino.cc/en/Tutorial/BuiltInExamples/BlinkWithoutDelay.
The timeout in GetFrige(), GetTM() and GetSC() is okay.

There is something I have to tell you :cry: Take a deep breath and don't fall off your chair, but Arduino has redefined the round() function.
The perfectly fine round() in the library is replaced by something stupid.
See the issue here: round() macro in Arduino.h · Issue #76 · arduino/ArduinoCore-API · GitHub.
They are afraid to make it right, because the wrong round() could be used in a wrong way to make a library work.
I hope you are not a Java programmer, because I think that the round() function in Java was a mistake when they made that.

Luckily, you use round() for TimerM, which is not okay anyway. TimerM will go wrong after 50 days.

In the Arduino world we deal with incoming serial data in a different way.
When there is something available, then read it and add it to a buffer and immediate return to the loop(). When the full data was received, then it should be processed of course. We never wait for something. Keep the loop() running as fast as possible, to make the sketch responsive.
You are now stuck in a while-loop, wasting precious time and I think that you can not afford that.

When the Serial output to the serial monitor is too much, then the TX buffer gets full. It is 64 bytes and when it is full, then the Serial.print() will wait until there is a free space in the buffer. That waiting can make a sketch hundred times slower.
The baudrate is only 9600 baud and there is nothing that limits the amount of data send with Serial.print() in the loop().
I suggest to make a software-millis-timer to update the LCD display four times per second and another software-millis-timer for output to the serial monitor twice per second or so.

Thanks, once again, for the detailed response!

The altitude sensor is from Adafruit, and has a 3-5v vin. Real Time Clock is the same.

Before I get started, do you think I'd be better off using a software serial connection between the two Arduinos - rather than I2C? Seems like you're leaning towards I2C being the problem. Maybe 3 I2C busses? one between the to Arduinos, one for the Nano sensors, and one for the LCD screen? Or...?

Ground is shared with everything - all processors, I2C and Serial devices share a single ground.

No pullup resistors on the I2C - didn't know it needed them. I looked at your referenced posting about pullups - I can PROBABLY make sense of that tomorrow morning when my brain is fresh(er). Longest I2C cable is to the LCD - 25CM. Sensors are about 15cm. Inter-Arduino cable is short. I do have the I2C cables together here and there - I'll isolate them a bit.

TimerM is just there as a proof of life. This device is going in a car and will only be running while driving - 50 days on the timer is A-OK.

I could move the LCD display over to the Nano if that helps with the master/slave thing.

Serial Monitir is only used for troubleshooting - I'll minimize use of that.

By "I prefer that you use the I2C with fixed length of data packages", do you mean the communication between Arduinos? I can pad the fields out to make a fixed length.

I'm still chewing on what you sent me - I'll have more questions I am sure.

Thanks again - Steve.

This is the code on the Nano, in case there are any gotchas there:

// Blue Altitude and clock data provider v I2C
//
//-------  I2C master

#include <DS3231.h> // Real Time Clock
#include <Wire.h>
#include <SPI.h>
#include <Adafruit_Sensor.h>
#include "Adafruit_BMP3XX.h"  //Altitude board

#define PAYLOAD_SIZE 19
#define ANSWERSIZE 19
#define SEALEVELPRESSURE_HPA (1013.25)

Adafruit_BMP3XX bmp;
char Altx [7] = "99999";
char CurAx[5] = "CURA";
char CurBx[5] = "CURB";

DS3231 Clock;

bool h12;
bool PM;
char DateC[10];

void setup() {
  Serial.begin(9600);
  while (!Serial);
  Serial.println("Adafruit BMP388 / BMP390 test");

  if (!bmp.begin()) {   // hardware I2C mode, can pass in address & alt Wire
    Serial.println("Could not find a valid BMP3 sensor, check wiring!");
    while (1);
  }

  Wire.begin(); //I2C MASTER

  // Set up oversampling and filter initialization
  bmp.setTemperatureOversampling(BMP3_OVERSAMPLING_8X);
  bmp.setPressureOversampling(BMP3_OVERSAMPLING_4X);
  bmp.setIIRFilterCoeff(BMP3_IIR_FILTER_COEFF_3);
  bmp.setOutputDataRate(BMP3_ODR_50_HZ);
}

void loop() {
  unsigned long TimerMax;
  unsigned long TimerPrev = millis();
  unsigned long TimerCur;

  Serial.println();
  Serial.println("*****LoopStart");

  if (! bmp.performReading()) {
    Serial.println("Failed to perform reading :(");
    return;
  }

  GetAltitude(); ///////////////////////////////////

  GetTime();     ///////////////////////////////////

  I2CSend();     ///////////////////////////////////

  // delay loop
  TimerMax  = 3000;
  TimerPrev = millis();
  TimerCur  = millis();
  while
  ((TimerCur - TimerPrev) < TimerMax)
  {
    TimerCur = millis();
  }
  Serial.println();
  Serial.println("*****LoopEnd");
} // END loop

void GetTime() {

  Serial.println();
  Serial.println("*****GetTime");
  char MinC[03];
  char HourC[03];
  bool h12 = true;
  char AMPM;
  int Hour = Clock.getHour(h12, PM);
  int Min  = Clock.getMinute();

  // Add AM/PM indicator
  if (PM) {
    AMPM = 'P';
  }
  else {
    AMPM = 'A';
  }
  /*
    Serial.println();
    Serial.print(" ZZZHour ");
    Serial.print(Hour);
    Serial.print(':');
    Serial.print(Min);
    Serial.print(AMPM); // [0] and [1]
  */
  sprintf(HourC, "%2d", Hour);
  sprintf(MinC, "%2d", Min);
  DateC[0] = HourC[0];
  DateC[1] = HourC[1];
  DateC[2] = ':';
  DateC[3] = MinC[0];
  DateC[4] = MinC[1];
  DateC[5] = AMPM;
  DateC[6] = '\0';
  Serial.print("  DateC:");
  Serial.println(DateC);
}


void GetAltitude()
{ int   Alt100;
  float Altitude;
  char  xx;

  Serial.println();
  Serial.println("*****GetAltitude");

  Altitude = bmp.readAltitude(SEALEVELPRESSURE_HPA) * 3.28084 * 1.06; //1.06 is my own factor
  Alt100   = 100 * round(Altitude / 100); // round to nearest 100
  if (Alt100 < 0) {
    Alt100 = 0;
  }

  sprintf(Altx, "%5d", Alt100);
  /*
    Serial.println();
    Serial.print("Alt100: ");
    Serial.print(Alt100);
    Serial.print("  Altx: |");
    Serial.println(Altx);
    Serial.print('|');
  */
} // end GetAltitude


void I2CSend()
{ Serial.println();
  Serial.println("*****I2CSend");
  Serial.print("I2Cc:");
  Serial.print('|');
  Serial.print(Altx);
  Serial.print(CurAx);
  Serial.print(CurBx);
  Serial.print(DateC);
  //Serial.print(CurAx);
  //Serial.println(CurBx);

  Wire.beginTransmission(4); // transmit to device #4
  Wire.write('|');
  Wire.write(Altx);
  Wire.write(CurAx);
  Wire.write(CurBx);
  Wire.write(DateC);
  //Wire.write("|||");
  Wire.endTransmission();    // stop transmitting
} // end I2CSend

In case you have not noticed, I like links !
Is this your altitude sensor ? Adafruit BMP388 - Precision Barometric Pressure and Altimeter [STEMMA QT] : ID 3966 : $9.95 : Adafruit Industries, Unique & fun DIY electronics and kits.
It has onboard pullup resistors. If you connect that to the I2C bus, then there should be enough pullup already.

SoftwareSerial claims almost all of the Arduino processing time. If you can use a Micro or Pro Micro, then there is a spare hardware serial port. The choice between SoftwareSerial and I2C between Arduino boards is so-so :confused:

Yes, please move the I2C display to the real Master. If you can change that in your sketches without major problems, that would be a very good solution.

Fixed length for I2C communication between Arduino boards makes it easier. I must say that I don't know why you sometimes have less data. If sending padded data takes to long, perhaps it should be shortened. The Arduino Nano has a maximum size of 32 bytes.

You can use a delay() instead of that millis-thing at the bottom of the loop().

void loop()
{
  ...
  
  delay( 3000); // slow down the I2C transactions, Serial output and everything else
}

When using millis(), that means not slowing down at all, but aiming at the very maximum times per second that the loop() is executed. You would need a software-millis-timer to, for example, request the data over I2C every 2 seconds. When your I2C bus is okay, then there is no problem to poll data from another Arduino board a few times per second.

Ask what you don't understand. English is not my native language, some confusion is always possible.
I was hoping you joined me in my rant about round(), well, better luck next time :wink:

Koepel - your English and your ability to explain are excellent! I appreciate the humor as well.

FTI, I just posted to the General guidance forum to see if I am making this more work for myself than need be. The only reason I added the Nano into the mix was because I was having trouble getting the Mega to do all the work. I'm going to step back for a couple days and try to figure out the best way to move forward.

Here is the other thread:

https://forum.arduino.cc/index.php?topic=722671.msg4858082#msg4858082

Hi,
A schematic of both your projects, the single and the dual controllers, would be very helpful.
Have you got pullup resistors on the I2C lines?

Thanks. Tom.. :slight_smile:

After getting a little feedback on my other post - re: the two processor solution, I'm going to go back to a single processor (Mega) solution and ask for help WHEN (not if) I have trouble. I'll implement some of your suggestions at the same time. Crossing my fingers! You'll probably hear from me in a day or two.....

Looking at your code, the way you are receiving data from the Trimetric Battery Monitor seems rather cumbersome. There is an older post on here suggesting using the comma as the terminating character, essentially giving you each parameter as a separate input line, that would likely simplify the parsing of the input data. (reference Convert streaming ASCII to Text to read Battery Monitor Data ) You have a function to "clear" the serial input buffers, there really should be no need for that, although I have not looked at the code in enough detail to see why buffer overflow might be an issue. Clearing the buffer will likely corrupt your data if it occurs in the middle of a piece of data.

Try this code to see if it will print each parameter from the battery monitor on a separate line in the serial monitor. It is non-blocking, and would let you simplify the parsing code since it would not have to handle an unknown number of possible data parameters, search for commas, etc.

// Example 2 - Receive with an end-marker - from Serial Input Basics

const byte numChars = 32;
char receivedChars[numChars];   // an array to store the received data

boolean newData = false;

void setup() {
    Serial.begin(9600);
    Serial1.begin(2400);
    Serial.println("<Arduino is ready>");
}

void loop() {
    recvWithEndMarker();

    if (newData == true) {
        Serial.print("TM data: ");
        Serial.println(receivedChars);
        newData = false;
    }
}

void recvWithEndMarker() {
    static byte ndx = 0;
    char endMarker = ','; //endmaker changed to comma
    char rc;
   
    while (Serial1.available() > 0 && newData == false) {
        rc = Serial1.read();

        if (rc != endMarker) {
            receivedChars[ndx] = rc;
            ndx++;
            if (ndx >= numChars) {
                ndx = numChars - 1;
            }
        }
        else {
            receivedChars[ndx] = '\0'; // terminate the string
            ndx = 0;
            newData = true;
        }
    }
}

I've never seen a LiquidCrystal library that specified the display size in HEX, probably should be 40 instead of 28.

LiquidCrystal_I2C lcd(0x27, 28, 2); // HEX? 40 col 2 lines

If you don't mind, post the code for using a single arduino board when you have it ready.

Koepel - Can you give me more info on your comment: "In the Arduino world we deal with incoming serial data in a different way"? One of the reasons I have each of the Serial Streams in separate timeout loops, waiting until I've got a full set - Not all streams are available at any given time, so I either timeout on a given stream and use defaults for the data elements, or I parse the data. My thought was if I try to read all the streams back-and-forth and put the data into a buffer (same thing as a char field, yes?) that I'd potentially time out on a given stream, taking a long time waiting for every read of that stream. I could test the streams every so often and set availability flags, then only read those that are active...
Thanks

This is the basic way to read serial data.

void loop()
{
  if( Serial1.available() > 0)   // is there something ?
  {
    int inChar = Serial1.read();  // grab it !
  }

  if( Serial2.available() > 0)   // is there something ?
  {
    int inChar = Serial2.read();  // give me that byte !
  }

  if( Serial3.available() > 0)   // is there something ?
  {
    int inChar = Serial3.read();  // thank you for that byte !
  }
}

Instead of the if-statement, it is also possible to read more than one byte and use: while( Serialx.available() > 0). However, when there are no more bytes available(), then continue.

After grabbing the serial data, it needs to be stored somewhere. So you need three buffers with three indexes. See also the code by david_2018 in his reply #7. It is also possible to append something to a String object.
If there is a LineFeed at the end, then you know when it is time to process the command that is in the buffer.

If you don't have long delays in the sketch, then it seems as if you are reading three serial streams independent of each other. If a serial stream stops, then everything will still run as normal.
When you need to know if a serial stream has stopped, then it is possible to make a timestamp of the moment that the last byte was received with millis().

This also means that you may not use the parseInt(), readBytesUntil() and all those functions. Those functions wait for serial input, and you are reading three serial inputs at the same time.

Thanks for that, I'll think about how I might change my code to work that way. By "buffer", you mean a char[n] variable, right?

Hi,
A schematic of both your projects, the single and the dual controllers, would be very helpful.
Have you got pullup resistors on the I2C lines?

Thanks. Tom.. :)

I notice quite a few Serial.print() debug statements in your code. They will block your code once the 64char TX buffer fills up which can confuse the issue.
Three suggestions
i) increase baud rate to 115200, i.e. Serial.begin(115200)
ii) abbreviate the debug msgs eg replace *** STARTTM *** with STM
iii) change to non-blocking output for the debug msg, see the BufferedOutput section of Text I/O for the Real World

Adding a loopTimer can also help track down where the time is going, see my Multi-tasking in Arduino tutorial
The loopTimer and BufferedOutput classes are included in my SafeString library

Re the I2C bus. It can become locked up. There is an I2C_ClearBus method that can help in those cases
see Reliable Startup for I2C Battery Backed RTC Why the Arduino Wire library is not enough.

This topic was automatically closed 120 days after the last reply. New replies are no longer allowed.