Actions

EmSys

Difference between revisions of "Embedded Systems Design II FreeRTOS"

From EdWiki

m
m
Line 13: Line 13:
 
   </div>
 
   </div>
 
   <div class="content" id="FreeRTOSTab2">
 
   <div class="content" id="FreeRTOSTab2">
    <p>{{EmSys:FreeRTOS - Task Management}}</p>
+
<!-- FreeRTOS Task Sub Tabs Start Here -->
 +
<ul class="tabs" data-tab>
 +
  <li class="tab-title active">Introduction</li>
 +
  <li class="tab-title">Blinky</li>
 +
  <li class="tab-title">Assignments</li>
 +
</ul>
 +
<div class="tabs-content">
 +
  <div class="content active" id="FreeRTOSSubTab1">
 +
    {{EmSys:FreeRTOS - Task Management}}
 +
  </div>
 +
  <div class="content" id="FreeRTOSSubTab2">
 +
    <p>This is the second panel of the basic tab example. This is the second panel of the basic tab example.</p>
 +
  </div>
 +
  <div class="content" id="FreeRTOSSubTab3">
 +
    <p>This is the third panel of the basic tab example. This is the second panel of the basic tab example.</p>
 +
  </div>
 +
</div>
 +
<!-- FreeRTOS Task Sub Tabs End Here -->
 
   </div>
 
   </div>
 
   <div class="content" id="FreeRTOSTab3">
 
   <div class="content" id="FreeRTOSTab3">

Revision as of 09:29, 3 March 2020

Getting Started with FreeRTOS

An Introduction to FressRTOS

FreeRTOS is a real-time kernel (or real-time scheduler) targeting at hard real-time applications.

  • Simple
  • Portable
  • Royalty free
  • Concise
  • Primarily written in C
  • Few assembler functions
  • Thumb mode supported

The Cortex-M3 Port of FreeRTOS

The Cortex-M3 port includes all the standard FreeRTOS features:

  • Pre-emptive or co-operative scheduler operation
  • Very flexible task priority assignment
  • Queues
  • Binary semaphores
  • Counting semaphores
  • Recursive semaphores
  • Mutexes
  • Tick hook functions
  • Idle hook functions
  • Stack overflow checking
  • Trace hook macros

Data Types in FreeRTOS

macro or typedef actual type
portCHAR char
portSHORT short
portLONG long
portFLOAT float
portDOUBLE double
portSTACK_TYPE uint32_t
portBASE_TYPE long
Typically, this is a 32-bit type on a 32-bit architecture,
a 16-bit type on a 16-bit architecture, and
an 8-bit type on an 8-bit architecture.
portBASE_TYPE is generally used for return types
that can take only a very limited range of values,
and for pdTRUE/pdFALSE type Booleans.
UBaseType_t unsigned long
TickType_t TickType_t is the data type used to hold the
tick count value, and to specify times

Variables and Functions Naming

Variables are prefixed with their type:

  • c: for char
  • s: for short
  • l: for long
  • x: for portBASE_TYPE and any other non-standard types (structures, task handles, queue handles, etc.).
  • u: for unsigned
  • p: pointer
  • combinations are possible

If a variable is unsigned, it is also prefixed with a ‘u’. If a variable is a pointer, it is also prefixed with a ‘p’. For example, a variable of type uint8_t will be prefixed with ‘uc’, and a variable of type pointer to char will be prefixed with ‘pc’.

Functions are prefixed with both the type they return, and the file they are defined within. For example:

  • vTaskPrioritySet() returns a void and is defined within task.c.
  • xQueueReceive() returns a variable of type BaseType_t and is defined within queue.c.
  • pvTimerGetTimerID() returns a pointer to void and is defined within timers.c.

File scope (private) functions are prefixed with ‘prv’.

Macro Names

Most macros are written in upper case, and prefixed with lower case letters that indicate where the macro is defined.

Macro Value
pdTRUE 1
pdFALSE 0
pdPASS 1
pdFAIL 0

Customisation

