Cannot use while and millis correctly in countdown

I started a thread yesterday about manually starting BlinkWithoutDelay with a button switch. How to manually start Blink Without Delay - #5 by Pimpom. Thanks to @PaulRB and others, I inserted a while line in setup() and it worked perfectly with my slightly modified version of BWoD.

Then I tried the same principle with a somewhat longer program and it doesn't work so well. The object is to drive four outputs HIGH progressively - not one at a time. That is, 1, 1+2, 1+2+3, 1+2+3+4.

There are two points of failure, one related to the while line and the other not related to it.

  1. Unrelated to while: I tried to apply the idea of moving zero time forward at each step so that it can be used as the starting time by the next step. thus -
if (currentMillis - previousMillis >= xxx) //where xxx is the desired interval
{
previousMillis = currentMillis;
digitalWrite(n, HIGH);	//where n is the particularly pin to go high
}

This doesn't work, with or without while.. It doesn't progress beyond the first step.

  1. Related to while: I removed the call to move previousMillis forward at each step and specified the time separately for each step. Thus -
if (currentMillis - previousMillis >= xxx) //xxx is different for each step
digitalWrite(n, HIGH);

This works as intended without while in setup. But the process begins immediately at startup. When I introduce while in setup, things become erratic. Either all outputs go HIGH immediately or only one or two happen in progression.

The initial code is as below, modifications tried as above and in other ways. What am I doing wrong?

unsigned long previousMillis = 0;

void setup ()
{
pinMode(SREG_T, INPUT_PULLUP);  //Reset
pinMode(2, INPUT_PULLUP);       //Start countdown
pinMode(3, INPUT_PULLUP);     // Jumpstart L
pinMode(4, INPUT_PULLUP);     // Jumpstart R
pinMode(5, INPUT_PULLUP);     //Reserved
pinMode(7, OUTPUT);     //Count1 amber
pinMode(8, OUTPUT);     //Count2 amber
pinMode(9, OUTPUT);     //Count3 amber
pinMode(10, OUTPUT);    //GO! green
pinMode(11, OUTPUT);    //Jumpstart R red
pinMode(12, OUTPUT);    //Jumpstart L red
while (digitalRead(2) == HIGH);   //Hold start until pressed
}

void  loop()
{
if (digitalRead(3) == LOW)  //Jumpstart input L
digitalWrite(12, HIGH);     //Jumpstart light L

if (digitalRead(4) == LOW)  //Jumpstart input R
digitalWrite(11, HIGH);     //Jumpstart light R

unsigned long currentMillis = millis();

if (currentMillis - previousMillis >= 500)
{
previousMillis = currentMillis;
digitalWrite(7, HIGH);
}

if (currentMillis - previousMillis >= 500)
{
previousMillis = currentMillis;
digitalWrite(8, HIGH);
}

if (currentMillis - previousMillis >= 500)
{
previousMillis = currentMillis;
digitalWrite(9, HIGH);
}

if (currentMillis - previousMillis >= 500)
{
previousMillis = currentMillis;
digitalWrite(10, HIGH);
}
}

each of these timers needs its own previousMillis

you can uses arrays for those previousMillis variables as well as the pins and the timePeriods for each

Given the code posted, that is what I would expect. the first IF statement evaluates to true and pin 7 goes high. The value of previousMillis is set to currentMillis. The following statements then evaluate to false until 500 milliseconds later when the cycle repeats.

Since you have not posted the alternative while loop version, it is difficult to comment on that question

const int  N = 3;

const byte    PinOut      [N] = { 12, 11, 10 };
unsigned long MsecPeriod  [N] = { 500, 200, 300 };
unsigned long msecPrev    [N];

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

    for (int n = 0; n < N; n++)  {
        if (msec - msecPrev [n] >= MsecPeriod [n])  {
            msecPrev [n] = msec;
            digitalWrite (PinOut [n], ! digitalRead (PinOut [n]));
        }
    }
}

void setup ()
{
    for (int n = 0; n < N; n++)
        pinMode (PinOut [n], OUTPUT);
}

Thanks for the replies, gcjr and BitSeeker.
Give me some time to digest your advices and try them out. I'm very new to any kind of programming and it will take me some time.

To reiterate the second part of the problem: The process works when I removed the progressive reference to a changing previousMillis, but not when I control the start time with while in setup.

That's what I thought I did with this modification. It works as intended without while to control the start time. Inserting while (digitalRead(2) == HIGH); in setup messes up the operation.

unsigned long previousMillis = 0;

