Chapter 13: Task Synchronization: Semaphores and Mutexes

Chapter Objectives

Upon completing this chapter, you will be able to:

  • Explain the concept of a “critical section” and why protecting shared resources is essential in multitasking.
  • Define race conditions and understand how they occur.
  • Describe the purpose and function of Binary Semaphores for signaling and basic synchronization.
  • Describe the purpose and function of Counting Semaphores for managing multiple resources.
  • Create, take (acquire), and give (release) Binary and Counting Semaphores.
  • Define Mutexes (Mutual Exclusion Semaphores) and explain their primary use case: protecting shared data access.
  • Understand the concept of Mutex ownership and the importance of releasing a Mutex.
  • Create, take, and give Standard and Recursive Mutexes.
  • Explain Priority Inversion and how FreeRTOS Mutexes help mitigate it using Priority Inheritance.
  • Use ISR-safe semaphore functions (xSemaphoreGiveFromISR).
  • Implement synchronization patterns using semaphores and mutexes to solve common concurrency problems.
  • Differentiate between when to use a Semaphore versus a Mutex.

Introduction

In Chapter 12, we explored how FreeRTOS Queues provide a robust way to pass copies of data between tasks, avoiding the pitfalls of directly sharing variables. However, there are scenarios where tasks need to coordinate access to a single, shared resource rather than just passing data copies. This resource could be a peripheral like a UART or I2C bus, a section of memory, a global data structure, or even just the right to execute a specific piece of code.

When multiple tasks attempt to access and modify the same shared resource concurrently, we risk encountering race conditions, leading to corrupted data and unpredictable system behavior. Consider a scenario where two tasks try to update the same counter or write to the same display simultaneously – the final state could be incorrect depending on the exact timing of task preemption.

To prevent these issues, we need mechanisms to ensure that only one task can access a specific resource or execute a critical piece of code at any given time. This process is called synchronization, and FreeRTOS provides powerful primitives for this purpose: Semaphores and Mutexes. This chapter introduces these fundamental synchronization tools, explaining their theory and demonstrating their practical application in ESP-IDF.

Theory

The Shared Resource Problem & Critical Sections

Any piece of code that accesses a resource shared between multiple tasks (or between a task and an ISR) is known as a critical section. Resources can include:

Resource Category Examples Potential Issues if Unprotected
Hardware Peripherals SPI bus, I2C bus, UART, GPIO pins (output mode), ADC Conflicting configurations, garbled data transmission/reception, device malfunction.
Memory Areas Global variables, static variables, shared data structures (e.g., arrays, structs), memory-mapped registers Data corruption, inconsistent state, overwritten values, race conditions.
Software Constructs Shared memory buffers, message queues (if not inherently thread-safe for complex operations), logging systems Buffer overflows/underflows, corrupted messages, interleaved log entries making debugging difficult.
System Resources Filesystem access (SD card), network sockets, system configuration parameters File corruption, inconsistent network state, incorrect system behavior.
Code Sections Non-reentrant functions, sequences of operations that must be atomic Unpredictable behavior if a task is preempted mid-sequence and another task calls the same non-reentrant code.

Race Condition Example: Imagine two tasks, Task_Logger and Task_Updater, both accessing a shared global structure system_status.

C
// Shared global structure (Simplified Example)
typedef struct {
    int active_users;
    float system_load;
} system_status_t;

system_status_t g_system_status = {0, 0.0};

