The usage of delay() versus millis()

I am working on a coal loader for my model railroad. It uses 3 servos to control its operation. It has been programmed using 3 push buttons and a 4-line LCD display running on a NANO. Everything works well until I wanted to eliminate the LCD display and pushbuttons and convert the display to a Nextion display with buttons for control.

I programmed the Nextion display to control the servos as the pushbuttons did. All was well, and the servos moved as they should until data was written back to the Nextion display. At that point, the servos act like they received erroneous data and moved erratically.

The movement of the servos has to operate in a sequence. Meaning servo 1 has to finish its movement before 2 can move, and so on for 3. In the sketch, I am using the dreaded delay() statement for all the movements just to get it operating with the pushbuttons.

So I have added up all the delays in the program to a sum on 8 seconds. I have been studying the use of Millis() to replace all the delay() statements, as I think that may be causing my servo jitter when writing to the display.

At this point, while the servo is in motion, nothing else can happen until it is finished. So, using Millis() to be able to do other things doesn't seem to be useful here. I have included the file I'm currently using to see if you can see improvements to make it work with the Nextion display. Thank you for any help you can provide.


CoalLoaderMasterControl_4_19_2026.ino (6.3 KB)

Please post the code here, using code tags when you do. This will avoid the need for users to download it

  • In the Arduino IDE, use Ctrl T or CMD T to format your code then copy the complete sketch. Use the < CODE / > icon from the ‘posting menu’ to attach the copied sketch.

  • There are a lot of examples of non-blocking timing here, use the search feature.

This is your sketch:


#include <Servo.h>
#include <SoftwareSerial.h>
SoftwareSerial Serial2(2, 3);  // RX, TX Setup for second serial port for the ITEAD display

//End charactor string to send to the new display
String endChar = String(char(0xff)) + String(char(0xff)) + String(char(0xff));

unsigned long currentMillis;  //hold the current board time

Servo servo[3];  //code used for attaching upto 3 servos

int n = 0;
int pos = 0;  // variable to store the servo position

int ldrLights = 13;  // Pin 13 to control lights


// Servo angles for Chute up/down, Lower gate closed/open, Upper gate closed/open
byte minAngle[] = { 0, 5, 0 };     // min angle for each servo
byte maxAngle[] = { 60, 90, 95 };  // max angle for each servo

enum {  //Setup veriables to represent the servos
  Chute,
  lowerGate,
  upperGate
};

enum {  // Setup variables for chute position
  Lower = 0,
  Raise = 1
};

enum {  // Setup variables for gate position
  Open = 1,
  Close = 0
};

//Servo control pins
const byte servoPin[] = { 10, 11, 12 };

void setup() {
  Serial.begin(9600);
  Serial2.begin(9600);
  Serial.println("Hello");

  // Attach all servos to their drive pins
  for (int n = 0; n < 3; n++) {
    servo[n].attach(servoPin[n]);
  }

  //Home all Servos
  for (int x = 0; x < 2; x++) {
    servo[x].write(minAngle[x]);
  }

  //Add pin 13 to control the loading lights
  pinMode(ldrLights, OUTPUT);
  initDisplay();
}

void loop() {

  currentMillis = millis(); // read the system system time in ms

  // dfd = Data From Display
  if (Serial2.available()) {
    String dfd = "";
    delay(30);
    while (Serial2.available()) {
      dfd += char(Serial2.read());
    }
    Serial2.print(dfd);
    Serial.println(dfd);
    sendData(dfd);
  }
}

void sendData(String dfd) {  // The eon & eoff text does turn the LED on and off
  if (dfd == "eon") {
    digitalWrite(13, HIGH);
  }
  if (dfd == "eoff") {
    digitalWrite(13, LOW);
  }
  if (dfd == "load") {                        // received the 'load' command from the display
    Serial2.print(endChar);                   // Send 3 x char string (0xff) to clear buffer
    Serial2.print("t1.pco=63488" + endChar);  // Turn textbox text red

    // Start by calling the Load routine Here
    load();
    // delay(4000);                          //Simulat the loading procedure not needed now
    //Serial2.print(endChar);               // Send 3 x char string (0xff)
    Serial2.print("t1.pco=0" + endChar);  //Change the text back to black
    Serial2.print("t1.txt=\"   Car is done loading.         Move to the next car.\"" + endChar);
    Serial2.print("bt2.val=0" + endChar);
    Serial2.print("bt2.bco=50712" + endChar);  // This does not change the color on my display.
  }
}

