FreeRTOS tutorial on STM32

A High-density line of STM32 microcontrollers has quite many features that can be used in your programs. However, the more features you add to the source, the more complicated the program becomes, and it may become challenging to keep up with all things. Relying on the main loop and interrupts becomes a time-consuming task to manage.

If you do not want to struggle while tuning things up manually, you can use one of the best free real-time operating systems (RTOS). It is handy when you need many separate functions to run in parallel without missing a task. RTOS scheduler takes care of giving each task a required period to perform. There are many great RTOS systems around. Many of them are free and open source. Follow the FreeRTOS tutorial to see how easy it is to run complex tasks.

FreeRTOS demo task on STM32 microcontrolelr

I love using FreeRTOS, which has a long successful history and is flexible to fit multiple hardware types. You can check out my demo FreeRTOS tutorial on Atmega128. I also encourage you to give it a try for other RTOS systems like ChibiOS, BeRTOS, and others.

FreeRTOS is quite simple and easy to use. It has practically all of the features you may need for an RTOS. Some of the critical elements would include a preemptive, cooperative, and hybrid scheduler, task and co-routine support, queues, semaphores, and mutexes for task synchronization and communication. There are many demos, many ports (about 35 microcontrollers) to get started.

You can find a few of our demos for the STM32F103ZET6 board that controls LEDs, Buttons, USART, and LCD. We wrote some code using the main loop and interrupts for managing a few events. This is efficient and OK, while the program is simple. However, when your application grows big, it is better to start building it using RTOS to keep it manageable until the end.

FreeRTOS example for STM32

Follow this FreeRTOS tutorial on the STM32 microcontroller to see how it is easy to scale your project and still have full control of operations. First of all, we need to build a template that includes all necessary FreeRTOS source files. We need to import the FreeRTOS folder to our project tree.

Then we need to add FreeRTOSConfig.h file to the project where all RTOS configurations are set. The next thing is to match interrupts handlers for systick, SVC and PendSV interrupt. They are used by the scheduler and have a bit of different terminology. If you look at the port.c file you’ll see that it uses exception handlers:

voidxPortPendSVHandler( void ) __attribute__ (( naked ));
voidxPortSysTickHandler( void );
voidvPortSVCHandler( void ) __attribute__ (( naked ));

All we have to place them instead of

void SVC_Handler (void) __attribute__((weak));
void PendSV_Handler (void) __attribute__((weak));
void SysTick_Handler (void) __attribute__((weak));

In vector table where start-up code residues.

Before we start writing code, let’s configure FreeRTOS. To do so, we need to edit FreeRTOSConfig.h file contents. There are lots of them, but most important are the following:

#define configUSE_PREEMPTION 1
#define configCPU_CLOCK_HZ ( ( unsignedlong ) 72000000 )
#define configTICK_RATE_HZ ( ( portTickType ) 1000 )
#define configMAX_PRIORITIES ( ( unsigned portBASE_TYPE ) 5 )
#define configMINIMAL_STACK_SIZE ( ( unsignedshort ) 120 )
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 18 * 1024 ) )
#define configMAX_TASK_NAME_LEN ( 16 )
#define configUSE_TRACE_FACILITY 1
#define configIDLE_SHOULD_YIELD 1
#define configUSE_MUTEXES 1
#define configUSE_COUNTING_SEMAPHORES 1
#define INCLUDE_vTaskPrioritySet 1
#define INCLUDE_vTaskDelayUntil 1
#define INCLUDE_vTaskDelay 1

Just a quick overview of these. We will use preemption, so we set it to 1, then we select the CPU clock rate, which is 72MHz, also we configure the tick timer, which means that the scheduler will run every 1ms.

Then we select a minimum stack size for a task and set a total heap size.

Our code is going to use task priorities, so we set vTaskPrioritySet to 1. Also, we are going to use vTaskDelay utilities that help with task timing. So we select them too.

There are a lot more settings you’ll find in the Config file. Many of them are self-explanatory, but checking their meaning before using them as setting one or another may significantly increase in ram or CPU usage.

Writing a FreeRTOS task routine

If you are familiar with the RTOS concept, you know that the program written for FreeRTOS is organized as a set of independent tasks. Each task normally is not directly related to other tasks and run within its context. Practically speaking, the task is a function with its own stack and runs a separate small program.