void setup ()
{
pinMode(SREG_T, INPUT_PULLUP);  //Reset
pinMode(2, INPUT_PULLUP);   //Start
pinMode(3, INPUT_PULLUP);     // Jumpstart L
pinMode(4, INPUT_PULLUP);     // Jumpstart R
pinMode(5, INPUT_PULLUP);     //Reserved
pinMode(7, OUTPUT);     //Count1 amber
pinMode(8, OUTPUT);     //Count2 amber
pinMode(9, OUTPUT);     //Count3 amber
pinMode(10, OUTPUT);    //GO! green
pinMode(11, OUTPUT);    //Jumpstart R red
pinMode(12, OUTPUT);    //Jumpstart L red
while (digitalRead(2) == HIGH);   //Hold start until pressed
}

void  loop()
{
if (digitalRead(3) == LOW)  //Jumpstart input L
digitalWrite(12, HIGH);   //Jumpstart light L

if (digitalRead(4) == LOW)  //Jumpstart input R
digitalWrite(11, HIGH);   //Jumpstart light R

unsigned long currentMillis = millis();

if (currentMillis - previousMillis >= 500)
digitalWrite(7, HIGH);

if (currentMillis - previousMillis >= 1000)
digitalWrite(8, HIGH);

if (currentMillis - previousMillis >= 1500)
digitalWrite(9, HIGH);

if (currentMillis - previousMillis >= 2000)
digitalWrite(10, HIGH);
}

don't see why. looks like you wnated different periods: 500, 1000, 1500, 2000

Can you please describe the project, overall?
Is this for a game or something like that?

After some thought, I think I see why.

No matter what the code is, the MCU starts counting millis from bootup. When nothing delays the loop, everything works as expected. But if starting the loop is delayed by waiting for a button press, the countdown process has already reached its end so that all outputs immediately go HIGH.

What's needed is a way to make the timer start counting the millis only when the start button is pressed. I thought the line "unsigned long currentMillis = millis();" would do that. Apparently, it doesn't.

  playerState = digitalRead(playerButton);
  while (playerState != lastPlayerState) {
    playerState = digitalRead(playerButton);
    awaitingPlayerMessage();
    viewHighScore();
  }
  lastPlayerState = playerState;

Here's a snippet of how I performed a "hold until button pressed" in void setup() for a game.

Ok, the whole code from which my previous reply's snippet was extracted does what you're after, I think.

/* Blast 'Em! Note to self: DO NOT MODIFY!
 
  by Hallowed31
    May 14-15, 2024

    Additional parts needed: 
     - One or two light-dependent resistors. No 
       additional pullup or pulldown resistors needed.
     - One or two servos. Attach some target to the servo
       horn after installing LDR through each one, like a 
       bullseye.
     - One pushbutton. 
     - Some light source as blaster. Cat laser toy, perhaps
     - External power supply for servos. Never use Arduino
       +5V regulator to power servos.
     - Computer connected to Arduino to use Serial Monitor
       or why not try your favorite terminal emulation program
       such as puTTY or CoolTerm?  

    Gameplay circuit:
     - LDRs from A0 and A1 to ground.

     - Servos powered externally (+5V) from Arduino, signal pins
        to D9 and D10 and don't forget
        to ground servos to both Arduino AND external power supply

     - Start button (pushbutton) to D2 and Arduino GND. 

     - Works with one sensor/servo combo or two. 

     - Known issue: the timer will occasionally overflow/keep running
          when cycling between modes such as manual, BIOS, gameplay

     - Ensure Serial monitor is open, default size prints prettiest.
        Autoscroll should be checked, show timestamp unchecked, 
        set to 115200 baud, no line ending.

     - CoolTerm settings: "blastEm.stc" 
        
*/
#include <EEPROM.h>
#include <Servo.h>

Servo servoOne, servoTwo;

int targetOne = A0;
int targetTwo = A1;
int led = 13;
int playerButton = 2;
int target1Value, target2Value;
int gameMode, highScore, playerState, lastPlayerState;
int eeAddress;
unsigned long score, gameTime, lastGameTime, gameTimeLimit, targetDown, lastTargetDown, tar2Down, lastTar2Down;
unsigned long previousGameTimer;
const long interval = 1000;
const long biosWait = 3200;
unsigned long prevBios = 0;
int timer = 30;
int biosTimer = 3;
int bios;

