Home

Awesome

AceRoutine

AUnit Tests

NEW: Profiling in v1.5: Version 1.5 adds the ability to profile the execution time of Coroutine::runCoroutine() and render the histogram as a table or a JSON object. See Coroutine Profiling for details.

A low-memory, fast-switching, cooperative multitasking library using stackless coroutines on Arduino platforms.

This library is an implementation of the ProtoThreads library for the Arduino platform. It emulates a stackless coroutine that can suspend execution using a yield() or delay() functionality to allow other coroutines to execute. When the scheduler makes its way back to the original coroutine, the execution continues right after the yield() or delay().

There are only 2 core classes in this library:

The following classes are used for profiling:

The following is an experimental feature whose API and functionality may change considerably in the future:

The library provides a number of macros to help create coroutines and manage their life cycle:

Here are some of the compelling features of this library compared to others (in my opinion of course):

Some limitations are:

After I had completed most of this library, I discovered that I had essentially reimplemented the <ProtoThread.h> library in the Cosa framework. The difference is that AceRoutine is a self-contained library that works on any platform supporting the Arduino API (AVR, Teensy, ESP8266, ESP32, etc), and it provides a handful of additional macros that can reduce boilerplate code.

Version: 1.5.1 (2022-09-20)

Changelog: CHANGELOG.md

Table of Contents

<a name="HelloCoroutines"></a>

Hello Coroutines

<a name="HelloCoroutine"></a>

HelloCoroutine

This is the HelloCoroutine.ino sample sketch which uses the COROUTINE() macro to automatically handle a number of boilerplate code, and some internal bookkeeping operations. Using the COROUTINE() macro works well for relatively small and simple coroutines.

#include <AceRoutine.h>
using namespace ace_routine;

const int LED = LED_BUILTIN;
const int LED_ON = HIGH;
const int LED_OFF = LOW;

COROUTINE(blinkLed) {
  COROUTINE_LOOP() {
    digitalWrite(LED, LED_ON);
    COROUTINE_DELAY(100);
    digitalWrite(LED, LED_OFF);
    COROUTINE_DELAY(500);
  }
}

COROUTINE(printHelloWorld) {
  COROUTINE_LOOP() {
    Serial.print(F("Hello, "));
    Serial.flush();
    COROUTINE_DELAY(1000);
    Serial.println(F("World"));
    COROUTINE_DELAY(4000);
  }
}

void setup() {
  delay(1000);
  Serial.begin(115200);
  while (!Serial); // Leonardo/Micro
  pinMode(LED, OUTPUT);
}

void loop() {
  blinkLed.runCoroutine();
  printHelloWorld.runCoroutine();
}

The printHelloWorld coroutine prints "Hello, ", waits 1 second, then prints "World", then waits 4 more seconds, then repeats from the start. At the same time, the blinkLed coroutine blinks the builtin LED on and off, on for 100 ms and off for 500 ms.

<a name="HelloScheduler"></a>

HelloScheduler

The HelloScheduler.ino sketch implements the same thing using the CoroutineScheduler:

#include <AceRoutine.h>
using namespace ace_routine;

... // same as above

void setup() {
  delay(1000);
  Serial.begin(115200);
  while (!Serial); // Leonardo/Micro
  pinMode(LED, OUTPUT);

  CoroutineScheduler::setup();
}

void loop() {
  CoroutineScheduler::loop();
}

The CoroutineScheduler can automatically manage all coroutines defined by the COROUTINE() macro, which eliminates the need to itemize your coroutines in the loop() method manually. Unfortunately, this convenience is not free (see MemoryBenchmark):

On 8-bit processors with limited memory, the additional resource consumption can be important. On 32-bit processors with far more memory, these additional resources are often inconsequential. Therefore the CoroutineScheduler is recommended mostly on 32-bit processors.

<a name="HelloManualCoroutine"></a>

HelloManualCoroutine

The HelloManualCoroutine.ino program shows what the code looks like without the convenience of the COROUTINE() macro. For more complex programs, with more than a few coroutines, especially if the coroutines need to communicate with each other, this coding structure can be more powerful.

