A very simple Arduino task manager

The LED chain project I'm working on requires that the AVR microcontroller handles several different tasks:

  • Read a rotary encoder and switch
  • Drive a seven-segment display
  • Drive a radio
  • Drive four LED strips each containing twenty LEDs
  • Provide logic to tie all the above together

Normally that sort of multiple-task workload might suggest the use of a RTOS, but the Arduino Pro Mini has only 32Kb of program memory (of which 2Kb is used by the bootloader) and 2Kb of RAM, so every byte is precious. So, whatever we come up with has to be as minimal as possible. OK, let's see if we can make some assumptions and trade-offs that will help keep things simple and create a small set of C++ classes we can use to implement the task management we need:

  • Use cooperative multitasking rather than preemptive multitasking. Out of that fall a couple of constraints:
    • Each task must complete quickly when it runs. Long-running operations such as delay() are forbidden.
    • Any interrupt service routines must also complete quickly, preferably by recording the details of the event and scheduling a task to handle it.
  • The task management system should not require the use of dynamic memory management (e.g. malloc()) so as to minimise memory requirements.
  • The list of tasks will be fixed at compile-time. That's reasonable as the configuration of the system is fixed - we aren't going to be adding new hardware on the fly.
  • Tasks will be scheduled in priority order to allow processing that has strict time constraints to be handled first, but to keep things simple the priority order will be fixed at compile time. Again, that's reasonable as the configuration of the system is known in advance.
  • Tasks can communicate with each other by making standard C++ method calls but (as for interrupts) any such methods should simply store the details of the event and schedule themselves to be run to handle the event.
  • Much of the processing we are doing is time-driven, e.g. sequencing the LED patterns, so explicit support for scheduling tasks at specific intervals should be provided, as well as more general 'triggered' tasks.

The last constraint needs some further thought - what timer 'tick' interval should we use? The 'real world' events we will be dealing with won't be happening quicker that 1 millisecond apart and the CPU is clocked at 16MHz which equates to 16000 instructions per millisecond. Scheduling time-based tasks at millisecond resolution will allow us to run several tasks within the same 'tick' and will be more than fast enough to deal with the events we have to handle.

OK, so given all that, what would a suitable task implementation look like?

class Task {
public:
    virtual bool canRun(uint32_t now) = 0;
    virtual void run(uint32_t now) = 0;
};

class TimedTask : public Task {
public:
    inline TimedTask(uint32_t when) { runTime = when; }
    virtual bool canRun(uint32_t now);
    inline void setRunTime(uint32_t when) { runTime = when; }
    inline void incRunTime(uint32_t inc) { runTime += inc; }
    inline uint32_t getRunTime() { return runTime; }
protected:
    uint32_t runTime;
};

bool TimedTask::canRun(uint32_t now) {
    return now >= runTime;
}

Yep, that's really all there is to it. Each task can be queried to see if it can be run via the canRun(), method and if it can, it will be executed via a call to its run() method. We pass in the current time in milliseconds to avoid each task having to separately determine it. The canRun() and run() methods could be merged, but having them separate allows us to provide more flexible scheduling if we ever need to, e.g. by penalising tasks that are runnable too often.

OK, next step is to implement the actual task scheduler:

class TaskScheduler {
public:
    TaskScheduler(Task **task, uint8_t numTasks);
    void run();
private:
    Task **tasks;
    int numTasks;
};

TaskScheduler::TaskScheduler(Task **_tasks, uint8_t _numTasks) :
  tasks(_tasks),
  numTasks(_numTasks) {
}

void TaskScheduler::run() {
    while (1) {
        uint32_t now = millis();
        Task **tpp = tasks;
        for (int t = 0; t < numTasks; t++) {
            Task *tp = *tpp;
            if (tp->canRun(now)) {
                tp->run(now);
                break;
            }
            tpp++;
        }
    }
}

Again, that really is all there is to it. We create the task scheduler with a fixed list of tasks, then call its run method. The run method iterates endlessly over the task list, calling the canRun() method on each in turn to see if it needs to be run. If it does, its run() method is called to execute the task. One very important note: after running a task we break out of the iteration over the task list and start back at the top of the list. That gives us the fixed task priority feature - if multiple tasks are runnable the earlier tasks on the list will always be dispatched first and the later tasks on the list will be lower priority,

The last part is to show an example of how to actually use the scheduler. Each task is derived from either the base Task class or from TimedTask, depending on how it needs to be run. And example TimedTask that blinks the pin 13 LED might look something like this:

