After some time using the FreeRTOS real-time operating system along with the ESP32, I've noticed that it's quite laborious to only use the bare FreeRTOS functionalities for inter-task communication.
Following points particularly struck me:
- X different queues had to be created to pass X different datatypes over to another task
- Since you have to speficy the length of the queue + size of future messages during queue-creation
- If you have multiple connections to other tasks, the number of different queues will be confusing after some time
- No obvious cut between different connections (Queues defined all over the place, who is the reading task at the end of the queue?)
→ Therefore I've decided to build the FreeRTOS inter-task communication wrapper-libray FreeRTOS-Transceiver as a project for my college. The library is still in an early stage and will probably be, along with some other planned FreeRTOS inter-task libraries, my focus for a while (Better usage of C++, Code refactoring etc.). New ideas and suggestions for improvement are welcome.
The FreeRTOS-Transceiver C++ library simplifies the use of FreeRTOS inter-task communication and encapsulates related queues (tx/rx) to one communication line.
Example inter-task communication setup with FreeRTOS-Transceiver
This blockdiagram shows an example of how a possible FreeRTOS-Transceiver setup could look like. We have a normal communication between 'Task A' and 'Task_Data_1'. Normal, because the communication line is only used by two tasks. 'Task A' and 'Task_Data_1' are set up this way, so that a bidirectional communication is possible. Removing one of the queues could change it to a unidirectional one. Ultimately, it is up to the developer what kind of communication is needed.
Connection types possible with the library:
- Unidirectional
- Bidirectional
- Echo
- Multi-sender-queue
- Multi-reader-queue (Can be set up, but not tested and certainly not as secure as other types)
There is also a Multi-Sender-Queue in the block-diagram above, where 'Task_Data_1', 'Task_Data_2' and 'Task_Data_3' are the senders. 'Task_DataProcessor' is the receiver, although it is important to mention that he does not know who the senders on the Multi-Sender-Queue are. So in case that 'Task_DataProcessor' wants to talk to the senders, it must open a seperate connection to each of them.
In the following blockdiagram you can see a very short incomplete description about the internal library structure. Each communication line has its seperate rx buffer of length FRTTRANSCEIVER_MAXELEMENTSIZEONQUEUE
, but only queue length
positions of the buffer will be in use (because we read max. queue length
amount of data into rx buffers...new data will be inserted into the buffer if old one is released first)
Internal rx buffers
In order to pass different types of data to the queue, the FRTT::FRTTDataContainerOnQueue
structure was introduced:
/* Creating a queue with FRTT::FRTTCreateQueue(queueLength) will create a queue for queueLength
amount of FRTT::FRTTDateContainerOnQueue structures */
struct FRTTDataContainerOnQueue{
FRTTTaskHandle senderAddress;
void * data;
uint8_t u8DataType;
#if defined(FRTTRANSCEIVER_32BITADDITIONALDATA)
uint32_t u32AdditionalData;
#elif defined(FRTTRANSCEIVER_64BITADDITIONALDATA)
uint64_t u64AdditionalData;
#endif
};
With this method it is now possible to transmit a pointer to any data, and specify the type of data with u8DataType
, so that the receiving task can perform an explicit cast.
The red blocks in the blockdiagram represent the data allocator/data de-allocator callbacks, a user must provide to the library. Those are essentially used to copy original data (FRTT::FRTTDataContainerOnQueue
), read from the queue, into the corresponding rx buffer position.
In the allocator callback a user can now do (Basically what ever your project requires):
- Allocate memory with malloc/new (not recommended) and make a copy of the data the
void * data
points to (switch case overu8DataType
)- You can provide the length of a buffer (e.g uint8_t * buff) through the u32AdditionalData/u64AdditionalData variable and use it inside the allocator/de-allocator callbacks
- Just copy the
void * data
over and make sure that the pointer points to a valid address during access - Future implementations will introduce some sort of memory pool class which can be used inside the callbacks to dynamically allocate memory/free memory
In the de-allocator callback a user must do:
- Free/deleete the memory previously allocated with malloc/new
- Future implementations will introduce some sort of memory pool class which can be used inside the callbacks to dynamically allocate memory/free memory
→ Using dynamic memory allocation in the allocator callback lets you create a copy of the data you received, right between FRTT:FRTTransceiver::readFromQueue
and 'moveIntoBuffer'
. Through this you can keep data as long as you want in your buffer without being dependent on the sender to keep the pointer valid. (example of dynamic memory allocation inside the callbacks)
Another option is to just copy the pointer over and manually copy received data into a local variable (get buffered data with the various FRTT::FRTTransceiver::getxXxBufferedDataFrom()
methods).
An additional call to FRTT::FRTTransceiver::writeToQueue()
could act as a signal to the sender (or just use Task Notification as a better method for signaling something)
To further understand how the library works, please take a look at the documented source code (You might open the index.html inside your browser). Also check out the various examples that are provided in this repo click.
There are two different ways to create an instance of the FRTT::FRTTransceiver
class.
-
Locally allocated array of structures holding all partner-communication (static memory)
FRTT::FRTTTaskHandle OWNER = (FRTT::FRTTTaskHandle)0x1; // Use a self build FRTT::FRTTTaskHandle, or better acquire the real handle! uint8_t u8Partners = 5; FRTT::FRTTCommunicationPartner partners[u8Partners] // 5 possible connection to partner tasks FRTT::FRTTransceiver comm(OWNER,&partners[0],u8Partners);
The length of the array must match the
u8Partners
parameter! -
Dynamically allocated inside the library
FRTT::FRTTTaskHandle OWNER = (FRTT::FRTTTaskHandle)0x1; // Use a self build FRTT::FRTTTaskHandle, or acquire the real handle! uint8_t u8Partners = 5; FRTT::FRTTransceiver comm(OWNER,u8Partners);
Passing a
0
asu8Partners
will increment it to1
inside the constructor!
Before we begin with examples it is important to understand how the library tells different communication partners apart.
The library distinguishes communication partners with the value of their FRTT::FRTTTaskHandle
(void *
). Unless you plan to use the Task Notification functionality, you can build your own FRTT::FRTTTaskHandles
like this (not recommended, always acquire the real one during task-creation):
FRTT::FRTTTaskHandle TASK1 = (FRTT::FRTTTaskHandle)0x1;
FRTT::FRTTTaskHandle TASK2 = (FRTT::FRTTTaskHandle)0x2;
You can add a communication partner to the list like this (Read also Important data for a Task Notification setup and Important data for a Queue setup):
using namespace FRTT;
// comm instance for 4 partner connections
FRTTTaskHandle TASK1 // This on has been acquired through FRTTCreateTask();
FRTTTaskHandle TASK2; // This on has been acquired through FRTTCreateTask();
FRTTTaskHandle TASK3; // This on has been acquired through FRTTCreateTask();
int QUEUELENGTH = 1;
FRTTQueueHandle QUEUE; // This on has been acquired through FRTTCreateQueue();
FRTTSemaphoreHandle S; // This on has been acquired through FRTTCreateSemaphore();
int QUEUELENGTH = 1;
FRTTQueueHandle QUEUE1; // This on has been acquired through FRTTCreateQueue();
FRTTSemaphoreHandle S1; // This on has been acquired through FRTTCreateSemaphore();
int QUEUELENGTH = 1;
FRTTQueueHandle QUEUE2; // This on has been acquired through FRTTCreateQueue();
FRTTSemaphoreHandle S2; // This on has been acquired through FRTTCreateSemaphore();
/* No Task Notification (because 0x0, self-build handle), only Queue-Read operations*/
comm.addCommPartner((FRTTTaskHandle)0x1,QUEUE,QUEUELENGTH,S,nullptr,0,nullptr,"COMM-TO-TASK0x1);
/* Only usable for Task Notification */
comm.addCommPartner(TASK1,nullptr,0,nullptr,nullptr,0,nullptr,"COMM-TO-TASK1);
/* Usable for Task Notification and Queue-Read operations */
comm.addCommPartner(TASK2,QUEUE1,QUEUELENGTH,S1,nullptr,0,nullptr,"COMM-TO-TASK2);
/* Usable for Task Notification and Queue-Write operations*/
comm.addCommPartner(TASK3,nullptr,0,nullptr,QUEUE2,QUEUELENGTH,S2,"COMM-TO-TASK3");
// Another comm.addCommPartner(...) will fail because we have no connections left
You can acquire the real task handles while task-creation with FRTT::FRTTCreateTask
, by passing the address to your FRTT::FRTTTaskHandle
as a parameter. Those real task handles are important when you try to notify that task, because FreeRTOS uses the address (void *
) to access the tasks control block of that task!
What you cant do (reference to Important data for a Task Notification setup and Important data for a Queue setup):
- Use the same
FRTT::FRTTTaskHandle
for two connections - Use the same
FRTT::FRTTQueueHandle
that was previously linked to a partner (same queue for rx & tx inFRTT::FRTTransceiver::addCommPartner()
works ->ECHO
) - Use the same
FRTT::FRTTSemaphoreHandle
that was previously linked to a partner (same semaphore for rx & tx inFRTT::FRTTransceiver::addCommPartner()
works ->ECHO
) - Use invalid
FRTT::FRTTTaskHandles/FRTT::FRTTQueueHandles
and plan to use them for functionality (queue read/write, task notification) where FreeRTOS will access the memory address stored in those variables
Adding a Multi-Sender-Queue as a connection is similiar to a normal connection. In case of a Multi-Sender-Queue you wont use a standard FRTT::FRTTTaskHandle
. Instead you just provide the FRTT::FRTTQueueHandle
.
The Multi-Sender-Queue added to the list of connections is then accessible via FRTT::eMultiSenderQueue::eMULTISENDERQ0
enumerator, the second via FRTT::eMultiSenderQueue::eMULTISENDERQ1
enumerator, and so on. Those connections are read-only.
using namespace FRTT;
// comm instance for 4 partner connections
int QUEUELENGTH = 1;
FRTTQueueHandle QUEUE; // This on has been acquired through FRTTCreateQueue();
FRTTSemaphoreHandle S; // This on has been acquired through FRTTCreateSemaphore();
int QUEUELENGTH = 1;
FRTTQueueHandle QUEUE1; // This on has been acquired through FRTTCreateQueue();
FRTTSemaphoreHandle S1; // This on has been acquired through FRTTCreateSemaphore();
int QUEUELENGTH = 1;
FRTTQueueHandle QUEUE2; // This on has been acquired through FRTTCreateQueue();
FRTTSemaphoreHandle S2; // This on has been acquired through FRTTCreateSemaphore();
comm.addMultiSenderPartner(QUEUE,QUEUELENGTH,S,"MULTI1"); // accessible via eMultiSenderQueue::eMULTISENDERQ0
comm.addMultiSenderPartner(QUEUE1,QUEUELENGTH,S1,"MULTI2"); // accessible via eMultiSenderQueue::eMULTISENDERQ1
comm.addMultiSenderPartner(QUEUE2,QUEUELENGTH,S2,"MULTI3"); // accessible via eMultiSenderQueue::eMULTISENDERQ2
....
// reads from 'QUEUE' that has been added as the first queue
comm.readFromQueue(nullptr,eMultiSenderQueue::eMULTISENDERQ0,false,100,100);
....
Being one of the senders of a Multi-Sender-Queue is simply achievable by adding queue and semaphore as a normal communication comm.addCommPartner(...)
.
One sender does not know who any of the other sender tasks are, the reader on the other hand can check the senderAddress
field of the FRTT::FRTTDataContainerOnQueue
structure for the source of the data package (if the sender provided his real address).
-
Task Notification (Receive functionality)
- Owner Address: Can be nullptr (FreeRTOS knows who the task asking for the notification state/value will be)
- Partner FRTT::FRTTTaskHandle:
FRTTTaskHandle
!= nullptr (some val > 0x0, or valid handle) && theFRTT::FRTTTaskHandle
has not been added yet - FRTT::FRTTQueueHandle: Can be nullptr
- FRTT::FRTTSemaphoreHandle: Can be nullptr
-
Task Notification (Notify functionality)
- Owner Address: Can be nullptr
- Partner FRTTTaskHandle: MUST be a valid address != nullptr, acquired through
FRTT::FRTTCreateTask
&&FRTT::FRTTTaskHandle
has not been added yet - FRTTQueueHandle: Can be nullptr
- Semaphore: Can be nullptr
-
Queue (Read functionality)
- Owner Address: Can be nullptr
- Partner FRTT::FRTTTaskHandle:
FRTT::FRTTTaskHandle
!= nullptr &&FRTT::FRTTTaskHandle
has not been added yet.Tx-queue
can be nullptr. - FRTT::FRTTSemaphoreHandle: The
rx-semaphore
for therx-queue
must be a valid address != nullptr, acquired throughFRTT::FRTTCreateSemaphore
&&rx-semahpore
has not been added yet.Tx-semaphore
can be nullptr.
-
Queue (Write functionality)
- Owner Address: Can be nullptr (But you might add it because the address of the owner will be put into data packages as the 'source')
- Partner FRTT::FRTTTaskHandle:
FRTT::FRTTTaskHandle
!= nullptr &&FRTT::FRTTTaskHandle
has not been added yet - FRTT::FRTTQueueHandle: The
tx-queue
must be a valid address != nullptr, acquired throughFRTT::FRTTCreateQueue
&&tx-queue
has not been added yet.Rx-queue
can be nullptr. - FRTT::SemaphoreHandle:
Tx-semaphore
for thetx-queue
must be a valid address != nullptr, acquired throughFRTT::FRTTCreateSemaphore
&&tx-semahpore
has not been added yet.Rx-semaphore
can be nullptr.
Lets begin with an empty arduino sketch (Reminder: loop() is running on core 1)
void setup(){}
void loop(){}
The minimal useful setup is a unidirectional
connection between two tasks (besides echo
queues).
Lets setup the code to create two tasks and other important objects. 'Task 1' will be created with 5000 bytes stacksize and priority 8 on CORE-0. 'Task 2' will be created with 5000 bytes stacksize and priority 8 on CORE-1.
.....
.....
using namespace FRTT;
FRTTTaskHandle TASK1_HANDLE; // will hold the address of TASK1
FRTTTaskHandle TASK2_HANDLE; // will hold the address of TASK2
FRTTQueueHandle QUEUE_TO_TASK2; // queue address
FRTTSemaphoreHandle SMPH; // unidirectional == only one semaphore needed
#define STACKSIZE 5000u
void TASK1 (void * pvParams) // basic FreeRTOS style
{
while(TASK1_HANDLE == nullptr || TASK2_HANDLE == nullptr) vTaskDelay(pdMS_TO_TICKS(2)); // wait until freertos created tasks, we dont want nullptr
for(;;){
// here "loop()" kind of stuff
}
vTaskDelete(nullptr);
}
void TASK2(void * pvParams) // basic FreeRTOS style
{
while(TASK1_HANDLE == nullptr || TASK2_HANDLE == nullptr) vTaskDelay(pdMS_TO_TICKS(2)); // wait until freertos created tasks, we dont want nullptr
for(;;){
// here "loop()" kind of stuff
}
vTaskDelete(nullptr);
}
// Setup() runs at the beginning, so we do setup everything in here
void setup(){
//disableCore0WDT(); // maybe needed (esp32 only)
//disableCore1WDT(); // maybe needed (esp32 only)
QUEUE_TO_TASK2 = FRTTCreateQueue(2); // Create queue with length 2
SMPH = FRTTCreateSemaphore(); // Create semaphore
FRTTCreateTask(TASK1,"task-1",STACKSIZE,NULL,8,&TASK1_HANDLE,0); // Creates TASK1 (without core param for esp8266)
FRTTCreateTask(TASK2,"task-2",STACKSIZE,NULL,8,&TASK2_HANDLE,1); // Creates TASK2 (without core param for esp8266)
}
void loop(){} // not needed anymore
Now that we've created both tasks, we can now proceed to establish a connection with the FreeRTOS-Transceiver library.
It is very important to add the informations regarding a communication line in the right way.
Lets zoom into both tasks and setup the communication lines:
......
void TASK1 (void * pvParams) // basic FreeRTOS style
{
while(TASK1_HANDLE == nullptr || TASK2_HANDLE == nullptr) vTaskDelay(pdMS_TO_TICKS(2)); // wait until freertos created tasks, we dont want nullptr
FRTTCommunicationPartner partners[1];
FRTTransceiver comm(TASK1_HANDLE,&partners[0],1); // Add owner address and amount of desired connections
comm.addDataAllocateCallback(dataAllocator); // Please check examples to understand how to create a basic callback
comm.addDataFreeCallback(dataDestroyer); // Please check examples to understand how to create a basic callback
comm.addCommPartner(TASK2_HANDLE,nullptr,0,nullptr,QUEUE_TO_TASK2,2,SMPH,"COMM-TO-TASK2"); // Here TASK1 is the sender so add the queue and semaphore as TX
// from here on write etc...
for(;;){
// here "loop()" kind of stuff
}
vTaskDelete(nullptr);
}
void TASK2(void * pvParams) // basic FreeRTOS style
{
while(TASK1_HANDLE == nullptr || TASK2_HANDLE == nullptr) vTaskDelay(pdMS_TO_TICKS(2)); // wait until freertos created tasks, we dont want nullptr
FRTTCommunicationPartner partners[1];
FRTTransceiver comm(TASK1_HANDLE,&partners[0],1); // Add owner address and amount of desired connections
comm.addDataAllocateCallback(dataAllocator); // Please check examples to understand how to create a callback
comm.addDataFreeCallback(dataDestroyer); // Please check examples to understand how to create a callback
comm.addCommPartner(TASK2_HANDLE,QUEUE_TO_TASK2,2,SMPH,nullptr,0,nullptr,"COMM-TO-TASK1"); // Here TASK2 is the receiver so add the queue and semaphore as RX
// from here on read etc...
for(;;){
// here "loop()" kind of stuff
}
vTaskDelete(nullptr);
}
.....
Now with this setup you can proceed to write to the receiver (TASK2) and read from the sender (TASK1). Now please check out the several different examples to understand how to write/read from/to the queue (and many other things).
-
Passing data over the queue
- Sending data to every possible task
- Simultaneous transmission of x different datatypes (over the same queue)
- Simultaneous transmission of x different datatypes to y different queues (databroadcast)
-
Receiving data over the queue
- Receiving data sent by any task in the system
-
Formatted representation of details regarding all connections to other tasks
- Address of the owner task
- Maximum possible connections
- Amount of current normal connections to partner tasks
- Amount of current multi-sender-queue connections
- Info, whether neccessary callbacks are supplied by the user or not
- Amount of databroadcasts carried out
- Amount of notifications sent
- Amount of notifactions received
- Latest notification value
- FRTTransceiver object runtime in either seconds or minutes (if minutes at least 1)
- Informations for each connection
- Name of the communication partner
- Address of the communication partner
- Communication type (Normal, Multi-Sender-Queue, Echo)
- Information whether tx/rx queues are ON or OFF
- Length of tx/rx queues
- Amount of datapackages sent
- Amount of datapackages received
- Information if buffered data available
-
A single queue can be used by multiple tasks (Multi-Sender-Queue)
- Tasks can add their taskhandle as a "source" address
- 1...n transmitter of data
- Multi-Sender-Queue connections are read only. It is not possible to add a tx queue to this communication line.
-
Queue/Buffer manipulation
- Check if datatype x available in buffer
- Removing an element in buffer
- Removing the x. element from buffer
- Removing the oldest element from buffer
- Removing the newest element from buffer
- Buffer flush
- Queue Flush
-
Task notification functionality
- Basic Notify/Receive Notification functionality added
- Notifier can simply 'increment' the receving tasks notification value
- Receiver can check for a notification value > 0 (with additional block-time in ms)
- Extended Notify/Receive Notification functionality added
- Multiple ways to notify the partner task (bit-mask to set in receivers notif. value, increment, only setting notif. state to pending etc.)
- Receiver can clear his notifcation value onEntry/onExit with a u32 bit mask (with additional block-time in ms)
- Internal 32 bit variable holding the last notification value
- Accessible via get-method
- Internal boolean variable holding the information whether the last call to 'NotifyReceivexXx' resulted in a notification or not
- Accessible via get-method
- Basic Notify/Receive Notification functionality added
-
Secure access to data
- Maximum of 2 semaphores per connection. One for the tx queue, one for the rx queue
- E.g preventing sender from overwriting previously sent data, while the receiver does a copy (callbacks) right in that moment
- Enough time to dynamically allocate memory to make a copy for your internal buffers
- Maximum of 2 semaphores per connection. One for the tx queue, one for the rx queue
This library has been developed and tested on an ESP32-WROOM-32 microcontroller inside a PlatformIO environment.
- Example code plus an installation guide is located in examples-esp32
FRTTransceiver Library also works for the ESP8266
- Besides developing and testing on the ES32, Ive also tried to adjust parts for the ESP8266. An example along with an installation guide can be found in examples-esp8266
-
Mainly in development for the ESP32 microcontroller
-
Works for the ESP8266 but the installation is 'harder'
- See Installation
- FRTTransceiver library is not useable with the PlatformIO
arduino-framework
for the ESP8266 (Thearduino-framework
does not use FreeRTOS,instead they use theESP-NONOS-SDK
) - FRTTransceiver library is currently not adjusted to the PlatformIO
ESP8266-RTOS-SDK
framework (did not get it to work yet, PlatformIO also uses a very outdated version of theESP8266-RTOS-SDK
)
Apache 2.0 License