Millis() in 2 var: 2nd var is smaller than first?

Hello everyone,

Currently I'm designing a reaction timer game for my students. 1 Button, 1 LED and the serial monitor is all I need (I think).

I'm not a complete noob at programming but I must admit that I'm more familiar with ladder diagrams (PLC). That's why I try to keep the programming to functions/statements I understand, so I can explain them in detail if my students would be interested.

My goal is to have some instructions in the serial monitor: the user presses and holds the button, that would trigger a "3" "2" "1" on serial monitor, timed with 1 second in between them. Then a random time would be added and when the time is reached the LED would come on: signaling the user to release the button. When released, the serial monitor should show the difference in the internal timer and when the user released the button.

ATM; The problem is that it does not wait a second for the next number, in fact, it does not wait at all. I have filtered it down to being a lower value being subtracted by an higher value, creating an overflow.

Both variables are being derived from the Millis() function, one is the starting value (static), the other one is the value that the user has been waiting until the LED would come on.
The weird thing is that I fill in the currentvalue before the waitvalue which should result in the waitvalue being higher than the currentvalue, but it doesn't seem to work.. Any suggestions?

This is (part of) the serial monitor output:

DEBUG currenttime value 0

3

DEBUG currenttime value 1001

DEBUG starttime value 1004

2

DEBUG currenttime value 1001

DEBUG starttime value 1004

1

DEBUG currenttime value 1001

DEBUG starttime value 1004

0 ... Watch the LED!

DEBUG bttnState value 1

DEBUG steps value 2

DEBUG LED value 0

DEBUG currenttime value 1001

DEBUG waittime value 1214