// Timed task to blink a LED.
class Blinker : public TimedTask
{
public:
    // Create a new blinker for the specified pin and rate.
    Blinker(uint8_t _pin, uint32_t _rate);
    virtual void run(uint32_t now);
private:
    uint8_t pin;      // LED pin.
    uint32_t rate;    // Blink rate.
    bool on;          // Current state of the LED.
};

Blinker::Blinker(uint8_t _pin, uint32_t _rate)
: TimedTask(millis()),
  pin(_pin),
  rate(_rate),
  on(false)
{
    pinMode(pin, OUTPUT);     // Set pin for output.
}

void Blinker::run(uint32_t now)
{
    // If the LED is on, turn it off and remember the state.
    if (on) {
        digitalWrite(pin, LOW);
        on = false;
    // If the LED is off, turn it on and remember the state.
    } else {
        digitalWrite(pin, HIGH);
        on = true;
    }
    // Run again in the required number of milliseconds.
    incRunTime(rate);
}

If you compare this to the standard Blink sketch you'll see there are no calls to delay() in the run() method. Instead the method simply toggles the LED state, sets up when it is next to run and returns. If we did call delay() we'd sit there waiting and no other tasks would get a chance to run - by returning as quickly as possible we return control to the scheduler which can then find any other tasks that are runnable and run them. The scheduler will take care of calling the Blinker::run() again when the required time interval has passed.

Having defined our tasks classes, in the main body of the program we create a list of the tasks, pass it to a TaskScheduler instance and then run the scheduler, which then takes care of calling the run() methods of the tasks at the right time:

    // Create the tasks.
    Blinker blinker(13, 25);
    Echoer echoer;

    // Initialise the task list and scheduler.
    Task *tasks[] = { &blinker, &echoer };
    TaskScheduler sched(tasks, NUM_TASKS(tasks));

    // Run the scheduler - never returns.
    sched.run();

That's all there is to it. With this approach it's possible to provide a lightweight set of communicating tasks that are scheduled in priority order. The code is both high performance and lightweight, two vital attributes considering the constrained environment it must operate in. Providing the various run() methods are reasonably short, it will run tasks within less than one millisecond of when they are scheduled, which is perfectly adequate for our needs - I'll give an example of using this library to perform a timing-critical process (switch de-bouncing) in a later post.

It's simple enough to implement your own variant, but if you want the code it's available here. The archive also includes an example sketch that blinks the pin13 LED whilst simultaneously reading the serial port and echoing characters back.

Acknowledgement

I'd like to acknowledge my MSc tutor Dr. Colin Machin who sat down with me one afternoon back in 1984 and outlined this approach to me, which I then used for the Z80 robot controller that was the subject of my MSc thesis. He'd used used the same technique for a LIDAR data acquisition system he'd written to collect data on the wingtip vortices caused by commercial aircraft as they land. Good ideas always stand the test of time - thanks Colin :-)

Categories : AVR, Tech


Re: A very simple Arduino task manager

Can You show example for Display class?

Re: A very simple Arduino task manager

The Display class is just an example rather than an actual class.

Re: A very simple Arduino task manager

Alan-

Thank you for sharing this, I really like the look of your solution, and have an immediate need for it.

I am an old-school, procedural programmer who has not yet fully wrapped his head around OO coding practices. Can you share a basic sketch that has a simple task declared and implemented? I am really struggling to instantiate the task class and know how to code it.

Any help is greatly appreciated.

Thanks again!

Re: A very simple Arduino task manager

Hi Brian, I have added a simple example to the body of the post above which should give you enough to get started.  When I get a chance I'll add a demo sketch to the Task library and upload a new version.

Re: A very simple Arduino task manager

Alan-

I cannot thank you enough, which makes this even harder to beg for more help. I have the code structured as best I can figure out, but still get this exception:

Task.cpp: In constructor 'Blinker::Blinker()':
Task.cpp:30: error: no matching function for call to 'TimedTask::TimedTask()'
Task.h:63: note: candidates are: TimedTask::TimedTask(uint32_t)
Task.h:60: note:                 TimedTask::TimedTask(const TimedTask&)

I know that there are many ways for me to mess this up, but I've tried to restructure and tweak this to no avail. Any help you can provide is very much appreciated. I have a project that I need to break apart into 2 AVR controllers, because I've run out of SRAM. I think that this task scheduling mechanism is going to save me from a procedural quagmire. Many thanks to you, and especially for the fast reply!

PS

I did notice and resolve a glitch in the same Blinker::Blinker declaration, where there is not a method called incRunTime() but rather setRunTime().

