Cooperating Sequential Tasks

  1. Introduction
    1. Arduino
    2. Interrupts
  2. Simple Tasks
  3. Multiple Tasks
  4. Communicating Tasks
  5. Communicating Values
  6. Synchronising Tasks
  7. Buffered Communication
  8. Multiple Senders
  9. Conditional Tasks
  10. Transput
  11. Implementation
    1. Common Data
    2. Tasks
    3. task_builder
    4. task
    5. channel
    6. virtual_machine
    7. set
    8. queue
    9. clock
    10. standard
    11. Adjusting Limits

Communicating Tasks

So far the blinker task example has shown how multiple independent tasks can be created in a highly modular fashion. If you have to turn a set of naturally concurrent tasks into a single sequential task, because you only have one, or a limited number of processors, you end up tangling the individual task descriptions together into something that is always going to be more complicated to understand and maintain. Using multitasking avoids this problem.

Now we will consider how to describe tasks that need to interact with each other, i.e. to communicate. For this we will define two different tasks. The first one will manage a simple button connected so as to pull an input low when it is pressed. The second will toggle the state of an LED every time it receives a single to do so. The button_manager task is shown below.

#include "Tasks.h"

void button_manager_setup(int button_pin)
  pinMode(button_pin, INPUT_PULLUP);

bool button_down(int button_pin)
  return digitalRead(button_pin) == LOW;

bool button_up(int button_pin)
  return digitalRead(button_pin) == HIGH;

void button_manager(int button_pin, int button_signal)
  const int debounce_interval = 30;
  new_task(button_manager_setup, button_pin);
    wait(button_down, button_pin);
    wait(button_up, button_pin);

This task is designed to wait until the button is pressed, and then wait until it is released again before sending a signal to some other task. Because physical buttons do not provide clean on/off signals due to contact bounce[1], it is necessary to pause for some time after the button is pressed or released to avoid the task responding to the rapid bouncing of the contacts. In this case the wait time is to be 30 milliseconds, but some buttons may require longer.

The sequence of actions described by the button_manager function starts with a call to the button_manager_setup function to set the button_pin as an input with a built-in pull-up resistor[2] so that a button connected between ground and the input pin will generate a LOW level on the pin when the button is pressed, but a resistor internal to the Arduino will pull it to a HIGH level when the button is not pressed. The first action the task takes is to wait for the button to be pressed. The multitasking library provides a wait function for this purpose. This either accepts as a parameter a function with no parameters that returns a boolean result, or it excepts a function which takes one integer parameter and returns a boolean result. In the latter case a second integer parameter must also be supplied to the wait function. This works in much the same way that we have already seen used with the new_task and action functions. In this case the call of wait(button_down, button_pin) specifies that, when the task is executed, it should wait at this point until a call of button_down(button_pin) returns true. This will happen when a digitalRead of the button_pin returns a LOW value.

The complete sequence of actions to be performed by the task is therefore, wait for the button to be pressed, pause for 30 milliseconds until the contacts should have stopped bouncing, wait for the button to be released, pause again to wait for the contacts to stop bouncing and then send a signal to tell another task that a complete button click has occurred. Sending a signal is specified by the multiprocessing library's send_on function. This takes an integer parameter which identifies a communication channel between tasks. As we will see later, it is not necessary to know which integers identify which channels as this is handled by the multiprocessing library. In this case the identity of the channel to be used is passed as the second parameter to the button_manager function. The task cannot proceed after it elects to send a signal until another task is ready to receive it on the same channel.

As an aside, it should be pointed out that the method used to “de-bounce” the button here is much simpler than would often be found in conventional Aduino programs, where the equivalent would involve using the delay function instead of pause. However delay blocks the normal operation of the processor and more complex constructs involving recording when button events occur are often used instead[3]. In the multitasking version we simply fall back on the fact that, whereas delays are bad because they block the normal sequential execution of instructions, pauses are good because they let the processor do something else!

Now we will define an led_manger task which will toggle the state of an LED every time it receives a signal from another task, which could be the button_manager described above, or any other task that sends it a signal. The led_manger task is show below. This is very much like the blinker task we have already seen, but uses an external signal to decide when to change the state of the LED.

#include "Tasks.h"

void led_manager_setup(int led_pin)
  pinMode(led_pin, OUTPUT);

void managed_led_off(int led_pin)
  digitalWrite(led_pin, LOW);

void managed_led_on(int led_pin)
  digitalWrite(led_pin, HIGH);

void led_manager(int led_pin, int toggle_signal)
  new_task(led_manager_setup, led_pin);
    action(managed_led_off, led_pin);
    action(managed_led_on, led_pin);

As with the blinker task the led_manager starts by setting the led_pin as an output. It then turns the managed_led off by writing LOW to the appropriate pin. The task then waits until it receives a signal from another task. This is specified by the multitasking library function called receive_from. As with the send_on function used in the button_manager, this takes an integer parameter identifying a communication channel. Again it is not important to know the particular value of the integer used as it is passed as a parameter to the led_manager function. The task cannot proceed once it has elected to receive a signal until another task sends one on the specified channel.

Before describing sending and receiving signals in more detail, we will define the main program required to connect the button_manager and the led_manger together. This is shown below.

#include "Tasks.h"

void setup()
  int button_signal = new_channel();
  button_manager( 2, button_signal);
  led_manager   (13, button_signal);

void loop()

We can describe the complete system this creates graphically as follows.


As previously, the first step is to call init_tasks to setup the multiprocessing library. The new step introduced here is the use of new_channel to create an integer value associated with a communication channel. The corresponding channel is maintained by the multitasking library and is created in an inactive state, i.e. neither ready to send or to receive a signal. When the channel has been created, we can call the button_manager and led_manager functions to create the two tasks and, by passing the same channel identifier to both, cause them to be connected together. If this program is uploaded to an Arduino with an LED connected to pin 13 and a switch connected to pin 2, the LED will change its state every time button is pressed and then released.

The kind of communication used here is often call “block on send and receive”, “fully synchronous communication” or “unbuffered communication”. This implies that if the sender sends before the receiver is is ready to receive, the sender has to wait for the receiver. The converse is also true, i.e. if the receiver tries to receive before the sender is ready, the receiver has to wait for the sender. An alternative, not implemented here, is called “buffered communication” where the sender can send without waiting as often as it likes, and the receiver only has to wait if nothing has been sent. It turns out the unbuffered communication is the more general case as it can be used to implement buffered communication but the converse is not true.

In the context of the preceding example it is important realise that the button_manager could be connected to any other task that requires a signal to tell it when a button has been pressed. Similarly the led_manager could accept signals from any other task in order to toggle the state of the LED. This demonstrates that tasks connected by channels can provide a source of reusable components for modularising systems as do functions and classes, i.e. libraries of simple tasks can be used as a basis for the construction of more complex systems.

Next: Communicating Values Between Tasks