Arduino Millis and Indexing Code

I was looking for a sketch to would display 4 different screens on an I2C LCD on my ESP32 board project. I wanted to display text, wait, display more text, etc., all without the use of delay(), which I try to avoid if possible. I finally found some on line and it works great, but for the life of me, I do not understand how the code works. I really do not want to use code I do not understand. By the way, I understand how millis works, just not the indexing part of this code. Could someone help me understand what is happening here? Thank you in advance!!

#include <Wire.h>
#include <LiquidCrystal_I2C.h>

// Set the LCD address to 0x27 for a 16 chars and 2 line display
LiquidCrystal_I2C lcd(0x27, 16, 2);

// Timing variables
unsigned long previousMillis = 0;
const unsigned long interval = 2000;  // 2 seconds

// Track the current screen index
int screenIndex = 0;

// Example screen content
const char* screens[][2] = {
  {"Screen 1:", "Hello, World!"},
  {"Screen 2:", "Arduino Rocks!"},
  {"Screen 3:", "Using millis()"},
  {"Screen 4:", "I2C LCD Demo"}
};

const int numScreens = sizeof(screens) / sizeof(screens[0]);

void setup() {
  lcd.init();                      
  lcd.backlight();                
  lcd.clear();                    
  displayScreen(screenIndex);     
}

void loop() {
  unsigned long currentMillis = millis();

  if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis;
    screenIndex = (screenIndex + 1) % numScreens;
    displayScreen(screenIndex);
  }
}

void displayScreen(int index) {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(screens[index][0]);
  lcd.setCursor(0, 1);
  lcd.print(screens[index][1]);
}

You could rewrite

screenIndex = (screenIndex + 1) % numScreens;

as

screenIndex = screenIndex + 1;
if( screenIndex >= numScreens ) {
  screenIndex = 0;
}

and it would do exactly the same thing. When it's time to display the next screen, advance the index by 1. If the value is greater than or equal to the number of screens (remembering that array index should be [0..numScreens-1]), wrap around to 0.

1 Like

The % operator gets the remainder of the division of (screenIndex+1)/numScreens, so when:

  • screenIndex == 0: remainder of 1/4 = 1. screenIndex = 1
  • screenIndex == 1: remainder of 2/4 = 2. screenIndex = 2
  • screenIndex == 2: remainder of 3/4 = 3. screenIndex = 3
  • screenIndex == 3: remainder of 4/4 = 0. screenIndex = 0

Then it just repeats.

Picky little detail. If you finish setup() with
previousMillis = millis();

Your first message won't be visible for less time than the others. As it is, the time from start of the millis counter to the time when the first message gets displayed is counted as if the message had been displayed at startup.

  • Alternatively you might want to look into using a screen State Machine for this too.
1 Like

@van_der_decken and @Dave_Lowther have explained how this line works. If the array size is a power of 2 (as in this case) it could be written more confusingly for the unwary as.
screenIndex = (screenIndex + 1) & (numScreens-1);

numScreens - 1 = 3;
when screenIndex + 1 = 4 anding it with 3 will give 0.

This will be faster because an and operation will be faster than the divide to get the % (modulo) result. But probably not worth the confusion factor. Particularly for those that don't understand it and use it for non power of 2 sizes. :grinning_face:

I like the idea you have presented here but I am still a bit confused how to code this.

Thank you to all who have responded. Let me go at this from another angle. Is there perhaps a different way to go about doing what the original code did? One that is simpler to understand? The task is:

  1. Use an I2C LCD
  2. Display a message on LCD
  3. Wait 4 seconds, change the LCD message to a 2nd message
  4. Wait 4 seconds, display yet another message
  5. Use Millis instead of the dreaded delay()
    Surely there is a simpler way that isn’t complex code. If it isn’t super efficient code, that’s okay - I’ve got plenty of memory. Thanks again everyone!
  • Do you know what a State Machine is ?

  • Do you fully understand how to make a TIMER based on millis( ) ?

I don't know why people seem to be so resistant to hiding the complexity of doing timing with millis() in separate functions; it make code so much easier to read...

void loop() {
   if (next_message_time()) {
     // compose and display the next messge
     // perhaps by indexing through an array
   }
   // Other stuff
}

