Microsoft's Azure RTOS ThreadX is available to use! We want to show you the basics of ThreadX so you can start using this industrial-grade RTOS in your Arduino projects.
Estimated Time: Setup: 5 min; Part 1: 5 min; Part 2: 30 min; Part 3: 15 min
Estimated Cost: $ of device with ATSAMD21 or ATSAMD51 chip
IMPORTANT: Please view the full license disclosure. ThreadX is available to use only for non-commercial and evaluation purposes, with the exception of full use for the licensed hardware.
IntroductionThis tutorial will show you how to use multi-threading with Azure RTOS ThreadX for Arduino. You will start with the classic Blink example and convert it to a ThreadX application.
Azure RTOS: A Microsoft development suite for embedded IoT applications on microcontrollers (MCUs). Azure RTOS does not require Azure to run.
Azure RTOS ThreadX: One component of the Azure RTOS product offering. ThreadX is the real time operating system (RTOS) designed to run on MCUs.
Azure RTOS ThreadX for Arduino: A port of Azure RTOS ThreadX to Arduino as a library. Please visit AzureRTOS-ThreadX-For-Arduino on GitHub for the source code.
What is coveredBy the end of this tutorial, you should understand the following:
Terms: kernel, thread, thread control block, priority level, preemption, preemption threshold
Actions: How to implement a single thread using ThreadX; How to implement multiple threads using ThreadX
Final code: View the full ThreadX multi-threaded Blink code example on GitHub.
Prerequisites- Have the Arduino IDE 1.8.x installed.
- Have a device using an ATSAMD21 or ATSAMD51 chip. View this list of verified boards.
The following was run on Windows 11, Arduino IDE 1.8.19, the Arduino MKR WiFi 1010, and the Seeed Studio Wio Terminal.
SetupEstimated Time: 5 min
Step 1. Open the Arduino IDE.
Step 2. Install the Azure RTOS Arduino library.
- Navigate to Tools > Manage Libraries...
- Search for 'Azure RTOS'.
- Install 'Azure RTOS ThreadX'. Be sure to install the latest version.
Step 3. Install the board package for your device. (This example uses the Arduino MKR WiFi 1010.)
- Navigate to Tools > Board:... > Boards Manager...
- Search for 'MKR WiFi 1010'.
- Install 'Arduino SAMD Boards (32-bits ARM Cortex-M0+)'. Be sure to install the latest version.
In this section we will run the traditional Blink example to confirm the device is setup properly.
Estimated Time: 5 min
Step 1. Open the Blink example.
- Navigate to File > Examples > 01.Basics.
- Select 'Blink'.
Step 2. Connect your device.
- Plug in your device to your PC.
- Navigate to Tools > Board:... > Arduino SAMD Boards (32-bits ARM Cortex-M0+)
- Select 'Arduino MKR WiFi 1010'.
- Navigate to Tools > Port.
- Select '<Port associated with device>'.
Step 3. Run the example.
- In the top left corner, select the 'Upload' icon. Verification will automatically occur first.
- Observe the LED blink on and off every 1 second.
Code
// the setup function runs once when you press reset or power the board
void setup() {
// initialize digital pin LED_BUILTIN as an output.
pinMode(LED_BUILTIN, OUTPUT);
}
// the loop function runs over and over again forever
void loop() {
digitalWrite(LED_BUILTIN, HIGH); // turn the LED on (HIGH is the voltage level)
delay(1000); // wait for a second
digitalWrite(LED_BUILTIN, LOW); // turn the LED off by making the voltage LOW
delay(1000); // wait for a second
}
What is going on?
Arduino makes use of two core functions: setup()
and loop()
. Once setup()
completes, loop()
is internally kicked off and runs the remainder of the program. Because there is no RTOS present, this code can be considered bare metal programming.
See full Arduino Blink example for more information.
Part 2: Convert the Blink example via ThreadXIn this section we will convert the bare metal Blink example to a single-threaded RTOS version using ThreadX.
Estimated Time: 30 min
Step 1. Save the example.
- Navigate to File > Save As.
- Save the sketch as 'Blink_ThreadX'.
Step 2. (1) Add the Azure RTOS ThreadX library header file tx_api.h
near the top of the file. Place it after the commentary, but before the setup()
function.
/* (1) Add the Azure RTOS ThreadX library header file. */
#include <tx_api.h>
What is going on?
tx_api.h
is the only header file you need to include to use ThreadX for Arduino. tx
is short for ThreadX. All functions in the API will begin with tx
. All constants and data types will begin with TX
.
Step 3. (2) Add the kernel entry function tx_kernel_enter()
to setup()
.
// the setup function runs once when you press reset or power the board
void setup() {
// initialize digital pin LED_BUILTIN as an output.
pinMode(LED_BUILTIN, OUTPUT);
/* (2) Add the kernel entry function. */
tx_kernel_enter();
}
What is going on?
The kernel is the core component of an RTOS. Think of it as the lead coordinator or director of logistics for a project. By "entering" the kernel, the RTOS kernel can start running and managing your embedded application.
The program will never return from tx_kernel_enter()
. As a result, the application will not return from setup()
and loop()
will not be called.
IMPORTANT: "The call to tx_kernel_enter() does not return, so do not place any processing after it."
Please see Microsoft Learn's ThreadX Chapter 3: Functional Components of ThreadX for more information on tx_kernel_enter()
.
Step 4. (3) Add the thread stack memory and the thread control block. Place this near the top of the file after #include <tx_api.h>
and before setup()
.
/* (3) Add the thread stack memory and thread control block. */
#define THREAD_STACK_SIZE 512
TX_THREAD thread_0;
UCHAR thread_0_stack[THREAD_STACK_SIZE];
What is going on?
A thread is a specific execution path within a process (i.e., a running application). A thread shares memory space with other threads but has its own allocated stack space. We define this stack size to be THREAD_STACK_SIZE
bytes and use the array thread_0_stack
to allocate the memory. Please see Microsoft Learn's ThreadX Chapter 3: Functional Components of ThreadX for more information on the thread stack area.
A thread control block contains specific data for the thread. TX_THREAD
is the ThreadX data type for a thread control block. Please see Microsoft Learn's ThreadX Chapter 3: Functional Components of ThreadX for more information on TX_THREAD
.
IMPORTANT: "ThreadX does not use the term task. Instead, the more descriptive and contemporary name thread is used." Please see Microsoft Learn's ThreadX Chapter 1: Introduction to ThreadX for more information on tasks vs. threads.
Step 5. (4) Define the thread's entry function thread_0_entry()
. Place the function definition after the thread_0_stack
array and before setup()
.
/* (4) Define the thread's entry function. */
void thread_0_entry(ULONG thread_input)
{
(VOID)thread_input;
while(1)
{
/* Add thread logic to execute here. */
}
}
What is going on?
The thread's entry function is called by the kernel and contains the thread execution logic. Typically, this function will contain an infinite loop (i.e., while(1)
) that will execute throughout the running program. The name of this function is determined by the user.
Step 6. (5) Move the LED blink logic from loop()
into the thread's entry function. Replace the delay(1000)
with tx_thread_sleep(TX_TIMER_TICKS_PER_SECOND)
.
void thread_0_entry(ULONG thread_input)
{
(VOID)thread_input;
while(1)
{
/* (5) Move the LED blink logic into the thread's entry function. */
digitalWrite(LED_BUILTIN, HIGH); // turn the LED on
tx_thread_sleep(TX_TIMER_TICKS_PER_SECOND); // wait for a second
digitalWrite(LED_BUILTIN, LOW); // turn the LED off
tx_thread_sleep(TX_TIMER_TICKS_PER_SECOND); // wait for a second
}
}
// the loop function runs over and over again forever
void loop() {
/* (5) Move the LED blink logic into the thread's entry function. */
/* This will never be called. */
}
What is going on?
Because loop()
will no longer be called, the blink logic must be moved into the new thread. The delay()
function has limitations and since we will later want to suspend the thread to allow other threads to execute, we will use ThreadX's tx_thread_sleep()
function instead. This function takes timer ticks as its parameter instead of milliseconds.
Step 7. (6) Add the application's environment setup function tx_application_define()
. Place this function after thread_0_entry()
and before setup()
.
/* (6) Add the application's environment setup function. */
void tx_application_define(void *first_unused_memory)
{
(VOID)first_unused_memory;
/* Put system definition stuff in here, e.g. thread creates and other assorted
create information. */
}
What is going on?
The kernel entry function tx_kernel_enter()
will call the function tx_application_define()
to setup the application environment and system resources. It is the user's responsibility to implement this function with the logic to create system resources for the RTOS environment.
Please see Microsoft Learn's ThreadX Chapter 3: Functional Components of ThreadX for more information on tx_application_define()
.
Step 8. (7) Create the thread with tx_thread_create()
. Add this function call to tx_application_define()
.
void tx_application_define(void *first_unused_memory)
{
(VOID)first_unused_memory;
/* Put system definition stuff in here, e.g. thread creates and other assorted
create information. */
/* (7) Create the thread. */
tx_thread_create(&thread_0, "thread 0", thread_0_entry, 0,
thread_0_stack, THREAD_STACK_SIZE,
1, 1, TX_NO_TIME_SLICE, TX_AUTO_START);
}
What is going on?
tx_thread_create()
creates a thread with specified arguments. The arguments used in this example reflect the following:
&thread_0
: Pointer to the defined thread control block. (See step 4.)"thread_0"
: The thread name (i.e., pointer to the name).thread_0_entry
: The user-defined thread entry function. (See step 5.)0
: Entry input to the thread. We are not utilizing this argument.thread_0_stack
: Pointer to the start of the thread's stack. (See step 4.)THREAD_STACK_SIZE
: Size of the thread's stack in bytes. (See step 4.)1
: The priority level of the thread.1
: The preemption threshold of the thread.TX_NO_TIME_SLICE
: Time slicing is disabled.TX_AUTO_START
: The thread is automatically started.
The priority level of a thread helps the thread scheduler determine what thread to execute next. Some threads may be more critical to execute and are therefore given a higher priority relative to others. ThreadX has 32 default priority levels from 0 to 31, with 0 being the highest priority and 31 being the lowest.
Preemption refers to an existing thread's execution being stopped so a higher priority can run instead. The scheduler controls this and when the interrupting thread completes, execution returns back to the thread that was suspended.
The preemption threshold is unique to ThreadX. Only priorities higher than this threshold may preempt the thread.
Please see Microsoft Learn's ThreadX Chapter 3: Functional Components of ThreadX for more information on thread execution, thread priorities, thread scheduling, and thread preemption.
Step 9. Run the Blink example using Azure RTOS ThreadX.
Follow Step 2. Connect your device and Step 3. Run the example from Part 1: Run the Arduino Blink example.
Deep DiveCode
/* (1) Add the Azure RTOS ThreadX library header file. */
#include <tx_api.h>
/* (3) Add the thread stack memory and thread control block. */
#define THREAD_STACK_SIZE 512
TX_THREAD thread_0;
UCHAR thread_0_stack[THREAD_STACK_SIZE];
/* (4) Define the thread's entry function. */
void thread_0_entry(ULONG thread_input)
{
(VOID)thread_input;
while(1)
{
/* (5) Move the LED blink logic into the thread's entry function. */
digitalWrite(LED_BUILTIN, HIGH); /* Turn the LED on. */
tx_thread_sleep(TX_TIMER_TICKS_PER_SECOND); /* Wait for a second. */
digitalWrite(LED_BUILTIN, LOW); /* turn the LED off. */
tx_thread_sleep(TX_TIMER_TICKS_PER_SECOND); /* wait for a second. */
}
}
/* (6) Add the application's environment setup function. */
void tx_application_define(void *first_unused_memory)
{
(VOID)first_unused_memory;
/* Put system definition stuff in here, e.g. thread creates and other assorted
create information. */
/* (7) Create the thread. */
tx_thread_create(&thread_0, "thread 0", thread_0_entry, 0,
thread_0_stack, THREAD_STACK_SIZE,
1, 1, TX_NO_TIME_SLICE, TX_AUTO_START);
}
/* The setup function runs once when you press reset or power the board. */
void setup()
{
/* Initialize digital pin LED_BUILTIN as an output. */
pinMode(LED_BUILTIN, OUTPUT);
/* (2) Add the kernel entry function. */
tx_kernel_enter();
}
void loop()
{
/* (5) Move the LED blink logic into the thread's entry function. */
/* This will never be called. */
}
NOTE: Arduino Blink formatting and single-line style commenting//
have been converted to ThreadX formatting and multi-line style/* */
.
What is going on?
The code above demonstrates how to replace the Arduino bare metal single-threaded approach of setup()
and loop()
with Azure RTOS ThreadX.
The prior bare metal code flow was:
setup()
->loop()
-> infinite loop Blink logic.
The ThreadX new code flow is:
setup()
->tx_kernel_enter()
->tx_application_define()
->thread_0_entry()
-> infinite loop Blink logic.
Although this approach still maintains the same single-threaded functionality, it is now setup to add additional threads as needed. Part 3 will demonstrate how to do this.
Part 3: Apply multi-threading to the Blink example via ThreadXIn this section we will use the single-threaded ThreadX Blink code to create a multi-threaded version that also reads serial input.
Estimated Time: 15 min
Step 1. Save the example.
- Navigate to File > Save As.
- Save the sketch as 'Blink_SerialRead_ThreadX'.
Step 2. (8) Add the thread stack memory and the thread control block for one more thread.
/* (3)(8) Add the thread stack memory and thread control block. */
#define THREAD_STACK_SIZE 512
TX_THREAD thread_0;
TX_THREAD thread_1;
UCHAR thread_0_stack[THREAD_STACK_SIZE];
UCHAR thread_1_stack[THREAD_STACK_SIZE];
What is going on?
This action mimics Part 2: Step 4. For each thread you want to add, you will need to allocate its stack memory and declare its thread control block TX_THREAD
.
Step 3. (9) Define the new thread's entry function thread_1_entry()
. Place the function definition after thread_0_entry()
and before tx_application_define()
.
/* (9) Define the thread's entry function. */
void thread_1_entry(ULONG thread_input)
{
(VOID)thread_input;
while(1)
{
/* Add thread logic to execute here. */
}
}
What is going on?
This action mimics Part 2: Step 5. Each thread needs a user-defined entry function to execute the thread logic.
Step 4. (10) Add serial read logic into the thread's entry function.
void thread_1_entry(ULONG thread_input)
{
(VOID)thread_input;
/* (10) Add serial read logic to the thread's entry function. */
Serial.begin(115200);
while(1)
{
if (Serial.available() > 0)
{
char byte_read = Serial.read();
Serial.print(byte_read);
}
}
}
What is going on?
The serial read logic receives bytes from serial input and prints them to the serial monitor. Notice this logic is placed within while(1)
, but the serial initialization Serial.begin()
function is not. Because initialization only needs to occur once, it is placed before while(1)
. Alternatively, it could be placed in setup()
prior to tx_kernel_enter()
.
Step 5. (11) Create the new thread with tx_thread_create()
. Add this function call to tx_application_define()
after the creation of thread_0
.
void tx_application_define(void *first_unused_memory)
{
(VOID)first_unused_memory;
/* Put system definition stuff in here, e.g. thread creates and other assorted
create information. */
/* (7)(11) Create the thread. */
tx_thread_create(&thread_0, "thread 0", thread_0_entry, 0,
thread_0_stack, THREAD_STACK_SIZE,
1, 1, TX_NO_TIME_SLICE, TX_AUTO_START);
tx_thread_create(&thread_1, "thread 1", thread_1_entry, 0,
thread_1_stack, THREAD_STACK_SIZE,
4, 4, TX_NO_TIME_SLICE, TX_AUTO_START);
}
What is going on?
This action mimics Part2: Step 8. The differences are found in the arguments used. Notice how naming has changed to reflect thread_1
:
&thread_1
: Pointer to the defined thread control block."thread_1"
: The thread name (i.e., pointer to the name).thread_1_entry
: The user-defined thread entry function.thread_1_stack
: Pointer to the start of the thread's stack.
Another argument set that has changed is the priority level and preemption threshold:
4
: The priority level of the thread.4
: The preemption threshold of the thread.
Because 4 is a lower priority level than 1, thread_0
will execute first, and only when it suspends (tx_thread_sleep()
) will the scheduler execute the thread next in line (thread_1
). Once thread_0
has completed its suspension, the scheduler will preempt thread_1
and return execution to thread_0
.
The arguments that remain the same are:
0
: Entry input to the thread.THREAD_STACK_SIZE
: Size of the thread's stack in bytes.TX_NO_TIME_SLICE
: Time slicing is disabled.TX_AUTO_START
: The thread is automatically started.
Step 6. Run the multi-threaded Blink example using Azure RTOS ThreadX.
- Follow Step 2. Connect your device and Step 3. Run the example from Part 1: Run the Arduino Blink example.
- Observe the LED blink on and off every 1 second.
- Navigate to Tools > SerialMonitor.
- Type Hello Blinky! into the serial input line.
- Select 'Send'.
Code
/* (1) Add the Azure RTOS ThreadX library header file. */
#include <tx_api.h>
/* (3)(8) Add the thread stack memory and thread control block. */
#define THREAD_STACK_SIZE 512
TX_THREAD thread_0;
TX_THREAD thread_1;
UCHAR thread_0_stack[THREAD_STACK_SIZE];
UCHAR thread_1_stack[THREAD_STACK_SIZE];
/* (4) Define the thread's entry function. */
void thread_0_entry(ULONG thread_input)
{
(VOID)thread_input;
while(1)
{
/* (5) Move the LED blink logic into the thread's entry function. */
digitalWrite(LED_BUILTIN, HIGH); /* Turn the LED on. */
tx_thread_sleep(TX_TIMER_TICKS_PER_SECOND); /* Wait for a second. */
digitalWrite(LED_BUILTIN, LOW); /* Turn the LED off. */
tx_thread_sleep(TX_TIMER_TICKS_PER_SECOND); /* Wait for a second. */
}
}
/* (9) Define the thread's entry function. */
void thread_1_entry(ULONG thread_input)
{
(VOID)thread_input;
/* (10) Add serial read logic to the thread's entry function. */
Serial.begin(115200);
while(1)
{
if (Serial.available() > 0)
{
char byte_read = Serial.read();
Serial.print(byte_read);
}
}
}
/* (6) Add the application's environment setup function. */
void tx_application_define(void *first_unused_memory)
{
(VOID)first_unused_memory;
/* Put system definition stuff in here, e.g. thread creates and other assorted
create information. */
/* (7)(11) Create the thread. */
tx_thread_create(&thread_0, "thread 0", thread_0_entry, 0,
thread_0_stack, THREAD_STACK_SIZE,
1, 1, TX_NO_TIME_SLICE, TX_AUTO_START);
tx_thread_create(&thread_1, "thread 1", thread_1_entry, 0,
thread_1_stack, THREAD_STACK_SIZE,
4, 4, TX_NO_TIME_SLICE, TX_AUTO_START);
}
/* The setup function runs once when you press reset or power the board. */
void setup()
{
/* Initialize digital pin LED_BUILTIN as an output. */
pinMode(LED_BUILTIN, OUTPUT);
/* (2) Add the kernel entry function. */
tx_kernel_enter();
}
void loop()
{
/* (5) Move the LED blink logic into the thread's entry function. */
/* This will never be called. */
}
NOTE: Arduino Blink formatting and single-line style commenting//
have been converted to ThreadX formatting and multi-line style/* */
.
What is going on?
The code above demonstrates how to add an additional thread to an Arduino application using Azure RTOS ThreadX. One thread blinks the LED while another thread receives serial input and prints it to the serial monitor. Threads are very helpful to execute different tasks simultaneously and independently of each other.
Although not demonstrated in this tutorial, threads can also by synchronized and communicate with one another. This can be useful when a program needs to receive incoming data at any time, but also needs to process it. These two tasks can be split across two threads.
Further ReadingPlease visit What is Azure RTOS ThreadX? | Microsoft Learn to learn more.
Comments