SkillAgentSearch skills...

AceRoutine

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

Install / Use

/learn @bxparks/AceRoutine
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

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:

  • Coroutine class provides the context variables for all coroutines
  • CoroutineScheduler class handles the scheduling (optional)

The following classes are used for profiling:

  • CoroutineProfiler interface
  • LogBinProfiler provides an implementation that tracks the execution time in 32 logarithmic bins from 1us to 4295s.
  • LogBinTableRenderer prints the histogram as a table
  • LogBinJsonRenderer prints the histogram as a JSON object

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

  • Channel class allows coroutines to send messages to each other

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

  • COROUTINE(): defines an instance of the Coroutine class or an instance of a user-defined subclass of Coroutine
  • COROUTINE_BEGIN(): must occur at the start of a coroutine body
  • COROUTINE_END(): must occur at the end of the coroutine body
  • COROUTINE_YIELD(): yields execution back to the caller, often CoroutineScheduler but not necessarily
  • COROUTINE_AWAIT(condition): yield until condition becomes true
  • COROUTINE_DELAY(millis): yields back execution for millis. The millis parameter is defined as a uint16_t.
  • COROUTINE_DELAY_MICROS(micros): yields back execution for micros. The micros parameter is defined as a uint16_t.
  • COROUTINE_DELAY_SECONDS(seconds): yields back execution for seconds. The seconds parameter is defined as a uint16_t.
  • COROUTINE_LOOP(): convenience macro that loops forever
  • COROUTINE_CHANNEL_WRITE(channel, value): writes a value to a Channel
  • COROUTINE_CHANNEL_READ(channel, value): reads a value from a Channel

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

  • low memory usage
    • 8-bit (e.g. AVR) processors:
      • the first Coroutine consumes about 230 bytes of flash
      • each additional Coroutine consumes 170 bytes of flash
      • each Coroutine consumes 16 bytes of static RAM
      • CoroutineScheduler consumes only about 40 bytes of flash and 2 bytes of RAM independent of the number of coroutines
    • 32-bit (e.g. STM32, ESP8266, ESP32) processors
      • the first Coroutine consumes between 120-450 bytes of flash
      • each additional Coroutine consumes about 130-160 bytes of flash,
      • each Coroutine consumes 28 bytes of static RAM
      • CoroutineScheduler consumes only about 40-60 bytes of flash and 4 bytes of static RAM independent of the number of coroutines
  • extremely fast context switching
    • Direct Scheduling (call Coroutine::runCoroutine() directly)
      • ~1.0 microseconds on a 16 MHz ATmega328P
      • ~0.4 microseconds on a 48 MHz SAMD21
      • ~0.3 microseconds on a 72 MHz STM32
      • ~0.3 microseconds on a 80 MHz ESP8266
      • ~0.1 microseconds on a 240 MHz ESP32
      • ~0.17 microseconds on 96 MHz Teensy 3.2 (depending on compiler settings)
    • Coroutine Scheduling (use CoroutineScheduler::loop()):
      • ~5.2 microseconds on a 16 MHz ATmega328P
      • ~1.3 microseconds on a 48 MHz SAMD21
      • ~0.9 microseconds on a 72 MHz STM32
      • ~0.8 microseconds on a 80 MHz ESP8266
      • ~0.3 microseconds on a 240 MHz ESP32
      • ~0.4 microseconds on 96 MHz Teensy 3.2 (depending on compiler settings)
  • uses the computed goto feature of the GCC compiler (also supported by Clang) to avoid the Duff's Device hack
    • allows switch statements in the coroutines
  • C/C++ macros eliminate boilerplate code and make the code easy to read
  • the base Coroutine class is easy to subclass to add additional variables and functions
  • fully unit tested using AUnit

Some limitations are:

  • A Coroutine cannot return any values.
  • A Coroutine is stackless and therefore cannot preserve local stack variables across multiple calls. Often the class member variables or function static variables are reasonable substitutes.
  • Coroutines are designed to be statically allocated, not dynamically created and destroyed on the heap. Dynamic memory allocation on an 8-bit microcontroller with 2kB of RAM would cause too much heap fragmentation. And the virtual destructor pulls in malloc() and free() which increases flash memory by 600 bytes on AVR processors.
  • A Channel is an experimental feature and has limited features. It is currently an unbuffered, synchronized channel. It can be used by only one reader and one writer.

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):

  • The CoroutineScheduler singleton instance increases the flash memory by about 110 bytes.
  • The CoroutineScheduler::loop() method calls the Coroutine::runCoroutine() method through the virtual dispatch instead of directly, which is slower and takes more flash memory.
  • Each Coroutine instance consumes an additional ~70 bytes of flash when using the CoroutineScheduler.

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 prog

Related Skills

View on GitHub
GitHub Stars189
CategoryDevelopment
Updated11d ago
Forks19

Languages

C++

Security Score

95/100

Audited on Mar 20, 2026

No findings