freertos-patron-productor-consumidor

Producer-Consumer Pattern in FreeRTOS

  • 4 min

The Producer-Consumer pattern is a way to separate who generates data from who processes it by using an intermediate queue to decouple both tasks.

It is one of the most useful patterns in FreeRTOS because it appears everywhere. A sensor produces readings, a task consumes them. An interrupt produces events, a task processes them. A connection receives messages, another interprets them. The idea is always the same.

The problem

Suppose we have a task that reads a sensor every 100ms and another that sends that data over WiFi. We could use a global variable:

float temperature;
Copied!

And that’s it, right? Well, not so fast.

If one task writes while another reads, we can have inconsistent data. Furthermore, if the WiFi transmission takes too long, the sensor reading gets mixed up with logic it shouldn’t be handling.

The clean approach is to separate responsibilities:

  • The producer reads the sensor and puts data into a queue.
  • The consumer waits for data and processes it when it arrives.

The queue acts as an “inbox”. If the consumer is a bit slower, messages accumulate up to the configured limit.

Pattern structure

The basic scheme is this:

Producer Task  ->  Queue  ->  Consumer Task
Copied!

In FreeRTOS, the queue gives us three very valuable things:

  1. Safe communication between tasks.
  2. Efficient blocking, without wasting CPU while waiting.
  3. Temporary buffer, to absorb small speed differences.

Complete example

Let’s create a queue for sensor readings. The producer generates values and the consumer prints them over the serial port.

struct SensorReading {
  uint32_t time;
  float temperature;
};

QueueHandle_t readingsQueue;

void ProducerTask(void *pvParameters) {
  for(;;) {
    SensorReading reading;
    reading.time = millis();
    reading.temperature = readTemperature();

    xQueueSend(readingsQueue, &reading, portMAX_DELAY);

    vTaskDelay(pdMS_TO_TICKS(100));
  }
}

void ConsumerTask(void *pvParameters) {
  SensorReading reading;

  for(;;) {
    if (xQueueReceive(readingsQueue, &reading, portMAX_DELAY) == pdTRUE) {
      Serial.print("t=");
      Serial.print(reading.time);
      Serial.print(" temp=");
      Serial.println(reading.temperature);
    }
  }
}

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

  readingsQueue = xQueueCreate(10, sizeof(SensorReading));

  if (readingsQueue == NULL) {
    Serial.println("Could not create queue");
    return;
  }

  xTaskCreate(ProducerTask, "Producer", 2048, NULL, 1, NULL);
  xTaskCreate(ConsumerTask, "Consumer", 4096, NULL, 1, NULL);
}

void loop() {
  vTaskDelay(pdMS_TO_TICKS(1000));
}
Copied!

The producer task creates a SensorReading structure and sends it via the queue. The consumer task remains blocked on xQueueReceive until data arrives. While waiting, it does not consume CPU.

What size should the queue be

The queue size depends on the difference between the producer and consumer speeds, and the maximum time the consumer might be busy.

If the producer generates data every 100ms and the consumer takes 20ms to process it, a small queue is sufficient. If the consumer sometimes gets blocked by WiFi or SD card writing, we need more margin.

A large queue does not fix a consumer that is too slow. It only delays the problem. If the queue constantly fills up, the system is producing more data than it can process.

We can decide what to do when the queue is full:

  • Wait with portMAX_DELAY.
  • Wait for a limited time.
  • Discard the data.
  • Overwrite the last value with xQueueOverwrite() (only for queues of length 1).

If the producer uses portMAX_DELAY, the queue applies backpressure: when it’s full, the producer stops maintaining its period until a slot becomes available. This might be desirable, but we must decide explicitly.

Common variants

The Producer-Consumer pattern has several forms depending on the case.

One producer, one consumer

This is the simplest case. One task generates data and another processes it. Ideal for sensors, buttons, or data acquisition.

Multiple producers, one consumer

Several tasks send messages to a common queue. The consumer centralizes the processing.

This is very useful for a logging task:

Sensor Task -> |
WiFi Task   -> | -> Log Queue -> Logger Task
Motor Task  -> |
Copied!

This way, we avoid multiple tasks writing simultaneously to the serial port, an SD card, or a display.

One producer, multiple consumers

This case requires more care. A normal queue delivers each message to only one consumer. If we want all consumers to receive the same data, we need one queue per subscriber or a publish/subscribe architecture. An Event Group can broadcast signals represented by bits, but it does not transport the complete reading.