// Task_Logger reads the status
void Task_Logger(void *pvParameters) {
    while(1) {
        // --- Start of Critical Section ---
        ESP_LOGI("Logger", "Users: %d, Load: %.2f",
                 g_system_status.active_users,
                 g_system_status.system_load);
        // --- End of Critical Section ---
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

// Task_Updater modifies the status
void Task_Updater(void *pvParameters) {
    while(1) {
        // Simulate updating status
        // --- Start of Critical Section ---
        g_system_status.active_users++;
        // *** PREEMPTION COULD HAPPEN HERE! ***
        vTaskDelay(pdMS_TO_TICKS(5)); // Simulate work
        g_system_status.system_load = (float)g_system_status.active_users * 1.5;
         // --- End of Critical Section ---

        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

If Task_Updater is preempted by Task_Logger after incrementing active_users but before updating system_load, Task_Logger will read an inconsistent state (active_users updated, but system_load based on the old value). This is a race condition.

To prevent this, we must ensure that the sequence of operations within the critical section (reading, modifying, writing back) is atomic with respect to other tasks trying to access the same resource. That is, once a task enters the critical section, it must be allowed to finish its operations on the shared resource before any other task can enter it. Semaphores and Mutexes provide mechanisms to enforce this mutual exclusion.

%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#f5f5f5', 'primaryTextColor': '#333', 'primaryBorderColor': '#999', 'lineColor': '#666', 'secondaryColor': '#f0f0f0', 'tertiaryColor': '#fff' }}}%%
sequenceDiagram
    participant A as Task A
    participant M as Memory (X = 5)
    participant B as Task B
    
    Note over M: Initial value: X = 5
    
    A->>+M: Read X = 5
    M-->>-A: Value: 5
    Note over A,M: Task A prepares to increment X to 6
    
    rect rgb(255, 235, 235)
      Note over A: Task A gets preempted
    end
    
    B->>+M: Read X = 5
    M-->>-B: Value: 5
    Note over B,M: Task B prepares to increment X to 6
    B->>+M: Write X = 6
    M-->>-B: Success
    Note over M: Current value: X = 6
    
    rect rgb(230, 255, 230)
      Note over A: Task A resumes
    end
    
    rect rgb(255, 200, 200)
      Note over A: Still has old value X = 5!
      A->>+M: Write X = 6
      M-->>-A: Success
      
      Note over M: Final value: X = 6 (Incorrect!)
      Note over M: Expected if no race condition: X = 7
    end

Semaphores

A Semaphore is a signaling mechanism. Conceptually, it’s like having a fixed number of permits or tokens.

  • Taking (or Pending/Waiting on) a Semaphore: A task tries to acquire a token.
    • If a token is available, the task takes one and continues execution.
    • If no tokens are available, the task enters the Blocked state until a token becomes available or a timeout expires.
  • Giving (or Posting/Signaling) a Semaphore: A task (or ISR) adds a token back, potentially unblocking a task waiting for one.

FreeRTOS provides two main types of semaphores:

1. Binary Semaphores

  • Concept: Has only two states – available (token count = 1) or unavailable (token count = 0). Think of it like a single key to a room.
  • Creation:SemaphoreHandle_t xSemaphoreCreateBinary( void );
    • Creates a binary semaphore. It is created in the ’empty’ or ‘unavailable’ state (token count 0). You must call xSemaphoreGive() once initially if you want it to start in the ‘available’ state.
  • Use Cases:
    • Signaling/Notification: One task or ISR signals an event to another task (e.g., “data ready”, “operation complete”). The receiving task waits (xSemaphoreTake) until the signal (xSemaphoreGive) arrives.
    • Basic Mutual Exclusion: Can be used to guard a critical section (take before entering, give after exiting). However, Mutexes are generally preferred for mutual exclusion due to features like priority inheritance.
  • Key Functions:
Function Description Key Parameters & Return
xSemaphoreCreateBinary() Creates a binary semaphore. Initially, the semaphore is unavailable (token count = 0). Call xSemaphoreGive() once to make it available if needed at start. Returns: SemaphoreHandle_t (handle to the created semaphore, or NULL if creation failed).
xSemaphoreTake(xSemaphore, xTicksToWait) Attempts to “take” or acquire the binary semaphore. If the semaphore is available (count = 1), it’s taken (count becomes 0), and the task continues. If unavailable (count = 0), the task blocks for up to xTicksToWait. xSemaphore: Handle of the semaphore.
xTicksToWait: Max time to block (e.g., portMAX_DELAY for indefinite wait).
Returns: pdPASS if taken, pdFAIL on timeout or error.
xSemaphoreGive(xSemaphore) “Gives” or releases the binary semaphore. If the semaphore was unavailable (count = 0), it becomes available (count = 1). If tasks are blocked waiting for it, one task is unblocked. xSemaphore: Handle of the semaphore.
Returns: pdPASS if successful. pdFAIL might occur if trying to give an already available binary semaphore (behavior can depend on FreeRTOS config configASSERT_DEFINED or specific checks).
vSemaphoreDelete(xSemaphore) Deletes a semaphore, freeing the memory it used. Only delete semaphores that are not currently in use (no tasks blocked on it). xSemaphore: Handle of the semaphore to delete.
%%{ init: { 'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans' } } }%%
graph TD;
    %% Node Styles
    classDef startNode fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6;
    classDef processNode fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;
    classDef decisionNode fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E;
    classDef endNode fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46;
    classDef eventNode fill:#FFF3E0,stroke:#FF9800,stroke-width:1px,color:#E65100; %% Custom for event trigger

    %% Define Nodes
    A[Task A: Receiver]:::startNode;
    B[Task B: Signaler / ISR_Handler]:::startNode;
    C{"Binary Semaphore (Sema)"};
    D["Task A calls <br>xSemaphoreTake(Sema, portMAX_DELAY)"]:::processNode;
    E{"Sema Available?"}:::decisionNode;
    F["Task A Blocked"]:::processNode;
    G["Task A Takes Sema (count=0)<br>Continues Execution"]:::endNode;
    H["Event Occurs<br>(e.g., Data Ready, Button Press)"]:::eventNode;
    I["Task B/ISR calls<br>xSemaphoreGive(Sema) or xSemaphoreGiveFromISR(Sema)"]:::processNode;
    J["Sema Becomes Available (count=1)"]:::processNode;
    K["Task A Unblocked"]:::processNode;
    
    %% Note for binary semaphore
    C_Note["*If created with xSemaphoreCreateBinary(),<br>it starts at 0 (unavailable).<br>A xSemaphoreGive() is needed to make it 1 (available)."]
    C --- C_Note

    %% Define Flo

    subgraph "Task A (Receiver)"
        A --> D;
        D --> E;
        E --"No (Count = 0)"--> F;
        F --> E; 
        E --"Yes (Count = 1)"--> G;
    end

    subgraph "Task B (Signaler) / ISR"
        B --> H;
        H --> I;
        I --> J;
    end

    J --> K;
    K --> G; 

    %% Apply Styles
    class A,B startNode;
    class D,F,I,J,K processNode;
    class E decisionNode;
    class G endNode;
    class H eventNode;

2. Counting Semaphores

  • Concept: Maintains a count between 0 and a specified maximum value. Think of managing a pool of identical resources (e.g., 3 available network connections).
  • Creation:SemaphoreHandle_t xSemaphoreCreateCounting( UBaseType_t uxMaxCount, UBaseType_t uxInitialCount );
    • uxMaxCount: The maximum value the semaphore’s count can reach. xSemaphoreGive will fail if the count is already at uxMaxCount.
    • uxInitialCount: The initial count when the semaphore is created.
  • Use Cases:
    • Resource Management: Control access to a finite number of identical resources. Tasks take before using a resource and give it back when done.
    • Event Counting: Track the occurrences of multiple events. A task gives the semaphore each time an event occurs, and another task takes the semaphore to process each event.
  • Key Functions:
Function Description Key Parameters & Return
xSemaphoreCreateCounting(uxMaxCount, uxInitialCount) Creates a counting semaphore. It maintains a count between 0 and uxMaxCount. uxMaxCount: The maximum count value the semaphore can hold. xSemaphoreGive will fail if the count is already at uxMaxCount.
uxInitialCount: The initial count value when the semaphore is created.
Returns: SemaphoreHandle_t (handle to the created semaphore, or NULL if creation failed).
xSemaphoreTake(xSemaphore, xTicksToWait) Attempts to “take” or acquire the semaphore. If the count is greater than 0, it decrements the count, and the task continues. If the count is 0, the task blocks for up to xTicksToWait. xSemaphore: Handle of the semaphore.
xTicksToWait: Max time to block (e.g., portMAX_DELAY for indefinite wait).
Returns: pdPASS if taken, pdFAIL on timeout or error.
xSemaphoreGive(xSemaphore) “Gives” or releases the semaphore. It increments the semaphore’s count, up to uxMaxCount. If tasks are blocked waiting for it, one task is unblocked. xSemaphore: Handle of the semaphore.
Returns: pdPASS if successful. pdFAIL if the semaphore count is already at uxMaxCount.
uxSemaphoreGetCount(xSemaphore) Returns the current count of the semaphore. Useful for debugging or informational purposes. Should not typically be used for synchronization logic itself. xSemaphore: Handle of the semaphore.
Returns: UBaseType_t (the current semaphore count).
vSemaphoreDelete(xSemaphore) Deletes a semaphore, freeing the memory it used. Only delete semaphores that are not currently in use (no tasks blocked on it). xSemaphore: Handle of the semaphore to delete.

%%{ init: { 'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans' } } }%%
sequenceDiagram
    participant Car1 as Car 1
    participant Car2 as Car 2
    participant Car3 as Car 3
    participant Car4 as Car 4
    participant Car5 as Car 5
    participant Car6 as Car 6
    participant ParkingLot as Parking Lot Semaphore (Spaces=5)
    
    Note over ParkingLot: Analogy: A parking lot with 5 spaces (MaxCount=5).<br>Cars (Tasks) arrive. If space available (Count>0),<br>take a space (Take, decrement count). If full (Count=0),<br>wait. When car leaves, give back space (Give, increment count).
    
    Note over ParkingLot: InitialCount = 5<br>CurrentCount = 5
    
    Car1->>+ParkingLot: xSemaphoreTake() [Car arrives]
    ParkingLot-->>-Car1: Success (Spaces left = 4)
    Note over Car1: Parked in lot
    
    Car2->>+ParkingLot: xSemaphoreTake() [Car arrives]
    ParkingLot-->>-Car2: Success (Spaces left = 3)
    Note over Car2: Parked in lot
    
    Car3->>+ParkingLot: xSemaphoreTake() [Car arrives]
    ParkingLot-->>-Car3: Success (Spaces left = 2)
    Note over Car3: Parked in lot
    
    Car4->>+ParkingLot: xSemaphoreTake() [Car arrives]
    ParkingLot-->>-Car4: Success (Spaces left = 1)
    Note over Car4: Parked in lot
    
    Car5->>+ParkingLot: xSemaphoreTake() [Car arrives]
    ParkingLot-->>-Car5: Success (Spaces left = 0)
    Note over Car5: Parked in lot - Lot now FULL
    
    Car6->>+ParkingLot: xSemaphoreTake() [Car arrives]
    Note over Car6,ParkingLot: Waiting (No spaces available)
    
    Car2->>+ParkingLot: xSemaphoreGive() [Car leaves]
    ParkingLot-->>-Car2: Released (Spaces left = 1)
    
    ParkingLot-->>Car6: Unblocked (Space now available)
    Car6-->>Car6: Takes available space
    Note over Car6: Parked in lot (Spaces left = 0)

Mutexes (Mutual Exclusion Semaphores)

While binary semaphores can be used for mutual exclusion, FreeRTOS provides Mutexes, which are specifically designed for this purpose and offer crucial advantages.

  • Concept: A Mutex is like a special binary semaphore used exclusively to protect shared resources. Only one task can “hold” or “own” the mutex at a time.
  • Ownership: Unlike semaphores, a mutex has an owner. Only the task that successfully took the mutex can give it back. This prevents accidental release by another task.
  • Priority Inheritance: This is a key feature. If a high-priority task (Task H) blocks trying to take a mutex held by a low-priority task (Task L), the scheduler can temporarily boost Task L’s priority to that of Task H. This allows Task L to finish its critical section and release the mutex faster, preventing Task H from being blocked for an excessive amount of time by Task L (and potentially medium-priority tasks preempting Task L). This mechanism helps mitigate Priority Inversion.
  • Creation:
    • SemaphoreHandle_t xSemaphoreCreateMutex( void );
      • Creates a standard mutex. It is created in the ‘available’ state.
    • SemaphoreHandle_t xSemaphoreCreateRecursiveMutex( void );
      • Creates a recursive mutex. The same task can successfully ‘take’ a recursive mutex multiple times. It must be ‘given’ back the same number of times before it becomes available to other tasks. Use recursive mutexes sparingly, as they can sometimes indicate complex or potentially flawed resource locking logic.
  • Use Case: The primary mechanism for protecting critical sections accessing shared data or resources.
  • Key Functions:
Function Description Key Parameters & Return
xSemaphoreCreateMutex() Creates a standard (non-recursive) mutex. It is created in the ‘available’ state (unlocked). Returns: SemaphoreHandle_t (handle to the created mutex, or NULL if creation failed).
xSemaphoreCreateRecursiveMutex() Creates a recursive mutex. The same task can ‘take’ a recursive mutex multiple times. It must be ‘given’ back the same number of times before it becomes available to other tasks. Returns: SemaphoreHandle_t (handle to the created recursive mutex, or NULL if creation failed).
Standard Mutex Functions
xSemaphoreTake(xMutex, xTicksToWait) Attempts to take ownership of the standard mutex. Blocks if unavailable for up to xTicksToWait. Only one task can hold the mutex at a time. A task cannot take a standard mutex it already holds (will deadlock or error). xMutex: Handle of the mutex.
xTicksToWait: Max time to block.
Returns: pdPASS if taken, pdFAIL on timeout/error.
xSemaphoreGive(xMutex) Releases ownership of the standard mutex. Must only be called by the task that currently holds the mutex. xMutex: Handle of the mutex.
Returns: pdPASS if successful, pdFAIL if called by a task that doesn’t own it or other error.
Recursive Mutex Functions
xSemaphoreTakeRecursive(xMutex, xTicksToWait) Attempts to take ownership of the recursive mutex. Blocks if unavailable (held by another task) for up to xTicksToWait. The calling task can take it multiple times if it already owns it, incrementing an internal counter. xMutex: Handle of the recursive mutex.
xTicksToWait: Max time to block.
Returns: pdPASS if taken, pdFAIL on timeout/error.
xSemaphoreGiveRecursive(xMutex) Releases ownership of the recursive mutex. Decrements the internal counter. The mutex only becomes available to other tasks when the count returns to zero (i.e., it has been given as many times as it was taken by the owning task). Must only be called by the owning task. xMutex: Handle of the recursive mutex.
Returns: pdPASS if successful, pdFAIL if called by a task that doesn’t own it or other error.
vSemaphoreDelete(xMutex) Deletes a mutex (standard or recursive), freeing its memory. Only delete mutexes that are not currently in use. xMutex: Handle of the mutex to delete.

Talking Stick Analogy:

%%{ init: { 
  'theme': 'base', 
  'themeVariables': { 
    'fontFamily': 'Open Sans',
    'primaryColor': '#6B46C1',
    'primaryTextColor': '#0A0A0A',
    'primaryBorderColor': '#4C1D95',
    'lineColor': '#6B46C1',
    'secondaryColor': '#F0F4F8',
    'tertiaryColor': '#EEEEF3'
  }
} }%%

sequenceDiagram
    participant Person1 as Person 1
    participant Person2 as Person 2
    participant Person3 as Person 3
    participant TalkingStick as Mutex (Talking Stick)
    
    rect rgb(226, 232, 240)
    Note over TalkingStick: Mutex Analogy: A single "talking stick".<br>Only the task holding the stick can talk (access the resource).<br>The stick must be passed back (given) before<br>another task can take it.
    end
    
    Note over TalkingStick: Available = true<br>(Stick is available)
    
    Person1->>+TalkingStick: xSemaphoreTake() [Request stick]
    TalkingStick-->>-Person1: Success (Stick granted)
    rect rgb(209, 250, 229)
    Note over Person1: Holds stick<br>Allowed to talk/access resource
    end
    
    Person2->>+TalkingStick: xSemaphoreTake() [Request stick]
    rect rgb(254, 226, 226)
    Note over Person2,TalkingStick: Blocked (Stick not available)
    end
    
    Person3->>+TalkingStick: xSemaphoreTake() [Request stick]
    rect rgb(254, 226, 226)
    Note over Person3,TalkingStick: Blocked (Stick not available)
    end
    rect rgb(254, 243, 199)
    Note right of TalkingStick: Multiple tasks can be<br>waiting for the mutex
    end
    
    Person1->>+TalkingStick: xSemaphoreGive() [Return stick]
    TalkingStick-->>-Person1: Released (Stick available)
    rect rgb(226, 232, 240)
    Note over Person1: No longer allowed to talk
    end
    
    TalkingStick-->>Person2: Unblocked (Stick available)
    rect rgb(209, 250, 229)
    Note over Person2: Holds stick<br>Allowed to talk/access resource
    end
    
    Person2->>+TalkingStick: xSemaphoreGive() [Return stick]
    TalkingStick-->>-Person2: Released (Stick available)
    
    TalkingStick-->>Person3: Unblocked (Stick available)
    rect rgb(209, 250, 229)
    Note over Person3: Holds stick<br>Allowed to talk/access resource
    end
    
    Person3->>+TalkingStick: xSemaphoreGive() [Return stick]
    TalkingStick-->>-Person3: Released (Stick available)

Priority Inversion Problem:

%%{ init: { 
  'theme': 'base', 
  'themeVariables': { 
    'fontFamily': 'Open Sans',
    'primaryColor': '#CDCFCF',
    'primaryTextColor': '#0A0A0A',
    'primaryBorderColor': '#4C1D95',
    'lineColor': '#6B46C1',
    'secondaryColor': '#F0F4F8',
    'tertiaryColor': '#EEEEF3'
  }
} }%%

sequenceDiagram
    participant TaskH as Task H (High Priority)
    participant TaskM as Task M (Medium Priority)
    participant TaskL as Task L (Low Priority)
    participant Mutex as Mutex (Resource)
    participant Scheduler as OS Scheduler
    
    rect rgb(226, 232, 240)
    Note over TaskH,Scheduler: Priority Inversion Problem:<br>Task L holds mutex. Task H preempts other tasks but blocks waiting for mutex.<br>Task M (medium priority) runs, preventing Task L from running and releasing the mutex.<br>Task H remains blocked indefinitely.
    end
    
    Note over TaskH: Not running
    Note over TaskM: Not running
    
    rect rgb(209, 250, 229)
    Note over TaskL: Running (Low Priority)
    end
    
    TaskL->>+Mutex: xSemaphoreTake() [Acquires mutex]
    Mutex-->>-TaskL: Success (Mutex granted)
    rect rgb(209, 250, 229)
    Note over TaskL: Running with mutex
    end
    
    rect rgb(254, 243, 199)
    Note over TaskH: Task H becomes ready
    end
    
    Scheduler->>TaskL: Preempt (Higher priority task ready)
    Scheduler->>TaskH: Schedule (Highest priority)
    
    rect rgb(209, 250, 229)
    Note over TaskH: Running (High Priority)
    end
    Note over TaskL: Preempted
    
    TaskH->>+Mutex: xSemaphoreTake() [Requests mutex]
    rect rgb(254, 226, 226)
    Note over TaskH,Mutex: Blocked (Mutex held by Task L)
    end
    
    Scheduler->>TaskM: Schedule (Highest ready priority)
    rect rgb(209, 250, 229)
    Note over TaskM: Running (Medium Priority)
    end
    Note over TaskH: Blocked
    Note over TaskL: Preempted (Can't run to release mutex)
    
    rect rgb(254, 226, 226)
    Note over TaskH,TaskL: PRIORITY INVERSION: Low priority task<br>holding resource needed by high priority task,<br>but medium priority task is running instead
    end
    
    rect rgb(254, 243, 199)
    Note over TaskH,Scheduler: Priority Inheritance Solution
    end
    
    Mutex->>TaskL: Inherit priority from Task H
    rect rgb(216, 180, 254)
    Note over TaskL: Priority temporarily<br>boosted to HIGH
    end
    
    Scheduler->>TaskM: Preempt (Higher priority task ready)
    Scheduler->>TaskL: Schedule (Now has highest priority)
    
    rect rgb(209, 250, 229)
    Note over TaskL: Running with INHERITED<br>High Priority
    end
    Note over TaskH: Blocked
    Note over TaskM: Preempted
    
    TaskL->>+Mutex: xSemaphoreGive() [Releases mutex]
    Mutex-->>-TaskL: Released (Mutex available)
    
    rect rgb(226, 232, 240)
    Note over TaskL: Priority returns to LOW
    end
    
    Mutex-->>TaskH: Unblocked (Mutex available)
    Scheduler->>TaskL: Preempt (Lower priority)
    Scheduler->>TaskH: Schedule (Highest priority)
    
    rect rgb(209, 250, 229)
    Note over TaskH: Running (High Priority)<br>with mutex
    end
    Note over TaskM: Preempted
    Note over TaskL: Preempted

Semaphore/Mutex Usage from ISRs

Interrupt Service Routines (ISRs) cannot block. Therefore, you cannot use xSemaphoreTake or mutex-specific functions from an ISR. However, ISRs often need to signal tasks (e.g., data received via UART interrupt).

Function Applicable To Description Key Parameters & Return
xSemaphoreGiveFromISR(xSemaphore, pxHigherPriorityTaskWoken) Binary Semaphores, Counting Semaphores Gives (releases) a semaphore from an Interrupt Service Routine (ISR). This is the primary way for an ISR to signal a task. xSemaphore: Handle of the semaphore.
pxHigherPriorityTaskWoken: Pointer to a BaseType_t. Set to pdTRUE by the function if giving the semaphore unblocked a task with higher priority than the interrupted task. This signals that a context switch might be needed at the end of the ISR.
Returns: pdPASS if successful, pdFAIL otherwise (e.g., trying to give to a full counting semaphore).
Important Note: Standard xSemaphoreTake(), xSemaphoreTakeRecursive(), xSemaphoreGive() (for mutexes), and xSemaphoreGiveRecursive() must NOT be called from an ISR because they can block. Mutexes, in general, are not designed for direct ISR manipulation.
portYIELD_FROM_ISR(xHigherPriorityTaskWoken)
or taskYIELD_FROM_ISR(xHigherPriorityTaskWoken) (older API)
ISR Context If pxHigherPriorityTaskWoken was set to pdTRUE by an ISR-safe FreeRTOS function (like xSemaphoreGiveFromISR), this macro should be called at the end of the ISR to request a context switch. xHigherPriorityTaskWoken: The variable that was passed to the ISR-safe FreeRTOS function.
  • Giving Semaphores from ISRs: Use xSemaphoreGiveFromISR().
    • BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken );
      • Gives a semaphore (binary or counting).
      • pxHigherPriorityTaskWoken: A pointer to a BaseType_t variable. The function sets this variable to pdTRUE if giving the semaphore unblocked a task with a higher priority than the currently running task. This is used to request a context switch after the ISR completes.
  • Taking Semaphores from ISRs: Generally not done, as it implies blocking. Use alternative patterns if an ISR needs data protected by a semaphore/mutex (e.g., ISR puts data in a queue, task processes it later).

Choosing Between Semaphores and Mutexes

  • Use a Mutex: Primarily for mutual exclusion – protecting shared data or resources from concurrent access by multiple tasks. Always prefer mutexes over binary semaphores for this due to ownership tracking and priority inheritance.
  • Use a Binary Semaphore: Primarily for signaling or synchronization between tasks or between an ISR and a task (e.g., event notification).
  • Use a Counting Semaphore: For managing access to a pool of multiple identical resources or for counting events.
Feature Semaphore (Binary/Counting) Mutex (Mutual Exclusion Semaphore)
Primary Purpose
  • Signaling/Notification: ISR to task, task to task (e.g., event occurred, data ready). (Binary)
  • Basic Synchronization: Coordinating task execution order. (Binary)
  • Resource Pool Management: Controlling access to a finite number of identical resources. (Counting)
  • Event Counting: Tracking multiple event occurrences. (Counting)
  • Mutual Exclusion: Protecting shared resources (data, peripherals) from concurrent access to prevent race conditions and ensure data integrity. This is its main and highly specialized role.
Ownership No concept of ownership. Any task that knows the semaphore handle can xSemaphoreTake() or xSemaphoreGive() (if conditions allow, e.g., count < max for give). Has an owner. Only the task that successfully took (locked) the mutex can give (unlock) it. This prevents accidental release by another task.
Priority Inversion Handling No built-in mechanism like priority inheritance. If a binary semaphore is used for mutual exclusion, it’s susceptible to priority inversion. Priority Inheritance: Yes. If a high-priority task blocks on a mutex held by a low-priority task, the low-priority task’s priority is temporarily boosted to that of the high-priority task. This helps mitigate priority inversion.
Recursive Taking Not applicable. Taking a binary semaphore that is already taken (by the same or different task) will cause the task to block (or fail if timeout is 0). Standard mutexes (xSemaphoreCreateMutex) cannot be taken recursively by the same task (leads to deadlock).
Recursive mutexes (xSemaphoreCreateRecursiveMutex) allow the owning task to take the same mutex multiple times. It must be given back an equal number of times.
Typical Creation & Initial State Binary: xSemaphoreCreateBinary() – created empty/unavailable (count 0). Needs an initial xSemaphoreGive() to become available.
Counting: xSemaphoreCreateCounting(max, initial) – count set by initial.
xSemaphoreCreateMutex() / xSemaphoreCreateRecursiveMutex() – created available/unlocked.
Use from ISR Can be given from an ISR using xSemaphoreGiveFromISR(). Cannot be taken from an ISR. Generally not used directly from ISRs. Mutex operations (Take/Give) are blocking and rely on task context. ISRs should signal tasks, which then might interact with mutexes.
When to Choose
  • For signaling events between tasks or ISR-to-task.
  • To guard a fixed number of identical resources (counting).
  • When simple task synchronization is needed without shared data protection complexities.
  • Always for protecting shared data or resources to prevent race conditions (critical sections).
  • When priority inversion is a concern and priority inheritance is beneficial.

Practical Examples

Let’s implement these concepts in ESP-IDF.

Example 1: Binary Semaphore for Task Notification

An ISR signals a task when a simulated event (like a button press) occurs.

Code:

C
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h" // Include semaphore header
#include "esp_log.h"
#include "driver/gpio.h" // For ISR example (simulated)

static const char *TAG = "SEMA_BINARY";

// Declare a handle for the binary semaphore
static SemaphoreHandle_t binary_sema = NULL;

// Task that waits for the semaphore signal
void worker_task(void *pvParameter)
{
    ESP_LOGI(TAG, "Worker Task started. Waiting for signal...");

    while(1) {
        // Wait indefinitely (portMAX_DELAY) to take the semaphore
        if (xSemaphoreTake(binary_sema, portMAX_DELAY) == pdPASS) {
            // Semaphore received! Process the event.
            ESP_LOGW(TAG, "Worker Task: Signal received! Processing event...");
            // Simulate processing time
            vTaskDelay(pdMS_TO_TICKS(500));
            ESP_LOGI(TAG, "Worker Task: Event processing finished.");
        } else {
            // This should not happen with portMAX_DELAY unless semaphore deleted
            ESP_LOGE(TAG, "Worker Task: Failed to take semaphore!");
        }
    }
}

// Simulate an ISR giving the semaphore periodically
// In a real scenario, this would be attached to a hardware interrupt (e.g., GPIO)
void simulated_isr_task(void *pvParameter) {
    ESP_LOGI(TAG, "Simulated ISR Task started.");
    while(1) {
        // Simulate an event occurring every 3 seconds
        vTaskDelay(pdMS_TO_TICKS(3000));

        ESP_LOGI(TAG, "Simulated ISR: Event occurred! Giving semaphore.");

        // --- This is how you would give from a REAL ISR ---
        // BaseType_t xHigherPriorityTaskWoken = pdFALSE;
        // xSemaphoreGiveFromISR(binary_sema, &xHigherPriorityTaskWoken);
        // if (xHigherPriorityTaskWoken == pdTRUE) {
        //    portYIELD_FROM_ISR(); // Request context switch if needed
        // }
        // --- End of real ISR code ---

        // --- For simulation purposes, just give from a task ---
         BaseType_t result = xSemaphoreGive(binary_sema);
         if (result != pdPASS) {
             // This might happen if worker task hasn't taken the previous signal yet
             ESP_LOGW(TAG, "Simulated ISR: Semaphore could not be given (already available?)");
         }
         // --- End of simulation code ---

    }
}


void app_main(void)
{
    ESP_LOGI(TAG, "App Main: Starting Binary Semaphore Example.");

    // Create the binary semaphore. It starts empty.
    binary_sema = xSemaphoreCreateBinary();

    if (binary_sema == NULL) {
        ESP_LOGE(TAG, "Failed to create binary semaphore.");
        return; // Handle error
    }
    ESP_LOGI(TAG, "Binary semaphore created successfully.");

    // Create the worker task and the simulated ISR task
    xTaskCreate(worker_task, "Worker", 2048, NULL, 5, NULL);
    xTaskCreate(simulated_isr_task, "SimISR", 2048, NULL, 6, NULL); // Higher priority

    ESP_LOGI(TAG, "App Main: Tasks created.");
}

Build, Flash, Monitor:

  1. Save the code (e.g., main/sema_binary_example.c).
  2. Ensure your main/CMakeLists.txt includes this source file.
  3. Use the VS Code ESP-IDF Extension: Build, Flash, and Monitor the project.

Observe:

  • The “Worker Task” starts and immediately blocks (“Waiting for signal…”).
  • Every 3 seconds, the “Simulated ISR Task” logs “Event occurred! Giving semaphore.”
  • Immediately after the semaphore is given, the “Worker Task” unblocks, logs “Signal received!”, simulates work, and then waits again.
  • This demonstrates the basic signaling pattern using a binary semaphore.

Example 2: Mutex for Protecting Shared Data

Two tasks increment a shared counter, protected by a mutex.

Code:

C
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h" // Include semaphore header for mutexes
#include "esp_log.h"

static const char *TAG = "MUTEX_SHARED";

// Shared resource (a simple counter)
static int g_shared_counter = 0;

// Declare a handle for the mutex
static SemaphoreHandle_t mutex_handle = NULL;

// Task that increments the shared counter
void updating_task(void *pvParameter)
{
    int task_num = (int)pvParameter; // Get task number passed as argument
    ESP_LOGI(TAG, "Task %d started.", task_num);

    while(1) {
        ESP_LOGI(TAG, "Task %d: Attempting to take mutex...", task_num);

        // Attempt to take the mutex, wait indefinitely if necessary
        if (xSemaphoreTake(mutex_handle, portMAX_DELAY) == pdPASS) {
            // --- Critical Section Start ---
            ESP_LOGW(TAG, "Task %d: Mutex acquired! Accessing shared resource.");

            // Read the shared counter
            int current_value = g_shared_counter;
            // Simulate some processing time while holding the mutex
            vTaskDelay(pdMS_TO_TICKS(50 + (task_num * 20))); // Different delays
            // Increment the counter
            current_value++;
            g_shared_counter = current_value;

            ESP_LOGW(TAG, "Task %d: Updated counter to %d. Releasing mutex.", task_num, g_shared_counter);
            // --- Critical Section End ---

            // Release the mutex - MUST be done!
            if (xSemaphoreGive(mutex_handle) != pdPASS) {
                 ESP_LOGE(TAG, "Task %d: Failed to give mutex!", task_num);
                 // This shouldn't happen if logic is correct
            }
             ESP_LOGI(TAG, "Task %d: Mutex released.", task_num);

        } else {
            // Should not happen with portMAX_DELAY unless mutex deleted
            ESP_LOGE(TAG, "Task %d: Failed to take mutex!", task_num);
        }

        // Wait before trying to access the resource again
        vTaskDelay(pdMS_TO_TICKS(500 + (task_num * 100)));
    }
}


void app_main(void)
{
    ESP_LOGI(TAG, "App Main: Starting Mutex Example.");

    // Create the mutex. Mutexes are created available.
    mutex_handle = xSemaphoreCreateMutex();

    if (mutex_handle == NULL) {
        ESP_LOGE(TAG, "Failed to create mutex.");
        return; // Handle error
    }
    ESP_LOGI(TAG, "Mutex created successfully.");

    // Create two tasks, passing a unique number to each
    // Use slightly different priorities to observe potential blocking
    xTaskCreate(updating_task, "Updater1", 2048, (void *)1, 5, NULL);
    xTaskCreate(updating_task, "Updater2", 2048, (void *)2, 6, NULL); // Higher priority

    ESP_LOGI(TAG, "App Main: Tasks created.");
}

Build, Flash, Monitor:

  1. Save the code (e.g., main/mutex_example.c).
  2. Update main/CMakeLists.txt.
  3. Build, Flash, and Monitor.

Observe:

  • Both tasks start and try to acquire the mutex.
  • Only one task can acquire the mutex at a time (“Mutex acquired!”). The other task will block if it tries to take the mutex while it’s held.
  • The task holding the mutex performs its critical section (read, delay, increment, log) and then releases the mutex (“Mutex released.”).
  • The other task can then acquire the mutex.
  • The shared counter g_shared_counter increments correctly, even though access is interleaved between tasks, because the mutex ensures only one task modifies it at a time.
  • You might see the higher-priority task (Task 2) preempting Task 1, but if Task 1 holds the mutex, Task 2 will block until Task 1 releases it (demonstrating mutual exclusion).

Example 3: Counting Semaphore for Resource Pool Management

Simulate managing a pool of 2 “printer” resources. Tasks must acquire a semaphore before “printing”.

Code:

C
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "esp_log.h"
#include "esp_random.h"

static const char *TAG = "SEMA_COUNTING";

#define MAX_PRINTERS 2 // Maximum number of available printers

// Declare a handle for the counting semaphore
static SemaphoreHandle_t printer_semaphore = NULL;

// Task that simulates wanting to use a printer
void print_job_task(void *pvParameter)
{
    int task_num = (int)pvParameter;
    ESP_LOGI(TAG, "Print Job Task %d started.", task_num);

    while(1) {
        ESP_LOGI(TAG, "Task %d: Wants to print, waiting for available printer...", task_num);

        // Wait indefinitely to acquire a printer 'token'
        if (xSemaphoreTake(printer_semaphore, portMAX_DELAY) == pdPASS) {
            // Got access to a printer
            ESP_LOGW(TAG, "Task %d: Acquired printer resource (Semaphore count: %u). Printing...",
                     task_num, uxSemaphoreGetCount(printer_semaphore)); // Get current count (for info)

            // Simulate printing time
            uint32_t print_time = 500 + (esp_random() % 1500); // Random time 0.5s - 2s
            vTaskDelay(pdMS_TO_TICKS(print_time));

            ESP_LOGW(TAG, "Task %d: Finished printing (%u ms). Releasing printer resource.", task_num, print_time);

            // Release the printer 'token' back to the pool
            if (xSemaphoreGive(printer_semaphore) != pdPASS) {
                 ESP_LOGE(TAG, "Task %d: Failed to give semaphore!", task_num);
                 // Should not happen if count < MAX_PRINTERS
            }
             ESP_LOGI(TAG, "Task %d: Printer resource released (Semaphore count: %u).",
                      task_num, uxSemaphoreGetCount(printer_semaphore) + 1); // Count *after* giving

        } else {
             ESP_LOGE(TAG, "Task %d: Failed to take semaphore!", task_num);
        }

        // Wait a random time before needing to print again
        vTaskDelay(pdMS_TO_TICKS(1000 + (esp_random() % 2000)));
    }
}

void app_main(void)
{
    ESP_LOGI(TAG, "App Main: Starting Counting Semaphore Example.");

    // Create the counting semaphore: Max count = 2, Initial count = 2
    printer_semaphore = xSemaphoreCreateCounting(MAX_PRINTERS, MAX_PRINTERS);

    if (printer_semaphore == NULL) {
        ESP_LOGE(TAG, "Failed to create counting semaphore.");
        return; // Handle error
    }
    ESP_LOGI(TAG, "Counting semaphore created (Max: %d, Initial: %d).", MAX_PRINTERS, MAX_PRINTERS);

    // Create multiple tasks that want to print (more tasks than printers)
    xTaskCreate(print_job_task, "PrintJob1", 2048, (void *)1, 5, NULL);
    xTaskCreate(print_job_task, "PrintJob2", 2048, (void *)2, 5, NULL);
    xTaskCreate(print_job_task, "PrintJob3", 2048, (void *)3, 5, NULL);
    xTaskCreate(print_job_task, "PrintJob4", 2048, (void *)4, 5, NULL);

    ESP_LOGI(TAG, "App Main: Tasks created.");
}

Build, Flash, Monitor:

  1. Save the code (e.g., main/sema_counting_example.c).
  2. Update main/CMakeLists.txt.
  3. Build, Flash, and Monitor.

Observe:

  • Four tasks are created, but only two “printer” resources are available (semaphore count starts at 2).
  • The first two tasks that successfully xSemaphoreTake will acquire a printer and start “printing”. The semaphore count drops to 0.
  • The other two tasks will block when they try to xSemaphoreTake, waiting for a printer to become free.
  • As tasks finish printing and call xSemaphoreGive, the semaphore count increases, allowing blocked tasks to acquire a resource and run.
  • At most, two tasks will be “printing” simultaneously, demonstrating how the counting semaphore limits access to the shared resource pool. You can check the semaphore count using uxSemaphoreGetCount() (mainly for debugging).

Variant Notes

  • API Consistency: The FreeRTOS Semaphore and Mutex APIs (xSemaphoreCreate..., xSemaphoreTake..., xSemaphoreGive..., vSemaphoreDelete) are part of the core FreeRTOS kernel and behave identically across all ESP32 variants supported by ESP-IDF v5.x (ESP32, S2, S3, C3, C6, H2).
  • Performance: The underlying context switching time and instruction execution speed may differ slightly between Xtensa and RISC-V cores and different clock speeds, but the functional behavior of semaphores and mutexes remains the same. Taking or giving a semaphore/mutex involves minimal overhead beyond potential task rescheduling.
  • Priority Inheritance: Priority inheritance for mutexes is typically enabled by default in the ESP-IDF FreeRTOS configuration (configUSE_MUTEXES and configUSE_PRIORITY_INHERITANCE set to 1).

Common Mistakes & Troubleshooting Tips

Mistake / Symptom Detailed Explanation & Impact Troubleshooting Tips & Prevention
Forgetting to Give Back Semaphore/Mutex
Symptom:
Tasks block indefinitely; resource permanently unavailable; potential deadlock if task tries to re-take a non-recursive mutex.
If a task acquires a semaphore/mutex but doesn’t release it (due to early return, error, or oversight), the resource it protects becomes inaccessible to other tasks. For mutexes, if the owning task tries to take it again (and it’s not recursive), it will deadlock itself.
  • Code Review: Ensure every xSemaphoreTake (or equivalent) has a corresponding xSemaphoreGive on ALL possible execution paths within the critical section.
  • Careful Error Handling: If a critical section can exit early due to an error, ensure the semaphore/mutex is released in an error handling block or a `finally`-like structure (e.g., using `goto` for cleanup in C).
  • Debugging: Use uxSemaphoreGetCount() for counting semaphores. For mutexes, FreeRTOS offers debugging features to see the mutex holder (if configASSERT_DEFINED or trace features are enabled).
  • RAII (C++): If using C++, leverage Resource Acquisition Is Initialization (RAII) wrappers that automatically release the mutex/semaphore when the wrapper object goes out of scope.
Using Semaphore for Mutual Exclusion (instead of Mutex)
Symptom:
Code might seem to work under light load but can suffer from prolonged priority inversion; lacks ownership tracking.
While a binary semaphore *can* technically provide mutual exclusion, it lacks crucial features of a mutex, primarily priority inheritance. This makes the system vulnerable to priority inversion where a high-priority task is starved because a low-priority task holding the semaphore is preempted by medium-priority tasks.
  • Use Correct Primitive: Always use mutexes (xSemaphoreCreateMutex()) for mutual exclusion to protect shared resources or critical sections.
  • Reserve Semaphores: Use binary semaphores primarily for signaling/notification and counting semaphores for resource pool management or event counting.
Deadlock
Symptom:
Involved tasks (and potentially others waiting on them) freeze completely.
  • Simple Deadlock: A task tries to take a non-recursive mutex it already holds.
  • Complex/Circular Deadlock: Task A takes Mutex 1, then tries for Mutex 2. Task B takes Mutex 2, then tries for Mutex 1. Both are blocked waiting for the other.
  • Recursive Mutexes: If a task genuinely needs to re-acquire a mutex it holds, use a recursive mutex (xSemaphoreCreateRecursiveMutex()). However, reconsider the design first, as this can sometimes indicate overly complex locking.
  • Mutex Ordering: Establish a strict, system-wide ordering for acquiring multiple mutexes. If tasks need Mutex A and Mutex B, they must *always* acquire them in the same order (e.g., A then B). This prevents circular dependencies.
  • Keep Critical Sections Short: Minimize the time a mutex is held.
  • Avoid Blocking Calls in Critical Sections: Don’t call functions within a critical section that might themselves try to acquire other mutexes in a conflicting order or block for long periods.
  • Timeout on Take: Consider using timeouts with xSemaphoreTake() to detect and recover from potential deadlocks, though this doesn’t solve the underlying issue.
Calling Blocking Functions from ISRs
Symptom:
System crash, assertion failure (if enabled), or unpredictable behavior.
Interrupt Service Routines (ISRs) must execute as quickly as possible and must NEVER block. Functions like xSemaphoreTake() (for any semaphore/mutex type) or mutex-specific xSemaphoreGive() can cause the calling context to block, which is catastrophic in an ISR.
  • Use ISR-Safe Versions: For giving semaphores from an ISR, always use xSemaphoreGiveFromISR().
  • Deferred Processing: If an ISR needs to trigger an action that requires mutex protection or might block, the ISR should do minimal work (e.g., read data, acknowledge interrupt) and then signal a regular task (using xSemaphoreGiveFromISR() or xQueueSendFromISR()). The task then performs the blocking operations or acquires the mutex.
  • No Mutex Take/Give from ISR: Mutexes are not designed for direct manipulation (take/give) from ISRs.
Incorrect pxHigherPriorityTaskWoken Handling
Symptom:
A high-priority task unblocked by an ISR might not run immediately, introducing unnecessary latency. The unblocked task might only run after the next scheduler tick or when the interrupted lower-priority task blocks.
When an ISR-safe function like xSemaphoreGiveFromISR() unblocks a task, it can optionally set a variable (pxHigherPriorityTaskWoken) to pdTRUE if the unblocked task has a higher priority than the task that was interrupted. If this flag is set, a context switch should be explicitly requested at the end of the ISR.
  • Always Check and Yield:
    1. Inside the ISR, declare BaseType_t xHigherPriorityTaskWoken = pdFALSE;.
    2. Pass the address &xHigherPriorityTaskWoken to the ISR-safe FreeRTOS function (e.g., xSemaphoreGiveFromISR(sema, &xHigherPriorityTaskWoken);).
    3. At the very end of the ISR, call portYIELD_FROM_ISR(xHigherPriorityTaskWoken); (or taskYIELD_FROM_ISR(xHigherPriorityTaskWoken); for older ESP-IDF versions). This macro will request a context switch only if xHigherPriorityTaskWoken is true.
  • Pass Valid Pointer: Do not pass NULL for pxHigherPriorityTaskWoken if you care about immediate rescheduling.

Exercises

  1. Shared Resource Logger:
    • Create a global structure holding several data fields (e.g., int id, float value, char status[10]).
    • Create two tasks:
      • Task Writer: Periodically (e.g., every second) modifies all fields of the global structure with new data. It must protect access using a mutex.
      • Task Reader: Periodically (e.g., every 750ms) reads all fields from the global structure and prints them to the console. It must also use the same mutex to ensure it reads a consistent state (not halfway through an update by Task Writer).
    • Observe the output to verify that the Reader task always prints consistent data sets, even though the Writer is modifying them concurrently.
  2. Debounced Button Press Counter:
    • Configure a GPIO pin as an input with a pull-up resistor, connected to a physical button (connecting the pin to GND when pressed).
    • Set up a GPIO ISR that triggers on the falling edge (button press).
    • Inside the ISR, do not process the button directly. Instead, use xSemaphoreGiveFromISR to give a binary semaphore. Remember to handle pxHigherPriorityTaskWoken and potentially yield.
    • Create a Button Handler Task that waits (xSemaphoreTake) for this semaphore.
    • When the semaphore is received, the Button Handler Task should:
      • Implement a simple software debounce: Record the time, wait for a short period (e.g., 50ms), and check if the GPIO pin is still low.
      • If it’s still low after the debounce delay, increment a global counter (protected by a mutex if you plan to access this counter from other tasks, though for just this task, it might be okay without if only this task writes).
      • Print the current button press count.
    • This separates the immediate ISR action (signaling) from the potentially longer processing (debouncing, counting) done in a task context.

Summary

  • Critical Sections: Code segments accessing shared resources require protection to prevent race conditions.
  • Race Conditions: Occur when the outcome of operations depends on the unpredictable timing of task interleaving, often leading to corrupted data.
  • Semaphores: Signaling mechanisms.
    • Binary: 0 or 1. Used for task notification/signaling or basic synchronization. Created with xSemaphoreCreateBinary.
    • Counting: 0 to MaxCount. Used for managing pools of resources or counting events. Created with xSemaphoreCreateCounting.
  • Mutexes: Specialized binary semaphores for mutual exclusion (protecting shared resources).
    • Provide Priority Inheritance to mitigate Priority Inversion.
    • Have ownership: Only the task that takes can give.
    • Created with xSemaphoreCreateMutex or xSemaphoreCreateRecursiveMutex.
  • Key Operations:
    • xSemaphoreTake / xSemaphoreTakeRecursive: Acquire semaphore/mutex (potentially block).
    • xSemaphoreGive / xSemaphoreGiveRecursive: Release semaphore/mutex.
    • xSemaphoreGiveFromISR: Release semaphore from an ISR (use with pxHigherPriorityTaskWoken).
  • Usage: Use Mutexes for protecting shared data/resources. Use Semaphores for signaling and resource pool management.
  • ISR Context: Never call blocking functions (Take) from ISRs. Use ...FromISR versions for signaling.
  • Pitfalls: Deadlock, forgetting to release resources, using semaphores where mutexes are needed, calling blocking functions from ISRs.

Further Reading

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top