Learning FreeRTOS: Mutex Semaphores

Scan to followLearn Embedded Together, learn and grow together

Learning FreeRTOS: Mutex Semaphores

A mutex semaphore (Mutex, short for Mutual Exclusion) is a special type of binary semaphore in FreeRTOS, specifically designed for implementing mutual access to resources.

Compared to ordinary semaphores, mutex semaphores have the following key characteristics:

  • Ownership Concept: Only the task that takes the mutex can give it back
  • Priority Inheritance Mechanism: Prevents priority inversion issues
  • Recursive Acquisition: The same task can take the same mutex multiple times (requires configuration)

Working Principle

Basic Workflow

  1. Task Takes Mutex: Gains exclusive access to shared resources
  2. Task Uses Resource: Safely accesses shared resources
  3. Task Gives Mutex: Allows other tasks to access the resource

Priority Inheritance Mechanism

When a low-priority task holds a mutex and a high-priority task attempts to take it:

  1. The low-priority task temporarily inherits the priority of the high-priority task
  2. The low-priority task completes execution faster and releases the mutex
  3. After releasing, it returns to its original priority

This mechanism effectively reduces the duration of priority inversion.

Main APIs for Mutex Semaphores

Create Mutex

SemaphoreHandle_t xSemaphoreCreateMutex(void);
  • Return Value: Returns the mutex handle on success, NULL on failure
  • The created mutex is initially available

Example:

SemaphoreHandle_t xMutex = xSemaphoreCreateMutex();
if(xMutex == NULL) {
    // Error handling
}

Create Recursive Mutex

SemaphoreHandle_t xSemaphoreCreateRecursiveMutex(void);
  • Allows the same task to take the same mutex multiple times
  • Must be released the same number of times to truly release it

Take Mutex

Normal Mutex:

BaseType_t xSemaphoreTake(SemaphoreHandle_t xMutex, TickType_t xTicksToWait);

Recursive Mutex:

BaseType_t xSemaphoreTakeRecursive(SemaphoreHandle_t xMutex, TickType_t xTicksToWait);

Parameters:

  • <span>xMutex</span>: Mutex handle
  • <span>xTicksToWait</span>: Maximum wait time (portMAX_DELAY means wait indefinitely)

Return Value:

  • pdTRUE: Successfully taken
  • pdFALSE: Timeout or error

Release Mutex

Normal Mutex:

BaseType_t xSemaphoreGive(SemaphoreHandle_t xMutex);

Recursive Mutex:

BaseType_t xSemaphoreGiveRecursive(SemaphoreHandle_t xMutex);

Parameters: Mutex handle

Return Value:

  • pdTRUE: Successfully released
  • pdFALSE: Error (e.g., task does not hold the mutex)

Application Scenarios

Shared Resource Protection

Protect exclusive access to hardware peripherals (e.g., UART, SPI) or software resources (e.g., global variables, data structures).

Critical Section Protection

Protect short critical section code instead of disabling interrupts, improving system responsiveness.

Thread-Safe Data Structures

Implement thread-safe access to data structures like linked lists and queues.

Examples

Basic Usage Example

SemaphoreHandle_t xUARTMutex;

void vTask1(void *pvParameters) {
    while(1) {
        // Take UART mutex
        if(xSemaphoreTake(xUARTMutex, portMAX_DELAY) == pdTRUE) {
            // Exclusive access to UART
            printf("Task1 using UART\n");
            vTaskDelay(pdMS_TO_TICKS(100));
            
            // Release mutex
            xSemaphoreGive(xUARTMutex);
        }
        vTaskDelay(1);
    }
}

void vTask2(void *pvParameters) {
    while(1) {
        // Take UART mutex (wait up to 10ms)
        if(xSemaphoreTake(xUARTMutex, pdMS_TO_TICKS(10)) == pdTRUE) {
            // Exclusive access to UART
            printf("Task2 using UART\n");
            vTaskDelay(pdMS_TO_TICKS(50));
            
            // Release mutex
            xSemaphoreGive(xUARTMutex);
        } else {
            printf("Task2 failed to get UART\n");
        }
        vTaskDelay(1);
    }
}

int main(void) {
    // Create mutex
    xUARTMutex = xSemaphoreCreateMutex();
    
    // Create tasks
    xTaskCreate(vTask1, "Task1", configMINIMAL_STACK_SIZE, NULL, 2, NULL);
    xTaskCreate(vTask2, "Task2", configMINIMAL_STACK_SIZE, NULL, 2, NULL);
    
    // Start scheduler
    vTaskStartScheduler();
    
    while(1);
}