void  setup() {
  Serial.begin(115200);
  eeAddress = 0;
  pinMode(targetOne, INPUT_PULLUP);
  pinMode(targetTwo, INPUT_PULLUP);
  pinMode(playerButton, INPUT_PULLUP);
  pinMode(led, OUTPUT);
  /* uncomment resetHighScore() function and upload to reset
    saved high score to zero,
    then comment out again, upload sketch again
    to resume playing normal game */
  // resetHighScore();
  servoOne.attach(10);
  servoTwo.attach(11);
  servoOne.write(0);
  servoTwo.write(0);
  cycleServos();
  gameTimeLimit = 31330;
  targetDown = 500;
  tar2Down = 500;
  lastTargetDown = 0;
  lastTar2Down = 0;
  lastPlayerState = LOW;
  target1Value = 0;
  target2Value = 0;
  credits();
  biosPrompt();
  unsigned long biosT = millis();
  while (biosT + biosWait >= millis()) {
    unsigned long thisBiosTimer = millis();
    if (Serial.available() > 0) {
      char biosMode = Serial.read();
      switch (biosMode) {
        case 'b':
          bios = true;
          break;
        case 'B':
          bios = true;
          break;
      }
    }
    if (thisBiosTimer - prevBios >= interval) {
      if (biosTimer > 0) {
        Serial.println(biosTimer);
      }
      biosTimer --;
      if (biosTimer == -1) {
        timer = 0;
      }
      prevBios = thisBiosTimer;
    }
  }
  playerState = digitalRead(playerButton);
  while (playerState != lastPlayerState) {
    playerState = digitalRead(playerButton);
    awaitingPlayerMessage();
    viewHighScore();
  }
  lastPlayerState = playerState;
  score = 0;
  gameTime = 0;
  lastGameTime = 0;
  previousGameTimer = 0;
  if (bios == true) {
    gameMode = 1;
  }
  else if (bios == false) {
    gameMode = 2;
  }
}

void loop() {
  // start game timer
  gameTime = millis();
  if (Serial.available() > 0) {
    char switchMode = Serial.read();
    switch (switchMode) {
      case 'b':
        gameMode = 1; // viewBios/menu
        break;
      case 'n':
        gameMode = 2; // new player
        break;
      case ' ':
        gameMode = 3; // play game
        break;
      case 'r':
        gameMode = 4; // gameover
        break;
      case '?':
        gameMode = 5; // view raw
        break;
      case 'h':
        gameMode = 6; // view hi scores
        break;
      case 'm':
        gameMode = 7; // read manual
        break;
    }
  }
  switch (gameMode) {
    case 1:
      viewBios();
      lastGameTime = gameTime;
      break;
    case 2:
      newPlayer();
      lastGameTime = gameTime;
      break;
    case 3:
      normalGameplay();
      if (gameTime - lastGameTime > gameTimeLimit) { // outta time
        gameMode = 4;
      }
      break;
    case 4:
      gameOver();
      break;
    case 5:
      viewRawTargetReadings();
      break;
    case 6:
      viewHighScore();
      break;
    case 7:
      manual();
      break;
  }
}
// gameMode 2
void newPlayer() {
  newPlayerMessage();
  lastGameTime = 0;
  score = 0;
  timer = 30;
  gameMode = 3;
}
// gameMode 3
void normalGameplay() {
  unsigned long thisGameTimer = millis();
  if (thisGameTimer - previousGameTimer >= interval) {
    timer --;
    if (timer == -1) {
      timer = 30;
    }
    previousGameTimer = thisGameTimer;
  }
  target1Value = analogRead(targetOne);
  target2Value = analogRead(targetTwo);
  if (target1Value < 75) {
    lastTargetDown = gameTime;
    if (gameTime - lastTargetDown <= targetDown) {
      digitalWrite(led,  HIGH);
      servoOne.write(90);
      score = score + 1;
      if (score > highScore) {
        highScore = score;
        EEPROM.put(eeAddress, highScore);
      }
    }
  }
  if (target2Value < 75) {
    lastTar2Down = gameTime;
    if (gameTime - lastTar2Down <= tar2Down) {
      digitalWrite(led,  HIGH);
      servoTwo.write(90);
      score = score + 1;
      if (score > highScore) {
        highScore = score;
        EEPROM.put(eeAddress, highScore);
      }
    }
  }
  if (gameTime - lastTargetDown >= targetDown) {
    digitalWrite(led, LOW);
    servoOne.write(0);
  }
  if (gameTime - lastTar2Down >= tar2Down) {
    digitalWrite(led, LOW);
    servoTwo.write(0);
  }
  Serial.print("score ");
  Serial.print(score);
  Serial.print("\t\t");
  if (timer >= 10) {
    Serial.print("Time ");
  }
  else if (timer < 10) {
    Serial.print("Time 0");
  }
  Serial.println(timer);
}