When multiple tasks are created, a scheduler switches between tasks according to assigned priorities. The task itself is a function with an endless loop, which never returns from it:

void vATaskFunction( void *pvParameters )
{
    for( ;; )
    {
        -- Task application code here. --
    }
}

I put all my tasks in separate source file mytasks.c (and mytasks.h).

Let’s write a simple LED flasher task. This is a basic routine that flashes LED every 1s. This is how the tasks look like:

void vLEDFlashTask( void *pvParameters )
{
  portTickType xLastWakeTime;
  const portTickType xFrequency = 1000;
  xLastWakeTime=xTaskGetTickCount();
    for( ;; )
    {
      LEDToggle(5);
      vTaskDelayUntil(&xLastWakeTime,xFrequency);
    }
}

To set the timing, we are using the vTaskDelayUntil function. FreeRTOS counts ticks every time scheduler is called (every 1ms by default). By setting the frequency value to 1000, we are getting a 1s delay.

Sending messages between FreeRTOS tasks

Another task can be checking the button state. To pass the button state, we do not need to use any global variables. It is better to keep the code modular and independent for better consistency and security.  FreeRTOS has unique means for this – binary semaphores. They are convenient when we need to send a binary value between tasks. In my example, I declared semaphore handlers for each button:

staticxSemaphoreHandle xButtonWakeupSemaphore = NULL;
staticxSemaphoreHandle xButtonTamperSemaphore = NULL;
staticxSemaphoreHandle xButtonUser1Semaphore = NULL;
staticxSemaphoreHandle xButtonUser2Semaphore = NULL;

Then within vButtonCheckTask before the main loop, I created all semaphores for later use:

vSemaphoreCreateBinary(xButtonWakeupSemaphore);
vSemaphoreCreateBinary(xButtonTamperSemaphore);
vSemaphoreCreateBinary(xButtonUser1Semaphore);
vSemaphoreCreateBinary(xButtonUser2Semaphore);

Adding more FreeRTOS tasks to the code

Once semaphores are created, we can start using them to send messages from one want FreeRTOS task to another. There are few special functions available for manipulating semaphores. In this tutorial, we are going to use two of them: Give and Take. When we check a button state, and if it was pressed, we give semaphore, which is setting the boolean value to ‘1’:

if (ButtonRead(BWAKEUPPORT, BWAKEUP)==pdTRUE)
       {
         count++;
         if(count==DEBOUNCECOUNTS)
           {
             xSemaphoreGive(xButtonWakeupSemaphore);
             count = 0;
           }
       }

Semaphores stay set until they are taken. Other tasks have to use the Take function to reset the semaphore. For this, I created another task which toggles a particular LED when the button is pressed:

void vButtonLEDsTask( void *pvParameters )
{
  portTickType xLastWakeTime;
  const portTickType xFrequency = 100;
  xLastWakeTime=xTaskGetTickCount();
  for( ;; )
  {
      if((xButtonWakeupSemaphore!=NULL))
      {
         if (xSemaphoreTake(xButtonWakeupSemaphore, (portTickType)10)==pdTRUE)
             {
               LEDToggle(1);
               //give semaphore back
               xSemaphoreGive(xButtonWakeupSemaphore);
             }
      }
    vTaskDelayUntil(&xLastWakeTime,xFrequency);
  }
}

Of course, in this shortened version of the task function, you can see how a single semaphore is taken. The If statement checks if semaphore was given. If the condition is met, then the LED is toggled. The XsemaphoreTake function also resets semaphore automatically.
As you can see, after toggling the LED I give semaphore back because I may also detect button press within another task – a vLCDTask:

void vLCDTask( void *pvParameters )
{
  extern uint8_t Image_Table[];
  portTickType xLastWakeTime;
  const portTickType xFrequency = 100;
  xLastWakeTime=xTaskGetTickCount();
  LCD_SetDisplayWindow(00, 00, 239, 319);
  LCD_Clear(Black);
  LCD_DisplayStringLine(Line4,  (char*)pcLCDTaskStartMsg, White, Black);
  for(;;)
    {
      if((xButtonWakeupSemaphore!=NULL)&&(xButtonTamperSemaphore!=NULL)
       &&(xButtonUser1Semaphore!=NULL)&&(xButtonUser2Semaphore!=NULL))
      {
         if (xSemaphoreTake(xButtonWakeupSemaphore, (portTickType)10)==pdTRUE)
             {
             LCD_Clear(Blue);
             LCD_WriteBMP_Dim(30, 30, 210, 210, Image_Table);
             }
      }
    vTaskDelayUntil(&xLastWakeTime,xFrequency);
    }
}

