Index

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
images/8-1.png

Buffered Communication



In the earlier section on Communicating Tasks it was asserted that unbuffered communication is the most general form of communication because buffered communication can be described in terms of it, whilst the converse is not true. How then can buffered communication be described?

A simple one-place buffer is simply a task which manages a variable. Initially it waits to receive a value from some sender to store in the variable, and then waits to send the stored value to some receiver. We have used unbuffered communication in previous examples.

images/8-2.png

Now we are going to create a simple buffer to insert between the sender and the receiver as follows.

images/8-3.png

Given an initial unbuffered system, it will be possible to insert a buffer without modifying the sender or the receiver. For demonstration purposes the sender and receiver tasks will be very simple. The sender will send a sequence of integer values starting at zero. It will pause before sending the next value to simulate doing some actual work. So that we can see what is happening, the sender will print each value via the serial output.

#include "Tasks.h"

int sender_data = 0;

int next_data() 
{  
  sender_data = sender_data + 1;
  Serial.print("Send "); Serial.println(sender_data);
  return sender_data;
}

void sender(byte output, int delay_time)
{
  init_serial(9600);
  new_task();
    send_on(output, next_data);
    pause(delay_time);
  end_task();
}


The receiver will wait for a value to be sent to it and then print it to the serial output. It will also pause after printing the value to simulate doing some actual work.

#include "Tasks.h"

void show_data(int data)
{
  Serial.print("Receive "); Serial.println(data);
}

void receiver(byte input, int delay_time)
{
  init_serial(9600);
  new_task();
    receive_from(input, show_data);
    pause(delay_time);
  end_task();
}


We can connect and run these two tasks without a buffer as follows.

#include "Tasks.h"

void setup()
{
  init_tasks();
  
  byte c = new_channel();
  sender  (c, 100);
  receiver(c, 100);
}

void loop()
{
  run_system();
}


To extend this system to include a simple one-place buffer, we can define a buffer task as shown below.

#include "Tasks.h"

int buffer_data;

void put_data(int data)
{
  buffer_data = data;
}

int get_data()
{
  return buffer_data;
}

void buffer(byte c_in, byte c_out)
{
  new_task();
    receive_from(c_in,  put_data);
    send_on     (c_out, get_data);
  end_task();
}


As described previously, all this does is to repeatedly receive a value and then send it on. Once the data has been received, the sender can be doing something until it needs to send out another data value. Only then will it be delayed if the receiver has not accepted the previous value. Conversely, after receiving a value, the receiver can be doing something until it needs a new value. It will only be delayed if the sender has not provided one. The new system including the buffer looks like this.

#include "Tasks.h"

void setup()
{
  init_tasks();
  
  byte c_in  = new_channel();
  byte c_out = new_channel();
  sender  (c_in,  100  );
  buffer  (c_in,  c_out);
  receiver(c_out, 100  );
}

void loop()
{
  run_system();
}


The descriptions of the sender and receiver are unchanged. To create a multi-place buffer, all we need to do is connect more buffer tasks between the sender and the receiver. Here is a system with a three place buffer which will provide greater isolation between the rate at which the sender and receiver create and consume data. Data generated by the sender will propagate rapidly down the chain of buffers until it reaches the end accessible to the receiver. If the sender fills all three buffers, as shown below, the receiver can consume three data values before it gets held up waiting if the sender, possibly temporarily, stops generating data quickly enough.

images/8-4.png

The only problem with the existing buffer description is that no matter how many instances of the buffer task we create, they will all use the same buffer_data variable. This would cause the system above to behave contrary to what we require. It is therefore a good point to introduce a way of giving each task its own private data, no matter how many instances of it we create. The multitasking library provides two functions for this purpose.

The first is called set_task_data and allows a single integer value of our choosing to be uniquely associated with a particular task. The second is called get_task_data and allows the value associated with a task to be retrieved while it is running. The set_task_data function must be called when a task is being defined after the call of new_task. The get_task_data function can be called in any initialisation or action function, in a wait condition, or in a data source or sink function used by send_to and receive_from.

It might seem that a single fixed integer value associate with each task is a bit restrictive, but this value can be used as an index into an array, which might contain an integer value for each task, as will be used in the following extended buffer example, or could contain a struct value with many fields for each task.

The extended buffer task illustrates how this mechanism can be used to provide every instance of the task with its own integer variable. To do this we will associate an index value with the task when it is defined. The single integer variable buffer_data used previously is now replaced by an array of integers. To select the correct one, each instance of the task can retrieve its index value and use it to select an element from this array. Well written tasks using this mechanism will not abuse it by accessing another task's data. This would create unpredictable behaviour.

#include "Tasks.h"

int buffer_data[buffers];

void put_data(int data)
{
  int index = get_task_data();
  buffer_data[index] = data;
}

int get_data()
{
  int index = get_task_data();
  return buffer_data[index];
}

void buffer(int index, byte c_in, byte c_out)
{
  new_task();
    set_task_data(index);
    receive_from(c_in,  put_data);
    send_on     (c_out, get_data);
  end_task();
}


The new definition can be exploited in a very general way to provide buffering of arbitrary size as follows.

#include "Tasks.h"

const int buffers = 3;

void setup()
{
  init_tasks();
  
  byte c_in  = new_channel();
  byte c_out = c_in;
  sender(c_in, 100);
  for (byte i = 0; i < buffers; i = i + 1)
  {
    c_out = new_channel();
    buffer(i, c_in, c_out);
    c_in = c_out;
  }
  receiver(c_out, 1000);
}

void loop()
{
  run_system();
}


This creates a channel called c_in for the first buffer and connects it to a sender task. A sequence of buffers, the length of which is defined by the buffers constant, are then created, each with a new channel to be its output c_out. The input to each buffer is the output from the previous one, or the output from the sender. The output from the final buffer is connected to a sender. It should be noted that, as with all good software structures, this works in the degenerate case where the buffers constant is zero. In that case, as you should expect, the output from the sender is connected directly to the receiver and their communication is unbuffered.

Notice that the in the above program the sender has been configured to pause for 100 milliseconds between sending values while the receiver is configured to pause for 1000 milliseconds (1 isecond) between receiving values. If we upload and run this program, we should see something like the following output in a serial terminal.

Send 1
Receive 1
Send 2
Send 3
Send 4
Receive 2
Send 5
Receive 3
Send 6
Receive 4
Send 7
Receive 5
Send 8
Receive 6
Send 9
Receive 7
Send 10
...


Because the receiver runs much slower the the sender, the sender is initially able to rapidly fill the buffer. After this it must slow down to match the speed of the receiver. In real systems buffers are used to smooth out variations in transmission and reception speeds where sometimes the sender may run faster than the receiver, and at other sometimes it is the other way round.

Next: Multiple Senders