FreeRTOS is customised using a configuration file called FreeRTOSConfig.h. Every FreeRTOS application must have a FreeRTOSConfig.h header file in its pre-processor include path. FreeRTOSConfig.h tailors the RTOS kernel to the application being built. It is therefore specific to the application, not the RTOS, and should be located in an application directory, not in one of the RTOS kernel source code directories.

  • Constants that start with the text “config” define attributes of the kernel, or include or exclude features of the kernel.
  • Constants that start with the text “INCLUDE_” are used to included or excluded FreeRTOS API functions from the application.

Some important fields/features:

  • configUSE_PREEMPTION: This is set to 1 if the preemptive kernel is desired.
  • configUSE_IDLE HOOK: An idle task hook will execute a function during each cycle of the idle task.
  • configUSE_TICK HOOK: A tick hook function will execute on each RTOS tick interrupt if this value is set to 1.
  • configTICK_RATE HZ: This is the frequency at which the RTOS tick will operate.
  • configMAX_PRIORITIES: The total number of priority levels that can be assigned when prioritizing a task.
  • configUSE_COUNTING_SEMAPHORES: This is set to 1 if counting semaphores are required.
  • configUSE_MUTEXES: This is set to 1 if mutexes are needed. Priority inheritance will then be enforced.
  • etc...

Resources Used By FreeRTOS

FreeRTOS makes use of SysTick, PendSV, and SVC interrupts. These interrupts are not available for use by the application. FreeRTOS has a very small footprint. A typical kernel build will consume approximately 6KB of Flash space and a few hundred bytes of RAM. Each task also requires RAM to be allocated for use as the task stack.

Task Management

Tasks in FreeRTOS

  • With FreeRTOS, an application can be structured as a set of autonomous tasks.
  • Each task executes within its own context (e.g., stack) with no coincidental dependency on other tasks.
  • The scheduler is responsible for starting, stoping, swapping in, and swapping out tasks.

Tasks Functions

Tasks are implemented as C functions. The prototype of a task function must return void and take a void pointer parameter. A task will typically execute indefinitely in an infinite loop:

  • must never terminate by attempting to return to its caller
  • If required, a task can delete itself prior to reaching the function end.

A single task function definition can be used to create any number of tasks:

  • Each created task is a separate execution instance
  • Each created task has its own stack
  • Each created task has its own copy of any automatic variables defined within the task itself.

The structure of a typical task function:

void vTaskFunction(void *pvParameters)
{
    int iVariable = 0;
    /* 
     * Each instance of this task function will have its own copy of the iVariable.
     * Except that if the variable was declared static – in which case only one copy of the variable
     * would exist and would be shared by all created instance.
    */
 
    /* A task will normally be implemented as an infinite loop. */
    while(1) {
        ;      /* The code to implement the task functionality will go here. */
    }
 
    /*
     * Should the task implementation ever break out of the above loop then the task
     * must be deleted before reaching the end of this function. The NULL parameter 
     * passed to the  vTaskDelete() function indicates that the task to be deleted is the
     * calling (this) task.
     */
     vTaskDelete( NULL );
}

Task States

An application can consist of many tasks. Only one task of the application can be executed at any given time on the microcontroller (single core). Thus, a task can exist in one of two states:

  • Running or
  • Not Running.

Only the scheduler can decide which task should enter the Running state. A task is said to have been “switched in” or “swapped in” when transitioned from the Not Running to the Running state, (“switched out” or “swapped out”) when transitioned from the Running state to the Not Running state).

The scheduler is responsible for managing the processor context:

  • Registers values
  • Stack contents

Task States and Transitions

Freertos task states.png
  • Running state
    The processor is executing its code.
  • Not Running state
    The task is dormant, its status having been saved ready for resuming execution the next time
  • Scheduler is the only entity that can switch a task in and out a running state.

Creating a Task

A task can be created by calling xTaskCreate() API