#include <Arduino.h>
#include <AceRoutine.h>
using namespace ace_routine;

const int LED = LED_BUILTIN;
const int LED_ON = HIGH;
const int LED_OFF = LOW;

class BlinkLedCoroutine: public Coroutine {
  public:
    int runCoroutine() override {
      COROUTINE_LOOP() {
        digitalWrite(LED, LED_ON);
        COROUTINE_DELAY(100);
        digitalWrite(LED, LED_OFF);
        COROUTINE_DELAY(500);
      }
    }
};

class PrintHelloWorldCoroutine: public Coroutine {
  public:
    int runCoroutine() override {
      COROUTINE_LOOP() {
        Serial.print(F("Hello, "));
        Serial.flush();
        COROUTINE_DELAY(1000);
        Serial.println(F("World"));
        COROUTINE_DELAY(4000);
      }
    }
};

BlinkLedCoroutine blinkLed;
PrintHelloWorldCoroutine printHelloWorld;

void setup() {
  delay(1000);
  Serial.begin(115200);
  while (!Serial); // Leonardo/Micro
  pinMode(LED, OUTPUT);
}

void loop() {
  blinkLed.runCoroutine();
  printHelloWorld.runCoroutine();
}

HelloCoroutineWithProfiler

Version 1.5 added support for profiling the execution time of Coroutine::runCoroutine() through the CoroutineProfiler interface. Currently only a single implementation (LogBinProfiler) is provided.

The HelloCoroutineWithProfiler.ino program shows how to setup the profilers and extract the profiling information using the Coroutine::runCoroutineWithProfiler() instead of the usual Coroutine::runCoroutine():

#include <AceRoutine.h>
using namespace ace_routine;

const int PIN = 2;
const int LED = LED_BUILTIN;
const int LED_ON = HIGH;
const int LED_OFF = LOW;

COROUTINE(blinkLed) {
  COROUTINE_LOOP() {
    digitalWrite(LED, LED_ON);
    COROUTINE_DELAY(100);
    digitalWrite(LED, LED_OFF);
    COROUTINE_DELAY(500);
  }
}

COROUTINE(printHelloWorld) {
  COROUTINE_LOOP() {
    Serial.print(F("Hello, "));
    Serial.flush();
    COROUTINE_DELAY(1000);
    Serial.println(F("World"));
    COROUTINE_DELAY(4000);
  }
}

COROUTINE(printProfiling) {
  COROUTINE_LOOP() {
    LogBinTableRenderer::printTo(
        Serial, 3 /*startBin*/, 14 /*endBin*/, false /*clear*/);
    LogBinJsonRenderer::printTo(
        Serial, 3 /*startBin*/, 14 /*endBin*/);

    COROUTINE_DELAY(5000);
  }
}

LogBinProfiler profiler1;
LogBinProfiler profiler2;
LogBinProfiler profiler3;

void setup() {
  delay(1000);
  Serial.begin(115200);
  while (!Serial); // Leonardo/Micro

  pinMode(LED, OUTPUT);
  pinMode(PIN, INPUT);

  // Coroutine names can be either C-string or F-string.
  blinkLed.setName("blinkLed");
  readPin.setName(F("readPin"));

  // Manually attach the profilers to the coroutines.
  blinkLed.setProfiler(&profiler1);
  readPin.setProfiler(&profiler2);
  printProfiling.setProfiler(&profiler3);
}

void loop() {
  blinkLed.runCoroutineWithProfiling();
  printHelloWorld.runCoroutineWithProfiling();
  printProfiling.runCoroutineWithProfiler();
}

Every 5 seconds, the printProfiling coroutine will print the profiling information in 2 formats on the Serial port:

name         <16us <32us <64us<128us<256us<512us  <1ms  <2ms  <4ms  <8ms    >>
0x1DB        16921 52650     0     0     0     0     0     0     0     0     1
readPin      65535  1189     0     0     0     0     0     0     0     0     0
blinkLed     65535   830     0     0     0     0     0     0     0     0     0
{
"0x1DB":[16921,52650,0,0,0,0,0,0,0,0,1],
"readPin":[65535,1189,0,0,0,0,0,0,0,0,0],
"blinkLed":[65535,830,0,0,0,0,0,0,0,0,0]
}

