The steps to integrate incoming UART data with FreeRTOS on a PSoC 6.

The focus is on saving processor power. The design does not poll for incoming data.

It yields all powers to the RTOS scheduler (possibly going to low power mode) until a trigger fires after a defined number of bites arrived at the UART input.

 

Define a UART with TRIGGER Support

 

In the initial part of the exercise, I follow the PSoC 6 documentation for Serial Communication Block configuration.

You will recognise 90% of it. The only changes I made to their example is:

  • Deal with a board that has a 100 MHz peripheral clock instead of a 50 MHz one.
  • Make a 9600 baud UART instead of a 115200 one.
  • every hardware allocation is a define, so that it's easy to switch between all available UART ports,  pins and clock dividers.
  • integrate with FreeRTOS

 

The PSoC 6 documentation is good and linked above, so I will not repeat any of that. Just show how I changed it.

The initialisation code is fully RTOS agnostic. But before going to it, let's check the constants I defined to make it easy to change ports and speed:

 

/* dependent on the SCB and pins used */
#define UART_SCB SCB5
#define UART_PORT       P5_0_PORT
#define UART_RX_NUM     P5_0_NUM
#define UART_TX_NUM     P5_1_NUM
#define UART_RX_PIN     P5_0_SCB5_UART_RX
#define UART_TX_PIN     P5_1_SCB5_UART_TX
#define UART_DIVIDER_NUMBER 5U

#define UART_CLOCK PCLK_SCB5_CLOCK

  /* Assign UART interrupt number and priority */
#define UART_INTR_NUM        ((IRQn_Type) scb_5_interrupt_IRQn)
#define UART_INTR_PRIORITY   (7U)

/* dependent on the BAUD used */
// jc 20210313: 9600 baud needs a 16 bit divider
/* UART desired baud rate is 9600 bps
 * The UART baud rate = (clk_scb / Oversample).
 * For clk_peri = 100 MHz, select divider value 864 and get SCB clock = (100 MHz / 864) = 115.7 kHz.
 * Select Oversample = 12. These setting results UART data rate = 115.7 kHz / 12 = 9645 bps.
 */

#define UART_CLK_DIV_TYPE     (CY_SYSCLK_DIV_16_BIT)
#define UART_CLK_DIV_NUMBER   (UART_DIVIDER_NUMBER)
#define UART_OVERSAMPLE 12UL
#define UART_DIVISION 863UL

/* application dependent UART settings */
#define UART_BUFFER_SIZE 16

 

Now the init code. I call it in my main(), after the board configuration and before the interrupts are activated:

  /* Initialize the device and board peripherals */
  result = cybsp_init() ;
  if (result != CY_RSLT_SUCCESS)
  {
    CY_ASSERT(0);
  }
  initUART();
  __enable_irq();

 

Here is the code. It uses SCB 5 (the debug port) at 9600 baud. With interrupt enable.

 

/* Allocate context for UART operation */
cy_stc_scb_uart_context_t uartContext;