With button clicks, I trigger different things that are displayed on the LCD. You have to be careful when using such a technique because you can’t predict that the LCD task will be called exactly after the LED task. It can happen that first will be LCD task and therefore LED won’t be toggled as semaphore will be already taken. Probably, it would be better to create two semaphores for each task.

Thre USART task in FreeRTOS

The last task I included in this FreeRTOS tutorial is the USART task, which sends and receives messages via terminal. With USART, things start to be more complicated because we don’t want to miss any received character to ensure that all messages have been sent correctly. This can be achieved by using interrupt-based transferring. Doing this within the task where the scheduler runs every 1m would undoubtedly lead to data loss. Another issue is to ensure proper buffering so that the data could be retained until the task is called. Who knows, there might be 100 characters received before the task even runs and processes the data. So there is another helpful FreeRTOS feature – the queues. The queues are like predefined lengths buffers capable of storing any messages. To start using queues, we need to declare two queue handlers:

xQueueHandle RxQueue, TxQueue;

Where one is for receiving and another for transmitting, within a task, we can create both queues with xQueueCreate function, where we put queue length as the first parameter and size of the message, which, in our case, will be char size (8-bit).

void vUSARTTask( void *pvParameters )
{
  portTickType xLastWakeTime;
  const portTickType xFrequency = 50;
  xLastWakeTime=xTaskGetTickCount();
  char ch;
  // Create a queue capable of containing 128 characters.
  RxQueue = xQueueCreate( configCOM0_RX_BUFFER_LENGTH, sizeof( portCHAR ) );
  TxQueue = xQueueCreate( configCOM0_TX_BUFFER_LENGTH, sizeof( portCHAR ) );
  USART1PutString(pcUsartTaskStartMsg,strlen( pcUsartTaskStartMsg ));
  for( ;; )
  {
      //Echo back
      if (Usart1GetChar(&ch))
        {
          Usart1PutChar(ch);
        }
      vTaskDelayUntil(&xLastWakeTime,xFrequency);
  }
}

The Usart1GetChar function pulls char value out of the queue as follows:

uint32_t Usart1GetChar(char *ch){
  if(xQueueReceive( RxQueue, ch, 0 ) == pdPASS)
    {
      return pdTRUE;
    }
  return pdFALSE;
}

The Usart1PutChar FreeRTOS task sends char to queue:

uint32_t Usart1PutChar(char ch)
{
  if( xQueueSend( TxQueue, &ch, 10 ) == pdPASS )
    {
      USART_ITConfig(USART1, USART_IT_TXE, ENABLE);
      return pdTRUE;
    }else{
     return pdFAIL;
    }
}

The rest load is left for the interrupt handler, which responds to interrupt requests and sends bytes from TxQueue or receives and places them to RxQueue.

void USART1_IRQHandler(void)
{
  long xHigherPriorityTaskWoken = pdFALSE;
  uint8_t ch;
  //if Receive interrupt
  if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
    {
      ch=(uint8_t)USART_ReceiveData(USART1);
      xQueueSendToBackFromISR( RxQueue, &ch, &xHigherPriorityTaskWoken );
    }
  if (USART_GetITStatus(USART1, USART_IT_TXE) != RESET)
        {
      if( xQueueReceiveFromISR( TxQueue, &ch, &xHigherPriorityTaskWoken ) )
        {
          USART_SendData(USART1, ch);
        }else{
           //disable Transmit Data Register empty interrupt
           USART_ITConfig(USART1, USART_IT_TXE, DISABLE);
             }
        }
  portEND_SWITCHING_ISR( xHigherPriorityTaskWoken );
}

It is necessary to know that special queue handling functions have to be used inside interrupt handlers, such as xQueueReceiveFromISR and xQueueSentoBackFromISR.

Running all FreeRTOs tasks in the main routine

Once all tasks a prepared, we can add them to the scheduler queue in our main source file:

