Last Updated on 18 January 2021 by Suffocation
A task is a (recurring) job for the processor. Multiple tasks compete for the CPU's attention. This article briefly outlines how they are created and what data structures are available for data transmission and synchronisation.
Tasks can become a very complex matter, which is why I can only scratch the surface here. Nevertheless, this is by far the longest contribution to the series to date. For further research, please also refer to the Espressif Website - FreeRTOS Chapter referred.
Basics
Tasks can be viewed as encapsulated units of work. Within the Espressif IDF, one or more tasks can be executed in parallel/concurrently, depending on the number of processor cores. Fundamentally, however, multiple tasks can be initiated that compete for the processor core(s). Whenever a task no longer requires the processor core, it releases it, and the next task takes its turn.
Which task is preferred next fundamentally depends on its priority and how long it has been waiting. The exact execution order depends on the scheduler's implementation. The tasks in the IDF originate from FreeRTOS, where a watchdog additionally monitors tasks to ensure they don't take too long. Furthermore, it is assumed that tasks run indefinitely. If a task needs to be terminated, it must be deleted beforehand, otherwise an error will occur.
Many of the functions described below exist in multiple forms. For one thing, it can be determined during creation whether the memory should be reserved dynamically or statically at compile time. The static functions always end with static.
On the other hand, there are functions that can be called from an interrupt, these always end with ‚FromISR‘.
The following primarily addresses functions with dynamic memory allocation, for further information please refer to the IDF reference.
Create Task
Libraries
#include "freertos/FreeRTOS.h"
#include "freertos/task.h""
Tasks are created with the command:
xTaskCreate(
<pointer zur="" task="" funktion,="" ,
,
,
,
);;
It's good programming practice to always have a task only to run for as long as is truly necessary. If a task has to wait for a value or action, it should go to sleep and pass the processor on to another task. Sleeping can be initiated with the following function.
void vTaskDelay(const TickType_t xTicksToDelay);
With this command, the task waits for a specific amount of time before it is ready to run again. Generally, you can put the task to sleep with the following command:
// Suspend a specific task
void vTaskSuspend();
// Suspend a task from an interrupt service routine
BaseType_t xTaskResumeFromISR()
// Suspend your own task
vTaskSuspend(NULL);;
In this case, the task will wait until it is activated from the outside again. This is done with the following command:
// normal resumption
void xTaskResume();
// from interrupt service routine
void xTaskResumeFromISR();;
FreeRTOS tasks do not normally terminate. If they do end as in the example, an error is thrown. To prevent this, the task must be deleted before it ends. This is done with the statement:
vTaskDelete( xHandles[task_index] );
It should be noted that, as in the example, if xTaskDelete is called within its own task function, all instructions following this command will no longer be executed because the task will already have been deleted.
The following example creates multiple tasks, each running for a different length of time and producing output when active:
Synchronisation
Freetos offers data structures for synchronisation and passing data from one task to another. I was able to find the following tools:
- Queues - This is a list. The reading task waits here until an entry is available in the list.
- Traffic lights Used to synchronise tasks with each other or to protect shared resources from data errors.
- Event Groups Are used for synchronising tasks, they wait for the setting of defined bits by tasks. The group can wait for one or all bits. Once the bits are set, the group's waiting is ended.
- Stream Buffer Data is transferred via stream. The reading task waits until there is data available in the stream.
- Message Buffer - Based on the stream buffer. One task can put messages into the buffer and another can wait for them.
- Hooks Used to perform tasks when the processor has nothing else to do.
- Ring Buffer A ring buffer is a data structure with a fixed memory. When the end of the memory is reached, it attempts to find memory again in the front area. Since the ring buffer is a bit more complex, I have left it out for now.
- Timer - Start timer Tasks once or periodically, for timers I have my own Post created.
If these data structures are used from interrupt routines, the ISR functions with the same name must be used for the call.
With data structures, with the exception of semaphores, it is always assumed that there is only one sender task and one receiver task. If multiple resources need to be sent, for example, the sending function must be protected as a critical section (e.g. with a semaphore).
Queues
Queues are used to exchange data. The sending task hands data over to the queue and can then continue running. The consuming task retrieves the data when it is ready.
Library
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h""
Create Queue:
QueueHandle_t xQueue;
xQueue = xQueueCreate( , );
if (xQueue == 0)
{
// Error detection
ESP_LOGE(TAG, "Error during queue creation");
}
Send data
if(!xQueueSendToBack(, (void*)&, )) {
// queue full
ESP_LOGW(TAG, "q - Queue Full");
}
Receive data
if( ! xQueueReceive( , , ) ){
ESP_LOGW(TAG, "q - Queue empty!");
}
The example below works with two tasks. One fills the queue, the other reads the values from it. Because the reading task is slower than the writing task at the beginning, the queue fills up until it is full and can no longer accept any values. Due to the subsequent acceleration of the reading task, the queue gets shorter until it is empty and the reading task has to wait for the writing task.
Traffic lights
Semaphores and mutexes are used to secure areas and synchronise threads. Here, I will describe their use with the help of binary semaphores. Mutexes and recursive semaphores are created in a similar way but have additional functionalities.
Library
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h""
Create semaphore:
SemaphoreHandle_t xSemaphore = NULL;
xSemaphore = xSemaphoreCreateBinary();
if( xSemaphore == NULL ) {
// Error
}
Before first use, the semaphore must be released once.
Fetch semaphore
if( xSemaphoreTake( , timeout) == pdTRUE )
{
// it worked ...
xSemaphoreGive( );
} else {
// it didn't work
}
Release the semaphore:
xSemaphoreGive();;
The example below creates 5 tasks. All of them want to increment a variable. To avoid data errors, the critical section (incrementing) is protected by a semaphore. Within this section, the processor is briefly handed over to the other tasks, allowing them to attempt to acquire the semaphore. Explicitly, the value of the variable is first copied, then the other processes are given the opportunity to change it, and only then is the changed value written back. This is not good practice, but it helps the example ;). You can remove the semaphore by commenting out the #define below. The tasks now behave differently ;).
// (de)activate this to disable/enable the semaphores
#define enable_semaphore
Regarding recursive mutexes, it should be noted that they can be acquired repeatedly by the owner task. The number of acquisitions is counted and must also be mirrored by the number of releases. These can be used, for example, for recursion, where the same function needs to access the same data repeatedly by calling itself.
Event Groups
Library
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h""
Setting the bit:
EventBits_t xEventGroupSetBits(
,
);;
When reusing an existing group, the bits must be reset.
EventBits_t xEventGroupClearBits(
,
);;
Wait for the set bits, this is done with:
xEventGroupWaitBits(
,
,
,
,
);
// Auswertung
if( xEventGroup == NULL )
{
// EVG konnte nicht erstellt werden
ESP_LOGE(TAG,"Event groupe not created");
} else if( (uxBits & ( BIT_0 | BIT_1| ... | BIT_n) ) == ( BIT_0 | BIT_1| BIT_2| ... | BIT_n ) )
{
// alle bits wurden gesetzt
ESP_LOGI(TAG,"Program finshed successfull");
} else {
// nicht alle Bits wurden gesetzt, timeout
ESP_LOGW(TAG,"Timeout");
}
Here is a complete example with multiple tasks. The main task waits until all bits are set. Once all are present, the main task continues.
Stream Buffer
Library
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/stream_buffer.h""
Create:
Triggerlevel and MaxAnzBytes are both of type const size_t. The former indicates how large the buffer stream is. The latter describes how many bytes the receiver will be released from its waiting state.
xStreamBufferCreate(, )
Function to send:
The function takes a stream buffer and the data to be sent. The return value provides the number of bytes sent.
size_t xStreamBufferSend(
,
,
,
)
Received:
size_t xStreamBufferReceive(
,
( void * )&,
,
);;
Example Programme
Message Buffer
The message buffer is built on top of the stream buffer. Additionally, data can be encapsulated into messages. A message is defined by its length. Before receiving a message, its length can be queried from the buffer.
Library
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/message_buffer.h""
Create:
xMessageBufferCreate(
);
Write
size_t xMessageBufferSend(
,
( void * ) ,
,
);;
Reading a message:
First, I'll ask about the length of the next message:
size_t xMessageBufferNextLengthBytes(<BufferHandle>);
Für den eigentlichen Empfang wird die folgende Funktion verwendet
size_t xStreamBufferReceive(
<BufferHandle>,
( void * ) <EmpfangsBuffer>,
<LaengeDerNachricht>,
<TimeOut>
);
So sieht der Gesamtcode aus. Dieser unterscheidet sich nur geringfügig vom Code des StreamBuffers.
Hooks
Es gibt Idle und Tick Hooks. Idle Hooks werden ausgelöst wenn die CPU im leerlauf ist. Tickhooks werden über den Tickinterrupt ausgelöst. Zweiteres soll Zeitlich nicht sehr zuverlässig sein. Für zeitliche Abfolgen sollten eher Timer oder PWM Signale verwendet werden.
Weiterhin gibt es die standard Hooks des FreeRtos und eigene Varianten der IDF.
FreeRTOS Implementation
- Zur Nutzung CONFIG_FREERTOS_LEGACY_HOOKS aktivieren
- Es kann nur einen Idle und eine Tick Hook geben
- FreeRtos Implementierung ist auf single core ausgelegt.
IDF Implementation
- Es können mehrer Hooks/Ticks registriert werden.
- Ein Hooks/Ticks mus seiner CPU zugewiesen werden
- Die registrierten Hooks/Ticks werden routierend ausgeführt.
Es dürfen niemals blockierende Codestellen in Hooks und Ticks verwendet werden. Diese müssen immer komplett durhlaufen werden und sollten möglichst kurz sein, da sie sonst andere Tasks behindern könnten.
In Ticks dürfen zusätzlich keine API Funktionionen ohne "FromIsr" verwendet werden. LogAusgaben sind ebenfalls nicht möglich.
Library
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_freertos_hooks.h"
Idle Hook erstellen
esp_err_t esp_register_freertos_idle_hook_for_cpu(
<Funktion für Callback>,
<CPU ID>
)
// oder
esp_err_t esp_register_freertos_idle_hook_for_cpu(
<Funktion für Callback>
)
// Funktionsprototyp
static bool hook1(void)
{}
Tick erstellen
esp_err_t esp_register_freertos_tick_hook(
<tickFunctionName>
<CPU ID>
);
// oder
esp_err_t esp_register_freertos_tick_hook(
<tickFunctionName>
);
// Funktionsprototyp
static void tick2(void)
{}
Fehler
- ESP_OK: Hat geklappt
- ESP_ERR_NO_MEM: Kein Speicher für den Prozessorkern um einen Hook zu registrieren
- ESP_ERR_INVALID_ARG: Ungültige CPU
CPU ID abfragen
// core des aktuellen Tasks
const uint32_t core_id = xPortGetCoreID();
// bei zwei Kernen den anderen bekommen
const uint32_t core_id = !xPortGetCoreID();
Hier das Beispielprogramm.
Ring buffer
Der Ringbuffer der IDF sind eine "interessante" Sache. Ich spare sie mir an dieser Stelle mal auf für einen anderen Zeitpunkt auf, andem ich sie wirklich brauche ;).
Library
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h""
Conclusion
Jede Aktion in einen eigenen Task zu verpacken bedarf einiger Übung und Planung. Um so wichtiger ist es die richtigen Bibliotheksfunktionen zur Verfügung zu habe. Ob diese Funktionalitäten ausreichend sind, muss sich in der Praxis zeigen. Grundsätzlich scheint aber alle was man brauch zur Verfügung zu stehen.
Hello Markus,
Danke für deinen Kommentar, es freut mich immer wieder wenn der Blog hilft.
Grusd
Stefan
Hello Stefan,
vielen Dank für die Mühe die du dir hier gibst! Dieser Blog zum Thema Tasks mit der IDF ist echt genial und hat mir super weiter geholfen.
5 *****
Gruß – Markus