<a name="HelloSchedulerWithProfiler"></a>

HelloSchedulerWithProfiler

The HelloSchedulerWithProfiler.ino sketch implements the same thing as HelloCoroutineWithProfiler using 2 techniques to handle more than a handful of coroutines:

#include <AceRoutine.h>
using namespace ace_routine;

const int PIN = 2;
const int LED = LED_BUILTIN;
const int LED_ON = HIGH;
const int LED_OFF = LOW;

COROUTINE(blinkLed) {
  COROUTINE_LOOP() {
    digitalWrite(LED, LED_ON);
    COROUTINE_DELAY(100);
    digitalWrite(LED, LED_OFF);
    COROUTINE_DELAY(500);
  }
}

COROUTINE(printHelloWorld) {
  COROUTINE_LOOP() {
    Serial.print(F("Hello, "));
    Serial.flush();
    COROUTINE_DELAY(1000);
    Serial.println(F("World"));
    COROUTINE_DELAY(4000);
  }
}

COROUTINE(printProfiling) {
  COROUTINE_LOOP() {
    LogBinTableRenderer::printTo(
        Serial, 3 /*startBin*/, 14 /*endBin*/, false /*clear*/);
    LogBinJsonRenderer::printTo(
        Serial, 3 /*startBin*/, 14 /*endBin*/);

    COROUTINE_DELAY(5000);
  }
}

void setup() {
  delay(1000);
  Serial.begin(115200);
  while (!Serial); // Leonardo/Micro

  pinMode(LED, OUTPUT);
  pinMode(PIN, INPUT);

  // Coroutine names can be either C-string or F-string.
  blinkLed.setName("blinkLed");
  readPin.setName(F("readPin"));

  // Create profilers on the heap and attach them to all coroutines.
  LogBinProfiler::createProfilers();

  CoroutineScheduler::setup();
}

void loop() {
  CoroutineScheduler::loopWithProfiler();
}

The printProfiling coroutine will print the same information as before every 5 seconds:

name         <16us <32us <64us<128us<256us<512us  <1ms  <2ms  <4ms  <8ms    >>
0x1DB        16921 52650     0     0     0     0     0     0     0     0     1
readPin      65535  1189     0     0     0     0     0     0     0     0     0
blinkLed     65535   830     0     0     0     0     0     0     0     0     0
{
"0x1DB":[16921,52650,0,0,0,0,0,0,0,0,1],
"readPin":[65535,1189,0,0,0,0,0,0,0,0,0],
"blinkLed":[65535,830,0,0,0,0,0,0,0,0,0]
}

<a name="Installation"></a>

Installation

The latest stable release is available in the Arduino IDE Library Manager. Two libraries need to be installed as of v1.5.0:

The development version can be installed by cloning the following git repos:

You can copy these directories to the ./libraries directory used by the Arduino IDE. (You should see 2 directories, named ./libraries/AceRoutine and ./libraries/AceCommon). Or you can create symlinks from /.libraries to these directories.

The develop branch contains the latest working version. The master branch contains the stable release.

<a name="SourceCode"></a>

Source Code

The source files are organized as follows:

<a name="Documentation"></a>

Documentation

<a name="Examples"></a>

Examples

The following programs are provided under the examples directory:

<a name="Comparisons"></a>

Comparisons to Other Multitasking Libraries

There are several interesting and useful multithreading libraries for Arduino. I'll divide the libraries in to 2 camps:

Task Managers

Task managers run a set of tasks. They do not provide a way to resume execution after yield() or delay().

Threads or Coroutines

In order of increasing complexity, here are some libraries that provide broader abstraction of threads or coroutines:

Comparing AceRoutine to Other Libraries

The AceRoutine library falls in the "Threads or Coroutines" camp. The inspiration for this library came from ProtoThreads and Coroutines in C where an incredibly brilliant and ugly technique called Duff's Device is used to perform labeled goto statements inside the "coroutines" to resume execution from the point of the last yield() or delay(). It occurred to me that I could make the code a lot cleaner and easier to use in a number of ways:

I looked around to see if there already was a library that implemented these ideas and I couldn't find one. However, after writing most of this library, I discovered that my implementation was very close to the <ProtoThread.h> module in the Cosa framework. It was eerie to see how similar the 2 implementations had turned out at the lower level. I think the AceRoutine library has a couple of advantages:

<a name="ResourceConsumption"></a>

Resource Consumption

<a name="StaticMemory"></a>

Static Memory

All objects are statically allocated (i.e. not heap or stack).

On 8-bit processors (AVR Nano, Uno, etc):

sizeof(Coroutine): 16
sizeof(CoroutineScheduler): 2
sizeof(Channel<int>): 5
sizeof(LogBinProfiler): 66
sizeof(LogBinTableRenderer): 1
sizeof(LogBinJsonRenderer): 1

On 32-bit processors (e.g. Teensy ARM, ESP8266, ESP32):

sizeof(Coroutine): 28
sizeof(CoroutineScheduler): 4
sizeof(Channel<int>): 12
sizeof(LogBinProfiler): 68
sizeof(LogBinTableRenderer): 1
sizeof(LogBinJsonRenderer): 1

The CoroutineScheduler consumes only 2 bytes (8-bit processors) or 4 bytes (32-bit processors) of static memory no matter how many coroutines are created. That's because it depends on a singly-linked list whose pointers live on the Coroutine object, not in the CoroutineScheduler. But using the CoroutineScheduler::loop() instead of calling Coroutine::runCoroutine() directly increases flash memory usage by 70-100 bytes.

The Channel object requires 2 copies of the parameterized <T> type so its size is equal to 1 + 2 * sizeof(T), rounded to the nearest memory alignment boundary (i.e. a total of 12 bytes for a 32-bit processor).

<a name="FlashMemory"></a>

Flash Memory

The examples/MemoryBenchmark program gathers flash and memory consumption numbers for various boards (AVR, ESP8266, ESP32, etc) for a handful of AceRoutine features. Here are some highlights:

Arduino Nano (8-bits)

+--------------------------------------------------------------------+
| functionality                         |  flash/  ram |       delta |
|---------------------------------------+--------------+-------------|
| Baseline                              |   1616/  186 |     0/    0 |
|---------------------------------------+--------------+-------------|
| One Delay Function                    |   1664/  188 |    48/    2 |
| Two Delay Functions                   |   1726/  190 |   110/    4 |
|---------------------------------------+--------------+-------------|
| One Coroutine (millis)                |   1804/  212 |   188/   26 |
| Two Coroutines (millis)               |   1998/  236 |   382/   50 |
|---------------------------------------+--------------+-------------|
| One Coroutine (micros)                |   1776/  212 |   160/   26 |
| Two Coroutines (micros)               |   1942/  236 |   326/   50 |
|---------------------------------------+--------------+-------------|
| One Coroutine (seconds)               |   1904/  212 |   288/   26 |
| Two Coroutines (seconds)              |   2130/  236 |   514/   50 |
|---------------------------------------+--------------+-------------|
| One Coroutine, Profiler               |   1874/  212 |   258/   26 |
| Two Coroutines, Profiler              |   2132/  236 |   516/   50 |
|---------------------------------------+--------------+-------------|
| Scheduler, One Coroutine (millis)     |   1928/  214 |   312/   28 |
| Scheduler, Two Coroutines (millis)    |   2114/  238 |   498/   52 |
|---------------------------------------+--------------+-------------|
| Scheduler, One Coroutine (micros)     |   1900/  214 |   284/   28 |
| Scheduler, Two Coroutines (micros)    |   2058/  238 |   442/   52 |
|---------------------------------------+--------------+-------------|
| Scheduler, One Coroutine (seconds)    |   2028/  214 |   412/   28 |
| Scheduler, Two Coroutines (seconds)   |   2246/  238 |   630/   52 |
|---------------------------------------+--------------+-------------|
| Scheduler, One Coroutine (setup)      |   1978/  214 |   362/   28 |
| Scheduler, Two Coroutines (setup)     |   2264/  238 |   648/   52 |
|---------------------------------------+--------------+-------------|
| Scheduler, One Coroutine (man setup)  |   1956/  214 |   340/   28 |
| Scheduler, Two Coroutines (man setup) |   2250/  238 |   634/   52 |
|---------------------------------------+--------------+-------------|
| Scheduler, One Coroutine, Profiler    |   1992/  214 |   376/   28 |
| Scheduler, Two Coroutines, Profiler   |   2178/  238 |   562/   52 |
|---------------------------------------+--------------+-------------|
| Scheduler, LogBinProfiler             |   2112/  286 |   496/  100 |
| Scheduler, LogBinTableRenderer        |   3514/  304 |  1898/  118 |
| Scheduler, LogBinJsonRenderer         |   3034/  308 |  1418/  122 |
|---------------------------------------+--------------+-------------|
| Blink Function                        |   1948/  189 |   332/    3 |
| Blink Coroutine                       |   2118/  212 |   502/   26 |
+--------------------------------------------------------------------+

