How to Choose Communication Methods Between RTOS Tasks in Embedded Systems

Introduction

In practical embedded project development, have you ever encountered the dilemma of needing multiple tasks to work together but not knowing whether to use global variables, message queues, or semaphores? An improper choice may lead to slow system response, data contention, or even deadlocks. This article will systematically outline the inter-task communication mechanisms in RTOS for intermediate embedded developers and provide a practical selection guide.

Inter-task communication in RTOS is one of the core issues in embedded system design. The appropriate communication method not only affects system performance but also determines the maintainability of the code and the reliability of the system. We will start with three major categories of communication mechanisms, analyze their characteristics and applicable scenarios in depth, and finally provide a systematic selection framework.

Shared Memory Communication: Simple and Direct but Requires Caution

Global Variable Communication

Global variables are the most intuitive way to share data between tasks. Their advantages lie in fast access speed and simple implementation, but they also have significant limitations:

// Simple global variable communication
volatile int sensor_data = 0;

void sensor_task(void *param) {
    while(1) {
        sensor_data = read_sensor(); // Data producer
        vTaskDelay(100);
    }
}

void display_task(void *param) {
    while(1) {
        printf("Sensor: %d", sensor_data); // Data consumer
        vTaskDelay(500);
    }
}

The main issue with this method is the lack of synchronization mechanisms. When the data structure is complex, inconsistencies may occur.

Shared Buffer Design

For scenarios that require passing large amounts of data, a shared buffer can be used:

Write
Read
Read
Memory Layout
Buffer Header Metadata
Data Area 1
Data Area 2
...
Task A
Shared Buffer
Task B
Task C

Applicable Scenarios:

  • • Fast sharing of simple data types
  • • Scenarios with extremely high real-time requirements
  • • Resource-constrained systems (reducing memory copies)

Considerations:

  • • The volatile keyword must be used to prevent compiler optimization
  • • Complex data structures require additional protection mechanisms
  • • Cache consistency issues need to be considered

Synchronization Primitives Communication: Safe and Reliable Coordination Mechanisms

Semaphore Mechanism

Semaphores are the most basic synchronization primitives in RTOS, divided into binary semaphores and counting semaphores:


xSemaphoreTake()
xSemaphoreGive()
Other Tasks Take()
Semaphore Available
Available
Taken
Blocked

Binary Semaphore Example:

SemaphoreHandle_t data_ready_sem;

void producer_task(void *param) {
    while(1) {
        // Produce data
        prepare_data();
        // Notify consumer
        xSemaphoreGive(data_ready_sem);
        vTaskDelay(1000);
    }
}

void consumer_task(void *param) {
    while(1) {
        // Wait for data to be ready
        if(xSemaphoreTake(data_ready_sem, portMAX_DELAY)) {
            process_data();
        }
    }
}

Mutex and Priority Inheritance

Mutexes are specifically used to protect shared resources and support priority inheritance mechanisms to avoid priority inversion:

MutexHandle_t resource_mutex;

void high_priority_task(void *param) {
    while(1) {
        xSemaphoreTake(resource_mutex, portMAX_DELAY);
        // Access shared resource
        access_shared_resource();
        xSemaphoreGive(resource_mutex);
        vTaskDelay(100);
    }
}

Event Flag Groups

Event flag groups are suitable for multi-condition synchronization scenarios:

EventGroupHandle_t event_group;
#define SENSOR_READY_BIT (1 << 0)
#define NETWORK_READY_BIT (1 << 1)

void monitoring_task(void *param) {
    while(1) {
        EventBits_t bits = xEventGroupWaitBits(
            event_group,
            SENSOR_READY_BIT | NETWORK_READY_BIT,
            pdTRUE, pdTRUE, portMAX_DELAY);

        // Execute when both conditions are met
        if((bits & (SENSOR_READY_BIT | NETWORK_READY_BIT)) ==
           (SENSOR_READY_BIT | NETWORK_READY_BIT)) {
            upload_sensor_data();
        }
    }
}

Applicable Scenarios:

  • • Synchronization coordination between tasks
  • • Mutual access to shared resources
  • • Event-driven task activation

Message Passing Communication: The Best Choice for Decoupled Design

Message Queue Mechanism

Message queues are the most flexible communication method, supporting the transmission of messages of different sizes:

Message 1
Message 2
Message 3
FIFO Order
Queue Internal Structure
Queue Head Pointer
Queue Tail Pointer
Message 1
Message 2
Message 3
Empty Slot
Sending Task 1
Message Queue
Sending Task 2
Sending Task 3
Receiving Task

Implementation Example:

typedef struct {
    uint8_t sensor_id;
    float value;
    uint32_t timestamp;
} SensorMessage_t;

QueueHandle_t sensor_queue;

void sensor_task(void *param) {
    SensorMessage_t msg;
    while(1) {
        msg.sensor_id = 1;
        msg.value = read_temperature();
        msg.timestamp = xTaskGetTickCount();

        xQueueSend(sensor_queue, &msg, pdMS_TO_TICKS(100));
        vTaskDelay(1000);
    }
}

