The PQRST library: Priority Queue for Running Simple Tasks

This is a library, initially targeted at the Arduino, to provide simple ‘thread’ support (very much in scare-quotes). This is version 1.2.1, 2022 December 27 (rev 3828920d0249).

The software is Copyright 2017–19, Norman Gray, and is available under the terms of the 2-clause BSD licence. It is therefore perfectly happy to disappear into your source tree.

Links:

The code is available at the project home page, which includes a link to the repository and to downloads. The code is now reasonably stable, and is used in production. See also the documentation and release notes.

Principles

This module implements a Priority Queue for Running Simple Tasks. It is intended to support a simple but very flexible threading framework for Arduino tasks.

When code uses this module, it should declare one or more subclasses of the Task class, and in each of those declare and implement at least the run method (overriding Task::run. These can then be queued using the Task::start method. There is a pre-existing subclass called LoopTask, which is a task which automatically re-queues itself at a specified cadence.

The main loop of the program should then call TaskQueue::run_ready at a high cadence. This method runs any task which has become due. The library's general expectation is that the program main loop consists of little more than

while (1) { Queue.run_ready(millis()); }

but it is reasonable for this queue to instead service low-priority tasks, for example, or even to act as a general to-do queue.

Although the module is nominally targeted at the Arduino, it has only a rather loose dependence on the Arduino framework (it uses the Serial object for some non-essential debugging and the PROGMEM type for a version string in Flash). It could therefore be ported to a different framework without much difficulty. In particular, the library does not depend on the Arduino millis() function, and the run_ready() method can be called with any monotonically increasing 32-bit ‘time’ value.

The code behaves correctly when the ‘time’ value wraps around, as the Arduino millis() function does after 2^32 milliseconds, or 49.7 days, and as the micros() function does after 71.5 minutes.

Notes:

Example

For example, here is the Arduino ‘blink’ program, implemented with a task loop:


#include "pqrst.h"

class BlinkTask : public LoopTask {
private:
    int my_pin_;
    bool light_on_p_;
public:
    BlinkTask(int pin, ms_t cadence);
    void run(ms_t) override;
};

BlinkTask::BlinkTask(int pin, ms_t cadence)
    : LoopTask(cadence),
      my_pin_(pin),
      light_on_p_(false)
{
    // empty
}
void BlinkTask::run(ms_t t)
{
    // toggle the LED state every time we are called
    light_on_p_ = !light_on_p_;
    digitalWrite(my_pin_, light_on_p_);
}

// flash the built-in LED at a 500ms cadence
BlinkTask flasher(LED_BUILTIN, 500);

void setup()
{
    pinMode(LED_BUILTIN, OUTPUT);
    flasher.start(2000);  // start after 2000ms (=2s)
}

void loop()
{
    Queue.run_ready(millis());
}

The code has compile-time options for

Use and documentation

To use, drop the files pqrst.cpp and pqrst.h into your source tree.

To run the unit tests,

% make check

The main documentation, currently, is the class and function documentation, generated by Doxygen.

Norman Gray
2022 December 27