portBASE_TYPE xTaskCreate( pdTASK_CODE pvTaskCode,
			   const signed portCHAR * const pcName,
			   unsigned portSHORT usStackDepth,
			   void *pvParameters,
			   unsigned portBASE_TYPE uxPriority,
			   xTaskHandle *pxCreatedTask );
  • pvTaskCode
    is a pointer to the function that implement the task
  • pcName
    is a descriptive name for the task, not used by FreeRTOS
    configMAX_TASK_NAME_LEN: the application defined constant that defines the maximum length a task name can including the NULL terminator.
  • usStackDepth
    specifies the number of words the stack of this task can hold
    E.g., Cortex-M3 stack is 32 bits wide, if usStackDepth is passed in as 100, 400 bytes of stack space will be allocated (100*4 bytes)
    Size of the stack used by the idle task is defined by configMINIMAL_STACK_SIZE.
  • pvParameters
    is the parameter that can be passed to the task function (pointer to a complex data structure)
  • uxPriority
    is the priority of the task (0 is the lowest priority, configMAX_PRIORITIES-1 is the highest priority)
    Passing a value above (configMAX_PRIOIRTIES -1) will result in the priority being capped the maximum legitimate value.
  • pxCreatedTask
    is the handler of the generated task, which can be used to reference the task within API calls
  • Returned value
    There are two possible return values:
    1. pdTRUE
      when the task was created successfully
    2. errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY
      the task could not be created because there was insufficient heap memory available for FreeRTOS to allocate enough RAM to hold the task data structures and stack.

Expanding the "Not Running" State

  • The Blocked state
    A task that is waiting for an event
    • Tasks can enter the Blocked state to wait for two different types of events:
      1. Temporal (time related) events where a delay period expiring or an absolute time being reached
        For example, wait for 10 ms to pass
      2. Synchronization events where the events originate from another task or interrupt
        For example, wait for data to arrive on a queue
    • It is possible for a task to block on a synchronization event with a timeout
      For example, wait for a maximum of 10 ms for data to arrive on a queue
  • The Suspended state
    Tasks in the Suspended state are not available to the scheduler.
    • The only way into the Suspended state is through a call to the vTaskSuspend() API function
    • The only way out through a call to the vTaskResume() or xTaskResumeFromISR() API functions
  • The Ready State
    Tasks that are in the Not Running but are not Blocked or Suspended
    • Tasks are able to run, and therefore ready to run, but not currently in the Running state

Task State Transitions

Freertos task state transitions.png

Using the Blocked state to create a delay

  • The tasks may generate delay using a null loop – polling a counter until it reaches a fixed value
    Waste of processor cycles
  • An efficient method is to create a delay with a call to the vTaskDelay() API function
    • vTaskDelay() places the calling task into the Blocked state for a fixed number of tick interrupts
    • The task in the Blocked state will not use any processing time at all
  • An alternative method is to use the vTaskDelayUntil() API function

vTaskDelay()

void vTaskDelay( portTickType xTicksToDelay ); 
  • xTicksToDelay
    is the number of tick interrupts that the calling task should remain in the Blocked state before being transitioned back into the Ready state.

The constant portTICK_RATE_MS stores the time in milliseconds of a tick, which can be used to convert milliseconds into ticks.

vTaskDelayUntil()

void vTaskDelayUntil(portTickType * pxPreviousWakeTime, portTickType xTicksToDelay ); 
  • pxPreviousWakeTime
    • Holds the time at which the task last left the Blocked state (was ‘woken’ up). This time is used as a reference point to calculate the time at which the task should next leave the Blocked state.
    • The variable pointed to by pxPreviousWakeTime is updated automatically within the vTaskDelayUntil() function and should be initialized by the application code first.
  • xTicksToDelay
    vTaskDelayUntil() is normally being used to implement a task that executes periodically; the frequency being set by this value which is specified in ‘ticks’


This is the second panel of the basic tab example. This is the second panel of the basic tab example.

This is the third panel of the basic tab example. This is the second panel of the basic tab example.

Queue management