void load() {
  // START HERE TO CREATE THE AUTOMATED LOADING PROCEEDURE
  //Systen is enabled and Loading process started
  chutePos(Chute, Lower);  // lower the chute
  delay(500);              // wait 1/2 sec

  // Open lower gate control
  gatePos(lowerGate, Open);   // open lower gate
  delay(1500);                // wait 1 sec
  gatePos(lowerGate, Close);  // close the lower gate
  delay(500);                 // wait 1/2 sec

  chutePos(Chute, Raise);  //Raise chute
  Serial.println("Car loading complete. Move to the next car.");

  //Open the upper gate to fill the lower bin
  gatePos(upperGate, Close);  //open the upper gate - reversed for servo placement
  delay(4000);                // 4 sec delay used for reloading the lower bin
  gatePos(upperGate, Open);   // close the upper gate  - reversed for servo placement
  delay(500);                 // wait 1/2 sec
}

// Controlling the chute - Up and Down
void chutePos(int i, int d) {  // i selects chute servo and d selects up or down
  if (d == 0) {
    Serial.println("Lowering chute");
    for (pos = minAngle[i]; pos <= maxAngle[i]; pos += 1) {
      servo[i].write(pos);  // Move servo[i] to max angle, speed
      delay(20);
    }
  } else {
    Serial.println("Raising chute");
    for (pos = maxAngle[i]; pos >= minAngle[i]; pos -= 1) {
      servo[i].write(pos);  // Move servo[i] to max angle, speed
      delay(20);
    }
  }
}

// Controlling the opening and closing of the gates
// i selects the servo control ( 0 = chute 1 = lower gate 2 = upper gate)
// p selects the gate open/close (1 = open gate  0 = close gate)

void gatePos(int i, int p) {

  if (p == 1) {
    Serial.print("Opening ");
  } else {
    Serial.print("Closing ");
  }

  if (i == 1) {
    Serial.println("Lower gate");  //Opening lower gate
  } else {
    Serial.println("Upper gate");
  }

  switch (p) {
    case 1:
      for (pos = minAngle[i]; pos <= maxAngle[i]; pos += 1) {
        servo[i].write(pos);  // Move servo[i] to max angle, speed - open gate
        delay(20);
      }
      break;

    case 0:
      for (pos = maxAngle[i]; pos >= minAngle[i]; pos -= 1) {
        servo[i].write(pos);  // Move servo[i] to max angle, speed - close gate
        delay(20);
      }
      break;
  }
}

void initDisplay() {
  //resets Nextion display to power on status
  Serial2.print("page 0" + endChar);
  Serial2.print("rest" + endChar);
}

Yes, post the .ino code using the code tags.

Since there are Nextion issues, it will be important to also post the .hmi file. That file will need to be zipped and posted as an upload as the ide does not allow for a .hmi file upload.

What Arduino are you using?

would you mind describing that sequence? (rather than trying to revese engineer it from the code)

There may be a conflict between SoftwareSerial and Servo, because they both rely heavily on interrupts.

< edit > SoftwareSerial disables interrupts while sending data, so it would cause the interrupts for Servo to be delayed, producing jitter. Either use an Arduino with two or more hardware serial ports, or eliminate the use of Serial to communicate over USB and use that port for the display.

I am using a NANO to control this project. Operation is as follows:

  1. The car is positioned under the loader.

  2. The first button turns on the "Loading Lights" and enables the Load button.

  3. Once you press the load button the "Chute", servo1, is lowered into the car.

  4. Next the "Lower Gate", servo2, is opened to fill the car. Once the "Lower Gate" closes.

  5. The "Upper Gate", servo3, which operates in reverse because of mounting issues, opens to refill the lower bin.

  6. The message is sent to the display that the loading process is complete and move to the next car.

CoalLoaderNextion1.zip (49.6 KB)

There is something corrupted with the .zip file and I can not extract the ,hmi file.
Can you please try again.

As a stated by @david_2018 the use of software serial is likely to create issues. A processor with an additional hardware serial port like the Arduino Mega or a Nano Every is a far better choice for Nextion applications.

EDIT: Don't bother with the .zip file. I managed to extract it with 7-Zip. .

These delays can be accommodated in a state machine, no need for blocking delay(. . .)

void load() {
  // START HERE TO CREATE THE AUTOMATED LOADING PROCEEDURE
  //Systen is enabled and Loading process started
  chutePos(Chute, Lower);  // lower the chute
  delay(500);              // wait 1/2 sec

  // Open lower gate control
  gatePos(lowerGate, Open);   // open lower gate
  delay(1500);                // wait 1 sec
  gatePos(lowerGate, Close);  // close the lower gate
  delay(500);                 // wait 1/2 sec

  chutePos(Chute, Raise);  //Raise chute
  Serial.println("Car loading complete. Move to the next car.");

  //Open the upper gate to fill the lower bin
  gatePos(upperGate, Close);  //open the upper gate - reversed for servo placement
  delay(4000);                // 4 sec delay used for reloading the lower bin
  gatePos(upperGate, Open);   // close the upper gate  - reversed for servo placement
  delay(500);                 // wait 1/2 sec
}