Re: A very simple Arduino task manager

Hi Brian - yes, unfortunately there was an error in the example code - I've fixed it.  The incRunTime() method is only in the newer version of the Task library - I've uploaded the new version of the library so you should grab a copy.  I've also put an example sketch in the new library version, so you should be able to use that as a template.

Re: A very simple Arduino task manager

 Alan - you are truly a kind soul! Thank you for helping me out so much. I will try this out tonight and hopefully I'll have some great news to report back. :-)

Many, many thanks to you!

Re: A very simple Arduino task manager

Alan-

I just wanted to let you know that this really did the trick for me. I set up my master/slave 'duinos, with the slave handling a number scheduled tasks, and the master periodically (based on a scheduled task) asking for some data, and receiving it promptly. It is working like a charm.

Thanks again, you are a life saver.

Re: A very simple Arduino task manager

I'm pleased to hear it worked out for you, sorry about the hiccough with the incorrect example :-)

Re: A very simple Arduino task manager

 Happy 2012 to you Alan. I was just ploughing through Arduino 1.0 breakage, and have hit your libraries. I was wondering if you'd had the occasion to look at compatibility issues with the new Arduino 1.0 release?

Many thanks!

Re: A very simple Arduino task manager

 Alan - please disregard. The errors were my mistake. :-) (oops)

Re: A very simple Arduino task manager

 Alan-

I had to stop by and once again thank you for your task manager code. Months later, I am still expanding my project, and your code is still at the heart of it - making it simple and efficient to add new features and workload. The mighty little AVR ATMEGA328 can do an amazing amount of work when you have an efficient architecture such as yours, instead of crippling it with calls to sleep().

Thanks again! Your work continuously makes me smile. :-)

My project, and props to you here: http://flic.kr/p/by8jZB

Re: A very simple Arduino task manager

You are welcome, glad you find it useful and thanks for the feedback :-)

Re: A very simple Arduino task manager

Thank you, this is exactly what I have been looking for. 

Re: A very simple Arduino task manager

Alan-

Thank you, sincerely, for all your hard work. This Task Scheduler implementation has really saved me a lot of time. With the limited experience I have using C/C++, my own rendition was little more than a Rube Goldberg machine.

I noticed there is a class named "triggertask." About half of my tasks fall into this category, but admittedly I'm not having much success getting it to work. Would you happen to have an example/ code snippet of its basic use? I've been struggling for a while now, and thought I'd suck it up and ask for a little help.

Thanks in advance!

Jeff

Re: A very simple Arduino task manager

A TriggeredTask is just a Task with a predefined flag field in it to say when it is runnable, inherit from it as normal. To set the flag and mark it as runnable, call the setRunnable() method. There's a predefined canRun() method that just checks the flag and returns true if it is set. Then, in your run() method, call resetRunnable() to clear the flag otherwise the task will remain permanently runnable, which almost certainly isn't what you want.

Re: A very simple Arduino task manager

Jeff-

I am also a very happy consumer of Alan's task scheduler work, and I have successfully used the trigger-type task. The key is in the "canRun" method returning true. For instance, in my latest project, I have a triggered task that fires if a global boolean variable is true. The "canRun" method looks like this:

bool serialWriter::canRun(uint32_t now)
{
    return runWirelssCommunications;
}

The bool runWirelessCommunication is set to true in a timed task when it is time to fire this "triggered" task, and it works great. I just make sure that I reset the boolean runWirelessCommunication to false when I initialize it, and at the end of my serialWriter task run method. Does that make sense?

Re: A very simple Arduino task manager

Hi Brian - Thanks for the help! I'll try to wrap my head around it late night when I get some free time. Would you mind posting a pared down example or an actual fragment of your successful code (or a link to it), including the full class of the triggered task, and its setup and call? (all thats required, that is) When it comes to OO stuff, it really helps to be able to see it. I would really appreciate it and I'm sure it would help others as well. Jeff O.

Re: A very simple Arduino task manager

Jeff-

Sure thing. Hopefully this one doesn't get HTML tag whacked like the last one. :-/ There are two tasks that are interplaying here, I read a sensor (dustSensor) and when that is done, I set that global boolean variable to true, to allow the other task, serialWriter, to fire. I don't want to fire the latter until the sensor has been read.

Here is the dustSensor timed task:

// Timed task to monitor the Dust Sensors
class dustSensorCheck : public TimedTask
{
public:
    // Create a new blinker for the specified pin and rate.
    dustSensorCheck(uint32_t _rate);
    virtual void run(uint32_t now);
private:
    uint32_t rate;    // Blink rate.
};

