© Warren Gay 2018

Warren Gay, Beginning STM32, https://doi.org/10.1007/978-1-4842-3624-6_5

5. FreeRTOS

Warren Gay

(1)St. Catharines, Ontario, Canada

Early in this book, we transition to the use of FreeRTOS. Doing so offers a number of advantages, mainly because the programming becomes much simpler and offers greater reliability as a result.

Not all platforms are capable of supporting an RTOS (real-time operating system). Each task within a multi-tasking system requires some stack space to be allocated where variables and return addresses of function calls are stored. There simply isn’t much RAM on an ATmega328, for example, with only 2K. The STM32F103C8T6, on the other hand, has 20K of SRAM available to divide among a reasonable complement of tasks.

This chapter introduces FreeRTOS, which is open sourced and available for free. The FreeRTOS source code is licensed under GPL License 2. There is a special provision to allow you to distribute your linked product without requiring the distribution of your own proprietary source code. Look for the text file named LICENSE for the details.

FreeRTOS Facilities

What makes an RTOS desirable? What does it provide? Let’s examine some major categories of services found in FreeRTOS:

  • Multi-tasking and scheduling

  • Message queues

  • Semaphores and mutexes

  • Timers

  • Event groups

Tasking

In the Arduino environment, everything runs as a single task, with a single stack for variables and return addresses. This style of programming requires you to run a loop polling each event to be serviced. With every iteration, you may need to poll the temperature sensor and then invoke another routine as part of the loop to broadcast that result.

With FreeRTOS (an RTOS), logical functions are placed into separate tasks that run independently. One task might be responsible for reading and computing the current temperature. Another task could be responsible for broadcasting that last computed temperature. In effect, it becomes a pair of programs running at the same time.

For very simple applications, this overhead of task scheduling might be seen as overkill. However, as complexity increases, the advantages of partitioning the problem into tasks become much more pronounced.

FreeRTOS is very flexible. It provides two types of task scheduling:

  • Preemptive multi-tasking

  • Cooperative multi-tasking (coroutines)

With preemptive multi-tasking, a task runs until it runs out of its time slice, or becomes blocked, or yields control explicitly. The task scheduler manages which task is run next, taking priorities into account. This is the type of multi-tasking that will be used within this book’s projects.

Another form of multi-tasking is coroutines. The difference is that the current task runs until it gives up control. There is no time slice or timeout. If no function call would block (like a mutex), then a coroutine must call a yield function in order to hand control over to another task. The task scheduler then decides which task to pass control to next. This form of scheduling is desirable for safety-critical applications needing strict control of CPU time.

Message Queues

As soon as you adopt multi-tasking, you inherit a communication problem. Using our temperature-reading example, how does the temperature-reading task safely communicate the value to the temperature-broadcasting task? If the temperature is stored as four bytes, how do you pass that value without interruption? Preemptive multi-tasking means that copying four bytes of data to another location might get interrupted partway through.

A crude way to solve this would be to inhibit interrupts while copying your temperature to a location used by the broadcast task. But this approach could be intolerable if you have frequently occurring interrupts. The problem worsens when the objects to be copied increase in size.

The message-queue facility within FreeRTOS provides a task-safe way to communicate a complete message. The message queue guarantees that only complete messages will be received. Additionally, it limits the length of the queue so that a sending task can’t use up all the memory. By using a predetermined queue length, the task adding messages becomes blocked until space is available. When a task becomes blocked, the task scheduler automatically switches to another task that is ready to run, which may remove messages from that same queue. The fixed length gives the message queue a form of flow control.

Semaphores and Mutexes

Within the implementation of a queue, there is a mutex operation at work. The process of adding a message may require several instructions to complete. Yet, in a preemptive multi-tasking system, it is possible for a message to be half added before being interrupted to execute another task.

Within FreeRTOS , the queue is designed to have messages added in an atomic manner. To accomplish this, some sort of mutex device is used behind the scenes. The mutex is an all-or-nothing device. You either have the lock or don’t.

