In our previous FreeRTOS tutorial, we discussed the fundamentals of RTOS (Real Timer Operating System) and learned how to get started with FreeRTOS in STM32 Microcontroller. In this tutorial, we will discuss about the FreeRTOS Task Scheduling and how the Task Scheduling works. To demonstrate the process we will write some example code in STM32CubeIDE for the STM32 Nucleo development board.
Understanding FreeRTOS Task Scheduling
In the FreeRTOS, tasks are the building blocks of execution. Each task represents a specific function or operation within the system. To manage these tasks effectively, FreeRTOS employs a priority-based preemptive scheduling approach. This means tasks with higher priorities take precedence over lower-priority tasks, ensuring critical operations are handled promptly.
Tasks in FreeRTOS are designed for preemptive multitasking, allowing the system to switch between tasks based on their priorities. This mechanism ensures that the processor is allocated to the most critical task at any given time, optimizing system responsiveness.
How the Task Scheduling works
Let’s consider an example to understand how Task Scheduling works. Imagine we have three tasks (task_1, task_2, and task_3). Let’s say task_1 and task_2 have the same priority and task_3 has a higher priority than the other two. Each of our three tasks runs in a while forever loop.
In the above diagram, let’s imagine we have a one-core processor and look at processor unilization over time which moves from left to write. We divide the processor unilization time among the different tasks that we showed in the diagram which is called the time slicing in FreeRTOS.
Assume one of our hardware timers interrupts the processor every one milliseconds which is commonly seen in the FreeRTOS. We listed priority slots as rows (low to high) in the diagram and tasks will run in those slots depending on the priority we have given them. The operating system will run every time slice (1 ms) to figure out which task to schedule next. This interval of 1 millisecond is also known as “tick”. The scheduler decides which task to run and chooses the one with the highest priority.
From our diagram, we assumed task_1 and task_2 have the same priority and task_3 has a higher priority than the two. Let’s say task_1 and task_2 run first and they execute in a round-robin fashion means task_1 executes first and then task_2. The Operation System needs two ticks for that. After completing the execution, both tasks call the TaskDelay function and go to sleep for one millisecond.
After one millisecond all three tasks are ready to execute but because task_3 has a higher priority than the other two, taks_3 will execute first then task_1 and task_2 respectively. But when task_1 completes the execution a hardware interrupt is ready to run. So, the hardware interrupt runs first because it has the highest priority among all other tasks, as shown in the diagram.
When the hardware interrupt completes its execution the Operating system will return to whichever task was running. After complete execution of the running task, the OS looks again for the higher-priority task to execute first. This is known as preemptive scheduling which we discussed earlier.
FreeRTOS Task states
Tasks in FreeRTOS mainly have four states and they are:
- Ready State: Upon creating a task in FreeRTOS, it transitions into the Ready state, signaling to the scheduler that it’s ready for execution. With each clock tick, the scheduler selects one task in the ready state to run. In a multi-core system, the scheduler can choose multiple tasks per tick, although we will not delve into multi-core systems in this context because our STM32 is a single-core microcontroller.
- Running State: During execution, a task resides in the Running state and can be reverted to the Ready state by the scheduler.
- Block State: Functions that induce task waiting, such as osDelay(), place the task in the Blocked state. In this state, the task awaits the occurrence of specific events, like the expiration of the timer set by osDelay(). Alternatively, the task might be waiting for the release of FreeRTOS resources, such as a semaphore or mutex, by another task. Tasks in the Blocked state enable the execution of other tasks in the meantime.
- Suspended State: a task can enter the Suspended mode through an explicit invocation of vTaskSuspend(), akin to putting the task to sleep. Any task can place another task, including itself, in the Suspended mode. To return a task to the Ready state, another task must explicitly call vTaskResume().
STM32 FreeRTOS TASK Scheduling: Example Code
In this section of the tutorial, we will write some code in STM32CubeIDE for the STM32 NUCLEO–F446RE Development board. You can use any STM32 microcontroller for this project, just set up the project according to your MCU in STM32CubeIDE.
If you have not set up FreeRTOS yet with your STM32 board, you will need to read the following tutorial below:
Required Hardware
Some extremely useful test equipment for troubleshooting electronic circuits
Equipment Name | Links |
---|---|
Best Oscilloscope for Professionals | Amazon |
Best Oscilloscope for Beginners and Students | Amazon |
Best Budget Multimeter | Amazon |
Adjustable Bench Power Supply | Amazon |
Affiliate Disclosure: When you click on links to make a purchase, this can result in this website earning a commission.
Code Explanation
In the below code, we take 2 tasks named as defaultTask and myTask02. myTask02 has a higher priority than the defaultTask . We send some text via UART (UART 2) when each task is active. We will use an open-source free software terminal emulator for Windows called Tera Term to communicate with the STM32 Microcontroller via UART.
Full Code
/* Includes ------------------------------------------------------------------*/ #include "main.h" #include "cmsis_os.h" #include <string.h> /* Private variables ---------------------------------------------------------*/ UART_HandleTypeDef huart2; /* Definitions for defaultTask */ osThreadId_t defaultTaskHandle; const osThreadAttr_t defaultTask_attributes = { .name = "defaultTask", .stack_size = 128 * 4, .priority = (osPriority_t) osPriorityNormal, }; /* Definitions for myTask02 */ osThreadId_t myTask02Handle; const osThreadAttr_t myTask02_attributes = { .name = "myTask02", .stack_size = 128 * 4, .priority = (osPriority_t) osPriorityAboveNormal, }; /* Private function prototypes -----------------------------------------------*/ void SystemClock_Config(void); static void MX_GPIO_Init(void); static void MX_USART2_UART_Init(void); void StartDefaultTask(void *argument); void StartTask02(void *argument); char task1_string[50] = "Task 1 is running.\r\n"; char task2_string[50] = "Task 2 is running.\r\n"; /** * @brief The application entry point. * @retval int */ int main(void) { /* MCU Configuration--------------------------------------------------------*/ /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ HAL_Init(); /* Configure the system clock */ SystemClock_Config(); /* Initialize all configured peripherals */ MX_GPIO_Init(); MX_USART2_UART_Init(); /* Init scheduler */ osKernelInitialize(); /* Create the thread(s) */ /* creation of defaultTask */ defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes); /* creation of myTask02 */ myTask02Handle = osThreadNew(StartTask02, NULL, &myTask02_attributes); /* 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 BEGIN Header_StartDefaultTask */ /** * @brief Function implementing the defaultTask thread. * @param argument: Not used * @retval None */ /* USER CODE END Header_StartDefaultTask */ void StartDefaultTask(void *argument) { /* USER CODE BEGIN 5 */ /* Infinite loop */ for(;;) { HAL_UART_Transmit(&huart2, (uint8_t*) task1_string, strlen(task1_string), HAL_MAX_DELAY); osDelay(1000); } /* USER CODE END 5 */ } /* USER CODE BEGIN Header_StartTask02 */ /** * @brief Function implementing the myTask02 thread. * @param argument: Not used * @retval None */ /* USER CODE END Header_StartTask02 */ void StartTask02(void *argument) { /* USER CODE BEGIN StartTask02 */ /* Infinite loop */ for(;;) { HAL_UART_Transmit(&huart2, (uint8_t*) task2_string, strlen(task2_string), HAL_MAX_DELAY); osDelay(2000); } /* USER CODE END StartTask02 */ }
Please Note: Certain portions of the code, specifically those automatically generated by STM32CubeIDE, have been omitted.
Output of the code
Once the code is uploaded to the microcontroller, you should see that Task 2 is running first following Task 1 because we selected Task 2 as a higher priority than Task 1. After this, both tasks go into the block state (Task 1 for 100 Milliseconds and Task 2 for 200 Milliseconds).
After 100 Milliseconds Task 1 runs and goes to block state again. When another 100 Milliseconds passed both the task ready for execution. This time Task 2 runs first because we set Task 2 higher priority than Task 1. In this schedule both the tasks will run continuously.
STM32 FreeRTOS TASK States and Task Management: Example Code
Code Explanation
In the below code, we create three tasks and use vTaskSuspend(), vTaskResume(), and vTaskDelete() to show how we can manage the different states of the task in runtime. vTaskSuspend() will sleep a task until we call the vTaskResume() function. vTaskDelete() will remove the task from the OS and never get retributed.
Full Code
/* USER CODE END Header */ /* Includes ------------------------------------------------------------------*/ #include "main.h" #include "cmsis_os.h" #include <stdio.h> #include <string.h> /* Private variables ---------------------------------------------------------*/ UART_HandleTypeDef huart2; /* Definitions for defaultTask */ osThreadId_t defaultTaskHandle; const osThreadAttr_t defaultTask_attributes = { .name = "defaultTask", .stack_size = 128 * 4, .priority = (osPriority_t) osPriorityNormal, }; /* Definitions for myTask02 */ osThreadId_t myTask02Handle; const osThreadAttr_t myTask02_attributes = { .name = "myTask02", .stack_size = 128 * 4, .priority = (osPriority_t) osPriorityAboveNormal, }; /* Definitions for myTask03 */ osThreadId_t myTask03Handle; const osThreadAttr_t myTask03_attributes = { .name = "myTask03", .stack_size = 128 * 4, .priority = (osPriority_t) osPriorityLow, }; /* 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 StartDefaultTask(void *argument); void StartTask02(void *argument); void StartTask03(void *argument); /* USER CODE BEGIN PFP */ int count=0; char int_to_str[30]; char* task1_data = "Task one is running\r\n"; char* task2_data = "Task two is running\r\n"; char* task3_data = "Task three is running\r\n"; char* task2_suspended = "Task two suspended\r\n"; char* task2_resume = "Task two resume\r\n"; char* task3_delete = "Task three deleted\r\n"; /* USER CODE END PFP */ /** * @brief The application entry point. * @retval int */ int main(void) { /* MCU Configuration--------------------------------------------------------*/ /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ HAL_Init(); /* Configure the system clock */ SystemClock_Config(); /* Initialize all configured peripherals */ MX_GPIO_Init(); MX_USART2_UART_Init(); /* Init scheduler */ osKernelInitialize(); /* Create the thread(s) */ /* creation of defaultTask */ defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes); /* creation of myTask02 */ myTask02Handle = osThreadNew(StartTask02, NULL, &myTask02_attributes); /* creation of myTask03 */ myTask03Handle = osThreadNew(StartTask03, NULL, &myTask03_attributes); /* 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 BEGIN Header_StartDefaultTask */ /** * @brief Function implementing the defaultTask thread. * @param argument: Not used * @retval None */ /* USER CODE END Header_StartDefaultTask */ void StartDefaultTask(void *argument) { /* USER CODE BEGIN 5 */ /* Infinite loop */ for(;;) { HAL_UART_Transmit(&huart2, (uint8_t*)task1_data, strlen(task1_data), HAL_MAX_DELAY); osDelay(1000); } /* USER CODE END 5 */ } /* USER CODE BEGIN Header_StartTask02 */ /** * @brief Function implementing the myTask02 thread. * @param argument: Not used * @retval None */ /* USER CODE END Header_StartTask02 */ void StartTask02(void *argument) { /* USER CODE BEGIN StartTask02 */ /* Infinite loop */ for(;;) { HAL_UART_Transmit(&huart2, (uint8_t*)task2_data, strlen(task2_data), HAL_MAX_DELAY); osDelay(2000); } /* USER CODE END StartTask02 */ } /* USER CODE BEGIN Header_StartTask03 */ /** * @brief Function implementing the myTask03 thread. * @param argument: Not used * @retval None */ /* USER CODE END Header_StartTask03 */ void StartTask03(void *argument) { /* USER CODE BEGIN StartTask03 */ /* Infinite loop */ for(;;) { HAL_UART_Transmit(&huart2, (uint8_t*)task3_data, strlen(task3_data), HAL_MAX_DELAY); osDelay(1000); count++; sprintf(int_to_str, "Count value: %d\r\n", count); HAL_UART_Transmit(&huart2, (uint8_t*)int_to_str, strlen(int_to_str), HAL_MAX_DELAY); if(count==3){ HAL_UART_Transmit(&huart2, (uint8_t*)task2_suspended, strlen(task2_suspended), HAL_MAX_DELAY); vTaskSuspend(myTask02Handle); } if(count==5){ HAL_UART_Transmit(&huart2, (uint8_t*)task2_resume, strlen(task2_resume), HAL_MAX_DELAY); vTaskResume(myTask02Handle); } if(count==7){ HAL_UART_Transmit(&huart2, (uint8_t*)task3_delete, strlen(task3_delete), HAL_MAX_DELAY); vTaskDelete(myTask03Handle); } } /* USER CODE END StartTask03 */ }
Output of the code
In the below serial terminal output, we can see Task Two is run first following Task One(defaultTask) and Task Three because Task Two has the height priority (osPriorityAboveNormal) then Task One (osPriorityNormal), and Task Three has the lowest priority (osPriorityLow). You can see the priority in the code above.
After that Task One, Task Two, and Task Three are going to block state for 1000 Miliseconds, 2000 Miliseconds, and 1000 Miliseconds respectively. This time Task One ran first because it had higher priority than Task Three Then the scheduler went to Task Three incremented the count value to 1 and printed text inside the task after Task Three completed the execution Task Two ran.
This process is continued until the count value is three. When the count value is three, Task Three suspends Task Two. At that time only Task One and Task Three are active. When the count value goes to five then we resume Task Two from the Task Three function. When the count value is seven Task Three deleted itself. Finally, Task One and Task Two are run. Task One ran twice and then Task One ran once because of the Priroty and delay.