Characteristics of a Queue

  • Queue is used for inter-communication among autonomous tasks
  • A queue can hold a finite number of fixed size data items
  • Normally, it is First In First Out (FIFO) buffers
  • Writing data to a queue causes a byte for byte copy of data
  • Reading data from a queue will remove the data
  • Any number of tasks can write/read to/from the same queue

Blocking on Queue Reads

If the queue is empty, a task attempting to read from the queue will be blocked

  • When new data is placed into the queue, the blocked task will automatically be moved from the Blocked state to the Ready state
  • When multiple tasks are blocked, the task with the highest priority will be unblocked
  • If the blocked tasks have equal priority, the task that has been waiting for data the longest will be unblocked
  • The blocked task will also be automatically moved to the Ready state if the specified block time expires before data becomes available.

Blocking on Queue Writes

If the queue is already full, a task attempting to write to the queue will be blocked

  • When there is space available in the queue, the blocked task will automatically be moved from the Blocked state to the Ready state
  • When multiple tasks are blocked, the task waiting for space with the highest priority will be unblocked
  • If the blocked tasks have equal priority, the task that has been waiting for space the longest will be unblocked
  • The blocked task will also be automatically moved to the Ready state if the specified block time expires

Using a Queue

A queue must be explicitly created before it can be used. xQueueCreate() is used to create a queue and returns an xQueueHandle to reference the queue it creates.

xQueueHandle xQueueCreate(unsigned portBASE_TYPE uxQueueLength, unsigned portBASE_TYPE uxItemSize );
  • uxQueueLength
    The maximum number of items that the queue being created can hold at any one time
  • uxItemSize
    The size in bytes of each data item that can be stored in the queue
  • Return value
    If NULL is returned then the queue could not be created because there was insufficient heap memory available
    A non-NULL value being returned indicates that the queue was created successfully.

Write to a Queue

  • xQueueSendToFront() and xQueueSendToBack() is used to send data to the back/tail (front/head) of a queue
    They should never be called from an interrupt service routine
  • In ISRs, xQueueSendToFrontFromISR() and xQueueSendToBackFromISR() should be used.

Read from a Queue

  • xQueueReceive() is used to receive (read) an item from a queue and the item is removed from the queue
  • xQueuePeek() is used to receive an item from a queue without the item being removed from the queue
  • In ISRs, xQueueReceiveFromISR() API function should be used

Query a Queue

  • uxQueueMessagesWaiting() is used to query the number of items that are currently in a queue
  • In ISRs, uxQueueMessagesWaitingFromISR() API function should be used

Working with Large Data

  • If the size of data being stored in the queue is large, it is preferable to use pointers to the data rather than copy the data itself
    More efficient in both processing time and the amount of RAM required
  • It is essential to ensure that both tasks do not modify the memory contents simultaneously
  • Dynamically allocated memory should be freed by exactly one task and no tasks should attempt to access the memory after it has been freed.

FreeRTOS - Task Notifications

Applications that use FreeRTOS are structured as a set of independent tasks, and that it is likely that these autonomous tasks will have to communicate with each other so that, collectively, they can provide useful system functionality. Tasks can communicate using communications objects like queues, event groups, and various different types of semaphore.

When a communication object is used, events and data are not sent directly to a receiving task, or a receiving ISR, but are instead sent to the communication object. Likewise, tasks and ISRs receive events and data from the communication object, rather than directly from the task or ISR that sent the event or data.

Freertos using communication object.png

Task Notifications — Direct to Task Communication

‘Task Notifications’ allow tasks to interact with other tasks, and to synchronize with ISRs, without the need for a separate communication object. By using a task notification, a task or ISR can send an event directly to the receiving task.

Freertos task notifications.png


  • To include task notification functionality set configUSE_TASK_NOTIFICATIONS to 1 in FreeRTOSConfig.h.
  • When configUSE_TASK_NOTIFICATIONS is set to 1, each task has a ‘Notification State’, which can be either ‘Pending’ or ‘Not-Pending’, and a ‘Notification Value’, which is a 32-bit unsigned integer.