void initUART() {
  // https://cypresssemiconductorco.github.io/psoc6pdl/pdl_api_reference_manual/html/group__group__scb__uart.html


  /* Populate configuration structure */
  const cy_stc_scb_uart_config_t uartConfig = {
      .uartMode                   = CY_SCB_UART_STANDARD,
      .enableMutliProcessorMode   = false,
      .smartCardRetryOnNack       = false,
      .irdaInvertRx               = false,
      .irdaEnableLowPowerReceiver = false,
      .oversample                 = UART_OVERSAMPLE,
      .enableMsbFirst             = false,
      .dataWidth                  = 8UL,
      .parity                     = CY_SCB_UART_PARITY_NONE,
      .stopBits                   = CY_SCB_UART_STOP_BITS_1,
      .enableInputFilter          = false,
      .breakWidth                 = 11UL,
      .dropOnFrameError           = false,
      .dropOnParityError          = false,
      .receiverAddress            = 0UL,
      .receiverAddressMask        = 0UL,
      .acceptAddrInFifo           = false,
      .enableCts                  = false,
      .ctsPolarity                = CY_SCB_UART_ACTIVE_LOW,
      .rtsRxFifoLevel             = 0UL,
      .rtsPolarity                = CY_SCB_UART_ACTIVE_LOW,
      .rxFifoTriggerLevel  = 0UL,
      .rxFifoIntEnableMask = 0UL,
      .txFifoTriggerLevel  = 0UL,
      .txFifoIntEnableMask = 0UL,
  };
  /* Configure UART to operate */
  (void) Cy_SCB_UART_Init(UART_SCB, &uartConfig, &uartContext);


  /* Assign pins for UART on SCBx */


  /* Connect SCB UART function to pins */
  Cy_GPIO_SetHSIOM(UART_PORT, UART_RX_NUM, UART_RX_PIN);
  Cy_GPIO_SetHSIOM(UART_PORT, UART_TX_NUM, UART_TX_PIN);
  /* Configure pins for UART operation */
  Cy_GPIO_SetDrivemode(UART_PORT, UART_RX_NUM, CY_GPIO_DM_HIGHZ);
  Cy_GPIO_SetDrivemode(UART_PORT, UART_TX_NUM, CY_GPIO_DM_STRONG_IN_OFF);


  /* Assign divider type and number for UART */


  /* Connect assigned divider to be a clock source for UART */
  Cy_SysClk_PeriphAssignDivider(UART_CLOCK, UART_CLK_DIV_TYPE, UART_CLK_DIV_NUMBER);




  // set baud
  Cy_SysClk_PeriphSetDivider   (UART_CLK_DIV_TYPE, UART_CLK_DIV_NUMBER, UART_DIVISION);
  Cy_SysClk_PeriphEnableDivider(UART_CLK_DIV_TYPE, UART_CLK_DIV_NUMBER);


  /* Populate configuration structure (code specific for CM4) */
  cy_stc_sysint_t uartIntrConfig =
  {
      .intrSrc      = UART_INTR_NUM,
      .intrPriority = UART_INTR_PRIORITY,
  };
  /* Hook interrupt service routine and enable interrupt */
  (void) Cy_SysInt_Init(&uartIntrConfig, &UART_Isr);
  NVIC_EnableIRQ(UART_INTR_NUM);


  /* Enable UART to operate */
  Cy_SCB_UART_Enable(UART_SCB);
}

 

Interrupt Handler with FreeRTOS brain

 

The interrupt handler is, by my design choice, FreeRTOS aware. Based on the FreeRTOS tutorial for sending data, but I changed it to work on the receive side.

It's written to release a task as soon as an agreed number of characters arrived:

 

// UART interrupt handler
void UART_Isr() {
  Cy_SCB_UART_Interrupt(UART_SCB, &uartContext);

  BaseType_t xHigherPriorityTaskWoken = pdFALSE;

  configASSERT( xTaskToNotify != NULL );

  /* Notify the task that the receive is complete. */
  vTaskNotifyGiveFromISR( xTaskToNotify, &xHigherPriorityTaskWoken );
  /* There are no receive in progress, so no tasks to notify. */
  xTaskToNotify = NULL;

  /* If xHigherPriorityTaskWoken is now set to pdTRUE then a
  context switch should be performed to ensure the interrupt
  returns directly to the highest priority task.  The macro used
  for this purpose is dependent on the port in use and may be
  called portEND_SWITCHING_ISR(). */
  portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}

 

The design has a FreeRTOS task that patiently waits on a notification. The code above in the interrupt is the code that generates that notification.

It fires exactly at the right time: when the agreed number of bytes are available and have been moved to the read buffer.

What happens is:

  • the default interrupt processing for the PSoC 6 is done at line 03.
  • We ask FreeRTOS to notify the waiting task that the agreed bytes have been received, at line 10.
  • Then we inform FreeRTOS that our interrupt handler has finished.

I did not invent this. It's boilerplate RTOS interrupt processing.

 

The PSoC 6 Function to enable UART read with interrupt handling

 

Then the read function that primes the PSoC 6 UART read function, every time the buffer has been filled:

 

// UART activate a receive with interrupt. Wait for ever for UART_BUFFER_SIZE bytes
void UART_receive() {
  /* At this point xTaskToNotify should be NULL as no receive
  is in progress.  A mutex can be used to guard access to the
  peripheral if necessary. */
  configASSERT( xTaskToNotify == NULL );

  /* Store the handle of the calling task. */
  xTaskToNotify = xTaskGetCurrentTaskHandle();

  /* Start receive operation (do not check status) */
  (void) Cy_SCB_UART_Receive(UART_SCB, rxBuffer, sizeof(rxBuffer), &uartContext);
}

 