ESP8266 (32-bits)

+--------------------------------------------------------------------+
| functionality                         |  flash/  ram |       delta |
|---------------------------------------+--------------+-------------|
| Baseline                              | 264981/27984 |     0/    0 |
|---------------------------------------+--------------+-------------|
| One Delay Function                    | 265045/27992 |    64/    8 |
| Two Delay Functions                   | 265109/27992 |   128/    8 |
|---------------------------------------+--------------+-------------|
| One Coroutine (millis)                | 265177/28028 |   196/   44 |
| Two Coroutines (millis)               | 265337/28060 |   356/   76 |
|---------------------------------------+--------------+-------------|
| One Coroutine (micros)                | 265209/28028 |   228/   44 |
| Two Coroutines (micros)               | 265369/28060 |   388/   76 |
|---------------------------------------+--------------+-------------|
| One Coroutine (seconds)               | 265209/28028 |   228/   44 |
| Two Coroutines (seconds)              | 265385/28060 |   404/   76 |
|---------------------------------------+--------------+-------------|
| One Coroutine, Profiler               | 265257/28028 |   276/   44 |
| Two Coroutines, Profiler              | 265433/28060 |   452/   76 |
|---------------------------------------+--------------+-------------|
| Scheduler, One Coroutine (millis)     | 265241/28036 |   260/   52 |
| Scheduler, Two Coroutines (millis)    | 265385/28060 |   404/   76 |
|---------------------------------------+--------------+-------------|
| Scheduler, One Coroutine (micros)     | 265257/28036 |   276/   52 |
| Scheduler, Two Coroutines (micros)    | 265401/28060 |   420/   76 |
|---------------------------------------+--------------+-------------|
| Scheduler, One Coroutine (seconds)    | 265257/28036 |   276/   52 |
| Scheduler, Two Coroutines (seconds)   | 265417/28060 |   436/   76 |
|---------------------------------------+--------------+-------------|
| Scheduler, One Coroutine (setup)      | 265273/28036 |   292/   52 |
| Scheduler, Two Coroutines (setup)     | 265433/28060 |   452/   76 |
|---------------------------------------+--------------+-------------|
| Scheduler, One Coroutine (man setup)  | 265257/28036 |   276/   52 |
| Scheduler, Two Coroutines (man setup) | 265433/28060 |   452/   76 |
|---------------------------------------+--------------+-------------|
| Scheduler, One Coroutine, Profiler    | 265321/28036 |   340/   52 |
| Scheduler, Two Coroutines, Profiler   | 265449/28060 |   468/   76 |
|---------------------------------------+--------------+-------------|
| Scheduler, LogBinProfiler             | 265465/28100 |   484/  116 |
| Scheduler, LogBinTableRenderer        | 267381/28100 |  2400/  116 |
| Scheduler, LogBinJsonRenderer         | 266789/28104 |  1808/  120 |
|---------------------------------------+--------------+-------------|
| Blink Function                        | 265669/28064 |   688/   80 |
| Blink Coroutine                       | 265801/28100 |   820/  116 |
+--------------------------------------------------------------------+