Task Notification API Functions

Task notifications are a very powerful feature that can often be used in place of a binary semaphore, a counting semaphore, an event group, and sometimes even a queue.

The xTaskNotifyGive() API Function

BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify );

xTaskNotifyGive() sends a notification directly to a task, and increments (adds one to) the receiving task’s notification value. Calling xTaskNotifyGive() will set the receiving task’s notification state to pending, if it was not already pending.

The vTaskNotifyGiveFromISR() API Function

void vTaskNotifyGiveFromISR( TaskHandle_t xTaskToNotify, BaseType_t *pxHigherPriorityTaskWoken );

vTaskNotifyGiveFromISR() is a version of xTaskNotifyGive() that can be used in an interrupt service routine.

The ulTaskNotifyTake() API Function

uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait );

ulTaskNotifyTake() allows a task to wait in the Blocked state for its notification value to be greater than zero, and either decrements (subtracts one from) or clears the task’s notification value before it returns.

The xTaskNotify() and xTaskNotifyFromISR() API Functions

BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction );
BaseType_t xTaskNotifyFromISR( TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction,
BaseType_t *pxHigherPriorityTaskWoken );

xTaskNotify() is a more capable version of xTaskNotifyGive() that can be used to update the receiving task’s notification value in any of the following ways:

  • Increment (add one to) the receiving task’s notification value, in which case xTaskNotify() is equivalent to xTaskNotifyGive().
  • Set one or more bits in the receiving task’s notification value. This allows a task’s notification value to be used as a lighter weight and faster alternative to an event group.
  • Write a completely new number into the receiving task’s notification value, but only if the receiving task has read its notification value since it was last updated. This allows a task’s notification value to provide similar functionality to that provided by a queue that has a length of one.
  • Write a completely new number into the receiving task’s notification value, even if the receiving task has not read its notification value since it was last updated. This allows a task’s notification value to provide similar functionality to that provided by the xQueueOverwrite() API function. The resultant behavior is sometimes referred to as a ‘mailbox’.

xTaskNotifyFromISR() is a version of xTaskNotify() that can be used in an interrupt service routine, and therefore has an additional pxHigherPriorityTaskWoken parameter.

The xTaskNotifyWait() API Function

BaseType_t xTaskNotifyWait(uint32_t ulBitsToClearOnEntry, uint32_t ulBitsToClearOnExit,  uint32_t *pulNotificationValue,
TickType_t xTicksToWait);

xTaskNotifyWait() is a more capable version of ulTaskNotifyTake(). It allows a task to wait, with an optional timeout, for the calling task’s notification state to become pending, should it not already be pending.

Interrupt management

An interrupt service routine is a hardware feature because the hardware controls which interrupt service routine will run, and when it will run. Tasks will only run when there are no ISRs running, so the lowest priority interrupt will interrupt the highest priority task, and there is no way for a task to pre-empt an ISR.

The Interrupt Safe API

Often it is necessary to use the functionality provided by a FreeRTOS API function from an interrupt service routine (ISR), but many FreeRTOS API functions perform actions that are not valid inside an ISR — the most notable of which is placing the task that called the API function into the Blocked state; if an API function is called from an ISR, then it is not being called from a task, so there is no calling task that can be placed into the Blocked state.

FreeRTOS solves this problem by providing two versions of some API functions; one version for use from tasks, and one version for use from ISRs. Functions intended for use from ISRs have “FromISR” appended to their name.

Note: Never call a FreeRTOS API function that does not have “FromISR” in its name from an ISR

Deferred Interrupt Processing

It is normally considered best practice to keep ISRs as short as possible. Reasons for this include:

  • Even if tasks have been assigned a very high priority, they will only run if no interrupts are being serviced by the hardware.
  • ISRs can disrupt both the start time, and the execution time, of a task.
  • It might not be possible to accept any new interrupts, or at least a subset of new interrupts, while an ISR is executing.
  • Guarding against, resources such as variables, peripherals, and memory buffers being accessed by a task and an ISR at the same time.
  • Interrupt nesting can increase complexity and reduce predictability. The shorter an interrupt is, the less likely it is to nest.