Similar to mutexes , there are semaphores. In some situations where you might want to limit a certain number of concurrent requests, for example, a semaphore can manage that in an atomic manner. It might allow a maximum value of three, for example. Then, up to three “take” requests will succeed. Additional “take” requests will block until one or more “give” requests have been made to give back the resource.

Timers

Timers are important for many applications, including the blink variety of program. When you have multiple tasks consuming CPU time, a delay routine is not only unreliable, but it also robs other tasks of CPU time that could have been used more productively.

Within an RTOS system, there is usually a “systick” interrupt that helps with time management. This systick interrupt not only tracks the current number of “ticks” issued so far but is also used by the task scheduler to switch tasks.

Within FreeRTOS, you can choose to delay execution by a specified number of ticks. This works by noting the current “tick time” and yielding to another task until the required tick time has arrived. In this way, the delay precision is limited only to the tick interval configured. It also permits other tasks to do real work until the right time arrives.

FreeRTOS also has the facility of software timers that can be created. Only when the timer expires is your function callback executed. This approach is memory frugal because all timers will make use of the same stack.

Event Groups

One problem that often occurs is that a task may need to monitor multiple queues at once. For example, a task might need to block until a message arrives from either of two different queues. FreeRTOS provides for the creation of “queue sets.” This allows a task to block until a message from any of the queues in the set has a message.

What about user-defined events? Event groups can be created to allow binary bits to represent an event. Once established, the FreeRTOS API permits a task to wait until a specific combination of events occurs. Events can be triggered from normal task code or from within an ISR (interrupt service routine).

The blinky2 Program

Change to the blinky2 demo directory:

$ cd ~/stm32f103c8t6/rtos/blinky2

This example program uses the FreeRTOS API to implement a blink program in an RTOS environment. Listing 5-1 illustrates the top of the source code file main. c. From this listing, notice the include files used:

  • FreeRTOS.h

  • task.h

  • libopencm3/stm32/rcc.h

  • libopencm3/stm32/gpio.h

You have seen the libopencm3 header files before. The task.h header file defines macros and functions related to the creation of tasks. Finally, there is FreeRTOS.h, which every project needs in order to customize and configure FreeRTOS. We’ll examine that after we finish with main.c.

The program main.c also defines the function prototype for the function named vApplicationStackOverflowHook() in lines 11–13 of Listing 5-1. FreeRTOS does not provide a function prototype for it, so we must provide it here to avoid having the compiler complain about it.

Listing 5-1 The Top of stm32/rtos/blinky2/main.c Source Code
0001: /* Simple LED task demo, using timed delays:
0002:  *
0003:  * The LED on PC13 is toggled in task1.
0004:  */
0005: #include "FreeRTOS.h"
0006: #include "task.h"
0007:
0008: #include <libopencm3/stm32/rcc.h>
0009: #include <libopencm3/stm32/gpio.h>
0010:
0011: extern void vApplicationStackOverflowHook(
0012:   xTaskHandle *pxTask,
0013:   signed portCHAR *pcTaskName);

Listing 5-2 lists the definition of the vApplicationStackOverflowHook() optional function. This function could have been left out of the program without causing a problem. It is provided here to illustrate how you would define it, if you wanted it.

Listing 5-2 blinky2/main.c, Function vApplicationStackOverflowHook()
0017: void
0018: vApplicationStackOverflowHook(
0019:   xTaskHandle *pxTask __attribute((unused)),
0020:   signed portCHAR *pcTaskName __attribute((unused))
0021: ) {
0022:   for(;;);    // Loop forever here..
0023: }

If the function is defined, FreeRTOS will invoke it when it detects that it has overrun a stack limit. This allows the application designer to decide what should be done about it. You might, for example, want to flash a special red LED to indicate program failure.

Listing 5-3 illustrates the task that performs the LED blinking. It accepts a void * argument, which is unused in this example. The __attribute((unused)) is a gcc attribute to indicate to the compiler that the argument args is unused, and it prevents warnings about it.

