How do I run LCD and a stepper at the same time?

I have an Arduino Mega with a RAMPS 1.4 to control two stepper motors and an LCD screen. This is a snippet of code, which is a function that displays the elapsed time on the experiment and simultaneously controls the motors.

I am getting a skip every minute when the LCD updates. (exp_in_progress is an LCD update function).

void motor_cyclic() // MOTOR ID = 2
  {
    uint32_t time_start = millis();
    uint32_t time_current = millis()-time_start;

    static int d = 0;
    static int h = 0;
    static int m = -1;

    while (time_current < exp_time)
    {    
        int endstop_rmin = digitalRead(3);  // R-min
        int endstop_rmax = digitalRead(2);  // R-max
        int endstop_fmin = digitalRead(14); // F-min
        int endstop_fmax = digitalRead(15); // F-max
        
        if (time_current%60000 == 0)
          {
            m++;
            if (m == 60)
              {
                m = 0;
                h++;
                if (h == 24)
                  {
                    h = 0;
                    d++;
                  }
              }
            exp_in_progress(newBreaths, flowRate, d, h, m);     // LCD UPDATE//
          }
         time_current = millis()-time_start;

            if (time_current % 10000 < 5000) {
              digitalWrite(X_DIR_PIN, HIGH);
              digitalWrite(Y_DIR_PIN, HIGH);
            }
            else {
              digitalWrite(X_DIR_PIN, LOW);
              digitalWrite(Y_DIR_PIN, LOW);
            } 
           
            digitalWrite(X_STEP_PIN, HIGH);
            digitalWrite(Y_STEP_PIN, HIGH);
          
            delay(0.25);
            
            digitalWrite(X_STEP_PIN, LOW);
            digitalWrite(Y_STEP_PIN, LOW);
            if (endstop_rmin or endstop_rmax or endstop_fmin or endstop_fmax == HIGH)
            {
             time_current = exp_time + 1000; 
             break;
            }
            if (!digitalRead(selectbutton))
            {
             time_current = exp_time + 1000;  
             break;
            }            
          
    }
    m=-1;
    h=0;
    d=0;
    menu_return();
  }

I think we need to see the exp_in_progress function.

delay(0.25)?

You may need to post all of the code.

What LCD?

LiquidCrystal_I2C lcd(0x27, 16, 4); // lcd, 4 rows

void exp_in_progress(int r, int f, int d, int h, int m)
{
  lcd.clear();
  lcd.setCursor(0,0);
  lcd.print("  Exp in Progress");
  lcd.setCursor(0,1);
  lcd.print("Resp: ");
  lcd.print(r);
  lcd.print(" B/Min");
  lcd.setCursor(0,2);  
  lcd.print("Flow: ");
  lcd.print(f);
  lcd.print(" L/Min");
  lcd.setCursor(0,3);
  lcd.print("Elapsed: ");
  lcd.print(d);
  lcd.print("D:");
  lcd.print(h);
  lcd.print("H:");
  lcd.print(m);
  lcd.print("M");    
  return;
}

Do not use the clear() function. It takes a lot of time.

Update only data that changes. Use a function, in setup() perhaps, that writes the more permanent labels. Then use the setCursor command to place the cursor where the changeable data goes and print the data. If you need to clear a section do it by placing the cursor, printing to overwrite the old data and use setCursor to move the cursor back and write the data.

Change to the hd44780 library. It is a faster library and all around better than the older LiquidCrystal libraries.
The library is available in the Library Manager. Go to Library Manager (in the IDE menus, Sketch, Include Libraries, Manage Libraries) and in the Topics dropdown choose Display and in the Filter your search box enter hd44780. Select and install the hd44780 library by Bill Perry.

The class that you want to use is the hd44780_I2Cexp class. There are examples to show how to use the library. The nice thing about the hd44780 library is that it will autodetect the I2C address

Here is example code to illustrate the above.

#include <Wire.h>
#include <hd44780.h>                       // main hd44780 header
#include <hd44780ioClass/hd44780_I2Cexp.h> // i2c expander i/o class header

hd44780_I2Cexp lcd; // declare lcd object: auto locate & auto config expander chip
                   // note no need for I2C address or pin map

const int LCD_COLS = 20;
const int LCD_ROWS = 4;

void setup()
{
   Serial.begin(115200);
   lcd.begin(LCD_COLS, LCD_ROWS);
   setUpDisplay();
}

void loop()
{
   static unsigned long timer = 0;
   unsigned long interval = 1000;
   if (millis() - timer >= interval)
   {
      timer = millis();
      // call function with random test data
      exp_in_progress(random(130),random(99),random(99),random(99), random(99));
   }
}