An interrupt service routine must record the cause of the interrupt, and clear the interrupt. Any other processing necessitated by the interrupt can often be performed in a task, allowing the interrupt service routine to exit as quickly as is practical. This is called ‘deferred interrupt processing’, because the processing necessitated by the interrupt is ‘deferred’ from the ISR to a task.

Deferring interrupt processing to a task also allows the application writer to prioritize the processing relative to other tasks in the application, and use all the FreeRTOS API functions.

If the priority of the task to which interrupt processing is deferred is above the priority of any other task, then the processing will be performed immediately, just as if the processing had been performed in the ISR itself.

This scenario is shown in the following figure, in which Task 1 is a normal application task, and Task 2 is the task to which interrupt processing is deferred.

Freertos int process in high priorit task.png
Figure: Completing interrupt processing in a high priority task

In the above figure, interrupt processing starts at time t2, and effectively ends at time t4, but only the period between times t2 and t3 is spent in the ISR. If deferred interrupt processing had not been used then the entire period between times t2 and t4 would have been spent in the ISR.

Binary Semaphores

The following figure shows how the execution of the deferred processing task can be controlled using a semaphore.

Freertos interrupt and handler task.png
Figure: Using a binary semaphore to implement deferred interrupt processing


In the interrupt synchronization scenario, the binary semaphore can be considered conceptually as a queue with a length of one. The queue can contain a maximum of one item at any time, so is always either empty or full (hence, binary). By calling xSemaphoreTake(), the task to which interrupt processing is deferred effectively attempts to read from the queue with a block time, causing the task to enter the Blocked state if the queue is empty. When the event occurs, the ISR uses the xSemaphoreGiveFromISR() function to place a token (the semaphore) into the queue, making the queue full. This causes the task to exit the Blocked state and remove the token, leaving the queue empty once more. When the task has completed its processing, it once more attempts to read from the queue and, finding the queue empty, re-enters the Blocked state to wait for the next event. This sequence is demonstrated in the following figure.

Freertos interrupt and handler task 1.png
Freertos interrupt and handler task 2.png
Figure: Using a binary semaphore to synchronize a task with an interrupt

Binary Semaphores are used for Synchronization. A Binary Semaphore can be used to unblock a task each time a particular interrupt occurs:

  • The majority of the interrupt event processing can be implemented within the synchronized task in the ISR
  • Only a very fast and short portion remaining directly
  • The interrupt processing is said to have been ‘deferred’ to a ‘handler’ task
  • The handler task uses a blocking ‘take’ call to a semaphore and enters the Blocked state to wait for the event to occur
  • When the event occurs, the ISR uses a ‘give’ operation on the same semaphore to unblock the task

Create a Binary Semaphore

Use the vSemaphoreCreateBinary() API function (macro) to create a binary semaphore

void vSemaphoreCreateBinary(xSemaphoreHandle xSemaphore);

‘Take’ a Semaphore

Use xSemaphoreTake() to ‘take’ a semaphore

portBASE_TYPE xSemaphoreTake(xSemaphoreHandle xSemaphore, portTickType xTicksToWait );

‘Give’ a Semaphore

Use xSemaphoreGive() to ‘give’ a semaphore or xSemaphoreGiveFromISR() when in an ISR

portBASE_TYPE xSemaphoreGiveFromISR(xSemaphoreHandle xSemaphore, portBASE_TYPE *pxHigherPriorityTaskWoken );

Counting Semaphores

When interrupts come at a speed faster than the handler task can process, events will be lost. Counting semaphore can be thought of as queues that have a length of more than one. Each time a counting semaphore is ‘given’, another space in its queue is used, count value is the length of the queue.

Counting semaphores are typically used for two things:

  1. Counting events
    The count value is the difference between the number of events that have occurred and the number that have been processed
  2. Resource management
    The count value indicates the number of resources available; a task must first obtains a semaphore before obtains the control of a resource; a task returns a semaphore when finishing with the resource.