Listing 5-3 blinky2/main.c, Function task1()
0025: static void
0026: task1(void *args __attribute((unused))) {
0027:
0028:   for (;;) {
0029:       gpio_toggle(GPIOC,GPIO13);
0030:       vTaskDelay(pdMS_TO_TICKS(500));
0031:   }
0032: }

The body of the function task1() otherwise is very simple. At the top of the loop, it toggles the on/off state of GPIO PC13. Next, a delay is executed for 500 ms. The vTaskDelay() function requires the number of ticks to delay. It is often more convenient to specify milliseconds instead. The macro pdMS_TO_TICKS() converts milliseconds to ticks according to your FreeRTOS configuration.

This task, of course, assumes that all of the necessary setup has been done beforehand. This is taken care of by the main program, illustrated in Listing 5-4.

Listing 5-4 blinky2/main.c, main() Function
0034: int
0035: main(void) {
0036:
0037:   rcc_clock_setup_in_hse_8mhz_out_72mhz(); // For "blue pill"
0038:
0039:   rcc_periph_clock_enable(RCC_GPIOC);
0040:   gpio_set_mode(
0041:       GPIOC,
0042:       GPIO_MODE_OUTPUT_2_MHZ,
0043:       GPIO_CNF_OUTPUT_PUSHPULL,
0044:       GPIO13);
0045:
0046:   xTaskCreate(task1,"LED",100,NULL,configMAX_PRIORITIES-1,NULL);
0047:   vTaskStartScheduler();
0048:
0049:   for (;;);
0050:   return 0;
0051: }

The main() program is defined as returning an int in lines 34 and 35, even though the main program should never return in this MCU context. This simply satisfies the compiler that it is conforming to POSIX (Portable Operating System Inferface) standards. The return statement in line 50 is never executed.

Line 37 illustrates something new—the establishment of the CPU clock speed. For your Blue Pill device, you’ll normally want to invoke this function for best performance. It configures clocks so that the HSE (high-speed external oscillator) is using an 8 MHz crystal, multiplied by 9 (implied) by a PLL (phase-locked loop), to arrive at a CPU clock rate of 72 MHz. Without this call, we would rely on the RC clock (resistor/capacitor clock).

Line 39 enables the GPIO clock for port C. This is the first step in the ducks-in-a-row setup for GPIO PC13, which drives the built-in LED. Lines 40–44 define the remaining ducks in a row so that PC13 is an output pin, at 2 MHz, in push/pull configuration.

Line 46 creates a new task, using our function named task1(). We give the task a symbolic name of “LED,” which can be a name of your choosing. The third argument specifies how many stack words are required for the stack space. Notice the emphasis on “words.” For the STM32 platform, a word is four bytes. Estimating stack space is often tricky, and there are ways to measure it (see Chapter 21, “Troubleshooting”). For now, accept that 400 bytes (100 words) is enough.

The fourth argument in line 46 points to any data that you want to pass to your task. We don’t need to here, so we specify NULL. This pointer is passed to the argument args in task1(). The fifth argument specifies the task priority. We only have one task in this example (aside from the main task). We simply give it a high priority. The last argument allows a task handle to be returned if we provide a pointer. We don’t need the handle returned, so NULL is supplied.

Creating a task alone is not enough to start it running. You can create several tasks before you start the task scheduler. Once you invoke the FreeRTOS function vTaskStartScheduler(), the tasks will start from the function address that you named in argument one.

Exercise some care in choosing functions to call prior to the start of the task scheduler. Some of the more advanced functions may only be called after the scheduler is running. There are still others that can only be called prior to the scheduler being started. Check the FreeRTOS documentation when necessary.

Once the task scheduler is running, it never returns from line 47 of Listing 5-4 unless the scheduler is stopped. In case it does return, it is customary to put a forever loop (line 49) after the call to prevent it from returning from main (line 50).

Build and Test blinky2

With your programmer hooked up to your device, perform the following:

$ make clobber
$ make
# make flash

The make clobber deletes any built or partially built components so that a plain make will completely recompile in the project again. The make flash will invoke the st-flash utility to write the new program to your device. Press the Reset button if necessary, but it may start on its own.

