freertos-colas-queues-basico

FreeRTOS Queues: create, send and receive data

  • 6 min

A queue in FreeRTOS is a safe buffer that allows sending data between tasks without sharing global variables recklessly.

We are starting a new block in this course. So far, our tasks were solitary islands; they lived, worked, and died without knowing anything about their neighbors. In a real system, tasks need to collaborate.

Think of a task that reads a temperature sensor every 100 ms and another that must display the value on an OLED screen or send it via WiFi. How do we pass the data from one to the other?

The beginner’s temptation is to use a global variable.

// DANGER! Don't do this at home
int globalTemperature;

void SensorTask() { globalTemperature = readSensor(); }
void DisplayTask() { display.print(globalTemperature); }
Copied!

This is a terrible bad practice. It causes race conditions, data corruption (if one writes while the other reads half the data), and makes the code difficult to debug.

To solve this elegantly and safely (Thread Safe), FreeRTOS gives us its most robust communication mechanism: Queues.

What is a Queue?

A queue is, conceptually, a pipe or data conveyor belt. It works on the FIFO (First In, First Out) principle: the first data that enters is the first to leave.

It has two fundamental characteristics that make it perfect for RTOS:

  1. Concurrency protection: FreeRTOS handles locks internally. You don’t need to worry about whether two tasks try to write at the same time; the system brings order.
  2. Blocking capability: If a task tries to read from an empty queue, it can sleep (enter Blocked state) automatically until data arrives. Goodbye to inefficient Polling!

Creating a Queue

Before using it, we must create it and define its dimensions. Queues in FreeRTOS have a fixed size pre-allocated in RAM.

We use the xQueueCreate function:

QueueHandle_t xQueueCreate(
    UBaseType_t uxQueueLength, // Max number of elements that fit
    UBaseType_t uxItemSize     // Size in bytes of EACH element
);
Copied!

For example, to create a queue that can store up to 10 integer numbers:

QueueHandle_t integerQueue;

void setup() {
    // Queue of 10 slots, each slot the size of an int
    integerQueue = xQueueCreate(10, sizeof(int));

    if(integerQueue == NULL) {
        Serial.println("Error creating queue (insufficient RAM)");
    }
}
Copied!

It is important to check if it returns NULL. Queues consume Heap RAM, and if you create a very large queue, you could run out of memory.

Sending data: xQueueSend

To put data into the queue, we use xQueueSend.

BaseType_t xQueueSend(
    QueueHandle_t xQueue,       // The queue handle
    const void * pvItemToQueue, // Pointer to the data we want to copy
    TickType_t xTicksToWait     // Wait time if the queue is FULL
);
Copied!

The “Copy by Value” Concept

This is critical: FreeRTOS copies the data inside the queue. It does not store a reference to your variable, but performs a memcpy of the content.

  • If you send an int, it copies the 4 bytes of the integer.
  • This is great because you can use local variables in the sending function, and even if that variable disappears when the function ends, the data is already safe in the queue.

The Timeout

The third parameter defines what to do if the queue is full (no more data fits):

  • 0: Attempts to write and if it can’t, returns an error immediately.
  • pdMS_TO_TICKS(100): Waits 100ms to see if a slot frees up. If not, gives up.
  • portMAX_DELAY: Waits indefinitely when INCLUDE_vTaskSuspend is enabled, as is the case in the default ESP32 configuration.
int measurement = 25;
// Send by copying the value of 'measurement'. Waits 0 ticks if full.
xQueueSend(integerQueue, &measurement, 0);
Copied!

Receiving data: xQueueReceive

To retrieve data, we use xQueueReceive. This function removes the data from the queue (deletes it) and copies it into our local buffer.

BaseType_t xQueueReceive(
    QueueHandle_t xQueue,   // The handle
    void *pvBuffer,         // Where to store the received data
    TickType_t xTicksToWait // Wait time if the queue is EMPTY
);
Copied!

Here xTicksToWait indicates how long we are willing to wait. Normally we will want to set portMAX_DELAY so that the consumer task does not waste CPU if there is no data. It will wake up automatically as soon as something arrives.

Practical example: the counter

Let’s implement the classic Producer-Consumer pattern.

  • Producer: Generates incremental numbers and sends them.
  • Consumer: Waits for numbers and prints them.
// 1. Define the global Handle
QueueHandle_t simpleQueue;

void ProducerTask(void *pvParameters) {
  int counter = 0;
  for(;;) {
    // Increment
    counter++;

    Serial.printf("Producer: Sending %d\n", counter);

    // Send to the queue.
    // We use &counter because it asks for a pointer to the data,
    // but remember: it copies the VALUE of counter inside the queue.
    xQueueSend(simpleQueue, &counter, portMAX_DELAY);

    vTaskDelay(pdMS_TO_TICKS(1000)); // Wait 1 sec
  }
}

void ConsumerTask(void *pvParameters) {
  int receivedData = 0;
  for(;;) {
    // This line BLOCKS the task until something arrives.
    // It does not consume CPU while waiting.
    if(xQueueReceive(simpleQueue, &receivedData, portMAX_DELAY) == pdTRUE) {
      // If we get here, we have received something
      Serial.printf("--- Consumer: Received %d \n", receivedData);
    }
  }
}

void setup() {
  Serial.begin(115200);

  // 2. Create the queue before the tasks
  simpleQueue = xQueueCreate(10, sizeof(int));

  if(simpleQueue == NULL) {
    Serial.println("Error creating queue");
    for(;;) {
      delay(1000);
    }
  }

  // 3. Create the tasks
  xTaskCreate(ProducerTask, "Producer", 2048, NULL, 1, NULL);
  xTaskCreate(ConsumerTask, "Consumer", 2048, NULL, 1, NULL);
}

void loop() {}
Copied!

What happens if the producer is faster?

If we change the Producer’s delay to 100ms and the Consumer takes 500ms to process:

  1. The queue will fill up (1, 2, 3…).
  2. Upon reaching 10 elements (the size we defined), the queue is full.
  3. Since we set portMAX_DELAY in the Producer’s xQueueSend, the Producer will block when trying to insert the 11th.
  4. The system self-regulates. The Producer is forced to slow down to the Consumer’s pace.

Supported data types

In this example we used int. But xQueueCreate accepts any fixed size:

  • sizeof(byte)
  • sizeof(float)
  • sizeof(bool)

What if we want to send a complex structure (struct) with multiple data (temperature, humidity, sensor_id)? Or a long String?

Here things change. Since FreeRTOS does “copy by value”, copying giant structures is inefficient and slow.

Do not send a String, a std::string, or any other object with internal resources as if it were a flat structure. The queue performs a byte-by-byte copy, it does not call constructors or manage the ownership of that memory.