Skip to main content

Threading: Run Multiple Functions at Once

Threading refers to running multiple code blocks, or threads, at the same time. In the context of the Quarto, it is about running multiple functions in parallel (as opposed to sequentially) and controlling when those functions run. While the Quarto hardware can only execute one instruction at a time, by pausing the execution of one function to run another function, we can give the appearance of running multiple functions at the same time.

To program such behavior, we will utilize the qNimble variant of the protoThreads library.

Writing Threaded Functions

To write a function that can be threaded with protoThreads, we have to make a few changes to the function:

  1. Wrap the function name and arguments around PT_THREAD(...)
  2. Begin the function with PT_FUNC_START(pt)
  3. Add calls to protoThreads functions that enable the main function to pause its execution. These functions include:
    • PT_SLEEP()
    • PT_YIELD()
    • PT_WAIT_UNTIL()
    • PT_WAIT_WHILE()
    • PT_WAIT_THREAD()
  4. Look for any variables whose value is stored across any of the above functions and, if so, add the word static to their declaration
  5. End the function with PT_FUNC_END(pt)

Once we have the threaded function, we can call it with the PT_SCHEDULE() function. Typically this is put in the loop() function to cause the function to run continuously. Finally, we need to include the protoThreads library to the sketch by adding the line:

#include <protothreads.h>

Example 1: Two Blinking Lights

The above instructions will make a lot more sense in the context of an example. For this example, we want to toggle two different LED's at different rates: the red LED at 1 Hz, and the blue red at 1.1Hz. Essentially, we want to run the following two functions independently:

void blinkRed(void) {
while(true) {
toggleLEDRed();
delay(500); //wait 500ms
}
}
void blinkBlue(void) {
while(true) {
toggleLEDBlue();
delay(450); //wait 450ms
}
}

Both these functions will run forever as we want to blink our LEDs forever. However, if you run either function on the Quarto, it would run that function forever and never run the other function. Note, that this functionality could be achieved with interrupts, see the section interrupts vs threads for a discussion of pros and cons of interrupts and threads). To transform the above functions into thread functions, we first follow step #1 and replace