Recursive Mutex Example

SemaphoreHandle_t xRecursiveMutex;

void vRecursiveFunction(int depth) {
    // Recursively take mutex
    if(xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY) != pdTRUE) {
        return;
    }
    
    printf("Depth %d - Mutex acquired\n", depth);
    
    if(depth < 3) {
        vRecursiveFunction(depth + 1); // Recursive call
    }
    
    // Recursively release mutex
    xSemaphoreGiveRecursive(xRecursiveMutex);
    printf("Depth %d - Mutex released\n", depth);
}

void vRecursiveTask(void *pvParameters) {
    while(1) {
        vRecursiveFunction(0);
        vTaskDelay(1000);
    }
}

int main(void) {
    // Create recursive mutex
    xRecursiveMutex = xSemaphoreCreateRecursiveMutex();
    
    // Create task
    xTaskCreate(vRecursiveTask, "RecTask", configMINIMAL_STACK_SIZE, NULL, 1, NULL);
    
    // Start scheduler
    vTaskStartScheduler();
    
    while(1);
}

Mutex Semaphores vs Binary Semaphores

Feature Mutex Semaphore Binary Semaphore
Ownership Must be released by the holder Any task can release
Priority Inheritance Supported Not supported
Recursive Acquisition Configurable support Not supported
Initial State Available immediately after creation Configurable initial state
Usage Scenario Resource protection Task synchronization/event notification
On Release Only the holder can release Any task can release

Considerations

Usage Principles

  1. Keep Hold Time Short: Minimize the time holding the mutex
  2. Avoid Nested Acquisition: Avoid multiple acquisitions unless using a recursive mutex
  3. Consistent Acquisition Order: Acquire multiple mutexes in a fixed order to prevent deadlocks
  4. Always Check Return Values: Especially for timed acquisition operations

Error Handling

  1. Handling Acquisition Failures:

    if(xSemaphoreTake(xMutex, pdMS_TO_TICKS(100)) != pdTRUE) {
        // Handle timeout or error
    }
    
  2. Handling Release Failures:

    if(xSemaphoreGive(xMutex) != pdTRUE) {
        // Usually indicates the task does not hold the mutex
    }
    

Performance Considerations

  1. Avoid Using in ISR: Mutexes are designed for inter-task communication; ISRs should use semaphores
  2. Optimize Short Critical Sections: For very short critical sections, consider suspending the task scheduler or disabling interrupts
  3. Evaluate Alternatives: For simple variable protection, consider using atomic operations

Common Issues and Solutions

Deadlock Issues

Scenario:

  • Task A holds mutex X and requests mutex Y
  • Task B holds mutex Y and requests mutex X

Solution:

  1. Fix the mutex acquisition order
  2. Use timed acquisition operations
  3. Avoid cross-acquisition in design

Priority Inversion

Scenario:

  • Low-priority task holds a mutex
  • Medium-priority task preempts CPU
  • High-priority task waits for the mutex

Solution:

  1. Ensure priority inheritance is enabled (default support in FreeRTOS mutexes)
  2. Design task priorities reasonably
  3. Reduce mutex hold time

Resource Leaks

Scenario: Task is deleted before releasing the mutex

Solution:

  1. Ensure all held mutexes are released before deleting the task
  2. Use resource tracking mechanisms
  3. Consider using RAII patterns (e.g., in C++)

Advanced

Implementation Principles of Mutexes

FreeRTOS mutexes internally use the following mechanisms:

  1. Task blocking list: Manages tasks waiting for the mutex
  2. Holder tracking: Records the current task holding the mutex
  3. Priority adjustment: Implements priority inheritance

Comparison with Task Notifications

For simple resource protection, task notifications may be more efficient:

  • Less memory usage
  • Faster operations
  • But does not support multiple tasks waiting

Cross-Platform Considerations

Different RTOS implementations of mutexes may vary:

  • Recursive behavior
  • Details of priority inheritance
  • Error handling methods

Conclusion

FreeRTOS mutex semaphores are a key mechanism for protecting shared resources. Proper usage can:

  1. Ensure data consistency
  2. Prevent race conditions
  3. Resolve priority inversion issues

Key points:

  • Prefer using mutexes over binary semaphores for resource protection
  • Keep critical section code as short as possible
  • Always check API return values
  • Design task priorities and resource access order reasonably

Learning FreeRTOS: Mutex Semaphores

Follow 【Learn Embedded Together】 to become better together

If you find this article useful, click “Share”, “Like”, or “Recommend”!

Leave a Comment