void processing_task(void *param) {
    SensorMessage_t received_msg;
    while(1) {
        if(xQueueReceive(sensor_queue, &received_msg, portMAX_DELAY)) {
            printf("Sensor %d: %.2f at %lu\n",
                received_msg.sensor_id,
                received_msg.value,
                received_msg.timestamp);
        }
    }
}

Mailbox System

Mailboxes are suitable for lightweight pointer passing, avoiding the overhead of copying large data:

typedef struct {
    uint8_t *data_ptr;
    size_t data_size;
} DataPacket_t;

QueueHandle_t mailbox;

void sender_task(void *param) {
    static uint8_t large_buffer[1024];
    DataPacket_t packet;

    // Only pass the pointer, do not copy data
    packet.data_ptr = large_buffer;
    packet.data_size = sizeof(large_buffer);
    xQueueSend(mailbox, &packet, 0);
}

Applicable Scenarios:

  • • Complex data exchange between tasks
  • • Scenarios requiring decoupling of sender and receiver
  • • Multi-producer single-consumer or single-producer multi-consumer patterns

Selection Decision Framework: How to Make Informed Choices

Decision Factor Analysis

Choosing the appropriate communication method requires considering the following key factors:


Simple Types int, char, etc.
Complex Structures Arrays, Structs
Extremely High <1ms
General >=1ms
Yes
No
Loose Coupling
No Tight Coupling
Start Choosing Communication Method
Data Size
Real-time Requirements
Need for Decoupling
Global Variable + volatile
Need Synchronization?
Semaphore Event Group
Message Queue Mailbox
Shared Buffer + Mutex

Performance Comparison Analysis

Communication Method Latency Memory Overhead CPU Overhead Reliability Applicable Scenarios
Global Variable Extremely Low (<1μs) Extremely Small Extremely Small Low Fast sharing of simple data
Semaphore Low (1-10μs) Small Small High Task synchronization, event notification
Mutex Low (1-10μs) Small Small High Resource protection
Message Queue Medium (10-100μs) Large Medium High Complex data exchange
Mailbox Medium (5-50μs) Medium Small High Large data pointer passing

Selection Guide

  1. 1. Simple Data + Extremely High Real-time → Global Variable + volatile
  2. 2. Task Synchronization + Event Notification → Semaphore/Event Group
  3. 3. Shared Resource Protection → Mutex
  4. 4. Complex Data Exchange + Decoupled Design → Message Queue
  5. 5. Large Data Block Passing → Mailbox System

Practical Case: Smart Home Control System

Let’s look at an example of a smart home control system to see how to apply these communication mechanisms in a real project:

System Composition:

  • • Sensor acquisition tasks (temperature, humidity, light)
  • • Display update tasks
  • • Network communication tasks
  • • Control decision tasks

Communication Scheme Design:

// 1. Sensor data - use message queue
typedef struct {
    SensorType_t type;
    float value;
    uint32_t timestamp;
} SensorData_t;
QueueHandle_t sensor_data_queue;

// 2. Display update notification - use binary semaphore
SemaphoreHandle_t display_update_sem;

// 3. Network status - use event flag group
EventGroupHandle_t network_events;
#define WIFI_CONNECTED_BIT (1 << 0)
#define MQTT_CONNECTED_BIT (1 << 1)

// 4. Current environmental parameters - use protected global variable
typedef struct {
    float temperature;
    float humidity;
    uint16_t light_level;
} EnvironmentData_t;

EnvironmentData_t current_env;
MutexHandle_t env_data_mutex;

Reasons for Selection:

  • • Sensor data uses a message queue because it requires passing complex structures and decoupling the sender and processor
  • • Display updates use a semaphore because only a simple notification mechanism is needed
  • • Network status uses an event group to support multi-condition waiting
  • • Environmental parameters use a globally protected variable with a mutex because multiple tasks need to read the latest values

Common Issues and Best Practices

Avoid Common Pitfalls

  1. 1. Priority Inversion: Use mutexes instead of binary semaphores to protect shared resources
  2. 2. Deadlocks: Ensure consistent lock acquisition order and set reasonable timeouts
  3. 3. Memory Leaks: Handling strategies when the queue is full, timely release of dynamically allocated messages

Performance Optimization Suggestions

  1. 1. Set queue lengths based on actual needs to avoid memory waste
  2. 2. For frequent small data exchanges, consider using circular buffers
  3. 3. Use priorities reasonably to avoid low-priority tasks occupying resources for long periods

Conclusion

There is no standard answer for choosing communication methods between RTOS tasks; it requires comprehensive consideration of real-time performance, data complexity, system coupling, and other factors based on specific application scenarios. Mastering the characteristics and applicable scenarios of various communication mechanisms and establishing a systematic selection framework is an essential skill for embedded developers.

In practical projects, it is often necessary to combine multiple communication methods. The key is to understand the essence of each mechanism and make trade-offs based on specific needs. I hope the analytical framework in this article can help you make more informed technical choices when facing complex embedded system designs.

Leave a Comment