Demonstration code for several things at the same time

NOTE for newcomers. The demo is entirely contained in this Post and Reply #1. There is no need to read further unless you are interested. ...added 25Sep2014

There have been a few occasions recently where newcomers seemed to have difficulty applying the "blink without delay" and "state" concepts to a sketch that is intended to manage a number of concurrent actions.

Its very time consuming to respond with suitable code examples - particularly as every case is a little different.

I have prepared the attached example sketch in the hope that it will be a suitable model. I have tested it on an Uno and a Mega. I have called it "SeveralThingsAtTheSameTime.ino". (It seems to be too long to show the code directly in this post)

My sketch uses the concept in "blink without delay" to cause three LEDs to blink at different intervals, a fourth LED is controlled by a button and a servo sweeps back and forth at two different speeds. The idea is to demonstrate how different processes can be accommodated in the same general framework.

It also uses the "state machine" concept to manage the various activities and enable the different functions to determine what to do.

I have deliberately designed the sketch as a series of short functions. Short pieces of code are much easier to understand and debug and it will be an easy matter for a user to delete functions that they don't need or to duplicate and modify functions if they need more of the same (for example to flash 5 LEDS at different intervals).

There is a case for saying there is too much in the sketch but I concluded that the larger number of activities is a better demonstration of the capabilities of this approach.

Comments are welcome.

...R

Edit ... the attached sketch has been revised to take account of comments below about improving the timekeeping ...R

SeveralThingsAtTheSameTimeRev1.ino (9.37 KB)

21 Likes
// SeveralThingsAtTheSameTimeRev1.ino

// An expansion of the BlinkWithoutDelay concept to illustrate how a script
//  can appear to do several things at the same time

// this sketch does the following
//    it blinks the onboard LED (as in the blinkWithoutDelay sketch)
//    it blinks two external LEDs (LedA and LedB) that are connected to pins 12 and 11.
//    it turns another Led (buttonLed connected to pin 10) on or off whenever a button
//       connected to pin 7 is pressed
//    it sweeps a servo (connected to pin 5) back and forth at different speeds

//  One leg of each LED should be connected to the relevant pin and the other leg should be connected to a
//   resistor of 470 ohms or more and the other end of the resistor to the Arduino GND. 
//   If the LED doesn't light its probably connected the wrong way round.

//  On my Uno and Mega the "button" is just a piece of wire inserted into pin 7. 
//   Touching the end of the wire with a moist finger is sufficient to cause the switching action
//   Of course a proper press-on-release-off button switch could also be used!

//  The Arduino is not capable of supplying enough 5v power to operate a servo
//    The servo should have it's own power supply and the power supply Ground should
//      be connected to the Arduino Ground.

// The sketch is written to illustrate a few different programming features.
//    The use of many functions with short pieces of code. 
//       Short pieces of code are much easier to follow and debug
//    The use of variables to record the state of something (e.g. onBoardLedState) as a means to
//       enable the different functions to determine what to do.
//    The use of millis() to manage the timing of activities
//    The definition of all numbers used by the program at the top of the sketch where 
//       they can easily be found if they need to be changed

//=======

// -----LIBRARIES

#include <Servo.h>

// ----CONSTANTS (won't change)

const int onBoardLedPin =  13;      // the pin numbers for the LEDs
const int led_A_Pin = 12;
const int led_B_Pin = 11;
const int buttonLed_Pin = 10;

const int buttonPin = 7; // the pin number for the button

const int servoPin = 5; // the pin number for the servo signal

const int onBoardLedInterval = 500; // number of millisecs between blinks
const int led_A_Interval = 2500;
const int led_B_Interval = 4500;

const int blinkDuration = 500; // number of millisecs that Led's are on - all three leds use this

const int buttonInterval = 300; // number of millisecs between button readings

const int servoMinDegrees = 20; // the limits to servo movement
const int servoMaxDegrees = 150;


//------- VARIABLES (will change)

