Using Semaphores to Replace All Status Bit Polling in Peripheral Drivers

I’m working on writing peripheral drivers for SPI, I2C, UART, etc. and I’ve been replacing all status bit polling, e.g., while (!(I2C1->SR1 & I2C_SR1_SB)); with interrupt/semaphore synchronization. My question; is this proper and/or reasonable use of FreeRTOS? Some driver level functions require repeated instances of status bit checking where I have interrupt/semaphore synchronization and I am starting to wonder if this is overkill, or a reasonable way to write a driver?

Whether it is reasonable or not depends on:

  1. If peripherals are shared between tasks. Often in small systems there is only one task that accesses a peripheral at a time, in which case you only need a signally mechanism rather than a mutual exclusion mechanism. In this case using a direct to task notification in place of a semaphore will save execution time and RAM. You can either pass the task to notify in as a parameter to the API that starts a transaction on a peripheral, have a const for the task to notify (the task’s handle) if it really is only one task, or other such mechanism. The ISR that executes when the peripheral is ready for servicing, or when a DMA transfer initiated by the peripheral completes, simply sends a notification to to the noted task handle. Best to send a notification that increments the task’s notification count so you don’t get race conditions - one notification per interrupt that can be serviced in turn.

  2. The rate at which interrupts occur. If they are very fast then a semaphore, which uses a queue underneath (with an item size of zero no no memory copies occur) is a bit heavy weight.

1 Like

Many thanks for your insight and quick response! I’ll consider these points and will plan to go with task notifications for higher rate interrupts, e.g., from the ADC interrupts. Regarding, the specifics of the communication driver functions I’m working on; SPI, I2C and UART, I was thinking to go with binary Semaphores (one for each peripheral instance, e.g., I2C1, I2C2, I2C3) for synchronizing with interrupt flags.

For example, in this I2C transmit function, which will probably be used for getting environmental sensor data called from sensor driver (not at fast intervals) - binary semaphores seemed to be the most straightforward implementation for removing polling status bits by enabling an interrupt and waiting on the semaphore to be given from the ISR:

error_t I2C_MasterTransmit(I2C_TypeDef *I2Cx, uint8_t SlaveAddress, uint8_t *pTxData, uint16_t DataLength)
{
  error_t status = ERR_OK;

  if (I2C_Master_Start(I2Cx, SlaveAddress) == ERR_OK)
  {
    uint16_t TxCount = DataLength;
    uint16_t TxByteNum = (uint16_t) 0;

    // Send the data
    while (TxCount)
    {
      // Enable TXE interrupt
      I2C_EnableIT_TXE(I2Cx);

      // Non-blocking wait for the I2Cx semaphore until TXE bit is set
      if (xSemaphoreTake(I2C_GetSemaphoreHandle(I2Cx), I2C_TIMEOUT_MS) != pdTRUE)
      {
        status = ERR_TIMEOUT;
        break;
      }
      else
      {
        // Send the byte
        *(__IO uint8_t *)&I2Cx->DR = pTxData[TxByteNum++];

        TxCount--;
      }
    }
    if (status == ERR_OK)
    {
      // Enable event interrupts - wait for BTF
      I2C_EnableIT_EVT(I2Cx);

      // Non-blocking wait for the I2Cx semaphore until the BTF bit is set
      if (xSemaphoreTake(I2C_GetSemaphoreHandle(I2Cx), I2C_TIMEOUT_MS) != pdTRUE)
      {
        status = ERR_TIMEOUT;
      }

      // Send STOP condition
      I2Cx->CR1 |= (I2C_CR1_STOP);
    }
  }
  else
  {
    status = ERR_FAIL;
  }

  return status;
}

The ISR checks for the various flags triggered by the driver functions, e.g., the master transmit and gives the semaphore:

void I2C1_EV_IRQHandler(void)
{
 portBASE_TYPE xHigherPriorityTaskWoken = pdFALSE;

 /* Check for start bit */
 if ((I2C1->SR1 & I2C_SR1_SB) == I2C_SR1_SB)
 {
   // Event interrupt disable
   I2C_DisableIT_EVT(I2C1);

   xSemaphoreGiveFromISR(I2C1_SemaphoreHandle, &xHigherPriorityTaskWoken);
 }
.....
 /* Check for TXE */
 if (I2C_IsEnabledIT_TXe(I2C1))
 {
   if ((I2C1->SR1 & I2C_SR1_TXE) == I2C_SR1_TXE)
   {
     // TXE interrupt disable
     I2C_DisableIT_TXE(I2C1);

     xSemaphoreGiveFromISR(I2C1_SemaphoreHandle, &xHigherPriorityTaskWoken);
   }
....
 /* Immediately switch to the higher priority task */
 portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
 } 

Some functions require more interrupt synchronization (status bit waiting) than others, so I was questioning their feasibility…

Are you saying that you cannot differentiate between the different “semaphore give” events and therefore, cannot be sure which event happened (TXE bit/BTF bit)? If so, you can use event group and use different bits to wait for different events.

1 Like

This was becoming a concern for me yes - thank you for the excellent suggestion. This sounds like it could make for a clearly defined solution for differentiating between events.