Cooperative Multitasking on Arduino - With Pretty Blinky Lights!

875

8

About: I am an IT professional working in Big Data, but love messing around with IoT devices such as Arduino, Raspberry PI and electronics.

In my first Instructable Motion Activated Automatic LED Stair Lights with Arduino I mentioned the use of "Cooperative Multitasking". So what is it and why is it helpful?

This Instructable attempts to answer the "what is it" and "why is it helpful" question with a simple example that step by step builds up into a more complex example. The end result will hopefully answer an additional question being "Cool! How can I do it?".

As a bonus we will end up with a project consisting of some randomly blinking LED's that will be almost as mesmerizing as a good old log fire - maybe even more so if you use enough LED's! Even if you are not interested in cooperative multitasking, surely, mesmerizing, randomly blinking LED's would be worth your while reading to the end!

This is quite a long instructable, but really it is several projects in one. Each of the projects builds upon the other. Primarily to illustrate how things might start out simple, but can get complicated very easily. Ultimately, a solution is presented that makes life much simpler. If you are time poor, or feel that things are progressing too slowly, you can skip some of the steps as outlined a bit later.

If you just want to get to building the Mega-Blink-Light project, simply read "the bits list", "hooking everything up" and jump straight to the last step.

If you complete the steps in between, you will have the basis of a system that can manage multiple concurrent operations in your own projects.

Step 1: Cooperative Multitasking on Arduino

Definitions

Lets start with some definitions:

  • Multitasking - the ability to perform two or more tasks simultaneously.
  • Cooperative - working together for a common purpose.
  • Finite State Machine - the technique used by the individual cooperatively multitasking tasks. Read more about it on Wikipedia - Finite State Machine.

Hopefully "cooperative" is self explanatory, so let's look a bit closer at Multitasking - what exactly does it mean? How can Arduino work on two or more tasks simultaneously? There is only one CPU, so surely it can only do one thing at a time!?!?!?!?

The answer is "time slicing". That is, dividing up the available time (or CPU cycles) to perform different tasks.

A real life multitasking example (the Chef)

I'm not sure the explanation of time slicing clears much up, so lets consider a real world example. Specifically, a chef cooking a meal. Chances are, chef will be doing lots of things at once. For example, chef might be:

  • Pre-heating the oven,
  • Boiling some water,
  • Chopping some vegetables,
  • Seasoning some steak,,
  • Mixing some ingredients together for a sauce
  • and no doubt more.
    I'm not a good cook, so I'm only guessing. My meals typically consist of boiling water for instant coffee, pouring milk over cereal and/or making toast - almost never at the same time!

Can the chef do all of those things at the same time?

The answer, although some may vehemently disagree, is quite clearly (drum roll please): No!

Chef only has one pair of hands. So unless chef is on octopus or chef is chopping vegetables with just one hand (sounds dangerous) and seasoning steak with another, chef is only able to do one thing at a time. And unless chef has a third hand (perhaps spoon in mouth? Eww!!!) chef certainly isn't mixing the sauce ingredients together at the same time as chef is chopping vegetables with one hand and seasoning steak with the other.

However, we would still say that a chef is multi-tasking. So, what are chefs actually doing?

Good question, I am glad you asked. Chef is (at this point) cooperatively multitasking multiple "sub-tasks" that, together, combine to achieve the larger task of cooking a meal. I expect that the chef might operate more like this:

  1. Turn on the oven. Once turned on, the oven will pre-heat all by itself. The oven doesn't need chef's undivided attention to ensure it is warming up in an approved manner or not warming at all, nor does it require chef to not do anything else. In short, the oven will pre-heat all by itself. Next, chef might...
  2. Find a pot and pour some water into it.
  3. Check that the oven is indeed on and starting to warm.
  4. Put the pot on a hot plate and turn it on. Again, the water will heat up all by itself.
  5. Place all of the sauce ingredients into a bowl. Put it in a mixer and turn it on. The mixer also will mix all by itself.
  6. Season the steak.
  7. Pause seasoning the steak.
  8. Stop the mixer check the consistency of the sauce. It needs more mixing, so turn the mixer back on.
  9. Resume seasoning the steak. This task is now complete. Set the steaks aside.
  10. Start chopping the vegetables.
  11. Pause chopping the vegetables.
  12. Check the oven and note that it is now warm enough. Put steaks in oven. At this point, the steaks will cook, you guessed it, all by themselves.
  13. Stop the mixer. Check the consistency of the sauce. It needs more mixing, so turn the mixer back on.
  14. Resume chopping vegetables.
  15. Pause chopping vegetables. Check Steaks - they need longer. Resume chopping vegetables.
  16. And so on...

Note that the chef isn't personally doing multiple things at the same time. What chef is doing is dividing up the available time, or time slicing, to progress multiple tasks simultaneously.

The above is what multi-tasking is. Also, as I mentioned earlier, the chef cooking a meal example described above is "cooperative multitasking". That is, chef will perform a task for a period of time, voluntarily stop doing it in an orderly fashion then work on something else. Maybe chef will come back to a previously started task (e.g. resume chopping, resume seasoning, pause mixer to check consistency etc) or move on to something new. It will all depend upon what needs to be done next to progress the overall job of cooking the meal.

In each of the sub-tasks listed above, each one will maintain some sort of "state" information that describes exactly where it is in the process. For example, the oven will maintain state information along the lines of "current temperature", "target temperature", "on or off". The mixer will maintain state information of the form "current speed" (e.g. off, low, medium or high). Even the vegetables and steak maintain state information. For example, a vegetable is "washed" or "not washed", "chopped" or "not chopped" etc. Steak has state information in the form of "doneness" (e.g. raw, very rare, rare, medium etc), seasoned or not seasoned, and so on.

I know it might sound a little bit "out there" to describe a steak as having states and therefore treating it as a finite state machine, but it is important to recognise the concept that most, if not everything, can be described as having states. The ability to recognise the states and how things transition from one state to another is important to enable cooperative multi-tasking. So, how does a steak move from one state to another? Within the "doneness" domain, there is a one way transition from raw to very rare to rare etc via the application of heat and time.

Preemptive Multitasking

Another form of multitasking is "preemptive multitasking". An example of preemptive multitasking is as follows:

  • Chef is chopping vegetables, while chopping ....
  • The smoke alarm sounds (it seems chef forgot the steaks which are now in the "too well done" state).
    • Chef immediately stops chopping - possibly mid-chop!
    • Turns off oven
    • Extracts steak from oven
    • Opens window
    • resets smoke alarm
  • Chef resumes chopping vegetables
    Simultaneously chef tries to think of a way to explain to the family how "extra dry and crispy steak" is actually a good thing...

In the above example, chef was not intending to stop chopping vegetables, but an external factor unexpectedly came into play that required chef to stop the current task - urgently! In this case the smoke alarm sounded and this "interrupt" immediately caused chef to "context switch" to dealing with the smoke alarm. Other external factors might be time-slicing. For example, OH&S rules might say that the maximum chopping time is 5 minutes. After 5 minutes, you must do something else - even if you are not finished chopping vegetables. So after 5 minutes, the "chopping vegetables" task will be preempted when it has used up its allotted slice of time (5 minutes) and something else will be done. In a preemptive multitasking system, this switching from one task to another is handled automatically by the operating system. Often, if not always, a running task is not even aware that it was preempted (or resumed).

Preemptive multitasking is much more complicated to implement. Preemptive events occur pretty much at random. As such, there needs to be support for a "context switch". In the case of the vegetables, they do not really know that they've been preempted while being chopped due to the smoke alarm. They are just lying there on the cutting board mid-chop. From the vegetables perspective, time has frozen. At some point the chopping will be resumed.

While cooperative multitasking sounds great, it isn't perfect. It requires that the tasks, well, cooperate. Unlike preemptive multitasking systems, cooperative multitasking systems require that the sub-tasks be coded in such a way as to minimise their use of the CPU and to give up control of the CPU as soon as they can.

This means not hogging the CPU for endless calculations (and in the case of Arduino, almost never executing the delay function or do nothing while loops in a cooperative multitasking environment). If the smoke alarm scenario was managed using a cooperative multitasking model, then when the alarm was triggered, chef would simply ignore it (maybe chef has noise cancelling head phones on). Chef's task current task was to chop vegetables; not constantly monitor for every conceivable disaster that might occur - chef has to focus on the task at hand. In cooperative multitasking, chef will continue chopping vegetables until they are all done (or the 5 minute OH&S limit is reached). At that point chef will terminate the chopping vegetables task and determine what to do next. At that point chef might say: "Oh, the smoke alarm is sounding", or "Oh, the pot has been boiling over" and deal with those disasters.

Obviously in the real world chef wouldn't ignore the alarm until the vegetables are chopped (I hope). This is merely to illustrate the difference between preemptive and cooperative multitasking.

Multi-threading

The above example also mentions another type of multi-tasking. Specifically "Simultaneously, chef tries to think of a way ...". This is concurrent multitasking (I think I made that phrase up - but it gives you the idea). In this case, chef is using chef's hands and eyes to manage the task of chopping vegetables. However, chef is using another resource, chef's brain, to perform another task - the "thinking of an excuse task" - at the same time as the "chopping vegetables" task. Chef can do this because the "brain resource" isn't fully utilised by the "chopping vegetables" task. Hopefully chef is not using all of chef's "brain resource" on the "think of an excuse task", otherwise chef may experience another high priority external preemption in the form of "cut fingers"!