// gameMode 4
void gameOver() {
  gameOverMessage();
  awaitingPlayerMessage();
  playerState = digitalRead(playerButton);
  delay(20); // simple debouncing
  playerState = digitalRead(playerButton);
  if (playerState == LOW) {
    gameMode = 2; // reset
  }
}

/*****************************************************
 ***********        UTILITIES       ******************
 *****************************************************/
 unsigned long ma, mb, mc, lastMa, lastMb, lastMc;

void viewBios() {
  Serial.println();
  Serial.println(F("                              Blast 'Em v2"));
  Serial.println();
  Serial.println(F("            Type SPACE during game to resume normal gameplay"));
  Serial.println();
  Serial.println(("                   Type r during game to reset game"));
  Serial.println();
  Serial.println(("                        Type m to view manual"));
  Serial.println(("               Type ? during game to view raw sensor readings"));
  Serial.println(("           Type h during game to pause game and see high score"));
  EEPROM.get(eeAddress, highScore);
  Serial.println();
  Serial.print(("                 All Time High Score: "));
  Serial.println(highScore);
  Serial.println();
  Serial.println(("                     Type X to exit BIOS and return"));
  Serial.println();
  while (!Serial.available()) {
  }
  if (Serial.available() > 0) {
    char myBios = Serial.read();
    switch (myBios) {
      case 'x':
        gameMode = 2;
        bios = false;
        lastGameTime = gameTime;
        break;
      case 'X':
        gameMode = 2;
        bios = false;
        lastGameTime = gameTime;
        break;
      case ' ':
        gameMode = 3; // play game
        break;
      case 'r':
        gameMode = 4; // gameover
        break;
      case '?':
        gameMode = 5; // view raw
        break;
      case 'h':
        gameMode = 6; // view hi scores
        break;
      case 'm':
        gameMode = 7; // read manual
        break;
    }
  }
}


void manual() {
  Serial.println(F("press button when prompted to begin normal play"));
  Serial.println();
  Serial.println(F("Enter key commands during game and press ENTER"));
  Serial.println();
  Serial.println(("Key Commands: "));
  Serial.println(("  ? during game to view sensor data for calibration"));
  Serial.println(("  h to view high score"));
  Serial.println(("  r to reset game"));
  Serial.println(("  h to view high score"));
  Serial.println(("  Space bar + Enter to return to game time and score"));
  Serial.println(("  r to reset game"));
  Serial.println();
  Serial.println();
  Serial.println(("                             Type X to return"));
  Serial.println();
   while (!Serial.available()) {
  }
    if (Serial.available() > 0) {
      char escape = Serial.read();
      switch (escape) {
        case 'x':
          gameMode = 2;
          lastGameTime = gameTime;
          break;
        case 'X':
          gameMode = 2;
          lastGameTime = gameTime;
          break;
      }
    }
}

// called in newPlayer() gameMode 2
void newPlayerMessage() {
  score = 0;
  Serial.println();
  Serial.println(F("                         New Player Blast Em!"));
  Serial.println();
  delay(2000);
  Serial.print("score ");
  Serial.println(score);
}

// called in gameOver() gameMode 4
void awaitingPlayerMessage() {
  ma = millis();
  const unsigned long maTime = 5000;
  if (ma - lastMa >= maTime) {
    lastMa = ma;
    Serial.println();
    Serial.println(F("                          Press Button to Play"));
    Serial.println();
    Serial.println(F("                            Awaiting Player"));
    Serial.println();
  }
}

// called in gameOver() gameMode 4
void gameOverMessage() {
  mb = millis();
  const unsigned long mbTime = 7000;
  if (mb - lastMb >= mbTime) {
    lastMb = mb;
    Serial.println();
    Serial.println(F("                     Time's Up -------- Game Over"));
    Serial.println();
    Serial.print(F("                   Your Score: "));
    Serial.print(score);
    Serial.print(F("     High Score: "));
    Serial.println(highScore);
    Serial.println();
  }
}

// gameMode 6
void viewHighScore() {
  mc = millis();
  EEPROM.get(eeAddress, highScore);
  const unsigned long mcTime = 11000;
  if (mc - lastMc >= mcTime) {
    lastMc = mc;
    Serial.println();
    Serial.print(F("                            High Score: "));
    Serial.println(highScore);
    Serial.println();
  }
}

