Kernel - Event driven Delays and Intervals

Evan Boldt's picture

Reasons to use Kernel

delay() is Bad

When you use delay or delayMicroseconds in Arduino, nothing else can happen until the delay is finished, which can be a huge problem if you have more than one thing going on simulteneously, as will always be the case when building a more advanced robot.

Imagine that you want to check a sensor once a second. Easy enough. Just delay then check the sensor. What if you want to blink an LED on and off every 40ms, but still checking that sensor. Any delays used by one will mess up the timing on the other. If you delay one second after checking the sensor, the light will not blink during that time. You could make a fixed number of blinks between sensor checks, but that is inflexible and accumulates error.

Problems with Alternatives

Two alternatives already exist. Both are based on the idea of constantly asking "can I go yet?" in an if statement.

  • You can check the times yourself,  but that requires a lot of work and can make the code hard to read.
  • You can also use Metro , which wraps the check up into a nice class where you just set an interval and run a .check() method. 

I really like Metro, but it still requires the program to be written somewhat linearly instead of in seperate functions, and it is not really meant to be used for one-time delays.

If you are using an Arduino Due, there the Scheduler library that provides an easier interface, but is exclusive to the Due's ARM CPU.

Events are Easy and Powerful

If you've ever dealt with GUIs or Javascript, you're probably comfortable with events. They're a nice way to make the most use out of a single threaded application (like a browser window, or an Arduino) because, as long as you avoid any blocking, things happen when they're supposed to.

Essentially, each "task" is a function which gets called only when the "event" happens. The event, for example, can be a timing trigger or a user interaction.

Kernel takes care of timing the event calls. It can automatically maintain both run-once and run-repeatedly events simulteneously.

Since Kernel is keeps track of which event needs to happen next, but still using delayMicroseconds, better timing precision and consistency is achived.

Other Potential Benefits

Since a Kernel is such a advanced framework, with a little additional programming, some really useful features can be added:

It could keep track of how long a task takes to complete, or give more important tasks priorities, and factor that into decisions of which task can go when, like a true kernel would for process management.

A single "background" process could be run, without interfering too much with the time-sensitive tasks. Which could be accomplished with the runNow method.

How it Works

There is a queue of functions.  They are in the ordered chronologically. Since it is a linked list, the functions can be inserted in before other functions, but after others.

Now all that Kernel does is look at the next task coming up to see if it is the right time. If it is not time yet, it will use delayMicroseconds to give it a precise start time. 

Repeating events (called intervals) will then be re-added to the queue in the correct chronological order.

Since the soonest event is always in the front of the queue, no events will be passed over while using delay.

Basic Usage

First, download the kit -  kernelTest.zip. It includes Kernel (GPL by me), fastDelegate (Public Domain from Code Project), a modified version of the QueueList library (from the Arduino Playground), and a simple Arduino .ino file to show some basic usage.

The usage is intentionally very similar javascript, which uses setInterval and setTimeout for repeating and one-time tasks respectively.

//may need to use <> instead of ""
// depending on if you installed Kernel as a library.
#include "kernel.h" Kernel kernel; //this should be a global, so other files can use it too. void test() { Serial.println( micros() ); } void setup() { Serial.begin(19200); //add a function to interval. It will run every 10,000 microseconds. kernel.setInterval(10000UL, test); // UL means the unsigned long } void loop(){ //should avoid any code that could cause much delay here

// runNext decides whether to delay or run the next function kernel.runNext(); }

That's it! In a lot of ways, it's even cleaner than a Metro. Of course, there is a lot more going on under the hood.

This above example shows how to add an interval. It is also possible to add a timeout by using setTimeout instead, which will only happen once.

Sidenote about Unsigned Longs (UL)

Intervals and timeouts are set in microseconds. Since microseconds add up so quickly, Arduino deals with them as unsigned longs, but they still overflow (go back to zero) after about 70 minutes. The setInterval and setTimeout functions take durations in microseconds stored in unsigned longs too. Sometimes, it might be necessary to say that the what the type of the constant you have in your code is. For example, if you are dividing numbers, you might prefer to use a 2.0 instead of a 2, since a 2 is an integer and might typecast the other number into an integer also - removing the decimal precision. Similarly, problems might or might not happen with very long numbers represented by long and unsigned long. Arguments will by typecast into an unsigned long, so it will probably not be necessary to specify "UL" at the end of your numbers.

Advanced Usage

Remove a task

Both setInterval and setTimeout return a unique integer PID. It can be used to later remove a task from the queue.

int newPID = kernel.setInterval( 100000UL, test);
kernel.clearInterval( newPID );

Remove all tasks

Perhaps all current tasks need to stop happening. Maybe you've come accross an error and want to stop everything.

kernel.stopALL();

Break Up a Long Task

Sure, you could create a seperate task for a background task, but if your loop runs quickly enough, you can essentially run kernel like you might a Metro, except it is only checking the next pending task.

void loop() {
for( int index = 0; i < hugeArray; i++ ) {
//do some operations

// will not delay until next task, tasks may be overdue
kernel.runNow();
}
}

You need to do either runNow() frequently, or do runNext() at the end. Otherwise, no events will be called. 

Attachments: