Process the UART data in batch instead of in byte

I’m working on a project to establish communication between a host MCU and a third-party chip using the UART interface.

Currently, when a character is received by the host, a UART interrupt is triggered. The received character is then stored in a ring buffer, and xSemaphoreGiveFromISR() is called to release a semaphore. A receiving task waits for this semaphore, and once awakened, it processes the received data.

The incoming data follows a packet structure, including a header and a data length field, with packet sizes reaching up to 500 bytes. In the current design, the receiving task wakes up for every single received character, meaning it could be triggered up to 500 times for a full packet. This leads to unnecessary overhead and inefficiencies. Here are the code:

void UartRxISR(void)
{
  // Put the char in the ring buffer
  // . . .
  
  xSemaphoreGiveFromISR(xReceiveSemaphore, &xHigherPriorityTaskWoken)
}

void ReceiveTask(void *pvParameter)
{
  for(;;)
  {
    xSemaphoreTake(xReceiveSemaphore, portMAX_DELAY)
    
    if (ring buffer is all processed)
    {
      // This part can be called up to 500 times for each packet of data
      continue;
    }

    // Process the ring buffer until all of the chars are processed in the ring buffer
    // . . . 
  }
}

To optimize this, we attempted a solution where the receiving task is only woken up when the first character is received and remains active until a flag is set, preventing repeated wake-ups. However, during testing, we observed cases where the receiving task did not wake up even when new characters were received.
Here are some code for this approach:

void UartRxISR(void)
{
  // Put the char in the ring buffer
  // . . .
  
  if (!gReceiveSemaReleased)
  {
    gReceiveSemaReleased = true;
    xSemaphoreGiveFromISR(xReceiveSemaphore, &xHigherPriorityTaskWoken)
  }
}

void ReceiveTask(void *pvParameter)
{
  for(;;)
  {
    xSemaphoreTake(xReceiveSemaphore, portMAX_DELAY)
    
    // Process the ring buffer until all of the chars are processed in the ring buffer
    // . . . 
    
    gReceiveSemaReleased = false;
  }
}

Is there a better way to design the solution?
Thanks!

What would be best is if the ISR parsed the header, and didn’t send a wakeup until a full message was received.

Your system would seem to still have the task wake up for each character received, unless it takes longer to process than to receive a character. Your system also has a race between noticing no characters in the buffer and setting the flag, a character received in that time period won’t trigger the semaphore.

As @richard-damon suggested, the ideal solution would have the ISR parse the header and only wake up the task when a full message is received. However, if that’s not feasible, using task notifications is also a good alternative. Have the ISR increment a character counter and only notify the task when either a minimum packet size is reached or an end-of-packet marker is detected. The task can then process multiple characters at once, potentially handling all 500 bytes in a single wake-up.

Task notifications can be used as a lightweight binary semaphore using ulTaskNotifyTake() and vTaskNotifyGiveFromISR(), you can check as per FreeRTOS documentation (RTOS Task Notifications - FreeRTOS™).

A lot of it depends on the protocol. If the length of the payload is either fixed or can be deducted from the packet header, what you can do is parse the header until you know how many bytes will follow and then submit a dma request with the number of chars passed to dma.

There are multiple things you could do depending on the MCUs UART peripheral.

  • if you have something like a current STM32 part, there is the so called “Support for Modbus communication” which actually means automatic detection of end of transmission, either at a known character or after a set amount of bus idle time.
    So you just need to set up a DMA channel for the UART and will get an end of message interrupt once all data was received. (if there is at least some bus idle time between packages)
  • If you don’t have that, you can parse character wise up to the length field and then arm the DMA for the rest of the data.
  • If you don’t even have a DMA, you can at least count the bytes after the length byte in the interrupt handler and only notify the task afterwards (notifications are cheaper than semaphores by the way)
1 Like