void credits() {
  Serial.println(F("Blast 'Em!"));
  Serial.println(F("by Hallowed31"));
  delay(1000);
  for (int i = 0; i < 3; i++) {
    Serial.println();
    delay(200);
  }
}

void biosPrompt() {
  Serial.println(F("Type in B and use button"));
  Serial.println(F("at prompt to enter BIOS"));
  delay(1500);
  for (int i = 0; i < 3; i++) {
    Serial.println();
    delay(200);
  }
}

// gameMode 5
void viewRawTargetReadings() {
  unsigned long readInterval = 1000;
  if (gameTime - lastGameTime >= readInterval) {
    lastGameTime = gameTime;
    target1Value = analogRead(targetOne);
    target2Value = analogRead(targetTwo);
    Serial.print(F("ADC readings :  Target 1 (A0) value is "));
    Serial.print(target1Value);  // for checking Photoresistor  input
    Serial.print(F("  Target 2 (A1) value is "));
    Serial.println(target2Value);
    Serial.println();
  }
}

void resetHighScore() {
  highScore = 0;
  EEPROM.put(eeAddress, highScore);
  while (1);
}

void cycleServos() {
  servoOne.write(90);
  servoTwo.write(90);
  delay(250);
  servoOne.write(0);
  servoTwo.write(0);
}

It's a starting system for drag races, often called a "Christmas Tree" because it has 36 coloured lights on a frame. There are many simple projects on the internet about lighting up LEDs to display a countdown but a full-fledged Christmas Tree is a bit more involved.

Several sensors monitor the position of each rider at the starting line. Once the countdown has started, if a rider moves even an inch out of position, the sensors detect it, a red light comes on and the rider is disqualified.

I made a full-fledged Tree 15 years ago complying with FIA and NHRA standards for a motorsports association. It worked perfectly and is still in good condition. At that time, I knew even less about MCUs and programming than I do now (which is still very little). So I did everything with analog and fixed logic chips. I even threw in a winner detector as a bonus. They loved it.

A recent face-to-face chat with some racing enthusiasts made me think about building a new Christmas Tree, this time with an MCU at the heart. The existing one has no functional shortcomings. I just want to do it differently this time.

1 Like

look this over

const int  N          = 4;
const byte PinLeds [] = { 9, 10, 11, 12 };

const byte PinStart   = 2;
const byte PinJumpL   = 3;
const byte PinJumpR   = 4;

unsigned long msec0;
bool          start;
int           idx;

enum { Off = HIGH, On = LOW };

// -----------------------------------------------------------------------------
void  loop()
{
    if (digitalRead(PinJumpL) == LOW)  //Jumpstart input L
        digitalWrite(12, HIGH);   //Jumpstart light L

    if (digitalRead(PinJumpR) == LOW)  //Jumpstart input R
        digitalWrite(11, HIGH);   //Jumpstart light R

    // timer
    unsigned long msec = millis();

    if (start)  {
        if (msec - msec0 >= 500) {
            msec0 = msec;

            Serial.println (idx);
            digitalWrite(PinLeds [idx], On);
            idx++;
            if (N <= idx)
                start = false;
        }
    }

    // check if start pressed
    if (! start && digitalRead(PinStart) == LOW)  {
        Serial.println ("start");
        for (int n = 0; n < N; n++)
            digitalWrite(PinLeds [n], Off);
        start = true;
        msec0 = msec;
        idx   = 0;
    }
}


void setup ()
{
    Serial.begin (9600);

    pinMode(SREG_T, INPUT_PULLUP);  //Reset
    pinMode(2, INPUT_PULLUP);   //Start
    pinMode(3, INPUT_PULLUP);     // Jumpstart L
    pinMode(4, INPUT_PULLUP);     // Jumpstart R
    pinMode(5, INPUT_PULLUP);     //Reserved
    pinMode(7, OUTPUT);     //Count1 amber
    pinMode(8, OUTPUT);     //Count2 amber
    pinMode(9, OUTPUT);     //Count3 amber
    pinMode(10, OUTPUT);    //GO! green
    pinMode(11, OUTPUT);    //Jumpstart R red
    pinMode(12, OUTPUT);    //Jumpstart L red

    for (int n = 0; n < N; n++)
        digitalWrite(PinLeds [n], Off);
}

Check out namespaces.
If you have "unsigned long currentMillis = millis();" in setup(), then currentMillis cannot be seen outside of setup().

unsigned long currentMillis;  \\ declare here in the global space

setup (){
    ...
    while (digitalRead(2) == HIGH);
    currentMillis = millis();  \\ use here and everywhere
}

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