Comparing Blink Function and Blink Coroutine is probably the most fair comparison, because they implement the exact same functionality. The code is given in Comparison To NonBlocking Function. The Blink Function implements the asymmetric blink (HIGH and LOW having different durations) functionality using a simple, non-blocking function with an internal prevMillis static variable. The Blink Coroutine implements the same logic using a Coroutine. The Coroutine version is far more readable and maintainable, with only about 220 additional bytes of flash on AVR, and 130 bytes on an ESP8266. In many situations, the increase in flash memory size may be worth paying to get easier code maintenance.

<a name="CPU"></a>

CPU

See examples/AutoBenchmark. Here are 2 samples:

Arduino Nano:

+---------------------------------+--------+-------------+--------+
| Functionality                   |  iters | micros/iter |   diff |
|---------------------------------+--------+-------------+--------|
| EmptyLoop                       |  10000 |       1.700 |  0.000 |
|---------------------------------+--------+-------------+--------|
| DirectScheduling                |  10000 |       2.900 |  1.200 |
| DirectSchedulingWithProfiler    |  10000 |       5.700 |  4.000 |
|---------------------------------+--------+-------------+--------|
| CoroutineScheduling             |  10000 |       7.100 |  5.400 |
| CoroutineSchedulingWithProfiler |  10000 |       9.300 |  7.600 |
+---------------------------------+--------+-------------+--------+

ESP8266:

+---------------------------------+--------+-------------+--------+
| Functionality                   |  iters | micros/iter |   diff |
|---------------------------------+--------+-------------+--------|
| EmptyLoop                       |  10000 |       0.200 |  0.000 |
|---------------------------------+--------+-------------+--------|
| DirectScheduling                |  10000 |       0.500 |  0.300 |
| DirectSchedulingWithProfiler    |  10000 |       0.800 |  0.600 |
|---------------------------------+--------+-------------+--------|
| CoroutineScheduling             |  10000 |       0.900 |  0.700 |
| CoroutineSchedulingWithProfiler |  10000 |       1.100 |  0.900 |
+---------------------------------+--------+-------------+--------+

<a name="SystemRequirements"></a>

System Requirements

<a name="Hardware"></a>

Hardware

Tier 1: Fully Supported

These boards are tested on each release:

Tier 2: Should work

These boards should work, but they are not tested frequently by me, or I don't own the specific hardware so they were tested by a community member:

Tier 3: May work, but not supported

Tier Blacklisted

The following boards are not supported and are explicitly blacklisted to allow the compiler to print useful error messages instead of hundreds of lines of compiler errors:

<a name="ToolChain"></a>

Tool Chain

This library was developed and tested using:

This library is not compatible with:

It should work with PlatformIO but I have not tested it.

The library works on Linux or MacOS (using both g++ and clang++ compilers) using the EpoxyDuino emulation layer.

<a name="OperatingSystem"></a>

Operating System

I use Ubuntu 20.04 for the vast majority of my development. I expect that the library will work fine under MacOS and Windows, but I have not explicitly tested them.

<a name="License"></a>

License

MIT License

<a name="FeedbackAndSupport"></a>

Feedback and Support

If you have any questions, comments, or feature requests for this library, please use the GitHub Discussions for this project. If you have bug reports, please file a ticket in GitHub Issues. Feature requests should go into Discussions first because they often have alternative solutions which are useful to remain visible, instead of disappearing from the default view of the Issue tracker after the ticket is closed.

Please refrain from emailing me directly unless the content is sensitive. The problem with email is that I cannot reference the email conversation when other people ask similar questions later.

<a name="Authors"></a>

Authors

Created by Brian T. Park (brian@xparks.net).