Arduino IDE is a great platform to tinker and access different libraries within an instant, But it does not support multitasking effectively, or does it?
FreeRTOS is a simple implementation that supports multitasking efficiently and is also a library in Arduino IDE. In this blog, we will see a simple blinking LED to run two tasks concurrently and control the led.
What is RTOS?A GPOS (General Purpose Operating System) runs multiple tasks at the same time. The scheduler, which is present in the Operating system switches multiple tasks on and off the processing cores so fast that it creates the illusion of multitasking. A well know example is Windows OS. GPOS also does not run with any time constraints, i.e. it can be unpredictable how long each task can take to finish.
A Real-Time Operating System(RTOS) can run multiple tasks simultaneously, but with time constraints and priority orders. It only allows a certain time for which the task can run.
FreeRTOSRTOS is a relatively large operating system, and cannot run on a microcontroller like ESP32, instead, a smaller variant called FreeRTOS, designed for microcontrollers is used. In FreeRTOS thread refers to tasks. Tasks can be prioritized depending on their importance.
Preemptive SchedulingIn a single-core MCU (Microcontroller Unit), only one task can execute at any instance. At timeout, the FreeRTOS scheduler saves the state of the current task by saving its registers. The current task is said to have been preempted by the timer. Then the next task starts running on the same core. This happens once every tick.
In preemptive scheduling, the CPU is allocated to the tasks for a limited time before it gets preempted and the next task starts. After a task gets preempted, it waits for another tick and joins the queue.
Task SchedulerIf a task is ready to run but waiting, it is said to be in the queue. The Scheduler is a piece of software inside the operating system in charge of figuring out which task should run at each tick. A task with higher priority will be moved forward to the queue against a task with lower priority.
Time SlicingTime-slicing by the RTOS is having multiple tasks be given a slice of the CPU's time. If your system has a few tasks that need to be executed on the processor, FreeRTOSs ability to time-slice allows each task to receive some fixed number of system ticks of runtime for each task.
Installing FreeRTOSFreeRTOS libraries are present in the ESP32, so installing ESP32 Board Manager to support ESP32 will install the required FreeRTOS Libraries.
1) Go to Arduino IDE, Open File -> Preferences
2) Enter the following into the "Additional Board Manager URLs” field:
https://dl.espressif.com/dl/package_esp32_index.json
3) Click the "OK" button
4) Open the Boards Manager. Go to Tools -> Board -> Boards Manager
5) Search for ESP32 and press the install button for the “ESP32 by Espressif Systems“ and click install.
And now, we have successfully installed ESP32.
Let's get StartedESP32 has two cores, and the task can run on either of the cores. Two cores mean faster and more efficient, but it is difficult to manage them. So for running them on the same core we limit the ESP32 to one core.
#if CONFIG_FREERTOS_UNICORE
static const BaseType_t app_cpu = 0;
#else
static const BaseType_t app_cpu = 1;
#endif
We will create one task that turns on and off LED every 500 milliseconds and another that does the same, but once every 600 milliseconds.
The blinking LED is written inside a while loop, so the task never ends
vTaskDelay() and delay() are both non-blocking weights, i.e. it lets run other tasks till the delay ends. But vTaskDelay() uses the number of ticks instead of the time in milliseconds. Hence we obtain the number of ticks taken for delay by dividing time in milliseconds by the time taken for each tick in milliseconds using the function portTICK_PERIOD_MS. Either vTaskDelay() or delay() can be used, but using vTaskDelay is better to avoid confusion in further RTOS.to
void toggle_LED_1(void *parameter)
{
while(1)
{
digitalWrite(13,HIGH);
vTaskDelay(500/portTICK_PERIOD_MS);
digitalWrite(13,LOW);
vTaskDelay(500/portTICK_PERIOD_MS);
}
}
Now, in the void setup part, we set a pin to OUTPUT to make the LED blink
xTaskCreate() is used to create the task which is supposed to run from the function. xTaskCreatePinnedToCore() also does the same but limits the code to use only one processor at a time. Since we want to run the tasks on a single core, we use xTaskCreatePinnedToCore().
The syntax of xTaskCreatePinnedToCore() is
xTaskCreatePinnedToCore(<function to be called>, <name of the task>, <size of the stack>, <parameter if any needed to be added>, <Task priority, maximum is 25>, <Task Handle>, <name used in making ESP32 in one core>)
void setup()
{
pinMode(13,OUTPUT);
xTaskCreatePinnedToCore(toggle_LED_1,
"Toggle 1",
1024,
NULL,
1,
NULL,
app_cpu);
xTaskCreatePinnedToCore(toggle_LED_2,
"Toggle 2",
1024,
NULL,
1,
NULL,
app_cpu);
}
A parameter is used if any variables are needed to be introduced into the task. A Task Handle is used to control tasks anywhere from the code. Either of these is not used here for the sake of simplicity.
The loop part of the function is to be left empty.
Multiplexing LED Matrix with FreeRTOSMultiplexing is the technique employed to operate LED matrices. By multiplexing, only one row of the LED matrix is activated at any one time. This approach is required because one end of the LED (either the anode or the cathode) is tied to a single row.
Multiplexing in LED Matrix is activating one row at a time and switching the rows which are powered, fast to give the illusion of all LEDs being switched on at the same time. The main advantage of multiplexing is that they reduce the number of wires needed to connect the Microcontroller and the matrix. Switching multiple tasks very fast can be done using RTOS, even for multiplexing.
For the project, we will be using an 8x8 matrix, consisting of 64 LEDs
Since each task runs for 1 millisecond in FreeRTOS, we will make sure that the previous row is turned off and the current row is powered on. Once every 8 milliseconds, the tasks loop again, powering each LED for a total of 125 milliseconds. Due to this, brightness will be lower than normal.
How does the Code workWe will run this program also on a single core, so we need to configure
#if CONFIG_FREERTOS_UNICORE
static const BaseType_t app_cpu = 0;
#else
static const BaseType_t app_cpu = 1;
#endif
We will declare all the pins used. In this case, all the pins acting as positive are labeled from b1 to b8, and pins acting as negative are labeled from a1 to a8.
We will fix the positive terminals as rows and the negative terminals as columns.
In every function, we will connect the positive terminal to one row, and connect certain columns to the negative to generate the desired pattern in the row.
int a1=13;
int a2=12;
int a3=14;
int a4=27;
int a5=26;
int a6=25;
int a7=33;
int a8=32;
int b1=18;
int b2=19;
int b3=21;
int b4=22;
int b5=15;
int b6=2;
int b7=4;
int b8=5;
int i=0 //this declaration is for a variable, used to change the image
Now we will create an array containing the data which we wish to display on the screen.
For this project, I will be using 4 different images which I want to display on the 8x8 matrix. So here I create a 32x8 array in the code to store the data.
Image 1 is stored in a[0][0] to a[7][7], a 8x8 grid.
Image 2 is stored in a[8][0] to a[15][7], a 8x8 grid.
Image 3 is stored in a[16][0] to a[23][7], a 8x8 grid.
Image 3 is stored in a[24][0] to a[31][7], a 8x8 grid.
Note: Since we are changing the negative terminals to accept current or not, setting the pin state as HIGH will stop the current flow and LED will not be turned on, and setting the pin to a LOW state will let the current flow through the circuit and the LED will be turned on. So we need to upload the negative image which we need to display. In this case, if a LED needs to be on at a certain point, we need to set it to zero in the array in the code.
int a[32][8]={{1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1},
{1,1,1,0,0,1,1,1},
{1,1,1,0,0,1,1,1},
{1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1},
{1,1,0,0,0,0,1,1},
{1,1,0,1,1,0,1,1},
{1,1,0,1,1,0,1,1},
{1,1,0,0,0,0,1,1},
{1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1},
{1,0,0,0,0,0,0,1},
{1,0,1,1,1,1,0,1},
{1,0,1,1,1,1,0,1},
{1,0,1,1,1,1,0,1},
{1,0,1,1,1,1,0,1},
{1,0,0,0,0,0,0,1},
{1,1,1,1,1,1,1,1},
{0,0,0,0,0,0,0,0},
{0,1,1,1,1,1,1,0},
{0,1,1,1,1,1,1,0},
{0,1,1,1,1,1,1,0},
{0,1,1,1,1,1,1,0},
{0,1,1,1,1,1,1,0},
{0,1,1,1,1,1,1,0},
{0,0,0,0,0,0,0,0}};
Accessingthegrid
If we want to power LED in ( 4, 5 ) position, values a[ 3 ][ 4 ], a[ 11 ][ 4 ], a[ 19 ][ 4 ], and a[ 27 ][ 4 ], are used in images 1, 2, 3, 4 respectively. This can be generalized into
a[ (3 + i) ][ 5 ] where i=0, 8, 16, 24 for images 1, 2, 3, 4 respectively
Since we have a general function to turn on LED in a certain position, we can just change the"i" value to get the data for different images for the LED.
To switch the image once every 500 milliseconds (for example), we will create a function, which increases the value of "i" every once in 500 milliseconds till I
void frameswitcher(void *parameter)
{
while(1)
{
i=8;
vTaskDelay(500/portTICK_PERIOD_MS);
i=16;
vTaskDelay(500/portTICK_PERIOD_MS);
i=24;
vTaskDelay(500/portTICK_PERIOD_MS);
i=0;
vTaskDelay(500/portTICK_PERIOD_MS);
}
}
In the first part of the function which controls each row, we turn off all the LEDs, then we connect the positive terminal of the row. and we get the data for each column for the same row and connect the respective columns to negative.
This will turn on the LEDs that we wish, and then we send the task back to the queue, by giving it a small delay.
void toggleLED_1(void *parameter)
{
while(1)
{
digitalWrite(a1,1);
digitalWrite(a2,1);
digitalWrite(a3,1);
digitalWrite(a4,1);
digitalWrite(a5,1);
digitalWrite(b6,1);
digitalWrite(a7,1);
digitalWrite(a8,1);
digitalWrite(b1,0);
digitalWrite(b2,0);
digitalWrite(b3,0);
digitalWrite(b4,0);
digitalWrite(b5,0);
digitalWrite(b6,0);
digitalWrite(b7,0);
digitalWrite(b8,0);
digitalWrite(b1,HIGH);
digitalWrite(a1,a[0+i][0]);
digitalWrite(a2,a[0+i][1]);
digitalWrite(a3,a[0+i][2]);
digitalWrite(a4,a[0+i][3]);
digitalWrite(a5,a[0+i][4]);
digitalWrite(a6,a[0+i][5]);
digitalWrite(a7,a[0+i][6]);
digitalWrite(a8,a[0+i][7]);
vTaskDelay(2/portTICK_PERIOD_MS);
}
}
Since the function is row-wise, we create 7 more functions similar to this for the remaining 7 rows.
voidsetup()
In the setup() part, we define all the pins used here, from a1 to a8 and b1 to b8
pinMode(a1,OUTPUT);
pinMode(a2,OUTPUT);
pinMode(a3,OUTPUT);
pinMode(a4,OUTPUT);
pinMode(a5,OUTPUT);
pinMode(a6,OUTPUT);
pinMode(a7,OUTPUT);
pinMode(a8,OUTPUT);
pinMode(b1,OUTPUT);
pinMode(b2,OUTPUT);
pinMode(b3,OUTPUT);
pinMode(b4,OUTPUT);
pinMode(b5,OUTPUT);
pinMode(b6,OUTPUT);
pinMode(b7,OUTPUT);
pinMode(b8,OUTPUT);
And for the final part, we will create all nine tasks, eight for controlling the LEDs and one for changing the image.
xTaskCreatePinnedToCore(toggleLED_1,"Toggle 1",1024,NULL,1,NULL,app_cpu);
xTaskCreatePinnedToCore(toggleLED_2,"Toggle 2",1024,NULL,1,NULL,app_cpu);
xTaskCreatePinnedToCore(toggleLED_3,"Toggle 3",1024,NULL,1,NULL,app_cpu);
xTaskCreatePinnedToCore(toggleLED_4,"Toggle 4",1024,NULL,1,NULL,app_cpu);
xTaskCreatePinnedToCore(toggleLED_5,"Toggle 5",1024,NULL,1,NULL,app_cpu);
xTaskCreatePinnedToCore(toggleLED_6,"Toggle 6",1024,NULL,1,NULL,app_cpu);
xTaskCreatePinnedToCore(toggleLED_7,"Toggle 7",1024,NULL,1,NULL,app_cpu);
xTaskCreatePinnedToCore(toggleLED_8,"Toggle 8",1024,NULL,1,NULL,app_cpu);
xTaskCreatePinnedToCore(frameswitcher,"Frame Switcher",1024,NULL,1,NULL,app_cpu);
void loop(), as always should be left empty
The code and schematics are available at the end of the blog, upload the code and try it out for yourselves.
Conclusion
So far we have seen how FreeRTOS works and how it is used to run multiple tasks at the same time.
Hope you guys understood and had fun reading the blog. If anyone has queries, feel free to ask them in the comments.
Comments