boolean next_message_time() {
   static unsigned long previousMsgTime = 0;
   // return TRUE every 4 seconds
   unsigned long currentMillis = millis();
   if (currentMillis - previousMillis >= 4000) {
      // four seconds has elapsed.
      previousMsg = currentMillis;
      return true;
   }
   return false;
}
1 Like

One is flat, the other passes stuff to functions. I used "delay()" just so you can practice with millis();

#include <Wire.h>
#include <LiquidCrystal_I2C.h>

LiquidCrystal_I2C lcd(0x27, 16, 2);

void setup() {
  lcd.init();
  lcd.backlight();
  lcd.clear();
}

void loop() {
  nevergonna(); // <-- call this function to get the "flat" method
  delay(2000);
  lcd.clear();
  giveyouup(); // <-- call this function to get the "pass stuff to function" method
  delay(2000);
  lcd.clear();
}

// ==============================

void nevergonna() {
  lcd.setCursor(0, 0);
  lcd.print("Screen 1:");
  lcd.setCursor(0, 1);
  lcd.print("Hello, World!");
  delay(2000);

  lcd.setCursor(0, 0);
  lcd.print("Screen 2:");
  lcd.setCursor(0, 1);
  lcd.print("Arduino Rocks!");
  delay(2000);

  lcd.setCursor(0, 0);
  lcd.print("Screen 3:");
  lcd.setCursor(0, 1);
  lcd.print("Using millis()");
  delay(2000);

  lcd.setCursor(0, 0);
  lcd.print("Screen 4:");
  lcd.setCursor(0, 1);
  lcd.print("I2C LCD Demo");
  delay(2000);
}

// ==============================

void giveyouup() {
  fillIn("Screen 1:", "Hello, World!   "); // pass two arrays of characters to "fillitin()" function
  fillIn("Screen 2:", "Arduino Rocks!  ");
  fillIn("Screen 3:", "Using millis()  ");
  fillIn("Screen 4:", "I2C LCD Demo    ");
}

void fillIn(char one[], char two[]) { // receive two arrays of characters
  lcd.setCursor(0, 0);
  lcd.print(one);
  lcd.setCursor(0, 1);
  lcd.print(two);
  delay(2000);
}


LarryD, no Sir, I do not know what a State Machine is, since I consider myself an advanced beginner at C++ coding. I am certainly willing to learn though. Do I fully understand how to make a TIMER based on millis( )? Probably not. Again, I want to know how and I'm willing to learn.

  • I use a skeleton sketch to speed program/sketch development.

  • It uses a Class for making TIMERs; a blank State Machine and code for handling inputs.

  • If you are willing to learn and contribute, we can use this skeleton sketch to teach you to some advanced techniques ?

This will take some time to complete.

1 Like

Sure, let's do it.

  • BTW, what time zone are you in ?

  • Do you have an Arduino UNO ?

CST and yes, an UNO and also like the ESP32.

  • Okay let's start.
    Let me know when you need a rest, we can carry on the next day.

  • Attached is the skeleton Sketch I mentioned.
    For now, we will use the Arduino UNO as our target, this sketch will work on the ESP32 with some GPIO changes.

  • We always start our projects with a schematic showing our component interconnections.

  • Wire up the components to your UNO as seen above.

  • Upload the sketch below, set serial monitor to 115200, report back what you see on the LEDs, push the Switch on GPIO #2.

  • You may need to get the hd44780.h Library before your LCD will work:
    GitHub - duinoWitchery/hd44780: Extensible hd44780 LCD library

  • Do not worry about the code for now, we will cover things a bit at a time.

//
//================================================^================================================
//                               B a s i c   S k e l e t o n   S k e t c h
//
//  https://forum.arduino.cc/t/arduino-millis-and-indexing-code/1376338/17
//
//  LarryD
//
//  Version    YY/MM/DD    Comments
//  =======    ========    ========================================================================
//  1.00       23/01/14    Running code
//
//
//
//
//  Notes:
//
//
//
//

//================================================
#include <Wire.h>

//Use I2C library:     https://github.com/duinoWitchery/hd44780
//LCD Reference:       https://www.arduino.cc/en/Reference/LiquidCrystal

#include <hd44780.h>   //main hd44780 header