The code shows that the built-in LED should change state every 500 ms. If you have a scope, you can confirm that this does indeed happen (scope pin PC13). This not only confirms that the program works as intended, but also confirms that our FreeRTOS.h file has been properly configured.

Execution

The example program is rather simple, but let’s summarize the high-level activities of what is happening:

  • The function task1() is concurrently executing, toggling the built-in LED on and off. This is timed by the timer facility through the use of vTaskDelay().

  • The main() function has called vTaskStartScheduler(). This gives control to the FreeRTOS scheduler, which starts and switches various tasks. The main thread will continue to execute within FreeRTOS (within the scheduler) unless a task stops the scheduler.

Task task1() has a stack allocated from the heap to execute with (we gave it 100 words). If that task were ever deleted, this storage would be returned to the heap. The main task is currently executing within the FreeRTOS scheduler, using the stack it was given.

While these may seem like elementary points to make, it is important to know where the resources are allocated. Larger applications need to carefully allocate memory and CPU so that no task becomes starved. This also outlines the overall control structure that is operating.

The use of preemptive multi-tasking requires new responsibility. Sharing data between tasks requires thread-safe disciplines to be used. This simple example skirts the issue because there is only one task. Later projects will require inter-task communication.

FreeRTOSConfig.h

Each of the projects found in the ~/stm32f103c8t6/rtos subdirectories has its own copy of FreeRTOSConfig.h. This is by design since this configures your RTOS resources and features, which may vary by project. This permits some projects to leave out FreeRTOS features that they don’t require, resulting in a smaller executable. In other cases, there can be differences in timing, memory allocation, and other RTOS-related features.

Listing 5-1, line 5, illustrated that FreeRTOS.h is included. This file in turn causes your local FreeRTOSConfig.h file to be included. Let’s now examine some of the important configuration elements within the FreeRTOSConfig.h file, shown in Listing 5-5.

Listing 5-5 Some Configuration Macros Defined in the FreeRTOSConfig.h File
0088: #define configUSE_PREEMPTION      1
0089: #define configUSE_IDLE_HOOK       0
0090: #define configUSE_TICK_HOOK       0
0091: #define configCPU_CLOCK_HZ        ( ( unsigned long ) 72000000 )
0092: #define configSYSTICK_CLOCK_HZ    ( configCPU_CLOCK_HZ / 8 )
0093: #define configTICK_RATE_HZ        ( ( TickType_t ) 250 )
0094: #define configMAX_PRIORITIES      ( 5 )
0095: #define configMINIMAL_STACK_SIZE  ( ( unsigned short ) 128 )
0096: #define configTOTAL_HEAP_SIZE     ( ( size_t ) ( 17 * 1024 ) )
0097: #define configMAX_TASK_NAME_LEN   ( 16 )
0098: #define configUSE_TRACE_FACILITY  0
0099: #define configUSE_16_BIT_TICKS    0
0100: #define configIDLE_SHOULD_YIELD   1
0101: #define configUSE_MUTEXES         0
0102: #define configCHECK_FOR_STACK_OVERFLOW 1

The most important of these configuration macros is perhaps the configUSE_PREEMPTION macro. When set to non-zero, it indicates that we want preemptive scheduling in FreeRTOS. There are two hook functions, which were not used, so configUSE_IDLE_HOOK and configUSE_TICK_HOOK are set to zero.

The following three macros configure FreeRTOS so that it can compute the correct timing for us:

0091: #define configCPU_CLOCK_HZ       ( ( unsigned long ) 72000000 )
0092: #define configSYSTICK_CLOCK_HZ   ( configCPU_CLOCK_HZ / 8 )
0093: #define configTICK_RATE_HZ       ( ( TickType_t ) 250 )

These declarations indicate a 72 MHz CPU clock rate, a system timer counter that will increment every 8 CPU cycles, and that we want a system click interrupt to happen 250 times per second (every 4 ms). If you get these values incorrect then FreeRTOS won’t get timings or delays correct.