byte onBoardLedState = LOW;             // used to record whether the LEDs are on or off
byte led_A_State = LOW;           //   LOW = off
byte led_B_State = LOW;
byte buttonLed_State = LOW;

Servo myservo;  // create servo object to control a servo 

int servoPosition = 90;     // the current angle of the servo - starting at 90.
int servoSlowInterval = 80; // millisecs between servo moves
int servoFastInterval = 10;
int servoInterval = servoSlowInterval; // initial millisecs between servo moves
int servoDegrees = 2;       // amount servo moves at each step 
                            //    will be changed to negative value for movement in the other direction

unsigned long currentMillis = 0;    // stores the value of millis() in each iteration of loop()
unsigned long previousOnBoardLedMillis = 0;   // will store last time the LED was updated
unsigned long previousLed_A_Millis = 0;
unsigned long previousLed_B_Millis = 0;

unsigned long previousButtonMillis = 0; // time when button press last checked

unsigned long previousServoMillis = 0; // the time when the servo was last moved

//========

void setup() {

  Serial.begin(9600);
  Serial.println("Starting SeveralThingsAtTheSameTimeRev1.ino");  // so we know what sketch is running
  
      // set the Led pins as output:
  pinMode(onBoardLedPin, OUTPUT);
  pinMode(led_A_Pin, OUTPUT);
  pinMode(led_B_Pin, OUTPUT);
  pinMode(buttonLed_Pin, OUTPUT);
  
      // set the button pin as input with a pullup resistor to ensure it defaults to HIGH
  pinMode(buttonPin, INPUT_PULLUP);
  
  myservo.write(servoPosition); // sets the initial position
  myservo.attach(servoPin);
 
}

//=======

void loop() {

      // Notice that none of the action happens in loop() apart from reading millis()
      //   it just calls the functions that have the action code

  currentMillis = millis();   // capture the latest value of millis()
                              //   this is equivalent to noting the time from a clock
                              //   use the same time for all LED flashes to keep them synchronized
  
  readButton();               // call the functions that do the work
  updateOnBoardLedState();
  updateLed_A_State();
  updateLed_B_State();
  switchLeds();
  servoSweep();

}

//========

void updateOnBoardLedState() {

  if (onBoardLedState == LOW) {
          // if the Led is off, we must wait for the interval to expire before turning it on
    if (currentMillis - previousOnBoardLedMillis >= onBoardLedInterval) {
          // time is up, so change the state to HIGH
       onBoardLedState = HIGH;
          // and save the time when we made the change
       previousOnBoardLedMillis += onBoardLedInterval;
          // NOTE: The previous line could alternatively be
          //              previousOnBoardLedMillis = currentMillis
          //        which is the style used in the BlinkWithoutDelay example sketch
          //        Adding on the interval is a better way to ensure that succesive periods are identical

    }
  }
  else {  // i.e. if onBoardLedState is HIGH
  
          // if the Led is on, we must wait for the duration to expire before turning it off
    if (currentMillis - previousOnBoardLedMillis >= blinkDuration) {
          // time is up, so change the state to LOW
       onBoardLedState = LOW;
          // and save the time when we made the change
       previousOnBoardLedMillis += blinkDuration;
    } 
  }
}

//=======

void updateLed_A_State() {

  if (led_A_State == LOW) {
    if (currentMillis - previousLed_A_Millis >= led_A_Interval) {
       led_A_State = HIGH;
       previousLed_A_Millis += led_A_Interval;
    }
  }
  else {
    if (currentMillis - previousLed_A_Millis >= blinkDuration) {
       led_A_State = LOW;
       previousLed_A_Millis += blinkDuration;
    } 
  }    
}

//=======

void updateLed_B_State() {

  if (led_B_State == LOW) {
    if (currentMillis - previousLed_B_Millis >= led_B_Interval) {
       led_B_State = HIGH;
       previousLed_B_Millis += led_B_Interval;
    }
  }
  else {
    if (currentMillis - previousLed_B_Millis >= blinkDuration) {
       led_B_State = LOW;
       previousLed_B_Millis += blinkDuration;
    }
  }    
}