The first two lines of code are again FreeRTOS common, to set the deactivation of the UART RTOS task until the read received its data over the UART.

The last line actually activates the PSoC 6 read function. Non-blocking, with the trigger primed. It tells the SCB on the PSoC to accept data by itself, buffer it and trigger the trigger when the agreed amount of bytes arrived.

 

FreeRTOS Task that wakes up when a buffer of UART data is received

 

Now the FreeRTOS task:

 

void uart_task(void *pvParameters) {

  /* To avoid compiler warnings */
  (void)pvParameters;
  uint32_t ulNotificationValue;

  while (true) {
    /* Start the receiving from UART. */
    UART_receive();
    /* Wait to be notified that the receive is complete.  Note
        the first parameter is pdTRUE, which has the effect of clearing
        the task's notification value back to 0, making the notification
        value act like a binary (rather than a counting) semaphore.  */
    ulNotificationValue = ulTaskNotifyTake(pdTRUE, portMAX_DELAY  );

    if( ulNotificationValue == 1 )
    {
      /* Blocking wait until buffer is full */
      // in this example, it will never handle because the UART interrupt fires exaxtly when the buffer is full
      while (0UL != (CY_SCB_UART_RECEIVE_ACTIVE & Cy_SCB_UART_GetReceiveStatus(UART_SCB, &uartContext))) {}
      /* Handle received data */
    }
  }
}

 

It calls the non-blocking read function we just defined a step earlier. Then it goes to sleep at line 14  because the read function holds it up.

Once the PSoC UART  gets the agreed number of characters, the trigger fires and calls this function in the handler (see a few steps above where the ,interrupt handler is described):

 

vTaskNotifyGiveFromISR( xTaskToNotify, &xHigherPriorityTaskWoken );

 

This will push our FreeRTOS task over line 14. The received data sits in the rxBuffer by that time. This works so well together that it's almost magic. Without burning the microcontroller ticks to poll the UART.

You can then put your logic here:

 

   if( ulNotificationValue == 1 )
    {
      /* Blocking wait until buffer is full */
      // in this example, it will never handle because the UART interrupt fires exaxtly when the buffer is full
      while (0UL != (CY_SCB_UART_RECEIVE_ACTIVE & Cy_SCB_UART_GetReceiveStatus(UART_SCB, &uartContext))) {}
      /* Handle received data */
    }

 

Line 05 is not really needed, because we already know all data arrived. The task notification told it. Because it was generated by the interrupt. And the interrupt fires when the agreed number of bytes are in.

 

In a project I'm working on with balearicdynamics, this will be the place where we push that received data on a FreeRTOS queue.

 

 

There are a few globals I defined for this exercise. They are only used in one .c file. If you prefer no global variables, you can use the established C practices to avoid them.

 

cy_stc_scb_uart_context_t uartContext;
static TaskHandle_t xTaskToNotify = NULL;
const UBaseType_t xArrayIndex = 1;
uint8_t rxBuffer[UART_BUFFER_SIZE];

 

 

When you try to use the debugger on the PSoC 6, and it does not break at your breakpoints, here are a few watch outs:

 

optimiser settings

The GCC optimiser is active. Even if your make file has: CONFIG=Debug

The documents seem to tell otherwise, but trust me on this one.

This will cause that the debugger may just ignore the source line you put a breakpoint on, because it's optimised out by the smart compiler.

You can override this by changing the line above to: CONFIG=Custom

and amend this line: CFLAGS=-O0

 

debugging with FreeRTOS and OpenOCD (the debugger that comes with many PSoC 6 kits)

To allow the debugger to deal with FreeRTOS, you have to prevent that your jobs take the highest priority.

You do that by adding these lines in your code before yielding to the scheduler:

 

  /* This enables RTOS aware debugging in OpenOCD. */

  uxTopUsedPriority = configMAX_PRIORITIES - 1;

 

PSoC 6 series
Create a Project with its own Board- and Module configurations
Low Power Management - prepare the board for current measurements
Power consumption without WiFi or Bluetooth
Create a FreeRTOS 10.3 Project
UART receiver with FreeRTOS
FreeRTOS message queue
UART receiver with FreeRTOS - Deep Sleep support