Simple Arduino Scheduler

This Simple Arduino Scheduler library allows multiple loop() functions to be run in a collaborative multi-tasking style. Please find an minimalistic implementation on GitHub - mikaelpatel/Arduino-Scheduler: Portable Cooperative Multi-tasking Scheduler for Arduino. There is a large set of example sketches ranging from multi-blink with tasks to event queues.

The Scheduler function for starting a task is:

bool Scheduler.start(taskSetup, taskLoop [, taskStackSize]);

The start function will return true if the task could be created otherwise false. The task will run when the main task yields. The taskSetup and taskLoop functions are run by the created task. The taskSetup is run once while the taskLoop is repeatedly called by the task (just as the Arduino main function calls setup() and loop()). There is an optional parameter to set the task stack size. The default value is 128 bytes.

The Scheduler interface contains two additional functions; begin() to set the main task stack size (if the default is not sufficient) and the second to check available stack().

A multi-blink example sketch with three tasks used to blink three LEDs could look something like this:

#include <Scheduler.h>

void setup()
{
  Scheduler.start(setup1, loop1);
  Scheduler.start(setup2, loop2);
  Scheduler.start(setup3, loop3);
}

void loop()
{
  yield();
}

void blink(int pin, unsigned int ms)
{
  digitalWrite(pin, HIGH);
  delay(ms);
  digitalWrite(pin, LOW);
  delay(ms);
}

const int led1 = 11;

void setup1()
{
  pinMode(led1, OUTPUT);
}

void loop1()
{
  blink(led1, 500);
}

const int led2 = 12;

void setup2()
{
  pinMode(led2, OUTPUT);
}

void loop2()
{
  blink(led2, 250);
}

const int led3 = 13;

void setup3()
{
  pinMode(led3, OUTPUT);
}

void loop3()
{
  blink(led3, 1000);
}

Cheers!

PS: This implementation is a scaled down port of the Cosa Nucleo multi-tasking support library.

setjump is unsafe, not is equal to save/restore context!
why you not use ufficial scheduler for all Arduino Board?

Why not just use millis() as in Several Things at a Time

...R

not as millis, It is very different.
with the scheduler you can simplify tasks that are too complex

vbextreme:
setjump is unsafe, not is equal to save/restore context!
why you not use ufficial scheduler for all Arduino Board?

That implementation is over 400 lines of code and uses malloc/free. This implementation is only approx. 40 lines of code and much easier to verify (and will also run on ATtiny).

kowalski:
This implementation is only approx. 40 lines of code

The essential part of Serial Input Basics uses 2 lines of code