dustSensorCheck::dustSensorCheck(uint32_t _rate)
: TimedTask(millis()),
  rate(_rate)
{
}

void dustSensorCheck::run(uint32_t now)
{
    // DO SOMETHING HERE
    runWirelssCommunications = true;
    // Run again in the required number of milliseconds.
    incRunTime(rate);
}

Here is the triggered "Task" serialWriter:

// Task to output data to the JeeLink
class serialWriter : public Task
{
public:
    serialWriter();
    virtual void run(uint32_t now);
    virtual bool canRun(uint32_t now);
};

serialWriter::serialWriter()
: Task()
{
    // initialize
}

bool serialWriter::canRun(uint32_t now)
{
    return runWirelssCommunications;
}

void serialWriter::run(uint32_t now)

    // DO SOMETHING
    runWirelssCommunications = false;
}

Here are the scheduler lines:

dustSensorCheck dustCheck(1000);
serialWriter sendWirelessData;
// Initialise the task list and scheduler.
Task *tasks[] = { &sendWirelessData, &dustCheck };
TaskScheduler sched(tasks, NUM_TASKS(tasks));

Good luck!!

Re: A very simple Arduino task manager

I've tidied up both of your entries, FYI if you want to put up code snippets the best thing is to put them inside

 ..   
tags which will preserve the whitespace and layout. To do that, click the 'Source' button in the toolbar and use the text editor to enter the code shippet, not forgetting to HTML-escape any &, < and > characters :-)

Re: A very simple Arduino task manager

Rather than using a global variable it would be better to have serialWriter inherit from TriggeredTask and then pass the address of the serialWriter as a parameter to the dustSensorCheck constructor - that of course means declaring serialWriter first, then dustSensorCheck. Then when dustSensorCheck wants to trigger serialWriter it calls serialWriter->setRunnable(). And finally, the run method of serialWriter should call resetRunnable() to clear the flag that says it is runnable.

Re: A very simple Arduino task manager

 Jeff O - You know what - I just realized that I have been talking about the "Task" task, not the "TriggeredTask" - I am sorry. Hopefully this helps you out anyway, it effectively does what I think you're looking for. Sorry about that.

Re: A very simple Arduino task manager

Hi Brian (and Alan) thanks again. its not super clear to me at this point (with limited c++ experience), but i will look at it all this weekend and try to piece it together. It sounds like Alan has a bit of a cleaner solution in mind, which uses his triggertask directly. my goal is to get a simple blink triggeredtask written as an example for other users, and as something to build upon for my own project. as always, any help toward this end will be greatly appreciated!! jeffo

Re: A very simple Arduino task manager

 Very nice.

Couldn't find NUM_TASKS, so used "sizeof(tasks)/sizeof(Task)"

Re: A very simple Arduino task manager

It's defined in TaskManager.h, but what you used is exactly equivalent to the macro.

Re: A very simple Arduino task manager

Hi Alan,

thank you for this great piece of code. I adapted it to C and now use it happily in my project. I posted the code on my blog, maybe it helps someone else.

-Ferdinand

Re: A very simple Arduino task manager

Hi Alan,

Have you run this task manager until the millisecond counter overflows?  If I read the code correctly, then if runTime ends up as something like 2^32 - 1 and canRun() is called after millis() overflows, then canRun() will always return false and the task will never run again.

 

It's not important in most cases, since this scenario would take about 50 days to happen, but I'm curious what the best way to work around it would be.  Mostly because I'm thinking of implementing a similar sort of scheduler, but with a resolution of 16 microseconds instead of 1 millisecond, and that overflow point would be reached in about 19 hours.

 

Thanks,

-Kent

Re: A very simple Arduino task manager

Alan,

What license is this code published under?  It looks useful, and I generally go with GPLV3.  I've obviously given you attribution in the comments...

I've implemented this run loop / message pump / etc several times in C and C++, but never given it a proper class - I like your implementation.

Thanks 

Re: A very simple Arduino task manager

Hi Alan,

Just want to thank you for this article. Has helped me a lot with getting things done in a logical (for me :-) ) way on Arduino.

I would like to start putting my various Arduino projects up on github just so I can have everything in one place, and was wondering if you would mind either putting this task manager up on github yourself, or otherwise allowing me to do so with all acknowledgments pointing to you and this page.

Since my projects mostly use your task manager, it will just be easier to have that in it's own place on github instead of having to include it with every project.

Cheers,
  Tom