The best tools to make your project dreams come true

Login or Signup


By ShawnHymel

Getting Started with STM32 - Introduction to FreeRTOS

FreeRTOS is a free and open source real-time operating system (RTOS) that runs on many popular microcontrollers, including STM32. In 2017, Amazon took control of the FreeRTOS project and now provides regular maintenance and support.

If you have not set up STM32CubeIDE with your Nucleo board, you will need to do so following the steps outlined in this tutorial.

If video is your preferred medium, check out this video for how to use FreeRTOS and CMSIS-RTOS with STM32:

 

What is an RTOS?

An operating system (OS) is a piece of software that manages other software and hardware resources in a computer system. You are probably familiar with most of the popular general purpose operating systems, such as Windows, macOS, Linux, iOS, and Android. A general purpose OS is normally designed with a focus on user experience. 

For example, let’s say we’re developing an application on an operating system for a phone, like Android or iOS. A user might want to stream a movie, so we can break that streaming experience into two jobs: downloading chunks of video from the Internet (job 1) and displaying each chunk to the user (job 2). These jobs might be part of the same program, in which case, they might be implemented as concurrently running threads.

If our processor only has 1 core available, our streaming application might need to jump between job 1 and job 2 rapidly to give the user the impression that downloading and viewing is happening at the same time.

General purpose OS

Note that I’ve listed a third job here: the underlying OS software. The OS is in charge of figuring out which job needs to run in order to give the user a seamless experience. This swapping of jobs is known as context switching and incurs some processing overhead as the OS needs to save things like registers, memory, and program counter so that it can return to a previously running thread without losing any information.

A job might be a little late, a packet might be lost, or a frame might be dropped so that the video can catch up. Since the focus is on user experience in a general purpose OS, these minor variances in job execution will likely go unnoticed by the user, or the user might not care (too much).

The focus changes in most microcontroller applications. Often, meeting strict timing deadlines is critically important. Think about a motor controller or an automatic braking system in an electric vehicle. In these cases, if timing is off by even a few milliseconds, human lives could be in danger. As a result, an RTOS would be your best bet to manage several jobs running concurrently.

In this second example, a vehicle’s electronic control unit (ECU) might be in charge of controlling and assisting with braking. Job 2 monitors the driver’s input and helps apply the brakes and turns on the tail lights. However, let’s say that our ECU gets notification that the car’s sensors have detected an impending crash. As a result, job 1 will preempt job 2 to control the brakes. This all assumes, of course, that this is a vehicle equipped with automatic braking assist.

Real Time Operating System example

Keep in mind this is a fairly simple example. Hardware interrupts can also be used to preempt running tasks. An RTOS allows you to create software jobs instead of relying on hardware interrupts and assign them priorities. Additionally, most RTOSes can also act as an abstraction layer, allowing you to write code that can be easily ported to other microcontrollers.

FreeRTOS vs. CMSIS-RTOS

It’s important to understand how STM32CubeIDE has bundled FreeRTOS. While FreeRTOS is an underlying software framework that allows for switching tasks, scheduling, etc., we won’t be making calls to FreeRTOS directly.

ARM has created the CMSIS-RTOS library, which allows us to make calls to an underlying RTOS, thus improving the portability of code among various ARM processors. This image describes how ARM’s CMSIS libraries interact with third party software:

ARM CMSIS-RTOS library

Image credits: ARM CMSIS overview

FreeRTOS is our “RTOS Kernel” as depicted in the diagram. We will be making calls to CMSIS-RTOS (version 2, specifically) in order to control the underlying FreeRTOS.

Required Hardware

Most Nucleo boards should work, but this tutorial will showcase the Nucleo-L476RG: https://www.digikey.com/short/pbm7c8

Enable FreeRTOS in STM32CubeIDE

Create a new C project for your Nucleo board and give it a name, like nucleo-l476rg-freertos-blinky. In CubeMX, go to Categories > Middleware > FREERTOS. Under Mode, change Interface to CMSIS_V2.

STM32CubeIDE

In the Configuration pane, under Tasks and Queues, double-click on the default task to make changes. Change the Task Name to blink01 and change the Entry Function to StartBlink01

STM32CubeIDE

Click OK and click Add to create a new task. Change the task name to blink02, set the priority to osPriorityBelowNormal, and change the entry function name to StartBlink02. Note that the priority of task 2 is below that of task 1, which means that in the event of blink01 and blink02 trying to run concurrently, blink01 will take priority.

STM32CubeIDE

