-1

I am using Ardunio/ESP32 and I am very new to FreeRTOS. I want to have a task that is solely responsible for printing text on serial port and other tasks can push messages. So, I decided to use a Queue of char arrays (or std::string) with 10 item capacity. But I am not sure how the queue storage allocation works for elements with different lengths!

Can you enlighten me on how I should create and consume the queue and what consideration I should take into account?

Reza
  • 43
  • 7
  • This is somewhat opinion based, but your approach sounds reasonable as it is fairly generally stated. Read the documentation for queues in detail, as well as documentation about FreeRTOS memory requirements. The memory requirements for a queue depends upon whether you are queuing references to the data or copies of the data. FreeRTOS queues allow either. If your strings are few and very small, copies may be ok. If not, then you should use references. – lurker May 01 '21 at 13:47
  • The std::string has also small string optimization, so the strings up to 16characters (including '\0') are not allocated in heap. However I'd expect issues with new/delete from multiple threads (at least I have suspicion on it) – KIIV May 01 '21 at 17:20

3 Answers3

3

TL;DR: create a Queue of pointers to std::string and handle new/delete on either side. Make sure both producer and consumer are using a shared memory space.

The problem with using std::string in a "raw" memory API like FreeRTOS Queue isn't actually an issue with the size of the object. In fact the std::string object size is fixed, regardless of the size of the character array stored by the object. Don't believe me? Try compiling and running this simple program on your own machine:

#include <iostream>

int main(int argc, char **argv)
{
    std::string str1 = "short";
    std::string str2 = "very very very very very very long";

    std::cout << "str1 length = " << sizeof(str1) << "\n";
    std::cout << "str2 length = " << sizeof(str2) << "\n";
    return 0;
}

You'll get something like this (actual size will vary depending on your platform):

str1 length = 24
str2 length = 24

Note that this result would be different if you used str1.size().

The reason for this is that standard library containers like std::string usually store their contents in malloc'ed blocks, so the std::string object itself will just store a pointer to an array that contains the character data in the string. The sizeof() in this case tells you the size of the object from the compiler's perspective, which would be the size of the pointer and other fixed metadata (i.e. the fields of the struct). The .size() method tells you the size of the string from the implementation, which will include a string length calculation on the allocated character array.

The reason you can't just copy a std::string object into the xQueueSend() variants is a problem of lifecycle management. The FreeRTOS API does *Send and *Receive via raw memcpy. Standard library containers are usually not POD types, which means they must be copied via a dedicated copy constructor. If you don't do this, you're likely to invalidate some internal state of the object unless you really know what you're doing.

So the easiest way to make this work would look something like this:

  1. On xQueueCreate(), set your object size to sizeof(std::string *):
xQueue = xQueueCreate(NUM_OBJECTS, sizeof(std::string *));
  1. On xQueueSend(), create a std::string via operator new, and pass an address to this pointer to be copied. Do not delete the object.
std::string *pStr = new std::string("hello world!");
xQueueSend(xQueue, &pStr, (TickType_t)0);
  1. On xQueueReceive(), copy out the pointer. Do what you need to do with this pointer, and then delete it.
std::string *pStr = NULL;
xQueueReceive(xQueue, &pStr, (TickType_t)10);
// do stuff with pStr
delete pStr;
Jon Reeves
  • 2,426
  • 3
  • 14
  • Thank you Jon for your comprehensive solution. I totally believe you since I noticed unusual behavior when using std::string in the queue. I was very nervous about the memory issue and not sure who will take care of the release part. I don't even know If FreeRTOS promises to free them. But your solution was quite clear and straightforward. One last question, do you think using std::string would be better or pure char array? – Reza May 01 '21 at 20:00
  • 1
    @Reza Unless you're working with a system that is constrained in memory, I would go ahead and use `std::string`. This will give you all of the power of the STL class without having to re-implement functionality yourself. The only real advantage you get in using a pure char array is avoiding the overhead of each std::string struct allocation. If you *are* memory constrained, definitely use the pure char array, just be aware that you'll end up writing more code for string management on either side of send/receive. – Jon Reeves May 02 '21 at 05:56
  • Thanks for your help. I might be a bit memory-constrained but most likely I move the strings (maybe the task) into PSRAM so no more memory limitation. – Reza May 02 '21 at 06:36
1

Hi in This example I'll send a string data in the queue using a struct :

First We create the Struct:

typedef struct log_payload_t
{
    uint8_t message[256];
} log_payload_t;

Create the Queue:

xQueueHandle log_queue;
int main(int argc, char **argv)
{
   log_queue = xQueueCreate(10, sizeof(log_payload_t));
}

Send to String to the Queue:

log_payload_t payload;

memset(payload.message, 0, 256);
memcpy(payload.message, "Example text", strlen(str));

if (xQueueSend(log_queue, &payload, 0))
    {
            printf("added message to log queue  \n");
    }
    else
    {
            printf("failed to add message to log queue\n");
        
    }

Receive from Queue:

log_payload_t payload;
if (xQueueReceive(log_queue, &payload, 0))
{
    printf("Log Queue Data %s \n", payload.message);

}
else
{
    vTaskDelay(10);
}           
Youcef Ali
  • 189
  • 1
  • 6
  • 1
    Thanks for your reply. I afraid the other solution with std::string is offering a better utilization for ram. In your solution, we gonna reserve 2kb regardless and we might need even more for some of the messages. – Reza May 11 '21 at 18:48
  • 1
    Yes i Agree with you, this proposition is for C, there is no std string type. – Youcef Ali May 12 '21 at 21:51
0

From my point of view, using a container of a fixed size buffer can be less that sub-optimal. Each operation of sending/receiving to/from a queue will involve a full copy of the whole buffer. It would also require larger and carefully selected stack sizes.

freeRTOS message buffers seem to me very convenient to solve your needs of sending chunks of data of nearly arbitrary sizes from one task to another.

V.Lorz
  • 293
  • 2
  • 8
  • Thanks for your suggestion, that looks a good solution. however, I am still undecided how message buffers can be more useful than regular queues. – Reza May 11 '21 at 18:52
  • 1
    You want to send strings, by nature variable length data, and have one sending task and one receiving task. For this scenario message buffers are more efficient, in terms of memory use, than [this solution](https://stackoverflow.com/a/67383128/15086628). And if you can get rid of the overhead of creating-freeing std::string objects, it's another optimization in terms of heap operations. IMO, the most flexible solution is [this one](https://stackoverflow.com/a/67349866/15086628), to use a queue of pointers to std::string, to allocate in the sending task and to free in the receiving task. – V.Lorz May 12 '21 at 07:43