Here is a loop of the code that I used:

       void loop() 
{
  bttnState = digitalRead(BTTN);
  digitalWrite(LED, ledState);

      switch (steps)
      {
        case 0: // State: button not pushed yet, game is ready
          if (bttnState == true && (bttnState != bttnPrevState)) // Check if button is pressed since last cycle
          {
            bttnPrevState = bttnState; // Shift current state into last state
            ledState = false; // set to false once before case 2
            printFlag0 = true; // set to true once before case 1
            printFlag1 = true;
            printFlag2 = true;
            printFlag3 = true;
            randomizerFlag = true; // set to true once before case 2

            steps = 1; // Go to next case
          }
        break;
        case 1: // State: button is pressed, start counting
          if (bttnState == true && (bttnState == bttnPrevState)) // Check if button is still pressed down
          {
            currentTimeMs = millis(); // FIll in current time

              if (printFlag3 == true) // Check if printed only once to serial monitor or not
              {
                Serial.println("3");
                startTimeMs = millis(); // Take time when the program starts
                printFlag3= false; // After printing to serial monitor, we don't want to print again
              }
              if ((currentTimeMs - startTimeMs) >= 1000) // Checking if passed time is  > 1s
              {
                if (printFlag2 == true) // Check if printed only once to serial monitor or not
                {
                  Serial.println("2");
                  printFlag2 = false; // After printing to serial monitor, we don't want to print again
                }
              }
                if ((currentTimeMs - startTimeMs) >= 2000) // Checking if passed time is  > 2s
                {
                  if (printFlag1 == true) // Check if printed only once to serial monitor or not
                  {
                    Serial.println("1");
                    printFlag1 = false; // After printing to serial monitor, we don't want to print again
                  }
                }
                if ((currentTimeMs - startTimeMs) >= 3000) // Checking if passed time is  > 3s
                {
                  if (printFlag0 == true) // Check if printed only once to serial monitor or not
                  {
                    Serial.println("0 ... Watch the LED!");
                    printFlag0 = false; // After printing to serial monitor, we don't want to print again
                    waitTimeMs = millis(); // Write time since program will ask to wait on LED
                    steps = 2; // Go to next case
                  }
                }
          }
          else if (bttnState == false) // do this if button is not pressed anymore
          {
            bttnPrevState = bttnState;
            Serial.println("Button Released far too soon!");
            Serial.println("Start again!");
            steps = 0;
          }
        break;
        case 2: // State: LED will go on and time will be made visual, then reset.
        currentTimeMs = millis();
        if ((bttnState == true) && (bttnState == bttnPrevState)) // Check if button is still pressed down
        {
            if (randomizerFlag == true)
            {
              randomTimeMs = random(2000,7000);
              randomizerFlag = false;
              endTimeMs = (waitTimeMs + randomTimeMs);
            }
            else if ( randomizerFlag == false)
            {
              if (currentTimeMs >= endTimeMs )
              {
                ledState = true;
              }
            }
        }
        else if ( (bttnState == false) && (currentTimeMs < endTimeMs) ) // do this if button is not pressed anymore & endtime not reached
        {
          Serial.println("Button Released far too soon!");
          Serial.println("Loser!");
          Serial.println("Start again!");
          digitalWrite(LED, LOW); // Make sure to initialize LED for next round
          steps = 0;
        }
        else if ( (bttnState == false) && (currentTimeMs >= endTimeMs) ) // do this if button is not pressed anymore & endtime is reached
        {
          Serial.print("Nice! Your time is: "); // Give user information
          Serial.println(currentTimeMs);
          Serial.print("Press button to start again!");
          digitalWrite(LED, LOW); // Make sure to initialize LED for next round
          steps = 0;
        }
        break;
      }

you should use a button library to make your code easier. It will handle bouncing for you.

As things need to be asynchronous (non blocking) you might benefit from studying state machines. Here is a small introduction to the topic: Yet another Finite State Machine introduction

Hi Jackson,
Thanks for the fast response, I'll look into it. :slightly_smiling_face:

here is a simple example on how you can use the Toggle library to detect the press and release of the button.

the rest of your logic is simply going through a state machine

1 Like
#define LED 13
#define BTTN 2

void setup() {
  Serial.begin(115200);
  pinMode(LED, OUTPUT);
  pinMode(BTTN, INPUT_PULLUP);
}

void loop() {
  static byte steps = 0;
  static uint32_t startTimeMs = 0;
  static uint32_t randomTimeMs = 0;
  static bool bttnPrevState = false;

  bool bttnState = !digitalRead(BTTN);

  switch (steps)  {

    case 0: // State: button not pushed yet, game is ready
      if (bttnState && !bttnPrevState) // Check if button is pressed since last cycle
      {
        startTimeMs = millis(); // Take time when the program starts
        digitalWrite(LED, LOW);
        steps = 1;
        delay(20);
      }
      bttnPrevState = bttnState; // Shift current state into last state
      break;

    case 1:
      if (bttnState )       {
        if (millis() - startTimeMs >= 1000) {
          Serial.println("3");
          steps = 2;
        }
      } else steps = 10;
      break;

    case 2:
      if (bttnState )       {
        if (millis() - startTimeMs >= 2000) {
          Serial.println("2");
          steps = 3;
        }
      } else steps = 10;
      break;

    case 3:
      if (bttnState )       {
        if (millis() - startTimeMs >= 3000) {
          Serial.println("1");
          steps = 4;
        }
      } else steps = 10;
      break;

    case 4:
      if (bttnState) {
        startTimeMs = millis();
        digitalWrite(LED, HIGH);
        randomTimeMs = random(2000, 7000);
        steps = 5;
      } else steps = 10;
      break;

    case 5:
      if (bttnState) {
        if (millis() - startTimeMs >= randomTimeMs )  {
          digitalWrite(LED, LOW);
          startTimeMs = millis();
          steps = 6;
        }
      }
      else steps = 10;
      break;

    case 6: // State: button is pressed, start counting
      while (digitalRead(BTTN) ); // Check if button is still pressed down
      Serial.print("Nice! Your time is: "); // Give user information
      Serial.println(millis() - startTimeMs);
      Serial.print("Press button to start again.");
      steps = 0;
      break;

    case 10:
      Serial.println("Button Released far too soon.");
      Serial.println("You lose. Try again.");
      steps = 0;
      break;
  }
}

as a general rule:
always post your complete sketch.

Just posting function loop() creates a question that would not be nescessary to ask if you would jave posted your complete sketch right in your first post:

What variable-type do the variables

currentTimeMs 
startTimeMs

have?

They should be type unsigned long
In case of unsigned long the specialties how unsigned integers are calculated the result of

currentTimeMs - startTimeMs

would be always correct
even in case that currentTimeMs holds a smaller value than startTimeMs

The math for unsigned long arithmetic is well defined for what happens when you subtract a larger number from a smaller number, (it rolls over) but the interpretation of the rolled over number might be confusing.

Consider that:

unsigned long currentTimeMs, startTimeMs, diffTimeMs;

void setup() {
  currentTimeMs = 100;
  startTimeMs = 101;
  diffTimeMs = currentTimeMs - startTimeMs;

  Serial.begin(115200);
  Serial.println(currentTimeMs);
  Serial.println(startTimeMs);
  Serial.println(diffTimeMs);
  Serial.println((signed long)diffTimeMs);
}

void loop() {
}

prints:

100
101
4294967295
-1

The unsigned long math operations are well defined, and the 4294967295 result is of the unsigned long rolling over below 0 to ULONG_MAX(=2^32-1 = 4294967295). Interpretation of the rolled-over unsigned long as a signed long is also pretty well defined for Gnu compilers and gives the expected answer.

If you are trying to schedule future startTimeMs times, and expect the difference to be negative, such as in:

... you need to make that signed interpretation of the rolled-over unsigned long difference explicit:

  if (long(currentTimeMs - startTimeMs) >= 1000) // Checking if passed time is  > 1s

Thanks,
They are both defined as unsigned long before the void setup, and as you demonstrated I believe the value to roll over (what I wrongly called overflow) and therefore meet the criteria.

The suggestion of making the interpretation explicit is much appreciated!

here is something to study and play with

I implemented a very simple state machine, if you read my tutorial I think you'll recognise the structure.

click to see the code
 /* ============================================
  code is placed under the MIT license
  Copyright (c) 2024 J-M-L
  For the Arduino Forum : https://forum.arduino.cc/u/j-m-l

  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files (the "Software"), to deal
  in the Software without restriction, including without limitation the rights
  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  copies of the Software, and to permit persons to whom the Software is
  furnished to do so, subject to the following conditions:

  The above copyright notice and this permission notice shall be included in
  all copies or substantial portions of the Software.

  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  THE SOFTWARE.
  ===============================================
*/


#include <Toggle.h>

const byte buttonPin = 9;
const byte ledPin = 3;
Toggle button;
unsigned long chrono, randomTime;
byte counter;

enum {STARTING, WAITING_PRESS, COUNTING_DOWN, IN_RANDOM_WAIT, WAITING_RELEASE} state = STARTING;

void setup() {
  pinMode(ledPin, OUTPUT);
  Serial.begin(115200);
  button.begin(buttonPin);
  // make sure we start with the button released
  while (true) {
    button.poll();
    if (button.isReleased()) break;
  }
}

void loop() {
  button.poll();
  switch (state) {
    case STARTING:
      digitalWrite(ledPin, LOW);
      Serial.println(F("\n-------------------------------"));
      Serial.println(F("Ready, press and hold to start."));
      Serial.println(F("-------------------------------"));
      Serial.flush();
      state = WAITING_PRESS;
      break;

    case WAITING_PRESS:
      if (button.onPress()) {
        digitalWrite(ledPin, HIGH);
        chrono = millis();
        counter = 3;
        state = COUNTING_DOWN;
        Serial.println(counter);
      }
      break;

    case COUNTING_DOWN:
      if (millis() - chrono >= 1000) {
        chrono = millis();
        Serial.println(--counter);
        if (counter == 0) {
          randomTime = random(5000);
          state = IN_RANDOM_WAIT;
        }
      }
      else if (button.onRelease()) {
        Serial.println(F("Released while counting down... It's waaay too early! You lost."));
        state = STARTING;
      }
      break;

    case IN_RANDOM_WAIT:
      if (button.onRelease()) {
        Serial.println(F("Released during random wait. It's too early! You lost."));
        state = STARTING;
      }
      else if (millis() - chrono >= randomTime) {
        chrono = micros();
        digitalWrite(ledPin, LOW);
        state = WAITING_RELEASE;
      }
      break;

    case WAITING_RELEASE:
      if (button.onRelease()) {
        unsigned long reactionTime = micros() - chrono;
        Serial.print(F("Reaction time = "));
        Serial.print(reactionTime / 1000.0, 2);
        Serial.println(F(" ms."));
        state = STARTING;
      }
      break;
  }
}
1 Like

Wow, this made the amount of code much smaller!
Going through the provided documentation is on my to-do list!

Everything seems to work fine now and I got a lot of good feedback which contributes to my knowledge of coding, but I can't help but be curious about one more thing:

Would there be an explanation for why the first called value through "Millis()" is larger than the second called value of the same function?

in which version of the code?

It is hard to say. The output in #1 doesn't seem possible from the code in #1. But in some places, it looks like the second value is set from a later call to mills():

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