Using a counting semaphore to ‘count’ events

Freertos using counting semaphore.png

Create a Counting Semaphore

Use xSemaphoreCreateCounting() to create a counting semaphore

xSemaphoreHandle xSemaphoreCreateCounting( unsigned portBASE_TYPE uxMaxCount, unsigned portBASE_TYPE uxInitialCount );

Resource Management

Resources that are shared between tasks or between tasks and interrupts needs to be managed using a ‘mutual exclusion’ technique to ensure data consistency.

Basic Critical Sections

  • Basic critical sections protect a region of code from access by other tasks and by interrupts
    Regions of code that are surrounded by calls to the macros taskENTER_CRITICAL() and taskEXIT_CRITICAL() respectively,


   /* Ensure access to the PORTA register cannot be interrupted by placing it with in a critical section. */
   taskENTER_CRITICAL();
 
   /* A switch to another task cannot occur between the call to taskENTER_CRITICAL()
    * and  the call to taskEXIT_CRITICAL(). Interrupts may still execute on FreeRTOS Ports
    * that allow interrupt nesting, but only interrupts whose priority is above the value
    * assigned to the configMAX_SYSCALL_INTERRUPT_PRIORITY constant - and those interrupts
    * are not permitted to call  FreeRTOS API functions.
    */
 
    PORTA |= 0x01;
 
    /* We have finished accessing PORTA so can safely leave the critical section */
    taskEXIT_CRITICAL();


  • A very crude method of providing mutual exclusion as all or partial interrupts are disabled

Suspending (Locking) the Scheduler

  • Critical sections can also be created by suspending the scheduler
  • A critical section that is too long to be implemented by simply disabling interrupts can instead be implemented by suspending the scheduler
  • The scheduler is suspended by calling vTaskSuspendAll()
void vTaskSuspendAll( void );

The scheduler is resumed by calling xTaskResumeAll()

portBASE_TYPE xTaskResumeAll( void );

Mutexes

  • A Mutex is a special type of binary semaphore that is used to control access to a resource that is shared between two or more tasks
  • The mutex can be conceptually thought of as a token associated with the resource being shared
  • For a task to legitimately access the resource it must first successfully ‘take’ the token
  • When the token holder has finished with the resource it must ‘give’ the token back.
Freertos mutual exclusion using mutex.png


Create a Mutex

Use xSemaphoreCreateMutex() to create a mutex

xSemaphoreHandle xSemaphoreCreateMutex( void );

Priority Inversion & Priority Inheritance

  • With a mutex, it is possible that a task with a higher priority has to wait for a task with a lower priority which hold the mutex to give up the control of the mutex
  • A higher priority task being delayed by a lower priority task in this manner is called ‘priority inversion’.
  • Priority inheritance works by temporarily raising the priority of the mutex holder to that of the highest priority task that is attempting to obtain the same mutex
  • The priority of the mutex holder is automatically reset back to its original value when it gives the mutex back
  • Priority inheritance does not ‘fix’ priority inversion, it merely lessens its impact.

Deadlock

Deadlock occurs when two tasks are both waiting for a resource that is held by the other, e.g.,

  1. Task A executes and successfully takes mutex X.
  2. Task A is pre-empted by Task B.
  3. Task B successfully takes mutex Y before attempting to also take mutex X – but mutex X is held by Task A so is not available to Task B. Task B opts to enter the Blocked state to wait for mutex X to be released.
  4. Task A continues executing. It attempts to take mutex Y – but mutex Y is held by Task B so is not available to Task A. Task A opts to enter the Blocked state to wait for mutex Y to be released.

Gatekeeper Tasks

  • Gatekeeper tasks provide a clean method of implementing mutual exclusion without the worry of priority inversion or deadlock
  • A gatekeeper task is a task that has sole ownership of a resource
  • A task needing to access the resource can only do so indirectly by using the services of the gatekeeper