//NOTE:
//hd44780_I2Cexp control LCD using I2C I/O expander backpack (PCF8574 or MCP23008)
//hd44780_I2Clcd control LCD with native I2C interface (PCF2116, PCF2119x, etc...)

#include <hd44780ioClass/hd44780_I2Cexp.h> //I2C expander i/o class header

//If you do not know what your I2C address is, first run the "I2C_Scanner" sketch
//OR
//run the "I2CexpDiag" sketch that comes with the hd44780 library
//hd44780_I2Cexp lcd(0x3F);

hd44780_I2Cexp lcd(0x27);


//================================================
#define LEDon              HIGH   //PIN---[220R]---A[LED]K---GND
#define LEDoff             LOW

#define PRESSED            LOW    //+5V---[Internal 50k]---PIN---[Switch]---GND
#define RELEASED           HIGH

#define CLOSED             LOW    //+5V---[Internal 50k]---PIN---[Switch]---GND
#define OPENED             HIGH

#define ENABLED            true
#define DISABLED           false

#define RELAYon            LOW
#define RELAYoff           HIGH


//================================================
#define ONE_SECOND   1000ul              //milliseconds
#define ONE_MINUTE   (ONE_SECOND * 60ul) //60 seconds in one minute 
#define ONE_HOUR     (ONE_MINUTE * 60ul) //60 minutes in one hour
#define ONE_DAY      (ONE_HOUR * 24ul)   //24 hours in one day



//                                   c l a s s   m a k e T I M E R
//================================================^================================================
//
/*
  //========================
  makeTIMER toggleLED =
  {
     //.TimerType, .Interval, .TimerFlag, .Restart, .SpeedAdjustPin
     MILLIS/MICROS, 500ul, ENABLED/DISABLED, YES/NO, A0-A5

     //.SpeedAdjustPin defaults to 0 i.e. no speed adjustment is used
     //if .SpeedAdjustPin = A0-A5, a potentiometer on this pin adjusts the TIMER's speed (for diagnostics)
     //class static flag "makeTIMER::normalFlag" can be used to ENABLE/DISABLE adjustable TIMER speed,
     //ENABLE = normal speed, DISABLED = potentiometer controls TIMER speed
  };

  TIMER functions we can access:
  toggleLED.checkTIMER();
  toggleLED.enableRestartTIMER();
  toggleLED.restartTIMER()
  toggleLED.disableTIMER();
  toggleLED.expireTimer();
  toggleLED.setInterval(100ul);

  Static variable access
  makeTIMER::normalFlag = ENABLED/DISABLED  //defaults to DISABLED at power up time i.e. variable speed is allowed
*/

//                          millis() / micros()   B a s e d   T I M E R S
//================================================^================================================
//
//These TIMER objects are non-blocking
class makeTIMER
{
#define MILLIS             0
#define MICROS             1

#define ENABLED            true
#define DISABLED           false

#define YES                true
#define NO                 false

#define STILLtiming        0
#define EXPIRED            1
#define TIMERdisabled      2

  private:
  public:

    static bool              s_normalFlag;    //when ENABLED, adjustable TIMERs run at normal speed

    unsigned long            Time;            //when the TIMER started

    //these "members" are needed to define a TIMER
    byte                     TimerType;       //what kind of TIMER is this? MILLIS/MICROS
    unsigned long            Interval;        //delay time which we are looking for
    bool                     TimerFlag;       //is the TIMER enabled ? ENABLED/DISABLED
    bool                     Restart;         //do we restart this TIMER   ? YES/NO
    byte                     SpeedAdjustPin;  //a potentiometer on this pin, A0-A5, adjusts TIMER speed


    //================================================
    //constructor with no parameters
    makeTIMER()
    {
      TimerType = MILLIS;
      Interval = 1000ul;
      TimerFlag = ENABLED;
      Restart = YES;
      SpeedAdjustPin = 0;

      Time = 0;
    }

    //================================================
    //constructor with parameters
    makeTIMER(byte _TimerType, unsigned long _Interval,
              bool _TimerFlag, bool _Restart, byte _SpeedAdjustPin = 0)
    {
      TimerType = _TimerType;
      Interval = _Interval;
      TimerFlag = _TimerFlag;
      Restart = _Restart;
      SpeedAdjustPin = _SpeedAdjustPin;

      Time = 0;
    }