if (currentMillis - previousServoMillis >= servoInterval) {
   previousServoMillis += servoInterval;

But lot's of people tell me that bigger is better. :slight_smile:

...R

backstage setjump performs many operations. But the point is that setjump does not save the context and then you will have errors.
malloc was made by developers of Arduino, I have just ported.
the alloca function is deprecated.
For Arduino it's more safe malloc.

For attiny I prefer millis.

Robin2:
The essential part of Serial Input Basics uses 2 lines of code

if (currentMillis - previousServoMillis >= servoInterval) {

previousServoMillis += servoInterval;




But lot's of people tell me that bigger is better. :)

@Robin2

Do you want to compare this with collaborative multi-tasking with context saving? Or are you just claiming that many of the multi-tasking issues in Arduino sketches can be handled with this simple pattern? I believe you are claiming the second as there is a great difference in the amount of code needed in the application space. For a single delay(servoInterval) three lines of code is needed with this pattern. Add an additional delay() or a state and this explodes in a lot of extra code.

delay(servoInterval);

Becomes

if (currentMillis - previousServoMillis >= servoInterval) {
   previousServoMillis += servoInterval;
   ...
   }

What does this transform to with the pattern?

delay(T1);
action1();
delay(T2); 
action2();

The pattern may seem minimum but all state handling needs to be in sketch. Multi-tasking with context switch maintains the stack (control and data state). These needs to be global state variables in the pattern. The timing (delay) value in the pattern is "previousXXXMillis". In the context switch variant this is actually hidden in the delay() (and the yield() that it contains).

What is easier to read? The pattern with control structure and timing variables or simply a delay()?

Cheers!

vbextreme:
backstage setjump performs many operations. But the point is that setjump does not save the context and then you will have errors.
malloc was made by developers of Arduino, I have just ported.
the alloca function is deprecated.
For Arduino it's more safe malloc.

@vbextreme
Please give some references to these claims. Actually none of them are true.

malloc/free/alloca are part of libc and not made by the developers of Arduino. They are made by the team behind AVR GCC. alloca is not deprecated. Please check the AVR online documentation.

The implementation of setjmp/longjmp is actually a context switch with a context buffer (again check the online documentation and read the code). The main difference is that yield() cannot be used from an ISR (preemptive scheduling) as all registers are not saved. Only the registers required by the calling convention are saved in the context. There are many collaborative multi-tasking libraries that use setjmp/longjmp.

Malloc is a very bad thing to use in embedded systems. Search this forum and others for all the heap fragmentation issues caused by the String class. For static allocated threads (stacks) this may be less of a risk but the program size grows due to malloc.

Last, do you have any benchmarks for your implementation? It would be interesting to compare.

Cheers!

kowalski:
Or are you just claiming that many of the multi-tasking issues in Arduino sketches can be handled with this simple pattern? I believe you are claiming the second

Yes

What does this transform to with the pattern?

delay(T1);

action1();
delay(T2);
action2();

You post a complete program (including your library) that does that and I will post a "millis()" equivalent some time tomorrow. Then we can compare line counts.

I do agree there is value in readability.

...R

Robin2:
You post a complete program (including your library) that does that and I will post a "millis()" equivalent some time tomorrow. Then we can compare line counts.

I do agree there is value in readability.

@Robin2
Thanks for taking the time to help illustrate this.

#include <Scheduler.h>

void setup()
{
  ...
  Scheduler.start(actionLoop);
  ...
}

void loop()
{
  ...
}

const unsigned long T1 = ...;
const unsigned long T2 = ...;

void actionLoop()
{
  delay(T1);
  action1();
  delay(T2);
  action2();
}

void action1() 
{
  ...
}

void action2()
{
  ...
}

For completeness I would like to add that context switching may require both more SRAM and processing cycles. This is the cost of reducing algorithm complexity and improving readability.

Each thread stack must be able to hold any ISR frame/work-space together with max call stack depth. The extra stack requirement is in the order of number of threads times the max ISR frame/work-space. Some of this can be optimized as the context buffer could be used by the ISR.

This extra stack requirement may be much to large for MCUs with smaller SRAM sizes. A single stack and global state may require less SRAM but more program memory (for the explicit state saving, etc).

Consider the extra effort needed to achieve the below modified control structure:

void action2()
{
  while (digitalRead(BUTTON) == HIGH) yield();
  ...
  for (int i = 0; i < 10; i++) {
    action3();
    delay(T3);
  }
  ...
} 

void action3()
{
}

Cheers!

BW: Could any of the Scheduler examples be used to illustrate the different approaches?

First i try but your code not make.

#include <Scheduler.h>


void loop1(void)
{
    uint32_t test1 = 0x12ABCDEF;
    uint32_t test2 = 0xFEDCBA21;

    delay(1000);
    
    if ( test1 != 0x12ABCDEF )
        Serial.println("bug!");

    if ( test2 != 0xFEDCBA21 )
        Serial.println("bug!");
    
}



void setup(void) {
    Serial.begin(9600);
    
    Scheduler.begin();
    Scheduler.start(loop1);

}

void loop(void) {
    uint32_t bigop = 23;
    uint32_t fk = digitalRead(13);
    uint32_t ur = fk * bigop++;
    uint32_t abc = ur + digitalRead(13) - bigop;

    delay(1000);
}

but theoretically with there code you can view a bug, because setjmp save context only the first time it is called and register used after the call are not saved.For resolve a problem you need to declare volatile all variable you want use after setjump.
If you give me a working code I show the bug.

Alloca is deprecated, it's unsafe and useless.

there's a reason why developers Arduino used malloc instead of a static allocation.
Scheduler can create a "one shot task", when finished the task is freed memory with malloc is very simple and safe, with static memory it's very difficult to manage the fragmented memory.
And the last for beginner user it's more simple use Scheduler with dinamic memory.

Benchmark It's funny for arduino context.
my porting uses a lot of code just because based on defensive programming, but now you have a code and you can move to static memory and reduce code of 50%.Defensive programming required more code for safe code.

The last, download my porting and fun with preemption:

#include <Scheduler.h>
#include <avr/interrupt.h>
#include <avr/wdt.h>

/* PREEMPTIVE EXAMPLE */

ISR(WDT_vect,ISR_NOBLOCK)
{
    yield();
}


void watchdogI(byte e, byte p) 
{
    cli();
    wdt_reset();
    MCUSR &= ~(1 << WDRF);
    
    if( !e ) 
    {
        WDTCSR |= (1<<WDCE) | (1<<WDE);
        WDTCSR = 0x00;
        sei();
        return;
    } 
        
    WDTCSR |= (1<<WDCE) | (1<<WDE);
    WDTCSR =  (1<<WDIE) | ( p & 0x0E);
    sei();
}

void loop1(void)
{
    Serial.println("Infinite Loop without yield or delay");
    while(1);
}

void setup(void) {
    Serial.begin(9600);
    pinMode(13,1);
    Serial.println("start");
    watchdogI(1, 0);
    Scheduler.startLoop(loop1);
}

void loop(void) {
    /*not use delay or yield for create a infinite loop*/
    uint32_t t;
    digitalWrite(13,1);
    t = millis();
    while ( millis() - t < 1000 );
    digitalWrite(13,0);
    t = millis();
    while ( millis() - t < 1000 );
    
}

sorry for my English and write +1 on Pull request

@vbextreme

Please run the example sketches. The code above works just fine. If you run the example sketches you will also see that your claim about setjmp/longjmp is not correct. Volatile is needed when using read-write on global variables between threads. This is also needed in your solution.

Please provide reference to statement that alloca is deprecated in clib. It is just as unsafe as malloc (if you do not know what you are doing). Malloc may check that it was possible to allocate thread and stack in your implementation BUT this does not help stack overrun (which is the issue with alloca and can be checked, I will add this). Your code does check for NULL return from malloc() BUT there is no check on context switch for stack overrun. Also there is no return value indicating that the thread start/startLoop failed. The error handling is just dropped. I would not call that defensive programming.

One-shot thread allocated with malloc is also a very bad idea as this may give heap fragmentation. A static server thread pattern is much better. Beginners should not use multi-tasking before they understand the consequences. It is simply too easy to create very hard bugs.

Cheers!

Impressive, as always, Mssr. Kowalski. Pearls, you know...

@everyone, be sure to check out his Cosa framework. Very tight, very clean!

@vbextreme, I don't want to hijack this into a different and already long-settled discussion, but heap functions have no place in an embedded environment. I cut some slack for the tinkerer, but as soon as the sketch will really be used for something, I'm like flies on ugly. Predictability and determinism are paramount. It is irresponsible to recommend malloc et al to beginners, as it takes them down a path fraught with peril and frustration.

Cheers,
/dev

@/dev I did not write the scheduler
I have write a porting!!!!!!!
the original scheduler use malloc!

to be able to do the porting I removed the bug to function delay() and I have write only a context switch the rest of the class was unchanged.

With this code you can run the same code on any Arduino Boards.

I also think that a scheduler should be very different, not only in the method of allocating memory, it takes a lot of work and I think you have to take one step at a time, so take me +1 on pull request if you really want to contribute to a scheduler for Arduino.

@/dev cosa have a true context switch? or as a simulator?

@kowalski your code not work for me, not make! I'll show you when you do work all the bugs caused by setjump.
You have try my porting? work? make? it has been difficult? you could use this code?

@kowalski Alloca

GCC setjmp:
When you use setjmp and longjmp, the only automatic variables guaranteed to remain valid are those declared volatile. This is a consequence of automatic register allocation. Consider this function:

jmp_buf j;

foo ()
{
int a, b;

a = fun1 ();
if (setjmp (j))
return a;

a = fun2 ();
/* longjmp (j) may occur in fun3. */
return a + fun3 ();
}

Here a may or may not be restored to its first value when the longjmp occurs. If a is allocated in a register, then its first value is restored; otherwise, it keeps the last value stored in it.

vbextreme:
@kowalski your code not work for me, not make! I'll show you when you do work all the bugs caused by setjump.
...
@kowalski Alloca

@vbextreme
I am sorry to learn that you could not build the example sketches. Please add info about what Arduino IDE and board you are using. I have tested with 1.6.7 (Linux 32-bit), Nano, Pro-Mini and Mega. The MultipleBlinks tutorial works just fine (just change startLoop to start).

The reference to alloca was not that it was deprecated. It was a good description about how to use and not use it. The risks with alloca, etc. There is no problem replacing alloca as C++ allows variable array allocation on stack. And you can guess how that is implemented (man3).

Cheers!

I used 1.6.8 because not have a bug on function Delay(), With old delay (<=1.6.7) you can't write a scheduler for Avr.
Tomorrow post a error.

Alloca it is Like malloc.
The true scheduler not need Alloca or Malloc!
a bug caused by alloca() it would be impossible to find, you may have noticed that in my scheduler unless all 32 registers +SP +PC , but not necessary! at least half could not be saved, take for example r1 fixed to 0, is saved only for security, which put an unsuspecting user to modify the registry and we do not save, as you would find the bug? which saint you should pray?

It's not simple write a true scheduler, the Arduino scheduler not save current SREG (interrupt status) and all task share interrupt, it's correct or no? You could be discussed years on this topic.
Arduino scheduler not have priority, mutex, semaphore and more and more....

But I showed you how a scheduler cooperative can become simply a scheduler preemptive.
Would not you like to have a method that enables the automatic preemptive?

Scheduler.preemptive(true);

write +1 on pull request.

kowalski:
@Robin2
Thanks for taking the time to help illustrate this.

I was / am hoping you would/will post a complete program that I can run on my Uno.

...R

I think that it is important try to merge the vbextreme pull request in the official scheduler, and work togheter on the owfficial scheduler.
It only the beginning, but if anyone work on a personal scheduler version we will never have a good official scheduler.

The actual official scheduler is an android porting and work only on DUE.
If all will put a +1 on the vb PR we extend it on all board, and this is important, and after we can start to change all that we want.

Testato:
I think that it is important try to merge the vbextreme pull request in the official scheduler, and work togheter on the owfficial scheduler.
It only the beginning, but if anyone work on a personal scheduler version we will never have a good official scheduler.

The actual official scheduler is an android porting and work only on DUE.
If all will put a +1 on the vb PR we extend it on all board, and this is important, and after we can start to change all that we want.

porting scheduler for Avr by vbextreme · Pull Request #1 · arduino-libraries/Scheduler · GitHub

Official code should be much better than that before +1 on PR. There is a lot to be cleaned up. Please start a new topic, use gist and let us discuss that. This topic was intended to present some work on a very Simple Scheduler for Arduino (while waiting for an official from the Arduino team).

Getting a Scheduler into the official release of Arduino will take even more discussions. And this is not the right place. All the +1 on PR will not help. There are many ripple effects to consider on the Arduino and other cores. And this might explain why the "official" Scheduler was not made available to all boards from the beginning.

Anyway here is some news on the latest update of this Scheduler.

  1. Improved thread allocation
    Alternative thread allocation using C++ variable vector. Detect when thread allocation fails (no more SRAM). Update to Scheduler.start(); will return true if successful otherwise false.

  2. Additional example sketches

  3. Improved benchmarking
    Some results for Arduino 1.6.7, Mega, Nano and Pro-Mini
    a. Initiate scheduler and main thread: 4 us
    b. Start a thread: 24 us
    c. Yield thread: 11 us

Cheers!

BW: This is the topic you should be looking at if you are interested in a professional Scheduler.