david_2018, Thank you for that information! That was the problem. I went in and removed all the calls to the serial port for the print statement and moved the serial connection to pins 0 & 1. No more problems!

  • If you decide to remove the delays, volunteers can help with the process.

LarryD, the delay(1500) and the delay(4000) are values I have determined for the time the gates need to be open to empty the lower bin and refill the upper bin. the two delay(500) statements are not really needed. They are more of an effect of the loading process before it moves to the next step.

A Nano based on the Atmega328PB processor is easily available, and doesn't cost any more than it would with an Atmega328P. That would give you a second UART (TX and RX would be on D11 and D12). That conflicts with the SPI port, but if you need one of those, there's also a second one among the analog pins.

Of course, if the issue is that one part of the program is turning interrupts off, and you need to have interrupts in order to (for instance) read the serial port, you're stuck. You'd need to find some replacement for that routine, which isn't so arrogant about blocking interrupts.

Note: During uploading of a sketch, remove external circuit connections from DPin-0 and 1; after uploading, put those connections back.

LarryD, once I followed david_2018's thoughts, I removed all the Serial.print & Serial.println statements, and everything works as it did with the pushbuttons. So it was a problem using USB serial port for debugging, along with the second serial port for the Nextion display.

Thanks

  • Yes I saw this, just suggesting you might want to investigate how to avoid delay(. . .)

  • This might interest you:

#define IMMEDIATE     0
#define WAIT_FOREVER  0xFFFFFFFF
. . .

enum LoadStates
{
  LOAD_IDLE,
  LOAD_START,
  LOAD_CHUTE_LOWER,
  LOAD_LOWER_GATE_OPEN,
  LOAD_LOWER_GATE_CLOSE,
  LOAD_CHUTE_RAISE,
  LOAD_UPPER_GATE_OPEN,
  LOAD_UPPER_GATE_CLOSE,
  LOAD_DONE
};

LoadStates mState = LOAD_IDLE;

unsigned long stateTimer;
unsigned long statePeriod;

. . .


void setState(LoadStates nextState, unsigned long waitTime)
{
  mState = nextState;
  stateTimer = millis();
  statePeriod = waitTime;
}

. . .

void startLoad()
{
    setState(LOAD_START, IMMEDIATE);
}

. . .

//========================================================================  State machine
// Is it time to service the State ?
//
// IMMEDIATE (0) causes immediate execution (no delay)
//
if (statePeriod != WAIT_FOREVER &&
    (statePeriod == IMMEDIATE || millis() - stateTimer >= statePeriod))
{
  //================================================
  switch (mState)
  {
    //========================
    case LOAD_IDLE:
      {
        // Do nothing, wait for startLoad()
      }
      break;

    //========================
    case LOAD_START:
      {
        chutePos(Chute, Lower);

        setState(LOAD_CHUTE_LOWER, 500);
      }
      break;

    //========================
    case LOAD_CHUTE_LOWER:
      {
        gatePos(lowerGate, Open);

        setState(LOAD_LOWER_GATE_OPEN, 1500);
      }
      break;

    //========================
    case LOAD_LOWER_GATE_OPEN:
      {
        gatePos(lowerGate, Close);

        setState(LOAD_LOWER_GATE_CLOSE, 500);
      }
      break;

    //========================
    case LOAD_LOWER_GATE_CLOSE:
      {
        chutePos(Chute, Raise);
        Serial.println("Car loading complete. Move to the next car.");

        setState(LOAD_CHUTE_RAISE, IMMEDIATE);
      }
      break;

    //========================
    case LOAD_CHUTE_RAISE:
      {
        gatePos(upperGate, Close);  // reversed

        setState(LOAD_UPPER_GATE_OPEN, 4000);
      }
      break;

    //========================
    case LOAD_UPPER_GATE_OPEN:
      {
        gatePos(upperGate, Open);  // reversed

        setState(LOAD_UPPER_GATE_CLOSE, 500);
      }
      break;

    //========================
    case LOAD_UPPER_GATE_CLOSE:
      {
        setState(LOAD_DONE, IMMEDIATE);
      }
      break;

    //========================
    case LOAD_DONE:
      {
        // Finished cycle
        setState(LOAD_IDLE, WAIT_FOREVER);
      }
      break;
  }
}




i think the Stop conditions can be tuned better, this version handles things more cleanly, avoids blocking and handles a Stop command

