Search code examples
carmstm32microcontrollerfreertos

Using Binary Semaphore and Mutex Together


I am new to FreeRTOS and have been reading the FreeRTOS documentation and writing simple code using FreeRTOS on an STM32F767 Nucleo Board. In the simple program that I wrote, I used Binary Semaphores only to signal certain tasks when LPTIM and GPIO interrupts occur through xSemaphoreGiveFromISR(), and to signal a different task to perform certain operations from another task through xSemaphoreGive().

Suppose that I have an I2C1 peripheral connected to two different equipments:

  • An accelerometer that triggers a GPIO interrupt to the microcontroller whenever an activity/movement occurs. This GPIO interrupt signals the microcontroller that a piece of data inside its Interrupt Event registers must be read so that the next activity/movement event can be signalled again.
  • An equipment that must be read from periodically, which will be triggered through an LPTIM or TIM peripheral

Can I use a Mutex and a Binary Semaphore in the situation above?

The Binary Semaphores will indicate to the task that an operation needs to be performed based on the respective interrupts that were triggered, but the Mutex will be shared between those two tasks, where Task1 will be responsible with reading data from the accelerometer, and Task2 will be responsible for reading data from the other equipment. I was thinking that a Mutex will be used since these two operations should never occur together, so that there are no overlapping I2C transactions that happen on the bus that could potentially lock up either of the I2C devices.

The code would look like the following:

void Task1_AccelerometerOperations(void *argument)
{
   /* The Semaphore will be given from the GPIO Interrupt Handler, signalling that a piece of 
      data needs to be read from the accelerometer through I2C. */
   if(xSemaphoreTake(xSemaphore_GPIOInterruptFlag, portMAX_DELAY) == pdTRUE)
   {
      /* This Mutex should ensure that only one I2C transaction can happen at a time */
      if(xSemaphoreTakeRecursive(xMutex_I2CBus, 2000/portTICK_PERIOD_MS) == pdTRUE)
      {
         /* Perform I2C Transaction */
         /* Perform operations with the data received */

         /* Mutex will be given back, indicating that the shared I2C Bus is now available */
         xSemaphoreGiveRecursive(xMutex_I2CBus);
      }
      else
      {
         /* Mutex was not available even after 2 seconds since the GPIO interrupt triggered. 
            Perform Error Handling for the event that the I2C bus was locked */
      }

      /* Piece of code that could take a few hundreds milliseconds to execute */
   }
}

void Task2_OtherEquipmentOperations(void *argument)
{
   /* The Semaphore will be given from the LPTIM Interrupt Handler, signalling that some maintenance 
      or periodic operation needs to be performed through I2C. */
   if(xSemaphoreTake(xSemaphore_LPTIMInterruptFlag, portMAX_DELAY) == pdTRUE)
   {
      /* Only perform the I2C operations when the Mutex is available */
      if(xSemaphoreTakeRecursive(xMutex_I2CBus, 2000/portTICK_PERIOD_MS) == pdTRUE)
      {
         /* Perform I2C Transaction */

         /* Mutex will be given back, indicating that the shared I2C Bus is now available */
         xSemaphoreGiveRecursive(xMutex_I2CBus);
      }
      else
      {
         /* Mutex was not available even after 2 seconds since the LPTIM interrupt triggered. 
            Perform Error Handling for the event that the I2C bus was locked */
      }

      /* Piece of code that could take a few seconds to execute */
   }
}

Are Mutexes often used to avoid Priority Inversion scenarios, or are they (more often) widely used to prevent two operations from possibly happening together? I can't think of a simple scenario where if a Priority Inversion occurs, it could be critical for the software.

Thank you!


Solution

  • In general, I think your design could work. The semaphores are signals for the two tasks to do work. And the mutex protects the shared I2C resource.

    However, sharing a resource with a mutex can lead to complications. First, your operations tasks are not responsive to new semaphore signals/events while they are waiting for the I2C resource mutex. Second, if the application gets more complex and you add more blocking calls then the design can get into a vicious cycle of blocking, starvation, race conditions, and deadlocks. Your simple design isn't there yet but you're starting down a path.

    As an alternative, consider making a third task responsible for handling all I2C communications. The I2C task will wait for a message (in a queue or mailbox). When a message arrives then the I2C task will perform the associated I2C communication. The operations tasks will wait for the semaphore signal/event like they do now. But rather than waiting for the I2C mutex to become available, now the operations tasks will send/post a message to the I2C task. With this design you don't need a mutex to serialize access to the I2C resource because the I2C task's queue/mailbox does the job of serializing the message communication requests from the other tasks. Also in this new design each task blocks at only one place, which is cleaner and allows the operations tasks to be more responsive to the signals/events.