SysTick is a special timer in most ARM processors that’s generally reserved for operating system purposes. By default, SysTick in an STM32 will trigger an interrupt every 1 ms. If we’re using the STM32 HAL, by default, SysTick will be used for things like HAL_Delay() and HAL_GetTick(). As a result, the STM32 HAL framework gives SysTick a very high priority. However, FreeRTOS needs SysTick for its scheduler, and it requires SysTick to be a much lower priority.

We can fix this conflict in a few ways, but the easiest is to assign another, unused timer as the timebase source for HAL. Timers 6 and 7 in most STM32 microcontrollers are usually very basic, which makes them perfect as a timebase for HAL. Go to System Core > SYS and under Mode, change the Timebase Source to TIM6.

STM32CubeIDE

Save and generate code.

Write Concurrent Threads

What we referred to as “jobs” earlier could theoretically be anything that accomplishes our desired task: separate programs, threads, etc. You should keep a few terminology differences in mind when working with FreeRTOS. A “task” in FreeRTOS is a piece of a program that can run concurrently with other pieces in the same program. It’s analogous to “threads,” if you’ve done other concurrent programming. However, note that CMSIS-RTOS, the abstraction layer for our RTOS, refers to these concurrent pieces as “threads.” As a result, you might see them used interchangeably throughout the program, even if they don’t quite mean the same thing.

Change main.c to the following:

Copy Code
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body
******************************************************************************
* @attention
*
* <h2><center>&copy; Copyright (c) 2019 STMicroelectronics.
* All rights reserved.</center></h2>
*
* This software component is licensed by ST under BSD 3-Clause license,
* the "License"; You may not use this file except in compliance with the
* License. You may obtain a copy of the License at:
* opensource.org/licenses/BSD-3-Clause
*
******************************************************************************
*/
/* USER CODE END Header */

/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "cmsis_os.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */

/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/
UART_HandleTypeDef huart2;

osThreadId_t blink01Handle;
osThreadId_t blink02Handle;
/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_USART2_UART_Init(void);
void StartBlink01(void *argument);
void StartBlink02(void *argument);

/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */

/* USER CODE END 1 */


/* MCU Configuration--------------------------------------------------------*/

/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();

/* USER CODE BEGIN Init */

/* USER CODE END Init */

/* Configure the system clock */
SystemClock_Config();

/* USER CODE BEGIN SysInit */

/* USER CODE END SysInit */

/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART2_UART_Init();
/* USER CODE BEGIN 2 */

/* USER CODE END 2 */

osKernelInitialize();

/* USER CODE BEGIN RTOS_MUTEX */
/* add mutexes, ... */
/* USER CODE END RTOS_MUTEX */

/* USER CODE BEGIN RTOS_SEMAPHORES */
/* add semaphores, ... */
/* USER CODE END RTOS_SEMAPHORES */

/* USER CODE BEGIN RTOS_TIMERS */
/* start timers, add new ones, ... */
/* USER CODE END RTOS_TIMERS */

/* USER CODE BEGIN RTOS_QUEUES */
/* add queues, ... */
/* USER CODE END RTOS_QUEUES */

/* Create the thread(s) */
/* definition and creation of blink01 */
const osThreadAttr_t blink01_attributes = {
.name = "blink01",
.priority = (osPriority_t) osPriorityNormal,
.stack_size = 128
};
blink01Handle = osThreadNew(StartBlink01, NULL, &blink01_attributes);

/* definition and creation of blink02 */
const osThreadAttr_t blink02_attributes = {
.name = "blink02",
.priority = (osPriority_t) osPriorityBelowNormal,
.stack_size = 128
};
blink02Handle = osThreadNew(StartBlink02, NULL, &blink02_attributes);

/* USER CODE BEGIN RTOS_THREADS */
/* add threads, ... */
/* USER CODE END RTOS_THREADS */

/* Start scheduler */
osKernelStart();

/* We should never get here as control is now taken by the scheduler */

/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */

/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}

/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};

/** Initializes the CPU, AHB and APB busses clocks
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI;
RCC_OscInitStruct.PLL.PLLM = 1;
RCC_OscInitStruct.PLL.PLLN = 10;
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7;
RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2;
RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB busses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK)
{
Error_Handler();
}
PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART2;
PeriphClkInit.Usart2ClockSelection = RCC_USART2CLKSOURCE_PCLK1;
if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
{
Error_Handler();
}
/** Configure the main internal regulator output voltage
*/
if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK)
{
Error_Handler();
}
}