    //================================================
    //condition returned: STILLtiming (0), EXPIRED (1) or TIMERdisabled (2)
    //function to check the state of our TIMER        ex: if(myTimer.checkTIMER() == EXPIRED);
    byte checkTIMER()
    {
      //========================
      //is this TIMER enabled ?
      if (TimerFlag == ENABLED)
      {
        //============
        //is this an adjustable TIMER OR is the "normalSpeed" switch closed ?
        if (SpeedAdjustPin == 0 || s_normalFlag == ENABLED)
        {
          //============
          //this TIMER "is not" speed adjustable,
          //has this TIMER expired ?
          if (getTime() - Time >= Interval)
          {
            //============
            //should this TIMER restart again?
            if (Restart == YES)
            {
              //restart this TIMER
              Time = getTime();
            }

            //this TIMER has expired
            return EXPIRED;
          }
        }

        //============
        //this TIMER is speed adjustable
        else
        {
          //============
          //for diagnostics, we use a potentiometer to adjust TIMER speed,
          //has this TIMER expired ?
          if (getTime() - Time >= Interval / adjustInterval())
          {
            //============
            //should this TIMER restart again?
            if (Restart == YES)
            {
              //restart this TIMER
              Time = getTime();
            }

            //this TIMER has expired
            return EXPIRED;
          }
        }

        return STILLtiming;

      } //END of   if (TimerFlag == ENABLED)

      //========================
      else
      {
        //this TIMER is disabled
        return TIMERdisabled;
      }

    } //END of   checkTime()

    //================================================
    //function to enable and restart this TIMER       ex: myTimer.enableRestartTIMER();
    void enableRestartTIMER()
    {
      TimerFlag = ENABLED;

      //restart this TIMER
      Time = getTime();

    } //END of   enableRestartTIMER()

    //================================================
    //function to disable this TIMER                  ex: myTimer.disableTIMER();
    void disableTIMER()
    {
      TimerFlag = DISABLED;

    } //END of    disableTIMER()

    //================================================
    //function to restart this TIMER                  ex: myTimer.restartTIMER();
    void restartTIMER()
    {
      Time = getTime();

    } //END of    restartTIMER()

    //================================================
    //function to force this TIMER to expire          ex: myTimer.expireTimer();
    void expireTimer()
    {
      //force this TIMER to expire
      Time = getTime() - Interval;

    } //END of   expireTimer()

    //================================================
    //function to set the Interval for this TIMER     ex: myTimer.setInterval(100);
    void setInterval(unsigned long value)
    {
      //set the Interval
      Interval = value;

    } //END of   setInterval()

    //================================================
    //function to return the current time
    unsigned long getTime()
    {
      //return the time             i.e. millis() or micros()
      //========================
      if (TimerType == MILLIS)
      {
        return millis();
      }

      //========================
      else
      {
        return micros();
      }

    } //END of   getTime()


    //================================================
    //for diagnostics, a potentiometer on an analog pin is used to adjust TIMER speed, thanks alto777
    unsigned int adjustInterval()
    {
      unsigned int Speed = analogRead(SpeedAdjustPin);

      //using integer math to save on memory
      Speed = 1 + (Speed * 14) / 1023;  //Speed will have a range from 1 to 15

      return Speed;

    } //END of   adjustInterval()

}; //END of   class makeTIMER

//================================================
//initialize the static "s_normalFlag" variable,
//when ENABLED, adjustable TIMERs run at normal speed
bool makeTIMER::s_normalFlag = DISABLED;


//                                T I M E R   D e f i n i t i o n s
//================================================^================================================
//
//========================
//example: uses default library values
//.TimerType, .Interval, .TimerFlag, .Restart, .SpeedAdjustPin
//    MILLIS,    1000ul,    ENABLED,      YES,       0
//makeTIMER testTIMER{};

//========================
makeTIMER heartbeatTIMER =
{
  //.TimerType, .Interval, .TimerFlag, .Restart, .SpeedAdjustPin 
  MILLIS, 500ul, ENABLED, YES, 0
};

