Taskfun. Preemptive multitasking for AVR and SAMD21

Hi All,

I have this multitasking library that I hope will be useful. GitHub - glutio/Taskfun: Minimalist preemptive multitasking. I would like to ask the community to review and give feedback and/or help test. I submitted it to Arduino library repository and it should be available there shortly. I tested the basic functionality on two AVR and two SAMD21 boards but it needs more testing I think. I bought an Arduino kit for fun and ended up writing this library. I know how to program but I do not know what scenarios are typical in the world of hobby microcontrollers, so asking the community to test this library in typical scenarios reading sensors, displaying temperature or whatever. This is not an RTOS, and it makes no such claims, this is a task switcher which adds minimal but true preemptive multitasking to Arduino sketches on boards that target beginners. Thanks for your feedback!

Some highlights:

  • strongly typed argument for tasks instead of void*,
  • support for both functions and class methods as tasks,
  • 3 task priority levels (maps to % of CPU time),
  • SyncVar<> class for synchronized access for simple types (overloads all operators and adds noInterrupts/interrupts())

Here is an example of how you can blink in the main loop and run a chatbot as a task.

#include <Arduino.h>
#include "Taskfun.h"

void chatBot(const char* botName)
{
  while(1) {
    Serial.print(botName);
    Serial.println("> What is your name?");

    while(!Serial.available());
    auto userName = Serial.readString();

    Serial.print(botName);
    Serial.print("> Hello, ");
    Serial.println(userName);
  }
}

void setup() {
  Serial.begin(115200);
  while(!Serial);
  pinMode(LED_BUILTIN, OUTPUT);
  
  noInterrupts();
  setupTasks();
  runTask(chatBot, "Arduino");
  interrupts();
}

void loop() {
  digitalWrite(LED_BUILTIN, HIGH);
  delay(1000);
  digitalWrite(LED_BUILTIN, LOW);
  delay(1000);
}

3 Likes

Only one example ? TaskBlink - Wokwi ESP32, STM32, Arduino Simulator

I did not test it yet, but I have a few notes:
A library is included with #include <Taskfun.h>
You don't have to include Arduino.h in a sketch.
A empty loop() is a task running crazy doing nothing ?
Is there a way to use the Serial output from two tasks ?

My first suggestion would be to enable more compiler warnings (e.g., -Wall -Wextra -pedantic) and to compile the example:

In file included from .../Arduino/libraries/Taskfun/src/Taskfun.h:4:0,
                 from .../Taskfun/examples/TaskBlink/TaskBlink.ino:1:
.../Arduino/libraries/Taskfun/src/BTaskSwitcher.h: In function 'int runTask(void (*)(T), T, unsigned int, uint8_t)':
.../Arduino/libraries/Taskfun/src/BTaskSwitcher.h:153:1: warning: no return statement in function returning non-void [-Wreturn-type]
 }
 ^
.../Arduino/libraries/Taskfun/src/BTaskSwitcher.h: In function 'int runTask(const T*, void (T::*)(U), U, unsigned int, uint8_t)':
.../Arduino/libraries/Taskfun/src/BTaskSwitcher.h:158:1: warning: no return statement in function returning non-void [-Wreturn-type]
 }
 ^