//========

void switchLeds() {
      // this is the code that actually switches the LEDs on and off

  digitalWrite(onBoardLedPin, onBoardLedState);
  digitalWrite(led_A_Pin, led_A_State);
  digitalWrite(led_B_Pin, led_B_State);
  digitalWrite(buttonLed_Pin, buttonLed_State);
}

//=======

void readButton() {

      // this only reads the button state after the button interval has elapsed
      //  this avoids multiple flashes if the button bounces
      // every time the button is pressed it changes buttonLed_State causing the Led to go on or off
      // Notice that there is no need to synchronize this use of millis() with the flashing Leds
  
  if (millis() - previousButtonMillis >= buttonInterval) {

    if (digitalRead(buttonPin) == LOW) {
      buttonLed_State = ! buttonLed_State; // this changes it to LOW if it was HIGH 
                                           //   and to HIGH if it was LOW
      previousButtonMillis += buttonInterval;
    }
  }

}

//========

void servoSweep() {

      // this is similar to the servo sweep example except that it uses millis() rather than delay()

      // nothing happens unless the interval has expired
      // the value of currentMillis was set in loop()
  
  if (currentMillis - previousServoMillis >= servoInterval) {
        // its time for another move
    previousServoMillis += servoInterval;
    
    servoPosition = servoPosition + servoDegrees; // servoDegrees might be negative

    if (servoPosition <= servoMinDegrees) {
          // when the servo gets to its minimum position change the interval to change the speed
       if (servoInterval == servoSlowInterval) {
         servoInterval = servoFastInterval;
       }
       else {
        servoInterval = servoSlowInterval;
       }
    }
    if ((servoPosition >= servoMaxDegrees) || (servoPosition <= servoMinDegrees))  {
          // if the servo is at either extreme change the sign of the degrees to make it move the other way
      servoDegrees = - servoDegrees; // reverse direction
          // and update the position to ensure it is within range
      servoPosition = servoPosition + servoDegrees; 
    }
        // make the servo move to the next position
    myservo.write(servoPosition);
        // and record the time when the move happened
  }
}

//=====END
6 Likes

Darn good idea :slight_smile:

1 Like

I prefer using switch statements for may state variables rather than ifs, even if there's only two states something can be in. I find it easier to follow than trying to match an else with the right if to find out what's going on, especially if they're nested.

So instead of:

void updateLed_A_State() {

  if (led_A_State == LOW) {
    if (currentMillis - previousLed_A_Millis >= led_A_Interval) {
       led_A_State = HIGH;
       previousLed_A_Millis = currentMillis;
    }
  }
  else {
    if (currentMillis - previousLed_A_Millis >= blinkDuration) {
       led_A_State = LOW;
       previousLed_A_Millis = currentMillis;
    } 
  }    
}

I prefer:

void updateLed_A_State()
{
  switch( led_A_State )
  {
    case LOW:
      if (currentMillis - previousLed_A_Millis >= led_A_Interval)
      {
        led_A_State = HIGH;
        previousLed_A_Millis = currentMillis;
      }
    
    case HIGH:
      if (currentMillis - previousLed_A_Millis >= blinkDuration) 
      {
        led_A_State = LOW;
        previousLed_A_Millis = currentMillis;
      }
    default:
      ;
  }
}

I also prefer a different bracketing style to yours, but that's neither here nor there.

4 Likes

Jiggy-Ninja:
I also prefer a different bracketing style to yours, but that's neither here nor there.

Ah, but if you use "Auto Format", it arranges things in a certain way for you.

I debated using switch statements (even just using them in one function to illustrate how they are an alternative to if statements). In the end I decided that it would just add to the learning burden for new users. It would be an unusual program that could avoid if statements but switch statements are always optional - even if convenient when you know them.

I like my bracketing system. The formatting was done manually in Gedit. If I have not been consistent everywhere I apologize.

...R