/**
* @brief USART2 Initialization Function
* @param None
* @retval None
*/
static void MX_USART2_UART_Init(void)
{

/* USER CODE BEGIN USART2_Init 0 */

/* USER CODE END USART2_Init 0 */

/* USER CODE BEGIN USART2_Init 1 */

/* USER CODE END USART2_Init 1 */
huart2.Instance = USART2;
huart2.Init.BaudRate = 115200;
huart2.Init.WordLength = UART_WORDLENGTH_8B;
huart2.Init.StopBits = UART_STOPBITS_1;
huart2.Init.Parity = UART_PARITY_NONE;
huart2.Init.Mode = UART_MODE_TX_RX;
huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart2.Init.OverSampling = UART_OVERSAMPLING_16;
huart2.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE;
huart2.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT;
if (HAL_UART_Init(&huart2) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN USART2_Init 2 */

/* USER CODE END USART2_Init 2 */

}

/**
* @brief GPIO Initialization Function
* @param None
* @retval None
*/
static void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};

/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOH_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();

/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, GPIO_PIN_RESET);

/*Configure GPIO pin : B1_Pin */
GPIO_InitStruct.Pin = B1_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(B1_GPIO_Port, &GPIO_InitStruct);

/*Configure GPIO pin : LD2_Pin */
GPIO_InitStruct.Pin = LD2_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(LD2_GPIO_Port, &GPIO_InitStruct);

}

/* USER CODE BEGIN 4 */

/* USER CODE END 4 */

/* USER CODE BEGIN Header_StartBlink01 */
/**
* @brief Function implementing the blink01 thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartBlink01 */
void StartBlink01(void *argument)
{
/* USER CODE BEGIN 5 */
/* Infinite loop */
for(;;)
{
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
osDelay(500);
}

// In case we accidentally exit from task loop
osThreadTerminate(NULL);

/* USER CODE END 5 */
}

/* USER CODE BEGIN Header_StartBlink02 */
/**
* @brief Function implementing the blink02 thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartBlink02 */
void StartBlink02(void *argument)
{
/* USER CODE BEGIN StartBlink02 */
/* Infinite loop */
for(;;)
{
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
osDelay(600);
}

// In case we accidentally exit from task loop
osThreadTerminate(NULL);

/* USER CODE END StartBlink02 */
}

/**
* @brief Period elapsed callback in non blocking mode
* @note This function is called when TIM6 interrupt took place, inside
* HAL_TIM_IRQHandler(). It makes a direct call to HAL_IncTick() to increment
* a global variable "uwTick" used as application time base.
* @param htim : TIM handle
* @retval None
*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
/* USER CODE BEGIN Callback 0 */

/* USER CODE END Callback 0 */
if (htim->Instance == TIM6) {
HAL_IncTick();
}
/* USER CODE BEGIN Callback 1 */

/* USER CODE END Callback 1 */
}

/**
* @brief This function is executed in case of error occurrence.
* @retval None
*/
void Error_Handler(void)
{
/* USER CODE BEGIN Error_Handler_Debug */
/* User can add his own implementation to report the HAL error return state */

/* USER CODE END Error_Handler_Debug */
}

#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(char *file, uint32_t line)
{
/* USER CODE BEGIN 6 */
/* User can add his own implementation to report the file name and line number,
tex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/

In the beginning of main(), you should see the threads being defined for us, as set up by CubeMX. Notice that we pass our entry function names into the osThreadNew() function, which will call these functions as soon as we call osKernelStart(). Once osKernelStart() has been called, we do not want to have any code after it in main(), as the program should, ideally, never return from osKernelStart(). 

At this point, our threads should be running simultaneously, with their own setup code and forever while loops. There is also a background scheduler task that runs, which is in charge of switching context between our threads.

StartBlink01() and StartBlink02() are our threads. Each has its own forever loops, and they should run concurrently. While they can’t take up the same space and time in our single-core processor, the scheduler will switch them in and out to give the appearance that we’re running 2 threads at the same time.

Note that instead of HAL_Delay(), we need to use osDelay(). HAL_Delay() in a high priority task might hog the processor, preventing a context switch. It also prevents the scheduler from idling, which could save on power. So, we use osDelay() to tell the scheduler that it’s OK to switch to a different task while we wait.

We are using different delay times for blink01 and blink02 in order to have the two tasks fight over toggling the LED.

Test It!

Build the project and start a debugging session. Press the Resume button to begin running the program on your Nucleo board. You should see the LD2 LED blinking on and off, but at a varying duty cycle that increases or decreases in distinct steps. This is a result of the two threads with different wait times.

STM32 Nucleo blinking LED

Resources

Play around with some of the features in FreeRTOS and CMSIS-RTOS to see what you can do!

Download STM32CubeIDE: https://www.st.com/en/development-tools/stm32cubeide.html

Key Parts and Components

Add all Digi-Key Parts to Cart
  • 497-15881-ND
  • AE9929-ND