The value of configMAX_PRIORITIES defines the maximum number of priorities that will be supported. Each priority level requires RAM within RTOS, so the levels should not be set higher than necessary.

The minimum stack size (in words) specifies how much space the FreeRTOS idle task needs. This should not normally be modified. The heap size in bytes declares how much RAM can be dynamically allocated. In this example, the 17K of SRAM out of the 20K total is available as heap:

0095: #define configMINIMAL_STACK_SIZE    ( ( unsigned short ) 128 )
0096: #define configTOTAL_HEAP_SIZE       ( ( size_t ) ( 17 * 1024 ) )

The configIDLE_SHOULD_YIELD macro should be enabled if you want the idle task to invoke another task that is ready to run. Finally, configCHECK_FOR_STACK_OVERFLOW was enabled for this application so that we could demonstrate the function vApplicationStackOverflowHook() in Listing 5-2. If you don’t need this functionality, turn it off by setting it to zero.

The following macros are examples of other customizations. In our example program, we never use the vTaskDelete() function, for example, so INCLUDE_vTaskDelete is set to zero. This reduces the overhead of the compiled FreeRTOS code. We do, however, need the vTaskDelay() function, so the macro INCLUDE_vTaskDelay is configured as 1:

0111: #define INCLUDE_vTaskPrioritySet        0
0112: #define INCLUDE_uxTaskPriorityGet       0
0113: #define INCLUDE_vTaskDelete             0
0114: #define INCLUDE_vTaskCleanUpResources   0
0115: #define INCLUDE_vTaskSuspend            0
0116: #define INCLUDE_vTaskDelayUntil         0
0117: #define INCLUDE_vTaskDelay              1

FreeRTOS Naming Convention

The FreeRTOS naming convention differs from that used by libopencm3. The FreeRTOS group uses a unique naming convention for variables, macros, and functions. As a software developer myself, I don’t recommend the practice of including type information in named entities. The problem is that types can change as the project matures or is ported to a new platform. When that happens, you’re faced with two ugly choices, as follows:

  1. Leave the entity names as they are and live with the fact that the type information is not correct.

  2. Edit all of the name references to reflect the new type.

The UNIX convention of including a “p” to indicate a pointer variable is usually acceptable because it is uncommon for a variable to change from a pointer to an instance. Yet this too can happen in C++, where reference variables can be used.

Despite this odd naming convention, let’s not waste time rewriting FreeRTOS or putting layers around it. Here, I’ll simply identify the conventions that they have used so that it is easier for you to make use of their API in Table 5-1.

Table 5-1 FreeRTOS Type Prefix Characters

Prefix

Description

v

void (function return value)

c

char type

s

short type

l

long type

x

BaseType_t and any other type not covered

u

unsigned type

p

pointer

So, if the variable has a type of unsigned char, they will use the prefix “uc.” If the variable is a pointer to an unsigned character, they will use “puc.”

You have already seen the function named vTaskDelay(), which indicates that there is no return value (void). The FreeRTOS function named xQueueReceive() returns a type BaseType_t, which is why the function name prefix is “x.”

FreeRTOS Macros

FreeRTOS writes macro names with a prefix to indicate where they are defined. Table 5-2 lists these.

Table 5-2 Macro Prefixes Used by FreeRTOS

PREFIX

Example

Source

port

portMAX_DELAY

portable.h

task

taskENTER_CRITICAL()

task.h

pd

pdTRUE

projdefs.h

config

configUSE_PREEMPTION

FreeRTOSConfig.h

err

errQUEUE_ FULL

projdefs.h

Summary

The blink program, as simple as it is, was presented running under FreeRTOS as a task. And yet it still remained uncomplicated and provided a reliable timing, changing state every 500 ms.

We also saw how to configure the CPU clock rate so that we would not have to accept a default RC clock for it. This is important for FreeRTOS so that its timing will be accurate. An optional hook function for capturing a stack overrun event was illustrated. FreeRTOS configuration and conventions were covered, and you saw how easy it is to create preemptive tasks.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.135.188.121