.../Arduino/libraries/Taskfun/src/BTaskSwitcher.h: In instantiation of 'static int Buratino::BTaskSwitcher::run_task(Buratino::BTask<T>, typename Buratino::BTask<T>::ArgumentType, unsigned int, uint8_t) [with T = int; typename Buratino::BTask<T>::ArgumentType = int; uint8_t = unsigned char]':
.../Arduino/libraries/Taskfun/src/BTaskSwitcher.h:152:39:   required from 'int runTask(void (*)(T), T, unsigned int, uint8_t) [with T = int; uint8_t = unsigned char]'
.../Taskfun/examples/TaskBlink/TaskBlink.ino:31:38:   required from here
.../Arduino/libraries/Taskfun/src/BTaskSwitcher.h:113:21: warning: comparison between signed and unsigned integer expressions [-Wsign-compare]
     while (new_task < _tasks.Length() && _tasks[new_task]) {
            ~~~~~~~~~^~~~~~~~~~~~~~~~~
.../Arduino/libraries/Taskfun/src/BTaskSwitcher.h:118:18: warning: comparison between signed and unsigned integer expressions [-Wsign-compare]
     if (new_task == _tasks.Length()) {
         ~~~~~~~~~^~~~~~~~~~~~~~~~~~
In file included from .../Arduino/libraries/Taskfun/src/BTaskSwitcher.h:6:0,
                 from .../Arduino/libraries/Taskfun/src/Taskfun.h:4,
                 from .../Taskfun/examples/TaskBlink/TaskBlink.ino:1:
.../Arduino/libraries/Taskfun/src/BList.h: In instantiation of 'void Buratino::BList<T>::Resize(unsigned int) [with T = Buratino::BTaskSwitcher::BTaskInfoBase*]':
.../Arduino/libraries/Taskfun/src/BList.h:33:13:   required from 'void Buratino::BList<T>::Add(T) [with T = Buratino::BTaskSwitcher::BTaskInfoBase*]'
.../Arduino/libraries/Taskfun/src/BTaskSwitcher.h:119:14:   required from 'static int Buratino::BTaskSwitcher::run_task(Buratino::BTask<T>, typename Buratino::BTask<T>::ArgumentType, unsigned int, uint8_t) [with T = int; typename Buratino::BTask<T>::ArgumentType = int; uint8_t = unsigned char]'
.../Arduino/libraries/Taskfun/src/BTaskSwitcher.h:152:39:   required from 'int runTask(void (*)(T), T, unsigned int, uint8_t) [with T = int; uint8_t = unsigned char]'
.../Taskfun/examples/TaskBlink/TaskBlink.ino:31:38:   required from here
.../Arduino/libraries/Taskfun/src/BList.h:56:24: warning: comparison between signed and unsigned integer expressions [-Wsign-compare]
     for (auto i = 0; i < min(capacity, _capacity); ++i) {

-pedantic / -Wpedantic is more challenging, but enabling the -Wall -Wextra compiler warning flags can be done easily by adjusting your Arduino IDE preferences:

  1. Select File > Preferences... from the Arduino IDE menus.
    The "Preferences" dialog will open.
  2. Select "All" from the "Compiler warnings" menu in the "Preferences" dialog.
  3. Click the "OK" button.
1 Like

Thank you for good feedback. I now fixed all compiler warnings and updated the library in the repo.

Wow, thanks for this!

I was able to print from multiple tasks just fine. The safest way would be to do

noInterrupts();
Serial.println("Hello from task");
Serial.flush();
interrupts();

But simply using Serial.println() did not crash so maybe it's ok too.

Will try to come up with more examples. Any suggestions what scenario to demonstrate especially something that could run in the simulator? Thanks!

Here is another example, playing music while blinking :slight_smile: TaskTone - Wokwi ESP32, STM32, Arduino Simulator
Eight tasks (including loop()) are running in parallel, each responsible for playing one of the 7 notes.

#include "Taskfun.h"

#define PIN 3

const char* _melody[] = {
  "4G4", "4F4", "4G4", "4D4", "8B3", "4D4", "2G3",
  "4G4", "4F4", "4G4", "4D4", "8B3", "4D4", "2G3",
  "4G4", "4A4", "4B4", "8A4", "4B4", "4B4", "8G4", "4A4", "8G4", "4A4", "4A4", "8F4", "4G4", "8F4", "4G4", "4G4", "8E4", "2G4"
};
const int _melodyLength = sizeof(_melody) / sizeof(_melody[0]);

// Frequency table for notes (in Hz)
const int _freq[9][7] = {
  { 27, 31, 16, 18, 21, 22, 25 },                // A0 - G1
  { 55, 62, 33, 37, 41, 44, 49 },                // A1 - G2
  { 110, 123, 65, 73, 82, 87, 98 },              // A2 - G3
  { 220, 247, 131, 147, 165, 175, 196 },         // A3 - G4
  { 440, 494, 261, 294, 330, 349, 392 },         // A4 - G5
  { 880, 988, 523, 587, 659, 698, 784 },         // A5 - G6
  { 1760, 1976, 1047, 1174, 1319, 1397, 1568 },  // A6 - G7
};

SyncVar<int> _note(0);

int getFreq(const char* note) {
  return _freq[(note[2] - '0')][note[1] - 'A'];
}

int getDuration(const char* note) {
  return 800 / (note[0] - '0');
}

void playNote(char note) {
  while (1) {
    if (_melody[_note][1] == note) {
      tone(PIN, getFreq(_melody[_note]));
      delay(getDuration(_melody[_note]));
      noTone(PIN);
      delay(20);
      _note = (_note + 1) % _melodyLength;
    }
  }
}

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  pinMode(PIN, OUTPUT);
  setupTasks('G' - 'A' + 1);

  noInterrupts();
  for (auto i = 'A'; i <= 'G'; i++) {
    runTask(playNote, i);
  }
  interrupts();
}

void loop() {
  digitalWrite(LED_BUILTIN, HIGH);
  delay(1000);
  digitalWrite(LED_BUILTIN, LOW);
  delay(1000);
}

If you look at your TaskBlink that I put in Wokwi, then you see the "Library Manager". Wokwi uses the same list as the Arduino IDE for the libraries. So if the "Library Manager" of the Arduino IDE can use your library then Wokwi can use it as well.

1 Like

That will turn the interrupts off for a long time. The Serial.flush() will wait until everything is transmitted. When the TX buffer inside the Serial library is full, then the Serial.println() waits for a empty spot in that buffer, that will also take a long time.

Would this be a valid option ?

// Library: https://github.com/glutio/Taskfun
// This Wokwi project: https://wokwi.com/projects/366407158206025729
// Test to output messages to the Serial monitor from different tasks


#include <Taskfun.h>

SyncVar<bool> serialInUse;

void setup() {
  Serial.begin(9600);
  serialInUse = false;
  pinMode(LED_BUILTIN, OUTPUT);
  setupTasks();
  noInterrupts();
  runTask(TaskOne, 0);
  runTask(TaskTwo, 0);
  interrupts();
}

void loop() 
{
  digitalWrite(LED_BUILTIN, HIGH);
  delay(150);
  digitalWrite(LED_BUILTIN, LOW);
  delay(350);
}

void TaskOne(int) 
{
  while (true) 
  {
    Print("Hello");
    delay(2);
  }
}

void TaskTwo(int) 
{
  while (true) 
  {
    Print("!---------------!");
    delay(3);
  }
}

// Print the whole message.
void Print( char *s)
{
  while (serialInUse)   // Is another task using the Serial port ?
    yield();            // come back later

  serialInUse = true;   // set the flag
  Serial.println(s);
  serialInUse = false;  // release the flag
}

Try it in Wokwi, press the pause button to check that each whole message is printed: Taskfun mutex - Wokwi ESP32, STM32, Arduino Simulator

If I press pause I see incomplete text but maybe that is related to the pause itself not to mutlitasking, other than being cut on paus text prints fine, switching between tasks.
image

Also, based on your example, adding Semaphore to library examples. Don't want it to be a part of the library itself, since I target beginners and they should implement it themselves for fun or use my example.

class Semaphore {
protected:
  unsigned _count;
  
public:
  Semaphore(unsigned count)
    : _count(count) {}

  void Acquire() {
    while (1) {
      noInterrupts();
      if (_count > 0) {
        --_count;
        break;
      } 
      yield();
      interrupts();
    }
    interrupts();
  }

  void Release() {
    noInterrupts();
    ++_count;
    interrupts();
  }
};

class Mutex : public Semaphore {
public:
  Mutex()
    : Semaphore(1) {}
};

Would love for this library to become a part of Arduino core, like tone(), after it stabilizes. I wonder if it needs to support all Arduino platforms for that or can runTask() only be a feature for AVR and SAMD21 since I understand ESP32 already comes with built-in FreeRTOS support? I think in general there is a desire to add multitasking to Arduino, and there are many coop multitasking solutions but I could not find a simple preemptive multitasking one so hope this library will be useful. I used ChatGPT4 heavily while implementing it, since I am an Arduino beginner it helped me with inline asm, setting up interrupts and other, saved me alot of time...

The reason that there are many unfinished multi-tasking libraries for AVR, in the vast majority of them are written by beginners, is that in fact, for boards with such limited resourses, as a rule, real RTOS do not need.
And there is a much easier way to ensure the execution of two or three simultaneous tasks, without any multitask-system.

I don't think this applies to me :slight_smile:

I agree, it seems the way to think about programming microcontrollers is a bit diffrent than regular PCs. But I also base my understading of the need for multitasking because of this RFC discussion Multitasking · arduino/language · Discussion #2 (github.com)

of course not, sir :slight_smile:

1 Like

Yes, that's where you pressed the pause button. Every text before that is printed as a whole and the complete string together.

Another example of multitasking, Arduino playing pong TaskPong - Wokwi ESP32, STM32, Arduino Simulator.

Three tasks running in parallel - the main loop is drawing the game and computing score and one independent task for each player computing position of the paddle

image

1 Like

I get this often with Wokwi

image

Example of using Semaphore is here TaskPrimitives - Wokwi ESP32, STM32, Arduino Simulator

You have 3 LEDs that can signal in morse code the messages that you enter via serial input. You can enter more than 3 messages at a time, each message gets its own task that is waiting on the semaphore before using availabe LED. Not sure if this is a good example, looking for feedback, thanks!

I have not seen that "Build failed!" message by Wokwi. Maybe you are in a other part of the world with other servers.

The examples shows a number of things, that's good, but the result is not so fun.

Yes, I was wondering about that too.. I get it quite often for it to be a blocker at times like right now for example

Thank you for honest feedback :slight_smile: I'll see if I can improve it

I made it a bit more fun.. Now you can press the respective button to hear the morse code.. TaskPrimitives - Wokwi ESP32, STM32, Arduino Simulator

really a great job, providing MT which is pre-emptive!
Just for

void setupTasks(int numTasks = 3, int msSlice = 1);

I would suggest shorter time slices, e.g. by about 1 microsecond, perhaps in case for time-sensitive parallel tasks doing i2c a/o UART a/o SPI simultaneously.

1 Like