void exp_in_progress(int r, int f, int d, int h, int m)
{
   lcd.setCursor(6, 1);  // place cursor
   lcd.print("    ");        // overwrite old data
   lcd.setCursor(6, 1);  // reposition cursor
   lcd.print(r);             // print new data
   lcd.setCursor(6, 2);
   lcd.print("    ");
   lcd.setCursor(6, 2);
   lcd.print(f);
   lcd.setCursor(9, 3);
   lcd.print("  ");
   lcd.setCursor(9, 3);
   lcd.print(d);
   lcd.setCursor(13, 3);
   lcd.print("  ");
   lcd.setCursor(13, 3);
   lcd.print(h);
   lcd.setCursor(17, 3);
   lcd.print("  ");
   lcd.setCursor(17, 3);
   lcd.print(m);   
   // return;  return not required, you are not returning a value.  Function declared void
}

void setUpDisplay()
{
   lcd.setCursor(0, 0);
   lcd.print("  Exp in Progress");
   lcd.setCursor(0, 1);
   lcd.print("Resp: ");
   lcd.setCursor(9, 1);
   lcd.print(" B/Min");
   lcd.setCursor(0, 2);
   lcd.print("Flow: ");
   lcd.setCursor(9, 2);
   lcd.print(" L/Min");
   lcd.setCursor(0, 3);
   lcd.print("Elapsed: ");
   lcd.setCursor(11, 3);
   lcd.print("D:");
   lcd.setCursor(15,3);
   lcd.print("H:");
   lcd.setCursor(19,3);
   lcd.print("M");
}

You can try adding stepper motor update calls between lcd.print() statements.
See my tutorial Simple Multitasking Arduino which covers adding a loopTimer to monitor how fast your stepper code is being called and includes a detailed stepper example controlled by user commands and the adding extra calls to keep the stepper running consistently.

Here is some information on how much time it takes to update the display when using i2c.
The LiquidCrystal_I2C library can transfer a byte to the display in 1486us or about 1.5ms
The hd44780_I2Cexp i/o class can transfer a byte to the display in 550us or about 0.55ms nearly 3x the speed of LiquidCrstal_I2C

Each character you write the LCD display or each time you set the cursor it takes writing 1 byte to the display.
Doing a clear() takes 2ms

You can add up all the operations you are doing to calculate the total time that exp_in_progress() will take.
With numbers above you can calculate the time for either/both libraries.

Once you come up with the overall time for exp_in_progress(),
Can your other code tolerate that amount of latency?

If you want to minimize the time you need to pre-fill the screen with everything that is fixed and only update the changing part.
For the numbers, I would print them as fixed width fields on the display.
vs printing spaces then overwriting the spaces.
The reason being a fixed width field will update faster and will avoid the flicker from clearing the number and re-drawing it and it will keep the digits steady since each numeric decimal position will not move.
i.e. the 0nes position, the 10s position, the 100s position, will always be in the same location.
It makes reading the display easier.

Either pad the leading unused positions of the number with spaces or zeros.
The easiest way to do that is use sprintf() using the fill formatting options.
if you have the extra 2k or code space that the xxprintf() code will use, it will be the easiest way to do the output formatting.
You would simply

  • format the fixed width output field for the number into a buffer.
  • set the cursor position where the field needs to start on the lcd
  • print the field using the formatted buffer.

Another alternative would be to bump the i2c clock to 400kHz which will reduce the byte transfer times.
The LiquidCrystal_I2C byte transfer time will drop to 608us
The hd44780_I2Cexp byte transfer time will drop to 205us
The time for clear() does not change.

If you do decide to bump the i2c clock, keep in mind that while 400kHz seems to work for PCF8574 chips, it is beyond the rated spec. Also some i2c slaves like many RTC chips cannot run this fast.

If you didn't do anything but switch to hd44780_I2Cexp and bumped the clock to 400kHz, all the code after the clear() would speed up over 7x

--- bill

in general stepper-motors need the step-pulses in a well calculated regular pattern.
Most of the time the pulse-train has a constant frequency. only of you do acceleration/decceleration the frequency changes but still in a well caclulated manner.

Doing other things like serial-printing or sending data to an LCD needs too much time.
So you shouldn't do that while creating step-pulses.

One way to do it in parallel is to create the step-pulses using a timer-interrupt.
The basic principle is to set up the timer-interrupt to call a interrupt-service-routine and use two variables to control it.
One variable holds the number of steps that shall done and the second variable is of the bool-type to control the start of creating steps.
The NoOfSteps variable is initialised with the number of the steps and then set th ebool-variable true to make the ISR start "stepping" and decrement the NoOfSteps until zero is reached. If zero is reached the ISR stops creating steps
=> don't move endlessly but just the amount of steps given before start.

As long as you don't need acceleration/decceleration and different speeds this works very well.

If you need different speeds You would have to setup the timing-constant to another value or use fractions of a maximum speed 1/2, 1/3, 1/4 1/5 etc. making the IRS create a step only every 2, 3, 4, 5 calls

best regards Stefan