Suggestion: add simple multitasking to the stdlib

I sometimes need to put in a sketch loop different code parts with different timing requirements. E.g. a sensor that must be read every 20 milliseconds, a PWM that must be updated every 50 milliseconds, a status update that must be sent over the serial every 120 ms, and so on.

AFAIK there's no simple and obvious way to integrate these tasks in the loop() function.

So my proposal is to add to the Arduino standard library a few functions that make easier to integrate different tasks in the main loop without having to worry about how their timings interact with each other.

I've implemented a first version of it, using cooperative multitasking.

Here's some demo code, a fancy version of the "blink" example:

void appendTask(int (*func)());

int ledPin = 13;
int isLedOn = false;

void setup() {
    pinMode(ledPin, OUTPUT);
    appendTask(blinkDemoTask);
}

void loop() {
    runNextTask();
}

int blinkDemoTask() {
    if (isLedOn) {
        digitalWrite(ledPin, LOW);
        isLedOn = false;
    }
    else {
        digitalWrite(ledPin, HIGH);
        isLedOn = true;
    }
    return 1000;
}

struct Task {
    unsigned long time;
    int (*func)();
};

struct Task tasks[10];
int tasksLen = 0;

void appendTask(int (*func)()) {
    tasks[tasksLen] = (struct Task) {
        millis(), func
    };
    tasksLen++;
}

void sortTasks() {
    unsigned long now = millis();
    for (int i = 0; i < tasksLen; i++) {
        if (tasks[i].time < now) {
            tasks[i].time = now;
        }
    }
    qsort(tasks, tasksLen, sizeof(tasks[0]), taskCompar);
}

int taskCompar(const void *a, const void *b) {
    struct Task *ta = (struct Task *)a;
    struct Task *tb = (struct Task *)b;
    if (ta->time < tb->time) {
        return -1;
    }
    else if (ta->time == tb->time) {
        return 0;
    }
    return 1;
}

void runNextTask() {
    if (tasksLen > 0) {
        sortTasks();
        struct Task *task = &tasks[0];
        while (task->time > millis()) {
            // pass
        }
        task->time = task->func();
        task->time += millis();
    }
}

The important user code is "appendTask(blinkDemoTask);" in setup() which creates a new task and the function blinkDemoTask() which implements the task. It toggles the led and requests with "return 1000;" to be called again after 1000 ms. It's also a good example of how tasks are usually implemented: a status variable and an "if" or a "switch" that does a bit of work, updates the status variable and returns.

There can be any number of tasks running independently at the same time, as long as they are written to return ASAP. I.e. calls to delay() must be replaced with a "return ;".

I suggest to add to Arduino functions equivalent to appendTask(), runNextTask(), sortTasks() and taskCompar() in this example. Only appendTask() and runNextTask() are part of the public API.

Obviously this is only a first draft to start a discussion, two clear shortcomings are that it uses a static array of 10 tasks (should probably use dynamically allocated memory; each task requires 6 bytes) and there isn't yet a way do cancel a task (shouldn't be hard to add). But I have already used them with success a few times, including a sketch with 6 tasks (PWMs, sensors, web server and misc digital outputs).

Minor detail: I added the function prototype "void appendTask(int (*func)());" at the beginning of this sketch because Arduino didn't automatically generate it. Probably because it doesn't like function pointers used as parameters.

So what do you think? It's a good idea that could be developed into something useful or there's a better way to do this that I don't know?

Some kind of timing / scheduling functionality like this is something that I'd like to see included at some point. It probably requires a good conversation on the developers list about the best way to do it, though, and we haven't done that yet.

I was in need for a task switching thingy, too. So I took the above code as an inspiration. I changed the following:

  • longest wait time now unsigned long (longer wait than 32s)
  • removed the sorting of the task list
    (is it really needed to have a sorted list? is it faster than looking up?
    qsort costs about 1,5k, so I think: no)
  • changed task indexer to byte
  • appendTask does not add to many tasks...
  • at least handle the millis()-overflow somehow

So I got a small little piece of code that lives well in a IDE-TAB and can be compiled with only adding one line of code (due to some Arduino IDE limitations).

Now it would be a good idea to optimize the wait-while-loop. Something like "let it sleep" or so.

Comments welcome.

#define maxtasks 10

// Die Task-Struktur
struct Task {
  unsigned long time;
  unsigned long (*func)();
};

struct Task tasks[maxtasks];
byte tasksLen = 0;

void appendTask(unsigned long (*func)()) {
  if (tasksLen<maxtasks) {
    tasks[tasksLen] = (struct Task) {
      millis(), func
    };
    tasksLen++;
  }  
  // und wie kriegt man mit, dass es zu viele Tasks sind?
}


void runNextTask() {
  byte t;
  static unsigned long lastmillis = 0;

  // overrun detect
  if (lastmillis>millis()) {
    // reset all times. 
    for (byte i=0;i<tasksLen;i++) {
      tasks[i].time=0;
    }
  }
  lastmillis=millis();

  if (tasksLen > 0) {
    t=0;

    for (byte i=0;i<tasksLen;i++) {
      if (tasks[i].time<tasks[t].time) {
        t=i;
      }
    }

    while (tasks[t].time > millis()) {
      // pass
    }
    tasks[t].time = tasks[t].func();
    tasks[t].time += millis();
  } 
}

Blink looks like this

const int ledPin = 13;

// this should not be needed and been catched by Arduino... but it isn't.
void appendTask(unsigned long (*func)());

unsigned long blink() {
  digitalWrite(ledPin, ! digitalRead(ledPin));
  return 1000;
}

void setup() {
  pinMode(ledPin, OUTPUT);
  appendTask(blink);
}  

void loop() {
  runNextTask();
}