FreeRTOS is an open-source, real-time operating system for microcontrollers and microprocessors that makes small, low-power devices easier to program.
Unlike typical real-time operating systems, FreeRTOS is specially designed for microcontrollers. Because microcontrollers come with limited resources, we need an operating system as per the available resources of microcontrollers. It is an open-source kernel, which means it can be downloaded free of cost and be used in RTOS-based applications. However, it also comes in two commercial flavors: OpenRTOS and SafeRTOS.
Prerequisites
- Visual Studio Code
- PlatformIO
- ESP32 development board
- Breadboard
- Motor
- Jumper wires
FreeRTOS Create Tasks
FreeRTOS permits us to run multiple tasks on a single core or on multiple cores. For example, we can run a group of tasks on core0 and another group of tasks on core1 of the ESP32.
Create Task to run on a specific core
Now, we will create the first task of the Task1 handle using the xTaskCreatePinnedToCore() function. We have set its priority as high and it will be run on core0.
xTaskCreatePinnedToCore(Task1code,"Task1",10000,NULL,1,&Task1,0);
Next, we will create the second task of the Task2 handle using the xTaskCreatePinnedToCore() function. We have also set its priority as high, but this task will be run on core1.
xTaskCreatePinnedToCore(Task2code,"Task2",10000,NULL,1,&Task2,1);
Pointer to Task Function
MyTask_pointer: This first argument to the task creation function is a pointer to a function definition of a task, because we need to define a task with the help function. We will see later how to define a function and pass this pointer to a function as an argument.
task_name: This argument is just the name of the function or task that we will create.
Stack Size
StackDepth: In multitasking, each task or thread has its own stack. It defines the stack size of a task in bytes. Therefore, you should make sure to select the stack size according to the complexity of the computation. For instance, we select 100 bytes in this example.
Parameter: If we want to pass a pointer to a variable as an argument to the task function, we can use this argument. Otherwise, we can pass the NULL value. This argument is a pointer to a variable that the task (function) can receive.
How to set Priority ?
Priority: This is an important parameter because it is used to set the priority of tasks. We set priority with passing numbers as an argument. For example, if we create four tasks and assign them priority 0, 1, 2, and 3. Hence, zero means the lowest priority, and 3 means the highest priority.
TaskHandle: This argument keeps the handle of the function that we can use to change function features such as the deletion of a task, changing its priority, etc.
Defining Task Handle
First, we will define the task handle. We want to run two separate tasks on different cores, so we will define the task handle as follows:
TaskHandle_t Task1;
TaskHandle_t Task2;
Creating Task
Secondly, in the setup() function, we will use the xTaskCreatePinnedToCore() function to create the tasks for core0 and core1 respectively. This function takes in seven parameters. They are listed in order below:
- The first parameter is the name of the function that will implement the task, e.g., SpinClockwise or SpinAntiClockwise.
- The second parameter is the name that we will allot to the task, e.g., Task1 or Task2.
- Next, is the stack size that is given to the task, e.g., 10000.
- This is the task input parameter.
- The priority of the specified task comes next, where 0 signifies the lowest priority.
- The sixth parameter is the task handle which we previously defined, e.g., &Task1 or &Task2
- Lastly, we will specify the core ID where that particular task will be run, e.g. 0 or 1, where 0 is core0 and 1 is core1. Here, we are creating the task for Task1 set at the highest priority, which will run in core0.
xTaskCreatePinnedToCore(SpinClockwise, "Task1", 10000, NULL, 1, &Task1, 0);
Here, we are creating the task for Task2 set at the highest priority, which will run in core1.
xTaskCreatePinnedToCore(SpinAntiClockwise, "Task1", 10000, NULL, 1, &Task1, 0);
Defining Task Function
Thirdly, we will create the SpinClockwise and SpinAntiClockwise functions. These functions will do their respective jobs of spinning the motor in clockwise and anticlockwise directions.
For Task1 we want the motor to spin in a clockwise direction. This function takes a single global argument called a parameter. First, we will display on the serial monitor the core ID on which the particular task is running. Next, we will use for(;;) to create an infinite loop where the motor will spin in a clockwise direction every 1 second. This will be achieved by using the digitalWrite() function and passing the GPIO pin connected to in1 of the motor driver as the first parameter and ‘HIGH’ or ‘LOW’ as the second parameter.
void SpinClockwise(void *parameter)
{
// Identify the core ID on which the code is running
Serial.print("Task1 is running on core ");
Serial.println(xPortGetCoreID());
for (;;)
{
// Set motors to maximum speed
// For PWM maximum possible values are 0 to 255
analogWrite(enA, 255);
// Open ball valve
digitalWrite(in1, HIGH);
digitalWrite(in2, LOW);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
Similarly, for Task2 we want to spin the motor anticlockwise every 1 second.
void SpinAntiClockwise(void *parameter)
{
Serial.print("Task2 is running on core ");
Serial.println(xPortGetCoreID());
for (;;)
{
// Set motors to maximum speed
// For PWM maximum possible values are 0 to 255
analogWrite(enA, 255);
// Close ball valve
digitalWrite(in1, LOW);
digitalWrite(in2, HIGH);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
Additionally, if you want to delete any created task, use the following function and specify the task handle as a parameter inside it.
vTaskDelete(taskHandle);
Arduino sketch
#include <Arduino.h>
TaskHandle_t Task1;
TaskHandle_t Task2;
// Motor A connections
int enA = 9;
int in1 = 8;
int in2 = 7;
void SpinClockwise(void *parameter)
{
// Identify the core ID on which the code is running
Serial.print("Task1 is running on core ");
Serial.println(xPortGetCoreID());
for (;;)
{
// Set motors to maximum speed
// For PWM maximum possible values are 0 to 255
analogWrite(enA, 255);
// Open ball valve
digitalWrite(in1, HIGH);
digitalWrite(in2, LOW);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
void SpinAntiClockwise(void *parameter)
{
Serial.print("Task2 is running on core ");
Serial.println(xPortGetCoreID());
for (;;)
{
// Set motors to maximum speed
// For PWM maximum possible values are 0 to 255
analogWrite(enA, 255);
// Close ball valve
digitalWrite(in1, LOW);
digitalWrite(in2, HIGH);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
void setup()
{
Serial.begin(15200);
// Set all the motor control pins to outputs
pinMode(enA, OUTPUT);
pinMode(in1, OUTPUT);
pinMode(in2, OUTPUT);
xTaskCreatePinnedToCore(SpinClockwise, "Task1", 10000, NULL, 1, &Task1, 0);
xTaskCreatePinnedToCore(SpinAntiClockwise, "Task2", 10000, NULL, 1, &Task2, 1);
}
void loop()
{
}