FreeRTOS is a simple way of adding an operating system to your microprocessor projects. It allows you to write more complex programs without worrying about the underlying details. It is quite user friendly and easy to use. However, as with any complex software, there are some details to get just right before it will work as desired. Here I present an example of how two periodic tasks can be created in FreeRTOS. It is intended as a starting point where the basic setup has been completed and working. It is easy to further modify to your own needs. That said, using a RTOS and a multitasking method to accomplish your goals is far from simple, and requires careful design.
Ingredients:
- GCC ARM EMBEDDED is used, running on a STM32F103RB processor.
- The tests were done on a STM32 Nucleo FRB103 board.
- FreeRTOS version is V8.2.1, which is recent at the time of writing.
- It can be built not only using commandline tools, but also the same source codes in a GUI environment such as Eclipse, Keil, Attollic etc. Please remember to modify the Makefile to suit your own directory structure.
The code can be found in my Github pages.
There are some pitfalls in the initial setup of the processor which you must take care of:
1.
FreeRTOSConfig.h. It is necessary to set pointers to interrupt handlers to those provided by the FreeRTOS. This is done in the lines:
//Needed for the STM32F10x processors:
#define vPortSVCHandler SVC_Handler
#define xPortPendSVHandler PendSV_Handler
#define xPortSysTickHandler SysTick_Handler
The remaining series of lines in
FreeRTOSConfig.h are to set the properties of the processor etc. They are fairly easy to understand.
2. The main body of the program; freertos_testXX.c. This sets up the processor to run the tasks. It is fairly easy to understand and get running, using the FreeRTOS supplied functions. It is necessary to add the line to initialize the nested vectored interrupt controller on the STM32F103:
NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 ); //Needed for STM32F10x
Sometimes it is necessary to pass a structure to the tasks e.g., to initialize task parameters. An example of that is shown in the last two examples. Task priorities etc. can be set as desired, as documented in the FreeRTOS homepage.
The project is intended to run on a STM32 Nucleo F103RB board. But any similar is fine.
Several versions of freertos_testxx.c have been included. The project can be built using any of them by changing the Makefile. I wrote them when working with the project. Their function is shown below:
freertos_test01.c
First tests of periodic task creation using vTaskDelayUntil. Tasks are created with no passed data structures. Their operation as intended was confirmed.
As set up, T1 and T2 are "periodic" tasks; T2 has lower priority than T1. T2 will only run when T1 is done. T1 flashes the on board LED when it is running. Since T1 has a long execution time, the start of T2 is delayed, and you can see this in the flashing pattern of T2; it is irregular. However, when we exchange the priorities and T2 becomes the high priority task, its flashing becomes regular; whenever it is released, it is immediately executed by the the RTOS kernel.
freertos_test02.c
Same as previous.
Added a single value data passed to the task; its period. The passed value is declared in the function call as a void pointer. In the target function body, it must be either assigned to the correct pointer type (by declaring a pointer of the correct type, and equating it to the function argument void pointer through a suitable type-cast), or each time it is used, type-cast it to the correct pointer type. This may even be done implicitly since the function argument where this is used, or the left hand side of the equality will dictate a correct type-cast. However, better not to leave it to the compiler as implicit; some ambiguity might break the program.
Tasks are also truly periodic (in the sense of FreeRTOS) in which vTaskDelayUntil() function call is used.
It works fine.
freertos_test03.c
Same as previous.
Added a data structure to pass the data to the task. The data structure must be in the memory when the task runs. So that precludes the possibility of creating a data structure on the fly for the task in main() and remove it after task creation. It must be there all the time.
The data structure must be created by using a malloc call (apparently this costs time, but it is done only during initialization, so it is possible).
TODO: Check if
malloc is allowed in the heap-1 model.
In this example, T1 parameters are passed using a structure. T2 has only one parameter. This is passed in a simpler manner, by type casting to void * on the fly. Even a literal (directly written number) can be passed in this way.
When created correctly, the data is passed to the task correctly. However, if a normal structure declaration is used within the main function, then apparently, the values are on the stack of main, and are not available to the calling function. The latter method does not work. Program compiles but the tasks do not get the intended values.
freertos_test04.c
Tasks receive all the critical values through the passed variable structure, i.e. period, number of instructions, and GPIO port to toggle for execution. This simplifies the structure for demos, because only 1 task function can be written, and shared among all created tasks.
freertos_test04a.c
Only one task function is written and all tasks are created as different instances of this. Their parameters are set by the passed data structure during the creation function call. This works fine. The body of the same task is called with different arguments to create several tasks.
The execution of the tasks can be recorded and visualized using a (cheap) logic analyzer and a viewing program such as
GTK Wave, if they set a GPIO pin at entry, and reset it at completion. One sample run of 500ms is shown in the photo below. Task T1 period is 30ms and it has a higher priority. Task T2 period is 50ms and priority is lower at 50ms.
|
The task execution times seen in GTK Wave |
You can see the signals repeat at the least common multiple of the periods (150ms), also called the hyperperiod. You can also see T1 preemts T2 just after zero; T2 seems to execute longer than usual, and T1 and T2 are setting their GPIO pins at the same time. What is actually happening is that when the time to execute T1 comes, T2 is preempted (suspended), and T1 runs instead. This happens three more times in the example above.
You can also see that the execution times of T1 are regular, but the execution times of T2 are irregular. Being lower priority, even if T2 is released, it must first wait for T1 to complete.