//========================  (5ms * s_filter) i.e. 5ms * 10 = 50ms for checking a valid switch operation
makeTIMER switchesTIMER =
{
  //.TimerType, .Interval, .TimerFlag, .Restart, .SpeedAdjustPin
  MILLIS, 5ul, ENABLED, YES, 0
};

//========================
makeTIMER machineTIMER =
{
  //.TimerType, .Interval, .TimerFlag, .Restart, .SpeedAdjustPin
  MICROS, 1000ul, ENABLED, YES, 0
};

//========================
makeTIMER commonTIMER =
{
  //.TimerType, .Interval, .TimerFlag, .Restart, .SpeedAdjustPin
  MILLIS, 1000ul, DISABLED, NO, 0
};


//                                  c l a s s    m a k e I n p u t
//================================================^================================================
//
//a class to define "Input" objects, switches or sensors

//================================================
class makeInput
{
#define NOTvalidated       0
#define VALIDATED          1
#define NOchange           2

  private:

  public:

    static byte s_filter;
    //say the above validating "s_filter" variable is set to 10
    //if we scan "inputs" every 5ms
    //i.e. we sample our inputs every 5ms looking for a change in state.
    //5ms * 10 = 50ms is needed to validate a switch change in state.
    //i.e. a switch change in state is valid "only after" 10 identical changes are detected.
    //This technique is used to filter out EMI (spikes), noise, etc.
    //i.e. we ignore switch changes in state that are less than 50ms.

    unsigned long switchTime;       //the time the switch was closed
    byte counter;                   //a counter used for validating a switch change in state

    //these "members" are needed to define an "Input"
    byte pin;                       //the digital input pin number
    byte lastState;                 //the state the input was last in


    //================================================
    //constructor with parameters
    makeInput(byte _pin, byte _lastState)
    {
      pin = _pin;
      lastState = _lastState;

      switchTime = 0;
      counter = 0;

      pinMode(pin, INPUT_PULLUP);
    }

    //================================================
    //condition returned: NOTvalidated (0), VALIDATED (1) or NOchange (2)
    //check to see if the input object has had a valid state change
    byte validChange()
    {
      byte currentState = digitalRead(pin);

      //===================================
      //has there been an input change in state ?
      if (lastState != currentState)
      {
        //we have had another similar change in state
        counter++;

        //is the "change in state" stable ?
        if (counter >= s_filter)
        {
          //an input change has been validated
          //get ready for the next scanning sequence
          counter = 0;

          //update to this new state
          lastState = currentState;

          if (currentState == CLOSED)
          {
            //capture the time when the switch closed
            switchTime = millis();
          }

          return VALIDATED;
        }

        return NOTvalidated;
      }

      //===================================
      //there has not been an input change in state
      counter = 0;

      return NOchange;

    } //END of   validChange()

}; //END of   class makeInput

//================================================
//a change in state is confirmed/validated when 10 identical state changes in a row are seen
byte makeInput::s_filter = 10;


//                                    S t a t e   M a c h i n e
//================================================^================================================
//
//the states in our State Machine
enum STATES : byte
{
  STARTUP, STATE1, STATE2, STATE3, STATE4, FINISHED
};

STATES mState = STARTUP;


//                              G P I O s   A n d   V a r i a b l e s
//================================================^================================================
//

//Analogs
//================================================
//

//INPUTS
//================================================
//

//============                  GPIO 2
makeInput mySwitch =
{
  //.pin, .lastState
  2, OPENED
};


//OUTPUTS
//================================================
//
const byte testLED                = 12;
const byte heartbeatLED           = 13;

//VARIABLES
//================================================
//
const unsigned long shortPushTime = 500ul;
const unsigned long longPushTime  = 2000ul;


//                                           s e t u p ( )
//================================================^================================================
//
void setup()
{
  Serial.begin(115200);

  digitalWrite(heartbeatLED, LEDoff);
  pinMode(heartbeatLED, OUTPUT);

  digitalWrite(testLED, LEDoff);
  pinMode(testLED, OUTPUT);

  //================================================
  //LCD stuff
  lcd.begin(16, 2);
  lcd.clear();

  lcd.setCursor(0, 0);
  //                   111111
  //         0123456789012345
  //             Skeleton
  lcd.print("    Skeleton    ");

  lcd.setCursor(0, 1);
  //                   111111
  //         0123456789012345
  //              Sketch
  lcd.print("     Sketch     ");


} //END of   setup()


