FreeRTOS in STM32 -#1 (getting started)
The development environment of this tutorial is as follows:
- Software: MDK Keil, CubeMX (V6.1.2), VSCode (only as code editor)
- Hardware: STM32F4VET6 development board (other development boards can also be used, and the principle is the same)
By default, the reader of this tutorial has a certain STM32 programming foundation and is familiar with the use of CubeMX. Some operation details are only text prompts or omitted.
RTOS (Real Time Operating System), as its name implies, can handle tasks like an operating system (such as Windows). The main purpose of the operating system is to "handle multiple tasks at the same time", which can not be realized in the traditional "main while (1) {...}" programming.
This tutorial is the first in a series of tutorials and mainly includes the following contents:
(1) Configure Free RTOS using CubeMX;
(2) Advantages of using Free RTOS;
(3) Create tasks by "using CubeMX" or "not using CubeMX";
(4) Use task priorities to solve some common problems.
Now start configuring CubeMX:
Configuring CubeMX
After selecting the target chip model, CubeMX will open the default page for you. Now select FREERTOS and follow the screenshot below
Here we choose CMSIS_V1, because most STM32 chip models support this version.
Next, go to the tasks and queues tab, where you will see that the software has automatically created a default task. Double click the default task to see the following information:
As you can see, a task contains many settings, but don't be intimidated. In this tutorial, we only focus on taskname, priority and entry function.
Now, we will create a task here, and the following are the properties of the task
The task name can be set to normal priority by default, and the entry function remains the default. Once we write the program, you will have a deeper understanding of these settings.
**Note: * * once the real-time operating system is used, we cannot use systick as the time base of HAL library code (because it is occupied by the operating system). Therefore, go to SYS and select TIM1 as the time base as shown below
In addition, I use UART-1 to transmit data, and PA6 and PA7 are used as push-pull output pins (these two pins are the pins of LED-0 and LED-1 on the tutorial demonstration development board. Please set them according to the pin of LED on your own development board). After the code is generated, open the main.c file. Now it's time to understand the importance of using RTOS.
Advantages of using RTOS
Generally, the first example of learning MCU programming is to control LED flashing. Now suppose there is a scenario where the flashing cycle of LED-0 is 100ms and the flashing cycle of LED-1 is 300ms. This programming can be done in the While cycle and is relatively easy because the two cycles are integer multiples. However, if our demand is changed to: the flashing cycle of LED-0 is 100ms, and the general state of LED-1 is off. When a key is pressed, LED1 is on, and LED1 is off after a delay of 5 seconds. During this period, the serial port outputs the on / off status of the current LED-1 every 1 second. At this time, the situation becomes complicated. To complete the requirements perfectly, you still have to spend some time and energy arranging the code. In order to make CPU resource scheduling easier, it is necessary for us to learn to use real-time operating system.
In this tutorial, we will not cover the above "complex" requirements. Through the above steps, we have created two tasks. Let's take defaultTask as an example to see how CubeMX defines a task.
// Reference header file /* Includes ------------------------------------------------------------------*/ #include "FreeRTOS.h" #include "task.h" #include "main.h" #include "cmsis_os.h" // Define task handle osThreadId defaultTaskHandle; // Prototype of task function void StartdefaultTask(void const * argument); // FREERTOS initialization function void MX_FREERTOS_Init(void) { /*......Other codes*/ // Create task /* Create the thread(s) */ /* definition and creation of defaultTask */ osThreadDef(defaultTask, StartdefaultTask, osPriorityNormal, 0, 128); defaultTaskHandle = osThreadCreate(osThread(defaultTask), NULL); /*......Other codes*/ } /* USER CODE END Header_StartdefaultTask */ void StartdefaultTask(void const * argument) { /* USER CODE BEGIN StartdefaultTask */ /* Infinite loop */ for(;;) { osDelay(1); } /* USER CODE END StartdefaultTask */ }
It can be seen that defining a task requires the following things
- Import header file
- Define task handle
- Define the task function. Like other functions, write the function prototype in front of the file before use
- In the FreeRTOS initialization function, bind the task handle to the task function, and then use the task handle to operate the task
Add the code to control the LED flashing in the task function. In order to test the task scheduling performance, we set the flashing period as 1ms
void StartdefaultTask(void const * argument) { /* USER CODE BEGIN 5 */ /* Infinite loop */ for(;;) { HAL_GPIO_TogglePin(BSP_LED0_GPIO_Port,BSP_LED0_Pin); osDelay(1); } /* USER CODE END 5 */ } /* USER CODE BEGIN Header_Task2_init */ /** * @brief Function implementing the Task2 thread. * @param argument: Not used * @retval None */ /* USER CODE END Header_Task2_init */ void StartTask02(void const * argument) { /* USER CODE BEGIN Task2_init */ /* Infinite loop */ for(;;) { HAL_GPIO_TogglePin(BSP_LED1_GPIO_Port,BSP_LED1_Pin); osDelay(1); } /* USER CODE END Task2_init */ }
We will flip the level of pin LED-0(PA6) in defaultTask and pin LED-1(PA7) in task 2. In this way, the RTOS scheduler will schedule time for these two tasks so that they have enough time to execute. When the above code is executed, the waveform of the wave generator is shown in the following figure (yellow - PA6, green PA7)
It can be seen that the pins of the two LED s flip almost at the same time, but after amplification, it can be seen that there is still a time difference of about 25 microseconds on the rising edge, because in the macro, the execution of instructions is simultaneous, but in the micro, or in the instruction execution time scale, the single-chip computer still executes instructions one by one. System task transfer will also consume a certain amount of time, so it is absolutely impossible at the same time. Therefore, when I introduced RTOS at the beginning of the article, I added quotation marks to "simultaneous".
Create a task manually
**Prompt: * * the following operations are carried out in the "freertos.c" file. Please refer to the code given by CubeMX for the writing position of the code, and then write it in the user-defined code area near the corresponding code, otherwise the code written by yourself may be cleared when the software generates the code again.
Although it is convenient for CubeMX to generate code, the disadvantage is the lack of flexibility. For example, after changing the name of the entry function of the created task in CubeMX, regenerate the code, and all the code written in the original task function will be cleared. In other words, once the task is created, the entry function name is not easy to change (subsequent versions may optimize this problem).
Let's manually create a custom Task. To create a new Task, you need to follow some steps, as shown below:
(1) define a ThreadID (thread ID, equivalent to the ID number) for the task. Once created, this variable stores the unique ID of the task. Later, all operations will need this ID.
/* USER CODE END Variables */ osThreadId myTask03Handle; // Define the task 3 handle and write it according to the code generated by the software
(2) Define the entry function of the task. This is the task function. In the future, the code of this task will be written in it. Note that tasks in Free RTOS are not designed to handle any return values. Therefore, the entry function should contain an infinite loop (for or while), and the whole program should be written in this loop.
// Don't forget to write the declaration of this function in front of the task implementation void StartTask03 (void const * argument) { while (1) { // do something osDelay (1000); // 1 sec delay } }
(3) In the * * MX_FREERTOS_Init(void) * * initialization function, we need to create a task (similar to object instantiation in PC programming).
osThreadDef(myTask03, StartTask03, osPriorityBelowNormal, 0, 128); myTask03Handle = osThreadCreate(osThread (myTask03), NULL);
In the FreeRTOS native function, only one line of code is required, but in the library encapsulated by CMSIS, there are two lines of code with the same function. For details, see the two macros osThreadDef and osThreadCreate.
- osThreadDef is a macro. Its essence is to define a structure variable and assign values to the new structure variable with the parameters in parentheses, i.e. task name, entry function, priority, instance and stack size.
- osThreadCreate is also a macro. After defining a task, it is used to create a thread for the task as the name suggests, and assign the ID of the task to myTask03Handle.
Processing priorities in FreeRTOS
- So far, we know how to use RTOS for multitasking. But there are some problems. In the above oscilloscope waveform, we can see that the pin of LED-0 always turns over first than that of LED-1. What if we want to turn over the pin of LED-1 first?
- Furthermore, suppose we want all three tasks to send some data through UART1 at the same time. What should we do?
- When we write a program to do this, the results will not be exactly the same. On the contrary, serial port transmission will be carried out in such a way that one task will send data in one second, another task will send data in another second, and so on.
- This happens when we try to use shared resources between tasks with the same priority. The second task must wait for the first task to complete its execution, and then only control enters it. Similarly, the third task will wait for the second task to complete. That is, tasks cannot interrupt each other, but can only be queued for execution.
Because the delay of 1ms is too short for serial port output, please change the delay in the task function to 1000ms before starting the following steps, that is, osdelay (1); - > osdelay (1000);
To avoid these situations, we use different priorities for different tasks. This means that we must redefine the task priorities in the main functions (only modify the priorities).
/* definition and creation of defaultTask */ osThreadDef(defaultTask, StartDefaultTask, osPriorityNormal, 0, 128); defaultTaskHandle = osThreadCreate(osThread(defaultTask), NULL); /* definition and creation of myTask02 */ osThreadDef(myTask02, StartTask02, osPriorityAboveNormal, 0, 128); myTask02Handle = osThreadCreate(osThread(myTask02), NULL); /* USER CODE BEGIN RTOS_THREADS */ /* add threads, ... */ osThreadDef(myTask03, StartTask03, osPriorityBelowNormal, 0, 128); myTask03Handle = osThreadCreate(osThread (myTask03), NULL); /* USER CODE END RTOS_THREADS */
Redirect printf function for serial port output:
#include <stdio.h> // Reference header file int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff); return ch; }
In each task implementation function, add the following code:
// Task 1 printf("Task1 is going"); // Task 2 printf("Task2 is going"); // Task 3 printf("Task3 is going");
- Now task 2 has the highest priority, higher than the default task, and task 3 has the lowest priority.
- When the program is running, first execute task 2, then execute the default task (task 1), and finally execute task 3.
- These three tasks send data at the same time.
Please note that the sending time of serial port data is almost one second, and the three tasks are executed in turn.
[end of this tutorial]