In a multitasking system, there is potential for conflict if one task start to access a resource, but does not complete its access before being transitioned out of the Running State. If the task leaves the resource in an inconsistent state, then access to the same resource by any other task or interrupt could result in data corruption. For instance:
- Task A executes and starts to write "Hello world" to LCD
- Task A is pre-empted by task B after outputting just the beginning of "Hello w"
- Task B writes "Abort, Retry?" to LCD before entering the Blocked state
- Task A continues from the point at which it was pre-empted and completes outputting "orld"
- The result is "Hello wAbort, Retry?orld"
Non-Atomic Access to Variables
Updating multiple members of a structure, or updating a variable that is larger than the natural word size of the architecture, are examples of non-atomic operations. If they are interrupted, they can result in data loss or corruption.
Non-Atomic operation:
/*The C code being compiled*/
GlobalVar |= 0x01;
/*the assembly code produced*/
LDR r4, [pc, #284]
LDR r0, [r4, #0x08]
ORR r0, r0, #0x01
STR r0, [r4, #0x08]
Function Reentrancy
A function is reentrant if it safe to call the function from more than one task, or from both tasks and interrupts. the following code shows two fucntion.
long lvar2;
long addOneHundred( long lVar1 )
{
/*this function is not reentrant becuase is using
a global variable store in a fixed ans shared place in memory*/
lvar2 = lvar1 + 100;
return lvar2;
}
long addOneHundred( long lVar1, long lvar2; )
{
/*this function is reentrant becuase is using
only local variables created at the moment the function runs*/
lvar2 = lvar1 + 100;
return lvar2;
}
Mutual Exclusion
Access to a resource that is shared between tasks, must be managed using a mutual exclusion technique, to ensure data consistency. FreeRTOS provides several features that can be used to implement mutual exclusion, but the best mutual exclusion is to design the application in such way that the application does not share any resource.
Critical Sections
Critical sections are region of code that are surrounded by calls to the special macros. This kind of critical sections works by disabling interrupts up to the interrupt priority set by configMAX_SYSCALL_INTERRUPT_PRIORITY in FreeRTOSConfig.h Critical sections must be kept very short; otherwise, they will adversely affect interrupt response times.
void vCriticalFunction( void )
{
taskENTER_CRITICAL();
GlobalVar |= 0x01;
taskEXIT_CRITICAL();
}
Suspending the Scheduler
Suspending the scheduler prevents a context switch from occurring but leaves interrupts enabled.
void vPrintString(char *pcString)
{
vTaskSuspendAll();
printf("%s", pcString);
xTaskResumeAll();
}
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. When used in a mutual exclusion scenario, the mutex can be thought of as a token that is associated with the resource being shared.
Two task each want to access the resource, but a task is not permitted to access the resource unless it is the mutex holder
Task A attempts to take the mutex. Because the mutex is available Task A successfully becomes the mutex holder so is permitted to access the resource
Task B executes and attempts to take the same mutex. Task A still has the mutex so the attempt fails and Task B is not permitted to access the resource
Task B opts to enter the Blocked state to wait for the mutex allowing Task A to run again. Task A finishes with the resource so gives the mutex back
Task A giving the mutex back causes Task B to exit the Blocked state. Task B can now successfully obtain the mutex, and having done so is permitted to access
When Task B finishes accessing the resource it too gives the mutex back. The mutex is now once again available to both tasks.
The previous example is translated to the following code
xSemaphoreHandle xMutex;
int main( void )
{
vSetupHardware();
xMutex = xSemaphoreCreateMutex();
srand( 567 );
xTaskCreate( vPrintTask, "Print1", 240, "Task1***\n", 1, NULL );
xTaskCreate( vPrintTask, "Print2", 240, "Task2---\n", 2, NULL );
vTaskStartScheduler();
}
void prvNewPrintString( const portCHAR *pcString )
{
xSemaphoreTake( xMutex, portMAX_DELAY );
{
printf( "%s", pcString );
flush( stdout );
}
xSemaphoreGive( xMutex );
}
void vPrintTask( void *pvParameters )
{
char *pcStringToPrint;
pcStringToPrint = ( char * ) pvParameters;
for( ;; )
{
prvNewPrintString( pcStringToPrint );
vTaskDelay( ( rand() & 0x1FF ) );
}
}
Time diagram
Priority Inversion
There is one potential pitfall of using a mutex to provide mutual exclusion. “A higher priority task being delayed by a lower priority task” And things can get worse if we add a medium priority task, as shown in the image
FreeRTOS mutexes include a basic priority inheritance mechanism. which minimize the negative effect of priority inversion. It does not fixed but lessens its impact. The low priority task that holds the mutex, inherits the priority of the task waiting for the mutex. It means, raises its own priority during the time that is holding the mutex.
Deadlock
Deadlock is another potential pitfall that occur when using mutexes for mutual exclusion. Occurs when two task cannot proceed because they are both waiting for a resource that is held by the other.
Both tasks take one different mutex, so access two different resources.
Both tasks try to take the resource that holds the other one without given back the mutex that already hold them.
Gatekeeper Task
Gatekeeper tasks provide a clean method of implementing mutual exclusion without the risk of priority inversion or deadlock. A gatekeeper task is a task that has sole ownership of a resource. Only the gatekeeper task is allowed to access the resource directly.
Two task indirectly access to a resource. Both send information to a gatekeeper task who is in control of the resource
Code Snippets
- MUTEX: DeadLock
- MUTEX: Priority inversion
- MUTEX: Code protection using Mutex
- MUTEX: Recursive Mutex
- MUTEX: Interrupts and Mutex
Exercises (printf)
- Create a program to use the mutex semaphore, two tasks must be created, the first task with a periodicity of 600ms and the second task with a period of 200ms. The first task going to take the mutex and give it until the task finishes, second task going to try to get the semaphore. Prints messages on the terminal to identify each task.
- Develop a program to block a part of code, create a function to protect the code inside, function must contain a message to be displayed on the screen, and create two tasks that going to take and give the mutex semaphore. The message must be different for each task. Use the periods of the last exercise.
- Modify the last example, now the function will send the message using a Queue to another task. The new task “ReadMsg“ going to be executed every 1 second to read all the messages on the queue, the tasks created in the last example must be blocked while the new task reads all the buffer, and unlocks them when new task finishes. Use another mutex to block the tasks.
Exercises
- Modify the last exercise(printf) to send a series of LEDs to turn on the first execution, the second execution LEDs must be turned off.
- Develop a program to use a recursive mutex, 2 tasks will be created, the first task to turn on 4 of 8 LEDs (It means that LED0, LED1, LED2, LED3 must be ON ) , the second task to turn off the LEDs. When the second task finishes, first task will turn on the next 4 LEDs ( LED4, LED5, LED6, LED7 must be ON ), second task again turn off LEDs and program should start again.
- Modify the previous program, let's add two buttons. Now with buttons, we can select which part of the series of LEDs control. The first button is to control the first part of the series and the second button is to control the other part.
- Modify example 2 and use a counter event using semaphores instead of the recursive mutex. Now you should use the mutex to prevent raised conditions between tasks.