const char *Version = "Coal Loader - 260419e";

#include <Servo.h>

const byte PinLed = 13;

const char *ServoStr [] = { "Chute", "LwrGate", "UprGate" };

enum                    { Chute, LwrGate, UprGate };   // servo indices
const byte servoPin[] = {    10,      11,      12 };
const byte minAngle[] = {    0,        5,       0 };
const byte maxAngle[] = {    60,      90,      95 };
const int  Nservo     = sizeof(servoPin);

byte servoPos  [Nservo];
byte servoTarg [Nservo];

Servo servo[Nservo]; 

unsigned long msec;
unsigned long msec0;

char s [90];

// -----------------------------------------------------------------------------
bool
servoAdjust (
    int  id )
{
    if (servoPos [id] == servoTarg [id])
        return true;

    delay (20);
    if (servoPos [id] < servoTarg [id])
        servo[id].write (++servoPos [id]);
    else if (servoPos [id] > servoTarg [id])
        servo[id].write (--servoPos [id]);

    if (! (servoPos [id] % 10))  {
        sprintf (s, "        servoAdjust: %3d %d %s",
                                    id, servoPos [id], ServoStr [id]);
        Serial.println (s);
    }

    return false;
}

// -----------------------------------------------------------------------------
const char *StStr [] = {
    "Idle", "Lower", "OpenLwr", "CloseLwr", "Raise", "OpenUpr", "CloseUpr" };
enum { Idle, Lower, OpenLwr, CloseLwr, Raise, OpenUpr, CloseUpr };
enum { Nul, Start, Stop };
int state;

// ---------------------------------------------------------
void loader (
    int  stim )
{
    switch (state)  {
    case Idle:
        if (Start == stim)  {
            state = Lower;
            Serial.println (StStr [state]);
            servoTarg [Chute] = maxAngle [Chute];
        }
        break;

    case Lower:
        if (Stop == stim)  {
            state = Raise;
            Serial.println (StStr [state]);
            servoTarg [Chute] = minAngle [Chute];
        }

        else if (servoAdjust (Chute))  {
            state = OpenLwr;
            Serial.println (StStr [state]);
            servoTarg [LwrGate] = maxAngle [LwrGate];
        }
        break;

    case OpenLwr:
        if (Stop == stim)  {
            state = CloseLwr;
            Serial.println (StStr [state]);
            servoTarg [LwrGate] = minAngle [Chute];
        }

        else if (servoAdjust (LwrGate))  {
            state = CloseLwr;
            Serial.println (StStr [state]);
            servoTarg [LwrGate] = minAngle [LwrGate];
            msec0 = msec;
        }
        break;

    case CloseLwr:
        if (Stop == stim)  {
            msec0 = msec + 3000;
        }

        else if (msec - msec0 >= 3000 && servoAdjust (LwrGate))  {
            state = Raise;
            Serial.println (StStr [state]);
            servoTarg [Chute] = minAngle [Chute];
        }
        break;

    case Raise:
        if (servoAdjust (Chute))  {
            state = OpenUpr;
            Serial.println (StStr [state]);
            servoTarg [UprGate] = maxAngle [UprGate];
        }
        break;

    case OpenUpr:
        if (Stop == stim)  {
            state = CloseLwr;
            Serial.println (StStr [state]);
            servoTarg [LwrGate] = minAngle [UprGate];
            msec0 = msec + 3000;
        }

        if (servoAdjust (UprGate))  {
            state = CloseUpr;
            Serial.println (StStr [state]);
            servoTarg [UprGate] = minAngle [UprGate];
            msec0 = msec;
        }
        break;

    case CloseUpr:
        if (Stop == stim)  {
            msec0 = msec + 3000;
        }

        else if (msec - msec0 >= 3000 && servoAdjust (UprGate))  {
            state = Idle;
            Serial.println (StStr [state]);
        }
        break;
    }
}

// -----------------------------------------------------------------------------
void loop () {
    msec = millis ();

    if (Serial.available ())  {
        char c = Serial.read ();
        if ('l' == c)
            loader (Start);
        if ('s' == c)
            loader (Stop);
    }

    loader (Nul);
}

// -----------------------------------------------------------------------------
void setup () {
    Serial.begin (9600);
    Serial.println (Version);

    // Attach/initialize servos
    for (int n = 0; n < Nservo; n++) {
        servo[n].attach (servoPin[n]);
        servo[n].write  (minAngle[n]);

        servoTarg [n] =servoPos [n] = minAngle [n];
    }

    pinMode (PinLed, OUTPUT);
}

Thank you, LarryD and gcjr! I copied both of your code examples down so I can improve my coding experience. I am always learning something new when visiting this forum with a question!