void blinkRed(void) {

with

PT_THREAD(blinkRed(void)) {

For step #2, we add PT_FUNC_START() as the first line in the function. So now we have:

PT_THREAD(blinkRed(void)) {
PT_FUNC_START(pt);

// Loop forever
while(true) {
toggleLEDRed();
delay(500); //wait 500ms
}
}

Step #3 replaces the delay function with PT_SLEEP(), a function that basically does the same thing as delay except that it lets the Quarto run other functions while its waiting for the required delay to occur. This example uses no variables, so step #4 does not apply. Lastly we add PT_FUNC_END(pt) to the end of the function and now we have:

PT_THREAD(blinkRed(void)) {
PT_FUNC_START(pt);
// Loop forever
while(true) {
toggleLEDRed();
PT_SLEEP(pt, 500);
}

PT_FUNC_END(pt);
}

The function doesn't look that different, and it appears as if it too will loop forever and not allow another function to run. However, the trick is that the PT_SLEEP function will actually exit the function if enough time hasn't elapsed. And when the function is run again, the PT_FUNC_START function will jump ahead (over) to where the function previously left off and run PT_SLEEP again. With this new function, we can now run both functions (threads) in the main loop with

void loop() {
// Run the two threads
PT_SCHEDULE(blinkRed());
PT_SCHEDULE(blinkBlue());
}

What's happening is that loop runs blinkRed and when blinkRed hits the PT_SLEEP it will exit and then loop will run blinkBlue. If not enough time has elapsed for PT_SLEEP in blinkBlue, it will return without running toggleBlue and loop will then try blinkRed, which will also return without running toggleRed if not enough time has elapsed. This will continue until the PT_SLEEP in one of the functions has been met, in which case the function execution will proceed, the LED will toggle and then it will hit PT_SLEEP again and the exit as it again needs to wait for enough time to have passed. Both loops can now execute at their own speed and if you watch the Quarto, sometimes the red LED will stay on for (nearly) half a second while the blue is off and sometimes the red will barely blink without the blue LED on. The timing between when the LED's turn on is constantly changing because the blue loop is running a bit faster than the red loop.

Note for Advanced Users

If you are used to c / c++ syntax, the syntax around the PT functions may look a little odd. The reason for this is that protoThreads is written entirely in macros using the preprocessor. When these macros are expanded they produce clean, normal c/c++ code, but before that substitution you have macro functions like the PT_THREAD wrapper that look a little strange.

While the Quarto is only executing one command at a time, by jumping back and forth between the two functions, it can mimic the behavior of independently running multiple functions at the same time.

Click here for the full code
#include <protothreads.h>

PT_THREAD(blinkRed(void)) {
PT_FUNC_START(pt);
while(true) {
PT_SLEEP(pt, 500);
toggleLEDRed();
}
PT_FUNC_END(pt);
}
PT_THREAD(blinkBlue(void)) {
PT_FUNC_START(pt);
while(true) {
PT_SLEEP(pt, 450);
toggleLEDBlue();
}
PT_FUNC_END(pt);
}
// the main loop runs forever and runs the threads as needed
void loop() {
// Run the two threads
PT_SCHEDULE(blinkRed());
PT_SCHEDULE(blinkBlue());
}

The previous example showed two slow or infrequent loops running, but what if we want our main function that is processing data to run quickly but we will still want an LED to blink while its processing the data. In this case, let's have the Quarto blink red every 500ms while the Quarto is processing some data, but then change to blinking green once the processing is done.

For the function that blinks the LED, it will look very similar to the previous example, except we will query if the processData thread has completed to determine the LED color to blink:

PT_THREAD(blinkLED(void)) {
PT_FUNC_START(pt);

while(true) {
PT_SLEEP(pt, 500);
if (PT_SCHEDULE(processData())) {
setLED(1,0,0); //turn on RED LED
} else {
setLED(0,1,0); //turn on Green LED
}
PT_SLEEP(pt, 500);
setLED(0,0,0); //turn off LEDs
}

PT_FUNC_END(pt);
}

The PT_SCHEDULE function returns 0 if the function returned early (from PT_SLEEP or PT_YIELD and the like), and 1 if got to the end, so we can use that to query if the function has completed.

As an example, for the data processing function, we will do the following slow calculation:

processData(void) {
double calc = 1.234;
for(uint i=0; i< 50000000;i++) {
double temp = cos(calc*(calc-1.23*i));
calc = temp*sin((calc-0.23*i)*3.456)+calc*calc/9.8765;
}
}

The first step is to convert the processData function into a protoThread function. Most of what we did previously with the blinkRed example will apply here. In blinkRed, the function PT_SLEEP did two things: it both paused the execution of the thread and it let the function pause to go execute other threads. Here, we need only that latter functionality, and we can accomplish it with the PT_YIELD function. That function yields control over execution once so other threads can run and then resumes from where it left off. With those changes we have:

PT_THREAD(processData(void)) {
PT_FUNC_START(pt);
double calc = 1.234;
for(uint i=0; i< 5000000;i++) {
double temp = cos(calc*(calc-1.23*i));
calc = temp*sin((calc-0.23*i)*3.456)+calc*calc/9.8765;
PT_YIELD(pt); // pause here to check if other threads need to be run
}

Serial.printf("Result of calculation is %f\n",calc);
PT_FUNC_END(pt);
}

If you compile this function, you will get warnings about variables being used without be initialized. The reason for this is that if you run the function the first time, it will start at PT_FUNC_START() on line 2 and execute up to PT_YIELD on line 7. But then PT_YIELD will cause the function to exit. When the thread is run again, PT_FUNC_START will cause it to jump ahead to where it was previously, the PT_YIELD on line 7. This means that the code is jumping over the initialization of both the double calc variable (line 3) and the uint i variable (line 4). If you are paying attention to compiler warnings (and you should be), you'll notice its telling you about this problem:

ProcessData.ino:3:10: warning: 'calc' may be used uninitialized in this function [-Wmaybe-uninitialized]
30 | double calc = 1.234;
| ^~~~

The solution to this in step #4 from the Writing Threaded Functions section. By declaring these variables as static, they retain their value across multiple calls of the threaded function and do not need to get initialized every time the function runs. Note that the variable temp does not need to be static because its value always gets set before being used. This is because if PT_FUNC_START jumps to the PT_YIELD location, it skips the temp initialization, but it then goes to the top of the for loop (where it reads the variable i) and then it initializes temp. If we put the PT_YIELD between lines 5 and 6, then we would need to declare temp as a static variable as it could be accessed after PT_YIELD and before getting initialized. With these changes, the function is now:

PT_THREAD(processData(void)) {
PT_FUNC_START(pt);
static double calc = 1.234;
for(static uint i=0; i< 5000000;i++) {
double temp = cos(calc*(calc-1.23*i));
calc = temp*sin((calc-0.23*i)*3.456)+calc*calc/9.8765;
PT_YIELD(pt); // pause here to check if other threads need to be run
}

Serial.printf("Result of calculation is %f\n",calc);
PT_FUNC_END(pt);
}
Click here for the full code
#include <protothreads.h>

PT_THREAD(blinkLED(void)) {
PT_FUNC_START(pt);
while(true) {
PT_SLEEP(pt, 500);
if (PT_SCHEDULE(processData())) {
setLED(1,0,0); //turn on RED LED
} else {
setLED(0,1,0); //turn on Green LED
}
PT_SLEEP(pt, 500);
setLED(0,0,0); //turn off LEDs
}
PT_FUNC_END(pt);
}

PT_THREAD(processData(void)) {
PT_FUNC_START(pt);
static double calc = 1.234;
for(static uint i=0; i< 5000000;i++) {
double temp = cos(calc*(calc-1.23*i));
calc = temp*sin((calc-0.23*i)*3.456)+calc*calc/9.8765;
PT_YIELD(pt); // pause here to check if other threads need to be run
}

Serial.printf("Result of calculation is %f\n",calc);
PT_FUNC_END(pt);
}

void loop() {
// Run the two threads
PT_SCHEDULE(processData());
PT_SCHEDULE(blinkLED());
}

Example 3: Restarting a Completed Thread

In example #2, once the processData thread completes, it never runs again. If, however, we wanted to re-run a thread that has completed, we can do that with the function PT_RESTART. However, to do that PT_RESTART needs the (pointer to the) pt object that stores the thread's internal state and in the above examples that object is not exposed outside of the thread itself. So we'll have to change the structure a bit to create the pt object outside the function so we have access to it in other functions. First we create that pt object and a pointer to it as globals by instantiating them outside of any function with:

pt ptProcessData = {0};
pt* ptProcess = &ptProcessData;

Then when we create the processThread function, we will start it with PT_FUNC_START_EXT(ptProcess) instead of the PT_FUNC_START(pt). Other PT functions like PT_FUNC_END and PT_SLEEP, etc will take ptProcess, the pointer to the global object as the argument instead of pt, which typically is created (locally) by the PT_FUNC_START function. So now we have

PT_THREAD(processData(void)) {
PT_FUNC_START_EXT(ptProcess);

static double calc = 0.1234;
static uint i=0;

for(i=0; i< 2500000;i++) {
double temp = cos(calc*(calc-1.23*i));
calc = temp*sin((calc-0.23*i)*3.456)+calc*calc/9.8765;
PT_YIELD(ptProcess); *// pause here to check if other threads need to be run*
}
PT_FUNC_END(ptProcess);
}

Finally, we will create a new thread which waits for the processData thread to finish, then waits 10 seconds and then restarts the processData thread. Here's how that function is written:

PT_THREAD(reProcess(void)) {
PT_FUNC_START(pt);
while(true) {
PT_WAIT_THREAD(pt,processData()); //wait until processData thread completes
PT_SLEEP(pt,10000); //wait 10s after processData is done
PT_RESTART(ptProcess); //Restart the finished thread
}
PT_FUNC_END(pt);
}

Putting that all together, the Quarto will blink red while it is processing data, then it will wait for 10 seconds while blinking green and then it will start processing data again and blink red, and repeat that pattern indefintely.

Click here for the full code
#include <protothreads.h>

pt ptProcessData = {0};
pt* ptProcess = &ptProcessData;

PT_THREAD(blinkLED(void)) {
PT_FUNC_START(pt);
while(true) {
PT_SLEEP(pt, 500);
if (PT_SCHEDULE(processData())) {
setLED(1,0,0); //turn on RED LED
} else {
setLED(0,1,0); //turn on Green LED
}
PT_SLEEP(pt, 500);
setLED(0,0,0); //turn off LEDs
}
PT_FUNC_END(pt);
}

PT_THREAD(reProcess(void)) {
PT_FUNC_START(pt);

while(true) {
PT_SPAWN(pt,ptProcess,processData()); // go process data
PT_SLEEP(pt,10000); //wait 10s after processData is done
}
PT_FUNC_END(pt);
}

PT_THREAD(processData(void)) {
PT_FUNC_START_EXT(ptProcess);

static double calc = 0.1234;
static uint i=0;

for(i=0; i< 2500000;i++) {
double temp = cos(calc*(calc-1.23*i));
calc = temp*sin((calc-0.23*i)*3.456)+calc*calc/9.8765;
PT_YIELD(ptProcess); // pause here to check if other threads need to be run
}

PT_FUNC_END(ptProcess);
}

void loop() {
PT_SCHEDULE(reProcess());
PT_SCHEDULE(blinkLED());
}

Example 4: Threads Calling Threads

Finally, we can have threads call (or spawn) other threads. In this example, we will reproduce the behavior of example #3 but instead of having the main loop run three threads (blinkLED, processData, reProcess), we need only two: blinkLED and reProcess and that second thread will spawn the other thread, processData. So instead restarting the processData thread that runs in loop, we simply spawn it as needed from another thread. The command to run a new thread is

PT_SPAWN(pt,thread_pt,thread(args));

where pt is the (pointer to the) pt object of the current thread. thread_pt is the (pointer to the) pt object for the thread we are going to run, so as in example #3, that object will need to be defined externally. Finally, the last argument is the threaded function itself, including any arguments we are passing to it.

Now we can slightly alter the reProcess thread to run PT_SPAWN and not PT_WAIT_THREAD or PT_RESTART

PT_THREAD(reProcess(void)) {
PT_FUNC_START(pt);
while(true) {
//PT_WAIT_THREAD(pt,processData()); //replaced with PT_SPAWN
PT_SPAWN(pt,ptProcess,processData());
PT_SLEEP(pt,10000); //wait 10s after processData is done
//PT_RESTART(ptProcess); //also repalced by PT_SPAWN
}
PT_FUNC_END(pt);
}

Finally, we can remove processData from the main loop as it now gets called on demand, instead of being restarted.

Click here for the full code
#include <protothreads.h>

pt ptProcessData = {0};
pt* ptProcess = &ptProcessData;

PT_THREAD(blinkLED(void)) {
PT_FUNC_START(pt);
while(true) {
PT_SLEEP(pt, 500);
if (PT_SCHEDULE(processData())) {
setLED(1,0,0); //turn on RED LED
} else {
setLED(0,1,0); //turn on Green LED
}
PT_SLEEP(pt, 500);
setLED(0,0,0); //turn off LEDs
}
PT_FUNC_END(pt);
}

PT_THREAD(reProcess(void)) {
PT_FUNC_START(pt);

while(true) {
PT_WAIT_THREAD(pt,processData()); //wait until processData thread completes
PT_SLEEP(pt,10000); //wait 10s after processData is done
PT_RESTART(ptProcess); //Restart the finished thread
}
PT_FUNC_END(pt);
}

PT_THREAD(processData(void)) {
PT_FUNC_START_EXT(ptProcess);

static double calc = 0.1234;
static uint i=0;

for(i=0; i< 2500000;i++) {
double temp = cos(calc*(calc-1.23*i));
calc = temp*sin((calc-0.23*i)*3.456)+calc*calc/9.8765;
PT_YIELD(ptProcess); // pause here to check if other threads need to be run
}

PT_FUNC_END(ptProcess);
}

void loop() {
// Run the two threads
PT_SCHEDULE(reProcess());
PT_SCHEDULE(blinkLED());
}

protoThreads vs Interrupts

You may have noticed that all of these examples could have been accomplished by having the LED blinking controlled by a function that runs via a Internal Timer interrupt (see the Arbitrary Output Waveform for an example). A simple couple lines of code could have a LED-toggling function run every 500ms:

IntervalTimer LED_Toggle;
void setup() {
LED_Toggle.begin(runFunction,500000);
}

More generally, interrupts can be used instead of threads to allow jumping between different functions. Below we will look at some of the strengths of using interrupts instead of threads.

Interrupt-based timing has some distinct advances:

  • Excellent Timing Precision
  • No processing overhead except when interrupt fires
  • No need for static (global) variables that are contained within function.
  • No software modifications needed to pause and restart execution of main loop

Because the interrupt interrupts the processor, it can do so, for example, exactly every 500ms instead of the first time after 500ms that the processor happens to check if the thread needs to run. With protoThreads, this timing and delay jitter can be small (<1µs) or arbitrarily large depending on how often your program gets to a PT_YIELD or PT_SLEEP function and how many threads you have running. Additionally, with protoThreads there is some processing overhead with constantly checking if other threads need to run. The lower the timing delay and jitter, the more often we are checking on the threads, the higher the overhead and visa-versa. That said, checking on the status of threads typically takes about 200ns, so even processes that run for up to no more than 10µs (thus 10µs of timing jitter and delay if the other threads' delay is negligible ) run at 98% full processing power. By 1ms, that goes to 99.98%.

Also with interrupts, the Quarto keeps the memory state of the function that gets interrupted, so there is no need to declare local variables as static like there is with protoThreads. In certain cases this can save significant memory. In fact no changes to the main loop need to be made to support an interrupt function unless that function needs to interact with the main code.

protoThread-based timing has some distinct advantages:

  • Unlimited Number of protoThreads
  • Can interrupt other interrupts
  • More flexible prioritization, can runs multiple loops as fast as possible

One limitation is that the Quarto only supports 4 IntervalTimers running at the same time, while the number of protoThreads is not limited. Additionally, all IntervalTimer interrupts run at the same priority, and they cannot interrupt each other. This means that if one interrupt is slow, it will block and delay the running of another interrupt, which can undermine the precise timing possible with InternalTimer interrupts. Additionally, with interval interrupts the only way to prioritize one interrupt over the other is to disable the one you don't want until the other on finishes. This can be done, but can get complicated while protoThreads has a simple PT_WAIT_THREAD() function for one thread to wait until the other completes.

Also with protoThreads, by controlling where you place PT_YIELD and the like, you control when a function gets pre-empted, which can be tricky with interrupts. If you have two threads interacting with the same data, this can be very helpful because you can guarantee that, say, a consumer of the data never runs while a different thread is in the middle of preparing that data.

Perhaps most importantly, you can have two or more threads running without delay with protoThreads, and yielding to other threads when they don't have work to. Reproducing that behavior with interrupts would be very challenging.

Final Thoughts

Hopefully these examples show how flexible and powerful protoThreads can be. You can have lots of different threads, some waiting on timers, others waiting on threads to complete, and others running as fast as they can. In general, IntervalTimers are great for very fast functions (toggle an LED, set a flag, etc) that need precise timing. But for functions with greater complexity, especially if these functions interact with each other, protoThreads can be a great way to move beyond running every function sequentially.