1 Like
...
    if (currentMillis - previousOnBoardLedMillis >= onBoardLedInterval) {
...
       previousOnBoardLedMillis = currentMillis;
    }

The average interval will be slightly to significantly more than onBoardLedInterval. For most uses (like blinking an LED) the longer interval is insignificant. For some uses the longer interval is a failure.

       previousOnBoardLedMillis += onBoardLedInterval;

...produces a bit more code but keeps the average interval at exactly onBoardLedInterval.

2 Likes

@CodingBadly, I'm interested but I don't understand exactly what you mean by "The average interval will be slightly to significantly more than onBoardLedInterval"

My plan was/is to use a single instant in time (as captured in currentMillis) as the reference point for the flashing of all of the leds so that their timing can't drift apart.

I can't figure out what effect your code would have.

...R

Nice long example (not all examples can be 1<10 lines).

I wondered what the difference was between the attached file in the first and reply#1 with the code inline. My editor only shows comments lines are shorter :grin:

I would do it slightly different (which means neither better nor worse) :slight_smile:

I do not quite get the educational value of having update_LED_state() and switchLEDs() as seperate functions - it means you have to carry LED-desired-state as global variables, when they could be nicely encapsulated. Place most variables as static inside the function where they are exclusivly used - this way you can not inadvertently modify a timer variable elsewhere.

CodingBadly's comment is that if your loop takes - say - 5 millisec to execute (because of some not-yet-written heavy calculation code), then the LEDs will blink at the interval+5ms. By adding "interval" to the "timer" (not the measured millis() value) you get it to run at the "interval" rate. For a "real" project it may be desirable to include the loop overhead/jitter, for some you want a steady rate.

That is my preferred approach too.

When your things are each implemented in separate self-contained functions s in the original example, I also like to define the state data as local static variables rather than globals. Reducing the scope is beneficial in its own right but in particular this makes copy/paste much safer, and that's something that I would expect novice coders to be doing a lot when starting from examples like this. For example:

void blinkLed()
{
	static const int LED_PIN = 13;
	static const unsigned long BLINK_INTERVAL = 500; // duration of each blink phase
	static unsigned long lastBlinkTime = 0;
	
	if(millis() - lastBlinkTime >= BLINK_INTERVAL)
	{
		digitalWrite(LED_PIN, !digitalRead(LED_PIN));
		lastBlinkTime += BLINK_INTERVAL
	}
}
2 Likes

PeterH:
That is my preferred approach too.

I'm hoping you or @GodingBadly will explain why.

I did consider static local variables but personally I prefer the simplicity and visibility of globals when I'm working with such a small memory space. (I wouldn't do that with PC code). Also I was trying to limit the amount of new stuff that a newbie has to take in.

I understand what you say about copy/paste, but I guess I'm old fashioned enough to prefer users to understand what's going on.

There is an opportunity for someone to use my code as a starting point for a separate lesson on the use of static local variables :slight_smile:

And thanks very much for all the comments.

...R

Two hints...

  1. You assume the delta is always 1. It is not.
  2. You assume the time to execute loop is <= 1 millisecond. In your example that may be true but that will not be true for every application.

Assume millis returns values evenly divisible by seven (0, 7, 14, etcetera) and work through the code by hand.

3 Likes

@jiggy-ninja:
I also prefer the switch statement but you need break statements in there to make it equivalent to the original code. I would write it like this:

void updateLed_A_State()
{
  switch( led_A_State )
  {
    case LOW:
      if (currentMillis - previousLed_A_Millis >= led_A_Interval)
      {
        led_A_State = HIGH;
        previousLed_A_Millis = currentMillis;
      }
      break;
      
    case HIGH:
      if (currentMillis - previousLed_A_Millis >= blinkDuration) 
      {
        led_A_State = LOW;
        previousLed_A_Millis = currentMillis;
      }
      break;
      
    default:
      break;
  }
}

Pete

Msquare:
I wondered what the difference was between the attached file in the first and reply#1 with the code inline. My editor only shows comments lines are shorter :grin:

Sorry, I meant to mention this earlier ...

You are quite right, I was just trying to save enough bytes to get within the upload limit. I actually haven't tested the visible version :frowning:

...R

el_supremo:
@jiggy-ninja:
I also prefer the switch statement but you need break statements in there to make it equivalent to the original code. I would write it like this:

Pete

.< Doh!

And that, ladies and gentlemen, is one argument against using switches.

2 Likes

Hey, no, don't give up on switches!
'if' statements can go haywire too if you forget an 'else'! :slight_smile:

Pete

@CodingBadly and @PeterH,

I think I now see the point you are making. I wrote a couple of spreadsheets to experiment with numbers.

I then set out to modify my sketch but I've run into a very strange problem. All of the functions work fine your way ( += interval) EXCEPT the servo function. After a short period it goes haywire, yet it works perfectly using " = currentMillis".

I think the problem may be due to the much shorter interval (or may be showing up sooner because of the shorter interval). And I think the problem arises because at some stage prevMillis exceeds currentMillis so that currentMillis - prevMillis gives a "negative" number or, since it is an unsigned long, a very large positive number which always satisfies the if statement.

I've had enough for today. I will experiment more tomorrow.

I suspect that the error associated with doing it the "wrong" way (i.e. "= currentMillis") depends on the ratio between the interval between successive values of millis() and the size of interval for the blinks (or whatever). If the blink interval is relatively large the error may not matter. I haven't explored this with my spreadsheet yet.

...R

Robin2:
I suspect that the error associated with doing it the "wrong" way (i.e. "= currentMillis") depends on the ratio between the interval between successive values of millis() and the size of interval for the blinks (or whatever). If the blink interval is relatively large the error may not matter. I haven't explored this with my spreadsheet yet.

The basic problem is that the code may not evaluate the condition at the precise instant that the interval has elapsed. Suppose for the sake of argument that you were trying to take some action every 100ms and your loop took 5ms to execute. The original code would take up to 5ms to notice that the interval had elapsed. Since the next interval is calculated based on the value of millis() at the point where we notice the interval elapsed, this means the next interval will be delayed by up to 5ms too. In effect, any latency noticing that the condition is true will cause all subsequent processing to slip.

The preferred approach always schedules the next event based on when the current event became due, not when the code noticed that it had occurred. This avoids slippage when there is any latency noticing the event.

1 Like

I eventually figured out the problem while lying in bed.

The problem arises in the servo function because I change the interval after every sweep. As initially written "prevMillis = currentMillis" is at the bottom of the function after the interval has been changed. And when the revised version "prevMillis += interval" is used at the same location it erroneously adds the revised interval. When there is a switch from a short to a long interval that means the prevMillis is actually greater than the next version of currentMillis which gives a "negative" answer to the subtraction (actually a large +ve number) which always evaluates to true.

The solution to that problem is to move "prevMillis += interval" to a position before the interval is changed.

HOWEVER .... (and I'm delighted that this problem gave me time to look at the bigger picture)

FIRST ... @CodingBadly said in Reply #11 that I was assuming that delta (the difference between successive values of millis()) was 1. Of course, as he suspected, I hadn't thought about that at all. BUT ... millis() does in fact increment by 1.

SECOND ... (though, I had forgotten) The code in "BlinkWithouDelay" uses "previousMillis = currentMillis; "

MY CONCLUSION ... is that this is a discussion about angels on the head of a pin. So, in the absence of evidence of a real problem I will not post an updated version of my demo sketch. The present version works and is consistent with BlinkWithoutDelay.

I am wondering if I might write a short accompanying essay that covers the points raised here (this one, the use of Switch and the maintaining State in static local variables).

As always, comments are welcome.

...R

Robin2:
BUT ... millis() does in fact increment by 1.

There you go again making assumptions. This time you assume I made a mistake (and failed to test your assumption).

What about when the processor is running at 8 MHz? What about when the fractional part reaches a whole number?