//STM32F103ZET6 FreeRTOS Test
#include "stm32f10x.h"
//#include "stm32f10x_it.h"
#include "mytasks.h"
//task priorities
#define mainLED_TASK_PRIORITY          ( tskIDLE_PRIORITY )
#define mainButton_TASK_PRIORITY                   ( tskIDLE_PRIORITY )
#define mainButtonLEDs_TASK_PRIORITY                   ( tskIDLE_PRIORITY + 1 )
#define mainLCD_TASK_PRIORITY                   ( tskIDLE_PRIORITY )
#define mainUSART_TASK_PRIORITY                   ( tskIDLE_PRIORITY )
#define mainLCD_TASK_STACK_SIZE configMINIMAL_STACK_SIZE+50
#define mainUSART_TASK_STACK_SIZE configMINIMAL_STACK_SIZE+50
int main(void)
{
  //init hardware
  LEDsInit();
  ButtonsInit();
  LCD_Init();
  Usart1Init();
  xTaskCreate( vLEDFlashTask, ( signed char * ) "LED", configMINIMAL_STACK_SIZE, NULL, mainLED_TASK_PRIORITY, NULL );
  xTaskCreate( vButtonCheckTask, ( signed char * ) "Button", configMINIMAL_STACK_SIZE, NULL, mainButton_TASK_PRIORITY, NULL );
  xTaskCreate( vButtonLEDsTask, ( signed char * ) "ButtonLED", configMINIMAL_STACK_SIZE, NULL, mainButtonLEDs_TASK_PRIORITY, NULL );
  xTaskCreate( vLCDTask, ( signed char * ) "LCD", mainLCD_TASK_STACK_SIZE, NULL, mainLCD_TASK_PRIORITY, NULL );
  xTaskCreate( vUSARTTask, ( signed char * ) "USART", mainUSART_TASK_STACK_SIZE, NULL, mainUSART_TASK_PRIORITY, NULL );
  //start scheduler
  vTaskStartScheduler();
  //you should never get here
  while(1)
    { }
}

The optimizing of the stack size for the FreeRTOS task

As you can see, we have created 5 tasks. Each of them has a priority level and stack size. The hardest part is defining proper stack size – if it’s too small, it may crash your program; if it’s too large, then we are wasting limited resources of the microcontroller. To detect stack overflow, you can use a particular function called

voidvApplicationStackOverflowHook( xTaskHandle *pxTask, signed portCHAR *pcTaskName )

In this function, you can set up any indicators such as LED flash whenever stack overflow occurs. This way, you can leverage the stack size for the task and start over.

This FreeRTOS tutorial was written some time ago (updated in June 2019), and some parts of the code may be used differently today, according to the latest FreeRTOS builds.

Working source code from Codesourcery with Eclipse IDE is here: STM32F103ZET6FreeRTOS

5 Comments:

  1. پروژه الکترونیک

    Really amazing!

    I work with STM32F103RBT6 chip and will try this,
    although its RAM is 20 kB.

    Thank you

  2. thank you for your good work
    i am working on stm32rbt6 and i am using freertos.there were many concepts in defining and using freertos on a project.for example when you define a task and you expect that it works regularly , you must set scheduler setting for type of doing this task.
    i have got some questions:
    1.how can i set the timing for doing my tasks?

    in freertos , interrupts and semaphores are coupled together and everytime i want to use ISR , i must set semaphores to handle it.

    2.can you explain the way we set sempahores to achieve good performance of ISR?

    i want to launch an alphanumerical LCD to freertos . i have designed a lcd.c and added it to freertos stm32 based project in lcd task but it doesnt play the defined tasks.

    3.how can we define the scheduler for doing tasks that we define it ourself?

  3. Thank you very much, excelent work!

  4. Can u please help me to write a task in RTOS for blinking LED. I am having STM32F microcontroller and am using STM324*G-Eval folder from free rtos

  5. Hi ,
    Good work !!
    But i still have few questions :

    1- Why to use 2 queues instead of only one queue because we can send data from isr to rxqueue then we can get it back from this same rxqueue to transmit it via uart ?

    2- Which priority have you chosen for the uart ? The lowest one meaning 15 ??

    3- I get a problem with this function : if(xQueueReceive( RxQueue, ch, 0 ) == pdPASS) . I could never get the pdPASS as result . What could be the problem ?

    Thanks again,

Leave a Reply