//                                            l o o p ( )
//================================================^================================================
//
void loop()
{
  //========================================================================
  //Print the time it takes to return to this same spot.
  //Comment the next 3 lines when no longer needed
  //static unsigned long startTime;
  //Serial.println(micros() - startTime);
  //startTime = micros();


  //========================================================================  T I M E R  heartbeatLED
  //condition returned: STILLtiming, EXPIRED or TIMERdisabled
  //is it time to toggle the heartbeat LED ?
  if (heartbeatTIMER.checkTIMER() == EXPIRED)
  {
    //toggle the heartbeat LED
    digitalWrite(heartbeatLED, digitalRead(heartbeatLED) == HIGH ? LOW : HIGH);
  }

  //========================================================================  T I M E R  switches
  //condition returned: STILLtiming, EXPIRED or TIMERdisabled
  //is it time to check our switches ?
  if (switchesTIMER.checkTIMER() == EXPIRED)
  {
    checkSwitches();
  }

  //========================================================================  T I M E R  machine
  //condition returned: STILLtiming, EXPIRED or TIMERdisabled
  //is it time to service our State Machine ?
  if (machineTIMER.checkTIMER() == EXPIRED)
  {
    checkMachine();
  }


  //================================================
  //       Other non blocking code goes here
  //================================================


} //END of   loop()


//                                    c h e c k M a c h i n e ( )
//================================================^================================================
//
void checkMachine()
{
  //================================================
  //service the current "state"
  switch (mState)
  {
    //========================
    case STARTUP:
      {
        //do startup stuff
      }
      break;

    //========================
    case STATE1:
      {
        //condition returned: STILLtiming, EXPIRED or TIMERdisabled
        if (commonTIMER.checkTIMER() == EXPIRED)
        {
          digitalWrite(testLED, LEDoff);

          //we are finished with this TIMER
          commonTIMER.disableTIMER();

          //next state
          mState = STARTUP;
        }
      }
      break;

    //========================
    case STATE2:
      {
        //Do something
      }
      break;

    //========================
    case STATE3:
      {
        //Do something
      }
      break;

    //========================
    case STATE4:
      {
        //Do something
      }
      break;

    //========================
    case FINISHED:
      {
        //Do something
      }
      break;

  } //END of  switch/case

} //END of   checkMachine()


//                                   c h e c k S w i t c h e s ( )
//================================================^================================================
//
//we have access to:
//object.validChange()    - checks to see if there was a valid state change
//object.pin              - input hardware pin number
//object.lastState        - the state the input was/is in
//object.switchTime       - the millis() value when the switch closes

void checkSwitches()
{
  //========================================================================  mySwitch
  //was there a valid input change ?
  //condition returned: NOTvalidated (0), VALIDATED (1) or NOchange (2)
  if (mySwitch.validChange() == VALIDATED)
  {
    //========================
    //was this switch closed ?
    if (mySwitch.lastState == CLOSED)
    {
      digitalWrite(testLED, LEDon);

      //make this TIMER 2 seconds
      commonTIMER.setInterval(2000ul);

      //start this TIMER
      commonTIMER.enableRestartTIMER();

      //next state
      mState = STATE1;
    }

    //========================
    //this switch was opened
    else
    {
      Serial.print("The switch was closed for ");
      Serial.print(millis() - mySwitch.switchTime);
      Serial.println("ms.");

      //================================================  Short Push
      //was this a short push ?
      if (millis() - mySwitch.switchTime <= shortPushTime)
      {
        //do something
        Serial.println("Short Push");
      }

      //================================================  Long Push
      //was this a long push ?
      else if (millis() - mySwitch.switchTime >= longPushTime)
      {
        //do something
        Serial.println("Long Push");
      }

      //================================================  Regular Push
      //this was a regular push
      else
      {
        //do something
        Serial.println("Regular Push");
      }
    }

  } //END of mySwitch


} //END of   checkSwitches()


//
//================================================^================================================
//

Sorry, got to run. I’ll look at this again tomorrow. Thanks for taking the time Sir!

  • When you are ready then . . . .

LarryD, having to work late today. I very much want to learn about the work you’ve done!