In computing, this "concurrent multitasking" is more likely to be referred to as "multi-threading" or "concurrent programming". This requires multiple resources (i.e. multiple CPU's or multiple cores or multiple computers) to support this. One Arduino does not support concurrent multitasking as it is single CPU. Additionally the Arduino CPU's are typically single core (unlike, say, modern Intel CPUs which are multi-core) and some computers which are multi CPU with each CPU having multiple cores.

I could go on a lot more, but I think that is enough theory. Let's roll our sleeves up, get the breadboard out, hook up some LED's and start blinking!

Lets start

But first, here are some links to Wikipedia where you can read even more theory if that is what "floats your boat":

Step 2: The Bits List - A.k.a. What You Will Need.

In this project we don't use many different parts. However, we do use a lot of them. You will need:

  • An Arduino.
    All my examples will initially be built on Leonardo. You can also use a UNO, or pretty much any other Arduino with as many digital pins as you care to try connecting to LED's.
  • LED's - 8 will do.
    The more the better (the code examples only use 8 LED's)
  • 470 Ohm resistors (yellow, purple/violet, black, black and one other colour - usually gold).
    You will need 1 for each LED you connect. So, for 8 LED's, you need 8 470 Ohm resistors.
  • One push button switch SPST - momentary closed.
  • One 10K resistor (brown, black, black, red and one other colour - usually gold).
  • Medium to large Breadboard.
  • Hookup wire.

If you want to make the final (Mega Blinky LED Extravaganze) project, you will need:

  • An Arduino Mega
  • 32 LED's (or more if you are game). A mixture of colours is best.
  • 32 of 470 Ohm resistors per LED (yellow, purple/violet, black, black and one other colour - usually gold).
    NB: If you have more than 32 LED's, you will need extra 470 ohm resistors. Basically, you need one 470 ohm resistor for each and every LED!
  • One push button switch SPST - momentary closed.
  • One 10K resistor (brown, black, black, red and one other colour - usually gold).
  • Large breadboard
  • Hookup wire.

Step 3: Hook Everything Up

Hook up the components as shown in the diagrams. Unfortunately the breadboard view does not show the LED hookup very well, so I've also included the circuit diagram.

Fear not if you can not follow the diagrams, the connections are pretty straight forward:

  • Power Connections
    • Connect the 5V pin on Arduino to the two red rails on the breadboard (red wires)
    • Connect the GND pin on Arduino to the two black (or blue) rails (Black wires).
  • LED Connections
    • Connect one end of the eight 470 ohm resistors to the 5V rail (no wire - just plug them in directly)
    • Connect the other end of the eight 470 ohm resistors to the Anode (long wire) on each of the LED's.
    • Connect the other end of the eight LED's to one of the Digital pins numbered 6 though 13 on the Arduino (Yellow wires).
  • Push button Switch Connections
    • Connect one pin of the switch to 5V (Red wire)
    • Connect the diagonally opposite pin on the switch (refer to the breadboard diagram) to Pin 2 on the Arduino (Blue wire)
    • Connect the 10K ohm resistor from the same pin to GND (again refer to the breadboard diagram).

That's it. When running the programs below, if the LED's do not light up, try checking their orientation:

  • The Cathode (short lead) connects to one of the Arduino's digital pins numbered 6 through 13.
  • The Anode (long lead) connects to the 470 ohm resistor. The other end of the resistor connects to +5V.

If that is not the problem, double check all of the other connections.

Note about the examples

I've tried to provide plenty of examples to gradually illustrate the concepts.

I encourage you to try all of the examples, in sequence. However, if you are short of time, than please try at least these:

  • Combined Chase and Blink.
  • Interleaved LED Blink.
  • Multitasking Chase and Blink.
  • Object Oriented Chase and Blink.

Step 4: Blink an LED

In this simplest of examples, we will see how one task hogs the Arduino's attention to the exclusion of everything else. This is non-cooperative behaviour which is also sometimes referred to as Single Threaded.

To see this uncooperative behaviour:

  • Load the program below into your Arduino.
  • Push the button. What does the LED do? It should eventually freeze in the off state when the button is pushed.
  • Release the button. The LED should continue its rather monotonous blinking.
  • Press the button while the LED is lit, then release it before 2 seconds passes (i.e. the delay time when it is turned off). What happens?
    Nothing! it just keeps blinking (This will be better illustrated in the next example).
    Why? Because the Blink task is hogging the CPU for a full 4 seconds. This prevents the Button press task to see that the button was pressed.

What we are seeing here is an example of a task occupying the Arduino's CPU to the exclusivity of doing anything else. Specifically, when you press the button and this fact is eventually detected by the Arduino's program (sketch), all other processing stops until such time as the button is released. This is not cooperative as each of the tasks (Blink and check button press) hog the CPU, without doing anything useful, until they have completed their individual operation.

Sure, we could change the algorithm in the checkButton function so that it does not wait until the button is released. We could replace the delay in the blink function with a loop and continuously poll the button, but that is not the point of this example. We will modify their behaviours, but not their function, when we start multitasking, without the need to add special polling code for looking for other activity throughout the program.

This way of programming is akin to the chef not being able to respond to the smoke alarm in the introduction. Programming the Arduino to continuously poll unrelated events is akin to asking the chef to continuously check for all possible disasters while chef is doing every other activity. This could make the programming very complex because if there were N activities and M possible "interrupts", then the complexity of the code will be N x M. That is in each of the N activities, you will need to check for all possible M "interrupts" that might occur. Indeed, this style of programming is worse than the example we presented above. If we translated this example to the chef, it would be akin to the chef turning on the oven and sitting in front of it doing nothing else but waiting until the oven reached the target temperature. Only at that point in time, does chef start the next task - maybe filling the pot of water and sitting in front of it watching and waiting for it boil.

As we progress through the examples, hopefully you will come to realise that things can start to get complicated very quickly if we start changing basic operations and try to intermingle all of the function among each another. The fourth example - "Interleaved LED Blink" attempts to explicitly draw this complexity out.

Following is the code for example 1 - blink the LED

/******************************************************************************
* Cooperative Multitasking * 01 - Blink LED * * This is the first in a series of programs to illustrate the benefits of * a simple multitasking mechanism for Arduino. * * This program (sketch) starts with the most basic non-cooperative multitasking example. * There are two subtasks: * - A "blink LED" task that blinks the LED. It will hold the CPU for the entire duration of a single * LED blink operation. * - A "check for button press" task. It will hold the CPU for as long as you hold the button down. * * When running this program we will note that: * a) The messages relating to the LED Blinking will continue to be displayed - even if the button is pressed * until such time as the blink operation completes. * b) The messages relating to the LED Blinking and the blinking itself will cease being displayed if the * button is pressed (once the "check button press" task gets a hold of the CPU). * * In short, neither routine is sharing the CPU (blinking stops during button press), even though the * "check button press" task isn't actually doing anything except waiting for the button to be released. * Similarly, the "Blink LED" task isn't doing anything useful once it turns the LED off or on. Indeed it * justs wastes CPU cycles courtesy of the "delay" function calls. * * In this simplistic example, we could code it differently to avoid much of the "system hanging" symptoms, * but that is not the purpose this example. The purposes of this example is to show how easily it is to * code a routine that is "blocking" other tasks from doing their thing. * * By the time we get to example 5, we will address this. Examples 2 and 3 build upon this example, but * still in a non-cooperative multi-tasking way. */

// Define the pin for the input button #define BUTTON_PIN 2 // define the LED PIN for blinking #define LED_PIN 6

void setup() {

Serial.begin (9600); while (!Serial) ;

Serial.println("Blinker will be on pin: "); Serial.println(LED_PIN); pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, HIGH); // Turn the LED off.

// Set the push button's pin as an input. Serial.print("Setting input for push button on pin: "); Serial.println(BUTTON_PIN); pinMode(BUTTON_PIN, INPUT); Serial.println("Ready"); }

/********************** * Check Button Pressed. * Checks to see if the button is pressed. * If it is, return true. * * The function will first check to see if the button is pressed, then wait for a bit to check * if the button is still pressed. If it is, then the button is "debounced" and this function * returns true (Button pressed). * * Otherwise this function returns false (Button not pressed). * * Return: true if button pressed, false otherwise. * */ boolean checkButtonPressed() { Serial.println("Check button press"); // The complexity of this code is to "debounce" the button press. // Has the button been pressed. if (digitalRead(BUTTON_PIN) == HIGH) { // Check for a short period of time, that the button remains pressed. // In this case 50 x 1 ms checks. const int numChecks = 50; int i = 0; while (digitalRead(BUTTON_PIN) == HIGH && i < numChecks) { delay(1); i++; } // Did we exit the loop before the required time (i.e. was the button released / still being debounced)? if (i < numChecks) { return false; // Return "Button not pressed" }

// At this point, we confirm that we have a button press. // Wait for the button to be released. During this time, whatever else was happening // on the Arduino (e.g. blinking an LED) will be suspended. Serial.println("Button pressed, waiting for release"); while (digitalRead(BUTTON_PIN) == HIGH) { delay(10); } return true; // Return "Button pressed" } return false; // Return "Button not pressed" }

/********************** * Blink LED * * Blink the LED a single time. * */ void blinkLed() { Serial.println("LED On"); digitalWrite(LED_PIN, LOW); delay(2000);

Serial.println("LED Off"); digitalWrite(LED_PIN, HIGH); delay(2000); }

/********************** * Loop * * Continuously call the active routine (chase of blink LED). * Upon completion of the active routine, check to see if the button has been pressed. * If it has, print a message. */ void loop() { // Blink our LED for 2 seconds on, 2 seconds off. blinkLed(); if(checkButtonPressed()) { Serial.println("Button was pressed (now it is released)."); } }

Step 5: LED Chaser

In this step, we will use a slightly more complicated example. The things to observe are similar as the Blink example, but we shall see more clearly how the pressing of the button is ignored until such time as the sequence completes.

Upload the program (sketch) below and the LED's should light up one at a time giving a "chaser" type of effect.

  • Observe the chaser pattern and push the button.
    What happens? Nothing - until the sequence reaches the end at which time the chaser is frozen. The chase will remain frozen until such time as you release the button.
  • Release the button.
    What happens? The chase resumes.
  • Press the button while the chaser is in motion and release it before the chaser completes its cycle.
    What happens? Nothing! the chaser continues as though nothing has happened. This is because the "check button press" task never sees that the button is pressed.

Repeat the above, while monitoring the debug messages to get a different view on what is going on. Specifically, look at what happens at the "Check button press" message.

Here is the code for the Chaser:

/******************************************************************************
* Cooperative Multitasking * 02 - LED Chasser * * This is the second in a series of programs to illustrate the benefits of * a simple multitasking mechanism for Arduino. * * This program (sketch) builds upon the blink programs by combining converting it into a chaser. * There are two subtasks: * - A "chase LED" task that causes each LED to light up one after the other. It will hold the CPU for * the entire duration of a single chase operation. That is, individually light up the led's in one direction * then the other. * - A "check for button press" task. It will hold the CPU for as long as you hold the button down. * * When running this program we will note that: * a) The messages relating to the Chase operation will continue to be displayed - even if the button is pressed * until such time as the chase operation completes. * b) The messages relating to the LED Chase and the chase itself will cease being displayed if the * button is pressed (once the "check button press" task gets a hold of the CPU). * * In short, neither routine is sharing the CPU (chase stops during button press), even though the * "check button press" task isn't actually doing anything except waiting for the button to be released. * Similarly, the "Chase LED" task isn't doing anything useful once it turns the LED off or on. Indeed it * justs wastes CPU cycles courtesy of the "delay" function calls. * * In this simplistic example, we could code it differently to avoid much of the "system hanging" symptoms, * but that is not the purpose this example. The purposes of this example is to show how easily it is to * code a routine that is "blocking" other tasks from doing their thing. * * By the time we get to example 5, we will address this. Examples 2 and 3 build upon this example, but * still in a non-cooperative multi-tasking way. * */

// Define the pin for the input button #define BUTTON_PIN 2

// Define the pins to be used in tracing mode. unsigned int ledPins [] = {6, 7, 8, 9, 10, 11, 12, 13};

void setup() {

Serial.begin (9600); while (!Serial) ;

// Initialise the LED Pins for output and set them to High (turn the LED's off). Serial.print("Initialising tracer pin: "); for (int i = 0; i < sizeof (ledPins) / sizeof(ledPins[0]); i++) { pinMode(ledPins[i], OUTPUT); digitalWrite(ledPins[i], HIGH); // Turn the LED off. if (i > 0) { Serial.print(", "); } Serial.print(ledPins[i]); } Serial.println(); // Set the push button's pin as an input. Serial.print("Setting input for push button on pin: "); Serial.println(BUTTON_PIN); pinMode(BUTTON_PIN, INPUT); Serial.println("Ready"); }

/********************** * Check Button Pressed. * Checks to see if the button is pressed. * If it is, return true. * * The function will first check to see if the button is pressed, then wait for a bit to check * if the button is still pressed. If it is, then the button is "debounced" and this function * returns true (Button pressed). * * Otherwise this function returns false (Button not pressed) * * Return: true if button pressed, false otherwise. * */ boolean checkButtonPressed() { Serial.println("Check button press"); // The complexity of this code is to "debounce" the button press. // Has the button been pressed. if (digitalRead(BUTTON_PIN) == HIGH) { // Check for a short period of time, that the button remains pressed. // In this case 50 x 1 ms checks. const int numChecks = 50; int i = 0; while (digitalRead(BUTTON_PIN) == HIGH && i < numChecks) { delay(1); i++; } // Did we exit the loop before the required time (i.e. was the button released / still being debounced)? if (i < numChecks) { return false; // Return "Button not pressed" }

// At this point, we confirm that we have a button press. // Wait for the button to be released. During this time, whatever else was happening // on the Arduino (e.g. blinking an LED) will be suspended. Serial.println("Button pressed, waiting for release"); while (digitalRead(BUTTON_PIN) == HIGH) { delay(10); } return true; // Return "Button pressed" } return false; // Return "Button not pressed" }

/********************** * Tracer * * Cause the LED's to chase one another along the list of pins defined in "ledPins". * The chase will go from "left to right" as defined by the order of the pins in "ledPins". Then * "right to left". * * The chase will be executed one time. */ void tracer() { Serial.println("Tracer - up"); for (int i = 1; i < sizeof (ledPins) / sizeof(ledPins[0]); i++) { digitalWrite(ledPins[i - 1], HIGH); // Off digitalWrite(ledPins[i], LOW); // On // Serial.print(ledPins[i]); // Serial.println(" On"); delay(250); } Serial.println("Tracer - down"); for (int i = sizeof (ledPins) / sizeof(ledPins[0]) - 2; i >= 0; i--) { digitalWrite(ledPins[i + 1], HIGH); // Off digitalWrite(ledPins[i], LOW); // On // Serial.print(ledPins[i]); // Serial.println(" On"); delay(250); } }

/********************** * Loop * * Continuously call the active routine (chase of blink LED). * Upon completion of the active routine, check to see if the button has been pressed. * If it has, switch modes. */ void loop() { tracer(); if (checkButtonPressed()) { Serial.println("Button was pressed (now it is released)."); } }

Step 6: Combined Chase and Blink

In this third example, I've combined the chase and the blink. The enhancement is that the button press will switch modes between chasing and blinking. We should see similar behaviour as before. Specifically:

  • Pressing the button and releasing it before a cycle completes results in no change in behavior.
  • Pressing and holding the button until a cycle completes will result in the LED freezing in the final state of its sequence until such time as you release the button.
    When you release the button, it will switch to the other mode.

Following is the code for the combined Chase and Blink:

/******************************************************************************
* Cooperative Multitasking * 03 - Blink and Trace * * This is the third in a series of programs to illustrate the benefits of * a simple multitasking mechanism for Arduino. * * This program (sketch) builds upon the blink and trace programs by combining them into one. * Additionally the button's function has been altered to do something useful. * The button will cause a switch between the two modes. That is, if the program is blinking an LED, * a button press will switch to chase mode. * * The desired outcome of this program is to verify that the LED's are working and illustrate the * effect of the sequential processing when multi-tasking is not enabled. * Specifically: * - Button presses are only detected when the program polls the button. * Otherwise button presses are simply ignored. * - If the button is pressed and detected by the Arduino, all other activity will cease * i.e. the LED's will freeze. * * Of interest, but outside the scope of this activity, we could still achieve the desired result * of allowing the LED's to continue blinking and detect the button press without two much complicating code. * This would require the use of interrupts and toggling the mode of operation in the Interrupt * Service Routine. If I get the time do a preemptive multi-tasking instructable, we will cover this * capability there. * */

// Define the pin for the input button #define BUTTON_PIN 2 // define the LED PIN for blinking #define LED_PIN ledPins[0]

// Define the pins to be used in tracing mode. unsigned int ledPins [] = {6, 7, 8, 9, 10, 11, 12, 13}; // Start off in the tracer mode (because it's slightly cooler than just blinking). boolean tracerMode = true;

void setup() {

Serial.begin (9600); while (!Serial) ;

// Initialise the LED Pins for output and set them to High (turn the LED's off). Serial.print("Initialising tracer pin: "); for (int i = 0; i < sizeof (ledPins) / sizeof(ledPins[0]); i++) { pinMode(ledPins[i], OUTPUT); digitalWrite(ledPins[i], HIGH); // Turn the LED off. if (i > 0) { Serial.print(", "); } Serial.print(ledPins[i]); } Serial.println(); Serial.println("Blinker will be on pin: "); Serial.println(LED_PIN);

// Set the push button's pin as an input. Serial.print("Setting input for push button on pin: "); Serial.println(BUTTON_PIN); pinMode(BUTTON_PIN, INPUT); Serial.println("Ready"); }

/********************** * Check Button Pressed. * Checks to see if the button is pressed. * If it is, return true. * * The function will first check to see if the button is pressed, then wait for a bit to check * if the button is still pressed. If it is, then the button is "debounced" and this function * returns true (Button pressed). * * Otherwise this function returns false (Button not pressed) * * Return: true if button pressed, false otherwise. * */ boolean checkButtonPressed() { Serial.println("Check button press"); // The complexity of this code is to "debounce" the button press. // Has the button been pressed. if (digitalRead(BUTTON_PIN) == HIGH) { // Check for a short period of time, that the button remains pressed. // In this case 50 x 1 ms checks. const int numChecks = 50; int i = 0; while (digitalRead(BUTTON_PIN) == HIGH && i < numChecks) { delay(1); i++; } // Did we exit the loop before the required time (i.e. was the button released / still being debounced)? if (i < numChecks) { return false; // Return "Button not pressed" }

// At this point, we confirm that we have a button press. // Wait for the button to be released. During this time, whatever else was happening // on the Arduino (e.g. blinking an LED) will be suspended. Serial.println("Button pressed, waiting for release"); while (digitalRead(BUTTON_PIN) == HIGH) { delay(10); } return true; // Return "Button pressed" } return false; // Return "Button not pressed" }

/********************** * Blink LED * * Blink the LED a single time. * */ void blinkLed() { Serial.println("LED On"); digitalWrite(LED_PIN, LOW); delay(2000);

Serial.println("LED Off"); digitalWrite(LED_PIN, HIGH); delay(2000); }

/********************** * Tracer * * Cause the LED's to chase one another along the list of pins defined in "ledPins". * The chase will go from "left to right" as defined by the order of the pins in "ledPins". Then * "right to left". * * The chase will be executed one time. */ void tracer() { Serial.println("Tracer - up"); for (int i = 1; i < sizeof (ledPins) / sizeof(ledPins[0]); i++) { digitalWrite(ledPins[i - 1], HIGH); // Off digitalWrite(ledPins[i], LOW); // On // Serial.print(ledPins[i]); // Serial.println(" On"); delay(250); } Serial.println("Tracer - down"); for (int i = sizeof (ledPins) / sizeof(ledPins[0]) - 2; i >= 0; i--) { digitalWrite(ledPins[i + 1], HIGH); // Off digitalWrite(ledPins[i], LOW); // On // Serial.print(ledPins[i]); // Serial.println(" On"); delay(250); } }

/********************** * Loop * * Continuously call the active routine (chase of blink LED). * Upon completion of the active routine, check to see if the button has been pressed. * If it has, switch modes. */ void loop() { // Blink our LED for 2 seconds on, 2 seconds off. if (tracerMode) { tracer(); } else { blinkLed(); } if (checkButtonPressed()) { tracerMode = ! tracerMode; Serial.print("Switching modes: tracermode = "); Serial.println(tracerMode); } }

Step 7: Interleaved LED Blink

This example switches gears a bit. In this example, we will look at the problem of blinking multiple LED's at different rates.

As long as the duty cycles (blink rates) are the same (e.g. 2 seconds on and 2 seconds off) it is relatively easy to blink multiple LED's seemingly concurrently. But only if the only difference is the point in time that they started (the duration of a blink must be the same). For example consider the following sequence (I will give the LED's names - e.g. A, B etc to make it easier to follow):

  • we turn LED A on (we want it to stay on for 2 seconds then turn off for two seconds).
  • one half of a second later we want LED B to turn on (this will also be on for 2 seconds and off for 2 seconds).
  • one half of a second later we want LED C to turn on (also 2 seconds on and 2 seconds off).
  • So far 1 second has elapsed, so we wait for 1 second and turn LED A off (it will have been on for two seconds).
  • Wait for half a second and turn LED B off.
  • Wait for half a second and turn LED C off.
  • Finally wait for 1 second (because the process of turning LED B and C off took 1 second and we must leave LED A for a whole 'nother second).
  • repeat.

However, even with this simple example, if we change the duty cycle (time on -vs- time off) or the delay between "On" and "Off" events, we need to recalculate and reprogram the entire sequence. Consider changing it so that instead of a half second delay between subsequent LED's turning on or off to 3/4 of a second. The sequence will become:

  • turn LED A on.
  • 750 ms later turn on LED B.
  • 750 ms later turn on LED C.
  • So far 1.5 seconds has elapsed, so we wait for 0.5 seconds (500ms) to turn LED A off (it will now have been on for two seconds).
  • 750 ms later turn LED B off.
  • 750 ms later turn LED C off.
  • Finally wait for 0.5 second and repeat.

If we change the duty cycle then the calculation becomes slightly more complicated. If we change the length of the cycles so that each LED has a different cycle length (e.g. A has a cycle length of 5 seconds, B has 3 seconds and C has 1.5 seconds), then the order that LED's are turned on and off will change from one sequence to the next. This will make it very complicated to program using a non-multitasking approach. Have a think about what the code for blinking multiple LED's with different blink durations using delays, bearing in mind that the LED's must be turned on and on off in different orders to each other.

Following is the code for blinking LED's with a fixed blink duration (e.g. 2 seconds on and 2 seconds off):

/******************************************************************************
* Cooperative Multitasking * 04 - Interleaved LED Blinking * * This is the fourth in a series of programs to illustrate the benefits of * a simple multitasking mechanism for Arduino. * * This program (sketch) illustrates the complexity of managing multiple independent * tasks (blinking of LED's). * * When *modifying* this program note that: * a) a change to the cycle of one LED, requires recalculating aspects of the neighbouring LED activity. * b) if not impossible, it is very difficult to manage the on/off sequence if the LED's blink at different * rates and their cycles differ in duration with this single threaded approach. * * In example 5, we will enable multitasking to overcome the above problems. Hopefully, you will agree that * the solution is relatively simple - you may have even done something along similar lines. In example 5, * we will blink the LED's at randomly determined intervals. * */

// Define the pin for the input button #define BUTTON_PIN 2

// Define the pins to be used in tracing mode. unsigned int ledPins [] = {6, 7, 8, 9, 10, 11, 12, 13};

void setup() {

Serial.begin (9600); while (!Serial) ;

// Initialise the LED Pins for output and set them to High (turn the LED's off). Serial.print("Initialising tracer pin: "); for (int i = 0; i < sizeof (ledPins) / sizeof(ledPins[0]); i++) { pinMode(ledPins[i], OUTPUT); digitalWrite(ledPins[i], HIGH); // Turn the LED off. if (i > 0) { Serial.print(", "); } Serial.print(ledPins[i]); } Serial.println(); // Set the push button's pin as an input. Serial.print("Setting input for push button on pin: "); Serial.println(BUTTON_PIN); pinMode(BUTTON_PIN, INPUT); Serial.println("Ready"); }

/********************** * Check Button Pressed. * Checks to see if the button is pressed. * If it is, return true. * * The function will first check to see if the button is pressed, then wait for a bit to check * if the button is still pressed. If it is, then the button is "debounced" and this function * returns true (Button pressed). * * Otherwise this function returns false (Button not pressed) * * Return: true if button pressed, false otherwise. * */ boolean checkButtonPressed() { Serial.println("Check button press"); // The complexity of this code is to "debounce" the button press. // Has the button been pressed. if (digitalRead(BUTTON_PIN) == HIGH) { // Check for a short period of time, that the button remains pressed. // In this case 50 x 1 ms checks. const int numChecks = 50; int i = 0; while (digitalRead(BUTTON_PIN) == HIGH && i < numChecks) { delay(1); i++; } // Did we exit the loop before the required time (i.e. was the button released / still being debounced)? if (i < numChecks) { return false; // Return "Button not pressed" }

// At this point, we confirm that we have a button press. // Wait for the button to be released. During this time, whatever else was happening // on the Arduino (e.g. blinking an LED) will be suspended. Serial.println("Button pressed, waiting for release"); while (digitalRead(BUTTON_PIN) == HIGH) { delay(10); } return true; // Return "Button pressed" } return false; // Return "Button not pressed" }

#define LED_A ledPins[0] #define LED_B ledPins[1] #define LED_C ledPins[2]

/********************** * LED Cycle Z * * Blink the LED's so that they are on for 2 seconds and off for 2 seconds. * LED A (ledPins[0]) comes on first. * LED B (ledPins[1]) comes on 500ms after A. * LED C (ledPins[2]) comes on 500ms after B. * After another 1 second: * LED A turns off. * 500ms later LED B turns off. * 500ms later LED C turns off. * After another 1 second, the sequence is complete, so control is returned. */ void cycleA() { Serial.println("Cycle A"); digitalWrite(LED_A, HIGH); delay (500); digitalWrite(LED_B, HIGH); delay (500); digitalWrite(LED_C, HIGH); delay (1000); digitalWrite(LED_A, LOW); delay (500); digitalWrite(LED_B, LOW); delay (500); digitalWrite(LED_C, LOW); delay (1000); }

void cycleB() { Serial.println("Cycle B"); digitalWrite(LED_A, HIGH); delay (750); digitalWrite(LED_B, HIGH); delay (750); digitalWrite(LED_C, HIGH); delay (500); digitalWrite(LED_A, LOW); delay (750); digitalWrite(LED_B, LOW); delay (750); digitalWrite(LED_C, LOW); delay (500); }

boolean modeA = true; /********************** * Loop * * Continuously call the active routine (chase of blink LED). * Upon completion of the active routine, check to see if the button has been pressed. * If it has, switch modes. */ void loop() { if (modeA) { cycleA(); } else { cycleB(); } if (checkButtonPressed()) { Serial.println("Button was pressed (now it is released)."); modeA = ! modeA; } }


Step 8: Multitasking Chase and Blink

Finally! Lets look at a multitasking version that starts addressing the problems.

This version will combine all three of the themes that were introduced in the previous examples:

  • Push button will be actioned when you release the button.
  • While holding the button down, the other activity won't be affected - no matter how long or when you press it.
  • The Blink LED will be expanded to blink all 8 LED's at randomly generated:
    • Duration (somewhere between 0.5 and 2 seconds)
    • Duty cycle (somewhere between 80:20 and 20:80)
    • Independently
  • The push button will switch between chasing and blinking.
    • When switching to blinking the LED blink pattern will be randomly regenerated.

Here is the code.

/******************************************************************************
* Cooperative Multitasking * 05 - Multitasking trace and blink * * This is the fifth in a series of programs to illustrate the benefits of * a simple multitasking mechanism for Arduino. * * This program (sketch) shows how to multitask a few different tasks using "cooperative * multitasking". The tasks that are being multitasked include: * - Monitoring a switch / button press * - Executing an LED trace pattern * - Randomly blinking multiple LEDs. * The blink task is actually 1 task per LED being blinked. So for 8 LED's, it * is 8 sub tasks. * * The big differences between the previous non-multitasking examples and this one include: * - use of delay () and long running loops (e.g. while (digitalRead(BUTTON_PIN) == HIGH);) are * removed. * - The tasks track what they are doing in what is known as it's context and just let the hardware * get on with doing what it does all by itself. * For example, once an LED is turned ON (or OFF), it will remain on (or off) all by itself. * While the LED is remaining on (or off) all by itself, the tasks' code simply returns (exits). * This allows the Arduino's CPU to go on and do something else (e.g. turn a different LED on or off). * The task's context consists of whatever information is needed to track what we have done so far and * work out what to do next. */

// Define the pin for the input button #define BUTTON_PIN 2

// Define the pins to be used in tracing mode. unsigned int ledPins [] = {6, 7, 8, 9, 10, 11, 12, 13}; #define LED_COUNT (sizeof (ledPins) / sizeof(ledPins[0])) // Context information for sub-tasks unsigned long timePrev = 0;

#define BLINK_MODE 0 #define CHASER_MODE 1 boolean mode = BLINK_MODE; // The current mode of operation. By using an ID to define the // mode, we can have any number of display modes.

// Chaser context. // Number of milliseconds between chaser actions. #define CHASER_DELAY 200 unsigned long chaserNextEventTime; unsigned long chaserTimerCnt; int chaserIndex = 0; boolean chaserIndexGoingUp = true;

// Blink context. typedef struct { unsigned long timerCnt; // Number of milliseconds that have passed since the last activity. unsigned long nextEventTime; // Number of millisends that must pass until the next activity.

boolean ledOn; // tracks whether the LED is currently on or not. int ledPin; // the Pin to which the LED is connected. unsigned long onTime; // number of milliseconds that this LED should remain on. unsigned long offTime; // number of milliseconds that the LED should remain off. } BlinkContext;

// Declare 1 blink context for each LED being controlled. // This is used by the blink subtasks to determine what to do next and when. BlinkContext blinkCtx[LED_COUNT];

// Button context. boolean isButtonPressed = false; int prevButtonState = LOW; unsigned long buttonNextEventTime = 10; unsigned long buttonTimerCnt = 0; unsigned long debounceCnt = 0;

void printBlinkContext(BlinkContext *ctx) { Serial.print("Blink Context: Next Evt Time: "); Serial.print(ctx->nextEventTime); Serial.print(", cur tim: "); Serial.print(ctx->timerCnt); Serial.print(", led on: " ); Serial.print(ctx->ledOn); Serial.print(", on time: "); Serial.print(ctx->onTime); Serial.print(", off time: "); Serial.print(ctx->offTime); Serial.print(", LED Pin: " ); Serial.println(ctx->ledPin); }

void randomiseBlink(BlinkContext *ctx) { ctx->onTime = 500 + random(1500); // On time is a random number between 500 and 2000 ms. ctx->offTime = 500 + random(1500); // Off time is a random number between 500 and 2000 ms. ctx->nextEventTime = ctx->offTime; // Specify the next action time. }

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

// Initialise the LED Pins for output and set them to High (turn the LED's off). Serial.print("Initialising LED pin: "); for (int i = 0; i < LED_COUNT; i++) { pinMode(ledPins[i], OUTPUT); digitalWrite(ledPins[i], HIGH); // Turn the LED off. blinkCtx[i].ledOn = false; // track that the LED is off. blinkCtx[i].ledPin = ledPins[i]; // track the pin that this task's LED is attached to. blinkCtx[i].timerCnt = 0; // We are at the beginning of time.

if (i > 0) { Serial.print(", "); } Serial.print(ledPins[i]); } Serial.println();

for (int i = 0; i < LED_COUNT; i++) { randomiseBlink(&blinkCtx[i]); printBlinkContext(&blinkCtx[i]); } // Set the push button's pin as an input. Serial.print("Setting input for push button on pin: "); Serial.println(BUTTON_PIN); pinMode(BUTTON_PIN, INPUT); Serial.println("Ready"); timePrev = millis(); // Initialise the "previous time" value to the current time. }

void turnAllLed(int state) { for (int i = 0; i < LED_COUNT; i++) { digitalWrite(ledPins[i], HIGH); // Turn off the LED. } }

unsigned long blinkLEDTask(BlinkContext *ctx) { unsigned long nextEventTime;

// Are we in blink mode? if (mode != BLINK_MODE) { // If we are not in the blink mode, then there is nothing to do here. return 10000000; // return a high value to effectively prevent further calls // to this task. Note that the randomise routine will reset // this to a proper value when we switch modes. }

// Check our current state. if (ctx->ledOn) { // Is the LED currently on? Serial.print("Turn ON - "); printBlinkContext(ctx); digitalWrite(ctx->ledPin, HIGH); // Yes, so turn it off nextEventTime = ctx->offTime; } else { Serial.print("Turn OFF - "); printBlinkContext(ctx); digitalWrite(ctx->ledPin, LOW); // Yes, so turn it off nextEventTime = ctx->onTime; } ctx->ledOn = !ctx->ledOn; return nextEventTime; }

unsigned long chaserTask() {

if (mode != CHASER_MODE) { return 10000000; // Return a high value to effectively prevent further calls to // this task. The mode switch (handleButtonPress) will reset // the next time value to a sensible value. }

if (chaserIndexGoingUp) { digitalWrite(ledPins[chaserIndex], HIGH); // Turn the previous LED OFF. chaserIndex += 1; // Point to the next higher LED. digitalWrite(ledPins[chaserIndex], LOW); // Turn the LED ON. // Go up as long as the index is < the // highest index in the ledPins array. chaserIndexGoingUp = (chaserIndex < LED_COUNT - 1); } else { digitalWrite(ledPins[chaserIndex], HIGH); // Turn the previous LED OFF. chaserIndex -= 1; // Point to the next lower LED. digitalWrite(ledPins[chaserIndex], LOW); // Turn the LED ON. chaserIndexGoingUp = (chaserIndex <= 0); // When we reach zero, switch to going up. } return CHASER_DELAY; }

/********************** * Check Button Pressed. * Checks to see if the button is pressed. * If it is, return true. * * The function will first check to see if the button is pressed, then wait for a bit to check * if the button is still pressed. If it is, then the button is "debounced" and this function * returns true (Button pressed). * * Otherwise this function returns false (Button not pressed) * * Return: true if button pressed, false otherwise. * */ boolean checkButtonPressed() { Serial.println("Check button press"); // The complexity of this code is to "debounce" the button press. // Has the button been pressed. if (digitalRead(BUTTON_PIN) == HIGH) { // Check for a short period of time, that the button remains pressed. // In this case 50 x 1 ms checks. const int numChecks = 50; int i = 0; while (digitalRead(BUTTON_PIN) == HIGH && i < numChecks) { delay(1); i++; } // Did we exit the loop before the required time (i.e. was the button released / still being debounced)? if (i < numChecks) { return false; // Return "Button not pressed" }

// At this point, we confirm that we have a button press. // Wait for the button to be released. During this time, whatever else was happening // on the Arduino (e.g. blinking an LED) will be suspended. Serial.println("Button pressed, waiting for release"); while (digitalRead(BUTTON_PIN) == HIGH) { delay(10); } return true; // Return "Button pressed" } return false; // Return "Button not pressed" }

unsigned long checkButtonTask() { boolean currButtonState = digitalRead(BUTTON_PIN);

isButtonPressed = false; // Assume button is not pressed until determined otherwise if (prevButtonState == LOW) { if (currButtonState == HIGH) { // prev = LOW and curr = LOW // prev = LOW and curr = HIGH // state is changing. debounceCnt = 0; // debounce the button. Serial.println("Button press detected"); } } else { // the previous state of the button was HIGH (pressed) if (currButtonState == HIGH) { debounceCnt += 1; // count a debounce. } else { // prev state = HIGH, curr = LOW - button released. // if we have passed the debounce threshold, then // the is button pressed will be set to true. isButtonPressed = (debounceCnt > 10); Serial.print("Button released, debounceCnt: "); Serial.println(debounceCnt); debounceCnt = 0; } } // Check the button more frequently if it is pressed. prevButtonState = currButtonState; return currButtonState == HIGH ? 1 : 10; }

boolean wasButtonPressedAndReset() { boolean result = isButtonPressed; isButtonPressed = false; return result; }

void handleButtonPress() { switch (mode) { case BLINK_MODE: Serial.println("Switching to chase mode"); turnAllLed(HIGH); mode = CHASER_MODE; chaserIndex = 0; chaserTimerCnt = 0; chaserNextEventTime = CHASER_DELAY; break; case CHASER_MODE: Serial.println("Switching to blink mode. Contexts:"); for(int i = 0; i < LED_COUNT; i++) { randomiseBlink(&blinkCtx[i]); printBlinkContext(&blinkCtx[i]); turnAllLed(HIGH); } mode = BLINK_MODE; break; default: mode = BLINK_MODE; break; } }

/********************** * Loop * */ void loop() { unsigned long timeNow = millis();

if (timeNow != timePrev) { unsigned long timeDelta = timeNow - timePrev; timePrev = timeNow;

// Execute the blink sub tasks. for (int i = 0; i < LED_COUNT; i++) { BlinkContext *ctx = &blinkCtx[i]; ctx->timerCnt += timeDelta; if (ctx->timerCnt >= ctx->nextEventTime) { ctx->nextEventTime = blinkLEDTask(ctx); ctx->timerCnt = 0; } } // Execute the chaser sub task. chaserTimerCnt += timeDelta; if (chaserTimerCnt >= chaserNextEventTime) { chaserNextEventTime = chaserTask(); chaserTimerCnt = 0; }

// Execute the button subtask. buttonTimerCnt += timeDelta; if (buttonTimerCnt >= buttonNextEventTime) { buttonNextEventTime = checkButtonTask(); buttonTimerCnt = 0;

if (wasButtonPressedAndReset()) { handleButtonPress(); } } } }

Step 9: Object Oriented Chase and Blink

This version of code is the same as the previous version, except:

  • The sub-tasks (e.g. blink) are coded as objects (classes). This brings all of the bits and pieces (variables tracking state and operations) into a single structure (a class).

When used correctly, you can realise significant benefits in using Object Oriented (OO) techniques.

Note how the variables (or members) defined in the struct in the previous program have been brought together into the BlinkTask class as private fields. This class encapsulates both the function (turn on or of the LED) and the relevant data into a single block of code.

We can also see inheritence and abstraction. Inheritence is seen where a task (e.g. blink an LED) leverages the capabilities of the TimedTask.

Abstraction is seen in the TimedTask class. The TimedTask class knows that at some point it will need to execute whatever the function might be (e.g. blink an LED, increment the chaser, check the status of a button etc). But it does not know, and very importantly nor does it care, what that function is. So, the TimedTask defines an abstract method (execute). This is what TimedTask will call at the appropriate time. It is the responsibility of someone else (e.g. BlinkTask) to provide the necessary details of what to do (e.g. turn an LED on or off). So when the TimedTask calls the execute method, it will actually invoke the execute method in the class that was actually created (e.g. BlinkTask, or ButtonTask).

There is one TimedTask for:

  • each LED (i.e. One BlinkTask for each LED = 8 BlinkTasks)
  • each push button (i.e. one ButtonTask for the one button we have).
  • the chaser (ChaserTask - we only have one of these as we only want to execute one of these at any one time)
    Should we decide to have two Chaser Tasks (maybe one chasing the first four LED's at one rate and another chasing the second four LED's at a different rate, or maybe one chasing the odd LED's and the other chasing the even LED's again at different rates), then we would need two chaser tasks. We aren't doing that, so we only need on Chaser task. Should anyone try to implement two chasers, I'd be keen to hear about it (and see the video).

OO pundits will claim that using OO techniques will reduce complexity and volume of code. Many will also claim the program runs more efficiently. I must admit I tend to agree with those assertions. Although I have no data on the last "more efficiently" point. In this example, the number of lines of code was reduced slightly (about 10%), but I also added several new functions (refer to the comments in the code for a list) while attaining the reduction in code lines.

An example of this can be seen in the loop functions of this and the previous example. Because each of the different subtask types inherit capability from the TimedTask class, I can put all of my subtasks into an array (taskList). This means, that I only need one statement to record elapsed time against each and every different type of task. Compare this to the non Object Oriented version where there are individual if statements for each type of class (i.e. 3 seperate code blocks) to determine if a task is due to be invoked.

Additionally, not only are there less code blocks to be created, I can get away without defining multiple variables (or the struct) to track elapsed time. All of this is done within the properties defined in the TimedTask class.

There are some additional benefits to using object oriented techniques that are a bit less subtle. For example:

  • debugging task execution related issues - I only need to do it once.
  • Sub task logic is "standalone" - consider the BlinkTask. All it needs to worry about is turning an LED on or off. Something else, i.e. the TimedTask, will invoke it at the right time. There isn't any need to intermingle scheduling related code into the sub task. This makes it easier to see what is going on - and as just mentioned easier to debug when something doesn't work.

In short, there are considerable advantages when using OO techniques, even in a simple program like this.

Warning: this version of the code uses pointers - specifically pointers to instantiated objects (classes) - this translates to pointers to code. This is not a symptom of OO, this is a problem of not being careful enough with pointers. If, like I did, you screw up your pointers, it is easy to execute random code. Executing random code will produce, i guess, random results. Random results, in this case, means anything can happen such as the Arduino will hang, weird characters appear on the Serial monitor and if you are lucky (or unlucky) enough corrupt your Arduino's boot loader. If this last thing happens to you, you can recover it by following the instructions here: Arduino as an ISP you will need another (working) Arduino to recover your toasted Arduino.

Here is the code (it's pointers have been debugged, so I expect you won't have any problems if you use it as is):

/******************************************************************************
* Cooperative Multitasking * 06 - Multitasking trace and blink with OO * * This is the sixth in a series of programs to illustrate the benefits of * a simple multitasking mechanism for Arduino. * The enhancement on this occassion is to use Object Oriented concepts to make * the program simpler. * * This program (sketch) shows how to multitask a few different tasks using "cooperative * multitasking". The tasks that are being multitasked include: * - Monitoring a switch / button press * - Executing an LED trace pattern * - Randomly blinking multiple LEDs. * The blink task is actually 1 task per LED being blinked. So for 8 LED's, it * is 8 sub tasks. * * The big differences between the previous multitasking example and this one include: * - less lines of code (about 30 less ~ 9%). * - More functionality including: * - named tasks * - disable / enable task support * - bespoke code required to manage timers for the individual types of tasks is * encapsulated into an abstract class extended by the 3 task types. * - encapsulation of the context of a task within the specific task's class. * - support for a "do not change" the interval between subtask executions return value. * - ability to easily add more Task types to support additional animations. * - (hopefully) easier to debug and follow what is going on. */

// Define the pin for the input button #define BUTTON_PIN 2

// Define the pins to be used in tracing mode. unsigned int ledPins [] = {6, 7, 8, 9, 10, 11, 12, 13}; #define LED_COUNT (sizeof (ledPins) / sizeof(ledPins[0]))

// Context information for sub-tasks unsigned long timePrev = 0;

// Define constants (as opposed to a boolean) to define which mode we are operating in. // To add a new MODE, simply extend the list of constants. Make sure that "MODE_CNT" is // one more than the highest task ID. #define BLINK_MODE 0 #define CHASER_MODE 1 #define MODE_CNT 2 boolean mode = BLINK_MODE;

/************************************************ * Class TimedTask. * * An abstract (incomplete) class that manages the scheduling of sub tasks. * * This class tracks the elapsed amount of time on behalf of it's subclasses. * When the alotted time has passed, the "execute" method will be invoked to * allow the task to do whatever it needs to do. * NB: This TimedTask class would ideally be "factorised" out into a library. * If we did this, then the entire functionality could be accessed by a single * line of code (as opposed to the 70 odd lines here. The single line of code * would be something like: #include . */ class TimedTask { public: // Constructor - capture the time that has to pass until the task needs to be invoked. TimedTask(unsigned long nextEventTime) { this->nextEventTime = nextEventTime; }

// Set the next event time. void setNextEventTime(unsigned long nextEventTime) { this->nextEventTime = nextEventTime; }

// Abstract methods which must be implemented (defined) in a subclass. virtual unsigned long execute(); // Execute the task. virtual String getName(); // Retrieve the name of the task. virtual void disableTask(); // Invoked when this task is being disabled. virtual void enableTask(); // Invoked when this task is being enabled.

// Enable this task. void enable() { enabled = true; timeSinceLastEvent = 0; // Reset the elapsed time counter. enableTask(); // Notify the subclass that the task has been enabled. }

// disable this task. void disable() { enabled = false; disableTask(); // Notify the subclass that the task has been disabled. }

// Return the enabled/disabled state of the task. boolean isEnabled() { return enabled; }

// Record the fact that time has passed. void recordTime(unsigned long delta) { timeSinceLastEvent += delta; // Record the time and check if this task is due to be // executed. NB: the task is only executed if it is enabled. if (timeSinceLastEvent >= nextEventTime && enabled) { // Serial.print ("Executing task: "); // Serial.println(getName()); unsigned long nev = execute(); // Notify the subclass to do it's thing. if (nev > 0) { // Record the next event time if it is non zero. nextEventTime = nev; } timeSinceLastEvent = 0; // Reset the time counter. } }

private: unsigned long nextEventTime; // Time that must pass before we invoke the subtask. unsigned long timeSinceLastEvent = 0; // The time has passed since the last invocation of the subtask. boolean enabled = true; };

/************************************************ * Class BlinkTask. * Extends TimedTask. * * An implementation (i.e. complete) of a TimedTask that blinks a single LED. * * This class toggles the state of the LED when it is invoked. */ class BlinkTask : public TimedTask { public: // Constructor needs to know which pin the LED is connected to. BlinkTask(int ledPin) : TimedTask(0){ pinMode(ledPin, OUTPUT); // Initialise the PIN as output. digitalWrite(ledPin, HIGH); // Turn the LED off. this->ledPin = ledPin; // track the pin. setNextEventTime(offTime); // set the time to the next invocation. outputDetails(); // print the details of this BlinkTask. taskName.reserve(15); taskName = "blink "; taskName += ledPin; }

// Execute the Blink Task // This simply toggles the current state of the LED and returns the period // of time that must pass before the next invocation. unsigned long execute () { ledOn = !ledOn; // Toggle the LED on/off flag. // Serial.print("Turning LED "); // Serial.println(ledOn ? "on" : "off"); digitalWrite(ledPin, ledOn ? LOW : HIGH); // Set the LED state. return ledOn ? onTime : offTime; // return the next delay time. }

// Retrieve the name of the task as "Blink " + the led's Digital Pin number. String getName() { return taskName; }

// disable the blink task void disableTask() { digitalWrite(ledPin, HIGH); // Turn the LED off. }

// enable the blink task - nothing to do here. void enableTask() { onTime = randomInterval(); // The time the led will be on (ms) offTime = randomInterval(); // The time the led will be off (ms) setNextEventTime(offTime); digitalWrite(ledPin, HIGH); Serial.print("Enabling: "); outputDetails(); }

// print the details of this blink task. void outputDetails() { Serial.print("Blink task pin: "); Serial.print(ledPin); Serial.print(", onTime: "); Serial.print(onTime); Serial.print(", offTime: "); Serial.println(offTime); }

private: unsigned long randomInterval() { return 500 + random(1500); } unsigned long onTime = randomInterval(); // The time the led will be on (ms) unsigned long offTime = randomInterval(); // The time the led will be off (ms) boolean ledOn = false; // Initially the LED will be off. int ledPin; // The digital I/O pin for the LED. String taskName; // the name of the task };

/************************************************ * Class ChaserTask. * Extends TimedTask. * * An implementation (i.e. complete) of a TimedTask that execute a LED chaser. * * This causes each LED in a chain to turn on. The task moves the LED up the chain * then back down again. */ class ChaserTask : public TimedTask { public: // constructor requires the delay time between updates to the chaser display. ChaserTask (unsigned long nextEventTime) : TimedTask(nextEventTime) { }

// Move the LED up (or down) one step. unsigned long execute() { if (chaseUp) { // If going up, turn of the current LED. digitalWrite(ledPins[chaserIndex], HIGH); chaserIndex += 1; // point to the next one up the line and turn it on. digitalWrite(ledPins[chaserIndex], LOW); // Go up as long as the index is < the // Highest index in the LEDpins array. chaseUp = chaserIndex < LED_COUNT - 1; } else { // Going down. turn off the current LED. digitalWrite(ledPins[chaserIndex], HIGH); chaserIndex -= 1; // point to the next one down the line and turn it on. digitalWrite(ledPins[chaserIndex], LOW); // Continue going down until we reach // the beginning of the LEDpins array. chaseUp = chaserIndex <= 0; } return 0; // do not change the scheduling interval. }

// Enable the chaser task. // Reset to the "beginning" state. void enableTask() { chaserIndex = 0; // Resume from the beginning; chaseUp = true; // Go up. }

// disable the chaser task. // Turn off the current LED void disableTask() { // Turn the current LED OFF. digitalWrite(ledPins[chaserIndex], HIGH); }

// Return the name of the task as "chaser". String getName() { return "Chaser"; }

private: boolean chaseUp = true; // direction of travel int chaserIndex = 0; // currently illuminated LED. };

/************************************************ * Class ButtonTask. * Extends TimedTask. * * An implementation (i.e. complete) of a TimedTask that detects a button press. * * Ancillary methods may be invoked to ascertain if the button has been pressed or not. */ class ButtonTask : public TimedTask { public: // Constructor: // button Pin - the pin the button to be monitored is connected to. // nextEventTime - the time delay between checks to see if the button has been pressed. ButtonTask(int buttonPin, unsigned long nextEventTime) : TimedTask(nextEventTime) { pinMode(buttonPin, INPUT); this->buttonPin = buttonPin;

taskName.reserve(15); taskName = "**** button "; taskName += buttonPin; }

// Checks to see if the button has been pressed. // If it has been pressed, it will debounce the press and // set appropriate indicators recording the press when the button is released. unsigned long execute() { // Read the current state of the button. int currButtonState = digitalRead(buttonPin); if (prevButtonState == LOW) { if (currButtonState == HIGH) { // Button was just pressed. debounceCnt = 0; // Start the debounce count. Serial.println("Button pressed"); } } else { // Previously the button has tracked as "pressed". if (currButtonState == HIGH) { // Is it still pressed? debounceCnt += 1; // Count the number of intervals that it has remained pressed } else { // Button has been released. Serial.print ("Button released. Debounce Cnt: "); Serial.println(debounceCnt); // Set the button pressed flag to true, if the button has remained pressed // for the required amount of time. buttonPressedInd = (debounceCnt > debounceThreshold); debounceCnt = 0; // reset the debounce count. } } prevButtonState = currButtonState; // Remember this button state for next time. return currButtonState == HIGH ? 1 : 10; // Check every 1 ms while button is pressed, // otherwise just check once every 10 ms. }

// Retrieve the task name as "button " + digital pin I/O number. String getName() { return taskName; }

// Enable the task - nothing special to do. void enableTask() { }

// disable the task - nothing special to do. void disableTask() { }

// Ancilliary method to query the state of the button. boolean isButtonPressed() { return buttonPressedInd; }

// Ancilliary method to query the state of the button // and reset it's state to false (not pressed) boolean isButtonPressedAndReset() { boolean result = buttonPressedInd; buttonPressedInd = false; return result; }

// The number of times (ms) that the button must remain pressed to // count as an actual press. Any "presses" less than this duration // are ignored as noise. const int debounceThreshold = 10; private: int buttonPin; // The digital I/O pin to which the button is connected. int debounceCnt = 0; // How many contiguous "pressed" readings have we observed? int prevButtonState = LOW; // State of the button - last time we checked. boolean buttonPressedInd = false; String taskName; // the name of this task. };

/***************************************************** * The task list. * * Establish an array of tasks. All of the tasks go into this array irrespective of their * specific type. * We seperately track the button task and chaser task, although not strictly necessary, in * individual variables (as well as in the array) so we can more cleanly interact with them. * * The array is an array of pointers to TimedTasks. This is to allow any class that extends TimedTask * to be placed into the array. The draw back is that technically we do not know what the task types are, * so we can only directly invoke the methods in TimedTask. Fortunately, through inheritence and abstraction, * the TimedTask can invoke the specific methods that are defined as abstract to obtain the specific tasks * function (i.e. the TimedTask can activate/call the specific task's individual "execute" method to get it to * do whatever it does). */ // Define how many tasks there are in total. #define TASK_COUNT (sizeof taskList / sizeof taskList[0]) TimedTask *taskList[LED_COUNT + 2]; ButtonTask *buttonTask; ChaserTask *chaserTask;

// Create as many blink tasks as defined in the ledPins array. There will be one // Blink Task for each defined pin. // This is called from setup and when the button is pressed (activating Blink Mode) void resetBlinkers(boolean create) { for (int i = 0; i < LED_COUNT; i++) { if (create) { taskList[i] = new BlinkTask(ledPins[i]); } else { taskList[i]->enable(); // Enable the blink task and recalculate its operating parameters. } } }

// disable all of the blinker tasks and prevent them from executing. // This is called when the button is pressed (disabling Blink Mode). void disableBlinkers() { for (int i = 0; i < LED_COUNT; i++) { taskList[i]->disable(); } }

/*********************************************** * Setup. * Initialise the serial monitor * Create all of the tasks. * Capture the starting time. */ void setup() { Serial.begin (9600); int cnt = 0; // Initialise the Serial port - but don't wait too long. while (!Serial && cnt < 100) { cnt++; delay(1); } Serial.println("Initialising");

resetBlinkers(true); // Create the blink tasks. chaserTask = new ChaserTask(250); // Create the chaser task taskList[LED_COUNT] = chaserTask; // add it to the list of all tasks. chaserTask->disable(); // initially, disable the chaser. buttonTask = new ButtonTask(2, 10); // Create the button task. taskList[LED_COUNT + 1] = buttonTask; // add it to the list of all tasks. Serial.println("Ready");

timePrev = millis(); // Initialise the "previous time" value to the current time. }

/********************** * Loop * */ void loop() { unsigned long timeNow = millis(); // Obtain the "current time". if (timeNow != timePrev) { // Has time moved on? unsigned long timeDelta = timeNow - timePrev; timePrev = timeNow; // Work out how much time has passed and capture "now" for next time. // For each and every task (8 Blink + 1 chaser + 1 button monitor) // record the fact that time has passed. // This will invoke the individual tasks' "execute" method if it is due // to be invoked. for (int i = 0; i < TASK_COUNT; i++) { taskList[i]->recordTime(timeDelta); } // Check if the button has been pushed. if (buttonTask->isButtonPressedAndReset()) { mode = (mode + 1) % MODE_CNT; // Move on to the next mode. switch(mode) { // Turn on the new mode (and turn off the previous mode). case BLINK_MODE: Serial.println("Switching to Blink Mode"); resetBlinkers(false); // When moving into BLINK_MODE, generate new blinkers (with new random on/off times) chaserTask->disable(); // disable the chaser. break; case CHASER_MODE: Serial.println("Switching to Chaser Mode"); disableBlinkers(); // When moving into CHASER_MODE, disable the blinkers chaserTask->enable(); // enable the chaser. break; default: // Unexpected mode??? - should never happen Serial.print("Warning: Unexepected mode: "); Serial.print(mode); Serial.println(" - resetting to blink mode."); mode = BLINK_MODE; // If it does, revert to BLINK mode. resetBlinkers(false); chaserTask->disable(); break; } } } }

Step 10: Mega Blinky LED Extravaganza

So, this is the moment you might have been waiting for: The Arduino Mega Blinky LED Extravaganza!

Below is the code for the Mega Blinky LED Extravaganza => independently, simultaneously blinking 32 LED's at random rates. Note you will need an Arduino Mega to run this. Why? Because it uses 32 I/O pins.

The only differences between this and the previous example are:

  • Pushing the button just changes the blink rate of the LED's (there is no Chaser mode)
  • It simultaneously blinks 32 LED's at random
  • It looks prettier (especially in the dark)

The key takeaway from this example, is how easy it is to add more functionality through cooperative multitasking. Apart from removing the chaser and "mode switch" logic attached to the button press, the only substantive modification was to change the ledPins array definition to list 32 pins on the Arduino Mega.

Hooking up the LEDs

You can layout the LED's anyway you like. I elected to lay them out in an 8 x 2 rectangle format. The following instructions explain how I did it (it was a little bit tedious - but worth it IMHO).

Basically the layout is a series of eight blocks of LED's. Each block consists of 4 LED's in a 2 x 2 arrangement. I've outlined each block in the photo. The photo shows 6 complete blocks and two missing blocks.

Each block of 4 LED's occupies five (5) rails on the breadboard. There is one central rail that brings power (5V) to the 4 LED's in that block. This is shown by the red wires at the center of each block.

The other 4 rails in each block are the cathodes of LED's (one per LED) and are used to connect the 470 ohm current limiting resistors. Thus each block (from left to right) consists of 2 x 470 ohm resistors, the power connector (red wire) followed by 2 x 470 ohm resistors. This is repeated four (or more) times along the breadboard. Finally, the 4 blocks on one line of the breadboard is replicated on the other side of the divider.

To layout the LED's on the board in a matrix, start by inserting the resistors. As mentioned, there should be 2 resistors followed by the 5V connection followed by 2 more resistors.

Next, insert the LED's starting with the one closest to the resistor. As shown by the lines in the photo, the Anode of each of the LED's plugs into the 5 Volt rail. The cathode of the first LED connects to the rail closest to the 5 Volt rail this is shown by the blue lines. The second LED is the same, except that it connects to the rail closest to the 5 Volt rail but on the other side. Connect the remaining two LED's but reach each LED over the rail closest to the 5V line to connect to the outer 2 rails in the block.

I used the above connection arrangement to try to minimise the risk of the LED or resistor legs accidentally touching one another.

Once you've inserted all of the resistors followed by the LED's, connect the other end of each resistor to the Arduino Mega using jump wires. I used a sequence of colours to try to ensure that I connected them in order - but it doesn't really matter what order you connect them.

Connect each of the jump wires to one of the Digital I/O pins numbered 22 through 53 on the Mega.

The photos show how the completed LED matrix is wired and how the jumper wires connect to the base of the Arduino Mega.

Hooking up the button

The connections to the button are exactly the same as the earlier project. Refer to the appropriate step above for details and diagrams. Basically the connections are:

  • Connect one pin of the button to 5V.
  • Connect the diagonally opposite pin to a 10K ohm resistor.
  • Connect the other end of the 10K ohm resistor to GND.
  • Connect the junction of the 10K ohm resistor and the button to pin 2 on the Arduino Mega.

/******************************************************************************
* Cooperative Multitasking * 07 - Mega Multitasking trace and blink with OO * * This is the seventh in a series of programs to illustrate the benefits of * a simple multitasking mechanism for Arduino. * The enhancement is to simultaneously blink 32 LEDs. * * This version also removes the chaser mode. Pressing the button, resets the * LED blink rates. * * The key takeaway from this project is how easy it is to add as many new * tasks as we need. In this case, simply by adding entries to the ledPins * array. */

// Define the pin for the input button #define BUTTON_PIN 2

// Define the pins to be used in tracing mode. unsigned int ledPins [] = { 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53}; #define LED_COUNT (sizeof (ledPins) / sizeof(ledPins[0]))

// Context information for sub-tasks unsigned long timePrev = 0;

// Comment or uncomment this next line to disable or enable debugging // messages. // Note: debug messages can take a long time to output and will // interfere with the mulitasking. //#define DEBUG

/************************************************ * Class TimedTask. * * An abstract (incomplete) class that manages the scheduling of sub tasks. * * This class tracks the elapsed amount of time on behalf of it's subclasses. * When the alotted time has passed, the "execute" method will be invoked to * allow the task to do whatever it needs to do. * NB: This TimedTask class would ideally be "factorised" out into a library. * If we did this, then the entire functionality could be accessed by a single * line of code (as opposed to the 70 odd lines here. The single line of code * would be something like: #include . */ class TimedTask { public: // Constructor - capture the time that has to pass until the task needs to be invoked. TimedTask(unsigned long nextEventTime) { this->nextEventTime = nextEventTime; }

// Set the next event time. void setNextEventTime(unsigned long nextEventTime) { this->nextEventTime = nextEventTime; }

// Abstract methods which must be implemented (defined) in a subclass. virtual unsigned long execute(); // Execute the task. virtual String getName(); // Retrieve the name of the task. virtual void disableTask(); // Invoked when this task is being disabled. virtual void enableTask(); // Invoked when this task is being enabled.

// Enable this task. void enable() { enabled = true; timeSinceLastEvent = 0; // Reset the elapsed time counter. enableTask(); // Notify the subclass that the task has been enabled. }

// disable this task. void disable() { enabled = false; disableTask(); // Notify the subclass that the task has been disabled. }

// Return the enabled/disabled state of the task. boolean isEnabled() { return enabled; }

// Record the fact that time has passed. void recordTime(unsigned long delta) { timeSinceLastEvent += delta; // Record the time and check if this task is due to be // executed. NB: the task is only executed if it is enabled. if (timeSinceLastEvent >= nextEventTime && enabled) { #ifdef DEBUG // Serial.print ("Executing task: "); // Serial.println(getName()); #endif unsigned long nev = execute(); // Notify the subclass to do it's thing. if (nev > 0) { // Record the next event time if it is non zero. nextEventTime = nev; } timeSinceLastEvent = 0; // Reset the time counter. } }

private: unsigned long nextEventTime; // Time that must pass before we invoke the subtask. unsigned long timeSinceLastEvent = 0; // The time has passed since the last invocation of the subtask. boolean enabled = true; };

/************************************************ * Class BlinkTask. * Extends TimedTask. * * An implementation (i.e. complete) of a TimedTask that blinks a single LED. * * This class toggles the state of the LED when it is invoked. */ class BlinkTask : public TimedTask { public: // Constructor needs to know which pin the LED is connected to. BlinkTask(int ledPin) : TimedTask(0){ pinMode(ledPin, OUTPUT); // Initialise the PIN as output. digitalWrite(ledPin, HIGH); // Turn the LED off. this->ledPin = ledPin; // track the pin. setNextEventTime(offTime); // set the time to the next invocation. taskName.reserve(15); taskName = "blink "; taskName += ledPin; #ifdef DEBUG outputDetails(); // print the details of this BlinkTask. #endif }

// Execute the Blink Task // This simply toggles the current state of the LED and returns the period // of time that must pass before the next invocation. unsigned long execute () { ledOn = !ledOn; // Toggle the LED on/off flag. // Serial.print("Turning LED "); // Serial.println(ledOn ? "on" : "off"); digitalWrite(ledPin, ledOn ? LOW : HIGH); // Set the LED state. return ledOn ? onTime : offTime; // return the next delay time. }

// Retrieve the name of the task as "Blink " + the led's Digital Pin number. String getName() { return taskName; }

// disable the blink task void disableTask() { digitalWrite(ledPin, HIGH); // Turn the LED off. }

// enable the blink task - nothing to do here. void enableTask() { onTime = randomInterval(); // The time the led will be on (ms) offTime = randomInterval(); // The time the led will be off (ms) setNextEventTime(offTime); digitalWrite(ledPin, HIGH); #ifdef DEBUG Serial.print("Enabling: "); outputDetails(); #endif }

// print the details of this blink task. void outputDetails() { Serial.print("Blink task pin: "); Serial.print(ledPin); Serial.print(", onTime: "); Serial.print(onTime); Serial.print(", offTime: "); Serial.println(offTime); }

private: unsigned long randomInterval() { return 500 + random(1500); } unsigned long onTime = randomInterval(); // The time the led will be on (ms) unsigned long offTime = randomInterval(); // The time the led will be off (ms) boolean ledOn = false; // Initially the LED will be off. int ledPin; // The digital I/O pin for the LED. String taskName; // the name of the task };

/************************************************ * Class ButtonTask. * Extends TimedTask. * * An implementation (i.e. complete) of a TimedTask that detects a button press. * * Ancillary methods may be invoked to ascertain if the button has been pressed or not. */ class ButtonTask : public TimedTask { public: // Constructor: // button Pin - the pin the button to be monitored is connected to. // nextEventTime - the time delay between checks to see if the button has been pressed. ButtonTask(int buttonPin, unsigned long nextEventTime) : TimedTask(nextEventTime) { pinMode(buttonPin, INPUT); this->buttonPin = buttonPin;

taskName.reserve(15); taskName = "**** button "; taskName += buttonPin; }

// Checks to see if the button has been pressed. // If it has been pressed, it will debounce the press and // set appropriate indicators recording the press when the button is released. unsigned long execute() { // Read the current state of the button. int currButtonState = digitalRead(buttonPin); if (prevButtonState == LOW) { if (currButtonState == HIGH) { // Button was just pressed. debounceCnt = 0; // Start the debounce count. Serial.println("Button pressed"); } } else { // Previously the button has tracked as "pressed". if (currButtonState == HIGH) { // Is it still pressed? debounceCnt += 1; // Count the number of intervals that it has remained pressed } else { // Button has been released. Serial.print ("Button released. Debounce Cnt: "); Serial.println(debounceCnt); // Set the button pressed flag to true, if the button has remained pressed // for the required amount of time. buttonPressedInd = (debounceCnt > debounceThreshold); debounceCnt = 0; // reset the debounce count. } } prevButtonState = currButtonState; // Remember this button state for next time. return currButtonState == HIGH ? 1 : 10; // Check every 1 ms while button is pressed, // otherwise just check once every 10 ms. }

// Retrieve the task name as "button " + digital pin I/O number. String getName() { return taskName; }

// Enable the task - nothing special to do. void enableTask() { }

// disable the task - nothing special to do. void disableTask() { }

// Ancilliary method to query the state of the button. boolean isButtonPressed() { return buttonPressedInd; }

// Ancilliary method to query the state of the button // and reset it's state to false (not pressed) boolean isButtonPressedAndReset() { boolean result = buttonPressedInd; buttonPressedInd = false; return result; }

// The number of times (ms) that the button must remain pressed to // count as an actual press. Any "presses" less than this duration // are ignored as noise. const int debounceThreshold = 10; private: int buttonPin; // The digital I/O pin to which the button is connected. int debounceCnt = 0; // How many contiguous "pressed" readings have we observed? int prevButtonState = LOW; // State of the button - last time we checked. boolean buttonPressedInd = false; String taskName; // the name of this task. };

/***************************************************** * The task list. * * Establish an array of tasks. All of the tasks go into this array irrespective of their * specific type. * We seperately track the button task and chaser task, although not strictly necessary, in * individual variables (as well as in the array) so we can more cleanly interact with them. * * The array is an array of pointers to TimedTasks. This is to allow any class that extends TimedTask * to be placed into the array. The draw back is that technically we do not know what the task types are, * so we can only directly invoke the methods in TimedTask. Fortunately, through inheritence and abstraction, * the TimedTask can invoke the specific methods that are defined as abstract to obtain the specific tasks * function (i.e. the TimedTask can activate/call the specific task's individual "execute" method to get it to * do whatever it does). */ // Define how many tasks there are in total. #define TASK_COUNT (sizeof taskList / sizeof taskList[0]) TimedTask *taskList[LED_COUNT + 1]; // Note that the number of tasks is the number of LED's plus one // as we do not need to have space for the (non-existant) chaser task. ButtonTask *buttonTask;

// Create as many blink tasks as defined in the ledPins array. There will be one // Blink Task for each defined pin. // This is called from setup and when the button is pressed (activating Blink Mode) void resetBlinkers(boolean create) { for (int i = 0; i < LED_COUNT; i++) { if (create) { taskList[i] = new BlinkTask(ledPins[i]); } else { taskList[i]->enable(); // Our enable implementation, resets the operating parameters. } } }

/*********************************************** * Setup. * Initialise the serial monitor * Create all of the tasks. * Capture the starting time. */ void setup() { Serial.begin (9600); int cnt = 0; // Initialise the Serial port - but don't wait too long. while (!Serial && cnt < 100) { cnt++; delay(1); } Serial.println("Initialising");

resetBlinkers(true); // Create the blink tasks. buttonTask = new ButtonTask(2, 10); // Create the button task. taskList[LED_COUNT] = buttonTask; // add it to the list of all tasks. Serial.println("Ready");

timePrev = millis(); // Initialise the "previous time" value to the current time. }

/********************** * Loop * */ void loop() { unsigned long timeNow = millis(); // Obtain the "current time". if (timeNow != timePrev) { // Has time moved on? unsigned long timeDelta = timeNow - timePrev; timePrev = timeNow; // Work out how much time has passed and capture "now" for next time. // For each and every task (8 Blink + 1 chaser + 1 button monitor) // record the fact that time has passed. // This will invoke the individual tasks' "execute" method if it is due // to be invoked. for (int i = 0; i < TASK_COUNT; i++) { taskList[i]->recordTime(timeDelta); } // Check if the button has been pushed. if (buttonTask->isButtonPressedAndReset()) { resetBlinkers(false); // When moving into BLINK_MODE, generate new blinkers (with new random on/off times) } #ifdef DEBUG timePrev = millis(); // Reset the timer when debugging to allow for the fact // that it takes a heck of a long time to output the debug messages. // If you press the button when a large proportion of the LED's are lit, // you can actually see the slow progress of the program as the LED's are // turned off as each one's duty cycle is reset when the DEBUG messages // are enabled. // Compare this to the "instaneous" turning off of the LED's when // the button is pressed with DEBUG messages disabled. #endif } }

Epilog X Contest

This is an entry in the
Epilog X Contest

Share

    Recommendations

    • Weaving Challenge

      Weaving Challenge
    • Remix Contest

      Remix Contest
    • Pie Contest

      Pie Contest

    Discussions