How to Write Multi-Threaded Arduino Programs
2022-05-23 | By Maker.io Staff
Recent articles introduced you to the basic idea of multithreaded programs, possible problems, and how to solve common issues in concurrent applications. However, all those articles discussed the basic concept of parallel programs, but they didn’t utilize any specific development boards. In this article, we’ll look at how you can leverage the power of multi-threading to speed up their Arduino-based projects.
Achieve Real Multi-threading with MBed OS
As discussed previously, multi-threading is mainly a feature provided by a high-level operating system. In addition, a system’s CPU will have to have multiple processing cores to simultaneously execute multiple threads in parallel. However, having only a single processing core doesn’t mean that your application can’t utilize threads. Recall that threads are also a fantastic way to maintain an application’s responsiveness while it waits for a long-running task to finish.
Therefore, you can employ the methods discussed in the previous articles to program multithreaded Arduino applications for devices that run the MBed core, for example, the Arduino Nano 33 BLE and Nano 33 BLE Sense. To get started, install the respective board support files using the Arduino IDE:
Install the appropriate board support files for your MBed OS Arduino device. The list of supported devices also includes the Arduino professional range, for example, the Portenta and Nicla boards.
For these examples, I’m using a single-core Arduino Nano 33 BLE, so my application won’t experience significant speed-up. However, other models might significantly benefit from multi-threaded implementations. Either way, consider the following short example:
#include <mbed.h> #include <rtos.h> #include <platform/Callback.h> using namespace rtos; Semaphore s1(1); Semaphore s2(0); Thread t2; Thread t3; void printPing(void) { while(true) { s1.acquire(); Serial.print("Ping "); s2.release(); } } void printPong(void) { s2.acquire(); Serial.println("Pong"); s1.release(); } void setup() { Serial.begin(9600); t2.start(mbed::callback(printPing)); t3.start(mbed::callback(printPong)); } void loop() {}
Start by including the necessary MBed OS headers, and then use the MBed namespace in your program to make things shorter to write. As you can see, the global variables define two semaphores and two threads. One semaphore starts at zero, while the other starts at one. As discussed in previous articles, you can use this approach to implement an alternating execution order together with mutual exclusion. The goal of this program is to make t2 repeatedly run before t3.
The setup method of this Arduino sketch starts the serial monitor and the two threads. Then, the callback functions define what code each thread should execute. In this case, that’s the two callback methods printPing and printPong.
Note how the method names differ from those used with standard POSIX threads. However, the overall concepts remain, and once you know how to utilize them in one system, you’ll most likely easily be able to apply them to other development environments. Of course, you can always refer to the most recent MBed API documentation to learn more about functions and their parameters.
Pseudo Multi-threading on Non-MBed Arduino Boards
The section above made use of a high-level operating system that manages the threads of a program. However, using various tricks and tips, you can still achieve some multi-threading-like behavior even if your Arduino doesn’t support the MBed core.
Start by avoiding blocking calls when using a board like the Arduino UNO. You can almost always replace blocking calls with their non-blocking counterparts. Doing so will already significantly improve an application's responsiveness to external events and user input.
Next, you should make use of hardware interrupts instead of permanently polling a physical input pin’s state, for example, a GPIO pin attached to a push button. Doing so also reduces the unnecessary load your application puts on the Arduino’s microcontroller, and the whole system can respond faster to external events such as user inputs.
Lastly, you can utilize Protothreads, which are lightweight stackless threads designed for memory-constrained systems such as AVR Arduinos. The library provides an event-driven system with a blocking context on top. Developers can leverage Protothreads to implement a sequential control flow without worrying about complex state machines. However, keep in mind that this library doesn’t provide full multi-threading in the traditional sense. You can install the library using the Arduino IDE’s built-in library manager or by downloading the source code from the official repository.
A Simple Blink Example Using Protothreads
Protothreads are perfect for writing applications that have to perform scheduled actions regularly. While they are not like traditional POSIX threads, Protothreads still have the potential to significantly speed up the development of more complex state-machine-like Arduino programs. For example, the following is a basic Arduino sketch that makes use of Protothreads to blink two LEDs in different intervals:
#include "protothreads.h" #define LED_1 2 #define LED_2 3 pt ptSlowBlink; pt ptFastBlink; int fastBlinkThread(struct pt* pt) { PT_BEGIN(pt); while(true) { digitalWrite(LED_1, HIGH); PT_SLEEP(pt, 250); digitalWrite(LED_1, LOW); PT_SLEEP(pt, 250); } PT_END(pt); } int slowBlinkThread(struct pt* pt) { PT_BEGIN(pt); while(true) { digitalWrite(LED_2, HIGH); PT_SLEEP(pt, 1000); digitalWrite(LED_2, LOW); PT_SLEEP(pt, 1000); } PT_END(pt); } void setup() { PT_INIT(&ptSlowBlink); PT_INIT(&ptFastBlink); pinMode(LED_1, OUTPUT); pinMode(LED_2, OUTPUT); } void loop() { PT_SCHEDULE(slowBlinkThread(&ptSlowBlink)); PT_SCHEDULE(fastBlinkThread(&ptFastBlink)); }
The first few lines of the program import the necessary library, define the LED pins, and then create two protothread objects. You can think of these objects as two individual more light-weight threads. The fastBlinkThread and slowBlinkThread methods contain the code that the two threads will execute seemingly in parallel. Next, the setup method initializes the two threads and the output pins. Finally, the loop schedules the threads to run in every iteration.
While this approach is not precisely like multi-threading, it still enables programmers to write elegant, short source code for many standard tasks. Of course, you could’ve also written this example using nothing but standard Arduino calls.
Conclusion
Multi-threading is an immensely powerful tool when used correctly. You can leverage the benefits of Multi-threading on many systems that offer a high-level operating system, such as MBed OS. Unfortunately, the drawbacks of incorrectly implemented concurrent programs can be significant. However, programmers can utilize a few methods that help manage the problems that multi-threading might introduce into their programs.
Using the Arduino IDE and an Arduino board that runs the MBed OS, you can employ the MBed scheduler to implement multithreaded programs on supported Arduino boards. However, most supported development boards still only contain a single-core CPU, which dramatically limits the speed-up multi-threading can achieve.
Finally, for every other Arduino board that doesn’t support MBed OS, you can use Protothreads to implement pseudo-parallelism in their programs. This library tremendously helps build clean Arduino programs that periodically execute scheduled tasks. The library uses standard C code, and you can achieve the same effects by manually implementing the schedule in their programs. However, doing so could drastically reduce the readability and maintainability of the resulting code in many cases.
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.
Visit TechForum